From 042d480b67d13c490dac86dc5988ef00375fca83 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 11 Dec 2024 19:16:28 +0100 Subject: [PATCH] ItemPalette --- crates/tek/src/api/phrase.rs | 8 +- crates/tek/src/core/color.rs | 6 +- crates/tek/src/core/space.rs | 5 +- crates/tek/src/layout/measure.rs | 30 +- crates/tek/src/layout/split.rs | 61 ++- crates/tek/src/tui/app_arranger.rs | 15 +- crates/tek/src/tui/app_groovebox.rs | 0 crates/tek/src/tui/app_sequencer.rs | 83 ++-- crates/tek/src/tui/app_transport.rs | 64 +-- crates/tek/src/tui/engine_theme.rs | 3 + crates/tek/src/tui/phrase_editor.rs | 645 +++++++++++++++------------- crates/tek/src/tui/phrase_list.rs | 9 +- 12 files changed, 505 insertions(+), 424 deletions(-) create mode 100644 crates/tek/src/tui/app_groovebox.rs diff --git a/crates/tek/src/api/phrase.rs b/crates/tek/src/api/phrase.rs index 05a88f71..94aa24c2 100644 --- a/crates/tek/src/api/phrase.rs +++ b/crates/tek/src/api/phrase.rs @@ -75,7 +75,7 @@ impl Command for PhrasePoolCommand { Some(Self::SetLength(index, old_len)) }, SetColor(index, color) => { - let mut color = ItemColorTriplet::from(color); + let mut color = ItemPalette::from(color); std::mem::swap(&mut color, &mut model.phrases()[index].write().unwrap().color); Some(Self::SetColor(index, color.base)) }, @@ -104,7 +104,7 @@ pub struct Phrase { /// All notes are displayed with minimum length pub percussive: bool, /// Identifying color of phrase - pub color: ItemColorTriplet, + pub color: ItemPalette, } /// MIDI message structural @@ -116,7 +116,7 @@ impl Phrase { loop_on: bool, length: usize, notes: Option, - color: Option, + color: Option, ) -> Self { Self { uuid: uuid::Uuid::new_v4(), @@ -128,7 +128,7 @@ impl Phrase { loop_start: 0, loop_length: length, percussive: true, - color: color.unwrap_or_else(ItemColorTriplet::random) + color: color.unwrap_or_else(ItemPalette::random) } } pub fn set_length (&mut self, length: usize) { diff --git a/crates/tek/src/core/color.rs b/crates/tek/src/core/color.rs index 9464e1f2..8c245a85 100644 --- a/crates/tek/src/core/color.rs +++ b/crates/tek/src/core/color.rs @@ -10,7 +10,7 @@ pub struct ItemColor { } /// A color in OKHSL and RGB with lighter and darker variants. #[derive(Debug, Default, Copy, Clone, PartialEq)] -pub struct ItemColorTriplet { +pub struct ItemPalette { pub base: ItemColor, pub light: ItemColor, pub dark: ItemColor, @@ -44,7 +44,7 @@ impl ItemColor { self.okhsl.mix(other.okhsl, distance).into() } } -impl From for ItemColorTriplet { +impl From for ItemPalette { fn from (base: ItemColor) -> Self { let mut light = base.okhsl.clone(); light.lightness = (light.lightness * 1.15).min(Okhsl::::max_lightness()); @@ -54,7 +54,7 @@ impl From for ItemColorTriplet { Self { base, light: light.into(), dark: dark.into() } } } -impl ItemColorTriplet { +impl ItemPalette { pub fn random () -> Self { ItemColor::random().into() } diff --git a/crates/tek/src/core/space.rs b/crates/tek/src/core/space.rs index a83f78c7..6942f437 100644 --- a/crates/tek/src/core/space.rs +++ b/crates/tek/src/core/space.rs @@ -110,7 +110,10 @@ pub trait Area: Copy { [self.x(), self.y(), a, self.h()], [self.x() + a, self.y(), self.w().minus(a), self.h()], ), - _ => todo!(), + Direction::Left => ( + [self.x() + self.w() - a, self.y(), a, self.h()], + [self.x(), self.y(), self.w() - a, self.h()], + ), } } } diff --git a/crates/tek/src/layout/measure.rs b/crates/tek/src/layout/measure.rs index aaffbc18..cc528435 100644 --- a/crates/tek/src/layout/measure.rs +++ b/crates/tek/src/layout/measure.rs @@ -1,7 +1,8 @@ use crate::*; /// A widget that tracks its render width and height -pub struct Measure(PhantomData, AtomicUsize, AtomicUsize); +#[derive(Default)] +pub struct Measure(PhantomData, AtomicUsize, AtomicUsize, bool); impl Clone for Measure { fn clone (&self) -> Self { @@ -9,6 +10,7 @@ impl Clone for Measure { Default::default(), AtomicUsize::from(self.1.load(Ordering::Relaxed)), AtomicUsize::from(self.2.load(Ordering::Relaxed)), + self.3 ) } } @@ -16,8 +18,8 @@ impl Clone for Measure { impl std::fmt::Debug for Measure { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("Measure") - .field("width", &self.0) - .field("height", &self.1) + .field("width", &self.1) + .field("height", &self.2) .finish() } } @@ -29,18 +31,24 @@ impl Measure { pub fn set_w (&self, w: impl Into) { self.1.store(w.into(), Ordering::Relaxed) } pub fn set_h (&self, h: impl Into) { self.2.store(h.into(), Ordering::Relaxed) } pub fn set_wh (&self, w: impl Into, h: impl Into) { self.set_w(w); self.set_h(h); } - pub fn new () -> Self { Self(PhantomData::default(), 0.into(), 0.into()) } + pub fn new () -> Self { Self(PhantomData::default(), 0.into(), 0.into(), false) } + pub fn debug () -> Self { Self(PhantomData::default(), 0.into(), 0.into(), true) } pub fn format (&self) -> String { format!("{}x{}", self.w(), self.h()) } } -impl Render for Measure { - fn min_size (&self, _: E::Size) -> Perhaps { +impl Render for Measure { + fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> { Ok(Some([0u16.into(), 0u16.into()].into())) } - fn render (&self, to: &mut E::Output) -> Usually<()> { - self.set_w(to.area().w()); - self.set_h(to.area().h()); - Ok(()) + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + let w = to.area().w(); + self.set_w(w); + let h = to.area().h(); + self.set_h(h); + Ok(if self.3 { + to.blit(&format!(" {w} x {h} "), to.area.x(), to.area.y(), Some( + Style::default().bold().italic().bg(Color::Rgb(255, 0, 255)).fg(Color::Rgb(0,0,0)) + )) + }) } } - diff --git a/crates/tek/src/layout/split.rs b/crates/tek/src/layout/split.rs index 5b78d405..6da13980 100644 --- a/crates/tek/src/layout/split.rs +++ b/crates/tek/src/layout/split.rs @@ -4,41 +4,52 @@ impl LayoutSplit for E {} pub trait LayoutSplit { fn split , B: Render> ( - direction: Direction, amount: E::Unit, a: A, b: B + flip: bool, direction: Direction, amount: E::Unit, a: A, b: B ) -> Split { - Split::new(direction, amount, a, b) + Split::new(flip, direction, amount, a, b) } fn split_up , B: Render> ( - amount: E::Unit, a: A, b: B + flip: bool, amount: E::Unit, a: A, b: B ) -> Split { - Split::new(Direction::Up, amount, a, b) + Self::split(flip, Direction::Up, amount, a, b) + } + fn split_down , B: Render> ( + flip: bool, amount: E::Unit, a: A, b: B + ) -> Split { + Self::split(flip, Direction::Down, amount, a, b) + } + fn split_left , B: Render> ( + flip: bool, amount: E::Unit, a: A, b: B + ) -> Split { + Self::split(flip, Direction::Left, amount, a, b) + } + fn split_right , B: Render> ( + flip: bool, amount: E::Unit, a: A, b: B + ) -> Split { + Self::split(flip, Direction::Right, amount, a, b) } - - //fn split_flip > ( - //self, direction: Direction, amount: E::Unit, other: W - //) -> Split { Split::new(direction, amount, other, self) } } /// A binary split with fixed proportion pub struct Split, B: Render>( - pub Direction, pub E::Unit, A, B, PhantomData + pub bool, pub Direction, pub E::Unit, A, B, PhantomData ); impl, B: Render> Split { - pub fn new (direction: Direction, proportion: E::Unit, a: A, b: B) -> Self { - Self(direction, proportion, a, b, Default::default()) + pub fn new (flip: bool, direction: Direction, proportion: E::Unit, a: A, b: B) -> Self { + Self(flip, direction, proportion, a, b, Default::default()) } - pub fn up (proportion: E::Unit, a: A, b: B) -> Self { - Self(Direction::Up, proportion, a, b, Default::default()) + pub fn up (flip: bool, proportion: E::Unit, a: A, b: B) -> Self { + Self(flip, Direction::Up, proportion, a, b, Default::default()) } - pub fn down (proportion: E::Unit, a: A, b: B) -> Self { - Self(Direction::Down, proportion, a, b, Default::default()) + pub fn down (flip: bool, proportion: E::Unit, a: A, b: B) -> Self { + Self(flip, Direction::Down, proportion, a, b, Default::default()) } - pub fn left (proportion: E::Unit, a: A, b: B) -> Self { - Self(Direction::Left, proportion, a, b, Default::default()) + pub fn left (flip: bool, proportion: E::Unit, a: A, b: B) -> Self { + Self(flip, Direction::Left, proportion, a, b, Default::default()) } - pub fn right (proportion: E::Unit, a: A, b: B) -> Self { - Self(Direction::Right, proportion, a, b, Default::default()) + pub fn right (flip: bool, proportion: E::Unit, a: A, b: B) -> Self { + Self(flip, Direction::Right, proportion, a, b, Default::default()) } } @@ -47,9 +58,13 @@ impl, B: Render> Render for Split { Ok(Some(to)) } fn render (&self, to: &mut E::Output) -> Usually<()> { - let (a, b) = to.area().split_fixed(self.0, self.1); - to.render_in(a.into(), &self.2)?; - to.render_in(b.into(), &self.3)?; - Ok(()) + let (a, b) = to.area().split_fixed(self.1, self.2); + Ok(if self.0 { + to.render_in(a.into(), &self.4)?; + to.render_in(b.into(), &self.3)?; + } else { + to.render_in(a.into(), &self.3)?; + to.render_in(b.into(), &self.4)?; + }) } } diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 2f7de25d..f3e1026e 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -147,6 +147,7 @@ render!(|self: ArrangerTui|{ )) ])), Split::right( + false, self.splits[1], PhraseListView::from(self), PhraseView::from(self), @@ -174,6 +175,18 @@ impl HasPhrases for ArrangerTui { } } +impl HasEditor for ArrangerTui { + fn editor (&self) -> &PhraseEditorModel { + &self.editor + } + fn editor_focused (&self) -> bool { + self.focused() == ArrangerFocus::PhraseEditor + } + fn editor_entered (&self) -> bool { + self.entered() && self.editor_focused() + } +} + /// Sections in the arranger app that may be focused #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum ArrangerFocus { @@ -1173,7 +1186,7 @@ impl ArrangerControl for ArrangerTui { }, ArrangerSelection::Clip(t, s) => { if let Some(phrase) = &self.scenes_mut()[s].clips[t] { - phrase.write().unwrap().color = ItemColorTriplet::random(); + phrase.write().unwrap().color = ItemPalette::random(); } } } diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 4f67ef2e..bfd1b849 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -41,7 +41,7 @@ impl TryFrom<&Arc>> for SequencerTui { midi_buf: vec![vec![];65536], note_buf: vec![], perf: PerfModel::default(), - focus: FocusState::Focused(SequencerFocus::PhraseEditor) + focus: FocusState::Entered(SequencerFocus::PhraseEditor) }) } @@ -107,9 +107,9 @@ impl Audio for SequencerTui { } } -render!(|self: SequencerTui|lay!([self.size, Tui::split_up(1, - Tui::fill_xy(Tui::at_s(SequencerStatusBar::from(self))), - Tui::split_up(2, +render!(|self: SequencerTui|lay!([self.size, Tui::split_up(false, 1, + Tui::fill_xy(SequencerStatusBar::from(self)), + Tui::split_down(false, 2, TransportView::from(( self, self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten(), @@ -119,28 +119,22 @@ render!(|self: SequencerTui|lay!([self.size, Tui::split_up(1, false } )), - row!([ - Tui::fixed_x(20, Tui::split_up(4, col!([ - PhraseSelector::next_phrase( - &self.player, - self.focused() == SequencerFocus::PhraseNext, - self.entered() - ), + Tui::split_left(false, 20, + Tui::fixed_x(20, Tui::split_down(false, 4, col!([ PhraseSelector::play_phrase( - &self.player, - self.focused() == SequencerFocus::PhrasePlay, - self.entered() + &self.player, self.focused() == SequencerFocus::PhrasePlay, self.entered() ), - ]), col!([ + PhraseSelector::next_phrase( + &self.player, self.focused() == SequencerFocus::PhraseNext, self.entered() + ), + ]), Tui::split_up(false, 2, PhraseSelector::edit_phrase( - &self.editor.phrase, - self.focused() == SequencerFocus::PhraseEditor, - self.entered() + &self.editor.phrase, self.focused() == SequencerFocus::PhraseEditor, self.entered() ), PhraseListView::from(self), - ]))), + ))), PhraseView::from(self) - ]) + ) ) )])); @@ -159,6 +153,18 @@ impl HasPhrases for SequencerTui { } } +impl HasEditor for SequencerTui { + fn editor (&self) -> &PhraseEditorModel { + &self.editor + } + fn editor_focused (&self) -> bool { + self.focused() == SequencerFocus::PhraseEditor + } + fn editor_entered (&self) -> bool { + self.entered() && self.editor_focused() + } +} + /// Sections in the sequencer app that may be focused #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum SequencerFocus { @@ -176,7 +182,7 @@ pub enum SequencerFocus { impl From<&SequencerTui> for Option { fn from (state: &SequencerTui) -> Self { match state.focus.inner() { - SequencerFocus::Transport(focus) => Some(focus), + Transport(focus) => Some(focus), _ => None } } @@ -219,7 +225,7 @@ impl_focus!(SequencerTui SequencerFocus [ PhraseEditor, ], ] => [self: { - if self.focus.is_entered() && self.focus.inner() == SequencerFocus::PhraseEditor { + if self.focus.is_entered() && self.focus.inner() == PhraseEditor { self.editor.edit_mode = PhraseEditMode::Note } else { self.editor.edit_mode = PhraseEditMode::Scroll @@ -403,9 +409,9 @@ pub enum SequencerCommand { Phrases(PhrasesCommand), Editor(PhraseCommand), Enqueue(Option>>), - Clear, - Undo, - Redo, + //Clear, + //Undo, + //Redo, } impl Command for SequencerCommand { @@ -419,9 +425,9 @@ impl Command for SequencerCommand { state.player.enqueue_next(phrase.as_ref()); None }, - Self::Undo => { todo!() }, - Self::Redo => { todo!() }, - Self::Clear => { todo!() }, + //Self::Undo => { todo!() }, + //Self::Redo => { todo!() }, + //Self::Clear => { todo!() }, }) } } @@ -453,37 +459,34 @@ impl InputToCommand for SequencerCommand { } pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option { + let stopped = state.clock().is_stopped(); Some(match input.event() { // Play/pause - key!(Char(' ')) => Clock( - if state.clock().is_stopped() { Play(None) } else { Pause(None) } - ), + key!(Char(' ')) => Clock(if stopped { Play(None) } else { Pause(None) }), // Play from start/rewind to start - key!(Shift-Char(' ')) => Clock( - if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) } - ), + key!(Shift-Char(' ')) => Clock(if stopped { Play(Some(0)) } else { Pause(Some(0)) }), // Edit phrase key!(Char('e')) => match state.focused() { - SequencerFocus::PhrasePlay => Editor(Show( + PhrasePlay => Editor(Show( state.player.play_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone()) )), - SequencerFocus::PhraseNext => Editor(Show( + PhraseNext => Editor(Show( state.player.next_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone()) )), - SequencerFocus::PhraseList => Editor(Show( + PhraseList => Editor(Show( Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()) )), _ => return None, }, _ => match state.focused() { - SequencerFocus::Transport(_) => match TransportCommand::input_to_command(state, input)? { + Transport(_) => match TransportCommand::input_to_command(state, input)? { TransportCommand::Clock(command) => Clock(command), _ => return None, }, - SequencerFocus::PhraseEditor => Editor( + PhraseEditor => Editor( PhraseCommand::input_to_command(&state.editor, input)? ), - SequencerFocus::PhraseList => match input.event() { + PhraseList => match input.event() { key!(Enter) => Enqueue(Some( state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone() )), diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index 31b25284..d4ca3d59 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -75,13 +75,13 @@ pub struct TransportView { current_second: f64, } -impl From<(&T, Option, bool)> for TransportView { - fn from ((state, color, focused): (&T, Option, bool)) -> Self { +impl From<(&T, Option, bool)> for TransportView { + fn from ((state, color, focused): (&T, Option, bool)) -> Self { let clock = state.clock(); let sr = format!("{:.1}k", clock.timebase.sr.get() / 1000.0); let bpm = format!("{:.3}", clock.timebase.bpm.get()); let ppq = format!("{:.0}", clock.timebase.ppq.get()); - let color = color.unwrap_or(ItemColorTriplet::from(ItemColor::from(TuiTheme::g(100)))); + let color = color.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(100)))); let bg = if focused { color.light.rgb } else { color.dark.rgb }; if let Some(started) = clock.started.read().unwrap().as_ref() { let current_sample = (clock.global.sample.get() - started.sample.get())/1000.; @@ -114,7 +114,7 @@ impl From<(&T, Option, bool)> for TransportView { global_second: format!("{:.1}s", clock.global.usec.get()/1000000.), current_sample: 0.0, current_second: 0.0, - beat: format!("0.0.00") + beat: format!("000.0.00") } } } @@ -133,31 +133,39 @@ render!(|self: TransportView|{ let border_style = Style::default() .bg(bg) .fg(TuiTheme::border_fg(self.focused)); + let play_bg = if self.started{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)}; Tui::bg(bg, lay!(move|add|{ - add(&Tui::fill_x(Tui::at_w(lay!(move|add|{ - add(&Lozenge(border_style))?; - add(&Tui::outset_x(1, row!([ - TransportField("Beat", self.beat.as_str()), " ", - TransportField("BPM ", self.bpm.as_str()), " ", - ]))) - }))))?; - add(&Tui::fill_x(Tui::center_x(Tui::pull_x(2, row!([ - col!(|add|{ - if self.started { - add(&col!([Tui::fg(Color::Rgb(0, 255, 0), "▶ PLAYING"), ""])) - } else { - add(&col!(["", Tui::fg(Color::Rgb(255, 128, 0), "⏹ STOPPED")])) - } - }), - ])))))?; - add(&Tui::fill_x(Tui::at_e(lay!(move|add|{ - add(&Lozenge(border_style))?; - add(&Tui::outset_x(1, row!([ - TransportField("Second", format!("{:.1}s", self.current_second).as_str()), " ", - TransportField("Rate ", self.sr.as_str()), " ", - TransportField("Sample", format!("{:.0}k", self.current_sample).as_str()), - ]))) - })))) + add(&Tui::fill_x(Tui::at_w(lay!([ + //Lozenge(border_style), + Tui::outset_x(0, row!([ + Tui::bg(play_bg, Tui::outset_x(1, Tui::fixed_x(9, col!(|add|{ + if self.started { + add(&col!([Tui::fg(Color::Rgb(0, 255, 0), "▶ PLAYING"), ""])) + } else { + add(&col!(["", Tui::fg(Color::Rgb(255, 128, 0), "⏹ STOPPED")])) + } + })))), " ", + Tui::fixed_x(10, TransportField("Beat", + self.beat.as_str())), " ", + Tui::fixed_x(8, TransportField("BPM ", + self.bpm.as_str())), " ", + Tui::fixed_x(8, TransportField("Second", + format!("{:.1}s", self.current_second).as_str())), " ", + Tui::fixed_x(8, TransportField("Rate ", + self.sr.as_str())), " ", + Tui::fixed_x(8, TransportField("Sample", + format!("{:.0}k", self.current_sample).as_str())), + ])) + ]))))?; + //add(&Tui::fill_x(Tui::center_x(Tui::pull_x(2, row!([ + //
+ //])))))?; + //add(&Tui::fill_x(Tui::at_e(lay!(move|add|{ + ////add(&Lozenge(border_style))?; + //add(&Tui::outset_x(1, row!([ + //]))) + //})))) + Ok(()) })) }); diff --git a/crates/tek/src/tui/engine_theme.rs b/crates/tek/src/tui/engine_theme.rs index ccb66234..a07dc7fa 100644 --- a/crates/tek/src/tui/engine_theme.rs +++ b/crates/tek/src/tui/engine_theme.rs @@ -10,6 +10,9 @@ pub trait Theme { fn null () -> Color { Color::Reset } + fn bg0 () -> Color { + Color::Rgb(20, 20, 20) + } fn bg () -> Color { Color::Rgb(28, 35, 25) } diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 5fde1eec..88ddd669 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -1,13 +1,17 @@ use crate::*; +pub trait HasEditor { + fn editor (&self) -> &PhraseEditorModel; + fn editor_focused (&self) -> bool; + fn editor_entered (&self) -> bool; +} + /// Contains state for viewing and editing a phrase pub struct PhraseEditorModel { /// Phrase being played pub(crate) phrase: Option>>, /// Length of note that will be inserted, in pulses pub(crate) note_len: usize, - /// The full piano roll is rendered to this buffer - pub(crate) buffer: BigBuffer, /// Notes currently held at input pub(crate) notes_in: Arc>, /// Notes currently held at output @@ -24,25 +28,7 @@ pub struct PhraseEditorModel { pub(crate) time_point: AtomicUsize, pub(crate) edit_mode: PhraseEditMode, - pub(crate) view_mode: PhraseViewMode, -} - -#[derive(Copy, Clone, Debug)] -pub enum PhraseEditMode { - Note, - Scroll, -} - -#[derive(Copy, Clone, Debug)] -pub enum PhraseViewMode { - PianoHorizontal { - time_zoom: usize, - note_zoom: PhraseViewNoteZoom, - }, - PianoVertical { - time_zoom: usize, - note_zoom: PhraseViewNoteZoom, - }, + pub(crate) view_mode: Box, } impl std::fmt::Debug for PhraseEditorModel { @@ -65,7 +51,6 @@ impl Default for PhraseEditorModel { Self { phrase: None, note_len: 24, - buffer: Default::default(), notes_in: RwLock::new([false;128]).into(), notes_out: RwLock::new([false;128]).into(), now: Pulse::default().into(), @@ -75,10 +60,11 @@ impl Default for PhraseEditorModel { note_point: 0.into(), time_start: 0.into(), time_point: 0.into(), - view_mode: PhraseViewMode::PianoHorizontal { + view_mode: Box::new(PianoHorizontal { + buffer: Default::default(), time_zoom: 24, note_zoom: PhraseViewNoteZoom::N(1) - }, + }), } } } @@ -96,7 +82,7 @@ impl PhraseEditorModel { let end = (start + self.note_len) % phrase.length; phrase.notes[time].push(MidiMessage::NoteOn { key, vel }); phrase.notes[end].push(MidiMessage::NoteOff { key, vel }); - self.buffer = self.view_mode.draw(&phrase); + self.view_mode.show(Some(&phrase), self.note_len); } } /// Move time cursor forward by current note length @@ -109,61 +95,43 @@ impl PhraseEditorModel { /// Select which pattern to display. This pre-renders it to the buffer at full resolution. pub fn show_phrase (&mut self, phrase: Option>>) { if phrase.is_some() { - self.buffer = self.view_mode.draw(&*phrase.as_ref().unwrap().read().unwrap()); self.phrase = phrase; + let phrase = &*self.phrase.as_ref().unwrap().read().unwrap(); + self.view_mode.show(Some(&phrase), self.note_len); } else { - self.buffer = Default::default(); + self.view_mode.show(None, self.note_len); self.phrase = None; } } } -pub trait HasEditor { - fn editor (&self) -> &PhraseEditorModel; - fn editor_focused (&self) -> bool; - fn editor_entered (&self) -> bool; -} - -impl HasEditor for SequencerTui { - fn editor (&self) -> &PhraseEditorModel { - &self.editor - } - fn editor_focused (&self) -> bool { - self.focused() == SequencerFocus::PhraseEditor - } - fn editor_entered (&self) -> bool { - self.entered() && self.editor_focused() - } -} - -impl HasEditor for ArrangerTui { - fn editor (&self) -> &PhraseEditorModel { - &self.editor - } - fn editor_focused (&self) -> bool { - self.focused() == ArrangerFocus::PhraseEditor - } - fn editor_entered (&self) -> bool { - self.entered() && self.editor_focused() - } -} - pub struct PhraseView<'a> { - focused: bool, - entered: bool, - phrase: &'a Option>>, - buffer: &'a BigBuffer, - note_len: usize, - now: &'a Arc, - size: &'a Measure, - view_mode: &'a PhraseViewMode, note_point: usize, note_range: (usize, usize), - note_names: (&'a str, &'a str), time_start: usize, time_point: usize, + note_len: usize, + phrase: &'a Option>>, + view_mode: &'a Box, + now: &'a Arc, + size: &'a Measure, + focused: bool, + entered: bool, } - +render!(|self: PhraseView<'a>|{ + let bg = if self.focused { TuiTheme::g(32) } else { Color::Reset }; + let fg = self.phrase.as_ref() + .map(|p|p.read().unwrap().color.clone()) + .unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))); + Tui::bg(bg, lay!([ + PhraseTimeline(&self, fg), + PhraseViewNotes(&self, fg), + PhraseViewCursor(&self), + PhraseViewKeys(&self, fg), + PhraseViewStats(&self, fg), + //Measure::debug(), + ])) +}); impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { fn from (state: &'a T) -> Self { let editor = state.editor(); @@ -181,88 +149,118 @@ impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { editor.note_lo.store(note_lo, Ordering::Relaxed); } Self { - focused: state.editor_focused(), - entered: state.editor_entered(), - note_len: editor.note_len, - phrase: &editor.phrase, - buffer: &editor.buffer, - now: &editor.now, - size: &editor.size, - view_mode: &editor.view_mode, - note_point, note_range: (note_lo, note_hi), - note_names: (to_note_name(note_lo), to_note_name(note_hi)), - time_start: editor.time_start.load(Ordering::Relaxed), time_point: editor.time_point.load(Ordering::Relaxed), + note_len: editor.note_len, + phrase: &editor.phrase, + view_mode: &editor.view_mode, + size: &editor.size, + now: &editor.now, + focused: state.editor_focused(), + entered: state.editor_entered(), } } } -render!(|self: PhraseView<'a>|{ - let Self { - focused, entered, size, - phrase, view_mode, buffer, - note_point, note_len, - note_range: (note_lo, note_hi), - time_start, time_point, - //now: _, - .. - } = self; - let phrase_color = phrase.as_ref().map(|p|p.read().unwrap().color.clone()) - .unwrap_or(ItemColorTriplet::from(ItemColor::from(TuiTheme::g(64)))); - let title_color = if *focused{phrase_color.light.rgb}else{phrase_color.dark.rgb}; - let bg = if self.focused { TuiTheme::bg() } else { Color::Reset }; +pub struct PhraseTimeline<'a>(&'a PhraseView<'a>, ItemPalette); +render!(|self: PhraseTimeline<'a>|Tui::fg(TuiTheme::g(224), Tui::push_x(5, format!("|000.00.00")))); + +pub struct PhraseViewStats<'a>(&'a PhraseView<'a>, ItemPalette); +render!(|self: PhraseViewStats<'a>|{ + let title_color = if self.0.focused{self.1.light.rgb}else{self.1.dark.rgb}; lay!([ - Tui::bg(bg, Tui::inset_x(1, Tui::fill_x(row!([ - Tui::push_y(1, Tui::fill_y(Widget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{ - Ok(if to.area().h() >= 2 { view_mode.render_keys(to, *note_hi, *note_lo) }) - }))), - Tui::fill_x(lay!([ - Tui::push_y(1, Tui::fill_x(Widget::new(|to|Ok(Some(to)), |to: &mut TuiOutput|{ - size.set_wh(to.area.w(), to.area.h() as usize - 1); - let draw = to.area().h() >= 2; - Ok(if draw { view_mode.render_notes(to, buffer, *time_start, *note_hi) }) - }))), - Tui::push_y(1, Widget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ - Ok(if *focused && *entered { - view_mode.render_cursor( - to, - *time_point, *time_start, view_mode.time_zoom(), - *note_point, *note_len, *note_hi, *note_lo, - ) - }) - })) - ])), - ])))), - lay!(move|add|{ - add(&Lozenge(Style::default().bg(phrase_color.base.rgb).fg(phrase_color.base.rgb)))?; - let upper_left = format!("{}", - phrase.as_ref().map(|p|p.read().unwrap().name.clone()).unwrap_or(String::new()) - ); - let lower_left = format!(""); - let mut lower_right = format!(" {} ", size.format()); - if *focused && *entered { - lower_right = format!("Note: {} ({}) {} {lower_right}", - note_point, to_note_name(*note_point), pulses_to_name(*note_len) + Tui::at_sw({ + let mut lower_right = format!(" {} ", self.0.size.format()); + if self.0.focused && self.0.entered { + lower_right = format!( + "Note: {} ({}) {} {lower_right}", + self.0.note_point, + to_note_name(self.0.note_point), + pulses_to_name(self.0.note_len), ); } - let mut upper_right = format!("[{}]", if *entered {"■"} else {" "}); - if let Some(phrase) = phrase { - upper_right = format!("Time: {}/{} {} {upper_right}", - time_point, phrase.read().unwrap().length, pulses_to_name(view_mode.time_zoom()), + Tui::bg(title_color, Tui::fg(TuiTheme::g(224), lower_right)) + }), + Tui::fill_xy(Tui::at_se({ + let mut upper_right = format!("[{}]", if self.0.entered {"■"} else {" "}); + if let Some(phrase) = self.0.phrase { + upper_right = format!( + "Time: {}/{} {} {upper_right}", + self.0.time_point, + phrase.read().unwrap().length, + pulses_to_name(self.0.view_mode.time_zoom()), ) }; - add(&Tui::push_x(1, Tui::at_nw(Tui::fg(TuiTheme::g(224), Tui::bg(title_color, upper_left)))))?; - add(&Tui::at_sw(Tui::bg(title_color, Tui::fg(TuiTheme::g(224), lower_left))))?; - add(&Tui::fill_xy(Tui::at_ne(Tui::pull_x(1, Tui::bg(title_color, Tui::fg(TuiTheme::g(224), upper_right))))))?; - add(&Tui::fill_xy(Tui::at_se(Tui::pull_x(1, Tui::bg(title_color, Tui::fg(TuiTheme::g(224), lower_right))))))?; - Ok(()) - }), + Tui::pull_x(1, Tui::bg(title_color, Tui::fg(TuiTheme::g(224), upper_right))) + })), ]) }); +struct PhraseViewKeys<'a>(&'a PhraseView<'a>, ItemPalette); +render!(|self: PhraseViewKeys<'a>|{ + let layout = |to:[u16;2]|Ok(Some(to.clip_w(5))); + Tui::fill_xy(Widget::new(layout, |to: &mut TuiOutput|Ok( + self.0.view_mode.render_keys(to, self.1.dark.rgb, Some(self.0.note_point), self.0.note_range) + ))) +}); + +struct PhraseViewNotes<'a>(&'a PhraseView<'a>, ItemPalette); +render!(|self: PhraseViewNotes<'a>|Tui::fill_xy(render(|to: &mut TuiOutput|{ + self.0.size.set_wh(to.area.w(), to.area.h() as usize); + Ok(self.0.view_mode.render_notes(to, self.0.time_start, self.0.note_range.1)) +}))); + +struct PhraseViewCursor<'a>(&'a PhraseView<'a>); +render!(|self: PhraseViewCursor<'a>|Tui::fill_xy(render(|to: &mut TuiOutput|Ok( + self.0.view_mode.render_cursor( + to, + self.0.time_point, + self.0.time_start, + self.0.view_mode.time_zoom(), + self.0.note_point, + self.0.note_len, + self.0.note_range.1, + self.0.note_range.0, + ) +)))); + +#[derive(Copy, Clone, Debug)] +pub enum PhraseEditMode { + Note, + Scroll, +} + +pub trait PhraseViewMode { + fn show (&mut self, phrase: Option<&Phrase>, note_len: usize); + fn time_zoom (&self) -> usize; + fn set_time_zoom (&mut self, time_zoom: usize); + fn buffer_width (&self, phrase: &Phrase) -> usize; + fn buffer_height (&self, phrase: &Phrase) -> usize; + fn render_keys (&self, + to: &mut TuiOutput, color: Color, point: Option, range: (usize, usize)); + fn render_notes (&self, + to: &mut TuiOutput, time_start: usize, note_hi: usize); + fn render_cursor ( + &self, + to: &mut TuiOutput, + time_point: usize, + time_start: usize, + time_zoom: usize, + note_point: usize, + note_len: usize, + note_hi: usize, + note_lo: usize, + ); +} + +pub struct PianoHorizontal { + time_zoom: usize, + note_zoom: PhraseViewNoteZoom, + buffer: BigBuffer, +} + #[derive(Copy, Clone, Debug)] pub enum PhraseViewNoteZoom { N(usize), @@ -270,97 +268,100 @@ pub enum PhraseViewNoteZoom { Octant, } -impl PhraseViewMode { - pub fn time_zoom (&self) -> usize { - match self { - Self::PianoHorizontal { time_zoom, .. } => *time_zoom, +impl PhraseViewMode for PianoHorizontal { + fn time_zoom (&self) -> usize { + self.time_zoom + } + fn set_time_zoom (&mut self, time_zoom: usize) { + self.time_zoom = time_zoom + } + fn show (&mut self, phrase: Option<&Phrase>, note_len: usize) { + if let Some(phrase) = phrase { + self.buffer = BigBuffer::new(self.buffer_width(phrase), self.buffer_height(phrase)); + draw_piano_horizontal(&mut self.buffer, phrase, self.time_zoom, note_len); + } else { + self.buffer = Default::default(); + } + } + fn buffer_width (&self, phrase: &Phrase) -> usize { + phrase.length / self.time_zoom + } + /// Determine the required height to render the phrase. + fn buffer_height (&self, phrase: &Phrase) -> usize { + match self.note_zoom { + PhraseViewNoteZoom::Half => 64, + PhraseViewNoteZoom::N(n) => 128*n, _ => unimplemented!() } } - pub fn set_time_zoom (&mut self, time_zoom: usize) { - *self = match self { - Self::PianoHorizontal { note_zoom, .. } => Self::PianoHorizontal { - note_zoom: *note_zoom, - time_zoom, - }, - _ => unimplemented!() - } - } - /// Return a new [BigBuffer] containing a render of the phrase. - pub fn draw (&self, phrase: &Phrase) -> BigBuffer { - let mut buffer = BigBuffer::new(self.buffer_width(phrase), self.buffer_height(phrase)); - match self { - Self::PianoHorizontal { time_zoom, note_zoom } => match note_zoom { - PhraseViewNoteZoom::N(_) => Self::draw_piano_horizontal( - &mut buffer, phrase, *time_zoom - ), - _ => unimplemented!(), - }, - _ => unimplemented!(), - } - buffer - } - /// Draw a subsection of the [BigBuffer] onto a regular ratatui [Buffer]. fn render_notes ( &self, - target: &mut TuiOutput, - source: &BigBuffer, + target: &mut TuiOutput, time_start: usize, note_hi: usize, ) { - let area = target.area(); + let [x0, y0, w, h] = target.area().xywh(); + let source = &self.buffer; let target = &mut target.buffer; - match self { - Self::PianoHorizontal { .. } => { - let [x0, y0, w, h] = area.xywh(); - for (x, target_x) in (x0..x0+w).enumerate() { - for (y, target_y) in (y0..y0+h).enumerate() { - if y > note_hi { - break - } - let source_x = time_start + x; - let source_y = note_hi - y; - // TODO: enable loop rollover: - //let source_x = (time_start + x) % source.width.max(1); - //let source_y = (note_hi - y) % source.height.max(1); - if source_x < source.width && source_y < source.height { - let target_cell = target.get_mut(target_x, target_y); - if let Some(source_cell) = source.get(source_x, source_y) { - *target_cell = source_cell.clone(); - } - } + for (x, target_x) in (x0..x0+w).enumerate() { + + for (y, target_y) in (y0..y0+h).enumerate() { + + if y > note_hi { + break + } + + let source_x = time_start + x; + let source_y = note_hi - y; + + // TODO: enable loop rollover: + //let source_x = (time_start + x) % source.width.max(1); + //let source_y = (note_hi - y) % source.height.max(1); + if source_x < source.width && source_y < source.height { + let target_cell = target.get_mut(target_x, target_y); + if let Some(source_cell) = source.get(source_x, source_y) { + *target_cell = source_cell.clone(); } } - }, - _ => unimplemented!() + + } + } } - fn render_keys (&self, to: &mut TuiOutput, note_hi: usize, note_lo: usize) { - let style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0))); - match self { - Self::PianoHorizontal { .. } => { - let [x0, y0, _, _] = to.area().xywh(); - for (y, note) in (note_lo..=note_hi).rev().enumerate() { - let key = match note % 12 { - 11 => "█████", - 10 => " ", - 9 => "█████", - 8 => " ", - 7 => "█████", - 6 => " ", - 5 => "█████", - 4 => "█████", - 3 => " ", - 2 => "█████", - 1 => " ", - 0 => "█████", - _ => unreachable!(), - }; - to.blit(&key, x0, y0 + y as u16, style); - to.blit(&format!("{}", to_note_name(note)), x0, y0 + y as u16, None); - } - }, - _ => unimplemented!() + fn render_keys ( + &self, + to: &mut TuiOutput, + color: Color, + point: Option, + (note_lo, note_hi): (usize, usize) + ) { + let [x, y0, _, _] = to.area().xywh(); + let key_style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0))); + let note_off_style = Some(Style::default().fg(TuiTheme::g(160))); + let note_on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(color).bold()); + for (y, note) in (note_lo..=note_hi).rev().enumerate().map(|(y, n)|(y0 + y as u16, n)) { + let key = match note % 12 { + 11 => "████▌", + 10 => " ", + 9 => "████▌", + 8 => " ", + 7 => "████▌", + 6 => " ", + 5 => "████▌", + 4 => "████▌", + 3 => " ", + 2 => "████▌", + 1 => " ", + 0 => "████▌", + _ => unreachable!(), + }; + to.blit(&key, x, y, key_style); + + if Some(note) == point { + to.blit(&format!("{:<5}", to_note_name(note)), x, y, note_on_style) + } else { + to.blit(&to_note_name(note), x, y, note_off_style) + }; } } fn render_cursor ( @@ -374,121 +375,147 @@ impl PhraseViewMode { note_hi: usize, note_lo: usize, ) { + let [x0, y0, w, _] = to.area().xywh(); let style = Some(Style::default().fg(Color::Rgb(0,255,0))); - match self { - Self::PianoHorizontal { .. } => { - let [x0, y0, w, _] = to.area().xywh(); - for (y, note) in (note_lo..=note_hi).rev().enumerate() { - if note == note_point { - for x in 0..w { - let time_1 = time_start + x as usize * time_zoom; - let time_2 = time_1 + time_zoom; - if time_1 <= time_point && time_point < time_2 { - to.blit(&"█", x0 + x as u16, y0 + y as u16, style); - let tail = note_len as u16 / time_zoom as u16; - for x_tail in (x0 + x + 1)..(x0 + x + tail) { - to.blit(&"▄", x_tail, y0 + y as u16, style); - } - break - } + for (y, note) in (note_lo..=note_hi).rev().enumerate() { + if note == note_point { + for x in 0..w { + let time_1 = time_start + x as usize * time_zoom; + let time_2 = time_1 + time_zoom; + if time_1 <= time_point && time_point < time_2 { + to.blit(&"█", x0 + x as u16, y0 + y as u16, style); + let tail = note_len as u16 / time_zoom as u16; + for x_tail in (x0 + x + 1)..(x0 + x + tail) { + to.blit(&"▄", x_tail, y0 + y as u16, style); } break } } - }, - _ => unimplemented!() + break + } } } - /// Determine the required width to render the phrase. - fn buffer_width (&self, phrase: &Phrase) -> usize { - match self { - Self::PianoHorizontal { time_zoom, .. } => { - phrase.length / time_zoom - }, - Self::PianoVertical { note_zoom, .. } => match note_zoom { - PhraseViewNoteZoom::Half => 64, - PhraseViewNoteZoom::N(n) => 128*n, - _ => unimplemented!() - }, +} + +fn draw_piano_horizontal_bg ( + target: &mut BigBuffer, + phrase: &Phrase, + time_zoom: usize, + note_len: usize, +) { + for (y, note) in (0..127).rev().enumerate() { + for (x, time) in (0..target.width).map(|x|(x, x*time_zoom)) { + let cell = target.get_mut(x, y).unwrap(); + //cell.set_fg(Color::Rgb(48, 55, 45)); + cell.set_fg(phrase.color.dark.rgb); + cell.set_char(if time % 384 == 0 { + '│' + } else if time % 96 == 0 { + '╎' + } else if time % note_len == 0 { + '┊' + } else if (127 - note) % 12 == 1 { + '=' + } else { + '·' + }); } } - /// Determine the required height to render the phrase. - fn buffer_height (&self, phrase: &Phrase) -> usize { - match self { - Self::PianoHorizontal { note_zoom, .. } => match note_zoom { - PhraseViewNoteZoom::Half => 64, - PhraseViewNoteZoom::N(n) => 128*n, - _ => unimplemented!() - }, - Self::PianoVertical { time_zoom, .. } => { - phrase.length / time_zoom - }, - } - } - /// Draw the piano roll using full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_piano_horizontal ( - target: &mut BigBuffer, - phrase: &Phrase, - time_zoom: usize, - ) { - let color = phrase.color.light.rgb; - let style = Style::default().fg(color);//.bg(Color::Rgb(0, 0, 0)); +} + +fn draw_piano_horizontal_fg ( + target: &mut BigBuffer, + phrase: &Phrase, + time_zoom: usize, +) { + let style = Style::default().fg(phrase.color.light.rgb);//.bg(Color::Rgb(0, 0, 0)); + let mut notes_on = [false;128]; + for (x, time_start) in (0..phrase.length).step_by(time_zoom).enumerate() { + for (y, note) in (0..127).rev().enumerate() { - for (x, time) in (0..target.width).map(|x|(x, x*time_zoom)) { - let cell = target.get_mut(x, y).unwrap(); - //cell.set_fg(Color::Rgb(48, 55, 45)); - cell.set_fg(phrase.color.dark.rgb); - cell.set_char(if time % 384 == 0 { - '│' - } else if time % 96 == 0 { - '╎' - } else if note % 12 == 0 { - '=' - } else { - '·' - }); + let cell = target.get_mut(x, note).unwrap(); + if notes_on[note] { + cell.set_char('▄'); + cell.set_style(style); } } - let mut notes_on = [false;128]; - for (x, time_start) in (0..phrase.length).step_by(time_zoom).enumerate() { - let time_end = time_start + time_zoom; - for (y, note) in (0..127).rev().enumerate() { - let cell = target.get_mut(x, note).unwrap(); - if notes_on[note] { - cell.set_char('▄'); - cell.set_style(style); - } - } - for time in time_start..time_end { - for event in phrase.notes[time].iter() { - match event { - MidiMessage::NoteOn { key, .. } => { - let note = key.as_int() as usize; - let cell = target.get_mut(x, note).unwrap(); - cell.set_char('█'); - cell.set_style(style); - notes_on[note] = true - }, - MidiMessage::NoteOff { key, .. } => { - notes_on[key.as_int() as usize] = false - }, - _ => {} - } + + let time_end = time_start + time_zoom; + for time in time_start..time_end { + for event in phrase.notes[time].iter() { + match event { + MidiMessage::NoteOn { key, .. } => { + let note = key.as_int() as usize; + let cell = target.get_mut(x, note).unwrap(); + cell.set_char('█'); + cell.set_style(style); + notes_on[note] = true + }, + MidiMessage::NoteOff { key, .. } => { + notes_on[key.as_int() as usize] = false + }, + _ => {} } } } + } - /// TODO: Draw the piano roll using octant blocks (U+1CD00-U+1CDE5) - fn draw_piano_horizontal_octant ( - _: &mut BigBuffer, _: &Phrase, _: usize - ) { - unimplemented!() +} + +/// Draw the piano roll using full blocks on note on and half blocks on legato: █▄ █▄ █▄ +fn draw_piano_horizontal ( + target: &mut BigBuffer, + phrase: &Phrase, + time_zoom: usize, + note_len: usize, +) { + let color = phrase.color.light.rgb; + let style = Style::default().fg(color);//.bg(Color::Rgb(0, 0, 0)); + for (y, note) in (0..127).rev().enumerate() { + for (x, time) in (0..target.width).map(|x|(x, x*time_zoom)) { + let cell = target.get_mut(x, y).unwrap(); + //cell.set_fg(Color::Rgb(48, 55, 45)); + cell.set_fg(phrase.color.dark.rgb); + cell.set_char(if time % 384 == 0 { + '│' + } else if time % 96 == 0 { + '╎' + } else if time % note_len == 0 { + '┊' + } else if (127 - note) % 12 == 1 { + '=' + } else { + '·' + }); + } } - /// TODO: Draw the piano roll using half blocks: ▄▀▄ - fn draw_piano_horizontal_half ( - _: &mut BigBuffer, _: &Phrase, _: usize - ) { - unimplemented!() + let mut notes_on = [false;128]; + for (x, time_start) in (0..phrase.length).step_by(time_zoom).enumerate() { + let time_end = time_start + time_zoom; + for (y, note) in (0..127).rev().enumerate() { + let cell = target.get_mut(x, note).unwrap(); + if notes_on[note] { + cell.set_char('▄'); + cell.set_style(style); + } + } + for time in time_start..time_end { + for event in phrase.notes[time].iter() { + match event { + MidiMessage::NoteOn { key, .. } => { + let note = key.as_int() as usize; + let cell = target.get_mut(x, note).unwrap(); + cell.set_char('█'); + cell.set_style(style); + notes_on[note] = true + }, + MidiMessage::NoteOff { key, .. } => { + notes_on[key.as_int() as usize] = false + }, + _ => {} + } + } + } } } diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index d11d202b..d0c2f8b7 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -122,12 +122,13 @@ impl<'a, T: HasPhraseList> From<&'a T> for PhraseListView<'a> { // TODO: Display phrases always in order of appearance render!(|self: PhraseListView<'a>|{ let Self { title, focused, entered, phrases, index, mode } = self; + let bg = if *focused {TuiTheme::g(32)} else {TuiTheme::null()}; let border_bg = if *focused {TuiTheme::bg()} else {TuiTheme::null()}; let border_color = if *entered {TuiTheme::bo1()} else {TuiTheme::bo2()}; let title_color = if *focused {TuiTheme::ti1()} else {TuiTheme::ti2()}; let upper_left = format!("{title}"); let upper_right = format!("({})", phrases.len()); - Tui::bg(border_bg, lay!(move|add|{ + Tui::bg(bg, lay!(move|add|{ //add(&Lozenge(Style::default().bg(border_bg).fg(border_color)))?; add(&Tui::inset_xy(0, 1, Tui::fill_xy(col!(move|add|match mode { Some(PhrasesMode::Import(_, ref browser)) => { @@ -289,14 +290,14 @@ fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option Cmd::Phrase(Pool::Add(count, Phrase::new( - String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random()) + String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random()) ))), key!(Char('i')) => Cmd::Phrase(Pool::Add(index + 1, Phrase::new( - String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random()) + String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random()) ))), key!(Char('d')) => { let mut phrase = state.phrases()[index].read().unwrap().duplicate(); - phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25); + phrase.color = ItemPalette::random_near(phrase.color, 0.25); Cmd::Phrase(Pool::Add(index + 1, phrase)) }, _ => return None