From 5ffa9472e1fa7344258bc2125f29c6436c541e5b Mon Sep 17 00:00:00 2001 From: i do not exist Date: Mon, 13 Apr 2026 17:31:15 +0300 Subject: [PATCH 01/31] '197e' is as much as i can tell ya --- src/arrange.rs | 186 ++++++----- src/browse.rs | 4 +- src/editor.rs | 818 +++++++++++++++++++++++++++++++++++++++++++++++ src/sample.rs | 2 +- src/sequence.rs | 824 +----------------------------------------------- src/tek.rs | 87 ++--- tengri | 2 +- 7 files changed, 975 insertions(+), 948 deletions(-) create mode 100644 src/editor.rs diff --git a/src/arrange.rs b/src/arrange.rs index d93501e9..b8ac5062 100644 --- a/src/arrange.rs +++ b/src/arrange.rs @@ -1,8 +1,7 @@ use ::std::sync::{Arc, RwLock}; use ::tengri::{space::east, color::ItemTheme}; use ::tengri::{draw::*, term::*}; -use crate::{*, device::*, sequence::*, clock::*, select::*, sample::*}; -impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } +use crate::{*, device::*, editor::*, sequence::*, clock::*, select::*, sample::*}; /// Arranger. /// @@ -20,9 +19,9 @@ impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &sel /// TODO rename to "render_cache" or smth pub arranger: Arc>, /// Display size - pub size: [AtomicUsize; 2], + pub size: Size, /// Display size of clips area - pub size_inner: [AtomicUsize; 2], + pub size_inner: Size, /// Source of time #[cfg(feature = "clock")] pub clock: Clock, /// Allows one MIDI clip to be edited @@ -84,7 +83,11 @@ impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &sel } impl_has!(Jack<'static>: |self: Arrangement| self.jack); -impl_has!([AtomicUsize; 2]: |self: Arrangement| self.size); +impl HasJack<'static> for Arrangement { + fn jack (&self) -> &Jack<'static> { &self.jack } +} + +impl_has!(Size: |self: Arrangement| self.size); impl_has!(Vec: |self: Arrangement| self.tracks); impl_has!(Vec: |self: Arrangement| self.scenes); impl_has!(Vec: |self: Arrangement| self.midi_ins); @@ -110,67 +113,69 @@ pub trait ClipsView: TracksView + ScenesView { { self.clips_size().of(wh_full(above( wh_full(origin_se(fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))), - thunk(|to: &mut Tui|for ( - track_index, track, _, _ - ) in self.tracks_with_sizes() { - to.place(&w_exact(track.width as u16, - h_full(self.view_track_clips(track_index, track)))) + thunk(|to: &mut Tui|{ + for (track_index, track, _, _) in self.tracks_with_sizes() { + to.place(&w_exact(track.width as u16, + h_full(self.view_track_clips(track_index, track)))) + } + Ok(XYWH(0, 0, 0, 0)) })))) } fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Draw + 'a { - thunk(move|to: &mut Tui|for ( - scene_index, scene, .. - ) in self.scenes_with_sizes() { - let (name, theme): (Arc, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) { - let clip = clip.read().unwrap(); - (format!(" ⏹ {}", &clip.name).into(), clip.color) - } else { - (" ⏹ -- ".into(), ItemTheme::G[32]) - }; - let fg = theme.lightest.term; - let mut outline = theme.base.term; - let bg = if self.selection().track() == Some(track_index) - && self.selection().scene() == Some(scene_index) - { - outline = theme.lighter.term; - theme.light.term - } else if self.selection().track() == Some(track_index) - || self.selection().scene() == Some(scene_index) - { - outline = theme.darkest.term; - theme.base.term - } else { - theme.dark.term - }; - let w = if self.selection().track() == Some(track_index) - && let Some(editor) = self.editor () - { - (editor.measure_width() as usize).max(24).max(track.width) - } else { - track.width - } as u16; - let y = if self.selection().scene() == Some(scene_index) - && let Some(editor) = self.editor () - { - (editor.measure_height() as usize).max(12) - } else { - Self::H_SCENE as usize - } as u16; + thunk(move|to: &mut Tui|{ + for (scene_index, scene, ..) in self.scenes_with_sizes() { + let (name, theme): (Arc, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) { + let clip = clip.read().unwrap(); + (format!(" ⏹ {}", &clip.name).into(), clip.color) + } else { + (" ⏹ -- ".into(), ItemTheme::G[32]) + }; + let fg = theme.lightest.term; + let mut outline = theme.base.term; + let bg = if self.selection().track() == Some(track_index) + && self.selection().scene() == Some(scene_index) + { + outline = theme.lighter.term; + theme.light.term + } else if self.selection().track() == Some(track_index) + || self.selection().scene() == Some(scene_index) + { + outline = theme.darkest.term; + theme.base.term + } else { + theme.dark.term + }; + let w = if self.selection().track() == Some(track_index) + && let Some(editor) = self.editor () + { + (editor.size.w() as usize).max(24).max(track.width) + } else { + track.width + } as u16; + let y = if self.selection().scene() == Some(scene_index) + && let Some(editor) = self.editor () + { + (editor.size.h() as usize).max(12) + } else { + Self::H_SCENE as usize + } as u16; - let is_selected = - self.selection().track() == Some(track_index) && - self.selection().scene() == Some(scene_index) && - self.is_editing(); + let is_selected = + self.selection().track() == Some(track_index) && + self.selection().scene() == Some(scene_index) && + self.is_editing(); - to.place(&wh_exact(w, y, below( - wh_full(Outer(true, Style::default().fg(outline))), - wh_full(below( - below( - fg_bg(outline, bg, wh_full("")), - wh_full(origin_nw(fg_bg(fg, bg, bold(true, name)))), - ), - wh_full(when(is_selected, self.editor().view()))))))); + to.place(&wh_exact(Some(w), Some(y), below( + wh_full(Outer(true, Style::default().fg(outline))), + wh_full(below( + below( + fg_bg(outline, bg, wh_full("")), + wh_full(origin_nw(fg_bg(fg, bg, bold(true, &name)))), + ), + wh_full(when(is_selected, self.editor().map(|e|e.view())))))))); + } + Ok(XYWH(0, 0, 0, 0)) }) } @@ -179,12 +184,12 @@ pub trait ClipsView: TracksView + ScenesView { pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { fn tracks_width_available (&self) -> u16 { - (self.measure_width() as u16).saturating_sub(40) + (self.size.width() as u16).saturating_sub(40) } /// Iterate over tracks with their corresponding sizes. fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { - let _editor_width = self.editor().map(|e|e.measure_width()); + let _editor_width = self.editor().map(|e|e.size.w()); let _active_track = self.selection().track(); let mut x = 0; self.tracks().iter().enumerate().map_while(move |(index, track)|{ @@ -222,23 +227,31 @@ pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { }, south(w_full(origin_nw(east( format!("·t{index:02} "), fg(Rgb(255, 255, 255), bold(true, &track.name)) - ))), ""))) ));}})))) + ))), ""))) ));} + Ok(XYWH(0, 0, 0, 0)) + })))) } fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw { view_track_row_section(theme, south(w_full(origin_w(button_2("o", "utput", false))), - thunk(|to: &mut Tui|for port in self.midi_outs().iter() { - to.place(&w_full(origin_w(port.port_name()))); + thunk(|to: &mut Tui|{ + for port in self.midi_outs().iter() { + to.place(&w_full(origin_w(port.port_name()))); + } + Ok(XYWH(0, 0, 0, 0)) })), button_2("O", "+", false), bg(theme.darker.term, origin_w(thunk(|to: &mut Tui|{ for (index, track, _x1, _x2) in self.tracks_with_sizes() { to.place(&w_exact(track_width(index, track), - origin_nw(h_full(iter_south(1, ||track.sequencer.midi_outs.iter(), + origin_nw(h_full(iter_south(||track.sequencer.midi_outs.iter(), |port, index|fg(Rgb(255, 255, 255), h_exact(1, bg(track.color.dark.term, w_full(origin_w( - format!("·o{index:02} {}", port.port_name())))))))))));}})))) + format!("·o{index:02} {}", port.port_name()))))))))))); + } + Ok(XYWH(0, 0, 0, 0)) + })))) } fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw { @@ -246,18 +259,21 @@ pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { for track in self.tracks().iter() { h = h.max(track.sequencer.midi_ins.len() as u16); } - let content = thunk(move|to: &mut Tui|for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&wh_exact(track_width(index, track), h + 1, - origin_nw(south( - bg(track.color.base.term, - w_full(origin_w(east!( - either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "), - either(track.sequencer.recording, fg(Red, "●rec "), "·rec "), - either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "), - )))), - iter_south(1, ||track.sequencer.midi_ins.iter(), - |port, index|fg_bg(Rgb(255, 255, 255), track.color.dark.term, - w_full(origin_w(format!("·i{index:02} {}", port.port_name()))))))))); + let content = thunk(move|to: &mut Tui|{ + for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&wh_exact(track_width(index, track), h + 1, + origin_nw(south( + bg(track.color.base.term, + w_full(origin_w(east!( + either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "), + either(track.sequencer.recording, fg(Red, "●rec "), "·rec "), + either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "), + )))), + iter_south(1, ||track.sequencer.midi_ins.iter(), + |port, index|fg_bg(Rgb(255, 255, 255), track.color.dark.term, + w_full(origin_w(format!("·i{index:02} {}", port.port_name()))))))))); + } + Ok(XYWH(0, 0, 0, 0)) }); view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), bg(theme.darker.term, origin_w(content))) @@ -522,9 +538,9 @@ pub trait HasTrack: AsRefOpt + AsMutOpt { impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } impl HasSceneScroll for Arrangement { fn scene_scroll (&self) -> usize { self.scene_scroll } } impl ScenesView for App { - fn w_mid (&self) -> u16 { (self.measure_width() as u16).saturating_sub(self.w_side()) } + fn w_mid (&self) -> u16 { (self.size.w() as u16).saturating_sub(self.w_side()) } fn w_side (&self) -> u16 { 20 } - fn h_scenes (&self) -> u16 { (self.measure_height() as u16).saturating_sub(20) } + fn h_scenes (&self) -> u16 { (self.size.h() as u16).saturating_sub(20) } } impl Scene { /// Returns the pulse length of the longest clip in the scene @@ -780,17 +796,17 @@ impl Arrangement { } impl ScenesView for Arrangement { fn h_scenes (&self) -> u16 { - (self.measure_height() as u16).saturating_sub(20) + (self.size.h() as u16).saturating_sub(20) } fn w_side (&self) -> u16 { - (self.measure_width() as u16 * 2 / 10).max(20) + (self.size.w() as u16 * 2 / 10).max(20) } fn w_mid (&self) -> u16 { - (self.measure_width() as u16).saturating_sub(2 * self.w_side()).max(40) + (self.size.width() as u16).saturating_sub(2 * self.w_side()).max(40) } } impl HasClipsSize for Arrangement { - fn clips_size (&self) -> &[AtomicUsize; 2] { &self.size_inner } + fn clips_size (&self) -> &Size { &self.size_inner } } pub type SceneWith<'a, T> = diff --git a/src/browse.rs b/src/browse.rs index 83d02a2e..71ae746c 100644 --- a/src/browse.rs +++ b/src/browse.rs @@ -21,7 +21,7 @@ def_command!(FileBrowserCommand: |sampler: Sampler|{ pub filter: String, pub index: usize, pub scroll: usize, - pub size: [AtomicUsize; 2], + pub size: Size, } pub(crate) struct EntriesIterator<'a> { @@ -330,7 +330,7 @@ impl Browse { } } impl<'a> Iterator for EntriesIterator<'a> { - type Item = Modify<&'a str>; + type Item = (); fn next (&mut self) -> Option { let dirs = self.browser.dirs.len(); let files = self.browser.files.len(); diff --git a/src/editor.rs b/src/editor.rs new file mode 100644 index 00000000..82187504 --- /dev/null +++ b/src/editor.rs @@ -0,0 +1,818 @@ +use crate::*; + +/// Contains state for viewing and editing a clip. +/// +/// ``` +/// use std::sync::{Arc, RwLock}; +/// let clip = tek::MidiClip::stop_all(); +/// let mut editor = tek::MidiEditor { +/// mode: tek::PianoHorizontal::new(Some(&Arc::new(RwLock::new(clip)))), +/// size: Default::default(), +/// //keys: Default::default(), +/// }; +/// let _ = editor.put_note(true); +/// let _ = editor.put_note(false); +/// let _ = editor.clip_status(); +/// let _ = editor.edit_status(); +/// ``` +pub struct MidiEditor { + /// Size of editor on screen + pub size: Size, + /// View mode and state of editor + pub mode: PianoHorizontal, +} + +/// A clip, rendered as a horizontal piano roll. +/// +/// ``` +/// let piano = tek::PianoHorizontal::default(); +/// ``` +#[derive(Clone, Default)] +pub struct PianoHorizontal { + pub clip: Option>>, + /// Buffer where the whole clip is rerendered on change + pub buffer: Arc>, + /// Size of actual notes area + pub size: Size, + /// The display window + pub range: MidiSelection, + /// The note cursor + pub point: MidiCursor, + /// The highlight color palette + pub color: ItemTheme, + /// Width of the keyboard + pub keys_width: u16, +} + +/// 12 piano keys, some highlighted. +/// +/// ``` +/// let keys = tek::OctaveVertical::default(); +/// ``` +#[derive(Copy, Clone)] +pub struct OctaveVertical { + pub on: [bool; 12], + pub colors: [Color; 3] +} + +/// +/// ``` +/// let _ = tek::MidiCursor::default(); +/// ``` +#[derive(Debug, Clone)] pub struct MidiCursor { + /// Time coordinate of cursor + pub time_pos: Arc, + /// Note coordinate of cursor + pub note_pos: Arc, + /// Length of note that will be inserted, in pulses + pub note_len: Arc, +} + +/// +/// ``` +/// use tek::{TimeRange, NoteRange}; +/// let model = tek::MidiSelection::from((1, false)); +/// +/// let _ = model.get_time_len(); +/// let _ = model.get_time_zoom(); +/// let _ = model.get_time_lock(); +/// let _ = model.get_time_start(); +/// let _ = model.get_time_axis(); +/// let _ = model.get_time_end(); +/// +/// let _ = model.get_note_lo(); +/// let _ = model.get_note_axis(); +/// let _ = model.get_note_hi(); +/// ``` +#[derive(Debug, Clone, Default)] pub struct MidiSelection { + pub time_len: Arc, + /// Length of visible time axis + pub time_axis: Arc, + /// Earliest time displayed + pub time_start: Arc, + /// Time step + pub time_zoom: Arc, + /// Auto rezoom to fit in time axis + pub time_lock: Arc, + /// Length of visible note axis + pub note_axis: Arc, + // Lowest note displayed + pub note_lo: Arc, +} + +/// ``` +/// use tek::{*, tengri::*}; +/// +/// struct Test(Option); +/// impl_as_ref_opt!(MidiEditor: |self: Test|self.0.as_ref()); +/// impl_as_mut_opt!(MidiEditor: |self: Test|self.0.as_mut()); +/// +/// let mut host = Test(Some(MidiEditor::default())); +/// let _ = host.editor(); +/// let _ = host.editor_mut(); +/// let _ = host.is_editing(); +/// let _ = host.editor_w(); +/// let _ = host.editor_h(); +/// ``` +pub trait HasEditor: AsRefOpt + AsMutOpt { + fn editor (&self) -> Option<&MidiEditor> { self.as_ref_opt() } + fn editor_mut (&mut self) -> Option<&mut MidiEditor> { self.as_mut_opt() } + fn is_editing (&self) -> bool { self.editor().is_some() } + fn editor_w (&self) -> usize { self.editor().map(|e|e.size.w()).unwrap_or(0) as usize } + fn editor_h (&self) -> usize { self.editor().map(|e|e.size.h()).unwrap_or(0) as usize } +} + +impl MidiPoint for T {} + +impl MidiRange for T {} + +impl +AsMutOpt> HasEditor for T {} + +def_command!(MidiEditCommand: |editor: MidiEditor| { + Show { clip: Option>> } => { + editor.set_clip(clip.as_ref()); editor.redraw(); Ok(None) }, + DeleteNote => { + editor.redraw(); todo!() }, + AppendNote { advance: bool } => { + editor.put_note(*advance); editor.redraw(); Ok(None) }, + SetNotePos { pos: usize } => { + editor.set_note_pos((*pos).min(127)); editor.redraw(); Ok(None) }, + SetNoteLen { len: usize } => { + editor.set_note_len(*len); editor.redraw(); Ok(None) }, + SetNoteScroll { scroll: usize } => { + editor.set_note_lo((*scroll).min(127)); editor.redraw(); Ok(None) }, + SetTimePos { pos: usize } => { + editor.set_time_pos(*pos); editor.redraw(); Ok(None) }, + SetTimeScroll { scroll: usize } => { + editor.set_time_start(*scroll); editor.redraw(); Ok(None) }, + SetTimeZoom { zoom: usize } => { + editor.set_time_zoom(*zoom); editor.redraw(); Ok(None) }, + SetTimeLock { lock: bool } => { + editor.set_time_lock(*lock); editor.redraw(); Ok(None) }, + // TODO: 1-9 seek markers that by default start every 8th of the clip +}); + +pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync { + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize); + fn redraw (&self); + fn clip (&self) -> &Option>>; + fn clip_mut (&mut self) -> &mut Option>>; + fn set_clip (&mut self, clip: Option<&Arc>>) { + *self.clip_mut() = clip.cloned(); + self.redraw(); + } + /// Make sure cursor is within note range + fn autoscroll (&self) { + let note_pos = self.get_note_pos().min(127); + let note_lo = self.get_note_lo(); + let note_hi = self.get_note_hi(); + if note_pos < note_lo { + self.note_lo().set(note_pos); + } else if note_pos > note_hi { + self.note_lo().set((note_lo + note_pos).saturating_sub(note_hi)); + } + } + /// Make sure time range is within display + fn autozoom (&self) { + if self.time_lock().get() { + let time_len = self.get_time_len(); + let time_axis = self.get_time_axis(); + let time_zoom = self.get_time_zoom(); + loop { + let time_zoom = self.time_zoom().get(); + let time_area = time_axis * time_zoom; + if time_area > time_len { + let next_time_zoom = note_duration_prev(time_zoom); + if next_time_zoom <= 1 { + break + } + let next_time_area = time_axis * next_time_zoom; + if next_time_area >= time_len { + self.time_zoom().set(next_time_zoom); + } else { + break + } + } else if time_area < time_len { + let prev_time_zoom = note_duration_next(time_zoom); + if prev_time_zoom > 384 { + break + } + let prev_time_area = time_axis * prev_time_zoom; + if prev_time_area <= time_len { + self.time_zoom().set(prev_time_zoom); + } else { + break + } + } + } + if time_zoom != self.time_zoom().get() { + self.redraw() + } + } + //while time_len.div_ceil(time_zoom) > time_axis { + //println!("\r{time_len} {time_zoom} {time_axis}"); + //time_zoom = Note::next(time_zoom); + //} + //self.time_zoom().set(time_zoom); + } +} + +pub trait NotePoint { + fn note_len (&self) -> &AtomicUsize; + /// Get the current length of the note cursor. + fn get_note_len (&self) -> usize { + self.note_len().load(Relaxed) + } + /// Set the length of the note cursor, returning the previous value. + fn set_note_len (&self, x: usize) -> usize { + self.note_len().swap(x, Relaxed) + } + + fn note_pos (&self) -> &AtomicUsize; + /// Get the current pitch of the note cursor. + fn get_note_pos (&self) -> usize { + self.note_pos().load(Relaxed).min(127) + } + /// Set the current pitch fo the note cursor, returning the previous value. + fn set_note_pos (&self, x: usize) -> usize { + self.note_pos().swap(x.min(127), Relaxed) + } +} + +pub trait TimePoint { + fn time_pos (&self) -> &AtomicUsize; + /// Get the current time position of the note cursor. + fn get_time_pos (&self) -> usize { + self.time_pos().load(Relaxed) + } + /// Set the current time position of the note cursor, returning the previous value. + fn set_time_pos (&self, x: usize) -> usize { + self.time_pos().swap(x, Relaxed) + } +} + +pub trait MidiPoint: NotePoint + TimePoint { + /// Get the current end of the note cursor. + fn get_note_end (&self) -> usize { + self.get_time_pos() + self.get_note_len() + } +} + +pub trait TimeRange { + fn time_len (&self) -> &AtomicUsize; + fn get_time_len (&self) -> usize { + self.time_len().load(Relaxed) + } + fn time_zoom (&self) -> &AtomicUsize; + fn get_time_zoom (&self) -> usize { + self.time_zoom().load(Relaxed) + } + fn set_time_zoom (&self, value: usize) -> usize { + self.time_zoom().swap(value, Relaxed) + } + fn time_lock (&self) -> &AtomicBool; + fn get_time_lock (&self) -> bool { + self.time_lock().load(Relaxed) + } + fn set_time_lock (&self, value: bool) -> bool { + self.time_lock().swap(value, Relaxed) + } + fn time_start (&self) -> &AtomicUsize; + fn get_time_start (&self) -> usize { + self.time_start().load(Relaxed) + } + fn set_time_start (&self, value: usize) -> usize { + self.time_start().swap(value, Relaxed) + } + fn time_axis (&self) -> &AtomicUsize; + fn get_time_axis (&self) -> usize { + self.time_axis().load(Relaxed) + } + fn get_time_end (&self) -> usize { + self.time_start().get() + self.time_axis().get() * self.time_zoom().get() + } +} + +pub trait NoteRange { + fn note_lo (&self) -> &AtomicUsize; + fn note_axis (&self) -> &AtomicUsize; + + fn get_note_lo (&self) -> usize { + self.note_lo().load(Relaxed) + } + fn set_note_lo (&self, x: usize) -> usize { + self.note_lo().swap(x, Relaxed) + } + fn get_note_axis (&self) -> usize { + self.note_axis().load(Relaxed) + } + fn get_note_hi (&self) -> usize { + (self.get_note_lo() + self.get_note_axis().saturating_sub(1)).min(127) + } +} + +pub trait MidiRange: TimeRange + NoteRange {} + +impl_has!(Size: |self: MidiEditor| self.size); +impl_has!(Size: |self: PianoHorizontal| self.size); + +impl Draw for MidiEditor { + fn draw(self, to: &mut Tui) -> Usually> { + self.tui().draw(to) + } +} +impl Draw for PianoHorizontal { + fn draw(self, to: &mut Tui) -> Usually> { + self.tui().draw(to) + } +} +impl std::fmt::Debug for MidiEditor { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("MidiEditor").field("mode", &self.mode).finish() + } +} +impl_from!(MidiEditor: |clip: &Arc>| { + let model = Self::from(Some(clip.clone())); + model.redraw(); + model +}); +impl_from!(MidiEditor: |clip: Option>>| { + let mut model = Self::default(); + *model.clip_mut() = clip; + model.redraw(); + model +}); +impl_default!(MidiEditor: Self { + size: [0, 0].into(), mode: PianoHorizontal::new(None) +}); +impl_default!(OctaveVertical: Self { + on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] +}); +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.get_time_pos(); + let note_pos = self.get_note_pos(); + let note_len = self.get_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 + 1) % clip.length); + } + redraw = true; + } + if redraw { + self.mode.redraw(); + } + } + fn _todo_opt_clip_stub (&self) -> Option>> { todo!() } + fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } + fn note_length (&self) -> usize { self.get_note_len() } + fn note_pos (&self) -> usize { self.get_note_pos() } + fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 } + fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 } + fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) } + fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) } + fn note_len (&self) -> usize { self.get_note_len() } + fn note_len_next (&self) -> usize { self.get_note_len() + 1 } + fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) } + fn note_range (&self) -> usize { self.get_note_axis() } + fn note_range_next (&self) -> usize { self.get_note_axis() + 1 } + fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) } + fn time_zoom (&self) -> usize { self.get_time_zoom() } + fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 } + fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 } + fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } + fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } + fn time_lock (&self) -> bool { self.get_time_lock() } + fn time_lock_toggled (&self) -> bool { !self.get_time_lock() } + fn time_pos (&self) -> usize { self.get_time_pos() } + fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() } + fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() } + fn time_pos_prev (&self) -> usize { + let step = self.get_note_len(); + self.get_time_pos().overflowing_sub(step) + .0.min(self.clip_length().saturating_sub(step)) + } + fn time_pos_prev_fine (&self) -> usize { + self.get_time_pos().overflowing_sub(1) + .0.min(self.clip_length().saturating_sub(1)) + } + pub fn clip_status (&self) -> impl Draw + '_ { + 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) }; + w_exact(20, south!( + w_full(origin_w(east( + button_2("f2", "name ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), + w_full(origin_w(east( + button_2("l", "ength ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), + w_full(origin_w(east( + button_2("r", "epeat ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), + )) + } + pub fn edit_status (&self) -> impl Draw + '_ { + 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.get_time_pos(); + let time_zoom = self.get_time_zoom(); + let time_lock = if self.get_time_lock() { "[lock]" } else { " " }; + let note_pos = self.get_note_pos(); + let note_name = format!("{:4}", note_pitch_to_name(note_pos)); + let note_pos = format!("{:>3}", note_pos); + let note_len = format!("{:>4}", self.get_note_len()); + w_exact(20, south!( + w_full(origin_w(east( + button_2("t", "ime ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), + format!("{length} /{time_zoom} +{time_pos} "))))))), + w_full(origin_w(east( + button_2("z", "lock ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), + format!("{time_lock}"))))))), + w_full(origin_w(east( + button_2("x", "note ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), + 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) -> &AtomicUsize { self.mode.note_len() } + fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() } +} + +impl TimePoint for MidiEditor { + fn time_pos (&self) -> &AtomicUsize { self.mode.time_pos() } +} + +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) } +} + +impl View for MidiEditor { + fn view (&self) -> impl Draw { + self.autoscroll(); + /*self.autozoom();*/ + self.size.of(&self.mode) + } +} + + +impl PianoHorizontal { + pub fn new (clip: Option<&Arc>>) -> Self { + let size = [0, 0].into(); + let mut range = MidiSelection::from((12, true)); + range.time_axis = size.x.clone(); + range.note_axis = size.y.clone(); + let piano = Self { + keys_width: 5, + size, + range, + buffer: RwLock::new(Default::default()).into(), + point: MidiCursor::default(), + clip: clip.cloned(), + color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), + }; + piano.redraw(); + piano + } +} + +impl PianoHorizontal { + fn tui (&self) -> impl Draw { + south( + east(w_exact(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), + east(self.keys(), self.size.of(below(wh_full(self.notes()), wh_full(self.cursor())))), + ) + } +} + +impl PianoHorizontal { + /// Draw the piano roll background. + /// + /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize, note_point: usize, time_point: usize) { + for (y, note) in (0..=127).rev().enumerate() { + for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { + let cell = buf.get_mut(x, y).unwrap(); + if note == (127-note_point) || time == time_point { + cell.set_bg(Rgb(0,0,0)); + } else { + cell.set_bg(clip.color.darkest.term); + } + if time % 384 == 0 { + cell.set_fg(clip.color.darker.term); + cell.set_char('│'); + } else if time % 96 == 0 { + cell.set_fg(clip.color.dark.term); + cell.set_char('╎'); + } else if time % note_len == 0 { + cell.set_fg(clip.color.darker.term); + cell.set_char('┊'); + } else if (127 - note) % 12 == 0 { + cell.set_fg(clip.color.darker.term); + cell.set_char('='); + } else if (127 - note) % 6 == 0 { + cell.set_fg(clip.color.darker.term); + cell.set_char('—'); + } else { + cell.set_fg(clip.color.darker.term); + cell.set_char('·'); + } + } + } + } + /// Draw the piano roll foreground. + /// + /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { + let style = Style::default().fg(clip.color.base.term);//.bg(Rgb(0, 0, 0)); + let mut notes_on = [false;128]; + for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { + for (_y, note) in (0..=127).rev().enumerate() { + if let Some(cell) = buf.get_mut(x, note) { + if notes_on[note] { + cell.set_char('▂'); + cell.set_style(style); + } + } + } + let time_end = time_start + zoom; + for time in time_start..time_end.min(clip.length) { + for event in clip.notes[time].iter() { + match event { + MidiMessage::NoteOn { key, .. } => { + let note = key.as_int() as usize; + if let Some(cell) = buf.get_mut(x, note) { + cell.set_char('█'); + cell.set_style(style); + } + notes_on[note] = true + }, + MidiMessage::NoteOff { key, .. } => { + notes_on[key.as_int() as usize] = false + }, + _ => {} + } + } + } + + } + } + fn notes (&self) -> impl Draw { + let time_start = self.get_time_start(); + let note_lo = self.get_note_lo(); + let note_hi = self.get_note_hi(); + let buffer = self.buffer.clone(); + Thunk::new(move|to: &mut Tui|{ + let source = buffer.read().unwrap(); + let XYWH(x0, y0, w, _h) = to.area(); + //if h as usize != note_axis { + //panic!("area height mismatch: {h} <> {note_axis}"); + //} + for (area_x, screen_x) in (x0..x0+w).enumerate() { + for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { + let source_x = time_start + area_x; + let source_y = note_hi - area_y; + // TODO: enable loop rollover: + //let source_x = (time_start + area_x) % source.width.max(1); + //let source_y = (note_hi - area_y) % source.height.max(1); + let is_in_x = source_x < source.width; + let is_in_y = source_y < source.height; + if is_in_x && is_in_y { + if let Some(source_cell) = source.get(source_x, source_y) { + if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { + *cell = source_cell.clone(); + } + } + } + } + } + }) + } + fn cursor (&self) -> impl Draw { + let note_hi = self.get_note_hi(); + let note_lo = self.get_note_lo(); + let note_pos = self.get_note_pos(); + let note_len = self.get_note_len(); + let time_pos = self.get_time_pos(); + let time_start = self.get_time_start(); + let time_zoom = self.get_time_zoom(); + let style = Some(Style::default().fg(self.color.lightest.term)); + Thunk::new(move|to: &mut Tui|{ + let XYWH(x0, y0, w, _) = to.area(); + for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { + if note == note_pos { + for x in 0..w { + let screen_x = x0 + x; + let time_1 = time_start + x as usize * time_zoom; + let time_2 = time_1 + time_zoom; + if time_1 <= time_pos && time_pos < time_2 { + to.blit(&"█", screen_x, screen_y, style); + let tail = note_len as u16 / time_zoom as u16; + for x_tail in (screen_x + 1)..(screen_x + tail) { + to.blit(&"▂", x_tail, screen_y, style); + } + break + } + } + break + } + } + }) + } + fn keys (&self) -> impl Draw { + let state = self; + let color = state.color; + let note_lo = state.get_note_lo(); + let note_hi = state.get_note_hi(); + let note_pos = state.get_note_pos(); + let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); + let off_style = Some(Style::default().fg(Tui::g(255))); + let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.term).bold()); + h_full(w_exact(self.keys_width, Thunk::new(move|to: &mut Tui|{ + let XYWH(x, y0, _w, _h) = to.area(); + for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { + to.blit(&to_key(note), x, screen_y, key_style); + if note > 127 { + continue + } + if note == note_pos { + to.blit(&format!("{:<5}", note_pitch_to_name(note)), x, screen_y, on_style) + } else { + to.blit(¬e_pitch_to_name(note), x, screen_y, off_style) + }; + } + }))) + } + fn timeline (&self) -> impl Draw + '_ { + w_full(h_exact(1, Thunk::new(move|to: &mut Tui|{ + let XYWH(x, y, w, _h) = to.area(); + let style = Some(Style::default().dim()); + let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); + for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { + let t = area_x as usize * self.time_zoom().get(); + if t < length { + to.blit(&"|", screen_x, y, style); + } + } + }))) + } +} + +impl TimeRange for PianoHorizontal { + fn time_len (&self) -> &AtomicUsize { self.range.time_len() } + fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } + fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } + fn time_start (&self) -> &AtomicUsize { self.range.time_start() } + fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } +} + +impl NoteRange for PianoHorizontal { + fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } + fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } +} + +impl NotePoint for PianoHorizontal { + fn note_len (&self) -> &AtomicUsize { self.point.note_len() } + fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } +} + +impl TimePoint for PianoHorizontal { + fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } +} + +impl MidiViewer for PianoHorizontal { + fn clip (&self) -> &Option>> { &self.clip } + fn clip_mut (&mut self) -> &mut Option>> { &mut self.clip } + /// Determine the required space to render the clip. + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { + (clip.length / self.range.time_zoom().get(), 128) + } + fn redraw (&self) { + *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { + let clip = clip.read().unwrap(); + let buf_size = self.buffer_size(&clip); + let mut buffer = BigBuffer::from(buf_size); + let time_zoom = self.get_time_zoom(); + self.time_len().set(clip.length); + PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); + PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); + buffer + } else { + Default::default() + } + } + fn set_clip (&mut self, clip: Option<&Arc>>) { + *self.clip_mut() = clip.cloned(); + self.color = clip.map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]); + self.redraw(); + } +} + +impl std::fmt::Debug for PianoHorizontal { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + let buffer = self.buffer.read().unwrap(); + f.debug_struct("PianoHorizontal") + .field("time_zoom", &self.range.time_zoom) + .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) + .finish() + } +} +impl OctaveVertical { + fn color (&self, pitch: usize) -> Color { + let pitch = pitch % 12; + self.colors[if self.on[pitch] { 2 } else { match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } }] + } +} +impl OctaveVertical { + fn tui (&self) -> impl Draw { + east!( + Tui::fg_bg(self.color(0), self.color(1), "▙"), + Tui::fg_bg(self.color(2), self.color(3), "▙"), + Tui::fg_bg(self.color(4), self.color(5), "▌"), + Tui::fg_bg(self.color(6), self.color(7), "▟"), + Tui::fg_bg(self.color(8), self.color(9), "▟"), + Tui::fg_bg(self.color(10), self.color(11), "▟"), + ) + } +} +impl_from!(MidiSelection: |data:(usize, bool)| Self { + time_len: Arc::new(0.into()), + note_axis: Arc::new(0.into()), + note_lo: Arc::new(0.into()), + time_axis: Arc::new(0.into()), + time_start: Arc::new(0.into()), + time_zoom: Arc::new(data.0.into()), + time_lock: Arc::new(data.1.into()), +}); +impl_default!(MidiCursor: Self { + time_pos: Arc::new(0.into()), + note_pos: Arc::new(36.into()), + note_len: Arc::new(24.into()), +}); + +impl NotePoint for MidiCursor { + fn note_len (&self) -> &AtomicUsize { + &self.note_len + } + fn note_pos (&self) -> &AtomicUsize { + &self.note_pos + } +} + +impl TimePoint for MidiCursor { + fn time_pos (&self) -> &AtomicUsize { + self.time_pos.as_ref() + } +} + +impl TimeRange for MidiSelection { + fn time_len (&self) -> &AtomicUsize { &self.time_len } + fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom } + fn time_lock (&self) -> &AtomicBool { &self.time_lock } + fn time_start (&self) -> &AtomicUsize { &self.time_start } + fn time_axis (&self) -> &AtomicUsize { &self.time_axis } +} + +impl NoteRange for MidiSelection { + fn note_lo (&self) -> &AtomicUsize { &self.note_lo } + fn note_axis (&self) -> &AtomicUsize { &self.note_axis } +} diff --git a/src/sample.rs b/src/sample.rs index 01dfbbe0..09b238ff 100644 --- a/src/sample.rs +++ b/src/sample.rs @@ -78,7 +78,7 @@ def_command!(SamplerCommand: |sampler: Sampler| { /// Currently active modal, if any. pub mode: Option, /// Size of rendered sampler. - pub size: [AtomicUsize;2], + pub size: Size, /// Lowest note displayed. pub note_lo: AtomicUsize, /// Currently selected note. diff --git a/src/sequence.rs b/src/sequence.rs index 11d852d3..fe15f60f 100644 --- a/src/sequence.rs +++ b/src/sequence.rs @@ -1,85 +1,7 @@ use crate::{*, clock::*, device::*}; -impl +AsMut> HasSequencer for T {} -impl +AsMutOpt> HasEditor for T {} -impl MidiPoint for T {} -impl MidiRange for T {} -def_command!(MidiEditCommand: |editor: MidiEditor| { - Show { clip: Option>> } => { - editor.set_clip(clip.as_ref()); editor.redraw(); Ok(None) }, - DeleteNote => { - editor.redraw(); todo!() }, - AppendNote { advance: bool } => { - editor.put_note(*advance); editor.redraw(); Ok(None) }, - SetNotePos { pos: usize } => { - editor.set_note_pos((*pos).min(127)); editor.redraw(); Ok(None) }, - SetNoteLen { len: usize } => { - editor.set_note_len(*len); editor.redraw(); Ok(None) }, - SetNoteScroll { scroll: usize } => { - editor.set_note_lo((*scroll).min(127)); editor.redraw(); Ok(None) }, - SetTimePos { pos: usize } => { - editor.set_time_pos(*pos); editor.redraw(); Ok(None) }, - SetTimeScroll { scroll: usize } => { - editor.set_time_start(*scroll); editor.redraw(); Ok(None) }, - SetTimeZoom { zoom: usize } => { - editor.set_time_zoom(*zoom); editor.redraw(); Ok(None) }, - SetTimeLock { lock: bool } => { - editor.set_time_lock(*lock); editor.redraw(); Ok(None) }, - // TODO: 1-9 seek markers that by default start every 8th of the clip -}); +impl +AsMut> HasSequencer for T {} -/// Contains state for viewing and editing a clip. -/// -/// ``` -/// use std::sync::{Arc, RwLock}; -/// let clip = tek::MidiClip::stop_all(); -/// let mut editor = tek::MidiEditor { -/// mode: tek::PianoHorizontal::new(Some(&Arc::new(RwLock::new(clip)))), -/// size: Default::default(), -/// //keys: Default::default(), -/// }; -/// let _ = editor.put_note(true); -/// let _ = editor.put_note(false); -/// let _ = editor.clip_status(); -/// let _ = editor.edit_status(); -/// ``` -pub struct MidiEditor { - /// Size of editor on screen - pub size: [AtomicUsize; 2], - /// View mode and state of editor - pub mode: PianoHorizontal, -} - -/// A clip, rendered as a horizontal piano roll. -/// -/// ``` -/// let piano = tek::PianoHorizontal::default(); -/// ``` -#[derive(Clone, Default)] pub struct PianoHorizontal { - pub clip: Option>>, - /// Buffer where the whole clip is rerendered on change - pub buffer: Arc>, - /// Size of actual notes area - pub size: [AtomicUsize; 2], - /// The display window - pub range: MidiSelection, - /// The note cursor - pub point: MidiCursor, - /// The highlight color palette - pub color: ItemTheme, - /// Width of the keyboard - pub keys_width: u16, -} - -/// 12 piano keys, some highlighted. -/// -/// ``` -/// let keys = tek::OctaveVertical::default(); -/// ``` -#[derive(Copy, Clone)] pub struct OctaveVertical { - pub on: [bool; 12], - pub colors: [Color; 3] -} /// A MIDI sequence. /// /// ``` @@ -160,17 +82,11 @@ pub struct Sequencer { pub trait HasPlayClip: HasClock { - fn reset (&self) -> bool; - fn reset_mut (&mut self) -> &mut bool; - fn play_clip (&self) -> &Option<(Moment, Option>>)>; - fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; - fn next_clip (&self) -> &Option<(Moment, Option>>)>; - fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; fn pulses_since_start (&self) -> Option { @@ -307,76 +223,9 @@ pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip { } } -pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync { - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize); - fn redraw (&self); - fn clip (&self) -> &Option>>; - fn clip_mut (&mut self) -> &mut Option>>; - fn set_clip (&mut self, clip: Option<&Arc>>) { - *self.clip_mut() = clip.cloned(); - self.redraw(); - } - /// Make sure cursor is within note range - fn autoscroll (&self) { - let note_pos = self.get_note_pos().min(127); - let note_lo = self.get_note_lo(); - let note_hi = self.get_note_hi(); - if note_pos < note_lo { - self.note_lo().set(note_pos); - } else if note_pos > note_hi { - self.note_lo().set((note_lo + note_pos).saturating_sub(note_hi)); - } - } - /// Make sure time range is within display - fn autozoom (&self) { - if self.time_lock().get() { - let time_len = self.get_time_len(); - let time_axis = self.get_time_axis(); - let time_zoom = self.get_time_zoom(); - loop { - let time_zoom = self.time_zoom().get(); - let time_area = time_axis * time_zoom; - if time_area > time_len { - let next_time_zoom = note_duration_prev(time_zoom); - if next_time_zoom <= 1 { - break - } - let next_time_area = time_axis * next_time_zoom; - if next_time_area >= time_len { - self.time_zoom().set(next_time_zoom); - } else { - break - } - } else if time_area < time_len { - let prev_time_zoom = note_duration_next(time_zoom); - if prev_time_zoom > 384 { - break - } - let prev_time_area = time_axis * prev_time_zoom; - if prev_time_area <= time_len { - self.time_zoom().set(prev_time_zoom); - } else { - break - } - } - } - if time_zoom != self.time_zoom().get() { - self.redraw() - } - } - //while time_len.div_ceil(time_zoom) > time_axis { - //println!("\r{time_len} {time_zoom} {time_axis}"); - //time_zoom = Note::next(time_zoom); - //} - //self.time_zoom().set(time_zoom); - } -} -pub type MidiData = - Vec>; -pub type ClipPool = - Vec>>; -pub type CollectedMidiInput<'a> = - Vec, MidiError>)>>; +pub type MidiData = Vec>; +pub type ClipPool = Vec>>; +pub type CollectedMidiInput<'a> = Vec, MidiError>)>>; pub trait HasClips { fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>; @@ -387,180 +236,21 @@ pub trait HasClips { (self.clips().len() - 1, clip) } } -/// ``` -/// use tek::{*, tengri::*}; -/// -/// struct Test(Option); -/// impl_as_ref_opt!(MidiEditor: |self: Test|self.0.as_ref()); -/// impl_as_mut_opt!(MidiEditor: |self: Test|self.0.as_mut()); -/// -/// let mut host = Test(Some(MidiEditor::default())); -/// let _ = host.editor(); -/// let _ = host.editor_mut(); -/// let _ = host.is_editing(); -/// let _ = host.editor_w(); -/// let _ = host.editor_h(); -/// ``` -pub trait HasEditor: AsRefOpt + AsMutOpt { - fn editor (&self) -> Option<&MidiEditor> { self.as_ref_opt() } - fn editor_mut (&mut self) -> Option<&mut MidiEditor> { self.as_mut_opt() } - fn is_editing (&self) -> bool { self.editor().is_some() } - fn editor_w (&self) -> usize { self.editor().map(|e|e.size.w()).unwrap_or(0) as usize } - fn editor_h (&self) -> usize { self.editor().map(|e|e.size.h()).unwrap_or(0) as usize } -} + pub trait HasMidiClip { fn clip (&self) -> Option>>; } + pub trait HasSequencer: AsRef + AsMut { fn sequencer_mut (&mut self) -> &mut Sequencer { self.as_mut() } fn sequencer (&self) -> &Sequencer { self.as_ref() } } + pub trait HasMidiBuffers { fn note_buf_mut (&mut self) -> &mut Vec; fn midi_buf_mut (&mut self) -> &mut Vec>>; } - -pub trait NotePoint { - fn note_len (&self) -> &AtomicUsize; - /// Get the current length of the note cursor. - fn get_note_len (&self) -> usize { - self.note_len().load(Relaxed) - } - /// Set the length of the note cursor, returning the previous value. - fn set_note_len (&self, x: usize) -> usize { - self.note_len().swap(x, Relaxed) - } - - fn note_pos (&self) -> &AtomicUsize; - /// Get the current pitch of the note cursor. - fn get_note_pos (&self) -> usize { - self.note_pos().load(Relaxed).min(127) - } - /// Set the current pitch fo the note cursor, returning the previous value. - fn set_note_pos (&self, x: usize) -> usize { - self.note_pos().swap(x.min(127), Relaxed) - } -} - -pub trait TimePoint { - fn time_pos (&self) -> &AtomicUsize; - /// Get the current time position of the note cursor. - fn get_time_pos (&self) -> usize { - self.time_pos().load(Relaxed) - } - /// Set the current time position of the note cursor, returning the previous value. - fn set_time_pos (&self, x: usize) -> usize { - self.time_pos().swap(x, Relaxed) - } -} - -pub trait MidiPoint: NotePoint + TimePoint { - /// Get the current end of the note cursor. - fn get_note_end (&self) -> usize { - self.get_time_pos() + self.get_note_len() - } -} - -pub trait TimeRange { - fn time_len (&self) -> &AtomicUsize; - fn get_time_len (&self) -> usize { - self.time_len().load(Relaxed) - } - fn time_zoom (&self) -> &AtomicUsize; - fn get_time_zoom (&self) -> usize { - self.time_zoom().load(Relaxed) - } - fn set_time_zoom (&self, value: usize) -> usize { - self.time_zoom().swap(value, Relaxed) - } - fn time_lock (&self) -> &AtomicBool; - fn get_time_lock (&self) -> bool { - self.time_lock().load(Relaxed) - } - fn set_time_lock (&self, value: bool) -> bool { - self.time_lock().swap(value, Relaxed) - } - fn time_start (&self) -> &AtomicUsize; - fn get_time_start (&self) -> usize { - self.time_start().load(Relaxed) - } - fn set_time_start (&self, value: usize) -> usize { - self.time_start().swap(value, Relaxed) - } - fn time_axis (&self) -> &AtomicUsize; - fn get_time_axis (&self) -> usize { - self.time_axis().load(Relaxed) - } - fn get_time_end (&self) -> usize { - self.time_start().get() + self.time_axis().get() * self.time_zoom().get() - } -} - -pub trait NoteRange { - fn note_lo (&self) -> &AtomicUsize; - fn get_note_lo (&self) -> usize { - self.note_lo().load(Relaxed) - } - fn set_note_lo (&self, x: usize) -> usize { - self.note_lo().swap(x, Relaxed) - } - fn note_axis (&self) -> &AtomicUsize; - fn get_note_axis (&self) -> usize { - self.note_axis().load(Relaxed) - } - fn get_note_hi (&self) -> usize { - (self.note_lo().get() + self.note_axis().get().saturating_sub(1)).min(127) - } -} - -pub trait MidiRange: TimeRange + NoteRange {} - -/// -/// ``` -/// let _ = tek::MidiCursor::default(); -/// ``` -#[derive(Debug, Clone)] pub struct MidiCursor { - /// Time coordinate of cursor - pub time_pos: Arc, - /// Note coordinate of cursor - pub note_pos: Arc, - /// Length of note that will be inserted, in pulses - pub note_len: Arc, -} - -/// -/// ``` -/// use tek::{TimeRange, NoteRange}; -/// let model = tek::MidiSelection::from((1, false)); -/// -/// let _ = model.get_time_len(); -/// let _ = model.get_time_zoom(); -/// let _ = model.get_time_lock(); -/// let _ = model.get_time_start(); -/// let _ = model.get_time_axis(); -/// let _ = model.get_time_end(); -/// -/// let _ = model.get_note_lo(); -/// let _ = model.get_note_axis(); -/// let _ = model.get_note_hi(); -/// ``` -#[derive(Debug, Clone, Default)] pub struct MidiSelection { - pub time_len: Arc, - /// Length of visible time axis - pub time_axis: Arc, - /// Earliest time displayed - pub time_start: Arc, - /// Time step - pub time_zoom: Arc, - /// Auto rezoom to fit in time axis - pub time_lock: Arc, - /// Length of visible note axis - pub note_axis: Arc, - // Lowest note displayed - pub note_lo: Arc, -} - impl MidiClip { pub fn new ( name: impl AsRef, @@ -646,8 +336,6 @@ impl_has!(Sequencer: |self: Track| self.sequencer); impl_has!(Clock: |self: Sequencer| self.clock); impl_has!(Vec: |self: Sequencer| self.midi_ins); impl_has!(Vec: |self: Sequencer| self.midi_outs); -impl_has!([AtomicUsize; 2]: |self: MidiEditor| self.size); -impl_has!([AtomicUsize; 2]: |self: PianoHorizontal| self.size); impl_default!(Sequencer: Self { clock: Clock::default(), play_clip: None, @@ -863,502 +551,6 @@ impl Audio for Sequencer { } } } -impl Draw for MidiEditor { - fn draw(self, to: &mut Tui) -> Usually> { - self.tui().draw(to) - } -} -impl Draw for PianoHorizontal { - fn draw(self, to: &mut Tui) -> Usually> { - self.tui().draw(to) - } -} -impl std::fmt::Debug for MidiEditor { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("MidiEditor").field("mode", &self.mode).finish() - } -} -impl_from!(MidiEditor: |clip: &Arc>| { - let model = Self::from(Some(clip.clone())); - model.redraw(); - model -}); -impl_from!(MidiEditor: |clip: Option>>| { - let mut model = Self::default(); - *model.clip_mut() = clip; - model.redraw(); - model -}); -impl_default!(MidiEditor: Self { - size: [0, 0].into(), mode: PianoHorizontal::new(None) -}); -impl_default!(OctaveVertical: Self { - on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] -}); -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.get_time_pos(); - let note_pos = self.get_note_pos(); - let note_len = self.get_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 + 1) % clip.length); - } - redraw = true; - } - if redraw { - self.mode.redraw(); - } - } - fn _todo_opt_clip_stub (&self) -> Option>> { todo!() } - fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } - fn note_length (&self) -> usize { self.get_note_len() } - fn note_pos (&self) -> usize { self.get_note_pos() } - fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 } - fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 } - fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) } - fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) } - fn note_len (&self) -> usize { self.get_note_len() } - fn note_len_next (&self) -> usize { self.get_note_len() + 1 } - fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) } - fn note_range (&self) -> usize { self.get_note_axis() } - fn note_range_next (&self) -> usize { self.get_note_axis() + 1 } - fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) } - fn time_zoom (&self) -> usize { self.get_time_zoom() } - fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 } - fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 } - fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } - fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } - fn time_lock (&self) -> bool { self.get_time_lock() } - fn time_lock_toggled (&self) -> bool { !self.get_time_lock() } - fn time_pos (&self) -> usize { self.get_time_pos() } - fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() } - fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() } - fn time_pos_prev (&self) -> usize { - let step = self.get_note_len(); - self.get_time_pos().overflowing_sub(step) - .0.min(self.clip_length().saturating_sub(step)) - } - fn time_pos_prev_fine (&self) -> usize { - self.get_time_pos().overflowing_sub(1) - .0.min(self.clip_length().saturating_sub(1)) - } - pub fn clip_status (&self) -> impl Draw + '_ { - 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) }; - w_exact(20, south!( - w_full(origin_w(east( - button_2("f2", "name ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), - w_full(origin_w(east( - button_2("l", "ength ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), - w_full(origin_w(east( - button_2("r", "epeat ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), - )) - } - pub fn edit_status (&self) -> impl Draw + '_ { - 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.get_time_pos(); - let time_zoom = self.get_time_zoom(); - let time_lock = if self.get_time_lock() { "[lock]" } else { " " }; - let note_pos = self.get_note_pos(); - let note_name = format!("{:4}", note_pitch_to_name(note_pos)); - let note_pos = format!("{:>3}", note_pos); - let note_len = format!("{:>4}", self.get_note_len()); - w_exact(20, south!( - w_full(origin_w(east( - button_2("t", "ime ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), - format!("{length} /{time_zoom} +{time_pos} "))))))), - w_full(origin_w(east( - button_2("z", "lock ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), - format!("{time_lock}"))))))), - w_full(origin_w(east( - button_2("x", "note ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), - 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) -> &AtomicUsize { self.mode.note_len() } - fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() } -} - -impl TimePoint for MidiEditor { - fn time_pos (&self) -> &AtomicUsize { self.mode.time_pos() } -} - -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) } -} - -impl MidiEditor { - fn tui (&self) -> impl Draw { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) } -} - - -impl PianoHorizontal { - pub fn new (clip: Option<&Arc>>) -> Self { - let size = [0, 0].into(); - let mut range = MidiSelection::from((12, true)); - range.time_axis = size.x.clone(); - range.note_axis = size.y.clone(); - let piano = Self { - keys_width: 5, - size, - range, - buffer: RwLock::new(Default::default()).into(), - point: MidiCursor::default(), - clip: clip.cloned(), - color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), - }; - piano.redraw(); - piano - } -} - -impl PianoHorizontal { - fn tui (&self) -> impl Draw { - south( - east(w_exact(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), - east(self.keys(), self.size.of(below(wh_full(self.notes()), wh_full(self.cursor())))), - ) - } -} - -impl PianoHorizontal { - /// Draw the piano roll background. - /// - /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize, note_point: usize, time_point: usize) { - for (y, note) in (0..=127).rev().enumerate() { - for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { - let cell = buf.get_mut(x, y).unwrap(); - if note == (127-note_point) || time == time_point { - cell.set_bg(Rgb(0,0,0)); - } else { - cell.set_bg(clip.color.darkest.term); - } - if time % 384 == 0 { - cell.set_fg(clip.color.darker.term); - cell.set_char('│'); - } else if time % 96 == 0 { - cell.set_fg(clip.color.dark.term); - cell.set_char('╎'); - } else if time % note_len == 0 { - cell.set_fg(clip.color.darker.term); - cell.set_char('┊'); - } else if (127 - note) % 12 == 0 { - cell.set_fg(clip.color.darker.term); - cell.set_char('='); - } else if (127 - note) % 6 == 0 { - cell.set_fg(clip.color.darker.term); - cell.set_char('—'); - } else { - cell.set_fg(clip.color.darker.term); - cell.set_char('·'); - } - } - } - } - /// Draw the piano roll foreground. - /// - /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { - let style = Style::default().fg(clip.color.base.term);//.bg(Rgb(0, 0, 0)); - let mut notes_on = [false;128]; - for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { - for (_y, note) in (0..=127).rev().enumerate() { - if let Some(cell) = buf.get_mut(x, note) { - if notes_on[note] { - cell.set_char('▂'); - cell.set_style(style); - } - } - } - let time_end = time_start + zoom; - for time in time_start..time_end.min(clip.length) { - for event in clip.notes[time].iter() { - match event { - MidiMessage::NoteOn { key, .. } => { - let note = key.as_int() as usize; - if let Some(cell) = buf.get_mut(x, note) { - cell.set_char('█'); - cell.set_style(style); - } - notes_on[note] = true - }, - MidiMessage::NoteOff { key, .. } => { - notes_on[key.as_int() as usize] = false - }, - _ => {} - } - } - } - - } - } - fn notes (&self) -> impl Draw { - let time_start = self.get_time_start(); - let note_lo = self.get_note_lo(); - let note_hi = self.get_note_hi(); - let buffer = self.buffer.clone(); - Thunk::new(move|to: &mut Tui|{ - let source = buffer.read().unwrap(); - let XYWH(x0, y0, w, _h) = to.area(); - //if h as usize != note_axis { - //panic!("area height mismatch: {h} <> {note_axis}"); - //} - for (area_x, screen_x) in (x0..x0+w).enumerate() { - for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { - let source_x = time_start + area_x; - let source_y = note_hi - area_y; - // TODO: enable loop rollover: - //let source_x = (time_start + area_x) % source.width.max(1); - //let source_y = (note_hi - area_y) % source.height.max(1); - let is_in_x = source_x < source.width; - let is_in_y = source_y < source.height; - if is_in_x && is_in_y { - if let Some(source_cell) = source.get(source_x, source_y) { - if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { - *cell = source_cell.clone(); - } - } - } - } - } - }) - } - fn cursor (&self) -> impl Draw { - let note_hi = self.get_note_hi(); - let note_lo = self.get_note_lo(); - let note_pos = self.get_note_pos(); - let note_len = self.get_note_len(); - let time_pos = self.get_time_pos(); - let time_start = self.get_time_start(); - let time_zoom = self.get_time_zoom(); - let style = Some(Style::default().fg(self.color.lightest.term)); - Thunk::new(move|to: &mut Tui|{ - let XYWH(x0, y0, w, _) = to.area(); - for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - if note == note_pos { - for x in 0..w { - let screen_x = x0 + x; - let time_1 = time_start + x as usize * time_zoom; - let time_2 = time_1 + time_zoom; - if time_1 <= time_pos && time_pos < time_2 { - to.blit(&"█", screen_x, screen_y, style); - let tail = note_len as u16 / time_zoom as u16; - for x_tail in (screen_x + 1)..(screen_x + tail) { - to.blit(&"▂", x_tail, screen_y, style); - } - break - } - } - break - } - } - }) - } - fn keys (&self) -> impl Draw { - let state = self; - let color = state.color; - let note_lo = state.get_note_lo(); - let note_hi = state.get_note_hi(); - let note_pos = state.get_note_pos(); - let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); - let off_style = Some(Style::default().fg(Tui::g(255))); - let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.term).bold()); - h_full(w_exact(self.keys_width, Thunk::new(move|to: &mut Tui|{ - let XYWH(x, y0, _w, _h) = to.area(); - for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - to.blit(&to_key(note), x, screen_y, key_style); - if note > 127 { - continue - } - if note == note_pos { - to.blit(&format!("{:<5}", note_pitch_to_name(note)), x, screen_y, on_style) - } else { - to.blit(¬e_pitch_to_name(note), x, screen_y, off_style) - }; - } - }))) - } - fn timeline (&self) -> impl Draw + '_ { - w_full(h_exact(1, Thunk::new(move|to: &mut Tui|{ - let XYWH(x, y, w, _h) = to.area(); - let style = Some(Style::default().dim()); - let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { - let t = area_x as usize * self.time_zoom().get(); - if t < length { - to.blit(&"|", screen_x, y, style); - } - } - }))) - } -} - -impl TimeRange for PianoHorizontal { - fn time_len (&self) -> &AtomicUsize { self.range.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.range.time_start() } - fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } -} - -impl NoteRange for PianoHorizontal { - fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } -} - -impl NotePoint for PianoHorizontal { - fn note_len (&self) -> &AtomicUsize { self.point.note_len() } - fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } -} - -impl TimePoint for PianoHorizontal { - fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } -} - -impl MidiViewer for PianoHorizontal { - fn clip (&self) -> &Option>> { &self.clip } - fn clip_mut (&mut self) -> &mut Option>> { &mut self.clip } - /// Determine the required space to render the clip. - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { - (clip.length / self.range.time_zoom().get(), 128) - } - fn redraw (&self) { - *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { - let clip = clip.read().unwrap(); - let buf_size = self.buffer_size(&clip); - let mut buffer = BigBuffer::from(buf_size); - let time_zoom = self.get_time_zoom(); - self.time_len().set(clip.length); - PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); - PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); - buffer - } else { - Default::default() - } - } - fn set_clip (&mut self, clip: Option<&Arc>>) { - *self.clip_mut() = clip.cloned(); - self.color = clip.map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]); - self.redraw(); - } -} - -impl std::fmt::Debug for PianoHorizontal { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - let buffer = self.buffer.read().unwrap(); - f.debug_struct("PianoHorizontal") - .field("time_zoom", &self.range.time_zoom) - .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) - .finish() - } -} -impl OctaveVertical { - fn color (&self, pitch: usize) -> Color { - let pitch = pitch % 12; - self.colors[if self.on[pitch] { 2 } else { match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } }] - } -} -impl OctaveVertical { - fn tui (&self) -> impl Draw { - east!( - Tui::fg_bg(self.color(0), self.color(1), "▙"), - Tui::fg_bg(self.color(2), self.color(3), "▙"), - Tui::fg_bg(self.color(4), self.color(5), "▌"), - Tui::fg_bg(self.color(6), self.color(7), "▟"), - Tui::fg_bg(self.color(8), self.color(9), "▟"), - Tui::fg_bg(self.color(10), self.color(11), "▟"), - ) - } -} -impl_from!(MidiSelection: |data:(usize, bool)| Self { - time_len: Arc::new(0.into()), - note_axis: Arc::new(0.into()), - note_lo: Arc::new(0.into()), - time_axis: Arc::new(0.into()), - time_start: Arc::new(0.into()), - time_zoom: Arc::new(data.0.into()), - time_lock: Arc::new(data.1.into()), -}); -impl_default!(MidiCursor: Self { - time_pos: Arc::new(0.into()), - note_pos: Arc::new(36.into()), - note_len: Arc::new(24.into()), -}); - -impl NotePoint for MidiCursor { - fn note_len (&self) -> &AtomicUsize { - &self.note_len - } - fn note_pos (&self) -> &AtomicUsize { - &self.note_pos - } -} - -impl TimePoint for MidiCursor { - fn time_pos (&self) -> &AtomicUsize { - self.time_pos.as_ref() - } -} - -impl TimeRange for MidiSelection { - fn time_len (&self) -> &AtomicUsize { &self.time_len } - fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom } - fn time_lock (&self) -> &AtomicBool { &self.time_lock } - fn time_start (&self) -> &AtomicUsize { &self.time_start } - fn time_axis (&self) -> &AtomicUsize { &self.time_axis } -} - -impl NoteRange for MidiSelection { - fn note_lo (&self) -> &AtomicUsize { &self.note_lo } - fn note_axis (&self) -> &AtomicUsize { &self.note_axis } -} impl Iterator for Ticker { type Item = (usize, usize); @@ -1382,7 +574,7 @@ impl Iterator for Ticker { } } -fn to_key (note: usize) -> &'static str { +pub fn to_key (note: usize) -> &'static str { match note % 12 { 11 | 9 | 7 | 5 | 4 | 2 | 0 => "████▌", 10 | 8 | 6 | 3 | 1 => " ", diff --git a/src/tek.rs b/src/tek.rs index 89e49323..2c6476cf 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -46,6 +46,7 @@ pub mod clock; pub mod config; pub mod device; pub mod dialog; +pub mod editor; pub mod menu; pub mod mix; pub mod mode; @@ -57,16 +58,17 @@ pub mod view; #[cfg(feature = "plugin")] pub mod plugin; use clap::{self, Parser, Subcommand}; -use builder_pattern::Builder; use self::{ - arrange::*, clock::*, dialog::*, browse::*, select::*, sequence::*, device::*, - config::*, mode::*, view::*, bind::*, sample::*, menu::* + config::*, mode::*, view::*, bind::*, + dialog::*, browse::*, menu::*, + clock::*, sequence::*, editor::*, + arrange::*, select::*, device::*, sample::*, }; extern crate xdg; pub(crate) use ::xdg::BaseDirectories; pub extern crate atomic_float; -pub(crate) use atomic_float::AtomicF64; +//pub(crate) use atomic_float::AtomicF64; //pub extern crate jack; //pub(crate) use ::jack::{*, contrib::{*, ClosureProcessHandler}}; pub extern crate midly; @@ -198,34 +200,39 @@ fn tek_dec (state: &mut App, axis: &ControlAxis) -> Perhaps { }) } -fn collect_commands (app: &App, input: &TuiIn) -> Usually> { - let mut commands = vec![]; - for id in app.mode.keys.iter() { - if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref()) - && let Some(bindings) = event_map.query(input.event()) { - for binding in bindings { - for command in binding.commands.iter() { - if let Some(command) = app.namespace(command)? as Option { - commands.push(command) - } - } - } - } - } - Ok(commands) -} - -fn execute_commands ( - app: &mut App, commands: Vec -) -> Usually)>> { - let mut history = vec![]; - for command in commands.into_iter() { - let result = command.act(app); - match result { Err(err) => { history.push((command, None)); return Err(err) } - Ok(undo) => { history.push((command, undo)); } }; - } - Ok(history) -} +//impl_handle!(TuiIn: |self: App, input|{ + //let commands = tek_collect_commands(self, input)?; + //let history = tek_execute_commands(self, commands)?; + //self.history.extend(history.into_iter()); + //Ok(None) +//}); +//fn tek_collect_commands (app: &App, input: &TuiIn) -> Usually> { + //let mut commands = vec![]; + //for id in app.mode.keys.iter() { + //if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref()) + //&& let Some(bindings) = event_map.query(input.event()) { + //for binding in bindings { + //for command in binding.commands.iter() { + //if let Some(command) = app.namespace(command)? as Option { + //commands.push(command) + //} + //} + //} + //} + //} + //Ok(commands) +//} +//fn tek_execute_commands ( + //app: &mut App, commands: Vec +//) -> Usually)>> { + //let mut history = vec![]; + //for command in commands.into_iter() { + //let result = command.act(app); + //match result { Err(err) => { history.push((command, None)); return Err(err) } + //Ok(undo) => { history.push((command, undo)); } }; + //} + //Ok(history) +//} pub fn tek_jack_process (app: &mut App, client: &Client, scope: &ProcessScope) -> Control { let t0 = app.perf.get_t0(); @@ -590,7 +597,7 @@ pub(crate) const HEADER: &'static str = r#" /// Must not be dropped for the duration of the process pub jack: Jack<'static>, /// Display size - pub size: [AtomicUsize;2], + pub size: Size, /// Performance counter pub perf: PerfModel, /// Available view modes and input bindings @@ -613,7 +620,7 @@ impl_has!(Vec: |self: App|self.project.midi_ins); impl_has!(Vec: |self: App|self.project.midi_outs); impl_has!(Dialog: |self: App|self.dialog); impl_has!(Jack<'static>: |self: App|self.jack); -impl_has!([AtomicUsize;2]: |self: App|self.size); +impl_has!(Size: |self: App|self.size); impl_has!(Pool: |self: App|self.pool); impl_has!(Selection: |self: App|self.project.selection); impl_as_ref!(Vec: |self: App|self.project.as_ref()); @@ -622,17 +629,11 @@ impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); impl_has_clips!( |self: App|self.pool.clips); impl_audio!(App: tek_jack_process, tek_jack_event); -impl_handle!(TuiIn: |self: App, input|{ - let commands = collect_commands(self, input)?; - let history = execute_commands(self, commands)?; - self.history.extend(history.into_iter()); - Ok(None) -}); namespace!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); }); namespace!(App: u16 { literal = |dsl|try_to_u16(dsl); symbol = |app| { ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), - ":h/sample-detail" => 6.max(app.measure_height() as u16 * 3 / 9), }; }); + ":h/sample-detail" => 6.max(app.size.h() as u16 * 3 / 9), }; }); namespace!(App: isize { literal = |dsl|try_to_isize(dsl); }); namespace!(App: usize { literal = |dsl|try_to_usize(dsl); symbol = |app| { ":scene-count" => app.scenes().len(), @@ -690,7 +691,7 @@ namespace!(App: Option>> { }; }); -pub trait HasClipsSize { fn clips_size (&self) -> &[AtomicUsize;2]; } +pub trait HasClipsSize { fn clips_size (&self) -> &Size; } pub trait HasDevices: AsRef> + AsMut> { fn devices (&self) -> &Vec { self.as_ref() } @@ -966,7 +967,7 @@ impl Draw for App { } } } -impl HasClipsSize for App { fn clips_size (&self) -> &[AtomicUsize;2] { &self.project.size_inner } } +impl HasClipsSize for App { fn clips_size (&self) -> &Size { &self.project.size_inner } } impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } impl_default!(AppCommand: Self::Nop); primitive!(u8: try_to_u8); diff --git a/tengri b/tengri index a06ea2ac..a93fe92a 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit a06ea2ac139d09ab9eba5931455c43a3a75f4151 +Subproject commit a93fe92a596b8a9bcec898e228fca909c0e2e9f4 From 89df3cf1919c66cff72f6a0b1936c4d5b9604af6 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Fri, 17 Apr 2026 04:18:25 +0300 Subject: [PATCH 02/31] argh --- src/arrange.rs | 84 +++++++++++++++++++++++++++++++++++++------------- src/browse.rs | 38 +++++++++++------------ src/device.rs | 12 ++++++++ src/editor.rs | 42 ++++++++++++------------- src/sample.rs | 4 +-- src/tek.rs | 77 +++++++++++++++++++++++++++------------------ tengri | 2 +- 7 files changed, 164 insertions(+), 95 deletions(-) diff --git a/src/arrange.rs b/src/arrange.rs index b8ac5062..31b01809 100644 --- a/src/arrange.rs +++ b/src/arrange.rs @@ -244,11 +244,12 @@ pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { button_2("O", "+", false), bg(theme.darker.term, origin_w(thunk(|to: &mut Tui|{ for (index, track, _x1, _x2) in self.tracks_with_sizes() { + let iter = ||track.sequencer.midi_outs.iter(); + let draw = |port: &MidiOutput|fg(Rgb(255, 255, 255), + h_exact(1, bg(track.color.dark.term, w_full(origin_w( + format!("·o{index:02} {}", port.port_name())))))); to.place(&w_exact(track_width(index, track), - origin_nw(h_full(iter_south(||track.sequencer.midi_outs.iter(), - |port, index|fg(Rgb(255, 255, 255), - h_exact(1, bg(track.color.dark.term, w_full(origin_w( - format!("·o{index:02} {}", port.port_name()))))))))))); + origin_nw(h_full(iter_south(iter, draw))))); } Ok(XYWH(0, 0, 0, 0)) })))) @@ -261,16 +262,16 @@ pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { } let content = thunk(move|to: &mut Tui|{ for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&wh_exact(track_width(index, track), h + 1, + to.place(&wh_exact(Some(track_width(index, track)), Some(h + 1), origin_nw(south( bg(track.color.base.term, w_full(origin_w(east!( - either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "), - either(track.sequencer.recording, fg(Red, "●rec "), "·rec "), + either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "), + either(track.sequencer.recording, fg(Red, "●rec "), "·rec "), either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "), )))), - iter_south(1, ||track.sequencer.midi_ins.iter(), - |port, index|fg_bg(Rgb(255, 255, 255), track.color.dark.term, + iter_south(||track.sequencer.midi_ins.iter(), + |port|fg_bg(Rgb(255, 255, 255), track.color.dark.term, w_full(origin_w(format!("·i{index:02} {}", port.port_name()))))))))); } Ok(XYWH(0, 0, 0, 0)) @@ -308,8 +309,11 @@ pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + } fn view_scenes_names (&self) -> impl Draw { - w_exact(20, thunk(|to: &mut Tui|for (index, scene, ..) in self.scenes_with_sizes() { - to.place(&self.view_scene_name(index, scene)); + w_exact(20, thunk(|to: &mut Tui|{ + for (index, scene, ..) in self.scenes_with_sizes() { + to.place(&self.view_scene_name(index, scene)); + } + Ok(XYWH(1, 1, 1, 1)) })) } @@ -319,18 +323,18 @@ pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + } else { Self::H_SCENE as u16 }; - let bg = if self.selection().scene() == Some(index) { - scene.color.light.term - } else { - scene.color.base.term - }; let a = w_full(origin_w(east(format!("·s{index:02} "), fg(g(255), bold(true, &scene.name))))); let b = when(self.selection().scene() == Some(index) && self.is_editing(), wh_full(origin_nw(south( self.editor().as_ref().map(|e|e.clip_status()), self.editor().as_ref().map(|e|e.edit_status()))))); - wh_exact(20, h, bg(bg, origin_nw(south(a, b)))) + let c = if self.selection().scene() == Some(index) { + scene.color.light.term + } else { + scene.color.base.term + }; + wh_exact(Some(20), Some(h), bg(c, origin_nw(south(a, b)))) } } @@ -577,6 +581,7 @@ pub trait HasTrack: AsRefOpt + AsMutOpt { } impl Arrangement { + /// Create a new arrangement. pub fn new ( jack: &Jack<'static>, @@ -596,27 +601,33 @@ impl Arrangement { ..Default::default() } } + /// Width of display pub fn w (&self) -> u16 { self.size.w() as u16 } + /// Width allocated for sidebar. pub fn w_sidebar (&self, is_editing: bool) -> u16 { self.w() / if is_editing { 16 } else { 8 } as u16 } + /// Width available to display tracks. pub fn w_tracks_area (&self, is_editing: bool) -> u16 { self.w().saturating_sub(self.w_sidebar(is_editing)) } + /// Height of display pub fn h (&self) -> u16 { self.size.h() as u16 } + /// Height taken by visible device slots. pub fn h_devices (&self) -> u16 { 2 //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } + /// Add multiple tracks #[cfg(feature = "track")] pub fn tracks_add ( &mut self, @@ -634,6 +645,7 @@ impl Arrangement { } Ok(()) } + /// Add a track #[cfg(feature = "track")] pub fn track_add ( &mut self, @@ -669,6 +681,7 @@ impl Arrangement { } Ok((index, &mut self.tracks_mut()[index])) } + #[cfg(feature = "track")] pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw + '_ { south( h_exact(1, self.view_inputs_header()), @@ -679,6 +692,7 @@ impl Arrangement { }) ) } + #[cfg(feature = "track")] fn view_inputs_header (&self) -> impl Draw + '_ { east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), west(w_exact(4, button_2("I", "+", false)), thunk(move|to: &mut Tui|for (_index, track, x1, _x2) in self.tracks_with_sizes() { @@ -690,6 +704,7 @@ impl Arrangement { )))))) }))) } + #[cfg(feature = "track")] fn view_inputs_row (&self, port: &MidiInput) -> impl Draw { east(w_exact(20, origin_w(east(" ● ", bold(true, fg(Rgb(255,255,255), port.port_name()))))), west(w_exact(4, ()), thunk(move|to: &mut Tui|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { @@ -701,14 +716,20 @@ impl Arrangement { ))))) }))) } + #[cfg(feature = "track")] pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw { let mut h = 1; + for output in self.midi_outs().iter() { h += 1 + output.connections.len(); } + let h = h as u16; + let list = south( - h_exact(1, w_full(origin_w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))), + h_exact(1, w_full(origin_w(button_3( + "o", "utput", format!("{}", self.midi_outs.len()), false + )))), h_exact(h - 1, wh_full(origin_nw(thunk(|to: &mut Tui|{ for (_index, port) in self.midi_outs().iter().enumerate() { to.place(&h_exact(1,w_full(east( @@ -720,7 +741,9 @@ impl Arrangement { to.place(&h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info()))))); } } - }))))); + })))) + ); + h_exact(h, view_track_row_section(theme, list, button_2("O", "+", false), bg(theme.darker.term, origin_w(w_full( thunk(|to: &mut Tui|{ @@ -739,7 +762,13 @@ impl Arrangement { for (_index, _conn) in port.connections.iter().enumerate() { to.place(&h_exact(1, w_full(""))); } - }})))}})))))) + } + }) + )) + } + }) + ))) + )) } #[cfg(feature = "track")] pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw { let mut h = 2u16; @@ -756,9 +785,15 @@ impl Arrangement { fg_bg( ItemTheme::G[32].lightest.term, ItemTheme::G[32].dark.term, - origin_nw(format!(" · {}", "--"))))))))); - })) + origin_nw(format!(" · {}", "--")) + ) + ))) + ) + )); + }) + ) } + /// Put a clip in a slot #[cfg(feature = "clip")] pub fn clip_put ( &mut self, track: usize, scene: usize, clip: Option>> @@ -767,6 +802,7 @@ impl Arrangement { self.scenes[scene].clips[track] = clip; old } + /// Change the color of a clip, returning the previous one #[cfg(feature = "clip")] pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) -> Option @@ -779,20 +815,24 @@ impl Arrangement { //old }) } + /// Toggle looping for the active clip #[cfg(feature = "clip")] pub fn toggle_loop (&mut self) { if let Some(clip) = self.selected_clip() { clip.write().unwrap().toggle_loop() } } + /// Get the first sampler of the active track #[cfg(feature = "sampler")] pub fn sampler (&self) -> Option<&Sampler> { self.selected_track()?.sampler(0) } + /// Get the first sampler of the active track #[cfg(feature = "sampler")] pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { self.selected_track_mut()?.sampler_mut(0) } + } impl ScenesView for Arrangement { fn h_scenes (&self) -> u16 { diff --git a/src/browse.rs b/src/browse.rs index 71ae746c..658f32ba 100644 --- a/src/browse.rs +++ b/src/browse.rs @@ -245,28 +245,28 @@ impl Pool { impl<'a> PoolView<'a> { fn tui (&self) -> impl Draw { let Self(pool) = self; - //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into()); - //let on_bg = |x|x;//below(Repeat(" "), Tui::bg(color.darkest.term, x)); + //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||g(32).into()); + //let on_bg = |x|x;//below(Repeat(" "), bg(color.darkest.term, x)); //let border = |x|x;//Outer(Style::default().fg(color.dark.term).bg(color.darkest.term)).enclose(x); //let height = pool.clips.read().unwrap().len() as u16; w_exact(20, h_full(origin_n(iter( ||pool.clips().clone().into_iter(), move|clip: Arc>, i: usize|{ - let item_height = 1; - let item_offset = i as u16 * item_height; - let selected = i == pool.clip_index(); - let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); - let bg = if selected { color.light.term } else { color.base.term }; - let fg = color.lightest.term; - let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; - let length = if false { String::default() } else { format!("{length} ") }; - h_exact(1, iter_south(item_offset, item_height, Tui::bg(bg, below!( - w_full(origin_w(Tui::fg(fg, Tui::bold(selected, name)))), - w_full(origin_e(Tui::fg(fg, Tui::bold(selected, length)))), - w_full(origin_w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))), - w_full(origin_e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))), - )))) - })))) + let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); + let item_height = 1; + let item_offset = i as u16 * item_height; + let selected = i == pool.clip_index(); + let b = if selected { color.light.term } else { color.base.term }; + let f = color.lightest.term; + let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; + let length = if false { String::default() } else { format!("{length} ") }; + h_exact(1, iter_south(item_offset, item_height, bg(b, below!( + w_full(origin_w(fg(fg, bold(selected, name)))), + w_full(origin_e(fg(fg, bold(selected, length)))), + w_full(origin_w(When::new(selected, bold(true, fg(g(255), "▶"))))), + w_full(origin_e(When::new(selected, bold(true, fg(g(255), "◀"))))), + )))) + })))) } } impl ClipLength { @@ -337,10 +337,10 @@ impl<'a> Iterator for EntriesIterator<'a> { let index = self.index; if self.index < dirs { self.index += 1; - Some(Tui::bold(true, self.browser.dirs[index].1.as_str())) + Some(bold(true, self.browser.dirs[index].1.as_str())) } else if self.index < dirs + files { self.index += 1; - Some(Tui::bold(false, self.browser.files[index - dirs].1.as_str())) + Some(bold(false, self.browser.files[index - dirs].1.as_str())) } else { None } diff --git a/src/device.rs b/src/device.rs index c87f7c2a..c3ea587d 100644 --- a/src/device.rs +++ b/src/device.rs @@ -460,6 +460,18 @@ impl JackPort for MidiOutput { } } +impl View for MidiInput { + fn view (&self) -> impl Draw { + "TODO: MIDI IN" + } +} + +impl View for MidiOutput { + fn view (&self) -> impl Draw { + "TODO: MIDI OUT" + } +} + impl MidiOutput { /// Clear the section of the output buffer that we will be using, /// emitting "all notes off" at start of buffer if requested. diff --git a/src/editor.rs b/src/editor.rs index 82187504..c792c39f 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -167,19 +167,19 @@ pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync { let note_lo = self.get_note_lo(); let note_hi = self.get_note_hi(); if note_pos < note_lo { - self.note_lo().set(note_pos); + self.note_lo().store(note_pos, Relaxed); } else if note_pos > note_hi { - self.note_lo().set((note_lo + note_pos).saturating_sub(note_hi)); + self.note_lo().store((note_lo + note_pos).saturating_sub(note_hi), Relaxed); } } /// Make sure time range is within display fn autozoom (&self) { - if self.time_lock().get() { + if self.time_lock().load(Relaxed) { let time_len = self.get_time_len(); let time_axis = self.get_time_axis(); let time_zoom = self.get_time_zoom(); loop { - let time_zoom = self.time_zoom().get(); + let time_zoom = self.time_zoom().load(Relaxed); let time_area = time_axis * time_zoom; if time_area > time_len { let next_time_zoom = note_duration_prev(time_zoom); @@ -205,7 +205,7 @@ pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync { } } } - if time_zoom != self.time_zoom().get() { + if time_zoom != self.time_zoom().load(Relaxed) { self.redraw() } } @@ -289,7 +289,7 @@ pub trait TimeRange { self.time_axis().load(Relaxed) } fn get_time_end (&self) -> usize { - self.time_start().get() + self.time_axis().get() * self.time_zoom().get() + self.time_start().load(Relaxed) + self.time_axis().load(Relaxed) * self.time_zoom().load(Relaxed) } } @@ -419,13 +419,13 @@ impl MidiEditor { w_exact(20, south!( w_full(origin_w(east( button_2("f2", "name ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), + w_full(origin_e(fg(Rgb(255, 255, 255), format!("{name} "))))))), w_full(origin_w(east( button_2("l", "ength ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), + w_full(origin_e(fg(Rgb(255, 255, 255), format!("{length} "))))))), w_full(origin_w(east( button_2("r", "epeat ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), + w_full(origin_e(fg(Rgb(255, 255, 255), format!("{looped} "))))))), )) } pub fn edit_status (&self) -> impl Draw + '_ { @@ -442,15 +442,15 @@ impl MidiEditor { w_exact(20, south!( w_full(origin_w(east( button_2("t", "ime ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), + w_full(origin_e(fg(Rgb(255, 255, 255), format!("{length} /{time_zoom} +{time_pos} "))))))), w_full(origin_w(east( button_2("z", "lock ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), + w_full(origin_e(fg(Rgb(255, 255, 255), format!("{time_lock}"))))))), w_full(origin_w(east( button_2("x", "note ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), + w_full(origin_e(fg(Rgb(255, 255, 255), format!("{note_name} {note_pos} {note_len}"))))))), )) } @@ -665,7 +665,7 @@ impl PianoHorizontal { let note_hi = state.get_note_hi(); let note_pos = state.get_note_pos(); let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); - let off_style = Some(Style::default().fg(Tui::g(255))); + let off_style = Some(Style::default().fg(g(255))); let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.term).bold()); h_full(w_exact(self.keys_width, Thunk::new(move|to: &mut Tui|{ let XYWH(x, y0, _w, _h) = to.area(); @@ -688,7 +688,7 @@ impl PianoHorizontal { let style = Some(Style::default().dim()); let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { - let t = area_x as usize * self.time_zoom().get(); + let t = area_x as usize * self.time_zoom().load(Relaxed); if t < length { to.blit(&"|", screen_x, y, style); } @@ -724,7 +724,7 @@ impl MidiViewer for PianoHorizontal { fn clip_mut (&mut self) -> &mut Option>> { &mut self.clip } /// Determine the required space to render the clip. fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { - (clip.length / self.range.time_zoom().get(), 128) + (clip.length / self.range.time_zoom().load(Relaxed), 128) } fn redraw (&self) { *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { @@ -765,12 +765,12 @@ impl OctaveVertical { impl OctaveVertical { fn tui (&self) -> impl Draw { east!( - Tui::fg_bg(self.color(0), self.color(1), "▙"), - Tui::fg_bg(self.color(2), self.color(3), "▙"), - Tui::fg_bg(self.color(4), self.color(5), "▌"), - Tui::fg_bg(self.color(6), self.color(7), "▟"), - Tui::fg_bg(self.color(8), self.color(9), "▟"), - Tui::fg_bg(self.color(10), self.color(11), "▟"), + fg_bg(self.color(0), self.color(1), "▙"), + fg_bg(self.color(2), self.color(3), "▙"), + fg_bg(self.color(4), self.color(5), "▌"), + fg_bg(self.color(6), self.color(7), "▟"), + fg_bg(self.color(8), self.color(9), "▟"), + fg_bg(self.color(10), self.color(11), "▟"), ) } } diff --git a/src/sample.rs b/src/sample.rs index 09b238ff..43763aa3 100644 --- a/src/sample.rs +++ b/src/sample.rs @@ -631,7 +631,7 @@ fn draw_viewer (sample: Option<&Arc>>) -> impl Draw + use<'_ //height as f64 / 2.0, //text.red() //); - }).render(area, &mut to.buffer); + }).render(area, &mut to.1); } else { Canvas::default() .x_bounds([0.0, width as f64]) @@ -644,7 +644,7 @@ fn draw_viewer (sample: Option<&Arc>>) -> impl Draw + use<'_ //text.red() //); }) - .render(area, &mut to.buffer); + .render(area, &mut to.1); } }) } diff --git a/src/tek.rs b/src/tek.rs index 2c6476cf..90955b66 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -426,11 +426,11 @@ pub fn button_play_pause (playing: bool) -> impl Draw { let compact = true;//self.is_editing(); bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, either(compact, - Thunk::new(move|to: &mut Tui|to.place(&w_exact(9, either(playing, + thunk(move|to: &mut Tui|to.place(&w_exact(9, either(playing, fg(Rgb(0, 255, 0), " PLAYING "), fg(Rgb(255, 128, 0), " STOPPED "))) )), - Thunk::new(move|to: &mut Tui|to.place(&w_exact(5, either(playing, + thunk(move|to: &mut Tui|to.place(&w_exact(5, either(playing, fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",)))) )) @@ -466,19 +466,19 @@ pub fn view_wrap (bg: Color, fg: Color, content: impl Draw) -> impl Draw (label: &'a str, value: f32) -> impl Draw + 'a { south!( field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)), - wh_exact(if value >= 0.0 { 13 } - else if value >= -1.0 { 12 } - else if value >= -2.0 { 11 } - else if value >= -3.0 { 10 } - else if value >= -4.0 { 9 } - else if value >= -6.0 { 8 } - else if value >= -9.0 { 7 } - else if value >= -12.0 { 6 } - else if value >= -15.0 { 5 } - else if value >= -20.0 { 4 } - else if value >= -25.0 { 3 } - else if value >= -30.0 { 2 } - else if value >= -40.0 { 1 } + wh_exact(if value >= 0.0 { &"13" } + else if value >= -1.0 { &"12" } + else if value >= -2.0 { &"11" } + else if value >= -3.0 { &"10" } + else if value >= -4.0 { &"9" } + else if value >= -6.0 { &"8" } + else if value >= -9.0 { &"7" } + else if value >= -12.0 { &"6" } + else if value >= -15.0 { &"5" } + else if value >= -20.0 { &"4" } + else if value >= -25.0 { &"3" } + else if value >= -30.0 { &"2" } + else if value >= -40.0 { &"1" } else { 0 }, 1, bg(if value >= 0.0 { Red } else if value >= -3.0 { Yellow } else { Green }, ()))) @@ -491,7 +491,7 @@ pub fn view_meters (values: &[f32;2]) -> impl Draw + use<'_> { } pub fn draw_info (sample: Option<&Arc>>) -> impl Draw + use<'_> { - when(sample.is_some(), Thunk::new(move|to: &mut Tui|{ + when(sample.is_some(), thunk(move|to: &mut Tui|{ let sample = sample.unwrap().read().unwrap(); let theme = sample.color; to.place(&east!( @@ -506,7 +506,7 @@ pub fn draw_info (sample: Option<&Arc>>) -> impl Draw + use< } pub fn draw_info_v (sample: Option<&Arc>>) -> impl Draw + use<'_> { - either(sample.is_some(), Thunk::new(move|to: &mut Tui|{ + either(sample.is_some(), thunk(move|to: &mut Tui|{ let sample = sample.unwrap().read().unwrap(); let theme = sample.color; to.place(&w_exact(20, south!( @@ -517,7 +517,7 @@ pub fn draw_info_v (sample: Option<&Arc>>) -> impl Draw + us w_full(origin_w(field_h(theme, "Trans ", "0"))), w_full(origin_w(field_h(theme, "Gain ", format!("{}", sample.gain)))), ))) - }), Thunk::new(|to: &mut Tui|to.place(&fg(Red, south!( + }), thunk(|to: &mut Tui|to.place(&fg(Red, south!( bold(true, "× No sample."), "[r] record", "[Shift-F9] import", @@ -543,9 +543,9 @@ pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, po let ins = ports.len() as u16; let frame = Outer(true, Style::default().fg(g(96))); let iter = move||ports.iter(); - let names = iter_south(1, iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name())))); + let names = iter_south(iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name())))); let field = field_v(theme, title, names); - wh_exact(20, 1 + ins, frame.enclose(wh_exact(20, 1 + ins, field))) + wh_exact(Some(20), Some(1 + ins), frame.enclose(wh_exact(Some(20), Some(1 + ins), field))) } pub fn io_ports <'a, T: PortsSizes<'a>> ( @@ -554,11 +554,22 @@ pub fn io_ports <'a, T: PortsSizes<'a>> ( iter(items, move|( _index, name, connections, y, y2 ): (usize, &'a Arc, &'a [Connect], usize, usize), _| - iter_south(y as u16, (y2-y) as u16, south( - h_full(bold(true, fg_bg(fg, bg, origin_w(east(&" 󰣲 ", name))))), - iter(||connections.iter(), move|connect: &'a Connect, index|iter_south(index as u16, 1, - h_full(origin_w(bold(false, fg_bg(fg, bg, - &connect.info))))))))) + iter_south( + y as u16, + (y2-y) as u16, + south( + h_full(bold(true, fg_bg(fg, bg, origin_w(east(" 󰣲 ", name))))), + iter( + ||connections.iter(), + move|connect: &'a Connect, index|iter_south( + index as u16, + 1, + h_full(origin_w(bold(false, fg_bg(fg, bg, &connect.info)))) + ) + ) + ) + ) + ) } /// CLI banner. @@ -758,7 +769,7 @@ fn app_understand_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usu Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &state.dialog { let items = items.clone(); let selected = selected; - Some(wh_full(Thunk::new(move|to: &mut Tui|{ + Some(wh_full(thunk(move|to: &mut Tui|{ for (index, MenuItem(item, _)) in items.0.iter().enumerate() { to.place(&y_push((2 * index) as u16, fg_bg( @@ -776,7 +787,7 @@ fn app_understand_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usu Some(":templates") => to.place(&{ let modes = state.config.modes.clone(); let height = (modes.read().unwrap().len() * 2) as u16; - h_exact(height, w_min(30, Thunk::new(move |to: &mut Tui|{ + h_exact(height, w_min(30, thunk(move |to: &mut Tui|{ for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); @@ -792,7 +803,7 @@ fn app_understand_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usu } }))) }), - Some(":sessions") => to.place(&h_exact(6, w_min(30, Thunk::new(|to: &mut Tui|{ + Some(":sessions") => to.place(&h_exact(6, w_min(30, thunk(|to: &mut Tui|{ let fg = Rgb(224, 192, 128); for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; @@ -811,7 +822,7 @@ fn app_understand_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usu }, w_shrink(3, h_exact(1, fg(g(96), x_repeat("🭻")))))))), Some(":device") => { let selected = state.dialog.device_kind().unwrap(); - to.place(&south(bold(true, "Add device"), iter_south(1, + to.place(&south(bold(true, "Add device"), iter_south( move||device_kinds().iter(), move|_label: &&'static str, i|{ let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; @@ -957,7 +968,7 @@ impl App { impl Draw for App { fn draw (self, to: &mut Tui) -> Usually> { if let Some(e) = self.error.read().unwrap().as_ref() { - to.place_at(to.area(), e); + to.place_at(to.area().into(), e.as_ref()); } for (index, dsl) in self.mode.view.iter().enumerate() { if let Err(e) = self.understand(to, dsl) { @@ -974,3 +985,9 @@ primitive!(u8: try_to_u8); primitive!(u16: try_to_u16); primitive!(usize: try_to_usize); primitive!(isize: try_to_isize); + +fn field_h (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { +} + +fn field_v (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { +} diff --git a/tengri b/tengri index a93fe92a..c9b9ff15 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit a93fe92a596b8a9bcec898e228fca909c0e2e9f4 +Subproject commit c9b9ff15191ec3a37e8da35f382ce0d4af74d0d8 From 790e33b40c917edff706c808c1cc7fee6bd29b29 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Tue, 16 Jun 2026 14:23:29 +0300 Subject: [PATCH 03/31] phocking wip (weep) --- src/arrange.rs | 66 +++++++++++++++-------------- src/tek.rs | 110 +++++++++++++++++++++++++------------------------ tengri | 2 +- 3 files changed, 92 insertions(+), 86 deletions(-) diff --git a/src/arrange.rs b/src/arrange.rs index 31b01809..de20fbbe 100644 --- a/src/arrange.rs +++ b/src/arrange.rs @@ -115,8 +115,9 @@ pub trait ClipsView: TracksView + ScenesView { wh_full(origin_se(fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))), thunk(|to: &mut Tui|{ for (track_index, track, _, _) in self.tracks_with_sizes() { - to.place(&w_exact(track.width as u16, - h_full(self.view_track_clips(track_index, track)))) + let w = track.width as u16; + let v = self.view_track_clips(track_index, track); + w_exact(w, h_full(v)).draw(to); } Ok(XYWH(0, 0, 0, 0)) })))) @@ -166,14 +167,14 @@ pub trait ClipsView: TracksView + ScenesView { self.selection().scene() == Some(scene_index) && self.is_editing(); - to.place(&wh_exact(Some(w), Some(y), below( + wh_exact(Some(w), Some(y), below( wh_full(Outer(true, Style::default().fg(outline))), wh_full(below( below( fg_bg(outline, bg, wh_full("")), wh_full(origin_nw(fg_bg(fg, bg, bold(true, &name)))), ), - wh_full(when(is_selected, self.editor().map(|e|e.view())))))))); + wh_full(when(is_selected, self.editor().map(|e|e.view()))))))).draw(to); } Ok(XYWH(0, 0, 0, 0)) }) @@ -219,7 +220,7 @@ pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { view_track_row_section(theme, button, button_2, bg(theme.darker.term, h_exact(2, thunk(|to: &mut Tui|{ for (index, track, x1, _x2) in self.tracks_with_sizes() { - to.place(&x_push(x1 as u16, w_exact(track_width(index, track), + x_push(x1 as u16, w_exact(track_width(index, track), bg(if selected.track() == Some(index) { track.color.light.term } else { @@ -227,7 +228,8 @@ pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { }, south(w_full(origin_nw(east( format!("·t{index:02} "), fg(Rgb(255, 255, 255), bold(true, &track.name)) - ))), ""))) ));} + ))), ""))) ).draw(to); + } Ok(XYWH(0, 0, 0, 0)) })))) } @@ -237,7 +239,7 @@ pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { south(w_full(origin_w(button_2("o", "utput", false))), thunk(|to: &mut Tui|{ for port in self.midi_outs().iter() { - to.place(&w_full(origin_w(port.port_name()))); + w_full(origin_w(port.port_name())).draw(to); } Ok(XYWH(0, 0, 0, 0)) })), @@ -245,11 +247,11 @@ pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { bg(theme.darker.term, origin_w(thunk(|to: &mut Tui|{ for (index, track, _x1, _x2) in self.tracks_with_sizes() { let iter = ||track.sequencer.midi_outs.iter(); - let draw = |port: &MidiOutput|fg(Rgb(255, 255, 255), + let draw = |port: &MidiOutput, _|fg(Rgb(255, 255, 255), h_exact(1, bg(track.color.dark.term, w_full(origin_w( format!("·o{index:02} {}", port.port_name())))))); - to.place(&w_exact(track_width(index, track), - origin_nw(h_full(iter_south(iter, draw))))); + w_exact(track_width(index, track), + origin_nw(h_full(iter_south(iter, draw)))).draw(to); } Ok(XYWH(0, 0, 0, 0)) })))) @@ -262,7 +264,7 @@ pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { } let content = thunk(move|to: &mut Tui|{ for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&wh_exact(Some(track_width(index, track)), Some(h + 1), + wh_exact(Some(track_width(index, track)), Some(h + 1), origin_nw(south( bg(track.color.base.term, w_full(origin_w(east!( @@ -271,8 +273,9 @@ pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "), )))), iter_south(||track.sequencer.midi_ins.iter(), - |port|fg_bg(Rgb(255, 255, 255), track.color.dark.term, - w_full(origin_w(format!("·i{index:02} {}", port.port_name()))))))))); + |port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term, + w_full(origin_w(format!("·i{index:02} {}", port.port_name())))))))) + .draw(to); } Ok(XYWH(0, 0, 0, 0)) }); @@ -311,7 +314,7 @@ pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + fn view_scenes_names (&self) -> impl Draw { w_exact(20, thunk(|to: &mut Tui|{ for (index, scene, ..) in self.scenes_with_sizes() { - to.place(&self.view_scene_name(index, scene)); + self.view_scene_name(index, scene).draw(to); } Ok(XYWH(1, 1, 1, 1)) })) @@ -687,7 +690,7 @@ impl Arrangement { h_exact(1, self.view_inputs_header()), thunk(|to: &mut Tui|{ for (index, port) in self.midi_ins().iter().enumerate() { - to.place(&x_push(index as u16 * 10, h_exact(1, self.view_inputs_row(port)))) + x_push(index as u16 * 10, h_exact(1, self.view_inputs_row(port))).draw(to) } }) ) @@ -697,11 +700,11 @@ impl Arrangement { east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), west(w_exact(4, button_2("I", "+", false)), thunk(move|to: &mut Tui|for (_index, track, x1, _x2) in self.tracks_with_sizes() { #[cfg(feature = "track")] - to.place(&x_push(x1 as u16, bg(track.color.dark.term, origin_w(w_exact(track.width as u16, east!( + x_push(x1 as u16, bg(track.color.dark.term, origin_w(w_exact(track.width as u16, east!( either(track.sequencer.monitoring, fg(Green, "mon "), "mon "), either(track.sequencer.recording, fg(Red, "rec "), "rec "), either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "), - )))))) + ))))).draw(to) }))) } @@ -709,11 +712,11 @@ impl Arrangement { east(w_exact(20, origin_w(east(" ● ", bold(true, fg(Rgb(255,255,255), port.port_name()))))), west(w_exact(4, ()), thunk(move|to: &mut Tui|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { #[cfg(feature = "track")] - to.place(&bg(track.color.darker.term, origin_w(w_exact(track.width as u16, east!( + bg(track.color.darker.term, origin_w(w_exact(track.width as u16, east!( either(track.sequencer.monitoring, fg(Green, " ● "), " · "), either(track.sequencer.recording, fg(Red, " ● "), " · "), either(track.sequencer.overdub, fg(Yellow, " ● "), " · "), - ))))) + )))).draw(to) }))) } @@ -732,13 +735,14 @@ impl Arrangement { )))), h_exact(h - 1, wh_full(origin_nw(thunk(|to: &mut Tui|{ for (_index, port) in self.midi_outs().iter().enumerate() { - to.place(&h_exact(1,w_full(east( + h_exact(1,w_full(east( origin_w(east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name())))), w_full(origin_e(format!("{}/{} ", port.port().get_connections().len(), - port.connections.len()))))))); + port.connections.len())))))).draw(to); for (index, conn) in port.connections.iter().enumerate() { - to.place(&h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info()))))); + h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info())))) + .draw(to); } } })))) @@ -748,23 +752,23 @@ impl Arrangement { bg(theme.darker.term, origin_w(w_full( thunk(|to: &mut Tui|{ for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&w_exact(track_width(index, track), + w_exact(track_width(index, track), thunk(|to: &mut Tui|{ - to.place(&h_exact(1, origin_w(east( + h_exact(1, origin_w(east( either(true, fg(Green, "play "), "play "), either(false, fg(Yellow, "solo "), "solo "), - )))); + ))).draw(to); for (_index, port) in self.midi_outs().iter().enumerate() { - to.place(&h_exact(1, origin_w(east( + h_exact(1, origin_w(east( either(true, fg(Green, " ● "), " · "), either(false, fg(Yellow, " ● "), " · "), - )))); + ))).draw(to); for (_index, _conn) in port.connections.iter().enumerate() { - to.place(&h_exact(1, w_full(""))); + h_exact(1, w_full("")).draw(to); } } }) - )) + ).draw(to) } }) ))) @@ -779,7 +783,7 @@ impl Arrangement { button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), button_2("D", "+", false), thunk(move|to: &mut Tui|for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&wh_exact(track_width(index, track), h + 1, + wh_exact(track_width(index, track), h + 1, bg(track.color.dark.term, origin_nw(iter_south(2, move||0..h, |_, _index|wh_exact(track.width as u16, 2, fg_bg( @@ -789,7 +793,7 @@ impl Arrangement { ) ))) ) - )); + ).draw(to); }) ) } diff --git a/src/tek.rs b/src/tek.rs index 90955b66..fb27be6f 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -426,14 +426,14 @@ pub fn button_play_pause (playing: bool) -> impl Draw { let compact = true;//self.is_editing(); bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, either(compact, - thunk(move|to: &mut Tui|to.place(&w_exact(9, either(playing, + thunk(move|to: &mut Tui|w_exact(9, either(playing, fg(Rgb(0, 255, 0), " PLAYING "), - fg(Rgb(255, 128, 0), " STOPPED "))) - )), - thunk(move|to: &mut Tui|to.place(&w_exact(5, either(playing, + fg(Rgb(255, 128, 0), " STOPPED ")) + ).draw(to)), + thunk(move|to: &mut Tui|w_exact(5, either(playing, fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), - fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",)))) - )) + fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",))) + ).draw(to)), ) ) } @@ -494,34 +494,36 @@ pub fn draw_info (sample: Option<&Arc>>) -> impl Draw + use< when(sample.is_some(), thunk(move|to: &mut Tui|{ let sample = sample.unwrap().read().unwrap(); let theme = sample.color; - to.place(&east!( + east!( field_h(theme, "Name", format!("{:<10}", sample.name.clone())), field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())), field_h(theme, "Start", format!("{:<8}", sample.start)), field_h(theme, "End", format!("{:<8}", sample.end)), field_h(theme, "Trans", "0"), field_h(theme, "Gain", format!("{}", sample.gain)), - )) + ).draw(to) })) } pub fn draw_info_v (sample: Option<&Arc>>) -> impl Draw + use<'_> { - either(sample.is_some(), thunk(move|to: &mut Tui|{ + let a = thunk(move|to: &mut Tui|{ let sample = sample.unwrap().read().unwrap(); let theme = sample.color; - to.place(&w_exact(20, south!( + w_exact(20, south!( w_full(origin_w(field_h(theme, "Name ", format!("{:<10}", sample.name.clone())))), w_full(origin_w(field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())))), w_full(origin_w(field_h(theme, "Start ", format!("{:<8}", sample.start)))), w_full(origin_w(field_h(theme, "End ", format!("{:<8}", sample.end)))), w_full(origin_w(field_h(theme, "Trans ", "0"))), w_full(origin_w(field_h(theme, "Gain ", format!("{}", sample.gain)))), - ))) - }), thunk(|to: &mut Tui|to.place(&fg(Red, south!( + )).draw(to) + }); + let b = thunk(|to: &mut Tui|fg(Red, south!( bold(true, "× No sample."), "[r] record", "[Shift-F9] import", - ))))) + ))); + either(sample.is_some(), a, b) } pub fn draw_status (sample: Option<&Arc>>) -> impl Draw { @@ -726,47 +728,47 @@ impl<'a> Namespace<'a, AppCommand> for App { "cancel" => AppCommand::Cancel, }); } -impl Understand for App { - fn understand_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { - app_understand_expr(self, to, lang) +impl Interpret for App { + fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { + app_interpret_expr(self, to, lang) } - fn understand_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { - app_understand_word(self, to, lang) + fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { + app_interpret_word(self, to, lang) } } -fn app_understand_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Usually<()> { +fn app_interpret_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Usually<()> { if eval_view(state, to, lang)? || eval_view_tui(state, to, lang)? { Ok(()) } else { - Err(format!("App::understand_expr: unexpected: {lang:?}").into()) + Err(format!("App::interpret_expr: unexpected: {lang:?}").into()) } } -fn app_understand_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually<()> { +fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually<()> { let mut frags = dsl.src()?.unwrap().split("/"); match frags.next() { - Some(":logo") => to.place(&view_logo()), - Some(":status") => to.place(&h_exact(1, "TODO: Status Bar")), + Some(":logo") => view_logo().draw(to), + Some(":status") => h_exact(1, "TODO: Status Bar").draw(to), Some(":meters") => match frags.next() { - Some("input") => to.place(&bg(Rgb(30, 30, 30), h_full(origin_s("Input Meters")))), - Some("output") => to.place(&bg(Rgb(30, 30, 30), h_full(origin_s("Output Meters")))), + Some("input") => bg(Rgb(30, 30, 30), h_full(origin_s("Input Meters"))).draw(to), + Some("output") => bg(Rgb(30, 30, 30), h_full(origin_s("Output Meters"))).draw(to), _ => panic!() }, Some(":tracks") => match frags.next() { - None => to.place(&"TODO tracks"), - Some("names") => to.place(&state.project.view_track_names(state.color.clone())),//bg(Rgb(40, 40, 40), w_full(origin_w("Track Names")))), - Some("inputs") => to.place(&bg(Rgb(40, 40, 40), w_full(origin_w("Track Inputs")))), - Some("devices") => to.place(&bg(Rgb(40, 40, 40), w_full(origin_w("Track Devices")))), - Some("outputs") => to.place(&bg(Rgb(40, 40, 40), w_full(origin_w("Track Outputs")))), + None => "TODO tracks".draw(to), + Some("names") => state.project.view_track_names(state.color.clone()).draw(to),//bg(Rgb(40, 40, 40), w_full(origin_w("Track Names")))), + Some("inputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Inputs"))).draw(to), + Some("devices") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Devices"))).draw(to), + Some("outputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Outputs"))).draw(to), _ => panic!() }, Some(":scenes") => match frags.next() { - None => to.place(&"TODO scenes"), - Some(":scenes/names") => to.place(&"TODO Scene Names"), + None => "TODO scenes".draw(to), + Some(":scenes/names") => "TODO Scene Names".draw(to), _ => panic!() }, - Some(":editor") => to.place(&"TODO Editor"), + Some(":editor") => "TODO Editor".draw(to), Some(":dialog") => match frags.next() { - Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &state.dialog { + Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog { let items = items.clone(); let selected = selected; Some(wh_full(thunk(move|to: &mut Tui|{ @@ -781,10 +783,10 @@ fn app_understand_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usu }))) } else { None - }), - _ => unimplemented!("App::understand_word: {dsl:?} ({frags:?})"), + }.draw(to), + _ => unimplemented!("App::interpret_word: {dsl:?} ({frags:?})"), }, - Some(":templates") => to.place(&{ + Some(":templates") => { let modes = state.config.modes.clone(); let height = (modes.read().unwrap().len() * 2) as u16; h_exact(height, w_min(30, thunk(move |to: &mut Tui|{ @@ -797,21 +799,21 @@ fn app_understand_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usu let field_name = w_full(origin_w(fg(fg1, name))); let field_id = w_full(origin_e(fg(fg2, id))); let field_info = w_full(origin_w(info)); - to.place(&y_push((2 * index) as u16, + y_push((2 * index) as u16, h_exact(2, w_full(bg(bg, south( - above(field_name, field_id), field_info)))))); + above(field_name, field_id), field_info))))).draw(to); } }))) - }), - Some(":sessions") => to.place(&h_exact(6, w_min(30, thunk(|to: &mut Tui|{ + }.draw(to), + Some(":sessions") => h_exact(6, w_min(30, thunk(|to: &mut Tui|{ let fg = Rgb(224, 192, 128); for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; - to.place(&y_push((2 * index) as u16, - &h_exact(2, w_full(bg(bg, origin_w(fg(fg, name))))))); + y_push((2 * index) as u16, + &h_exact(2, w_full(bg(bg, origin_w(fg(fg, name)))))).draw(to); } - })))), - Some(":browse/title") => to.place(&w_full(origin_w(field_v(ItemColor::default(), + }))).draw(to), + Some(":browse/title") => w_full(origin_w(field_v(ItemColor::default(), match state.dialog.browser_target().unwrap() { BrowseTarget::SaveProject => "Save project:", BrowseTarget::LoadProject => "Load project:", @@ -819,24 +821,24 @@ fn app_understand_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usu BrowseTarget::ExportSample(_) => "Export sample:", BrowseTarget::ImportClip(_) => "Import clip:", BrowseTarget::ExportClip(_) => "Export clip:", - }, w_shrink(3, h_exact(1, fg(g(96), x_repeat("🭻")))))))), + }, w_shrink(3, h_exact(1, fg(g(96), x_repeat("🭻"))))))).draw(to), Some(":device") => { let selected = state.dialog.device_kind().unwrap(); - to.place(&south(bold(true, "Add device"), iter_south( + south(bold(true, "Add device"), iter_south( move||device_kinds().iter(), move|_label: &&'static str, i|{ let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; let lb = if i == selected { "[ " } else { " " }; let rb = if i == selected { " ]" } else { " " }; - w_full(bg(bg, east(lb, west(rb, "FIXME device name")))) }))) + w_full(bg(bg, east(lb, west(rb, "FIXME device name")))) })).draw(to) }, - Some(":debug") => to.place(&h_exact(1, format!("[{:?}]", to.area()))), + Some(":debug") => h_exact(1, format!("[{:?}]", to.area())).draw(to), Some(_) => { let views = state.config.views.read().unwrap(); if let Some(dsl) = views.get(dsl.src()?.unwrap()) { let dsl = dsl.clone(); std::mem::drop(views); - state.understand(to, &dsl)? + state.interpret(to, &dsl)? } else { unimplemented!("{dsl:?}"); } @@ -968,10 +970,10 @@ impl App { impl Draw for App { fn draw (self, to: &mut Tui) -> Usually> { if let Some(e) = self.error.read().unwrap().as_ref() { - to.place_at(to.area().into(), e.as_ref()); + to.show(to.area().into(), e.as_ref()); } for (index, dsl) in self.mode.view.iter().enumerate() { - if let Err(e) = self.understand(to, dsl) { + if let Err(e) = self.interpret(to, dsl) { *self.error.write().unwrap() = Some(format!("view #{index}: {e}").into()); break; } @@ -986,8 +988,8 @@ primitive!(u16: try_to_u16); primitive!(usize: try_to_usize); primitive!(isize: try_to_isize); -fn field_h (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { +fn field_h (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { } -fn field_v (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { +fn field_v (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { } diff --git a/tengri b/tengri index c9b9ff15..2dc501c1 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit c9b9ff15191ec3a37e8da35f382ce0d4af74d0d8 +Subproject commit 2dc501c184c0404a50b220a9fe9f756b87e4635b From 9283c54eec7a576387c7e7d36b6ac1e9266ae630 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Tue, 16 Jun 2026 16:52:56 +0300 Subject: [PATCH 04/31] 183e... --- src/arrange.rs | 56 ++++++++++++++++++++++++++++---------------------- src/browse.rs | 23 +++++++-------------- src/tek.rs | 10 ++++++--- tengri | 2 +- 4 files changed, 47 insertions(+), 44 deletions(-) diff --git a/src/arrange.rs b/src/arrange.rs index de20fbbe..353e4df5 100644 --- a/src/arrange.rs +++ b/src/arrange.rs @@ -470,13 +470,12 @@ pub trait HasTrack: AsRefOpt + AsMutOpt { tracks: impl Fn() -> U + Send + Sync + 'a, callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a ) -> impl Draw + 'a { - iter(tracks, + iter_east(tracks, move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - let width = (x2 - x1) as u16; - iter_east(x1 as u16, width, w_exact(width, fg_bg( + w_exact((x2 - x1) as u16, fg_bg( track.color.lightest.term, track.color.base.term, - callback(index, track))))}) + callback(index, track)))}) } /// Create a new track connecting the [Sequencer] to a [Sampler]. #[cfg(feature = "sampler")] pub fn new_with_sampler ( @@ -530,13 +529,12 @@ pub trait HasTrack: AsRefOpt + AsMutOpt { tracks: impl Fn() -> U + Send + Sync + 'a, callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a ) -> impl Draw + 'a { - origin_x(bg(Reset, iter(tracks, + origin_x(bg(Reset, iter_east(tracks, move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - let width = (x2 - x1) as u16; - iter_east(x1 as u16, width, w_exact(width, fg_bg( + w_exact((x2 - x1) as u16, fg_bg( track.color.lightest.term, track.color.base.term, - callback(index, track))))}))) + callback(index, track))) }))) } #[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt()); #[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt()); @@ -690,33 +688,40 @@ impl Arrangement { h_exact(1, self.view_inputs_header()), thunk(|to: &mut Tui|{ for (index, port) in self.midi_ins().iter().enumerate() { - x_push(index as u16 * 10, h_exact(1, self.view_inputs_row(port))).draw(to) + x_push(index as u16 * 10, h_exact(1, self.view_inputs_row(port))).draw(to); } + todo!() }) ) } #[cfg(feature = "track")] fn view_inputs_header (&self) -> impl Draw + '_ { east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), - west(w_exact(4, button_2("I", "+", false)), thunk(move|to: &mut Tui|for (_index, track, x1, _x2) in self.tracks_with_sizes() { - #[cfg(feature = "track")] - x_push(x1 as u16, bg(track.color.dark.term, origin_w(w_exact(track.width as u16, east!( - either(track.sequencer.monitoring, fg(Green, "mon "), "mon "), - either(track.sequencer.recording, fg(Red, "rec "), "rec "), - either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "), - ))))).draw(to) + west(w_exact(4, button_2("I", "+", false)), thunk(move|to: &mut Tui|{ + for (_index, track, x1, _x2) in self.tracks_with_sizes() { + #[cfg(feature = "track")] + x_push(x1 as u16, bg(track.color.dark.term, origin_w(w_exact(track.width as u16, east!( + either(track.sequencer.monitoring, fg(Green, "mon "), "mon "), + either(track.sequencer.recording, fg(Red, "rec "), "rec "), + either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "), + ))))).draw(to); + } + todo!() }))) } #[cfg(feature = "track")] fn view_inputs_row (&self, port: &MidiInput) -> impl Draw { east(w_exact(20, origin_w(east(" ● ", bold(true, fg(Rgb(255,255,255), port.port_name()))))), - west(w_exact(4, ()), thunk(move|to: &mut Tui|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { - #[cfg(feature = "track")] - bg(track.color.darker.term, origin_w(w_exact(track.width as u16, east!( - either(track.sequencer.monitoring, fg(Green, " ● "), " · "), - either(track.sequencer.recording, fg(Red, " ● "), " · "), - either(track.sequencer.overdub, fg(Yellow, " ● "), " · "), - )))).draw(to) + west(w_exact(4, ()), thunk(move|to: &mut Tui|{ + for (_index, track, _x1, _x2) in self.tracks_with_sizes() { + #[cfg(feature = "track")] + bg(track.color.darker.term, origin_w(w_exact(track.width as u16, east!( + either(track.sequencer.monitoring, fg(Green, " ● "), " · "), + either(track.sequencer.recording, fg(Red, " ● "), " · "), + either(track.sequencer.overdub, fg(Yellow, " ● "), " · "), + )))).draw(to); + } + todo!() }))) } @@ -745,6 +750,7 @@ impl Arrangement { .draw(to); } } + todo!(); })))) ); @@ -767,9 +773,11 @@ impl Arrangement { h_exact(1, w_full("")).draw(to); } } + todo!() }) - ).draw(to) + ).draw(to); } + todo!() }) ))) )) diff --git a/src/browse.rs b/src/browse.rs index 658f32ba..f539b17d 100644 --- a/src/browse.rs +++ b/src/browse.rs @@ -215,15 +215,6 @@ impl ClipLength { pub fn ticks (&self) -> usize { self.pulses % self.ppq } - pub fn bars_string (&self) -> Arc { - format!("{}", self.bars()).into() - } - pub fn beats_string (&self) -> Arc { - format!("{}", self.beats()).into() - } - pub fn ticks_string (&self) -> Arc { - format!("{:>02}", self.ticks()).into() - } } impl Pool { @@ -272,14 +263,14 @@ impl<'a> PoolView<'a> { impl ClipLength { fn tui (&self) -> impl Draw { use ClipLengthFocus::*; - let bars = ||self.bars_string(); - let beats = ||self.beats_string(); - let ticks = ||self.ticks_string(); + let bars = format!("{}", self.bars()); + let beats = format!("{}", self.beats()); + let ticks = format!("{:>02}", self.ticks()); match self.focus { - None => east!(" ", bars(), ".", beats(), ".", ticks()), - Some(Bar) => east!("[", bars(), "]", beats(), ".", ticks()), - Some(Beat) => east!(" ", bars(), "[", beats(), "]", ticks()), - Some(Tick) => east!(" ", bars(), ".", beats(), "[", ticks()), + None => east!(" ", bars, ".", beats, ".", ticks), + Some(Bar) => east!("[", bars, "]", beats, ".", ticks), + Some(Beat) => east!(" ", bars, "[", beats, "]", ticks), + Some(Tick) => east!(" ", bars, ".", beats, "[", ticks), } } } diff --git a/src/tek.rs b/src/tek.rs index fb27be6f..bd9f08f0 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -25,6 +25,8 @@ } } +/// TODO: Preserve the generic passthru syntax; +/// remove this macro (only used twice) and potentially the trait. #[macro_export] macro_rules! impl_has_clips { (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { @@ -139,8 +141,8 @@ pub(crate) use tengri::{ })); // TODO: Sync these timings with main clock, so that things // "accidentally" fall on the beat in overload conditions. - let keyboard = run_tui_in(&exit, &state, Duration::from_millis(100))?; - let terminal = run_tui_out(&exit, &state, Duration::from_millis(10))?; + let keyboard = tui_keyboard(&exit, &state, Duration::from_millis(100))?; + let terminal = tui_output(&exit, &state, Duration::from_millis(10))?; (keyboard, terminal) }) }) @@ -518,11 +520,13 @@ pub fn draw_info_v (sample: Option<&Arc>>) -> impl Draw + us w_full(origin_w(field_h(theme, "Gain ", format!("{}", sample.gain)))), )).draw(to) }); + let b = thunk(|to: &mut Tui|fg(Red, south!( bold(true, "× No sample."), "[r] record", "[Shift-F9] import", ))); + either(sample.is_some(), a, b) } @@ -547,7 +551,7 @@ pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, po let iter = move||ports.iter(); let names = iter_south(iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name())))); let field = field_v(theme, title, names); - wh_exact(Some(20), Some(1 + ins), frame.enclose(wh_exact(Some(20), Some(1 + ins), field))) + wh_exact(Some(20), Some(1 + ins), border(true, frame, wh_exact(Some(20), Some(1 + ins), field))) } pub fn io_ports <'a, T: PortsSizes<'a>> ( diff --git a/tengri b/tengri index 2dc501c1..29035d0b 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 2dc501c184c0404a50b220a9fe9f756b87e4635b +Subproject commit 29035d0b36221f43902ead396a5e94c423f0581d From b44dc635ef61a9e8053acf2dbd24b3c5c23211ab Mon Sep 17 00:00:00 2001 From: i do not exist Date: Fri, 19 Jun 2026 10:17:17 +0300 Subject: [PATCH 05/31] 145e... --- src/arrange.rs | 13 ++++++------- src/tek.rs | 53 +++++++++++++++++++++++--------------------------- tengri | 2 +- 3 files changed, 31 insertions(+), 37 deletions(-) diff --git a/src/arrange.rs b/src/arrange.rs index 353e4df5..b6d80a71 100644 --- a/src/arrange.rs +++ b/src/arrange.rs @@ -790,18 +790,17 @@ impl Arrangement { view_track_row_section(theme, button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), button_2("D", "+", false), - thunk(move|to: &mut Tui|for (index, track, _x1, _x2) in self.tracks_with_sizes() { - wh_exact(track_width(index, track), h + 1, - bg(track.color.dark.term, origin_nw(iter_south(2, move||0..h, - |_, _index|wh_exact(track.width as u16, 2, + iter(||self.tracks_with_sizes(), move|(_, track, _x1, _x2), index| { + wh_exact(Some(track_width(index, track)), Some(h + 1), + bg(track.color.dark.term, origin_nw(iter_south(move||0..h, + |_, _index|wh_exact(Some(track.width as u16), Some(2), fg_bg( ItemTheme::G[32].lightest.term, ItemTheme::G[32].dark.term, origin_nw(format!(" · {}", "--")) ) - ))) - ) - ).draw(to); + ))))); + todo!() }) ) } diff --git a/src/tek.rs b/src/tek.rs index bd9f08f0..b0e709cf 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -404,20 +404,14 @@ pub fn view_transport ( /// let _ = tek::view_status(None, x.clone(), x.clone(), x.clone()); /// let _ = tek::view_status(Some("".into()), x.clone(), x.clone(), x.clone()); /// ``` -pub fn view_status ( - sel: Option>, - sr: Arc>, - buf: Arc>, - lat: Arc>, -) -> impl Draw { +pub fn view_status (sel: Option>, sr: &str, buf: &str, lat: &str) -> impl Draw { let theme = ItemTheme::G[96]; + let sr = field_h(theme, "SR", sr); + let buf = field_h(theme, "Buf", buf); + let lat = field_h(theme, "Lat", lat); bg(Black, east!(above( wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))), - wh_full(origin_e(east!( - field_h(theme, "SR", sr), - field_h(theme, "Buf", buf), - field_h(theme, "Lat", lat), - ))) + wh_full(origin_e(east!(sr, buf, lat))), ))) } @@ -466,24 +460,25 @@ pub fn view_wrap (bg: Color, fg: Color, content: impl Draw) -> impl Draw (label: &'a str, value: f32) -> impl Draw + 'a { - south!( - field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)), - wh_exact(if value >= 0.0 { &"13" } - else if value >= -1.0 { &"12" } - else if value >= -2.0 { &"11" } - else if value >= -3.0 { &"10" } - else if value >= -4.0 { &"9" } - else if value >= -6.0 { &"8" } - else if value >= -9.0 { &"7" } - else if value >= -12.0 { &"6" } - else if value >= -15.0 { &"5" } - else if value >= -20.0 { &"4" } - else if value >= -25.0 { &"3" } - else if value >= -30.0 { &"2" } - else if value >= -40.0 { &"1" } - else { 0 }, 1, bg(if value >= 0.0 { Red } - else if value >= -3.0 { Yellow } - else { Green }, ()))) + let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)); + let w = if value >= 0.0 { 13 } + else if value >= -1.0 { 12 } + else if value >= -2.0 { 11 } + else if value >= -3.0 { 10 } + else if value >= -4.0 { 9 } + else if value >= -6.0 { 8 } + else if value >= -9.0 { 7 } + else if value >= -12.0 { 6 } + else if value >= -15.0 { 5 } + else if value >= -20.0 { 4 } + else if value >= -25.0 { 3 } + else if value >= -30.0 { 2 } + else if value >= -40.0 { 1 } + else { 0 }; + let c = if value >= 0.0 { Red } + else if value >= -3.0 { Yellow } + else { Green }; + south!(f, wh_exact(Some(w), Some(1), bg(c, ()))) } pub fn view_meters (values: &[f32;2]) -> impl Draw + use<'_> { diff --git a/tengri b/tengri index 29035d0b..9c5bfafb 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 29035d0b36221f43902ead396a5e94c423f0581d +Subproject commit 9c5bfafb7a4163022c5a96c3a25d0aca258a28d2 From 80fe958476fa24374c2ea841549a3bf0287945d7 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Fri, 19 Jun 2026 20:23:53 +0300 Subject: [PATCH 06/31] 109e... --- src/arrange.rs | 25 +++++++++++++------------ src/browse.rs | 18 ++++++++++-------- src/editor.rs | 32 +++++++++++++++++++++++--------- src/mix.rs | 2 +- src/sample.rs | 11 +++++++---- src/sequence.rs | 12 ++++++++---- src/tek.rs | 12 ++++++------ tengri | 2 +- 8 files changed, 69 insertions(+), 45 deletions(-) diff --git a/src/arrange.rs b/src/arrange.rs index b6d80a71..33b3aeac 100644 --- a/src/arrange.rs +++ b/src/arrange.rs @@ -787,21 +787,22 @@ impl Arrangement { for track in self.tracks().iter() { h = h.max(track.devices.len() as u16 * 2); } + let tracks = ||self.tracks_with_sizes(); + let track = move|(_, track, _x1, _x2), index| wh_exact( + Some(track_width(index, track)), + Some(h + 1), + bg(track.color.dark.term, origin_nw(iter_south(move||0..h, + |_, _index|wh_exact(Some(track.width as u16), Some(2), + fg_bg( + ItemTheme::G[32].lightest.term, + ItemTheme::G[32].dark.term, + origin_nw(format!(" · {}", "--")) + ) + ))))); view_track_row_section(theme, button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), button_2("D", "+", false), - iter(||self.tracks_with_sizes(), move|(_, track, _x1, _x2), index| { - wh_exact(Some(track_width(index, track)), Some(h + 1), - bg(track.color.dark.term, origin_nw(iter_south(move||0..h, - |_, _index|wh_exact(Some(track.width as u16), Some(2), - fg_bg( - ItemTheme::G[32].lightest.term, - ItemTheme::G[32].dark.term, - origin_nw(format!(" · {}", "--")) - ) - ))))); - todo!() - }) + iter(tracks, track) ) } diff --git a/src/browse.rs b/src/browse.rs index f539b17d..773bfd86 100644 --- a/src/browse.rs +++ b/src/browse.rs @@ -251,12 +251,12 @@ impl<'a> PoolView<'a> { let f = color.lightest.term; let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; let length = if false { String::default() } else { format!("{length} ") }; - h_exact(1, iter_south(item_offset, item_height, bg(b, below!( - w_full(origin_w(fg(fg, bold(selected, name)))), - w_full(origin_e(fg(fg, bold(selected, length)))), - w_full(origin_w(When::new(selected, bold(true, fg(g(255), "▶"))))), - w_full(origin_e(When::new(selected, bold(true, fg(g(255), "◀"))))), - )))) + h_exact(1, bg(b, below!( + w_full(origin_w(fg(f, bold(selected, name)))), + w_full(origin_e(fg(f, bold(selected, length)))), + w_full(origin_w(when(selected, bold(true, fg(g(255), "▶"))))), + w_full(origin_e(when(selected, bold(true, fg(g(255), "◀"))))), + ))) })))) } } @@ -312,12 +312,14 @@ impl Browse { } impl Browse { fn tui (&self) -> impl Draw { - iter_south(1, ||EntriesIterator { + let item = |entry, _index|w_full(origin_w(entry)); + let iterate = ||EntriesIterator { offset: 0, index: 0, length: self.dirs.len() + self.files.len(), browser: self, - }, |entry, _index|w_full(origin_w(entry))) + }; + iter_south_fixed(1, iterate, item) } } impl<'a> Iterator for EntriesIterator<'a> { diff --git a/src/editor.rs b/src/editor.rs index c792c39f..4720a409 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -525,6 +525,7 @@ impl PianoHorizontal { } impl PianoHorizontal { + /// Draw the piano roll background. /// /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ @@ -559,6 +560,7 @@ impl PianoHorizontal { } } } + /// Draw the piano roll foreground. /// /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ @@ -596,14 +598,16 @@ impl PianoHorizontal { } } + fn notes (&self) -> impl Draw { let time_start = self.get_time_start(); let note_lo = self.get_note_lo(); let note_hi = self.get_note_hi(); let buffer = self.buffer.clone(); - Thunk::new(move|to: &mut Tui|{ + thunk(move|to: &mut Tui|{ + let xywh = to.area().into(); + let XYWH(x0, y0, w, _h) = xywh; let source = buffer.read().unwrap(); - let XYWH(x0, y0, w, _h) = to.area(); //if h as usize != note_axis { //panic!("area height mismatch: {h} <> {note_axis}"); //} @@ -618,15 +622,17 @@ impl PianoHorizontal { let is_in_y = source_y < source.height; if is_in_x && is_in_y { if let Some(source_cell) = source.get(source_x, source_y) { - if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { + if let Some(cell) = to.0.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { *cell = source_cell.clone(); } } } } } + Ok(xywh) }) } + fn cursor (&self) -> impl Draw { let note_hi = self.get_note_hi(); let note_lo = self.get_note_lo(); @@ -636,8 +642,9 @@ impl PianoHorizontal { let time_start = self.get_time_start(); let time_zoom = self.get_time_zoom(); let style = Some(Style::default().fg(self.color.lightest.term)); - Thunk::new(move|to: &mut Tui|{ - let XYWH(x0, y0, w, _) = to.area(); + thunk(move|to: &mut Tui|{ + let xywh = to.area().into(); + let XYWH(x0, y0, w, _h) = xywh; for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { if note == note_pos { for x in 0..w { @@ -656,8 +663,10 @@ impl PianoHorizontal { break } } + Ok(xywh) }) } + fn keys (&self) -> impl Draw { let state = self; let color = state.color; @@ -667,8 +676,9 @@ impl PianoHorizontal { let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); let off_style = Some(Style::default().fg(g(255))); let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.term).bold()); - h_full(w_exact(self.keys_width, Thunk::new(move|to: &mut Tui|{ - let XYWH(x, y0, _w, _h) = to.area(); + h_full(w_exact(self.keys_width, thunk(move|to: &mut Tui|{ + let xywh = to.area().into(); + let XYWH(x, y0, _w, _h) = xywh; for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { to.blit(&to_key(note), x, screen_y, key_style); if note > 127 { @@ -680,11 +690,14 @@ impl PianoHorizontal { to.blit(¬e_pitch_to_name(note), x, screen_y, off_style) }; } + Ok(xywh) }))) } + fn timeline (&self) -> impl Draw + '_ { - w_full(h_exact(1, Thunk::new(move|to: &mut Tui|{ - let XYWH(x, y, w, _h) = to.area(); + w_full(h_exact(1, thunk(move|to: &mut Tui|{ + let xywh = to.area().into(); + let XYWH(x, y, w, _h) = xywh; let style = Some(Style::default().dim()); let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { @@ -693,6 +706,7 @@ impl PianoHorizontal { to.blit(&"|", screen_x, y, style); } } + Ok(xywh) }))) } } diff --git a/src/mix.rs b/src/mix.rs index 64afce51..74db8388 100644 --- a/src/mix.rs +++ b/src/mix.rs @@ -67,7 +67,7 @@ pub enum MixingMode { } fn draw_meters (meters: &[f32]) -> impl Draw + use<'_> { - Tui::bg(Black, w_exact(2, iter_east(1, ||meters.iter(), |value, _index|{ + bg(Black, w_exact(2, iter_east_fixed(1, ||meters.iter(), |value, _index|{ h_full(RmsMeter(*value)) }))) } diff --git a/src/sample.rs b/src/sample.rs index 43763aa3..6b6fa4a9 100644 --- a/src/sample.rs +++ b/src/sample.rs @@ -596,8 +596,9 @@ fn draw_list_item (sample: &Option>>) -> String { fn draw_viewer (sample: Option<&Arc>>) -> impl Draw + use<'_> { let min_db = -64.0; - Thunk::new(move|to: &mut Tui|{ - let XYWH(x, y, width, height) = to.area(); + thunk(move|to: &mut Tui|{ + let xywh = to.area().into(); + let XYWH(x, y, width, height) = xywh; let area = Rect { x, y, width, height }; if let Some(sample) = &sample { let sample = sample.read().unwrap(); @@ -631,7 +632,8 @@ fn draw_viewer (sample: Option<&Arc>>) -> impl Draw + use<'_ //height as f64 / 2.0, //text.red() //); - }).render(area, &mut to.1); + }) + .render(area, to.as_mut()); } else { Canvas::default() .x_bounds([0.0, width as f64]) @@ -644,8 +646,9 @@ fn draw_viewer (sample: Option<&Arc>>) -> impl Draw + use<'_ //text.red() //); }) - .render(area, &mut to.1); + .render(area, to.as_mut()); } + Ok(xywh) }) } diff --git a/src/sequence.rs b/src/sequence.rs index fe15f60f..3e280965 100644 --- a/src/sequence.rs +++ b/src/sequence.rs @@ -118,12 +118,16 @@ pub trait HasPlayClip: HasClock { let MidiClip { ref name, color, .. } = *clip.read().unwrap(); (name.clone(), color) } else { - ("".into(), Tui::g(64).into()) + ("".into(), ItemTheme::G[64].into()) }; - let time: String = self.pulses_since_start_looped() + field_v(color, "Now:", format!("{} {}", self.play_status_time(), name)) + } + + fn play_status_time (&self) -> String { + self.pulses_since_start_looped() .map(|(times, time)|format!("{:>3}x {:>}", times+1.0, self.clock().timebase.format_beats_1(time))) - .unwrap_or_else(||String::from(" ")).into(); - field_v(color, "Now:", format!("{} {}", time, name)) + .unwrap_or_else(||String::from(" ")) + .into() } fn next_status (&self) -> impl Draw { diff --git a/src/tek.rs b/src/tek.rs index b0e709cf..7d786348 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -368,7 +368,7 @@ def_sizes_iter!(TracksSizes => Track); /// let _ = tek::view_logo(); /// ``` pub fn view_logo () -> impl Draw { - wh_exact(32, 7, bold(true, fg(Rgb(240,200,180), south!{ + wh_exact(Some(32), Some(7), bold(true, fg(Rgb(240, 200, 180), south!{ h_exact(1, ""), h_exact(1, ""), h_exact(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"), @@ -384,9 +384,9 @@ pub fn view_logo () -> impl Draw { /// ``` pub fn view_transport ( play: bool, - bpm: Arc>, - beat: Arc>, - time: Arc>, + bpm: &Arc>, + beat: &Arc>, + time: &Arc>, ) -> impl Draw { let theme = ItemTheme::G[96]; bg(Black, east!(above( @@ -788,7 +788,7 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua Some(":templates") => { let modes = state.config.modes.clone(); let height = (modes.read().unwrap().len() * 2) as u16; - h_exact(height, w_min(30, thunk(move |to: &mut Tui|{ + h_exact(height, w_min(Some(30), thunk(move |to: &mut Tui|{ for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); @@ -804,7 +804,7 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua } }))) }.draw(to), - Some(":sessions") => h_exact(6, w_min(30, thunk(|to: &mut Tui|{ + Some(":sessions") => h_exact(Some(6), w_min(Some(30), thunk(|to: &mut Tui|{ let fg = Rgb(224, 192, 128); for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; diff --git a/tengri b/tengri index 9c5bfafb..e0781571 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 9c5bfafb7a4163022c5a96c3a25d0aca258a28d2 +Subproject commit e0781571c42cb72e7445a2adbdb7a99e37600562 From b130471f12d547a4fea83d16c34210aa8c49ae67 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Fri, 19 Jun 2026 20:30:00 +0300 Subject: [PATCH 07/31] 79e... --- src/tek.rs | 39 +++++++++++++-------------------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/src/tek.rs b/src/tek.rs index 7d786348..cf8860b4 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -382,12 +382,7 @@ pub fn view_logo () -> impl Draw { /// let _ = tek::view_transport(true, x.clone(), x.clone(), x.clone()); /// let _ = tek::view_transport(false, x.clone(), x.clone(), x.clone()); /// ``` -pub fn view_transport ( - play: bool, - bpm: &Arc>, - beat: &Arc>, - time: &Arc>, -) -> impl Draw { +pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw { let theme = ItemTheme::G[96]; bg(Black, east!(above( wh_full(origin_w(button_play_pause(play))), @@ -404,7 +399,7 @@ pub fn view_transport ( /// let _ = tek::view_status(None, x.clone(), x.clone(), x.clone()); /// let _ = tek::view_status(Some("".into()), x.clone(), x.clone(), x.clone()); /// ``` -pub fn view_status (sel: Option>, sr: &str, buf: &str, lat: &str) -> impl Draw { +pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw { let theme = ItemTheme::G[96]; let sr = field_h(theme, "SR", sr); let buf = field_h(theme, "Buf", buf); @@ -520,7 +515,7 @@ pub fn draw_info_v (sample: Option<&Arc>>) -> impl Draw + us bold(true, "× No sample."), "[r] record", "[Shift-F9] import", - ))); + )).draw(to)); either(sample.is_some(), a, b) } @@ -554,30 +549,22 @@ pub fn io_ports <'a, T: PortsSizes<'a>> ( ) -> impl Draw + 'a { iter(items, move|( _index, name, connections, y, y2 - ): (usize, &'a Arc, &'a [Connect], usize, usize), _| - iter_south( - y as u16, - (y2-y) as u16, - south( - h_full(bold(true, fg_bg(fg, bg, origin_w(east(" 󰣲 ", name))))), - iter( - ||connections.iter(), - move|connect: &'a Connect, index|iter_south( - index as u16, - 1, - h_full(origin_w(bold(false, fg_bg(fg, bg, &connect.info)))) - ) - ) - ) - ) - ) + ): (usize, &'a Arc, &'a [Connect], usize, usize), _| { + let conns = ||connections.iter(); + let conn = move|connect: &'a Connect, index|iter_south_fixed( + index as u16, 1, h_full(origin_w(bold(false, fg_bg(fg, bg, &connect.info)))) + ); + iter_south(y as u16, (y2-y) as u16, + south(h_full(bold(true, fg_bg(fg, bg, origin_w(east(" 󰣲 ", name))))), + iter(conns, conn))) + }) } /// CLI banner. pub(crate) const HEADER: &'static str = r#" ~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ - █ term█▀ █▀▀▄ ~ v0.4.0, 2026 winter (or is it) ~ + █ █▀ █▀▀▄ ~ v0.4.0, 2026 winter (or is it) ~ ~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; /// Total state From 4ec2165e3df2bb4f944e403b43bfaad3ee3cf624 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Sat, 20 Jun 2026 21:54:31 +0300 Subject: [PATCH 08/31] 54e... --- src/arrange.rs | 8 ++++---- src/tek.rs | 50 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/arrange.rs b/src/arrange.rs index 33b3aeac..4880e9ec 100644 --- a/src/arrange.rs +++ b/src/arrange.rs @@ -184,9 +184,9 @@ pub trait ClipsView: TracksView + ScenesView { pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { - fn tracks_width_available (&self) -> u16 { - (self.size.width() as u16).saturating_sub(40) - } + //fn tracks_width_available (&self) -> u16 { + //(self.size.width() as u16).saturating_sub(40) + //} /// Iterate over tracks with their corresponding sizes. fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { @@ -854,7 +854,7 @@ impl ScenesView for Arrangement { (self.size.w() as u16 * 2 / 10).max(20) } fn w_mid (&self) -> u16 { - (self.size.width() as u16).saturating_sub(2 * self.w_side()).max(40) + (self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40) } } impl HasClipsSize for Arrangement { diff --git a/src/tek.rs b/src/tek.rs index cf8860b4..f2f63594 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -547,16 +547,13 @@ pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, po pub fn io_ports <'a, T: PortsSizes<'a>> ( fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a ) -> impl Draw + 'a { - iter(items, move|( - _index, name, connections, y, y2 - ): (usize, &'a Arc, &'a [Connect], usize, usize), _| { - let conns = ||connections.iter(); - let conn = move|connect: &'a Connect, index|iter_south_fixed( - index as u16, 1, h_full(origin_w(bold(false, fg_bg(fg, bg, &connect.info)))) - ); - iter_south(y as u16, (y2-y) as u16, + type Item<'a> = (usize, &'a Arc, &'a [Connect], usize, usize); + iter(items, move|(_index, name, connections, y, y2): Item<'a>, _| { + y_push(y as u16, h_exact((y2-y) as u16, south(h_full(bold(true, fg_bg(fg, bg, origin_w(east(" 󰣲 ", name))))), - iter(conns, conn))) + iter(||connections.iter(), move|connect: &'a Connect, index|y_push( + index as u16, h_exact(1, origin_w(bold(false, fg_bg(fg, bg, &connect.info)))) + ))))) }) } @@ -732,13 +729,17 @@ fn app_interpret_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Usu fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually<()> { let mut frags = dsl.src()?.unwrap().split("/"); match frags.next() { + Some(":logo") => view_logo().draw(to), + Some(":status") => h_exact(1, "TODO: Status Bar").draw(to), + Some(":meters") => match frags.next() { Some("input") => bg(Rgb(30, 30, 30), h_full(origin_s("Input Meters"))).draw(to), Some("output") => bg(Rgb(30, 30, 30), h_full(origin_s("Output Meters"))).draw(to), _ => panic!() }, + Some(":tracks") => match frags.next() { None => "TODO tracks".draw(to), Some("names") => state.project.view_track_names(state.color.clone()).draw(to),//bg(Rgb(40, 40, 40), w_full(origin_w("Track Names")))), @@ -747,12 +748,15 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua Some("outputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Outputs"))).draw(to), _ => panic!() }, + Some(":scenes") => match frags.next() { None => "TODO scenes".draw(to), Some(":scenes/names") => "TODO Scene Names".draw(to), _ => panic!() }, + Some(":editor") => "TODO Editor".draw(to), + Some(":dialog") => match frags.next() { Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog { let items = items.clone(); @@ -772,6 +776,7 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua }.draw(to), _ => unimplemented!("App::interpret_word: {dsl:?} ({frags:?})"), }, + Some(":templates") => { let modes = state.config.modes.clone(); let height = (modes.read().unwrap().len() * 2) as u16; @@ -791,7 +796,8 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua } }))) }.draw(to), - Some(":sessions") => h_exact(Some(6), w_min(Some(30), thunk(|to: &mut Tui|{ + + Some(":sessions") => h_exact(Some(6), w_min(30, thunk(|to: &mut Tui|{ let fg = Rgb(224, 192, 128); for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; @@ -799,6 +805,7 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua &h_exact(2, w_full(bg(bg, origin_w(fg(fg, name)))))).draw(to); } }))).draw(to), + Some(":browse/title") => w_full(origin_w(field_v(ItemColor::default(), match state.dialog.browser_target().unwrap() { BrowseTarget::SaveProject => "Save project:", @@ -808,17 +815,20 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua BrowseTarget::ImportClip(_) => "Import clip:", BrowseTarget::ExportClip(_) => "Export clip:", }, w_shrink(3, h_exact(1, fg(g(96), x_repeat("🭻"))))))).draw(to), + Some(":device") => { let selected = state.dialog.device_kind().unwrap(); south(bold(true, "Add device"), iter_south( move||device_kinds().iter(), move|_label: &&'static str, i|{ - let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; - let lb = if i == selected { "[ " } else { " " }; - let rb = if i == selected { " ]" } else { " " }; - w_full(bg(bg, east(lb, west(rb, "FIXME device name")))) })).draw(to) + let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; + let l = if i == selected { "[ " } else { " " }; + let r = if i == selected { " ]" } else { " " }; + w_full(bg(b, east(l, west(r, "FIXME device name")))) })).draw(to) }, + Some(":debug") => h_exact(1, format!("[{:?}]", to.area())).draw(to), + Some(_) => { let views = state.config.views.read().unwrap(); if let Some(dsl) = views.get(dsl.src()?.unwrap()) { @@ -829,6 +839,7 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua unimplemented!("{dsl:?}"); } }, + _ => unreachable!() } Ok(()) @@ -953,10 +964,17 @@ impl App { } } } + +/// The [Draw] implementation for [App] handles the loaded view, +/// which is defined in terms of [dizzle] DSL. +/// +/// If there is an error, the error is displayed. FIXME: overlay it +/// Then, every top-level form of the DSL description is rendered. impl Draw for App { fn draw (self, to: &mut Tui) -> Usually> { + let xywh = to.area().into(); if let Some(e) = self.error.read().unwrap().as_ref() { - to.show(to.area().into(), e.as_ref()); + to.show(xywh, e.as_ref()); } for (index, dsl) in self.mode.view.iter().enumerate() { if let Err(e) = self.interpret(to, dsl) { @@ -964,8 +982,10 @@ impl Draw for App { break; } } + Ok(xywh) } } + impl HasClipsSize for App { fn clips_size (&self) -> &Size { &self.project.size_inner } } impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } impl_default!(AppCommand: Self::Nop); From ae347eeef747488133b5594848a27871172ac102 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Tue, 23 Jun 2026 21:39:12 +0300 Subject: [PATCH 09/31] restruct: 92e --- src/app.rs | 626 ++++++++++++++++++++ src/{ => app}/bind.rs | 2 +- src/{ => app}/cli.rs | 175 +++--- src/app/config.rs | 192 ++++++ src/app/view.rs | 590 ++++++++++++++++++ src/arrange.rs | 904 ---------------------------- src/config.rs | 92 --- src/device.rs | 607 +------------------ src/device/arrange.rs | 123 ++++ src/device/arrange/clip.rs | 90 +++ src/device/arrange/scene.rs | 167 ++++++ src/{ => device/arrange}/select.rs | 0 src/device/arrange/track.rs | 339 +++++++++++ src/device/audio.rs | 0 src/{ => device}/browse.rs | 36 +- src/{ => device}/clock.rs | 0 src/{ => device}/dialog.rs | 1 - src/{ => device}/editor.rs | 6 +- src/{ => device}/menu.rs | 0 src/device/midi.rs | 0 src/{ => device}/mix.rs | 1 + src/{ => device}/plugin.rs | 0 src/device/port.rs | 163 +++++ src/device/port/audio.rs | 103 ++++ src/device/port/connect.rs | 83 +++ src/device/port/midi.rs | 256 ++++++++ src/{ => device}/sample.rs | 4 +- src/{ => device}/sequence.rs | 0 src/mode.rs | 99 ---- src/tek.rs | 918 ++--------------------------- src/view.rs | 10 - 31 files changed, 2924 insertions(+), 2663 deletions(-) create mode 100644 src/app.rs rename src/{ => app}/bind.rs (98%) rename src/{ => app}/cli.rs (63%) create mode 100644 src/app/config.rs create mode 100644 src/app/view.rs delete mode 100644 src/arrange.rs delete mode 100644 src/config.rs create mode 100644 src/device/arrange.rs create mode 100644 src/device/arrange/clip.rs create mode 100644 src/device/arrange/scene.rs rename src/{ => device/arrange}/select.rs (100%) create mode 100644 src/device/arrange/track.rs create mode 100644 src/device/audio.rs rename src/{ => device}/browse.rs (94%) rename src/{ => device}/clock.rs (100%) rename src/{ => device}/dialog.rs (99%) rename src/{ => device}/editor.rs (99%) rename src/{ => device}/menu.rs (100%) create mode 100644 src/device/midi.rs rename src/{ => device}/mix.rs (99%) rename src/{ => device}/plugin.rs (100%) create mode 100644 src/device/port.rs create mode 100644 src/device/port/audio.rs create mode 100644 src/device/port/connect.rs create mode 100644 src/device/port/midi.rs rename src/{ => device}/sample.rs (99%) rename src/{ => device}/sequence.rs (100%) delete mode 100644 src/mode.rs delete mode 100644 src/view.rs diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 00000000..fc86e571 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,626 @@ +use crate::*; +pub mod bind; pub use self::bind::*; +pub mod cli; pub use self::cli::*; +pub mod config; pub use self::config::*; +pub mod view; pub use self::view::*; + +/// Total application state. +/// +/// ``` +/// use tek::{HasTracks, HasScenes, TracksView, ScenesView}; +/// let mut app = tek::App::default(); +/// let _ = app.scene_add(None, None).unwrap(); +/// let _ = app.update_clock(); +/// app.project.editor = Some(Default::default()); +/// //let _: Vec<_> = app.project.inputs_with_sizes().collect(); +/// //let _: Vec<_> = app.project.outputs_with_sizes().collect(); +/// let _: Vec<_> = app.project.tracks_with_sizes().collect(); +/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect(); +/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect(); +/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect(); +/// let _ = app.project.w(); +/// //let _ = app.project.w_sidebar(); +/// //let _ = app.project.w_tracks_area(); +/// let _ = app.project.h(); +/// //let _ = app.project.h_tracks_area(); +/// //let _ = app.project.h_inputs(); +/// //let _ = app.project.h_outputs(); +/// let _ = app.project.h_scenes(); +/// ``` +#[derive(Default, Debug)] pub struct App { + /// Base color. + pub color: ItemTheme, + /// Must not be dropped for the duration of the process + pub jack: Jack<'static>, + /// Display size + pub size: Size, + /// Performance counter + pub perf: PerfModel, + /// Available view modes and input bindings + pub config: Config, + /// Currently selected mode + pub mode: Arc>>, + /// Undo history + pub history: Vec<(AppCommand, Option)>, + /// Dialog overlay + pub dialog: Dialog, + /// Contains all recently created clips. + pub pool: Pool, + /// Contains the currently edited musical arrangement + pub project: Arrangement, + /// Error, if any + pub error: Arc>>> +} + +impl App { + + /// Create a new application instance from a backend, project, config, and mode + /// + /// ``` + /// let jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack"); + /// let proj = tek::Arrangement::default(); + /// let mut conf = tek::Config::default(); + /// conf.add("(mode hello)"); + /// let tek = tek::tek(&jack, proj, conf, "hello"); + /// ``` + pub fn new ( + jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef + ) -> Self { + let mode: &str = mode.as_ref(); + App { + color: ItemTheme::random(), + dialog: Dialog::welcome(), + jack: jack.clone(), + mode: config.get_mode(mode).expect(&format!("failed to find mode '{mode}'")), + config, + project, + ..Default::default() + } + } + + pub fn new_shared ( + jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef + ) -> Arc> { + Arc::new(RwLock::new(App::new(jack, ":menu", config, project))) + } + + pub fn confirm (&mut self) -> Perhaps { + Ok(match &self.dialog { + Dialog::Menu(index, items) => { + let callback = items.0[*index].1.clone(); + callback(self)?; + None + }, + _ => todo!(), + }) + } + + pub fn inc (&mut self, axis: &ControlAxis) -> Perhaps { + Ok(match (&self.dialog, axis) { + (Dialog::None, _) => todo!(), + (Dialog::Menu(_, _), ControlAxis::Y) => + AppCommand::SetDialog { dialog: self.dialog.menu_next() }.act(self)?, + _ => todo!() + }) + } + + pub fn dec (&mut self, axis: &ControlAxis) -> Perhaps { + Ok(match (&self.dialog, axis) { + (Dialog::None, _) => None, + (Dialog::Menu(_, _), ControlAxis::Y) => + AppCommand::SetDialog { dialog: self.dialog.menu_prev() }.act(self)?, + _ => todo!() + }) + } + + pub fn jack_process ( + &mut self, client: &Client, scope: &ProcessScope + ) -> Control { + let t0 = self.perf.get_t0(); + self.clock().update_from_scope(scope).unwrap(); + let midi_in = self.project.midi_input_collect(scope); + if let Some(editor) = &self.editor() { + let mut pitch: Option = None; + for port in midi_in.iter() { + for event in port.iter() { + if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) + = event + { + pitch = Some(key.clone()); + } + } + } + if let Some(pitch) = pitch { + editor.set_note_pos(pitch.as_int() as usize); + } + } + let result = self.project.process_tracks(client, scope); + self.perf.update_from_jack_scope(t0, scope); + result + } + + pub fn jack_event ( + &mut self, event: JackEvent + ) { + use JackEvent::*; + match event { + SampleRate(sr) => { self.clock().timebase.sr.set(sr as f64); }, + PortRegistration(_id, true) => { + //let port = self.jack().port_by_id(id); + //println!("\rport add: {id} {port:?}"); + //println!("\rport add: {id}"); + }, + PortRegistration(_id, false) => { + /*println!("\rport del: {id}")*/ + }, + PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, + PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, + ClientRegistration(_id, true) => {}, + ClientRegistration(_id, false) => {}, + ThreadInit => {}, + XRun => {}, + GraphReorder => {}, + _ => { panic!("{event:?}"); } + } + } + + /// Update memoized render of clock values. + /// ``` + /// tek::App::default().update_clock(); + /// ``` + pub fn update_clock (&self) { + ClockView::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) + } + + /// Set modal dialog. + /// + /// ``` + /// let previous: tek::Dialog = tek::App::default().set_dialog(tek::Dialog::welcome()); + /// ``` + pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog { + std::mem::swap(&mut self.dialog, &mut dialog); + dialog + } + + /// FIXME: generalize. Set picked device in device pick dialog. + /// + /// ``` + /// tek::App::default().device_pick(0); + /// ``` + pub fn device_pick (&mut self, index: usize) { + self.dialog = Dialog::Device(index); + } + + /// FIXME: generalize. Add device to current track. + pub fn add_device (&mut self, index: usize) -> Usually<()> { + match index { + 0 => { + let name = self.jack.with_client(|c|c.name().to_string()); + let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); + let track = self.track().expect("no active track"); + let port = format!("{}/Sampler", &track.name); + let connect = Connect::exact(format!("{name}:{midi}")); + let sampler = if let Ok(sampler) = Sampler::new( + &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] + ) { + self.dialog = Dialog::None; + Device::Sampler(sampler) + } else { + self.dialog = Dialog::Message("Failed to add device.".into()); + return Err("failed to add device".into()) + }; + let track = self.track_mut().expect("no active track"); + track.devices.push(sampler); + Ok(()) + }, + 1 => { + todo!(); + //Ok(()) + }, + _ => unreachable!(), + } + } + + /// Return reference to content browser if open. + /// + /// ``` + /// assert_eq!(tek::App::default().browser(), None); + /// ``` + pub fn browser (&self) -> Option<&Browse> { + if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } + } + + /// Is a MIDI editor currently focused? + /// + /// ``` + /// tek::App::default().editor_focused(); + /// ``` + pub fn editor_focused (&self) -> bool { + false + } + + /// Toggle MIDI editor. + /// + /// ``` + /// tek::App::default().toggle_editor(None); + /// ``` + pub fn toggle_editor (&mut self, value: Option) { + //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); + let value = value.unwrap_or_else(||!self.editor().is_some()); + if value { + // Create new clip in pool when entering empty cell + if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && slot.is_none() + && let Some(track) = self.project.tracks.get_mut(track) + { + let (_index, clip) = self.pool.add_new_clip(); + // autocolor: new clip colors from scene and track color + let color = track.color.base.mix(scene.color.base, 0.5); + clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); + if let Some(editor) = &mut self.project.editor { + editor.set_clip(Some(&clip)); + } + *slot = Some(clip.clone()); + //Some(clip) + } else { + //None + } + } else if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && let Some(clip) = slot.as_mut() + { + // Remove clip from arrangement when exiting empty clip editor + let mut swapped = None; + if clip.read().unwrap().count_midi_messages() == 0 { + std::mem::swap(&mut swapped, slot); + } + if let Some(clip) = swapped { + self.pool.delete_clip(&clip.read().unwrap()); + } + } + } +} + +/// The [Draw] implementation for [App] handles the loaded view, +/// which is defined in terms of [dizzle] DSL. +/// +/// If there is an error, the error is displayed. FIXME: overlay it +/// Then, every top-level form of the DSL description is rendered. +impl Draw for App { + fn draw (self, to: &mut Tui) -> Usually> { + let xywh = to.area().into(); + if let Some(e) = self.error.read().unwrap().as_ref() { + to.show(xywh, e.as_ref()); + } + for (index, dsl) in self.mode.view.iter().enumerate() { + if let Err(e) = self.interpret(to, dsl) { + *self.error.write().unwrap() = Some(format!("view #{index}: {e}").into()); + break; + } + } + Ok(xywh) + } +} + +impl HasClipsSize for App { + fn clips_size (&self) -> &Size { &self.project.size_inner } +} + +impl HasJack<'static> for App { + fn jack (&self) -> &Jack<'static> { &self.jack } +} + +//impl_handle!(TuiIn: |self: App, input|{ + //let commands = tek_collect_commands(self, input)?; + //let history = tek_execute_commands(self, commands)?; + //self.history.extend(history.into_iter()); + //Ok(None) +//}); + +//fn tek_collect_commands (app: &App, input: &TuiIn) -> Usually> { + //let mut commands = vec![]; + //for id in app.mode.keys.iter() { + //if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref()) + //&& let Some(bindings) = event_map.query(input.event()) { + //for binding in bindings { + //for command in binding.commands.iter() { + //if let Some(command) = app.namespace(command)? as Option { + //commands.push(command) + //} + //} + //} + //} + //} + //Ok(commands) +//} + +//fn tek_execute_commands ( + //app: &mut App, commands: Vec +//) -> Usually)>> { + //let mut history = vec![]; + //for command in commands.into_iter() { + //let result = command.act(app); + //match result { Err(err) => { history.push((command, None)); return Err(err) } + //Ok(undo) => { history.push((command, undo)); } }; + //} + //Ok(history) +//} + +def_command!(AppCommand: |app: App| { + Nop => Ok(None), + Confirm => tek_confirm(app), + Cancel => todo!(), // TODO delegate: + Inc { axis: ControlAxis } => tek_inc(app, axis), + Dec { axis: ControlAxis } => tek_dec(app, axis), + SetDialog { dialog: Dialog } => { + swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) + }, +}); + +impl_default!(AppCommand: Self::Nop); + +impl_has!(Clock: |self: App|self.project.clock); +impl_has!(Vec: |self: App|self.project.midi_ins); +impl_has!(Vec: |self: App|self.project.midi_outs); +impl_has!(Dialog: |self: App|self.dialog); +impl_has!(Jack<'static>: |self: App|self.jack); +impl_has!(Size: |self: App|self.size); +impl_has!(Pool: |self: App|self.pool); +impl_has!(Selection: |self: App|self.project.selection); + +impl_as_ref!(Vec: |self: App|self.project.as_ref()); +impl_as_mut!(Vec: |self: App|self.project.as_mut()); + +impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); +impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); + +impl_has_clips!( |self: App|self.pool.clips); + +impl_audio!(App: tek_jack_process, tek_jack_event); + +namespace!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); + +namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); }); + +namespace!(App: u16 { literal = |dsl|try_to_u16(dsl); symbol = |app| { + ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), + ":h/sample-detail" => 6.max(app.size.h() as u16 * 3 / 9), }; }); + +namespace!(App: isize { literal = |dsl|try_to_isize(dsl); }); + +namespace!(App: usize { literal = |dsl|try_to_usize(dsl); symbol = |app| { + ":scene-count" => app.scenes().len(), + ":track-count" => app.tracks().len(), + ":device-kind" => app.dialog.device_kind().unwrap_or(0), + ":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0), + ":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), }; }); + +namespace!(App: bool { symbol = |app| { // Provide boolean values. + ":mode/editor" => app.project.editor.is_some(), + ":focused/dialog" => !matches!(app.dialog, Dialog::None), + ":focused/message" => matches!(app.dialog, Dialog::Message(..)), + ":focused/add_device" => matches!(app.dialog, Dialog::Device(..)), + ":focused/browser" => app.dialog.browser().is_some(), + ":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))), + ":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))), + ":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))), + ":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))), + ":focused/clip" => !app.editor_focused() && matches!(app.selection(), Selection::TrackClip{..}), + ":focused/track" => !app.editor_focused() && matches!(app.selection(), Selection::Track(..)), + ":focused/scene" => !app.editor_focused() && matches!(app.selection(), Selection::Scene(..)), + ":focused/mix" => !app.editor_focused() && matches!(app.selection(), Selection::Mix), +}; }); + +namespace!(App: ItemTheme {}); // TODO: provide colors here + // +namespace!(App: Selection { symbol = |app| { + ":select/scene" => app.selection().select_scene(app.tracks().len()), + ":select/scene/next" => app.selection().select_scene_next(app.scenes().len()), + ":select/scene/prev" => app.selection().select_scene_prev(), + ":select/track" => app.selection().select_track(app.tracks().len()), + ":select/track/next" => app.selection().select_track_next(app.tracks().len()), + ":select/track/prev" => app.selection().select_track_prev(), +}; }); + +namespace!(App: Color { + symbol = |app| { + ":color/bg" => Color::Rgb(28, 32, 36), + }; + expression = |app| { + "g" (n: u8) => Color::Rgb(n, n, n), + "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), + }; +}); + +namespace!(App: Option { symbol = |app| { + ":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) +}; }); + +namespace!(App: Option { symbol = |app| { + ":selected/scene" => app.selection().scene(), + ":selected/track" => app.selection().track(), +}; }); + +namespace!(App: Option>> { + symbol = |app| { + ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { + app.scenes()[*scene].clips[*track].clone() + } else { + None + } + }; +}); + +impl<'a> Namespace<'a, AppCommand> for App { + symbols!('a |app| -> AppCommand { + "x/inc" => AppCommand::Inc { axis: ControlAxis::X }, + "x/dec" => AppCommand::Dec { axis: ControlAxis::X }, + "y/inc" => AppCommand::Inc { axis: ControlAxis::Y }, + "y/dec" => AppCommand::Dec { axis: ControlAxis::Y }, + "confirm" => AppCommand::Confirm, + "cancel" => AppCommand::Cancel, + }); +} + +impl Interpret for App { + fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { + app_interpret_expr(self, to, lang) + } + fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { + app_interpret_word(self, to, lang) + } +} + +fn app_interpret_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Usually<()> { + if eval_view(state, to, lang)? || eval_view_tui(state, to, lang)? { + Ok(()) + } else { + Err(format!("App::interpret_expr: unexpected: {lang:?}").into()) + } +} + +fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually<()> { + let mut frags = dsl.src()?.unwrap().split("/"); + match frags.next() { + + Some(":logo") => view_logo().draw(to), + + Some(":status") => h_exact(1, "TODO: Status Bar").draw(to), + + Some(":meters") => match frags.next() { + Some("input") => bg(Rgb(30, 30, 30), h_full(origin_s("Input Meters"))).draw(to), + Some("output") => bg(Rgb(30, 30, 30), h_full(origin_s("Output Meters"))).draw(to), + _ => panic!() + }, + + Some(":tracks") => match frags.next() { + None => "TODO tracks".draw(to), + Some("names") => state.project.view_track_names(state.color.clone()).draw(to),//bg(Rgb(40, 40, 40), w_full(origin_w("Track Names")))), + Some("inputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Inputs"))).draw(to), + Some("devices") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Devices"))).draw(to), + Some("outputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Outputs"))).draw(to), + _ => panic!() + }, + + Some(":scenes") => match frags.next() { + None => "TODO scenes".draw(to), + Some(":scenes/names") => "TODO Scene Names".draw(to), + _ => panic!() + }, + + Some(":editor") => "TODO Editor".draw(to), + + Some(":dialog") => match frags.next() { + Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog { + let items = items.clone(); + let selected = selected; + Some(wh_full(thunk(move|to: &mut Tui|{ + for (index, MenuItem(item, _)) in items.0.iter().enumerate() { + to.place(&y_push((2 * index) as u16, + fg_bg( + if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, + if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, + h_exact(2, origin_n(w_full(item))) + ))); + } + }))) + } else { + None + }.draw(to), + _ => unimplemented!("App::interpret_word: {dsl:?} ({frags:?})"), + }, + + Some(":templates") => { + let modes = state.config.modes.clone(); + let height = (modes.read().unwrap().len() * 2) as u16; + h_exact(height, w_min(Some(30), thunk(move |to: &mut Tui|{ + for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { + let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; + let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); + let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); + let fg1 = Rgb(224, 192, 128); + let fg2 = Rgb(224, 128, 32); + let field_name = w_full(origin_w(fg(fg1, name))); + let field_id = w_full(origin_e(fg(fg2, id))); + let field_info = w_full(origin_w(info)); + y_push((2 * index) as u16, + h_exact(2, w_full(bg(bg, south( + above(field_name, field_id), field_info))))).draw(to); + } + }))) + }.draw(to), + + Some(":sessions") => h_exact(Some(6), w_min(30, thunk(|to: &mut Tui|{ + let fg = Rgb(224, 192, 128); + for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { + let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; + y_push((2 * index) as u16, + &h_exact(2, w_full(bg(bg, origin_w(fg(fg, name)))))).draw(to); + } + }))).draw(to), + + Some(":browse/title") => w_full(origin_w(field_v(ItemColor::default(), + match state.dialog.browser_target().unwrap() { + BrowseTarget::SaveProject => "Save project:", + BrowseTarget::LoadProject => "Load project:", + BrowseTarget::ImportSample(_) => "Import sample:", + BrowseTarget::ExportSample(_) => "Export sample:", + BrowseTarget::ImportClip(_) => "Import clip:", + BrowseTarget::ExportClip(_) => "Export clip:", + }, w_shrink(3, h_exact(1, fg(g(96), x_repeat("🭻"))))))).draw(to), + + Some(":device") => { + let selected = state.dialog.device_kind().unwrap(); + south(bold(true, "Add device"), iter_south( + move||device_kinds().iter(), + move|_label: &&'static str, i|{ + let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; + let l = if i == selected { "[ " } else { " " }; + let r = if i == selected { " ]" } else { " " }; + w_full(bg(b, east(l, west(r, "FIXME device name")))) })).draw(to) + }, + + Some(":debug") => h_exact(1, format!("[{:?}]", to.area())).draw(to), + + Some(_) => { + let views = state.config.views.read().unwrap(); + if let Some(dsl) = views.get(dsl.src()?.unwrap()) { + let dsl = dsl.clone(); + std::mem::drop(views); + state.interpret(to, &dsl)? + } else { + unimplemented!("{dsl:?}"); + } + }, + + _ => unreachable!() + } + Ok(()) +} + +impl HasTrackScroll for App { + fn track_scroll (&self) -> usize { + self.project.track_scroll() + } +} + +impl HasSceneScroll for App { + fn scene_scroll (&self) -> usize { + self.project.scene_scroll() + } +} + +impl ScenesView for App { + fn w_mid (&self) -> u16 { + (self.size.w() as u16).saturating_sub(self.w_side()) + } + fn w_side (&self) -> u16 { + 20 + } + fn h_scenes (&self) -> u16 { + (self.size.h() as u16).saturating_sub(20) + } +} diff --git a/src/bind.rs b/src/app/bind.rs similarity index 98% rename from src/bind.rs rename to src/app/bind.rs index a8eaae7d..be5a5705 100644 --- a/src/bind.rs +++ b/src/app/bind.rs @@ -61,7 +61,7 @@ impl Bind> { // TODO Ok(()) } else if let Ok(Some(_word)) = item.expr().head().word() { - if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? { + if let Some(key) = TuiEvent::named(item.expr()?.head()?)? { map.add(key, Binding { commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), condition: None, diff --git a/src/cli.rs b/src/app/cli.rs similarity index 63% rename from src/cli.rs rename to src/app/cli.rs index 4373c13f..99e0b53e 100644 --- a/src/cli.rs +++ b/src/app/cli.rs @@ -1,4 +1,4 @@ -use crate::{*, arrange::*, clock::*, config::*, device::*}; +use crate::*; /// The command-line interface descriptor. /// @@ -61,7 +61,7 @@ use crate::{*, arrange::*, clock::*, config::*, device::*}; #[arg(short='L', long)] left_to: Vec, /// Audio ins to connect from right output #[arg(short='R', long)] right_to: Vec, - /// Tracks to create + /// Tracks to creat #[arg(short='t', long)] tracks: Option, /// Scenes to create #[arg(short='s', long)] scenes: Option, @@ -75,96 +75,103 @@ use crate::{*, arrange::*, clock::*, config::*, device::*}; } /// Command-line configuration. -#[cfg(feature = "cli")] impl Cli { +#[cfg(feature = "cli")] +impl Cli { pub fn run (&self) -> Usually<()> { - if let Action::Version = self.action { - return Ok(tek_show_version()) - } + self.action.run(&Config::init_new(None)?) + } +} - let mut config = Config::new(None); - config.init()?; - - if let Action::Config = self.action { - tek_print_config(&config); - } else if let Action::List = self.action { - todo!("list sessions") - } else if let Action::Resume = self.action { - todo!("resume session") - } else if let Action::New { - name, bpm, tracks, scenes, sync_lead, sync_follow, - midi_from, midi_from_re, midi_to, midi_to_re, - left_from, right_from, left_to, right_to, .. - } = &self.action { - - // Connect to JACK - let name = name.as_ref().map_or("tek", |x|x.as_str()); - let jack = Jack::new(&name)?; - - // TODO: Collect audio IO: - let empty = &[] as &[&str]; - let left_froms = Connect::collect(&left_from, empty, empty); - let left_tos = Connect::collect(&left_to, empty, empty); - let right_froms = Connect::collect(&right_from, empty, empty); - let right_tos = Connect::collect(&right_to, empty, empty); - let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; - let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; - - // Create initial project: - let clock = Clock::new(&jack, *bpm)?; - let mut project = Arrangement::new( - &jack, - None, - clock, - vec![], - vec![], - Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate() - .map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()])) - .collect::, _>>()?, - Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() - .map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()])) - .collect::, _>>()? - ); - project.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; - project.scenes_add(scenes.unwrap_or(0))?; - - if matches!(self.action, Action::Status) { - // Show status and exit - tek_print_status(&project); - return Ok(()) +#[cfg(feature = "cli")] +impl Action { + fn run (&self, config: &Config) -> Usually<()> { + use Action::*; + match self { + Version => tek_show_version(), + Config => tek_print_config(&config), + List => todo!("list sessions"), + Resume => todo!("resume session"), + New { + name, bpm, tracks, scenes, sync_lead, sync_follow, + midi_from: mf, midi_from_re: mfr, midi_to: mt, midi_to_re: mtr, + left_from: lf, right_from: rf, left_to: lt, right_to: rt, .. + } => { + let name = name.as_ref().map_or("tek", |x|x.as_str()); + let jack = Jack::new(&name)?; + let proj = tek_project_new( + &jack, Clock::new(&jack, *bpm)?, &lf, <, &rf, &rt, &mf, &mt, &mfr, &mtr + )?; + proj.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; + proj.scenes_add(scenes.unwrap_or(0))?; + //if matches!(self, Action::Status) { + //// Show status and exit + //tek_print_status(&proj); + //return Ok(()) + //} + // Initialize the app state + let app = tek(&jack, proj, config, ":menu"); + //if matches!(self, Action::Headless) { + //// TODO: Headless mode (daemon + client over IPC, then over network...) + //println!("todo headless"); + //return Ok(()) + //} + // Run the [Tui] and [Jack] threads with the [App] state. + Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{ + // Between jack init and app's first cycle: + jack.sync_lead(sync_lead, |mut state|{ + let clock = app.clock(); + clock.playhead.update_from_sample(state.position.frame() as f64); + state.position.bbt = Some(clock.bbt()); + state.position + })?; + jack.sync_follow(sync_follow)?; + // FIXME: They don't work properly. + Ok(app) + })?)?; } - - // Initialize the app state - let app = tek(&jack, project, config, ":menu"); - if matches!(self.action, Action::Headless) { - // TODO: Headless mode (daemon + client over IPC, then over network...) - println!("todo headless"); - return Ok(()) - } - - // Run the [Tui] and [Jack] threads with the [App] state. - Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{ - - // Between jack init and app's first cycle: - - jack.sync_lead(*sync_lead, |mut state|{ - let clock = app.clock(); - clock.playhead.update_from_sample(state.position.frame() as f64); - state.position.bbt = Some(clock.bbt()); - state.position - })?; - - jack.sync_follow(*sync_follow)?; - - // FIXME: They don't work properly. - - Ok(app) - - })?)?; } Ok(()) } } +pub fn tek_project_new ( + jack: &Jack, + clock: Clock, + left_from: &[impl AsRef], + left_to: &[impl AsRef], + right_from: &[impl AsRef], + right_to: &[impl AsRef], + midi_from: &[impl AsRef], + midi_to: &[impl AsRef], + midi_from_re: &[impl AsRef], + midi_to_re: &[impl AsRef], +) -> Usually { + // TODO: Collect audio IO: + let empty = &[] as &[&str]; + let left_froms = Connect::collect(left_from, empty, empty); + let left_tos = Connect::collect(left_to, empty, empty); + let right_froms = Connect::collect(right_from, empty, empty); + let right_tos = Connect::collect(right_to, empty, empty); + let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; + let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; + + // Create initial project: + let mut project = Arrangement::new( + jack, + None, + clock, + vec![], + vec![], + Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate() + .map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()])) + .collect::, _>>()?, + Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() + .map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()])) + .collect::, _>>()? + ); + Ok(project) +} + pub fn tek_show_version () { println!("todo version"); } diff --git a/src/app/config.rs b/src/app/config.rs new file mode 100644 index 00000000..cceefff8 --- /dev/null +++ b/src/app/config.rs @@ -0,0 +1,192 @@ +use crate::{*, bind::*, view::*}; + +/// Configuration: mode, view, and bind definitions. +/// +/// ``` +/// let config = tek::Config::default(); +/// ``` +/// +/// ``` +/// // Some dizzle. +/// // What indentation to use here lol? +/// let source = stringify!((mode :menu (name Menu) +/// (info Mode selector.) (keys :axis/y :confirm) +/// (view (bg (g 0) (bsp/s :ports/out +/// (bsp/n :ports/in +/// (bg (g 30) (bsp/s (fixed/y 7 :logo) +/// (fill :dialog/menu))))))))); +/// // Add this definition to the config and try to load it. +/// // A "mode" is basically a state machine +/// // with associated input and output definitions. +/// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap(); +/// ``` +#[derive(Default, Debug)] pub struct Config { + /// XDG base directories of running user. + pub dirs: BaseDirectories, + /// Active collection of interaction modes. + pub modes: Modes, + /// Active collection of event bindings. + pub binds: Binds, + /// Active collection of view definitions. + pub views: Views, +} + +impl Config { + const CONFIG_DIR: &'static str = "tek"; + const CONFIG_SUB: &'static str = "v0"; + const CONFIG: &'static str = "tek.edn"; + const DEFAULTS: &'static str = include_str!("../tek.edn"); + pub fn init_new (dirs: Option) -> Usually { + let mut config = Self::new(None); + config.init()?; + Ok(config) + } + /// Create a new app configuration from a set of XDG base directories, + pub fn new (dirs: Option) -> Self { + let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); + let dirs = dirs.unwrap_or_else(default); + Self { dirs, ..Default::default() } + } + /// Write initial contents of configuration. + pub fn init (&mut self) -> Usually<()> { + self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{ + cfgs.add(&dsl)?; + Ok(()) + })?; + Ok(()) + } + /// Write initial contents of a configuration file. + pub fn init_one ( + &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> + ) -> Usually<()> { + if self.dirs.find_config_file(path).is_none() { + //println!("Creating {path:?}"); + std::fs::write(self.dirs.place_config_file(path)?, defaults)?; + } + Ok(if let Some(path) = self.dirs.find_config_file(path) { + //println!("Loading {path:?}"); + let src = std::fs::read_to_string(&path)?; + src.as_str().each(move|item|each(self, item))?; + } else { + return Err(format!("{path}: not found").into()) + }) + } + /// Add statements to configuration from [Dsl] source. + pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> { + dsl.each(|item|self.add_one(item))?; + Ok(self) + } + fn add_one (&self, item: impl Language) -> Usually<()> { + if let Some(expr) = item.expr()? { + let head = expr.head()?; + let tail = expr.tail()?; + let name = tail.head()?; + let body = tail.tail()?; + //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); + match head { + Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?, + Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, + Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, + _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) + } + Ok(()) + } else { + return Err(format!("Config::load: expected expr, got: {item:?}").into()) + } + } + pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { + self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() + } +} + +pub(crate) fn load_mode (modes: &Modes, name: &impl AsRef, body: &impl Language) -> Usually<()> { + let mut mode = Mode::default(); + body.each(|item|mode.add(item))?; + modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); + Ok(()) +} + +/// Collection of interaction modes. +pub type Modes = Arc, Arc>>>>>; + +impl Mode> { + /// Add a definition to the mode. + /// + /// Supported definitions: + /// + /// - (name ...) -> name + /// - (info ...) -> description + /// - (keys ...) -> key bindings + /// - (mode ...) -> submode + /// - ... -> view + /// + /// ``` + /// let mut mode: tek::Mode> = Default::default(); + /// mode.add("(name hello)").unwrap(); + /// ``` + pub fn add (&mut self, dsl: impl Language) -> Usually<()> { + Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { + //println!("Mode::add: {head} {:?}", expr.tail()); + let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + match head { + "name" => self.add_name(tail)?, + "info" => self.add_info(tail)?, + "keys" => self.add_keys(tail)?, + "mode" => self.add_mode(tail)?, + _ => self.add_view(tail)?, + }; + } else if let Ok(Some(word)) = dsl.word() { + self.add_view(word); + } else { + return Err(format!("Mode::add: unexpected: {dsl:?}").into()); + }) + + //DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into())) + //.word(|word|self.add_view(word)) + //.expr(|expr|expr.head(|head|{ + ////println!("Mode::add: {head} {:?}", expr.tail()); + //let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + //match head { + //"name" => self.add_name(tail), + //"info" => self.add_info(tail), + //"keys" => self.add_keys(tail)?, + //"mode" => self.add_mode(tail)?, + //_ => self.add_view(tail), + //}; + //})) + } + + fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.name.push(src.into()))) + } + fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.info.push(src.into()))) + } + fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.view.push(src.into()))) + } + fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?)) + } + fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(Some(if let Some(id) = dsl.head()? { + load_mode(&self.modes, &id, &dsl.tail())?; + } else { + return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); + })) + } +} + +/// Group of view and keys definitions. +/// +/// ``` +/// let mode = tek::Mode::>::default(); +/// ``` +#[derive(Default, Debug)] pub struct Mode { + pub path: PathBuf, + pub name: Vec, + pub info: Vec, + pub view: Vec, + pub keys: Vec, + pub modes: Modes, +} diff --git a/src/app/view.rs b/src/app/view.rs new file mode 100644 index 00000000..f5e2852e --- /dev/null +++ b/src/app/view.rs @@ -0,0 +1,590 @@ +use crate::*; + +/// Collection of custom view definitions. +pub type Views = Arc, Arc>>>; + +/// Load custom view definition. +pub(crate) fn load_view (views: &Views, name: &impl AsRef, body: &impl Language) -> Usually<()> { + views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into()); + Ok(()) +} + +/// ``` +/// let _ = tek::view_logo(); +/// ``` +pub fn view_logo () -> impl Draw { + wh_exact(Some(32), Some(7), bold(true, fg(Rgb(240, 200, 180), south!{ + h_exact(1, ""), + h_exact(1, ""), + h_exact(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"), + h_exact(1, east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), + h_exact(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"), + }))) +} + +/// ``` +/// let x = std::sync::Arc::>::default(); +/// let _ = tek::view_transport(true, x.clone(), x.clone(), x.clone()); +/// let _ = tek::view_transport(false, x.clone(), x.clone(), x.clone()); +/// ``` +pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw { + let theme = ItemTheme::G[96]; + bg(Black, east!(above( + wh_full(origin_w(button_play_pause(play))), + wh_full(origin_e(east!( + field_h(theme, "BPM", bpm), + field_h(theme, "Beat", beat), + field_h(theme, "Time", time), + ))) + ))) +} + +/// ``` +/// let x = std::sync::Arc::>::default(); +/// let _ = tek::view_status(None, x.clone(), x.clone(), x.clone()); +/// let _ = tek::view_status(Some("".into()), x.clone(), x.clone(), x.clone()); +/// ``` +pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw { + let theme = ItemTheme::G[96]; + let sr = field_h(theme, "SR", sr); + let buf = field_h(theme, "Buf", buf); + let lat = field_h(theme, "Lat", lat); + bg(Black, east!(above( + wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))), + wh_full(origin_e(east!(sr, buf, lat))), + ))) +} + +/// ``` +/// let _ = tek::button_play_pause(true); +/// ``` +pub fn button_play_pause (playing: bool) -> impl Draw { + let compact = true;//self.is_editing(); + bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + either(compact, + thunk(move|to: &mut Tui|w_exact(9, either(playing, + fg(Rgb(0, 255, 0), " PLAYING "), + fg(Rgb(255, 128, 0), " STOPPED ")) + ).draw(to)), + thunk(move|to: &mut Tui|w_exact(5, either(playing, + fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), + fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",))) + ).draw(to)), + ) + ) +} + +#[cfg(feature = "track")] pub fn view_track_row_section ( + _theme: ItemTheme, + button: impl Draw, + button_add: impl Draw, + content: impl Draw, +) -> impl Draw { + west(h_full(w_exact(4, origin_nw(button_add))), + east(w_exact(20, h_full(origin_nw(button))), wh_full(origin_c(content)))) +} + +/// ``` +/// let bg = tengri::ratatui::style::Color::Red; +/// let fg = tengri::ratatui::style::Color::Green; +/// let _ = tek::view_wrap(bg, fg, "and then blue, too!"); +/// ``` +pub fn view_wrap (bg: Color, fg: Color, content: impl Draw) -> impl Draw { + let left = fg_bg(bg, Reset, w_exact(1, y_repeat("▐"))); + let right = fg_bg(bg, Reset, w_exact(1, y_repeat("▌"))); + east(left, west(right, fg_bg(fg, bg, content))) +} + +/// ``` +/// let _ = tek::view_meter("", 0.0); +/// let _ = tek::view_meters(&[0.0, 0.0]); +/// ``` +pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw + 'a { + let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)); + let w = if value >= 0.0 { 13 } + else if value >= -1.0 { 12 } + else if value >= -2.0 { 11 } + else if value >= -3.0 { 10 } + else if value >= -4.0 { 9 } + else if value >= -6.0 { 8 } + else if value >= -9.0 { 7 } + else if value >= -12.0 { 6 } + else if value >= -15.0 { 5 } + else if value >= -20.0 { 4 } + else if value >= -25.0 { 3 } + else if value >= -30.0 { 2 } + else if value >= -40.0 { 1 } + else { 0 }; + let c = if value >= 0.0 { Red } + else if value >= -3.0 { Yellow } + else { Green }; + south!(f, wh_exact(Some(w), Some(1), bg(c, ()))) +} + +pub fn view_meters (values: &[f32;2]) -> impl Draw + use<'_> { + let left = format!("L/{:>+9.3}", values[0]); + let right = format!("R/{:>+9.3}", values[1]); + south(left, right) +} + +pub fn view_sample_info (sample: Option<&Arc>>) -> impl Draw + use<'_> { + when(sample.is_some(), thunk(move|to: &mut Tui|{ + let sample = sample.unwrap().read().unwrap(); + let theme = sample.color; + east!( + field_h(theme, "Name", format!("{:<10}", sample.name.clone())), + field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())), + field_h(theme, "Start", format!("{:<8}", sample.start)), + field_h(theme, "End", format!("{:<8}", sample.end)), + field_h(theme, "Trans", "0"), + field_h(theme, "Gain", format!("{}", sample.gain)), + ).draw(to) + })) +} + +pub fn view_sample_info_v (sample: Option<&Arc>>) -> impl Draw + use<'_> { + let a = thunk(move|to: &mut Tui|{ + let sample = sample.unwrap().read().unwrap(); + let theme = sample.color; + w_exact(20, south!( + w_full(origin_w(field_h(theme, "Name ", format!("{:<10}", sample.name.clone())))), + w_full(origin_w(field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())))), + w_full(origin_w(field_h(theme, "Start ", format!("{:<8}", sample.start)))), + w_full(origin_w(field_h(theme, "End ", format!("{:<8}", sample.end)))), + w_full(origin_w(field_h(theme, "Trans ", "0"))), + w_full(origin_w(field_h(theme, "Gain ", format!("{}", sample.gain)))), + )).draw(to) + }); + + let b = thunk(|to: &mut Tui|fg(Red, south!( + bold(true, "× No sample."), + "[r] record", + "[Shift-F9] import", + )).draw(to)); + + either(sample.is_some(), a, b) +} + +pub fn view_sample_status (sample: Option<&Arc>>) -> impl Draw { + bold(true, fg(g(224), sample + .map(|sample|{ + let sample = sample.read().unwrap(); + format!("Sample {}-{}", sample.start, sample.end) + }) + .unwrap_or_else(||"No sample".to_string()))) +} + +pub fn view_track_header (theme: ItemTheme, content: impl Draw) -> impl Draw { + w_exact(12, bg(theme.darker.term, w_full(origin_e(content)))) +} + +pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) + -> impl Draw + use<'a, T> +{ + let ins = ports.len() as u16; + let frame = Outer(true, Style::default().fg(g(96))); + let iter = move||ports.iter(); + let names = iter_south(iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name())))); + let field = field_v(theme, title, names); + wh_exact(Some(20), Some(1 + ins), border(true, frame, wh_exact(Some(20), Some(1 + ins), field))) +} + +pub fn view_io_ports <'a, T: PortsSizes<'a>> ( + fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a +) -> impl Draw + 'a { + type Item<'a> = (usize, &'a Arc, &'a [Connect], usize, usize); + iter(items, move|(_index, name, connections, y, y2): Item<'a>, _| { + y_push(y as u16, h_exact((y2-y) as u16, + south(h_full(bold(true, fg_bg(fg, bg, origin_w(east(" 󰣲 ", name))))), + iter(||connections.iter(), move|connect: &'a Connect, index|y_push( + index as u16, h_exact(1, origin_w(bold(false, fg_bg(fg, bg, &connect.info)))) + ))))) + }) +} + +pub fn view_scenes_clips ( + scenes: impl ScenesSizes<'_>, + tracks: impl TracksSizes<'_>, + select: &Selection, + editor: Option<&MidiEditor>, + size: &Size, + is_editing: bool, +) -> impl Draw { + size.of(wh_full(above(wh_full(origin_se(fg(Green, format!("{}x{}", size.w(), size.h())))), + thunk(|to: &mut Tui|{ + for (index, track, _, _) in tracks { + let clips = view_track_clips(scenes, select, editor, index, track, is_editing); + w_exact(track.width as u16, h_full(clips)).draw(to); + } + Ok(XYWH(0, 0, 0, 0)) + })))) +} + +pub fn view_track_clips ( + scenes: impl ScenesSizes<'_>, + select: &Selection, + editor: &Option, + index: usize, + track: &Track, + is_editing: bool, +) -> impl Draw { + thunk(move|to: &mut Tui|{ + + for (scene_index, scene, ..) in scenes { + + let ( + name, theme + ): (Arc, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(index) { + let clip = clip.read().unwrap(); + (format!(" ⏹ {}", &clip.name).into(), clip.color) + } else { + (" ⏹ -- ".into(), ItemTheme::G[32]) + }; + + let fg = theme.lightest.term; + + let mut outline = theme.base.term; + + let bg = if select.track() == Some(index) && select.scene() == Some(scene_index) { + outline = theme.lighter.term; + theme.light.term + } else if select.track() == Some(index) || select.scene() == Some(scene_index) { + outline = theme.darkest.term; + theme.base.term + } else { + theme.dark.term + }; + + let w = if select.track() == Some(index) && let Some(editor) = editor { + (editor.size.w() as usize).max(24).max(track.width) + } else { + track.width + } as u16; + + let y = if select.scene() == Some(scene_index) && let Some(editor) = editor { + (editor.size.h() as usize).max(12) + } else { + Self::H_SCENE as usize + } as u16; + + let is_selected = is_editing && select.track() == Some(index) && select.scene() == Some(scene_index); + + wh_exact(Some(w), Some(y), below( + wh_full(Outer(true, Style::default().fg(outline))), + wh_full(below( + below( + fg_bg(outline, bg, wh_full("")), + wh_full(origin_nw(fg_bg(fg, bg, bold(true, &name)))), + ), + wh_full(when(is_selected, editor.map(|e|e.view())))))) + ).draw(to); + + } + + Ok(XYWH(0, 0, 0, 0)) + }) +} + +pub fn view_track_names ( + theme: ItemTheme, + tracks: impl TracksSizes<'_>, + track_count: usize, + scene_count: usize, + selected: &Selection, +) -> impl Draw { + let button = south( + button_3("t", "rack ", format!("{}{track_count}", selected.track() + .map(|track|format!("{track}/")).unwrap_or_default()), false), + button_3("s", "cene ", format!("{}{scene_count}", selected.scene() + .map(|scene|format!("{scene}/")).unwrap_or_default()), false)); + let button_2 = south( + button_2("T", "+", false), + button_2("S", "+", false)); + view_track_row_section(theme, button, button_2, bg(theme.darker.term, + h_exact(2, thunk(|to: &mut Tui|{ + for (index, track, x1, _x2) in tracks { + x_push(x1 as u16, w_exact(track_width(index, track), + bg(if selected.track() == Some(index) { + track.color.light.term + } else { + track.color.base.term + }, south(w_full(origin_nw(east( + format!("·t{index:02} "), + fg(Rgb(255, 255, 255), bold(true, &track.name)) + ))), ""))) ).draw(to); + } + Ok(XYWH(0, 0, 0, 0)) + })))) +} + +pub fn view_track_outputs ( + theme: ItemTheme, + tracks: impl TracksSizes<'_>, + midi_outs: impl Iterator, +) -> impl Draw { + view_track_row_section(theme, + south(w_full(origin_w(button_2("o", "utput", false))), + thunk(|to: &mut Tui|{ + for port in midi_outs { + w_full(origin_w(port.port_name())).draw(to); + } + Ok(XYWH(0, 0, 0, 0)) + })), + button_2("O", "+", false), + bg(theme.darker.term, origin_w(thunk(|to: &mut Tui|{ + for (index, track, _x1, _x2) in tracks { + let iter = ||track.sequencer.midi_outs.iter(); + let draw = |port: &MidiOutput, _|fg(Rgb(255, 255, 255), + h_exact(1, bg(track.color.dark.term, w_full(origin_w( + format!("·o{index:02} {}", port.port_name())))))); + w_exact(track_width(index, track), + origin_nw(h_full(iter_south(iter, draw)))).draw(to); + } + Ok(XYWH(0, 0, 0, 0)) + })))) +} + +pub fn view_track_inputs ( + theme: ItemTheme, + tracks: impl TracksSizes<'_>, + h: u16, +) -> impl Draw { + view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), + bg(theme.darker.term, origin_w(thunk(move|to: &mut Tui|{ + for (index, track, _x1, _x2) in tracks { + wh_exact(Some(track_width(index, track)), Some(h + 1), + origin_nw(south( + bg(track.color.base.term, + w_full(origin_w(east!( + either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "), + either(track.sequencer.recording, fg(Red, "●rec "), "·rec "), + either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "), + )))), + iter_south(||track.sequencer.midi_ins.iter(), + |port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term, + w_full(origin_w(format!("·i{index:02} {}", port.port_name())))))))) + .draw(to); + } + Ok(XYWH(0, 0, 0, 0)) + })))) +} + +pub fn view_scenes_names ( + scenes: &impl ScenesSizes<'_> +) -> impl Draw { + w_exact(20, thunk(|to: &mut Tui|{ + for (index, scene, ..) in scenes { + view_scene_name(index, scene).draw(to); + } + Ok(XYWH(1, 1, 1, 1)) + })) +} + +pub fn view_scene_name ( + select: Option<&Selection>, + editor: Option<&MidiEditor>, + index: usize, + scene: &Scene, + editing: bool +) -> impl Draw { + let h = if select.scene() == Some(index) && let Some(_editor) = editor { + 7 + } else { + Self::H_SCENE as u16 + }; + let a = w_full(origin_w(east(format!("·s{index:02} "), + fg(g(255), bold(true, &scene.name))))); + let b = when(select.scene() == Some(index) && editing, + wh_full(origin_nw(south( + editor.as_ref().map(|e|e.clip_status()), + editor.as_ref().map(|e|e.edit_status()))))); + let c = if select.scene() == Some(index) { + scene.color.light.term + } else { + scene.color.base.term + }; + wh_exact(Some(20), Some(h), bg(c, origin_nw(south(a, b)))) +} + +pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) +} + +pub fn view_midi_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs)) +} + +pub fn view_audio_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins())) +} + +pub fn view_audio_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) +} + +pub fn view_track_per ( + tracks: impl TracksSizes<'_> +) -> impl Draw { + iter_east(tracks, move|(index, track, x1, x2): (usize, &Track, usize, usize), _|{ + w_exact((x2 - x1) as u16, fg_bg( + track.color.lightest.term, + track.color.base.term, + callback(index, track))) + }) +} + +pub fn view_per_track () -> impl Draw {} + +pub fn view_per_track_top () -> impl Draw {} + +pub fn view_inputs ( + tracks: impl TracksSizes<'_>, + midi_ins: &[MidiIn], +) -> impl Draw { + let header = h_exact(1, view_inputs_header(tracks)); + south(header, thunk(|to: &mut Tui|{ + for (index, port) in midi_ins.iter().enumerate() { + x_push(index as u16 * 10, h_exact(1, view_inputs_row(tracks, port))).draw(to); + } + todo!() + })) +} + +pub fn view_inputs_header ( + tracks: impl TracksSizes<'_>, + midi_ins: &[MidiIn], +) -> impl Draw { + east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", midi_ins.len()), false))), + west(w_exact(4, button_2("I", "+", false)), thunk(move|to: &mut Tui|{ + for (_index, track, x1, _x2) in tracks { + #[cfg(feature = "track")] + x_push(x1 as u16, bg(track.color.dark.term, origin_w(w_exact(track.width as u16, east!( + either(track.sequencer.monitoring, fg(Green, "mon "), "mon "), + either(track.sequencer.recording, fg(Red, "rec "), "rec "), + either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "), + ))))).draw(to); + } + todo!() + }))) +} + +pub fn view_inputs_row ( + tracks: impl TracksSizes<'_>, + port: () +) -> impl Draw { + east(w_exact(20, origin_w(east(" ● ", bold(true, fg(Rgb(255,255,255), port.port_name()))))), + west(w_exact(4, ()), thunk(move|to: &mut Tui|{ + for (_index, track, _x1, _x2) in tracks { + #[cfg(feature = "track")] + bg(track.color.darker.term, origin_w(w_exact(track.width as u16, east!( + either(track.sequencer.monitoring, fg(Green, " ● "), " · "), + either(track.sequencer.recording, fg(Red, " ● "), " · "), + either(track.sequencer.overdub, fg(Yellow, " ● "), " · "), + )))).draw(to); + } + todo!() + }))) +} + +pub fn view_outputs ( + theme: ItemTheme, + tracks: impl TracksSizes<'_>, + midi_outs: &[MidiOutput], + height: u16, +) -> impl Draw { + + let list = south( + h_exact(1, w_full(origin_w(button_3( + "o", "utput", format!("{}", midi_outs.len()), false + )))), + h_exact(height - 1, wh_full(origin_nw(thunk(|to: &mut Tui|{ + for (_index, port) in midi_outs.iter().enumerate() { + h_exact(1,w_full(east( + origin_w(east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name())))), + w_full(origin_e(format!("{}/{} ", + port.port().get_connections().len(), + port.connections.len())))))).draw(to); + for (index, conn) in port.connections.iter().enumerate() { + h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info())))) + .draw(to); + } + } + todo!(); + })))) + ); + + h_exact(height, view_track_row_section(theme, list, button_2("O", "+", false), + bg(theme.darker.term, origin_w(w_full( + thunk(|to: &mut Tui|{ + for (index, track, _x1, _x2) in tracks { + w_exact(track_width(index, track), + thunk(|to: &mut Tui|{ + h_exact(1, origin_w(east( + either(true, fg(Green, "play "), "play "), + either(false, fg(Yellow, "solo "), "solo "), + ))).draw(to); + for (_index, port) in midi_outs.iter().enumerate() { + h_exact(1, origin_w(east( + either(true, fg(Green, " ● "), " · "), + either(false, fg(Yellow, " ● "), " · "), + ))).draw(to); + for (_index, _conn) in port.connections.iter().enumerate() { + h_exact(1, w_full("")).draw(to); + } + } + todo!() + }) + ).draw(to); + } + todo!() + }) + ))) + )) +} + +pub fn view_track_devices ( + theme: ItemTheme, + tracks: &impl TracksSizes<'_>, + track: Option<&Track>, + h: u16, +) -> impl Draw { + view_track_row_section(theme, + button_3("d", "evice", format!("{}", track.map(|t|t.devices.len()).unwrap_or(0)), false), + button_2("D", "+", false), + iter(tracks, move|(_, track, _x1, _x2), index| wh_exact( + Some(track_width(index, track)), + Some(h + 1), + bg(track.color.dark.term, origin_nw(iter_south(move||0..h, + |_, _index|wh_exact(Some(track.width as u16), Some(2), + fg_bg( + ItemTheme::G[32].lightest.term, + ItemTheme::G[32].dark.term, + origin_nw(format!(" · {}", "--")) + ) + ))))))) +} + +pub fn per_track <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a +) -> impl Draw + 'a { + per_track_top(tracks, move|index, track|h_full(origin_y(callback(index, track)))) +} + +pub fn per_track_top <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a +) -> impl Draw + 'a { + origin_x(bg(Reset, iter_east(tracks, + move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ + w_exact((x2 - x1) as u16, fg_bg( + track.color.lightest.term, + track.color.base.term, + callback(index, track))) }))) +} + +pub fn field_h (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { +} + +pub fn field_v (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { +} diff --git a/src/arrange.rs b/src/arrange.rs deleted file mode 100644 index 4880e9ec..00000000 --- a/src/arrange.rs +++ /dev/null @@ -1,904 +0,0 @@ -use ::std::sync::{Arc, RwLock}; -use ::tengri::{space::east, color::ItemTheme}; -use ::tengri::{draw::*, term::*}; -use crate::{*, device::*, editor::*, sequence::*, clock::*, select::*, sample::*}; - -/// Arranger. -/// -/// ``` -/// let arranger = tek::Arrangement::default(); -/// ``` -#[derive(Default, Debug)] pub struct Arrangement { - /// Project name. - pub name: Arc, - /// Base color. - pub color: ItemTheme, - /// JACK client handle. - pub jack: Jack<'static>, - /// FIXME a render of the project arrangement, redrawn on update. - /// TODO rename to "render_cache" or smth - pub arranger: Arc>, - /// Display size - pub size: Size, - /// Display size of clips area - pub size_inner: Size, - /// Source of time - #[cfg(feature = "clock")] pub clock: Clock, - /// Allows one MIDI clip to be edited - #[cfg(feature = "editor")] pub editor: Option, - /// List of global midi inputs - #[cfg(feature = "port")] pub midi_ins: Vec, - /// List of global midi outputs - #[cfg(feature = "port")] pub midi_outs: Vec, - /// List of global audio inputs - #[cfg(feature = "port")] pub audio_ins: Vec, - /// List of global audio outputs - #[cfg(feature = "port")] pub audio_outs: Vec, - /// Selected UI element - #[cfg(feature = "select")] pub selection: Selection, - /// Last track number (to avoid duplicate port names) - #[cfg(feature = "track")] pub track_last: usize, - /// List of tracks - #[cfg(feature = "track")] pub tracks: Vec, - /// Scroll offset of tracks - #[cfg(feature = "track")] pub track_scroll: usize, - /// List of scenes - #[cfg(feature = "scene")] pub scenes: Vec, - /// Scroll offset of scenes - #[cfg(feature = "scene")] pub scene_scroll: usize, -} - -/// A track consists of a sequencer and zero or more devices chained after it. -/// -/// ``` -/// let track: tek::Track = Default::default(); -/// ``` -#[derive(Debug, Default)] pub struct Track { - /// Name of track - pub name: Arc, - /// Identifying color of track - pub color: ItemTheme, - /// Preferred width of track column - pub width: usize, - /// MIDI sequencer state - pub sequencer: Sequencer, - /// Device chain - pub devices: Vec, -} - -/// A scene consists of a set of clips to play together. -/// -/// ``` -/// let scene: tek::Scene = Default::default(); -/// let _ = scene.pulses(); -/// let _ = scene.is_playing(&[]); -/// ``` -#[derive(Debug, Default)] pub struct Scene { - /// Name of scene - pub name: Arc, - /// Identifying color of scene - pub color: ItemTheme, - /// Clips in scene, one per track - pub clips: Vec>>>, -} - -impl_has!(Jack<'static>: |self: Arrangement| self.jack); -impl HasJack<'static> for Arrangement { - fn jack (&self) -> &Jack<'static> { &self.jack } -} - -impl_has!(Size: |self: Arrangement| self.size); -impl_has!(Vec: |self: Arrangement| self.tracks); -impl_has!(Vec: |self: Arrangement| self.scenes); -impl_has!(Vec: |self: Arrangement| self.midi_ins); -impl_has!(Vec: |self: Arrangement| self.midi_outs); -impl_has!(Clock: |self: Arrangement| self.clock); -impl_has!(Selection: |self: Arrangement| self.selection); -impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref()); -impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut()); -impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track()); -impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut()); - -impl >+AsMut>> HasScenes for T {} -impl >+AsMut>> HasTracks for T {} -impl +AsMutOpt+Send+Sync> HasScene for T {} -impl +AsMutOpt+Send+Sync> HasTrack for T {} -impl TracksView for T {} -impl ClipsView for T {} - -pub trait ClipsView: TracksView + ScenesView { - - fn view_scenes_clips <'a> (&'a self) - -> impl Draw + 'a - { - self.clips_size().of(wh_full(above( - wh_full(origin_se(fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))), - thunk(|to: &mut Tui|{ - for (track_index, track, _, _) in self.tracks_with_sizes() { - let w = track.width as u16; - let v = self.view_track_clips(track_index, track); - w_exact(w, h_full(v)).draw(to); - } - Ok(XYWH(0, 0, 0, 0)) - })))) - } - - fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Draw + 'a { - thunk(move|to: &mut Tui|{ - for (scene_index, scene, ..) in self.scenes_with_sizes() { - let (name, theme): (Arc, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) { - let clip = clip.read().unwrap(); - (format!(" ⏹ {}", &clip.name).into(), clip.color) - } else { - (" ⏹ -- ".into(), ItemTheme::G[32]) - }; - let fg = theme.lightest.term; - let mut outline = theme.base.term; - let bg = if self.selection().track() == Some(track_index) - && self.selection().scene() == Some(scene_index) - { - outline = theme.lighter.term; - theme.light.term - } else if self.selection().track() == Some(track_index) - || self.selection().scene() == Some(scene_index) - { - outline = theme.darkest.term; - theme.base.term - } else { - theme.dark.term - }; - let w = if self.selection().track() == Some(track_index) - && let Some(editor) = self.editor () - { - (editor.size.w() as usize).max(24).max(track.width) - } else { - track.width - } as u16; - let y = if self.selection().scene() == Some(scene_index) - && let Some(editor) = self.editor () - { - (editor.size.h() as usize).max(12) - } else { - Self::H_SCENE as usize - } as u16; - - let is_selected = - self.selection().track() == Some(track_index) && - self.selection().scene() == Some(scene_index) && - self.is_editing(); - - wh_exact(Some(w), Some(y), below( - wh_full(Outer(true, Style::default().fg(outline))), - wh_full(below( - below( - fg_bg(outline, bg, wh_full("")), - wh_full(origin_nw(fg_bg(fg, bg, bold(true, &name)))), - ), - wh_full(when(is_selected, self.editor().map(|e|e.view()))))))).draw(to); - } - Ok(XYWH(0, 0, 0, 0)) - }) - } - -} - -pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { - - //fn tracks_width_available (&self) -> u16 { - //(self.size.width() as u16).saturating_sub(40) - //} - - /// Iterate over tracks with their corresponding sizes. - fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { - let _editor_width = self.editor().map(|e|e.size.w()); - let _active_track = self.selection().track(); - let mut x = 0; - self.tracks().iter().enumerate().map_while(move |(index, track)|{ - let width = track.width.max(8); - if x + width < self.clips_size().w() as usize { - let data = (index, track, x, x + width); - x += width + Self::TRACK_SPACING; - Some(data) - } else { - None - } - }) - } - - fn view_track_names (&self, theme: ItemTheme) -> impl Draw { - let track_count = self.tracks().len(); - let scene_count = self.scenes().len(); - let selected = self.selection(); - let button = south( - button_3("t", "rack ", format!("{}{track_count}", selected.track() - .map(|track|format!("{track}/")).unwrap_or_default()), false), - button_3("s", "cene ", format!("{}{scene_count}", selected.scene() - .map(|scene|format!("{scene}/")).unwrap_or_default()), false)); - let button_2 = south( - button_2("T", "+", false), - button_2("S", "+", false)); - view_track_row_section(theme, button, button_2, bg(theme.darker.term, - h_exact(2, thunk(|to: &mut Tui|{ - for (index, track, x1, _x2) in self.tracks_with_sizes() { - x_push(x1 as u16, w_exact(track_width(index, track), - bg(if selected.track() == Some(index) { - track.color.light.term - } else { - track.color.base.term - }, south(w_full(origin_nw(east( - format!("·t{index:02} "), - fg(Rgb(255, 255, 255), bold(true, &track.name)) - ))), ""))) ).draw(to); - } - Ok(XYWH(0, 0, 0, 0)) - })))) - } - - fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw { - view_track_row_section(theme, - south(w_full(origin_w(button_2("o", "utput", false))), - thunk(|to: &mut Tui|{ - for port in self.midi_outs().iter() { - w_full(origin_w(port.port_name())).draw(to); - } - Ok(XYWH(0, 0, 0, 0)) - })), - button_2("O", "+", false), - bg(theme.darker.term, origin_w(thunk(|to: &mut Tui|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - let iter = ||track.sequencer.midi_outs.iter(); - let draw = |port: &MidiOutput, _|fg(Rgb(255, 255, 255), - h_exact(1, bg(track.color.dark.term, w_full(origin_w( - format!("·o{index:02} {}", port.port_name())))))); - w_exact(track_width(index, track), - origin_nw(h_full(iter_south(iter, draw)))).draw(to); - } - Ok(XYWH(0, 0, 0, 0)) - })))) - } - - fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw { - let mut h = 0u16; - for track in self.tracks().iter() { - h = h.max(track.sequencer.midi_ins.len() as u16); - } - let content = thunk(move|to: &mut Tui|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - wh_exact(Some(track_width(index, track)), Some(h + 1), - origin_nw(south( - bg(track.color.base.term, - w_full(origin_w(east!( - either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "), - either(track.sequencer.recording, fg(Red, "●rec "), "·rec "), - either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "), - )))), - iter_south(||track.sequencer.midi_ins.iter(), - |port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term, - w_full(origin_w(format!("·i{index:02} {}", port.port_name())))))))) - .draw(to); - } - Ok(XYWH(0, 0, 0, 0)) - }); - view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), - bg(theme.darker.term, origin_w(content))) - } - -} - -pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync { - /// Default scene height. - const H_SCENE: usize = 2; - /// Default editor height. - const H_EDITOR: usize = 15; - fn h_scenes (&self) -> u16; - fn w_side (&self) -> u16; - fn w_mid (&self) -> u16; - fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> { - let mut y = 0; - self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{ - let height = if self.selection().scene() == Some(s) && self.editor().is_some() { - 8 - } else { - Self::H_SCENE - }; - if y + height <= self.clips_size().h() as usize { - let data = (s, scene, y, y + height); - y += height; - Some(data) - } else { - None - } - }) - } - - fn view_scenes_names (&self) -> impl Draw { - w_exact(20, thunk(|to: &mut Tui|{ - for (index, scene, ..) in self.scenes_with_sizes() { - self.view_scene_name(index, scene).draw(to); - } - Ok(XYWH(1, 1, 1, 1)) - })) - } - - fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Draw + 'a { - let h = if self.selection().scene() == Some(index) && let Some(_editor) = self.editor() { - 7 - } else { - Self::H_SCENE as u16 - }; - let a = w_full(origin_w(east(format!("·s{index:02} "), - fg(g(255), bold(true, &scene.name))))); - let b = when(self.selection().scene() == Some(index) && self.is_editing(), - wh_full(origin_nw(south( - self.editor().as_ref().map(|e|e.clip_status()), - self.editor().as_ref().map(|e|e.edit_status()))))); - let c = if self.selection().scene() == Some(index) { - scene.color.light.term - } else { - scene.color.base.term - }; - wh_exact(Some(20), Some(h), bg(c, origin_nw(south(a, b)))) - } - -} -pub trait HasSceneScroll: HasScenes { fn scene_scroll (&self) -> usize; } -pub trait HasTrackScroll: HasTracks { fn track_scroll (&self) -> usize; } -pub trait HasScene: AsRefOpt + AsMutOpt { - fn scene_mut (&mut self) -> Option<&mut Scene> { self.as_mut_opt() } - fn scene (&self) -> Option<&Scene> { self.as_ref_opt() } -} -pub trait HasScenes: AsRef> + AsMut> { - fn scenes (&self) -> &Vec { self.as_ref() } - fn scenes_mut (&mut self) -> &mut Vec { self.as_mut() } - /// Generate the default name for a new scene - fn scene_default_name (&self) -> Arc { format!("s{:3>}", self.scenes().len() + 1).into() } - fn scene_longest_name (&self) -> usize { self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) } - /// Add multiple scenes - fn scenes_add (&mut self, n: usize) -> Usually<()> where Self: HasTracks { - let scene_color_1 = ItemColor::random(); - let scene_color_2 = ItemColor::random(); - for i in 0..n { - let _ = self.scene_add(None, Some( - scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() - ))?; - } - Ok(()) - } - /// Add a scene - fn scene_add (&mut self, name: Option<&str>, color: Option) - -> Usually<(usize, &mut Scene)> where Self: HasTracks - { - let scene = Scene { - name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), - clips: vec![None;self.tracks().len()], - color: color.unwrap_or_else(ItemTheme::random), - }; - self.scenes_mut().push(scene); - let index = self.scenes().len() - 1; - Ok((index, &mut self.scenes_mut()[index])) - } -} -pub trait HasTracks: AsRef> + AsMut> { - fn tracks (&self) -> &Vec { self.as_ref() } - fn tracks_mut (&mut self) -> &mut Vec { self.as_mut() } - /// Run audio callbacks for every track and every device - fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control { - for track in self.tracks_mut().iter_mut() { - if Control::Quit == Audio::process(&mut track.sequencer, client, scope) { - return Control::Quit - } - for device in track.devices.iter_mut() { - if Control::Quit == DeviceAudio(device).process(client, scope) { - return Control::Quit - } - } - } - Control::Continue - } - fn track_longest_name (&self) -> usize { self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) } - /// Stop all playing clips - fn tracks_stop_all (&mut self) { for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } } - /// Stop all playing clips - fn tracks_launch (&mut self, clips: Option>>>>) { - if let Some(clips) = clips { - for (clip, track) in clips.iter().zip(self.tracks_mut()) { track.sequencer.enqueue_next(clip.as_ref()); } - } else { - for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } - } - } - /// Spacing between tracks. - const TRACK_SPACING: usize = 0; -} -pub trait HasTrack: AsRefOpt + AsMutOpt { - fn track (&self) -> Option<&Track> { self.as_ref_opt() } - fn track_mut (&mut self) -> Option<&mut Track> { self.as_mut_opt() } - #[cfg(feature = "port")] fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw + 'a { - self.track().map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) - } - #[cfg(feature = "port")] fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw + '_ { - self.track().map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs)) - } - #[cfg(feature = "port")] fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw { - self.track().map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins())) - } - #[cfg(feature = "port")] fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw { - self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) - } -} - impl_as_ref!(Vec: |self: App| self.project.as_ref()); - impl_as_mut!(Vec: |self: App| self.project.as_mut()); - #[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt()); - #[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt()); - impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } } - impl HasTrackScroll for Arrangement { fn track_scroll (&self) -> usize { self.track_scroll } } - - impl HasWidth for Track { - const MIN_WIDTH: usize = 9; - fn width_inc (&mut self) { self.width += 1; } - fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } } - } - - impl Track { - /// Create a new track with only the default [Sequencer]. - pub fn new ( - name: &impl AsRef, - color: Option, - jack: &Jack<'static>, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[Connect], - midi_to: &[Connect], - ) -> Usually { - Ok(Self { - name: name.as_ref().into(), - color: color.unwrap_or_default(), - sequencer: Sequencer::new(format!("{}/sequencer", name.as_ref()), jack, clock, clip, midi_from, midi_to)?, - ..Default::default() - }) - } - pub fn audio_ins (&self) -> &[AudioInput] { - self.devices.first().map(|x|x.audio_ins()).unwrap_or_default() - } - pub fn audio_outs (&self) -> &[AudioOutput] { - self.devices.last().map(|x|x.audio_outs()).unwrap_or_default() - } - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } - pub fn per <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Draw + 'a { - iter_east(tracks, - move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - w_exact((x2 - x1) as u16, fg_bg( - track.color.lightest.term, - track.color.base.term, - callback(index, track)))}) - } - /// Create a new track connecting the [Sequencer] to a [Sampler]. - #[cfg(feature = "sampler")] pub fn new_with_sampler ( - name: &impl AsRef, - color: Option, - jack: &Jack<'static>, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[Connect], - midi_to: &[Connect], - audio_from: &[&[Connect];2], - audio_to: &[&[Connect];2], - ) -> Usually { - let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?; - let client_name = jack.with_client(|c|c.name().to_string()); - let port_name = track.sequencer.midi_outs[0].port_name(); - let connect = [Connect::exact(format!("{client_name}:{}", port_name))]; - track.devices.push(Device::Sampler(Sampler::new( - jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to - )?)); - Ok(track) - } - #[cfg(feature = "sampler")] pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { - for device in self.devices.iter() { - match device { - Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, - _ => {} - } - } - None - } - #[cfg(feature = "sampler")] pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { - for device in self.devices.iter_mut() { - match device { - Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, - _ => {} - } - } - None - } - } - - pub fn per_track <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Draw + 'a { - per_track_top(tracks, move|index, track|h_full(origin_y(callback(index, track)))) - } - - pub fn per_track_top <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Draw + 'a { - origin_x(bg(Reset, iter_east(tracks, - move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - w_exact((x2 - x1) as u16, fg_bg( - track.color.lightest.term, - track.color.base.term, - callback(index, track))) }))) - } - #[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt()); - #[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt()); - #[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene()); - #[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut()); - impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } - impl HasSceneScroll for Arrangement { fn scene_scroll (&self) -> usize { self.scene_scroll } } - impl ScenesView for App { - fn w_mid (&self) -> u16 { (self.size.w() as u16).saturating_sub(self.w_side()) } - fn w_side (&self) -> u16 { 20 } - fn h_scenes (&self) -> u16 { (self.size.h() as u16).saturating_sub(20) } - } - impl Scene { - /// Returns the pulse length of the longest clip in the scene - pub fn pulses (&self) -> usize { - self.clips.iter().fold(0, |a, p|{ - a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) - }) - } - /// Returns true if all clips in the scene are - /// currently playing on the given collection of tracks. - pub fn is_playing (&self, tracks: &[Track]) -> bool { - self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() - .all(|(track_index, clip)|match clip { - Some(c) => tracks - .get(track_index) - .map(|track|{ - if let Some((_, Some(clip))) = track.sequencer().play_clip() { - *clip.read().unwrap() == *c.read().unwrap() - } else { - false - } - }) - .unwrap_or(false), - None => true - }) - } - pub fn clip (&self, index: usize) -> Option<&Arc>> { - match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } - } - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } - } - -impl Arrangement { - - /// Create a new arrangement. - pub fn new ( - jack: &Jack<'static>, - name: Option>, - clock: Clock, - tracks: Vec, - scenes: Vec, - midi_ins: Vec, - midi_outs: Vec, - ) -> Self { - Self { - clock, tracks, scenes, midi_ins, midi_outs, - jack: jack.clone(), - name: name.unwrap_or_default(), - color: ItemTheme::random(), - selection: Selection::TrackClip { track: 0, scene: 0 }, - ..Default::default() - } - } - - /// Width of display - pub fn w (&self) -> u16 { - self.size.w() as u16 - } - - /// Width allocated for sidebar. - pub fn w_sidebar (&self, is_editing: bool) -> u16 { - self.w() / if is_editing { 16 } else { 8 } as u16 - } - - /// Width available to display tracks. - pub fn w_tracks_area (&self, is_editing: bool) -> u16 { - self.w().saturating_sub(self.w_sidebar(is_editing)) - } - - /// Height of display - pub fn h (&self) -> u16 { - self.size.h() as u16 - } - - /// Height taken by visible device slots. - pub fn h_devices (&self) -> u16 { - 2 - //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) - } - - /// Add multiple tracks - #[cfg(feature = "track")] pub fn tracks_add ( - &mut self, - count: usize, width: Option, - mins: &[Connect], mouts: &[Connect], - ) -> Usually<()> { - let track_color_1 = ItemColor::random(); - let track_color_2 = ItemColor::random(); - for i in 0..count { - let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); - let track = self.track_add(None, Some(color), mins, mouts)?.1; - if let Some(width) = width { - track.width = width; - } - } - Ok(()) - } - - /// Add a track - #[cfg(feature = "track")] pub fn track_add ( - &mut self, - name: Option<&str>, color: Option, - mins: &[Connect], mouts: &[Connect], - ) -> Usually<(usize, &mut Track)> { - let name: Arc = name.map_or_else( - ||format!("trk{:02}", self.track_last).into(), - |x|x.to_string().into() - ); - self.track_last += 1; - let track = Track { - width: (name.len() + 2).max(12), - color: color.unwrap_or_else(ItemTheme::random), - sequencer: Sequencer::new( - &format!("{name}"), - self.jack(), - Some(self.clock()), - None, - mins, - mouts - )?, - name, - ..Default::default() - }; - self.tracks_mut().push(track); - let len = self.tracks().len(); - let index = len - 1; - for scene in self.scenes_mut().iter_mut() { - while scene.clips.len() < len { - scene.clips.push(None); - } - } - Ok((index, &mut self.tracks_mut()[index])) - } - - #[cfg(feature = "track")] pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw + '_ { - south( - h_exact(1, self.view_inputs_header()), - thunk(|to: &mut Tui|{ - for (index, port) in self.midi_ins().iter().enumerate() { - x_push(index as u16 * 10, h_exact(1, self.view_inputs_row(port))).draw(to); - } - todo!() - }) - ) - } - - #[cfg(feature = "track")] fn view_inputs_header (&self) -> impl Draw + '_ { - east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), - west(w_exact(4, button_2("I", "+", false)), thunk(move|to: &mut Tui|{ - for (_index, track, x1, _x2) in self.tracks_with_sizes() { - #[cfg(feature = "track")] - x_push(x1 as u16, bg(track.color.dark.term, origin_w(w_exact(track.width as u16, east!( - either(track.sequencer.monitoring, fg(Green, "mon "), "mon "), - either(track.sequencer.recording, fg(Red, "rec "), "rec "), - either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "), - ))))).draw(to); - } - todo!() - }))) - } - - #[cfg(feature = "track")] fn view_inputs_row (&self, port: &MidiInput) -> impl Draw { - east(w_exact(20, origin_w(east(" ● ", bold(true, fg(Rgb(255,255,255), port.port_name()))))), - west(w_exact(4, ()), thunk(move|to: &mut Tui|{ - for (_index, track, _x1, _x2) in self.tracks_with_sizes() { - #[cfg(feature = "track")] - bg(track.color.darker.term, origin_w(w_exact(track.width as u16, east!( - either(track.sequencer.monitoring, fg(Green, " ● "), " · "), - either(track.sequencer.recording, fg(Red, " ● "), " · "), - either(track.sequencer.overdub, fg(Yellow, " ● "), " · "), - )))).draw(to); - } - todo!() - }))) - } - - #[cfg(feature = "track")] pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw { - let mut h = 1; - - for output in self.midi_outs().iter() { - h += 1 + output.connections.len(); - } - - let h = h as u16; - - let list = south( - h_exact(1, w_full(origin_w(button_3( - "o", "utput", format!("{}", self.midi_outs.len()), false - )))), - h_exact(h - 1, wh_full(origin_nw(thunk(|to: &mut Tui|{ - for (_index, port) in self.midi_outs().iter().enumerate() { - h_exact(1,w_full(east( - origin_w(east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name())))), - w_full(origin_e(format!("{}/{} ", - port.port().get_connections().len(), - port.connections.len())))))).draw(to); - for (index, conn) in port.connections.iter().enumerate() { - h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info())))) - .draw(to); - } - } - todo!(); - })))) - ); - - h_exact(h, view_track_row_section(theme, list, button_2("O", "+", false), - bg(theme.darker.term, origin_w(w_full( - thunk(|to: &mut Tui|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - w_exact(track_width(index, track), - thunk(|to: &mut Tui|{ - h_exact(1, origin_w(east( - either(true, fg(Green, "play "), "play "), - either(false, fg(Yellow, "solo "), "solo "), - ))).draw(to); - for (_index, port) in self.midi_outs().iter().enumerate() { - h_exact(1, origin_w(east( - either(true, fg(Green, " ● "), " · "), - either(false, fg(Yellow, " ● "), " · "), - ))).draw(to); - for (_index, _conn) in port.connections.iter().enumerate() { - h_exact(1, w_full("")).draw(to); - } - } - todo!() - }) - ).draw(to); - } - todo!() - }) - ))) - )) - } - #[cfg(feature = "track")] pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw { - let mut h = 2u16; - for track in self.tracks().iter() { - h = h.max(track.devices.len() as u16 * 2); - } - let tracks = ||self.tracks_with_sizes(); - let track = move|(_, track, _x1, _x2), index| wh_exact( - Some(track_width(index, track)), - Some(h + 1), - bg(track.color.dark.term, origin_nw(iter_south(move||0..h, - |_, _index|wh_exact(Some(track.width as u16), Some(2), - fg_bg( - ItemTheme::G[32].lightest.term, - ItemTheme::G[32].dark.term, - origin_nw(format!(" · {}", "--")) - ) - ))))); - view_track_row_section(theme, - button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), - button_2("D", "+", false), - iter(tracks, track) - ) - } - - /// Put a clip in a slot - #[cfg(feature = "clip")] pub fn clip_put ( - &mut self, track: usize, scene: usize, clip: Option>> - ) -> Option>> { - let old = self.scenes[scene].clips[track].clone(); - self.scenes[scene].clips[track] = clip; - old - } - - /// Change the color of a clip, returning the previous one - #[cfg(feature = "clip")] pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) - -> Option - { - self.scenes[scene].clips[track].as_ref().map(|clip|{ - let mut clip = clip.write().unwrap(); - let old = clip.color.clone(); - clip.color = color.clone(); - panic!("{color:?} {old:?}"); - //old - }) - } - - /// Toggle looping for the active clip - #[cfg(feature = "clip")] pub fn toggle_loop (&mut self) { - if let Some(clip) = self.selected_clip() { - clip.write().unwrap().toggle_loop() - } - } - - /// Get the first sampler of the active track - #[cfg(feature = "sampler")] pub fn sampler (&self) -> Option<&Sampler> { - self.selected_track()?.sampler(0) - } - - /// Get the first sampler of the active track - #[cfg(feature = "sampler")] pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { - self.selected_track_mut()?.sampler_mut(0) - } - -} -impl ScenesView for Arrangement { - fn h_scenes (&self) -> u16 { - (self.size.h() as u16).saturating_sub(20) - } - fn w_side (&self) -> u16 { - (self.size.w() as u16 * 2 / 10).max(20) - } - fn w_mid (&self) -> u16 { - (self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40) - } -} -impl HasClipsSize for Arrangement { - fn clips_size (&self) -> &Size { &self.size_inner } -} - -pub type SceneWith<'a, T> = - (usize, &'a Scene, usize, usize, T); - -def_command!(SceneCommand: |scene: Scene| { - SetSize { size: usize } => { todo!() }, - SetZoom { size: usize } => { todo!() }, - SetName { name: Arc } => - swap_value(&mut scene.name, name, |name|Self::SetName{name}), - SetColor { color: ItemTheme } => - swap_value(&mut scene.color, color, |color|Self::SetColor{color}), -}); - -def_command!(TrackCommand: |track: Track| { - Stop => { track.sequencer.enqueue_next(None); Ok(None) }, - SetMute { mute: Option } => todo!(), - SetSolo { solo: Option } => todo!(), - SetSize { size: usize } => todo!(), - SetZoom { zoom: usize } => todo!(), - SetName { name: Arc } => - swap_value(&mut track.name, name, |name|Self::SetName { name }), - SetColor { color: ItemTheme } => - swap_value(&mut track.color, color, |color|Self::SetColor { color }), - SetRec { rec: Option } => - toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }), - SetMon { mon: Option } => - toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }), -}); - -def_command!(ClipCommand: |clip: MidiClip| { - SetColor { color: Option } => { - //(SetColor [t: usize, s: usize, c: ItemTheme] - //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); - //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) - todo!() - }, - SetLoop { looping: Option } => { - //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) - //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) - todo!() - } -}); diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index 415f99b8..00000000 --- a/src/config.rs +++ /dev/null @@ -1,92 +0,0 @@ -use crate::{*, bind::*, mode::*, view::*}; - -/// Configuration: mode, view, and bind definitions. -/// -/// ``` -/// let config = tek::Config::default(); -/// ``` -/// -/// ``` -/// // Some dizzle. -/// // What indentation to use here lol? -/// let source = stringify!((mode :menu (name Menu) -/// (info Mode selector.) (keys :axis/y :confirm) -/// (view (bg (g 0) (bsp/s :ports/out -/// (bsp/n :ports/in -/// (bg (g 30) (bsp/s (fixed/y 7 :logo) -/// (fill :dialog/menu))))))))); -/// // Add this definition to the config and try to load it. -/// // A "mode" is basically a state machine -/// // with associated input and output definitions. -/// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap(); -/// ``` -#[derive(Default, Debug)] pub struct Config { - /// XDG base directories of running user. - pub dirs: BaseDirectories, - /// Active collection of interaction modes. - pub modes: Modes, - /// Active collection of event bindings. - pub binds: Binds, - /// Active collection of view definitions. - pub views: Views, -} -impl Config { - const CONFIG_DIR: &'static str = "tek"; - const CONFIG_SUB: &'static str = "v0"; - const CONFIG: &'static str = "tek.edn"; - const DEFAULTS: &'static str = include_str!("./tek.edn"); - /// Create a new app configuration from a set of XDG base directories, - pub fn new (dirs: Option) -> Self { - let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); - let dirs = dirs.unwrap_or_else(default); - Self { dirs, ..Default::default() } - } - /// Write initial contents of configuration. - pub fn init (&mut self) -> Usually<()> { - self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{ - cfgs.add(&dsl)?; - Ok(()) - })?; - Ok(()) - } - /// Write initial contents of a configuration file. - pub fn init_one ( - &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> - ) -> Usually<()> { - if self.dirs.find_config_file(path).is_none() { - //println!("Creating {path:?}"); - std::fs::write(self.dirs.place_config_file(path)?, defaults)?; - } - Ok(if let Some(path) = self.dirs.find_config_file(path) { - //println!("Loading {path:?}"); - let src = std::fs::read_to_string(&path)?; - src.as_str().each(move|item|each(self, item))?; - } else { - return Err(format!("{path}: not found").into()) - }) - } - /// Add statements to configuration from [Dsl] source. - pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> { - dsl.each(|item|self.add_one(item))?; - Ok(self) - } - fn add_one (&self, item: impl Language) -> Usually<()> { - if let Some(expr) = item.expr()? { - let head = expr.head()?; - let tail = expr.tail()?; - let name = tail.head()?; - let body = tail.tail()?; - //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); - match head { - Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?, - Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, - Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, - _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) - } - Ok(()) - } else { - return Err(format!("Config::load: expected expr, got: {item:?}").into()) - } - } -} - diff --git a/src/device.rs b/src/device.rs index c3ea587d..83cc1ad3 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,30 +1,7 @@ use crate::*; -use ConnectName::*; -use ConnectScope::*; -use ConnectStatus::*; def_command!(DeviceCommand: |device: Device| {}); -def_command!(MidiInputCommand: |port: MidiInput| { - Close => todo!(), - Connect { midi_out: Arc } => todo!(), -}); - -def_command!(MidiOutputCommand: |port: MidiOutput| { - Close => todo!(), - Connect { midi_in: Arc } => todo!(), -}); - -def_command!(AudioInputCommand: |port: AudioInput| { - Close => todo!(), - Connect { audio_out: Arc } => todo!(), -}); - -def_command!(AudioOutputCommand: |port: AudioOutput| { - Close => todo!(), - Connect { audio_in: Arc } => todo!(), -}); - impl Device { pub fn name (&self) -> &str { match self { @@ -84,528 +61,6 @@ impl Device { /// Some sort of wrapper? pub struct DeviceAudio<'a>(pub &'a mut Device); -/// Audio input port. -#[derive(Debug)] pub struct AudioInput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -/// Audio output port. -#[derive(Debug)] pub struct AudioOutput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -/// MIDI input port. -#[derive(Debug)] pub struct MidiInput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of currently held notes. - pub held: Arc>, - /// List of ports to connect to. - pub connections: Vec, -} - -/// MIDI output port. -#[derive(Debug)] pub struct MidiOutput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, - /// List of currently held notes. - pub held: Arc>, - /// Buffer - pub note_buffer: Vec, - /// Buffer - pub output_buffer: Vec>>, -} - -#[derive(Clone, Debug, PartialEq)] pub enum ConnectName { - /** Exact match */ - Exact(Arc), - /** Match regular expression */ - RegExp(Arc), -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { - One, - All -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { - Missing, - Disconnected, - Connected, - Mismatch, -} - -/// Port connection manager. -/// -/// ``` -/// let connect = tek::Connect::default(); -/// ``` -#[derive(Clone, Debug, Default)] pub struct Connect { - pub name: Option, - pub scope: Option, - pub status: Arc, Arc, ConnectStatus)>>>, - pub info: Arc, -} - -impl Connect { - pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) - -> Vec - { - let mut connections = vec![]; - for port in exact.iter() { connections.push(Self::exact(port)) } - for port in re.iter() { connections.push(Self::regexp(port)) } - for port in re_all.iter() { connections.push(Self::regexp_all(port)) } - connections - } - /// Connect to this exact port - pub fn exact (name: impl AsRef) -> Self { - let info = format!("=:{}", name.as_ref()).into(); - let name = Some(Exact(name.as_ref().into())); - Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn regexp (name: impl AsRef) -> Self { - let info = format!("~:{}", name.as_ref()).into(); - let name = Some(RegExp(name.as_ref().into())); - Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn regexp_all (name: impl AsRef) -> Self { - let info = format!("+:{}", name.as_ref()).into(); - let name = Some(RegExp(name.as_ref().into())); - Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn info (&self) -> Arc { - format!(" ({}) {} {}", { - let status = self.status.read().unwrap(); - let mut ok = 0; - for (_, _, state) in status.iter() { - if *state == Connected { - ok += 1 - } - } - format!("{ok}/{}", status.len()) - }, match self.scope { - None => "x", - Some(One) => " ", - Some(All) => "*", - }, match &self.name { - None => format!("x"), - Some(Exact(name)) => format!("= {name}"), - Some(RegExp(name)) => format!("~ {name}"), - }).into() - } -} - -impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl> RegisterPorts for J { - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiInput::new(self.jack(), name, connect) - } - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiOutput::new(self.jack(), name, connect) - } - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioInput::new(self.jack(), name, connect) - } - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioOutput::new(self.jack(), name, connect) - } -} - -/// May create new MIDI input ports. -pub trait AddMidiIn { - fn midi_in_add (&mut self) -> Usually<()>; -} - -/// May create new MIDI output ports. -pub trait AddMidiOut { - fn midi_out_add (&mut self) -> Usually<()>; -} - -pub trait RegisterPorts: HasJack<'static> { - /// Register a MIDI input port. - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register a MIDI output port. - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio input port. - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio output port. - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; -} - -pub trait JackPort: HasJack<'static> { - - type Port: PortSpec + Default; - - type Pair: PortSpec + Default; - - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized; - - fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { - jack.with_client(|c|c.register_port::(name.as_ref(), Default::default())) - .map_err(|e|e.into()) - } - - fn port_name (&self) -> &Arc; - - fn connections (&self) -> &[Connect]; - - fn port (&self) -> &Port; - - fn port_mut (&mut self) -> &mut Port; - - fn into_port (self) -> Port where Self: Sized; - - fn close (self) -> Usually<()> where Self: Sized { - let jack = self.jack().clone(); - Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) - } - - fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { - self.with_client(|c|c.ports(re_name, re_type, flags)) - } - - fn port_by_id (&self, id: u32) -> Option> { - self.with_client(|c|c.port_by_id(id)) - } - - fn port_by_name (&self, name: impl AsRef) -> Option> { - self.with_client(|c|c.port_by_name(name.as_ref())) - } - - fn connect_to_matching <'k> (&'k self) -> Usually<()> { - for connect in self.connections().iter() { - match &connect.name { - Some(Exact(name)) => { - *connect.status.write().unwrap() = self.connect_exact(name)?; - }, - Some(RegExp(re)) => { - *connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?; - }, - _ => {}, - }; - } - Ok(()) - } - - fn connect_exact <'k> (&'k self, name: &str) -> - Usually, Arc, ConnectStatus)>> - { - self.with_client(move|c|{ - let mut status = vec![]; - for port in c.ports(None, None, PortFlags::empty()).iter() { - if port.as_str() == &*name { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected { - break - } - } - } - } - Ok(status) - }) - } - - fn connect_regexp <'k> ( - &'k self, re: &str, scope: Option - ) -> Usually, Arc, ConnectStatus)>> { - self.with_client(move|c|{ - let mut status = vec![]; - let ports = c.ports(Some(&re), None, PortFlags::empty()); - for port in ports.iter() { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected && scope == Some(One) { - break - } - } - } - Ok(status) - }) - } - - /** Connect to a matching port by name. */ - fn connect_to_name (&self, name: impl AsRef) -> Usually { - self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { - self.connect_to_unowned(port) - } else { - Ok(Missing) - }) - } - - /** Connect to a matching port by reference. */ - fn connect_to_unowned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } - - /** Connect to an owned matching port by reference. */ - fn connect_to_owned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } -} - -impl JackPort for MidiInput { - type Port = MidiIn; - type Pair = MidiOut; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec(), - held: Arc::new(RwLock::new([false;128])) - }; - port.connect_to_matching()?; - Ok(port) - } -} - -impl JackPort for MidiOutput { - type Port = MidiOut; - type Pair = MidiIn; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self::register(jack, name)?; - let jack = jack.clone(); - let name = name.as_ref().into(); - let connections = connect.to_vec(); - let port = Self { - jack, - port, - name, - connections, - held: Arc::new([false;128].into()), - note_buffer: vec![0;8], - output_buffer: vec![vec![];65536], - }; - port.connect_to_matching()?; - Ok(port) - } -} - -impl View for MidiInput { - fn view (&self) -> impl Draw { - "TODO: MIDI IN" - } -} - -impl View for MidiOutput { - fn view (&self) -> impl Draw { - "TODO: MIDI OUT" - } -} - -impl MidiOutput { - /// Clear the section of the output buffer that we will be using, - /// emitting "all notes off" at start of buffer if requested. - pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { - let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); - for frame in &mut self.output_buffer[0..n_frames] { - frame.clear(); - } - if reset { - all_notes_off(&mut self.output_buffer); - } - } - /// Write a note to the output buffer - pub fn buffer_write <'a> ( - &'a mut self, - sample: usize, - event: LiveEvent, - ) { - self.note_buffer.fill(0); - event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); - self.output_buffer[sample].push(self.note_buffer.clone()); - // Update the list of currently held notes. - if let LiveEvent::Midi { ref message, .. } = event { - update_keys(&mut*self.held.write().unwrap(), message); - } - } - /// Write a chunk of MIDI data from the output buffer to the output port. - pub fn buffer_emit (&mut self, scope: &ProcessScope) { - let samples = scope.n_frames() as usize; - let mut writer = self.port.writer(scope); - for (time, events) in self.output_buffer.iter().enumerate().take(samples) { - for bytes in events.iter() { - writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ - panic!("Failed to write MIDI data: {bytes:?}"); - }); - } - } - } -} -impl MidiInput { - pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { - parse_midi_input(self.port().iter(scope)) - } -} -impl> + AsMut>> HasMidiIns for T { - fn midi_ins (&self) -> &Vec { self.as_ref() } - fn midi_ins_mut (&mut self) -> &mut Vec { self.as_mut() } -} -impl> + AsMut>> HasMidiOuts for T { - fn midi_outs (&self) -> &Vec { self.as_ref() } - fn midi_outs_mut (&mut self) -> &mut Vec { self.as_mut() } -} -impl> AddMidiIn for T { - fn midi_in_add (&mut self) -> Usually<()> { - let index = self.midi_ins().len(); - let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?; - self.midi_ins_mut().push(port); - Ok(()) - } -} -/// Trail for thing that may gain new MIDI ports. -impl> AddMidiOut for T { - fn midi_out_add (&mut self) -> Usually<()> { - let index = self.midi_outs().len(); - let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?; - self.midi_outs_mut().push(port); - Ok(()) - } -} - -impl JackPort for AudioInput { - type Port = AudioIn; - type Pair = AudioOut; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec() - }; - port.connect_to_matching()?; - Ok(port) - } -} - -impl JackPort for AudioOutput { - type Port = AudioOut; - type Pair = AudioIn; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec() - }; - port.connect_to_matching()?; - Ok(port) - } -} - impl_audio!(|self: DeviceAudio<'a>, client, scope|{ use Device::*; match self.0 { @@ -635,48 +90,26 @@ impl> + AsMut>> HasDevices for T { self.as_mut() } } -/// Trait for thing that may receive MIDI. -pub trait HasMidiIns { - fn midi_ins (&self) -> &Vec; - fn midi_ins_mut (&mut self) -> &mut Vec; - /// Collect MIDI input from app ports (TODO preallocate large buffers) - fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> { - self.midi_ins().iter() - .map(|port|port.port().iter(scope) - .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) - .collect::>()) - .collect::>() + +pub trait HasDevices: AsRef> + AsMut> { + fn devices (&self) -> &Vec { + self.as_ref() } - fn midi_ins_with_sizes <'a> (&'a self) -> - impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a - { - let mut y = 0; - self.midi_ins().iter().enumerate().map(move|(i, input)|{ - let height = 1 + input.connections().len(); - let data = (i, input.port_name(), input.connections(), y, y + height); - y += height; - data - }) - } -} -/// Trait for thing that may output MIDI. -pub trait HasMidiOuts { - fn midi_outs (&self) -> &Vec; - fn midi_outs_mut (&mut self) -> &mut Vec; - fn midi_outs_with_sizes <'a> (&'a self) -> - impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a - { - let mut y = 0; - self.midi_outs().iter().enumerate().map(move|(i, output)|{ - let height = 1 + output.connections().len(); - let data = (i, output.port_name(), output.connections(), y, y + height); - y += height; - data - }) - } - fn midi_outs_emit (&mut self, scope: &ProcessScope) { - for port in self.midi_outs_mut().iter_mut() { - port.buffer_emit(scope) - } + fn devices_mut (&mut self) -> &mut Vec { + self.as_mut() } } + +pub mod arrange; pub use self::arrange::*; +pub mod browse; pub use self::browse::*; +pub mod clock; pub use self::clock::*; +pub mod dialog; pub use self::dialog::*; +pub mod editor; pub use self::editor::*; +pub mod menu; pub use self::menu::*; +pub mod mix; pub use self::mix::*; +pub mod port; pub use self::port::*; +pub mod sample; pub use self::sample::*; +pub mod sequence; pub use self::sequence::*; + +#[cfg(feature = "plugin")] pub mod plugin; +#[cfg(feature = "plugin")] pub use self::plugin::*; diff --git a/src/device/arrange.rs b/src/device/arrange.rs new file mode 100644 index 00000000..8c3f51d8 --- /dev/null +++ b/src/device/arrange.rs @@ -0,0 +1,123 @@ +use crate::*; + +/// Arranger. +/// +/// ``` +/// let arranger = tek::Arrangement::default(); +/// ``` +#[derive(Default, Debug)] pub struct Arrangement { + /// Project name. + pub name: Arc, + /// Base color. + pub color: ItemTheme, + /// JACK client handle. + pub jack: Jack<'static>, + /// FIXME a render of the project arrangement, redrawn on update. + /// TODO rename to "render_cache" or smth + pub arranger: Arc>, + /// Display size + pub size: Size, + /// Display size of clips area + pub size_inner: Size, + /// Source of time + #[cfg(feature = "clock")] pub clock: Clock, + /// Allows one MIDI clip to be edited + #[cfg(feature = "editor")] pub editor: Option, + /// List of global midi inputs + #[cfg(feature = "port")] pub midi_ins: Vec, + /// List of global midi outputs + #[cfg(feature = "port")] pub midi_outs: Vec, + /// List of global audio inputs + #[cfg(feature = "port")] pub audio_ins: Vec, + /// List of global audio outputs + #[cfg(feature = "port")] pub audio_outs: Vec, + /// Selected UI element + #[cfg(feature = "select")] pub selection: Selection, + /// Last track number (to avoid duplicate port names) + #[cfg(feature = "track")] pub track_last: usize, + /// List of tracks + #[cfg(feature = "track")] pub tracks: Vec, + /// Scroll offset of tracks + #[cfg(feature = "track")] pub track_scroll: usize, + /// List of scenes + #[cfg(feature = "scene")] pub scenes: Vec, + /// Scroll offset of scenes + #[cfg(feature = "scene")] pub scene_scroll: usize, +} + +impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl_has!(Jack<'static>: |self: Arrangement| self.jack); +impl_has!(Size: |self: Arrangement| self.size); +impl_has!(Vec: |self: Arrangement| self.midi_ins); +impl_has!(Vec: |self: Arrangement| self.midi_outs); +impl_has!(Clock: |self: Arrangement| self.clock); +impl_has!(Selection: |self: Arrangement| self.selection); +impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref()); +impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut()); + +impl Arrangement { + + /// Create a new arrangement. + pub fn new ( + jack: &Jack<'static>, + name: Option>, + clock: Clock, + tracks: Vec, + scenes: Vec, + midi_ins: Vec, + midi_outs: Vec, + ) -> Self { + Self { + clock, tracks, scenes, midi_ins, midi_outs, + jack: jack.clone(), + name: name.unwrap_or_default(), + color: ItemTheme::random(), + selection: Selection::TrackClip { track: 0, scene: 0 }, + ..Default::default() + } + } + + /// Width of display + pub fn w (&self) -> u16 { + self.size.w() as u16 + } + + /// Width allocated for sidebar. + pub fn w_sidebar (&self, is_editing: bool) -> u16 { + self.w() / if is_editing { 16 } else { 8 } as u16 + } + + /// Width available to display tracks. + pub fn w_tracks_area (&self, is_editing: bool) -> u16 { + self.w().saturating_sub(self.w_sidebar(is_editing)) + } + + /// Height of display + pub fn h (&self) -> u16 { + self.size.h() as u16 + } + + /// Height taken by visible device slots. + pub fn h_devices (&self) -> u16 { + 2 + //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) + } + + /// Get the first sampler of the active track + #[cfg(feature = "sampler")] + pub fn sampler (&self) -> Option<&Sampler> { + self.selected_track()?.sampler(0) + } + + /// Get the first sampler of the active track + #[cfg(feature = "sampler")] + pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { + self.selected_track_mut()?.sampler_mut(0) + } + +} + +#[cfg(feature = "clip")] mod clip; #[cfg(feature = "clip")] pub use self::clip::*; +#[cfg(feature = "scene")] mod scene; #[cfg(feature = "scene")] pub use self::scene::*; +#[cfg(feature = "track")] mod track; #[cfg(feature = "track")] pub use self::track::*; +#[cfg(feature = "select")] mod select; #[cfg(feature = "select")] pub use self::select::*; diff --git a/src/device/arrange/clip.rs b/src/device/arrange/clip.rs new file mode 100644 index 00000000..c2b4cad0 --- /dev/null +++ b/src/device/arrange/clip.rs @@ -0,0 +1,90 @@ +use crate::*; + +/// TODO: Preserve the generic passthru syntax; +/// remove this macro (only used twice) and potentially the trait. +#[macro_export] macro_rules! impl_has_clips { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { + fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { + $cb.read().unwrap() + } + fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { + $cb.write().unwrap() + } + } + } +} + +#[macro_export] macro_rules! has_clip { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? { + fn clip (&$self) -> Option>> { $cb } + } + } +} + +pub trait ClipsView: TracksView + ScenesView { + /// Draw clips per scene + fn view_scenes_clips <'a> (&'a self) -> impl Draw + 'a { + crate::view::view_scenes_clips( + self.tracks_with_sizes(), self.clips_size() + ) + } + /// Draw clips for tracks + fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Draw + 'a { + crate::view::view_track_clips( + self.scenes_with_sizes(), self.selection(), self.editor(), track_index, track + ) + } +} + +impl HasClipsSize for Arrangement { + fn clips_size (&self) -> &Size { &self.size_inner } +} + +def_command!(ClipCommand: |clip: MidiClip| { + SetColor { color: Option } => { + //(SetColor [t: usize, s: usize, c: ItemTheme] + //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); + //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) + todo!() + }, + SetLoop { looping: Option } => { + //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) + //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) + todo!() + } +}); + +impl Arrangement { + /// Put a clip in a slot + pub fn clip_put ( + &mut self, track: usize, scene: usize, clip: Option>> + ) -> Option>> { + let old = self.scenes[scene].clips[track].clone(); + self.scenes[scene].clips[track] = clip; + old + } + + /// Change the color of a clip, returning the previous one + pub fn clip_set_color ( + &self, track: usize, scene: usize, color: ItemTheme + ) -> Option { + self.scenes[scene].clips[track].as_ref().map(|clip|{ + let mut clip = clip.write().unwrap(); + let old = clip.color.clone(); + clip.color = color.clone(); + panic!("{color:?} {old:?}"); + //old + }) + } + + /// Toggle looping for the active clip + pub fn toggle_loop (&mut self) { + if let Some(clip) = self.selected_clip() { + clip.write().unwrap().toggle_loop() + } + } +} + +pub trait HasClipsSize { fn clips_size (&self) -> &Size; } diff --git a/src/device/arrange/scene.rs b/src/device/arrange/scene.rs new file mode 100644 index 00000000..be16abfb --- /dev/null +++ b/src/device/arrange/scene.rs @@ -0,0 +1,167 @@ +use crate::*; + +/// A scene consists of a set of clips to play together. +/// +/// ``` +/// let scene: tek::Scene = Default::default(); +/// let _ = scene.pulses(); +/// let _ = scene.is_playing(&[]); +/// ``` +#[derive(Debug, Default)] pub struct Scene { + /// Name of scene + pub name: Arc, + /// Identifying color of scene + pub color: ItemTheme, + /// Clips in scene, one per track + pub clips: Vec>>>, +} + +impl Scene { + /// Returns the pulse length of the longest clip in the scene + pub fn pulses (&self) -> usize { + self.clips.iter().fold(0, |a, p|{ + a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) + }) + } + /// Returns true if all clips in the scene are + /// currently playing on the given collection of tracks. + pub fn is_playing (&self, tracks: &[Track]) -> bool { + self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() + .all(|(track_index, clip)|match clip { + Some(c) => tracks + .get(track_index) + .map(|track|{ + if let Some((_, Some(clip))) = track.sequencer().play_clip() { + *clip.read().unwrap() == *c.read().unwrap() + } else { + false + } + }) + .unwrap_or(false), + None => true + }) + } + pub fn clip (&self, index: usize) -> Option<&Arc>> { + match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } + } + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } +} + +pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync { + /// Default scene height. + const H_SCENE: usize = 2; + /// Default editor height. + const H_EDITOR: usize = 15; + fn h_scenes (&self) -> u16; + fn w_side (&self) -> u16; + fn w_mid (&self) -> u16; + + fn view_scenes_names (&self) -> impl Draw { + crate::view::view_scenes_names( + self.scenes_with_sizes() + ) + } + fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Draw + 'a { + crate::view::view_scene_name( + self.selected(), self.editor(), index, scene + ) + } + fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> { + let mut y = 0; + self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{ + let height = if self.selection().scene() == Some(s) && self.editor().is_some() { + 8 + } else { + Self::H_SCENE + }; + if y + height <= self.clips_size().h() as usize { + let data = (s, scene, y, y + height); + y += height; + Some(data) + } else { + None + } + }) + } +} + +pub trait HasSceneScroll: HasScenes { + fn scene_scroll (&self) -> usize; +} + +pub trait HasScene: AsRefOpt + AsMutOpt { + fn scene_mut (&mut self) -> Option<&mut Scene> { self.as_mut_opt() } + fn scene (&self) -> Option<&Scene> { self.as_ref_opt() } +} + +pub trait HasScenes: AsRef> + AsMut> { + fn scenes (&self) -> &Vec { self.as_ref() } + fn scenes_mut (&mut self) -> &mut Vec { self.as_mut() } + /// Generate the default name for a new scene + fn scene_default_name (&self) -> Arc { format!("s{:3>}", self.scenes().len() + 1).into() } + fn scene_longest_name (&self) -> usize { self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) } + /// Add multiple scenes + fn scenes_add (&mut self, n: usize) -> Usually<()> where Self: HasTracks { + let scene_color_1 = ItemColor::random(); + let scene_color_2 = ItemColor::random(); + for i in 0..n { + let _ = self.scene_add(None, Some( + scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() + ))?; + } + Ok(()) + } + /// Add a scene + fn scene_add (&mut self, name: Option<&str>, color: Option) + -> Usually<(usize, &mut Scene)> where Self: HasTracks + { + let scene = Scene { + name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), + clips: vec![None;self.tracks().len()], + color: color.unwrap_or_else(ItemTheme::random), + }; + self.scenes_mut().push(scene); + let index = self.scenes().len() - 1; + Ok((index, &mut self.scenes_mut()[index])) + } +} + +impl HasSceneScroll for Arrangement { + fn scene_scroll (&self) -> usize { self.scene_scroll } +} + +impl ScenesView for Arrangement { + fn h_scenes (&self) -> u16 { + (self.size.h() as u16).saturating_sub(20) + } + fn w_side (&self) -> u16 { + (self.size.w() as u16 * 2 / 10).max(20) + } + fn w_mid (&self) -> u16 { + (self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40) + } +} + +pub type SceneWith<'a, T> = + (usize, &'a Scene, usize, usize, T); + +def_command!(SceneCommand: |scene: Scene| { + SetSize { size: usize } => { todo!() }, + SetZoom { size: usize } => { todo!() }, + SetName { name: Arc } => + swap_value(&mut scene.name, name, |name|Self::SetName{name}), + SetColor { color: ItemTheme } => + swap_value(&mut scene.color, color, |color|Self::SetColor{color}), +}); + +#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt()); +#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt()); +#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene()); +#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut()); +impl ClipsView for T {} +impl_has!(Vec: |self: Arrangement| self.scenes); +impl>+AsMut>> HasScenes for T {} +impl+AsMutOpt+Send+Sync> HasScene for T {} diff --git a/src/select.rs b/src/device/arrange/select.rs similarity index 100% rename from src/select.rs rename to src/device/arrange/select.rs diff --git a/src/device/arrange/track.rs b/src/device/arrange/track.rs new file mode 100644 index 00000000..e80a64f7 --- /dev/null +++ b/src/device/arrange/track.rs @@ -0,0 +1,339 @@ +use crate::*; + +/// A track consists of a sequencer and zero or more devices chained after it. +/// +/// ``` +/// let track: tek::Track = Default::default(); +/// ``` +#[derive(Debug, Default)] pub struct Track { + /// Name of track + pub name: Arc, + /// Identifying color of track + pub color: ItemTheme, + /// Preferred width of track column + pub width: usize, + /// MIDI sequencer state + pub sequencer: Sequencer, + /// Device chain + pub devices: Vec, +} + +impl Track { + /// Create a new track with only the default [Sequencer]. + pub fn new ( + name: &impl AsRef, + color: Option, + jack: &Jack<'static>, + clock: Option<&Clock>, + clip: Option<&Arc>>, + midi_from: &[Connect], + midi_to: &[Connect], + ) -> Usually { + Ok(Self { + name: name.as_ref().into(), + color: color.unwrap_or_default(), + sequencer: Sequencer::new( + format!("{}/sequencer", name.as_ref()), jack, clock, clip, midi_from, midi_to + )?, + ..Default::default() + }) + } + pub fn audio_ins (&self) -> &[AudioInput] { + self.devices.first().map(|x|x.audio_ins()).unwrap_or_default() + } + pub fn audio_outs (&self) -> &[AudioOutput] { + self.devices.last().map(|x|x.audio_outs()).unwrap_or_default() + } + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } + + pub fn per <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a + ) -> impl Draw + 'a { + per(tracks, callback) + } + + /// Create a new track connecting the [Sequencer] to a [Sampler]. + #[cfg(feature = "sampler")] pub fn new_with_sampler ( + name: &impl AsRef, + color: Option, + jack: &Jack<'static>, + clock: Option<&Clock>, + clip: Option<&Arc>>, + midi_from: &[Connect], + midi_to: &[Connect], + audio_from: &[&[Connect];2], + audio_to: &[&[Connect];2], + ) -> Usually { + let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?; + let client_name = jack.with_client(|c|c.name().to_string()); + let port_name = track.sequencer.midi_outs[0].port_name(); + let connect = [Connect::exact(format!("{client_name}:{}", port_name))]; + track.devices.push(Device::Sampler(Sampler::new( + jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to + )?)); + Ok(track) + } + + #[cfg(feature = "sampler")] pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { + for device in self.devices.iter() { + match device { + Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, + _ => {} + } + } + None + } + + #[cfg(feature = "sampler")] pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { + for device in self.devices.iter_mut() { + match device { + Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, + _ => {} + } + } + None + } +} + +impl HasWidth for Track { + const MIN_WIDTH: usize = 9; + fn width_inc (&mut self) { self.width += 1; } + fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } } +} + +pub trait HasTracks: AsRef> + AsMut> { + fn tracks (&self) -> &Vec { self.as_ref() } + fn tracks_mut (&mut self) -> &mut Vec { self.as_mut() } + /// Run audio callbacks for every track and every device + fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control { + for track in self.tracks_mut().iter_mut() { + if Control::Quit == Audio::process(&mut track.sequencer, client, scope) { + return Control::Quit + } + for device in track.devices.iter_mut() { + if Control::Quit == DeviceAudio(device).process(client, scope) { + return Control::Quit + } + } + } + Control::Continue + } + fn track_longest_name (&self) -> usize { + self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) + } + /// Stop all playing clips + fn tracks_stop_all (&mut self) { + for track in self.tracks_mut().iter_mut() { + track.sequencer.enqueue_next(None); + } + } + /// Stop all playing clips + fn tracks_launch (&mut self, clips: Option>>>>) { + if let Some(clips) = clips { + for (clip, track) in clips.iter().zip(self.tracks_mut()) { + track.sequencer.enqueue_next(clip.as_ref()); + } + } else { + for track in self.tracks_mut().iter_mut() { + track.sequencer.enqueue_next(None); + } + } + } + /// Spacing between tracks. + const TRACK_SPACING: usize = 0; +} + +pub trait HasTrack: AsRefOpt + AsMutOpt { + fn track (&self) -> Option<&Track> { self.as_ref_opt() } + fn track_mut (&mut self) -> Option<&mut Track> { self.as_mut_opt() } + + #[cfg(feature = "port")] + fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw + 'a { + crate::view::view_midi_ins_status(theme, self.track()) + } + + #[cfg(feature = "port")] + fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw + '_ { + crate::view::view_midi_outs_status(theme, self.track()) + } + + #[cfg(feature = "port")] + fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw { + crate::view::view_audio_ins_status(theme, self.track()) + } + + #[cfg(feature = "port")] + fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw { + crate::view::view_audio_outs_status(theme, self.track()) + } +} + +pub trait HasTrackScroll: HasTracks { + fn track_scroll (&self) -> usize; +} + +pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { + /// Draw name of each track + fn view_track_names (&self, theme: ItemTheme) -> impl Draw { + crate::view::view_track_names( + self.tracks_with_sizes(), self.tracks().len(), self.scenes().len(), self.selection() + ) + } + /// Draw outputs per track + fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw { + crate::view::view_track_outputs( + theme, self.tracks_with_sizes(), self.midi_outs().iter() + ) + } + /// Draw inputs per track + fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw { + let mut h = 0u16; + for track in self.tracks().iter() { + h = h.max(track.sequencer.midi_ins.len() as u16); + } + crate::view::view_track_inputs(theme, self.tracks_with_sizes(), h) + } + /// Iterate over tracks with their corresponding sizes. + fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { + let _editor_width = self.editor().map(|e|e.size.w()); + let _active_track = self.selection().track(); + let mut x = 0; + let w = self.clips_size().w() as usize; + self.tracks().iter().enumerate().map_while(move |(index, track)|{ + let width = track.width.max(8); + if x + width < w { + let data = (index, track, x, x + width); + x += width + Self::TRACK_SPACING; + Some(data) + } else { + None + } + }) + } + +} + +impl HasTrackScroll for Arrangement { + fn track_scroll (&self) -> usize { self.track_scroll } +} + +def_command!(TrackCommand: |track: Track| { + Stop => { track.sequencer.enqueue_next(None); Ok(None) }, + SetMute { mute: Option } => todo!(), + SetSolo { solo: Option } => todo!(), + SetSize { size: usize } => todo!(), + SetZoom { zoom: usize } => todo!(), + SetName { name: Arc } => + swap_value(&mut track.name, name, |name|Self::SetName { name }), + SetColor { color: ItemTheme } => + swap_value(&mut track.color, color, |color|Self::SetColor { color }), + SetRec { rec: Option } => + toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }), + SetMon { mon: Option } => + toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }), +}); + +impl>+AsMut>> HasTracks for T {} +impl+AsMutOpt+Send+Sync> HasTrack for T {} +impl TracksView for T {} +impl_as_ref!(Vec: |self: App| self.project.as_ref()); +impl_as_mut!(Vec: |self: App| self.project.as_mut()); +#[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt()); +#[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt()); +impl_has!(Vec: |self: Arrangement| self.tracks); +impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track()); +impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut()); + +impl Arrangement { + #[cfg(feature = "track")] + pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw + '_ { + crate::view::view_inputs() + } + + #[cfg(feature = "track")] + pub fn view_inputs_header (&self) -> impl Draw + '_ { + crate::view::view_inputs_header(self.tracks_with_sizes()) + } + + #[cfg(feature = "track")] + pub fn view_inputs_row (&self, port: &MidiInput) -> impl Draw { + crate::view::view_inputs_row(port) + } + + #[cfg(feature = "track")] + pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw { + let mut h = 1; + for output in self.midi_outs().iter() { + h += 1 + output.connections.len(); + } + let h = h as u16; + crate::view::view_outputs(self.tracks_with_sizes()) + } + + #[cfg(feature = "track")] + pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw { + let mut h = 2u16; + for track in self.tracks().iter() { + h = h.max(track.devices.len() as u16 * 2); + } + crate::view::view_track_devices(||self.tracks_with_sizes(), self.track(), h) + } + + /// Add multiple tracks + #[cfg(feature = "track")] pub fn tracks_add ( + &mut self, + count: usize, width: Option, + mins: &[Connect], mouts: &[Connect], + ) -> Usually<()> { + let track_color_1 = ItemColor::random(); + let track_color_2 = ItemColor::random(); + for i in 0..count { + let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); + let track = self.track_add(None, Some(color), mins, mouts)?.1; + if let Some(width) = width { + track.width = width; + } + } + Ok(()) + } + + /// Add a track + #[cfg(feature = "track")] pub fn track_add ( + &mut self, + name: Option<&str>, color: Option, + mins: &[Connect], mouts: &[Connect], + ) -> Usually<(usize, &mut Track)> { + let name: Arc = name.map_or_else( + ||format!("trk{:02}", self.track_last).into(), + |x|x.to_string().into() + ); + self.track_last += 1; + let track = Track { + width: (name.len() + 2).max(12), + color: color.unwrap_or_else(ItemTheme::random), + sequencer: Sequencer::new( + &format!("{name}"), + self.jack(), + Some(self.clock()), + None, + mins, + mouts + )?, + name, + ..Default::default() + }; + self.tracks_mut().push(track); + let len = self.tracks().len(); + let index = len - 1; + for scene in self.scenes_mut().iter_mut() { + while scene.clips.len() < len { + scene.clips.push(None); + } + } + Ok((index, &mut self.tracks_mut()[index])) + } +} diff --git a/src/device/audio.rs b/src/device/audio.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/browse.rs b/src/device/browse.rs similarity index 94% rename from src/browse.rs rename to src/device/browse.rs index 773bfd86..a10f486f 100644 --- a/src/browse.rs +++ b/src/device/browse.rs @@ -24,11 +24,12 @@ def_command!(FileBrowserCommand: |sampler: Sampler|{ pub size: Size, } -pub(crate) struct EntriesIterator<'a> { +pub(crate) struct EntriesIterator<'a, S: Screen> { pub browser: &'a Browse, pub offset: usize, pub length: usize, pub index: usize, + _screen: std::marker::PhantomData } #[derive(Clone, Debug)] pub enum BrowseTarget { @@ -318,12 +319,13 @@ impl Browse { index: 0, length: self.dirs.len() + self.files.len(), browser: self, + _screen: Default::default(), }; iter_south_fixed(1, iterate, item) } } -impl<'a> Iterator for EntriesIterator<'a> { - type Item = (); +impl<'a> Iterator for EntriesIterator<'a, Tui> { + type Item = impl Draw; fn next (&mut self) -> Option { let dirs = self.browser.dirs.len(); let files = self.browser.files.len(); @@ -360,24 +362,36 @@ def_command!(BrowseCommand: |browse: Browse| { def_command!(PoolCommand: |pool: Pool| { // Toggle visibility of pool - Show { visible: bool } => { pool.visible = *visible; Ok(Some(Self::Show { visible: !visible })) }, + Show { visible: bool } => { + pool.visible = *visible; + Ok(Some(Self::Show { visible: !visible })) + }, // Select a clip from the clip pool - Select { index: usize } => { pool.set_clip_index(*index); Ok(None) }, + Select { index: usize } => { + pool.set_clip_index(*index); + Ok(None) + }, // Update the contents of the clip pool - Clip { command: PoolClipCommand } => Ok(command.execute(pool)?.map(|command|Self::Clip{command})), + Clip { command: PoolClipCommand } => Ok( + command.act(pool)?.map(|command|Self::Clip{command}) + ), // Rename a clip - Rename { command: RenameCommand } => Ok(command.delegate(pool, |command|Self::Rename{command})?), + Rename { command: RenameCommand } => Ok( + command.act(pool)?.map(|command|Self::Rename{command}) + ), // Change the length of a clip - Length { command: CropCommand } => Ok(command.delegate(pool, |command|Self::Length{command})?), + Length { command: CropCommand } => Ok( + command.act(pool)?.map(|command|Self::Length{command}) + ), // Import from file Import { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() { - command.delegate(browse, |command|Self::Import{command})? + command.act(browse)?.map(|command|Self::Import{command}) } else { None }), // Export to file Export { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() { - command.delegate(browse, |command|Self::Export{command})? + command.act(browse)?.map(|command|Self::Export{command}) } else { None }), @@ -429,7 +443,7 @@ def_command!(PoolClipCommand: |pool: Pool| { for event in events.iter() { clip.notes[event.0 as usize].push(event.2); } - Ok(Self::Add { index, clip }.execute(pool)?) + Ok(Self::Add { index, clip }.act(pool)?) }, SetName { index: usize, name: Arc } => { let index = *index; diff --git a/src/clock.rs b/src/device/clock.rs similarity index 100% rename from src/clock.rs rename to src/device/clock.rs diff --git a/src/dialog.rs b/src/device/dialog.rs similarity index 99% rename from src/dialog.rs rename to src/device/dialog.rs index 56ae8043..1d861d6d 100644 --- a/src/dialog.rs +++ b/src/device/dialog.rs @@ -103,4 +103,3 @@ impl Dialog { /// FIXME: implement pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() } } - diff --git a/src/editor.rs b/src/device/editor.rs similarity index 99% rename from src/editor.rs rename to src/device/editor.rs index 4720a409..d9aa1dda 100644 --- a/src/editor.rs +++ b/src/device/editor.rs @@ -188,7 +188,7 @@ pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync { } let next_time_area = time_axis * next_time_zoom; if next_time_area >= time_len { - self.time_zoom().set(next_time_zoom); + self.time_zoom().store(next_time_zoom, Relaxed); } else { break } @@ -199,7 +199,7 @@ pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync { } let prev_time_area = time_axis * prev_time_zoom; if prev_time_area <= time_len { - self.time_zoom().set(prev_time_zoom); + self.time_zoom().store(prev_time_zoom, Relaxed); } else { break } @@ -746,7 +746,7 @@ impl MidiViewer for PianoHorizontal { let buf_size = self.buffer_size(&clip); let mut buffer = BigBuffer::from(buf_size); let time_zoom = self.get_time_zoom(); - self.time_len().set(clip.length); + self.time_len().store(clip.length, Relaxed); PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); buffer diff --git a/src/menu.rs b/src/device/menu.rs similarity index 100% rename from src/menu.rs rename to src/device/menu.rs diff --git a/src/device/midi.rs b/src/device/midi.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/mix.rs b/src/device/mix.rs similarity index 99% rename from src/mix.rs rename to src/device/mix.rs index 74db8388..860a7845 100644 --- a/src/mix.rs +++ b/src/device/mix.rs @@ -1,4 +1,5 @@ use crate::*; + #[derive(Debug, Default)] pub enum MeteringMode { #[default] Rms, Log10, diff --git a/src/plugin.rs b/src/device/plugin.rs similarity index 100% rename from src/plugin.rs rename to src/device/plugin.rs diff --git a/src/device/port.rs b/src/device/port.rs new file mode 100644 index 00000000..1375822f --- /dev/null +++ b/src/device/port.rs @@ -0,0 +1,163 @@ +use crate::*; +use ConnectName::*; +use ConnectScope::*; +use ConnectStatus::*; + +pub mod audio; pub use self::audio::*; +pub mod midi; pub use self::midi::*; +pub mod connect; pub use self::connect::*; + +pub trait JackPort: HasJack<'static> { + + type Port: PortSpec + Default; + + type Pair: PortSpec + Default; + + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized; + + fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { + jack.with_client(|c|c.register_port::(name.as_ref(), Default::default())) + .map_err(|e|e.into()) + } + + fn port_name (&self) -> &Arc; + + fn connections (&self) -> &[Connect]; + + fn port (&self) -> &Port; + + fn port_mut (&mut self) -> &mut Port; + + fn into_port (self) -> Port where Self: Sized; + + fn close (self) -> Usually<()> where Self: Sized { + let jack = self.jack().clone(); + Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) + } + + fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { + self.with_client(|c|c.ports(re_name, re_type, flags)) + } + + fn port_by_id (&self, id: u32) -> Option> { + self.with_client(|c|c.port_by_id(id)) + } + + fn port_by_name (&self, name: impl AsRef) -> Option> { + self.with_client(|c|c.port_by_name(name.as_ref())) + } + + fn connect_to_matching <'k> (&'k self) -> Usually<()> { + for connect in self.connections().iter() { + match &connect.name { + Some(Exact(name)) => { + *connect.status.write().unwrap() = self.connect_exact(name)?; + }, + Some(RegExp(re)) => { + *connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?; + }, + _ => {}, + }; + } + Ok(()) + } + + fn connect_exact <'k> (&'k self, name: &str) -> + Usually, Arc, ConnectStatus)>> + { + self.with_client(move|c|{ + let mut status = vec![]; + for port in c.ports(None, None, PortFlags::empty()).iter() { + if port.as_str() == &*name { + if let Some(port) = c.port_by_name(port.as_str()) { + let port_status = self.connect_to_unowned(&port)?; + let name = port.name()?.into(); + status.push((port, name, port_status)); + if port_status == Connected { + break + } + } + } + } + Ok(status) + }) + } + + fn connect_regexp <'k> ( + &'k self, re: &str, scope: Option + ) -> Usually, Arc, ConnectStatus)>> { + self.with_client(move|c|{ + let mut status = vec![]; + let ports = c.ports(Some(&re), None, PortFlags::empty()); + for port in ports.iter() { + if let Some(port) = c.port_by_name(port.as_str()) { + let port_status = self.connect_to_unowned(&port)?; + let name = port.name()?.into(); + status.push((port, name, port_status)); + if port_status == Connected && scope == Some(One) { + break + } + } + } + Ok(status) + }) + } + + /** Connect to a matching port by name. */ + fn connect_to_name (&self, name: impl AsRef) -> Usually { + self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { + self.connect_to_unowned(port) + } else { + Ok(Missing) + }) + } + + /** Connect to a matching port by reference. */ + fn connect_to_unowned (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { + Connected + } else if let Ok(_) = c.connect_ports(port, self.port()) { + Connected + } else { + Mismatch + })) + } + + /** Connect to an owned matching port by reference. */ + fn connect_to_owned (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { + Connected + } else if let Ok(_) = c.connect_ports(port, self.port()) { + Connected + } else { + Mismatch + })) + } +} + +pub trait RegisterPorts: HasJack<'static> { + /// Register a MIDI input port. + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register a MIDI output port. + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio input port. + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio output port. + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; +} + +impl> RegisterPorts for J { + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiInput::new(self.jack(), name, connect) + } + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiOutput::new(self.jack(), name, connect) + } + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioInput::new(self.jack(), name, connect) + } + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioOutput::new(self.jack(), name, connect) + } +} diff --git a/src/device/port/audio.rs b/src/device/port/audio.rs new file mode 100644 index 00000000..bb1db676 --- /dev/null +++ b/src/device/port/audio.rs @@ -0,0 +1,103 @@ +use crate::*; + +def_command!(AudioInputCommand: |port: AudioInput| { + Close => todo!(), + Connect { audio_out: Arc } => todo!(), +}); + +def_command!(AudioOutputCommand: |port: AudioOutput| { + Close => todo!(), + Connect { audio_in: Arc } => todo!(), +}); + +impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } + +/// Audio input port. +#[derive(Debug)] pub struct AudioInput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, +} + +/// Audio output port. +#[derive(Debug)] pub struct AudioOutput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, +} + + +impl JackPort for AudioInput { + type Port = AudioIn; + type Pair = AudioOut; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec() + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl JackPort for AudioOutput { + type Port = AudioOut; + type Pair = AudioIn; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec() + }; + port.connect_to_matching()?; + Ok(port) + } +} diff --git a/src/device/port/connect.rs b/src/device/port/connect.rs new file mode 100644 index 00000000..8571fa2e --- /dev/null +++ b/src/device/port/connect.rs @@ -0,0 +1,83 @@ +use crate::*; +use ConnectName::*; +use ConnectScope::*; +use ConnectStatus::*; + +#[derive(Clone, Debug, PartialEq)] pub enum ConnectName { + /** Exact match */ + Exact(Arc), + /** Match regular expression */ + RegExp(Arc), +} + +#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { + One, + All +} + +#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { + Missing, + Disconnected, + Connected, + Mismatch, +} + +/// Port connection manager. +/// +/// ``` +/// let connect = tek::Connect::default(); +/// ``` +#[derive(Clone, Debug, Default)] pub struct Connect { + pub name: Option, + pub scope: Option, + pub status: Arc, Arc, ConnectStatus)>>>, + pub info: Arc, +} + +impl Connect { + pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) + -> Vec + { + let mut connections = vec![]; + for port in exact.iter() { connections.push(Self::exact(port)) } + for port in re.iter() { connections.push(Self::regexp(port)) } + for port in re_all.iter() { connections.push(Self::regexp_all(port)) } + connections + } + /// Connect to this exact port + pub fn exact (name: impl AsRef) -> Self { + let info = format!("=:{}", name.as_ref()).into(); + let name = Some(Exact(name.as_ref().into())); + Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } + } + pub fn regexp (name: impl AsRef) -> Self { + let info = format!("~:{}", name.as_ref()).into(); + let name = Some(RegExp(name.as_ref().into())); + Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } + } + pub fn regexp_all (name: impl AsRef) -> Self { + let info = format!("+:{}", name.as_ref()).into(); + let name = Some(RegExp(name.as_ref().into())); + Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info } + } + pub fn info (&self) -> Arc { + format!(" ({}) {} {}", { + let status = self.status.read().unwrap(); + let mut ok = 0; + for (_, _, state) in status.iter() { + if *state == Connected { + ok += 1 + } + } + format!("{ok}/{}", status.len()) + }, match self.scope { + None => "x", + Some(One) => " ", + Some(All) => "*", + }, match &self.name { + None => format!("x"), + Some(Exact(name)) => format!("= {name}"), + Some(RegExp(name)) => format!("~ {name}"), + }).into() + } +} diff --git a/src/device/port/midi.rs b/src/device/port/midi.rs new file mode 100644 index 00000000..eb9160cc --- /dev/null +++ b/src/device/port/midi.rs @@ -0,0 +1,256 @@ +use crate::*; + +impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } + +impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } + +/// MIDI input port. +#[derive(Debug)] pub struct MidiInput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of currently held notes. + pub held: Arc>, + /// List of ports to connect to. + pub connections: Vec, +} + +/// MIDI output port. +#[derive(Debug)] pub struct MidiOutput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, + /// List of currently held notes. + pub held: Arc>, + /// Buffer + pub note_buffer: Vec, + /// Buffer + pub output_buffer: Vec>>, +} + +def_command!(MidiInputCommand: |port: MidiInput| { + Close => todo!(), + Connect { midi_out: Arc } => todo!(), +}); + +def_command!(MidiOutputCommand: |port: MidiOutput| { + Close => todo!(), + Connect { midi_in: Arc } => todo!(), +}); +/// Trait for thing that may receive MIDI. +pub trait HasMidiIns { + fn midi_ins (&self) -> &Vec; + fn midi_ins_mut (&mut self) -> &mut Vec; + /// Collect MIDI input from app ports (TODO preallocate large buffers) + fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> { + self.midi_ins().iter() + .map(|port|port.port().iter(scope) + .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) + .collect::>()) + .collect::>() + } + fn midi_ins_with_sizes <'a> (&'a self) -> + impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a + { + let mut y = 0; + self.midi_ins().iter().enumerate().map(move|(i, input)|{ + let height = 1 + input.connections().len(); + let data = (i, input.port_name(), input.connections(), y, y + height); + y += height; + data + }) + } +} +/// Trait for thing that may output MIDI. +pub trait HasMidiOuts { + fn midi_outs (&self) -> &Vec; + fn midi_outs_mut (&mut self) -> &mut Vec; + fn midi_outs_with_sizes <'a> (&'a self) -> + impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a + { + let mut y = 0; + self.midi_outs().iter().enumerate().map(move|(i, output)|{ + let height = 1 + output.connections().len(); + let data = (i, output.port_name(), output.connections(), y, y + height); + y += height; + data + }) + } + fn midi_outs_emit (&mut self, scope: &ProcessScope) { + for port in self.midi_outs_mut().iter_mut() { + port.buffer_emit(scope) + } + } +} + +impl JackPort for MidiInput { + type Port = MidiIn; + type Pair = MidiOut; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec(), + held: Arc::new(RwLock::new([false;128])) + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl JackPort for MidiOutput { + type Port = MidiOut; + type Pair = MidiIn; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self::register(jack, name)?; + let jack = jack.clone(); + let name = name.as_ref().into(); + let connections = connect.to_vec(); + let port = Self { + jack, + port, + name, + connections, + held: Arc::new([false;128].into()), + note_buffer: vec![0;8], + output_buffer: vec![vec![];65536], + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl View for MidiInput { + fn view (&self) -> impl Draw { + "TODO: MIDI IN" + } +} + +impl View for MidiOutput { + fn view (&self) -> impl Draw { + "TODO: MIDI OUT" + } +} + +impl MidiOutput { + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { + let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); + for frame in &mut self.output_buffer[0..n_frames] { + frame.clear(); + } + if reset { + all_notes_off(&mut self.output_buffer); + } + } + /// Write a note to the output buffer + pub fn buffer_write <'a> ( + &'a mut self, + sample: usize, + event: LiveEvent, + ) { + self.note_buffer.fill(0); + event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); + self.output_buffer[sample].push(self.note_buffer.clone()); + // Update the list of currently held notes. + if let LiveEvent::Midi { ref message, .. } = event { + update_keys(&mut*self.held.write().unwrap(), message); + } + } + /// Write a chunk of MIDI data from the output buffer to the output port. + pub fn buffer_emit (&mut self, scope: &ProcessScope) { + let samples = scope.n_frames() as usize; + let mut writer = self.port.writer(scope); + for (time, events) in self.output_buffer.iter().enumerate().take(samples) { + for bytes in events.iter() { + writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ + panic!("Failed to write MIDI data: {bytes:?}"); + }); + } + } + } +} +impl MidiInput { + pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { + parse_midi_input(self.port().iter(scope)) + } +} +impl> + AsMut>> HasMidiIns for T { + fn midi_ins (&self) -> &Vec { self.as_ref() } + fn midi_ins_mut (&mut self) -> &mut Vec { self.as_mut() } +} +impl> + AsMut>> HasMidiOuts for T { + fn midi_outs (&self) -> &Vec { self.as_ref() } + fn midi_outs_mut (&mut self) -> &mut Vec { self.as_mut() } +} +impl> AddMidiIn for T { + fn midi_in_add (&mut self) -> Usually<()> { + let index = self.midi_ins().len(); + let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?; + self.midi_ins_mut().push(port); + Ok(()) + } +} +/// Trail for thing that may gain new MIDI ports. +impl> AddMidiOut for T { + fn midi_out_add (&mut self) -> Usually<()> { + let index = self.midi_outs().len(); + let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?; + self.midi_outs_mut().push(port); + Ok(()) + } +} + +/// May create new MIDI input ports. +pub trait AddMidiIn { + fn midi_in_add (&mut self) -> Usually<()>; +} + +/// May create new MIDI output ports. +pub trait AddMidiOut { + fn midi_out_add (&mut self) -> Usually<()>; +} diff --git a/src/sample.rs b/src/device/sample.rs similarity index 99% rename from src/sample.rs rename to src/device/sample.rs index 6b6fa4a9..b9581142 100644 --- a/src/sample.rs +++ b/src/device/sample.rs @@ -4,10 +4,10 @@ def_command!(SamplerCommand: |sampler: Sampler| { RecordToggle { slot: usize } => { let slot = *slot; let recording = sampler.recording.as_ref().map(|x|x.0); - let _ = Self::RecordFinish.execute(sampler)?; + let _ = Self::RecordFinish.act(sampler)?; // autoslice: continue recording at next slot if recording != Some(slot) { - Self::RecordBegin { slot }.execute(sampler) + Self::RecordBegin { slot }.act(sampler) } else { Ok(None) } diff --git a/src/sequence.rs b/src/device/sequence.rs similarity index 100% rename from src/sequence.rs rename to src/device/sequence.rs diff --git a/src/mode.rs b/src/mode.rs deleted file mode 100644 index 8577d80b..00000000 --- a/src/mode.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::{*, config::*}; - -impl Config { - pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { - self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() - } -} - -pub(crate) fn load_mode (modes: &Modes, name: &impl AsRef, body: &impl Language) -> Usually<()> { - let mut mode = Mode::default(); - body.each(|item|mode.add(item))?; - modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); - Ok(()) -} - -/// Collection of interaction modes. -pub type Modes = Arc, Arc>>>>>; - -impl Mode> { - /// Add a definition to the mode. - /// - /// Supported definitions: - /// - /// - (name ...) -> name - /// - (info ...) -> description - /// - (keys ...) -> key bindings - /// - (mode ...) -> submode - /// - ... -> view - /// - /// ``` - /// let mut mode: tek::Mode> = Default::default(); - /// mode.add("(name hello)").unwrap(); - /// ``` - pub fn add (&mut self, dsl: impl Language) -> Usually<()> { - Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { - //println!("Mode::add: {head} {:?}", expr.tail()); - let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - match head { - "name" => self.add_name(tail)?, - "info" => self.add_info(tail)?, - "keys" => self.add_keys(tail)?, - "mode" => self.add_mode(tail)?, - _ => self.add_view(tail)?, - }; - } else if let Ok(Some(word)) = dsl.word() { - self.add_view(word); - } else { - return Err(format!("Mode::add: unexpected: {dsl:?}").into()); - }) - - //DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into())) - //.word(|word|self.add_view(word)) - //.expr(|expr|expr.head(|head|{ - ////println!("Mode::add: {head} {:?}", expr.tail()); - //let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - //match head { - //"name" => self.add_name(tail), - //"info" => self.add_info(tail), - //"keys" => self.add_keys(tail)?, - //"mode" => self.add_mode(tail)?, - //_ => self.add_view(tail), - //}; - //})) - } - - fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.name.push(src.into()))) - } - fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.info.push(src.into()))) - } - fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.view.push(src.into()))) - } - fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?)) - } - fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(Some(if let Some(id) = dsl.head()? { - load_mode(&self.modes, &id, &dsl.tail())?; - } else { - return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); - })) - } -} -/// Group of view and keys definitions. -/// -/// ``` -/// let mode = tek::Mode::>::default(); -/// ``` -#[derive(Default, Debug)] pub struct Mode { - pub path: PathBuf, - pub name: Vec, - pub info: Vec, - pub view: Vec, - pub keys: Vec, - pub modes: Modes, -} - diff --git a/src/tek.rs b/src/tek.rs index f2f63594..d5060d73 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -1,9 +1,37 @@ #![allow(clippy::unit_arg)] #![feature( - adt_const_params, associated_type_defaults, closure_lifetime_binder, + adt_const_params, associated_type_defaults, anonymous_lifetime_in_impl_trait, closure_lifetime_binder, impl_trait_in_assoc_type, trait_alias, type_alias_impl_trait, type_changing_struct_update )] +/// CLI banner. +pub(crate) const HEADER: &'static str = r#" + +~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ + █ █▀ █▀▀▄ ~ v0.4.0, 2026 winter (or is it) ~ + ~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; + +#[cfg(feature = "cli")] use clap::{self, Parser, Subcommand}; + +pub extern crate atomic_float; + +pub extern crate xdg; +pub(crate) use ::xdg::BaseDirectories; + +pub extern crate midly; +pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*}; + +pub extern crate tengri; +pub(crate) use tengri::{ + *, lang::*, exit::*, eval::*, keys::*, sing::*, time::*, draw::*, term::*, color::*, + crossterm::event::{Event, KeyEvent}, + ratatui::{ + self, + prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}}, + widgets::{Widget, canvas::{Canvas, Line}}, + }, +}; + /// Implement an arithmetic operation for a unit of time #[macro_export] macro_rules! impl_op { ($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => { @@ -25,76 +53,6 @@ } } -/// TODO: Preserve the generic passthru syntax; -/// remove this macro (only used twice) and potentially the trait. -#[macro_export] macro_rules! impl_has_clips { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { - fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { - $cb.read().unwrap() - } - fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { - $cb.write().unwrap() - } - } - } -} - -pub mod arrange; -pub mod bind; -pub mod browse; -pub mod cli; -pub mod clock; -pub mod config; -pub mod device; -pub mod dialog; -pub mod editor; -pub mod menu; -pub mod mix; -pub mod mode; -pub mod sample; -pub mod sequence; -pub mod select; -pub mod view; - -#[cfg(feature = "plugin")] pub mod plugin; - -use clap::{self, Parser, Subcommand}; -use self::{ - config::*, mode::*, view::*, bind::*, - dialog::*, browse::*, menu::*, - clock::*, sequence::*, editor::*, - arrange::*, select::*, device::*, sample::*, -}; - -extern crate xdg; -pub(crate) use ::xdg::BaseDirectories; -pub extern crate atomic_float; -//pub(crate) use atomic_float::AtomicF64; -//pub extern crate jack; -//pub(crate) use ::jack::{*, contrib::{*, ClosureProcessHandler}}; -pub extern crate midly; -pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*}; -pub extern crate tengri; -pub(crate) use tengri::{ - *, - lang::*, - exit::*, - eval::*, - keys::*, - sing::*, - time::*, - draw::*, - term::*, - color::*, - space::*, - crossterm::event::{Event, KeyEvent}, - ratatui::{ - self, - prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}}, - widgets::{Widget, canvas::{Canvas, Line}}, - }, -}; #[cfg(feature = "sampler")] pub(crate) use symphonia::{ default::get_codecs, core::{//errors::Error as SymphoniaError, @@ -102,6 +60,7 @@ pub(crate) use tengri::{ codecs::{Decoder, CODEC_TYPE_NULL}, }, }; + #[cfg(feature = "lv2_gui")] use ::winit::{ application::ApplicationHandler, event::WindowEvent, @@ -109,6 +68,7 @@ pub(crate) use tengri::{ window::{Window, WindowId}, platform::x11::EventLoopBuilderExtX11 }; + #[allow(unused)] pub(crate) use ::{ std::{ cmp::Ord, @@ -126,163 +86,23 @@ pub(crate) use tengri::{ }; /// Command-line entrypoint. -#[cfg(feature = "cli")] pub fn main () -> Usually<()> { +#[cfg(feature = "cli")] +pub fn main () -> Usually<()> { Config::watch(|config|{ Exit::enter(|exit|{ Jack::connect("tek", |jack|{ - let state = Arc::new(RwLock::new(App { - color: ItemTheme::random(), - config: Config::init(), - dialog: Dialog::welcome(), - jack: jack.clone(), - mode: ":menu", - project: Arrangement::new(&jack, &Clock::new(&jack, 51)), - ..Default::default() - })); - // TODO: Sync these timings with main clock, so that things - // "accidentally" fall on the beat in overload conditions. - let keyboard = tui_keyboard(&exit, &state, Duration::from_millis(100))?; + let project = Arrangement::new(&jack, &Clock::new(&jack, 51)); + let state = App::new_shared(jack, config, project, ":menu"); + let keyboard = tui_input(&exit, &state, Duration::from_millis(100))?; let terminal = tui_output(&exit, &state, Duration::from_millis(10))?; (keyboard, terminal) + // TODO: Sync I/O timings with main clock, so that things + // "accidentally" fall on the beat in overload conditions. }) }) }) } -/// Create a new application from a backend, project, config, and mode -/// -/// ``` -/// let jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack"); -/// let proj = tek::Arrangement::default(); -/// let mut conf = tek::Config::default(); -/// conf.add("(mode hello)"); -/// let tek = tek::tek(&jack, proj, conf, "hello"); -/// ``` -pub fn tek ( - jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef -) -> App { - let mode: &str = mode.as_ref(); - App { - color: ItemTheme::random(), - dialog: Dialog::welcome(), - jack: jack.clone(), - mode: config.get_mode(mode).expect(&format!("failed to find mode '{mode}'")), - config, - project, - ..Default::default() - } -} - -fn tek_confirm (state: &mut App) -> Perhaps { - Ok(match &state.dialog { - Dialog::Menu(index, items) => { - let callback = items.0[*index].1.clone(); - callback(state)?; - None - }, - _ => todo!(), - }) -} - -fn tek_inc (state: &mut App, axis: &ControlAxis) -> Perhaps { - Ok(match (&state.dialog, axis) { - (Dialog::None, _) => todo!(), - (Dialog::Menu(_, _), ControlAxis::Y) => AppCommand::SetDialog { dialog: state.dialog.menu_next() } - .act(state)?, - _ => todo!() - }) -} - -fn tek_dec (state: &mut App, axis: &ControlAxis) -> Perhaps { - Ok(match (&state.dialog, axis) { - (Dialog::None, _) => None, - (Dialog::Menu(_, _), ControlAxis::Y) => AppCommand::SetDialog { dialog: state.dialog.menu_prev() } - .act(state)?, - _ => todo!() - }) -} - -//impl_handle!(TuiIn: |self: App, input|{ - //let commands = tek_collect_commands(self, input)?; - //let history = tek_execute_commands(self, commands)?; - //self.history.extend(history.into_iter()); - //Ok(None) -//}); -//fn tek_collect_commands (app: &App, input: &TuiIn) -> Usually> { - //let mut commands = vec![]; - //for id in app.mode.keys.iter() { - //if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref()) - //&& let Some(bindings) = event_map.query(input.event()) { - //for binding in bindings { - //for command in binding.commands.iter() { - //if let Some(command) = app.namespace(command)? as Option { - //commands.push(command) - //} - //} - //} - //} - //} - //Ok(commands) -//} -//fn tek_execute_commands ( - //app: &mut App, commands: Vec -//) -> Usually)>> { - //let mut history = vec![]; - //for command in commands.into_iter() { - //let result = command.act(app); - //match result { Err(err) => { history.push((command, None)); return Err(err) } - //Ok(undo) => { history.push((command, undo)); } }; - //} - //Ok(history) -//} - -pub fn tek_jack_process (app: &mut App, client: &Client, scope: &ProcessScope) -> Control { - let t0 = app.perf.get_t0(); - app.clock().update_from_scope(scope).unwrap(); - let midi_in = app.project.midi_input_collect(scope); - if let Some(editor) = &app.editor() { - let mut pitch: Option = None; - for port in midi_in.iter() { - for event in port.iter() { - if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) - = event - { - pitch = Some(key.clone()); - } - } - } - if let Some(pitch) = pitch { - editor.set_note_pos(pitch.as_int() as usize); - } - } - let result = app.project.process_tracks(client, scope); - app.perf.update_from_jack_scope(t0, scope); - result -} - -pub fn tek_jack_event (app: &mut App, event: JackEvent) { - use JackEvent::*; - match event { - SampleRate(sr) => { app.clock().timebase.sr.set(sr as f64); }, - PortRegistration(_id, true) => { - //let port = app.jack().port_by_id(id); - //println!("\rport add: {id} {port:?}"); - //println!("\rport add: {id}"); - }, - PortRegistration(_id, false) => { - /*println!("\rport del: {id}")*/ - }, - PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, - PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, - ClientRegistration(_id, true) => {}, - ClientRegistration(_id, false) => {}, - ThreadInit => {}, - XRun => {}, - GraphReorder => {}, - _ => { panic!("{event:?}"); } - } -} - pub fn swap_value ( target: &mut T, value: &T, returned: impl Fn(T)->U ) -> Perhaps { @@ -307,18 +127,9 @@ pub fn toggle_bool ( } } - //take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref() //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); -#[macro_export] macro_rules! has_clip { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? { - fn clip (&$self) -> Option>> { $cb } - } - } -} - fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { let (mut subdirs, mut files) = std::fs::read_dir(dir)? .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ @@ -340,17 +151,6 @@ pub(crate) fn track_width (_index: usize, track: &Track) -> u16 { track.width as u16 } -def_command!(AppCommand: |app: App| { - Nop => Ok(None), - Confirm => tek_confirm(app), - Cancel => todo!(), // TODO delegate: - Inc { axis: ControlAxis } => tek_inc(app, axis), - Dec { axis: ControlAxis } => tek_dec(app, axis), - SetDialog { dialog: Dialog } => { - swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) - }, -}); - /// Define a type alias for iterators of sized items (columns). macro_rules! def_sizes_iter { ($Type:ident => $($Item:ty),+) => { @@ -358,341 +158,11 @@ macro_rules! def_sizes_iter { Iterator + Send + Sync + 'a; } } -def_sizes_iter!(InputsSizes => MidiInput); -def_sizes_iter!(OutputsSizes => MidiOutput); -def_sizes_iter!(PortsSizes => Arc, [Connect]); -def_sizes_iter!(ScenesSizes => Scene); -def_sizes_iter!(TracksSizes => Track); -/// ``` -/// let _ = tek::view_logo(); -/// ``` -pub fn view_logo () -> impl Draw { - wh_exact(Some(32), Some(7), bold(true, fg(Rgb(240, 200, 180), south!{ - h_exact(1, ""), - h_exact(1, ""), - h_exact(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"), - h_exact(1, east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), - h_exact(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"), - }))) -} - -/// ``` -/// let x = std::sync::Arc::>::default(); -/// let _ = tek::view_transport(true, x.clone(), x.clone(), x.clone()); -/// let _ = tek::view_transport(false, x.clone(), x.clone(), x.clone()); -/// ``` -pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw { - let theme = ItemTheme::G[96]; - bg(Black, east!(above( - wh_full(origin_w(button_play_pause(play))), - wh_full(origin_e(east!( - field_h(theme, "BPM", bpm), - field_h(theme, "Beat", beat), - field_h(theme, "Time", time), - ))) - ))) -} - -/// ``` -/// let x = std::sync::Arc::>::default(); -/// let _ = tek::view_status(None, x.clone(), x.clone(), x.clone()); -/// let _ = tek::view_status(Some("".into()), x.clone(), x.clone(), x.clone()); -/// ``` -pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw { - let theme = ItemTheme::G[96]; - let sr = field_h(theme, "SR", sr); - let buf = field_h(theme, "Buf", buf); - let lat = field_h(theme, "Lat", lat); - bg(Black, east!(above( - wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))), - wh_full(origin_e(east!(sr, buf, lat))), - ))) -} - -/// ``` -/// let _ = tek::button_play_pause(true); -/// ``` -pub fn button_play_pause (playing: bool) -> impl Draw { - let compact = true;//self.is_editing(); - bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - either(compact, - thunk(move|to: &mut Tui|w_exact(9, either(playing, - fg(Rgb(0, 255, 0), " PLAYING "), - fg(Rgb(255, 128, 0), " STOPPED ")) - ).draw(to)), - thunk(move|to: &mut Tui|w_exact(5, either(playing, - fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), - fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",))) - ).draw(to)), - ) - ) -} - -#[cfg(feature = "track")] pub fn view_track_row_section ( - _theme: ItemTheme, - button: impl Draw, - button_add: impl Draw, - content: impl Draw, -) -> impl Draw { - west(h_full(w_exact(4, origin_nw(button_add))), - east(w_exact(20, h_full(origin_nw(button))), wh_full(origin_c(content)))) -} - -/// ``` -/// let bg = tengri::ratatui::style::Color::Red; -/// let fg = tengri::ratatui::style::Color::Green; -/// let _ = tek::view_wrap(bg, fg, "and then blue, too!"); -/// ``` -pub fn view_wrap (bg: Color, fg: Color, content: impl Draw) -> impl Draw { - let left = fg_bg(bg, Reset, w_exact(1, y_repeat("▐"))); - let right = fg_bg(bg, Reset, w_exact(1, y_repeat("▌"))); - east(left, west(right, fg_bg(fg, bg, content))) -} - -/// ``` -/// let _ = tek::view_meter("", 0.0); -/// let _ = tek::view_meters(&[0.0, 0.0]); -/// ``` -pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw + 'a { - let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)); - let w = if value >= 0.0 { 13 } - else if value >= -1.0 { 12 } - else if value >= -2.0 { 11 } - else if value >= -3.0 { 10 } - else if value >= -4.0 { 9 } - else if value >= -6.0 { 8 } - else if value >= -9.0 { 7 } - else if value >= -12.0 { 6 } - else if value >= -15.0 { 5 } - else if value >= -20.0 { 4 } - else if value >= -25.0 { 3 } - else if value >= -30.0 { 2 } - else if value >= -40.0 { 1 } - else { 0 }; - let c = if value >= 0.0 { Red } - else if value >= -3.0 { Yellow } - else { Green }; - south!(f, wh_exact(Some(w), Some(1), bg(c, ()))) -} - -pub fn view_meters (values: &[f32;2]) -> impl Draw + use<'_> { - let left = format!("L/{:>+9.3}", values[0]); - let right = format!("R/{:>+9.3}", values[1]); - south(left, right) -} - -pub fn draw_info (sample: Option<&Arc>>) -> impl Draw + use<'_> { - when(sample.is_some(), thunk(move|to: &mut Tui|{ - let sample = sample.unwrap().read().unwrap(); - let theme = sample.color; - east!( - field_h(theme, "Name", format!("{:<10}", sample.name.clone())), - field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())), - field_h(theme, "Start", format!("{:<8}", sample.start)), - field_h(theme, "End", format!("{:<8}", sample.end)), - field_h(theme, "Trans", "0"), - field_h(theme, "Gain", format!("{}", sample.gain)), - ).draw(to) - })) -} - -pub fn draw_info_v (sample: Option<&Arc>>) -> impl Draw + use<'_> { - let a = thunk(move|to: &mut Tui|{ - let sample = sample.unwrap().read().unwrap(); - let theme = sample.color; - w_exact(20, south!( - w_full(origin_w(field_h(theme, "Name ", format!("{:<10}", sample.name.clone())))), - w_full(origin_w(field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())))), - w_full(origin_w(field_h(theme, "Start ", format!("{:<8}", sample.start)))), - w_full(origin_w(field_h(theme, "End ", format!("{:<8}", sample.end)))), - w_full(origin_w(field_h(theme, "Trans ", "0"))), - w_full(origin_w(field_h(theme, "Gain ", format!("{}", sample.gain)))), - )).draw(to) - }); - - let b = thunk(|to: &mut Tui|fg(Red, south!( - bold(true, "× No sample."), - "[r] record", - "[Shift-F9] import", - )).draw(to)); - - either(sample.is_some(), a, b) -} - -pub fn draw_status (sample: Option<&Arc>>) -> impl Draw { - bold(true, fg(g(224), sample - .map(|sample|{ - let sample = sample.read().unwrap(); - format!("Sample {}-{}", sample.start, sample.end) - }) - .unwrap_or_else(||"No sample".to_string()))) -} - -pub fn view_track_header (theme: ItemTheme, content: impl Draw) -> impl Draw { - w_exact(12, bg(theme.darker.term, w_full(origin_e(content)))) -} - -pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) - -> impl Draw + use<'a, T> -{ - let ins = ports.len() as u16; - let frame = Outer(true, Style::default().fg(g(96))); - let iter = move||ports.iter(); - let names = iter_south(iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name())))); - let field = field_v(theme, title, names); - wh_exact(Some(20), Some(1 + ins), border(true, frame, wh_exact(Some(20), Some(1 + ins), field))) -} - -pub fn io_ports <'a, T: PortsSizes<'a>> ( - fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a -) -> impl Draw + 'a { - type Item<'a> = (usize, &'a Arc, &'a [Connect], usize, usize); - iter(items, move|(_index, name, connections, y, y2): Item<'a>, _| { - y_push(y as u16, h_exact((y2-y) as u16, - south(h_full(bold(true, fg_bg(fg, bg, origin_w(east(" 󰣲 ", name))))), - iter(||connections.iter(), move|connect: &'a Connect, index|y_push( - index as u16, h_exact(1, origin_w(bold(false, fg_bg(fg, bg, &connect.info)))) - ))))) - }) -} - -/// CLI banner. -pub(crate) const HEADER: &'static str = r#" - -~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ - █ █▀ █▀▀▄ ~ v0.4.0, 2026 winter (or is it) ~ - ~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; - -/// Total state -/// -/// ``` -/// use tek::{HasTracks, HasScenes, TracksView, ScenesView}; -/// let mut app = tek::App::default(); -/// let _ = app.scene_add(None, None).unwrap(); -/// let _ = app.update_clock(); -/// app.project.editor = Some(Default::default()); -/// //let _: Vec<_> = app.project.inputs_with_sizes().collect(); -/// //let _: Vec<_> = app.project.outputs_with_sizes().collect(); -/// let _: Vec<_> = app.project.tracks_with_sizes().collect(); -/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect(); -/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect(); -/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect(); -/// let _ = app.project.w(); -/// //let _ = app.project.w_sidebar(); -/// //let _ = app.project.w_tracks_area(); -/// let _ = app.project.h(); -/// //let _ = app.project.h_tracks_area(); -/// //let _ = app.project.h_inputs(); -/// //let _ = app.project.h_outputs(); -/// let _ = app.project.h_scenes(); -/// ``` -#[derive(Default, Debug)] pub struct App { - /// Base color. - pub color: ItemTheme, - /// Must not be dropped for the duration of the process - pub jack: Jack<'static>, - /// Display size - pub size: Size, - /// Performance counter - pub perf: PerfModel, - /// Available view modes and input bindings - pub config: Config, - /// Currently selected mode - pub mode: Arc>>, - /// Undo history - pub history: Vec<(AppCommand, Option)>, - /// Dialog overlay - pub dialog: Dialog, - /// Contains all recently created clips. - pub pool: Pool, - /// Contains the currently edited musical arrangement - pub project: Arrangement, - /// Error, if any - pub error: Arc>>> -} -impl_has!(Clock: |self: App|self.project.clock); -impl_has!(Vec: |self: App|self.project.midi_ins); -impl_has!(Vec: |self: App|self.project.midi_outs); -impl_has!(Dialog: |self: App|self.dialog); -impl_has!(Jack<'static>: |self: App|self.jack); -impl_has!(Size: |self: App|self.size); -impl_has!(Pool: |self: App|self.pool); -impl_has!(Selection: |self: App|self.project.selection); -impl_as_ref!(Vec: |self: App|self.project.as_ref()); -impl_as_mut!(Vec: |self: App|self.project.as_mut()); -impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); -impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); -impl_has_clips!( |self: App|self.pool.clips); -impl_audio!(App: tek_jack_process, tek_jack_event); -namespace!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); -namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); }); -namespace!(App: u16 { literal = |dsl|try_to_u16(dsl); symbol = |app| { - ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), - ":h/sample-detail" => 6.max(app.size.h() as u16 * 3 / 9), }; }); -namespace!(App: isize { literal = |dsl|try_to_isize(dsl); }); -namespace!(App: usize { literal = |dsl|try_to_usize(dsl); symbol = |app| { - ":scene-count" => app.scenes().len(), - ":track-count" => app.tracks().len(), - ":device-kind" => app.dialog.device_kind().unwrap_or(0), - ":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0), - ":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), }; }); -namespace!(App: bool { symbol = |app| { // Provide boolean values. - ":mode/editor" => app.project.editor.is_some(), - ":focused/dialog" => !matches!(app.dialog, Dialog::None), - ":focused/message" => matches!(app.dialog, Dialog::Message(..)), - ":focused/add_device" => matches!(app.dialog, Dialog::Device(..)), - ":focused/browser" => app.dialog.browser().is_some(), - ":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))), - ":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))), - ":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))), - ":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))), - ":focused/clip" => !app.editor_focused() && matches!(app.selection(), Selection::TrackClip{..}), - ":focused/track" => !app.editor_focused() && matches!(app.selection(), Selection::Track(..)), - ":focused/scene" => !app.editor_focused() && matches!(app.selection(), Selection::Scene(..)), - ":focused/mix" => !app.editor_focused() && matches!(app.selection(), Selection::Mix), -}; }); -namespace!(App: ItemTheme {}); // TODO: provide colors here -namespace!(App: Selection { symbol = |app| { - ":select/scene" => app.selection().select_scene(app.tracks().len()), - ":select/scene/next" => app.selection().select_scene_next(app.scenes().len()), - ":select/scene/prev" => app.selection().select_scene_prev(), - ":select/track" => app.selection().select_track(app.tracks().len()), - ":select/track/next" => app.selection().select_track_next(app.tracks().len()), - ":select/track/prev" => app.selection().select_track_prev(), -}; }); -namespace!(App: Color { - symbol = |app| { - ":color/bg" => Color::Rgb(28, 32, 36), - }; - expression = |app| { - "g" (n: u8) => Color::Rgb(n, n, n), - "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), - }; -}); -namespace!(App: Option { symbol = |app| { - ":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) -}; }); -namespace!(App: Option { symbol = |app| { - ":selected/scene" => app.selection().scene(), - ":selected/track" => app.selection().track(), -}; }); -namespace!(App: Option>> { - symbol = |app| { - ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { - app.scenes()[*scene].clips[*track].clone() - } else { - None - } - }; -}); - -pub trait HasClipsSize { fn clips_size (&self) -> &Size; } - -pub trait HasDevices: AsRef> + AsMut> { - fn devices (&self) -> &Vec { self.as_ref() } - fn devices_mut (&mut self) -> &mut Vec { self.as_mut() } -} +primitive!(u8: try_to_u8); +primitive!(u16: try_to_u16); +primitive!(usize: try_to_usize); +primitive!(isize: try_to_isize); pub trait HasWidth { const MIN_WIDTH: usize; /// Increment track width. @@ -701,301 +171,11 @@ pub trait HasWidth { fn width_dec (&mut self); } -impl<'a> Namespace<'a, AppCommand> for App { - symbols!('a |app| -> AppCommand { - "x/inc" => AppCommand::Inc { axis: ControlAxis::X }, - "x/dec" => AppCommand::Dec { axis: ControlAxis::X }, - "y/inc" => AppCommand::Inc { axis: ControlAxis::Y }, - "y/dec" => AppCommand::Dec { axis: ControlAxis::Y }, - "confirm" => AppCommand::Confirm, - "cancel" => AppCommand::Cancel, - }); -} -impl Interpret for App { - fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { - app_interpret_expr(self, to, lang) - } - fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { - app_interpret_word(self, to, lang) - } -} -fn app_interpret_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Usually<()> { - if eval_view(state, to, lang)? || eval_view_tui(state, to, lang)? { - Ok(()) - } else { - Err(format!("App::interpret_expr: unexpected: {lang:?}").into()) - } -} -fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually<()> { - let mut frags = dsl.src()?.unwrap().split("/"); - match frags.next() { +pub mod device; pub use self::device::*; +pub mod app; pub use self::app::*; - Some(":logo") => view_logo().draw(to), - - Some(":status") => h_exact(1, "TODO: Status Bar").draw(to), - - Some(":meters") => match frags.next() { - Some("input") => bg(Rgb(30, 30, 30), h_full(origin_s("Input Meters"))).draw(to), - Some("output") => bg(Rgb(30, 30, 30), h_full(origin_s("Output Meters"))).draw(to), - _ => panic!() - }, - - Some(":tracks") => match frags.next() { - None => "TODO tracks".draw(to), - Some("names") => state.project.view_track_names(state.color.clone()).draw(to),//bg(Rgb(40, 40, 40), w_full(origin_w("Track Names")))), - Some("inputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Inputs"))).draw(to), - Some("devices") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Devices"))).draw(to), - Some("outputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Outputs"))).draw(to), - _ => panic!() - }, - - Some(":scenes") => match frags.next() { - None => "TODO scenes".draw(to), - Some(":scenes/names") => "TODO Scene Names".draw(to), - _ => panic!() - }, - - Some(":editor") => "TODO Editor".draw(to), - - Some(":dialog") => match frags.next() { - Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog { - let items = items.clone(); - let selected = selected; - Some(wh_full(thunk(move|to: &mut Tui|{ - for (index, MenuItem(item, _)) in items.0.iter().enumerate() { - to.place(&y_push((2 * index) as u16, - fg_bg( - if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, - if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, - h_exact(2, origin_n(w_full(item))) - ))); - } - }))) - } else { - None - }.draw(to), - _ => unimplemented!("App::interpret_word: {dsl:?} ({frags:?})"), - }, - - Some(":templates") => { - let modes = state.config.modes.clone(); - let height = (modes.read().unwrap().len() * 2) as u16; - h_exact(height, w_min(Some(30), thunk(move |to: &mut Tui|{ - for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { - let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; - let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); - let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); - let fg1 = Rgb(224, 192, 128); - let fg2 = Rgb(224, 128, 32); - let field_name = w_full(origin_w(fg(fg1, name))); - let field_id = w_full(origin_e(fg(fg2, id))); - let field_info = w_full(origin_w(info)); - y_push((2 * index) as u16, - h_exact(2, w_full(bg(bg, south( - above(field_name, field_id), field_info))))).draw(to); - } - }))) - }.draw(to), - - Some(":sessions") => h_exact(Some(6), w_min(30, thunk(|to: &mut Tui|{ - let fg = Rgb(224, 192, 128); - for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { - let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; - y_push((2 * index) as u16, - &h_exact(2, w_full(bg(bg, origin_w(fg(fg, name)))))).draw(to); - } - }))).draw(to), - - Some(":browse/title") => w_full(origin_w(field_v(ItemColor::default(), - match state.dialog.browser_target().unwrap() { - BrowseTarget::SaveProject => "Save project:", - BrowseTarget::LoadProject => "Load project:", - BrowseTarget::ImportSample(_) => "Import sample:", - BrowseTarget::ExportSample(_) => "Export sample:", - BrowseTarget::ImportClip(_) => "Import clip:", - BrowseTarget::ExportClip(_) => "Export clip:", - }, w_shrink(3, h_exact(1, fg(g(96), x_repeat("🭻"))))))).draw(to), - - Some(":device") => { - let selected = state.dialog.device_kind().unwrap(); - south(bold(true, "Add device"), iter_south( - move||device_kinds().iter(), - move|_label: &&'static str, i|{ - let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; - let l = if i == selected { "[ " } else { " " }; - let r = if i == selected { " ]" } else { " " }; - w_full(bg(b, east(l, west(r, "FIXME device name")))) })).draw(to) - }, - - Some(":debug") => h_exact(1, format!("[{:?}]", to.area())).draw(to), - - Some(_) => { - let views = state.config.views.read().unwrap(); - if let Some(dsl) = views.get(dsl.src()?.unwrap()) { - let dsl = dsl.clone(); - std::mem::drop(views); - state.interpret(to, &dsl)? - } else { - unimplemented!("{dsl:?}"); - } - }, - - _ => unreachable!() - } - Ok(()) -} -impl App { - /// Update memoized render of clock values. - /// ``` - /// tek::App::default().update_clock(); - /// ``` - pub fn update_clock (&self) { - ClockView::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) - } - - /// Set modal dialog. - /// - /// ``` - /// let previous: tek::Dialog = tek::App::default().set_dialog(tek::Dialog::welcome()); - /// ``` - pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog { - std::mem::swap(&mut self.dialog, &mut dialog); - dialog - } - - /// FIXME: generalize. Set picked device in device pick dialog. - /// - /// ``` - /// tek::App::default().device_pick(0); - /// ``` - pub fn device_pick (&mut self, index: usize) { - self.dialog = Dialog::Device(index); - } - - /// FIXME: generalize. Add device to current track. - pub fn add_device (&mut self, index: usize) -> Usually<()> { - match index { - 0 => { - let name = self.jack.with_client(|c|c.name().to_string()); - let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); - let track = self.track().expect("no active track"); - let port = format!("{}/Sampler", &track.name); - let connect = Connect::exact(format!("{name}:{midi}")); - let sampler = if let Ok(sampler) = Sampler::new( - &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] - ) { - self.dialog = Dialog::None; - Device::Sampler(sampler) - } else { - self.dialog = Dialog::Message("Failed to add device.".into()); - return Err("failed to add device".into()) - }; - let track = self.track_mut().expect("no active track"); - track.devices.push(sampler); - Ok(()) - }, - 1 => { - todo!(); - //Ok(()) - }, - _ => unreachable!(), - } - } - - /// Return reference to content browser if open. - /// - /// ``` - /// assert_eq!(tek::App::default().browser(), None); - /// ``` - pub fn browser (&self) -> Option<&Browse> { - if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } - } - - /// Is a MIDI editor currently focused? - /// - /// ``` - /// tek::App::default().editor_focused(); - /// ``` - pub fn editor_focused (&self) -> bool { - false - } - - /// Toggle MIDI editor. - /// - /// ``` - /// tek::App::default().toggle_editor(None); - /// ``` - pub fn toggle_editor (&mut self, value: Option) { - //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); - let value = value.unwrap_or_else(||!self.editor().is_some()); - if value { - // Create new clip in pool when entering empty cell - if let Selection::TrackClip { track, scene } = *self.selection() - && let Some(scene) = self.project.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && slot.is_none() - && let Some(track) = self.project.tracks.get_mut(track) - { - let (_index, clip) = self.pool.add_new_clip(); - // autocolor: new clip colors from scene and track color - let color = track.color.base.mix(scene.color.base, 0.5); - clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); - if let Some(editor) = &mut self.project.editor { - editor.set_clip(Some(&clip)); - } - *slot = Some(clip.clone()); - //Some(clip) - } else { - //None - } - } else if let Selection::TrackClip { track, scene } = *self.selection() - && let Some(scene) = self.project.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && let Some(clip) = slot.as_mut() - { - // Remove clip from arrangement when exiting empty clip editor - let mut swapped = None; - if clip.read().unwrap().count_midi_messages() == 0 { - std::mem::swap(&mut swapped, slot); - } - if let Some(clip) = swapped { - self.pool.delete_clip(&clip.read().unwrap()); - } - } - } -} - -/// The [Draw] implementation for [App] handles the loaded view, -/// which is defined in terms of [dizzle] DSL. -/// -/// If there is an error, the error is displayed. FIXME: overlay it -/// Then, every top-level form of the DSL description is rendered. -impl Draw for App { - fn draw (self, to: &mut Tui) -> Usually> { - let xywh = to.area().into(); - if let Some(e) = self.error.read().unwrap().as_ref() { - to.show(xywh, e.as_ref()); - } - for (index, dsl) in self.mode.view.iter().enumerate() { - if let Err(e) = self.interpret(to, dsl) { - *self.error.write().unwrap() = Some(format!("view #{index}: {e}").into()); - break; - } - } - Ok(xywh) - } -} - -impl HasClipsSize for App { fn clips_size (&self) -> &Size { &self.project.size_inner } } -impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl_default!(AppCommand: Self::Nop); -primitive!(u8: try_to_u8); -primitive!(u16: try_to_u16); -primitive!(usize: try_to_usize); -primitive!(isize: try_to_isize); - -fn field_h (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { -} - -fn field_v (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { -} +def_sizes_iter!(InputsSizes => MidiInput); +def_sizes_iter!(OutputsSizes => MidiOutput); +def_sizes_iter!(PortsSizes => Arc, [Connect]); +def_sizes_iter!(ScenesSizes => Scene); +def_sizes_iter!(TracksSizes => Track); diff --git a/src/view.rs b/src/view.rs deleted file mode 100644 index 018515fb..00000000 --- a/src/view.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::*; - -/// Collection of view definitions. -pub type Views = Arc, Arc>>>; - -pub(crate) fn load_view (views: &Views, name: &impl AsRef, body: &impl Language) -> Usually<()> { - views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into()); - Ok(()) -} - From b3e9cde8a1e210151fbcde2cbb1042cd72949ef4 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Tue, 23 Jun 2026 22:02:09 +0300 Subject: [PATCH 10/31] restruct: 65e --- src/app/view.rs | 59 +++++++++++++++++-------------------- src/device/arrange/clip.rs | 13 ++++---- src/device/arrange/scene.rs | 7 +---- src/device/arrange/track.rs | 8 ++--- 4 files changed, 38 insertions(+), 49 deletions(-) diff --git a/src/app/view.rs b/src/app/view.rs index f5e2852e..21699419 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -202,28 +202,28 @@ pub fn view_io_ports <'a, T: PortsSizes<'a>> ( }) } -pub fn view_scenes_clips ( - scenes: impl ScenesSizes<'_>, - tracks: impl TracksSizes<'_>, +pub fn view_scenes_clips <'a> ( + scenes: impl ScenesSizes<'a>, + tracks: impl TracksSizes<'a>, select: &Selection, editor: Option<&MidiEditor>, size: &Size, is_editing: bool, ) -> impl Draw { size.of(wh_full(above(wh_full(origin_se(fg(Green, format!("{}x{}", size.w(), size.h())))), - thunk(|to: &mut Tui|{ + thunk(move |to: &mut Tui|{ for (index, track, _, _) in tracks { - let clips = view_track_clips(scenes, select, editor, index, track, is_editing); + let clips = view_track_clips(&scenes, select, editor, index, track, is_editing); w_exact(track.width as u16, h_full(clips)).draw(to); } Ok(XYWH(0, 0, 0, 0)) })))) } -pub fn view_track_clips ( - scenes: impl ScenesSizes<'_>, +pub fn view_track_clips <'a> ( + scenes: &impl ScenesSizes<'a>, select: &Selection, - editor: &Option, + editor: Option<&MidiEditor>, index: usize, track: &Track, is_editing: bool, @@ -320,7 +320,7 @@ pub fn view_track_names ( pub fn view_track_outputs ( theme: ItemTheme, tracks: impl TracksSizes<'_>, - midi_outs: impl Iterator, + midi_outs: impl Iterator, ) -> impl Draw { view_track_row_section(theme, south(w_full(origin_w(button_2("o", "utput", false))), @@ -345,14 +345,14 @@ pub fn view_track_outputs ( } pub fn view_track_inputs ( - theme: ItemTheme, + theme: ItemTheme, tracks: impl TracksSizes<'_>, - h: u16, + height: u16, ) -> impl Draw { view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), bg(theme.darker.term, origin_w(thunk(move|to: &mut Tui|{ for (index, track, _x1, _x2) in tracks { - wh_exact(Some(track_width(index, track)), Some(h + 1), + wh_exact(Some(track_width(index, track)), Some(height + 1), origin_nw(south( bg(track.color.base.term, w_full(origin_w(east!( @@ -370,18 +370,21 @@ pub fn view_track_inputs ( } pub fn view_scenes_names ( - scenes: &impl ScenesSizes<'_> + scenes: impl ScenesSizes<'_>, + select: &Selection, + editor: Option<&MidiEditor>, + editing: bool, ) -> impl Draw { - w_exact(20, thunk(|to: &mut Tui|{ + w_exact(20, thunk(move |to: &mut Tui|{ for (index, scene, ..) in scenes { - view_scene_name(index, scene).draw(to); + view_scene_name(select, editor, index, scene, editing).draw(to); } Ok(XYWH(1, 1, 1, 1)) })) } pub fn view_scene_name ( - select: Option<&Selection>, + select: &Selection, editor: Option<&MidiEditor>, index: usize, scene: &Scene, @@ -422,8 +425,9 @@ pub fn view_audio_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl track.map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) } -pub fn view_track_per ( - tracks: impl TracksSizes<'_> +pub fn view_track_per <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a ) -> impl Draw { iter_east(tracks, move|(index, track, x1, x2): (usize, &Track, usize, usize), _|{ w_exact((x2 - x1) as u16, fg_bg( @@ -437,12 +441,9 @@ pub fn view_per_track () -> impl Draw {} pub fn view_per_track_top () -> impl Draw {} -pub fn view_inputs ( - tracks: impl TracksSizes<'_>, - midi_ins: &[MidiIn], -) -> impl Draw { - let header = h_exact(1, view_inputs_header(tracks)); - south(header, thunk(|to: &mut Tui|{ +pub fn view_inputs (tracks: impl TracksSizes<'_>, midi_ins: &[MidiInput]) -> impl Draw { + let header = h_exact(1, view_inputs_header(tracks, midi_ins)); + south(header, thunk(move |to: &mut Tui|{ for (index, port) in midi_ins.iter().enumerate() { x_push(index as u16 * 10, h_exact(1, view_inputs_row(tracks, port))).draw(to); } @@ -450,10 +451,7 @@ pub fn view_inputs ( })) } -pub fn view_inputs_header ( - tracks: impl TracksSizes<'_>, - midi_ins: &[MidiIn], -) -> impl Draw { +pub fn view_inputs_header (tracks: impl TracksSizes<'_>, midi_ins: &[MidiInput]) -> impl Draw { east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", midi_ins.len()), false))), west(w_exact(4, button_2("I", "+", false)), thunk(move|to: &mut Tui|{ for (_index, track, x1, _x2) in tracks { @@ -468,10 +466,7 @@ pub fn view_inputs_header ( }))) } -pub fn view_inputs_row ( - tracks: impl TracksSizes<'_>, - port: () -) -> impl Draw { +pub fn view_inputs_row (tracks: impl TracksSizes<'_>, port: &MidiInput) -> impl Draw { east(w_exact(20, origin_w(east(" ● ", bold(true, fg(Rgb(255,255,255), port.port_name()))))), west(w_exact(4, ()), thunk(move|to: &mut Tui|{ for (_index, track, _x1, _x2) in tracks { diff --git a/src/device/arrange/clip.rs b/src/device/arrange/clip.rs index c2b4cad0..4dc537c1 100644 --- a/src/device/arrange/clip.rs +++ b/src/device/arrange/clip.rs @@ -27,13 +27,12 @@ pub trait ClipsView: TracksView + ScenesView { /// Draw clips per scene fn view_scenes_clips <'a> (&'a self) -> impl Draw + 'a { crate::view::view_scenes_clips( - self.tracks_with_sizes(), self.clips_size() - ) - } - /// Draw clips for tracks - fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Draw + 'a { - crate::view::view_track_clips( - self.scenes_with_sizes(), self.selection(), self.editor(), track_index, track + self.scenes_with_sizes(), + self.tracks_with_sizes(), + self.selection(), + self.editor(), + self.clips_size(), + self.is_editing(), ) } } diff --git a/src/device/arrange/scene.rs b/src/device/arrange/scene.rs index be16abfb..f2b15aa2 100644 --- a/src/device/arrange/scene.rs +++ b/src/device/arrange/scene.rs @@ -61,12 +61,7 @@ pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + fn view_scenes_names (&self) -> impl Draw { crate::view::view_scenes_names( - self.scenes_with_sizes() - ) - } - fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Draw + 'a { - crate::view::view_scene_name( - self.selected(), self.editor(), index, scene + self.scenes_with_sizes(), self.selection(), self.editor(), self.is_editing() ) } fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> { diff --git a/src/device/arrange/track.rs b/src/device/arrange/track.rs index e80a64f7..e35a4dd7 100644 --- a/src/device/arrange/track.rs +++ b/src/device/arrange/track.rs @@ -53,7 +53,7 @@ impl Track { tracks: impl Fn() -> U + Send + Sync + 'a, callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a ) -> impl Draw + 'a { - per(tracks, callback) + view_track_per(tracks, callback) } /// Create a new track connecting the [Sequencer] to a [Sampler]. @@ -180,7 +180,7 @@ pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { /// Draw name of each track fn view_track_names (&self, theme: ItemTheme) -> impl Draw { crate::view::view_track_names( - self.tracks_with_sizes(), self.tracks().len(), self.scenes().len(), self.selection() + theme, self.tracks_with_sizes(), self.tracks().len(), self.scenes().len(), self.selection() ) } /// Draw outputs per track @@ -251,12 +251,12 @@ impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut()); impl Arrangement { #[cfg(feature = "track")] pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw + '_ { - crate::view::view_inputs() + crate::view::view_inputs(self.tracks_with_sizes(), self.midi_ins().as_slice()) } #[cfg(feature = "track")] pub fn view_inputs_header (&self) -> impl Draw + '_ { - crate::view::view_inputs_header(self.tracks_with_sizes()) + crate::view::view_inputs_header(self.tracks_with_sizes(), self.midi_ins().as_slice()) } #[cfg(feature = "track")] From 027f69cd500ee2e4d61cbefb0b71cb6dc23645ef Mon Sep 17 00:00:00 2001 From: i do not exist Date: Wed, 24 Jun 2026 06:39:43 +0300 Subject: [PATCH 11/31] restruct: 45e --- src/app.rs | 1 + src/app/config.rs | 99 ++-------------- src/app/mode.rs | 93 +++++++++++++++ src/app/view.rs | 224 +++++++++++++++++------------------- src/device/arrange/clip.rs | 2 +- src/device/arrange/scene.rs | 12 +- src/device/arrange/track.rs | 95 ++++++++------- src/tek.rs | 6 +- tengri | 2 +- 9 files changed, 264 insertions(+), 270 deletions(-) create mode 100644 src/app/mode.rs diff --git a/src/app.rs b/src/app.rs index fc86e571..1c3738d3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,6 +3,7 @@ pub mod bind; pub use self::bind::*; pub mod cli; pub use self::cli::*; pub mod config; pub use self::config::*; pub mod view; pub use self::view::*; +pub mod mode; pub use self::mode::*; /// Total application state. /// diff --git a/src/app/config.rs b/src/app/config.rs index cceefff8..172a2b55 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -36,6 +36,13 @@ impl Config { const CONFIG_SUB: &'static str = "v0"; const CONFIG: &'static str = "tek.edn"; const DEFAULTS: &'static str = include_str!("../tek.edn"); + + pub fn watch (callback: impl Fn(Self)->T) -> Usually { + let config = Self::init_new(None)?; + let result = callback(config); + Ok(result) + } + pub fn init_new (dirs: Option) -> Usually { let mut config = Self::new(None); config.init()?; @@ -98,95 +105,3 @@ impl Config { self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() } } - -pub(crate) fn load_mode (modes: &Modes, name: &impl AsRef, body: &impl Language) -> Usually<()> { - let mut mode = Mode::default(); - body.each(|item|mode.add(item))?; - modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); - Ok(()) -} - -/// Collection of interaction modes. -pub type Modes = Arc, Arc>>>>>; - -impl Mode> { - /// Add a definition to the mode. - /// - /// Supported definitions: - /// - /// - (name ...) -> name - /// - (info ...) -> description - /// - (keys ...) -> key bindings - /// - (mode ...) -> submode - /// - ... -> view - /// - /// ``` - /// let mut mode: tek::Mode> = Default::default(); - /// mode.add("(name hello)").unwrap(); - /// ``` - pub fn add (&mut self, dsl: impl Language) -> Usually<()> { - Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { - //println!("Mode::add: {head} {:?}", expr.tail()); - let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - match head { - "name" => self.add_name(tail)?, - "info" => self.add_info(tail)?, - "keys" => self.add_keys(tail)?, - "mode" => self.add_mode(tail)?, - _ => self.add_view(tail)?, - }; - } else if let Ok(Some(word)) = dsl.word() { - self.add_view(word); - } else { - return Err(format!("Mode::add: unexpected: {dsl:?}").into()); - }) - - //DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into())) - //.word(|word|self.add_view(word)) - //.expr(|expr|expr.head(|head|{ - ////println!("Mode::add: {head} {:?}", expr.tail()); - //let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - //match head { - //"name" => self.add_name(tail), - //"info" => self.add_info(tail), - //"keys" => self.add_keys(tail)?, - //"mode" => self.add_mode(tail)?, - //_ => self.add_view(tail), - //}; - //})) - } - - fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.name.push(src.into()))) - } - fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.info.push(src.into()))) - } - fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.view.push(src.into()))) - } - fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?)) - } - fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(Some(if let Some(id) = dsl.head()? { - load_mode(&self.modes, &id, &dsl.tail())?; - } else { - return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); - })) - } -} - -/// Group of view and keys definitions. -/// -/// ``` -/// let mode = tek::Mode::>::default(); -/// ``` -#[derive(Default, Debug)] pub struct Mode { - pub path: PathBuf, - pub name: Vec, - pub info: Vec, - pub view: Vec, - pub keys: Vec, - pub modes: Modes, -} diff --git a/src/app/mode.rs b/src/app/mode.rs new file mode 100644 index 00000000..e5c5f8ea --- /dev/null +++ b/src/app/mode.rs @@ -0,0 +1,93 @@ +use crate::*; + +pub(crate) fn load_mode (modes: &Modes, name: &impl AsRef, body: &impl Language) -> Usually<()> { + let mut mode = Mode::default(); + body.each(|item|mode.add(item))?; + modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); + Ok(()) +} + +/// Collection of interaction modes. +pub type Modes = Arc, Arc>>>>>; + +impl Mode> { + /// Add a definition to the mode. + /// + /// Supported definitions: + /// + /// - (name ...) -> name + /// - (info ...) -> description + /// - (keys ...) -> key bindings + /// - (mode ...) -> submode + /// - ... -> view + /// + /// ``` + /// let mut mode: tek::Mode> = Default::default(); + /// mode.add("(name hello)").unwrap(); + /// ``` + pub fn add (&mut self, dsl: impl Language) -> Usually<()> { + Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { + //println!("Mode::add: {head} {:?}", expr.tail()); + let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + match head { + "name" => self.add_name(tail)?, + "info" => self.add_info(tail)?, + "keys" => self.add_keys(tail)?, + "mode" => self.add_mode(tail)?, + _ => self.add_view(tail)?, + }; + } else if let Ok(Some(word)) = dsl.word() { + self.add_view(word); + } else { + return Err(format!("Mode::add: unexpected: {dsl:?}").into()); + }) + + //DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into())) + //.word(|word|self.add_view(word)) + //.expr(|expr|expr.head(|head|{ + ////println!("Mode::add: {head} {:?}", expr.tail()); + //let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + //match head { + //"name" => self.add_name(tail), + //"info" => self.add_info(tail), + //"keys" => self.add_keys(tail)?, + //"mode" => self.add_mode(tail)?, + //_ => self.add_view(tail), + //}; + //})) + } + + fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.name.push(src.into()))) + } + fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.info.push(src.into()))) + } + fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.view.push(src.into()))) + } + fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?)) + } + fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(Some(if let Some(id) = dsl.head()? { + load_mode(&self.modes, &id, &dsl.tail())?; + } else { + return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); + })) + } +} + +/// Group of view and keys definitions. +/// +/// ``` +/// let mode = tek::Mode::>::default(); +/// ``` +#[derive(Default, Debug)] pub struct Mode { + pub path: PathBuf, + pub name: Vec, + pub info: Vec, + pub view: Vec, + pub keys: Vec, + pub modes: Modes, +} diff --git a/src/app/view.rs b/src/app/view.rs index 21699419..b11832a8 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -30,7 +30,7 @@ pub fn view_logo () -> impl Draw { pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw { let theme = ItemTheme::G[96]; bg(Black, east!(above( - wh_full(origin_w(button_play_pause(play))), + wh_full(origin_w(button_play_pause(play, false))), wh_full(origin_e(east!( field_h(theme, "BPM", bpm), field_h(theme, "Beat", beat), @@ -56,10 +56,12 @@ pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl D } /// ``` -/// let _ = tek::button_play_pause(true); +/// let _ = tek::button_play_pause(true, true); +/// let _ = tek::button_play_pause(true, false); +/// let _ = tek::button_play_pause(false, true); +/// let _ = tek::button_play_pause(false, false); /// ``` -pub fn button_play_pause (playing: bool) -> impl Draw { - let compact = true;//self.is_editing(); +pub fn button_play_pause (playing: bool, compact: bool) -> impl Draw { bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, either(compact, thunk(move|to: &mut Tui|w_exact(9, either(playing, @@ -202,87 +204,84 @@ pub fn view_io_ports <'a, T: PortsSizes<'a>> ( }) } -pub fn view_scenes_clips <'a> ( - scenes: impl ScenesSizes<'a>, - tracks: impl TracksSizes<'a>, - select: &Selection, - editor: Option<&MidiEditor>, - size: &Size, - is_editing: bool, +pub fn view_scenes_clips <'a, S: ScenesSizes<'a>> ( + scenes: impl Fn()->S, + tracks: impl TracksSizes<'a>, + select: &Selection, + editor: Option<&MidiEditor>, + size: &Size, + editing: bool, ) -> impl Draw { - size.of(wh_full(above(wh_full(origin_se(fg(Green, format!("{}x{}", size.w(), size.h())))), - thunk(move |to: &mut Tui|{ - for (index, track, _, _) in tracks { - let clips = view_track_clips(&scenes, select, editor, index, track, is_editing); - w_exact(track.width as u16, h_full(clips)).draw(to); - } - Ok(XYWH(0, 0, 0, 0)) - })))) -} - -pub fn view_track_clips <'a> ( - scenes: &impl ScenesSizes<'a>, - select: &Selection, - editor: Option<&MidiEditor>, - index: usize, - track: &Track, - is_editing: bool, -) -> impl Draw { - thunk(move|to: &mut Tui|{ - - for (scene_index, scene, ..) in scenes { - - let ( - name, theme - ): (Arc, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(index) { - let clip = clip.read().unwrap(); - (format!(" ⏹ {}", &clip.name).into(), clip.color) - } else { - (" ⏹ -- ".into(), ItemTheme::G[32]) - }; - - let fg = theme.lightest.term; - - let mut outline = theme.base.term; - - let bg = if select.track() == Some(index) && select.scene() == Some(scene_index) { - outline = theme.lighter.term; - theme.light.term - } else if select.track() == Some(index) || select.scene() == Some(scene_index) { - outline = theme.darkest.term; - theme.base.term - } else { - theme.dark.term - }; - - let w = if select.track() == Some(index) && let Some(editor) = editor { - (editor.size.w() as usize).max(24).max(track.width) - } else { - track.width - } as u16; - - let y = if select.scene() == Some(scene_index) && let Some(editor) = editor { - (editor.size.h() as usize).max(12) - } else { - Self::H_SCENE as usize - } as u16; - - let is_selected = is_editing && select.track() == Some(index) && select.scene() == Some(scene_index); - + let status = wh_full(origin_se(fg(Green, format!("{}x{}", size.w(), size.h())))); + let tracks = iter_once(tracks, move|(track_index, track, _, _), _| { + let scenes = iter_once(scenes(), move|(scene_index, scene, _, _), _| { + let (name, theme): (Arc, ItemTheme) = scene_name_theme(scene, track_index); + let f = theme.lightest.term; + let (b, o) = scene_bg(theme, select, track_index, scene_index); + let w = scene_w(track, select, track_index, editor); + let y = scene_y(select, scene_index, editor); + let is_selected = scene_sel(select, track_index, scene_index, editing); wh_exact(Some(w), Some(y), below( - wh_full(Outer(true, Style::default().fg(outline))), + wh_full(Outer(true, Style::default().fg(o))), wh_full(below( below( - fg_bg(outline, bg, wh_full("")), - wh_full(origin_nw(fg_bg(fg, bg, bold(true, &name)))), + fg_bg(o, b, wh_full("")), + wh_full(origin_nw(fg_bg(f, b, bold(true, name)))), ), - wh_full(when(is_selected, editor.map(|e|e.view())))))) - ).draw(to); + wh_full(when(is_selected, editor.map(|e|e.view()))))))) + }); + w_exact(track.width as u16, h_full(scenes)) + }); + return size.of(wh_full(above(status, tracks))); + + fn scene_name_theme (scene: &Scene, track_index: usize) -> (Arc, ItemTheme) { + if let Some(Some(clip)) = &scene.clips.get(track_index) { + let clip = clip.read().unwrap(); + (format!(" ⏹ {}", &clip.name).into(), clip.color) + } else { + (" ⏹ -- ".into(), ItemTheme::G[32]) } + } - Ok(XYWH(0, 0, 0, 0)) - }) + fn scene_bg ( + theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize + ) -> (Color, Color) { + let mut outline = theme.base.term; + (if select.track() == Some(track_index) && select.scene() == Some(scene_index) { + outline = theme.lighter.term; + theme.light.term + } else if select.track() == Some(track_index) || select.scene() == Some(scene_index) { + outline = theme.darkest.term; + theme.base.term + } else { + theme.dark.term + }, outline) + } + + fn scene_w ( + track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor> + ) -> u16 { + if select.track() == Some(track_index) && let Some(editor) = editor { + (editor.size.w() as usize).max(24).max(track.width) as u16 + } else { + track.width as u16 + } + } + + fn scene_y ( + select: &Selection, scene_index: usize, editor: Option<&MidiEditor> + ) -> u16 { + if select.scene() == Some(scene_index) && let Some(editor) = editor { + editor.size.h().max(12) + } else { + H_SCENE as u16 + } + } + + fn scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool { + editing && select.track() == Some(track_index) && select.scene() == Some(scene_index) + } } pub fn view_track_names ( @@ -318,9 +317,7 @@ pub fn view_track_names ( } pub fn view_track_outputs ( - theme: ItemTheme, - tracks: impl TracksSizes<'_>, - midi_outs: impl Iterator, + theme: ItemTheme, tracks: impl TracksSizes<'_>, midi_outs: impl Iterator, ) -> impl Draw { view_track_row_section(theme, south(w_full(origin_w(button_2("o", "utput", false))), @@ -345,9 +342,7 @@ pub fn view_track_outputs ( } pub fn view_track_inputs ( - theme: ItemTheme, - tracks: impl TracksSizes<'_>, - height: u16, + theme: ItemTheme, tracks: impl TracksSizes<'_>, height: u16, ) -> impl Draw { view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), bg(theme.darker.term, origin_w(thunk(move|to: &mut Tui|{ @@ -393,7 +388,7 @@ pub fn view_scene_name ( let h = if select.scene() == Some(index) && let Some(_editor) = editor { 7 } else { - Self::H_SCENE as u16 + H_SCENE as u16 }; let a = w_full(origin_w(east(format!("·s{index:02} "), fg(g(255), bold(true, &scene.name))))); @@ -442,43 +437,34 @@ pub fn view_per_track () -> impl Draw {} pub fn view_per_track_top () -> impl Draw {} pub fn view_inputs (tracks: impl TracksSizes<'_>, midi_ins: &[MidiInput]) -> impl Draw { - let header = h_exact(1, view_inputs_header(tracks, midi_ins)); - south(header, thunk(move |to: &mut Tui|{ - for (index, port) in midi_ins.iter().enumerate() { - x_push(index as u16 * 10, h_exact(1, view_inputs_row(tracks, port))).draw(to); - } - todo!() - })) -} - -pub fn view_inputs_header (tracks: impl TracksSizes<'_>, midi_ins: &[MidiInput]) -> impl Draw { - east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", midi_ins.len()), false))), - west(w_exact(4, button_2("I", "+", false)), thunk(move|to: &mut Tui|{ - for (_index, track, x1, _x2) in tracks { - #[cfg(feature = "track")] + let title_1 = wh_exact(Some(20), Some(1), origin_w(button_3("i", "nput ", format!("{}", midi_ins.len()), false))); + let title_2 = wh_exact(Some(4), Some(1), w_exact(4, button_2("I", "+", false))); + east(title_1, west(title_2, thunk(move|to: &mut Tui|{ + for (_index, track, x1, _x2) in tracks { + south( x_push(x1 as u16, bg(track.color.dark.term, origin_w(w_exact(track.width as u16, east!( either(track.sequencer.monitoring, fg(Green, "mon "), "mon "), either(track.sequencer.recording, fg(Red, "rec "), "rec "), either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "), - ))))).draw(to); - } - todo!() - }))) -} - -pub fn view_inputs_row (tracks: impl TracksSizes<'_>, port: &MidiInput) -> impl Draw { - east(w_exact(20, origin_w(east(" ● ", bold(true, fg(Rgb(255,255,255), port.port_name()))))), - west(w_exact(4, ()), thunk(move|to: &mut Tui|{ - for (_index, track, _x1, _x2) in tracks { - #[cfg(feature = "track")] - bg(track.color.darker.term, origin_w(w_exact(track.width as u16, east!( - either(track.sequencer.monitoring, fg(Green, " ● "), " · "), - either(track.sequencer.recording, fg(Red, " ● "), " · "), - either(track.sequencer.overdub, fg(Yellow, " ● "), " · "), - )))).draw(to); - } - todo!() - }))) + ))))), + thunk(move |to: &mut Tui|{ + for (index, port) in midi_ins.iter().enumerate() { + x_push(index as u16 * 10, h_exact(1, east(w_exact(20, origin_w(east(" ● ", bold(true, fg(Rgb(255,255,255), port.port_name()))))), + west(w_exact(4, ()), thunk(move|to: &mut Tui|{ + bg(track.color.darker.term, origin_w(w_exact(track.width as u16, east!( + either(track.sequencer.monitoring, fg(Green, " ● "), " · "), + either(track.sequencer.recording, fg(Red, " ● "), " · "), + either(track.sequencer.overdub, fg(Yellow, " ● "), " · "), + )))).draw(to); + todo!() + }))))).draw(to); + } + todo!() + }) + ).draw(to); + } + todo!() + }))) } pub fn view_outputs ( @@ -539,14 +525,14 @@ pub fn view_outputs ( pub fn view_track_devices ( theme: ItemTheme, - tracks: &impl TracksSizes<'_>, + tracks: impl TracksSizes<'_>, track: Option<&Track>, h: u16, ) -> impl Draw { view_track_row_section(theme, button_3("d", "evice", format!("{}", track.map(|t|t.devices.len()).unwrap_or(0)), false), button_2("D", "+", false), - iter(tracks, move|(_, track, _x1, _x2), index| wh_exact( + iter_once(tracks, move|(_, track, _x1, _x2), index| wh_exact( Some(track_width(index, track)), Some(h + 1), bg(track.color.dark.term, origin_nw(iter_south(move||0..h, diff --git a/src/device/arrange/clip.rs b/src/device/arrange/clip.rs index 4dc537c1..05c7b9c5 100644 --- a/src/device/arrange/clip.rs +++ b/src/device/arrange/clip.rs @@ -27,7 +27,7 @@ pub trait ClipsView: TracksView + ScenesView { /// Draw clips per scene fn view_scenes_clips <'a> (&'a self) -> impl Draw + 'a { crate::view::view_scenes_clips( - self.scenes_with_sizes(), + ||self.scenes_with_sizes(), self.tracks_with_sizes(), self.selection(), self.editor(), diff --git a/src/device/arrange/scene.rs b/src/device/arrange/scene.rs index f2b15aa2..2d1c284e 100644 --- a/src/device/arrange/scene.rs +++ b/src/device/arrange/scene.rs @@ -1,5 +1,11 @@ use crate::*; +/// Default scene height. +pub const H_SCENE: usize = 2; + +/// Default editor height. +pub const H_EDITOR: usize = 15; + /// A scene consists of a set of clips to play together. /// /// ``` @@ -51,10 +57,6 @@ impl Scene { } pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync { - /// Default scene height. - const H_SCENE: usize = 2; - /// Default editor height. - const H_EDITOR: usize = 15; fn h_scenes (&self) -> u16; fn w_side (&self) -> u16; fn w_mid (&self) -> u16; @@ -70,7 +72,7 @@ pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + let height = if self.selection().scene() == Some(s) && self.editor().is_some() { 8 } else { - Self::H_SCENE + H_SCENE }; if y + height <= self.clips_size().h() as usize { let data = (s, scene, y, y + height); diff --git a/src/device/arrange/track.rs b/src/device/arrange/track.rs index e35a4dd7..5ea42948 100644 --- a/src/device/arrange/track.rs +++ b/src/device/arrange/track.rs @@ -223,71 +223,66 @@ impl HasTrackScroll for Arrangement { def_command!(TrackCommand: |track: Track| { Stop => { track.sequencer.enqueue_next(None); Ok(None) }, - SetMute { mute: Option } => todo!(), - SetSolo { solo: Option } => todo!(), - SetSize { size: usize } => todo!(), - SetZoom { zoom: usize } => todo!(), - SetName { name: Arc } => - swap_value(&mut track.name, name, |name|Self::SetName { name }), - SetColor { color: ItemTheme } => - swap_value(&mut track.color, color, |color|Self::SetColor { color }), - SetRec { rec: Option } => - toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }), - SetMon { mon: Option } => - toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }), + SetRec { rec: Option } => toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }), + SetMon { mon: Option } => toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }), + SetMute { mute: Option } => todo!(), + SetSolo { solo: Option } => todo!(), + SetSize { size: usize } => todo!(), + SetZoom { zoom: usize } => todo!(), + SetName { name: Arc } => swap_value(&mut track.name, name, |name|Self::SetName { name }), + SetColor { color: ItemTheme } => swap_value(&mut track.color, color, |color|Self::SetColor { color }), }); -impl>+AsMut>> HasTracks for T {} -impl+AsMutOpt+Send+Sync> HasTrack for T {} -impl TracksView for T {} +impl>+AsMut>> HasTracks for T {} +impl+AsMutOpt+Send+Sync> HasTrack for T {} +impl TracksView for T {} +impl_has!(Vec: |self: Arrangement| self.tracks); +impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track()); +impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut()); impl_as_ref!(Vec: |self: App| self.project.as_ref()); impl_as_mut!(Vec: |self: App| self.project.as_mut()); #[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt()); #[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt()); -impl_has!(Vec: |self: Arrangement| self.tracks); -impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track()); -impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut()); impl Arrangement { - #[cfg(feature = "track")] + pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw + '_ { - crate::view::view_inputs(self.tracks_with_sizes(), self.midi_ins().as_slice()) + crate::view::view_inputs( + self.tracks_with_sizes(), self.midi_ins().as_slice() + ) } - #[cfg(feature = "track")] - pub fn view_inputs_header (&self) -> impl Draw + '_ { - crate::view::view_inputs_header(self.tracks_with_sizes(), self.midi_ins().as_slice()) - } - - #[cfg(feature = "track")] - pub fn view_inputs_row (&self, port: &MidiInput) -> impl Draw { - crate::view::view_inputs_row(port) - } - - #[cfg(feature = "track")] pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw { + crate::view::view_outputs( + theme, self.tracks_with_sizes(), self.midi_outs(), self.outputs_height() + ) + } + + pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw { + crate::view::view_track_devices( + theme, self.tracks_with_sizes(), self.track(), self.devices_height() + ) + } + + fn devices_height (&self) -> u16 { + let mut h = 2; + for track in self.tracks().iter() { + h = h.max(track.devices.len() * 2); + } + h as u16 + } + + fn outputs_height (&self) -> u16 { let mut h = 1; for output in self.midi_outs().iter() { h += 1 + output.connections.len(); } - let h = h as u16; - crate::view::view_outputs(self.tracks_with_sizes()) - } - - #[cfg(feature = "track")] - pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw { - let mut h = 2u16; - for track in self.tracks().iter() { - h = h.max(track.devices.len() as u16 * 2); - } - crate::view::view_track_devices(||self.tracks_with_sizes(), self.track(), h) + h as u16 } /// Add multiple tracks - #[cfg(feature = "track")] pub fn tracks_add ( - &mut self, - count: usize, width: Option, - mins: &[Connect], mouts: &[Connect], + pub fn tracks_add ( + &mut self, count: usize, width: Option, mins: &[Connect], mouts: &[Connect], ) -> Usually<()> { let track_color_1 = ItemColor::random(); let track_color_2 = ItemColor::random(); @@ -302,10 +297,12 @@ impl Arrangement { } /// Add a track - #[cfg(feature = "track")] pub fn track_add ( + pub fn track_add ( &mut self, - name: Option<&str>, color: Option, - mins: &[Connect], mouts: &[Connect], + name: Option<&str>, + color: Option, + mins: &[Connect], + mouts: &[Connect], ) -> Usually<(usize, &mut Track)> { let name: Arc = name.map_or_else( ||format!("trk{:02}", self.track_last).into(), diff --git a/src/tek.rs b/src/tek.rs index d5060d73..115d37c4 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -89,9 +89,9 @@ pub(crate) use tengri::{ #[cfg(feature = "cli")] pub fn main () -> Usually<()> { Config::watch(|config|{ - Exit::enter(|exit|{ - Jack::connect("tek", |jack|{ - let project = Arrangement::new(&jack, &Clock::new(&jack, 51)); + Exit::run(|exit|{ + Jack::new_run("tek", |jack|{ + let project = Arrangement::new(&jack, &Clock::new(&jack, Some(51.))); let state = App::new_shared(jack, config, project, ":menu"); let keyboard = tui_input(&exit, &state, Duration::from_millis(100))?; let terminal = tui_output(&exit, &state, Duration::from_millis(10))?; diff --git a/tengri b/tengri index e0781571..0273d2ac 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit e0781571c42cb72e7445a2adbdb7a99e37600562 +Subproject commit 0273d2ac75b8a144ea03b4bdd94d08003f3589df From 8b6ab2fd08612697b1a80885e1dde87de1fded9d Mon Sep 17 00:00:00 2001 From: i do not exist Date: Wed, 24 Jun 2026 09:28:36 +0300 Subject: [PATCH 12/31] restruct: 40e --- src/app.rs | 11 +- src/app/cli.rs | 4 +- src/device/clock.rs | 287 ++--------------------------------- src/device/clock/moment.rs | 109 +++++++++++++ src/device/clock/ticker.rs | 16 ++ src/device/clock/timebase.rs | 160 +++++++++++++++++++ src/device/editor.rs | 6 +- src/tek.rs | 28 ++-- 8 files changed, 331 insertions(+), 290 deletions(-) create mode 100644 src/device/clock/moment.rs create mode 100644 src/device/clock/ticker.rs create mode 100644 src/device/clock/timebase.rs diff --git a/src/app.rs b/src/app.rs index 1c3738d3..85922694 100644 --- a/src/app.rs +++ b/src/app.rs @@ -82,7 +82,16 @@ impl App { pub fn new_shared ( jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef ) -> Arc> { - Arc::new(RwLock::new(App::new(jack, ":menu", config, project))) + Arc::new(RwLock::new(App::new(jack, project, config, ":menu"))) + } + + pub fn new_shared_run ( + exit: &Exit, jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef + ) -> Usually<(Task, Task)> { + let state = Self::new_shared(&jack, project, config, mode); + let keyboard = tui_input(&exit, &state, Duration::from_millis(100))?; + let terminal = tui_output(&exit, &state, Duration::from_millis(10))?; + Ok((keyboard, terminal)) } pub fn confirm (&mut self) -> Perhaps { diff --git a/src/app/cli.rs b/src/app/cli.rs index 99e0b53e..1698d68f 100644 --- a/src/app/cli.rs +++ b/src/app/cli.rs @@ -118,13 +118,13 @@ impl Action { // Run the [Tui] and [Jack] threads with the [App] state. Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{ // Between jack init and app's first cycle: - jack.sync_lead(sync_lead, |mut state|{ + jack.sync_lead(*sync_lead, |mut state|{ let clock = app.clock(); clock.playhead.update_from_sample(state.position.frame() as f64); state.position.bbt = Some(clock.bbt()); state.position })?; - jack.sync_follow(sync_follow)?; + jack.sync_follow(*sync_follow)?; // FIXME: They don't work properly. Ok(app) })?)?; diff --git a/src/device/clock.rs b/src/device/clock.rs index a0516c13..9c78dc96 100644 --- a/src/device/clock.rs +++ b/src/device/clock.rs @@ -43,80 +43,6 @@ pub trait HasClock: AsRef + AsMut { #[cfg(feature = "port")] pub click_out: Arc>>, } -/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) -/// -/// ``` -/// let _ = tek::Timebase::default(); -/// ``` -#[derive(Debug, Clone)] pub struct Timebase { - /// Audio samples per second - pub sr: SampleRate, - /// MIDI beats per minute - pub bpm: Bpm, - /// MIDI ticks per beat - pub ppq: Ppq, -} - -/// Iterator that emits subsequent ticks within a range. -/// -/// ``` -/// let iter = tek::Ticker::default(); -/// ``` -#[derive(Debug, Default)] pub struct Ticker { - pub spp: f64, - pub sample: usize, - pub start: usize, - pub end: usize, -} - -/// A point in time in all time scales (microsecond, sample, MIDI pulse) -/// -/// ``` -/// let _ = tek::Moment::default(); -/// ``` -#[derive(Debug, Default, Clone)] pub struct Moment { - pub timebase: Arc, - /// Current time in microseconds - pub usec: Microsecond, - /// Current time in audio samples - pub sample: SampleCount, - /// Current time in MIDI pulses - pub pulse: Pulse, -} - -/// -/// ``` -/// let _ = tek::Moment2::default(); -/// ``` -#[derive(Debug, Clone, Default)] pub enum Moment2 { - #[default] None, - Zero, - Usec(Microsecond), - Sample(SampleCount), - Pulse(Pulse), -} - -/// MIDI resolution in PPQ (pulses per quarter note) -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Ppq (pub(crate) AtomicF64); - -/// Timestamp in MIDI pulses -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Pulse (pub(crate) AtomicF64); - -/// Tempo in beats per minute -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Bpm (pub(crate) AtomicF64); - /// Quantization setting for launching clips /// /// ``` @@ -131,27 +57,6 @@ pub trait HasClock: AsRef + AsMut { /// ``` #[derive(Debug, Default)] pub struct Quantize (pub(crate) AtomicF64); -/// Timestamp in audio samples -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct SampleCount (pub(crate) AtomicF64); - -/// Audio sample rate in Hz (samples per second) -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct SampleRate (pub(crate) AtomicF64); - -/// Timestamp in microseconds -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Microsecond (pub(crate) AtomicF64); - /// A unit of time, represented as an atomic 64-bit float. /// /// According to https://stackoverflow.com/a/873367, as per IEEE754, @@ -172,6 +77,8 @@ pub trait TimeUnit: InteriorMutable {} pub time: Memo, String>, } +pub const DEFAULT_PPQ: f64 = 96.0; + /// FIXME: remove this and use PPQ from timebase everywhere: pub const PPQ: usize = 96; @@ -206,8 +113,6 @@ pub const NOTE_NAMES: [&str; 128] = [ "C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10", ]; -pub const DEFAULT_PPQ: f64 = 96.0; - def_command!(ClockCommand: |clock: Clock| { SeekUsec { usec: f64 } => { clock.playhead.update_from_usec(*usec); Ok(None) }, @@ -234,53 +139,6 @@ def_command!(ClockCommand: |clock: Clock| { }), }); -impl Moment { - pub fn zero (timebase: &Arc) -> Self { - Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() } - } - pub fn from_usec (timebase: &Arc, usec: f64) -> Self { - Self { - usec: usec.into(), - sample: timebase.sr.usecs_to_sample(usec).into(), - pulse: timebase.usecs_to_pulse(usec).into(), - timebase: timebase.clone(), - } - } - pub fn from_sample (timebase: &Arc, sample: f64) -> Self { - Self { - sample: sample.into(), - usec: timebase.sr.samples_to_usec(sample).into(), - pulse: timebase.samples_to_pulse(sample).into(), - timebase: timebase.clone(), - } - } - pub fn from_pulse (timebase: &Arc, pulse: f64) -> Self { - Self { - pulse: pulse.into(), - sample: timebase.pulses_to_sample(pulse).into(), - usec: timebase.pulses_to_usec(pulse).into(), - timebase: timebase.clone(), - } - } - #[inline] pub fn update_from_usec (&self, usec: f64) { - self.usec.set(usec); - self.pulse.set(self.timebase.usecs_to_pulse(usec)); - self.sample.set(self.timebase.sr.usecs_to_sample(usec)); - } - #[inline] pub fn update_from_sample (&self, sample: f64) { - self.usec.set(self.timebase.sr.samples_to_usec(sample)); - self.pulse.set(self.timebase.samples_to_pulse(sample)); - self.sample.set(sample); - } - #[inline] pub fn update_from_pulse (&self, pulse: f64) { - self.usec.set(self.timebase.pulses_to_usec(pulse)); - self.pulse.set(pulse); - self.sample.set(self.timebase.pulses_to_sample(pulse)); - } - #[inline] pub fn format_beat (&self) -> Arc { - self.timebase.format_beats_1(self.pulse.get()).into() - } -} impl LaunchSync { pub fn next (&self) -> f64 { note_duration_next(self.get() as usize) as f64 @@ -289,6 +147,7 @@ impl LaunchSync { note_duration_prev(self.get() as usize) as f64 } } + impl Quantize { pub fn next (&self) -> f64 { note_duration_next(self.get() as usize) as f64 @@ -297,133 +156,6 @@ impl Quantize { note_duration_prev(self.get() as usize) as f64 } } -impl Timebase { - /// Specify sample rate, BPM and PPQ - pub fn new ( - s: impl Into, - b: impl Into, - p: impl Into - ) -> Self { - Self { sr: s.into(), bpm: b.into(), ppq: p.into() } - } - /// Iterate over ticks between start and end. - #[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> Ticker { - Ticker { spp: self.samples_per_pulse(), sample: start, start, end } - } - /// Return the duration fo a beat in microseconds - #[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() } - /// Return the number of beats in a second - #[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 } - /// Return the number of microseconds corresponding to a note of the given duration - #[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 { - 4.0 * self.usec_per_beat() * num / den - } - /// Return duration of a pulse in microseconds (BPM-dependent) - #[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() } - /// Return duration of a pulse in microseconds (BPM-dependent) - #[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() } - /// Return number of pulses to which a number of microseconds corresponds (BPM-dependent) - #[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() } - /// Convert a number of pulses to a sample number (SR- and BPM-dependent) - #[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() } - /// Return number of pulses in a second (BPM-dependent) - #[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() } - /// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent) - #[inline] pub fn pulses_per_sample (&self) -> f64 { - self.usec_per_pulse() / self.sr.usec_per_sample() - } - /// Return number of samples in a pulse (SR- and BPM-dependent) - #[inline] pub fn samples_per_pulse (&self) -> f64 { - self.sr.get() / self.pulses_per_second() - } - /// Convert a number of pulses to a sample number (SR- and BPM-dependent) - #[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 { - self.pulses_per_sample() * p - } - /// Convert a number of samples to a pulse number (SR- and BPM-dependent) - #[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 { - s / self.pulses_per_sample() - } - /// Return the number of samples corresponding to a note of the given duration - #[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 { - self.usec_to_sample(self.note_to_usec(note)) - } - /// Return the number of samples corresponding to the given number of microseconds - #[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 { - usec * self.sr.get() / 1000f64 - } - /// Return the quantized position of a moment in time given a step - #[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) { - let step = self.note_to_usec(step); - (time / step, time % step) - } - /// Quantize a collection of events - #[inline] pub fn quantize_into + Sized, T> ( - &self, step: (f64, f64), events: E - ) -> Vec<(f64, f64)> { - events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 0 - #[inline] pub fn format_beats_0 (&self, pulse: f64) -> Arc { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; - format!("{}.{}.{pulses:02}", beats / 4, beats % 4).into() - } - /// Format a number of pulses into Beat.Bar starting from 0 - #[inline] pub fn format_beats_0_short (&self, pulse: f64) -> Arc { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let beats = if ppq > 0 { pulse / ppq } else { 0 }; - format!("{}.{}", beats / 4, beats % 4).into() - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 1 - #[inline] pub fn format_beats_1 (&self, pulse: f64) -> Arc { - let mut string = String::with_capacity(16); - self.format_beats_1_to(&mut string, pulse).expect("failed to format {pulse} into beat"); - string.into() - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 1 - #[inline] pub fn format_beats_1_to (&self, w: &mut impl std::fmt::Write, pulse: f64) -> Result<(), std::fmt::Error> { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; - write!(w, "{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1) - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 1 - #[inline] pub fn format_beats_1_short (&self, pulse: f64) -> Arc { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let beats = if ppq > 0 { pulse / ppq } else { 0 }; - format!("{}.{}", beats / 4 + 1, beats % 4 + 1).into() - } -} -impl SampleRate { - /// Return the duration of a sample in microseconds (floating) - #[inline] pub fn usec_per_sample (&self) -> f64 { - 1_000_000f64 / self.get() - } - /// Return the duration of a sample in microseconds (floating) - #[inline] pub fn sample_per_usec (&self) -> f64 { - self.get() / 1_000_000f64 - } - /// Convert a number of samples to microseconds (floating) - #[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 { - self.usec_per_sample() * samples - } - /// Convert a number of microseconds to samples (floating) - #[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 { - self.sample_per_usec() * usecs - } -} -impl Microsecond { - #[inline] pub fn format_msu (&self) -> Arc { - let usecs = self.get() as usize; - let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); - let (minutes, seconds) = (seconds / 60, seconds % 60); - format!("{minutes}:{seconds:02}:{msecs:03}").into() - } -} /// Define and implement a unit of time #[macro_export] macro_rules! impl_time_unit { @@ -467,6 +199,7 @@ impl std::fmt::Debug for Clock { .finish() } } + impl Clock { pub fn new (jack: &Jack<'static>, bpm: Option) -> Usually { let (chunk, transport) = jack.with_client(|c|(c.buffer_size(), c.transport())); @@ -625,6 +358,7 @@ impl Clock { self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize) } } + impl Clock { fn _todo_provide_u32 (&self) -> u32 { todo!() @@ -636,11 +370,13 @@ impl Clock { todo!() } } + impl Act for ClockCommand { fn act (&self, state: &mut T) -> Perhaps { self.act(state.clock_mut()) // awesome } } + impl ClockView { pub const BEAT_EMPTY: &'static str = "-.-.--"; pub const TIME_EMPTY: &'static str = "-.---s"; @@ -652,7 +388,7 @@ impl ClockView { let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); let mut cache = cache.write().unwrap(); cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}")); - cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms")); + cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms")); cache.sr.update(Some((compact, rate)), |buf,_,_|{ buf.clear(); if compact { @@ -678,6 +414,7 @@ impl ClockView { } } } + impl_default!(ClockView: { let mut beat = String::with_capacity(16); let _ = write!(beat, "{}", Self::BEAT_EMPTY); @@ -696,7 +433,7 @@ impl_default!(ClockView: { }); #[cfg(feature = "clock")] impl_has!(Clock: |self: Track|self.sequencer.clock); -impl_default!(Timebase: Self::new(48000f64, 150f64, DEFAULT_PPQ)); +impl_default!(Timebase: Self::new(48000f64, 150f64, DEFAULT_PPQ)); impl_time_unit!(SampleCount); impl_time_unit!(SampleRate); impl_time_unit!(Microsecond); @@ -705,3 +442,7 @@ impl_time_unit!(Ppq); impl_time_unit!(Pulse); impl_time_unit!(Bpm); impl_time_unit!(LaunchSync); + +mod moment; pub use self::moment::*; +mod ticker; pub use self::ticker::*; +mod timebase; pub use self::timebase::*; diff --git a/src/device/clock/moment.rs b/src/device/clock/moment.rs new file mode 100644 index 00000000..bf62a822 --- /dev/null +++ b/src/device/clock/moment.rs @@ -0,0 +1,109 @@ +use crate::*; +use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; +use ::atomic_float::AtomicF64; +use ::tengri::{draw::*, term::*}; + +/// A point in time in all time scales (microsecond, sample, MIDI pulse) +/// +/// ``` +/// let _ = tek::Moment::default(); +/// ``` +#[derive(Debug, Default, Clone)] pub struct Moment { + pub timebase: Arc, + /// Current time in microseconds + pub usec: Microsecond, + /// Current time in audio samples + pub sample: SampleCount, + /// Current time in MIDI pulses + pub pulse: Pulse, +} + +/// +/// ``` +/// let _ = tek::Moment2::default(); +/// ``` +#[derive(Debug, Clone, Default)] pub enum Moment2 { + #[default] None, + Zero, + Usec(Microsecond), + Sample(SampleCount), + Pulse(Pulse), +} + +/// Timestamp in microseconds +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Microsecond (pub(crate) AtomicF64); + +/// Timestamp in audio samples +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct SampleCount (pub(crate) AtomicF64); + +/// Timestamp in MIDI pulses +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Pulse (pub(crate) AtomicF64); + +impl Moment { + pub fn zero (timebase: &Arc) -> Self { + Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() } + } + pub fn from_usec (timebase: &Arc, usec: f64) -> Self { + Self { + usec: usec.into(), + sample: timebase.sr.usecs_to_sample(usec).into(), + pulse: timebase.usecs_to_pulse(usec).into(), + timebase: timebase.clone(), + } + } + pub fn from_sample (timebase: &Arc, sample: f64) -> Self { + Self { + sample: sample.into(), + usec: timebase.sr.samples_to_usec(sample).into(), + pulse: timebase.samples_to_pulse(sample).into(), + timebase: timebase.clone(), + } + } + pub fn from_pulse (timebase: &Arc, pulse: f64) -> Self { + Self { + pulse: pulse.into(), + sample: timebase.pulses_to_sample(pulse).into(), + usec: timebase.pulses_to_usec(pulse).into(), + timebase: timebase.clone(), + } + } + #[inline] pub fn update_from_usec (&self, usec: f64) { + self.usec.set(usec); + self.pulse.set(self.timebase.usecs_to_pulse(usec)); + self.sample.set(self.timebase.sr.usecs_to_sample(usec)); + } + #[inline] pub fn update_from_sample (&self, sample: f64) { + self.usec.set(self.timebase.sr.samples_to_usec(sample)); + self.pulse.set(self.timebase.samples_to_pulse(sample)); + self.sample.set(sample); + } + #[inline] pub fn update_from_pulse (&self, pulse: f64) { + self.usec.set(self.timebase.pulses_to_usec(pulse)); + self.pulse.set(pulse); + self.sample.set(self.timebase.pulses_to_sample(pulse)); + } + #[inline] pub fn format_beat (&self) -> Arc { + self.timebase.format_beats_1(self.pulse.get()).into() + } +} + +impl Microsecond { + #[inline] pub fn format_msu (&self) -> Arc { + let usecs = self.get() as usize; + let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); + let (minutes, seconds) = (seconds / 60, seconds % 60); + format!("{minutes}:{seconds:02}:{msecs:03}").into() + } +} diff --git a/src/device/clock/ticker.rs b/src/device/clock/ticker.rs new file mode 100644 index 00000000..50656d77 --- /dev/null +++ b/src/device/clock/ticker.rs @@ -0,0 +1,16 @@ +use crate::*; +use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; +use ::atomic_float::AtomicF64; +use ::tengri::{draw::*, term::*}; + +/// Iterator that emits subsequent ticks within a range. +/// +/// ``` +/// let iter = tek::Ticker::default(); +/// ``` +#[derive(Debug, Default)] pub struct Ticker { + pub spp: f64, + pub sample: usize, + pub start: usize, + pub end: usize, +} diff --git a/src/device/clock/timebase.rs b/src/device/clock/timebase.rs new file mode 100644 index 00000000..923e054a --- /dev/null +++ b/src/device/clock/timebase.rs @@ -0,0 +1,160 @@ +use crate::*; +use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; +use ::atomic_float::AtomicF64; +use ::tengri::{draw::*, term::*}; + +/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) +/// +/// ``` +/// let _ = tek::Timebase::default(); +/// ``` +#[derive(Debug, Clone)] pub struct Timebase { + /// Audio samples per second + pub sr: SampleRate, + /// MIDI beats per minute + pub bpm: Bpm, + /// MIDI ticks per beat + pub ppq: Ppq, +} + +/// Audio sample rate in Hz (samples per second) +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct SampleRate (pub(crate) AtomicF64); + +/// Tempo in beats per minute +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Bpm (pub(crate) AtomicF64); + +/// MIDI resolution in PPQ (pulses per quarter note) +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Ppq (pub(crate) AtomicF64); + +impl Timebase { + /// Specify sample rate, BPM and PPQ + pub fn new ( + s: impl Into, + b: impl Into, + p: impl Into + ) -> Self { + Self { sr: s.into(), bpm: b.into(), ppq: p.into() } + } + /// Iterate over ticks between start and end. + #[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> Ticker { + Ticker { spp: self.samples_per_pulse(), sample: start, start, end } + } + /// Return the duration fo a beat in microseconds + #[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() } + /// Return the number of beats in a second + #[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 } + /// Return the number of microseconds corresponding to a note of the given duration + #[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 { + 4.0 * self.usec_per_beat() * num / den + } + /// Return duration of a pulse in microseconds (BPM-dependent) + #[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() } + /// Return duration of a pulse in microseconds (BPM-dependent) + #[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() } + /// Return number of pulses to which a number of microseconds corresponds (BPM-dependent) + #[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() } + /// Convert a number of pulses to a sample number (SR- and BPM-dependent) + #[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() } + /// Return number of pulses in a second (BPM-dependent) + #[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() } + /// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent) + #[inline] pub fn pulses_per_sample (&self) -> f64 { + self.usec_per_pulse() / self.sr.usec_per_sample() + } + /// Return number of samples in a pulse (SR- and BPM-dependent) + #[inline] pub fn samples_per_pulse (&self) -> f64 { + self.sr.get() / self.pulses_per_second() + } + /// Convert a number of pulses to a sample number (SR- and BPM-dependent) + #[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 { + self.pulses_per_sample() * p + } + /// Convert a number of samples to a pulse number (SR- and BPM-dependent) + #[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 { + s / self.pulses_per_sample() + } + /// Return the number of samples corresponding to a note of the given duration + #[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 { + self.usec_to_sample(self.note_to_usec(note)) + } + /// Return the number of samples corresponding to the given number of microseconds + #[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 { + usec * self.sr.get() / 1000f64 + } + /// Return the quantized position of a moment in time given a step + #[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) { + let step = self.note_to_usec(step); + (time / step, time % step) + } + /// Quantize a collection of events + #[inline] pub fn quantize_into + Sized, T> ( + &self, step: (f64, f64), events: E + ) -> Vec<(f64, f64)> { + events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 0 + #[inline] pub fn format_beats_0 (&self, pulse: f64) -> Arc { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; + format!("{}.{}.{pulses:02}", beats / 4, beats % 4).into() + } + /// Format a number of pulses into Beat.Bar starting from 0 + #[inline] pub fn format_beats_0_short (&self, pulse: f64) -> Arc { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let beats = if ppq > 0 { pulse / ppq } else { 0 }; + format!("{}.{}", beats / 4, beats % 4).into() + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 1 + #[inline] pub fn format_beats_1 (&self, pulse: f64) -> Arc { + let mut string = String::with_capacity(16); + self.format_beats_1_to(&mut string, pulse).expect("failed to format {pulse} into beat"); + string.into() + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 1 + #[inline] pub fn format_beats_1_to (&self, w: &mut impl std::fmt::Write, pulse: f64) -> Result<(), std::fmt::Error> { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; + write!(w, "{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1) + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 1 + #[inline] pub fn format_beats_1_short (&self, pulse: f64) -> Arc { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let beats = if ppq > 0 { pulse / ppq } else { 0 }; + format!("{}.{}", beats / 4 + 1, beats % 4 + 1).into() + } +} + +impl SampleRate { + /// Return the duration of a sample in microseconds (floating) + #[inline] pub fn usec_per_sample (&self) -> f64 { + 1_000_000f64 / self.get() + } + /// Return the duration of a sample in microseconds (floating) + #[inline] pub fn sample_per_usec (&self) -> f64 { + self.get() / 1_000_000f64 + } + /// Convert a number of samples to microseconds (floating) + #[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 { + self.usec_per_sample() * samples + } + /// Convert a number of microseconds to samples (floating) + #[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 { + self.sample_per_usec() * usecs + } +} diff --git a/src/device/editor.rs b/src/device/editor.rs index d9aa1dda..adf787d2 100644 --- a/src/device/editor.rs +++ b/src/device/editor.rs @@ -317,12 +317,12 @@ impl_has!(Size: |self: MidiEditor| self.size); impl_has!(Size: |self: PianoHorizontal| self.size); impl Draw for MidiEditor { - fn draw(self, to: &mut Tui) -> Usually> { - self.tui().draw(to) + fn draw (self, to: &mut Tui) -> Usually> { + self.mode.tui().draw(to) } } impl Draw for PianoHorizontal { - fn draw(self, to: &mut Tui) -> Usually> { + fn draw (self, to: &mut Tui) -> Usually> { self.tui().draw(to) } } diff --git a/src/tek.rs b/src/tek.rs index 115d37c4..c2697b41 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -1,8 +1,7 @@ #![allow(clippy::unit_arg)] -#![feature( - adt_const_params, associated_type_defaults, anonymous_lifetime_in_impl_trait, closure_lifetime_binder, - impl_trait_in_assoc_type, trait_alias, type_alias_impl_trait, type_changing_struct_update -)] +#![feature(adt_const_params, associated_type_defaults, anonymous_lifetime_in_impl_trait, + closure_lifetime_binder, impl_trait_in_assoc_type, trait_alias, type_alias_impl_trait, + type_changing_struct_update)] /// CLI banner. pub(crate) const HEADER: &'static str = r#" @@ -23,7 +22,7 @@ pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, n pub extern crate tengri; pub(crate) use tengri::{ - *, lang::*, exit::*, eval::*, keys::*, sing::*, time::*, draw::*, term::*, color::*, + *, lang::*, exit::*, eval::*, keys::*, sing::*, time::*, draw::*, term::*, color::*, task::*, crossterm::event::{Event, KeyEvent}, ratatui::{ self, @@ -88,19 +87,26 @@ pub(crate) use tengri::{ /// Command-line entrypoint. #[cfg(feature = "cli")] pub fn main () -> Usually<()> { + Config::watch(|config|{ Exit::run(|exit|{ Jack::new_run("tek", |jack|{ - let project = Arrangement::new(&jack, &Clock::new(&jack, Some(51.))); - let state = App::new_shared(jack, config, project, ":menu"); - let keyboard = tui_input(&exit, &state, Duration::from_millis(100))?; - let terminal = tui_output(&exit, &state, Duration::from_millis(10))?; - (keyboard, terminal) + App::new_shared_run(&exit, &jack, Arrangement::new( + &jack, + None, + Clock::new(&jack, Some(51.))?, + vec![], + vec![], + vec![], + vec![], + ), config, ":menu") // TODO: Sync I/O timings with main clock, so that things // "accidentally" fall on the beat in overload conditions. }) }) - }) + })?; + + Ok(()) } pub fn swap_value ( From c737c7d8393cd1a0c1587787f84c0c8d0dc383dd Mon Sep 17 00:00:00 2001 From: i do not exist Date: Wed, 24 Jun 2026 10:18:45 +0300 Subject: [PATCH 13/31] restruct: 30e --- src/app.rs | 11 +- src/app/cli.rs | 9 +- src/app/config.rs | 2 +- src/device.rs | 1 + src/device/clock.rs | 28 +- src/device/editor.rs | 755 ++++++------------------------------ src/device/editor/octave.rs | 36 ++ src/device/editor/piano.rs | 302 +++++++++++++++ src/device/editor/point.rs | 76 ++++ src/device/editor/range.rs | 124 ++++++ src/device/meter.rs | 84 ++++ src/device/mix.rs | 82 ---- src/tek.rs | 8 +- 13 files changed, 771 insertions(+), 747 deletions(-) create mode 100644 src/device/editor/octave.rs create mode 100644 src/device/editor/piano.rs create mode 100644 src/device/editor/point.rs create mode 100644 src/device/editor/range.rs create mode 100644 src/device/meter.rs diff --git a/src/app.rs b/src/app.rs index 85922694..b7820826 100644 --- a/src/app.rs +++ b/src/app.rs @@ -87,11 +87,11 @@ impl App { pub fn new_shared_run ( exit: &Exit, jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef - ) -> Usually<(Task, Task)> { + ) -> Usually<(Self, Task, Task)> { let state = Self::new_shared(&jack, project, config, mode); let keyboard = tui_input(&exit, &state, Duration::from_millis(100))?; let terminal = tui_output(&exit, &state, Duration::from_millis(10))?; - Ok((keyboard, terminal)) + Ok((state, keyboard, terminal)) } pub fn confirm (&mut self) -> Perhaps { @@ -361,10 +361,10 @@ impl HasJack<'static> for App { def_command!(AppCommand: |app: App| { Nop => Ok(None), - Confirm => tek_confirm(app), Cancel => todo!(), // TODO delegate: - Inc { axis: ControlAxis } => tek_inc(app, axis), - Dec { axis: ControlAxis } => tek_dec(app, axis), + Confirm => app.confirm(), + Inc { axis: ControlAxis } => app.inc(axis), + Dec { axis: ControlAxis } => app.dec(axis), SetDialog { dialog: Dialog } => { swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) }, @@ -536,6 +536,7 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua h_exact(2, origin_n(w_full(item))) ))); } + Ok(to.area().into()) }))) } else { None diff --git a/src/app/cli.rs b/src/app/cli.rs index 1698d68f..78dea7a7 100644 --- a/src/app/cli.rs +++ b/src/app/cli.rs @@ -135,7 +135,7 @@ impl Action { } pub fn tek_project_new ( - jack: &Jack, + jack: &Jack<'static>, clock: Clock, left_from: &[impl AsRef], left_to: &[impl AsRef], @@ -154,9 +154,7 @@ pub fn tek_project_new ( let right_tos = Connect::collect(right_to, empty, empty); let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; - - // Create initial project: - let mut project = Arrangement::new( + Ok(Arrangement::new( jack, None, clock, @@ -168,8 +166,7 @@ pub fn tek_project_new ( Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() .map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()])) .collect::, _>>()? - ); - Ok(project) + )) } pub fn tek_show_version () { diff --git a/src/app/config.rs b/src/app/config.rs index 172a2b55..3ec4f94a 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -37,7 +37,7 @@ impl Config { const CONFIG: &'static str = "tek.edn"; const DEFAULTS: &'static str = include_str!("../tek.edn"); - pub fn watch (callback: impl Fn(Self)->T) -> Usually { + pub fn watch (callback: impl FnOnce(Self)->T) -> Usually { let config = Self::init_new(None)?; let result = callback(config); Ok(result) diff --git a/src/device.rs b/src/device.rs index 83cc1ad3..d271ec7c 100644 --- a/src/device.rs +++ b/src/device.rs @@ -106,6 +106,7 @@ pub mod clock; pub use self::clock::*; pub mod dialog; pub use self::dialog::*; pub mod editor; pub use self::editor::*; pub mod menu; pub use self::menu::*; +pub mod meter; pub use self::meter::*; pub mod mix; pub use self::mix::*; pub mod port; pub use self::port::*; pub mod sample; pub use self::sample::*; diff --git a/src/device/clock.rs b/src/device/clock.rs index 9c78dc96..6aedab97 100644 --- a/src/device/clock.rs +++ b/src/device/clock.rs @@ -387,16 +387,26 @@ impl ClockView { let lat = chunk / rate * 1000.; let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); let mut cache = cache.write().unwrap(); - cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}")); - cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms")); - cache.sr.update(Some((compact, rate)), |buf,_,_|{ - buf.clear(); - if compact { - write!(buf, "{:.1}kHz", rate / 1000.) - } else { - write!(buf, "{:.0}Hz", rate) + + cache.buf.update( + Some(chunk), rewrite!(buf, "{chunk}") + ); + + cache.lat.update( + Some(lat), rewrite!(buf, "{lat:.1}ms") + ); + + cache.sr.update( + Some((compact, rate)), |buf: &mut String,_,_|{ + buf.clear(); + if compact { + write!(buf, "{:.1}kHz", rate / 1000.) + } else { + write!(buf, "{:.0}Hz", rate) + } } - }); + ); + if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { let pulse = clock.timebase.usecs_to_pulse(now); let time = now/1000000.; diff --git a/src/device/editor.rs b/src/device/editor.rs index adf787d2..2ab03f36 100644 --- a/src/device/editor.rs +++ b/src/device/editor.rs @@ -22,82 +22,116 @@ pub struct MidiEditor { pub mode: PianoHorizontal, } -/// A clip, rendered as a horizontal piano roll. -/// -/// ``` -/// let piano = tek::PianoHorizontal::default(); -/// ``` -#[derive(Clone, Default)] -pub struct PianoHorizontal { - pub clip: Option>>, - /// Buffer where the whole clip is rerendered on change - pub buffer: Arc>, - /// Size of actual notes area - pub size: Size, - /// The display window - pub range: MidiSelection, - /// The note cursor - pub point: MidiCursor, - /// The highlight color palette - pub color: ItemTheme, - /// Width of the keyboard - pub keys_width: u16, -} +impl_default!(MidiEditor: Self { + size: [0, 0].into(), mode: PianoHorizontal::new(None) +}); -/// 12 piano keys, some highlighted. -/// -/// ``` -/// let keys = tek::OctaveVertical::default(); -/// ``` -#[derive(Copy, Clone)] -pub struct OctaveVertical { - pub on: [bool; 12], - pub colors: [Color; 3] -} - -/// -/// ``` -/// let _ = tek::MidiCursor::default(); -/// ``` -#[derive(Debug, Clone)] pub struct MidiCursor { - /// Time coordinate of cursor - pub time_pos: Arc, - /// Note coordinate of cursor - pub note_pos: Arc, - /// Length of note that will be inserted, in pulses - pub note_len: Arc, -} - -/// -/// ``` -/// use tek::{TimeRange, NoteRange}; -/// let model = tek::MidiSelection::from((1, false)); -/// -/// let _ = model.get_time_len(); -/// let _ = model.get_time_zoom(); -/// let _ = model.get_time_lock(); -/// let _ = model.get_time_start(); -/// let _ = model.get_time_axis(); -/// let _ = model.get_time_end(); -/// -/// let _ = model.get_note_lo(); -/// let _ = model.get_note_axis(); -/// let _ = model.get_note_hi(); -/// ``` -#[derive(Debug, Clone, Default)] pub struct MidiSelection { - pub time_len: Arc, - /// Length of visible time axis - pub time_axis: Arc, - /// Earliest time displayed - pub time_start: Arc, - /// Time step - pub time_zoom: Arc, - /// Auto rezoom to fit in time axis - pub time_lock: Arc, - /// Length of visible note axis - pub note_axis: Arc, - // Lowest note displayed - pub note_lo: Arc, +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.get_time_pos(); + let note_pos = self.get_note_pos(); + let note_len = self.get_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 + 1) % clip.length); + } + redraw = true; + } + if redraw { + self.mode.redraw(); + } + } + fn _todo_opt_clip_stub (&self) -> Option>> { todo!() } + fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } + fn note_length (&self) -> usize { self.get_note_len() } + fn note_pos (&self) -> usize { self.get_note_pos() } + fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 } + fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 } + fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) } + fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) } + fn note_len (&self) -> usize { self.get_note_len() } + fn note_len_next (&self) -> usize { self.get_note_len() + 1 } + fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) } + fn note_range (&self) -> usize { self.get_note_axis() } + fn note_range_next (&self) -> usize { self.get_note_axis() + 1 } + fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) } + fn time_zoom (&self) -> usize { self.get_time_zoom() } + fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 } + fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 } + fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } + fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } + fn time_lock (&self) -> bool { self.get_time_lock() } + fn time_lock_toggled (&self) -> bool { !self.get_time_lock() } + fn time_pos (&self) -> usize { self.get_time_pos() } + fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() } + fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() } + fn time_pos_prev (&self) -> usize { + let step = self.get_note_len(); + self.get_time_pos().overflowing_sub(step) + .0.min(self.clip_length().saturating_sub(step)) + } + fn time_pos_prev_fine (&self) -> usize { + self.get_time_pos().overflowing_sub(1) + .0.min(self.clip_length().saturating_sub(1)) + } + pub fn clip_status (&self) -> impl Draw + '_ { + 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) }; + w_exact(20, south!( + w_full(origin_w(east( + button_2("f2", "name ", false), + w_full(origin_e(fg(Rgb(255, 255, 255), format!("{name} "))))))), + w_full(origin_w(east( + button_2("l", "ength ", false), + w_full(origin_e(fg(Rgb(255, 255, 255), format!("{length} "))))))), + w_full(origin_w(east( + button_2("r", "epeat ", false), + w_full(origin_e(fg(Rgb(255, 255, 255), format!("{looped} "))))))), + )) + } + pub fn edit_status (&self) -> impl Draw + '_ { + 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.get_time_pos(); + let time_zoom = self.get_time_zoom(); + let time_lock = if self.get_time_lock() { "[lock]" } else { " " }; + let note_pos = self.get_note_pos(); + let note_name = format!("{:4}", note_pitch_to_name(note_pos)); + let note_pos = format!("{:>3}", note_pos); + let note_len = format!("{:>4}", self.get_note_len()); + w_exact(20, south!( + w_full(origin_w(east( + button_2("t", "ime ", false), + w_full(origin_e(fg(Rgb(255, 255, 255), + format!("{length} /{time_zoom} +{time_pos} "))))))), + w_full(origin_w(east( + button_2("z", "lock ", false), + w_full(origin_e(fg(Rgb(255, 255, 255), + format!("{time_lock}"))))))), + w_full(origin_w(east( + button_2("x", "note ", false), + w_full(origin_e(fg(Rgb(255, 255, 255), + format!("{note_name} {note_pos} {note_len}"))))))), + )) + } } /// ``` @@ -217,257 +251,32 @@ pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync { } } -pub trait NotePoint { - fn note_len (&self) -> &AtomicUsize; - /// Get the current length of the note cursor. - fn get_note_len (&self) -> usize { - self.note_len().load(Relaxed) - } - /// Set the length of the note cursor, returning the previous value. - fn set_note_len (&self, x: usize) -> usize { - self.note_len().swap(x, Relaxed) - } - - fn note_pos (&self) -> &AtomicUsize; - /// Get the current pitch of the note cursor. - fn get_note_pos (&self) -> usize { - self.note_pos().load(Relaxed).min(127) - } - /// Set the current pitch fo the note cursor, returning the previous value. - fn set_note_pos (&self, x: usize) -> usize { - self.note_pos().swap(x.min(127), Relaxed) - } -} - -pub trait TimePoint { - fn time_pos (&self) -> &AtomicUsize; - /// Get the current time position of the note cursor. - fn get_time_pos (&self) -> usize { - self.time_pos().load(Relaxed) - } - /// Set the current time position of the note cursor, returning the previous value. - fn set_time_pos (&self, x: usize) -> usize { - self.time_pos().swap(x, Relaxed) - } -} - -pub trait MidiPoint: NotePoint + TimePoint { - /// Get the current end of the note cursor. - fn get_note_end (&self) -> usize { - self.get_time_pos() + self.get_note_len() - } -} - -pub trait TimeRange { - fn time_len (&self) -> &AtomicUsize; - fn get_time_len (&self) -> usize { - self.time_len().load(Relaxed) - } - fn time_zoom (&self) -> &AtomicUsize; - fn get_time_zoom (&self) -> usize { - self.time_zoom().load(Relaxed) - } - fn set_time_zoom (&self, value: usize) -> usize { - self.time_zoom().swap(value, Relaxed) - } - fn time_lock (&self) -> &AtomicBool; - fn get_time_lock (&self) -> bool { - self.time_lock().load(Relaxed) - } - fn set_time_lock (&self, value: bool) -> bool { - self.time_lock().swap(value, Relaxed) - } - fn time_start (&self) -> &AtomicUsize; - fn get_time_start (&self) -> usize { - self.time_start().load(Relaxed) - } - fn set_time_start (&self, value: usize) -> usize { - self.time_start().swap(value, Relaxed) - } - fn time_axis (&self) -> &AtomicUsize; - fn get_time_axis (&self) -> usize { - self.time_axis().load(Relaxed) - } - fn get_time_end (&self) -> usize { - self.time_start().load(Relaxed) + self.time_axis().load(Relaxed) * self.time_zoom().load(Relaxed) - } -} - -pub trait NoteRange { - fn note_lo (&self) -> &AtomicUsize; - fn note_axis (&self) -> &AtomicUsize; - - fn get_note_lo (&self) -> usize { - self.note_lo().load(Relaxed) - } - fn set_note_lo (&self, x: usize) -> usize { - self.note_lo().swap(x, Relaxed) - } - fn get_note_axis (&self) -> usize { - self.note_axis().load(Relaxed) - } - fn get_note_hi (&self) -> usize { - (self.get_note_lo() + self.get_note_axis().saturating_sub(1)).min(127) - } -} - -pub trait MidiRange: TimeRange + NoteRange {} - impl_has!(Size: |self: MidiEditor| self.size); -impl_has!(Size: |self: PianoHorizontal| self.size); impl Draw for MidiEditor { fn draw (self, to: &mut Tui) -> Usually> { self.mode.tui().draw(to) } } -impl Draw for PianoHorizontal { - fn draw (self, to: &mut Tui) -> Usually> { - self.tui().draw(to) - } -} + impl std::fmt::Debug for MidiEditor { fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("MidiEditor").field("mode", &self.mode).finish() } } + impl_from!(MidiEditor: |clip: &Arc>| { let model = Self::from(Some(clip.clone())); model.redraw(); model }); + impl_from!(MidiEditor: |clip: Option>>| { let mut model = Self::default(); *model.clip_mut() = clip; model.redraw(); model }); -impl_default!(MidiEditor: Self { - size: [0, 0].into(), mode: PianoHorizontal::new(None) -}); -impl_default!(OctaveVertical: Self { - on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] -}); -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.get_time_pos(); - let note_pos = self.get_note_pos(); - let note_len = self.get_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 + 1) % clip.length); - } - redraw = true; - } - if redraw { - self.mode.redraw(); - } - } - fn _todo_opt_clip_stub (&self) -> Option>> { todo!() } - fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } - fn note_length (&self) -> usize { self.get_note_len() } - fn note_pos (&self) -> usize { self.get_note_pos() } - fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 } - fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 } - fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) } - fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) } - fn note_len (&self) -> usize { self.get_note_len() } - fn note_len_next (&self) -> usize { self.get_note_len() + 1 } - fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) } - fn note_range (&self) -> usize { self.get_note_axis() } - fn note_range_next (&self) -> usize { self.get_note_axis() + 1 } - fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) } - fn time_zoom (&self) -> usize { self.get_time_zoom() } - fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 } - fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 } - fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } - fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } - fn time_lock (&self) -> bool { self.get_time_lock() } - fn time_lock_toggled (&self) -> bool { !self.get_time_lock() } - fn time_pos (&self) -> usize { self.get_time_pos() } - fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() } - fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() } - fn time_pos_prev (&self) -> usize { - let step = self.get_note_len(); - self.get_time_pos().overflowing_sub(step) - .0.min(self.clip_length().saturating_sub(step)) - } - fn time_pos_prev_fine (&self) -> usize { - self.get_time_pos().overflowing_sub(1) - .0.min(self.clip_length().saturating_sub(1)) - } - pub fn clip_status (&self) -> impl Draw + '_ { - 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) }; - w_exact(20, south!( - w_full(origin_w(east( - button_2("f2", "name ", false), - w_full(origin_e(fg(Rgb(255, 255, 255), format!("{name} "))))))), - w_full(origin_w(east( - button_2("l", "ength ", false), - w_full(origin_e(fg(Rgb(255, 255, 255), format!("{length} "))))))), - w_full(origin_w(east( - button_2("r", "epeat ", false), - w_full(origin_e(fg(Rgb(255, 255, 255), format!("{looped} "))))))), - )) - } - pub fn edit_status (&self) -> impl Draw + '_ { - 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.get_time_pos(); - let time_zoom = self.get_time_zoom(); - let time_lock = if self.get_time_lock() { "[lock]" } else { " " }; - let note_pos = self.get_note_pos(); - let note_name = format!("{:4}", note_pitch_to_name(note_pos)); - let note_pos = format!("{:>3}", note_pos); - let note_len = format!("{:>4}", self.get_note_len()); - w_exact(20, south!( - w_full(origin_w(east( - button_2("t", "ime ", false), - w_full(origin_e(fg(Rgb(255, 255, 255), - format!("{length} /{time_zoom} +{time_pos} "))))))), - w_full(origin_w(east( - button_2("z", "lock ", false), - w_full(origin_e(fg(Rgb(255, 255, 255), - format!("{time_lock}"))))))), - w_full(origin_w(east( - button_2("x", "note ", false), - w_full(origin_e(fg(Rgb(255, 255, 255), - 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) -> &AtomicUsize { self.mode.note_len() } @@ -494,339 +303,7 @@ impl View for MidiEditor { } } - -impl PianoHorizontal { - pub fn new (clip: Option<&Arc>>) -> Self { - let size = [0, 0].into(); - let mut range = MidiSelection::from((12, true)); - range.time_axis = size.x.clone(); - range.note_axis = size.y.clone(); - let piano = Self { - keys_width: 5, - size, - range, - buffer: RwLock::new(Default::default()).into(), - point: MidiCursor::default(), - clip: clip.cloned(), - color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), - }; - piano.redraw(); - piano - } -} - -impl PianoHorizontal { - fn tui (&self) -> impl Draw { - south( - east(w_exact(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), - east(self.keys(), self.size.of(below(wh_full(self.notes()), wh_full(self.cursor())))), - ) - } -} - -impl PianoHorizontal { - - /// Draw the piano roll background. - /// - /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize, note_point: usize, time_point: usize) { - for (y, note) in (0..=127).rev().enumerate() { - for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { - let cell = buf.get_mut(x, y).unwrap(); - if note == (127-note_point) || time == time_point { - cell.set_bg(Rgb(0,0,0)); - } else { - cell.set_bg(clip.color.darkest.term); - } - if time % 384 == 0 { - cell.set_fg(clip.color.darker.term); - cell.set_char('│'); - } else if time % 96 == 0 { - cell.set_fg(clip.color.dark.term); - cell.set_char('╎'); - } else if time % note_len == 0 { - cell.set_fg(clip.color.darker.term); - cell.set_char('┊'); - } else if (127 - note) % 12 == 0 { - cell.set_fg(clip.color.darker.term); - cell.set_char('='); - } else if (127 - note) % 6 == 0 { - cell.set_fg(clip.color.darker.term); - cell.set_char('—'); - } else { - cell.set_fg(clip.color.darker.term); - cell.set_char('·'); - } - } - } - } - - /// Draw the piano roll foreground. - /// - /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { - let style = Style::default().fg(clip.color.base.term);//.bg(Rgb(0, 0, 0)); - let mut notes_on = [false;128]; - for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { - for (_y, note) in (0..=127).rev().enumerate() { - if let Some(cell) = buf.get_mut(x, note) { - if notes_on[note] { - cell.set_char('▂'); - cell.set_style(style); - } - } - } - let time_end = time_start + zoom; - for time in time_start..time_end.min(clip.length) { - for event in clip.notes[time].iter() { - match event { - MidiMessage::NoteOn { key, .. } => { - let note = key.as_int() as usize; - if let Some(cell) = buf.get_mut(x, note) { - cell.set_char('█'); - cell.set_style(style); - } - notes_on[note] = true - }, - MidiMessage::NoteOff { key, .. } => { - notes_on[key.as_int() as usize] = false - }, - _ => {} - } - } - } - - } - } - - fn notes (&self) -> impl Draw { - let time_start = self.get_time_start(); - let note_lo = self.get_note_lo(); - let note_hi = self.get_note_hi(); - let buffer = self.buffer.clone(); - thunk(move|to: &mut Tui|{ - let xywh = to.area().into(); - let XYWH(x0, y0, w, _h) = xywh; - let source = buffer.read().unwrap(); - //if h as usize != note_axis { - //panic!("area height mismatch: {h} <> {note_axis}"); - //} - for (area_x, screen_x) in (x0..x0+w).enumerate() { - for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { - let source_x = time_start + area_x; - let source_y = note_hi - area_y; - // TODO: enable loop rollover: - //let source_x = (time_start + area_x) % source.width.max(1); - //let source_y = (note_hi - area_y) % source.height.max(1); - let is_in_x = source_x < source.width; - let is_in_y = source_y < source.height; - if is_in_x && is_in_y { - if let Some(source_cell) = source.get(source_x, source_y) { - if let Some(cell) = to.0.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { - *cell = source_cell.clone(); - } - } - } - } - } - Ok(xywh) - }) - } - - fn cursor (&self) -> impl Draw { - let note_hi = self.get_note_hi(); - let note_lo = self.get_note_lo(); - let note_pos = self.get_note_pos(); - let note_len = self.get_note_len(); - let time_pos = self.get_time_pos(); - let time_start = self.get_time_start(); - let time_zoom = self.get_time_zoom(); - let style = Some(Style::default().fg(self.color.lightest.term)); - thunk(move|to: &mut Tui|{ - let xywh = to.area().into(); - let XYWH(x0, y0, w, _h) = xywh; - for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - if note == note_pos { - for x in 0..w { - let screen_x = x0 + x; - let time_1 = time_start + x as usize * time_zoom; - let time_2 = time_1 + time_zoom; - if time_1 <= time_pos && time_pos < time_2 { - to.blit(&"█", screen_x, screen_y, style); - let tail = note_len as u16 / time_zoom as u16; - for x_tail in (screen_x + 1)..(screen_x + tail) { - to.blit(&"▂", x_tail, screen_y, style); - } - break - } - } - break - } - } - Ok(xywh) - }) - } - - fn keys (&self) -> impl Draw { - let state = self; - let color = state.color; - let note_lo = state.get_note_lo(); - let note_hi = state.get_note_hi(); - let note_pos = state.get_note_pos(); - let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); - let off_style = Some(Style::default().fg(g(255))); - let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.term).bold()); - h_full(w_exact(self.keys_width, thunk(move|to: &mut Tui|{ - let xywh = to.area().into(); - let XYWH(x, y0, _w, _h) = xywh; - for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - to.blit(&to_key(note), x, screen_y, key_style); - if note > 127 { - continue - } - if note == note_pos { - to.blit(&format!("{:<5}", note_pitch_to_name(note)), x, screen_y, on_style) - } else { - to.blit(¬e_pitch_to_name(note), x, screen_y, off_style) - }; - } - Ok(xywh) - }))) - } - - fn timeline (&self) -> impl Draw + '_ { - w_full(h_exact(1, thunk(move|to: &mut Tui|{ - let xywh = to.area().into(); - let XYWH(x, y, w, _h) = xywh; - let style = Some(Style::default().dim()); - let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { - let t = area_x as usize * self.time_zoom().load(Relaxed); - if t < length { - to.blit(&"|", screen_x, y, style); - } - } - Ok(xywh) - }))) - } -} - -impl TimeRange for PianoHorizontal { - fn time_len (&self) -> &AtomicUsize { self.range.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.range.time_start() } - fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } -} - -impl NoteRange for PianoHorizontal { - fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } -} - -impl NotePoint for PianoHorizontal { - fn note_len (&self) -> &AtomicUsize { self.point.note_len() } - fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } -} - -impl TimePoint for PianoHorizontal { - fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } -} - -impl MidiViewer for PianoHorizontal { - fn clip (&self) -> &Option>> { &self.clip } - fn clip_mut (&mut self) -> &mut Option>> { &mut self.clip } - /// Determine the required space to render the clip. - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { - (clip.length / self.range.time_zoom().load(Relaxed), 128) - } - fn redraw (&self) { - *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { - let clip = clip.read().unwrap(); - let buf_size = self.buffer_size(&clip); - let mut buffer = BigBuffer::from(buf_size); - let time_zoom = self.get_time_zoom(); - self.time_len().store(clip.length, Relaxed); - PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); - PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); - buffer - } else { - Default::default() - } - } - fn set_clip (&mut self, clip: Option<&Arc>>) { - *self.clip_mut() = clip.cloned(); - self.color = clip.map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]); - self.redraw(); - } -} - -impl std::fmt::Debug for PianoHorizontal { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - let buffer = self.buffer.read().unwrap(); - f.debug_struct("PianoHorizontal") - .field("time_zoom", &self.range.time_zoom) - .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) - .finish() - } -} -impl OctaveVertical { - fn color (&self, pitch: usize) -> Color { - let pitch = pitch % 12; - self.colors[if self.on[pitch] { 2 } else { match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } }] - } -} -impl OctaveVertical { - fn tui (&self) -> impl Draw { - east!( - fg_bg(self.color(0), self.color(1), "▙"), - fg_bg(self.color(2), self.color(3), "▙"), - fg_bg(self.color(4), self.color(5), "▌"), - fg_bg(self.color(6), self.color(7), "▟"), - fg_bg(self.color(8), self.color(9), "▟"), - fg_bg(self.color(10), self.color(11), "▟"), - ) - } -} -impl_from!(MidiSelection: |data:(usize, bool)| Self { - time_len: Arc::new(0.into()), - note_axis: Arc::new(0.into()), - note_lo: Arc::new(0.into()), - time_axis: Arc::new(0.into()), - time_start: Arc::new(0.into()), - time_zoom: Arc::new(data.0.into()), - time_lock: Arc::new(data.1.into()), -}); -impl_default!(MidiCursor: Self { - time_pos: Arc::new(0.into()), - note_pos: Arc::new(36.into()), - note_len: Arc::new(24.into()), -}); - -impl NotePoint for MidiCursor { - fn note_len (&self) -> &AtomicUsize { - &self.note_len - } - fn note_pos (&self) -> &AtomicUsize { - &self.note_pos - } -} - -impl TimePoint for MidiCursor { - fn time_pos (&self) -> &AtomicUsize { - self.time_pos.as_ref() - } -} - -impl TimeRange for MidiSelection { - fn time_len (&self) -> &AtomicUsize { &self.time_len } - fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom } - fn time_lock (&self) -> &AtomicBool { &self.time_lock } - fn time_start (&self) -> &AtomicUsize { &self.time_start } - fn time_axis (&self) -> &AtomicUsize { &self.time_axis } -} - -impl NoteRange for MidiSelection { - fn note_lo (&self) -> &AtomicUsize { &self.note_lo } - fn note_axis (&self) -> &AtomicUsize { &self.note_axis } -} +mod octave; pub use self::octave::*; +mod piano; pub use self::piano::*; +mod point; pub use self::point::*; +mod range; pub use self::range::*; diff --git a/src/device/editor/octave.rs b/src/device/editor/octave.rs new file mode 100644 index 00000000..21124e98 --- /dev/null +++ b/src/device/editor/octave.rs @@ -0,0 +1,36 @@ +use crate::*; + +/// 12 piano keys, some highlighted. +/// +/// ``` +/// let keys = tek::OctaveVertical::default(); +/// ``` +#[derive(Copy, Clone)] +pub struct OctaveVertical { + pub on: [bool; 12], + pub colors: [Color; 3] +} + +impl OctaveVertical { + fn color (&self, pitch: usize) -> Color { + let pitch = pitch % 12; + self.colors[if self.on[pitch] { 2 } else { match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } }] + } +} + +impl OctaveVertical { + pub fn tui (&self) -> impl Draw { + east!( + fg_bg(self.color(0), self.color(1), "▙"), + fg_bg(self.color(2), self.color(3), "▙"), + fg_bg(self.color(4), self.color(5), "▌"), + fg_bg(self.color(6), self.color(7), "▟"), + fg_bg(self.color(8), self.color(9), "▟"), + fg_bg(self.color(10), self.color(11), "▟"), + ) + } +} + +impl_default!(OctaveVertical: Self { + on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] +}); diff --git a/src/device/editor/piano.rs b/src/device/editor/piano.rs new file mode 100644 index 00000000..d1c6fbd4 --- /dev/null +++ b/src/device/editor/piano.rs @@ -0,0 +1,302 @@ +use crate::*; + +/// A clip, rendered as a horizontal piano roll. +/// +/// ``` +/// let piano = tek::PianoHorizontal::default(); +/// ``` +#[derive(Clone, Default)] +pub struct PianoHorizontal { + pub clip: Option>>, + /// Buffer where the whole clip is rerendered on change + pub buffer: Arc>, + /// Size of actual notes area + pub size: Size, + /// The display window + pub range: MidiSelection, + /// The note cursor + pub point: MidiCursor, + /// The highlight color palette + pub color: ItemTheme, + /// Width of the keyboard + pub keys_width: u16, +} + +impl_has!(Size: |self: PianoHorizontal| self.size); +impl Draw for PianoHorizontal { + fn draw (self, to: &mut Tui) -> Usually> { + self.tui().draw(to) + } +} + +impl PianoHorizontal { + + pub fn new (clip: Option<&Arc>>) -> Self { + let size: Size = [0, 0].into(); + let mut range = MidiSelection::from((12, true)); + range.time_axis = size.x.clone(); + range.note_axis = size.y.clone(); + let piano = Self { + keys_width: 5, + size, + range, + buffer: RwLock::new(Default::default()).into(), + point: MidiCursor::default(), + clip: clip.cloned(), + color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), + }; + piano.redraw(); + piano + } + + pub fn tui (&self) -> impl Draw { + south( + east(w_exact(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), + east(self.keys(), self.size.of(below(wh_full(self.notes()), wh_full(self.cursor())))), + ) + } + + /// Draw the piano roll background. + /// + /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize, note_point: usize, time_point: usize) { + for (y, note) in (0..=127).rev().enumerate() { + for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { + let cell = buf.get_mut(x, y).unwrap(); + if note == (127-note_point) || time == time_point { + cell.set_bg(Rgb(0,0,0)); + } else { + cell.set_bg(clip.color.darkest.term); + } + if time % 384 == 0 { + cell.set_fg(clip.color.darker.term); + cell.set_char('│'); + } else if time % 96 == 0 { + cell.set_fg(clip.color.dark.term); + cell.set_char('╎'); + } else if time % note_len == 0 { + cell.set_fg(clip.color.darker.term); + cell.set_char('┊'); + } else if (127 - note) % 12 == 0 { + cell.set_fg(clip.color.darker.term); + cell.set_char('='); + } else if (127 - note) % 6 == 0 { + cell.set_fg(clip.color.darker.term); + cell.set_char('—'); + } else { + cell.set_fg(clip.color.darker.term); + cell.set_char('·'); + } + } + } + } + + /// Draw the piano roll foreground. + /// + /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { + let style = Style::default().fg(clip.color.base.term);//.bg(Rgb(0, 0, 0)); + let mut notes_on = [false;128]; + for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { + for (_y, note) in (0..=127).rev().enumerate() { + if let Some(cell) = buf.get_mut(x, note) { + if notes_on[note] { + cell.set_char('▂'); + cell.set_style(style); + } + } + } + let time_end = time_start + zoom; + for time in time_start..time_end.min(clip.length) { + for event in clip.notes[time].iter() { + match event { + MidiMessage::NoteOn { key, .. } => { + let note = key.as_int() as usize; + if let Some(cell) = buf.get_mut(x, note) { + cell.set_char('█'); + cell.set_style(style); + } + notes_on[note] = true + }, + MidiMessage::NoteOff { key, .. } => { + notes_on[key.as_int() as usize] = false + }, + _ => {} + } + } + } + + } + } + + fn notes (&self) -> impl Draw { + let time_start = self.get_time_start(); + let note_lo = self.get_note_lo(); + let note_hi = self.get_note_hi(); + let buffer = self.buffer.clone(); + thunk(move|to: &mut Tui|{ + let xywh = to.area().into(); + let XYWH(x0, y0, w, _h) = xywh; + let source = buffer.read().unwrap(); + //if h as usize != note_axis { + //panic!("area height mismatch: {h} <> {note_axis}"); + //} + for (area_x, screen_x) in (x0..x0+w).enumerate() { + for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { + let source_x = time_start + area_x; + let source_y = note_hi - area_y; + // TODO: enable loop rollover: + //let source_x = (time_start + area_x) % source.width.max(1); + //let source_y = (note_hi - area_y) % source.height.max(1); + let is_in_x = source_x < source.width; + let is_in_y = source_y < source.height; + if is_in_x && is_in_y { + if let Some(source_cell) = source.get(source_x, source_y) { + if let Some(cell) = to.0.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { + *cell = source_cell.clone(); + } + } + } + } + } + Ok(xywh) + }) + } + + fn cursor (&self) -> impl Draw { + let note_hi = self.get_note_hi(); + let note_lo = self.get_note_lo(); + let note_pos = self.get_note_pos(); + let note_len = self.get_note_len(); + let time_pos = self.get_time_pos(); + let time_start = self.get_time_start(); + let time_zoom = self.get_time_zoom(); + let style = Some(Style::default().fg(self.color.lightest.term)); + thunk(move|to: &mut Tui|{ + let xywh = to.area().into(); + let XYWH(x0, y0, w, _h) = xywh; + for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { + if note == note_pos { + for x in 0..w { + let screen_x = x0 + x; + let time_1 = time_start + x as usize * time_zoom; + let time_2 = time_1 + time_zoom; + if time_1 <= time_pos && time_pos < time_2 { + to.blit(&"█", screen_x, screen_y, style); + let tail = note_len as u16 / time_zoom as u16; + for x_tail in (screen_x + 1)..(screen_x + tail) { + to.blit(&"▂", x_tail, screen_y, style); + } + break + } + } + break + } + } + Ok(xywh) + }) + } + + fn keys (&self) -> impl Draw { + let state = self; + let color = state.color; + let note_lo = state.get_note_lo(); + let note_hi = state.get_note_hi(); + let note_pos = state.get_note_pos(); + let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); + let off_style = Some(Style::default().fg(g(255))); + let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.term).bold()); + h_full(w_exact(self.keys_width, thunk(move|to: &mut Tui|{ + let xywh = to.area().into(); + let XYWH(x, y0, _w, _h) = xywh; + for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { + to.blit(&to_key(note), x, screen_y, key_style); + if note > 127 { + continue + } + if note == note_pos { + to.blit(&format!("{:<5}", note_pitch_to_name(note)), x, screen_y, on_style) + } else { + to.blit(¬e_pitch_to_name(note), x, screen_y, off_style) + }; + } + Ok(xywh) + }))) + } + + fn timeline (&self) -> impl Draw + '_ { + w_full(h_exact(1, thunk(move|to: &mut Tui|{ + let xywh = to.area().into(); + let XYWH(x, y, w, _h) = xywh; + let style = Some(Style::default().dim()); + let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); + for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { + let t = area_x as usize * self.time_zoom().load(Relaxed); + if t < length { + to.blit(&"|", screen_x, y, style); + } + } + Ok(xywh) + }))) + } +} + +impl TimeRange for PianoHorizontal { + fn time_len (&self) -> &AtomicUsize { self.range.time_len() } + fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } + fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } + fn time_start (&self) -> &AtomicUsize { self.range.time_start() } + fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } +} + +impl NoteRange for PianoHorizontal { + fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } + fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } +} + +impl NotePoint for PianoHorizontal { + fn note_len (&self) -> &AtomicUsize { self.point.note_len() } + fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } +} + +impl TimePoint for PianoHorizontal { + fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } +} + +impl MidiViewer for PianoHorizontal { + fn clip (&self) -> &Option>> { &self.clip } + fn clip_mut (&mut self) -> &mut Option>> { &mut self.clip } + /// Determine the required space to render the clip. + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { + (clip.length / self.range.time_zoom().load(Relaxed), 128) + } + fn redraw (&self) { + *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { + let clip = clip.read().unwrap(); + let buf_size = self.buffer_size(&clip); + let mut buffer = BigBuffer::from(buf_size); + let time_zoom = self.get_time_zoom(); + self.time_len().store(clip.length, Relaxed); + PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); + PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); + buffer + } else { + Default::default() + } + } + fn set_clip (&mut self, clip: Option<&Arc>>) { + *self.clip_mut() = clip.cloned(); + self.color = clip.map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]); + self.redraw(); + } +} + +impl std::fmt::Debug for PianoHorizontal { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + let buffer = self.buffer.read().unwrap(); + f.debug_struct("PianoHorizontal") + .field("time_zoom", &self.range.time_zoom) + .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) + .finish() + } +} diff --git a/src/device/editor/point.rs b/src/device/editor/point.rs new file mode 100644 index 00000000..ee1b0b0e --- /dev/null +++ b/src/device/editor/point.rs @@ -0,0 +1,76 @@ +use crate::*; + +/// +/// ``` +/// let _ = tek::MidiCursor::default(); +/// ``` +#[derive(Debug, Clone)] pub struct MidiCursor { + /// Time coordinate of cursor + pub time_pos: Arc, + /// Note coordinate of cursor + pub note_pos: Arc, + /// Length of note that will be inserted, in pulses + pub note_len: Arc, +} + +impl_default!(MidiCursor: Self { + time_pos: Arc::new(0.into()), + note_pos: Arc::new(36.into()), + note_len: Arc::new(24.into()), +}); + +impl NotePoint for MidiCursor { + fn note_len (&self) -> &AtomicUsize { + &self.note_len + } + fn note_pos (&self) -> &AtomicUsize { + &self.note_pos + } +} + +impl TimePoint for MidiCursor { + fn time_pos (&self) -> &AtomicUsize { + self.time_pos.as_ref() + } +} + +pub trait NotePoint { + fn note_len (&self) -> &AtomicUsize; + /// Get the current length of the note cursor. + fn get_note_len (&self) -> usize { + self.note_len().load(Relaxed) + } + /// Set the length of the note cursor, returning the previous value. + fn set_note_len (&self, x: usize) -> usize { + self.note_len().swap(x, Relaxed) + } + + fn note_pos (&self) -> &AtomicUsize; + /// Get the current pitch of the note cursor. + fn get_note_pos (&self) -> usize { + self.note_pos().load(Relaxed).min(127) + } + /// Set the current pitch fo the note cursor, returning the previous value. + fn set_note_pos (&self, x: usize) -> usize { + self.note_pos().swap(x.min(127), Relaxed) + } +} + +pub trait TimePoint { + fn time_pos (&self) -> &AtomicUsize; + /// Get the current time position of the note cursor. + fn get_time_pos (&self) -> usize { + self.time_pos().load(Relaxed) + } + /// Set the current time position of the note cursor, returning the previous value. + fn set_time_pos (&self, x: usize) -> usize { + self.time_pos().swap(x, Relaxed) + } +} + +pub trait MidiPoint: NotePoint + TimePoint { + /// Get the current end of the note cursor. + fn get_note_end (&self) -> usize { + self.get_time_pos() + self.get_note_len() + } +} diff --git a/src/device/editor/range.rs b/src/device/editor/range.rs new file mode 100644 index 00000000..973a1e14 --- /dev/null +++ b/src/device/editor/range.rs @@ -0,0 +1,124 @@ +use crate::*; + +/// +/// ``` +/// use tek::{TimeRange, NoteRange}; +/// let model = tek::MidiSelection::from((1, false)); +/// +/// let _ = model.get_time_len(); +/// let _ = model.get_time_zoom(); +/// let _ = model.get_time_lock(); +/// let _ = model.get_time_start(); +/// let _ = model.get_time_axis(); +/// let _ = model.get_time_end(); +/// +/// let _ = model.get_note_lo(); +/// let _ = model.get_note_axis(); +/// let _ = model.get_note_hi(); +/// ``` +#[derive(Debug, Clone, Default)] pub struct MidiSelection { + pub time_len: Arc, + /// Length of visible time axis + pub time_axis: Arc, + /// Earliest time displayed + pub time_start: Arc, + /// Time step + pub time_zoom: Arc, + /// Auto rezoom to fit in time axis + pub time_lock: Arc, + /// Length of visible note axis + pub note_axis: Arc, + // Lowest note displayed + pub note_lo: Arc, +} + +pub trait MidiRange: TimeRange + NoteRange {} + +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_from!(MidiSelection: |data:(usize, bool)| Self { + time_len: Arc::new(0.into()), + note_axis: Arc::new(0.into()), + note_lo: Arc::new(0.into()), + time_axis: Arc::new(0.into()), + time_start: Arc::new(0.into()), + time_zoom: Arc::new(data.0.into()), + time_lock: Arc::new(data.1.into()), +}); + +impl TimeRange for MidiSelection { + fn time_len (&self) -> &AtomicUsize { &self.time_len } + fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom } + fn time_lock (&self) -> &AtomicBool { &self.time_lock } + fn time_start (&self) -> &AtomicUsize { &self.time_start } + fn time_axis (&self) -> &AtomicUsize { &self.time_axis } +} + +impl NoteRange for MidiSelection { + fn note_lo (&self) -> &AtomicUsize { &self.note_lo } + fn note_axis (&self) -> &AtomicUsize { &self.note_axis } +} + +pub trait TimeRange { + fn time_len (&self) -> &AtomicUsize; + fn get_time_len (&self) -> usize { + self.time_len().load(Relaxed) + } + fn time_zoom (&self) -> &AtomicUsize; + fn get_time_zoom (&self) -> usize { + self.time_zoom().load(Relaxed) + } + fn set_time_zoom (&self, value: usize) -> usize { + self.time_zoom().swap(value, Relaxed) + } + fn time_lock (&self) -> &AtomicBool; + fn get_time_lock (&self) -> bool { + self.time_lock().load(Relaxed) + } + fn set_time_lock (&self, value: bool) -> bool { + self.time_lock().swap(value, Relaxed) + } + fn time_start (&self) -> &AtomicUsize; + fn get_time_start (&self) -> usize { + self.time_start().load(Relaxed) + } + fn set_time_start (&self, value: usize) -> usize { + self.time_start().swap(value, Relaxed) + } + fn time_axis (&self) -> &AtomicUsize; + fn get_time_axis (&self) -> usize { + self.time_axis().load(Relaxed) + } + fn get_time_end (&self) -> usize { + self.time_start().load(Relaxed) + self.time_axis().load(Relaxed) * self.time_zoom().load(Relaxed) + } +} + +pub trait NoteRange { + fn note_lo (&self) -> &AtomicUsize; + fn note_axis (&self) -> &AtomicUsize; + + fn get_note_lo (&self) -> usize { + self.note_lo().load(Relaxed) + } + fn set_note_lo (&self, x: usize) -> usize { + self.note_lo().swap(x, Relaxed) + } + fn get_note_axis (&self) -> usize { + self.note_axis().load(Relaxed) + } + fn get_note_hi (&self) -> usize { + (self.get_note_lo() + self.get_note_axis().saturating_sub(1)).min(127) + } +} diff --git a/src/device/meter.rs b/src/device/meter.rs new file mode 100644 index 00000000..bfe9a48a --- /dev/null +++ b/src/device/meter.rs @@ -0,0 +1,84 @@ +use crate::*; + +#[derive(Debug, Default)] pub enum MeteringMode { + #[default] Rms, + Log10, +} + +#[derive(Debug, Default, Clone)] +pub struct Log10Meter(pub f32); + +#[derive(Debug, Default, Clone)] +pub struct RmsMeter(pub f32); + +impl Draw for RmsMeter { + fn draw (self, to: &mut Tui) -> Usually> { + let XYWH(x, y, w, h) = to.area().into(); + let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); + let v = (signal * h as f32).ceil() as u16; + let y2 = y + h; + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); + for y in y..(y + v) { + for x in x..(x + w) { + to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green())); + } + } + Ok(to.area().into()) + } +} + +impl Draw for Log10Meter { + fn draw(self, to: &mut Tui) -> Usually> { + let XYWH(x, y, w, h) = to.area().into(); + let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); + let v = (signal * h as f32 / 100.0).ceil() as u16; + let y2 = y + h; + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); + for y in y..(y + v) { + for x in x..(x + w) { + to.blit(&"▌", x, y2 - y, Some(Style::default().green())); + } + } + Ok(to.area().into()) + } +} + +fn draw_meters (meters: &[f32]) -> impl Draw + use<'_> { + bg(Black, w_exact(2, iter_east_fixed(1, ||meters.iter(), |value, _index|{ + h_full(RmsMeter(*value)) + }))) +} + +pub fn to_log10 (samples: &[f32]) -> f32 { + let total: f32 = samples.iter().map(|x|x.abs()).sum(); + let count = samples.len() as f32; + 10. * (total / count).log10() +} + +pub fn to_rms (samples: &[f32]) -> f32 { + let sum = samples.iter() + .map(|s|*s) + .reduce(|sum, sample|sum + sample.abs()) + .unwrap_or(0.0); + (sum / samples.len() as f32).sqrt() +} + +#[cfg(test)] mod test_view_meter { + use super::*; + use proptest::prelude::*; + proptest! { + + #[test] fn proptest_view_meter ( + label in "\\PC*", value in f32::MIN..f32::MAX + ) { + let _ = view_meter(&label, value); + } + + #[test] fn proptest_view_meters ( + value1 in f32::MIN..f32::MAX, + value2 in f32::MIN..f32::MAX + ) { + let _ = view_meters(&[value1, value2]); + } + } +} diff --git a/src/device/mix.rs b/src/device/mix.rs index 860a7845..7e4d5574 100644 --- a/src/device/mix.rs +++ b/src/device/mix.rs @@ -1,78 +1,11 @@ use crate::*; -#[derive(Debug, Default)] pub enum MeteringMode { - #[default] Rms, - Log10, -} - -#[derive(Debug, Default, Clone)] -pub struct Log10Meter(pub f32); - -#[derive(Debug, Default, Clone)] -pub struct RmsMeter(pub f32); - #[derive(Debug, Default)] pub enum MixingMode { #[default] Summing, Average, } -#[cfg(test)] mod test_view_meter { - use super::*; - use proptest::prelude::*; - proptest! { - - #[test] fn proptest_view_meter ( - label in "\\PC*", value in f32::MIN..f32::MAX - ) { - let _ = view_meter(&label, value); - } - - #[test] fn proptest_view_meters ( - value1 in f32::MIN..f32::MAX, - value2 in f32::MIN..f32::MAX - ) { - let _ = view_meters(&[value1, value2]); - } - } -} - - impl Draw for RmsMeter { - fn draw(self, to: &mut Tui) -> Usually> { - let XYWH(x, y, w, h) = to.area(); - let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); - let v = (signal * h as f32).ceil() as u16; - let y2 = y + h; - //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); - for y in y..(y + v) { - for x in x..(x + w) { - to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green())); - } - } - } - } - - impl Draw for Log10Meter { - fn draw(self, to: &mut Tui) -> Usually> { - let XYWH(x, y, w, h) = to.area(); - let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); - let v = (signal * h as f32 / 100.0).ceil() as u16; - let y2 = y + h; - //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); - for y in y..(y + v) { - for x in x..(x + w) { - to.blit(&"▌", x, y2 - y, Some(Style::default().green())); - } - } - } - } - - fn draw_meters (meters: &[f32]) -> impl Draw + use<'_> { - bg(Black, w_exact(2, iter_east_fixed(1, ||meters.iter(), |value, _index|{ - h_full(RmsMeter(*value)) - }))) - } - pub fn mix_summing ( buffer: &mut [Vec], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>, ) -> bool { @@ -107,18 +40,3 @@ pub fn mix_average ( } true } - -pub fn to_log10 (samples: &[f32]) -> f32 { - let total: f32 = samples.iter().map(|x|x.abs()).sum(); - let count = samples.len() as f32; - 10. * (total / count).log10() -} - - -pub fn to_rms (samples: &[f32]) -> f32 { - let sum = samples.iter() - .map(|s|*s) - .reduce(|sum, sample|sum + sample.abs()) - .unwrap_or(0.0); - (sum / samples.len() as f32).sqrt() -} diff --git a/src/tek.rs b/src/tek.rs index c2697b41..3b168c6e 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -87,11 +87,10 @@ pub(crate) use tengri::{ /// Command-line entrypoint. #[cfg(feature = "cli")] pub fn main () -> Usually<()> { - Config::watch(|config|{ Exit::run(|exit|{ - Jack::new_run("tek", |jack|{ - App::new_shared_run(&exit, &jack, Arrangement::new( + Jack::new_run("tek", move|jack|{ + Ok(App::new_shared_run(&exit, &jack, Arrangement::new( &jack, None, Clock::new(&jack, Some(51.))?, @@ -99,13 +98,12 @@ pub fn main () -> Usually<()> { vec![], vec![], vec![], - ), config, ":menu") + ), config, ":menu")?.0) // TODO: Sync I/O timings with main clock, so that things // "accidentally" fall on the beat in overload conditions. }) }) })?; - Ok(()) } From 3bbe52093e450a7a361bd8700ed759333ee660f8 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Wed, 24 Jun 2026 16:20:30 +0300 Subject: [PATCH 14/31] restruct: 25e --- src/app.rs | 57 +--- src/app/audio.rs | 50 ++++ src/device.rs | 2 +- src/device/browse.rs | 6 +- src/device/editor.rs | 7 +- src/device/editor/piano.rs | 6 +- src/device/{sample.rs => sampler.rs} | 433 ++++----------------------- src/device/sampler/sample.rs | 144 +++++++++ src/device/sampler/sample_add.rs | 126 ++++++++ src/device/sampler/sample_kit.rs | 26 ++ src/device/sampler/voice.rs | 29 ++ 11 files changed, 454 insertions(+), 432 deletions(-) create mode 100644 src/app/audio.rs rename src/device/{sample.rs => sampler.rs} (57%) create mode 100644 src/device/sampler/sample.rs create mode 100644 src/device/sampler/sample_add.rs create mode 100644 src/device/sampler/sample_kit.rs create mode 100644 src/device/sampler/voice.rs diff --git a/src/app.rs b/src/app.rs index b7820826..1bf483e7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,5 @@ use crate::*; +pub mod audio; pub use self::audio::*; pub mod bind; pub use self::bind::*; pub mod cli; pub use self::cli::*; pub mod config; pub use self::config::*; @@ -123,57 +124,6 @@ impl App { }) } - pub fn jack_process ( - &mut self, client: &Client, scope: &ProcessScope - ) -> Control { - let t0 = self.perf.get_t0(); - self.clock().update_from_scope(scope).unwrap(); - let midi_in = self.project.midi_input_collect(scope); - if let Some(editor) = &self.editor() { - let mut pitch: Option = None; - for port in midi_in.iter() { - for event in port.iter() { - if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) - = event - { - pitch = Some(key.clone()); - } - } - } - if let Some(pitch) = pitch { - editor.set_note_pos(pitch.as_int() as usize); - } - } - let result = self.project.process_tracks(client, scope); - self.perf.update_from_jack_scope(t0, scope); - result - } - - pub fn jack_event ( - &mut self, event: JackEvent - ) { - use JackEvent::*; - match event { - SampleRate(sr) => { self.clock().timebase.sr.set(sr as f64); }, - PortRegistration(_id, true) => { - //let port = self.jack().port_by_id(id); - //println!("\rport add: {id} {port:?}"); - //println!("\rport add: {id}"); - }, - PortRegistration(_id, false) => { - /*println!("\rport del: {id}")*/ - }, - PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, - PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, - ClientRegistration(_id, true) => {}, - ClientRegistration(_id, false) => {}, - ThreadInit => {}, - XRun => {}, - GraphReorder => {}, - _ => { panic!("{event:?}"); } - } - } - /// Update memoized render of clock values. /// ``` /// tek::App::default().update_clock(); @@ -389,8 +339,6 @@ impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); impl_has_clips!( |self: App|self.pool.clips); -impl_audio!(App: tek_jack_process, tek_jack_event); - namespace!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); }); @@ -564,13 +512,14 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua }))) }.draw(to), - Some(":sessions") => h_exact(Some(6), w_min(30, thunk(|to: &mut Tui|{ + Some(":sessions") => h_exact(Some(6), w_min(Some(30), thunk(|to: &mut Tui|{ let fg = Rgb(224, 192, 128); for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; y_push((2 * index) as u16, &h_exact(2, w_full(bg(bg, origin_w(fg(fg, name)))))).draw(to); } + Ok(()) }))).draw(to), Some(":browse/title") => w_full(origin_w(field_v(ItemColor::default(), diff --git a/src/app/audio.rs b/src/app/audio.rs new file mode 100644 index 00000000..c5eee668 --- /dev/null +++ b/src/app/audio.rs @@ -0,0 +1,50 @@ +use crate::*; + +impl_audio!(App: tek_jack_process, tek_jack_event); + +fn tek_jack_process (state: &mut App, client: &Client, scope: &ProcessScope) -> Control { + let t0 = state.perf.get_t0(); + state.clock().update_from_scope(scope).unwrap(); + let midi_in = state.project.midi_input_collect(scope); + if let Some(editor) = &state.editor() { + let mut pitch: Option = None; + for port in midi_in.iter() { + for event in port.iter() { + if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) + = event + { + pitch = Some(key.clone()); + } + } + } + if let Some(pitch) = pitch { + editor.set_note_pos(pitch.as_int() as usize); + } + } + let result = state.project.process_tracks(client, scope); + state.perf.update_from_jack_scope(t0, scope); + result +} + +fn tek_jack_event (state: &mut App, event: JackEvent) { + use JackEvent::*; + match event { + SampleRate(sr) => { state.clock().timebase.sr.set(sr as f64); }, + PortRegistration(_id, true) => { + //let port = self.jack().port_by_id(id); + //println!("\rport add: {id} {port:?}"); + //println!("\rport add: {id}"); + }, + PortRegistration(_id, false) => { + /*println!("\rport del: {id}")*/ + }, + PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, + PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, + ClientRegistration(_id, true) => {}, + ClientRegistration(_id, false) => {}, + ThreadInit => {}, + XRun => {}, + GraphReorder => {}, + _ => { panic!("{event:?}"); } + } +} diff --git a/src/device.rs b/src/device.rs index d271ec7c..614a8735 100644 --- a/src/device.rs +++ b/src/device.rs @@ -109,7 +109,7 @@ pub mod menu; pub use self::menu::*; pub mod meter; pub use self::meter::*; pub mod mix; pub use self::mix::*; pub mod port; pub use self::port::*; -pub mod sample; pub use self::sample::*; +pub mod sampler; pub use self::sampler::*; pub mod sequence; pub use self::sequence::*; #[cfg(feature = "plugin")] pub mod plugin; diff --git a/src/device/browse.rs b/src/device/browse.rs index a10f486f..4f0e444a 100644 --- a/src/device/browse.rs +++ b/src/device/browse.rs @@ -1,4 +1,4 @@ -use crate::{*, clock::*, sequence::*, sample::*}; +use crate::{*, clock::*, sequence::*, sampler::*}; def_command!(FileBrowserCommand: |sampler: Sampler|{ //("begin" [] Some(Self::Begin)) @@ -261,6 +261,7 @@ impl<'a> PoolView<'a> { })))) } } + impl ClipLength { fn tui (&self) -> impl Draw { use ClipLengthFocus::*; @@ -311,6 +312,7 @@ impl Browse { fn _todo_stub_usize (&self) -> usize { todo!() } fn _todo_stub_arc_str (&self) -> Arc { todo!() } } + impl Browse { fn tui (&self) -> impl Draw { let item = |entry, _index|w_full(origin_w(entry)); @@ -324,6 +326,7 @@ impl Browse { iter_south_fixed(1, iterate, item) } } + impl<'a> Iterator for EntriesIterator<'a, Tui> { type Item = impl Draw; fn next (&mut self) -> Option { @@ -341,6 +344,7 @@ impl<'a> Iterator for EntriesIterator<'a, Tui> { } } } + impl PartialEq for BrowseTarget { fn eq (&self, other: &Self) -> bool { match self { diff --git a/src/device/editor.rs b/src/device/editor.rs index 2ab03f36..943c8514 100644 --- a/src/device/editor.rs +++ b/src/device/editor.rs @@ -16,14 +16,15 @@ use crate::*; /// let _ = editor.edit_status(); /// ``` pub struct MidiEditor { - /// Size of editor on screen - pub size: Size, /// View mode and state of editor pub mode: PianoHorizontal, + /// Size of editor on screen + pub size: Size, } impl_default!(MidiEditor: Self { - size: [0, 0].into(), mode: PianoHorizontal::new(None) + mode: PianoHorizontal::new(None), + size: Size(0.into(), 0.into()), }); impl MidiEditor { diff --git a/src/device/editor/piano.rs b/src/device/editor/piano.rs index d1c6fbd4..d0b4d8f4 100644 --- a/src/device/editor/piano.rs +++ b/src/device/editor/piano.rs @@ -32,10 +32,10 @@ impl Draw for PianoHorizontal { impl PianoHorizontal { pub fn new (clip: Option<&Arc>>) -> Self { - let size: Size = [0, 0].into(); + let size = Size::default(); let mut range = MidiSelection::from((12, true)); - range.time_axis = size.x.clone(); - range.note_axis = size.y.clone(); + range.time_axis = size.0.clone(); + range.note_axis = size.1.clone(); let piano = Self { keys_width: 5, size, diff --git a/src/device/sample.rs b/src/device/sampler.rs similarity index 57% rename from src/device/sample.rs rename to src/device/sampler.rs index b9581142..61abcf40 100644 --- a/src/device/sample.rs +++ b/src/device/sampler.rs @@ -1,51 +1,9 @@ use crate::{*, device::*, browse::*, mix::*}; -def_command!(SamplerCommand: |sampler: Sampler| { - RecordToggle { slot: usize } => { - let slot = *slot; - let recording = sampler.recording.as_ref().map(|x|x.0); - let _ = Self::RecordFinish.act(sampler)?; - // autoslice: continue recording at next slot - if recording != Some(slot) { - Self::RecordBegin { slot }.act(sampler) - } else { - Ok(None) - } - }, - RecordBegin { slot: usize } => { - let slot = *slot; - sampler.recording = Some(( - slot, - Some(Arc::new(RwLock::new(Sample::new( - "Sample", 0, 0, vec![vec![];sampler.audio_ins.len()] - )))) - )); - Ok(None) - }, - RecordFinish => { - let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{ - std::mem::swap(sample, &mut sampler.samples.0[*index]); - sample - }); // TODO: undo - Ok(None) - }, - RecordCancel => { - sampler.recording = None; - Ok(None) - }, - PlaySample { slot: usize } => { - let slot = *slot; - if let Some(ref sample) = sampler.samples.0[slot] { - sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128))); - } - Ok(None) - }, - StopSample { slot: usize } => { - let _slot = *slot; - todo!(); - //Ok(None) - }, -}); +mod voice; pub use self::voice::*; +mod sample; pub use self::sample::*; +mod sample_add; pub use self::sample_add::*; +mod sample_kit; pub use self::sample_kit::*; /// Plays [Voice]s from [Sample]s. /// @@ -97,51 +55,17 @@ def_command!(SamplerCommand: |sampler: Sampler| { #[cfg(feature = "meter")] pub output_meters: Vec, } -/// Collection of samples, one per slot, fixed number of slots. -/// -/// History: Separated to cleanly implement [Default]. -/// -/// ``` -/// let samples = tek::SampleKit([None, None, None, None]); -/// ``` -#[derive(Debug)] pub struct SampleKit( - pub [Option>>;N] -); +impl_audio!(Sampler: sampler_jack_process); -/// A sound cut. -/// -/// ``` -/// let sample = tek::Sample::default(); -/// let sample = tek::Sample::new("test", 0, 0, vec![]); -/// ``` -#[derive(Default, Debug)] pub struct Sample { - pub name: Arc, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, - pub gain: f32, - pub color: ItemTheme, -} - -/// A currently playing instance of a sample. -#[derive(Default, Debug, Clone)] pub struct Voice { - pub sample: Arc>, - pub after: usize, - pub position: usize, - pub velocity: f32, -} - -#[derive(Default, Debug)] pub struct SampleAdd { - pub exited: bool, - pub dir: PathBuf, - pub subdirs: Vec, - pub files: Vec, - pub cursor: usize, - pub offset: usize, - pub sample: Arc>, - pub voices: Arc>>, - pub _search: Option, +pub(crate) fn sampler_jack_process (state: &mut Sampler, _: &Client, scope: &ProcessScope) -> Control { + if let Some(midi_in) = &state.midi_in { + for midi in midi_in.port().iter(scope) { + sampler_midi_in(&state.samples, &state.voices, midi) + } + } + state.process_audio_out(scope); + state.process_audio_in(scope); + Control::Continue } #[derive(Debug)] pub enum SamplerMode { @@ -152,28 +76,6 @@ def_command!(SamplerCommand: |sampler: Sampler| { pub type MidiSample = (Option, Arc>); -impl Default for SampleKit { - fn default () -> Self { Self([const { None }; N]) } -} -impl Iterator for Voice { - type Item = [f32;2]; - fn next (&mut self) -> Option { - if self.after > 0 { - self.after -= 1; - return Some([0.0, 0.0]) - } - let sample = self.sample.read().unwrap(); - if self.position < sample.end { - let position = self.position; - self.position += 1; - return sample.channels[0].get(position).map(|_amplitude|[ - sample.channels[0][position] * self.velocity * sample.gain, - sample.channels[0][position] * self.velocity * sample.gain, - ]) - } - None - } -} impl NoteRange for Sampler { fn note_lo (&self) -> &AtomicUsize { &self.note_lo @@ -182,6 +84,7 @@ impl NoteRange for Sampler { &self.size.y } } + impl NotePoint for Sampler { fn note_len (&self) -> &AtomicUsize { unreachable!(); @@ -205,6 +108,7 @@ impl NotePoint for Sampler { old } } + impl Sampler { pub fn new ( jack: &Jack<'static>, @@ -331,252 +235,6 @@ impl Sampler { } } } -impl SampleAdd { - fn exited (&self) -> bool { - self.exited - } - fn exit (&mut self) { - self.exited = true - } - pub fn new ( - sample: &Arc>, - voices: &Arc>> - ) -> Usually { - let dir = std::env::current_dir()?; - let (subdirs, files) = scan(&dir)?; - Ok(Self { - exited: false, - dir, - subdirs, - files, - cursor: 0, - offset: 0, - sample: sample.clone(), - voices: voices.clone(), - _search: None - }) - } - fn rescan (&mut self) -> Usually<()> { - scan(&self.dir).map(|(subdirs, files)|{ - self.subdirs = subdirs; - self.files = files; - }) - } - fn prev (&mut self) { - self.cursor = self.cursor.saturating_sub(1); - } - fn next (&mut self) { - self.cursor = self.cursor + 1; - } - fn try_preview (&mut self) -> Usually<()> { - if let Some(path) = self.cursor_file() { - if let Ok(sample) = Sample::from_file(&path) { - *self.sample.write().unwrap() = sample; - self.voices.write().unwrap().push( - Sample::play(&self.sample, 0, &u7::from(100u8)) - ); - } - //load_sample(&path)?; - //let src = std::fs::File::open(&path)?; - //let mss = MediaSourceStream::new(Box::new(src), Default::default()); - //let mut hint = Hint::new(); - //if let Some(ext) = path.extension() { - //hint.with_extension(&ext.to_string_lossy()); - //} - //let meta_opts: MetadataOptions = Default::default(); - //let fmt_opts: FormatOptions = Default::default(); - //if let Ok(mut probed) = symphonia::default::get_probe() - //.format(&hint, mss, &fmt_opts, &meta_opts) - //{ - //panic!("{:?}", probed.format.metadata()); - //}; - } - Ok(()) - } - fn cursor_dir (&self) -> Option { - if self.cursor < self.subdirs.len() { - Some(self.dir.join(&self.subdirs[self.cursor])) - } else { - None - } - } - fn cursor_file (&self) -> Option { - if self.cursor < self.subdirs.len() { - return None - } - let index = self.cursor.saturating_sub(self.subdirs.len()); - if index < self.files.len() { - Some(self.dir.join(&self.files[index])) - } else { - None - } - } - fn pick (&mut self) -> Usually { - if self.cursor == 0 { - if let Some(parent) = self.dir.parent() { - self.dir = parent.into(); - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - } - if let Some(dir) = self.cursor_dir() { - self.dir = dir; - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - if let Some(path) = self.cursor_file() { - let (end, channels) = read_sample_data(&path.to_string_lossy())?; - let mut sample = self.sample.write().unwrap(); - sample.name = path.file_name().unwrap().to_string_lossy().into(); - sample.end = end; - sample.channels = channels; - return Ok(true) - } - return Ok(false) - } -} -impl SampleKit { - pub fn get (&self, index: usize) -> &Option>> { - if index < self.0.len() { - &self.0[index] - } else { - &None - } - } -} -impl Sample { - pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { - Self { - name: name.as_ref().into(), - start, - end, - channels, - rate: None, - gain: 1.0, - color: ItemTheme::random(), - } - } - pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { - Voice { - sample: sample.clone(), - after, - position: sample.read().unwrap().start, - velocity: velocity.as_int() as f32 / 127.0, - } - } - pub fn handle_cc (&mut self, controller: u7, value: u7) { - let percentage = value.as_int() as f64 / 127.; - match controller.as_int() { - 20 => { - self.start = (percentage * self.end as f64) as usize; - }, - 21 => { - let length = self.channels[0].len(); - self.end = length.min( - self.start + (percentage * (length as f64 - self.start as f64)) as usize - ); - }, - 22 => { /*attack*/ }, - 23 => { /*decay*/ }, - 24 => { - self.gain = percentage as f32 * 2.0; - }, - 26 => { /* pan */ } - 25 => { /* pitch */ } - _ => {} - } - } - /// Read WAV from file - pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { - let mut channels: Vec> = vec![]; - for channel in wavers::Wav::from_path(src)?.channels() { - channels.push(channel); - } - let mut end = 0; - let mut data: Vec> = vec![]; - for samples in channels.iter() { - let channel = Vec::from(samples.as_ref()); - end = end.max(channel.len()); - data.push(channel); - } - Ok((end, data)) - } - pub fn from_file (path: &PathBuf) -> Usually { - let name = path.file_name().unwrap().to_string_lossy().into(); - let mut sample = Self { name, ..Default::default() }; - // Use file extension if present - let mut hint = Hint::new(); - if let Some(ext) = path.extension() { - hint.with_extension(&ext.to_string_lossy()); - } - let probed = symphonia::default::get_probe().format( - &hint, - MediaSourceStream::new( - Box::new(File::open(path)?), - Default::default(), - ), - &Default::default(), - &Default::default() - )?; - let mut format = probed.format; - let params = &format.tracks().iter() - .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) - .expect("no tracks found") - .codec_params; - let mut decoder = get_codecs().make(params, &Default::default())?; - loop { - match format.next_packet() { - Ok(packet) => sample.decode_packet(&mut decoder, packet)?, - Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), - Err(err) => return Err(err.into()), - }; - }; - sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); - Ok(sample) - } - fn decode_packet ( - &mut self, decoder: &mut Box, packet: Packet - ) -> Usually<()> { - // Decode a packet - let decoded = decoder - .decode(&packet) - .map_err(|e|Box::::from(e))?; - // Determine sample rate - let spec = *decoded.spec(); - if let Some(rate) = self.rate { - if rate != spec.rate as usize { - panic!("sample rate changed"); - } - } else { - self.rate = Some(spec.rate as usize); - } - // Determine channel count - while self.channels.len() < spec.channels.count() { - self.channels.push(vec![]); - } - // Load sample - let mut samples = SampleBuffer::new( - decoded.frames() as u64, - spec - ); - if samples.capacity() > 0 { - samples.copy_interleaved_ref(decoded); - for frame in samples.samples().chunks(spec.channels.count()) { - for (chan, frame) in frame.iter().enumerate() { - self.channels[chan].push(*frame) - } - } - } - Ok(()) - } -} -impl Draw for SampleAdd { - fn draw (self, _to: &mut Tui) -> Usually> { - todo!() - } -} fn draw_list_item (sample: &Option>>) -> String { if let Some(sample) = sample { @@ -652,18 +310,6 @@ fn draw_viewer (sample: Option<&Arc>>) -> impl Draw + use<'_ }) } -impl_audio!(Sampler: sampler_jack_process); -pub(crate) fn sampler_jack_process (state: &mut Sampler, _: &Client, scope: &ProcessScope) -> Control { - if let Some(midi_in) = &state.midi_in { - for midi in midi_in.port().iter(scope) { - sampler_midi_in(&state.samples, &state.voices, midi) - } - } - state.process_audio_out(scope); - state.process_audio_in(scope); - Control::Continue -} - /// Create [Voice]s from [Sample]s in response to MIDI input. fn sampler_midi_in ( samples: &SampleKit<128>, voices: &Arc>>, RawMidi { time, bytes }: RawMidi @@ -704,3 +350,50 @@ fn draw_sample ( fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { todo!(); } + +def_command!(SamplerCommand: |sampler: Sampler| { + RecordToggle { slot: usize } => { + let slot = *slot; + let recording = sampler.recording.as_ref().map(|x|x.0); + let _ = Self::RecordFinish.act(sampler)?; + // autoslice: continue recording at next slot + if recording != Some(slot) { + Self::RecordBegin { slot }.act(sampler) + } else { + Ok(None) + } + }, + RecordBegin { slot: usize } => { + let slot = *slot; + sampler.recording = Some(( + slot, + Some(Arc::new(RwLock::new(Sample::new( + "Sample", 0, 0, vec![vec![];sampler.audio_ins.len()] + )))) + )); + Ok(None) + }, + RecordFinish => { + let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{ + std::mem::swap(sample, &mut sampler.samples.0[*index]); + sample + }); // TODO: undo + Ok(None) + }, + RecordCancel => { + sampler.recording = None; + Ok(None) + }, + PlaySample { slot: usize } => { + let slot = *slot; + if let Some(ref sample) = sampler.samples.0[slot] { + sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128))); + } + Ok(None) + }, + StopSample { slot: usize } => { + let _slot = *slot; + todo!(); + //Ok(None) + }, +}); diff --git a/src/device/sampler/sample.rs b/src/device/sampler/sample.rs new file mode 100644 index 00000000..06caf01f --- /dev/null +++ b/src/device/sampler/sample.rs @@ -0,0 +1,144 @@ +use crate::*; + +/// A sound cut. +/// +/// ``` +/// let sample = tek::Sample::default(); +/// let sample = tek::Sample::new("test", 0, 0, vec![]); +/// ``` +#[derive(Default, Debug)] pub struct Sample { + pub name: Arc, + pub start: usize, + pub end: usize, + pub channels: Vec>, + pub rate: Option, + pub gain: f32, + pub color: ItemTheme, +} + +impl Sample { + pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { + Self { + name: name.as_ref().into(), + start, + end, + channels, + rate: None, + gain: 1.0, + color: ItemTheme::random(), + } + } + pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { + Voice { + sample: sample.clone(), + after, + position: sample.read().unwrap().start, + velocity: velocity.as_int() as f32 / 127.0, + } + } + pub fn handle_cc (&mut self, controller: u7, value: u7) { + let percentage = value.as_int() as f64 / 127.; + match controller.as_int() { + 20 => { + self.start = (percentage * self.end as f64) as usize; + }, + 21 => { + let length = self.channels[0].len(); + self.end = length.min( + self.start + (percentage * (length as f64 - self.start as f64)) as usize + ); + }, + 22 => { /*attack*/ }, + 23 => { /*decay*/ }, + 24 => { + self.gain = percentage as f32 * 2.0; + }, + 26 => { /* pan */ } + 25 => { /* pitch */ } + _ => {} + } + } + /// Read WAV from file + pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { + let mut channels: Vec> = vec![]; + for channel in wavers::Wav::from_path(src)?.channels() { + channels.push(channel); + } + let mut end = 0; + let mut data: Vec> = vec![]; + for samples in channels.iter() { + let channel = Vec::from(samples.as_ref()); + end = end.max(channel.len()); + data.push(channel); + } + Ok((end, data)) + } + pub fn from_file (path: &PathBuf) -> Usually { + let name = path.file_name().unwrap().to_string_lossy().into(); + let mut sample = Self { name, ..Default::default() }; + // Use file extension if present + let mut hint = Hint::new(); + if let Some(ext) = path.extension() { + hint.with_extension(&ext.to_string_lossy()); + } + let probed = symphonia::default::get_probe().format( + &hint, + MediaSourceStream::new( + Box::new(File::open(path)?), + Default::default(), + ), + &Default::default(), + &Default::default() + )?; + let mut format = probed.format; + let params = &format.tracks().iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + .expect("no tracks found") + .codec_params; + let mut decoder = get_codecs().make(params, &Default::default())?; + loop { + match format.next_packet() { + Ok(packet) => sample.decode_packet(&mut decoder, packet)?, + Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), + Err(err) => return Err(err.into()), + }; + }; + sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); + Ok(sample) + } + fn decode_packet ( + &mut self, decoder: &mut Box, packet: Packet + ) -> Usually<()> { + // Decode a packet + let decoded = decoder + .decode(&packet) + .map_err(|e|Box::::from(e))?; + // Determine sample rate + let spec = *decoded.spec(); + if let Some(rate) = self.rate { + if rate != spec.rate as usize { + panic!("sample rate changed"); + } + } else { + self.rate = Some(spec.rate as usize); + } + // Determine channel count + while self.channels.len() < spec.channels.count() { + self.channels.push(vec![]); + } + // Load sample + let mut samples = SampleBuffer::new( + decoded.frames() as u64, + spec + ); + if samples.capacity() > 0 { + samples.copy_interleaved_ref(decoded); + for frame in samples.samples().chunks(spec.channels.count()) { + for (chan, frame) in frame.iter().enumerate() { + self.channels[chan].push(*frame) + } + } + } + Ok(()) + } +} diff --git a/src/device/sampler/sample_add.rs b/src/device/sampler/sample_add.rs new file mode 100644 index 00000000..996e4b04 --- /dev/null +++ b/src/device/sampler/sample_add.rs @@ -0,0 +1,126 @@ +use crate::{*, device::sampler::*}; + +#[derive(Default, Debug)] pub struct SampleAdd { + pub exited: bool, + pub dir: PathBuf, + pub subdirs: Vec, + pub files: Vec, + pub cursor: usize, + pub offset: usize, + pub sample: Arc>, + pub voices: Arc>>, + pub _search: Option, +} + +impl SampleAdd { + fn exited (&self) -> bool { + self.exited + } + fn exit (&mut self) { + self.exited = true + } + pub fn new ( + sample: &Arc>, + voices: &Arc>> + ) -> Usually { + let dir = std::env::current_dir()?; + let (subdirs, files) = scan(&dir)?; + Ok(Self { + exited: false, + dir, + subdirs, + files, + cursor: 0, + offset: 0, + sample: sample.clone(), + voices: voices.clone(), + _search: None + }) + } + fn rescan (&mut self) -> Usually<()> { + scan(&self.dir).map(|(subdirs, files)|{ + self.subdirs = subdirs; + self.files = files; + }) + } + fn prev (&mut self) { + self.cursor = self.cursor.saturating_sub(1); + } + fn next (&mut self) { + self.cursor = self.cursor + 1; + } + fn try_preview (&mut self) -> Usually<()> { + if let Some(path) = self.cursor_file() { + if let Ok(sample) = Sample::from_file(&path) { + *self.sample.write().unwrap() = sample; + self.voices.write().unwrap().push( + Sample::play(&self.sample, 0, &u7::from(100u8)) + ); + } + //load_sample(&path)?; + //let src = std::fs::File::open(&path)?; + //let mss = MediaSourceStream::new(Box::new(src), Default::default()); + //let mut hint = Hint::new(); + //if let Some(ext) = path.extension() { + //hint.with_extension(&ext.to_string_lossy()); + //} + //let meta_opts: MetadataOptions = Default::default(); + //let fmt_opts: FormatOptions = Default::default(); + //if let Ok(mut probed) = symphonia::default::get_probe() + //.format(&hint, mss, &fmt_opts, &meta_opts) + //{ + //panic!("{:?}", probed.format.metadata()); + //}; + } + Ok(()) + } + fn cursor_dir (&self) -> Option { + if self.cursor < self.subdirs.len() { + Some(self.dir.join(&self.subdirs[self.cursor])) + } else { + None + } + } + fn cursor_file (&self) -> Option { + if self.cursor < self.subdirs.len() { + return None + } + let index = self.cursor.saturating_sub(self.subdirs.len()); + if index < self.files.len() { + Some(self.dir.join(&self.files[index])) + } else { + None + } + } + fn pick (&mut self) -> Usually { + if self.cursor == 0 { + if let Some(parent) = self.dir.parent() { + self.dir = parent.into(); + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + } + if let Some(dir) = self.cursor_dir() { + self.dir = dir; + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + if let Some(path) = self.cursor_file() { + let (end, channels) = read_sample_data(&path.to_string_lossy())?; + let mut sample = self.sample.write().unwrap(); + sample.name = path.file_name().unwrap().to_string_lossy().into(); + sample.end = end; + sample.channels = channels; + return Ok(true) + } + return Ok(false) + } +} + +impl Draw for SampleAdd { + fn draw (self, _to: &mut Tui) -> Usually> { + todo!() + } +} diff --git a/src/device/sampler/sample_kit.rs b/src/device/sampler/sample_kit.rs new file mode 100644 index 00000000..382ce678 --- /dev/null +++ b/src/device/sampler/sample_kit.rs @@ -0,0 +1,26 @@ +use crate::*; + +/// Collection of samples, one per slot, fixed number of slots. +/// +/// History: Separated to cleanly implement [Default]. +/// +/// ``` +/// let samples = tek::SampleKit([None, None, None, None]); +/// ``` +#[derive(Debug)] pub struct SampleKit ( + pub [Option>>;N] +); + +impl Default for SampleKit { + fn default () -> Self { Self([const { None }; N]) } +} + +impl SampleKit { + pub fn get (&self, index: usize) -> &Option>> { + if index < self.0.len() { + &self.0[index] + } else { + &None + } + } +} diff --git a/src/device/sampler/voice.rs b/src/device/sampler/voice.rs new file mode 100644 index 00000000..632e5202 --- /dev/null +++ b/src/device/sampler/voice.rs @@ -0,0 +1,29 @@ +use crate::*; + +/// A currently playing instance of a sample. +#[derive(Default, Debug, Clone)] pub struct Voice { + pub sample: Arc>, + pub after: usize, + pub position: usize, + pub velocity: f32, +} + +impl Iterator for Voice { + type Item = [f32;2]; + fn next (&mut self) -> Option { + if self.after > 0 { + self.after -= 1; + return Some([0.0, 0.0]) + } + let sample = self.sample.read().unwrap(); + if self.position < sample.end { + let position = self.position; + self.position += 1; + return sample.channels[0].get(position).map(|_amplitude|[ + sample.channels[0][position] * self.velocity * sample.gain, + sample.channels[0][position] * self.velocity * sample.gain, + ]) + } + None + } +} From 47ef33180cd60346a3a4edbd7d39da0251fff785 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Thu, 25 Jun 2026 11:09:45 +0300 Subject: [PATCH 15/31] restruct: 21e --- src/device/clock.rs | 78 +--------------------------------- src/device/clock/clock_view.rs | 78 ++++++++++++++++++++++++++++++++++ src/device/editor.rs | 5 ++- src/device/sampler.rs | 2 +- tengri | 2 +- 5 files changed, 85 insertions(+), 80 deletions(-) create mode 100644 src/device/clock/clock_view.rs diff --git a/src/device/clock.rs b/src/device/clock.rs index 6aedab97..11a85607 100644 --- a/src/device/clock.rs +++ b/src/device/clock.rs @@ -65,18 +65,6 @@ pub trait HasClock: AsRef + AsMut { /// can be clocked in microseconds with f64 without losing precision. pub trait TimeUnit: InteriorMutable {} -/// Contains memoized renders of clock values. -/// -/// Performance optimization. -#[derive(Debug)] pub struct ClockView { - pub sr: Memo, String>, - pub buf: Memo, String>, - pub lat: Memo, String>, - pub bpm: Memo, String>, - pub beat: Memo, String>, - pub time: Memo, String>, -} - pub const DEFAULT_PPQ: f64 = 96.0; /// FIXME: remove this and use PPQ from timebase everywhere: @@ -377,71 +365,6 @@ impl Act for ClockCommand { } } -impl ClockView { - pub const BEAT_EMPTY: &'static str = "-.-.--"; - pub const TIME_EMPTY: &'static str = "-.---s"; - pub const BPM_EMPTY: &'static str = "---.---"; - pub fn update_clock (cache: &Arc>, clock: &Clock, compact: bool) { - let rate = clock.timebase.sr.get(); - let chunk = clock.chunk.load(Relaxed) as f64; - let lat = chunk / rate * 1000.; - let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); - let mut cache = cache.write().unwrap(); - - cache.buf.update( - Some(chunk), rewrite!(buf, "{chunk}") - ); - - cache.lat.update( - Some(lat), rewrite!(buf, "{lat:.1}ms") - ); - - cache.sr.update( - Some((compact, rate)), |buf: &mut String,_,_|{ - buf.clear(); - if compact { - write!(buf, "{:.1}kHz", rate / 1000.) - } else { - write!(buf, "{:.0}Hz", rate) - } - } - ); - - if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { - let pulse = clock.timebase.usecs_to_pulse(now); - let time = now/1000000.; - let bpm = clock.timebase.bpm.get(); - cache.beat.update(Some(pulse), |buf, _, _|{ - buf.clear(); - clock.timebase.format_beats_1_to(buf, pulse) - }); - cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time)); - cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm)); - } else { - cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY)); - cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY)); - cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY)); - } - } -} - -impl_default!(ClockView: { - let mut beat = String::with_capacity(16); - let _ = write!(beat, "{}", Self::BEAT_EMPTY); - let mut time = String::with_capacity(16); - let _ = write!(time, "{}", Self::TIME_EMPTY); - let mut bpm = String::with_capacity(16); - let _ = write!(bpm, "{}", Self::BPM_EMPTY); - Self { - beat: Memo::new(None, beat), - time: Memo::new(None, time), - bpm: Memo::new(None, bpm), - sr: Memo::new(None, String::with_capacity(16)), - buf: Memo::new(None, String::with_capacity(16)), - lat: Memo::new(None, String::with_capacity(16)), - } -}); - #[cfg(feature = "clock")] impl_has!(Clock: |self: Track|self.sequencer.clock); impl_default!(Timebase: Self::new(48000f64, 150f64, DEFAULT_PPQ)); impl_time_unit!(SampleCount); @@ -456,3 +379,4 @@ impl_time_unit!(LaunchSync); mod moment; pub use self::moment::*; mod ticker; pub use self::ticker::*; mod timebase; pub use self::timebase::*; +mod clock_view; pub use self::clock_view::*; diff --git a/src/device/clock/clock_view.rs b/src/device/clock/clock_view.rs new file mode 100644 index 00000000..009982b1 --- /dev/null +++ b/src/device/clock/clock_view.rs @@ -0,0 +1,78 @@ +use crate::*; + +/// Contains memoized renders of clock values. +/// +/// Performance optimization. +#[derive(Debug)] pub struct ClockView { + pub sr: Memo, String>, + pub buf: Memo, String>, + pub lat: Memo, String>, + pub bpm: Memo, String>, + pub beat: Memo, String>, + pub time: Memo, String>, +} + +impl_default!(ClockView: { + let mut beat = String::with_capacity(16); + let _ = write!(beat, "{}", Self::BEAT_EMPTY); + let mut time = String::with_capacity(16); + let _ = write!(time, "{}", Self::TIME_EMPTY); + let mut bpm = String::with_capacity(16); + let _ = write!(bpm, "{}", Self::BPM_EMPTY); + Self { + beat: Memo::new(None, beat), + time: Memo::new(None, time), + bpm: Memo::new(None, bpm), + sr: Memo::new(None, String::with_capacity(16)), + buf: Memo::new(None, String::with_capacity(16)), + lat: Memo::new(None, String::with_capacity(16)), + } +}); + +impl ClockView { + pub const BEAT_EMPTY: &'static str = "-.-.--"; + pub const TIME_EMPTY: &'static str = "-.---s"; + pub const BPM_EMPTY: &'static str = "---.---"; + pub fn update_clock (cache: &Arc>, clock: &Clock, compact: bool) { + let rate = clock.timebase.sr.get(); + let chunk = clock.chunk.load(Relaxed) as f64; + let lat = chunk / rate * 1000.; + let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); + let mut cache = cache.write().unwrap(); + + cache.buf.update( + Some(chunk), rewrite!(buf, "{chunk}") + ); + + cache.lat.update( + Some(lat), rewrite!(buf, "{lat:.1}ms") + ); + + cache.sr.update( + Some((compact, rate)), |buf: &mut String,_,_|{ + buf.clear(); + if compact { + write!(buf, "{:.1}kHz", rate / 1000.) + } else { + write!(buf, "{:.0}Hz", rate) + } + } + ); + + if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { + let pulse = clock.timebase.usecs_to_pulse(now); + let time = now/1000000.; + let bpm = clock.timebase.bpm.get(); + cache.beat.update(Some(pulse), |buf, _, _|{ + buf.clear(); + clock.timebase.format_beats_1_to(buf, pulse) + }); + cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time)); + cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm)); + } else { + cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY)); + cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY)); + cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY)); + } + } +} diff --git a/src/device/editor.rs b/src/device/editor.rs index 943c8514..822ac099 100644 --- a/src/device/editor.rs +++ b/src/device/editor.rs @@ -24,7 +24,10 @@ pub struct MidiEditor { impl_default!(MidiEditor: Self { mode: PianoHorizontal::new(None), - size: Size(0.into(), 0.into()), + size: Size( + Arc::new(0usize.into()), + Arc::new(0usize.into()), + ), }); impl MidiEditor { diff --git a/src/device/sampler.rs b/src/device/sampler.rs index 61abcf40..da02443b 100644 --- a/src/device/sampler.rs +++ b/src/device/sampler.rs @@ -81,7 +81,7 @@ impl NoteRange for Sampler { &self.note_lo } fn note_axis (&self) -> &AtomicUsize { - &self.size.y + &self.size.1 } } diff --git a/tengri b/tengri index 0273d2ac..cb382cf1 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 0273d2ac75b8a144ea03b4bdd94d08003f3589df +Subproject commit cb382cf12e9169310ddab4eca2ca01e87fde9869 From 804a5bc905774de75ea99598905afc4b9a4723e7 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Thu, 25 Jun 2026 21:49:33 +0300 Subject: [PATCH 16/31] restruct: 14e --- src/app.rs | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/app.rs b/src/app.rs index 1bf483e7..2c88280a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -88,10 +88,14 @@ impl App { pub fn new_shared_run ( exit: &Exit, jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef - ) -> Usually<(Self, Task, Task)> { + ) -> Usually<(Arc>, Task, Task)> { let state = Self::new_shared(&jack, project, config, mode); - let keyboard = tui_input(&exit, &state, Duration::from_millis(100))?; - let terminal = tui_output(&exit, &state, Duration::from_millis(10))?; + let keyboard = tui_input( + exit.as_ref(), &state, Duration::from_millis(100) + )?; + let terminal = tui_output( + &mut std::io::stdout(), exit.as_ref(), &state, Duration::from_millis(10) + )?; Ok((state, keyboard, terminal)) } @@ -497,7 +501,7 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua let height = (modes.read().unwrap().len() * 2) as u16; h_exact(height, w_min(Some(30), thunk(move |to: &mut Tui|{ for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { - let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; + let b = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); let fg1 = Rgb(224, 192, 128); @@ -506,20 +510,23 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua let field_id = w_full(origin_e(fg(fg2, id))); let field_info = w_full(origin_w(info)); y_push((2 * index) as u16, - h_exact(2, w_full(bg(bg, south( - above(field_name, field_id), field_info))))).draw(to); + h_exact(2, w_full(bg(b, south( + above(field_name, field_id), + field_info + ))))).draw(to); } + Ok(to.area().into()) }))) }.draw(to), Some(":sessions") => h_exact(Some(6), w_min(Some(30), thunk(|to: &mut Tui|{ - let fg = Rgb(224, 192, 128); + let f = Rgb(224, 192, 128); for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { - let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; + let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; y_push((2 * index) as u16, - &h_exact(2, w_full(bg(bg, origin_w(fg(fg, name)))))).draw(to); + &h_exact(2, w_full(bg(b, origin_w(fg(f, name)))))).draw(to); } - Ok(()) + Ok(to.area().into()) }))).draw(to), Some(":browse/title") => w_full(origin_w(field_v(ItemColor::default(), @@ -557,7 +564,7 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua }, _ => unreachable!() - } + }; Ok(()) } From 19ab138320a67f106dc39717a70c58100b41c8b9 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Sat, 27 Jun 2026 22:10:04 +0300 Subject: [PATCH 17/31] restruct: 12e --- src/tek.rs | 27 ++++++++++++++++++++------- tengri | 2 +- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/tek.rs b/src/tek.rs index 3b168c6e..6e83d5e0 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -22,7 +22,7 @@ pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, n pub extern crate tengri; pub(crate) use tengri::{ - *, lang::*, exit::*, eval::*, keys::*, sing::*, time::*, draw::*, term::*, color::*, task::*, + *, lang::*, exit::*, eval::*, sing::*, time::*, draw::*, term::*, color::*, task::*, crossterm::event::{Event, KeyEvent}, ratatui::{ self, @@ -89,8 +89,8 @@ pub(crate) use tengri::{ pub fn main () -> Usually<()> { Config::watch(|config|{ Exit::run(|exit|{ - Jack::new_run("tek", move|jack|{ - Ok(App::new_shared_run(&exit, &jack, Arrangement::new( + let state = Jack::new_run("tek", move|jack|{ + Ok(App::new(&jack, Arrangement::new( &jack, None, Clock::new(&jack, Some(51.))?, @@ -98,15 +98,28 @@ pub fn main () -> Usually<()> { vec![], vec![], vec![], - ), config, ":menu")?.0) - // TODO: Sync I/O timings with main clock, so that things - // "accidentally" fall on the beat in overload conditions. - }) + ), config, ":menu")) + })?; + let keyboard = tui_input( + exit.as_ref(), &state, Duration::from_millis(100) + )?; + let terminal = tui_output( + &mut std::io::stdout(), exit.as_ref(), &state, Duration::from_millis(10) + )?; + Ok(()) }) })?; Ok(()) } +tui_keys!(|self: App, input| { + todo!() +}); + +tui_view!(|self: App| { + "" +}); + pub fn swap_value ( target: &mut T, value: &T, returned: impl Fn(T)->U ) -> Perhaps { diff --git a/tengri b/tengri index cb382cf1..7613c655 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit cb382cf12e9169310ddab4eca2ca01e87fde9869 +Subproject commit 7613c655000949180e6e838ce96c0f168b8d4cc7 From a8febbe96f0803b033205626d153c42b77003c36 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Sun, 28 Jun 2026 14:31:32 +0300 Subject: [PATCH 18/31] restruct: 9e --- src/app.rs | 22 ++++++++++++---------- src/app/bind.rs | 2 +- src/tek.rs | 17 ++++++----------- tengri | 2 +- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/app.rs b/src/app.rs index 2c88280a..c198381d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -86,16 +86,17 @@ impl App { Arc::new(RwLock::new(App::new(jack, project, config, ":menu"))) } - pub fn new_shared_run ( - exit: &Exit, jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef + pub fn new_shared_run ( + exit: &Exit, + output: W, + jack: &Jack<'static>, + project: Arrangement, + config: Config, + mode: impl AsRef ) -> Usually<(Arc>, Task, Task)> { let state = Self::new_shared(&jack, project, config, mode); - let keyboard = tui_input( - exit.as_ref(), &state, Duration::from_millis(100) - )?; - let terminal = tui_output( - &mut std::io::stdout(), exit.as_ref(), &state, Duration::from_millis(10) - )?; + let keyboard = tui_input(exit.as_ref(), &state, Duration::from_millis(100))?; + let terminal = tui_output(exit.as_ref(), &state, Duration::from_millis(10), output)?; Ok((state, keyboard, terminal)) } @@ -529,7 +530,7 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua Ok(to.area().into()) }))).draw(to), - Some(":browse/title") => w_full(origin_w(field_v(ItemColor::default(), + Some(":browse/title") => w_full(origin_w(field_v(ItemTheme::default(), match state.dialog.browser_target().unwrap() { BrowseTarget::SaveProject => "Save project:", BrowseTarget::LoadProject => "Load project:", @@ -537,7 +538,8 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua BrowseTarget::ExportSample(_) => "Export sample:", BrowseTarget::ImportClip(_) => "Import clip:", BrowseTarget::ExportClip(_) => "Export clip:", - }, w_shrink(3, h_exact(1, fg(g(96), x_repeat("🭻"))))))).draw(to), + }, h_exact(1, fg(g(96), x_repeat("🭻"))) + ))).draw(to), Some(":device") => { let selected = state.dialog.device_kind().unwrap(); diff --git a/src/app/bind.rs b/src/app/bind.rs index be5a5705..25f60a07 100644 --- a/src/app/bind.rs +++ b/src/app/bind.rs @@ -61,7 +61,7 @@ impl Bind> { // TODO Ok(()) } else if let Ok(Some(_word)) = item.expr().head().word() { - if let Some(key) = TuiEvent::named(item.expr()?.head()?)? { + if let Some(key) = TuiKey::from_dsl(item.expr()?.head()?)? { map.add(key, Binding { commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), condition: None, diff --git a/src/tek.rs b/src/tek.rs index 6e83d5e0..7efe2fe4 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -7,7 +7,7 @@ pub(crate) const HEADER: &'static str = r#" ~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ - █ █▀ █▀▀▄ ~ v0.4.0, 2026 winter (or is it) ~ + █ █▀ █▀▀▄ ~ v0.4.0, 2026 heatwave edition ~ ~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; #[cfg(feature = "cli")] use clap::{self, Parser, Subcommand}; @@ -90,21 +90,16 @@ pub fn main () -> Usually<()> { Config::watch(|config|{ Exit::run(|exit|{ let state = Jack::new_run("tek", move|jack|{ - Ok(App::new(&jack, Arrangement::new( - &jack, - None, - Clock::new(&jack, Some(51.))?, - vec![], - vec![], - vec![], - vec![], - ), config, ":menu")) + let clock = Clock::new(&jack, Some(51.))?; + let project = Arrangement::new(&jack, None, clock, vec![], vec![], vec![], vec![]); + let app = App::new(&jack, project, config, ":menu"); + Ok(app) })?; let keyboard = tui_input( exit.as_ref(), &state, Duration::from_millis(100) )?; let terminal = tui_output( - &mut std::io::stdout(), exit.as_ref(), &state, Duration::from_millis(10) + exit.as_ref(), &state, Duration::from_millis(10), std::io::stdout(), )?; Ok(()) }) diff --git a/tengri b/tengri index 7613c655..dd7091bd 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 7613c655000949180e6e838ce96c0f168b8d4cc7 +Subproject commit dd7091bda11504a16ba8de0c0b719359df37b5ef From 95ba0d6febd4825b53d58ee3901f0e5407bbb0cc Mon Sep 17 00:00:00 2001 From: i do not exist Date: Mon, 29 Jun 2026 19:57:44 +0300 Subject: [PATCH 19/31] restruct: 8e --- src/app/bind.rs | 6 +++--- tengri | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/bind.rs b/src/app/bind.rs index 25f60a07..a29b89d7 100644 --- a/src/app/bind.rs +++ b/src/app/bind.rs @@ -56,13 +56,13 @@ pub(crate) fn load_bind (binds: &Binds, name: &impl AsRef, body: &impl Lang impl Bind> { pub fn load (lang: &impl Language) -> Usually { - let mut map = Bind::new(); + let mut map = Self::new(); lang.each(|item|if item.expr().head() == Ok(Some("see")) { // TODO Ok(()) } else if let Ok(Some(_word)) = item.expr().head().word() { - if let Some(key) = TuiKey::from_dsl(item.expr()?.head()?)? { - map.add(key, Binding { + if let Some(event) = TuiKey::from_dsl(item.expr()?.head()?)?.to_crossterm() { + map.add(TuiEvent(event), Binding { commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), condition: None, description: None, diff --git a/tengri b/tengri index dd7091bd..53c9438f 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit dd7091bda11504a16ba8de0c0b719359df37b5ef +Subproject commit 53c9438f7d9990d60adc1a25cccfdd2971a07500 From 1e9a491b0c58ca6d8d9f1dfa18bbd73a80407c37 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Mon, 29 Jun 2026 20:38:23 +0300 Subject: [PATCH 20/31] restruct: 6e --- src/app/cli.rs | 37 ++++++++++++++++++++++--------------- src/tek.rs | 11 ++++++----- tengri | 2 +- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/app/cli.rs b/src/app/cli.rs index 78dea7a7..8c03d5b7 100644 --- a/src/app/cli.rs +++ b/src/app/cli.rs @@ -78,13 +78,13 @@ use crate::*; #[cfg(feature = "cli")] impl Cli { pub fn run (&self) -> Usually<()> { - self.action.run(&Config::init_new(None)?) + self.action.run(Config::init_new(None)?) } } #[cfg(feature = "cli")] impl Action { - fn run (&self, config: &Config) -> Usually<()> { + fn run (&self, config: Config) -> Usually<()> { use Action::*; match self { Version => tek_show_version(), @@ -109,25 +109,32 @@ impl Action { //return Ok(()) //} // Initialize the app state - let app = tek(&jack, proj, config, ":menu"); + let app = App::new_shared(&jack, proj, config, ":menu"); //if matches!(self, Action::Headless) { //// TODO: Headless mode (daemon + client over IPC, then over network...) //println!("todo headless"); //return Ok(()) //} + let (_keyboard, _terminal) = Exit::run(|exited|tui_io( + exited.as_ref(), + &app, + Duration::from_millis(100), + Duration::from_millis(10), + std::io::stdout() + ))?; // Run the [Tui] and [Jack] threads with the [App] state. - Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{ - // Between jack init and app's first cycle: - jack.sync_lead(*sync_lead, |mut state|{ - let clock = app.clock(); - clock.playhead.update_from_sample(state.position.frame() as f64); - state.position.bbt = Some(clock.bbt()); - state.position - })?; - jack.sync_follow(*sync_follow)?; - // FIXME: They don't work properly. - Ok(app) - })?)?; + //Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{ + //// Between jack init and app's first cycle: + //jack.sync_lead(*sync_lead, |mut state|{ + //let clock = app.clock(); + //clock.playhead.update_from_sample(state.position.frame() as f64); + //state.position.bbt = Some(clock.bbt()); + //state.position + //})?; + //jack.sync_follow(*sync_follow)?; + //// FIXME: They don't work properly. + //Ok(app) + //})?)?; } } Ok(()) diff --git a/src/tek.rs b/src/tek.rs index 7efe2fe4..43eed2e1 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -95,11 +95,12 @@ pub fn main () -> Usually<()> { let app = App::new(&jack, project, config, ":menu"); Ok(app) })?; - let keyboard = tui_input( - exit.as_ref(), &state, Duration::from_millis(100) - )?; - let terminal = tui_output( - exit.as_ref(), &state, Duration::from_millis(10), std::io::stdout(), + let (keyboard, terminal) = tui_io( + exit.as_ref(), + &state, + Duration::from_millis(100), + Duration::from_millis(10), + std::io::stdout(), )?; Ok(()) }) diff --git a/tengri b/tengri index 53c9438f..4e6c62a7 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 53c9438f7d9990d60adc1a25cccfdd2971a07500 +Subproject commit 4e6c62a7f1edd9e173065afbb8e9cf897baa2ead From 383e534692fac04a4573942be57061d9da62d89c Mon Sep 17 00:00:00 2001 From: i do not exist Date: Tue, 30 Jun 2026 15:38:56 +0300 Subject: [PATCH 21/31] restruct: 3e --- src/app.rs | 36 +++++++++++++++++++----------------- tengri | 2 +- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/app.rs b/src/app.rs index c198381d..58057bf4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -402,6 +402,8 @@ namespace!(App: Option { symbol = |app| { ":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) }; }); +namespace!(App: Option { symbol = |app| {}; }); + namespace!(App: Option { symbol = |app| { ":selected/scene" => app.selection().scene(), ":selected/track" => app.selection().track(), @@ -428,24 +430,26 @@ impl<'a> Namespace<'a, AppCommand> for App { }); } -impl Interpret for App { - fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { - app_interpret_expr(self, to, lang) +impl Interpret> for App { + fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually> { + tek_expr(self, to, lang) } - fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { - app_interpret_word(self, to, lang) + fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually> { + tek_word(self, to, lang) } } -fn app_interpret_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Usually<()> { - if eval_view(state, to, lang)? || eval_view_tui(state, to, lang)? { - Ok(()) +fn tek_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Usually> { + if let Some(area) = eval_view(state, to, lang)? { + Ok(area) + } else if let Some(area) = eval_view_tui(state, to, lang)? { + Ok(area) } else { Err(format!("App::interpret_expr: unexpected: {lang:?}").into()) } } -fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually<()> { +fn tek_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually> { let mut frags = dsl.src()?.unwrap().split("/"); match frags.next() { @@ -482,12 +486,11 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua let selected = selected; Some(wh_full(thunk(move|to: &mut Tui|{ for (index, MenuItem(item, _)) in items.0.iter().enumerate() { - to.place(&y_push((2 * index) as u16, - fg_bg( - if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, - if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, - h_exact(2, origin_n(w_full(item))) - ))); + y_push((2 * index) as u16,fg_bg( + if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, + if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, + h_exact(2, origin_n(w_full(item))) + )).draw(to); } Ok(to.area().into()) }))) @@ -566,8 +569,7 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua }, _ => unreachable!() - }; - Ok(()) + } } impl HasTrackScroll for App { diff --git a/tengri b/tengri index 4e6c62a7..347151f7 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 4e6c62a7f1edd9e173065afbb8e9cf897baa2ead +Subproject commit 347151f7fcc2f0500de2c293d5d83b79f1e833d9 From c53c5bf377142275ada4ee8941b57ece204b50b7 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Wed, 1 Jul 2026 16:35:20 +0300 Subject: [PATCH 22/31] retest: 7e, 1f --- src/app.rs | 16 ++++------------ src/app/bind.rs | 2 +- src/app/cli.rs | 5 +++-- src/app/view.rs | 27 +++++++++++++++++++++------ 4 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/app.rs b/src/app.rs index 58057bf4..e362debd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -59,11 +59,11 @@ impl App { /// Create a new application instance from a backend, project, config, and mode /// /// ``` - /// let jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack"); + /// let jack = tek::tengri::sing::Jack::new(&"test_tek").expect("failed to connect to jack"); /// let proj = tek::Arrangement::default(); /// let mut conf = tek::Config::default(); /// conf.add("(mode hello)"); - /// let tek = tek::tek(&jack, proj, conf, "hello"); + /// let tek = tek::App::new(&jack, proj, conf, "hello"); /// ``` pub fn new ( jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef @@ -523,15 +523,7 @@ fn tek_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually h_exact(Some(6), w_min(Some(30), thunk(|to: &mut Tui|{ - let f = Rgb(224, 192, 128); - for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { - let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; - y_push((2 * index) as u16, - &h_exact(2, w_full(bg(b, origin_w(fg(f, name)))))).draw(to); - } - Ok(to.area().into()) - }))).draw(to), + Some(":sessions") => view_sessions().draw(to), Some(":browse/title") => w_full(origin_w(field_v(ItemTheme::default(), match state.dialog.browser_target().unwrap() { @@ -562,7 +554,7 @@ fn tek_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually, body: &impl Lang /// /// ``` /// let lang = "(@x (nop)) (@y (nop) (nop))"; -/// let bind = tek::Bind::>::load(&lang).unwrap(); +/// let bind = tek::Bind::>::load(&lang).unwrap(); /// assert_eq!(bind.query(&'x'.into()).map(|x|x.len()), Some(1)); /// //assert_eq!(bind.query(&'y'.into()).map(|x|x.len()), Some(2)); /// ``` diff --git a/src/app/cli.rs b/src/app/cli.rs index 8c03d5b7..28c75e27 100644 --- a/src/app/cli.rs +++ b/src/app/cli.rs @@ -98,7 +98,7 @@ impl Action { } => { let name = name.as_ref().map_or("tek", |x|x.as_str()); let jack = Jack::new(&name)?; - let proj = tek_project_new( + let mut proj = tek_project_new( &jack, Clock::new(&jack, *bpm)?, &lf, <, &rf, &rt, &mf, &mt, &mfr, &mtr )?; proj.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; @@ -135,7 +135,8 @@ impl Action { //// FIXME: They don't work properly. //Ok(app) //})?)?; - } + }, + _ => todo!() } Ok(()) } diff --git a/src/app/view.rs b/src/app/view.rs index b11832a8..ca56d703 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -23,9 +23,9 @@ pub fn view_logo () -> impl Draw { } /// ``` -/// let x = std::sync::Arc::>::default(); -/// let _ = tek::view_transport(true, x.clone(), x.clone(), x.clone()); -/// let _ = tek::view_transport(false, x.clone(), x.clone(), x.clone()); +/// let x = ""; +/// let _ = tek::view_transport(true, x.as_ref(), x.as_ref(), x.as_ref()); +/// let _ = tek::view_transport(false, x.as_ref(), x.as_ref(), x.as_ref()); /// ``` pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw { let theme = ItemTheme::G[96]; @@ -40,9 +40,9 @@ pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Dr } /// ``` -/// let x = std::sync::Arc::>::default(); -/// let _ = tek::view_status(None, x.clone(), x.clone(), x.clone()); -/// let _ = tek::view_status(Some("".into()), x.clone(), x.clone(), x.clone()); +/// let x = ""; +/// let _ = tek::view_status(None, x.as_ref(), x.as_ref(), x.as_ref()); +/// let _ = tek::view_status(Some("".into()), x.as_ref(), x.as_ref(), x.as_ref()); /// ``` pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw { let theme = ItemTheme::G[96]; @@ -569,3 +569,18 @@ pub fn field_h (theme: ItemTheme, head: impl Draw, body: impl Dra pub fn field_v (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { } + +pub fn view_sessions () -> impl Draw { + let h = 6; + let w = Some(30); + let f = Rgb(224, 192, 128); + h_exact(h, w_min(w, thunk(move |to: &mut Tui|{ + for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { + let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; + let y = (2 * index) as u16; + let h = 2; + y_push(y, &h_exact(h, w_full(bg(b, origin_w(fg(f, name)))))).draw(to); + } + Ok(to.area().into()) + }))) +} From 89f8d073ebb58243bb166a36b41efb8d7c670d3b Mon Sep 17 00:00:00 2001 From: i do not exist Date: Wed, 1 Jul 2026 19:21:29 +0300 Subject: [PATCH 23/31] fix(test): use reexported macros --- src/device/editor.rs | 2 +- tengri | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/device/editor.rs b/src/device/editor.rs index 822ac099..a7455a22 100644 --- a/src/device/editor.rs +++ b/src/device/editor.rs @@ -139,7 +139,7 @@ impl MidiEditor { } /// ``` -/// use tek::{*, tengri::*}; +/// use tek::{*, tengri::{*, lang::*}}; /// /// struct Test(Option); /// impl_as_ref_opt!(MidiEditor: |self: Test|self.0.as_ref()); diff --git a/tengri b/tengri index 347151f7..2ab871d1 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 347151f7fcc2f0500de2c293d5d83b79f1e833d9 +Subproject commit 2ab871d10b26b40d129d62f68ecbd9e960aa4c86 From 3b1c31c78d917a6695b9c75c92a7145019ec4e35 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Thu, 2 Jul 2026 07:29:31 +0300 Subject: [PATCH 24/31] fix: runs, somehow --- src/app.rs | 207 +++++++++--------------------------------------- src/app/draw.rs | 139 ++++++++++++++++++++++++++++++++ src/app/view.rs | 24 ++++++ src/tek.rs | 26 ++---- tengri | 2 +- 5 files changed, 209 insertions(+), 189 deletions(-) create mode 100644 src/app/draw.rs diff --git a/src/app.rs b/src/app.rs index e362debd..ae9bcf87 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,6 +4,7 @@ pub mod bind; pub use self::bind::*; pub mod cli; pub use self::cli::*; pub mod config; pub use self::config::*; pub mod view; pub use self::view::*; +pub mod draw; pub use self::draw::*; pub mod mode; pub use self::mode::*; /// Total application state. @@ -278,42 +279,6 @@ impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } -//impl_handle!(TuiIn: |self: App, input|{ - //let commands = tek_collect_commands(self, input)?; - //let history = tek_execute_commands(self, commands)?; - //self.history.extend(history.into_iter()); - //Ok(None) -//}); - -//fn tek_collect_commands (app: &App, input: &TuiIn) -> Usually> { - //let mut commands = vec![]; - //for id in app.mode.keys.iter() { - //if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref()) - //&& let Some(bindings) = event_map.query(input.event()) { - //for binding in bindings { - //for command in binding.commands.iter() { - //if let Some(command) = app.namespace(command)? as Option { - //commands.push(command) - //} - //} - //} - //} - //} - //Ok(commands) -//} - -//fn tek_execute_commands ( - //app: &mut App, commands: Vec -//) -> Usually)>> { - //let mut history = vec![]; - //for command in commands.into_iter() { - //let result = command.act(app); - //match result { Err(err) => { history.push((command, None)); return Err(err) } - //Ok(undo) => { history.push((command, undo)); } }; - //} - //Ok(history) -//} - def_command!(AppCommand: |app: App| { Nop => Ok(None), Cancel => todo!(), // TODO delegate: @@ -430,140 +395,6 @@ impl<'a> Namespace<'a, AppCommand> for App { }); } -impl Interpret> for App { - fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually> { - tek_expr(self, to, lang) - } - fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually> { - tek_word(self, to, lang) - } -} - -fn tek_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Usually> { - if let Some(area) = eval_view(state, to, lang)? { - Ok(area) - } else if let Some(area) = eval_view_tui(state, to, lang)? { - Ok(area) - } else { - Err(format!("App::interpret_expr: unexpected: {lang:?}").into()) - } -} - -fn tek_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually> { - let mut frags = dsl.src()?.unwrap().split("/"); - match frags.next() { - - Some(":logo") => view_logo().draw(to), - - Some(":status") => h_exact(1, "TODO: Status Bar").draw(to), - - Some(":meters") => match frags.next() { - Some("input") => bg(Rgb(30, 30, 30), h_full(origin_s("Input Meters"))).draw(to), - Some("output") => bg(Rgb(30, 30, 30), h_full(origin_s("Output Meters"))).draw(to), - _ => panic!() - }, - - Some(":tracks") => match frags.next() { - None => "TODO tracks".draw(to), - Some("names") => state.project.view_track_names(state.color.clone()).draw(to),//bg(Rgb(40, 40, 40), w_full(origin_w("Track Names")))), - Some("inputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Inputs"))).draw(to), - Some("devices") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Devices"))).draw(to), - Some("outputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Outputs"))).draw(to), - _ => panic!() - }, - - Some(":scenes") => match frags.next() { - None => "TODO scenes".draw(to), - Some(":scenes/names") => "TODO Scene Names".draw(to), - _ => panic!() - }, - - Some(":editor") => "TODO Editor".draw(to), - - Some(":dialog") => match frags.next() { - Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog { - let items = items.clone(); - let selected = selected; - Some(wh_full(thunk(move|to: &mut Tui|{ - for (index, MenuItem(item, _)) in items.0.iter().enumerate() { - y_push((2 * index) as u16,fg_bg( - if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, - if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, - h_exact(2, origin_n(w_full(item))) - )).draw(to); - } - Ok(to.area().into()) - }))) - } else { - None - }.draw(to), - _ => unimplemented!("App::interpret_word: {dsl:?} ({frags:?})"), - }, - - Some(":templates") => { - let modes = state.config.modes.clone(); - let height = (modes.read().unwrap().len() * 2) as u16; - h_exact(height, w_min(Some(30), thunk(move |to: &mut Tui|{ - for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { - let b = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; - let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); - let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); - let fg1 = Rgb(224, 192, 128); - let fg2 = Rgb(224, 128, 32); - let field_name = w_full(origin_w(fg(fg1, name))); - let field_id = w_full(origin_e(fg(fg2, id))); - let field_info = w_full(origin_w(info)); - y_push((2 * index) as u16, - h_exact(2, w_full(bg(b, south( - above(field_name, field_id), - field_info - ))))).draw(to); - } - Ok(to.area().into()) - }))) - }.draw(to), - - Some(":sessions") => view_sessions().draw(to), - - Some(":browse/title") => w_full(origin_w(field_v(ItemTheme::default(), - match state.dialog.browser_target().unwrap() { - BrowseTarget::SaveProject => "Save project:", - BrowseTarget::LoadProject => "Load project:", - BrowseTarget::ImportSample(_) => "Import sample:", - BrowseTarget::ExportSample(_) => "Export sample:", - BrowseTarget::ImportClip(_) => "Import clip:", - BrowseTarget::ExportClip(_) => "Export clip:", - }, h_exact(1, fg(g(96), x_repeat("🭻"))) - ))).draw(to), - - Some(":device") => { - let selected = state.dialog.device_kind().unwrap(); - south(bold(true, "Add device"), iter_south( - move||device_kinds().iter(), - move|_label: &&'static str, i|{ - let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; - let l = if i == selected { "[ " } else { " " }; - let r = if i == selected { " ]" } else { " " }; - w_full(bg(b, east(l, west(r, "FIXME device name")))) })).draw(to) - }, - - Some(":debug") => h_exact(1, format!("[{:?}]", to.area())).draw(to), - - Some(_) => { - let views = state.config.views.read().unwrap(); - if let Some(dsl) = views.get(dsl.src()?.unwrap()) { - let dsl = dsl.clone(); - std::mem::drop(views); - state.interpret(to, &dsl) - } else { - unimplemented!("{dsl:?}"); - } - }, - - _ => unreachable!() - } -} - impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() @@ -587,3 +418,39 @@ impl ScenesView for App { (self.size.h() as u16).saturating_sub(20) } } + +//impl_handle!(TuiIn: |self: App, input|{ + //let commands = tek_collect_commands(self, input)?; + //let history = tek_execute_commands(self, commands)?; + //self.history.extend(history.into_iter()); + //Ok(None) +//}); + +//fn tek_collect_commands (app: &App, input: &TuiIn) -> Usually> { + //let mut commands = vec![]; + //for id in app.mode.keys.iter() { + //if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref()) + //&& let Some(bindings) = event_map.query(input.event()) { + //for binding in bindings { + //for command in binding.commands.iter() { + //if let Some(command) = app.namespace(command)? as Option { + //commands.push(command) + //} + //} + //} + //} + //} + //Ok(commands) +//} + +//fn tek_execute_commands ( + //app: &mut App, commands: Vec +//) -> Usually)>> { + //let mut history = vec![]; + //for command in commands.into_iter() { + //let result = command.act(app); + //match result { Err(err) => { history.push((command, None)); return Err(err) } + //Ok(undo) => { history.push((command, undo)); } }; + //} + //Ok(history) +//} diff --git a/src/app/draw.rs b/src/app/draw.rs new file mode 100644 index 00000000..dbe9d2b5 --- /dev/null +++ b/src/app/draw.rs @@ -0,0 +1,139 @@ +use crate::*; + +impl Interpret> for App { + fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) + -> Usually> + { + tek_draw_expr(self, to, lang) + } + fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) + -> Usually> + { + tek_draw_word(self, to, lang) + } +} + +fn tek_draw_expr (state: &App, to: &mut Tui, lang: &impl Expression) + -> Usually> +{ + if let Some(area) = eval_view(state, to, lang)? { + Ok(area) + } else if let Some(area) = eval_view_tui(state, to, lang)? { + Ok(area) + } else { + Err(format!("App::interpret_expr: unexpected: {lang:?}").into()) + } +} + +fn tek_draw_word (state: &App, to: &mut Tui, dsl: &impl Expression) + -> Usually> +{ + let mut frags = dsl.src()?.unwrap().split("/"); + match frags.next() { + Some(":logo") => view_logo().draw(to), + Some(":meters") => draw_meter_section(to, frags), + Some(":tracks") => draw_tracks(to, frags, state), + Some(":scenes") => draw_scenes(to, frags), + Some(":dialog") => draw_dialog(to, frags, state, dsl), + Some(":templates") => draw_templates(to, frags, state), + Some(":sessions") => view_sessions().draw(to), + Some(":browse/title") => view_browse_title(state).draw(to), + Some(":device") => view_device(state).draw(to), + Some(":status") => h_exact(1, "TODO: Status Bar").draw(to), + Some(":editor") => "TODO Editor".draw(to), + Some(":debug") => h_exact(1, format!("[{:?}]", to.area())).draw(to), + Some(_) => { + let views = state.config.views.read().unwrap(); + if let Some(dsl) = views.get(dsl.src()?.unwrap()) { + let dsl = dsl.clone(); + std::mem::drop(views); + state.interpret(to, &dsl) + } else { + unimplemented!("{dsl:?}"); + } + }, + _ => unreachable!() + } +} + +pub fn draw_meter_section (to: &mut Tui, mut frags: std::str::Split<&str>) + -> Usually> +{ + match frags.next() { + Some("input") => bg(Rgb(30, 30, 30), h_full(origin_s("Input Meters"))).draw(to), + Some("output") => bg(Rgb(30, 30, 30), h_full(origin_s("Output Meters"))).draw(to), + _ => panic!() + } +} + +pub fn draw_tracks (to: &mut Tui, mut frags: std::str::Split<&str>, state: &App) + -> Usually> +{ + match frags.next() { + None => "TODO tracks".draw(to), + Some("names") => state.project.view_track_names(state.color.clone()).draw(to),//bg(Rgb(40, 40, 40), w_full(origin_w("Track Names")))), + Some("inputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Inputs"))).draw(to), + Some("devices") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Devices"))).draw(to), + Some("outputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Outputs"))).draw(to), + _ => panic!() + } +} + +pub fn draw_scenes (to: &mut Tui, mut frags: std::str::Split<&str>) + -> Usually> +{ + match frags.next() { + None => "TODO Scenes".draw(to), + Some(":scenes/names") => "TODO Scene Names".draw(to), + _ => panic!() + } +} + +pub fn draw_dialog ( + to: &mut Tui, mut frags: std::str::Split<&str>, state: &App, dsl: &impl Expression +) -> Usually> { + match frags.next() { + Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog { + let items = items.clone(); + let selected = selected; + Some(wh_full(thunk(move|to: &mut Tui|{ + for (index, MenuItem(item, _)) in items.0.iter().enumerate() { + y_push((2 * index) as u16,fg_bg( + if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, + if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, + h_exact(2, origin_n(w_full(item))) + )).draw(to); + } + Ok(to.area().into()) + }))) + } else { + None + }.draw(to), + _ => unimplemented!("App::interpret_word: {dsl:?} ({frags:?})"), + } +} + +pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App) + -> Usually> +{ + let modes = state.config.modes.clone(); + let height = (modes.read().unwrap().len() * 2) as u16; + h_exact(height, w_min(Some(30), thunk(move |to: &mut Tui|{ + for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { + let b = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; + let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); + let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); + let fg1 = Rgb(224, 192, 128); + let fg2 = Rgb(224, 128, 32); + let field_name = w_full(origin_w(fg(fg1, name))); + let field_id = w_full(origin_e(fg(fg2, id))); + let field_info = w_full(origin_w(info)); + y_push((2 * index) as u16, + h_exact(2, w_full(bg(b, south( + above(field_name, field_id), + field_info + ))))).draw(to); + } + Ok(to.area().into()) + }))).draw(to) +} diff --git a/src/app/view.rs b/src/app/view.rs index ca56d703..6b2690bb 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -584,3 +584,27 @@ pub fn view_sessions () -> impl Draw { Ok(to.area().into()) }))) } + +pub fn view_browse_title (state: &App) -> impl Draw { + w_full(origin_w(field_v(ItemTheme::default(), + match state.dialog.browser_target().unwrap() { + BrowseTarget::SaveProject => "Save project:", + BrowseTarget::LoadProject => "Load project:", + BrowseTarget::ImportSample(_) => "Import sample:", + BrowseTarget::ExportSample(_) => "Export sample:", + BrowseTarget::ImportClip(_) => "Import clip:", + BrowseTarget::ExportClip(_) => "Export clip:", + }, h_exact(1, fg(g(96), x_repeat("🭻"))) + ))) +} + +pub fn view_device (state: &App) -> impl Draw { + let selected = state.dialog.device_kind().unwrap(); + south(bold(true, "Add device"), iter_south( + move||device_kinds().iter(), + move|_label: &&'static str, i|{ + let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; + let l = if i == selected { "[ " } else { " " }; + let r = if i == selected { " ]" } else { " " }; + w_full(bg(b, east(l, west(r, "FIXME device name")))) })) +} diff --git a/src/tek.rs b/src/tek.rs index 43eed2e1..53336e9c 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -87,25 +87,15 @@ pub(crate) use tengri::{ /// Command-line entrypoint. #[cfg(feature = "cli")] pub fn main () -> Usually<()> { + tui_setup_panic(); Config::watch(|config|{ - Exit::run(|exit|{ - let state = Jack::new_run("tek", move|jack|{ - let clock = Clock::new(&jack, Some(51.))?; - let project = Arrangement::new(&jack, None, clock, vec![], vec![], vec![], vec![]); - let app = App::new(&jack, project, config, ":menu"); - Ok(app) - })?; - let (keyboard, terminal) = tui_io( - exit.as_ref(), - &state, - Duration::from_millis(100), - Duration::from_millis(10), - std::io::stdout(), - )?; - Ok(()) - }) - })?; - Ok(()) + tui_run_main(Jack::new_run("tek", move|jack|{ + let clock = Clock::new(&jack, Some(51.))?; + let project = Arrangement::new(&jack, None, clock, vec![], vec![], vec![], vec![]); + let app = App::new(&jack, project, config, ":menu"); + Ok(app) + })?) + }).map(|_|()) } tui_keys!(|self: App, input| { diff --git a/tengri b/tengri index 2ab871d1..90ebae0f 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 2ab871d10b26b40d129d62f68ecbd9e960aa4c86 +Subproject commit 90ebae0f26fc4524659704df274d0aec815dba2e From 51faa82d7462b378a67a9497a7890eb3a1803e52 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Thu, 2 Jul 2026 07:55:23 +0300 Subject: [PATCH 25/31] refactor: move some things with their appropriate verticals --- src/app.rs | 185 +++++++++++--------------------------------- src/app/audio.rs | 2 + src/app/bind.rs | 65 ++++++++++++++++ src/app/cli.rs | 2 +- src/app/draw.rs | 21 +++++ src/app/size.rs | 57 ++++++++++++++ src/app/view.rs | 4 + src/device/audio.rs | 0 src/tek.rs | 79 ------------------- 9 files changed, 197 insertions(+), 218 deletions(-) create mode 100644 src/app/size.rs delete mode 100644 src/device/audio.rs diff --git a/src/app.rs b/src/app.rs index ae9bcf87..77158692 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,11 +1,12 @@ use crate::*; -pub mod audio; pub use self::audio::*; +pub mod audio; #[allow(unused)] pub use self::audio::*; pub mod bind; pub use self::bind::*; pub mod cli; pub use self::cli::*; pub mod config; pub use self::config::*; pub mod view; pub use self::view::*; pub mod draw; pub use self::draw::*; pub mod mode; pub use self::mode::*; +pub mod size; pub use self::size::*; /// Total application state. /// @@ -81,26 +82,6 @@ impl App { } } - pub fn new_shared ( - jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef - ) -> Arc> { - Arc::new(RwLock::new(App::new(jack, project, config, ":menu"))) - } - - pub fn new_shared_run ( - exit: &Exit, - output: W, - jack: &Jack<'static>, - project: Arrangement, - config: Config, - mode: impl AsRef - ) -> Usually<(Arc>, Task, Task)> { - let state = Self::new_shared(&jack, project, config, mode); - let keyboard = tui_input(exit.as_ref(), &state, Duration::from_millis(100))?; - let terminal = tui_output(exit.as_ref(), &state, Duration::from_millis(10), output)?; - Ok((state, keyboard, terminal)) - } - pub fn confirm (&mut self) -> Perhaps { Ok(match &self.dialog { Dialog::Menu(index, items) => { @@ -250,65 +231,24 @@ impl App { } } -/// The [Draw] implementation for [App] handles the loaded view, -/// which is defined in terms of [dizzle] DSL. -/// -/// If there is an error, the error is displayed. FIXME: overlay it -/// Then, every top-level form of the DSL description is rendered. -impl Draw for App { - fn draw (self, to: &mut Tui) -> Usually> { - let xywh = to.area().into(); - if let Some(e) = self.error.read().unwrap().as_ref() { - to.show(xywh, e.as_ref()); - } - for (index, dsl) in self.mode.view.iter().enumerate() { - if let Err(e) = self.interpret(to, dsl) { - *self.error.write().unwrap() = Some(format!("view #{index}: {e}").into()); - break; - } - } - Ok(xywh) - } -} - -impl HasClipsSize for App { - fn clips_size (&self) -> &Size { &self.project.size_inner } -} - -impl HasJack<'static> for App { - fn jack (&self) -> &Jack<'static> { &self.jack } -} - -def_command!(AppCommand: |app: App| { - Nop => Ok(None), - Cancel => todo!(), // TODO delegate: - Confirm => app.confirm(), - Inc { axis: ControlAxis } => app.inc(axis), - Dec { axis: ControlAxis } => app.dec(axis), - SetDialog { dialog: Dialog } => { - swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) - }, -}); - -impl_default!(AppCommand: Self::Nop); - impl_has!(Clock: |self: App|self.project.clock); impl_has!(Vec: |self: App|self.project.midi_ins); impl_has!(Vec: |self: App|self.project.midi_outs); impl_has!(Dialog: |self: App|self.dialog); impl_has!(Jack<'static>: |self: App|self.jack); -impl_has!(Size: |self: App|self.size); impl_has!(Pool: |self: App|self.pool); impl_has!(Selection: |self: App|self.project.selection); - impl_as_ref!(Vec: |self: App|self.project.as_ref()); impl_as_mut!(Vec: |self: App|self.project.as_mut()); - impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); - impl_has_clips!( |self: App|self.pool.clips); +primitive!(u8: try_to_u8); +primitive!(u16: try_to_u16); +primitive!(usize: try_to_usize); +primitive!(isize: try_to_isize); + namespace!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); }); @@ -374,83 +314,52 @@ namespace!(App: Option { symbol = |app| { ":selected/track" => app.selection().track(), }; }); -namespace!(App: Option>> { - symbol = |app| { - ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { - app.scenes()[*scene].clips[*track].clone() - } else { - None - } - }; -}); +namespace!(App: Option>> { symbol = |app| { + ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { + app.scenes()[*scene].clips[*track].clone() + } else { + None + } +}; }); -impl<'a> Namespace<'a, AppCommand> for App { - symbols!('a |app| -> AppCommand { - "x/inc" => AppCommand::Inc { axis: ControlAxis::X }, - "x/dec" => AppCommand::Dec { axis: ControlAxis::X }, - "y/inc" => AppCommand::Inc { axis: ControlAxis::Y }, - "y/dec" => AppCommand::Dec { axis: ControlAxis::Y }, - "confirm" => AppCommand::Confirm, - "cancel" => AppCommand::Cancel, - }); -} - -impl HasTrackScroll for App { - fn track_scroll (&self) -> usize { - self.project.track_scroll() +pub fn swap_value ( + target: &mut T, value: &T, returned: impl Fn(T)->U +) -> Perhaps { + if *target == *value { + Ok(None) + } else { + let mut value = value.clone(); + std::mem::swap(target, &mut value); + Ok(Some(returned(value))) } } -impl HasSceneScroll for App { - fn scene_scroll (&self) -> usize { - self.project.scene_scroll() +pub fn toggle_bool ( + target: &mut bool, value: &Option, returned: impl Fn(Option)->U +) -> Perhaps { + let mut value = value.unwrap_or(!*target); + if value == *target { + Ok(None) + } else { + std::mem::swap(target, &mut value); + Ok(Some(returned(Some(value)))) } } -impl ScenesView for App { - fn w_mid (&self) -> u16 { - (self.size.w() as u16).saturating_sub(self.w_side()) - } - fn w_side (&self) -> u16 { - 20 - } - fn h_scenes (&self) -> u16 { - (self.size.h() as u16).saturating_sub(20) - } +pub fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { + let (mut subdirs, mut files) = std::fs::read_dir(dir)? + .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ + let entry = entry.expect("failed to read drectory entry"); + let meta = entry.metadata().expect("failed to read entry metadata"); + if meta.is_file() { + files.push(entry.file_name()); + } else if meta.is_dir() { + subdirs.push(entry.file_name()); + } + (subdirs, files) + }); + subdirs.sort(); + files.sort(); + Ok((subdirs, files)) } -//impl_handle!(TuiIn: |self: App, input|{ - //let commands = tek_collect_commands(self, input)?; - //let history = tek_execute_commands(self, commands)?; - //self.history.extend(history.into_iter()); - //Ok(None) -//}); - -//fn tek_collect_commands (app: &App, input: &TuiIn) -> Usually> { - //let mut commands = vec![]; - //for id in app.mode.keys.iter() { - //if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref()) - //&& let Some(bindings) = event_map.query(input.event()) { - //for binding in bindings { - //for command in binding.commands.iter() { - //if let Some(command) = app.namespace(command)? as Option { - //commands.push(command) - //} - //} - //} - //} - //} - //Ok(commands) -//} - -//fn tek_execute_commands ( - //app: &mut App, commands: Vec -//) -> Usually)>> { - //let mut history = vec![]; - //for command in commands.into_iter() { - //let result = command.act(app); - //match result { Err(err) => { history.push((command, None)); return Err(err) } - //Ok(undo) => { history.push((command, undo)); } }; - //} - //Ok(history) -//} diff --git a/src/app/audio.rs b/src/app/audio.rs index c5eee668..e224068e 100644 --- a/src/app/audio.rs +++ b/src/app/audio.rs @@ -1,5 +1,7 @@ use crate::*; +impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } + impl_audio!(App: tek_jack_process, tek_jack_event); fn tek_jack_process (state: &mut App, client: &Client, scope: &ProcessScope) -> Control { diff --git a/src/app/bind.rs b/src/app/bind.rs index b8ae47f3..2f683fcc 100644 --- a/src/app/bind.rs +++ b/src/app/bind.rs @@ -1,4 +1,9 @@ use crate::*; + +tui_keys!(|self: App, input| { + todo!() +}); + /// A control axis. /// /// ``` @@ -128,3 +133,63 @@ impl Bind { } impl_debug!(Condition |self, w| { write!(w, "*") }); + +impl_default!(AppCommand: Self::Nop); + +def_command!(AppCommand: |app: App| { + Nop => Ok(None), + Cancel => todo!(), // TODO delegate: + Confirm => app.confirm(), + Inc { axis: ControlAxis } => app.inc(axis), + Dec { axis: ControlAxis } => app.dec(axis), + SetDialog { dialog: Dialog } => { + swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) + }, +}); + +impl<'a> Namespace<'a, AppCommand> for App { + symbols!('a |app| -> AppCommand { + "x/inc" => AppCommand::Inc { axis: ControlAxis::X }, + "x/dec" => AppCommand::Dec { axis: ControlAxis::X }, + "y/inc" => AppCommand::Inc { axis: ControlAxis::Y }, + "y/dec" => AppCommand::Dec { axis: ControlAxis::Y }, + "confirm" => AppCommand::Confirm, + "cancel" => AppCommand::Cancel, + }); +} + +//impl_handle!(TuiIn: |self: App, input|{ + //let commands = tek_collect_commands(self, input)?; + //let history = tek_execute_commands(self, commands)?; + //self.history.extend(history.into_iter()); + //Ok(None) +//}); + +//fn tek_collect_commands (app: &App, input: &TuiIn) -> Usually> { + //let mut commands = vec![]; + //for id in app.mode.keys.iter() { + //if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref()) + //&& let Some(bindings) = event_map.query(input.event()) { + //for binding in bindings { + //for command in binding.commands.iter() { + //if let Some(command) = app.namespace(command)? as Option { + //commands.push(command) + //} + //} + //} + //} + //} + //Ok(commands) +//} + +//fn tek_execute_commands ( + //app: &mut App, commands: Vec +//) -> Usually)>> { + //let mut history = vec![]; + //for command in commands.into_iter() { + //let result = command.act(app); + //match result { Err(err) => { history.push((command, None)); return Err(err) } + //Ok(undo) => { history.push((command, undo)); } }; + //} + //Ok(history) +//} diff --git a/src/app/cli.rs b/src/app/cli.rs index 28c75e27..b0c0c0c7 100644 --- a/src/app/cli.rs +++ b/src/app/cli.rs @@ -109,7 +109,7 @@ impl Action { //return Ok(()) //} // Initialize the app state - let app = App::new_shared(&jack, proj, config, ":menu"); + let app = Arc::new(RwLock::new(App::new(&jack, proj, config, ":menu"))); //if matches!(self, Action::Headless) { //// TODO: Headless mode (daemon + client over IPC, then over network...) //println!("todo headless"); diff --git a/src/app/draw.rs b/src/app/draw.rs index dbe9d2b5..4fcb06d1 100644 --- a/src/app/draw.rs +++ b/src/app/draw.rs @@ -1,5 +1,26 @@ use crate::*; +/// The [Draw] implementation for [App] handles the loaded view, +/// which is defined in terms of [dizzle] DSL. +/// +/// If there is an error, the error is displayed. FIXME: overlay it +/// Then, every top-level form of the DSL description is rendered. +impl Draw for App { + fn draw (self, to: &mut Tui) -> Usually> { + let xywh = to.area().into(); + if let Some(e) = self.error.read().unwrap().as_ref() { + to.show(xywh, e.as_ref())?; + } + for (index, dsl) in self.mode.view.iter().enumerate() { + if let Err(e) = self.interpret(to, dsl) { + *self.error.write().unwrap() = Some(format!("view #{index}: {e}").into()); + break; + } + } + Ok(xywh) + } +} + impl Interpret> for App { fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually> diff --git a/src/app/size.rs b/src/app/size.rs new file mode 100644 index 00000000..843328c8 --- /dev/null +++ b/src/app/size.rs @@ -0,0 +1,57 @@ +use crate::*; + +impl_has!(Size: |self: App|self.size); + +/// Define a type alias for iterators of sized items (columns). +macro_rules! def_sizes_iter { + ($Type:ident => $($Item:ty),+) => { + pub trait $Type<'a> = + Iterator + Send + Sync + 'a; + } +} + +def_sizes_iter!(InputsSizes => MidiInput); +def_sizes_iter!(OutputsSizes => MidiOutput); +def_sizes_iter!(PortsSizes => Arc, [Connect]); +def_sizes_iter!(ScenesSizes => Scene); +def_sizes_iter!(TracksSizes => Track); + +pub(crate) fn track_width (_index: usize, track: &Track) -> u16 { + track.width as u16 +} + +pub trait HasWidth { + const MIN_WIDTH: usize; + /// Increment track width. + fn width_inc (&mut self); + /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. + fn width_dec (&mut self); +} + +impl HasClipsSize for App { + fn clips_size (&self) -> &Size { &self.project.size_inner } +} + +impl HasTrackScroll for App { + fn track_scroll (&self) -> usize { + self.project.track_scroll() + } +} + +impl HasSceneScroll for App { + fn scene_scroll (&self) -> usize { + self.project.scene_scroll() + } +} + +impl ScenesView for App { + fn w_mid (&self) -> u16 { + (self.size.w() as u16).saturating_sub(self.w_side()) + } + fn w_side (&self) -> u16 { + 20 + } + fn h_scenes (&self) -> u16 { + (self.size.h() as u16).saturating_sub(20) + } +} diff --git a/src/app/view.rs b/src/app/view.rs index 6b2690bb..27b9313a 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -1,5 +1,9 @@ use crate::*; +tui_view!(|self: App| { + "" +}); + /// Collection of custom view definitions. pub type Views = Arc, Arc>>>; diff --git a/src/device/audio.rs b/src/device/audio.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/src/tek.rs b/src/tek.rs index 53336e9c..8535b8cc 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -98,87 +98,8 @@ pub fn main () -> Usually<()> { }).map(|_|()) } -tui_keys!(|self: App, input| { - todo!() -}); - -tui_view!(|self: App| { - "" -}); - -pub fn swap_value ( - target: &mut T, value: &T, returned: impl Fn(T)->U -) -> Perhaps { - if *target == *value { - Ok(None) - } else { - let mut value = value.clone(); - std::mem::swap(target, &mut value); - Ok(Some(returned(value))) - } -} - -pub fn toggle_bool ( - target: &mut bool, value: &Option, returned: impl Fn(Option)->U -) -> Perhaps { - let mut value = value.unwrap_or(!*target); - if value == *target { - Ok(None) - } else { - std::mem::swap(target, &mut value); - Ok(Some(returned(Some(value)))) - } -} - //take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref() //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); -fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { - let (mut subdirs, mut files) = std::fs::read_dir(dir)? - .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ - let entry = entry.expect("failed to read drectory entry"); - let meta = entry.metadata().expect("failed to read entry metadata"); - if meta.is_file() { - files.push(entry.file_name()); - } else if meta.is_dir() { - subdirs.push(entry.file_name()); - } - (subdirs, files) - }); - subdirs.sort(); - files.sort(); - Ok((subdirs, files)) -} - -pub(crate) fn track_width (_index: usize, track: &Track) -> u16 { - track.width as u16 -} - -/// Define a type alias for iterators of sized items (columns). -macro_rules! def_sizes_iter { - ($Type:ident => $($Item:ty),+) => { - pub trait $Type<'a> = - Iterator + Send + Sync + 'a; - } -} - -primitive!(u8: try_to_u8); -primitive!(u16: try_to_u16); -primitive!(usize: try_to_usize); -primitive!(isize: try_to_isize); -pub trait HasWidth { - const MIN_WIDTH: usize; - /// Increment track width. - fn width_inc (&mut self); - /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. - fn width_dec (&mut self); -} - pub mod device; pub use self::device::*; pub mod app; pub use self::app::*; - -def_sizes_iter!(InputsSizes => MidiInput); -def_sizes_iter!(OutputsSizes => MidiOutput); -def_sizes_iter!(PortsSizes => Arc, [Connect]); -def_sizes_iter!(ScenesSizes => Scene); -def_sizes_iter!(TracksSizes => Track); From 701a651fbd96cf2ffad00f060a8d3a1b8e475aa5 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Thu, 2 Jul 2026 09:21:47 +0300 Subject: [PATCH 26/31] refactor: Mode, View, Bind --- src/app.rs | 2 - src/app/bind.rs | 86 ++--- src/app/cli.rs | 22 +- src/app/config.rs | 16 +- src/app/{ => config}/mode.rs | 36 +- src/app/config/modes.rs | 27 ++ src/app/config/views.rs | 10 + src/app/draw.rs | 615 ++++++++++++++++++++++++++++++++++- src/app/size.rs | 32 -- src/app/view.rs | 614 ---------------------------------- src/deps.rs | 33 ++ src/device/arrange/clip.rs | 6 +- src/device/arrange/scene.rs | 40 ++- src/device/arrange/track.rs | 40 ++- src/device/clock.rs | 21 ++ src/device/dialog.rs | 2 +- src/device/plugin.rs | 8 + src/device/port.rs | 35 +- src/device/port/register.rs | 27 ++ src/device/sampler.rs | 8 + src/tek.rs | 101 +----- 21 files changed, 901 insertions(+), 880 deletions(-) rename src/app/{ => config}/mode.rs (87%) create mode 100644 src/app/config/modes.rs create mode 100644 src/app/config/views.rs delete mode 100644 src/app/view.rs create mode 100644 src/deps.rs create mode 100644 src/device/port/register.rs diff --git a/src/app.rs b/src/app.rs index 77158692..0be37c36 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,9 +3,7 @@ pub mod audio; #[allow(unused)] pub use self::audio::*; pub mod bind; pub use self::bind::*; pub mod cli; pub use self::cli::*; pub mod config; pub use self::config::*; -pub mod view; pub use self::view::*; pub mod draw; pub use self::draw::*; -pub mod mode; pub use self::mode::*; pub mod size; pub use self::size::*; /// Total application state. diff --git a/src/app/bind.rs b/src/app/bind.rs index 2f683fcc..a1814ace 100644 --- a/src/app/bind.rs +++ b/src/app/bind.rs @@ -1,16 +1,41 @@ use crate::*; tui_keys!(|self: App, input| { - todo!() + let commands = tek_commands_collect(self, input)?; + let results = tek_commands_execute(self, commands)?; + self.history.extend(results.into_iter()); + Ok(()) }); -/// A control axis. -/// -/// ``` -/// let axis = tek::ControlAxis::X; -/// ``` -#[derive(Debug, Copy, Clone)] pub enum ControlAxis { - X, Y, Z, I +fn tek_commands_collect (app: &App, input: &TuiEvent) + -> Usually> +{ + let mut commands = vec![]; + for id in app.mode.keys.iter() { + if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref()) + && let Some(bindings) = event_map.query(input) { + for binding in bindings { + for command in binding.commands.iter() { + if let Some(command) = app.namespace(command)? as Option { + commands.push(command) + } + } + } + } + } + Ok(commands) +} + +fn tek_commands_execute (app: &mut App, commands: Vec) + -> Usually)>> +{ + let mut history = vec![]; + for command in commands.into_iter() { + let result = command.act(app); + match result { Err(err) => { history.push((command, None)); return Err(err) } + Ok(undo) => { history.push((command, undo)); } }; + } + Ok(history) } /// Collection of input bindings. @@ -91,6 +116,7 @@ impl Bind> { impl Default for Bind { fn default () -> Self { Self(Default::default()) } } + impl Default for Binding { fn default () -> Self { Self { @@ -158,38 +184,14 @@ impl<'a> Namespace<'a, AppCommand> for App { }); } -//impl_handle!(TuiIn: |self: App, input|{ - //let commands = tek_collect_commands(self, input)?; - //let history = tek_execute_commands(self, commands)?; - //self.history.extend(history.into_iter()); - //Ok(None) -//}); +/// A control axis. +/// +/// ``` +/// let axis = tek::ControlAxis::X; +/// ``` +#[derive(Debug, Copy, Clone)] pub enum ControlAxis { + X, Y, Z, I +} -//fn tek_collect_commands (app: &App, input: &TuiIn) -> Usually> { - //let mut commands = vec![]; - //for id in app.mode.keys.iter() { - //if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref()) - //&& let Some(bindings) = event_map.query(input.event()) { - //for binding in bindings { - //for command in binding.commands.iter() { - //if let Some(command) = app.namespace(command)? as Option { - //commands.push(command) - //} - //} - //} - //} - //} - //Ok(commands) -//} - -//fn tek_execute_commands ( - //app: &mut App, commands: Vec -//) -> Usually)>> { - //let mut history = vec![]; - //for command in commands.into_iter() { - //let result = command.act(app); - //match result { Err(err) => { history.push((command, None)); return Err(err) } - //Ok(undo) => { history.push((command, undo)); } }; - //} - //Ok(history) -//} +//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref() + //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); diff --git a/src/app/cli.rs b/src/app/cli.rs index b0c0c0c7..61250d14 100644 --- a/src/app/cli.rs +++ b/src/app/cli.rs @@ -208,7 +208,7 @@ pub fn tek_print_config (config: &Config) { } } } - for (k, v) in config.modes.read().unwrap().iter() { + config.modes.for_each(|k, v|{ println!(); for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); } for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); } @@ -216,22 +216,16 @@ pub fn tek_print_config (config: &Config) { print!("\n{}", Blue.paint("KEYS")); for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); } println!(); - for (k, v) in v.modes.read().unwrap().iter() { - print!("{} {} {:?}", - Blue.paint("MODE"), - Green.bold().paint(format!("{k:<16}")), - v.name); - print!(" INFO={:?}", - v.info); - print!(" VIEW={:?}", - v.view); - println!(" KEYS={:?}", - v.keys); - } + v.modes.for_each(|k, v|{ + print!("{} {} {:?}", Blue.paint("MODE"), Green.bold().paint(format!("{k:<16}")), v.name); + print!( " INFO={:?}", v.info); + print!( " VIEW={:?}", v.view); + println!(" KEYS={:?}", v.keys); + }); print!("{}", Blue.paint("VIEW")); for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); } println!(); - } + }); } pub fn tek_print_status (project: &Arrangement) { diff --git a/src/app/config.rs b/src/app/config.rs index 3ec4f94a..fab190fa 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -1,4 +1,4 @@ -use crate::{*, bind::*, view::*}; +use crate::*; /// Configuration: mode, view, and bind definitions. /// @@ -48,12 +48,14 @@ impl Config { config.init()?; Ok(config) } + /// Create a new app configuration from a set of XDG base directories, pub fn new (dirs: Option) -> Self { let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); let dirs = dirs.unwrap_or_else(default); Self { dirs, ..Default::default() } } + /// Write initial contents of configuration. pub fn init (&mut self) -> Usually<()> { self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{ @@ -62,6 +64,7 @@ impl Config { })?; Ok(()) } + /// Write initial contents of a configuration file. pub fn init_one ( &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> @@ -78,11 +81,13 @@ impl Config { return Err(format!("{path}: not found").into()) }) } + /// Add statements to configuration from [Dsl] source. pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> { dsl.each(|item|self.add_one(item))?; Ok(self) } + fn add_one (&self, item: impl Language) -> Usually<()> { if let Some(expr) = item.expr()? { let head = expr.head()?; @@ -91,7 +96,7 @@ impl Config { let body = tail.tail()?; //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); match head { - Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?, + Some("mode") if let Some(name) = name => self.modes.add(&name, &body)?, Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) @@ -101,7 +106,12 @@ impl Config { return Err(format!("Config::load: expected expr, got: {item:?}").into()) } } + pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { - self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() + self.modes.get(mode) } } + +mod views; pub use self::views::*; +mod modes; pub use self::modes::*; +mod mode; pub use self::mode::*; diff --git a/src/app/mode.rs b/src/app/config/mode.rs similarity index 87% rename from src/app/mode.rs rename to src/app/config/mode.rs index e5c5f8ea..83d25ba9 100644 --- a/src/app/mode.rs +++ b/src/app/config/mode.rs @@ -1,15 +1,19 @@ use crate::*; -pub(crate) fn load_mode (modes: &Modes, name: &impl AsRef, body: &impl Language) -> Usually<()> { - let mut mode = Mode::default(); - body.each(|item|mode.add(item))?; - modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); - Ok(()) +/// Group of view and keys definitions. +/// +/// ``` +/// let mode = tek::Mode::>::default(); +/// ``` +#[derive(Default, Debug)] pub struct Mode { + pub path: PathBuf, + pub name: Vec, + pub info: Vec, + pub view: Vec, + pub keys: Vec, + pub modes: Modes, } -/// Collection of interaction modes. -pub type Modes = Arc, Arc>>>>>; - impl Mode> { /// Add a definition to the mode. /// @@ -71,23 +75,9 @@ impl Mode> { } fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { Ok(Some(if let Some(id) = dsl.head()? { - load_mode(&self.modes, &id, &dsl.tail())?; + self.modes.add(&id, &dsl.tail())?; } else { return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); })) } } - -/// Group of view and keys definitions. -/// -/// ``` -/// let mode = tek::Mode::>::default(); -/// ``` -#[derive(Default, Debug)] pub struct Mode { - pub path: PathBuf, - pub name: Vec, - pub info: Vec, - pub view: Vec, - pub keys: Vec, - pub modes: Modes, -} diff --git a/src/app/config/modes.rs b/src/app/config/modes.rs new file mode 100644 index 00000000..9ff83991 --- /dev/null +++ b/src/app/config/modes.rs @@ -0,0 +1,27 @@ +use crate::*; + +/// Collection of UI modes. +#[derive(Default, Debug, Clone)] +pub struct Modes( + Arc, Arc>>>>> +); + +impl Modes { + pub fn add (&self, name: &impl AsRef, body: &impl Language) -> Usually<()> { + let mut mode = Mode::default(); + body.each(|item|mode.add(item))?; + self.0.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); + Ok(()) + } + pub fn get (&self, name: impl AsRef) -> Option>>> { + self.0.read().unwrap().get(name.as_ref()).cloned() + } + pub fn for_each (&self, mut ator: impl FnMut(&str, &Mode>)->T) { + for (k, v) in self.0.read().unwrap().iter() { + let _ = ator(k.as_ref(), v.as_ref()); + } + } + pub fn len (&self) -> usize { + self.0.read().unwrap().len() + } +} diff --git a/src/app/config/views.rs b/src/app/config/views.rs new file mode 100644 index 00000000..79560813 --- /dev/null +++ b/src/app/config/views.rs @@ -0,0 +1,10 @@ +use crate::*; + +/// Collection of custom view definitions. +pub type Views = Arc, Arc>>>; + +/// Load custom view definition. +pub(crate) fn load_view (views: &Views, name: &impl AsRef, body: &impl Language) -> Usually<()> { + views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into()); + Ok(()) +} diff --git a/src/app/draw.rs b/src/app/draw.rs index 4fcb06d1..0c8578b0 100644 --- a/src/app/draw.rs +++ b/src/app/draw.rs @@ -1,5 +1,9 @@ use crate::*; +tui_view!(|self: App| { + "" +}); + /// The [Draw] implementation for [App] handles the loaded view, /// which is defined in terms of [dizzle] DSL. /// @@ -137,10 +141,10 @@ pub fn draw_dialog ( pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App) -> Usually> { - let modes = state.config.modes.clone(); - let height = (modes.read().unwrap().len() * 2) as u16; + let height = (state.config.modes.len() * 2) as u16; h_exact(height, w_min(Some(30), thunk(move |to: &mut Tui|{ - for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { + let mut index = 0; + state.config.modes.for_each(|id, profile| { let b = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); @@ -149,12 +153,613 @@ pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App) let field_name = w_full(origin_w(fg(fg1, name))); let field_id = w_full(origin_e(fg(fg2, id))); let field_info = w_full(origin_w(info)); - y_push((2 * index) as u16, + let _ = y_push((2 * index) as u16, h_exact(2, w_full(bg(b, south( above(field_name, field_id), field_info ))))).draw(to); - } + index += 1; + }); Ok(to.area().into()) }))).draw(to) } + +/// ``` +/// let _ = tek::view_logo(); +/// ``` +pub fn view_logo () -> impl Draw { + wh_exact(Some(32), Some(7), bold(true, fg(Rgb(240, 200, 180), south!{ + h_exact(1, ""), + h_exact(1, ""), + h_exact(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"), + h_exact(1, east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), + h_exact(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"), + }))) +} + +/// ``` +/// let x = ""; +/// let _ = tek::view_transport(true, x.as_ref(), x.as_ref(), x.as_ref()); +/// let _ = tek::view_transport(false, x.as_ref(), x.as_ref(), x.as_ref()); +/// ``` +pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw { + let theme = ItemTheme::G[96]; + bg(Black, east!(above( + wh_full(origin_w(button_play_pause(play, false))), + wh_full(origin_e(east!( + field_h(theme, "BPM", bpm), + field_h(theme, "Beat", beat), + field_h(theme, "Time", time), + ))) + ))) +} + +/// ``` +/// let x = ""; +/// let _ = tek::view_status(None, x.as_ref(), x.as_ref(), x.as_ref()); +/// let _ = tek::view_status(Some("".into()), x.as_ref(), x.as_ref(), x.as_ref()); +/// ``` +pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw { + let theme = ItemTheme::G[96]; + let sr = field_h(theme, "SR", sr); + let buf = field_h(theme, "Buf", buf); + let lat = field_h(theme, "Lat", lat); + bg(Black, east!(above( + wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))), + wh_full(origin_e(east!(sr, buf, lat))), + ))) +} + +/// ``` +/// let _ = tek::button_play_pause(true, true); +/// let _ = tek::button_play_pause(true, false); +/// let _ = tek::button_play_pause(false, true); +/// let _ = tek::button_play_pause(false, false); +/// ``` +pub fn button_play_pause (playing: bool, compact: bool) -> impl Draw { + bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + either(compact, + thunk(move|to: &mut Tui|w_exact(9, either(playing, + fg(Rgb(0, 255, 0), " PLAYING "), + fg(Rgb(255, 128, 0), " STOPPED ")) + ).draw(to)), + thunk(move|to: &mut Tui|w_exact(5, either(playing, + fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), + fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",))) + ).draw(to)), + ) + ) +} + +#[cfg(feature = "track")] pub fn view_track_row_section ( + _theme: ItemTheme, + button: impl Draw, + button_add: impl Draw, + content: impl Draw, +) -> impl Draw { + west(h_full(w_exact(4, origin_nw(button_add))), + east(w_exact(20, h_full(origin_nw(button))), wh_full(origin_c(content)))) +} + +/// ``` +/// let bg = tengri::ratatui::style::Color::Red; +/// let fg = tengri::ratatui::style::Color::Green; +/// let _ = tek::view_wrap(bg, fg, "and then blue, too!"); +/// ``` +pub fn view_wrap (bg: Color, fg: Color, content: impl Draw) -> impl Draw { + let left = fg_bg(bg, Reset, w_exact(1, y_repeat("▐"))); + let right = fg_bg(bg, Reset, w_exact(1, y_repeat("▌"))); + east(left, west(right, fg_bg(fg, bg, content))) +} + +/// ``` +/// let _ = tek::view_meter("", 0.0); +/// let _ = tek::view_meters(&[0.0, 0.0]); +/// ``` +pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw + 'a { + let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)); + let w = if value >= 0.0 { 13 } + else if value >= -1.0 { 12 } + else if value >= -2.0 { 11 } + else if value >= -3.0 { 10 } + else if value >= -4.0 { 9 } + else if value >= -6.0 { 8 } + else if value >= -9.0 { 7 } + else if value >= -12.0 { 6 } + else if value >= -15.0 { 5 } + else if value >= -20.0 { 4 } + else if value >= -25.0 { 3 } + else if value >= -30.0 { 2 } + else if value >= -40.0 { 1 } + else { 0 }; + let c = if value >= 0.0 { Red } + else if value >= -3.0 { Yellow } + else { Green }; + south!(f, wh_exact(Some(w), Some(1), bg(c, ()))) +} + +pub fn view_meters (values: &[f32;2]) -> impl Draw + use<'_> { + let left = format!("L/{:>+9.3}", values[0]); + let right = format!("R/{:>+9.3}", values[1]); + south(left, right) +} + +pub fn view_sample_info (sample: Option<&Arc>>) -> impl Draw + use<'_> { + when(sample.is_some(), thunk(move|to: &mut Tui|{ + let sample = sample.unwrap().read().unwrap(); + let theme = sample.color; + east!( + field_h(theme, "Name", format!("{:<10}", sample.name.clone())), + field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())), + field_h(theme, "Start", format!("{:<8}", sample.start)), + field_h(theme, "End", format!("{:<8}", sample.end)), + field_h(theme, "Trans", "0"), + field_h(theme, "Gain", format!("{}", sample.gain)), + ).draw(to) + })) +} + +pub fn view_sample_info_v (sample: Option<&Arc>>) -> impl Draw + use<'_> { + let a = thunk(move|to: &mut Tui|{ + let sample = sample.unwrap().read().unwrap(); + let theme = sample.color; + w_exact(20, south!( + w_full(origin_w(field_h(theme, "Name ", format!("{:<10}", sample.name.clone())))), + w_full(origin_w(field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())))), + w_full(origin_w(field_h(theme, "Start ", format!("{:<8}", sample.start)))), + w_full(origin_w(field_h(theme, "End ", format!("{:<8}", sample.end)))), + w_full(origin_w(field_h(theme, "Trans ", "0"))), + w_full(origin_w(field_h(theme, "Gain ", format!("{}", sample.gain)))), + )).draw(to) + }); + + let b = thunk(|to: &mut Tui|fg(Red, south!( + bold(true, "× No sample."), + "[r] record", + "[Shift-F9] import", + )).draw(to)); + + either(sample.is_some(), a, b) +} + +pub fn view_sample_status (sample: Option<&Arc>>) -> impl Draw { + bold(true, fg(g(224), sample + .map(|sample|{ + let sample = sample.read().unwrap(); + format!("Sample {}-{}", sample.start, sample.end) + }) + .unwrap_or_else(||"No sample".to_string()))) +} + +pub fn view_track_header (theme: ItemTheme, content: impl Draw) -> impl Draw { + w_exact(12, bg(theme.darker.term, w_full(origin_e(content)))) +} + +pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) + -> impl Draw + use<'a, T> +{ + let ins = ports.len() as u16; + let frame = Outer(true, Style::default().fg(g(96))); + let iter = move||ports.iter(); + let names = iter_south(iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name())))); + let field = field_v(theme, title, names); + wh_exact(Some(20), Some(1 + ins), border(true, frame, wh_exact(Some(20), Some(1 + ins), field))) +} + +pub fn view_io_ports <'a, T: PortsSizes<'a>> ( + fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a +) -> impl Draw + 'a { + type Item<'a> = (usize, &'a Arc, &'a [Connect], usize, usize); + iter(items, move|(_index, name, connections, y, y2): Item<'a>, _| { + y_push(y as u16, h_exact((y2-y) as u16, + south(h_full(bold(true, fg_bg(fg, bg, origin_w(east(" 󰣲 ", name))))), + iter(||connections.iter(), move|connect: &'a Connect, index|y_push( + index as u16, h_exact(1, origin_w(bold(false, fg_bg(fg, bg, &connect.info)))) + ))))) + }) +} + +pub fn view_scenes_clips <'a, S: ScenesSizes<'a>> ( + scenes: impl Fn()->S, + tracks: impl TracksSizes<'a>, + select: &Selection, + editor: Option<&MidiEditor>, + size: &Size, + editing: bool, +) -> impl Draw { + let status = wh_full(origin_se(fg(Green, format!("{}x{}", size.w(), size.h())))); + let tracks = iter_once(tracks, move|(track_index, track, _, _), _| { + let scenes = iter_once(scenes(), move|(scene_index, scene, _, _), _| { + let (name, theme): (Arc, ItemTheme) = scene_name_theme(scene, track_index); + let f = theme.lightest.term; + let (b, o) = scene_bg(theme, select, track_index, scene_index); + let w = scene_w(track, select, track_index, editor); + let y = scene_y(select, scene_index, editor); + let is_selected = scene_sel(select, track_index, scene_index, editing); + wh_exact(Some(w), Some(y), below( + wh_full(Outer(true, Style::default().fg(o))), + wh_full(below( + below( + fg_bg(o, b, wh_full("")), + wh_full(origin_nw(fg_bg(f, b, bold(true, name)))), + ), + wh_full(when(is_selected, editor.map(|e|e.view()))))))) + }); + w_exact(track.width as u16, h_full(scenes)) + }); + + return size.of(wh_full(above(status, tracks))); + + fn scene_name_theme (scene: &Scene, track_index: usize) -> (Arc, ItemTheme) { + if let Some(Some(clip)) = &scene.clips.get(track_index) { + let clip = clip.read().unwrap(); + (format!(" ⏹ {}", &clip.name).into(), clip.color) + } else { + (" ⏹ -- ".into(), ItemTheme::G[32]) + } + } + + fn scene_bg ( + theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize + ) -> (Color, Color) { + let mut outline = theme.base.term; + (if select.track() == Some(track_index) && select.scene() == Some(scene_index) { + outline = theme.lighter.term; + theme.light.term + } else if select.track() == Some(track_index) || select.scene() == Some(scene_index) { + outline = theme.darkest.term; + theme.base.term + } else { + theme.dark.term + }, outline) + } + + fn scene_w ( + track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor> + ) -> u16 { + if select.track() == Some(track_index) && let Some(editor) = editor { + (editor.size.w() as usize).max(24).max(track.width) as u16 + } else { + track.width as u16 + } + } + + fn scene_y ( + select: &Selection, scene_index: usize, editor: Option<&MidiEditor> + ) -> u16 { + if select.scene() == Some(scene_index) && let Some(editor) = editor { + editor.size.h().max(12) + } else { + H_SCENE as u16 + } + } + + fn scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool { + editing && select.track() == Some(track_index) && select.scene() == Some(scene_index) + } +} + +pub fn view_track_names ( + theme: ItemTheme, + tracks: impl TracksSizes<'_>, + track_count: usize, + scene_count: usize, + selected: &Selection, +) -> impl Draw { + let button = south( + button_3("t", "rack ", format!("{}{track_count}", selected.track() + .map(|track|format!("{track}/")).unwrap_or_default()), false), + button_3("s", "cene ", format!("{}{scene_count}", selected.scene() + .map(|scene|format!("{scene}/")).unwrap_or_default()), false)); + let button_2 = south( + button_2("T", "+", false), + button_2("S", "+", false)); + view_track_row_section(theme, button, button_2, bg(theme.darker.term, + h_exact(2, thunk(|to: &mut Tui|{ + for (index, track, x1, _x2) in tracks { + x_push(x1 as u16, w_exact(track_width(index, track), + bg(if selected.track() == Some(index) { + track.color.light.term + } else { + track.color.base.term + }, south(w_full(origin_nw(east( + format!("·t{index:02} "), + fg(Rgb(255, 255, 255), bold(true, &track.name)) + ))), ""))) ).draw(to); + } + Ok(XYWH(0, 0, 0, 0)) + })))) +} + +pub fn view_track_outputs ( + theme: ItemTheme, tracks: impl TracksSizes<'_>, midi_outs: impl Iterator, +) -> impl Draw { + view_track_row_section(theme, + south(w_full(origin_w(button_2("o", "utput", false))), + thunk(|to: &mut Tui|{ + for port in midi_outs { + w_full(origin_w(port.port_name())).draw(to); + } + Ok(XYWH(0, 0, 0, 0)) + })), + button_2("O", "+", false), + bg(theme.darker.term, origin_w(thunk(|to: &mut Tui|{ + for (index, track, _x1, _x2) in tracks { + let iter = ||track.sequencer.midi_outs.iter(); + let draw = |port: &MidiOutput, _|fg(Rgb(255, 255, 255), + h_exact(1, bg(track.color.dark.term, w_full(origin_w( + format!("·o{index:02} {}", port.port_name())))))); + w_exact(track_width(index, track), + origin_nw(h_full(iter_south(iter, draw)))).draw(to); + } + Ok(XYWH(0, 0, 0, 0)) + })))) +} + +pub fn view_track_inputs ( + theme: ItemTheme, tracks: impl TracksSizes<'_>, height: u16, +) -> impl Draw { + view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), + bg(theme.darker.term, origin_w(thunk(move|to: &mut Tui|{ + for (index, track, _x1, _x2) in tracks { + wh_exact(Some(track_width(index, track)), Some(height + 1), + origin_nw(south( + bg(track.color.base.term, + w_full(origin_w(east!( + either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "), + either(track.sequencer.recording, fg(Red, "●rec "), "·rec "), + either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "), + )))), + iter_south(||track.sequencer.midi_ins.iter(), + |port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term, + w_full(origin_w(format!("·i{index:02} {}", port.port_name())))))))) + .draw(to); + } + Ok(XYWH(0, 0, 0, 0)) + })))) +} + +pub fn view_scenes_names ( + scenes: impl ScenesSizes<'_>, + select: &Selection, + editor: Option<&MidiEditor>, + editing: bool, +) -> impl Draw { + w_exact(20, thunk(move |to: &mut Tui|{ + for (index, scene, ..) in scenes { + view_scene_name(select, editor, index, scene, editing).draw(to); + } + Ok(XYWH(1, 1, 1, 1)) + })) +} + +pub fn view_scene_name ( + select: &Selection, + editor: Option<&MidiEditor>, + index: usize, + scene: &Scene, + editing: bool +) -> impl Draw { + let h = if select.scene() == Some(index) && let Some(_editor) = editor { + 7 + } else { + H_SCENE as u16 + }; + let a = w_full(origin_w(east(format!("·s{index:02} "), + fg(g(255), bold(true, &scene.name))))); + let b = when(select.scene() == Some(index) && editing, + wh_full(origin_nw(south( + editor.as_ref().map(|e|e.clip_status()), + editor.as_ref().map(|e|e.edit_status()))))); + let c = if select.scene() == Some(index) { + scene.color.light.term + } else { + scene.color.base.term + }; + wh_exact(Some(20), Some(h), bg(c, origin_nw(south(a, b)))) +} + +pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) +} + +pub fn view_midi_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs)) +} + +pub fn view_audio_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins())) +} + +pub fn view_audio_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) +} + +pub fn view_track_per <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a +) -> impl Draw { + iter_east(tracks, move|(index, track, x1, x2): (usize, &Track, usize, usize), _|{ + w_exact((x2 - x1) as u16, fg_bg( + track.color.lightest.term, + track.color.base.term, + callback(index, track))) + }) +} + +pub fn view_per_track () -> impl Draw {} + +pub fn view_per_track_top () -> impl Draw {} + +pub fn view_inputs (tracks: impl TracksSizes<'_>, midi_ins: &[MidiInput]) -> impl Draw { + let title_1 = wh_exact(Some(20), Some(1), origin_w(button_3("i", "nput ", format!("{}", midi_ins.len()), false))); + let title_2 = wh_exact(Some(4), Some(1), w_exact(4, button_2("I", "+", false))); + east(title_1, west(title_2, thunk(move|to: &mut Tui|{ + for (_index, track, x1, _x2) in tracks { + south( + x_push(x1 as u16, bg(track.color.dark.term, origin_w(w_exact(track.width as u16, east!( + either(track.sequencer.monitoring, fg(Green, "mon "), "mon "), + either(track.sequencer.recording, fg(Red, "rec "), "rec "), + either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "), + ))))), + thunk(move |to: &mut Tui|{ + for (index, port) in midi_ins.iter().enumerate() { + x_push(index as u16 * 10, h_exact(1, east(w_exact(20, origin_w(east(" ● ", bold(true, fg(Rgb(255,255,255), port.port_name()))))), + west(w_exact(4, ()), thunk(move|to: &mut Tui|{ + bg(track.color.darker.term, origin_w(w_exact(track.width as u16, east!( + either(track.sequencer.monitoring, fg(Green, " ● "), " · "), + either(track.sequencer.recording, fg(Red, " ● "), " · "), + either(track.sequencer.overdub, fg(Yellow, " ● "), " · "), + )))).draw(to); + todo!() + }))))).draw(to); + } + todo!() + }) + ).draw(to); + } + todo!() + }))) +} + +pub fn view_outputs ( + theme: ItemTheme, + tracks: impl TracksSizes<'_>, + midi_outs: &[MidiOutput], + height: u16, +) -> impl Draw { + + let list = south( + h_exact(1, w_full(origin_w(button_3( + "o", "utput", format!("{}", midi_outs.len()), false + )))), + h_exact(height - 1, wh_full(origin_nw(thunk(|to: &mut Tui|{ + for (_index, port) in midi_outs.iter().enumerate() { + h_exact(1,w_full(east( + origin_w(east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name())))), + w_full(origin_e(format!("{}/{} ", + port.port().get_connections().len(), + port.connections.len())))))).draw(to); + for (index, conn) in port.connections.iter().enumerate() { + h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info())))) + .draw(to); + } + } + todo!(); + })))) + ); + + h_exact(height, view_track_row_section(theme, list, button_2("O", "+", false), + bg(theme.darker.term, origin_w(w_full( + thunk(|to: &mut Tui|{ + for (index, track, _x1, _x2) in tracks { + w_exact(track_width(index, track), + thunk(|to: &mut Tui|{ + h_exact(1, origin_w(east( + either(true, fg(Green, "play "), "play "), + either(false, fg(Yellow, "solo "), "solo "), + ))).draw(to); + for (_index, port) in midi_outs.iter().enumerate() { + h_exact(1, origin_w(east( + either(true, fg(Green, " ● "), " · "), + either(false, fg(Yellow, " ● "), " · "), + ))).draw(to); + for (_index, _conn) in port.connections.iter().enumerate() { + h_exact(1, w_full("")).draw(to); + } + } + todo!() + }) + ).draw(to); + } + todo!() + }) + ))) + )) +} + +pub fn view_track_devices ( + theme: ItemTheme, + tracks: impl TracksSizes<'_>, + track: Option<&Track>, + h: u16, +) -> impl Draw { + view_track_row_section(theme, + button_3("d", "evice", format!("{}", track.map(|t|t.devices.len()).unwrap_or(0)), false), + button_2("D", "+", false), + iter_once(tracks, move|(_, track, _x1, _x2), index| wh_exact( + Some(track_width(index, track)), + Some(h + 1), + bg(track.color.dark.term, origin_nw(iter_south(move||0..h, + |_, _index|wh_exact(Some(track.width as u16), Some(2), + fg_bg( + ItemTheme::G[32].lightest.term, + ItemTheme::G[32].dark.term, + origin_nw(format!(" · {}", "--")) + ) + ))))))) +} + +pub fn per_track <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a +) -> impl Draw + 'a { + per_track_top(tracks, move|index, track|h_full(origin_y(callback(index, track)))) +} + +pub fn per_track_top <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a +) -> impl Draw + 'a { + origin_x(bg(Reset, iter_east(tracks, + move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ + w_exact((x2 - x1) as u16, fg_bg( + track.color.lightest.term, + track.color.base.term, + callback(index, track))) }))) +} + +pub fn field_h (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { +} + +pub fn field_v (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { +} + +pub fn view_sessions () -> impl Draw { + let h = 6; + let w = Some(30); + let f = Rgb(224, 192, 128); + h_exact(h, w_min(w, thunk(move |to: &mut Tui|{ + for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { + let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; + let y = (2 * index) as u16; + let h = 2; + y_push(y, &h_exact(h, w_full(bg(b, origin_w(fg(f, name)))))).draw(to); + } + Ok(to.area().into()) + }))) +} + +pub fn view_browse_title (state: &App) -> impl Draw { + w_full(origin_w(field_v(ItemTheme::default(), + match state.dialog.browser_target().unwrap() { + BrowseTarget::SaveProject => "Save project:", + BrowseTarget::LoadProject => "Load project:", + BrowseTarget::ImportSample(_) => "Import sample:", + BrowseTarget::ExportSample(_) => "Export sample:", + BrowseTarget::ImportClip(_) => "Import clip:", + BrowseTarget::ExportClip(_) => "Export clip:", + }, h_exact(1, fg(g(96), x_repeat("🭻"))) + ))) +} + +pub fn view_device (state: &App) -> impl Draw { + let selected = state.dialog.device_kind().unwrap(); + south(bold(true, "Add device"), iter_south( + move||device_kinds().iter(), + move|_label: &&'static str, i|{ + let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; + let l = if i == selected { "[ " } else { " " }; + let r = if i == selected { " ]" } else { " " }; + w_full(bg(b, east(l, west(r, "FIXME device name")))) })) +} diff --git a/src/app/size.rs b/src/app/size.rs index 843328c8..2d336b34 100644 --- a/src/app/size.rs +++ b/src/app/size.rs @@ -16,10 +16,6 @@ def_sizes_iter!(PortsSizes => Arc, [Connect]); def_sizes_iter!(ScenesSizes => Scene); def_sizes_iter!(TracksSizes => Track); -pub(crate) fn track_width (_index: usize, track: &Track) -> u16 { - track.width as u16 -} - pub trait HasWidth { const MIN_WIDTH: usize; /// Increment track width. @@ -27,31 +23,3 @@ pub trait HasWidth { /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. fn width_dec (&mut self); } - -impl HasClipsSize for App { - fn clips_size (&self) -> &Size { &self.project.size_inner } -} - -impl HasTrackScroll for App { - fn track_scroll (&self) -> usize { - self.project.track_scroll() - } -} - -impl HasSceneScroll for App { - fn scene_scroll (&self) -> usize { - self.project.scene_scroll() - } -} - -impl ScenesView for App { - fn w_mid (&self) -> u16 { - (self.size.w() as u16).saturating_sub(self.w_side()) - } - fn w_side (&self) -> u16 { - 20 - } - fn h_scenes (&self) -> u16 { - (self.size.h() as u16).saturating_sub(20) - } -} diff --git a/src/app/view.rs b/src/app/view.rs deleted file mode 100644 index 27b9313a..00000000 --- a/src/app/view.rs +++ /dev/null @@ -1,614 +0,0 @@ -use crate::*; - -tui_view!(|self: App| { - "" -}); - -/// Collection of custom view definitions. -pub type Views = Arc, Arc>>>; - -/// Load custom view definition. -pub(crate) fn load_view (views: &Views, name: &impl AsRef, body: &impl Language) -> Usually<()> { - views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into()); - Ok(()) -} - -/// ``` -/// let _ = tek::view_logo(); -/// ``` -pub fn view_logo () -> impl Draw { - wh_exact(Some(32), Some(7), bold(true, fg(Rgb(240, 200, 180), south!{ - h_exact(1, ""), - h_exact(1, ""), - h_exact(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"), - h_exact(1, east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), - h_exact(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"), - }))) -} - -/// ``` -/// let x = ""; -/// let _ = tek::view_transport(true, x.as_ref(), x.as_ref(), x.as_ref()); -/// let _ = tek::view_transport(false, x.as_ref(), x.as_ref(), x.as_ref()); -/// ``` -pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw { - let theme = ItemTheme::G[96]; - bg(Black, east!(above( - wh_full(origin_w(button_play_pause(play, false))), - wh_full(origin_e(east!( - field_h(theme, "BPM", bpm), - field_h(theme, "Beat", beat), - field_h(theme, "Time", time), - ))) - ))) -} - -/// ``` -/// let x = ""; -/// let _ = tek::view_status(None, x.as_ref(), x.as_ref(), x.as_ref()); -/// let _ = tek::view_status(Some("".into()), x.as_ref(), x.as_ref(), x.as_ref()); -/// ``` -pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw { - let theme = ItemTheme::G[96]; - let sr = field_h(theme, "SR", sr); - let buf = field_h(theme, "Buf", buf); - let lat = field_h(theme, "Lat", lat); - bg(Black, east!(above( - wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))), - wh_full(origin_e(east!(sr, buf, lat))), - ))) -} - -/// ``` -/// let _ = tek::button_play_pause(true, true); -/// let _ = tek::button_play_pause(true, false); -/// let _ = tek::button_play_pause(false, true); -/// let _ = tek::button_play_pause(false, false); -/// ``` -pub fn button_play_pause (playing: bool, compact: bool) -> impl Draw { - bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - either(compact, - thunk(move|to: &mut Tui|w_exact(9, either(playing, - fg(Rgb(0, 255, 0), " PLAYING "), - fg(Rgb(255, 128, 0), " STOPPED ")) - ).draw(to)), - thunk(move|to: &mut Tui|w_exact(5, either(playing, - fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), - fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",))) - ).draw(to)), - ) - ) -} - -#[cfg(feature = "track")] pub fn view_track_row_section ( - _theme: ItemTheme, - button: impl Draw, - button_add: impl Draw, - content: impl Draw, -) -> impl Draw { - west(h_full(w_exact(4, origin_nw(button_add))), - east(w_exact(20, h_full(origin_nw(button))), wh_full(origin_c(content)))) -} - -/// ``` -/// let bg = tengri::ratatui::style::Color::Red; -/// let fg = tengri::ratatui::style::Color::Green; -/// let _ = tek::view_wrap(bg, fg, "and then blue, too!"); -/// ``` -pub fn view_wrap (bg: Color, fg: Color, content: impl Draw) -> impl Draw { - let left = fg_bg(bg, Reset, w_exact(1, y_repeat("▐"))); - let right = fg_bg(bg, Reset, w_exact(1, y_repeat("▌"))); - east(left, west(right, fg_bg(fg, bg, content))) -} - -/// ``` -/// let _ = tek::view_meter("", 0.0); -/// let _ = tek::view_meters(&[0.0, 0.0]); -/// ``` -pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw + 'a { - let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)); - let w = if value >= 0.0 { 13 } - else if value >= -1.0 { 12 } - else if value >= -2.0 { 11 } - else if value >= -3.0 { 10 } - else if value >= -4.0 { 9 } - else if value >= -6.0 { 8 } - else if value >= -9.0 { 7 } - else if value >= -12.0 { 6 } - else if value >= -15.0 { 5 } - else if value >= -20.0 { 4 } - else if value >= -25.0 { 3 } - else if value >= -30.0 { 2 } - else if value >= -40.0 { 1 } - else { 0 }; - let c = if value >= 0.0 { Red } - else if value >= -3.0 { Yellow } - else { Green }; - south!(f, wh_exact(Some(w), Some(1), bg(c, ()))) -} - -pub fn view_meters (values: &[f32;2]) -> impl Draw + use<'_> { - let left = format!("L/{:>+9.3}", values[0]); - let right = format!("R/{:>+9.3}", values[1]); - south(left, right) -} - -pub fn view_sample_info (sample: Option<&Arc>>) -> impl Draw + use<'_> { - when(sample.is_some(), thunk(move|to: &mut Tui|{ - let sample = sample.unwrap().read().unwrap(); - let theme = sample.color; - east!( - field_h(theme, "Name", format!("{:<10}", sample.name.clone())), - field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())), - field_h(theme, "Start", format!("{:<8}", sample.start)), - field_h(theme, "End", format!("{:<8}", sample.end)), - field_h(theme, "Trans", "0"), - field_h(theme, "Gain", format!("{}", sample.gain)), - ).draw(to) - })) -} - -pub fn view_sample_info_v (sample: Option<&Arc>>) -> impl Draw + use<'_> { - let a = thunk(move|to: &mut Tui|{ - let sample = sample.unwrap().read().unwrap(); - let theme = sample.color; - w_exact(20, south!( - w_full(origin_w(field_h(theme, "Name ", format!("{:<10}", sample.name.clone())))), - w_full(origin_w(field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())))), - w_full(origin_w(field_h(theme, "Start ", format!("{:<8}", sample.start)))), - w_full(origin_w(field_h(theme, "End ", format!("{:<8}", sample.end)))), - w_full(origin_w(field_h(theme, "Trans ", "0"))), - w_full(origin_w(field_h(theme, "Gain ", format!("{}", sample.gain)))), - )).draw(to) - }); - - let b = thunk(|to: &mut Tui|fg(Red, south!( - bold(true, "× No sample."), - "[r] record", - "[Shift-F9] import", - )).draw(to)); - - either(sample.is_some(), a, b) -} - -pub fn view_sample_status (sample: Option<&Arc>>) -> impl Draw { - bold(true, fg(g(224), sample - .map(|sample|{ - let sample = sample.read().unwrap(); - format!("Sample {}-{}", sample.start, sample.end) - }) - .unwrap_or_else(||"No sample".to_string()))) -} - -pub fn view_track_header (theme: ItemTheme, content: impl Draw) -> impl Draw { - w_exact(12, bg(theme.darker.term, w_full(origin_e(content)))) -} - -pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) - -> impl Draw + use<'a, T> -{ - let ins = ports.len() as u16; - let frame = Outer(true, Style::default().fg(g(96))); - let iter = move||ports.iter(); - let names = iter_south(iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name())))); - let field = field_v(theme, title, names); - wh_exact(Some(20), Some(1 + ins), border(true, frame, wh_exact(Some(20), Some(1 + ins), field))) -} - -pub fn view_io_ports <'a, T: PortsSizes<'a>> ( - fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a -) -> impl Draw + 'a { - type Item<'a> = (usize, &'a Arc, &'a [Connect], usize, usize); - iter(items, move|(_index, name, connections, y, y2): Item<'a>, _| { - y_push(y as u16, h_exact((y2-y) as u16, - south(h_full(bold(true, fg_bg(fg, bg, origin_w(east(" 󰣲 ", name))))), - iter(||connections.iter(), move|connect: &'a Connect, index|y_push( - index as u16, h_exact(1, origin_w(bold(false, fg_bg(fg, bg, &connect.info)))) - ))))) - }) -} - -pub fn view_scenes_clips <'a, S: ScenesSizes<'a>> ( - scenes: impl Fn()->S, - tracks: impl TracksSizes<'a>, - select: &Selection, - editor: Option<&MidiEditor>, - size: &Size, - editing: bool, -) -> impl Draw { - let status = wh_full(origin_se(fg(Green, format!("{}x{}", size.w(), size.h())))); - let tracks = iter_once(tracks, move|(track_index, track, _, _), _| { - let scenes = iter_once(scenes(), move|(scene_index, scene, _, _), _| { - let (name, theme): (Arc, ItemTheme) = scene_name_theme(scene, track_index); - let f = theme.lightest.term; - let (b, o) = scene_bg(theme, select, track_index, scene_index); - let w = scene_w(track, select, track_index, editor); - let y = scene_y(select, scene_index, editor); - let is_selected = scene_sel(select, track_index, scene_index, editing); - wh_exact(Some(w), Some(y), below( - wh_full(Outer(true, Style::default().fg(o))), - wh_full(below( - below( - fg_bg(o, b, wh_full("")), - wh_full(origin_nw(fg_bg(f, b, bold(true, name)))), - ), - wh_full(when(is_selected, editor.map(|e|e.view()))))))) - }); - w_exact(track.width as u16, h_full(scenes)) - }); - - return size.of(wh_full(above(status, tracks))); - - fn scene_name_theme (scene: &Scene, track_index: usize) -> (Arc, ItemTheme) { - if let Some(Some(clip)) = &scene.clips.get(track_index) { - let clip = clip.read().unwrap(); - (format!(" ⏹ {}", &clip.name).into(), clip.color) - } else { - (" ⏹ -- ".into(), ItemTheme::G[32]) - } - } - - fn scene_bg ( - theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize - ) -> (Color, Color) { - let mut outline = theme.base.term; - (if select.track() == Some(track_index) && select.scene() == Some(scene_index) { - outline = theme.lighter.term; - theme.light.term - } else if select.track() == Some(track_index) || select.scene() == Some(scene_index) { - outline = theme.darkest.term; - theme.base.term - } else { - theme.dark.term - }, outline) - } - - fn scene_w ( - track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor> - ) -> u16 { - if select.track() == Some(track_index) && let Some(editor) = editor { - (editor.size.w() as usize).max(24).max(track.width) as u16 - } else { - track.width as u16 - } - } - - fn scene_y ( - select: &Selection, scene_index: usize, editor: Option<&MidiEditor> - ) -> u16 { - if select.scene() == Some(scene_index) && let Some(editor) = editor { - editor.size.h().max(12) - } else { - H_SCENE as u16 - } - } - - fn scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool { - editing && select.track() == Some(track_index) && select.scene() == Some(scene_index) - } -} - -pub fn view_track_names ( - theme: ItemTheme, - tracks: impl TracksSizes<'_>, - track_count: usize, - scene_count: usize, - selected: &Selection, -) -> impl Draw { - let button = south( - button_3("t", "rack ", format!("{}{track_count}", selected.track() - .map(|track|format!("{track}/")).unwrap_or_default()), false), - button_3("s", "cene ", format!("{}{scene_count}", selected.scene() - .map(|scene|format!("{scene}/")).unwrap_or_default()), false)); - let button_2 = south( - button_2("T", "+", false), - button_2("S", "+", false)); - view_track_row_section(theme, button, button_2, bg(theme.darker.term, - h_exact(2, thunk(|to: &mut Tui|{ - for (index, track, x1, _x2) in tracks { - x_push(x1 as u16, w_exact(track_width(index, track), - bg(if selected.track() == Some(index) { - track.color.light.term - } else { - track.color.base.term - }, south(w_full(origin_nw(east( - format!("·t{index:02} "), - fg(Rgb(255, 255, 255), bold(true, &track.name)) - ))), ""))) ).draw(to); - } - Ok(XYWH(0, 0, 0, 0)) - })))) -} - -pub fn view_track_outputs ( - theme: ItemTheme, tracks: impl TracksSizes<'_>, midi_outs: impl Iterator, -) -> impl Draw { - view_track_row_section(theme, - south(w_full(origin_w(button_2("o", "utput", false))), - thunk(|to: &mut Tui|{ - for port in midi_outs { - w_full(origin_w(port.port_name())).draw(to); - } - Ok(XYWH(0, 0, 0, 0)) - })), - button_2("O", "+", false), - bg(theme.darker.term, origin_w(thunk(|to: &mut Tui|{ - for (index, track, _x1, _x2) in tracks { - let iter = ||track.sequencer.midi_outs.iter(); - let draw = |port: &MidiOutput, _|fg(Rgb(255, 255, 255), - h_exact(1, bg(track.color.dark.term, w_full(origin_w( - format!("·o{index:02} {}", port.port_name())))))); - w_exact(track_width(index, track), - origin_nw(h_full(iter_south(iter, draw)))).draw(to); - } - Ok(XYWH(0, 0, 0, 0)) - })))) -} - -pub fn view_track_inputs ( - theme: ItemTheme, tracks: impl TracksSizes<'_>, height: u16, -) -> impl Draw { - view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), - bg(theme.darker.term, origin_w(thunk(move|to: &mut Tui|{ - for (index, track, _x1, _x2) in tracks { - wh_exact(Some(track_width(index, track)), Some(height + 1), - origin_nw(south( - bg(track.color.base.term, - w_full(origin_w(east!( - either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "), - either(track.sequencer.recording, fg(Red, "●rec "), "·rec "), - either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "), - )))), - iter_south(||track.sequencer.midi_ins.iter(), - |port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term, - w_full(origin_w(format!("·i{index:02} {}", port.port_name())))))))) - .draw(to); - } - Ok(XYWH(0, 0, 0, 0)) - })))) -} - -pub fn view_scenes_names ( - scenes: impl ScenesSizes<'_>, - select: &Selection, - editor: Option<&MidiEditor>, - editing: bool, -) -> impl Draw { - w_exact(20, thunk(move |to: &mut Tui|{ - for (index, scene, ..) in scenes { - view_scene_name(select, editor, index, scene, editing).draw(to); - } - Ok(XYWH(1, 1, 1, 1)) - })) -} - -pub fn view_scene_name ( - select: &Selection, - editor: Option<&MidiEditor>, - index: usize, - scene: &Scene, - editing: bool -) -> impl Draw { - let h = if select.scene() == Some(index) && let Some(_editor) = editor { - 7 - } else { - H_SCENE as u16 - }; - let a = w_full(origin_w(east(format!("·s{index:02} "), - fg(g(255), bold(true, &scene.name))))); - let b = when(select.scene() == Some(index) && editing, - wh_full(origin_nw(south( - editor.as_ref().map(|e|e.clip_status()), - editor.as_ref().map(|e|e.edit_status()))))); - let c = if select.scene() == Some(index) { - scene.color.light.term - } else { - scene.color.base.term - }; - wh_exact(Some(20), Some(h), bg(c, origin_nw(south(a, b)))) -} - -pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { - track.map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) -} - -pub fn view_midi_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { - track.map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs)) -} - -pub fn view_audio_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { - track.map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins())) -} - -pub fn view_audio_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { - track.map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) -} - -pub fn view_track_per <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Draw { - iter_east(tracks, move|(index, track, x1, x2): (usize, &Track, usize, usize), _|{ - w_exact((x2 - x1) as u16, fg_bg( - track.color.lightest.term, - track.color.base.term, - callback(index, track))) - }) -} - -pub fn view_per_track () -> impl Draw {} - -pub fn view_per_track_top () -> impl Draw {} - -pub fn view_inputs (tracks: impl TracksSizes<'_>, midi_ins: &[MidiInput]) -> impl Draw { - let title_1 = wh_exact(Some(20), Some(1), origin_w(button_3("i", "nput ", format!("{}", midi_ins.len()), false))); - let title_2 = wh_exact(Some(4), Some(1), w_exact(4, button_2("I", "+", false))); - east(title_1, west(title_2, thunk(move|to: &mut Tui|{ - for (_index, track, x1, _x2) in tracks { - south( - x_push(x1 as u16, bg(track.color.dark.term, origin_w(w_exact(track.width as u16, east!( - either(track.sequencer.monitoring, fg(Green, "mon "), "mon "), - either(track.sequencer.recording, fg(Red, "rec "), "rec "), - either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "), - ))))), - thunk(move |to: &mut Tui|{ - for (index, port) in midi_ins.iter().enumerate() { - x_push(index as u16 * 10, h_exact(1, east(w_exact(20, origin_w(east(" ● ", bold(true, fg(Rgb(255,255,255), port.port_name()))))), - west(w_exact(4, ()), thunk(move|to: &mut Tui|{ - bg(track.color.darker.term, origin_w(w_exact(track.width as u16, east!( - either(track.sequencer.monitoring, fg(Green, " ● "), " · "), - either(track.sequencer.recording, fg(Red, " ● "), " · "), - either(track.sequencer.overdub, fg(Yellow, " ● "), " · "), - )))).draw(to); - todo!() - }))))).draw(to); - } - todo!() - }) - ).draw(to); - } - todo!() - }))) -} - -pub fn view_outputs ( - theme: ItemTheme, - tracks: impl TracksSizes<'_>, - midi_outs: &[MidiOutput], - height: u16, -) -> impl Draw { - - let list = south( - h_exact(1, w_full(origin_w(button_3( - "o", "utput", format!("{}", midi_outs.len()), false - )))), - h_exact(height - 1, wh_full(origin_nw(thunk(|to: &mut Tui|{ - for (_index, port) in midi_outs.iter().enumerate() { - h_exact(1,w_full(east( - origin_w(east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name())))), - w_full(origin_e(format!("{}/{} ", - port.port().get_connections().len(), - port.connections.len())))))).draw(to); - for (index, conn) in port.connections.iter().enumerate() { - h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info())))) - .draw(to); - } - } - todo!(); - })))) - ); - - h_exact(height, view_track_row_section(theme, list, button_2("O", "+", false), - bg(theme.darker.term, origin_w(w_full( - thunk(|to: &mut Tui|{ - for (index, track, _x1, _x2) in tracks { - w_exact(track_width(index, track), - thunk(|to: &mut Tui|{ - h_exact(1, origin_w(east( - either(true, fg(Green, "play "), "play "), - either(false, fg(Yellow, "solo "), "solo "), - ))).draw(to); - for (_index, port) in midi_outs.iter().enumerate() { - h_exact(1, origin_w(east( - either(true, fg(Green, " ● "), " · "), - either(false, fg(Yellow, " ● "), " · "), - ))).draw(to); - for (_index, _conn) in port.connections.iter().enumerate() { - h_exact(1, w_full("")).draw(to); - } - } - todo!() - }) - ).draw(to); - } - todo!() - }) - ))) - )) -} - -pub fn view_track_devices ( - theme: ItemTheme, - tracks: impl TracksSizes<'_>, - track: Option<&Track>, - h: u16, -) -> impl Draw { - view_track_row_section(theme, - button_3("d", "evice", format!("{}", track.map(|t|t.devices.len()).unwrap_or(0)), false), - button_2("D", "+", false), - iter_once(tracks, move|(_, track, _x1, _x2), index| wh_exact( - Some(track_width(index, track)), - Some(h + 1), - bg(track.color.dark.term, origin_nw(iter_south(move||0..h, - |_, _index|wh_exact(Some(track.width as u16), Some(2), - fg_bg( - ItemTheme::G[32].lightest.term, - ItemTheme::G[32].dark.term, - origin_nw(format!(" · {}", "--")) - ) - ))))))) -} - -pub fn per_track <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Draw + 'a { - per_track_top(tracks, move|index, track|h_full(origin_y(callback(index, track)))) -} - -pub fn per_track_top <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Draw + 'a { - origin_x(bg(Reset, iter_east(tracks, - move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - w_exact((x2 - x1) as u16, fg_bg( - track.color.lightest.term, - track.color.base.term, - callback(index, track))) }))) -} - -pub fn field_h (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { -} - -pub fn field_v (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { -} - -pub fn view_sessions () -> impl Draw { - let h = 6; - let w = Some(30); - let f = Rgb(224, 192, 128); - h_exact(h, w_min(w, thunk(move |to: &mut Tui|{ - for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { - let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; - let y = (2 * index) as u16; - let h = 2; - y_push(y, &h_exact(h, w_full(bg(b, origin_w(fg(f, name)))))).draw(to); - } - Ok(to.area().into()) - }))) -} - -pub fn view_browse_title (state: &App) -> impl Draw { - w_full(origin_w(field_v(ItemTheme::default(), - match state.dialog.browser_target().unwrap() { - BrowseTarget::SaveProject => "Save project:", - BrowseTarget::LoadProject => "Load project:", - BrowseTarget::ImportSample(_) => "Import sample:", - BrowseTarget::ExportSample(_) => "Export sample:", - BrowseTarget::ImportClip(_) => "Import clip:", - BrowseTarget::ExportClip(_) => "Export clip:", - }, h_exact(1, fg(g(96), x_repeat("🭻"))) - ))) -} - -pub fn view_device (state: &App) -> impl Draw { - let selected = state.dialog.device_kind().unwrap(); - south(bold(true, "Add device"), iter_south( - move||device_kinds().iter(), - move|_label: &&'static str, i|{ - let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; - let l = if i == selected { "[ " } else { " " }; - let r = if i == selected { " ]" } else { " " }; - w_full(bg(b, east(l, west(r, "FIXME device name")))) })) -} diff --git a/src/deps.rs b/src/deps.rs new file mode 100644 index 00000000..ad94c204 --- /dev/null +++ b/src/deps.rs @@ -0,0 +1,33 @@ +#[cfg(feature = "cli")] +pub(crate) use ::clap::{self, Parser, Subcommand}; + +pub(crate) use ::xdg::BaseDirectories; + +pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*}; + +pub(crate) use tengri::{ + *, lang::*, exit::*, eval::*, sing::*, time::*, draw::*, term::*, color::*, task::*, + crossterm::event::{Event, KeyEvent}, + ratatui::{ + self, + prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}}, + widgets::{Widget, canvas::{Canvas, Line}}, + }, +}; + +#[allow(unused)] +pub(crate) use ::{ + std::{ + cmp::Ord, + collections::BTreeMap, + error::Error, + ffi::OsString, + fmt::{Write, Debug, Formatter}, + fs::File, + ops::{Add, Sub, Mul, Div, Rem}, + path::{Path, PathBuf}, + sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}}, + time::Duration, + thread::{spawn, JoinHandle}, + }, +}; diff --git a/src/device/arrange/clip.rs b/src/device/arrange/clip.rs index 05c7b9c5..06679935 100644 --- a/src/device/arrange/clip.rs +++ b/src/device/arrange/clip.rs @@ -26,7 +26,7 @@ use crate::*; pub trait ClipsView: TracksView + ScenesView { /// Draw clips per scene fn view_scenes_clips <'a> (&'a self) -> impl Draw + 'a { - crate::view::view_scenes_clips( + view_scenes_clips( ||self.scenes_with_sizes(), self.tracks_with_sizes(), self.selection(), @@ -87,3 +87,7 @@ impl Arrangement { } pub trait HasClipsSize { fn clips_size (&self) -> &Size; } + +impl HasClipsSize for App { + fn clips_size (&self) -> &Size { &self.project.size_inner } +} diff --git a/src/device/arrange/scene.rs b/src/device/arrange/scene.rs index 2d1c284e..d7c57966 100644 --- a/src/device/arrange/scene.rs +++ b/src/device/arrange/scene.rs @@ -62,7 +62,7 @@ pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + fn w_mid (&self) -> u16; fn view_scenes_names (&self) -> impl Draw { - crate::view::view_scenes_names( + view_scenes_names( self.scenes_with_sizes(), self.selection(), self.editor(), self.is_editing() ) } @@ -130,16 +130,14 @@ impl HasSceneScroll for Arrangement { fn scene_scroll (&self) -> usize { self.scene_scroll } } +impl HasSceneScroll for App { + fn scene_scroll (&self) -> usize { self.project.scene_scroll() } +} + impl ScenesView for Arrangement { - fn h_scenes (&self) -> u16 { - (self.size.h() as u16).saturating_sub(20) - } - fn w_side (&self) -> u16 { - (self.size.w() as u16 * 2 / 10).max(20) - } - fn w_mid (&self) -> u16 { - (self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40) - } + fn h_scenes (&self) -> u16 { (self.size.h() as u16).saturating_sub(20) } + fn w_side (&self) -> u16 { (self.size.w() as u16 * 2 / 10).max(20) } + fn w_mid (&self) -> u16 { (self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40) } } pub type SceneWith<'a, T> = @@ -148,7 +146,7 @@ pub type SceneWith<'a, T> = def_command!(SceneCommand: |scene: Scene| { SetSize { size: usize } => { todo!() }, SetZoom { size: usize } => { todo!() }, - SetName { name: Arc } => + SetName { name: Arc } => swap_value(&mut scene.name, name, |name|Self::SetName{name}), SetColor { color: ItemTheme } => swap_value(&mut scene.color, color, |color|Self::SetColor{color}), @@ -158,7 +156,19 @@ def_command!(SceneCommand: |scene: Scene| { #[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt()); #[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene()); #[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut()); -impl ClipsView for T {} -impl_has!(Vec: |self: Arrangement| self.scenes); -impl>+AsMut>> HasScenes for T {} -impl+AsMutOpt+Send+Sync> HasScene for T {} +impl_has!(Vec: |self: Arrangement| self.scenes); +impl ClipsView for T {} +impl>+AsMut>> HasScenes for T {} +impl+AsMutOpt+Send+Sync> HasScene for T {} + +impl ScenesView for App { + fn w_mid (&self) -> u16 { + (self.size.w() as u16).saturating_sub(self.w_side()) + } + fn w_side (&self) -> u16 { + 20 + } + fn h_scenes (&self) -> u16 { + (self.size.h() as u16).saturating_sub(20) + } +} diff --git a/src/device/arrange/track.rs b/src/device/arrange/track.rs index 5ea42948..c480f694 100644 --- a/src/device/arrange/track.rs +++ b/src/device/arrange/track.rs @@ -55,9 +55,13 @@ impl Track { ) -> impl Draw + 'a { view_track_per(tracks, callback) } +} + +#[cfg(feature = "sampler")] +impl Track { /// Create a new track connecting the [Sequencer] to a [Sampler]. - #[cfg(feature = "sampler")] pub fn new_with_sampler ( + pub fn new_with_sampler ( name: &impl AsRef, color: Option, jack: &Jack<'static>, @@ -78,7 +82,7 @@ impl Track { Ok(track) } - #[cfg(feature = "sampler")] pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { + pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { for device in self.devices.iter() { match device { Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, @@ -88,7 +92,7 @@ impl Track { None } - #[cfg(feature = "sampler")] pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { + pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { for device in self.devices.iter_mut() { match device { Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, @@ -153,22 +157,22 @@ pub trait HasTrack: AsRefOpt + AsMutOpt { #[cfg(feature = "port")] fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw + 'a { - crate::view::view_midi_ins_status(theme, self.track()) + view_midi_ins_status(theme, self.track()) } #[cfg(feature = "port")] fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw + '_ { - crate::view::view_midi_outs_status(theme, self.track()) + view_midi_outs_status(theme, self.track()) } #[cfg(feature = "port")] fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw { - crate::view::view_audio_ins_status(theme, self.track()) + view_audio_ins_status(theme, self.track()) } #[cfg(feature = "port")] fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw { - crate::view::view_audio_outs_status(theme, self.track()) + view_audio_outs_status(theme, self.track()) } } @@ -179,13 +183,13 @@ pub trait HasTrackScroll: HasTracks { pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { /// Draw name of each track fn view_track_names (&self, theme: ItemTheme) -> impl Draw { - crate::view::view_track_names( + view_track_names( theme, self.tracks_with_sizes(), self.tracks().len(), self.scenes().len(), self.selection() ) } /// Draw outputs per track fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw { - crate::view::view_track_outputs( + view_track_outputs( theme, self.tracks_with_sizes(), self.midi_outs().iter() ) } @@ -195,7 +199,7 @@ pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { for track in self.tracks().iter() { h = h.max(track.sequencer.midi_ins.len() as u16); } - crate::view::view_track_inputs(theme, self.tracks_with_sizes(), h) + view_track_inputs(theme, self.tracks_with_sizes(), h) } /// Iterate over tracks with their corresponding sizes. fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { @@ -247,19 +251,19 @@ impl_as_mut!(Vec: |self: App| self.project.as_mut()); impl Arrangement { pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw + '_ { - crate::view::view_inputs( + view_inputs( self.tracks_with_sizes(), self.midi_ins().as_slice() ) } pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw { - crate::view::view_outputs( + view_outputs( theme, self.tracks_with_sizes(), self.midi_outs(), self.outputs_height() ) } pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw { - crate::view::view_track_devices( + view_track_devices( theme, self.tracks_with_sizes(), self.track(), self.devices_height() ) } @@ -334,3 +338,13 @@ impl Arrangement { Ok((index, &mut self.tracks_mut()[index])) } } + +impl HasTrackScroll for App { + fn track_scroll (&self) -> usize { + self.project.track_scroll() + } +} + +pub(crate) fn track_width (_index: usize, track: &Track) -> u16 { + track.width as u16 +} diff --git a/src/device/clock.rs b/src/device/clock.rs index 11a85607..ab53bbe5 100644 --- a/src/device/clock.rs +++ b/src/device/clock.rs @@ -145,6 +145,27 @@ impl Quantize { } } +/// Implement an arithmetic operation for a unit of time +#[macro_export] macro_rules! impl_op { + ($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => { + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output { + let $a = self.get(); let $b = other.get(); Self($impl.into()) + } + } + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output { + let $a = self.get(); let $b = other as f64; Self($impl.into()) + } + } + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output { + let $a = self.get(); let $b = other; Self($impl.into()) + } + } + } +} + /// Define and implement a unit of time #[macro_export] macro_rules! impl_time_unit { ($T:ident) => { diff --git a/src/device/dialog.rs b/src/device/dialog.rs index 1d861d6d..25aa4fc5 100644 --- a/src/device/dialog.rs +++ b/src/device/dialog.rs @@ -43,7 +43,7 @@ impl Dialog { MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ app.dialog = Dialog::None; - app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); + app.mode = app.config.modes.get(":arranger").unwrap(); })))), MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), ].into())) diff --git a/src/device/plugin.rs b/src/device/plugin.rs index 0c233240..e4d8c548 100644 --- a/src/device/plugin.rs +++ b/src/device/plugin.rs @@ -1,5 +1,13 @@ use crate::*; +#[cfg(feature = "lv2_gui")] use ::winit::{ + application::ApplicationHandler, + event::WindowEvent, + event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, + window::{Window, WindowId}, + platform::x11::EventLoopBuilderExtX11 +}; + /// A LV2 plugin. #[derive(Debug)] #[cfg(feature = "lv2")] pub struct Lv2 { /// JACK client handle (needs to not be dropped for standalone mode to work). diff --git a/src/device/port.rs b/src/device/port.rs index 1375822f..eb8cf92a 100644 --- a/src/device/port.rs +++ b/src/device/port.rs @@ -3,14 +3,13 @@ use ConnectName::*; use ConnectScope::*; use ConnectStatus::*; -pub mod audio; pub use self::audio::*; -pub mod midi; pub use self::midi::*; -pub mod connect; pub use self::connect::*; +mod audio; pub use self::audio::*; +mod midi; pub use self::midi::*; +mod connect; pub use self::connect::*; +mod register; pub use self::register::*; pub trait JackPort: HasJack<'static> { - type Port: PortSpec + Default; - type Pair: PortSpec + Default; fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) @@ -135,29 +134,3 @@ pub trait JackPort: HasJack<'static> { })) } } - -pub trait RegisterPorts: HasJack<'static> { - /// Register a MIDI input port. - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register a MIDI output port. - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio input port. - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio output port. - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; -} - -impl> RegisterPorts for J { - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiInput::new(self.jack(), name, connect) - } - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiOutput::new(self.jack(), name, connect) - } - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioInput::new(self.jack(), name, connect) - } - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioOutput::new(self.jack(), name, connect) - } -} diff --git a/src/device/port/register.rs b/src/device/port/register.rs new file mode 100644 index 00000000..0702a21a --- /dev/null +++ b/src/device/port/register.rs @@ -0,0 +1,27 @@ +use crate::*; + +pub trait RegisterPorts: HasJack<'static> { + /// Register a MIDI input port. + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register a MIDI output port. + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio input port. + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio output port. + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; +} + +impl> RegisterPorts for J { + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiInput::new(self.jack(), name, connect) + } + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiOutput::new(self.jack(), name, connect) + } + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioInput::new(self.jack(), name, connect) + } + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioOutput::new(self.jack(), name, connect) + } +} diff --git a/src/device/sampler.rs b/src/device/sampler.rs index da02443b..a537a795 100644 --- a/src/device/sampler.rs +++ b/src/device/sampler.rs @@ -1,5 +1,13 @@ use crate::{*, device::*, browse::*, mix::*}; +pub(crate) use symphonia::{ + default::get_codecs, + core::{//errors::Error as SymphoniaError, + audio::SampleBuffer, formats::Packet, io::MediaSourceStream, probe::Hint, + codecs::{Decoder, CODEC_TYPE_NULL}, + }, +}; + mod voice; pub use self::voice::*; mod sample; pub use self::sample::*; mod sample_add; pub use self::sample_add::*; diff --git a/src/tek.rs b/src/tek.rs index 8535b8cc..26391210 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -1,92 +1,19 @@ #![allow(clippy::unit_arg)] -#![feature(adt_const_params, associated_type_defaults, anonymous_lifetime_in_impl_trait, - closure_lifetime_binder, impl_trait_in_assoc_type, trait_alias, type_alias_impl_trait, - type_changing_struct_update)] - -/// CLI banner. -pub(crate) const HEADER: &'static str = r#" - -~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ - █ █▀ █▀▀▄ ~ v0.4.0, 2026 heatwave edition ~ - ~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; - -#[cfg(feature = "cli")] use clap::{self, Parser, Subcommand}; +#![feature( + adt_const_params, + anonymous_lifetime_in_impl_trait, + impl_trait_in_assoc_type, + trait_alias, + type_changing_struct_update +)] pub extern crate atomic_float; - pub extern crate xdg; -pub(crate) use ::xdg::BaseDirectories; - pub extern crate midly; -pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*}; - pub extern crate tengri; -pub(crate) use tengri::{ - *, lang::*, exit::*, eval::*, sing::*, time::*, draw::*, term::*, color::*, task::*, - crossterm::event::{Event, KeyEvent}, - ratatui::{ - self, - prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}}, - widgets::{Widget, canvas::{Canvas, Line}}, - }, -}; - -/// Implement an arithmetic operation for a unit of time -#[macro_export] macro_rules! impl_op { - ($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => { - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output { - let $a = self.get(); let $b = other.get(); Self($impl.into()) - } - } - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output { - let $a = self.get(); let $b = other as f64; Self($impl.into()) - } - } - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output { - let $a = self.get(); let $b = other; Self($impl.into()) - } - } - } -} - -#[cfg(feature = "sampler")] pub(crate) use symphonia::{ - default::get_codecs, - core::{//errors::Error as SymphoniaError, - audio::SampleBuffer, formats::Packet, io::MediaSourceStream, probe::Hint, - codecs::{Decoder, CODEC_TYPE_NULL}, - }, -}; - -#[cfg(feature = "lv2_gui")] use ::winit::{ - application::ApplicationHandler, - event::WindowEvent, - event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, - window::{Window, WindowId}, - platform::x11::EventLoopBuilderExtX11 -}; - -#[allow(unused)] pub(crate) use ::{ - std::{ - cmp::Ord, - collections::BTreeMap, - error::Error, - ffi::OsString, - fmt::{Write, Debug, Formatter}, - fs::File, - ops::{Add, Sub, Mul, Div, Rem}, - path::{Path, PathBuf}, - sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}}, - time::Duration, - thread::{spawn, JoinHandle}, - }, -}; /// Command-line entrypoint. -#[cfg(feature = "cli")] -pub fn main () -> Usually<()> { +#[cfg(feature = "cli")] pub fn main () -> Usually<()> { tui_setup_panic(); Config::watch(|config|{ tui_run_main(Jack::new_run("tek", move|jack|{ @@ -98,8 +25,14 @@ pub fn main () -> Usually<()> { }).map(|_|()) } -//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref() - //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); - +pub mod deps; pub use self::deps::*; pub mod device; pub use self::device::*; pub mod app; pub use self::app::*; + + +/// CLI banner. +pub(crate) const HEADER: &'static str = r#" + +~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ + █ █▀ █▀▀▄ ~ v0.4.0, 2026 heatwave edition ~ + ~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; From b9e7b9732e059402da3dbaefbbe3372fdd19854b Mon Sep 17 00:00:00 2001 From: i do not exist Date: Sun, 5 Jul 2026 18:46:33 +0300 Subject: [PATCH 27/31] wip: simplify and document --- src/app.rs | 2 +- src/app/draw.rs | 54 ++++++++++++++++++-------------------- src/app/size.rs | 2 +- src/device/arrange.rs | 6 ++--- src/device/arrange/clip.rs | 14 +++++----- src/device/browse.rs | 2 +- src/device/clock.rs | 14 +++++----- src/device/clock/memo.rs | 53 +++++++++++++++++++++++++++++++++++++ src/device/editor.rs | 14 +++------- src/device/editor/piano.rs | 23 +++++++--------- src/device/sampler.rs | 2 +- tengri | 2 +- 12 files changed, 116 insertions(+), 72 deletions(-) create mode 100644 src/device/clock/memo.rs diff --git a/src/app.rs b/src/app.rs index 0be37c36..85d60d81 100644 --- a/src/app.rs +++ b/src/app.rs @@ -35,7 +35,7 @@ pub mod size; pub use self::size::*; /// Must not be dropped for the duration of the process pub jack: Jack<'static>, /// Display size - pub size: Size, + pub size: Sizer, /// Performance counter pub perf: PerfModel, /// Available view modes and input bindings diff --git a/src/app/draw.rs b/src/app/draw.rs index 0c8578b0..35bc6697 100644 --- a/src/app/draw.rs +++ b/src/app/draw.rs @@ -1,27 +1,25 @@ use crate::*; -tui_view!(|self: App| { - "" -}); - /// The [Draw] implementation for [App] handles the loaded view, /// which is defined in terms of [dizzle] DSL. /// /// If there is an error, the error is displayed. FIXME: overlay it /// Then, every top-level form of the DSL description is rendered. -impl Draw for App { - fn draw (self, to: &mut Tui) -> Usually> { - let xywh = to.area().into(); - if let Some(e) = self.error.read().unwrap().as_ref() { - to.show(xywh, e.as_ref())?; - } - for (index, dsl) in self.mode.view.iter().enumerate() { - if let Err(e) = self.interpret(to, dsl) { - *self.error.write().unwrap() = Some(format!("view #{index}: {e}").into()); - break; +impl View for App { + fn view (&self) -> impl Draw { + thunk(|to: &mut Tui|{ + let xywh = to.area().into(); + if let Some(e) = self.error.read().unwrap().as_ref() { + to.show(Layout::XYWH(xywh, e.as_ref()))?; } - } - Ok(xywh) + for (index, dsl) in self.mode.view.iter().enumerate() { + if let Err(e) = self.interpret(to, dsl) { + *self.error.write().unwrap() = Some(format!("view #{index}: {e}").into()); + break; + } + } + Ok(xywh) + }) } } @@ -127,7 +125,7 @@ pub fn draw_dialog ( if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, h_exact(2, origin_n(w_full(item))) - )).draw(to); + )).draw(to)?; } Ok(to.area().into()) }))) @@ -364,7 +362,7 @@ pub fn view_scenes_clips <'a, S: ScenesSizes<'a>> ( tracks: impl TracksSizes<'a>, select: &Selection, editor: Option<&MidiEditor>, - size: &Size, + size: &Sizer, editing: bool, ) -> impl Draw { let status = wh_full(origin_se(fg(Green, format!("{}x{}", size.w(), size.h())))); @@ -465,7 +463,7 @@ pub fn view_track_names ( }, south(w_full(origin_nw(east( format!("·t{index:02} "), fg(Rgb(255, 255, 255), bold(true, &track.name)) - ))), ""))) ).draw(to); + ))), ""))) ).draw(to)?; } Ok(XYWH(0, 0, 0, 0)) })))) @@ -490,7 +488,7 @@ pub fn view_track_outputs ( h_exact(1, bg(track.color.dark.term, w_full(origin_w( format!("·o{index:02} {}", port.port_name())))))); w_exact(track_width(index, track), - origin_nw(h_full(iter_south(iter, draw)))).draw(to); + origin_nw(h_full(iter_south(iter, draw)))).draw(to)?; } Ok(XYWH(0, 0, 0, 0)) })))) @@ -513,7 +511,7 @@ pub fn view_track_inputs ( iter_south(||track.sequencer.midi_ins.iter(), |port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term, w_full(origin_w(format!("·i{index:02} {}", port.port_name())))))))) - .draw(to); + .draw(to)?; } Ok(XYWH(0, 0, 0, 0)) })))) @@ -527,7 +525,7 @@ pub fn view_scenes_names ( ) -> impl Draw { w_exact(20, thunk(move |to: &mut Tui|{ for (index, scene, ..) in scenes { - view_scene_name(select, editor, index, scene, editing).draw(to); + view_scene_name(select, editor, index, scene, editing).draw(to)?; } Ok(XYWH(1, 1, 1, 1)) })) @@ -639,10 +637,10 @@ pub fn view_outputs ( origin_w(east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name())))), w_full(origin_e(format!("{}/{} ", port.port().get_connections().len(), - port.connections.len())))))).draw(to); + port.connections.len())))))).draw(to)?; for (index, conn) in port.connections.iter().enumerate() { h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info())))) - .draw(to); + .draw(to)?; } } todo!(); @@ -658,14 +656,14 @@ pub fn view_outputs ( h_exact(1, origin_w(east( either(true, fg(Green, "play "), "play "), either(false, fg(Yellow, "solo "), "solo "), - ))).draw(to); + ))).draw(to)?; for (_index, port) in midi_outs.iter().enumerate() { h_exact(1, origin_w(east( either(true, fg(Green, " ● "), " · "), either(false, fg(Yellow, " ● "), " · "), - ))).draw(to); + ))).draw(to)?; for (_index, _conn) in port.connections.iter().enumerate() { - h_exact(1, w_full("")).draw(to); + h_exact(1, w_full("")).draw(to)?; } } todo!() @@ -734,7 +732,7 @@ pub fn view_sessions () -> impl Draw { let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; let y = (2 * index) as u16; let h = 2; - y_push(y, &h_exact(h, w_full(bg(b, origin_w(fg(f, name)))))).draw(to); + y_push(y, h_exact(h, w_full(bg(b, origin_w(fg(f, *name)))))).draw(to)?; } Ok(to.area().into()) }))) diff --git a/src/app/size.rs b/src/app/size.rs index 2d336b34..8c80c077 100644 --- a/src/app/size.rs +++ b/src/app/size.rs @@ -1,6 +1,6 @@ use crate::*; -impl_has!(Size: |self: App|self.size); +impl_has!(Sizer: |self: App|self.size); /// Define a type alias for iterators of sized items (columns). macro_rules! def_sizes_iter { diff --git a/src/device/arrange.rs b/src/device/arrange.rs index 8c3f51d8..e3836693 100644 --- a/src/device/arrange.rs +++ b/src/device/arrange.rs @@ -16,9 +16,9 @@ use crate::*; /// TODO rename to "render_cache" or smth pub arranger: Arc>, /// Display size - pub size: Size, + pub size: Sizer, /// Display size of clips area - pub size_inner: Size, + pub size_inner: Sizer, /// Source of time #[cfg(feature = "clock")] pub clock: Clock, /// Allows one MIDI clip to be edited @@ -47,7 +47,7 @@ use crate::*; impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } impl_has!(Jack<'static>: |self: Arrangement| self.jack); -impl_has!(Size: |self: Arrangement| self.size); +impl_has!(Sizer: |self: Arrangement| self.size); impl_has!(Vec: |self: Arrangement| self.midi_ins); impl_has!(Vec: |self: Arrangement| self.midi_outs); impl_has!(Clock: |self: Arrangement| self.clock); diff --git a/src/device/arrange/clip.rs b/src/device/arrange/clip.rs index 06679935..e572a9da 100644 --- a/src/device/arrange/clip.rs +++ b/src/device/arrange/clip.rs @@ -37,10 +37,6 @@ pub trait ClipsView: TracksView + ScenesView { } } -impl HasClipsSize for Arrangement { - fn clips_size (&self) -> &Size { &self.size_inner } -} - def_command!(ClipCommand: |clip: MidiClip| { SetColor { color: Option } => { //(SetColor [t: usize, s: usize, c: ItemTheme] @@ -86,8 +82,14 @@ impl Arrangement { } } -pub trait HasClipsSize { fn clips_size (&self) -> &Size; } +pub trait HasClipsSize { + fn clips_size (&self) -> &Sizer; +} impl HasClipsSize for App { - fn clips_size (&self) -> &Size { &self.project.size_inner } + fn clips_size (&self) -> &Sizer { &self.project.size_inner } +} + +impl HasClipsSize for Arrangement { + fn clips_size (&self) -> &Sizer { &self.size_inner } } diff --git a/src/device/browse.rs b/src/device/browse.rs index 4f0e444a..f8a196b8 100644 --- a/src/device/browse.rs +++ b/src/device/browse.rs @@ -21,7 +21,7 @@ def_command!(FileBrowserCommand: |sampler: Sampler|{ pub filter: String, pub index: usize, pub scroll: usize, - pub size: Size, + pub size: Sizer, } pub(crate) struct EntriesIterator<'a, S: Screen> { diff --git a/src/device/clock.rs b/src/device/clock.rs index ab53bbe5..5781a743 100644 --- a/src/device/clock.rs +++ b/src/device/clock.rs @@ -1,7 +1,12 @@ use crate::*; -use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; +use ::std::sync::{Arc, RwLock, atomic::AtomicUsize}; use ::atomic_float::AtomicF64; -use ::tengri::{draw::*, term::*}; + +mod memo; pub use self::memo::*; +mod moment; pub use self::moment::*; +mod ticker; pub use self::ticker::*; +mod timebase; pub use self::timebase::*; +mod clock_view; pub use self::clock_view::*; impl +AsMut> HasClock for T {} pub trait HasClock: AsRef + AsMut { @@ -396,8 +401,3 @@ impl_time_unit!(Ppq); impl_time_unit!(Pulse); impl_time_unit!(Bpm); impl_time_unit!(LaunchSync); - -mod moment; pub use self::moment::*; -mod ticker; pub use self::ticker::*; -mod timebase; pub use self::timebase::*; -mod clock_view; pub use self::clock_view::*; diff --git a/src/device/clock/memo.rs b/src/device/clock/memo.rs new file mode 100644 index 00000000..91a3290c --- /dev/null +++ b/src/device/clock/memo.rs @@ -0,0 +1,53 @@ +use crate::*; + +#[macro_export] macro_rules! rewrite { + ($buf:ident, $($rest:tt)*) => { + |$buf,_,_|{ + $buf.clear(); + write!($buf, $($rest)*) + } + }; +} + +pub struct Memo { + value: T, + view: U, +} + +impl Memo { + pub fn new (value: T, view: U) -> Self { + Self { value, view } + } +} + +impl Debug for Memo { + fn fmt (&self, _: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + todo!() + } +} + +impl Memo { + pub fn update (&mut self, newval: T, render: impl Fn(&mut U, &T, &T)->R) -> Option { + if newval != self.value { + let result = render(&mut self.view, &newval, &self.value); + self.value = newval; + return Some(result); + } + None + } +} + +pub trait Gettable { + /// Returns current value + fn get (&self) -> T; +} + +pub trait Mutable: Gettable { + /// Sets new value, returns old + fn set (&mut self, value: T) -> T; +} + +pub trait InteriorMutable: Gettable { + /// Sets new value, returns old + fn set (&self, value: T) -> T; +} diff --git a/src/device/editor.rs b/src/device/editor.rs index a7455a22..80133a4d 100644 --- a/src/device/editor.rs +++ b/src/device/editor.rs @@ -19,12 +19,12 @@ pub struct MidiEditor { /// View mode and state of editor pub mode: PianoHorizontal, /// Size of editor on screen - pub size: Size, + pub size: Sizer, } impl_default!(MidiEditor: Self { mode: PianoHorizontal::new(None), - size: Size( + size: Sizer( Arc::new(0usize.into()), Arc::new(0usize.into()), ), @@ -255,13 +255,7 @@ pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync { } } -impl_has!(Size: |self: MidiEditor| self.size); - -impl Draw for MidiEditor { - fn draw (self, to: &mut Tui) -> Usually> { - self.mode.tui().draw(to) - } -} +impl_has!(Sizer: |self: MidiEditor| self.size); impl std::fmt::Debug for MidiEditor { fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { @@ -303,7 +297,7 @@ impl View for MidiEditor { fn view (&self) -> impl Draw { self.autoscroll(); /*self.autozoom();*/ - self.size.of(&self.mode) + self.size.of(self.mode.view()) } } diff --git a/src/device/editor/piano.rs b/src/device/editor/piano.rs index d0b4d8f4..6f163b60 100644 --- a/src/device/editor/piano.rs +++ b/src/device/editor/piano.rs @@ -11,7 +11,7 @@ pub struct PianoHorizontal { /// Buffer where the whole clip is rerendered on change pub buffer: Arc>, /// Size of actual notes area - pub size: Size, + pub size: Sizer, /// The display window pub range: MidiSelection, /// The note cursor @@ -22,17 +22,21 @@ pub struct PianoHorizontal { pub keys_width: u16, } -impl_has!(Size: |self: PianoHorizontal| self.size); -impl Draw for PianoHorizontal { - fn draw (self, to: &mut Tui) -> Usually> { - self.tui().draw(to) +impl_has!(Sizer: |self: PianoHorizontal| self.size); + +impl View for PianoHorizontal { + fn view (&self) -> impl Draw { + south( + east(w_exact(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), + east(self.keys(), self.size.of(below(wh_full(self.notes()), wh_full(self.cursor())))), + ) } } impl PianoHorizontal { pub fn new (clip: Option<&Arc>>) -> Self { - let size = Size::default(); + let size = Sizer::default(); let mut range = MidiSelection::from((12, true)); range.time_axis = size.0.clone(); range.note_axis = size.1.clone(); @@ -49,13 +53,6 @@ impl PianoHorizontal { piano } - pub fn tui (&self) -> impl Draw { - south( - east(w_exact(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), - east(self.keys(), self.size.of(below(wh_full(self.notes()), wh_full(self.cursor())))), - ) - } - /// Draw the piano roll background. /// /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ diff --git a/src/device/sampler.rs b/src/device/sampler.rs index a537a795..40bd30a3 100644 --- a/src/device/sampler.rs +++ b/src/device/sampler.rs @@ -44,7 +44,7 @@ mod sample_kit; pub use self::sample_kit::*; /// Currently active modal, if any. pub mode: Option, /// Size of rendered sampler. - pub size: Size, + pub size: Sizer, /// Lowest note displayed. pub note_lo: AtomicUsize, /// Currently selected note. diff --git a/tengri b/tengri index 90ebae0f..0b9e9c06 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 90ebae0f26fc4524659704df274d0aec815dba2e +Subproject commit 0b9e9c06962e5cd55b639d9c0a7cef6f11405d81 From 2d64f63741a7d8018c91d46fb3f069a5dfb4db4d Mon Sep 17 00:00:00 2001 From: i do not exist Date: Mon, 6 Jul 2026 20:40:57 +0300 Subject: [PATCH 28/31] reorganize more --- src/app.rs | 2 +- src/app/bind.rs | 2 +- src/app/draw.rs | 185 ++++++++++++++----------------- src/deps.rs | 2 +- src/device/clock/moment.rs | 1 - src/device/clock/ticker.rs | 1 - src/device/clock/timebase.rs | 1 - src/device/editor/piano.rs | 8 +- src/device/meter.rs | 48 ++++---- src/device/sampler.rs | 2 +- src/device/sampler/sample_add.rs | 8 +- tengri | 2 +- 12 files changed, 117 insertions(+), 145 deletions(-) diff --git a/src/app.rs b/src/app.rs index 85d60d81..93c3d74a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -59,7 +59,7 @@ impl App { /// Create a new application instance from a backend, project, config, and mode /// /// ``` - /// let jack = tek::tengri::sing::Jack::new(&"test_tek").expect("failed to connect to jack"); + /// let jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack"); /// let proj = tek::Arrangement::default(); /// let mut conf = tek::Config::default(); /// conf.add("(mode hello)"); diff --git a/src/app/bind.rs b/src/app/bind.rs index a1814ace..01628e03 100644 --- a/src/app/bind.rs +++ b/src/app/bind.rs @@ -50,7 +50,7 @@ pub(crate) fn load_bind (binds: &Binds, name: &impl AsRef, body: &impl Lang /// /// ``` /// let lang = "(@x (nop)) (@y (nop) (nop))"; -/// let bind = tek::Bind::>::load(&lang).unwrap(); +/// let bind = tek::Bind::>::load(&lang).unwrap(); /// assert_eq!(bind.query(&'x'.into()).map(|x|x.len()), Some(1)); /// //assert_eq!(bind.query(&'y'.into()).map(|x|x.len()), Some(2)); /// ``` diff --git a/src/app/draw.rs b/src/app/draw.rs index 35bc6697..dd161ce0 100644 --- a/src/app/draw.rs +++ b/src/app/draw.rs @@ -10,7 +10,7 @@ impl View for App { thunk(|to: &mut Tui|{ let xywh = to.area().into(); if let Some(e) = self.error.read().unwrap().as_ref() { - to.show(Layout::XYWH(xywh, e.as_ref()))?; + to.show(area(xywh, e.as_ref()))?; } for (index, dsl) in self.mode.view.iter().enumerate() { if let Err(e) = self.interpret(to, dsl) { @@ -18,39 +18,31 @@ impl View for App { break; } } - Ok(xywh) + Ok(Some(xywh)) }) } } -impl Interpret> for App { - fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) - -> Usually> - { +impl Interpret>> for App { + fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Drawn { tek_draw_expr(self, to, lang) } - fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) - -> Usually> - { + fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Drawn { tek_draw_word(self, to, lang) } } -fn tek_draw_expr (state: &App, to: &mut Tui, lang: &impl Expression) - -> Usually> -{ - if let Some(area) = eval_view(state, to, lang)? { - Ok(area) +fn tek_draw_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Drawn { + Ok(Some(if let Some(area) = eval_view(state, to, lang)? { + area } else if let Some(area) = eval_view_tui(state, to, lang)? { - Ok(area) + area } else { - Err(format!("App::interpret_expr: unexpected: {lang:?}").into()) - } + return Err(format!("App::interpret_expr: unexpected: {lang:?}").into()) + })) } -fn tek_draw_word (state: &App, to: &mut Tui, dsl: &impl Expression) - -> Usually> -{ +fn tek_draw_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Drawn { let mut frags = dsl.src()?.unwrap().split("/"); match frags.next() { Some(":logo") => view_logo().draw(to), @@ -79,32 +71,26 @@ fn tek_draw_word (state: &App, to: &mut Tui, dsl: &impl Expression) } } -pub fn draw_meter_section (to: &mut Tui, mut frags: std::str::Split<&str>) - -> Usually> -{ +pub fn draw_meter_section (to: &mut Tui, mut frags: std::str::Split<&str>) -> Drawn { match frags.next() { - Some("input") => bg(Rgb(30, 30, 30), h_full(origin_s("Input Meters"))).draw(to), - Some("output") => bg(Rgb(30, 30, 30), h_full(origin_s("Output Meters"))).draw(to), + Some("input") => bg(Rgb(30, 30, 30), h_full(align_s("Input Meters"))).draw(to), + Some("output") => bg(Rgb(30, 30, 30), h_full(align_s("Output Meters"))).draw(to), _ => panic!() } } -pub fn draw_tracks (to: &mut Tui, mut frags: std::str::Split<&str>, state: &App) - -> Usually> -{ +pub fn draw_tracks (to: &mut Tui, mut frags: std::str::Split<&str>, state: &App) -> Drawn { match frags.next() { None => "TODO tracks".draw(to), - Some("names") => state.project.view_track_names(state.color.clone()).draw(to),//bg(Rgb(40, 40, 40), w_full(origin_w("Track Names")))), - Some("inputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Inputs"))).draw(to), - Some("devices") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Devices"))).draw(to), - Some("outputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Outputs"))).draw(to), + Some("names") => state.project.view_track_names(state.color.clone()).draw(to),//bg(Rgb(40, 40, 40), w_full(align_w("Track Names")))), + Some("inputs") => bg(Rgb(40, 40, 40), w_full(align_w("Track Inputs"))).draw(to), + Some("devices") => bg(Rgb(40, 40, 40), w_full(align_w("Track Devices"))).draw(to), + Some("outputs") => bg(Rgb(40, 40, 40), w_full(align_w("Track Outputs"))).draw(to), _ => panic!() } } -pub fn draw_scenes (to: &mut Tui, mut frags: std::str::Split<&str>) - -> Usually> -{ +pub fn draw_scenes (to: &mut Tui, mut frags: std::str::Split<&str>) -> Drawn { match frags.next() { None => "TODO Scenes".draw(to), Some(":scenes/names") => "TODO Scene Names".draw(to), @@ -113,8 +99,7 @@ pub fn draw_scenes (to: &mut Tui, mut frags: std::str::Split<&str>) } pub fn draw_dialog ( - to: &mut Tui, mut frags: std::str::Split<&str>, state: &App, dsl: &impl Expression -) -> Usually> { + to: &mut Tui, mut frags: std::str::Split<&str>, state: &App, dsl: &impl Expression) -> Drawn { match frags.next() { Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog { let items = items.clone(); @@ -124,10 +109,10 @@ pub fn draw_dialog ( y_push((2 * index) as u16,fg_bg( if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, - h_exact(2, origin_n(w_full(item))) + h_exact(2, align_n(w_full(item))) )).draw(to)?; } - Ok(to.area().into()) + Ok(Some(to.area().into())) }))) } else { None @@ -136,9 +121,7 @@ pub fn draw_dialog ( } } -pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App) - -> Usually> -{ +pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App) -> Drawn { let height = (state.config.modes.len() * 2) as u16; h_exact(height, w_min(Some(30), thunk(move |to: &mut Tui|{ let mut index = 0; @@ -148,9 +131,9 @@ pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App) let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); let fg1 = Rgb(224, 192, 128); let fg2 = Rgb(224, 128, 32); - let field_name = w_full(origin_w(fg(fg1, name))); - let field_id = w_full(origin_e(fg(fg2, id))); - let field_info = w_full(origin_w(info)); + let field_name = w_full(align_w(fg(fg1, name))); + let field_id = w_full(align_e(fg(fg2, id))); + let field_info = w_full(align_w(info)); let _ = y_push((2 * index) as u16, h_exact(2, w_full(bg(b, south( above(field_name, field_id), @@ -158,7 +141,7 @@ pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App) ))))).draw(to); index += 1; }); - Ok(to.area().into()) + Ok(Some(to.area().into())) }))).draw(to) } @@ -183,8 +166,8 @@ pub fn view_logo () -> impl Draw { pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw { let theme = ItemTheme::G[96]; bg(Black, east!(above( - wh_full(origin_w(button_play_pause(play, false))), - wh_full(origin_e(east!( + wh_full(align_w(button_play_pause(play, false))), + wh_full(align_e(east!( field_h(theme, "BPM", bpm), field_h(theme, "Beat", beat), field_h(theme, "Time", time), @@ -203,8 +186,8 @@ pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl D let buf = field_h(theme, "Buf", buf); let lat = field_h(theme, "Lat", lat); bg(Black, east!(above( - wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))), - wh_full(origin_e(east!(sr, buf, lat))), + wh_full(align_w(sel.map(|sel|field_h(theme, "Selected", sel)))), + wh_full(align_e(east!(sr, buf, lat))), ))) } @@ -235,8 +218,8 @@ pub fn button_play_pause (playing: bool, compact: bool) -> impl Draw { button_add: impl Draw, content: impl Draw, ) -> impl Draw { - west(h_full(w_exact(4, origin_nw(button_add))), - east(w_exact(20, h_full(origin_nw(button))), wh_full(origin_c(content)))) + west(h_full(w_exact(4, align_nw(button_add))), + east(w_exact(20, h_full(align_nw(button))), wh_full(align_c(content)))) } /// ``` @@ -302,12 +285,12 @@ pub fn view_sample_info_v (sample: Option<&Arc>>) -> impl Draw>>) -> impl Draw) -> impl Draw { - w_exact(12, bg(theme.darker.term, w_full(origin_e(content)))) + w_exact(12, bg(theme.darker.term, w_full(align_e(content)))) } pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) @@ -339,7 +322,7 @@ pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, po let ins = ports.len() as u16; let frame = Outer(true, Style::default().fg(g(96))); let iter = move||ports.iter(); - let names = iter_south(iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name())))); + let names = iter_south(iter, move|port, index|h_full(align_w(format!(" {index} {}", port.port_name())))); let field = field_v(theme, title, names); wh_exact(Some(20), Some(1 + ins), border(true, frame, wh_exact(Some(20), Some(1 + ins), field))) } @@ -350,9 +333,9 @@ pub fn view_io_ports <'a, T: PortsSizes<'a>> ( type Item<'a> = (usize, &'a Arc, &'a [Connect], usize, usize); iter(items, move|(_index, name, connections, y, y2): Item<'a>, _| { y_push(y as u16, h_exact((y2-y) as u16, - south(h_full(bold(true, fg_bg(fg, bg, origin_w(east(" 󰣲 ", name))))), + south(h_full(bold(true, fg_bg(fg, bg, align_w(east(" 󰣲 ", name))))), iter(||connections.iter(), move|connect: &'a Connect, index|y_push( - index as u16, h_exact(1, origin_w(bold(false, fg_bg(fg, bg, &connect.info)))) + index as u16, h_exact(1, align_w(bold(false, fg_bg(fg, bg, &connect.info)))) ))))) }) } @@ -365,7 +348,7 @@ pub fn view_scenes_clips <'a, S: ScenesSizes<'a>> ( size: &Sizer, editing: bool, ) -> impl Draw { - let status = wh_full(origin_se(fg(Green, format!("{}x{}", size.w(), size.h())))); + let status = wh_full(align_se(fg(Green, format!("{}x{}", size.w(), size.h())))); let tracks = iter_once(tracks, move|(track_index, track, _, _), _| { let scenes = iter_once(scenes(), move|(scene_index, scene, _, _), _| { let (name, theme): (Arc, ItemTheme) = scene_name_theme(scene, track_index); @@ -379,7 +362,7 @@ pub fn view_scenes_clips <'a, S: ScenesSizes<'a>> ( wh_full(below( below( fg_bg(o, b, wh_full("")), - wh_full(origin_nw(fg_bg(f, b, bold(true, name)))), + wh_full(align_nw(fg_bg(f, b, bold(true, name)))), ), wh_full(when(is_selected, editor.map(|e|e.view()))))))) }); @@ -460,12 +443,12 @@ pub fn view_track_names ( track.color.light.term } else { track.color.base.term - }, south(w_full(origin_nw(east( + }, south(w_full(align_nw(east( format!("·t{index:02} "), fg(Rgb(255, 255, 255), bold(true, &track.name)) ))), ""))) ).draw(to)?; } - Ok(XYWH(0, 0, 0, 0)) + Ok(Some(XYWH(0, 0, 0, 0))) })))) } @@ -473,24 +456,24 @@ pub fn view_track_outputs ( theme: ItemTheme, tracks: impl TracksSizes<'_>, midi_outs: impl Iterator, ) -> impl Draw { view_track_row_section(theme, - south(w_full(origin_w(button_2("o", "utput", false))), + south(w_full(align_w(button_2("o", "utput", false))), thunk(|to: &mut Tui|{ for port in midi_outs { - w_full(origin_w(port.port_name())).draw(to); + w_full(align_w(port.port_name())).draw(to); } - Ok(XYWH(0, 0, 0, 0)) + Ok(Some(XYWH(0, 0, 0, 0))) })), button_2("O", "+", false), - bg(theme.darker.term, origin_w(thunk(|to: &mut Tui|{ + bg(theme.darker.term, align_w(thunk(|to: &mut Tui|{ for (index, track, _x1, _x2) in tracks { let iter = ||track.sequencer.midi_outs.iter(); let draw = |port: &MidiOutput, _|fg(Rgb(255, 255, 255), - h_exact(1, bg(track.color.dark.term, w_full(origin_w( + h_exact(1, bg(track.color.dark.term, w_full(align_w( format!("·o{index:02} {}", port.port_name())))))); w_exact(track_width(index, track), - origin_nw(h_full(iter_south(iter, draw)))).draw(to)?; + align_nw(h_full(iter_south(iter, draw)))).draw(to)?; } - Ok(XYWH(0, 0, 0, 0)) + Ok(Some(XYWH(0, 0, 0, 0))) })))) } @@ -498,22 +481,22 @@ pub fn view_track_inputs ( theme: ItemTheme, tracks: impl TracksSizes<'_>, height: u16, ) -> impl Draw { view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), - bg(theme.darker.term, origin_w(thunk(move|to: &mut Tui|{ + bg(theme.darker.term, align_w(thunk(move|to: &mut Tui|{ for (index, track, _x1, _x2) in tracks { wh_exact(Some(track_width(index, track)), Some(height + 1), - origin_nw(south( + align_nw(south( bg(track.color.base.term, - w_full(origin_w(east!( + w_full(align_w(east!( either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "), either(track.sequencer.recording, fg(Red, "●rec "), "·rec "), either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "), )))), iter_south(||track.sequencer.midi_ins.iter(), |port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term, - w_full(origin_w(format!("·i{index:02} {}", port.port_name())))))))) + w_full(align_w(format!("·i{index:02} {}", port.port_name())))))))) .draw(to)?; } - Ok(XYWH(0, 0, 0, 0)) + Ok(Some(XYWH(0, 0, 0, 0))) })))) } @@ -527,7 +510,7 @@ pub fn view_scenes_names ( for (index, scene, ..) in scenes { view_scene_name(select, editor, index, scene, editing).draw(to)?; } - Ok(XYWH(1, 1, 1, 1)) + Ok(Some(XYWH(1, 1, 1, 1))) })) } @@ -543,10 +526,10 @@ pub fn view_scene_name ( } else { H_SCENE as u16 }; - let a = w_full(origin_w(east(format!("·s{index:02} "), + let a = w_full(align_w(east(format!("·s{index:02} "), fg(g(255), bold(true, &scene.name))))); let b = when(select.scene() == Some(index) && editing, - wh_full(origin_nw(south( + wh_full(align_nw(south( editor.as_ref().map(|e|e.clip_status()), editor.as_ref().map(|e|e.edit_status()))))); let c = if select.scene() == Some(index) { @@ -554,7 +537,7 @@ pub fn view_scene_name ( } else { scene.color.base.term }; - wh_exact(Some(20), Some(h), bg(c, origin_nw(south(a, b)))) + wh_exact(Some(20), Some(h), bg(c, align_nw(south(a, b)))) } pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { @@ -590,21 +573,21 @@ pub fn view_per_track () -> impl Draw {} pub fn view_per_track_top () -> impl Draw {} pub fn view_inputs (tracks: impl TracksSizes<'_>, midi_ins: &[MidiInput]) -> impl Draw { - let title_1 = wh_exact(Some(20), Some(1), origin_w(button_3("i", "nput ", format!("{}", midi_ins.len()), false))); + let title_1 = wh_exact(Some(20), Some(1), align_w(button_3("i", "nput ", format!("{}", midi_ins.len()), false))); let title_2 = wh_exact(Some(4), Some(1), w_exact(4, button_2("I", "+", false))); east(title_1, west(title_2, thunk(move|to: &mut Tui|{ for (_index, track, x1, _x2) in tracks { south( - x_push(x1 as u16, bg(track.color.dark.term, origin_w(w_exact(track.width as u16, east!( + x_push(x1 as u16, bg(track.color.dark.term, align_w(w_exact(track.width as u16, east!( either(track.sequencer.monitoring, fg(Green, "mon "), "mon "), either(track.sequencer.recording, fg(Red, "rec "), "rec "), either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "), ))))), thunk(move |to: &mut Tui|{ for (index, port) in midi_ins.iter().enumerate() { - x_push(index as u16 * 10, h_exact(1, east(w_exact(20, origin_w(east(" ● ", bold(true, fg(Rgb(255,255,255), port.port_name()))))), + x_push(index as u16 * 10, h_exact(1, east(w_exact(20, align_w(east(" ● ", bold(true, fg(Rgb(255,255,255), port.port_name()))))), west(w_exact(4, ()), thunk(move|to: &mut Tui|{ - bg(track.color.darker.term, origin_w(w_exact(track.width as u16, east!( + bg(track.color.darker.term, align_w(w_exact(track.width as u16, east!( either(track.sequencer.monitoring, fg(Green, " ● "), " · "), either(track.sequencer.recording, fg(Red, " ● "), " · "), either(track.sequencer.overdub, fg(Yellow, " ● "), " · "), @@ -628,18 +611,18 @@ pub fn view_outputs ( ) -> impl Draw { let list = south( - h_exact(1, w_full(origin_w(button_3( + h_exact(1, w_full(align_w(button_3( "o", "utput", format!("{}", midi_outs.len()), false )))), - h_exact(height - 1, wh_full(origin_nw(thunk(|to: &mut Tui|{ + h_exact(height - 1, wh_full(align_nw(thunk(|to: &mut Tui|{ for (_index, port) in midi_outs.iter().enumerate() { h_exact(1,w_full(east( - origin_w(east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name())))), - w_full(origin_e(format!("{}/{} ", + align_w(east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name())))), + w_full(align_e(format!("{}/{} ", port.port().get_connections().len(), port.connections.len())))))).draw(to)?; for (index, conn) in port.connections.iter().enumerate() { - h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info())))) + h_exact(1, w_full(align_w(format!(" c{index:02}{}", conn.info())))) .draw(to)?; } } @@ -648,17 +631,17 @@ pub fn view_outputs ( ); h_exact(height, view_track_row_section(theme, list, button_2("O", "+", false), - bg(theme.darker.term, origin_w(w_full( + bg(theme.darker.term, align_w(w_full( thunk(|to: &mut Tui|{ for (index, track, _x1, _x2) in tracks { w_exact(track_width(index, track), thunk(|to: &mut Tui|{ - h_exact(1, origin_w(east( + h_exact(1, align_w(east( either(true, fg(Green, "play "), "play "), either(false, fg(Yellow, "solo "), "solo "), ))).draw(to)?; for (_index, port) in midi_outs.iter().enumerate() { - h_exact(1, origin_w(east( + h_exact(1, align_w(east( either(true, fg(Green, " ● "), " · "), either(false, fg(Yellow, " ● "), " · "), ))).draw(to)?; @@ -688,12 +671,12 @@ pub fn view_track_devices ( iter_once(tracks, move|(_, track, _x1, _x2), index| wh_exact( Some(track_width(index, track)), Some(h + 1), - bg(track.color.dark.term, origin_nw(iter_south(move||0..h, + bg(track.color.dark.term, align_nw(iter_south(move||0..h, |_, _index|wh_exact(Some(track.width as u16), Some(2), fg_bg( ItemTheme::G[32].lightest.term, ItemTheme::G[32].dark.term, - origin_nw(format!(" · {}", "--")) + align_nw(format!(" · {}", "--")) ) ))))))) } @@ -702,14 +685,14 @@ pub fn per_track <'a, T: Draw + 'a, U: TracksSizes<'a>> ( tracks: impl Fn() -> U + Send + Sync + 'a, callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a ) -> impl Draw + 'a { - per_track_top(tracks, move|index, track|h_full(origin_y(callback(index, track)))) + per_track_top(tracks, move|index, track|h_full(align_y(callback(index, track)))) } pub fn per_track_top <'a, T: Draw + 'a, U: TracksSizes<'a>> ( tracks: impl Fn() -> U + Send + Sync + 'a, callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a ) -> impl Draw + 'a { - origin_x(bg(Reset, iter_east(tracks, + align_x(bg(Reset, iter_east(tracks, move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ w_exact((x2 - x1) as u16, fg_bg( track.color.lightest.term, @@ -732,14 +715,14 @@ pub fn view_sessions () -> impl Draw { let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; let y = (2 * index) as u16; let h = 2; - y_push(y, h_exact(h, w_full(bg(b, origin_w(fg(f, *name)))))).draw(to)?; + y_push(y, h_exact(h, w_full(bg(b, align_w(fg(f, *name)))))).draw(to)?; } - Ok(to.area().into()) + Ok(Some(to.area().into())) }))) } pub fn view_browse_title (state: &App) -> impl Draw { - w_full(origin_w(field_v(ItemTheme::default(), + w_full(align_w(field_v(ItemTheme::default(), match state.dialog.browser_target().unwrap() { BrowseTarget::SaveProject => "Save project:", BrowseTarget::LoadProject => "Load project:", diff --git a/src/deps.rs b/src/deps.rs index ad94c204..f599cc16 100644 --- a/src/deps.rs +++ b/src/deps.rs @@ -6,7 +6,7 @@ pub(crate) use ::xdg::BaseDirectories; pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*}; pub(crate) use tengri::{ - *, lang::*, exit::*, eval::*, sing::*, time::*, draw::*, term::*, color::*, task::*, + *, lang::*, crossterm::event::{Event, KeyEvent}, ratatui::{ self, diff --git a/src/device/clock/moment.rs b/src/device/clock/moment.rs index bf62a822..ed93e2e2 100644 --- a/src/device/clock/moment.rs +++ b/src/device/clock/moment.rs @@ -1,7 +1,6 @@ use crate::*; use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; use ::atomic_float::AtomicF64; -use ::tengri::{draw::*, term::*}; /// A point in time in all time scales (microsecond, sample, MIDI pulse) /// diff --git a/src/device/clock/ticker.rs b/src/device/clock/ticker.rs index 50656d77..1cb163b0 100644 --- a/src/device/clock/ticker.rs +++ b/src/device/clock/ticker.rs @@ -1,7 +1,6 @@ use crate::*; use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; use ::atomic_float::AtomicF64; -use ::tengri::{draw::*, term::*}; /// Iterator that emits subsequent ticks within a range. /// diff --git a/src/device/clock/timebase.rs b/src/device/clock/timebase.rs index 923e054a..fef79202 100644 --- a/src/device/clock/timebase.rs +++ b/src/device/clock/timebase.rs @@ -1,7 +1,6 @@ use crate::*; use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; use ::atomic_float::AtomicF64; -use ::tengri::{draw::*, term::*}; /// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) /// diff --git a/src/device/editor/piano.rs b/src/device/editor/piano.rs index 6f163b60..4102953f 100644 --- a/src/device/editor/piano.rs +++ b/src/device/editor/piano.rs @@ -156,7 +156,7 @@ impl PianoHorizontal { } } } - Ok(xywh) + Ok(Some(xywh)) }) } @@ -190,7 +190,7 @@ impl PianoHorizontal { break } } - Ok(xywh) + Ok(Some(xywh)) }) } @@ -217,7 +217,7 @@ impl PianoHorizontal { to.blit(¬e_pitch_to_name(note), x, screen_y, off_style) }; } - Ok(xywh) + Ok(Some(xywh)) }))) } @@ -233,7 +233,7 @@ impl PianoHorizontal { to.blit(&"|", screen_x, y, style); } } - Ok(xywh) + Ok(Some(xywh)) }))) } } diff --git a/src/device/meter.rs b/src/device/meter.rs index bfe9a48a..e7c493ac 100644 --- a/src/device/meter.rs +++ b/src/device/meter.rs @@ -11,37 +11,33 @@ pub struct Log10Meter(pub f32); #[derive(Debug, Default, Clone)] pub struct RmsMeter(pub f32); -impl Draw for RmsMeter { - fn draw (self, to: &mut Tui) -> Usually> { - let XYWH(x, y, w, h) = to.area().into(); - let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); - let v = (signal * h as f32).ceil() as u16; - let y2 = y + h; - //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); - for y in y..(y + v) { - for x in x..(x + w) { - to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green())); - } +impl_draw!(|self: RmsMeter, to: Tui|{ + let XYWH(x, y, w, h) = to.area().into(); + let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); + let v = (signal * h as f32).ceil() as u16; + let y2 = y + h; + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); + for y in y..(y + v) { + for x in x..(x + w) { + to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green())); } - Ok(to.area().into()) } -} + Ok(Some(to.area().into())) +}); -impl Draw for Log10Meter { - fn draw(self, to: &mut Tui) -> Usually> { - let XYWH(x, y, w, h) = to.area().into(); - let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); - let v = (signal * h as f32 / 100.0).ceil() as u16; - let y2 = y + h; - //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); - for y in y..(y + v) { - for x in x..(x + w) { - to.blit(&"▌", x, y2 - y, Some(Style::default().green())); - } +impl_draw!(|self: Log10Meter, to: Tui| { + let XYWH(x, y, w, h) = to.area().into(); + let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); + let v = (signal * h as f32 / 100.0).ceil() as u16; + let y2 = y + h; + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); + for y in y..(y + v) { + for x in x..(x + w) { + to.blit(&"▌", x, y2 - y, Some(Style::default().green())); } - Ok(to.area().into()) } -} + Ok(Some(to.area().into())) +}); fn draw_meters (meters: &[f32]) -> impl Draw + use<'_> { bg(Black, w_exact(2, iter_east_fixed(1, ||meters.iter(), |value, _index|{ diff --git a/src/device/sampler.rs b/src/device/sampler.rs index 40bd30a3..f82c0a68 100644 --- a/src/device/sampler.rs +++ b/src/device/sampler.rs @@ -314,7 +314,7 @@ fn draw_viewer (sample: Option<&Arc>>) -> impl Draw + use<'_ }) .render(area, to.as_mut()); } - Ok(xywh) + Ok(Some(xywh)) }) } diff --git a/src/device/sampler/sample_add.rs b/src/device/sampler/sample_add.rs index 996e4b04..4fa88b67 100644 --- a/src/device/sampler/sample_add.rs +++ b/src/device/sampler/sample_add.rs @@ -12,6 +12,8 @@ use crate::{*, device::sampler::*}; pub _search: Option, } +impl_draw!(|self: SampleAdd, to: Tui|{ todo!() }); + impl SampleAdd { fn exited (&self) -> bool { self.exited @@ -118,9 +120,3 @@ impl SampleAdd { return Ok(false) } } - -impl Draw for SampleAdd { - fn draw (self, _to: &mut Tui) -> Usually> { - todo!() - } -} diff --git a/tengri b/tengri index 0b9e9c06..1f60b43f 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 0b9e9c06962e5cd55b639d9c0a7cef6f11405d81 +Subproject commit 1f60b43f616ff49ddd8187bcca8d31e9b6597cec From d8a767326cadb276183f388c3d3cc8d9244bb4d8 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Tue, 7 Jul 2026 16:35:52 +0300 Subject: [PATCH 29/31] convert to layout suffixes --- src/app/draw.rs | 387 +++++++++++++++++++------------------ src/device/browse.rs | 18 +- src/device/editor.rs | 44 ++--- src/device/editor/piano.rs | 18 +- src/device/meter.rs | 6 +- tengri | 2 +- 6 files changed, 245 insertions(+), 230 deletions(-) diff --git a/src/app/draw.rs b/src/app/draw.rs index dd161ce0..f21fbba2 100644 --- a/src/app/draw.rs +++ b/src/app/draw.rs @@ -54,9 +54,9 @@ fn tek_draw_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Drawn view_sessions().draw(to), Some(":browse/title") => view_browse_title(state).draw(to), Some(":device") => view_device(state).draw(to), - Some(":status") => h_exact(1, "TODO: Status Bar").draw(to), + Some(":status") => "TODO: Status Bar".exact_h(1).draw(to), Some(":editor") => "TODO Editor".draw(to), - Some(":debug") => h_exact(1, format!("[{:?}]", to.area())).draw(to), + Some(":debug") => format!("[{:?}]", to.area()).exact_h(1).draw(to), Some(_) => { let views = state.config.views.read().unwrap(); if let Some(dsl) = views.get(dsl.src()?.unwrap()) { @@ -73,8 +73,8 @@ fn tek_draw_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Drawn) -> Drawn { match frags.next() { - Some("input") => bg(Rgb(30, 30, 30), h_full(align_s("Input Meters"))).draw(to), - Some("output") => bg(Rgb(30, 30, 30), h_full(align_s("Output Meters"))).draw(to), + Some("input") => bg(Rgb(30, 30, 30), align_s("Input Meters").full_h()).draw(to), + Some("output") => bg(Rgb(30, 30, 30), align_s("Output Meters").full_h()).draw(to), _ => panic!() } } @@ -82,10 +82,10 @@ pub fn draw_meter_section (to: &mut Tui, mut frags: std::str::Split<&str>) -> Dr pub fn draw_tracks (to: &mut Tui, mut frags: std::str::Split<&str>, state: &App) -> Drawn { match frags.next() { None => "TODO tracks".draw(to), - Some("names") => state.project.view_track_names(state.color.clone()).draw(to),//bg(Rgb(40, 40, 40), w_full(align_w("Track Names")))), - Some("inputs") => bg(Rgb(40, 40, 40), w_full(align_w("Track Inputs"))).draw(to), - Some("devices") => bg(Rgb(40, 40, 40), w_full(align_w("Track Devices"))).draw(to), - Some("outputs") => bg(Rgb(40, 40, 40), w_full(align_w("Track Outputs"))).draw(to), + Some("names") => state.project.view_track_names(state.color.clone()).draw(to),//bg(Rgb(40, 40, 40), full_w(align_w("Track Names")))), + Some("inputs") => bg(Rgb(40, 40, 40), align_w("Track Inputs").full_w()).draw(to), + Some("devices") => bg(Rgb(40, 40, 40), align_w("Track Devices").full_w()).draw(to), + Some("outputs") => bg(Rgb(40, 40, 40), align_w("Track Outputs").full_w()).draw(to), _ => panic!() } } @@ -104,16 +104,15 @@ pub fn draw_dialog ( Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog { let items = items.clone(); let selected = selected; - Some(wh_full(thunk(move|to: &mut Tui|{ + Some(thunk(move|to: &mut Tui|{ for (index, MenuItem(item, _)) in items.0.iter().enumerate() { - y_push((2 * index) as u16,fg_bg( - if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, - if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, - h_exact(2, align_n(w_full(item))) - )).draw(to)?; + let f = if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }; + let b = if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }; + fg_bg(f, b, align_n(item.full_w()).exact_h(2)) + .push_y((2 * index) as u16).draw(to)?; } Ok(Some(to.area().into())) - }))) + }).full_wh()) } else { None }.draw(to), @@ -123,7 +122,7 @@ pub fn draw_dialog ( pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App) -> Drawn { let height = (state.config.modes.len() * 2) as u16; - h_exact(height, w_min(Some(30), thunk(move |to: &mut Tui|{ + thunk(move |to: &mut Tui|{ let mut index = 0; state.config.modes.for_each(|id, profile| { let b = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; @@ -131,31 +130,28 @@ pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App) let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); let fg1 = Rgb(224, 192, 128); let fg2 = Rgb(224, 128, 32); - let field_name = w_full(align_w(fg(fg1, name))); - let field_id = w_full(align_e(fg(fg2, id))); - let field_info = w_full(align_w(info)); - let _ = y_push((2 * index) as u16, - h_exact(2, w_full(bg(b, south( - above(field_name, field_id), - field_info - ))))).draw(to); + let field_name = align_w(fg(fg1, name)).full_w(); + let field_id = align_e(fg(fg2, id)).full_w(); + let field_info = align_w(info).full_w(); + let _ = bg(b, south(above(field_name, field_id), field_info)) + .full_w().exact_h(2).push_y((2 * index) as u16).draw(to); index += 1; }); Ok(Some(to.area().into())) - }))).draw(to) + }).min_w(30).exact_h(height).draw(to) } /// ``` /// let _ = tek::view_logo(); /// ``` pub fn view_logo () -> impl Draw { - wh_exact(Some(32), Some(7), bold(true, fg(Rgb(240, 200, 180), south!{ - h_exact(1, ""), - h_exact(1, ""), - h_exact(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"), - h_exact(1, east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), - h_exact(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"), - }))) + bold(true, fg(Rgb(240, 200, 180), south!{ + "".exact_h(1), + "".exact_h(1), + "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~".exact_h(1), + east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(fg(Rgb(230,100,40), "v0.3.0"), " ~~")).exact_h(1), + "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~".exact_h(1), + })).exact_wh(32, 7) } /// ``` @@ -166,12 +162,12 @@ pub fn view_logo () -> impl Draw { pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw { let theme = ItemTheme::G[96]; bg(Black, east!(above( - wh_full(align_w(button_play_pause(play, false))), - wh_full(align_e(east!( + align_w(button_play_pause(play, false)).full_wh(), + align_e(east!( field_h(theme, "BPM", bpm), field_h(theme, "Beat", beat), field_h(theme, "Time", time), - ))) + )).full_wh() ))) } @@ -186,8 +182,8 @@ pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl D let buf = field_h(theme, "Buf", buf); let lat = field_h(theme, "Lat", lat); bg(Black, east!(above( - wh_full(align_w(sel.map(|sel|field_h(theme, "Selected", sel)))), - wh_full(align_e(east!(sr, buf, lat))), + align_w(sel.map(|sel|field_h(theme, "Selected", sel))).full_wh(), + align_e(east!(sr, buf, lat)).full_wh(), ))) } @@ -200,14 +196,14 @@ pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl D pub fn button_play_pause (playing: bool, compact: bool) -> impl Draw { bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, either(compact, - thunk(move|to: &mut Tui|w_exact(9, either(playing, + thunk(move|to: &mut Tui|either(playing, fg(Rgb(0, 255, 0), " PLAYING "), - fg(Rgb(255, 128, 0), " STOPPED ")) - ).draw(to)), - thunk(move|to: &mut Tui|w_exact(5, either(playing, + fg(Rgb(255, 128, 0), " STOPPED "), + ).exact_w(9).draw(to)), + thunk(move|to: &mut Tui|either(playing, fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), - fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",))) - ).draw(to)), + fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",)), + ).exact_w(5).draw(to)), ) ) } @@ -218,8 +214,13 @@ pub fn button_play_pause (playing: bool, compact: bool) -> impl Draw { button_add: impl Draw, content: impl Draw, ) -> impl Draw { - west(h_full(w_exact(4, align_nw(button_add))), - east(w_exact(20, h_full(align_nw(button))), wh_full(align_c(content)))) + west( + align_nw(button_add).exact_w(4).full_h(), + east( + align_nw(button).full_h().exact_w(20), + align_c(content).full_wh() + ) + ) } /// ``` @@ -228,8 +229,8 @@ pub fn button_play_pause (playing: bool, compact: bool) -> impl Draw { /// let _ = tek::view_wrap(bg, fg, "and then blue, too!"); /// ``` pub fn view_wrap (bg: Color, fg: Color, content: impl Draw) -> impl Draw { - let left = fg_bg(bg, Reset, w_exact(1, y_repeat("▐"))); - let right = fg_bg(bg, Reset, w_exact(1, y_repeat("▌"))); + let left = fg_bg(bg, Reset, y_repeat("▐").exact_w(1)); + let right = fg_bg(bg, Reset, y_repeat("▌").exact_w(1)); east(left, west(right, fg_bg(fg, bg, content))) } @@ -256,7 +257,7 @@ pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw + 'a { let c = if value >= 0.0 { Red } else if value >= -3.0 { Yellow } else { Green }; - south!(f, wh_exact(Some(w), Some(1), bg(c, ()))) + south!(f, bg(c, ()).exact_wh(w, 1)) } pub fn view_meters (values: &[f32;2]) -> impl Draw + use<'_> { @@ -284,14 +285,14 @@ pub fn view_sample_info_v (sample: Option<&Arc>>) -> impl Draw>>) -> impl Draw) -> impl Draw { - w_exact(12, bg(theme.darker.term, w_full(align_e(content)))) + bg(theme.darker.term, align_e(content).full_w()).exact_w(12) } pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) @@ -322,22 +323,22 @@ pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, po let ins = ports.len() as u16; let frame = Outer(true, Style::default().fg(g(96))); let iter = move||ports.iter(); - let names = iter_south(iter, move|port, index|h_full(align_w(format!(" {index} {}", port.port_name())))); + let names = iter_south(iter, move|port, index|align_w(format!(" {index} {}", port.port_name())).full_h()); let field = field_v(theme, title, names); - wh_exact(Some(20), Some(1 + ins), border(true, frame, wh_exact(Some(20), Some(1 + ins), field))) + border(true, frame, field.exact_wh(20, 1 + ins)).exact_wh(20, 1 + ins) } pub fn view_io_ports <'a, T: PortsSizes<'a>> ( fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a ) -> impl Draw + 'a { type Item<'a> = (usize, &'a Arc, &'a [Connect], usize, usize); - iter(items, move|(_index, name, connections, y, y2): Item<'a>, _| { - y_push(y as u16, h_exact((y2-y) as u16, - south(h_full(bold(true, fg_bg(fg, bg, align_w(east(" 󰣲 ", name))))), - iter(||connections.iter(), move|connect: &'a Connect, index|y_push( - index as u16, h_exact(1, align_w(bold(false, fg_bg(fg, bg, &connect.info)))) - ))))) - }) + iter(items, + move|(_index, name, connections, y, y2): Item<'a>, _| south( + bold(true, fg_bg(fg, bg, align_w(east(" 󰣲 ", name)))).full_h(), + iter(||connections.iter(), move|connect: &'a Connect, index|{ + align_w(bold(false, fg_bg(fg, bg, &connect.info)).exact_h(1)).push_y(index as u16) + }) + ).exact_h((y2 - y) as u16).push_y(y as u16)) } pub fn view_scenes_clips <'a, S: ScenesSizes<'a>> ( @@ -348,7 +349,7 @@ pub fn view_scenes_clips <'a, S: ScenesSizes<'a>> ( size: &Sizer, editing: bool, ) -> impl Draw { - let status = wh_full(align_se(fg(Green, format!("{}x{}", size.w(), size.h())))); + let status = align_se(fg(Green, format!("{}x{}", size.w(), size.h()))).full_wh(); let tracks = iter_once(tracks, move|(track_index, track, _, _), _| { let scenes = iter_once(scenes(), move|(scene_index, scene, _, _), _| { let (name, theme): (Arc, ItemTheme) = scene_name_theme(scene, track_index); @@ -357,19 +358,21 @@ pub fn view_scenes_clips <'a, S: ScenesSizes<'a>> ( let w = scene_w(track, select, track_index, editor); let y = scene_y(select, scene_index, editor); let is_selected = scene_sel(select, track_index, scene_index, editing); - wh_exact(Some(w), Some(y), below( - wh_full(Outer(true, Style::default().fg(o))), - wh_full(below( + below( + Outer(true, Style::default().fg(o)).full_wh(), + below( below( - fg_bg(o, b, wh_full("")), - wh_full(align_nw(fg_bg(f, b, bold(true, name)))), + fg_bg(o, b, "".full_wh()), + align_nw(fg_bg(f, b, bold(true, name))).full_wh(), ), - wh_full(when(is_selected, editor.map(|e|e.view()))))))) + when(is_selected, editor.map(|e|e.view())).full_wh() + ).full_wh() + ).exact_wh(w, y) }); - w_exact(track.width as u16, h_full(scenes)) + scenes.full_h().exact_w(track.width as u16) }); - return size.of(wh_full(above(status, tracks))); + return size.of(above(status, tracks).full_wh()); fn scene_name_theme (scene: &Scene, track_index: usize) -> (Arc, ItemTheme) { if let Some(Some(clip)) = &scene.clips.get(track_index) { @@ -436,42 +439,48 @@ pub fn view_track_names ( button_2("T", "+", false), button_2("S", "+", false)); view_track_row_section(theme, button, button_2, bg(theme.darker.term, - h_exact(2, thunk(|to: &mut Tui|{ + thunk(|to: &mut Tui|{ for (index, track, x1, _x2) in tracks { - x_push(x1 as u16, w_exact(track_width(index, track), - bg(if selected.track() == Some(index) { - track.color.light.term - } else { - track.color.base.term - }, south(w_full(align_nw(east( - format!("·t{index:02} "), - fg(Rgb(255, 255, 255), bold(true, &track.name)) - ))), ""))) ).draw(to)?; + let b = if selected.track() == Some(index) { + track.color.light.term + } else { + track.color.base.term + }; + bg(b, south(align_nw(east( + format!("·t{index:02} "), + fg(Rgb(255, 255, 255), bold(true, &track.name)) + )).full_w(), "")) + .exact_w(track_width(index, track)) + .push_x(x1 as u16) + .draw(to)?; } Ok(Some(XYWH(0, 0, 0, 0))) - })))) + }).exact_h(2))) } pub fn view_track_outputs ( theme: ItemTheme, tracks: impl TracksSizes<'_>, midi_outs: impl Iterator, ) -> impl Draw { view_track_row_section(theme, - south(w_full(align_w(button_2("o", "utput", false))), + south(align_w(button_2("o", "utput", false)).full_w(), thunk(|to: &mut Tui|{ for port in midi_outs { - w_full(align_w(port.port_name())).draw(to); + align_w(port.port_name()).full_w().draw(to); } Ok(Some(XYWH(0, 0, 0, 0))) })), button_2("O", "+", false), bg(theme.darker.term, align_w(thunk(|to: &mut Tui|{ for (index, track, _x1, _x2) in tracks { + let f = Rgb(255, 255, 255); + let b = track.color.dark.term; let iter = ||track.sequencer.midi_outs.iter(); - let draw = |port: &MidiOutput, _|fg(Rgb(255, 255, 255), - h_exact(1, bg(track.color.dark.term, w_full(align_w( - format!("·o{index:02} {}", port.port_name())))))); - w_exact(track_width(index, track), - align_nw(h_full(iter_south(iter, draw)))).draw(to)?; + let draw = |port: &MidiOutput, _|fg(f, bg(b, align_w( + format!("·o{index:02} {}", port.port_name()).full_w() + )).exact_h(1)); + align_nw(iter_south(iter, draw).full_h()) + .exact_w(track_width(index, track)) + .draw(to)?; } Ok(Some(XYWH(0, 0, 0, 0))) })))) @@ -483,18 +492,17 @@ pub fn view_track_inputs ( view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), bg(theme.darker.term, align_w(thunk(move|to: &mut Tui|{ for (index, track, _x1, _x2) in tracks { - wh_exact(Some(track_width(index, track)), Some(height + 1), - align_nw(south( - bg(track.color.base.term, - w_full(align_w(east!( - either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "), - either(track.sequencer.recording, fg(Red, "●rec "), "·rec "), - either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "), - )))), - iter_south(||track.sequencer.midi_ins.iter(), - |port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term, - w_full(align_w(format!("·i{index:02} {}", port.port_name())))))))) - .draw(to)?; + align_nw(south( + bg(track.color.base.term, + align_w(east!( + either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "), + either(track.sequencer.recording, fg(Red, "●rec "), "·rec "), + either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "), + )).full_w()), + iter_south(||track.sequencer.midi_ins.iter(), + |port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term, + align_w(format!("·i{index:02} {}", port.port_name())).full_w())) + )).exact_wh(track_width(index, track), height + 1).draw(to)?; } Ok(Some(XYWH(0, 0, 0, 0))) })))) @@ -506,12 +514,12 @@ pub fn view_scenes_names ( editor: Option<&MidiEditor>, editing: bool, ) -> impl Draw { - w_exact(20, thunk(move |to: &mut Tui|{ + thunk(move |to: &mut Tui|{ for (index, scene, ..) in scenes { view_scene_name(select, editor, index, scene, editing).draw(to)?; } Ok(Some(XYWH(1, 1, 1, 1))) - })) + }).exact_w(20) } pub fn view_scene_name ( @@ -526,18 +534,18 @@ pub fn view_scene_name ( } else { H_SCENE as u16 }; - let a = w_full(align_w(east(format!("·s{index:02} "), - fg(g(255), bold(true, &scene.name))))); - let b = when(select.scene() == Some(index) && editing, - wh_full(align_nw(south( - editor.as_ref().map(|e|e.clip_status()), - editor.as_ref().map(|e|e.edit_status()))))); + let a = align_w(east(format!("·s{index:02} "), + fg(g(255), bold(true, &scene.name)))).full_w(); + let b = when(select.scene() == Some(index) && editing, align_nw(south( + editor.as_ref().map(|e|e.clip_status()), + editor.as_ref().map(|e|e.edit_status()))).full_wh()); let c = if select.scene() == Some(index) { scene.color.light.term } else { scene.color.base.term }; - wh_exact(Some(20), Some(h), bg(c, align_nw(south(a, b)))) + bg(c, align_nw(south(a, b))) + .exact_wh(20, h) } pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { @@ -561,10 +569,11 @@ pub fn view_track_per <'a, T: Draw + 'a, U: TracksSizes<'a>> ( callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a ) -> impl Draw { iter_east(tracks, move|(index, track, x1, x2): (usize, &Track, usize, usize), _|{ - w_exact((x2 - x1) as u16, fg_bg( + fg_bg( track.color.lightest.term, track.color.base.term, - callback(index, track))) + callback(index, track) + ).exact_w((x2 - x1) as u16) }) } @@ -573,27 +582,34 @@ pub fn view_per_track () -> impl Draw {} pub fn view_per_track_top () -> impl Draw {} pub fn view_inputs (tracks: impl TracksSizes<'_>, midi_ins: &[MidiInput]) -> impl Draw { - let title_1 = wh_exact(Some(20), Some(1), align_w(button_3("i", "nput ", format!("{}", midi_ins.len()), false))); - let title_2 = wh_exact(Some(4), Some(1), w_exact(4, button_2("I", "+", false))); + let title_1 = align_w(button_3("i", "nput ", format!("{}", midi_ins.len()), false)).exact_wh(20, 1); + let title_2 = button_2("I", "+", false).exact_wh(4, 1); east(title_1, west(title_2, thunk(move|to: &mut Tui|{ for (_index, track, x1, _x2) in tracks { south( - x_push(x1 as u16, bg(track.color.dark.term, align_w(w_exact(track.width as u16, east!( + bg(track.color.dark.term, align_w(east!( either(track.sequencer.monitoring, fg(Green, "mon "), "mon "), either(track.sequencer.recording, fg(Red, "rec "), "rec "), either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "), - ))))), + ).exact_w(track.width as u16))).push_x(x1 as u16), thunk(move |to: &mut Tui|{ for (index, port) in midi_ins.iter().enumerate() { - x_push(index as u16 * 10, h_exact(1, east(w_exact(20, align_w(east(" ● ", bold(true, fg(Rgb(255,255,255), port.port_name()))))), - west(w_exact(4, ()), thunk(move|to: &mut Tui|{ - bg(track.color.darker.term, align_w(w_exact(track.width as u16, east!( - either(track.sequencer.monitoring, fg(Green, " ● "), " · "), - either(track.sequencer.recording, fg(Red, " ● "), " · "), - either(track.sequencer.overdub, fg(Yellow, " ● "), " · "), - )))).draw(to); - todo!() - }))))).draw(to); + east( + align_w(east( + " ● ", + bold(true, fg(Rgb(255,255,255), port.port_name())) + )).exact_w(20), + west( + ().exact_w(4), + thunk(move|to: &mut Tui|{ + bg(track.color.darker.term, align_w(east!( + either(track.sequencer.monitoring, fg(Green, " ● "), " · "), + either(track.sequencer.recording, fg(Red, " ● "), " · "), + either(track.sequencer.overdub, fg(Yellow, " ● "), " · "), + ).exact_w(track.width as u16))).draw(to); + todo!() + })) + ).push_x(index as u16 * 10).exact_h(1).draw(to); } todo!() }) @@ -611,52 +627,47 @@ pub fn view_outputs ( ) -> impl Draw { let list = south( - h_exact(1, w_full(align_w(button_3( + align_w(button_3( "o", "utput", format!("{}", midi_outs.len()), false - )))), - h_exact(height - 1, wh_full(align_nw(thunk(|to: &mut Tui|{ + )).full_w().exact_h(1), + align_nw(thunk(|to: &mut Tui|{ for (_index, port) in midi_outs.iter().enumerate() { - h_exact(1,w_full(east( + east( align_w(east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name())))), - w_full(align_e(format!("{}/{} ", + align_e(format!("{}/{} ", port.port().get_connections().len(), - port.connections.len())))))).draw(to)?; + port.connections.len()) + ).full_w().exact_h(1)).full_w().draw(to)?; for (index, conn) in port.connections.iter().enumerate() { - h_exact(1, w_full(align_w(format!(" c{index:02}{}", conn.info())))) - .draw(to)?; + align_w(format!(" c{index:02}{}", conn.info())).full_w().exact_h(1).draw(to)?; } } todo!(); - })))) + })).full_wh().exact_h(height - 1) ); - h_exact(height, view_track_row_section(theme, list, button_2("O", "+", false), - bg(theme.darker.term, align_w(w_full( - thunk(|to: &mut Tui|{ - for (index, track, _x1, _x2) in tracks { - w_exact(track_width(index, track), - thunk(|to: &mut Tui|{ - h_exact(1, align_w(east( - either(true, fg(Green, "play "), "play "), - either(false, fg(Yellow, "solo "), "solo "), - ))).draw(to)?; - for (_index, port) in midi_outs.iter().enumerate() { - h_exact(1, align_w(east( - either(true, fg(Green, " ● "), " · "), - either(false, fg(Yellow, " ● "), " · "), - ))).draw(to)?; - for (_index, _conn) in port.connections.iter().enumerate() { - h_exact(1, w_full("")).draw(to)?; - } - } - todo!() - }) - ).draw(to); - } - todo!() - }) - ))) - )) + view_track_row_section(theme, list, button_2("O", "+", false), + bg(theme.darker.term, align_w(thunk(|to: &mut Tui|{ + for (index, track, _x1, _x2) in tracks { + thunk(|to: &mut Tui|{ + align_w(east( + either(true, fg(Green, "play "), "play "), + either(false, fg(Yellow, "solo "), "solo "), + )).exact_h(1).draw(to)?; + for (_index, port) in midi_outs.iter().enumerate() { + align_w(east( + either(true, fg(Green, " ● "), " · "), + either(false, fg(Yellow, " ● "), " · "), + )).exact_h(1).draw(to)?; + for (_index, _conn) in port.connections.iter().enumerate() { + "".full_w().exact_h(1).draw(to)?; + } + } + todo!() + }).exact_w(track_width(index, track)).draw(to); + } + todo!() + }).full_w()))).exact_h(height) } pub fn view_track_devices ( @@ -668,24 +679,25 @@ pub fn view_track_devices ( view_track_row_section(theme, button_3("d", "evice", format!("{}", track.map(|t|t.devices.len()).unwrap_or(0)), false), button_2("D", "+", false), - iter_once(tracks, move|(_, track, _x1, _x2), index| wh_exact( - Some(track_width(index, track)), - Some(h + 1), - bg(track.color.dark.term, align_nw(iter_south(move||0..h, - |_, _index|wh_exact(Some(track.width as u16), Some(2), - fg_bg( - ItemTheme::G[32].lightest.term, - ItemTheme::G[32].dark.term, - align_nw(format!(" · {}", "--")) - ) - ))))))) + iter_once(tracks, move|(_, track, _x1, _x2), index|bg( + track.color.dark.term, + align_nw(iter_south(move||0..h, + |_, _index|fg_bg( + ItemTheme::G[32].lightest.term, + ItemTheme::G[32].dark.term, + align_nw(format!(" · {}", "--")) + ).exact_wh(track.width as u16, 2) + ))).exact_wh( + Some(track_width(index, track)), + Some(h + 1), + ))) } pub fn per_track <'a, T: Draw + 'a, U: TracksSizes<'a>> ( tracks: impl Fn() -> U + Send + Sync + 'a, callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a ) -> impl Draw + 'a { - per_track_top(tracks, move|index, track|h_full(align_y(callback(index, track)))) + per_track_top(tracks, move|index, track|align_y(callback(index, track).full_h())) } pub fn per_track_top <'a, T: Draw + 'a, U: TracksSizes<'a>> ( @@ -694,10 +706,12 @@ pub fn per_track_top <'a, T: Draw + 'a, U: TracksSizes<'a>> ( ) -> impl Draw + 'a { align_x(bg(Reset, iter_east(tracks, move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - w_exact((x2 - x1) as u16, fg_bg( + fg_bg( track.color.lightest.term, track.color.base.term, - callback(index, track))) }))) + callback(index, track) + ).exact_w((x2 - x1) as u16) + }))) } pub fn field_h (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { @@ -710,19 +724,19 @@ pub fn view_sessions () -> impl Draw { let h = 6; let w = Some(30); let f = Rgb(224, 192, 128); - h_exact(h, w_min(w, thunk(move |to: &mut Tui|{ + thunk(move |to: &mut Tui|{ for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; let y = (2 * index) as u16; let h = 2; - y_push(y, h_exact(h, w_full(bg(b, align_w(fg(f, *name)))))).draw(to)?; + bg(b, align_w(fg(f, *name))).full_w().exact_h(h).push_y(y).draw(to)?; } Ok(Some(to.area().into())) - }))) + }).min_w(w).exact_h(h) } pub fn view_browse_title (state: &App) -> impl Draw { - w_full(align_w(field_v(ItemTheme::default(), + align_w(field_v(ItemTheme::default(), match state.dialog.browser_target().unwrap() { BrowseTarget::SaveProject => "Save project:", BrowseTarget::LoadProject => "Load project:", @@ -730,8 +744,8 @@ pub fn view_browse_title (state: &App) -> impl Draw { BrowseTarget::ExportSample(_) => "Export sample:", BrowseTarget::ImportClip(_) => "Import clip:", BrowseTarget::ExportClip(_) => "Export clip:", - }, h_exact(1, fg(g(96), x_repeat("🭻"))) - ))) + }, fg(g(96), x_repeat("🭻")).exact_h(1) + )).full_w() } pub fn view_device (state: &App) -> impl Draw { @@ -742,5 +756,6 @@ pub fn view_device (state: &App) -> impl Draw { let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; let l = if i == selected { "[ " } else { " " }; let r = if i == selected { " ]" } else { " " }; - w_full(bg(b, east(l, west(r, "FIXME device name")))) })) + bg(b, east(l, west(r, "FIXME device name"))).full_w() + })) } diff --git a/src/device/browse.rs b/src/device/browse.rs index f8a196b8..5210a81a 100644 --- a/src/device/browse.rs +++ b/src/device/browse.rs @@ -241,7 +241,7 @@ impl<'a> PoolView<'a> { //let on_bg = |x|x;//below(Repeat(" "), bg(color.darkest.term, x)); //let border = |x|x;//Outer(Style::default().fg(color.dark.term).bg(color.darkest.term)).enclose(x); //let height = pool.clips.read().unwrap().len() as u16; - w_exact(20, h_full(origin_n(iter( + origin_n(iter( ||pool.clips().clone().into_iter(), move|clip: Arc>, i: usize|{ let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); @@ -252,13 +252,13 @@ impl<'a> PoolView<'a> { let f = color.lightest.term; let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; let length = if false { String::default() } else { format!("{length} ") }; - h_exact(1, bg(b, below!( - w_full(origin_w(fg(f, bold(selected, name)))), - w_full(origin_e(fg(f, bold(selected, length)))), - w_full(origin_w(when(selected, bold(true, fg(g(255), "▶"))))), - w_full(origin_e(when(selected, bold(true, fg(g(255), "◀"))))), - ))) - })))) + bg(b, below!( + origin_w(fg(f, bold(selected, name))).full_w(), + origin_e(fg(f, bold(selected, length))).full_w(), + origin_w(when(selected, bold(true, fg(g(255), "▶")))).full_w(), + origin_e(when(selected, bold(true, fg(g(255), "◀")))).full_w(), + )).exact_h(1) + })).full_h().exact_w(20) } } @@ -315,7 +315,7 @@ impl Browse { impl Browse { fn tui (&self) -> impl Draw { - let item = |entry, _index|w_full(origin_w(entry)); + let item = |entry, _index|origin_w(entry).full_w(); let iterate = ||EntriesIterator { offset: 0, index: 0, diff --git a/src/device/editor.rs b/src/device/editor.rs index 80133a4d..31000a45 100644 --- a/src/device/editor.rs +++ b/src/device/editor.rs @@ -98,17 +98,14 @@ impl MidiEditor { 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) }; - w_exact(20, south!( - w_full(origin_w(east( - button_2("f2", "name ", false), - w_full(origin_e(fg(Rgb(255, 255, 255), format!("{name} "))))))), - w_full(origin_w(east( - button_2("l", "ength ", false), - w_full(origin_e(fg(Rgb(255, 255, 255), format!("{length} "))))))), - w_full(origin_w(east( - button_2("r", "epeat ", false), - w_full(origin_e(fg(Rgb(255, 255, 255), format!("{looped} "))))))), - )) + south!( + origin_w(east(button_2("f2", "name ", false), + origin_e(fg(Rgb(255, 255, 255), format!("{name} "))).full_w())).full_w(), + origin_w(east(button_2("l", "ength ", false), + origin_e(fg(Rgb(255, 255, 255), format!("{length} "))).full_w())).full_w(), + origin_w(east(button_2("r", "epeat ", false), + origin_e(fg(Rgb(255, 255, 255), format!("{looped} "))).full_w())).full_w(), + ).exact_w(20) } pub fn edit_status (&self) -> impl Draw + '_ { let (_color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { @@ -121,20 +118,17 @@ impl MidiEditor { let note_name = format!("{:4}", note_pitch_to_name(note_pos)); let note_pos = format!("{:>3}", note_pos); let note_len = format!("{:>4}", self.get_note_len()); - w_exact(20, south!( - w_full(origin_w(east( - button_2("t", "ime ", false), - w_full(origin_e(fg(Rgb(255, 255, 255), - format!("{length} /{time_zoom} +{time_pos} "))))))), - w_full(origin_w(east( - button_2("z", "lock ", false), - w_full(origin_e(fg(Rgb(255, 255, 255), - format!("{time_lock}"))))))), - w_full(origin_w(east( - button_2("x", "note ", false), - w_full(origin_e(fg(Rgb(255, 255, 255), - format!("{note_name} {note_pos} {note_len}"))))))), - )) + south!( + origin_w(east(button_2("t", "ime ", false), + origin_e(fg(Rgb(255, 255, 255), + format!("{length} /{time_zoom} +{time_pos} "))).full_w())).full_w(), + origin_w(east(button_2("z", "lock ", false), + origin_e(fg(Rgb(255, 255, 255), + format!("{time_lock}"))).full_w())).full_w(), + origin_w(east(button_2("x", "note ", false), + origin_e(fg(Rgb(255, 255, 255), + format!("{note_name} {note_pos} {note_len}"))).full_w())).full_w(), + ).exact_w(20) } } diff --git a/src/device/editor/piano.rs b/src/device/editor/piano.rs index 4102953f..e3c4320a 100644 --- a/src/device/editor/piano.rs +++ b/src/device/editor/piano.rs @@ -27,8 +27,14 @@ impl_has!(Sizer: |self: PianoHorizontal| self.size); impl View for PianoHorizontal { fn view (&self) -> impl Draw { south( - east(w_exact(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), - east(self.keys(), self.size.of(below(wh_full(self.notes()), wh_full(self.cursor())))), + east( + format!("{}x{}", self.size.w(), self.size.h()).exact_w(5), + self.timeline() + ), + east( + self.keys(), + self.size.of(below(self.notes().full_wh(), self.cursor().full_wh())) + ), ) } } @@ -203,7 +209,7 @@ impl PianoHorizontal { let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); let off_style = Some(Style::default().fg(g(255))); let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.term).bold()); - h_full(w_exact(self.keys_width, thunk(move|to: &mut Tui|{ + thunk(move|to: &mut Tui|{ let xywh = to.area().into(); let XYWH(x, y0, _w, _h) = xywh; for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { @@ -218,11 +224,11 @@ impl PianoHorizontal { }; } Ok(Some(xywh)) - }))) + }).exact_w(self.keys_width).full_h() } fn timeline (&self) -> impl Draw + '_ { - w_full(h_exact(1, thunk(move|to: &mut Tui|{ + thunk(move|to: &mut Tui|{ let xywh = to.area().into(); let XYWH(x, y, w, _h) = xywh; let style = Some(Style::default().dim()); @@ -234,7 +240,7 @@ impl PianoHorizontal { } } Ok(Some(xywh)) - }))) + }).exact_h(1).full_w() } } diff --git a/src/device/meter.rs b/src/device/meter.rs index e7c493ac..fc28c19a 100644 --- a/src/device/meter.rs +++ b/src/device/meter.rs @@ -40,9 +40,9 @@ impl_draw!(|self: Log10Meter, to: Tui| { }); fn draw_meters (meters: &[f32]) -> impl Draw + use<'_> { - bg(Black, w_exact(2, iter_east_fixed(1, ||meters.iter(), |value, _index|{ - h_full(RmsMeter(*value)) - }))) + bg(Black, iter_east_fixed(1, ||meters.iter(), |value, _index|{ + RmsMeter(*value).full_h() + }).exact_w(2)) } pub fn to_log10 (samples: &[f32]) -> f32 { diff --git a/tengri b/tengri index 1f60b43f..09463649 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 1f60b43f616ff49ddd8187bcca8d31e9b6597cec +Subproject commit 09463649c673b0d883ca18746a36aa86c6bd114f From ae75019d973f689059905d84037fd7dbd9cb3bf3 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Fri, 10 Jul 2026 18:24:56 +0300 Subject: [PATCH 30/31] use origin/align methods --- src/app/draw.rs | 163 +++++++++++++++++++++---------------------- src/device/browse.rs | 21 +++--- src/device/editor.rs | 27 ++++--- tengri | 2 +- 4 files changed, 104 insertions(+), 109 deletions(-) diff --git a/src/app/draw.rs b/src/app/draw.rs index f21fbba2..3c534514 100644 --- a/src/app/draw.rs +++ b/src/app/draw.rs @@ -73,8 +73,8 @@ fn tek_draw_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Drawn) -> Drawn { match frags.next() { - Some("input") => bg(Rgb(30, 30, 30), align_s("Input Meters").full_h()).draw(to), - Some("output") => bg(Rgb(30, 30, 30), align_s("Output Meters").full_h()).draw(to), + Some("input") => bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h()).draw(to), + Some("output") => bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h()).draw(to), _ => panic!() } } @@ -83,9 +83,9 @@ pub fn draw_tracks (to: &mut Tui, mut frags: std::str::Split<&str>, state: &App) match frags.next() { None => "TODO tracks".draw(to), Some("names") => state.project.view_track_names(state.color.clone()).draw(to),//bg(Rgb(40, 40, 40), full_w(align_w("Track Names")))), - Some("inputs") => bg(Rgb(40, 40, 40), align_w("Track Inputs").full_w()).draw(to), - Some("devices") => bg(Rgb(40, 40, 40), align_w("Track Devices").full_w()).draw(to), - Some("outputs") => bg(Rgb(40, 40, 40), align_w("Track Outputs").full_w()).draw(to), + Some("inputs") => bg(Rgb(40, 40, 40), "Track Inputs".align_w().full_w()).draw(to), + Some("devices") => bg(Rgb(40, 40, 40), "Track Devices".align_w().full_w()).draw(to), + Some("outputs") => bg(Rgb(40, 40, 40), "Track Outputs".align_w().full_w()).draw(to), _ => panic!() } } @@ -108,7 +108,7 @@ pub fn draw_dialog ( for (index, MenuItem(item, _)) in items.0.iter().enumerate() { let f = if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }; let b = if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }; - fg_bg(f, b, align_n(item.full_w()).exact_h(2)) + fg_bg(f, b, item.full_w().align_w().exact_h(2)) .push_y((2 * index) as u16).draw(to)?; } Ok(Some(to.area().into())) @@ -130,9 +130,9 @@ pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App) let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); let fg1 = Rgb(224, 192, 128); let fg2 = Rgb(224, 128, 32); - let field_name = align_w(fg(fg1, name)).full_w(); - let field_id = align_e(fg(fg2, id)).full_w(); - let field_info = align_w(info).full_w(); + let field_name = fg(fg1, name).align_w().full_w(); + let field_id = fg(fg2, id).align_e().full_w(); + let field_info = info.align_w().full_w(); let _ = bg(b, south(above(field_name, field_id), field_info)) .full_w().exact_h(2).push_y((2 * index) as u16).draw(to); index += 1; @@ -162,12 +162,12 @@ pub fn view_logo () -> impl Draw { pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw { let theme = ItemTheme::G[96]; bg(Black, east!(above( - align_w(button_play_pause(play, false)).full_wh(), - align_e(east!( + button_play_pause(play, false).align_w().full_wh(), + east!( field_h(theme, "BPM", bpm), field_h(theme, "Beat", beat), field_h(theme, "Time", time), - )).full_wh() + ).align_e().full_wh() ))) } @@ -182,8 +182,8 @@ pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl D let buf = field_h(theme, "Buf", buf); let lat = field_h(theme, "Lat", lat); bg(Black, east!(above( - align_w(sel.map(|sel|field_h(theme, "Selected", sel))).full_wh(), - align_e(east!(sr, buf, lat)).full_wh(), + sel.map(|sel|field_h(theme, "Selected", sel)).align_w().full_wh(), + east!(sr, buf, lat).align_e().full_wh(), ))) } @@ -215,10 +215,10 @@ pub fn button_play_pause (playing: bool, compact: bool) -> impl Draw { content: impl Draw, ) -> impl Draw { west( - align_nw(button_add).exact_w(4).full_h(), + button_add.align_nw().exact_w(4).full_h(), east( - align_nw(button).full_h().exact_w(20), - align_c(content).full_wh() + button.align_nw().full_h().exact_w(20), + content.align_c().full_wh() ) ) } @@ -286,12 +286,12 @@ pub fn view_sample_info_v (sample: Option<&Arc>>) -> impl Draw>>) -> impl Draw) -> impl Draw { - bg(theme.darker.term, align_e(content).full_w()).exact_w(12) + bg(theme.darker.term, content.align_e().full_w()).exact_w(12) } pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) @@ -323,7 +323,7 @@ pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, po let ins = ports.len() as u16; let frame = Outer(true, Style::default().fg(g(96))); let iter = move||ports.iter(); - let names = iter_south(iter, move|port, index|align_w(format!(" {index} {}", port.port_name())).full_h()); + let names = iter_south(iter, move|port, index|format!(" {index} {}", port.port_name()).align_w().full_h()); let field = field_v(theme, title, names); border(true, frame, field.exact_wh(20, 1 + ins)).exact_wh(20, 1 + ins) } @@ -334,9 +334,9 @@ pub fn view_io_ports <'a, T: PortsSizes<'a>> ( type Item<'a> = (usize, &'a Arc, &'a [Connect], usize, usize); iter(items, move|(_index, name, connections, y, y2): Item<'a>, _| south( - bold(true, fg_bg(fg, bg, align_w(east(" 󰣲 ", name)))).full_h(), + bold(true, fg_bg(fg, bg, east(" 󰣲 ", name).align_w())).full_h(), iter(||connections.iter(), move|connect: &'a Connect, index|{ - align_w(bold(false, fg_bg(fg, bg, &connect.info)).exact_h(1)).push_y(index as u16) + bold(false, fg_bg(fg, bg, &connect.info)).exact_h(1).align_w().push_y(index as u16) }) ).exact_h((y2 - y) as u16).push_y(y as u16)) } @@ -349,7 +349,7 @@ pub fn view_scenes_clips <'a, S: ScenesSizes<'a>> ( size: &Sizer, editing: bool, ) -> impl Draw { - let status = align_se(fg(Green, format!("{}x{}", size.w(), size.h()))).full_wh(); + let status = fg(Green, format!("{}x{}", size.w(), size.h())).align_se().full_wh(); let tracks = iter_once(tracks, move|(track_index, track, _, _), _| { let scenes = iter_once(scenes(), move|(scene_index, scene, _, _), _| { let (name, theme): (Arc, ItemTheme) = scene_name_theme(scene, track_index); @@ -363,7 +363,7 @@ pub fn view_scenes_clips <'a, S: ScenesSizes<'a>> ( below( below( fg_bg(o, b, "".full_wh()), - align_nw(fg_bg(f, b, bold(true, name))).full_wh(), + fg_bg(f, b, bold(true, name)).align_nw().full_wh(), ), when(is_selected, editor.map(|e|e.view())).full_wh() ).full_wh() @@ -446,10 +446,10 @@ pub fn view_track_names ( } else { track.color.base.term }; - bg(b, south(align_nw(east( + bg(b, south(east( format!("·t{index:02} "), fg(Rgb(255, 255, 255), bold(true, &track.name)) - )).full_w(), "")) + ).align_nw().full_w(), "")) .exact_w(track_width(index, track)) .push_x(x1 as u16) .draw(to)?; @@ -462,50 +462,49 @@ pub fn view_track_outputs ( theme: ItemTheme, tracks: impl TracksSizes<'_>, midi_outs: impl Iterator, ) -> impl Draw { view_track_row_section(theme, - south(align_w(button_2("o", "utput", false)).full_w(), + south(button_2("o", "utput", false).align_w().full_w(), thunk(|to: &mut Tui|{ for port in midi_outs { - align_w(port.port_name()).full_w().draw(to); + port.port_name().align_w().full_w().draw(to); } Ok(Some(XYWH(0, 0, 0, 0))) })), button_2("O", "+", false), - bg(theme.darker.term, align_w(thunk(|to: &mut Tui|{ + bg(theme.darker.term, thunk(|to: &mut Tui|{ for (index, track, _x1, _x2) in tracks { let f = Rgb(255, 255, 255); let b = track.color.dark.term; let iter = ||track.sequencer.midi_outs.iter(); - let draw = |port: &MidiOutput, _|fg(f, bg(b, align_w( - format!("·o{index:02} {}", port.port_name()).full_w() - )).exact_h(1)); - align_nw(iter_south(iter, draw).full_h()) + let draw = |port: &MidiOutput, _|fg(f, bg(b, + format!("·o{index:02} {}", port.port_name()).full_w().align_w()).exact_h(1)); + iter_south(iter, draw).full_h().align_nw() .exact_w(track_width(index, track)) .draw(to)?; } Ok(Some(XYWH(0, 0, 0, 0))) - })))) + }).align_w())) } pub fn view_track_inputs ( theme: ItemTheme, tracks: impl TracksSizes<'_>, height: u16, ) -> impl Draw { view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), - bg(theme.darker.term, align_w(thunk(move|to: &mut Tui|{ + bg(theme.darker.term, thunk(move|to: &mut Tui|{ for (index, track, _x1, _x2) in tracks { - align_nw(south( + south( bg(track.color.base.term, - align_w(east!( + east!( either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "), either(track.sequencer.recording, fg(Red, "●rec "), "·rec "), either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "), - )).full_w()), + ).align_w().full_w()), iter_south(||track.sequencer.midi_ins.iter(), |port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term, - align_w(format!("·i{index:02} {}", port.port_name())).full_w())) - )).exact_wh(track_width(index, track), height + 1).draw(to)?; + format!("·i{index:02} {}", port.port_name()).align_w().full_w())) + ).align_nw().exact_wh(track_width(index, track), height + 1).draw(to)?; } Ok(Some(XYWH(0, 0, 0, 0))) - })))) + }).align_w())) } pub fn view_scenes_names ( @@ -534,18 +533,17 @@ pub fn view_scene_name ( } else { H_SCENE as u16 }; - let a = align_w(east(format!("·s{index:02} "), - fg(g(255), bold(true, &scene.name)))).full_w(); - let b = when(select.scene() == Some(index) && editing, align_nw(south( + let a = east(format!("·s{index:02} "), + fg(g(255), bold(true, &scene.name))).align_w().full_w(); + let b = when(select.scene() == Some(index) && editing, south( editor.as_ref().map(|e|e.clip_status()), - editor.as_ref().map(|e|e.edit_status()))).full_wh()); + editor.as_ref().map(|e|e.edit_status())).align_nw().full_wh()); let c = if select.scene() == Some(index) { scene.color.light.term } else { scene.color.base.term }; - bg(c, align_nw(south(a, b))) - .exact_wh(20, h) + bg(c, south(a, b).align_nw()).exact_wh(20, h) } pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { @@ -582,31 +580,31 @@ pub fn view_per_track () -> impl Draw {} pub fn view_per_track_top () -> impl Draw {} pub fn view_inputs (tracks: impl TracksSizes<'_>, midi_ins: &[MidiInput]) -> impl Draw { - let title_1 = align_w(button_3("i", "nput ", format!("{}", midi_ins.len()), false)).exact_wh(20, 1); + let title_1 = button_3("i", "nput ", format!("{}", midi_ins.len()), false).align_w().exact_wh(20, 1); let title_2 = button_2("I", "+", false).exact_wh(4, 1); east(title_1, west(title_2, thunk(move|to: &mut Tui|{ for (_index, track, x1, _x2) in tracks { south( - bg(track.color.dark.term, align_w(east!( + bg(track.color.dark.term, east!( either(track.sequencer.monitoring, fg(Green, "mon "), "mon "), either(track.sequencer.recording, fg(Red, "rec "), "rec "), either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "), - ).exact_w(track.width as u16))).push_x(x1 as u16), + ).exact_w(track.width as u16)).align_w().push_x(x1 as u16), thunk(move |to: &mut Tui|{ for (index, port) in midi_ins.iter().enumerate() { east( - align_w(east( + east( " ● ", bold(true, fg(Rgb(255,255,255), port.port_name())) - )).exact_w(20), + ).align_w().exact_w(20), west( ().exact_w(4), thunk(move|to: &mut Tui|{ - bg(track.color.darker.term, align_w(east!( + bg(track.color.darker.term, east!( either(track.sequencer.monitoring, fg(Green, " ● "), " · "), either(track.sequencer.recording, fg(Red, " ● "), " · "), either(track.sequencer.overdub, fg(Yellow, " ● "), " · "), - ).exact_w(track.width as u16))).draw(to); + ).exact_w(track.width as u16).align_w()).draw(to); todo!() })) ).push_x(index as u16 * 10).exact_h(1).draw(to); @@ -627,38 +625,37 @@ pub fn view_outputs ( ) -> impl Draw { let list = south( - align_w(button_3( + button_3( "o", "utput", format!("{}", midi_outs.len()), false - )).full_w().exact_h(1), - align_nw(thunk(|to: &mut Tui|{ + ).align_w().full_w().exact_h(1), + thunk(|to: &mut Tui|{ for (_index, port) in midi_outs.iter().enumerate() { east( - align_w(east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name())))), - align_e(format!("{}/{} ", + east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name()))).align_w(), + format!("{}/{} ", port.port().get_connections().len(), - port.connections.len()) - ).full_w().exact_h(1)).full_w().draw(to)?; + port.connections.len()).align_e().full_w().exact_h(1)).full_w().draw(to)?; for (index, conn) in port.connections.iter().enumerate() { - align_w(format!(" c{index:02}{}", conn.info())).full_w().exact_h(1).draw(to)?; + format!(" c{index:02}{}", conn.info()).align_w().full_w().exact_h(1).draw(to)?; } } todo!(); - })).full_wh().exact_h(height - 1) + }).align_nw().full_wh().exact_h(height - 1) ); view_track_row_section(theme, list, button_2("O", "+", false), - bg(theme.darker.term, align_w(thunk(|to: &mut Tui|{ + bg(theme.darker.term, thunk(|to: &mut Tui|{ for (index, track, _x1, _x2) in tracks { thunk(|to: &mut Tui|{ - align_w(east( + east( either(true, fg(Green, "play "), "play "), either(false, fg(Yellow, "solo "), "solo "), - )).exact_h(1).draw(to)?; + ).align_w().exact_h(1).draw(to)?; for (_index, port) in midi_outs.iter().enumerate() { - align_w(east( + east( either(true, fg(Green, " ● "), " · "), either(false, fg(Yellow, " ● "), " · "), - )).exact_h(1).draw(to)?; + ).align_w().exact_h(1).draw(to)?; for (_index, _conn) in port.connections.iter().enumerate() { "".full_w().exact_h(1).draw(to)?; } @@ -667,7 +664,7 @@ pub fn view_outputs ( }).exact_w(track_width(index, track)).draw(to); } todo!() - }).full_w()))).exact_h(height) + }).align_w().full_w())).exact_h(height) } pub fn view_track_devices ( @@ -681,13 +678,13 @@ pub fn view_track_devices ( button_2("D", "+", false), iter_once(tracks, move|(_, track, _x1, _x2), index|bg( track.color.dark.term, - align_nw(iter_south(move||0..h, + iter_south(move||0..h, |_, _index|fg_bg( ItemTheme::G[32].lightest.term, ItemTheme::G[32].dark.term, - align_nw(format!(" · {}", "--")) + format!(" · {}", "--").align_nw() ).exact_wh(track.width as u16, 2) - ))).exact_wh( + ).align_nw()).exact_wh( Some(track_width(index, track)), Some(h + 1), ))) @@ -697,21 +694,21 @@ pub fn per_track <'a, T: Draw + 'a, U: TracksSizes<'a>> ( tracks: impl Fn() -> U + Send + Sync + 'a, callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a ) -> impl Draw + 'a { - per_track_top(tracks, move|index, track|align_y(callback(index, track).full_h())) + per_track_top(tracks, move|index, track|callback(index, track).full_h().align_y()) } pub fn per_track_top <'a, T: Draw + 'a, U: TracksSizes<'a>> ( tracks: impl Fn() -> U + Send + Sync + 'a, callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a ) -> impl Draw + 'a { - align_x(bg(Reset, iter_east(tracks, + bg(Reset, iter_east(tracks, move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ fg_bg( track.color.lightest.term, track.color.base.term, callback(index, track) ).exact_w((x2 - x1) as u16) - }))) + }).align_x()) } pub fn field_h (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { @@ -729,14 +726,14 @@ pub fn view_sessions () -> impl Draw { let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; let y = (2 * index) as u16; let h = 2; - bg(b, align_w(fg(f, *name))).full_w().exact_h(h).push_y(y).draw(to)?; + bg(b, fg(f, *name).align_w()).full_w().exact_h(h).push_y(y).draw(to)?; } Ok(Some(to.area().into())) }).min_w(w).exact_h(h) } pub fn view_browse_title (state: &App) -> impl Draw { - align_w(field_v(ItemTheme::default(), + field_v(ItemTheme::default(), match state.dialog.browser_target().unwrap() { BrowseTarget::SaveProject => "Save project:", BrowseTarget::LoadProject => "Load project:", @@ -745,7 +742,7 @@ pub fn view_browse_title (state: &App) -> impl Draw { BrowseTarget::ImportClip(_) => "Import clip:", BrowseTarget::ExportClip(_) => "Export clip:", }, fg(g(96), x_repeat("🭻")).exact_h(1) - )).full_w() + ).align_w().full_w() } pub fn view_device (state: &App) -> impl Draw { diff --git a/src/device/browse.rs b/src/device/browse.rs index 5210a81a..7eccc2dd 100644 --- a/src/device/browse.rs +++ b/src/device/browse.rs @@ -241,7 +241,7 @@ impl<'a> PoolView<'a> { //let on_bg = |x|x;//below(Repeat(" "), bg(color.darkest.term, x)); //let border = |x|x;//Outer(Style::default().fg(color.dark.term).bg(color.darkest.term)).enclose(x); //let height = pool.clips.read().unwrap().len() as u16; - origin_n(iter( + iter( ||pool.clips().clone().into_iter(), move|clip: Arc>, i: usize|{ let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); @@ -253,12 +253,12 @@ impl<'a> PoolView<'a> { let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; let length = if false { String::default() } else { format!("{length} ") }; bg(b, below!( - origin_w(fg(f, bold(selected, name))).full_w(), - origin_e(fg(f, bold(selected, length))).full_w(), - origin_w(when(selected, bold(true, fg(g(255), "▶")))).full_w(), - origin_e(when(selected, bold(true, fg(g(255), "◀")))).full_w(), + fg(f, bold(selected, name)).origin_w().full_w(), + fg(f, bold(selected, length)).origin_e().full_w(), + when(selected, bold(true, fg(g(255), "▶"))).origin_w().full_w(), + when(selected, bold(true, fg(g(255), "◀"))).origin_e().full_w(), )).exact_h(1) - })).full_h().exact_w(20) + }).origin_n().full_h().exact_w(20) } } @@ -315,15 +315,16 @@ impl Browse { impl Browse { fn tui (&self) -> impl Draw { - let item = |entry, _index|origin_w(entry).full_w(); - let iterate = ||EntriesIterator { + iter_south_fixed(1, ||self.tui_entries(), |entry, _index|entry.origin_w().full_w()) + } + fn tui_entries (&self) -> EntriesIterator { + EntriesIterator { offset: 0, index: 0, length: self.dirs.len() + self.files.len(), browser: self, _screen: Default::default(), - }; - iter_south_fixed(1, iterate, item) + } } } diff --git a/src/device/editor.rs b/src/device/editor.rs index 31000a45..0abc07b5 100644 --- a/src/device/editor.rs +++ b/src/device/editor.rs @@ -99,12 +99,12 @@ impl MidiEditor { (clip.color, clip.name.clone(), clip.length, clip.looped) } else { (ItemTheme::G[64], String::new().into(), 0, false) }; south!( - origin_w(east(button_2("f2", "name ", false), - origin_e(fg(Rgb(255, 255, 255), format!("{name} "))).full_w())).full_w(), - origin_w(east(button_2("l", "ength ", false), - origin_e(fg(Rgb(255, 255, 255), format!("{length} "))).full_w())).full_w(), - origin_w(east(button_2("r", "epeat ", false), - origin_e(fg(Rgb(255, 255, 255), format!("{looped} "))).full_w())).full_w(), + east(button_2("f2", "name ", false), fg(Rgb(255, 255, 255), format!("{name} ")) + .origin_e().full_w()).origin_w().full_w(), + east(button_2("l", "ength ", false), fg(Rgb(255, 255, 255), format!("{length} ")).origin_e().full_w()) + .origin_w().full_w(), + east(button_2("r", "epeat ", false), fg(Rgb(255, 255, 255), format!("{looped} ")).origin_e().full_w()) + .origin_w().full_w(), ).exact_w(20) } pub fn edit_status (&self) -> impl Draw + '_ { @@ -119,15 +119,12 @@ impl MidiEditor { let note_pos = format!("{:>3}", note_pos); let note_len = format!("{:>4}", self.get_note_len()); south!( - origin_w(east(button_2("t", "ime ", false), - origin_e(fg(Rgb(255, 255, 255), - format!("{length} /{time_zoom} +{time_pos} "))).full_w())).full_w(), - origin_w(east(button_2("z", "lock ", false), - origin_e(fg(Rgb(255, 255, 255), - format!("{time_lock}"))).full_w())).full_w(), - origin_w(east(button_2("x", "note ", false), - origin_e(fg(Rgb(255, 255, 255), - format!("{note_name} {note_pos} {note_len}"))).full_w())).full_w(), + east(button_2("t", "ime ", false), + fg(Rgb(255, 255, 255), format!("{length} /{time_zoom} +{time_pos} ")).origin_e().full_w()).origin_w().full_w(), + east(button_2("z", "lock ", false), + fg(Rgb(255, 255, 255), format!("{time_lock}")).origin_e().full_w()).origin_w().full_w(), + east(button_2("x", "note ", false), + fg(Rgb(255, 255, 255), format!("{note_name} {note_pos} {note_len}")).origin_e().full_w()).origin_w().full_w(), ).exact_w(20) } } diff --git a/tengri b/tengri index 09463649..13c886d9 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 09463649c673b0d883ca18746a36aa86c6bd114f +Subproject commit 13c886d9e05a8df712461d44025196ae9be51b78 From 809b8e46cdbe53a1341e77678901471e1420d209 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Fri, 10 Jul 2026 19:42:19 +0300 Subject: [PATCH 31/31] use ssh clone urls --- .gitmodules | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index 04bb5796..4c548c4c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,13 +1,13 @@ [submodule "rust-jack"] path = rust-jack - url = https://codeberg.org/unspeaker/rust-jack + url = ssh://git@codeberg.org:unspeaker/rust-jack.git branch = timebase [submodule "tengri"] path = tengri - url = https://codeberg.org/unspeaker/tengri + url = ssh://git@codeberg.org:unspeaker/tengri.git [submodule "deps/rust-jack"] path = tengri/rust-jack - url = https://codeberg.org/unspeaker/rust-jack + url = ssh://git@codeberg.org:unspeaker/rust-jack.git [submodule "deps/dizzle"] path = tengri/dizzle url = ssh://git@codeberg.org/unspeaker/dizzle.git