WIP: half broken
This commit is contained in:
@@ -52,6 +52,7 @@ pub struct AudioConfig {
|
||||
pub input_device: Option<String>,
|
||||
pub channels: u16,
|
||||
pub buffer_size: u32,
|
||||
pub max_voices: usize,
|
||||
pub sample_rate: f32,
|
||||
pub sample_paths: Vec<PathBuf>,
|
||||
pub sample_count: usize,
|
||||
@@ -67,6 +68,7 @@ impl Default for AudioConfig {
|
||||
input_device: None,
|
||||
channels: 2,
|
||||
buffer_size: 512,
|
||||
max_voices: 32,
|
||||
sample_rate: 44100.0,
|
||||
sample_paths: Vec::new(),
|
||||
sample_count: 0,
|
||||
@@ -123,10 +125,12 @@ pub enum AudioFocus {
|
||||
InputDevice,
|
||||
Channels,
|
||||
BufferSize,
|
||||
Polyphony,
|
||||
RefreshRate,
|
||||
RuntimeHighlight,
|
||||
ShowScope,
|
||||
ShowSpectrum,
|
||||
ShowCompletion,
|
||||
SamplePaths,
|
||||
LinkEnabled,
|
||||
StartStopSync,
|
||||
@@ -198,11 +202,13 @@ impl AudioSettings {
|
||||
AudioFocus::OutputDevice => AudioFocus::InputDevice,
|
||||
AudioFocus::InputDevice => AudioFocus::Channels,
|
||||
AudioFocus::Channels => AudioFocus::BufferSize,
|
||||
AudioFocus::BufferSize => AudioFocus::RefreshRate,
|
||||
AudioFocus::BufferSize => AudioFocus::Polyphony,
|
||||
AudioFocus::Polyphony => AudioFocus::RefreshRate,
|
||||
AudioFocus::RefreshRate => AudioFocus::RuntimeHighlight,
|
||||
AudioFocus::RuntimeHighlight => AudioFocus::ShowScope,
|
||||
AudioFocus::ShowScope => AudioFocus::ShowSpectrum,
|
||||
AudioFocus::ShowSpectrum => AudioFocus::SamplePaths,
|
||||
AudioFocus::ShowSpectrum => AudioFocus::ShowCompletion,
|
||||
AudioFocus::ShowCompletion => AudioFocus::SamplePaths,
|
||||
AudioFocus::SamplePaths => AudioFocus::LinkEnabled,
|
||||
AudioFocus::LinkEnabled => AudioFocus::StartStopSync,
|
||||
AudioFocus::StartStopSync => AudioFocus::Quantum,
|
||||
@@ -216,11 +222,13 @@ impl AudioSettings {
|
||||
AudioFocus::InputDevice => AudioFocus::OutputDevice,
|
||||
AudioFocus::Channels => AudioFocus::InputDevice,
|
||||
AudioFocus::BufferSize => AudioFocus::Channels,
|
||||
AudioFocus::RefreshRate => AudioFocus::BufferSize,
|
||||
AudioFocus::Polyphony => AudioFocus::BufferSize,
|
||||
AudioFocus::RefreshRate => AudioFocus::Polyphony,
|
||||
AudioFocus::RuntimeHighlight => AudioFocus::RefreshRate,
|
||||
AudioFocus::ShowScope => AudioFocus::RuntimeHighlight,
|
||||
AudioFocus::ShowSpectrum => AudioFocus::ShowScope,
|
||||
AudioFocus::SamplePaths => AudioFocus::ShowSpectrum,
|
||||
AudioFocus::ShowCompletion => AudioFocus::ShowSpectrum,
|
||||
AudioFocus::SamplePaths => AudioFocus::ShowCompletion,
|
||||
AudioFocus::LinkEnabled => AudioFocus::SamplePaths,
|
||||
AudioFocus::StartStopSync => AudioFocus::LinkEnabled,
|
||||
AudioFocus::Quantum => AudioFocus::StartStopSync,
|
||||
@@ -267,6 +275,11 @@ impl AudioSettings {
|
||||
self.config.buffer_size = new_val;
|
||||
}
|
||||
|
||||
pub fn adjust_max_voices(&mut self, delta: i32) {
|
||||
let new_val = (self.config.max_voices as i32 + delta).clamp(1, 128) as usize;
|
||||
self.config.max_voices = new_val;
|
||||
}
|
||||
|
||||
pub fn toggle_refresh_rate(&mut self) {
|
||||
self.config.refresh_rate = self.config.refresh_rate.toggle();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use tui_textarea::TextArea;
|
||||
use cagire_ratatui::Editor;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Focus {
|
||||
@@ -17,7 +17,7 @@ pub struct EditorContext {
|
||||
pub pattern: usize,
|
||||
pub step: usize,
|
||||
pub focus: Focus,
|
||||
pub text: TextArea<'static>,
|
||||
pub editor: Editor,
|
||||
pub copied_step: Option<CopiedStep>,
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ impl Default for EditorContext {
|
||||
pattern: 0,
|
||||
step: 0,
|
||||
focus: Focus::Sequencer,
|
||||
text: TextArea::default(),
|
||||
editor: Editor::new(),
|
||||
copied_step: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ impl FileBrowserState {
|
||||
state
|
||||
}
|
||||
|
||||
fn current_dir(&self) -> PathBuf {
|
||||
pub fn current_dir(&self) -> PathBuf {
|
||||
if self.input.is_empty() {
|
||||
return std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
|
||||
}
|
||||
|
||||
@@ -3,16 +3,20 @@ pub mod editor;
|
||||
pub mod file_browser;
|
||||
pub mod live_keys;
|
||||
pub mod modal;
|
||||
pub mod panel;
|
||||
pub mod patterns_nav;
|
||||
pub mod playback;
|
||||
pub mod project;
|
||||
pub mod sample_browser;
|
||||
pub mod ui;
|
||||
|
||||
pub use audio::{AudioFocus, AudioSettings, Metrics};
|
||||
pub use editor::{CopiedStep, EditorContext, Focus, PatternField};
|
||||
pub use live_keys::LiveKeyState;
|
||||
pub use modal::Modal;
|
||||
pub use panel::{PanelFocus, PanelState, SidePanel};
|
||||
pub use patterns_nav::{PatternsColumn, PatternsNav};
|
||||
pub use playback::PlaybackState;
|
||||
pub use project::ProjectState;
|
||||
pub use sample_browser::SampleBrowserState;
|
||||
pub use ui::UiState;
|
||||
|
||||
@@ -37,7 +37,7 @@ pub enum Modal {
|
||||
input: String,
|
||||
},
|
||||
SetTempo(String),
|
||||
AddSamplePath(String),
|
||||
AddSamplePath(FileBrowserState),
|
||||
Editor,
|
||||
Preview,
|
||||
}
|
||||
|
||||
28
src/state/panel.rs
Normal file
28
src/state/panel.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use super::sample_browser::SampleBrowserState;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum PanelFocus {
|
||||
#[default]
|
||||
Main,
|
||||
Side,
|
||||
}
|
||||
|
||||
pub enum SidePanel {
|
||||
SampleBrowser(SampleBrowserState),
|
||||
}
|
||||
|
||||
pub struct PanelState {
|
||||
pub side: Option<SidePanel>,
|
||||
pub focus: PanelFocus,
|
||||
pub visible: bool,
|
||||
}
|
||||
|
||||
impl Default for PanelState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
side: None,
|
||||
focus: PanelFocus::Main,
|
||||
visible: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
414
src/state/sample_browser.rs
Normal file
414
src/state/sample_browser.rs
Normal file
@@ -0,0 +1,414 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use cagire_ratatui::{TreeLine, TreeLineKind};
|
||||
|
||||
const AUDIO_EXTENSIONS: &[&str] = &["wav", "flac", "ogg", "aiff", "aif", "mp3"];
|
||||
|
||||
pub enum SampleNode {
|
||||
Root {
|
||||
label: String,
|
||||
children: Vec<SampleNode>,
|
||||
expanded: bool,
|
||||
},
|
||||
Folder {
|
||||
name: String,
|
||||
children: Vec<SampleNode>,
|
||||
expanded: bool,
|
||||
},
|
||||
File {
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl SampleNode {
|
||||
fn expanded(&self) -> bool {
|
||||
match self {
|
||||
SampleNode::Root { expanded, .. } | SampleNode::Folder { expanded, .. } => *expanded,
|
||||
SampleNode::File { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_expanded(&mut self, val: bool) {
|
||||
match self {
|
||||
SampleNode::Root { expanded, .. } | SampleNode::Folder { expanded, .. } => {
|
||||
*expanded = val;
|
||||
}
|
||||
SampleNode::File { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_expandable(&self) -> bool {
|
||||
!matches!(self, SampleNode::File { .. })
|
||||
}
|
||||
|
||||
fn children(&self) -> &[SampleNode] {
|
||||
match self {
|
||||
SampleNode::Root { children, .. } | SampleNode::Folder { children, .. } => children,
|
||||
SampleNode::File { .. } => &[],
|
||||
}
|
||||
}
|
||||
|
||||
fn label(&self) -> &str {
|
||||
match self {
|
||||
SampleNode::Root { label, .. } => label,
|
||||
SampleNode::Folder { name, .. } | SampleNode::File { name, .. } => name,
|
||||
}
|
||||
}
|
||||
|
||||
fn flatten(&self, depth: u8, parent_folder: &str, file_index: usize, out: &mut Vec<TreeLine>) {
|
||||
let kind = match self {
|
||||
SampleNode::Root { expanded, .. } => TreeLineKind::Root { expanded: *expanded },
|
||||
SampleNode::Folder { expanded, .. } => TreeLineKind::Folder { expanded: *expanded },
|
||||
SampleNode::File { .. } => TreeLineKind::File,
|
||||
};
|
||||
out.push(TreeLine {
|
||||
depth,
|
||||
kind,
|
||||
label: self.label().to_string(),
|
||||
folder: parent_folder.to_string(),
|
||||
index: file_index,
|
||||
});
|
||||
if self.expanded() {
|
||||
let folder_name = self.label();
|
||||
let mut idx = 0;
|
||||
for child in self.children() {
|
||||
let child_idx = if matches!(child, SampleNode::File { .. }) {
|
||||
let i = idx;
|
||||
idx += 1;
|
||||
i
|
||||
} else {
|
||||
0
|
||||
};
|
||||
child.flatten(depth + 1, folder_name, child_idx, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SampleTree {
|
||||
roots: Vec<SampleNode>,
|
||||
}
|
||||
|
||||
impl SampleTree {
|
||||
pub fn from_paths(paths: &[PathBuf]) -> Self {
|
||||
if paths.len() == 1 {
|
||||
// Single path: show its contents directly at root level
|
||||
let roots = Self::scan_children(&paths[0]);
|
||||
return Self { roots };
|
||||
}
|
||||
let mut roots = Vec::new();
|
||||
for path in paths {
|
||||
if let Some(root) = Self::scan_root(path) {
|
||||
roots.push(root);
|
||||
}
|
||||
}
|
||||
Self { roots }
|
||||
}
|
||||
|
||||
fn scan_children(path: &Path) -> Vec<SampleNode> {
|
||||
let entries = match fs::read_dir(path) {
|
||||
Ok(e) => e,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
let mut files: Vec<String> = Vec::new();
|
||||
let mut folders: Vec<(String, PathBuf)> = Vec::new();
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let ft = entry.file_type().ok();
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
if ft.is_some_and(|t| t.is_dir()) {
|
||||
folders.push((name, entry.path()));
|
||||
} else if is_audio_file(&name) {
|
||||
files.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
folders.sort_by_key(|a| a.0.to_lowercase());
|
||||
files.sort_by_key(|a| a.to_lowercase());
|
||||
|
||||
let mut nodes = Vec::new();
|
||||
for (name, folder_path) in folders {
|
||||
if let Some(folder) = Self::scan_folder(&name, &folder_path) {
|
||||
nodes.push(folder);
|
||||
}
|
||||
}
|
||||
for name in files {
|
||||
nodes.push(SampleNode::File { name });
|
||||
}
|
||||
nodes
|
||||
}
|
||||
|
||||
fn scan_root(path: &Path) -> Option<SampleNode> {
|
||||
let entries = fs::read_dir(path).ok()?;
|
||||
let label = path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| path.display().to_string());
|
||||
|
||||
let mut children = Vec::new();
|
||||
let mut files: Vec<String> = Vec::new();
|
||||
let mut folders: Vec<(String, PathBuf)> = Vec::new();
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let ft = entry.file_type().ok();
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
if ft.is_some_and(|t| t.is_dir()) {
|
||||
folders.push((name, entry.path()));
|
||||
} else if is_audio_file(&name) {
|
||||
files.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
folders.sort_by_key(|a| a.0.to_lowercase());
|
||||
files.sort_by_key(|a| a.to_lowercase());
|
||||
|
||||
for (name, folder_path) in folders {
|
||||
if let Some(folder) = Self::scan_folder(&name, &folder_path) {
|
||||
children.push(folder);
|
||||
}
|
||||
}
|
||||
for name in files {
|
||||
children.push(SampleNode::File { name });
|
||||
}
|
||||
|
||||
Some(SampleNode::Root {
|
||||
label,
|
||||
children,
|
||||
expanded: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn scan_folder(name: &str, path: &Path) -> Option<SampleNode> {
|
||||
let entries = fs::read_dir(path).ok()?;
|
||||
let mut children: Vec<SampleNode> = Vec::new();
|
||||
|
||||
let mut files: Vec<String> = Vec::new();
|
||||
for entry in entries.flatten() {
|
||||
let ft = entry.file_type().ok();
|
||||
let entry_name = entry.file_name().to_string_lossy().into_owned();
|
||||
if ft.is_some_and(|t| t.is_file()) && is_audio_file(&entry_name) {
|
||||
files.push(entry_name);
|
||||
}
|
||||
}
|
||||
files.sort_by_key(|a| a.to_lowercase());
|
||||
|
||||
for file in files {
|
||||
children.push(SampleNode::File { name: file });
|
||||
}
|
||||
|
||||
if children.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(SampleNode::Folder {
|
||||
name: name.to_string(),
|
||||
children,
|
||||
expanded: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn visible_entries(&self) -> Vec<TreeLine> {
|
||||
let mut out = Vec::new();
|
||||
for root in &self.roots {
|
||||
root.flatten(0, "", 0, &mut out);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn node_at_mut(&mut self, visible_index: usize) -> Option<&mut SampleNode> {
|
||||
let mut count = 0;
|
||||
for root in &mut self.roots {
|
||||
if let Some(node) = Self::walk_mut(root, visible_index, &mut count) {
|
||||
return Some(node);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn walk_mut<'a>(
|
||||
node: &'a mut SampleNode,
|
||||
target: usize,
|
||||
count: &mut usize,
|
||||
) -> Option<&'a mut SampleNode> {
|
||||
if *count == target {
|
||||
return Some(node);
|
||||
}
|
||||
*count += 1;
|
||||
if node.expanded() {
|
||||
let children = match node {
|
||||
SampleNode::Root { children, .. } | SampleNode::Folder { children, .. } => {
|
||||
children
|
||||
}
|
||||
SampleNode::File { .. } => return None,
|
||||
};
|
||||
for child in children.iter_mut() {
|
||||
if let Some(found) = Self::walk_mut(child, target, count) {
|
||||
return Some(found);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SampleBrowserState {
|
||||
pub tree: SampleTree,
|
||||
pub cursor: usize,
|
||||
pub scroll_offset: usize,
|
||||
pub search_query: String,
|
||||
pub search_active: bool,
|
||||
filtered: Option<Vec<TreeLine>>,
|
||||
}
|
||||
|
||||
impl SampleBrowserState {
|
||||
pub fn new(paths: &[PathBuf]) -> Self {
|
||||
Self {
|
||||
tree: SampleTree::from_paths(paths),
|
||||
cursor: 0,
|
||||
scroll_offset: 0,
|
||||
search_query: String::new(),
|
||||
search_active: false,
|
||||
filtered: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn entries(&self) -> Vec<TreeLine> {
|
||||
if let Some(ref filtered) = self.filtered {
|
||||
return filtered.clone();
|
||||
}
|
||||
self.tree.visible_entries()
|
||||
}
|
||||
|
||||
pub fn current_entry(&self) -> Option<TreeLine> {
|
||||
let entries = self.entries();
|
||||
entries.into_iter().nth(self.cursor)
|
||||
}
|
||||
|
||||
pub fn visible_count(&self) -> usize {
|
||||
if let Some(ref filtered) = self.filtered {
|
||||
return filtered.len();
|
||||
}
|
||||
self.tree.visible_entries().len()
|
||||
}
|
||||
|
||||
pub fn move_up(&mut self) {
|
||||
if self.cursor > 0 {
|
||||
self.cursor -= 1;
|
||||
if self.cursor < self.scroll_offset {
|
||||
self.scroll_offset = self.cursor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_down(&mut self, visible_height: usize) {
|
||||
let count = self.visible_count();
|
||||
if count == 0 {
|
||||
return;
|
||||
}
|
||||
if self.cursor + 1 < count {
|
||||
self.cursor += 1;
|
||||
if self.cursor >= self.scroll_offset + visible_height {
|
||||
self.scroll_offset = self.cursor - visible_height + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_expand(&mut self) {
|
||||
if self.filtered.is_some() {
|
||||
return;
|
||||
}
|
||||
if let Some(node) = self.tree.node_at_mut(self.cursor) {
|
||||
if node.is_expandable() {
|
||||
let new_val = !node.expanded();
|
||||
node.set_expanded(new_val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collapse_at_cursor(&mut self) {
|
||||
if self.filtered.is_some() {
|
||||
return;
|
||||
}
|
||||
if let Some(node) = self.tree.node_at_mut(self.cursor) {
|
||||
if node.expanded() {
|
||||
node.set_expanded(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activate_search(&mut self) {
|
||||
self.search_active = true;
|
||||
}
|
||||
|
||||
pub fn update_search(&mut self) {
|
||||
if self.search_query.is_empty() {
|
||||
self.filtered = None;
|
||||
} else {
|
||||
let query = self.search_query.to_lowercase();
|
||||
let full = self.full_entries();
|
||||
let filtered: Vec<TreeLine> = full
|
||||
.into_iter()
|
||||
.filter(|line| line.label.to_lowercase().contains(&query))
|
||||
.collect();
|
||||
self.filtered = Some(filtered);
|
||||
}
|
||||
self.cursor = 0;
|
||||
self.scroll_offset = 0;
|
||||
}
|
||||
|
||||
pub fn clear_search(&mut self) {
|
||||
self.search_query.clear();
|
||||
self.search_active = false;
|
||||
self.filtered = None;
|
||||
self.cursor = 0;
|
||||
self.scroll_offset = 0;
|
||||
}
|
||||
|
||||
fn full_entries(&self) -> Vec<TreeLine> {
|
||||
let mut out = Vec::new();
|
||||
for root in &self.tree.roots {
|
||||
Self::flatten_all(root, 0, "", 0, &mut out);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn flatten_all(
|
||||
node: &SampleNode,
|
||||
depth: u8,
|
||||
parent_folder: &str,
|
||||
file_index: usize,
|
||||
out: &mut Vec<TreeLine>,
|
||||
) {
|
||||
let kind = match node {
|
||||
SampleNode::Root { .. } => TreeLineKind::Root { expanded: true },
|
||||
SampleNode::Folder { .. } => TreeLineKind::Folder { expanded: true },
|
||||
SampleNode::File { .. } => TreeLineKind::File,
|
||||
};
|
||||
out.push(TreeLine {
|
||||
depth,
|
||||
kind,
|
||||
label: node.label().to_string(),
|
||||
folder: parent_folder.to_string(),
|
||||
index: file_index,
|
||||
});
|
||||
let folder_name = node.label();
|
||||
let mut idx = 0;
|
||||
for child in node.children() {
|
||||
let child_idx = if matches!(child, SampleNode::File { .. }) {
|
||||
let i = idx;
|
||||
idx += 1;
|
||||
i
|
||||
} else {
|
||||
0
|
||||
};
|
||||
Self::flatten_all(child, depth + 1, folder_name, child_idx, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_audio_file(name: &str) -> bool {
|
||||
let lower = name.to_lowercase();
|
||||
AUDIO_EXTENSIONS.iter().any(|ext| lower.ends_with(ext))
|
||||
}
|
||||
@@ -19,6 +19,7 @@ pub struct UiState {
|
||||
pub doc_category: usize,
|
||||
pub show_title: bool,
|
||||
pub runtime_highlight: bool,
|
||||
pub show_completion: bool,
|
||||
}
|
||||
|
||||
impl Default for UiState {
|
||||
@@ -33,6 +34,7 @@ impl Default for UiState {
|
||||
doc_category: 0,
|
||||
show_title: true,
|
||||
runtime_highlight: false,
|
||||
show_completion: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user