tek/crates/tek_sequencer/src/sequencer_tui.rs

435 lines
18 KiB
Rust

use crate::*;
impl Content for Sequencer<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
Stack::down(move|add|{
add(&self.transport)?;
add(&self.phrases.clone()
.split(Direction::Right, 20, &self.editor as &dyn Widget<Engine = Tui>)
.min_y(20))
})
}
}
/// Handle top-level events in standalone arranger.
impl Handle<Tui> for Sequencer<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
if !match self.focused() {
SequencerFocus::Transport => self.transport.handle(from)?,
SequencerFocus::PhrasePool => self.phrases.handle(from)?,
SequencerFocus::PhraseEditor => self.editor.handle(from)?
}.unwrap_or(false) {
match from.event() {
// Tab navigation
key!(KeyCode::Tab) => { self.focus_next(); },
key!(Shift-KeyCode::Tab) => { self.focus_prev(); },
key!(KeyCode::BackTab) => { self.focus_prev(); },
key!(Shift-KeyCode::BackTab) => { self.focus_prev(); },
// Directional navigation
key!(KeyCode::Up) => { self.focus_up(); },
key!(KeyCode::Down) => { self.focus_down(); },
key!(KeyCode::Left) => { self.focus_left(); },
key!(KeyCode::Right) => { self.focus_right(); },
// Global play/pause binding
key!(KeyCode::Char(' ')) => match self.transport {
Some(ref mut transport) => { transport.write().unwrap().toggle_play()?; },
None => { return Ok(None) }
},
_ => {}
}
};
Ok(Some(true))
}
}
// TODO: Display phrases always in order of appearance
impl Content for PhrasePool<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let content = col!(
(i, phrase) in self.phrases.iter().enumerate() => Layers::new(|add|{
let Phrase { ref name, color, .. } = *phrase.read().unwrap();
let row1 = format!(" {i}");
let row2 = if let Some(PhrasePoolMode::Rename(phrase, _)) = self.mode {
if self.focused && i == phrase {
format!(" {}", name)
} else {
format!(" {}", name)
}
} else {
format!(" {}", name)
};
add(&col!(row1, row2).fill_x().bg(if i == self.phrase {
color //Color::Rgb(40, 50, 30)
} else {
color //Color::Rgb(28, 35, 25)
}))?;
if self.focused && i == self.phrase { add(&CORNERS)?; }
Ok(())
})
)
.fill_xy()
.bg(Color::Rgb(28, 35, 25))
.border(Lozenge(Style::default()
.bg(Color::Rgb(40, 50, 30))
.fg(if self.focused {
Color::Rgb(100, 110, 40)
} else {
Color::Rgb(70, 80, 50)
})));
lay!(content, TuiStyle::fg("Phrases", if self.focused {
Color::Rgb(150, 160, 90)
} else {
Color::Rgb(120, 130, 100)
}).push_x(1))
}
}
impl Handle<Tui> for PhrasePool<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
match self.mode {
Some(PhrasePoolMode::Rename(phrase, ref mut old_name)) => {
let mut phrase = self.phrases[phrase].write().unwrap();
match from.event() {
key!(KeyCode::Backspace) => { phrase.name.pop(); },
key!(KeyCode::Char(c)) => { phrase.name.push(*c); },
key!(Shift-KeyCode::Char(c)) => { phrase.name.push(*c); },
key!(KeyCode::Esc) => { phrase.name = old_name.clone(); self.mode = None; },
key!(KeyCode::Enter) => { self.mode = None; },
_ => return Ok(None)
}
},
None => match from.event() {
key!(KeyCode::Up) => self.phrase = if self.phrase > 0 {
self.phrase - 1
} else {
self.phrases.len() - 1
},
key!(KeyCode::Down) => {
self.phrase = (self.phrase + 1) % self.phrases.len()
},
key!(KeyCode::Char('a')) => { // append new
let mut phrase = Phrase::default();
phrase.name = String::from("(no name)");
phrase.color = random_color();
self.phrases.push(Arc::new(RwLock::new(phrase)));
self.phrase = self.phrases.len() - 1;
},
key!(KeyCode::Char('i')) => { // insert new
let mut phrase = Phrase::default();
phrase.name = String::from("(no name)");
phrase.color = random_color();
self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase)));
self.phrase += 1;
},
key!(KeyCode::Char('d')) => { // insert duplicate
let mut phrase = (*self.phrases[self.phrase].read().unwrap()).clone();
phrase.color = random_color_near(phrase.color, 0.1);
self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase)));
self.phrase += 1;
},
key!(KeyCode::Char('c')) => { // change color
let mut phrase = self.phrases[self.phrase].write().unwrap();
phrase.color = random_color();
},
key!(KeyCode::Char('n')) => { // change name
let phrase = self.phrases[self.phrase].read().unwrap();
self.mode = Some(PhrasePoolMode::Rename(self.phrase, phrase.name.clone()));
},
_ => return Ok(None),
}
}
return Ok(Some(true))
}
}
impl Content for PhraseEditor<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let field_bg = Color::Rgb(28, 35, 25);
let toolbar = Stack::down(move|add|{
//let name = format!("{:>9}", self.name.read().unwrap().as_str());
//add(&col!("Track:", TuiStyle::bg(name.as_str(), field_bg)))?;
if let Some(phrase) = &self.phrase {
let phrase = phrase.read().unwrap();
let length = format!("{}q{}p", phrase.length / PPQ, phrase.length % PPQ);
let length = format!("{:>9}", &length);
let loop_on = format!("{:>9}", if phrase.loop_on { "on" } else { "off" });
let loop_start = format!("{:>9}", phrase.loop_start);
let loop_end = format!("{:>9}", phrase.loop_length);
add(&"")?;
add(&col!("Length:", TuiStyle::bg(length.as_str(), field_bg)))?;
add(&col!("Loop:", TuiStyle::bg(loop_on.as_str(), field_bg)))?;
add(&col!("L. start:", TuiStyle::bg(loop_start.as_str(), field_bg)))?;
add(&col!("L. length:", TuiStyle::bg(loop_end.as_str(), field_bg)))?;
}
Ok(())
}).min_x(10);
let piano_roll = lay!(
// keys
CustomWidget::new(|_|Ok(Some([32u16,4u16])), |to: &mut TuiOutput|{
if to.area().h() < 2 { return Ok(()) }
Ok(to.buffer_update(to.area().set_w(5).shrink_y(2), &|cell, x, y|{
let y = y + self.note_axis.start as u16;
if x < self.keys.area.width && y < self.keys.area.height {
*cell = self.keys.get(x, y).clone()
}
}))
}).fill_y(),
// playhead
CustomWidget::new(|_|Ok(Some([32u16,2u16])), |to: &mut TuiOutput|{
if let Some(phrase) = &self.phrase {
let time_0 = self.time_axis.start;
let time_z = self.time_axis.scale;
let now = 0; // TODO FIXME: self.now % phrase.read().unwrap().length;
let [x, y, width, _] = to.area();
let x2 = x as usize + Self::H_KEYS_OFFSET;
let x3 = x as usize + width as usize;
for x in x2..x3 {
let step = (time_0 + x2) * time_z;
let next_step = (time_0 + x2 + 1) * time_z;
to.blit(&"-", x as u16, y, Some(PhraseEditor::<Tui>::style_timer_step(
now, step as usize, next_step as usize
)));
}
}
Ok(())
}).fill_x(),
// notes
CustomWidget::new(|_|Ok(Some([32u16,4u16])), |to: &mut TuiOutput|{
let offset = Self::H_KEYS_OFFSET as u16;
if to.area().h() < 2 || to.area().w() < offset { return Ok(()) }
let area = to.area().push_x(offset).shrink_x(offset).shrink_y(2);
Ok(to.buffer_update(area, &move |cell, x, y|{
cell.set_bg(Color::Rgb(20, 20, 20));
let src_x = ((x as usize + self.time_axis.start) * self.time_axis.scale) as usize;
let src_y = (y as usize + self.note_axis.start) as usize;
if src_x < self.buffer.width && src_y < self.buffer.height - 1 {
let src = self.buffer.get(src_x, self.buffer.height - src_y);
src.map(|src|{ cell.set_symbol(src.symbol()); cell.set_fg(src.fg); });
}
}))
}).fill_x(),
// note cursor
CustomWidget::new(|_|Ok(Some([1u16,1u16])), |to: &mut TuiOutput|{
let area = to.area();
if let (Some(time), Some(note)) = (self.time_axis.point, self.note_axis.point) {
let x = area.x() + Self::H_KEYS_OFFSET as u16 + time as u16;
let y = area.y() + 1 + note as u16 / 2;
let c = if note % 2 == 0 { "" } else { "" };
to.blit(&c, x, y, self.style_focus());
}
Ok(())
}),
// zoom
CustomWidget::new(|_|Ok(Some([10u16,1u16])), |to: &mut TuiOutput|{
let [x, y, w, h] = to.area.xywh();
let quant = ppq_to_name(self.time_axis.scale);
Ok(to.blit(
&quant,
x + w - 1 - quant.len() as u16,
y + h - 2,
self.style_focus()
))
}),
);
let content = row!(toolbar, piano_roll)
.fill_x()
.bg(Color::Rgb(40, 50, 30))
.border(Lozenge(Style::default()
.bg(Color::Rgb(40, 50, 30))
.fg(if self.focused {
Color::Rgb(100, 110, 40)
} else {
Color::Rgb(70, 80, 50)
})));
lay!(content, TuiStyle::fg("Sequencer", if self.focused {
Color::Rgb(150, 160, 90)
} else {
Color::Rgb(120, 130, 100)
}).push_x(1))
}
}
impl Handle<Tui> for PhraseEditor<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
match from.event() {
key!(KeyCode::Char('`')) => {
self.mode = !self.mode;
},
key!(KeyCode::Up) => match self.entered {
true => { self.note_axis.point_dec(); },
false => { self.note_axis.start_dec(); },
},
key!(KeyCode::Down) => match self.entered {
true => { self.note_axis.point_inc(); },
false => { self.note_axis.start_inc(); },
},
key!(KeyCode::Left) => match self.entered {
true => { self.time_axis.point_dec(); },
false => { self.time_axis.start_dec(); },
},
key!(KeyCode::Right) => match self.entered {
true => { self.time_axis.point_inc(); },
false => { self.time_axis.start_inc(); },
},
_ => {
return Ok(None)
}
}
return Ok(Some(true))
}
}
impl PhraseEditor<Tui> {
const H_KEYS_OFFSET: usize = 5;
/// Select which pattern to display. This pre-renders it to the buffer at full resolution.
pub fn show (&mut self, phrase: Option<&Arc<RwLock<Phrase>>>) {
if let Some(phrase) = phrase {
self.phrase = Some(phrase.clone());
let width = usize::MAX.min(phrase.read().unwrap().length);
let mut buffer = BigBuffer::new(width, 64);
let phrase = phrase.read().unwrap();
Self::fill_seq_bg(&mut buffer, phrase.length, phrase.ppq);
Self::fill_seq_fg(&mut buffer, &phrase);
self.buffer = buffer;
} else {
self.phrase = None;
self.buffer = Default::default();
}
}
fn fill_seq_bg (buf: &mut BigBuffer, length: usize, ppq: usize) {
for x in 0..buf.width {
if x as usize >= length {
break
}
let style = Style::default();
buf.get_mut(x, 0).map(|cell|{
cell.set_char('-');
cell.set_style(style);
});
for y in 0 .. buf.height {
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::Gray);
cell.modifier = Modifier::DIM;
});
}
}
}
fn fill_seq_fg (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/2 {
if y >= 64 {
break
}
if let Some(block) = half_block(
notes_on[y as usize * 2],
notes_on[y as usize * 2 + 1],
) {
buf.get_mut(x, y).map(|cell|{
cell.set_char(block);
cell.set_fg(Color::White);
});
}
}
if phrase.percussive {
notes_on.fill(false);
}
}
}
}
pub(crate) fn style_focus (&self) -> Option<Style> {
Some(if self.focused {
Style::default().green().not_dim()
} else {
Style::default().green().dim()
})
}
pub(crate) fn style_timer_step (now: usize, step: usize, next_step: usize) -> Style {
if step <= now && now < next_step {
Style::default().yellow().bold().not_dim()
} else {
Style::default()
}
}
pub fn index_to_color (&self, index: u16, default: Color) -> Color {
let index = index as usize;
let (notes_in, notes_out) = (self.notes_in.read().unwrap(), self.notes_out.read().unwrap());
if notes_in[index] && notes_out[index] {
Color::Yellow
} else if notes_in[index] {
Color::Red
} else if notes_out[index] {
Color::Green
} else {
default
}
}
}
/// Colors of piano keys
const KEY_COLORS: [(Color, Color);6] = [
(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), Color::Rgb(255, 255, 255)),
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
];
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 => {
cell.set_char('▀');
let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize];
cell.set_fg(fg);
cell.set_bg(bg);
},
1 => {
cell.set_char('▀');
cell.set_fg(Color::White);
cell.set_bg(Color::White);
},
2 => if y % 6 == 0 { cell.set_char('C'); },
3 => if y % 6 == 0 { cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]); },
_ => {}
}
});
buffer
}
/// Octave number (from -1 to 9)
const NTH_OCTAVE: [&'static str;11] = [
"-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
];