mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
1088 lines
43 KiB
Rust
1088 lines
43 KiB
Rust
use crate::*;
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
const CORNERS: CornersTall = CornersTall(NOT_DIM_GREEN);
|
|
|
|
tui_style!(GRAY_DIM =
|
|
Some(Color::Gray), None, None, Modifier::DIM, Modifier::empty());
|
|
tui_style!(GRAY_NOT_DIM_BOLD =
|
|
Some(Color::Gray), None, None, Modifier::BOLD, Modifier::DIM);
|
|
tui_style!(NOT_DIM_BOLD =
|
|
None, None, None, Modifier::BOLD, Modifier::DIM);
|
|
tui_style!(NOT_DIM_GREEN =
|
|
Some(Color::Rgb(96, 255, 32)), Some(COLOR_BG1), None, Modifier::empty(), Modifier::DIM);
|
|
tui_style!(NOT_DIM =
|
|
None, None, None, Modifier::empty(), Modifier::DIM);
|
|
tui_style!(WHITE_NOT_DIM_BOLD =
|
|
Some(Color::White), None, None, Modifier::BOLD, Modifier::DIM);
|
|
tui_style!(STYLE_LABEL =
|
|
Some(Color::Reset), None, None, Modifier::empty(), Modifier::BOLD);
|
|
tui_style!(STYLE_VALUE =
|
|
Some(Color::White), None, None, Modifier::BOLD, Modifier::DIM);
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
impl Arranger<Tui> {
|
|
pub fn rename_selected (&mut self) {
|
|
self.modal = Some(Box::new(ArrangerRenameModal::new(
|
|
self.selected,
|
|
&match self.selected {
|
|
ArrangerFocus::Mix => self.name.clone(),
|
|
ArrangerFocus::Track(t) => self.tracks[t].name.clone(),
|
|
ArrangerFocus::Scene(s) => self.scenes[s].name.clone(),
|
|
ArrangerFocus::Clip(t, s) => self.tracks[t].phrases[s].read().unwrap().name.clone(),
|
|
}
|
|
)));
|
|
}
|
|
}
|
|
impl Focusable<Tui> for Arranger<Tui> {
|
|
fn is_focused (&self) -> bool {
|
|
self.focused
|
|
}
|
|
fn set_focused (&mut self, focused: bool) {
|
|
self.focused = focused
|
|
}
|
|
}
|
|
impl Handle<Tui> for Arranger<Tui> {
|
|
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
if let Some(modal) = self.modal.as_mut() {
|
|
let result = modal.handle(from)?;
|
|
if from.is_done() {
|
|
self.modal = None;
|
|
}
|
|
return Ok(result)
|
|
}
|
|
match from.event() {
|
|
// mode_switch: switch the display mode
|
|
key!(KeyCode::Char('`')) => {
|
|
self.mode.to_next()
|
|
},
|
|
// cursor_up: move cursor up
|
|
key!(KeyCode::Up) => {
|
|
match self.mode {
|
|
ArrangerViewMode::Horizontal => self.track_prev(),
|
|
_ => self.scene_prev(),
|
|
};
|
|
self.show_phrase()?;
|
|
},
|
|
// cursor_down
|
|
key!(KeyCode::Down) => {
|
|
match self.mode {
|
|
ArrangerViewMode::Horizontal => self.track_next(),
|
|
_ => self.scene_next(),
|
|
};
|
|
self.show_phrase()?;
|
|
},
|
|
// cursor left
|
|
key!(KeyCode::Left) => {
|
|
match self.mode {
|
|
ArrangerViewMode::Horizontal => self.scene_prev(),
|
|
_ => self.track_prev(),
|
|
};
|
|
self.show_phrase()?;
|
|
},
|
|
// cursor right
|
|
key!(KeyCode::Right) => {
|
|
match self.mode {
|
|
ArrangerViewMode::Horizontal => self.scene_next(),
|
|
_ => self.track_next(),
|
|
};
|
|
self.show_phrase()?;
|
|
},
|
|
// increment: use next clip here
|
|
key!(KeyCode::Char('.')) => {
|
|
self.phrase_next();
|
|
},
|
|
// decrement: use previous next clip here
|
|
key!(KeyCode::Char(',')) => {
|
|
self.phrase_prev();
|
|
},
|
|
// decrement: use previous clip here
|
|
key!(KeyCode::Enter) => {
|
|
self.activate();
|
|
},
|
|
// scene_add: add a new scene
|
|
key!(Ctrl-KeyCode::Char('a')) => {
|
|
self.scene_add(None)?;
|
|
},
|
|
// track_add: add a new scene
|
|
key!(Ctrl-KeyCode::Char('t')) => {
|
|
self.track_add(None)?;
|
|
},
|
|
// rename: add a new scene
|
|
key!(KeyCode::Char('n')) => {
|
|
self.rename_selected();
|
|
},
|
|
// length: add a new scene
|
|
key!(KeyCode::Char('l')) => {
|
|
todo!();
|
|
},
|
|
// color: set color of item at cursor
|
|
key!(KeyCode::Char('c')) => {
|
|
todo!();
|
|
},
|
|
_ => return Ok(None)
|
|
}
|
|
Ok(Some(true))
|
|
}
|
|
}
|
|
impl Content for Arranger<Tui> {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
Layers::new(move |add|{
|
|
match self.mode {
|
|
ArrangerViewMode::Horizontal => add(&HorizontalArranger(&self)),
|
|
ArrangerViewMode::Vertical(factor) => add(&VerticalArranger(&self, factor))
|
|
}?;
|
|
add(&Align::SE(self.selected.description(
|
|
&self.tracks,
|
|
&self.scenes,
|
|
).as_str()))
|
|
})
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
impl<'a> Content for VerticalArranger<'a, Tui> {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
let Self(state, factor) = self;
|
|
let ppq = 96;
|
|
let (cols, rows) = if *factor == 0 {(
|
|
track_clip_name_lengths(state.tracks.as_slice()),
|
|
scene_ppqs(state.tracks.as_slice(), state.scenes.as_slice()),
|
|
)} else {(
|
|
track_clip_name_lengths(state.tracks.as_slice()),
|
|
(0..=state.scenes.len()).map(|i|(factor*ppq, factor*ppq*i)).collect::<Vec<_>>(),
|
|
)};
|
|
//let height = rows.last().map(|(w,y)|(y+w)/ppq).unwrap_or(16);
|
|
let tracks: &[Sequencer<Tui>] = state.tracks.as_ref();
|
|
let scenes: &[Scene] = state.scenes.as_ref();
|
|
let offset = 4 + scene_name_max_len(scenes) as u16;
|
|
Layers::new(move |add|{
|
|
let rows: &[(usize, usize)] = rows.as_ref();
|
|
let cols: &[(usize, usize)] = cols.as_ref();
|
|
let track_titles = row!((track, (w, _)) in tracks.iter().zip(cols) =>
|
|
(&track.name.read().unwrap().as_str() as &dyn Widget<Engine = Tui>)
|
|
.min_xy(*w as u16, 2).push_x(offset));
|
|
let scene_name = |scene, playing: bool, height|row!(
|
|
if playing { "▶ " } else { " " },
|
|
(scene as &Scene).name.read().unwrap().as_str(),
|
|
).fixed_xy(offset.saturating_sub(1), height);
|
|
let scene_clip = |scene, track: usize, w: u16, h: u16|Layers::new(move |add|{
|
|
let mut color = Color::Rgb(40, 50, 30);
|
|
match (tracks.get(track), (scene as &Scene).clips.get(track)) {
|
|
(Some(track), Some(Some(clip))) => match track.phrases.get(*clip) {
|
|
Some(phrase) => {
|
|
let name = &(phrase as &Arc<RwLock<Phrase>>).read().unwrap().name;
|
|
let name = name.read().unwrap();
|
|
let name = format!("{clip:02} {}", name);
|
|
add(&name.as_str().push_x(1))?;
|
|
if (track as &Sequencer<_>).sequence == Some(*clip) {
|
|
color = COLOR_PLAYING
|
|
} else {
|
|
color = COLOR_BG1
|
|
};
|
|
},
|
|
_ => {}
|
|
},
|
|
_ => {}
|
|
};
|
|
add(&Background(color))
|
|
}).fixed_xy(w, h);
|
|
let tracks_clips = col!((scene, (pulses, _)) in scenes.iter().zip(rows) => {
|
|
let height = 1.max((pulses / 96) as u16);
|
|
let playing = scene.is_playing(tracks);
|
|
Stack::right(move |add| {
|
|
add(&scene_name(scene, playing, height))?;
|
|
for (track, (w, _x)) in cols.iter().enumerate() {
|
|
add(&scene_clip(scene, track, *w as u16, height))?;
|
|
}
|
|
Ok(())
|
|
}).fixed_y(height)
|
|
});
|
|
add(&VerticalArrangerGrid(offset, &rows, &cols))?;
|
|
add(&VerticalArrangerCursor(state.focused, state.selected, offset, &cols, &rows))?;
|
|
add(&col!(track_titles, tracks_clips))?;
|
|
Ok(())
|
|
})
|
|
.bg(Color::Rgb(28, 35, 25))
|
|
.border(Lozenge(Style::default()
|
|
.bg(Color::Rgb(40, 50, 30))
|
|
.fg(Color::Rgb(70, 80, 50))))
|
|
}
|
|
}
|
|
|
|
pub fn track_clip_name_lengths <E: Engine> (tracks: &[Sequencer<E>]) -> Vec<(usize, usize)> {
|
|
let mut total = 0;
|
|
let mut lengths: Vec<(usize, usize)> = tracks.iter().map(|track|{
|
|
let len = 4 + track.phrases
|
|
.iter()
|
|
.fold(track.name.read().unwrap().len(), |len, phrase|{
|
|
len.max(phrase.read().unwrap().name.read().unwrap().len())
|
|
});
|
|
total = total + len;
|
|
(len, total - len)
|
|
}).collect();
|
|
lengths.push((0, total));
|
|
lengths
|
|
}
|
|
|
|
pub fn scene_ppqs <E: Engine> (tracks: &[Sequencer<E>], scenes: &[Scene]) -> Vec<(usize, usize)> {
|
|
let mut total = 0;
|
|
let mut scenes: Vec<(usize, usize)> = scenes.iter().map(|scene|{
|
|
let pulses = scene.pulses(tracks).max(96);
|
|
total = total + pulses;
|
|
(pulses, total - pulses)
|
|
}).collect();
|
|
scenes.push((0, total));
|
|
scenes
|
|
}
|
|
|
|
pub fn scene_name_max_len (scenes: &[Scene]) -> usize {
|
|
scenes.iter()
|
|
.map(|s|s.name.read().unwrap().len())
|
|
.fold(0, usize::max)
|
|
}
|
|
|
|
impl<'a> Widget for VerticalArrangerGrid<'a> {
|
|
type Engine = Tui;
|
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
|
let area = to.area();
|
|
let Self(offset, rows, cols) = self;
|
|
let style = Some(Style::default().fg(COLOR_SEPARATOR));
|
|
for (_, x) in cols.iter() {
|
|
let x = offset + area.x() + *x as u16 - 1;
|
|
for y in area.y()..area.y2() {
|
|
to.blit(&"▎", x, y, style);
|
|
}
|
|
}
|
|
for (_, y) in rows.iter() {
|
|
let y = area.y() + (*y / 96) as u16 + 1;
|
|
if y >= to.buffer.area.height {
|
|
break
|
|
}
|
|
for x in area.x()..area.x2().saturating_sub(2) {
|
|
if x < to.buffer.area.x && y < to.buffer.area.y {
|
|
let cell = to.buffer.get_mut(x, y);
|
|
cell.modifier = Modifier::UNDERLINED;
|
|
cell.underline_color = COLOR_SEPARATOR;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl<'a> Widget for VerticalArrangerCursor<'a> {
|
|
type Engine = Tui;
|
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
|
let area = to.area();
|
|
let Self(focused, selected, offset, cols, rows) = *self;
|
|
let get_track_area = |t: usize| [
|
|
offset + area.x() + cols[t].1 as u16 - 1,
|
|
area.y(),
|
|
cols[t].0 as u16,
|
|
area.h()
|
|
];
|
|
let get_scene_area = |s: usize| [
|
|
area.x(),
|
|
2 + area.y() + (rows[s].1 / 96) as u16,
|
|
area.w(),
|
|
(rows[s].0 / 96) as u16
|
|
];
|
|
let get_clip_area = |t: usize, s: usize| [
|
|
offset + area.x() + cols[t].1 as u16 - 1,
|
|
2 + area.y() + (rows[s].1 / 96) as u16,
|
|
cols[t].0 as u16,
|
|
(rows[s].0 / 96) as u16
|
|
];
|
|
let mut track_area: Option<[u16;4]> = None;
|
|
let mut scene_area: Option<[u16;4]> = None;
|
|
let mut clip_area: Option<[u16;4]> = None;
|
|
let area = match selected {
|
|
ArrangerFocus::Mix => {
|
|
if focused {
|
|
to.fill_bg(area, Color::Rgb(40, 50, 30));
|
|
}
|
|
area
|
|
},
|
|
ArrangerFocus::Track(t) => {
|
|
track_area = Some(get_track_area(t));
|
|
area
|
|
},
|
|
ArrangerFocus::Scene(s) => {
|
|
scene_area = Some(get_scene_area(s));
|
|
area
|
|
},
|
|
ArrangerFocus::Clip(t, s) => {
|
|
track_area = Some(get_track_area(t));
|
|
scene_area = Some(get_scene_area(s));
|
|
clip_area = Some(get_clip_area(t, s));
|
|
area
|
|
},
|
|
};
|
|
if let Some([x, y, width, height]) = track_area {
|
|
to.fill_fg([x, y, 1, height], Color::Rgb(70, 80, 50));
|
|
to.fill_fg([x + width, y, 1, height], Color::Rgb(70, 80, 50));
|
|
}
|
|
if let Some([_, y, _, height]) = scene_area {
|
|
to.fill_ul([area.x(), y - 1, area.w(), 1], Color::Rgb(70, 80, 50));
|
|
to.fill_ul([area.x(), y + height - 1, area.w(), 1], Color::Rgb(70, 80, 50));
|
|
}
|
|
if focused {
|
|
if let Some(clip_area) = clip_area {
|
|
to.render_in(clip_area, &CORNERS)?;
|
|
to.fill_bg(clip_area, Color::Rgb(40, 50, 30));
|
|
} else if let Some(track_area) = track_area {
|
|
to.render_in(track_area.clip_h(2), &CORNERS)?;
|
|
to.fill_bg(track_area, Color::Rgb(40, 50, 30));
|
|
} else if let Some(scene_area) = scene_area {
|
|
to.render_in(scene_area.clip_w(offset-1), &CORNERS)?;
|
|
to.fill_bg(scene_area, Color::Rgb(40, 50, 30));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
impl<'a> Content for HorizontalArranger<'a, Tui> {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
let Arranger { tracks, focused, selected, scenes, .. } = self.0;
|
|
let _tracks = tracks.as_slice();
|
|
lay!(
|
|
focused.then_some(Background(Color::Rgb(40, 50, 30))),
|
|
row!(
|
|
// name
|
|
CustomWidget::new(|_|{
|
|
todo!()
|
|
}, |_: &mut TuiOutput|{
|
|
todo!()
|
|
//let Self(tracks, selected) = self;
|
|
//let yellow = Some(Style::default().yellow().bold().not_dim());
|
|
//let white = Some(Style::default().white().bold().not_dim());
|
|
//let area = to.area();
|
|
//let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()];
|
|
//let offset = 0; // track scroll offset
|
|
//for y in 0..area.h() {
|
|
//if y == 0 {
|
|
//to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?;
|
|
//} else if y % 2 == 0 {
|
|
//let index = (y as usize - 2) / 2 + offset;
|
|
//if let Some(track) = tracks.get(index) {
|
|
//let selected = selected.track() == Some(index);
|
|
//let style = if selected { yellow } else { white };
|
|
//to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?;
|
|
//to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?;
|
|
//}
|
|
//}
|
|
//}
|
|
//Ok(Some(area))
|
|
}),
|
|
// monitor
|
|
CustomWidget::new(|_|{
|
|
todo!()
|
|
}, |_: &mut TuiOutput|{
|
|
todo!()
|
|
//let Self(tracks) = self;
|
|
//let mut area = to.area();
|
|
//let on = Some(Style::default().not_dim().green().bold());
|
|
//let off = Some(DIM);
|
|
//area.x += 1;
|
|
//for y in 0..area.h() {
|
|
//if y == 0 {
|
|
////" MON ".blit(to.buffer, area.x, area.y + y, style2)?;
|
|
//} else if y % 2 == 0 {
|
|
//let index = (y as usize - 2) / 2;
|
|
//if let Some(track) = tracks.get(index) {
|
|
//let style = if track.monitoring { on } else { off };
|
|
//to.blit(&" MON ", area.x(), area.y() + y, style)?;
|
|
//} else {
|
|
//area.height = y;
|
|
//break
|
|
//}
|
|
//}
|
|
//}
|
|
//area.width = 4;
|
|
//Ok(Some(area))
|
|
}),
|
|
// record
|
|
CustomWidget::new(|_|{
|
|
todo!()
|
|
}, |_: &mut TuiOutput|{
|
|
todo!()
|
|
//let Self(tracks) = self;
|
|
//let mut area = to.area();
|
|
//let on = Some(Style::default().not_dim().red().bold());
|
|
//let off = Some(Style::default().dim());
|
|
//area.x += 1;
|
|
//for y in 0..area.h() {
|
|
//if y == 0 {
|
|
////" REC ".blit(to.buffer, area.x, area.y + y, style2)?;
|
|
//} else if y % 2 == 0 {
|
|
//let index = (y as usize - 2) / 2;
|
|
//if let Some(track) = tracks.get(index) {
|
|
//let style = if track.recording { on } else { off };
|
|
//to.blit(&" REC ", area.x(), area.y() + y, style)?;
|
|
//} else {
|
|
//area.height = y;
|
|
//break
|
|
//}
|
|
//}
|
|
//}
|
|
//area.width = 4;
|
|
//Ok(Some(area))
|
|
}),
|
|
// overdub
|
|
CustomWidget::new(|_|{
|
|
todo!()
|
|
}, |_: &mut TuiOutput|{
|
|
todo!()
|
|
//let Self(tracks) = self;
|
|
//let mut area = to.area();
|
|
//let on = Some(Style::default().not_dim().yellow().bold());
|
|
//let off = Some(Style::default().dim());
|
|
//area.x = area.x + 1;
|
|
//for y in 0..area.h() {
|
|
//if y == 0 {
|
|
////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?;
|
|
//} else if y % 2 == 0 {
|
|
//let index = (y as usize - 2) / 2;
|
|
//if let Some(track) = tracks.get(index) {
|
|
//to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub {
|
|
//on
|
|
//} else {
|
|
//off
|
|
//})?;
|
|
//} else {
|
|
//area.height = y;
|
|
//break
|
|
//}
|
|
//}
|
|
//}
|
|
//area.width = 4;
|
|
//Ok(Some(area))
|
|
}),
|
|
// erase
|
|
CustomWidget::new(|_|{
|
|
todo!()
|
|
}, |_: &mut TuiOutput|{
|
|
todo!()
|
|
}),
|
|
// gain
|
|
CustomWidget::new(|_|{
|
|
todo!()
|
|
//let Self(tracks) = self;
|
|
//let mut area = to.area();
|
|
//let off = Some(Style::default().dim());
|
|
//area.x = area.x + 1;
|
|
//for y in 0..area.h() {
|
|
//if y == 0 {
|
|
////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?;
|
|
//} else if y % 2 == 0 {
|
|
//let index = (y as usize - 2) / 2;
|
|
//if let Some(_) = tracks.get(index) {
|
|
//to.blit(&" DEL ", area.x(), area.y() + y, off)?;
|
|
//} else {
|
|
//area.height = y;
|
|
//break
|
|
//}
|
|
//}
|
|
//}
|
|
//area.width = 4;
|
|
//Ok(Some(area))
|
|
}, |_: &mut TuiOutput|{
|
|
todo!()
|
|
//let Self(tracks) = self;
|
|
//let mut area = to.area();
|
|
//let off = Some(Style::default().dim());
|
|
//area.x = area.x() + 1;
|
|
//for y in 0..area.h() {
|
|
//if y == 0 {
|
|
////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?;
|
|
//} else if y % 2 == 0 {
|
|
//let index = (y as usize - 2) / 2;
|
|
//if let Some(_) = tracks.get(index) {
|
|
//to.blit(&" +0.0 ", area.x(), area.y() + y, off)?;
|
|
//} else {
|
|
//area.height = y;
|
|
//break
|
|
//}
|
|
//}
|
|
//}
|
|
//area.width = 7;
|
|
//Ok(Some(area))
|
|
}),
|
|
// scenes
|
|
CustomWidget::new(|_|{
|
|
todo!()
|
|
}, |to: &mut TuiOutput|{
|
|
let Arranger { tracks, scenes, selected, .. } = self.0;
|
|
let area = to.area();
|
|
let mut x2 = 0;
|
|
let [x, y, _, height] = area;
|
|
for (scene_index, scene) in scenes.iter().enumerate() {
|
|
let active_scene = selected.scene() == Some(scene_index);
|
|
let sep = Some(if active_scene {
|
|
Style::default().yellow().not_dim()
|
|
} else {
|
|
Style::default().dim()
|
|
});
|
|
for y in y+1..y+height {
|
|
to.blit(&"│", x + x2, y, sep);
|
|
}
|
|
let name = scene.name.read().unwrap();
|
|
let mut x3 = name.len() as u16;
|
|
to.blit(&*name, x + x2, y, sep);
|
|
for (i, clip) in scene.clips.iter().enumerate() {
|
|
let active_track = selected.track() == Some(i);
|
|
if let Some(clip) = clip {
|
|
let y2 = y + 2 + i as u16 * 2;
|
|
let label = match tracks[i].phrases.get(*clip) {
|
|
Some(phrase) => &format!("{}", phrase.read().unwrap().name.read().unwrap()),
|
|
None => "...."
|
|
};
|
|
to.blit(&label, x + x2, y2, Some(if active_track && active_scene {
|
|
Style::default().not_dim().yellow().bold()
|
|
} else {
|
|
Style::default().not_dim()
|
|
}));
|
|
x3 = x3.max(label.len() as u16)
|
|
}
|
|
}
|
|
x2 = x2 + x3 + 1;
|
|
}
|
|
//Ok(Some([x, y, x2, height]))
|
|
Ok(())
|
|
}),
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
pub fn track_name_max_len <E: Engine> (tracks: &[Sequencer<E>]) -> usize {
|
|
tracks.iter()
|
|
.map(|s|s.name.read().unwrap().len())
|
|
.fold(0, usize::max)
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
impl Content for ArrangerRenameModal<Tui> {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
todo!();
|
|
Layers::new(|add|{Ok(())})
|
|
//let area = to.area();
|
|
//let y = area.y() + area.h() / 2;
|
|
//let bg_area = [1, y - 1, area.w() - 2, 3];
|
|
//to.fill_bg(bg_area, COLOR_BG1);
|
|
//Lozenge(Style::default().bold().white().dim()).draw(to.with_rect(bg_area));
|
|
//let label = match self.target {
|
|
//ArrangerFocus::Mix => "Rename project:",
|
|
//ArrangerFocus::Track(_) => "Rename track:",
|
|
//ArrangerFocus::Scene(_) => "Rename scene:",
|
|
//ArrangerFocus::Clip(_, _) => "Rename clip:",
|
|
//};
|
|
//let style = Some(Style::default().not_bold().white().not_dim());
|
|
//to.blit(&label, area.x() + 3, y, style);
|
|
//let style = Some(Style::default().bold().white().not_dim());
|
|
//to.blit(&self.value, area.x() + 3 + label.len() as u16 + 1, y, style);
|
|
//let style = Some(Style::default().bold().white().not_dim().reversed());
|
|
//to.blit(&"▂", area.x() + 3 + label.len() as u16 + 1 + self.cursor as u16, y, style);
|
|
//Ok(Some(area))
|
|
//Ok(())
|
|
}
|
|
}
|
|
|
|
impl Handle<Tui> for ArrangerRenameModal<Tui> {
|
|
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
match from.event() {
|
|
TuiEvent::Input(Event::Key(k)) => {
|
|
match k.code {
|
|
KeyCode::Esc => {
|
|
self.exit();
|
|
},
|
|
KeyCode::Enter => {
|
|
*self.result.write().unwrap() = self.value.clone();
|
|
self.exit();
|
|
},
|
|
KeyCode::Left => {
|
|
self.cursor = self.cursor.saturating_sub(1);
|
|
},
|
|
KeyCode::Right => {
|
|
self.cursor = self.value.len().min(self.cursor + 1)
|
|
},
|
|
KeyCode::Backspace => {
|
|
let last = self.value.len().saturating_sub(1);
|
|
self.value = format!("{}{}",
|
|
&self.value[0..self.cursor.min(last)],
|
|
&self.value[self.cursor.min(last)..last]
|
|
);
|
|
self.cursor = self.cursor.saturating_sub(1)
|
|
}
|
|
KeyCode::Char(c) => {
|
|
self.value.insert(self.cursor, c);
|
|
self.cursor = self.value.len().min(self.cursor + 1)
|
|
},
|
|
_ => {}
|
|
}
|
|
Ok(Some(true))
|
|
},
|
|
_ => Ok(None),
|
|
}
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
impl Sequencer<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>>>) -> Usually<()> {
|
|
self.phrase = phrase.map(Clone::clone);
|
|
if let Some(ref phrase) = self.phrase {
|
|
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_fg(&mut buffer, &phrase)?;
|
|
self.buffer = buffer;
|
|
} else {
|
|
self.buffer = Default::default();
|
|
}
|
|
Ok(())
|
|
}
|
|
fn fill_seq_bg (buf: &mut BigBuffer, length: usize, ppq: usize) -> Usually<()> {
|
|
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;
|
|
});
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) -> Usually<()> {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
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;
|
|
if self.keys_in[index] && self.keys_out[index] {
|
|
Color::Yellow
|
|
} else if self.keys_in[index] {
|
|
Color::Red
|
|
} else if self.keys_out[index] {
|
|
Color::Green
|
|
} else {
|
|
default
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Content for Sequencer<Tui> {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
let toolbar = Stack::down(move|add|{
|
|
add(&col! {"Name", self.name.read().unwrap().as_str(),})?;
|
|
if let Some(phrase) = self.phrase.as_ref() {
|
|
panic!();
|
|
let phrase = phrase.read().unwrap();
|
|
let length = phrase.length;
|
|
let looped = phrase.looped;
|
|
add(&"")?;
|
|
add(&col! {"Length: ", format!("{length}").as_str(),})?;
|
|
add(&"")?;
|
|
add(&col! { "Loop [ ]", "From: ", " 1.1.1", "Length: ", " 1.0.0", })?;
|
|
add(&"")?;
|
|
add(&col! { "Notes: ", "C#0-C#9 ", "[ /2 ]", "[ x2 ]"
|
|
, "[ Rev ]", "[ Inv ]", "[ Dup ]" })?;
|
|
}
|
|
Ok(())
|
|
}).min_x(10);
|
|
let content = lay!(
|
|
// keys
|
|
CustomWidget::new(|_|Ok(Some([32,4])), |to: &mut TuiOutput|{
|
|
if to.area().h() < 2 { return 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()
|
|
}
|
|
});
|
|
Ok(())
|
|
}).fill_y(),
|
|
// playhead
|
|
CustomWidget::new(|_|Ok(Some([32,2])), |to: &mut TuiOutput|{
|
|
if let Some(phrase) = self.phrase.as_ref() {
|
|
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);
|
|
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); });
|
|
}
|
|
});
|
|
Ok(())
|
|
}).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);
|
|
let x = x + w - 1 - quant.len() as u16;
|
|
let y = y + h - 2;
|
|
to.blit(&quant, x, y, self.style_focus());
|
|
Ok(())
|
|
}),
|
|
);
|
|
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)),
|
|
(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"
|
|
];
|
|
|
|
impl Handle<Tui> for Sequencer<Tui> {
|
|
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
match from.event() {
|
|
// NONE, "seq_cursor_up", "move cursor up", |sequencer: &mut Sequencer| {
|
|
key!(KeyCode::Up) => {
|
|
match self.entered {
|
|
true => { self.note_axis.point_dec(); },
|
|
false => { self.note_axis.start_dec(); },
|
|
}
|
|
Ok(Some(true))
|
|
},
|
|
// NONE, "seq_cursor_down", "move cursor down", |self: &mut Sequencer| {
|
|
key!(KeyCode::Down) => {
|
|
match self.entered {
|
|
true => { self.note_axis.point_inc(); },
|
|
false => { self.note_axis.start_inc(); },
|
|
}
|
|
Ok(Some(true))
|
|
},
|
|
// NONE, "seq_cursor_left", "move cursor up", |self: &mut Sequencer| {
|
|
key!(KeyCode::Left) => {
|
|
match self.entered {
|
|
true => { self.time_axis.point_dec(); },
|
|
false => { self.time_axis.start_dec(); },
|
|
}
|
|
Ok(Some(true))
|
|
},
|
|
// NONE, "seq_cursor_right", "move cursor up", |self: &mut Sequencer| {
|
|
key!(KeyCode::Right) => {
|
|
match self.entered {
|
|
true => { self.time_axis.point_inc(); },
|
|
false => { self.time_axis.start_inc(); },
|
|
}
|
|
Ok(Some(true))
|
|
},
|
|
// NONE, "seq_mode_switch", "switch the display mode", |self: &mut Sequencer| {
|
|
key!(KeyCode::Char('`')) => {
|
|
self.mode = !self.mode;
|
|
Ok(Some(true))
|
|
},
|
|
_ => Ok(None)
|
|
}
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
impl TransportToolbar<Tui> {
|
|
fn handle_bpm (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
match from.event() {
|
|
key!(KeyCode::Char(',')) => { self.bpm -= 1.0; },
|
|
key!(KeyCode::Char('.')) => { self.bpm += 1.0; },
|
|
key!(KeyCode::Char('<')) => { self.bpm -= 0.001; },
|
|
key!(KeyCode::Char('>')) => { self.bpm += 0.001; },
|
|
_ => return Ok(None)
|
|
}
|
|
Ok(Some(true))
|
|
}
|
|
fn handle_quant (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
match from.event() {
|
|
key!(KeyCode::Char(',')) => {
|
|
self.quant = prev_note_length(self.quant);
|
|
Ok(Some(true))
|
|
},
|
|
key!(KeyCode::Char('.')) => {
|
|
self.quant = next_note_length(self.quant);
|
|
Ok(Some(true))
|
|
},
|
|
_ => Ok(None)
|
|
}
|
|
}
|
|
fn handle_sync (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
match from.event() {
|
|
key!(KeyCode::Char(',')) => {
|
|
self.sync = prev_note_length(self.sync);
|
|
Ok(Some(true))
|
|
},
|
|
key!(KeyCode::Char('.')) => {
|
|
self.sync = next_note_length(self.sync);
|
|
Ok(Some(true))
|
|
},
|
|
_ => Ok(None)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Handle<Tui> for TransportToolbar<Tui> {
|
|
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
match from.event() {
|
|
key!(KeyCode::Left) => { self.focus.prev(); },
|
|
key!(KeyCode::Right) => { self.focus.next(); },
|
|
_ => match self.focus {
|
|
TransportToolbarFocus::PlayPause => self.toggle_play().map(|_|())?,
|
|
TransportToolbarFocus::Bpm => self.handle_bpm(from).map(|_|())?,
|
|
TransportToolbarFocus::Quant => self.handle_quant(from).map(|_|())?,
|
|
TransportToolbarFocus::Sync => self.handle_sync(from).map(|_|())?,
|
|
TransportToolbarFocus::Clock => {/*todo*/},
|
|
}
|
|
}
|
|
Ok(Some(true))
|
|
}
|
|
}
|
|
|
|
impl Content for TransportToolbar<Tui> {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
row! {
|
|
|
|
// play/pause
|
|
self.focus.wrap(self.focused, TransportToolbarFocus::PlayPause, &Styled(
|
|
match self.playing {
|
|
Some(TransportState::Stopped) => Some(GRAY_DIM.bold()),
|
|
Some(TransportState::Starting) => Some(GRAY_NOT_DIM_BOLD),
|
|
Some(TransportState::Rolling) => Some(WHITE_NOT_DIM_BOLD),
|
|
_ => unreachable!(),
|
|
},
|
|
match self.playing {
|
|
Some(TransportState::Rolling) => "▶ PLAYING",
|
|
Some(TransportState::Starting) => "READY ...",
|
|
Some(TransportState::Stopped) => "⏹ STOPPED",
|
|
_ => unreachable!(),
|
|
}
|
|
).min_xy(11, 2).push_x(1)),
|
|
|
|
// bpm
|
|
self.focus.wrap(self.focused, TransportToolbarFocus::Bpm, &Outset::X(1u16, col! {
|
|
"BPM",
|
|
format!("{}.{:03}", self.bpm as usize, (self.bpm * 1000.0) % 1000.0).as_str()
|
|
})),
|
|
|
|
// quant
|
|
self.focus.wrap(self.focused, TransportToolbarFocus::Quant, &Outset::X(1u16, col! {
|
|
"QUANT", ppq_to_name(self.quant as usize)
|
|
})),
|
|
|
|
// sync
|
|
self.focus.wrap(self.focused, TransportToolbarFocus::Sync, &Outset::X(1u16, col! {
|
|
"SYNC", ppq_to_name(self.sync as usize)
|
|
})),
|
|
|
|
// clock
|
|
self.focus.wrap(self.focused, TransportToolbarFocus::Clock, &{
|
|
let Self { frame: _frame, pulse, ppq, usecs, .. } = self;
|
|
let (beats, pulses) = if *ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
|
|
let (bars, beats) = ((beats / 4) + 1, (beats % 4) + 1);
|
|
let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000);
|
|
let (minutes, seconds) = (seconds / 60, seconds % 60);
|
|
Outset::X(1u16, col! {
|
|
format!("{bars}.{beats}.{pulses:02}").as_str(),
|
|
format!("{minutes}:{seconds:02}:{msecs:03}").as_str(),
|
|
})
|
|
}),
|
|
|
|
}.fill_x().bg(Color::Rgb(40, 50, 30))
|
|
}
|
|
}
|
|
|
|
impl Focusable<Tui> for TransportToolbar<Tui> {
|
|
fn is_focused (&self) -> bool {
|
|
self.focused
|
|
}
|
|
fn set_focused (&mut self, focused: bool) {
|
|
self.focused = focused
|
|
}
|
|
}
|
|
|
|
impl TransportToolbarFocus {
|
|
pub fn wrap <'a, W: Widget<Engine = Tui>> (
|
|
self, parent_focus: bool, focus: Self, widget: &'a W
|
|
) -> impl Widget<Engine = Tui> + 'a {
|
|
Layers::new(move |add|{
|
|
if parent_focus && focus == self {
|
|
add(&CORNERS)?;
|
|
add(&Background(Color::Rgb(60, 70, 50)))?;
|
|
}
|
|
add(widget)
|
|
})
|
|
}
|
|
}
|