From a3beab0f36aa1566cecf97715993752a2488fef3 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 4 May 2025 18:57:31 +0300 Subject: [PATCH] modal -> dialog; extract dialog, selection, editor --- crates/app/src/api.rs | 26 ++- crates/app/src/model.rs | 363 +----------------------------- crates/app/src/model/dialog.rs | 33 +++ crates/app/src/model/editor.rs | 170 ++++++++++++++ crates/app/src/model/selection.rs | 147 ++++++++++++ crates/app/src/view.rs | 24 +- crates/device/src/lv2/lv2_tui.rs | 5 - 7 files changed, 391 insertions(+), 377 deletions(-) create mode 100644 crates/app/src/model/dialog.rs create mode 100644 crates/app/src/model/editor.rs create mode 100644 crates/app/src/model/selection.rs diff --git a/crates/app/src/api.rs b/crates/app/src/api.rs index 995e942d..4f7504bf 100644 --- a/crates/app/src/api.rs +++ b/crates/app/src/api.rs @@ -20,8 +20,8 @@ handle!(TuiIn: |self: Tek, input|Ok(if let Some(command) = self.config.keys.comm expose!([self: Tek] ([bool] (":mode-editor" self.is_editing()) - (":mode-message" matches!(self.modal, Some(Modal::Message(..)))) - (":mode-device-add" matches!(self.modal, Some(Modal::Device(..)))) + (":mode-message" matches!(self.dialog, Some(Dialog::Message(..)))) + (":mode-device-add" matches!(self.dialog, Some(Dialog::Device(..)))) (":mode-clip" !self.is_editing() && self.selected.is_clip()) (":mode-track" !self.is_editing() && self.selected.is_track()) (":mode-scene" !self.is_editing() && self.selected.is_scene()) @@ -48,17 +48,17 @@ expose!([self: Tek] ([usize] (":scene-last" self.scenes.len()) (":track-last" self.tracks.len()) - (":device-kind" if let Some(Modal::Device(index)) = self.modal { + (":device-kind" if let Some(Dialog::Device(index)) = self.dialog { index } else { 0 }) - (":device-kind-prev" if let Some(Modal::Device(index)) = self.modal { + (":device-kind-prev" if let Some(Dialog::Device(index)) = self.dialog { index.overflowing_sub(1).0.min(self.device_kinds().len().saturating_sub(1)) } else { 0 }) - (":device-kind-next" if let Some(Modal::Device(index)) = self.modal { + (":device-kind-next" if let Some(Dialog::Device(index)) = self.dialog { (index + 1) % self.device_kinds().len() } else { 0 @@ -192,6 +192,18 @@ impose!([app: Tek] ("rec" [] Some(Self::ToggleRec)) ("mon" [] Some(Self::ToggleMon)))); +//#[tengri_proc::input(TuiIn)] +//impl Tek { + //#[tengri::command("sampler", TekCommand::Sampler)] + //fn cmd_sampler (&mut self, cmd: SamplerCommand) -> Perhaps { + //self.sampler_mut().map(|s|cmd.delegate(s, Self::Sampler)).transpose()?.flatten()) + //} + //#[tengri::command("scene", TekCommand::Scene)] + //fn cmd_scene (&mut self, cmd: SceneCommand) -> Perhaps { + //cmd.delegate(self, scene) + //} +//} + defcom!([self, app: Tek] (TekCommand @@ -206,8 +218,8 @@ defcom!([self, app: Tek] (Message [cmd: MessageCommand] cmd.delegate(app, Self::Message)?) (Editor [cmd: MidiEditCommand] delegate_to_editor(app, cmd)?) (Pool [cmd: PoolCommand] delegate_to_pool(app, cmd)?) - (ToggleHelp [] cmd!(app.toggle_modal(Some(Modal::Help)))) - (ToggleMenu [] cmd!(app.toggle_modal(Some(Modal::Menu)))) + (ToggleHelp [] cmd!(app.toggle_dialog(Some(Dialog::Help)))) + (ToggleMenu [] cmd!(app.toggle_dialog(Some(Dialog::Menu)))) (Color [p: ItemTheme] app.set_color(Some(p)).map(Self::Color)) (Enqueue [c: MaybeClip] cmd_todo!("\n\rtodo: enqueue {c:?}")) (History [d: isize] cmd_todo!("\n\rtodo: history {d:?}")) diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index 8e95ba83..343bc02a 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -1,5 +1,9 @@ use crate::*; +mod dialog; pub use self::dialog::*; +mod editor; pub use self::editor::*; +mod selection; pub use self::selection::*; + #[derive(Default, Debug)] pub struct Tek { /// Must not be dropped for the duration of the process @@ -50,8 +54,8 @@ pub struct Tek { pub ports: std::collections::BTreeMap>, // Cache of formatted strings pub view_cache: Arc>, - // Modal overlay - pub modal: Option, + // Dialog overlay + pub dialog: Option, // View and input definition pub config: Configuration } @@ -204,14 +208,6 @@ impl Tek { } } - pub fn toggle_modal (&mut self, modal: Option) { - self.modal = if self.modal == modal { - None - } else { - modal - } - } - // Create new clip in pool when entering empty cell pub fn clip_auto_create (&mut self) { if let Some(ref pool) = self.pool @@ -401,11 +397,11 @@ impl Tek { } pub(crate) fn device_picker_show (&mut self) { - self.modal = Some(Modal::Device(0)); + self.dialog = Some(Dialog::Device(0)); } pub(crate) fn device_pick (&mut self, index: usize) { - self.modal = Some(Modal::Device(index)); + self.dialog = Some(Dialog::Device(index)); } pub(crate) fn device_add (&mut self, index: usize) -> Usually<()> { @@ -426,10 +422,10 @@ impl Tek { &[&[], &[]], &[&[], &[]] ) { - self.modal = None; + self.dialog = None; Device::Sampler(sampler) } else { - self.modal = Some(Modal::Message(Message::FailedToAddDevice)); + self.dialog = Some(Dialog::Message(Message::FailedToAddDevice)); return Err("failed to add device".into()) }; self.track_mut().expect("no active track").devices.push(sampler); @@ -440,11 +436,6 @@ impl Tek { todo!(); Ok(()) } - - pub(crate) fn message_dismiss (&mut self) { - self.modal = None; - } - } has_size!(|self: Tek|&self.size); @@ -466,171 +457,6 @@ has_editor!(|self: Tek|{ is_editing = self.editing.load(Relaxed); }); -pub trait HasSelection { - fn selected (&self) -> &Selection; - fn selected_mut (&mut self) -> &mut Selection; -} - -/// Various possible modal overlays -#[derive(PartialEq, Clone, Copy, Debug)] -pub enum Modal { - Help, - Menu, - Device(usize), - Message(Message) -} - -/// Various possible messages -#[derive(PartialEq, Clone, Copy, Debug)] -pub enum Message { - FailedToAddDevice, -} - -content!(TuiOut: |self: Message| match self { - Self::FailedToAddDevice => "Failed to add device." -}); - -/// Represents the current user selection in the arranger -#[derive(PartialEq, Clone, Copy, Debug, Default)] -pub enum Selection { - /// The whole mix is selected - #[default] Mix, - /// A MIDI input is selected. - Input(usize), - /// A MIDI output is selected. - Output(usize), - /// A scene is selected. - Scene(usize), - /// A track is selected. - Track(usize), - /// A clip (track × scene) is selected. - TrackClip { track: usize, scene: usize }, - /// A track's MIDI input connection is selected. - TrackInput { track: usize, port: usize }, - /// A track's MIDI output connection is selected. - TrackOutput { track: usize, port: usize }, - /// A track device slot is selected. - TrackDevice { track: usize, device: usize }, -} - -/// Focus identification methods -impl Selection { - pub fn is_mix (&self) -> bool { - matches!(self, Self::Mix) - } - pub fn is_track (&self) -> bool { - matches!(self, Self::Track(_)) - } - pub fn is_scene (&self) -> bool { - matches!(self, Self::Scene(_)) - } - pub fn is_clip (&self) -> bool { - matches!(self, Self::TrackClip {..}) - } - pub fn track (&self) -> Option { - use Selection::*; - match self { - Track(track) - | TrackClip { track, .. } - | TrackInput { track, .. } - | TrackOutput { track, .. } - | TrackDevice { track, .. } => Some(*track), - _ => None - } - } - pub fn track_next (&self, len: usize) -> Self { - use Selection::*; - match self { - Mix => Track(0), - Scene(s) => TrackClip { track: 0, scene: *s }, - Track(t) => if t + 1 < len { - Track(t + 1) - } else { - Mix - }, - TrackClip {track, scene} => if track + 1 < len { - TrackClip { track: track + 1, scene: *scene } - } else { - Scene(*scene) - }, - _ => todo!() - } - } - pub fn track_prev (&self) -> Self { - use Selection::*; - match self { - Mix => Mix, - Scene(s) => Scene(*s), - Track(0) => Mix, - Track(t) => Track(t - 1), - TrackClip { track: 0, scene } => Scene(*scene), - TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene }, - _ => todo!() - } - } - pub fn scene (&self) -> Option { - use Selection::*; - match self { - Scene(scene) | TrackClip { scene, .. } => Some(*scene), - _ => None - } - } - pub fn scene_next (&self, len: usize) -> Self { - use Selection::*; - match self { - Mix => Scene(0), - Track(t) => TrackClip { track: *t, scene: 0 }, - Scene(s) => if s + 1 < len { - Scene(s + 1) - } else { - Mix - }, - TrackClip { track, scene } => if scene + 1 < len { - TrackClip { track: *track, scene: scene + 1 } - } else { - Track(*track) - }, - _ => todo!() - } - } - pub fn scene_prev (&self) -> Self { - use Selection::*; - match self { - Mix | Scene(0) => Mix, - Scene(s) => Scene(s - 1), - Track(t) => Track(*t), - TrackClip { track, scene: 0 } => Track(*track), - TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 }, - _ => todo!() - } - } - pub fn describe (&self, tracks: &[Track], scenes: &[Scene]) -> Arc { - use Selection::*; - format!("{}", match self { - Mix => "Everything".to_string(), - Scene(s) => scenes.get(*s) - .map(|scene|format!("S{s}: {}", &scene.name)) - .unwrap_or_else(||"S??".into()), - Track(t) => tracks.get(*t) - .map(|track|format!("T{t}: {}", &track.name)) - .unwrap_or_else(||"T??".into()), - TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { - (Some(_), Some(s)) => match s.clip(*track) { - Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name), - None => format!("T{track} S{scene}: Empty") - }, - _ => format!("T{track} S{scene}: Empty"), - }, - _ => todo!() - }).into() - } -} - -impl HasSelection for Tek { - fn selected (&self) -> &Selection { &self.selected } - fn selected_mut (&mut self) -> &mut Selection { &mut self.selected } -} - pub trait HasScenes: HasSelection + HasEditor + Send + Sync { fn scenes (&self) -> &Vec; fn scenes_mut (&mut self) -> &mut Vec; @@ -1053,172 +879,3 @@ pub trait HasClips { } } } - -pub trait HasEditor { - fn editor (&self) -> &Option; - fn editor_mut (&mut self) -> &Option; - fn is_editing (&self) -> bool { true } - fn editor_w (&self) -> usize { 0 } - fn editor_h (&self) -> usize { 0 } -} - -#[macro_export] macro_rules! has_editor { - (|$self:ident: $Struct:ident|{ - editor = $e0:expr; - editor_w = $e1:expr; - editor_h = $e2:expr; - is_editing = $e3:expr; - }) => { - impl HasEditor for $Struct { - fn editor (&$self) -> &Option { &$e0 } - fn editor_mut (&mut $self) -> &Option { &mut $e0 } - fn editor_w (&$self) -> usize { $e1 } - fn editor_h (&$self) -> usize { $e2 } - fn is_editing (&$self) -> bool { $e3 } - } - }; - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? { - fn editor (&$self) -> &MidiEditor { &$cb } - } - }; -} - -/// Contains state for viewing and editing a clip -pub struct MidiEditor { - /// Size of editor on screen - pub size: Measure, - /// View mode and state of editor - pub mode: PianoHorizontal, -} - -impl std::fmt::Debug for MidiEditor { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("MidiEditor") - .field("mode", &self.mode) - .finish() - } -} - -impl Default for MidiEditor { - fn default () -> Self { - Self { - size: Measure::new(), - mode: PianoHorizontal::new(None), - } - } -} - - -has_size!(|self: MidiEditor|&self.size); - -content!(TuiOut: |self: MidiEditor| { - self.autoscroll(); - //self.autozoom(); - self.size.of(&self.mode) -}); - -from!(|clip: &Arc>|MidiEditor = { - let model = Self::from(Some(clip.clone())); - model.redraw(); - model -}); - -from!(|clip: Option>>|MidiEditor = { - let mut model = Self::default(); - *model.clip_mut() = clip; - model.redraw(); - model -}); - -impl MidiEditor { - /// Put note at current position - pub fn put_note (&mut self, advance: bool) { - let mut redraw = false; - if let Some(clip) = self.clip() { - let mut clip = clip.write().unwrap(); - let note_start = self.time_pos(); - let note_pos = self.note_pos(); - let note_len = self.note_len(); - let note_end = note_start + (note_len.saturating_sub(1)); - let key: u7 = u7::from(note_pos as u8); - let vel: u7 = 100.into(); - let length = clip.length; - let note_end = note_end % length; - let note_on = MidiMessage::NoteOn { key, vel }; - if !clip.notes[note_start].iter().any(|msg|*msg == note_on) { - clip.notes[note_start].push(note_on); - } - let note_off = MidiMessage::NoteOff { key, vel }; - if !clip.notes[note_end].iter().any(|msg|*msg == note_off) { - clip.notes[note_end].push(note_off); - } - if advance { - self.set_time_pos(note_end); - } - redraw = true; - } - if redraw { - self.mode.redraw(); - } - } - - pub fn clip_status (&self) -> impl Content + '_ { - let (color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { - (clip.color, clip.name.clone(), clip.length, clip.looped) - } else { (ItemTheme::G[64], String::new().into(), 0, false) }; - Bsp::e( - FieldH(color, "Edit", format!("{name} ({length})")), - FieldH(color, "Loop", looped.to_string()) - ) - } - - pub fn edit_status (&self) -> impl Content + '_ { - let (color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { - (clip.color, clip.length) - } else { (ItemTheme::G[64], 0) }; - let time_pos = self.time_pos(); - let time_zoom = self.time_zoom().get(); - let time_lock = if self.time_lock().get() { "[lock]" } else { " " }; - let note_pos = format!("{:>3}", self.note_pos()); - let note_name = format!("{:4}", Note::pitch_to_name(self.note_pos())); - let note_len = format!("{:>4}", self.note_len()); - Bsp::e( - FieldH(color, "Time", format!("{length}/{time_zoom}+{time_pos} {time_lock}")), - FieldH(color, "Note", format!("{note_name} {note_pos} {note_len}")), - ) - } -} - -impl TimeRange for MidiEditor { - fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } - fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } -} - -impl NoteRange for MidiEditor { - fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } -} - -impl NotePoint for MidiEditor { - fn note_len (&self) -> usize { self.mode.note_len() } - fn set_note_len (&self, x: usize) -> usize { self.mode.set_note_len(x) } - fn note_pos (&self) -> usize { self.mode.note_pos() } - fn set_note_pos (&self, x: usize) -> usize { self.mode.set_note_pos(x) } -} - -impl TimePoint for MidiEditor { - fn time_pos (&self) -> usize { self.mode.time_pos() } - fn set_time_pos (&self, x: usize) -> usize { self.mode.set_time_pos(x) } -} - -impl MidiViewer for MidiEditor { - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } - fn redraw (&self) { self.mode.redraw() } - fn clip (&self) -> &Option>> { self.mode.clip() } - fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } - fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } -} diff --git a/crates/app/src/model/dialog.rs b/crates/app/src/model/dialog.rs new file mode 100644 index 00000000..092025b9 --- /dev/null +++ b/crates/app/src/model/dialog.rs @@ -0,0 +1,33 @@ +use crate::*; + +impl Tek { + pub fn toggle_dialog (&mut self, dialog: Option) { + self.dialog = if self.dialog == dialog { + None + } else { + dialog + } + } + pub(crate) fn message_dismiss (&mut self) { + self.dialog = None; + } +} + +/// Various possible dialog overlays +#[derive(PartialEq, Clone, Copy, Debug)] +pub enum Dialog { + Help, + Menu, + Device(usize), + Message(Message) +} + +/// Various possible messages +#[derive(PartialEq, Clone, Copy, Debug)] +pub enum Message { + FailedToAddDevice, +} + +content!(TuiOut: |self: Message| match self { + Self::FailedToAddDevice => "Failed to add device." +}); diff --git a/crates/app/src/model/editor.rs b/crates/app/src/model/editor.rs new file mode 100644 index 00000000..355b0e8c --- /dev/null +++ b/crates/app/src/model/editor.rs @@ -0,0 +1,170 @@ +use crate::*; + +/// Contains state for viewing and editing a clip +pub struct MidiEditor { + /// Size of editor on screen + pub size: Measure, + /// View mode and state of editor + pub mode: PianoHorizontal, +} + +impl std::fmt::Debug for MidiEditor { + fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("MidiEditor") + .field("mode", &self.mode) + .finish() + } +} + +impl Default for MidiEditor { + fn default () -> Self { + Self { + size: Measure::new(), + mode: PianoHorizontal::new(None), + } + } +} + + +has_size!(|self: MidiEditor|&self.size); + +content!(TuiOut: |self: MidiEditor| { + self.autoscroll(); + //self.autozoom(); + self.size.of(&self.mode) +}); + +from!(|clip: &Arc>|MidiEditor = { + let model = Self::from(Some(clip.clone())); + model.redraw(); + model +}); + +from!(|clip: Option>>|MidiEditor = { + let mut model = Self::default(); + *model.clip_mut() = clip; + model.redraw(); + model +}); + +impl MidiEditor { + /// Put note at current position + pub fn put_note (&mut self, advance: bool) { + let mut redraw = false; + if let Some(clip) = self.clip() { + let mut clip = clip.write().unwrap(); + let note_start = self.time_pos(); + let note_pos = self.note_pos(); + let note_len = self.note_len(); + let note_end = note_start + (note_len.saturating_sub(1)); + let key: u7 = u7::from(note_pos as u8); + let vel: u7 = 100.into(); + let length = clip.length; + let note_end = note_end % length; + let note_on = MidiMessage::NoteOn { key, vel }; + if !clip.notes[note_start].iter().any(|msg|*msg == note_on) { + clip.notes[note_start].push(note_on); + } + let note_off = MidiMessage::NoteOff { key, vel }; + if !clip.notes[note_end].iter().any(|msg|*msg == note_off) { + clip.notes[note_end].push(note_off); + } + if advance { + self.set_time_pos(note_end); + } + redraw = true; + } + if redraw { + self.mode.redraw(); + } + } + + pub fn clip_status (&self) -> impl Content + '_ { + let (color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { + (clip.color, clip.name.clone(), clip.length, clip.looped) + } else { (ItemTheme::G[64], String::new().into(), 0, false) }; + Bsp::e( + FieldH(color, "Edit", format!("{name} ({length})")), + FieldH(color, "Loop", looped.to_string()) + ) + } + + pub fn edit_status (&self) -> impl Content + '_ { + let (color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { + (clip.color, clip.length) + } else { (ItemTheme::G[64], 0) }; + let time_pos = self.time_pos(); + let time_zoom = self.time_zoom().get(); + let time_lock = if self.time_lock().get() { "[lock]" } else { " " }; + let note_pos = format!("{:>3}", self.note_pos()); + let note_name = format!("{:4}", Note::pitch_to_name(self.note_pos())); + let note_len = format!("{:>4}", self.note_len()); + Bsp::e( + FieldH(color, "Time", format!("{length}/{time_zoom}+{time_pos} {time_lock}")), + FieldH(color, "Note", format!("{note_name} {note_pos} {note_len}")), + ) + } +} + +impl TimeRange for MidiEditor { + fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } + fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } + fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } + fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } + fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } +} + +impl NoteRange for MidiEditor { + fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } + fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } +} + +impl NotePoint for MidiEditor { + fn note_len (&self) -> usize { self.mode.note_len() } + fn set_note_len (&self, x: usize) -> usize { self.mode.set_note_len(x) } + fn note_pos (&self) -> usize { self.mode.note_pos() } + fn set_note_pos (&self, x: usize) -> usize { self.mode.set_note_pos(x) } +} + +impl TimePoint for MidiEditor { + fn time_pos (&self) -> usize { self.mode.time_pos() } + fn set_time_pos (&self, x: usize) -> usize { self.mode.set_time_pos(x) } +} + +impl MidiViewer for MidiEditor { + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } + fn redraw (&self) { self.mode.redraw() } + fn clip (&self) -> &Option>> { self.mode.clip() } + fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } + fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } +} + +pub trait HasEditor { + fn editor (&self) -> &Option; + fn editor_mut (&mut self) -> &Option; + fn is_editing (&self) -> bool { true } + fn editor_w (&self) -> usize { 0 } + fn editor_h (&self) -> usize { 0 } +} + +#[macro_export] macro_rules! has_editor { + (|$self:ident: $Struct:ident|{ + editor = $e0:expr; + editor_w = $e1:expr; + editor_h = $e2:expr; + is_editing = $e3:expr; + }) => { + impl HasEditor for $Struct { + fn editor (&$self) -> &Option { &$e0 } + fn editor_mut (&mut $self) -> &Option { &mut $e0 } + fn editor_w (&$self) -> usize { $e1 } + fn editor_h (&$self) -> usize { $e2 } + fn is_editing (&$self) -> bool { $e3 } + } + }; + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? { + fn editor (&$self) -> &MidiEditor { &$cb } + } + }; +} diff --git a/crates/app/src/model/selection.rs b/crates/app/src/model/selection.rs new file mode 100644 index 00000000..4f2141e3 --- /dev/null +++ b/crates/app/src/model/selection.rs @@ -0,0 +1,147 @@ +use crate::*; + +pub trait HasSelection { + fn selected (&self) -> &Selection; + fn selected_mut (&mut self) -> &mut Selection; +} + +/// Represents the current user selection in the arranger +#[derive(PartialEq, Clone, Copy, Debug, Default)] +pub enum Selection { + /// The whole mix is selected + #[default] Mix, + /// A MIDI input is selected. + Input(usize), + /// A MIDI output is selected. + Output(usize), + /// A scene is selected. + Scene(usize), + /// A track is selected. + Track(usize), + /// A clip (track × scene) is selected. + TrackClip { track: usize, scene: usize }, + /// A track's MIDI input connection is selected. + TrackInput { track: usize, port: usize }, + /// A track's MIDI output connection is selected. + TrackOutput { track: usize, port: usize }, + /// A track device slot is selected. + TrackDevice { track: usize, device: usize }, +} + +/// Focus identification methods +impl Selection { + pub fn is_mix (&self) -> bool { + matches!(self, Self::Mix) + } + pub fn is_track (&self) -> bool { + matches!(self, Self::Track(_)) + } + pub fn is_scene (&self) -> bool { + matches!(self, Self::Scene(_)) + } + pub fn is_clip (&self) -> bool { + matches!(self, Self::TrackClip {..}) + } + pub fn track (&self) -> Option { + use Selection::*; + match self { + Track(track) + | TrackClip { track, .. } + | TrackInput { track, .. } + | TrackOutput { track, .. } + | TrackDevice { track, .. } => Some(*track), + _ => None + } + } + pub fn track_next (&self, len: usize) -> Self { + use Selection::*; + match self { + Mix => Track(0), + Scene(s) => TrackClip { track: 0, scene: *s }, + Track(t) => if t + 1 < len { + Track(t + 1) + } else { + Mix + }, + TrackClip {track, scene} => if track + 1 < len { + TrackClip { track: track + 1, scene: *scene } + } else { + Scene(*scene) + }, + _ => todo!() + } + } + pub fn track_prev (&self) -> Self { + use Selection::*; + match self { + Mix => Mix, + Scene(s) => Scene(*s), + Track(0) => Mix, + Track(t) => Track(t - 1), + TrackClip { track: 0, scene } => Scene(*scene), + TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene }, + _ => todo!() + } + } + pub fn scene (&self) -> Option { + use Selection::*; + match self { + Scene(scene) | TrackClip { scene, .. } => Some(*scene), + _ => None + } + } + pub fn scene_next (&self, len: usize) -> Self { + use Selection::*; + match self { + Mix => Scene(0), + Track(t) => TrackClip { track: *t, scene: 0 }, + Scene(s) => if s + 1 < len { + Scene(s + 1) + } else { + Mix + }, + TrackClip { track, scene } => if scene + 1 < len { + TrackClip { track: *track, scene: scene + 1 } + } else { + Track(*track) + }, + _ => todo!() + } + } + pub fn scene_prev (&self) -> Self { + use Selection::*; + match self { + Mix | Scene(0) => Mix, + Scene(s) => Scene(s - 1), + Track(t) => Track(*t), + TrackClip { track, scene: 0 } => Track(*track), + TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 }, + _ => todo!() + } + } + pub fn describe (&self, tracks: &[Track], scenes: &[Scene]) -> Arc { + use Selection::*; + format!("{}", match self { + Mix => "Everything".to_string(), + Scene(s) => scenes.get(*s) + .map(|scene|format!("S{s}: {}", &scene.name)) + .unwrap_or_else(||"S??".into()), + Track(t) => tracks.get(*t) + .map(|track|format!("T{t}: {}", &track.name)) + .unwrap_or_else(||"T??".into()), + TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { + (Some(_), Some(s)) => match s.clip(*track) { + Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name), + None => format!("T{track} S{scene}: Empty") + }, + _ => format!("T{track} S{scene}: Empty"), + }, + _ => todo!() + }).into() + } +} + +impl HasSelection for Tek { + fn selected (&self) -> &Selection { &self.selected } + fn selected_mut (&mut self) -> &mut Selection { &mut self.selected } +} diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index ed73ae06..5778861b 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -60,30 +60,30 @@ impl Tek { self.sampler().map(|s|s.view_sample(self.editor().unwrap().note_pos())) } - #[tengri::view(":modal")] - fn view_modal (&self) -> impl Content + use<'_> { - When::new(self.modal.is_some(), Bsp::b( + #[tengri::view(":dialog")] + fn view_dialog (&self) -> impl Content + use<'_> { + When::new(self.dialog.is_some(), Bsp::b( Fill::xy(Tui::fg_bg(Rgb(64,64,64), Rgb(32,32,32), "")), Fixed::xy(30, 15, Tui::fg_bg(Rgb(255,255,255), Rgb(16,16,16), Bsp::b( Repeat(" "), Outer(true, Style::default().fg(Tui::g(96))) - .enclose(self.modal.as_ref().map(|modal|match modal { - Modal::Menu => self.view_modal_menu().boxed(), - Modal::Help => self.view_modal_help().boxed(), - Modal::Device(index) => self.view_modal_device(*index).boxed(), - Modal::Message(message) => self.view_modal_message(message).boxed(), + .enclose(self.dialog.as_ref().map(|dialog|match dialog { + Dialog::Menu => self.view_dialog_menu().boxed(), + Dialog::Help => self.view_dialog_help().boxed(), + Dialog::Device(index) => self.view_dialog_device(*index).boxed(), + Dialog::Message(message) => self.view_dialog_message(message).boxed(), })) ))) )) } - fn view_modal_menu (&self) -> impl Content { + fn view_dialog_menu (&self) -> impl Content { let options = ||["Projects", "Settings", "Help", "Quit"].iter(); let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a)); Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option))) } - fn view_modal_help (&self) -> impl Content + use<'_> { + fn view_dialog_help (&self) -> impl Content + use<'_> { let bindings = ||self.config.keys.layers.iter() .filter_map(|a|(a.0)(self).then_some(a.1)) .flat_map(|a|a) @@ -111,7 +111,7 @@ impl Tek { Bsp::s(Tui::bold(true, "Help"), Bsp::s("", Map::south(1, bindings, binding))) } - fn view_modal_device (&self, index: usize) -> impl Content + use<'_> { + fn view_dialog_device (&self, index: usize) -> impl Content + use<'_> { let choices = ||self.device_kinds().iter(); let choice = move|label, i| Fill::x(Tui::bg(if i == index { Rgb(64,128,32) } else { Rgb(0,0,0) }, @@ -121,7 +121,7 @@ impl Tek { Bsp::s(Tui::bold(true, "Add device"), Map::south(1, choices, choice)) } - fn view_modal_message <'a> (&'a self, message: &'a Message) -> impl Content + use<'a> { + fn view_dialog_message <'a> (&'a self, message: &'a Message) -> impl Content + use<'a> { Bsp::s(message, Bsp::s("", "[ OK ]")) } diff --git a/crates/device/src/lv2/lv2_tui.rs b/crates/device/src/lv2/lv2_tui.rs index c7a805ce..0c76a1c9 100644 --- a/crates/device/src/lv2/lv2_tui.rs +++ b/crates/device/src/lv2/lv2_tui.rs @@ -6,8 +6,6 @@ impl Content for Lv2 { let area = to.area(); let [x, y, _, height] = area; let mut width = 20u16; - //match &self.plugin { - //Some(PluginKind::LV2(LV2Plugin { port_list, instance, .. })) => { let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1)); let end = start + height as usize - 2; //draw_box(buf, Rect { x, y, width, height }); @@ -31,9 +29,6 @@ impl Content for Lv2 { break } } - //} - //_ => {} - //}; draw_header(self, to, x, y, width); } }