diff --git a/crates/tek_tui/src/lib.rs b/crates/tek_tui/src/lib.rs index 84375333..df9ae16c 100644 --- a/crates/tek_tui/src/lib.rs +++ b/crates/tek_tui/src/lib.rs @@ -97,7 +97,7 @@ pub fn to_focus_command (input: &TuiInput) -> Option { } #[macro_export] macro_rules! impl_focus { - ($Struct:ident $Focus:ident $Grid:expr) => { + ($Struct:ident $Focus:ident $Grid:expr $(=> [$self:ident : $update_focus:expr])?) => { impl HasFocus for $Struct { type Item = $Focus; /// Get the currently focused item. @@ -108,6 +108,7 @@ pub fn to_focus_command (input: &TuiInput) -> Option { fn set_focused (&mut self, to: Self::Item) { self.focus.set_inner(to) } + $(fn focus_updated (&mut $self) { $update_focus })? } impl HasEnter for $Struct { /// Get the currently focused item. diff --git a/crates/tek_tui/src/tui_app_sequencer.rs b/crates/tek_tui/src/tui_app_sequencer.rs index d2c08e68..26b7e91c 100644 --- a/crates/tek_tui/src/tui_app_sequencer.rs +++ b/crates/tek_tui/src/tui_app_sequencer.rs @@ -104,7 +104,13 @@ impl_focus!(SequencerTui SequencerFocus [ PhraseEditor, PhraseEditor, ], -]); +] => [self: { + if self.focus.is_entered() && self.focus.inner() == SequencerFocus::PhraseEditor { + self.editor.edit_mode = PhraseEditMode::Note + } else { + self.editor.edit_mode = PhraseEditMode::Scroll + } +}]); /// Status bar for sequencer app #[derive(Clone)] @@ -144,10 +150,13 @@ impl From<&SequencerTui> for SequencerStatusBar { Transport(Sync) => " LAUNCH SYNC ", Transport(Quant) => " REC QUANT ", Transport(Clock) => " SEEK ", - PhraseList => " PHRASES ", - PhraseEditor => " EDIT MIDI ", PhrasePlay => " TO PLAY ", PhraseNext => " UP NEXT ", + PhraseList => " PHRASES ", + PhraseEditor => match state.editor.edit_mode { + PhraseEditMode::Note => " EDIT MIDI ", + PhraseEditMode::Scroll => " VIEW MIDI ", + }, }, help: match state.focused() { Transport(PlayPause) => &[ @@ -164,6 +173,10 @@ impl From<&SequencerTui> for SequencerStatusBar { Transport(Quant) => &[ ("", ".,", " inc/dec"), ], + Transport(Clock) => &[ + ("", ".,", " by beat"), + ("", "<>", " by time"), + ], PhraseList => if state.entered() { &[ ("", "↕", " pick"), @@ -173,6 +186,14 @@ impl From<&SequencerTui> for SequencerStatusBar { ] } else { default_help + }, + PhraseEditor => match state.editor.edit_mode { + PhraseEditMode::Note => &[ + ("", "✣", " cursor"), + ], + PhraseEditMode::Scroll => &[ + ("", "✣", " scroll"), + ], } _ => if state.entered() { &[ diff --git a/crates/tek_tui/src/tui_control_phrase_editor.rs b/crates/tek_tui/src/tui_control_phrase_editor.rs index 71bd4510..8fa4807e 100644 --- a/crates/tek_tui/src/tui_control_phrase_editor.rs +++ b/crates/tek_tui/src/tui_control_phrase_editor.rs @@ -33,29 +33,30 @@ impl InputToCommand for PhraseCommand { key!(Char('_')) => SetTimeZoom(next_note_length(time_scale)), key!(Char('=')) => SetTimeZoom(prev_note_length(time_scale)), key!(Char('+')) => SetTimeZoom(prev_note_length(time_scale)), + key!(Char('a')) => AppendNote, + key!(Char('s')) => PutNote, + key!(Char('[')) => SetNoteLength(prev_note_length(state.note_len)), + key!(Char(']')) => SetNoteLength(next_note_length(state.note_len)), + key!(Char('n')) => { todo!("toggle keys vs notes") }, _ => match state.edit_mode { PhraseEditMode::Scroll => match from.event() { key!(Char('e')) => SetEditMode(PhraseEditMode::Note), - key!(Up) => SetNoteCursor(note_point + 1), - key!(Down) => SetNoteCursor(note_point.saturating_sub(1)), - key!(PageUp) => SetNoteCursor(note_point + 3), - key!(PageDown) => SetNoteCursor(note_point.saturating_sub(3)), - key!(Left) => SetTimeCursor(time_point.saturating_sub(time_scale)), - key!(Right) => SetTimeCursor((time_point + time_scale) % length), - _ => return None - }, - PhraseEditMode::Note => match from.event() { - key!(Char('e')) => SetEditMode(PhraseEditMode::Scroll), key!(Up) => SetNoteScroll(note_lo + 1), key!(Down) => SetNoteScroll(note_lo.saturating_sub(1)), key!(PageUp) => SetNoteScroll(note_lo + 3), key!(PageDown) => SetNoteScroll(note_lo.saturating_sub(3)), key!(Left) => SetTimeScroll(time_start.saturating_sub(1)), key!(Right) => SetTimeScroll(time_start + 1), - key!(Char('a')) => AppendNote, - key!(Char('s')) => PutNote, - key!(Char('[')) => SetNoteLength(prev_note_length(state.note_len)), - key!(Char(']')) => SetNoteLength(next_note_length(state.note_len)), + _ => return None + }, + PhraseEditMode::Note => match from.event() { + key!(Char('e')) => SetEditMode(PhraseEditMode::Scroll), + key!(Up) => SetNoteCursor(note_point + 1), + key!(Down) => SetNoteCursor(note_point.saturating_sub(1)), + key!(PageUp) => SetNoteCursor(note_point + 3), + key!(PageDown) => SetNoteCursor(note_point.saturating_sub(3)), + key!(Left) => SetTimeCursor(time_point.saturating_sub(time_scale)), + key!(Right) => SetTimeCursor((time_point + time_scale) % length), _ => return None }, } @@ -69,7 +70,7 @@ impl Command for PhraseCommand { use PhraseCommand::*; Ok(match self { Show(phrase) => { - state.phrase = phrase; + state.show_phrase(phrase); None }, ToggleDirection => { diff --git a/crates/tek_tui/src/tui_control_phrase_list.rs b/crates/tek_tui/src/tui_control_phrase_list.rs index aea620b1..035880ae 100644 --- a/crates/tek_tui/src/tui_control_phrase_list.rs +++ b/crates/tek_tui/src/tui_control_phrase_list.rs @@ -99,9 +99,13 @@ fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option Cmd::Length(Length::Begin), key!(Char('m')) => Cmd::Import(Browse::Begin), key!(Char('x')) => Cmd::Export(Browse::Begin), - key!(Up) => Cmd::Select(index.overflowing_sub(1).0.min(state.phrases().len() - 1)), - key!(Down) => Cmd::Select(index.saturating_add(1) % state.phrases().len()), key!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())), + key!(Up) => Cmd::Select( + index.overflowing_sub(1).0.min(state.phrases().len() - 1) + ), + key!(Down) => Cmd::Select( + index.saturating_add(1) % state.phrases().len() + ), key!(Char(',')) => if index > 1 { state.set_phrase_index(state.phrase_index().saturating_sub(1)); Cmd::Phrase(Pool::Swap(index - 1, index)) @@ -120,16 +124,12 @@ fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option Cmd::Phrase(Pool::Add( - count, Phrase::new( - String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random()) - ) - )), - key!(Char('i')) => Cmd::Phrase(Pool::Add( - index + 1, Phrase::new( - String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random()) - ) - )), + key!(Char('a')) => Cmd::Phrase(Pool::Add(count, Phrase::new( + String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random()) + ))), + key!(Char('i')) => Cmd::Phrase(Pool::Add(index + 1, Phrase::new( + String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random()) + ))), key!(Char('d')) => { let mut phrase = state.phrases()[index].read().unwrap().duplicate(); phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25); diff --git a/crates/tek_tui/src/tui_model_phrase_editor.rs b/crates/tek_tui/src/tui_model_phrase_editor.rs index 658a2947..adba5411 100644 --- a/crates/tek_tui/src/tui_model_phrase_editor.rs +++ b/crates/tek_tui/src/tui_model_phrase_editor.rs @@ -67,15 +67,40 @@ impl Default for PhraseEditorModel { } impl PhraseEditorModel { + /// Put note at current position pub fn put_note (&mut self) { - todo!("put_note") + if let Some(phrase) = &self.phrase { + let time = self.time_point.load(Ordering::Relaxed); + let note = self.note_point.load(Ordering::Relaxed); + let mut phrase = phrase.write().unwrap(); + let key: u7 = u7::from((127 - note) as u8); + let vel: u7 = 100.into(); + let start = time; + let end = (start + self.note_len) % phrase.length; + phrase.notes[time].push(MidiMessage::NoteOn { key, vel }); + phrase.notes[end].push(MidiMessage::NoteOff { key, vel }); + self.buffer = Self::redraw(&phrase); + } } + /// Move time cursor forward by current note length pub fn time_cursor_advance (&self) { let point = self.time_point.load(Ordering::Relaxed); let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); let forward = |time|(time + self.note_len) % length; self.time_point.store(forward(point), Ordering::Relaxed); } + /// Select which pattern to display. This pre-renders it to the buffer at full resolution. + pub fn show_phrase (&mut self, phrase: Option>>) { + if let Some(phrase) = phrase { + self.phrase = Some(phrase.clone()); + self.time_clamp.store(phrase.read().unwrap().length, Ordering::Relaxed); + self.buffer = Self::redraw(&*phrase.read().unwrap()); + } else { + self.phrase = None; + self.time_clamp.store(0, Ordering::Relaxed); + self.buffer = Default::default(); + } + } } #[derive(Copy, Clone, Debug)] diff --git a/crates/tek_tui/src/tui_view_phrase_editor.rs b/crates/tek_tui/src/tui_view_phrase_editor.rs index 955ff694..0565b283 100644 --- a/crates/tek_tui/src/tui_view_phrase_editor.rs +++ b/crates/tek_tui/src/tui_view_phrase_editor.rs @@ -1,25 +1,23 @@ use crate::*; pub struct PhraseView<'a> { - pub(crate) focused: bool, - pub(crate) entered: bool, - pub(crate) phrase: &'a Option>>, - pub(crate) buffer: &'a BigBuffer, - pub(crate) note_len: usize, - pub(crate) now: &'a Arc, + focused: bool, + entered: bool, + phrase: &'a Option>>, + buffer: &'a BigBuffer, + note_len: usize, + now: &'a Arc, - size: &'a Measure, - width: usize, - height: usize, + size: &'a Measure, note_point: usize, note_range: (usize, usize), note_names: (&'a str, &'a str), - pub(crate) time_start: usize, - pub(crate) time_point: usize, - pub(crate) time_clamp: usize, - pub(crate) time_scale: usize, + time_start: usize, + time_point: usize, + time_clamp: usize, + time_scale: usize, } impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { @@ -52,8 +50,6 @@ impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { now: &editor.now, size: &editor.size, - width, - height, note_point, note_range: (note_lo, note_hi), @@ -234,33 +230,7 @@ const NTH_OCTAVE: [&'static str; 11] = [ ]; impl PhraseEditorModel { - pub fn put (&mut self) { - if let Some(phrase) = &self.phrase { - let time = self.time_point.load(Ordering::Relaxed); - let note = self.note_point.load(Ordering::Relaxed); - let mut phrase = phrase.write().unwrap(); - let key: u7 = u7::from((127 - note) as u8); - let vel: u7 = 100.into(); - let start = time; - let end = (start + self.note_len) % phrase.length; - phrase.notes[time].push(MidiMessage::NoteOn { key, vel }); - phrase.notes[end].push(MidiMessage::NoteOff { key, vel }); - self.buffer = Self::redraw(&phrase); - } - } - /// Select which pattern to display. This pre-renders it to the buffer at full resolution. - pub fn show (&mut self, phrase: Option>>) { - if let Some(phrase) = phrase { - self.phrase = Some(phrase.clone()); - self.time_clamp.store(phrase.read().unwrap().length, Ordering::Relaxed); - self.buffer = Self::redraw(&*phrase.read().unwrap()); - } else { - self.phrase = None; - self.time_clamp.store(0, Ordering::Relaxed); - self.buffer = Default::default(); - } - } - fn redraw (phrase: &Phrase) -> BigBuffer { + pub(crate) fn redraw (phrase: &Phrase) -> BigBuffer { let mut buf = BigBuffer::new(usize::MAX.min(phrase.length), 65); Self::fill_seq_bg(&mut buf, phrase.length, phrase.ppq); Self::fill_seq_fg(&mut buf, &phrase); @@ -319,10 +289,10 @@ impl PhraseEditorModel { break } if let Some(block) = half_block( - notes_on[y as usize * 2], - notes_on[y as usize * 2 + 1], + notes_on[127 - (y as usize * 2)], + notes_on[127 - (y as usize * 2 + 1)], ) { - buf.get_mut(x, y).map(|cell|{ + buf.get_mut(x, 64 - y).map(|cell|{ cell.set_char(block); cell.set_fg(Color::White); });