diff --git a/crates/tek_core/src/lib.rs b/crates/tek_core/src/lib.rs index e74493ce..0f60545c 100644 --- a/crates/tek_core/src/lib.rs +++ b/crates/tek_core/src/lib.rs @@ -8,7 +8,7 @@ pub use std::collections::BTreeMap; pub use once_cell::sync::Lazy; pub use std::sync::atomic::{Ordering, AtomicBool, AtomicUsize, AtomicU16}; pub use std::rc::Rc; -pub use std::cell::RefCell; +pub use std::cell::{Cell, RefCell}; pub use std::marker::PhantomData; pub(crate) use std::error::Error; pub(crate) use std::io::{stdout}; diff --git a/crates/tek_tui/src/tui_control_phrase_editor.rs b/crates/tek_tui/src/tui_control_phrase_editor.rs index c9d82f78..720daf46 100644 --- a/crates/tek_tui/src/tui_control_phrase_editor.rs +++ b/crates/tek_tui/src/tui_control_phrase_editor.rs @@ -61,22 +61,22 @@ impl InputToCommand for PhraseCommand { PhraseEditMode::Note => match from.event() { key!(Char('e')) => SetEditMode(PhraseEditMode::Scroll), key!(Up) => SetNoteScroll( - state.note_start.load(Ordering::Relaxed) + 1 + state.note_lo.load(Ordering::Relaxed) + 1 ), key!(Down) => SetNoteScroll( - state.note_start.load(Ordering::Relaxed).saturating_sub(1) + state.note_lo.load(Ordering::Relaxed).saturating_sub(1) ), key!(PageUp) => SetNoteScroll( - state.note_start.load(Ordering::Relaxed) + 3 + state.note_lo.load(Ordering::Relaxed) + 3 ), key!(PageDown) => SetNoteScroll( - state.note_start.load(Ordering::Relaxed).saturating_sub(3) + state.note_lo.load(Ordering::Relaxed).saturating_sub(3) ), key!(Left) => SetTimeScroll( - state.note_start.load(Ordering::Relaxed).saturating_sub(1) + state.note_lo.load(Ordering::Relaxed).saturating_sub(1) ), key!(Right) => SetTimeScroll( - state.note_start.load(Ordering::Relaxed) + 1 + state.note_lo.load(Ordering::Relaxed) + 1 ), key!(Char('a')) => AppendNote, key!(Char('s')) => PutNote, @@ -127,7 +127,7 @@ impl Command for PhraseCommand { None }, SetNoteScroll(note) => { - state.note_start.store(note, Ordering::Relaxed); + state.note_lo.store(note, Ordering::Relaxed); None }, SetNoteLength(time) => { @@ -136,10 +136,10 @@ impl Command for PhraseCommand { }, SetNoteCursor(note) => { let note = 127.min(note); - let start = state.note_start.load(Ordering::Relaxed); + let start = state.note_lo.load(Ordering::Relaxed); state.note_point.store(note, Ordering::Relaxed); if note < start { - state.note_start.store((note / 2) * 2, Ordering::Relaxed); + state.note_lo.store((note / 2) * 2, Ordering::Relaxed); } None }, diff --git a/crates/tek_tui/src/tui_model_phrase_editor.rs b/crates/tek_tui/src/tui_model_phrase_editor.rs index c1befde3..658a2947 100644 --- a/crates/tek_tui/src/tui_model_phrase_editor.rs +++ b/crates/tek_tui/src/tui_model_phrase_editor.rs @@ -6,8 +6,6 @@ pub struct PhraseEditorModel { pub(crate) phrase: Option>>, /// Length of note that will be inserted, in pulses pub(crate) note_len: usize, - /// The full piano keys are rendered to this buffer - pub(crate) keys: Buffer, /// The full piano roll is rendered to this buffer pub(crate) buffer: BigBuffer, /// Notes currently held at input @@ -19,9 +17,8 @@ pub struct PhraseEditorModel { /// Width and height of notes area at last render pub(crate) size: Measure, - pub(crate) note_start: AtomicUsize, + pub(crate) note_lo: AtomicUsize, pub(crate) note_point: AtomicUsize, - pub(crate) note_clamp: AtomicUsize, pub(crate) time_start: AtomicUsize, pub(crate) time_point: AtomicUsize, @@ -34,10 +31,9 @@ pub struct PhraseEditorModel { impl std::fmt::Debug for PhraseEditorModel { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("PhraseEditorModel") - .field("note_axis", &format!("{} {} {}", - self.note_start.load(Ordering::Relaxed), + .field("note_axis", &format!("{} {}", + self.note_lo.load(Ordering::Relaxed), self.note_point.load(Ordering::Relaxed), - self.note_clamp.load(Ordering::Relaxed), )) .field("time_axis", &format!("{} {} {} {}", self.time_start.load(Ordering::Relaxed), @@ -54,16 +50,14 @@ impl Default for PhraseEditorModel { Self { phrase: None, note_len: 24, - keys: keys_vert(), buffer: Default::default(), notes_in: RwLock::new([false;128]).into(), notes_out: RwLock::new([false;128]).into(), now: Pulse::default().into(), size: Measure::new(), edit_mode: PhraseEditMode::Scroll, - note_start: 12.into(), - note_point: 36.into(), - note_clamp: 127.into(), + note_lo: 0.into(), + note_point: 24.into(), time_start: 0.into(), time_point: 0.into(), time_clamp: 0.into(), diff --git a/crates/tek_tui/src/tui_view_phrase_editor.rs b/crates/tek_tui/src/tui_view_phrase_editor.rs index c1f82944..22405384 100644 --- a/crates/tek_tui/src/tui_view_phrase_editor.rs +++ b/crates/tek_tui/src/tui_view_phrase_editor.rs @@ -4,17 +4,17 @@ pub struct PhraseView<'a> { pub(crate) focused: bool, pub(crate) entered: bool, pub(crate) phrase: &'a Option>>, - pub(crate) size: &'a Measure, - pub(crate) keys: &'a Buffer, pub(crate) buffer: &'a BigBuffer, pub(crate) note_len: usize, pub(crate) now: &'a Arc, - pub(crate) note_start: &'a AtomicUsize, - pub(crate) note_point: usize, - pub(crate) note_clamp: usize, + size: &'a Measure, + width: usize, + height: usize, - note_range: (&'a str, &'a str), + note_point: usize, + note_range: (usize, usize), + note_names: (&'a str, &'a str), pub(crate) time_start: usize, pub(crate) time_point: usize, @@ -24,30 +24,45 @@ pub struct PhraseView<'a> { impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { fn from (state: &'a T) -> Self { - let [w, h] = state.editor().size.wh(); - let note_start = state.editor().note_start.load(Ordering::Relaxed); - let note_clamp = state.editor().note_clamp.load(Ordering::Relaxed); - let note_end = (note_start + h.saturating_sub(2) / 2).min(127); - let note_range = (to_note_name(note_start), to_note_name(note_end)); + let editor = state.editor(); + + let [width, height] = editor.size.wh(); + + let note_point = editor.note_point.load(Ordering::Relaxed); + + let mut note_lo = editor.note_lo.load(Ordering::Relaxed); + if note_point < note_lo { + note_lo = note_point; + editor.note_lo.store(note_lo, Ordering::Relaxed); + } + + let mut note_hi = 127.min(note_lo + height * 2 + 1); + if note_point > note_hi { + note_lo += note_point - note_hi; + note_hi = note_point; + editor.note_lo.store(note_lo, Ordering::Relaxed); + } + Self { focused: state.editor_focused(), entered: state.editor_entered(), - note_len: state.editor().note_len, - phrase: &state.editor().phrase, - size: &state.editor().size, - keys: &state.editor().keys, - buffer: &state.editor().buffer, - now: &state.editor().now, + note_len: editor.note_len, + phrase: &editor.phrase, + buffer: &editor.buffer, + now: &editor.now, - note_start: &state.editor().note_start, - note_point: state.editor().note_point.load(Ordering::Relaxed), - note_clamp, - note_range, + size: &editor.size, + width, + height, - time_start: state.editor().time_start.load(Ordering::Relaxed), - time_point: state.editor().time_point.load(Ordering::Relaxed), - time_clamp: state.editor().time_clamp.load(Ordering::Relaxed), - time_scale: state.editor().time_scale.load(Ordering::Relaxed), + note_point, + note_range: (note_lo, note_hi), + note_names: (to_note_name(note_lo), to_note_name(note_hi)), + + time_start: editor.time_start.load(Ordering::Relaxed), + time_point: editor.time_point.load(Ordering::Relaxed), + time_clamp: editor.time_clamp.load(Ordering::Relaxed), + time_scale: editor.time_scale.load(Ordering::Relaxed), } } } @@ -56,33 +71,73 @@ impl<'a> Content for PhraseView<'a> { type Engine = Tui; fn content (&self) -> impl Widget { let Self { - focused, entered, phrase, size, keys, buffer, note_len, now, - note_point, note_range, - time_start, time_point, time_clamp, time_scale, .. + focused, + entered, + phrase, + size, + buffer, + note_len, + note_range: (note_lo, note_hi), + note_names: (note_lo_name, note_hi_name), + note_point, + time_start, + time_point, + time_clamp, + time_scale, + now, + .. } = self; - let note_start = self.note_start.load(Ordering::Relaxed); let keys = move|to: &mut TuiOutput|Ok(if to.area().h() >= 2 { - to.buffer_update(to.area().set_w(5), &|cell, x, y|{ - let y = y + (note_start / 2) as u16; - if x < keys.area.width && y < keys.area.height { - *cell = keys.get(x, y).clone() + for y in to.area.y()..to.area.y2() { + let n = y - to.area.y(); + let m = note_hi.saturating_sub(n as usize*2); + //let c = format!("{m:>03} {n}"); + //to.blit(&c, to.area.x(), y, None); + let x = to.area.x(); + let s = Some(Style::default().fg(Color::Rgb(255,255,255)).bg(Color::Rgb(0,0,0))); + match m % 2 { + 1 => match (m / 2) % 6 { + 5 => to.blit(&"▀", x, y, s), + 4 => to.blit(&"▀", x, y, s), + 3 => to.blit(&"▀", x, y, s), + 2 => to.blit(&"█", x, y, s), + 1 => to.blit(&"▄", x, y, s), + 0 => to.blit(&"▄", x, y, s), + _ => unreachable!(), + }, + 0 => match (m / 2) % 6 { + 5 => to.blit(&"▄", x, y, s), + 4 => to.blit(&"▄", x, y, s), + 3 => to.blit(&"▄", x, y, s), + 2 => to.blit(&"▀", x, y, s), + 1 => to.blit(&"▀", x, y, s), + 0 => to.blit(&"█", x, y, s), + _ => unreachable!(), + }, + _ => unreachable!() } - }); + + } + //to.buffer_update(to.area().set_w(5), &|cell, x, y|{ + ////let y = y + (note_hi / 2) as u16; + //if x < keys.area.width && y < keys.area.height { + //[>cell = keys.get(x, y).clone() + ////cell.set_symbol(&format!("{y}")); + ////} + //} + //}); }); let notes_bg_null = Color::Rgb(28, 35, 25); let notes = move|to: &mut TuiOutput|{ let area = to.area(); let h = area.h() as usize; - size.set_wh(area.w(), h); - if note_point.saturating_sub(note_start) > (h * 2).saturating_sub(1) { - self.note_start.store(note_start + 2, Ordering::Relaxed); - } + size.set_wh(area.w(), h - 1); Ok(if to.area().h() >= 2 { let area = to.area(); to.buffer_update(area, &move |cell, x, y|{ cell.set_bg(notes_bg_null); let src_x = (x as usize + time_start) * time_scale; - let src_y = y as usize + note_start / 2; + let src_y = y as usize + note_lo / 2; if src_x < buffer.width && src_y < buffer.height - 1 { buffer.get(src_x, buffer.height - src_y - 2).map(|src|{ cell.set_symbol(src.symbol()); @@ -97,10 +152,15 @@ impl<'a> Content for PhraseView<'a> { let area = to.area(); let x1 = area.x() + (time_point / time_scale) as u16; let x2 = x1 + (note_len / time_scale) as u16; - let y = area.y() + note_point.saturating_sub(note_start) as u16 / 2; - let c = if note_point % 2 == 0 { "▀" } else { "▄" }; + let y = area.y() + (note_hi - note_point) as u16 / 2; + let c = if note_lo % 2 == 0 { + if note_point % 2 == 0 { "▄" } else { "▀" } + } else { + if note_point % 2 == 0 { "▀" } else { "▄" } + }; + let style = Some(Style::default().fg(Color::Rgb(0,255,0))); for x in x1..x2 { - to.blit(&c, x, y, Some(Style::default().fg(Color::Rgb(0,255,0)))); + to.blit(&c, x, y, style); } }); let playhead_inactive = Style::default().fg(Color::Rgb(255,255,255)).bg(Color::Rgb(40,50,30)); @@ -129,7 +189,7 @@ impl<'a> Content for PhraseView<'a> { CustomWidget::new(|to|Ok(Some(to)), cursor) ).fill_x(); let piano_roll = row!( - CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), keys).fill_y(), + CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(2))), keys).fill_y(), note_area ).fill_x().bg(Color::Rgb(40, 50, 30)).border(border); let content = lay!( @@ -140,30 +200,22 @@ impl<'a> Content for PhraseView<'a> { if let Some(phrase) = phrase { name = phrase.read().unwrap().name.clone(); } - let mut upper_left = format!("{} [{}] {name}", note_range.1, if *entered {"■"} else {" "},); - let mut lower_left = format!("{}", note_range.0); + let mut upper_left = format!("{note_hi} {note_hi_name} {name}"); + let mut lower_left = format!("{note_lo} {note_lo_name}"); let mut lower_right = format!("┤{}├", size.format()); - lower_right = format!("┤Zoom: {}├─{lower_right}", pulses_to_name(*time_scale)); - //lower_right = format!("Zoom: {} (+{}:{}*{}|{})", - //pulses_to_name(time_scale), - //time_start, time_point.unwrap_or(0), - //time_scale, time_clamp.unwrap_or(0), - //); if *focused && *entered { - lower_right = format!("┤Note: {} {}├─{lower_right}", + lower_right = format!("┤Note: {} ({}) {}├─{lower_right}", note_point, - pulses_to_name(*note_len)); - //lower_right = format!("Note: {} (+{}:{}|{}) {upper_right}", - //pulses_to_name(*note_len), - //note_start, - //note_point.unwrap_or(0), - //note_clamp.unwrap_or(0), - //); + to_note_name(*note_point), + pulses_to_name(*note_len) + ); } - let upper_right = if let Some(phrase) = phrase { - format!("┤Length: {}├", phrase.read().unwrap().length) - } else { - String::new() + let mut upper_right = format!("[{}]", if *entered {"■"} else {" "}); + if let Some(phrase) = phrase { + upper_right = format!("┤Length: {}├─┤Zoom: {}├{upper_right}", + phrase.read().unwrap().length, + pulses_to_name(*time_scale), + ) }; lay!( content, @@ -175,49 +227,9 @@ impl<'a> Content for PhraseView<'a> { } } -/// Colors of piano keys -const KEY_COLORS: [(Color, Color);6] = [ - (Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)), - (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), - (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), - (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), - (Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)), - (Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)), -]; - -pub(crate) fn keys_vert () -> Buffer { - let area = [0, 0, 5, 64]; - let mut buffer = Buffer::empty(Rect { - x: area.x(), y: area.y(), width: area.w(), height: area.h() - }); - buffer_update(&mut buffer, area, &|cell, x, y| { - let y = 63 - y; - match x { - 0 => if y % 6 == 0 { - cell.set_char('C'); - }, - 1 => if y % 6 == 0 { - cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]); - }, - 2 => { - cell.set_char('▀'); - let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize]; - cell.set_fg(fg); - cell.set_bg(bg); - }, - 3 => { - cell.set_char('▀'); - cell.set_fg(Color::White); - cell.set_bg(Color::White); - }, - _ => {} - } - }); - buffer -} - const NTH_OCTAVE: [&'static str; 11] = [ - "-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "X", + //"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", ]; impl PhraseEditorModel {