diff --git a/src/proj_device.rs b/src/device.rs similarity index 100% rename from src/proj_device.rs rename to src/device.rs diff --git a/src/midi_edit.rs b/src/midi_edit.rs index bb08e820..355e8632 100644 --- a/src/midi_edit.rs +++ b/src/midi_edit.rs @@ -115,12 +115,8 @@ impl MidiEditor { 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 (&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) } @@ -140,28 +136,16 @@ impl MidiEditor { } } -/// ``` -/// use tek::{*, tengri::*}; -/// -/// struct Test(Option); -/// impl_as_ref!(Option: |self: Test|&self.0); -/// impl_as_mut!(Option: |self: Test|&mut self.0); -/// -/// 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(); -/// ``` - impl MidiPoint for T {} impl MidiRange for T {} pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync { + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize); + fn redraw (&self); + /// Make sure cursor is within note range fn autoscroll (&self) { let note_pos = self.get_note_pos().min(127); @@ -173,6 +157,7 @@ pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync { 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().load(Relaxed) { @@ -216,6 +201,7 @@ pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync { //} //self.time_zoom().set(time_zoom); } + } impl_has!(Sizer: |self: MidiEditor| self.size); @@ -244,59 +230,173 @@ impl MidiViewer for MidiEditor { } } -impl App { - /// 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); - if value.unwrap_or_else(||!self.editor().is_some()) { - self.toggle_editor_on() - } else { - self.toggle_editor_off() - } - } +/// 12 piano keys, some highlighted. +/// +/// ``` +/// let keys = tek::OctaveVertical::default(); +/// ``` +#[derive(Copy, Clone)] +pub struct OctaveVertical { + pub on: [bool; 12], + pub colors: [Color; 3] +} - fn toggle_editor_on (&mut self) { - // Create new clip in pool when entering empty cell - if let Selection::TrackClip { track: track_index, scene } = *self.selection() - && let Some(scene) = self.project.scenes.get_mut(scene) - && scene.clips.get(track_index).is_none() - && let Some(track) = self.project.tracks.get_mut(track_index) - { - let color = track.color.base.mix(scene.color.base, 0.5); - let color = ItemColor::random_near(color, 0.2).into(); - let clip: MidiClip = MidiClip { color, ..Default::default() }; - let clip = self.project.clips.append(Some(clip)); - // autocolor: new clip colors from scene and track color - if let Some(editor) = &mut self.project.editor { - *editor.mode.clip.write() = clip.0.read().clone(); - editor.mode.color = clip.0.read().as_ref().map(|p|p.read().color) - .unwrap_or(ItemTheme::G[64]); - } - *scene.clips.slot(track_index).write() = clip.0.read().clone(); - //Some(clip) - } else { - //None - } - } - - fn toggle_editor_off (&mut self) { - if let Selection::TrackClip { track, scene } = *self.selection() - && let Some(scene) = self.project.scenes.get_mut(scene) - { - // Remove clip from arrangement when exiting empty clip editor - let mut swapped = None; - if let Some(count) = scene.clips.slot(track).read().as_ref().map(|c|c.read().count_midi_messages()) - && count == 0 - { - std::mem::swap(&mut swapped, &mut scene.clips.slot(track).write()); - } - if let Some(clip) = swapped { - self.project.clips.remove(&*clip.read()); - } - } +impl OctaveVertical { + pub 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_default!(OctaveVertical: Self { + on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] +}); + +/// A clip, rendered as a horizontal piano roll. +/// +/// Uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ +/// +/// ``` +/// let piano = tek::PianoHorizontal::default(); +/// ``` +#[derive(Clone, Default)] pub struct PianoHorizontal { + pub clip: ClipSlot, + /// Buffer where the whole clip is rerendered on change + pub buffer: Arc>, + /// Size of actual notes area + pub size: Sizer, + /// 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!(Sizer: |self: PianoHorizontal| self.size); + +impl PianoHorizontal { + pub fn new () -> Self { + let size = Sizer::default(); + let mut range = MidiSelection::from((12, true)); + range.time_axis = size.0.clone(); + range.note_axis = size.1.clone(); + let piano = Self { keys_width: 5, size, range, ..Default::default() }; + piano.redraw(); + piano + } +} + +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 { + /// Determine the required space to render the clip. + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { + let zoom = self.range.time_zoom().load(Relaxed); + let size = if zoom > 0 { clip.length / zoom } else { 0 }; + (size, 128) + } + /// Draw the piano roll into the buffer. + fn redraw (&self) { + *self.buffer.write() = self.clip.map_inner(|clip|{ + let buf_size = self.buffer_size(&clip); + let mut buffer = BigBuffer::from(buf_size); + let time_zoom = self.get_time_zoom().max(1); + self.time_len().store(clip.length, Relaxed); + let note_len = self.get_note_len(); + let note_pos = self.get_note_pos(); + let time_pos = self.get_time_pos(); + draw_piano_fg(&mut buffer, &clip, time_zoom); + buffer + }).unwrap_or_default() + } +} + +impl std::fmt::Debug for PianoHorizontal { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + let buffer = self.buffer.read(); + f.debug_struct("PianoHorizontal") + .field("time_zoom", &self.range.time_zoom) + .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) + .finish() + } +} + +//impl App { + ///// 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); + //if value.unwrap_or_else(||!self.editor().is_some()) { + //self.toggle_editor_on() + //} else { + //self.toggle_editor_off() + //} + //} + + //fn toggle_editor_on (&mut self) { + //// Create new clip in pool when entering empty cell + //if let Selection::TrackClip { track: track_index, scene } = *self.selection() + //&& let Some(scene) = self.project.scenes.get_mut(scene) + //&& scene.clips.get(track_index).is_none() + //&& let Some(track) = self.project.tracks.get_mut(track_index) + //{ + //let color = track.color.base.mix(scene.color.base, 0.5); + //let color = ItemColor::random_near(color, 0.2).into(); + //let clip: MidiClip = MidiClip { color, ..Default::default() }; + //let clip = self.project.clips.append(Some(clip)); + //// autocolor: new clip colors from scene and track color + //if let Some(editor) = &mut self.project.editor { + //*editor.mode.clip.write() = clip.0.read().clone(); + //editor.mode.color = clip.0.read().as_ref().map(|p|p.read().color) + //.unwrap_or(ItemTheme::G[64]); + //} + //*scene.clips.slot(track_index).write() = clip.0.read().clone(); + ////Some(clip) + //} else { + ////None + //} + //} + + //fn toggle_editor_off (&mut self) { + //if let Selection::TrackClip { track, scene } = *self.selection() + //&& let Some(scene) = self.project.scenes.get_mut(scene) + //{ + //// Remove clip from arrangement when exiting empty clip editor + //let mut swapped = None; + //if let Some(count) = scene.clips.slot(track).read().as_ref().map(|c|c.read().count_midi_messages()) + //&& count == 0 + //{ + //std::mem::swap(&mut swapped, &mut scene.clips.slot(track).write()); + //} + //if let Some(clip) = swapped { + //self.project.clips.remove(&*clip.read()); + //} + //} + //} +//} diff --git a/src/midi_octave.rs b/src/midi_octave.rs deleted file mode 100644 index 5d3d15ed..00000000 --- a/src/midi_octave.rs +++ /dev/null @@ -1,23 +0,0 @@ -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 { - pub 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_default!(OctaveVertical: Self { - on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] -}); diff --git a/src/midi_piano.rs b/src/midi_piano.rs deleted file mode 100644 index 79a26af8..00000000 --- a/src/midi_piano.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::*; - -/// A clip, rendered as a horizontal piano roll. -/// -/// ``` -/// let piano = tek::PianoHorizontal::default(); -/// ``` -#[derive(Clone, Default)] pub struct PianoHorizontal { - pub clip: ClipSlot, - /// Buffer where the whole clip is rerendered on change - pub buffer: Arc>, - /// Size of actual notes area - pub size: Sizer, - /// 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!(Sizer: |self: PianoHorizontal| self.size); - -impl PianoHorizontal { - pub fn new () -> Self { - let size = Sizer::default(); - let mut range = MidiSelection::from((12, true)); - range.time_axis = size.0.clone(); - range.note_axis = size.1.clone(); - let piano = Self { keys_width: 5, size, range, ..Default::default() }; - piano.redraw(); - piano - } -} - -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 { - /// 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() = self.clip.map_inner(|clip|{ - 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 - }).unwrap_or_default() - } -} - -impl std::fmt::Debug for PianoHorizontal { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - let buffer = self.buffer.read(); - f.debug_struct("PianoHorizontal") - .field("time_zoom", &self.range.time_zoom) - .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) - .finish() - } -} diff --git a/src/proj_mixer.rs b/src/mixer.rs similarity index 100% rename from src/proj_mixer.rs rename to src/mixer.rs diff --git a/src/proj.rs b/src/proj.rs index 99bb095c..bea3a92d 100644 --- a/src/proj.rs +++ b/src/proj.rs @@ -48,6 +48,7 @@ use crate::*; /// Scroll offset of scenes #[cfg(feature = "scene")] pub scene_scroll: usize, } + impl Project { /// Create a new arrangement. pub fn new ( @@ -104,7 +105,7 @@ impl Project { //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } } -impl HasJack<'static> for Project { fn jack (&self) -> &Jack<'static> { &self.jack } } + impl_has!(Jack<'static>: |self: Project| self.jack); impl_has!(Sizer: |self: Project| self.size); impl_has!(Clock: |self: Project| self.clock); @@ -114,6 +115,13 @@ impl_as_ref_opt!(Scene: |self: Project| self.selected_scene()); impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut()); impl_as_ref!(Option: |self: Project| &self.editor); impl_as_mut!(Option: |self: Project| &mut self.editor); + +impl HasJack<'static> for Project { + fn jack (&self) -> &Jack<'static> { + &self.jack + } +} + impl HasClip for Project { fn clip_slot (&self) -> Option> { match self.selection { @@ -126,6 +134,25 @@ impl HasClip for Project { } } } + +impl HasClipsSize for Project { + fn clips_size (&self) -> &Sizer { + &self.size_inner + } +} + +impl ScenesView for Project { + 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 fn swap_value ( target: &mut T, value: &T, returned: impl Fn(T)->U ) -> Perhaps { @@ -137,6 +164,7 @@ pub fn swap_value ( Ok(Some(returned(value))) } } + pub fn toggle_bool ( target: &mut bool, value: &Option, returned: impl Fn(Option)->U ) -> Perhaps { @@ -148,6 +176,7 @@ pub fn toggle_bool ( Ok(Some(returned(Some(value)))) } } + /// Modes for clip pool #[derive(Debug, Clone)] pub enum PoolMode { /// Renaming a pattern @@ -159,6 +188,7 @@ pub fn toggle_bool ( /// Save clip to disk Export(usize, Browse), } + /// Focused field of `ClipLength` #[derive(Copy, Clone, Debug)] pub enum ClipLengthFocus { /// Editing the number of bars @@ -168,6 +198,7 @@ pub fn toggle_bool ( /// Editing the number of ticks Tick, } + impl ClipLengthFocus { pub fn next (&mut self) { use ClipLengthFocus::*; @@ -178,6 +209,7 @@ impl ClipLengthFocus { *self = match self { Bar => Tick, Beat => Bar, Tick => Beat, } } } + /// Displays and edits clip length. #[derive(Clone, Debug, Default)] pub struct ClipLength { /// Pulses per beat (quaver) @@ -189,6 +221,7 @@ impl ClipLengthFocus { /// Selected subdivision pub focus: Option, } + impl ClipLength { pub fn _new (pulses: usize, focus: Option) -> Self { Self { ppq: PPQ, bpb: 4, pulses, focus } @@ -232,6 +265,205 @@ impl HasSceneScroll for Project { fn scene_scroll (&self) -> usize { self.scene_scroll } } +def_sizes_iter!(PortsSizes => Arc, [Connect]); + +impl_has!(Vec: |self: Project| self.midi_ins); +impl_has!(Vec: |self: Project| self.midi_outs); +impl_has!(Vec: |self: App|self.project.midi_ins); +impl_has!(Vec: |self: App|self.project.midi_outs); + +pub struct Junction(T); + +impl Draw for Junction { + fn draw (&self, to: &mut Tui) -> Drawn { + T::KIND.draw(to) + } +} + +impl_as_ref_opt!(Track: |self: Project| self.selected_track()); +impl_as_mut_opt!(Track: |self: Project| self.selected_track_mut()); +impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt()); +impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt()); + +/// Represents the current user selection in the arranger +#[derive(PartialEq, Clone, Copy, Debug, Default)] +pub enum Selection { + #[default] + /// Nothing is selected + Nothing, + /// The whole mix is selected + Mix, + /// A MIDI input is selected. + Input(usize), + /// A MIDI output is selected. + Output(usize), + /// A clip is selected. + Clip(usize), + /// A sample is selected. + Sample(usize), + /// A scene is selected. + #[cfg(feature = "scene")] Scene(usize), + /// A track is selected. + #[cfg(feature = "track")] Track(usize), + /// A clip slot (track × scene) is selected. + #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, + /// A track's MIDI input connection is selected. + #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, + /// A track's MIDI output connection is selected. + #[cfg(feature = "track")] TrackOutput { track: usize, port: usize }, + /// A track device slot is selected. + #[cfg(feature = "track")] TrackDevice { track: usize, device: usize }, +} + +impl Selection { + + pub fn describe ( + &self, + #[cfg(feature = "track")] tracks: &[Track], + #[cfg(feature = "scene")] scenes: &[Scene], + ) -> Arc { + use Selection::*; + format!("{}", match self { + Mix => "Everything".to_string(), + #[cfg(feature = "scene")] Scene(s) => + scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()), + #[cfg(feature = "track")] Track(t) => + tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()), + TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { + (Some(_), Some(s)) => match &s.clips.get(*track) { + None => format!("T{track} S{scene}: Empty"), + Some(clip) => format!("T{track} S{scene} C{}", + clip.as_ref().map(|clip|clip.read().name.clone()).unwrap_or_default()), + }, + _ => format!("T{track} S{scene}: Empty"), + }, + _ => todo!() + }).into() + } + +} + +#[cfg(feature = "scene")] +impl Selection { + + pub fn scene (&self) -> Option { + use Selection::*; + match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None } + } + + pub fn select_scene (&self, scene_count: usize) -> Self { + use Selection::*; + match self { + Mix | Track(_) => Scene(0), + Scene(s) => Scene((s + 1) % scene_count), + TrackClip { scene, .. } => Track(*scene), + _ => todo!(), + } + } + + pub fn select_scene_next (&self, len: usize) -> Self { + use Selection::*; + match self { + Mix => Scene(0), + Track(t) => TrackClip { track: *t, scene: 0 }, + Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix }, + TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) }, + _ => todo!() + } + } + + pub fn select_scene_prev (&self) -> Self { + use Selection::*; + match self { + Mix | Scene(0) => Mix, + Scene(s) => Scene(s - 1), + Track(t) => Track(*t), + TrackClip { track, scene: 0 } => Track(*track), + TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 }, + _ => todo!() + } + } + +} + +#[cfg(feature = "track")] +impl Selection { + + pub fn track (&self) -> Option { + use Selection::*; + if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self { + Some(*track) + } else { + None + } + } + + pub fn select_track (&self, track_count: usize) -> Self { + use Selection::*; + match self { + Mix => Track(0), + Scene(_) => Mix, + Track(t) => Track((t + 1) % track_count), + TrackClip { track, .. } => Track(*track), + _ => todo!(), + } + } + + pub fn select_track_next (&self, len: usize) -> Self { + use Selection::*; + match self { + Mix => Track(0), + Scene(s) => TrackClip { track: 0, scene: *s }, + Track(t) => if t + 1 < len { Track(t + 1) } else { Mix }, + TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) }, + _ => todo!() + } + } + + pub fn select_track_prev (&self) -> Self { + use Selection::*; + match self { + Mix => Mix, + Scene(s) => Scene(*s), + Track(0) => Mix, + Track(t) => Track(t - 1), + TrackClip { track: 0, scene } => Scene(*scene), + TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene }, + _ => todo!() + } + } + +} + +impl SelectionModel for T {} + +pub trait SelectionModel: HasSelection { + /// Get the active track + #[cfg(feature = "track")] fn selected_track (&self) -> Option<&Track> where Self: HasTracks { + let index = self.selection().track()?; + self.tracks().get(index) + } + /// Get a mutable reference to the active track + #[cfg(feature = "track")] fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks { + let index = self.selection().track()?; + self.tracks_mut().get_mut(index) + } + /// Get the active scene + #[cfg(feature = "scene")] fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes { + let index = self.selection().scene()?; + self.scenes().get(index) + } + /// Get a mutable reference to the active scene + #[cfg(feature = "scene")] fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes { + let index = self.selection().scene()?; + self.scenes_mut().get_mut(index) + } + /// Get the active clip + #[cfg(feature = "clip")] fn selected_clip (&self) -> Option> where Self: HasScenes + HasTracks { + self.selected_scene()?.clips.read().get(self.selection().track()?).cloned() + } +} + //impl_from!(Pool: |clip:&Arc>|{ //let model = Self::default(); //model.clips.try_write().unwrap().push(clip.clone()); diff --git a/src/proj_port.rs b/src/proj_port.rs deleted file mode 100644 index 5df64797..00000000 --- a/src/proj_port.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::*; -def_sizes_iter!(PortsSizes => Arc, [Connect]); -impl_has!(Vec: |self: Project| self.midi_ins); -impl_has!(Vec: |self: Project| self.midi_outs); -impl_has!(Vec: |self: App|self.project.midi_ins); -impl_has!(Vec: |self: App|self.project.midi_outs); - -pub struct Junction(T); - -impl Draw for Junction { - fn draw (&self, to: &mut Tui) -> Drawn { - T::KIND.draw(to) - } -} - -#[tek_proc::command(AudioInput)] -#[tek_proc::keyword(AudioInput)] -#[derive(Debug)] -pub enum AudioInputCmd { - Close, - Connect(Arc), -} - -#[tek_proc::command(AudioOutput)] -#[tek_proc::keyword(AudioOutput)] -#[derive(Debug)] -pub enum AudioOutputCmd { - Close, - Connect(Arc), -} - -#[tek_proc::command(MidiInput)] -#[tek_proc::keyword(MidiInput)] -#[derive(Debug)] -pub enum MidiInputCmd { - Close, - Connect(Arc), -} - -#[tek_proc::command(MidiOutput)] -#[tek_proc::keyword(MidiOutput)] -#[derive(Debug)] -pub enum MidiOutputCmd { - Close, - Connect(Arc), -} diff --git a/src/proj_select.rs b/src/proj_select.rs deleted file mode 100644 index 4098f30f..00000000 --- a/src/proj_select.rs +++ /dev/null @@ -1,185 +0,0 @@ -use crate::*; - -impl_as_ref_opt!(Track: |self: Project| self.selected_track()); -impl_as_mut_opt!(Track: |self: Project| self.selected_track_mut()); -impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt()); -impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt()); - -/// Represents the current user selection in the arranger -#[derive(PartialEq, Clone, Copy, Debug, Default)] -pub enum Selection { - #[default] - /// Nothing is selected - Nothing, - /// The whole mix is selected - Mix, - /// A MIDI input is selected. - Input(usize), - /// A MIDI output is selected. - Output(usize), - /// A clip is selected. - Clip(usize), - /// A sample is selected. - Sample(usize), - /// A scene is selected. - #[cfg(feature = "scene")] Scene(usize), - /// A track is selected. - #[cfg(feature = "track")] Track(usize), - /// A clip slot (track × scene) is selected. - #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, - /// A track's MIDI input connection is selected. - #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, - /// A track's MIDI output connection is selected. - #[cfg(feature = "track")] TrackOutput { track: usize, port: usize }, - /// A track device slot is selected. - #[cfg(feature = "track")] TrackDevice { track: usize, device: usize }, -} - -impl Selection { - - pub fn describe ( - &self, - #[cfg(feature = "track")] tracks: &[Track], - #[cfg(feature = "scene")] scenes: &[Scene], - ) -> Arc { - use Selection::*; - format!("{}", match self { - Mix => "Everything".to_string(), - #[cfg(feature = "scene")] Scene(s) => - scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()), - #[cfg(feature = "track")] Track(t) => - tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()), - TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { - (Some(_), Some(s)) => match &s.clips.get(*track) { - None => format!("T{track} S{scene}: Empty"), - Some(clip) => format!("T{track} S{scene} C{}", - clip.as_ref().map(|clip|clip.read().name.clone()).unwrap_or_default()), - }, - _ => format!("T{track} S{scene}: Empty"), - }, - _ => todo!() - }).into() - } - -} - -#[cfg(feature = "scene")] -impl Selection { - - pub fn scene (&self) -> Option { - use Selection::*; - match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None } - } - - pub fn select_scene (&self, scene_count: usize) -> Self { - use Selection::*; - match self { - Mix | Track(_) => Scene(0), - Scene(s) => Scene((s + 1) % scene_count), - TrackClip { scene, .. } => Track(*scene), - _ => todo!(), - } - } - - pub fn select_scene_next (&self, len: usize) -> Self { - use Selection::*; - match self { - Mix => Scene(0), - Track(t) => TrackClip { track: *t, scene: 0 }, - Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix }, - TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) }, - _ => todo!() - } - } - - pub fn select_scene_prev (&self) -> Self { - use Selection::*; - match self { - Mix | Scene(0) => Mix, - Scene(s) => Scene(s - 1), - Track(t) => Track(*t), - TrackClip { track, scene: 0 } => Track(*track), - TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 }, - _ => todo!() - } - } - -} - -#[cfg(feature = "track")] -impl Selection { - - pub fn track (&self) -> Option { - use Selection::*; - if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self { - Some(*track) - } else { - None - } - } - - pub fn select_track (&self, track_count: usize) -> Self { - use Selection::*; - match self { - Mix => Track(0), - Scene(_) => Mix, - Track(t) => Track((t + 1) % track_count), - TrackClip { track, .. } => Track(*track), - _ => todo!(), - } - } - - pub fn select_track_next (&self, len: usize) -> Self { - use Selection::*; - match self { - Mix => Track(0), - Scene(s) => TrackClip { track: 0, scene: *s }, - Track(t) => if t + 1 < len { Track(t + 1) } else { Mix }, - TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) }, - _ => todo!() - } - } - - pub fn select_track_prev (&self) -> Self { - use Selection::*; - match self { - Mix => Mix, - Scene(s) => Scene(*s), - Track(0) => Mix, - Track(t) => Track(t - 1), - TrackClip { track: 0, scene } => Scene(*scene), - TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene }, - _ => todo!() - } - } - -} - -impl SelectionModel for T {} - -pub trait SelectionModel: HasSelection { - /// Get the active track - #[cfg(feature = "track")] fn selected_track (&self) -> Option<&Track> where Self: HasTracks { - let index = self.selection().track()?; - self.tracks().get(index) - } - /// Get a mutable reference to the active track - #[cfg(feature = "track")] fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks { - let index = self.selection().track()?; - self.tracks_mut().get_mut(index) - } - /// Get the active scene - #[cfg(feature = "scene")] fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes { - let index = self.selection().scene()?; - self.scenes().get(index) - } - /// Get a mutable reference to the active scene - #[cfg(feature = "scene")] fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes { - let index = self.selection().scene()?; - self.scenes_mut().get_mut(index) - } - /// Get the active clip - #[cfg(feature = "clip")] fn selected_clip (&self) -> Option> where Self: HasScenes + HasTracks { - self.selected_scene()?.clips.read().get(self.selection().track()?).cloned() - } -} diff --git a/src/proj_scene.rs b/src/scene.rs similarity index 100% rename from src/proj_scene.rs rename to src/scene.rs diff --git a/src/tek.edn b/src/tek.edn index 0079dcdc..b36befbc 100644 --- a/src/tek.edn +++ b/src/tek.edn @@ -6,16 +6,21 @@ (bg (g 48) (fg (g 224) (align/c (expand/wh 2 2 (text Nobody Will Help! )))))))))) (mode Quit + (keys (@y Quit)) (view (bg (g 32) (fg (g 64) (fill/xy (bg (g 48) (fg (g 224) (align/c (expand/wh 2 2 (text Are you sure? )))))))))) (mode Save (view (bg (g 32) (fg (g 64) (fill/xy - (bg (g 48) (fg (g 224) (align/c (expand/wh 2 2 (text Select save path )))))))))) + (bg (g 48) (fg (g 224) (align/c (bar/s 1 + (expand/wh 2 2 (text Save to: )) + (text )))))))))) (mode Load (view (bg (g 32) (fg (g 64) (fill/xy - (bg (g 48) (fg (g 224) (align/c (expand/wh 2 2 (text Select load path )))))))))) + (bg (g 48) (fg (g 224) (align/c (bar/s 1 + (expand/wh 2 2 (text Load from: )) + (text )))))))))) (mode History (view (bg (g 32) (fg (g 64) (fill/xy @@ -67,8 +72,8 @@ (view GrooveSequence (bsp/e (fill/y (align/n (bsp/s Status/v EditorStatus))) (bsp/e SamplesKeys Editor))) (view Transport (bg (g 0) (pad/x 1 (bar/e 4 PlayPause (bar/e 16 Bpm (bar/e 16 Beat Time)))))) (view StatusBar (bar/e 24 (bg (g 50) (full/x (pad/x 1 (bar/s 1 (text Selected) Selected)))) - (bar/e 24 (bg (g 40) (full/x (pad/x 1 (bar/s 1 (text Mode) ModeList)))) - (bg (g 30) (full/x (pad/x 1 (bar/s 1 (text History) HistoryCount))))))) + (bar/e 24 (bg (g 42) (full/x (pad/x 1 (bar/s 1 (text Mode) ModeList)))) + (bg (g 34) (full/x (pad/x 1 (bar/s 1 (text History) HistoryCount))))))) (keys Global (@escape leave) (@f1 (enter Help)) diff --git a/src/tek.rs b/src/tek.rs index 71b67820..611f54f5 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -1,27 +1,5 @@ #![allow(clippy::unit_arg)] -mod deps; pub use self::deps::*; - -mod clock; pub use self::clock::*; - -mod midi_octave; pub use self::midi_octave::*; -mod midi_piano; pub use self::midi_piano::*; -mod midi_point; pub use self::midi_point::*; -mod midi_range; pub use self::midi_range::*; - -mod smpl; pub use self::smpl::*; -mod smpl_sample; pub use self::smpl_sample::*; -mod smpl_add; pub use self::smpl_add::*; -mod smpl_kit; pub use self::smpl_kit::*; -mod smpl_voice; pub use self::smpl_voice::*; - -mod midi_clip; pub use self::midi_clip::*; -mod midi_edit; pub use self::midi_edit::*; -mod midi_mon; pub use self::midi_mon::*; -mod midi_play; pub use self::midi_play::*; -mod midi_rec; pub use self::midi_rec::*; -mod midi_seq; pub use self::midi_seq::*; - /// Define a type alias for iterators of sized items (columns). macro_rules! def_sizes_iter { ($Type:ident => $($Item:ty),+) => { @@ -30,26 +8,46 @@ macro_rules! def_sizes_iter { } } -mod proj; pub use self::proj::*; -mod proj_device; pub use self::proj_device::*; -mod proj_mixer; pub use self::proj_mixer::*; -mod proj_port; pub use self::proj_port::*; -mod proj_scene; pub use self::proj_scene::*; -mod proj_select; pub use self::proj_select::*; -mod proj_track; pub use self::proj_track::*; +mod deps; pub use self::deps::*; +mod clock; pub use self::clock::*; +mod proj; pub use self::proj::*; +mod device; pub use self::device::*; +mod scene; pub use self::scene::*; +mod track; pub use self::track::*; +mod mixer; pub use self::mixer::*; -mod tek_bind; pub use self::tek_bind::*; -mod tek_cli; pub use self::tek_cli::*; -mod tek_conf; pub use self::tek_conf::*; -mod tek_ctrl; pub use self::tek_ctrl::*; -mod tek_has; pub use self::tek_has::*; -mod tek_mode; pub use self::tek_mode::*; -mod tek_pool; pub use self::tek_pool::*; -mod tek_view; pub use self::tek_view::*; +mod midi_clip; pub use self::midi_clip::*; +mod midi_edit; pub use self::midi_edit::*; +mod midi_mon; pub use self::midi_mon::*; +mod midi_play; pub use self::midi_play::*; +mod midi_point; pub use self::midi_point::*; +mod midi_range; pub use self::midi_range::*; +mod midi_rec; pub use self::midi_rec::*; +mod midi_seq; pub use self::midi_seq::*; -#[cfg(feature = "lv2")] mod device_lv2; #[cfg(feature = "lv2")] pub use self::device_lv2::*; -#[cfg(feature = "lv2_gui")] mod device_lv2_gui; #[cfg(feature = "lv2_gui")] pub use self::device_lv2_gui::*; -#[cfg(feature = "vst2")] mod device_vst2; #[cfg(feature = "vst2")] pub use self::device_vst2::*; +mod smpl; pub use self::smpl::*; +mod smpl_add; pub use self::smpl_add::*; +mod smpl_kit; pub use self::smpl_kit::*; +mod smpl_sample; pub use self::smpl_sample::*; +mod smpl_voice; pub use self::smpl_voice::*; + +mod tek_bind; pub use self::tek_bind::*; +mod tek_cli; pub use self::tek_cli::*; +mod tek_conf; pub use self::tek_conf::*; +mod tek_ctrl; pub use self::tek_ctrl::*; +mod tek_has; pub use self::tek_has::*; +mod tek_mode; pub use self::tek_mode::*; +mod tek_pool; pub use self::tek_pool::*; +mod tek_view; pub use self::tek_view::*; + +#[cfg(feature = "lv2")] mod device_lv2; +#[cfg(feature = "lv2")] pub use self::device_lv2::*; + +#[cfg(feature = "lv2_gui")] mod device_lv2_gui; +#[cfg(feature = "lv2_gui")] pub use self::device_lv2_gui::*; + +#[cfg(feature = "vst2")] mod device_vst2; +#[cfg(feature = "vst2")] pub use self::device_vst2::*; /// Command-line entrypoint. #[allow(unused)] @@ -370,12 +368,27 @@ impl_as_ref!(Option: |self: App|todo!()); impl_as_mut!(Option: |self: App|todo!()); impl_as_ref!(Option: |self: App|self.project.as_ref()); impl_as_mut!(Option: |self: App|self.project.as_mut()); - impl HasClip for App { fn clip_slot (&self) -> Option> { self.project.clip_slot() } } +impl HasClipsSize for App { + fn clips_size (&self) -> &Sizer { + &self.project.size_inner + } +} +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) + } +} tui_keys!(self: App, input { #[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!"); @@ -536,16 +549,10 @@ impl Dialog { ]), MenuItem::new(" Load session ", [ - AppCmd::Dialog { - dialog: Dialog::Browse( - BrowseTarget::LoadProject, - Browse::new(None).unwrap().into() - ) - //dialog: Dialog::Browse { - //title: "Load session".into(), - //filter: "\\.tek$", - //} - } + AppCmd::Dialog { dialog: Dialog::Browse( + BrowseTarget::LoadProject, + Browse::new(None).unwrap().into() + ) } ]), MenuItem::new(" Exit ", [ @@ -642,21 +649,23 @@ pub const fn wrap_dec (index: usize, count: usize) -> usize { /// let item: tek::MenuItem = Default::default(); /// ``` #[derive(Clone)] pub struct MenuItem(pub Arc, pub Arc<[AppCmd]>); + impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); + impl_default!(MenuItem: Self::new("", [])); -impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } } + +impl PartialEq for MenuItem { + fn eq (&self, other: &Self) -> bool { + self.0 == other.0 + } +} + impl MenuItem { pub fn new (name: impl AsRef, actions: [AppCmd; N]) -> Self { Self(name.as_ref().into(), Arc::new(actions)) } } -//impl AsRef> for MenuItems { - //fn as_ref (&self) -> &Arc<[MenuItem]> { - //&self.0 - //} -//} - /// Browses for files to load/save. /// /// ``` @@ -672,14 +681,6 @@ impl MenuItem { pub size: Sizer, } -//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 { SaveProject, LoadProject, @@ -688,17 +689,6 @@ impl MenuItem { ImportClip(Arc>>), ExportClip(Arc>>), } - -// Commands supported by [Browse] -//#[derive(Debug, Clone, PartialEq)] -//pub enum BrowseCommand { - //Begin, - //Cancel, - //Confirm, - //Select(usize), - //Chdir(PathBuf), - //Filter(Arc), -//} impl Browse { pub fn new (cwd: Option) -> Usually { let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; @@ -794,5 +784,32 @@ impl HasTrackScroll for App { } impl HasSceneScroll for App { - fn scene_scroll (&self) -> usize { self.project.scene_scroll() } + fn scene_scroll (&self) -> usize { + self.project.scene_scroll() + } } + +//pub(crate) struct EntriesIterator<'a, S: Screen> { + //pub browser: &'a Browse, + //pub offset: usize, + //pub length: usize, + //pub index: usize, + //_screen: std::marker::PhantomData +//} + +// Commands supported by [Browse] +//#[derive(Debug, Clone, PartialEq)] +//pub enum BrowseCommand { + //Begin, + //Cancel, + //Confirm, + //Select(usize), + //Chdir(PathBuf), + //Filter(Arc), +//} + +//impl AsRef> for MenuItems { + //fn as_ref (&self) -> &Arc<[MenuItem]> { + //&self.0 + //} +//} diff --git a/src/tek_ctrl.rs b/src/tek_ctrl.rs index b1f3590a..4cadf774 100644 --- a/src/tek_ctrl.rs +++ b/src/tek_ctrl.rs @@ -87,13 +87,13 @@ impl_from!(AppCmd: |x: TracksCmd| AppCmd::Tracks { command: x }); #[command(MidiEdit = "edit")] pub fn cmd_midi_edit (&mut self, command: MidiEditCmd) -> Perhaps { Ok(command.dispatch(self)?.map(Into::into)) } - #[command(Scene = "scene")] pub fn cmd_scene (&mut self, command: SceneCmd) -> Perhaps { + #[command(Scene = "scene")] pub fn cmd_scene (&mut self, command: SceneCmd) -> Perhaps { Ok(command.dispatch(self)?.map(Into::into)) } #[command(Scenes = "scenes")] pub fn cmd_scenes (&mut self, command: ScenesCmd) -> Perhaps { Ok(command.dispatch(self)?.map(Into::into)) } - #[command(Track = "track")] pub fn cmd_track (&mut self, command: TrackCmd) -> Perhaps { + #[command(Track = "track")] pub fn cmd_track (&mut self, command: TrackCmd) -> Perhaps { Ok(command.dispatch(self)?.map(Into::into)) } #[command(Tracks = "tracks")] pub fn cmd_tracks (&mut self, command: TracksCmd) -> Perhaps { @@ -534,13 +534,23 @@ impl MidiClipControl for T where T: HasClip &mut self ) -> Perhaps { Ok(self.clip_mut().map(|mut clip|if clip.is_none() { - *clip = Some(Default::default()); + *clip = Some(Arc::new(RwLock::new(MidiClip { + ppq: 24, length: 96, + ..Default::default() + }))); None // TODO undo } else { None @@ -698,6 +711,38 @@ impl BrowseControl for T where T: } } +#[tek_proc::command(AudioInput)] +#[tek_proc::keyword(AudioInput)] +#[derive(Debug)] +pub enum AudioInputCmd { + Close, + Connect(Arc), +} + +#[tek_proc::command(AudioOutput)] +#[tek_proc::keyword(AudioOutput)] +#[derive(Debug)] +pub enum AudioOutputCmd { + Close, + Connect(Arc), +} + +#[tek_proc::command(MidiInput)] +#[tek_proc::keyword(MidiInput)] +#[derive(Debug)] +pub enum MidiInputCmd { + Close, + Connect(Arc), +} + +#[tek_proc::command(MidiOutput)] +#[tek_proc::keyword(MidiOutput)] +#[derive(Debug)] +pub enum MidiOutputCmd { + Close, + Connect(Arc), +} + //impl //+ Namespace diff --git a/src/tek_has.rs b/src/tek_has.rs index b74506a6..48c8209d 100644 --- a/src/tek_has.rs +++ b/src/tek_has.rs @@ -5,6 +5,20 @@ pub trait HasModeStack { fn modes_mut (&mut self) -> RwLockWriteGuard<'_, Vec>>; } +/// ``` +/// use tek::{*, tengri::*}; +/// +/// struct Test(Option); +/// impl_as_ref!(Option: |self: Test|&self.0); +/// impl_as_mut!(Option: |self: Test|&mut self.0); +/// +/// 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(); +/// ``` impl HasEditor for T where T: AsRef> + AsMut> {} pub trait HasEditor: diff --git a/src/tek_view.rs b/src/tek_view.rs index c8b2fcdc..d6f11292 100644 --- a/src/tek_view.rs +++ b/src/tek_view.rs @@ -8,75 +8,73 @@ use crate::*; impl Draw for App { fn draw (&self, to: &mut Tui) -> Drawn { #[cfg(feature = "prof2")] profiling::scope!("App::draw"); - //self.perf.cycle(&mut |_|{ - self.draw_error(to)?; - self.draw_modes(to)?; - self.draw_debug(to)?; + self.perf.cycle(&mut |_|{ + draw_error(to, self.error.as_ref())?; + draw_mode_stack(to, &self)?; + view_debug(&self.perf, &self.size).draw(to)?; #[cfg(feature = "prof2")] profiling::finish_frame!(); Ok(Some(to.area().into())) - //}) + }) } } -impl App { - #[allow(unused)] fn draw_debug (&self, to: &mut Tui) -> Drawn { - Split::Above.bar(0, - Split::West.bar(16, - format!("{}/{} {} ", - self.perf.used.load(Relaxed), - self.perf.window.load(Relaxed), - self.perf.clock.raw() / 10000000).align_e(), - format!("{}x{} ", - self.size.0.load(Relaxed), - self.size.1.load(Relaxed)).align_w(), - ).align_se(), - () - //Split::East.bar(16, - //format!("{}", self.modes.read()[0]), - //format!("{}", self.modes.read()[0]) - //).align_sw() - ).draw(to) +pub fn draw_error (to: &mut Tui, error: &RwLock>>) -> Usually<()> { + if let Some(e) = error.read().as_ref() { + e.as_ref().align_c().draw(to)?; } + Ok(()) +} - fn draw_error (&self, to: &mut Tui) -> Usually<()> { - if let Some(e) = self.error.try_read().unwrap().as_ref() { - e.as_ref().align_c().draw(to)?; - } - Ok(()) - } +pub fn view_debug (perf: &PerfModel, size: &Sizer) -> impl Draw { + Split::Above.bar(0, + Split::West.bar(16, + format!("{}/{} {} ", + perf.used.load(Relaxed), + perf.window.load(Relaxed), + perf.clock.raw() / 10000000).align_e(), + format!("{}x{} ", + size.0.load(Relaxed), + size.1.load(Relaxed)).align_w(), + ).align_se(), + () + //Split::East.bar(16, + //format!("{}", self.modes.read()[0]), + //format!("{}", self.modes.read()[0]) + //).align_sw() + ) +} - fn draw_modes (&self, to: &mut Tui) -> Usually<()> { - for name in self.modes.read().iter() { - let mut error = false; - if let Some(mode) = self.config.get_mode(name).as_ref().map(Arc::clone) { - for (index, view) in mode.view.iter().enumerate() { - match (view.render)(self, to) { - Ok(None) => {}, - Ok(Some(XYWH(.., w, h))) => { - self.size.0.store(w as usize, Relaxed); - self.size.1.store(h as usize, Relaxed); - }, - Err(e) => { - let message = format!( - "Mode: {:?}\n\nLayer: #{index}\n\nError: {e}\n\nSource:\n{}", - &mode.name, - &view.source - ); - *self.error.try_write().unwrap() = Some(message.into()); - error = true; - break; - } +pub fn draw_mode_stack (to: &mut Tui, state: &App) -> Usually<()> { + for name in state.modes.read().iter() { + let mut error = false; + if let Some(mode) = state.config.get_mode(name).as_ref().map(Arc::clone) { + for (index, view) in mode.view.iter().enumerate() { + match (view.render)(state, to) { + Ok(None) => {}, + Ok(Some(XYWH(.., w, h))) => { + state.size.0.store(w as usize, Relaxed); + state.size.1.store(h as usize, Relaxed); + }, + Err(e) => { + let message = format!( + "Mode: {:?}\n\nLayer: #{index}\n\nError: {e}\n\nSource:\n{}", + &mode.name, + &view.source + ); + *state.error.try_write().unwrap() = Some(message.into()); + error = true; + break; } } - if error { - break - } else { - *self.error.try_write().unwrap() = None; - } + } + if error { + break + } else { + *state.error.try_write().unwrap() = None; } } - Ok(()) } + Ok(()) } /// Collection of custom view definitions. @@ -365,58 +363,48 @@ impl View { fn compile_word (word: Arc) -> UsuallyDrawn + Send + Sync>>> { - macro_rules! draw { - (|$state:ident|$body:expr)=>{Self::boxed(move|$state, to|$body.draw(to))}; - ($const:expr)=>{Self::boxed(move|_, to|$const.draw(to))}; - ()=>{Self::boxed(move|_, __|Ok(None))}; + macro_rules! view { + (|$state:ident|$body:expr)=>{Box::new(move|$state, to|$body.draw(to))}; + ($const:expr)=>{Box::new(move|_, to|$const.draw(to))}; + ()=>{Box::new(move|_, __|Ok(None))}; } Ok(Arc::new(match word.split("/").next() { - Some("Beat") => draw!(), - Some("Bpm") => draw!(), - Some("BrowseTitle") => draw!(|app|view_browse_title(app)), - Some("Debug") => Self::boxed(move|_, to|format!("[{:?}]", to.area()).exact_h(1).draw(to)), - Some("Device") => draw!(|app|view_device(app)), - Some("Menu") => draw!(|app|{ - if let Dialog::Menu(selected, items) = &app.dialog { - Some(iter_south(move||items.iter().enumerate().map(move|(index, MenuItem(item, _))|{ - 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, item).exact_h(2).align_x() - })).exact_w(20).align_x()) - } else { - None - } - }), - Some("Editor") => draw!("TODO editor"), - Some("MetersIn") => draw!(|___|bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h())), - Some("MetersOut") => draw!(|___|bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h())), - Some("ModeList") => draw!(|app|view_modes(app)), - Some("HistoryCount") => draw!(|app|app.history.len()), - Some("ScenesClips") => draw!(|app|view_scenes_clips(app)), - Some("ScenesNames") => draw!(|app|app.view_scenes_names()), - Some("Sessions") => draw!(|___|view_sessions()), - Some("Selected") => draw!(|app|view_selection(app)), - Some("Templates") => draw!(|app|view_templates(app)), - Some("Time") => draw!(), - Some("Tracks") => draw!("TODO tracks"), - Some("TracksDevices") => draw!(|app|app.project.view_track_devices(app.color.clone())), - Some("TracksDevicesAdd") => draw!(|app|app.project.view_track_device_add()), - Some("TracksDevicesCells") => draw!(|app|app.project.view_track_device_cells(app.tracks_devices_height())), - Some("TracksDevicesCount") => draw!(|app|app.project.view_track_device_count()), - Some("TracksInputs") => draw!(|app|app.project.view_track_inputs(app.color.clone())), - Some("TracksInputsAdd") => draw!(|app|app.project.view_track_input_add()), - Some("TracksInputsCells") => draw!(|app|app.project.view_track_input_cells(app.tracks_inputs_height())), - Some("TracksInputsCount") => draw!(|app|app.project.view_track_input_count()), - Some("TracksNames") => draw!(|app|app.project.view_track_names(app.color.clone())), - Some("TracksNamesAdd") => draw!(|app|app.project.view_track_names_add()), - Some("TracksNamesCells") => draw!(|app|app.project.view_track_names_cells()), - Some("TracksNamesCount") => draw!(|app|app.project.view_track_names_count()), - Some("TracksOutputs") => draw!(|app|app.project.view_track_outputs(app.color.clone(), 0)), - Some("TracksOutputsAdd") => draw!(|app|app.project.view_track_output_add()), - Some("TracksOutputsCells") => draw!(|app|app.project.view_track_output_cells()), - Some("TracksOutputsCount") => draw!(|app|app.project.view_track_output_count()), - Some(_) => Self::boxed(move|state, to|if let Some(view) = state.config.get_view(word.as_ref()) { + Some("Beat") => view!(), + Some("Bpm") => view!(), + Some("BrowseTitle") => view!(|app|view_browse_title(app)), + Some("Debug") => Box::new(move|_, to|format!("[{:?}]", to.area()).exact_h(1).draw(to)), + Some("Device") => view!(|app|view_device(app)), + Some("Menu") => view!(|app|view_menu(app)), + Some("Editor") => view!(|app|app.editor()), + Some("MetersIn") => view!(|___|bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h())), + Some("MetersOut") => view!(|___|bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h())), + Some("ModeList") => view!(|app|view_modes(app)), + Some("HistoryCount") => view!(|app|app.history.len()), + Some("ScenesClips") => view!(|app|view_scenes_clips(app)), + Some("ScenesNames") => view!(|app|app.view_scenes_names()), + Some("Sessions") => view!(|___|view_sessions()), + Some("Selected") => view!(|app|view_selection(app)), + Some("Templates") => view!(|app|view_templates(app)), + Some("Time") => view!(), + Some("Tracks") => view!("TODO tracks"), + Some("TracksDevices") => view!(|app|app.project.view_track_devices(app.color.clone())), + Some("TracksDevicesAdd") => view!(|app|app.project.view_track_device_add()), + Some("TracksDevicesCells") => view!(|app|app.project.view_track_device_cells(app.tracks_devices_height())), + Some("TracksDevicesCount") => view!(|app|app.project.view_track_device_count()), + Some("TracksInputs") => view!(|app|app.project.view_track_inputs(app.color.clone())), + Some("TracksInputsAdd") => view!(|app|app.project.view_track_input_add()), + Some("TracksInputsCells") => view!(|app|app.project.view_track_input_cells(app.tracks_inputs_height())), + Some("TracksInputsCount") => view!(|app|app.project.view_track_input_count()), + Some("TracksNames") => view!(|app|app.project.view_track_names(app.color.clone())), + Some("TracksNamesAdd") => view!(|app|app.project.view_track_names_add()), + Some("TracksNamesCells") => view!(|app|app.project.view_track_names_cells()), + Some("TracksNamesCount") => view!(|app|app.project.view_track_names_count()), + Some("TracksOutputs") => view!(|app|app.project.view_track_outputs(app.color.clone(), 0)), + Some("TracksOutputsAdd") => view!(|app|app.project.view_track_output_add()), + Some("TracksOutputsCells") => view!(|app|app.project.view_track_output_cells()), + Some("TracksOutputsCount") => view!(|app|app.project.view_track_output_count()), + Some(_) => Box::new(move|state, to|if let Some(view) = state.config.get_view(word.as_ref()) { (view.render)(state, to) } else { fg(Color::Rgb(128, 32, 32), format!("undefined: {word:?}")).draw(to) @@ -426,6 +414,18 @@ impl View { } } +pub fn view_menu (app: &App) -> impl Draw { + if let Dialog::Menu(selected, items) = &app.dialog { + Some(iter_south(move||items.iter().enumerate().map(move|(index, MenuItem(item, _))|{ + 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, item).exact_h(2).align_x() + })).exact_w(20).align_x()) + } else { + None + } +} + /// ``` /// let _ = tek::button_play_pause(true, true); /// let _ = tek::button_play_pause(true, false); @@ -489,7 +489,9 @@ pub fn view_piano_h_notes (state: &PianoHorizontal) -> impl Draw { 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.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { + if let Some(cell) = to.cell_mut( + ratatui::prelude::Position::from((screen_x, screen_y)) + ) { *cell = source_cell.clone(); } } @@ -605,47 +607,43 @@ impl OctaveVertical { } } -impl HasClipsSize for App { fn clips_size (&self) -> &Sizer { &self.project.size_inner } } -impl HasClipsSize for Project { fn clips_size (&self) -> &Sizer { &self.size_inner } } +pub fn view_editor_clip_status (editor: &MidiEditor) -> impl Draw + '_ { + let ( + _color, name, length, looped + ) = if let Some(clip) = editor.mode.clip.read().as_ref().map(|p|p.read()) { + (clip.color, clip.name.clone(), clip.length, clip.looped) + } else { + (ItemTheme::G[64], String::new().into(), 0, false) + }; + south!( + 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) +} -impl MidiEditor { - pub fn clip_status (&self) -> impl Draw + '_ { - let ( - _color, name, length, looped - ) = if let Some(clip) = self.mode.clip.read().as_ref().map(|p|p.read()) { - (clip.color, clip.name.clone(), clip.length, clip.looped) - } else { - (ItemTheme::G[64], String::new().into(), 0, false) - }; - south!( - 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 + '_ { - let (_color, length) = if let Some(clip) = self.mode.clip.read().as_ref().map(|p|p.read()) { - (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()); - south!( - 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) - } +pub fn view_editor_edit_status (editor: &MidiEditor) -> impl Draw + '_ { + let (_color, length) = if let Some(clip) = editor.mode.clip.read().as_ref().map(|p|p.read()) { + (clip.color, clip.length) + } else { (ItemTheme::G[64], 0) }; + let time_pos = editor.get_time_pos(); + let time_zoom = editor.get_time_zoom(); + let time_lock = if editor.get_time_lock() { "[lock]" } else { " " }; + let note_pos = editor.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}", editor.get_note_len()); + south!( + 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) } impl Draw for MidiEditor { @@ -660,28 +658,31 @@ impl TracksView for T where T: HasTracks + ScenesView + HasMidiIns + HasMidiOuts + HasTrack {} +fn iter_tracks_with_sizes <'t, T> (state: &'t T) -> impl TracksSizes<'t> where + T: HasTracks + HasEditor + HasSelection + HasClipsSize + ?Sized +{ + let _editor_width = state.editor().as_ref().map(|e|e.size.w()); + let _active_track = state.selection().track(); + let mut x = 0; + let w = state.clips_size().w() as usize; + state.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; + Some(data) + } else { + None + } + }) +} + pub trait TracksView: HasTracks + ScenesView + HasMidiIns + HasMidiOuts + HasTrack { - /// Spacing between tracks. - const TRACK_SPACING: usize = 0; - /// Iterate over tracks with their corresponding sizes. fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { - let _editor_width = self.editor().as_ref().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 - } - }) + iter_tracks_with_sizes(self) } fn view_track_column <'t, S, D, F> (&'t self, f: F) -> impl Draw where @@ -830,7 +831,13 @@ pub trait TracksView: fn view_outputs (&self, theme: ItemTheme) -> impl Draw { let height = self.outputs_height(); - let list = south( + let list = self.view_outputs_list(height); + view_track_row_section(theme, list, self.view_track_output_add(), + bg(theme.darker.term, self.view_outputs_grid().align_w().full_w())).exact_h(height) + } + + fn view_outputs_list (&self, height: u16) -> impl Draw { + south( button_3("o", "utput", self.midi_outs().len(), false).align_w().full_w().exact_h(1), bg(Color::Rgb(0, 0, 0), draw(|to: &mut Tui|{ for (_index, port) in self.midi_outs().iter().enumerate() { @@ -844,26 +851,28 @@ pub trait TracksView: } } todo!(); - })).align_nw().full_wh().exact_h(height - 1).min_h(1) - ); - view_track_row_section(theme, list, self.view_track_output_add(), - bg(theme.darker.term, draw(|to: &mut Tui|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - let _ = draw(|to: &mut Tui|{ - east(either(true, fg(Green, "play "), "play "), - either(false, fg(Yellow, "solo "), "solo ")).align_w().exact_h(1).draw(to)?; - for (_index, port) in self.midi_outs().iter().enumerate() { - east(either(true, fg(Green, " ● "), " · "), - either(false, fg(Yellow, " ● "), " · ")).align_w().exact_h(1).draw(to)?; - for (_index, _conn) in port.connections.iter().enumerate() { - "".full_w().exact_h(1).draw(to)?; - } + })).align_nw().full_wh().exact_h(height.minus(1)).min_h(1) + ) + } + + fn view_outputs_grid (&self) -> impl Draw { + draw(|to: &mut Tui|{ + for (index, track, _x1, _x2) in self.tracks_with_sizes() { + let _ = draw(|to: &mut Tui|{ + east(either(true, fg(Green, "play "), "play "), + either(false, fg(Yellow, "solo "), "solo ")).align_w().exact_h(1).draw(to)?; + for (_index, port) in self.midi_outs().iter().enumerate() { + east(either(true, fg(Green, " ● "), " · "), + either(false, fg(Yellow, " ● "), " · ")).align_w().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!() - }).align_w().full_w())).exact_h(height) + } + todo!() + }).exact_w(track_width(index, track)).draw(to)?; + } + todo!() + }) } } @@ -876,7 +885,9 @@ pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + fn h_scenes (&self) -> u16; fn w_side (&self) -> u16; fn w_mid (&self) -> u16; - + fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> { + iter_scenes_with_sizes(self) + } fn view_scenes_names (&self) -> impl Draw where Self: Sized { let select = self.selection(); let editor = self.editor(); @@ -885,50 +896,33 @@ pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + view_scene_name(select, editor.as_ref(), index, scene, editing) })) } - - 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) - && let Some(editor) = self.editor() - { - editor.size.h() as usize - } else { - Scene::DEFAULT_HEIGHT - }; - if y + height <= self.clips_size().h() as usize { - let data = (s, scene, y, y + height); - y += height; - Some(data) - } else { - None - } - }) - } } -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) - } -} +fn iter_scenes_with_sizes <'s, T> (state: &'s T) -> impl ScenesSizes<'s> where T: + HasScenes + HasSceneScroll + HasEditor + HasSelection + HasClipsSize + ?Sized +{ + let mut y = 0; + let skip = state.scene_scroll(); + let selected = state.selection(); + let editor = state.editor(); + let clips_size = state.clips_size(); + state.scenes().iter().enumerate().skip(skip).map_while(move|(s, scene)|{ + let height = if selected.scene() == Some(s) + && let Some(editor) = editor + { + editor.size.h() as usize + } else { + Scene::DEFAULT_HEIGHT + }; -impl ScenesView for Project { - 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) - } + if y + height <= clips_size.h() as usize { + let data = (s, scene, y, y + height); + y += height; + Some(data) + } else { + None + } + }) } fn view_modes <'a, S: HasModeStack> (state: &'a S) -> impl Draw + 'a { @@ -968,7 +962,7 @@ fn view_scenes_clips (state: &S) -> impl Draw let is_sel = view_scene_sel(select, track_index, scene_index, editing); below( Outer(true, Style::default().fg(o)).full_wh(), - below( + above( below( fg_bg(o, b, "".full_wh()), fg_bg(f, b, bold(true, name)).align_nw().full_wh(), @@ -1204,9 +1198,11 @@ pub fn view_scene_name <'a> ( Scene::DEFAULT_HEIGHT as u16 }; let a = east!("·s", index, " ", 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())).align_nw().full_wh()); + let s = select.scene() == Some(index) && editing; + let b = when(s, south( + editor.map(view_editor_clip_status), + editor.map(view_editor_edit_status) + ).full_wh().align_se()); let c = if select.scene() == Some(index) { scene.color.light.term } else { @@ -1462,62 +1458,67 @@ pub fn view_wrap <'a> (bg: Color, fg: Color, content: impl Draw) east(left, west(right, fg_bg(fg, bg, content))) } -impl PianoHorizontal { - /// Draw the piano roll background. - /// - /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ - pub 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 background. +pub fn draw_piano_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: █▄ █▄ █▄ - pub 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); - } +/// Draw the piano roll foreground. +pub fn draw_piano_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() { + } + let time_end = time_start + zoom; + for time in time_start..time_end.min(clip.length) { + if let Some(notes) = clip.notes.get(time) { + for event in notes.iter() { match event { MidiMessage::NoteOn { key, .. } => { let note = key.as_int() as usize; diff --git a/src/proj_track.rs b/src/track.rs similarity index 100% rename from src/proj_track.rs rename to src/track.rs