diff --git a/midi/src/keys_edit.edn b/midi/src/keys_edit.edn index 43c129ae..da01532a 100644 --- a/midi/src/keys_edit.edn +++ b/midi/src/keys_edit.edn @@ -1,20 +1,22 @@ -(:enter add :false) -(:shift-enter add :true) -(:del del :false) -(:shift-del del :true) -(:comma note/length :prev-note-length) -(:period note/length :next-note-length) -(:plus note/range :next-note-range) -(:underscore note/range :prev-note-range) -(:up note/point :next-note-point) -(:down note/point :prev-note-point) -(:left time/point :next-time-point) -(:right time/point :prev-time-point) -(:z time/lock :next-time-lock) -(:equal time/zoom :next-time-zoom) -(:minus time/zoom :prev-time-zoom) -(:space clock/play :play-current) -(:shift-space clock/play :play-start) -(:u history :history-prev) -(:r history :history-next) -(:tab compact :next-compact) +(@up note/pos :note-pos-next) +(@down note/pos :note-pos-prev) + +(@left time/pos :time-pos-next) +(@right time/pos :time-pos-prev) + +(@enter add :false) +(@shift-enter add :true) +(@del del :false) +(@shift-del del :true) +(@comma note/length :prev-note-length) +(@period note/length :next-note-length) +(@plus note/range :next-note-range) +(@underscore note/range :prev-note-range) +(@z time/lock :next-time-lock) +(@equal time/zoom :next-time-zoom) +(@minus time/zoom :prev-time-zoom) +(@space clock/play :play-current) +(@shift-space clock/play :play-start) +(@u history :history-prev) +(@r history :history-next) +(@tab compact :next-compact) diff --git a/midi/src/midi_edit.rs b/midi/src/midi_edit.rs index d5b344fd..7de89d96 100644 --- a/midi/src/midi_edit.rs +++ b/midi/src/midi_edit.rs @@ -56,10 +56,17 @@ edn_provide!(bool: |self: MidiEditor| { ":false" => false }); edn_provide!(usize: |self: MidiEditor| { - ":note-length" => self.note_len(), - ":note-point" => self.note_point(), - ":time-point" => self.time_point(), - ":time-zoom" => self.time_zoom().get(), + ":note-length" => self.note_len(), + + ":note-pos" => self.note_point(), + ":note-pos-next" => self.note_point() + 1, + ":note-pos-prev" => self.note_point().saturating_sub(1), + + ":time-pos" => self.time_point(), + ":time-pos-next" => self.time_point() + self.time_zoom().get(), + ":time-pos-prev" => self.time_point().saturating_sub(self.time_zoom().get()), + + ":time-zoom" => self.time_zoom().get(), }); impl std::fmt::Debug for MidiEditor { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { diff --git a/tek/src/keys.edn b/tek/src/keys.edn index 1322e8a7..308e926d 100644 --- a/tek/src/keys.edn +++ b/tek/src/keys.edn @@ -5,4 +5,4 @@ (@e editor show :pool-clip) (@ctrl-a scene add) (@ctrl-t track add) -(@tab compact) +(@tab edit) diff --git a/tek/src/lib.rs b/tek/src/lib.rs index 8549ae22..ebe58ef1 100644 --- a/tek/src/lib.rs +++ b/tek/src/lib.rs @@ -5,7 +5,7 @@ #![feature(if_let_guard)] #![feature(impl_trait_in_assoc_type)] #![feature(type_alias_impl_trait)] -pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::{self, *}}}; +pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::Relaxed}}; /// Standard result type. pub type Usually = std::result::Result>; /// Standard optional result type. @@ -119,7 +119,6 @@ impl TekCli { pub edn: String, pub clock: Clock, pub color: ItemPalette, - pub editing: AtomicBool, pub pool: Option, pub editor: Option, pub player: Option, @@ -136,7 +135,7 @@ impl TekCli { pub splits: Vec, pub size: Measure, pub perf: PerfModel, - pub compact: bool, + pub editing: AtomicBool, pub history: Vec, } has_size!(|self: Tek|&self.size); @@ -174,10 +173,10 @@ edn_view!(TuiOut: |self: Tek| self.size.of(EdnView::from_source(self, self.edn.a // Provide sizes: u16 { ":sidebar-w" => self.sidebar_w(), - ":sample-h" => if self.compact() { 0 } else { 5 }, - ":samples-w" => if self.compact() { 4 } else { 11 }, - ":samples-y" => if self.compact() { 1 } else { 0 }, - ":pool-w" => if self.compact() { 5 } else { + ":sample-h" => if self.is_editing() { 0 } else { 5 }, + ":samples-w" => if self.is_editing() { 4 } else { 11 }, + ":samples-y" => if self.is_editing() { 1 } else { 0 }, + ":pool-w" => if self.is_editing() { 5 } else { let w = self.size.w(); if w > 60 { 20 } else if w > 40 { 15 } else { 10 } } }; // Provide components: @@ -193,7 +192,7 @@ edn_view!(TuiOut: |self: Tek| self.size.of(EdnView::from_source(self, self.edn.a ":outputs" => self.view_row(self.w(), 3, self.output_header(), self.output_cells()).boxed(), ":scenes" => Outer(Style::default().fg(TuiTheme::g(0))).enclose_bg(self.view_row( self.w(), self.size.h().saturating_sub(12) as u16, - self.scene_header(), self.scene_cells() + self.scene_header(), self.clip_columns() )).boxed() }}); impl Tek { fn new_arranger ( @@ -289,8 +288,6 @@ impl Tek { fn sync_follow (&self) { // TODO } - fn compact (&self) -> bool { self.compact } - fn is_editing (&self) -> bool { self.editing.load(Relaxed) } fn editor (&self) -> impl Content + '_ { &self.editor } fn view_clock (&self) -> impl Content + use<'_> { Outer(Style::default().fg(TuiTheme::g(0))).enclose(row!( @@ -300,14 +297,14 @@ impl Tek { )) } fn view_beat_stats (&self) -> impl Content + use<'_> { - let Self { compact, ref clock, .. } = self; + let clock = self.clock(); let now = clock.started.read().unwrap().as_ref().map(|start|clock.global.usec.get() - start.usec.get()); let beat = ||now.map(|now|clock.timebase.format_beats_1(clock.timebase.usecs_to_pulse(now))) .unwrap_or("-.-.--".into()); let time = ||now.map(|now|format!("{:.3}s", now/1000000.)) .unwrap_or("-.---s".into()); let bpm = ||format!("{:.3}", clock.timebase.bpm.get()); - Either::new(self.compact, + Either::new(self.is_editing(), row!(FieldV(TuiTheme::g(128).into(), "BPM", bpm()), FieldV(TuiTheme::g(128).into(), "Beat", beat()), FieldV(TuiTheme::g(128).into(), "Time", time()),), @@ -316,13 +313,14 @@ impl Tek { Bsp::e("Time ", Tui::fg(TuiTheme::g(255), time())))) } fn view_engine_stats (&self) -> impl Content + use<'_> { - let Self { compact, ref clock, .. } = self; + let clock = self.clock(); + let compact = self.is_editing(); let rate = clock.timebase.sr.get(); let chunk = clock.chunk.load(Relaxed); - let sr = move||format!("{}", if *compact {format!("{:.1}kHz", rate / 1000.)} else {format!("{:.0}Hz", rate)}); + let sr = move||format!("{}", if compact {format!("{:.1}kHz", rate / 1000.)} else {format!("{:.0}Hz", rate)}); let buffer_size = move||format!("{chunk}"); let latency = move||format!("{:.1}ms", chunk as f64 / rate * 1000.); - Either::new(self.compact, + Either::new(compact, row!(FieldV(TuiTheme::g(128).into(), "SR", sr()), FieldV(TuiTheme::g(128).into(), "Buf", buffer_size()), FieldV(TuiTheme::g(128).into(), "Lat", latency())), @@ -360,7 +358,7 @@ impl Tek { let playing = self.clock.is_rolling(); Tui::bg( if playing{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)}, - Either::new(self.compact, + Either::new(self.is_editing(), Thunk::new(move||Fixed::x(9, Either::new(playing, Tui::fg(Color::Rgb(0, 255, 0), " PLAYING "), Tui::fg(Color::Rgb(255, 128, 0), " STOPPED ")))), @@ -372,11 +370,11 @@ impl Tek { self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status())) } fn view_pool (&self) -> impl Content + use<'_> { - self.pool.as_ref().map(|pool|PoolView(self.compact(), pool)) + self.pool.as_ref().map(|pool|PoolView(self.is_editing(), pool)) } fn pool (&self) -> impl Content + use<'_> { self.pool.as_ref() - .map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))) + .map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.is_editing(), pool)))) } fn w (&self) -> u16 { self.tracks_sizes(self.is_editing(), self.editor_w()) @@ -406,6 +404,12 @@ impl Tek { clip.write().unwrap().toggle_loop() } } + fn track_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + (||Tui::bg(TuiTheme::g(32), Bsp::s( + Fill::x(Align::w(button(" C-a ".to_string(), format!(" add scene ({})", self.scenes().len())))), + Fill::x(Align::w(button(" C-t ".to_string(), format!(" add track ({})", self.tracks().len())))), + )).boxed()).into() + } fn track_add ( &mut self, name: Option<&str>, @@ -478,7 +482,7 @@ impl Tek { } Ok(()) } - fn scene_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + fn clip_columns <'a> (&'a self) -> BoxThunk<'a, TuiOut> { let editing = self.is_editing(); let tracks = move||self.tracks_sizes(editing, self.editor_w()); let scenes = move||self.scenes_sizes(editing, 2, 15); @@ -498,24 +502,22 @@ impl Tek { } else { ("⏹ ".to_string(), TuiTheme::g(64), TuiTheme::g(32)) }; - let last = last_color.read().unwrap().clone(); - let active = editing && selected_scene == Some(s) && selected_track == Some(t); - let editor = Thunk::new(||self.editor()); - let cell = Thunk::new(move||phat_sel_3( - selected_track == Some(t) && selected_scene == Some(s), - Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))), - Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))), - if selected_track == Some(t) && selected_scene.map(|s|s+1) == Some(s) { - None - } else { - Some(bg.into()) - }, - bg.into(), - bg.into(), - )); - let cell = Either::new(active, editor, cell); - *last_color.write().unwrap() = bg.into(); - map_south(y1 as u16, h, Push::y(1, Fixed::y(h, cell))) + map_south(y1 as u16, h, Push::y(1, Fixed::y(h, Either::new( + editing && selected_scene == Some(s) && selected_track == Some(t), + Thunk::new(||self.editor()), + Thunk::new(move||phat_sel_3( + selected_track == Some(t) && selected_scene == Some(s), + Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))), + Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))), + if selected_track == Some(t) && selected_scene.map(|s|s+1) == Some(s) { + None + } else { + Some(bg.into()) + }, + bg.into(), + bg.into(), + )), + )))) }))).boxed() })).boxed()).into() } @@ -553,6 +555,12 @@ const KEYS_MIX: &str = include_str!("keys_mix.edn"); handle!(TuiIn: |self: Tek, input|Ok({ use EdnItem::*; let mut command: Option = None; + // If editing, editor keys take priority + if self.is_editing() { + if self.editor.handle(input)? == Some(true) { + return Ok(Some(true)) + } + } // Handle from root keymap if let Some(command) = EdnKeyMapToCommand::new(KEYS_APP)?.from::<_, TekCommand>(self, input) { if let Some(undo) = command.execute(self)? { self.history.push(undo); } @@ -576,7 +584,7 @@ handle!(TuiIn: |self: Tek, input|Ok({ Clip(ClipCommand), Clock(ClockCommand), Color(ItemPalette), - Compact(Option), + Edit(Option), Editor(MidiEditCommand), Enqueue(Option>>), History(isize), @@ -589,8 +597,8 @@ handle!(TuiIn: |self: Tek, input|Ok({ Zoom(Option), } edn_command!(TekCommand: |app: Tek| { - ("compact" [] Self::Compact(None)) - ("compact" [c: bool] Self::Compact(c)) + ("edit" [] Self::Edit(None)) + ("edit" [c: bool] Self::Edit(c)) ("color" [c: Color] Self::Color(c.map(ItemPalette::from).unwrap_or_default())) @@ -661,19 +669,16 @@ command!(|self: TekCommand, app: Tek|match self { } else { None }, - Self::Compact(compact) => match compact { - Some(compact) => { - if app.compact != compact { - app.compact = compact; - Some(Self::Compact(Some(!compact))) - } else { - None - } - }, - None => { - app.compact = !app.compact; - Some(Self::Compact(Some(!app.compact))) + Self::Edit(value) => if let Some(value) = value { + if app.is_editing() != value { + app.editing.store(value, Relaxed); + Some(Self::Edit(Some(!value))) + } else { + None } + } else { + app.editing.store(!app.is_editing(), Relaxed); + Some(Self::Edit(Some(!app.is_editing()))) } }); /// Represents the current user selection in the arranger @@ -817,12 +822,6 @@ trait HasTracks: HasSelection + HasClock + HasJack + HasEditor + Send + Sync { fn track_mut (&mut self) -> Option<&mut Track> { self.selected().track().and_then(|s|self.tracks_mut().get_mut(s)) } - fn track_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - (||Tui::bg(TuiTheme::g(32), Bsp::s( - button(" C-t ", " add track "), - button(" C-a ", " add scene "), - )).boxed()).into() - } fn track_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { let iter = ||self.tracks_sizes(self.is_editing(), self.editor_w()); (move||Align::x(Map::new(iter, move|(_, track, x1, x2), i| { @@ -841,7 +840,7 @@ trait HasTracks: HasSelection + HasClock + HasJack + HasEditor + Send + Sync { fn input_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { let fg = TuiTheme::g(224); let bg = TuiTheme::g(64); - (move||Bsp::s(button(" I ", " midi ins "), self.midi_ins().get(0).map(|inp|Bsp::s( + (move||Bsp::s(Fill::x(Align::w(button(" I ".to_string(), format!(" midi ins ({})", self.midi_ins().len())))), self.midi_ins().get(0).map(|inp|Bsp::s( Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(inp.name.clone())))), inp.connect.get(0).map(|connect|Fill::x(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg, connect.info()))))), @@ -869,7 +868,7 @@ trait HasTracks: HasSelection + HasClock + HasJack + HasEditor + Send + Sync { fn output_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { let fg = TuiTheme::g(224); let bg = TuiTheme::g(64); - (move||Bsp::s(button(" O ", " midi outs "), self.midi_outs().get(0).map(|out|Bsp::s( + (move||Bsp::s(Fill::x(Align::w(button(" O ".to_string(), format!(" midi outs ({}) ", self.midi_outs().len())))), self.midi_outs().get(0).map(|out|Bsp::s( Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(out.name.clone())))), out.connect.get(0).map(|connect|Fill::x(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg, connect.info()))))), @@ -1138,7 +1137,7 @@ audio!(|self: Tek, client, scope|{ self.perf.update(t0, scope); Control::Continue }); -fn button <'a> (key: &'a str, label: &'a str) -> impl Content + 'a { +fn button (key: String, label: String) -> impl Content { Tui::bold(true, Bsp::e( Margin::x(1, Tui::fg_bg(TuiTheme::g(0), TuiTheme::orange(), key)), Margin::x(1, Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(96), label)),