tek/crates/tek_tui/src/tui_pool.rs

459 lines
16 KiB
Rust

use crate::*;
pub struct PhrasePoolView<E: Engine> {
_engine: PhantomData<E>,
/// Collection of phrases
pub phrases: Vec<Arc<RwLock<Phrase>>>,
/// Selected phrase
pub phrase: usize,
/// Scroll offset
pub scroll: usize,
/// Mode switch
pub mode: Option<PhrasePoolMode>,
/// Whether this widget is focused
pub focused: bool,
/// Whether this widget is entered
pub entered: bool,
}
/// Modes for phrase pool
pub enum PhrasePoolMode {
/// Renaming a pattern
Rename(usize, String),
/// Editing the length of a pattern
Length(usize, usize, PhraseLengthFocus),
}
impl<E: Engine> PhrasePoolView<E> {
pub fn new (phrases: Vec<Arc<RwLock<Phrase>>>) -> Self {
Self {
_engine: Default::default(),
scroll: 0,
phrase: 0,
mode: None,
focused: false,
entered: false,
phrases,
}
}
pub fn len (&self) -> usize {
self.phrases.len()
}
pub fn phrase (&self) -> &Arc<RwLock<Phrase>> {
&self.phrases[self.phrase]
}
pub fn index_before (&self, index: usize) -> usize {
index.overflowing_sub(1).0.min(self.len() - 1)
}
pub fn index_after (&self, index: usize) -> usize {
(index + 1) % self.len()
}
pub fn index_of (&self, phrase: &Phrase) -> Option<usize> {
for i in 0..self.phrases.len() {
if *self.phrases[i].read().unwrap() == *phrase { return Some(i) }
}
return None
}
fn new_phrase (name: Option<&str>, color: Option<ItemColorTriplet>) -> Arc<RwLock<Phrase>> {
Arc::new(RwLock::new(Phrase::new(
String::from(name.unwrap_or("(new)")), true, 4 * PPQ, None, color
)))
}
pub fn delete_selected (&mut self) {
if self.phrase > 0 {
self.phrases.remove(self.phrase);
self.phrase = self.phrase.min(self.phrases.len().saturating_sub(1));
}
}
pub fn append_new (&mut self, name: Option<&str>, color: Option<ItemColorTriplet>) {
self.phrases.push(Self::new_phrase(name, color));
self.phrase = self.phrases.len() - 1;
}
pub fn insert_new (&mut self, name: Option<&str>, color: Option<ItemColorTriplet>) {
self.phrases.insert(self.phrase + 1, Self::new_phrase(name, color));
self.phrase += 1;
}
pub fn insert_dup (&mut self) {
let mut phrase = self.phrases[self.phrase].read().unwrap().duplicate();
phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25);
self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase)));
self.phrase += 1;
}
pub fn move_up (&mut self) {
if self.phrase > 1 {
self.phrases.swap(self.phrase - 1, self.phrase);
self.phrase -= 1;
}
}
pub fn move_down (&mut self) {
if self.phrase < self.phrases.len().saturating_sub(1) {
self.phrases.swap(self.phrase + 1, self.phrase);
self.phrase += 1;
}
}
}
// TODO: Display phrases always in order of appearance
impl Content for PhrasePoolView<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let Self { focused, phrases, mode, .. } = self;
let content = col!(
(i, phrase) in phrases.iter().enumerate() => Layers::new(|add|{
let Phrase { ref name, color, length, .. } = *phrase.read().unwrap();
let mut length = PhraseLength::new(length, None);
if let Some(PhrasePoolMode::Length(phrase, new_length, focus)) = mode {
if *focused && i == *phrase {
length.pulses = *new_length;
length.focus = Some(*focus);
}
}
let length = length.align_e().fill_x();
let row1 = lay!(format!(" {i}").align_w().fill_x(), length).fill_x();
let mut row2 = format!(" {name}");
if let Some(PhrasePoolMode::Rename(phrase, _)) = mode {
if *focused && i == *phrase { row2 = format!("{row2}"); }
};
let row2 = TuiStyle::bold(row2, true);
add(&col!(row1, row2).fill_x().bg(color.base.rgb))?;
Ok(if *focused && i == self.phrase { add(&CORNERS)?; })
})
);
let border_color = if *focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)};
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
let content = content.fill_xy().bg(Color::Rgb(28, 35, 25)).border(border);
let title_color = if *focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)};
let upper_left = format!("[{}] Phrases", if self.entered {""} else {" "});
let upper_right = format!("({})", phrases.len());
lay!(
content,
TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(),
TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(),
)
}
}
#[derive(Clone, PartialEq, Debug)]
pub enum PhrasePoolViewCommand {
Select(usize),
Edit(PhrasePoolCommand),
Rename(PhraseRenameCommand),
Length(PhraseLengthCommand),
}
impl Handle<Tui> for PhrasePoolView<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
PhrasePoolViewCommand::execute_with_state(self, from)
}
}
impl InputToCommand<Tui, PhrasePoolView<Tui>> for PhrasePoolViewCommand {
fn input_to_command (state: &PhrasePoolView<Tui>, input: &TuiInput) -> Option<Self> {
use PhrasePoolViewCommand as Cmd;
use PhrasePoolCommand as Edit;
use PhraseRenameCommand as Rename;
use PhraseLengthCommand as Length;
match input.event() {
key!(KeyCode::Up) => Some(Cmd::Select(0)),
key!(KeyCode::Down) => Some(Cmd::Select(0)),
key!(KeyCode::Char(',')) => Some(Cmd::Edit(Edit::Swap(0, 0))),
key!(KeyCode::Char('.')) => Some(Cmd::Edit(Edit::Swap(0, 0))),
key!(KeyCode::Delete) => Some(Cmd::Edit(Edit::Delete(0))),
key!(KeyCode::Char('a')) => Some(Cmd::Edit(Edit::Add(0))),
key!(KeyCode::Char('i')) => Some(Cmd::Edit(Edit::Add(0))),
key!(KeyCode::Char('d')) => Some(Cmd::Edit(Edit::Duplicate(0))),
key!(KeyCode::Char('c')) => Some(Cmd::Edit(Edit::RandomColor(0))),
key!(KeyCode::Char('n')) => Some(Cmd::Rename(Rename::Begin)),
key!(KeyCode::Char('t')) => Some(Cmd::Length(Length::Begin)),
_ => match state.mode {
Some(PhrasePoolMode::Rename(..)) => {
Rename::input_to_command(state, input).map(Cmd::Rename)
},
Some(PhrasePoolMode::Length(..)) => {
Length::input_to_command(state, input).map(Cmd::Length)
},
_ => None
}
}
}
}
impl<E: Engine> Command<PhrasePoolView<E>> for PhrasePoolViewCommand {
fn execute (self, view: &mut PhrasePoolView<E>) -> Perhaps<Self> {
use PhraseRenameCommand as Rename;
use PhraseLengthCommand as Length;
match self {
Self::Select(phrase) => {
view.phrase = phrase
},
Self::Edit(command) => {
return Ok(command.execute(&mut view)?.map(Self::Edit))
}
Self::Rename(command) => match command {
Rename::Begin => {
view.mode = Some(PhrasePoolMode::Rename(
view.phrase,
view.phrases[view.phrase].read().unwrap().name.clone()
))
},
_ => {
return Ok(command.execute(view)?.map(Self::Rename))
}
},
Self::Length(command) => match command {
Length::Begin => {
view.mode = Some(PhrasePoolMode::Length(
view.phrase,
view.phrases[view.phrase].read().unwrap().length,
PhraseLengthFocus::Bar
))
},
_ => {
return Ok(command.execute(view)?.map(Self::Length))
}
},
}
Ok(None)
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum PhraseLengthCommand {
Begin,
Next,
Prev,
Inc,
Dec,
Set(usize),
Cancel,
}
impl InputToCommand<Tui, PhrasePoolView<Tui>> for PhraseLengthCommand {
fn input_to_command (view: &PhrasePoolView<Tui>, from: &TuiInput) -> Option<Self> {
if let Some(PhrasePoolMode::Length(_, length, _)) = view.mode {
Some(match from.event() {
key!(KeyCode::Up) => Self::Inc,
key!(KeyCode::Down) => Self::Dec,
key!(KeyCode::Right) => Self::Next,
key!(KeyCode::Left) => Self::Prev,
key!(KeyCode::Enter) => Self::Set(length),
key!(KeyCode::Esc) => Self::Cancel,
_ => return None
})
} else {
unreachable!()
}
}
}
impl<E: Engine> Command<PhrasePoolView<E>> for PhraseLengthCommand {
fn execute (self, view: &mut PhrasePoolView<E>) -> Perhaps<Self> {
use PhraseLengthFocus::*;
use PhraseLengthCommand::*;
if let Some(PhrasePoolMode::Length(phrase, ref mut length, ref mut focus)) = view.mode {
match self {
Self::Cancel => {
view.mode = None;
},
Self::Prev => {
focus.prev()
},
Self::Next => {
focus.next()
},
Self::Inc => match focus {
Bar => { *length += 4 * PPQ },
Beat => { *length += PPQ },
Tick => { *length += 1 },
},
Self::Dec => match focus {
Bar => { *length = length.saturating_sub(4 * PPQ) },
Beat => { *length = length.saturating_sub(PPQ) },
Tick => { *length = length.saturating_sub(1) },
},
Self::Set(length) => {
let mut phrase = view.phrases[phrase].write().unwrap();
let old_length = phrase.length;
phrase.length = length;
view.mode = None;
return Ok(Some(Self::Set(old_length)))
},
_ => unreachable!()
}
Ok(None)
} else if self == Begin {
view.mode = Some(PhrasePoolMode::Length(
view.phrase,
view.phrases[view.phrase].read().unwrap().length,
PhraseLengthFocus::Bar
));
Ok(None)
} else {
unreachable!()
}
}
}
/// Displays and edits phrase length.
pub struct PhraseLength<E: Engine> {
_engine: PhantomData<E>,
/// Pulses per beat (quaver)
pub ppq: usize,
/// Beats per bar
pub bpb: usize,
/// Length of phrase in pulses
pub pulses: usize,
/// Selected subdivision
pub focus: Option<PhraseLengthFocus>,
}
impl<E: Engine> PhraseLength<E> {
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
Self { _engine: Default::default(), ppq: PPQ, bpb: 4, pulses, focus }
}
pub fn bars (&self) -> usize {
self.pulses / (self.bpb * self.ppq)
}
pub fn beats (&self) -> usize {
(self.pulses % (self.bpb * self.ppq)) / self.ppq
}
pub fn ticks (&self) -> usize {
self.pulses % self.ppq
}
pub fn bars_string (&self) -> String {
format!("{}", self.bars())
}
pub fn beats_string (&self) -> String {
format!("{}", self.beats())
}
pub fn ticks_string (&self) -> String {
format!("{:>02}", self.ticks())
}
}
impl Content for PhraseLength<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
Layers::new(move|add|{
match self.focus {
None => add(&row!(
" ", self.bars_string(),
".", self.beats_string(),
".", self.ticks_string(),
" "
)),
Some(PhraseLengthFocus::Bar) => add(&row!(
"[", self.bars_string(),
"]", self.beats_string(),
".", self.ticks_string(),
" "
)),
Some(PhraseLengthFocus::Beat) => add(&row!(
" ", self.bars_string(),
"[", self.beats_string(),
"]", self.ticks_string(),
" "
)),
Some(PhraseLengthFocus::Tick) => add(&row!(
" ", self.bars_string(),
".", self.beats_string(),
"[", self.ticks_string(),
"]"
)),
}
})
}
}
/// Focused field of `PhraseLength`
#[derive(Copy, Clone)]
pub enum PhraseLengthFocus {
/// Editing the number of bars
Bar,
/// Editing the number of beats
Beat,
/// Editing the number of ticks
Tick,
}
impl PhraseLengthFocus {
pub fn next (&mut self) {
*self = match self {
Self::Bar => Self::Beat,
Self::Beat => Self::Tick,
Self::Tick => Self::Bar,
}
}
pub fn prev (&mut self) {
*self = match self {
Self::Bar => Self::Tick,
Self::Beat => Self::Bar,
Self::Tick => Self::Beat,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum PhraseRenameCommand {
Begin,
Set(String),
Confirm,
Cancel,
}
impl InputToCommand<Tui, PhrasePoolView<Tui>> for PhraseRenameCommand {
fn input_to_command (view: &PhrasePoolView<Tui>, from: &TuiInput) -> Option<Self> {
if let Some(PhrasePoolMode::Rename(_, ref old_name)) = view.mode {
Some(match from.event() {
key!(KeyCode::Char(c)) => {
let mut new_name = old_name.clone();
new_name.push(*c);
Self::Set(new_name)
},
key!(KeyCode::Backspace) => {
let mut new_name = old_name.clone();
new_name.pop();
Self::Set(new_name)
},
key!(KeyCode::Enter) => Self::Confirm,
key!(KeyCode::Esc) => Self::Cancel,
_ => return None
})
} else {
unreachable!()
}
}
}
impl<E: Engine> Command<PhrasePoolView<E>> for PhraseRenameCommand {
fn execute (self, view: &mut PhrasePoolView<E>) -> Perhaps<Self> {
use PhraseRenameCommand::*;
if let Some(PhrasePoolMode::Rename(phrase, ref mut old_name)) = view.mode {
match self {
Set(s) => {
view.phrases[phrase].write().unwrap().name = s.into();
return Ok(Some(Self::Set(old_name.clone())))
},
Confirm => {
let old_name = old_name.clone();
view.mode = None;
return Ok(Some(Self::Set(old_name)))
},
Cancel => {
let mut phrase = view.phrases[phrase].write().unwrap();
phrase.name = old_name.clone();
},
_ => unreachable!()
};
Ok(None)
} else if self == Begin {
view.mode = Some(PhrasePoolMode::Rename(
view.phrase,
view.phrases[view.phrase].read().unwrap().name.clone()
));
Ok(None)
} else {
unreachable!()
}
}
}