use super::*; pub mod phrase_length; pub(crate) use phrase_length::*; pub mod phrase_rename; pub(crate) use phrase_rename::*; pub mod phrase_selector; pub(crate) use phrase_selector::*; #[derive(Debug)] pub struct PoolModel { pub(crate) visible: bool, /// Collection of phrases pub(crate) phrases: Vec>>, /// Selected phrase pub(crate) phrase: AtomicUsize, /// Mode switch pub(crate) mode: Option, /// Rendered size size: Measure, /// Scroll offset scroll: usize, } /// Modes for phrase pool #[derive(Debug, Clone)] pub enum PoolMode { /// Renaming a pattern Rename(usize, String), /// Editing the length of a pattern Length(usize, usize, PhraseLengthFocus), /// Load phrase from disk Import(usize, FileBrowser), /// Save phrase to disk Export(usize, FileBrowser), } #[derive(Clone, PartialEq, Debug)] pub enum PoolCommand { Show(bool), /// Update the contents of the phrase pool Phrase(PhrasePoolCommand), /// Select a phrase from the phrase pool Select(usize), /// Rename a phrase Rename(PhraseRenameCommand), /// Change the length of a phrase Length(PhraseLengthCommand), /// Import from file Import(FileBrowserCommand), /// Export to file Export(FileBrowserCommand), } command!(|self:PoolCommand, state: PoolModel|{ use PoolCommand::*; match self { Show(visible) => { state.visible = visible; Some(Self::Show(!visible)) } Rename(command) => match command { PhraseRenameCommand::Begin => { let length = state.phrases()[state.phrase_index()].read().unwrap().length; *state.phrases_mode_mut() = Some( PoolMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar) ); None }, _ => command.execute(state)?.map(Rename) }, Length(command) => match command { PhraseLengthCommand::Begin => { let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone(); *state.phrases_mode_mut() = Some( PoolMode::Rename(state.phrase_index(), name) ); None }, _ => command.execute(state)?.map(Length) }, Import(command) => match command { FileBrowserCommand::Begin => { *state.phrases_mode_mut() = Some( PoolMode::Import(state.phrase_index(), FileBrowser::new(None)?) ); None }, _ => command.execute(state)?.map(Import) }, Export(command) => match command { FileBrowserCommand::Begin => { *state.phrases_mode_mut() = Some( PoolMode::Export(state.phrase_index(), FileBrowser::new(None)?) ); None }, _ => command.execute(state)?.map(Export) }, Select(phrase) => { state.set_phrase_index(phrase); None }, Phrase(command) => command.execute(state)?.map(Phrase), } }); input_to_command!(PoolCommand:|state: PoolModel,input|match state.phrases_mode() { Some(PoolMode::Rename(..)) => Self::Rename(PhraseRenameCommand::input_to_command(state, input)?), Some(PoolMode::Length(..)) => Self::Length(PhraseLengthCommand::input_to_command(state, input)?), Some(PoolMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?), Some(PoolMode::Export(..)) => Self::Export(FileBrowserCommand::input_to_command(state, input)?), _ => to_phrases_command(state, input)? }); fn to_phrases_command (state: &PoolModel, input: &TuiIn) -> Option { use KeyCode::{Up, Down, Delete, Char}; use PoolCommand as Cmd; let index = state.phrase_index(); let count = state.phrases().len(); Some(match input.event() { key_pat!(Char('n')) => Cmd::Rename(PhraseRenameCommand::Begin), key_pat!(Char('t')) => Cmd::Length(PhraseLengthCommand::Begin), key_pat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin), key_pat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin), key_pat!(Char('c')) => Cmd::Phrase(PhrasePoolCommand::SetColor(index, ItemColor::random())), key_pat!(Char('[')) | key_pat!(Up) => Cmd::Select( index.overflowing_sub(1).0.min(state.phrases().len() - 1) ), key_pat!(Char(']')) | key_pat!(Down) => Cmd::Select( index.saturating_add(1) % state.phrases().len() ), key_pat!(Char('<')) => if index > 1 { state.set_phrase_index(state.phrase_index().saturating_sub(1)); Cmd::Phrase(PhrasePoolCommand::Swap(index - 1, index)) } else { return None }, key_pat!(Char('>')) => if index < count.saturating_sub(1) { state.set_phrase_index(state.phrase_index() + 1); Cmd::Phrase(PhrasePoolCommand::Swap(index + 1, index)) } else { return None }, key_pat!(Delete) => if index > 0 { state.set_phrase_index(index.min(count.saturating_sub(1))); Cmd::Phrase(PhrasePoolCommand::Delete(index)) } else { return None }, key_pat!(Char('a')) | key_pat!(Shift-Char('A')) => Cmd::Phrase(PhrasePoolCommand::Add(count, MidiClip::new( String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random()) ))), key_pat!(Char('i')) => Cmd::Phrase(PhrasePoolCommand::Add(index + 1, MidiClip::new( String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random()) ))), key_pat!(Char('d')) | key_pat!(Shift-Char('D')) => { let mut phrase = state.phrases()[index].read().unwrap().duplicate(); phrase.color = ItemPalette::random_near(phrase.color, 0.25); Cmd::Phrase(PhrasePoolCommand::Add(index + 1, phrase)) }, _ => return None }) } impl Default for PoolModel { fn default () -> Self { Self { visible: true, phrases: vec![RwLock::new(MidiClip::default()).into()], phrase: 0.into(), scroll: 0, mode: None, size: Measure::new(), } } } from!(|phrase:&Arc>|PoolModel = { let mut model = Self::default(); model.phrases.push(phrase.clone()); model.phrase.store(1, Relaxed); model }); has_phrases!(|self: PoolModel|self.phrases); has_phrase!(|self: PoolModel|self.phrases[self.phrase_index()]); impl PoolModel { pub(crate) fn phrase_index (&self) -> usize { self.phrase.load(Relaxed) } pub(crate) fn set_phrase_index (&self, value: usize) { self.phrase.store(value, Relaxed); } pub(crate) fn phrases_mode (&self) -> &Option { &self.mode } pub(crate) fn phrases_mode_mut (&mut self) -> &mut Option { &mut self.mode } pub fn file_picker (&self) -> Option<&FileBrowser> { match self.mode { Some(PoolMode::Import(_, ref file_picker)) => Some(file_picker), Some(PoolMode::Export(_, ref file_picker)) => Some(file_picker), _ => None } } } pub struct PoolView<'a>(pub(crate) &'a PoolModel); // TODO: Display phrases always in order of appearance render!(Tui: (self: PoolView<'a>) => { let PoolModel { phrases, mode, .. } = self.0; let color = self.0.phrase().read().unwrap().color; let content = Tui::bg(Color::Black, Tui::map(||self.0.phrases().iter(), |clip, i|{ let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); let item_height = 1; let item_offset = i as u16 * item_height; let selected = i == self.0.phrase_index(); let offset = |a|Push::y(item_offset, Align::n(Fixed::y(item_height, Fill::x(a)))); offset(Tui::bg(if selected { color.light.rgb } else { color.base.rgb }, lay!( Align::w(Tui::fg(color.lightest.rgb, Tui::bold(selected, format!(" {i:>3} {name}")))), Align::e(Tui::fg(color.lightest.rgb, Tui::bold(selected, format!("{length} ")))), Align::w(Tui::when(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "▶")))), Align::e(Tui::when(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "◀")))), )))/* //format!(" {i} {name} {length} ")[> //Push::y(i as u16 * 2, Fixed::y(2, Tui::bg(color.base.rgb, Fill::x( //format!(" {i} {name} {length} ")))))[>, name.clone()))))Bsp::s( Fill::x(Bsp::a( Align::w(format!(" {i}")), Align::e(Pull::x(1, length)), )), Tui::bold(true, { let mut row2 = format!(" {name}"); if let Some(PoolMode::Rename(clip, _)) = self.0.mode { if i == clip { row2 = format!("{row2}▄"); } }; row2 }), ))))//lay!(clip, Tui::when(i == self.0.clip_index(), CORNERS)))*/ })); let border = Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)); let enclose = |x|Tui::bg(color.darkest.rgb, lay!(Fill::xy(border), x)); ////let with_files = |x|Tui::either(self.0.file_picker().is_some(), ////Thunk::new(||self.0.file_picker().unwrap()), ////Thunk::new(x)); //let mut length = PhraseLength::new(length, None); //if let Some(PoolMode::Length(clip, new_length, focus)) = self.0.mode { //if i == clip { //length.pulses = new_length; //length.focus = Some(focus); //} //} enclose(content) //enclose(lay!( ////add(&Lozenge(Style::default().bg(border_bg).fg(border_color)))?; //Fill::xy(content), //Fill::x(Align::nw(Push::x(1, Tui::fg(title_color, upper_left.to_string())))), //Fill::x(Align::ne(Pull::x(1, Tui::fg(title_color, upper_right.to_string())))), //self.0.size.clone() //)) }); command!(|self: FileBrowserCommand, state: PoolModel|{ use PoolMode::*; use FileBrowserCommand::*; let mode = &mut state.mode; match mode { Some(Import(index, ref mut browser)) => match self { Cancel => { *mode = None; }, Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); }, Select(index) => { browser.index = index; }, Confirm => if browser.is_file() { let index = *index; let path = browser.path(); *mode = None; PhrasePoolCommand::Import(index, path).execute(state)?; } else if browser.is_dir() { *mode = Some(Import(*index, browser.chdir()?)); }, _ => todo!(), }, Some(Export(index, ref mut browser)) => match self { Cancel => { *mode = None; }, Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); }, Select(index) => { browser.index = index; }, _ => unreachable!() }, _ => unreachable!(), }; None }); input_to_command!(FileBrowserCommand:|state: PoolModel,from|{ use FileBrowserCommand::*; use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char}; if let Some(PoolMode::Import(_index, browser)) = &state.mode { match from.event() { key_pat!(Up) => Select(browser.index.overflowing_sub(1).0 .min(browser.len().saturating_sub(1))), key_pat!(Down) => Select(browser.index.saturating_add(1) % browser.len()), key_pat!(Right) => Chdir(browser.cwd.clone()), key_pat!(Left) => Chdir(browser.cwd.clone()), key_pat!(Enter) => Confirm, key_pat!(Char(_)) => { todo!() }, key_pat!(Backspace) => { todo!() }, key_pat!(Esc) => Cancel, _ => return None } } else if let Some(PoolMode::Export(_index, browser)) = &state.mode { match from.event() { key_pat!(Up) => Select(browser.index.overflowing_sub(1).0 .min(browser.len())), key_pat!(Down) => Select(browser.index.saturating_add(1) % browser.len()), key_pat!(Right) => Chdir(browser.cwd.clone()), key_pat!(Left) => Chdir(browser.cwd.clone()), key_pat!(Enter) => Confirm, key_pat!(Char(_)) => { todo!() }, key_pat!(Backspace) => { todo!() }, key_pat!(Esc) => Cancel, _ => return None } } else { unreachable!() } });