tek/src/pool.rs
2025-01-01 22:03:31 +01:00

329 lines
13 KiB
Rust

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<Arc<RwLock<MidiClip>>>,
/// Selected phrase
pub(crate) phrase: AtomicUsize,
/// Mode switch
pub(crate) mode: Option<PoolMode>,
/// Rendered size
size: Measure<Tui>,
/// 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:<Tui>|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<PoolCommand> {
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<RwLock<MidiClip>>|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<PoolMode> {
&self.mode
}
pub(crate) fn phrases_mode_mut (&mut self) -> &mut Option<PoolMode> {
&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:<Tui>|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!()
}
});