mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
500 lines
18 KiB
Rust
500 lines
18 KiB
Rust
use crate::*;
|
|
|
|
pub struct PhraseView<'a> {
|
|
focused: bool,
|
|
entered: bool,
|
|
phrase: &'a Option<Arc<RwLock<Phrase>>>,
|
|
buffer: &'a BigBuffer,
|
|
note_len: usize,
|
|
now: &'a Arc<Pulse>,
|
|
size: &'a Measure<Tui>,
|
|
view_mode: &'a PhraseViewMode,
|
|
|
|
note_point: usize,
|
|
note_range: (usize, usize),
|
|
note_names: (&'a str, &'a str),
|
|
|
|
time_start: usize,
|
|
time_point: usize,
|
|
}
|
|
|
|
impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> {
|
|
fn from (state: &'a T) -> Self {
|
|
let editor = state.editor();
|
|
|
|
let height = editor.size.h();
|
|
|
|
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 + 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: editor.note_len,
|
|
phrase: &editor.phrase,
|
|
buffer: &editor.buffer,
|
|
now: &editor.now,
|
|
size: &editor.size,
|
|
view_mode: &editor.view_mode,
|
|
|
|
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),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> Content for PhraseView<'a> {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
let Self {
|
|
focused,
|
|
entered,
|
|
phrase,
|
|
size,
|
|
buffer,
|
|
view_mode,
|
|
note_len,
|
|
note_range: (note_lo, note_hi),
|
|
note_names: (note_lo_name, note_hi_name),
|
|
note_point,
|
|
time_start,
|
|
time_point,
|
|
now,
|
|
..
|
|
} = self;
|
|
|
|
let upper_left = format!(
|
|
"{note_hi} {note_hi_name} {}",
|
|
phrase.as_ref().map(|p|p.read().unwrap().name.clone()).unwrap_or(String::new())
|
|
);
|
|
|
|
let lower_left = format!(
|
|
"{note_lo} {note_lo_name}"
|
|
|
|
);
|
|
|
|
let mut lower_right = format!(
|
|
"┤{}├", size.format()
|
|
);
|
|
if *focused && *entered {
|
|
lower_right = format!("┤Note: {} ({}) {}├─{lower_right}",
|
|
note_point, to_note_name(*note_point), pulses_to_name(*note_len)
|
|
);
|
|
}
|
|
|
|
let mut upper_right = format!(
|
|
"[{}]",
|
|
if *entered {"■"} else {" "}
|
|
);
|
|
if let Some(phrase) = phrase {
|
|
upper_right = format!("┤Time: {}/{} {}├{upper_right}",
|
|
time_point, phrase.read().unwrap().length, pulses_to_name(view_mode.time_zoom()),
|
|
)
|
|
};
|
|
|
|
let title_color = if *focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)};
|
|
|
|
lay!(
|
|
row!(
|
|
CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(2))), move|to: &mut TuiOutput|{
|
|
Ok(if to.area().h() >= 2 {
|
|
view_mode.blit_keys(to, *note_hi, *note_lo)
|
|
})
|
|
}).fill_y(),
|
|
lay!(
|
|
CustomWidget::new(|to|Ok(Some(to)), |to: &mut TuiOutput|{
|
|
size.set_wh(to.area.w(), to.area.h() as usize - 1);
|
|
let draw = to.area().h() >= 2;
|
|
Ok(if draw {
|
|
view_mode.blit_notes(
|
|
to, buffer, *time_start, *note_hi
|
|
)
|
|
})
|
|
}).fill_x(),
|
|
CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{
|
|
Ok(if *focused && *entered {
|
|
view_mode.blit_cursor(
|
|
to,
|
|
*time_point, *time_start, view_mode.time_zoom(),
|
|
*note_point, *note_len, *note_hi, *note_lo,
|
|
)
|
|
})
|
|
})
|
|
).fill_x()
|
|
).fill_x().bg(Color::Rgb(40, 50, 30)).border(Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(if *focused{
|
|
Color::Rgb(100, 110, 40)
|
|
} else {
|
|
Color::Rgb(70, 80, 50)
|
|
}))),
|
|
CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_h(1))), move|to: &mut TuiOutput|{
|
|
//let playhead_inactive = Style::default().fg(Color::Rgb(255,255,255)).bg(Color::Rgb(40,50,30));
|
|
//let playhead_active = playhead_inactive.clone().yellow().bold().not_dim();
|
|
//if let Some(_) = phrase {
|
|
//let now = now.get() as usize; // TODO FIXME: self.now % phrase.read().unwrap().length;
|
|
//let time_clamp = time_clamp;
|
|
//for x in 0..(time_clamp/time_scale).saturating_sub(*time_start) {
|
|
//let this_step = time_start + (x + 0) * time_scale;
|
|
//let next_step = time_start + (x + 1) * time_scale;
|
|
//let x = to.area().x() + x as u16;
|
|
//let active = this_step <= now && now < next_step;
|
|
//let character = if active { "|" } else { "·" };
|
|
//let style = if active { playhead_active } else { playhead_inactive };
|
|
//to.blit(&character, x, to.area.y(), Some(style));
|
|
//}
|
|
//}
|
|
Ok(())
|
|
}).push_x(6).align_sw(),
|
|
TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw(),
|
|
TuiStyle::fg(lower_left.to_string(), title_color).push_x(1).align_sw(),
|
|
TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(),
|
|
TuiStyle::fg(lower_right.to_string(), title_color).pull_x(1).align_se().fill_xy(),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
pub enum PhraseViewMode {
|
|
PianoHorizontal {
|
|
time_zoom: usize,
|
|
note_zoom: PhraseViewNoteZoom,
|
|
},
|
|
PianoVertical {
|
|
time_zoom: usize,
|
|
note_zoom: PhraseViewNoteZoom,
|
|
},
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
pub enum PhraseViewNoteZoom {
|
|
N(usize),
|
|
Half,
|
|
Octant,
|
|
}
|
|
|
|
impl PhraseViewMode {
|
|
pub fn time_zoom (&self) -> usize {
|
|
match self {
|
|
Self::PianoHorizontal { time_zoom, .. } => *time_zoom,
|
|
_ => unimplemented!()
|
|
}
|
|
}
|
|
pub fn set_time_zoom (&mut self, time_zoom: usize) {
|
|
*self = match self {
|
|
Self::PianoHorizontal { note_zoom, .. } => Self::PianoHorizontal {
|
|
note_zoom: *note_zoom,
|
|
time_zoom,
|
|
},
|
|
_ => unimplemented!()
|
|
}
|
|
}
|
|
/// Return a new [BigBuffer] containing a render of the phrase.
|
|
pub fn draw (&self, phrase: &Phrase) -> BigBuffer {
|
|
let mut buffer = BigBuffer::new(self.buffer_width(phrase), self.buffer_height(phrase));
|
|
match self {
|
|
Self::PianoHorizontal { time_zoom, note_zoom } => match note_zoom {
|
|
PhraseViewNoteZoom::N(_) => Self::draw_piano_horizontal(
|
|
&mut buffer, phrase, *time_zoom, 1
|
|
),
|
|
_ => unimplemented!(),
|
|
},
|
|
_ => unimplemented!(),
|
|
}
|
|
buffer
|
|
}
|
|
/// Draw a subsection of the [BigBuffer] onto a regular ratatui [Buffer].
|
|
fn blit_notes (
|
|
&self,
|
|
target: &mut TuiOutput,
|
|
source: &BigBuffer,
|
|
time_start: usize,
|
|
note_hi: usize,
|
|
) {
|
|
let area = target.area();
|
|
let target = &mut target.buffer;
|
|
match self {
|
|
Self::PianoHorizontal { .. } => {
|
|
let [x0, y0, w, h] = area.xywh();
|
|
for (x, target_x) in (x0..x0+w).enumerate() {
|
|
for (y, target_y) in (y0..y0+h).enumerate() {
|
|
let source_x = time_start + x;
|
|
let source_y = note_hi - y;
|
|
// TODO: enable loop rollover:
|
|
//let source_x = (time_start + x) % source.width.max(1);
|
|
//let source_y = (note_hi - y) % source.height.max(1);
|
|
if source_x < source.width && source_y < source.height {
|
|
let target_cell = target.get_mut(target_x, target_y);
|
|
target_cell.set_char('x');
|
|
if let Some(source_cell) = source.get(source_x, source_y) {
|
|
*target_cell = source_cell.clone();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
_ => unimplemented!()
|
|
}
|
|
}
|
|
fn blit_keys (&self, to: &mut TuiOutput, note_hi: usize, note_lo: usize) {
|
|
let style = Some(Style::default().fg(Color::White).bg(Color::Rgb(0, 0, 0)));
|
|
match self {
|
|
Self::PianoHorizontal { .. } => {
|
|
let [x0, y0, _, h] = to.area().xywh();
|
|
for (y, note) in (note_lo..note_hi).rev().enumerate() {
|
|
to.blit(&match note % 12 {
|
|
11 => "██",
|
|
10 => " ",
|
|
9 => "██",
|
|
8 => " ",
|
|
7 => "██",
|
|
6 => " ",
|
|
5 => "██",
|
|
4 => "██",
|
|
3 => " ",
|
|
2 => "██",
|
|
1 => " ",
|
|
0 => "██",
|
|
_ => unreachable!(),
|
|
}, x0, y0 + y as u16, style)
|
|
}
|
|
},
|
|
_ => unimplemented!()
|
|
}
|
|
}
|
|
fn blit_cursor (
|
|
&self,
|
|
to: &mut TuiOutput,
|
|
time_point: usize,
|
|
time_start: usize,
|
|
time_scale: usize,
|
|
note_point: usize,
|
|
note_len: usize,
|
|
note_hi: usize,
|
|
note_lo: usize,
|
|
) {
|
|
match self {
|
|
Self::PianoHorizontal { .. } => {
|
|
let [x0, y0, w, h] = to.area().xywh();
|
|
for (y, note) in (note_lo..note_hi).rev().enumerate() {
|
|
if note == note_point {
|
|
for x in 0..w {
|
|
let time_1 = time_start + x as usize * time_scale;
|
|
let time_2 = time_1 + time_scale;
|
|
if time_1 <= time_point && time_point < time_2 {
|
|
to.blit(&"█", x0 + x as u16, y0 + y as u16, None);
|
|
break
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
},
|
|
_ => unimplemented!()
|
|
}
|
|
}
|
|
/// Determine the required width to render the phrase.
|
|
fn buffer_width (&self, phrase: &Phrase) -> usize {
|
|
match self {
|
|
Self::PianoHorizontal { time_zoom, .. } => {
|
|
phrase.length / time_zoom
|
|
},
|
|
Self::PianoVertical { note_zoom, .. } => match note_zoom {
|
|
PhraseViewNoteZoom::Half => 64,
|
|
PhraseViewNoteZoom::N(n) => 128*n,
|
|
_ => unimplemented!()
|
|
},
|
|
}
|
|
}
|
|
/// Determine the required height to render the phrase.
|
|
fn buffer_height (&self, phrase: &Phrase) -> usize {
|
|
match self {
|
|
Self::PianoHorizontal { note_zoom, .. } => match note_zoom {
|
|
PhraseViewNoteZoom::Half => 64,
|
|
PhraseViewNoteZoom::N(n) => 128*n,
|
|
_ => unimplemented!()
|
|
},
|
|
Self::PianoVertical { time_zoom, .. } => {
|
|
phrase.length / time_zoom
|
|
},
|
|
}
|
|
}
|
|
/// Draw the piano roll using full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
|
fn draw_piano_horizontal (
|
|
target: &mut BigBuffer, phrase: &Phrase, time_zoom: usize, _: usize
|
|
) {
|
|
//cell.set_char(if ppq == 0 {
|
|
//'·'
|
|
//} else if x % (4 * ppq) == 0 {
|
|
//'│'
|
|
//} else if x % ppq == 0 {
|
|
//'╎'
|
|
//} else {
|
|
//'·'
|
|
//});
|
|
//cell.set_fg(Color::Rgb(48, 64, 56));
|
|
let mut notes_on = [false;128];
|
|
for (y, _) in (127..0).enumerate() {
|
|
for (x, _) in (0..phrase.length).step_by(time_zoom).enumerate() {
|
|
let cell = target.get_mut(x, y).unwrap();
|
|
cell.set_fg(Color::Rgb(28, 35, 25));
|
|
cell.set_char('·');
|
|
}
|
|
}
|
|
for (x, time_start) in (0..phrase.length).step_by(time_zoom).enumerate() {
|
|
let time_end = time_start + time_zoom;
|
|
for time in time_start..time_end {
|
|
for (y, note) in (127..0).enumerate() {
|
|
let cell = target.get_mut(x, y).unwrap();
|
|
if notes_on[note] {
|
|
cell.set_fg(Color::Rgb(255, 255, 255));
|
|
cell.set_bg(Color::Rgb(0, 0, 0));
|
|
cell.set_char('▄');
|
|
} else {
|
|
cell.set_char('x');
|
|
}
|
|
}
|
|
for event in phrase.notes[time].iter() {
|
|
match event {
|
|
MidiMessage::NoteOn { key, .. } => {
|
|
let note = key.as_int() as usize;
|
|
target.get_mut(x, 127 - note).unwrap().set_char('█');
|
|
notes_on[note] = true
|
|
},
|
|
MidiMessage::NoteOff { key, .. } => {
|
|
notes_on[key.as_int() as usize] = false
|
|
},
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/// TODO: Draw the piano roll using octant blocks (U+1CD00-U+1CDE5)
|
|
fn draw_piano_horizontal_octant (
|
|
_: &mut BigBuffer, _: &Phrase, _: usize
|
|
) {
|
|
unimplemented!()
|
|
}
|
|
/// TODO: Draw the piano roll using half blocks: ▄▀▄
|
|
fn draw_piano_horizontal_half (
|
|
_: &mut BigBuffer, _: &Phrase, _: usize
|
|
) {
|
|
unimplemented!()
|
|
}
|
|
}
|
|
|
|
//impl PhraseEditorModel {
|
|
//pub(crate) fn redraw (phrase: &Phrase, mode: PhraseViewMode) -> BigBuffer {
|
|
//let mut buf = BigBuffer::new(usize::MAX.min(phrase.length), 65);
|
|
//Self::fill_seq_bg(mode, &mut buf, phrase.length, phrase.ppq);
|
|
//Self::fill_seq_fg(mode, &mut buf, &phrase);
|
|
//buf
|
|
//}
|
|
//fn fill_seq_bg (_mode: PhraseViewMode, buf: &mut BigBuffer, length: usize, ppq: usize) {
|
|
//for x in 0..buf.width {
|
|
//// Only fill as far as phrase length
|
|
//if x as usize >= length { break }
|
|
//// Fill each row with background characters
|
|
//for y in 0..buf.height {
|
|
//if y >= 64 {
|
|
//break
|
|
//}
|
|
//buf.get_mut(x, y).map(|cell|{
|
|
//cell.set_char(if ppq == 0 {
|
|
//'·'
|
|
//} else if x % (4 * ppq) == 0 {
|
|
//'│'
|
|
//} else if x % ppq == 0 {
|
|
//'╎'
|
|
//} else {
|
|
//'·'
|
|
//});
|
|
//cell.set_fg(Color::Rgb(48, 64, 56));
|
|
//cell.modifier = Modifier::DIM;
|
|
//});
|
|
//}
|
|
//}
|
|
//}
|
|
//fn fill_seq_fg (mode: PhraseViewMode, buf: &mut BigBuffer, phrase: &Phrase) {
|
|
//match mode {
|
|
//PhraseViewMode::Horizontal =>
|
|
//Self::fill_seq_fg_horizontal(buf, phrase),
|
|
//PhraseViewMode::HorizontalHalf =>
|
|
//Self::fill_seq_fg_horizontal_half(buf, phrase),
|
|
//PhraseViewMode::Vertical =>
|
|
//Self::fill_seq_fg_vertical(buf, phrase),
|
|
//}
|
|
//}
|
|
//fn fill_seq_fg_horizontal (buf: &mut BigBuffer, phrase: &Phrase) {
|
|
//let mut notes_on = [false;128];
|
|
//for x in 0..buf.width {
|
|
//}
|
|
//}
|
|
//fn fill_seq_fg_horizontal_half (buf: &mut BigBuffer, phrase: &Phrase) {
|
|
//let mut notes_on = [false;128];
|
|
//for x in 0..buf.width {
|
|
//if x as usize >= phrase.length {
|
|
//break
|
|
//}
|
|
//if let Some(notes) = phrase.notes.get(x as usize) {
|
|
//if phrase.percussive {
|
|
//for note in notes {
|
|
//match note {
|
|
//MidiMessage::NoteOn { key, .. } =>
|
|
//notes_on[key.as_int() as usize] = true,
|
|
//_ => {}
|
|
//}
|
|
//}
|
|
//} else {
|
|
//for note in notes {
|
|
//match note {
|
|
//MidiMessage::NoteOn { key, .. } =>
|
|
//notes_on[key.as_int() as usize] = true,
|
|
//MidiMessage::NoteOff { key, .. } =>
|
|
//notes_on[key.as_int() as usize] = false,
|
|
//_ => {}
|
|
//}
|
|
//}
|
|
//}
|
|
//for y in 0..buf.height {
|
|
//if y > 63 {
|
|
//break
|
|
//}
|
|
//let y = 63 - y;
|
|
//if let Some(block) = half_block(
|
|
//notes_on[y as usize * 2 + 1],
|
|
//notes_on[y as usize * 2],
|
|
//) {
|
|
//buf.get_mut(x, y).map(|cell|{
|
|
//cell.set_char(block);
|
|
//cell.set_fg(Color::White);
|
|
//});
|
|
//}
|
|
//}
|
|
//if phrase.percussive {
|
|
//notes_on.fill(false);
|
|
//}
|
|
//}
|
|
//}
|
|
//}
|
|
//}
|
|
////const NTH_OCTAVE: [&'static str; 11] = [
|
|
////"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "X",
|
|
//////"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8",
|
|
////];
|
|
|