wip: separate PhrasePlayer vs PhraseEditor

This commit is contained in:
🪞👃🪞 2024-10-08 12:05:47 +03:00
parent 7668a6f339
commit 25e54eba4e
8 changed files with 491 additions and 636 deletions

View file

@ -1,5 +1,4 @@
use crate::*;
impl Content for PhrasePool<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
@ -14,21 +13,154 @@ impl Content for PhrasePool<Tui> {
.fg(Color::Rgb(70, 80, 50))))
}
}
impl Sequencer<Tui> {
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 content = lay!(
// keys
CustomWidget::new(|_|Ok(Some([32,4])), |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([32,2])), |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 = 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([32,4])), |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([1,1])), |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([10,1])), |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()
))
}),
);
row!(toolbar, content).fill_x()
.bg(Color::Rgb(40, 50, 30))
.border(Lozenge(Style::default()
.bg(Color::Rgb(40, 50, 30))
.fg(Color::Rgb(70, 80, 50))))
}
}
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 Focusable<Tui> for PhraseEditor<Tui> {
fn is_focused (&self) -> bool {
self.focused
}
fn set_focused (&mut self, focused: bool) {
self.focused = focused
}
}
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, index: Option<usize>) {
self.viewing_phrase = index;
if let Some(phrase) = index.map(|index|self.phrases.get(index)).flatten() {
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, self.ppq);
Self::fill_seq_bg(&mut buffer, phrase.length, phrase.ppq);
Self::fill_seq_fg(&mut buffer, &phrase);
self.buffer = buffer;
} else {
self.viewing_phrase = None;
self.phrase = None;
self.buffer = Default::default();
}
}
@ -121,130 +253,18 @@ impl Sequencer<Tui> {
}
pub fn index_to_color (&self, index: u16, default: Color) -> Color {
let index = index as usize;
if self.keys_in[index] && self.keys_out[index] {
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 self.keys_in[index] {
} else if notes_in[index] {
Color::Red
} else if self.keys_out[index] {
} else if notes_out[index] {
Color::Green
} else {
default
}
}
}
impl Content for Sequencer<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.viewing_phrase
.map(|index|self.phrases.get(index))
.flatten()
{
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 content = lay!(
// keys
CustomWidget::new(|_|Ok(Some([32,4])), |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([32,2])), |to: &mut TuiOutput|{
if let Some(phrase) = self.viewing_phrase
.map(|index|self.phrases.get(index))
.flatten()
{
let time_0 = self.time_axis.start;
let time_z = self.time_axis.scale;
let now = self.now % phrase.read().unwrap().length;
let [x, y, width, _] = to.area();
let x2 = x as usize + Sequencer::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(Sequencer::<Tui>::style_timer_step(
now, step as usize, next_step as usize
)));
}
}
Ok(())
}).fill_x(),
// notes
CustomWidget::new(|_|Ok(Some([32,4])), |to: &mut TuiOutput|{
let offset = Sequencer::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([1,1])), |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() + Sequencer::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([10,1])), |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()
))
}),
);
row!(toolbar, content).fill_x()
.bg(Color::Rgb(40, 50, 30))
.border(Lozenge(Style::default()
.bg(Color::Rgb(40, 50, 30))
.fg(Color::Rgb(70, 80, 50))))
}
}
impl Focusable<Tui> for Sequencer<Tui> {
fn is_focused (&self) -> bool {
self.focused
}
fn set_focused (&mut self, focused: bool) {
self.focused = focused
}
}
/// Colors of piano keys
const KEY_COLORS: [(Color, Color);6] = [
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
@ -254,7 +274,6 @@ const KEY_COLORS: [(Color, Color);6] = [
(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 {
@ -281,38 +300,7 @@ pub(crate) fn keys_vert () -> Buffer {
});
buffer
}
/// Octave number (from -1 to 9)
const NTH_OCTAVE: [&'static str;11] = [
"-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
];
impl Handle<Tui> for Sequencer<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))
}
}