mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-02-01 08:36:42 +01:00
unify arranger, sequencer, transport
This commit is contained in:
parent
cd2555ffc7
commit
77519dbb5c
18 changed files with 1618 additions and 1639 deletions
|
|
@ -106,14 +106,22 @@ impl Widget for Arranger<Tui> {
|
||||||
}
|
}
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
let area = (|to|match self.mode {
|
let area = (|to|match self.mode {
|
||||||
ArrangerViewMode::Horizontal =>
|
ArrangerViewMode::Horizontal => draw_h(self, to),
|
||||||
super::arranger_view_h::draw(self, to),
|
ArrangerViewMode::VerticalCompact1 => {
|
||||||
ArrangerViewMode::VerticalCompact1 =>
|
let track_cols = track_clip_name_lengths(self.tracks.as_slice());
|
||||||
super::arranger_view_v::draw_compact_1(self, to),
|
let scene_rows = (0..=self.scenes.len()).map(|i|(96, 96*i)).collect::<Vec<_>>();
|
||||||
ArrangerViewMode::VerticalCompact2 =>
|
draw_v(self, to, track_cols.as_slice(), scene_rows.as_slice())
|
||||||
super::arranger_view_v::draw_compact_2(self, to),
|
},
|
||||||
ArrangerViewMode::VerticalExpanded =>
|
ArrangerViewMode::VerticalCompact2 => {
|
||||||
super::arranger_view_v::draw_expanded(self, to),
|
let track_cols = track_clip_name_lengths(self.tracks.as_slice());
|
||||||
|
let scene_rows = (0..=self.scenes.len()).map(|i|(192, 192*i)).collect::<Vec<_>>();
|
||||||
|
draw_v(self, to, track_cols.as_slice(), scene_rows.as_slice())
|
||||||
|
},
|
||||||
|
ArrangerViewMode::VerticalExpanded => {
|
||||||
|
let track_cols = track_clip_name_lengths(self.tracks.as_slice());
|
||||||
|
let scene_rows = scene_ppqs(self.tracks.as_slice(), self.scenes.as_slice());
|
||||||
|
draw_v(self, to, track_cols.as_slice(), scene_rows.as_slice())
|
||||||
|
},
|
||||||
})(&mut to.alter_area(|[x, y, w, h]|[
|
})(&mut to.alter_area(|[x, y, w, h]|[
|
||||||
x + 1,
|
x + 1,
|
||||||
y + 1,
|
y + 1,
|
||||||
|
|
@ -137,3 +145,879 @@ impl Focusable<Tui> for Arranger<Tui> {
|
||||||
self.focused = focused
|
self.focused = focused
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Copy)]
|
||||||
|
/// Represents the current user selection in the arranger
|
||||||
|
pub enum ArrangerFocus {
|
||||||
|
/** The whole mix is selected */
|
||||||
|
Mix,
|
||||||
|
/// A track is selected.
|
||||||
|
Track(usize),
|
||||||
|
/// A scene is selected.
|
||||||
|
Scene(usize),
|
||||||
|
/// A clip (track × scene) is selected.
|
||||||
|
Clip(usize, usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Focus identification methods
|
||||||
|
impl ArrangerFocus {
|
||||||
|
pub fn is_mix (&self) -> bool {
|
||||||
|
match self { Self::Mix => true, _ => false }
|
||||||
|
}
|
||||||
|
pub fn is_track (&self) -> bool {
|
||||||
|
match self { Self::Track(_) => true, _ => false }
|
||||||
|
}
|
||||||
|
pub fn is_scene (&self) -> bool {
|
||||||
|
match self { Self::Scene(_) => true, _ => false }
|
||||||
|
}
|
||||||
|
pub fn is_clip (&self) -> bool {
|
||||||
|
match self { Self::Clip(_, _) => true, _ => false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Track focus methods
|
||||||
|
impl ArrangerFocus {
|
||||||
|
pub fn track (&self) -> Option<usize> {
|
||||||
|
match self {
|
||||||
|
Self::Clip(t, _) => Some(*t),
|
||||||
|
Self::Track(t) => Some(*t),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn track_next (&mut self, last_track: usize) {
|
||||||
|
*self = match self {
|
||||||
|
Self::Mix => Self::Track(0),
|
||||||
|
Self::Track(t) => Self::Track(last_track.min(*t + 1)),
|
||||||
|
Self::Scene(s) => Self::Clip(0, *s),
|
||||||
|
Self::Clip(t, s) => Self::Clip(last_track.min(*t + 1), *s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn track_prev (&mut self) {
|
||||||
|
*self = match self {
|
||||||
|
Self::Mix => Self::Mix,
|
||||||
|
Self::Scene(s) => Self::Scene(*s),
|
||||||
|
Self::Track(t) => if *t == 0 {
|
||||||
|
Self::Mix
|
||||||
|
} else {
|
||||||
|
Self::Track(*t - 1)
|
||||||
|
},
|
||||||
|
Self::Clip(t, s) => if *t == 0 {
|
||||||
|
Self::Scene(*s)
|
||||||
|
} else {
|
||||||
|
Self::Clip(t.saturating_sub(1), *s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scene focus methods
|
||||||
|
impl ArrangerFocus {
|
||||||
|
pub fn scene (&self) -> Option<usize> {
|
||||||
|
match self {
|
||||||
|
Self::Clip(_, s) => Some(*s),
|
||||||
|
Self::Scene(s) => Some(*s),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn scene_next (&mut self, last_scene: usize) {
|
||||||
|
*self = match self {
|
||||||
|
Self::Mix => Self::Scene(0),
|
||||||
|
Self::Track(t) => Self::Clip(*t, 0),
|
||||||
|
Self::Scene(s) => Self::Scene(last_scene.min(*s + 1)),
|
||||||
|
Self::Clip(t, s) => Self::Clip(*t, last_scene.min(*s + 1)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn scene_prev (&mut self) {
|
||||||
|
*self = match self {
|
||||||
|
Self::Mix => Self::Mix,
|
||||||
|
Self::Track(t) => Self::Track(*t),
|
||||||
|
Self::Scene(s) => if *s == 0 {
|
||||||
|
Self::Mix
|
||||||
|
} else {
|
||||||
|
Self::Scene(*s - 1)
|
||||||
|
},
|
||||||
|
Self::Clip(t, s) => if *s == 0 {
|
||||||
|
Self::Track(*t)
|
||||||
|
} else {
|
||||||
|
Self::Clip(*t, s.saturating_sub(1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Handle<Tui> for Arranger<Tui> {
|
||||||
|
fn handle (&mut self, from: &Tui) -> Perhaps<bool> {
|
||||||
|
if let Some(modal) = self.modal.as_mut() {
|
||||||
|
let result = modal.handle(from)?;
|
||||||
|
if modal.exited() {
|
||||||
|
self.modal = None;
|
||||||
|
}
|
||||||
|
return Ok(result)
|
||||||
|
}
|
||||||
|
match from.event() {
|
||||||
|
// mode_switch: switch the display mode
|
||||||
|
key!(KeyCode::Char('`')) => {
|
||||||
|
self.mode.to_next();
|
||||||
|
Ok(Some(true))
|
||||||
|
},
|
||||||
|
// cursor_up: move cursor up
|
||||||
|
key!(KeyCode::Up) => {
|
||||||
|
match self.mode {
|
||||||
|
ArrangerViewMode::Horizontal => self.track_prev(),
|
||||||
|
_ => self.scene_prev(),
|
||||||
|
};
|
||||||
|
self.show_phrase()?;
|
||||||
|
Ok(Some(true))
|
||||||
|
},
|
||||||
|
// cursor_down
|
||||||
|
key!(KeyCode::Down) => {
|
||||||
|
match self.mode {
|
||||||
|
ArrangerViewMode::Horizontal => self.track_next(),
|
||||||
|
_ => self.scene_next(),
|
||||||
|
};
|
||||||
|
self.show_phrase()?;
|
||||||
|
Ok(Some(true))
|
||||||
|
},
|
||||||
|
// cursor left
|
||||||
|
key!(KeyCode::Left) => {
|
||||||
|
match self.mode {
|
||||||
|
ArrangerViewMode::Horizontal => self.scene_prev(),
|
||||||
|
_ => self.track_prev(),
|
||||||
|
};
|
||||||
|
self.show_phrase()?;
|
||||||
|
Ok(Some(true))
|
||||||
|
},
|
||||||
|
// cursor right
|
||||||
|
key!(KeyCode::Right) => {
|
||||||
|
match self.mode {
|
||||||
|
ArrangerViewMode::Horizontal => self.scene_next(),
|
||||||
|
_ => self.track_next(),
|
||||||
|
};
|
||||||
|
self.show_phrase()?;
|
||||||
|
Ok(Some(true))
|
||||||
|
},
|
||||||
|
// increment: use next clip here
|
||||||
|
key!(KeyCode::Char('.')) => {
|
||||||
|
self.phrase_next();
|
||||||
|
Ok(Some(true))
|
||||||
|
},
|
||||||
|
// decrement: use previous next clip here
|
||||||
|
key!(KeyCode::Char(',')) => {
|
||||||
|
self.phrase_prev();
|
||||||
|
Ok(Some(true))
|
||||||
|
},
|
||||||
|
// decrement: use previous clip here
|
||||||
|
key!(KeyCode::Enter) => {
|
||||||
|
self.activate();
|
||||||
|
Ok(Some(true))
|
||||||
|
},
|
||||||
|
// scene_add: add a new scene
|
||||||
|
key!(Ctrl-KeyCode::Char('a')) => {
|
||||||
|
self.scene_add(None)?;
|
||||||
|
Ok(Some(true))
|
||||||
|
},
|
||||||
|
// track_add: add a new scene
|
||||||
|
key!(Ctrl-KeyCode::Char('t')) => {
|
||||||
|
self.track_add(None)?;
|
||||||
|
Ok(Some(true))
|
||||||
|
},
|
||||||
|
// rename: add a new scene
|
||||||
|
key!(KeyCode::Char('n')) => {
|
||||||
|
self.rename_selected();
|
||||||
|
Ok(Some(true))
|
||||||
|
},
|
||||||
|
// length: add a new scene
|
||||||
|
key!(KeyCode::Char('l')) => {
|
||||||
|
todo!();
|
||||||
|
Ok(Some(true))
|
||||||
|
},
|
||||||
|
// color: set color of item at cursor
|
||||||
|
key!(KeyCode::Char('c')) => {
|
||||||
|
todo!();
|
||||||
|
Ok(Some(true))
|
||||||
|
},
|
||||||
|
_ => Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Appears on first run (i.e. if state dir is missing).
|
||||||
|
pub struct ArrangerRenameModal<E: Engine> {
|
||||||
|
_engine: std::marker::PhantomData<E>,
|
||||||
|
done: bool,
|
||||||
|
target: ArrangerFocus,
|
||||||
|
value: String,
|
||||||
|
result: Arc<RwLock<String>>,
|
||||||
|
cursor: usize
|
||||||
|
}
|
||||||
|
impl<E: Engine> ArrangerRenameModal<E> {
|
||||||
|
pub fn new (target: ArrangerFocus, value: &Arc<RwLock<String>>) -> Self {
|
||||||
|
Self {
|
||||||
|
_engine: Default::default(),
|
||||||
|
done: false,
|
||||||
|
target,
|
||||||
|
value: value.read().unwrap().clone(),
|
||||||
|
cursor: value.read().unwrap().len(),
|
||||||
|
result: value.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Widget for ArrangerRenameModal<Tui> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
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, Nord::BG0);
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Handle<Tui> for ArrangerRenameModal<Tui> {
|
||||||
|
fn handle (&mut self, from: &Tui) -> 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<E: Engine + Send> Exit for ArrangerRenameModal<E> {
|
||||||
|
fn exited (&self) -> bool {
|
||||||
|
self.done
|
||||||
|
}
|
||||||
|
fn exit (&mut self) {
|
||||||
|
self.done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Track management methods
|
||||||
|
impl<E: Engine> Arranger<E> {
|
||||||
|
pub fn track (&self) -> Option<&Sequencer<E>> {
|
||||||
|
self.selected.track().map(|t|self.tracks.get(t)).flatten()
|
||||||
|
}
|
||||||
|
pub fn track_mut (&mut self) -> Option<&mut Sequencer<E>> {
|
||||||
|
self.selected.track().map(|t|self.tracks.get_mut(t)).flatten()
|
||||||
|
}
|
||||||
|
pub fn track_next (&mut self) {
|
||||||
|
self.selected.track_next(self.tracks.len() - 1)
|
||||||
|
}
|
||||||
|
pub fn track_prev (&mut self) {
|
||||||
|
self.selected.track_prev()
|
||||||
|
}
|
||||||
|
pub fn track_add (&mut self, name: Option<&str>) -> Usually<&mut Sequencer<E>> {
|
||||||
|
self.tracks.push(name.map_or_else(
|
||||||
|
|| Sequencer::new(&self.track_default_name()),
|
||||||
|
|name| Sequencer::new(name),
|
||||||
|
));
|
||||||
|
let index = self.tracks.len() - 1;
|
||||||
|
Ok(&mut self.tracks[index])
|
||||||
|
}
|
||||||
|
pub fn track_del (&mut self) {
|
||||||
|
unimplemented!("Arranger::track_del");
|
||||||
|
}
|
||||||
|
pub fn track_default_name (&self) -> String {
|
||||||
|
format!("Track {}", self.tracks.len() + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw arranger with 1 row per scene.
|
||||||
|
pub fn draw_v_compact_1 <'a> (
|
||||||
|
state: &Arranger<Tui>, to: &mut Tui
|
||||||
|
) -> Perhaps<[u16;4]> {
|
||||||
|
let track_cols = track_clip_name_lengths(state.tracks.as_slice());
|
||||||
|
let scene_rows = (0..=state.scenes.len()).map(|i|(96, 96*i)).collect::<Vec<_>>();
|
||||||
|
draw_v(state, to, track_cols.as_slice(), scene_rows.as_slice())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw arranger with 2 rows per scene.
|
||||||
|
pub fn draw_v_compact_2 <'a> (
|
||||||
|
state: &Arranger<Tui>, to: &mut Tui
|
||||||
|
) -> Perhaps<[u16;4]> {
|
||||||
|
let track_cols = track_clip_name_lengths(state.tracks.as_slice());
|
||||||
|
let scene_rows = (0..=state.scenes.len()).map(|i|(192, 192*i)).collect::<Vec<_>>();
|
||||||
|
draw_v(state, to, track_cols.as_slice(), scene_rows.as_slice())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw arranger with number of rows per scene proportional to duration of scene.
|
||||||
|
pub fn draw_v_expanded <'a> (
|
||||||
|
state: &Arranger<Tui>, to: &mut Tui
|
||||||
|
) -> Perhaps<[u16;4]> {
|
||||||
|
let track_cols = track_clip_name_lengths(state.tracks.as_slice());
|
||||||
|
let scene_rows = scene_ppqs(state.tracks.as_slice(), state.scenes.as_slice());
|
||||||
|
draw_v(state, to, track_cols.as_slice(), scene_rows.as_slice())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_v <'a, 'b> (
|
||||||
|
state: &Arranger<Tui>,
|
||||||
|
to: &mut Tui,
|
||||||
|
cols: &'b [(usize, usize)],
|
||||||
|
rows: &'b [(usize, usize)],
|
||||||
|
) -> Perhaps<[u16;4]> {
|
||||||
|
let area = to.area();
|
||||||
|
let area = [area.x(), area.y(), area.w(), 2 + (rows[rows.len() - 1].1 / 96) as u16];
|
||||||
|
let offset = 3 + scene_name_max_len(state.scenes.as_ref()) as u16;
|
||||||
|
let tracks = state.tracks.as_ref();
|
||||||
|
let scenes = state.scenes.as_ref();
|
||||||
|
Layers::new(|add|{
|
||||||
|
//.add_ref(&FillBg(Color::Rgb(30, 33, 36)))//COLOR_BG1))//bg_lo(state.focused, state.entered)))
|
||||||
|
add(&ColumnSeparators(offset, cols))?;
|
||||||
|
add(&RowSeparators(rows))?;
|
||||||
|
add(&CursorFocus(state.selected, offset, cols, rows))?;
|
||||||
|
add(&Split::right(|add|{
|
||||||
|
add(&TracksHeader(offset, cols, tracks))?;
|
||||||
|
add(&SceneRows(offset, cols, rows, tracks, scenes))
|
||||||
|
}))
|
||||||
|
}).render(to.with_rect(area))
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ColumnSeparators<'a>(u16, &'a [(usize, usize)]);
|
||||||
|
|
||||||
|
impl<'a> Widget for ColumnSeparators<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let area = to.area();
|
||||||
|
let Self(offset, cols) = self;
|
||||||
|
let style = Some(Style::default().fg(Nord::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)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some(area))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RowSeparators<'a>(&'a [(usize, usize)]);
|
||||||
|
|
||||||
|
impl<'a> Widget for RowSeparators<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let area = to.area();
|
||||||
|
let Self(rows) = self;
|
||||||
|
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) {
|
||||||
|
let cell = to.buffer().get_mut(x, y);
|
||||||
|
cell.modifier = Modifier::UNDERLINED;
|
||||||
|
cell.underline_color = Nord::SEPARATOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some(area))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CursorFocus<'a>(
|
||||||
|
ArrangerFocus, u16, &'a [(usize, usize)], &'a [(usize, usize)]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl<'a> Widget for CursorFocus<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let area = to.area();
|
||||||
|
let Self(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 => {
|
||||||
|
to.fill_bg(area, COLOR_BG0);
|
||||||
|
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_BG5);
|
||||||
|
to.fill_fg([x + width, y, 1, height], COLOR_BG5);
|
||||||
|
}
|
||||||
|
if let Some([_, y, _, height]) = scene_area {
|
||||||
|
to.fill_ul([area.x(), y - 1, area.w(), 1], COLOR_BG5);
|
||||||
|
to.fill_ul([area.x(), y + height - 1, area.w(), 1], COLOR_BG5);
|
||||||
|
}
|
||||||
|
if let Some(clip_area) = clip_area {
|
||||||
|
to.fill_bg(clip_area, COLOR_BG0);
|
||||||
|
} else if let Some(track_area) = track_area {
|
||||||
|
to.fill_bg(track_area, COLOR_BG0);
|
||||||
|
} else if let Some(scene_area) = scene_area {
|
||||||
|
to.fill_bg(scene_area, COLOR_BG0);
|
||||||
|
}
|
||||||
|
Ok(Some(area))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TracksHeader<'a>(u16, &'a[(usize, usize)], &'a [Sequencer<Tui>]);
|
||||||
|
|
||||||
|
impl<'a> Widget for TracksHeader<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let area = to.area();
|
||||||
|
let Self(offset, track_cols, tracks) = *self;
|
||||||
|
let [x, y, width, _] = area;
|
||||||
|
for (track, (w, x)) in tracks.iter().zip(track_cols) {
|
||||||
|
let x = *x as u16;
|
||||||
|
if x > width {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let name = track.name.read().unwrap();
|
||||||
|
to.fill_bg([offset + x, y, *w as u16, 2], COLOR_BG1);
|
||||||
|
to.blit(&*name, offset + x + 1, y, Some(Style::default().white()))?;
|
||||||
|
}
|
||||||
|
Ok(Some([x, y, width, 2]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SceneRows<'a>(u16, &'a[(usize, usize)], &'a[(usize, usize)], &'a[Sequencer<Tui>], &'a[Scene]);
|
||||||
|
|
||||||
|
impl<'a> Widget for SceneRows<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let area = to.area();
|
||||||
|
let Self(offset, track_cols, scene_rows, tracks, scenes) = *self;
|
||||||
|
let black = Some(Style::default().fg(Nord::SEPARATOR));
|
||||||
|
let [_, mut y, _, _height] = area;
|
||||||
|
for (_, x) in track_cols.iter() {
|
||||||
|
let x = *x as u16;
|
||||||
|
if x > 0 {
|
||||||
|
for y in area.y()-2..y-2 {
|
||||||
|
to.blit(&"▎", x - 1, y, black)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (scene, (pulses, _)) in scenes.iter().zip(scene_rows) {
|
||||||
|
//if y > height {
|
||||||
|
//break
|
||||||
|
//}
|
||||||
|
let h = 1.max((pulses / 96) as u16);
|
||||||
|
SceneRow(tracks, scene, track_cols, offset)
|
||||||
|
.render(to.with_area(area.x(), y, area.w(), h))?;
|
||||||
|
y = y + h
|
||||||
|
}
|
||||||
|
Ok(Some(area))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SceneRow<'a>(&'a[Sequencer<Tui>], &'a Scene, &'a[(usize, usize)], u16);
|
||||||
|
|
||||||
|
impl<'a> Widget for SceneRow<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let area = to.area();
|
||||||
|
let Self(tracks, scene, track_cols, offset) = self;
|
||||||
|
let [x, y, width, _] = area;
|
||||||
|
let playing = scene.is_playing(tracks);
|
||||||
|
to.blit(&if playing { "▶" } else { " " }, x, y, None)?;
|
||||||
|
to.blit(&*scene.name.read().unwrap(), x + 1, y, Some(Style::default().white()))?;
|
||||||
|
to.fill_bg([x, y, offset.saturating_sub(1), area.h()], COLOR_BG1);
|
||||||
|
for (track, (w, x)) in track_cols.iter().enumerate() {
|
||||||
|
let x = *x as u16 + offset;
|
||||||
|
if x > width {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if let (Some(track), Some(Some(clip))) = (
|
||||||
|
tracks.get(track), scene.clips.get(track)
|
||||||
|
) {
|
||||||
|
SceneClip(track, *clip).render(to.with_area(x, y, *w as u16, area.h()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some(area))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SceneClip<'a>(&'a Sequencer<Tui>, usize);
|
||||||
|
|
||||||
|
impl<'a> Widget for SceneClip<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let area = to.area();
|
||||||
|
let Self(track, clip) = self;
|
||||||
|
let style = Some(Style::default().white());
|
||||||
|
if let Some(phrase) = track.phrases.get(*clip) {
|
||||||
|
let phrase = phrase.read().unwrap();
|
||||||
|
let name = phrase.name.read().unwrap();
|
||||||
|
to.blit(&format!("{clip:02} {name}"), area.x() + 1, area.y(), style)?;
|
||||||
|
to.fill_bg(area, if track.sequence == Some(*clip) {
|
||||||
|
Nord::PLAYING
|
||||||
|
} else {
|
||||||
|
COLOR_BG1
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
to.fill_bg(area, COLOR_BG0)
|
||||||
|
}
|
||||||
|
Ok(Some(area))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_h (state: &Arranger<Tui>, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let area = to.area();
|
||||||
|
let area = [area.x(), area.y(), area.w(), area.h().min((2 + state.tracks.len() * 2) as u16)];
|
||||||
|
let tracks = state.tracks.as_slice();
|
||||||
|
Layers::new(|add|{
|
||||||
|
add(&state.focused.then_some(FillBg(COLOR_BG0)))?;
|
||||||
|
add(&Split::right(|add|{
|
||||||
|
add(&TrackNameColumn(tracks, state.selected))?;
|
||||||
|
add(&TrackMonitorColumn(tracks))?;
|
||||||
|
add(&TrackRecordColumn(tracks))?;
|
||||||
|
add(&TrackOverdubColumn(tracks))?;
|
||||||
|
add(&TrackEraseColumn(tracks))?;
|
||||||
|
add(&TrackGainColumn(tracks))?;
|
||||||
|
add(&TrackScenesColumn(tracks, state.scenes.as_slice(), state.selected))?;
|
||||||
|
Ok(())
|
||||||
|
}))
|
||||||
|
}).render(to.with_rect(area))
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TrackNameColumn<'a>(&'a [Sequencer<Tui>], ArrangerFocus);
|
||||||
|
|
||||||
|
impl<'a> Widget for TrackNameColumn<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TrackMonitorColumn<'a>(&'a [Sequencer<Tui>]);
|
||||||
|
|
||||||
|
impl<'a> Widget for TrackMonitorColumn<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TrackRecordColumn<'a>(&'a [Sequencer<Tui>]);
|
||||||
|
|
||||||
|
impl<'a> Widget for TrackRecordColumn<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TrackOverdubColumn<'a>(&'a [Sequencer<Tui>]);
|
||||||
|
|
||||||
|
impl<'a> Widget for TrackOverdubColumn<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TrackEraseColumn<'a>(&'a [Sequencer<Tui>]);
|
||||||
|
|
||||||
|
impl<'a> Widget for TrackEraseColumn<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TrackGainColumn<'a>(&'a [Sequencer<Tui>]);
|
||||||
|
|
||||||
|
impl<'a> Widget for TrackGainColumn<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TrackScenesColumn<'a>(&'a [Sequencer<Tui>], &'a [Scene], ArrangerFocus);
|
||||||
|
|
||||||
|
impl<'a> Widget for TrackScenesColumn<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let Self(tracks, scenes, selected) = self;
|
||||||
|
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]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Copy)]
|
|
||||||
/// Represents the current user selection in the arranger
|
|
||||||
pub enum ArrangerFocus {
|
|
||||||
/** The whole mix is selected */
|
|
||||||
Mix,
|
|
||||||
/// A track is selected.
|
|
||||||
Track(usize),
|
|
||||||
/// A scene is selected.
|
|
||||||
Scene(usize),
|
|
||||||
/// A clip (track × scene) is selected.
|
|
||||||
Clip(usize, usize),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Focus identification methods
|
|
||||||
impl ArrangerFocus {
|
|
||||||
pub fn is_mix (&self) -> bool {
|
|
||||||
match self { Self::Mix => true, _ => false }
|
|
||||||
}
|
|
||||||
pub fn is_track (&self) -> bool {
|
|
||||||
match self { Self::Track(_) => true, _ => false }
|
|
||||||
}
|
|
||||||
pub fn is_scene (&self) -> bool {
|
|
||||||
match self { Self::Scene(_) => true, _ => false }
|
|
||||||
}
|
|
||||||
pub fn is_clip (&self) -> bool {
|
|
||||||
match self { Self::Clip(_, _) => true, _ => false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Track focus methods
|
|
||||||
impl ArrangerFocus {
|
|
||||||
pub fn track (&self) -> Option<usize> {
|
|
||||||
match self {
|
|
||||||
Self::Clip(t, _) => Some(*t),
|
|
||||||
Self::Track(t) => Some(*t),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn track_next (&mut self, last_track: usize) {
|
|
||||||
*self = match self {
|
|
||||||
Self::Mix => Self::Track(0),
|
|
||||||
Self::Track(t) => Self::Track(last_track.min(*t + 1)),
|
|
||||||
Self::Scene(s) => Self::Clip(0, *s),
|
|
||||||
Self::Clip(t, s) => Self::Clip(last_track.min(*t + 1), *s),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn track_prev (&mut self) {
|
|
||||||
*self = match self {
|
|
||||||
Self::Mix => Self::Mix,
|
|
||||||
Self::Scene(s) => Self::Scene(*s),
|
|
||||||
Self::Track(t) => if *t == 0 {
|
|
||||||
Self::Mix
|
|
||||||
} else {
|
|
||||||
Self::Track(*t - 1)
|
|
||||||
},
|
|
||||||
Self::Clip(t, s) => if *t == 0 {
|
|
||||||
Self::Scene(*s)
|
|
||||||
} else {
|
|
||||||
Self::Clip(t.saturating_sub(1), *s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scene focus methods
|
|
||||||
impl ArrangerFocus {
|
|
||||||
pub fn scene (&self) -> Option<usize> {
|
|
||||||
match self {
|
|
||||||
Self::Clip(_, s) => Some(*s),
|
|
||||||
Self::Scene(s) => Some(*s),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn scene_next (&mut self, last_scene: usize) {
|
|
||||||
*self = match self {
|
|
||||||
Self::Mix => Self::Scene(0),
|
|
||||||
Self::Track(t) => Self::Clip(*t, 0),
|
|
||||||
Self::Scene(s) => Self::Scene(last_scene.min(*s + 1)),
|
|
||||||
Self::Clip(t, s) => Self::Clip(*t, last_scene.min(*s + 1)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn scene_prev (&mut self) {
|
|
||||||
*self = match self {
|
|
||||||
Self::Mix => Self::Mix,
|
|
||||||
Self::Track(t) => Self::Track(*t),
|
|
||||||
Self::Scene(s) => if *s == 0 {
|
|
||||||
Self::Mix
|
|
||||||
} else {
|
|
||||||
Self::Scene(*s - 1)
|
|
||||||
},
|
|
||||||
Self::Clip(t, s) => if *s == 0 {
|
|
||||||
Self::Track(*t)
|
|
||||||
} else {
|
|
||||||
Self::Clip(*t, s.saturating_sub(1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
impl Handle<Tui> for Arranger<Tui> {
|
|
||||||
fn handle (&mut self, from: &Tui) -> Perhaps<bool> {
|
|
||||||
if let Some(modal) = self.modal.as_mut() {
|
|
||||||
let result = modal.handle(from)?;
|
|
||||||
if modal.exited() {
|
|
||||||
self.modal = None;
|
|
||||||
}
|
|
||||||
return Ok(result)
|
|
||||||
}
|
|
||||||
match from.event() {
|
|
||||||
// mode_switch: switch the display mode
|
|
||||||
key!(KeyCode::Char('`')) => {
|
|
||||||
self.mode.to_next();
|
|
||||||
Ok(Some(true))
|
|
||||||
},
|
|
||||||
// cursor_up: move cursor up
|
|
||||||
key!(KeyCode::Up) => {
|
|
||||||
match self.mode {
|
|
||||||
ArrangerViewMode::Horizontal => self.track_prev(),
|
|
||||||
_ => self.scene_prev(),
|
|
||||||
};
|
|
||||||
self.show_phrase()?;
|
|
||||||
Ok(Some(true))
|
|
||||||
},
|
|
||||||
// cursor_down
|
|
||||||
key!(KeyCode::Down) => {
|
|
||||||
match self.mode {
|
|
||||||
ArrangerViewMode::Horizontal => self.track_next(),
|
|
||||||
_ => self.scene_next(),
|
|
||||||
};
|
|
||||||
self.show_phrase()?;
|
|
||||||
Ok(Some(true))
|
|
||||||
},
|
|
||||||
// cursor left
|
|
||||||
key!(KeyCode::Left) => {
|
|
||||||
match self.mode {
|
|
||||||
ArrangerViewMode::Horizontal => self.scene_prev(),
|
|
||||||
_ => self.track_prev(),
|
|
||||||
};
|
|
||||||
self.show_phrase()?;
|
|
||||||
Ok(Some(true))
|
|
||||||
},
|
|
||||||
// cursor right
|
|
||||||
key!(KeyCode::Right) => {
|
|
||||||
match self.mode {
|
|
||||||
ArrangerViewMode::Horizontal => self.scene_next(),
|
|
||||||
_ => self.track_next(),
|
|
||||||
};
|
|
||||||
self.show_phrase()?;
|
|
||||||
Ok(Some(true))
|
|
||||||
},
|
|
||||||
// increment: use next clip here
|
|
||||||
key!(KeyCode::Char('.')) => {
|
|
||||||
self.phrase_next();
|
|
||||||
Ok(Some(true))
|
|
||||||
},
|
|
||||||
// decrement: use previous next clip here
|
|
||||||
key!(KeyCode::Char(',')) => {
|
|
||||||
self.phrase_prev();
|
|
||||||
Ok(Some(true))
|
|
||||||
},
|
|
||||||
// decrement: use previous clip here
|
|
||||||
key!(KeyCode::Enter) => {
|
|
||||||
self.activate();
|
|
||||||
Ok(Some(true))
|
|
||||||
},
|
|
||||||
// scene_add: add a new scene
|
|
||||||
key!(Ctrl-KeyCode::Char('a')) => {
|
|
||||||
self.scene_add(None)?;
|
|
||||||
Ok(Some(true))
|
|
||||||
},
|
|
||||||
// track_add: add a new scene
|
|
||||||
key!(Ctrl-KeyCode::Char('t')) => {
|
|
||||||
self.track_add(None)?;
|
|
||||||
Ok(Some(true))
|
|
||||||
},
|
|
||||||
// rename: add a new scene
|
|
||||||
key!(KeyCode::Char('n')) => {
|
|
||||||
self.rename_selected();
|
|
||||||
Ok(Some(true))
|
|
||||||
},
|
|
||||||
// length: add a new scene
|
|
||||||
key!(KeyCode::Char('l')) => {
|
|
||||||
todo!();
|
|
||||||
Ok(Some(true))
|
|
||||||
},
|
|
||||||
// color: set color of item at cursor
|
|
||||||
key!(KeyCode::Char('c')) => {
|
|
||||||
todo!();
|
|
||||||
Ok(Some(true))
|
|
||||||
},
|
|
||||||
_ => Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
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(),
|
|
||||||
}
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Appears on first run (i.e. if state dir is missing).
|
|
||||||
pub struct ArrangerRenameModal<E: Engine> {
|
|
||||||
_engine: std::marker::PhantomData<E>,
|
|
||||||
done: bool,
|
|
||||||
target: ArrangerFocus,
|
|
||||||
value: String,
|
|
||||||
result: Arc<RwLock<String>>,
|
|
||||||
cursor: usize
|
|
||||||
}
|
|
||||||
impl<E: Engine> ArrangerRenameModal<E> {
|
|
||||||
pub fn new (target: ArrangerFocus, value: &Arc<RwLock<String>>) -> Self {
|
|
||||||
Self {
|
|
||||||
_engine: Default::default(),
|
|
||||||
done: false,
|
|
||||||
target,
|
|
||||||
value: value.read().unwrap().clone(),
|
|
||||||
cursor: value.read().unwrap().len(),
|
|
||||||
result: value.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Widget for ArrangerRenameModal<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
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, Nord::BG0);
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Handle<Tui> for ArrangerRenameModal<Tui> {
|
|
||||||
fn handle (&mut self, from: &Tui) -> 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<E: Engine + Send> Exit for ArrangerRenameModal<E> {
|
|
||||||
fn exited (&self) -> bool {
|
|
||||||
self.done
|
|
||||||
}
|
|
||||||
fn exit (&mut self) {
|
|
||||||
self.done = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
use super::Arranger;
|
|
||||||
|
|
||||||
/// Track management methods
|
|
||||||
impl<E: Engine> Arranger<E> {
|
|
||||||
pub fn track (&self) -> Option<&Sequencer<E>> {
|
|
||||||
self.selected.track().map(|t|self.tracks.get(t)).flatten()
|
|
||||||
}
|
|
||||||
pub fn track_mut (&mut self) -> Option<&mut Sequencer<E>> {
|
|
||||||
self.selected.track().map(|t|self.tracks.get_mut(t)).flatten()
|
|
||||||
}
|
|
||||||
pub fn track_next (&mut self) {
|
|
||||||
self.selected.track_next(self.tracks.len() - 1)
|
|
||||||
}
|
|
||||||
pub fn track_prev (&mut self) {
|
|
||||||
self.selected.track_prev()
|
|
||||||
}
|
|
||||||
pub fn track_add (&mut self, name: Option<&str>) -> Usually<&mut Sequencer<E>> {
|
|
||||||
self.tracks.push(name.map_or_else(
|
|
||||||
|| Sequencer::new(&self.track_default_name()),
|
|
||||||
|name| Sequencer::new(name),
|
|
||||||
));
|
|
||||||
let index = self.tracks.len() - 1;
|
|
||||||
Ok(&mut self.tracks[index])
|
|
||||||
}
|
|
||||||
pub fn track_del (&mut self) {
|
|
||||||
unimplemented!("Arranger::track_del");
|
|
||||||
}
|
|
||||||
pub fn track_default_name (&self) -> String {
|
|
||||||
format!("Track {}", self.tracks.len() + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
@ -1,263 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
pub fn draw (state: &Arranger<Tui>, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let area = to.area();
|
|
||||||
let area = [area.x(), area.y(), area.w(), area.h().min((2 + state.tracks.len() * 2) as u16)];
|
|
||||||
let tracks = state.tracks.as_slice();
|
|
||||||
Layers::new(|add|{
|
|
||||||
add(&state.focused.then_some(FillBg(COLOR_BG0)))?;
|
|
||||||
add(&Split::right(|add|{
|
|
||||||
add(&TrackNameColumn(tracks, state.selected))?;
|
|
||||||
add(&TrackMonitorColumn(tracks))?;
|
|
||||||
add(&TrackRecordColumn(tracks))?;
|
|
||||||
add(&TrackOverdubColumn(tracks))?;
|
|
||||||
add(&TrackEraseColumn(tracks))?;
|
|
||||||
add(&TrackGainColumn(tracks))?;
|
|
||||||
add(&TrackScenesColumn(tracks, state.scenes.as_slice(), state.selected))?;
|
|
||||||
Ok(())
|
|
||||||
}))
|
|
||||||
}).render(to.with_rect(area))
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TrackNameColumn<'a>(&'a [Sequencer<Tui>], ArrangerFocus);
|
|
||||||
|
|
||||||
impl<'a> Widget for TrackNameColumn<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TrackMonitorColumn<'a>(&'a [Sequencer<Tui>]);
|
|
||||||
|
|
||||||
impl<'a> Widget for TrackMonitorColumn<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TrackRecordColumn<'a>(&'a [Sequencer<Tui>]);
|
|
||||||
|
|
||||||
impl<'a> Widget for TrackRecordColumn<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TrackOverdubColumn<'a>(&'a [Sequencer<Tui>]);
|
|
||||||
|
|
||||||
impl<'a> Widget for TrackOverdubColumn<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TrackEraseColumn<'a>(&'a [Sequencer<Tui>]);
|
|
||||||
|
|
||||||
impl<'a> Widget for TrackEraseColumn<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TrackGainColumn<'a>(&'a [Sequencer<Tui>]);
|
|
||||||
|
|
||||||
impl<'a> Widget for TrackGainColumn<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TrackScenesColumn<'a>(&'a [Sequencer<Tui>], &'a [Scene], ArrangerFocus);
|
|
||||||
|
|
||||||
impl<'a> Widget for TrackScenesColumn<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let Self(tracks, scenes, selected) = self;
|
|
||||||
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]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,262 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
/// Draw arranger with 1 row per scene.
|
|
||||||
pub fn draw_compact_1 <'a> (
|
|
||||||
state: &Arranger<Tui>, to: &mut Tui
|
|
||||||
) -> Perhaps<[u16;4]> {
|
|
||||||
let track_cols = track_clip_name_lengths(state.tracks.as_slice());
|
|
||||||
let scene_rows = (0..=state.scenes.len()).map(|i|(96, 96*i)).collect::<Vec<_>>();
|
|
||||||
draw(state, to, track_cols.as_slice(), scene_rows.as_slice())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw arranger with 2 rows per scene.
|
|
||||||
pub fn draw_compact_2 <'a> (
|
|
||||||
state: &Arranger<Tui>, to: &mut Tui
|
|
||||||
) -> Perhaps<[u16;4]> {
|
|
||||||
let track_cols = track_clip_name_lengths(state.tracks.as_slice());
|
|
||||||
let scene_rows = (0..=state.scenes.len()).map(|i|(192, 192*i)).collect::<Vec<_>>();
|
|
||||||
draw(state, to, track_cols.as_slice(), scene_rows.as_slice())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw arranger with number of rows per scene proportional to duration of scene.
|
|
||||||
pub fn draw_expanded <'a> (
|
|
||||||
state: &Arranger<Tui>, to: &mut Tui
|
|
||||||
) -> Perhaps<[u16;4]> {
|
|
||||||
let track_cols = track_clip_name_lengths(state.tracks.as_slice());
|
|
||||||
let scene_rows = scene_ppqs(state.tracks.as_slice(), state.scenes.as_slice());
|
|
||||||
draw(state, to, track_cols.as_slice(), scene_rows.as_slice())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw <'a, 'b> (
|
|
||||||
state: &Arranger<Tui>,
|
|
||||||
to: &mut Tui,
|
|
||||||
cols: &'b [(usize, usize)],
|
|
||||||
rows: &'b [(usize, usize)],
|
|
||||||
) -> Perhaps<[u16;4]> {
|
|
||||||
let area = to.area();
|
|
||||||
let area = [area.x(), area.y(), area.w(), 2 + (rows[rows.len() - 1].1 / 96) as u16];
|
|
||||||
let offset = 3 + scene_name_max_len(state.scenes.as_ref()) as u16;
|
|
||||||
let tracks = state.tracks.as_ref();
|
|
||||||
let scenes = state.scenes.as_ref();
|
|
||||||
Layers::new(|add|{
|
|
||||||
//.add_ref(&FillBg(Color::Rgb(30, 33, 36)))//COLOR_BG1))//bg_lo(state.focused, state.entered)))
|
|
||||||
add(&ColumnSeparators(offset, cols))?;
|
|
||||||
add(&RowSeparators(rows))?;
|
|
||||||
add(&CursorFocus(state.selected, offset, cols, rows))?;
|
|
||||||
add(&Split::right(|add|{
|
|
||||||
add(&TracksHeader(offset, cols, tracks))?;
|
|
||||||
add(&SceneRows(offset, cols, rows, tracks, scenes))
|
|
||||||
}))
|
|
||||||
}).render(to.with_rect(area))
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ColumnSeparators<'a>(u16, &'a [(usize, usize)]);
|
|
||||||
|
|
||||||
impl<'a> Widget for ColumnSeparators<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let area = to.area();
|
|
||||||
let Self(offset, cols) = self;
|
|
||||||
let style = Some(Style::default().fg(Nord::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)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Some(area))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RowSeparators<'a>(&'a [(usize, usize)]);
|
|
||||||
|
|
||||||
impl<'a> Widget for RowSeparators<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let area = to.area();
|
|
||||||
let Self(rows) = self;
|
|
||||||
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) {
|
|
||||||
let cell = to.buffer().get_mut(x, y);
|
|
||||||
cell.modifier = Modifier::UNDERLINED;
|
|
||||||
cell.underline_color = Nord::SEPARATOR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Some(area))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CursorFocus<'a>(
|
|
||||||
ArrangerFocus, u16, &'a [(usize, usize)], &'a [(usize, usize)]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl<'a> Widget for CursorFocus<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let area = to.area();
|
|
||||||
let Self(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 => {
|
|
||||||
to.fill_bg(area, COLOR_BG0);
|
|
||||||
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_BG5);
|
|
||||||
to.fill_fg([x + width, y, 1, height], COLOR_BG5);
|
|
||||||
}
|
|
||||||
if let Some([_, y, _, height]) = scene_area {
|
|
||||||
to.fill_ul([area.x(), y - 1, area.w(), 1], COLOR_BG5);
|
|
||||||
to.fill_ul([area.x(), y + height - 1, area.w(), 1], COLOR_BG5);
|
|
||||||
}
|
|
||||||
if let Some(clip_area) = clip_area {
|
|
||||||
to.fill_bg(clip_area, COLOR_BG0);
|
|
||||||
} else if let Some(track_area) = track_area {
|
|
||||||
to.fill_bg(track_area, COLOR_BG0);
|
|
||||||
} else if let Some(scene_area) = scene_area {
|
|
||||||
to.fill_bg(scene_area, COLOR_BG0);
|
|
||||||
}
|
|
||||||
Ok(Some(area))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TracksHeader<'a>(u16, &'a[(usize, usize)], &'a [Sequencer<Tui>]);
|
|
||||||
|
|
||||||
impl<'a> Widget for TracksHeader<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let area = to.area();
|
|
||||||
let Self(offset, track_cols, tracks) = *self;
|
|
||||||
let [x, y, width, _] = area;
|
|
||||||
for (track, (w, x)) in tracks.iter().zip(track_cols) {
|
|
||||||
let x = *x as u16;
|
|
||||||
if x > width {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
let name = track.name.read().unwrap();
|
|
||||||
to.fill_bg([offset + x, y, *w as u16, 2], COLOR_BG1);
|
|
||||||
to.blit(&*name, offset + x + 1, y, Some(Style::default().white()))?;
|
|
||||||
}
|
|
||||||
Ok(Some([x, y, width, 2]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SceneRows<'a>(u16, &'a[(usize, usize)], &'a[(usize, usize)], &'a[Sequencer<Tui>], &'a[Scene]);
|
|
||||||
|
|
||||||
impl<'a> Widget for SceneRows<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let area = to.area();
|
|
||||||
let Self(offset, track_cols, scene_rows, tracks, scenes) = *self;
|
|
||||||
let black = Some(Style::default().fg(Nord::SEPARATOR));
|
|
||||||
let [_, mut y, _, _height] = area;
|
|
||||||
for (_, x) in track_cols.iter() {
|
|
||||||
let x = *x as u16;
|
|
||||||
if x > 0 {
|
|
||||||
for y in area.y()-2..y-2 {
|
|
||||||
to.blit(&"▎", x - 1, y, black)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (scene, (pulses, _)) in scenes.iter().zip(scene_rows) {
|
|
||||||
//if y > height {
|
|
||||||
//break
|
|
||||||
//}
|
|
||||||
let h = 1.max((pulses / 96) as u16);
|
|
||||||
SceneRow(tracks, scene, track_cols, offset)
|
|
||||||
.render(to.with_area(area.x(), y, area.w(), h))?;
|
|
||||||
y = y + h
|
|
||||||
}
|
|
||||||
Ok(Some(area))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SceneRow<'a>(&'a[Sequencer<Tui>], &'a Scene, &'a[(usize, usize)], u16);
|
|
||||||
|
|
||||||
impl<'a> Widget for SceneRow<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let area = to.area();
|
|
||||||
let Self(tracks, scene, track_cols, offset) = self;
|
|
||||||
let [x, y, width, _] = area;
|
|
||||||
let playing = scene.is_playing(tracks);
|
|
||||||
to.blit(&if playing { "▶" } else { " " }, x, y, None)?;
|
|
||||||
to.blit(&*scene.name.read().unwrap(), x + 1, y, Some(Style::default().white()))?;
|
|
||||||
to.fill_bg([x, y, offset.saturating_sub(1), area.h()], COLOR_BG1);
|
|
||||||
for (track, (w, x)) in track_cols.iter().enumerate() {
|
|
||||||
let x = *x as u16 + offset;
|
|
||||||
if x > width {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if let (Some(track), Some(Some(clip))) = (
|
|
||||||
tracks.get(track), scene.clips.get(track)
|
|
||||||
) {
|
|
||||||
SceneClip(track, *clip).render(to.with_area(x, y, *w as u16, area.h()))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Some(area))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SceneClip<'a>(&'a Sequencer<Tui>, usize);
|
|
||||||
|
|
||||||
impl<'a> Widget for SceneClip<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let area = to.area();
|
|
||||||
let Self(track, clip) = self;
|
|
||||||
let style = Some(Style::default().white());
|
|
||||||
if let Some(phrase) = track.phrases.get(*clip) {
|
|
||||||
let phrase = phrase.read().unwrap();
|
|
||||||
let name = phrase.name.read().unwrap();
|
|
||||||
to.blit(&format!("{clip:02} {name}"), area.x() + 1, area.y(), style)?;
|
|
||||||
to.fill_bg(area, if track.sequence == Some(*clip) {
|
|
||||||
Nord::PLAYING
|
|
||||||
} else {
|
|
||||||
COLOR_BG1
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
to.fill_bg(area, COLOR_BG0)
|
|
||||||
}
|
|
||||||
Ok(Some(area))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -8,25 +8,9 @@ pub(crate) use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
submod! {
|
submod! {
|
||||||
arranger
|
arranger
|
||||||
arranger_focus
|
|
||||||
arranger_handle
|
|
||||||
arranger_track
|
|
||||||
arranger_rename
|
|
||||||
midi
|
midi
|
||||||
phrase
|
phrase
|
||||||
scene
|
scene
|
||||||
sequencer
|
sequencer
|
||||||
sequencer_cli
|
|
||||||
sequencer_handle
|
|
||||||
sequencer_view
|
|
||||||
transport
|
transport
|
||||||
transport_handle
|
|
||||||
transport_view
|
|
||||||
}
|
|
||||||
|
|
||||||
pubmod! {
|
|
||||||
arranger_view_h
|
|
||||||
arranger_view_v
|
|
||||||
sequencer_view_h
|
|
||||||
sequencer_view_v
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -197,3 +197,490 @@ impl<E: Engine> Sequencer<E> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Widget for Sequencer<Tui> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
self.horizontal_draw(to)?;
|
||||||
|
if self.focused && self.entered {
|
||||||
|
Corners(Style::default().green().not_dim()).draw(to)?;
|
||||||
|
}
|
||||||
|
Ok(Some(to.area()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Engine> Sequencer<E> {
|
||||||
|
/// 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();
|
||||||
|
fill_seq_bg(&mut buffer, phrase.length, self.ppq)?;
|
||||||
|
fill_seq_fg(&mut buffer, &phrase)?;
|
||||||
|
self.buffer = buffer;
|
||||||
|
} else {
|
||||||
|
self.buffer = Default::default();
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nth_octave (index: u16) -> &'static str {
|
||||||
|
match index {
|
||||||
|
0 => "-1",
|
||||||
|
1 => "0",
|
||||||
|
2 => "1",
|
||||||
|
3 => "2",
|
||||||
|
4 => "3",
|
||||||
|
5 => "4",
|
||||||
|
6 => "5",
|
||||||
|
7 => "6",
|
||||||
|
8 => "7",
|
||||||
|
9 => "8",
|
||||||
|
10 => "9",
|
||||||
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_colors (index: u16) -> (Color, Color) {
|
||||||
|
match index % 6 {
|
||||||
|
0 => (Color::White, Color::Black),
|
||||||
|
1 => (Color::White, Color::Black),
|
||||||
|
2 => (Color::White, Color::White),
|
||||||
|
3 => (Color::Black, Color::White),
|
||||||
|
4 => (Color::Black, Color::White),
|
||||||
|
5 => (Color::Black, Color::White),
|
||||||
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(char_seq_bg(ppq, x as u16));
|
||||||
|
cell.set_fg(Color::Gray);
|
||||||
|
cell.modifier = Modifier::DIM;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn char_seq_bg (ppq: usize, x: u16) -> char {
|
||||||
|
if ppq == 0 {
|
||||||
|
'·'
|
||||||
|
} else if x % (4 * ppq as u16) == 0 {
|
||||||
|
'│'
|
||||||
|
} else if x % ppq as u16 == 0 {
|
||||||
|
'╎'
|
||||||
|
} else {
|
||||||
|
'·'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
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));
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sequencer<Tui> {
|
||||||
|
|
||||||
|
const H_KEYS_OFFSET: usize = 5;
|
||||||
|
|
||||||
|
pub(crate) fn horizontal_draw <'a> (&self, to: &mut Tui) -> Usually<()> {
|
||||||
|
let area = to.area();
|
||||||
|
Split::down(|add|{
|
||||||
|
add(&SequenceName(&self))?;
|
||||||
|
add(&SequenceRange)?;
|
||||||
|
add(&SequenceLoopRange)?;
|
||||||
|
add(&SequenceNoteRange)?;
|
||||||
|
Ok(())
|
||||||
|
}).render(to.with_area(area.x(), area.y(), 10, area.h()))?;
|
||||||
|
let area = [area.x() + 10, area.y(), area.w().saturating_sub(10), area.h().min(66)];
|
||||||
|
Lozenge(Style::default().fg(Nord::BG2)).draw(to.with_rect(area))?;
|
||||||
|
let area = [area.x() + 1, area.y(), area.w().saturating_sub(1), area.h()];
|
||||||
|
Layers::new(|add|{
|
||||||
|
add(&SequenceKeys(&self))?;
|
||||||
|
add(&self.phrase.as_ref().map(|phrase|SequenceTimer(&self, phrase.clone())))?;
|
||||||
|
add(&SequenceNotes(&self))?;
|
||||||
|
add(&SequenceCursor(&self))?;
|
||||||
|
add(&SequenceZoom(&self))
|
||||||
|
}).render(to.with_rect(area))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const STYLE_LABEL: Option<Style> = Some(Style {
|
||||||
|
fg: Some(Color::Reset),
|
||||||
|
bg: None,
|
||||||
|
underline_color: None,
|
||||||
|
add_modifier: Modifier::empty(),
|
||||||
|
sub_modifier: Modifier::BOLD,
|
||||||
|
});
|
||||||
|
|
||||||
|
const STYLE_VALUE: Option<Style> = Some(Style {
|
||||||
|
fg: Some(Color::White),
|
||||||
|
bg: None,
|
||||||
|
underline_color: None,
|
||||||
|
add_modifier: Modifier::BOLD,
|
||||||
|
sub_modifier: Modifier::DIM,
|
||||||
|
});
|
||||||
|
|
||||||
|
struct SequenceName<'a>(&'a Sequencer<Tui>);
|
||||||
|
|
||||||
|
impl<'a> Widget for SequenceName<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let [x, y, ..] = to.area();
|
||||||
|
let frame = [x, y, 10, 4];
|
||||||
|
Lozenge(Style::default().fg(Nord::BG2)).draw(to.with_rect(frame))?;
|
||||||
|
to.blit(&"Name:", x + 1, y + 1, STYLE_LABEL)?;
|
||||||
|
to.blit(&*self.0.name.read().unwrap(), x + 1, y + 2, STYLE_VALUE)?;
|
||||||
|
Ok(Some(frame))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SequenceRange;
|
||||||
|
|
||||||
|
impl Widget for SequenceRange {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let [x, y, ..] = to.area();
|
||||||
|
let frame = [x, y, 10, 6];
|
||||||
|
Lozenge(Style::default().fg(Nord::BG2)).draw(to.with_rect(frame))?;
|
||||||
|
to.blit(&"Start: ", x + 1, y + 1, STYLE_LABEL)?;
|
||||||
|
to.blit(&" 1.1.1", x + 1, y + 2, STYLE_VALUE)?;
|
||||||
|
to.blit(&"End: ", x + 1, y + 3, STYLE_LABEL)?;
|
||||||
|
to.blit(&" 2.1.1", x + 1, y + 4, STYLE_VALUE)?;
|
||||||
|
Ok(Some(frame))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SequenceLoopRange;
|
||||||
|
|
||||||
|
impl Widget for SequenceLoopRange {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let [x, y, ..] = to.area();
|
||||||
|
let range = [x, y, 10, 7];
|
||||||
|
Lozenge(Style::default().fg(Nord::BG2)).draw(to.with_rect(range))?;
|
||||||
|
to.blit(&"Loop [ ]", x + 1, y + 1, STYLE_LABEL)?;
|
||||||
|
to.blit(&"From: ", x + 1, y + 2, STYLE_LABEL)?;
|
||||||
|
to.blit(&" 1.1.1", x + 1, y + 3, STYLE_VALUE)?;
|
||||||
|
to.blit(&"Length: ", x + 1, y + 4, STYLE_LABEL)?;
|
||||||
|
to.blit(&" 1.0.0", x + 1, y + 5, STYLE_VALUE)?;
|
||||||
|
Ok(Some(range))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SequenceNoteRange;
|
||||||
|
|
||||||
|
impl Widget for SequenceNoteRange {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let [x, y, ..] = to.area();
|
||||||
|
let range = [x, y, 10, 9];
|
||||||
|
Lozenge(Style::default().fg(Nord::BG2)).draw(to.with_rect(range))?;
|
||||||
|
to.blit(&"Notes: ", x + 1, y + 1, STYLE_LABEL)?;
|
||||||
|
to.blit(&"C#0-C#9 ", x + 1, y + 2, STYLE_VALUE)?;
|
||||||
|
to.blit(&"[ /2 ]", x + 1, y + 3, STYLE_LABEL)?;
|
||||||
|
to.blit(&"[ x2 ]", x + 1, y + 4, STYLE_LABEL)?;
|
||||||
|
to.blit(&"[ Rev ]", x + 1, y + 5, STYLE_LABEL)?;
|
||||||
|
to.blit(&"[ Inv ]", x + 1, y + 6, STYLE_LABEL)?;
|
||||||
|
to.blit(&"[ Dup ]", x + 1, y + 7, STYLE_LABEL)?;
|
||||||
|
Ok(Some(to.area()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SequenceKeys<'a>(&'a Sequencer<Tui>);
|
||||||
|
|
||||||
|
impl<'a> Widget for SequenceKeys<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let area = to.area();
|
||||||
|
if area.h() < 2 {
|
||||||
|
return Ok(Some(area))
|
||||||
|
}
|
||||||
|
let area = [area.x(), area.y() + 1, 5, area.h() - 2];
|
||||||
|
buffer_update(to.buffer(), area, &|cell, x, y|{
|
||||||
|
let y = y + self.0.note_axis.start as u16;
|
||||||
|
if x < self.0.keys.area.width && y < self.0.keys.area.height {
|
||||||
|
*cell = self.0.keys.get(x, y).clone()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Ok(Some(area))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SequenceNotes<'a>(&'a Sequencer<Tui>);
|
||||||
|
|
||||||
|
impl<'a> Widget for SequenceNotes<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let area = to.area();
|
||||||
|
if area.h() < 2 {
|
||||||
|
return Ok(Some(area))
|
||||||
|
}
|
||||||
|
let area = [
|
||||||
|
area.x() + Sequencer::H_KEYS_OFFSET as u16,
|
||||||
|
area.y() + 1,
|
||||||
|
area.w().saturating_sub(Sequencer::H_KEYS_OFFSET as u16),
|
||||||
|
area.h().saturating_sub(2),
|
||||||
|
];
|
||||||
|
to.buffer_update(area, &move |cell, x, y|{
|
||||||
|
let src_x = ((x as usize + self.0.time_axis.start) * self.0.time_axis.scale) as usize;
|
||||||
|
let src_y = (y as usize + self.0.note_axis.start) as usize;
|
||||||
|
if src_x < self.0.buffer.width && src_y < self.0.buffer.height - 1 {
|
||||||
|
let src = self.0.buffer.get(src_x, self.0.buffer.height - src_y);
|
||||||
|
src.map(|src|{
|
||||||
|
cell.set_symbol(src.symbol());
|
||||||
|
cell.set_fg(src.fg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Ok(Some(area))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SequenceCursor<'a>(&'a Sequencer<Tui>);
|
||||||
|
|
||||||
|
impl<'a> Widget for SequenceCursor<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let area = to.area();
|
||||||
|
if let (Some(time), Some(note)) = (self.0.time_axis.point, self.0.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.0.style_focus())
|
||||||
|
} else {
|
||||||
|
Ok(Some([0,0,0,0]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SequenceZoom<'a>(&'a Sequencer<Tui>);
|
||||||
|
|
||||||
|
impl<'a> Widget for SequenceZoom<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let area = to.area();
|
||||||
|
let quant = ppq_to_name(self.0.time_axis.scale);
|
||||||
|
let quant_x = area.x() + area.w() - 1 - quant.len() as u16;
|
||||||
|
let quant_y = area.y() + area.h() - 2;
|
||||||
|
to.blit(&quant, quant_x, quant_y, self.0.style_focus())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SequenceTimer<'a>(&'a Sequencer<Tui>, Arc<RwLock<Phrase>>);
|
||||||
|
|
||||||
|
impl<'a> Widget for SequenceTimer<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let area = to.area();
|
||||||
|
let phrase = self.1.read().unwrap();
|
||||||
|
let (time0, time_z, now) = (
|
||||||
|
self.0.time_axis.start, self.0.time_axis.scale, self.0.now % phrase.length
|
||||||
|
);
|
||||||
|
let [x, _, width, _] = 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 = (time0 + x2) * time_z;
|
||||||
|
let next_step = (time0 + x2 + 1) * time_z;
|
||||||
|
let style = Sequencer::<Tui>::style_timer_step(now, step as usize, next_step as usize);
|
||||||
|
to.blit(&"-", x as u16, area.y(), Some(style))?;
|
||||||
|
}
|
||||||
|
return Ok(Some([area.x(), area.y(), area.w(), 1]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handle<Tui> for Sequencer<Tui> {
|
||||||
|
fn handle (&mut self, from: &Tui) -> 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
use tek_core::clap::{self, Parser};
|
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
|
||||||
#[command(version, about, long_about = None)]
|
|
||||||
pub struct SequencerCli {
|
|
||||||
/// Name of JACK client
|
|
||||||
#[arg(short, long)] name: Option<String>,
|
|
||||||
/// Pulses per quarter note (sequencer resolution; default: 96)
|
|
||||||
#[arg(short, long)] ppq: Option<usize>,
|
|
||||||
/// Default phrase duration (in pulses; default: 4 * PPQ = 1 bar)
|
|
||||||
#[arg(short, long)] length: Option<usize>,
|
|
||||||
/// Whether to include a transport toolbar (default: true)
|
|
||||||
#[arg(short, long)] transport: Option<bool>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sequencer<Tui> {
|
|
||||||
pub fn from_args () -> Self {
|
|
||||||
let args = SequencerCli::parse();
|
|
||||||
let mut seq = Self::new("");
|
|
||||||
if let Some(name) = args.name {
|
|
||||||
seq.name = Arc::new(RwLock::new(name.clone()));
|
|
||||||
}
|
|
||||||
if let Some(ppq) = args.ppq {
|
|
||||||
seq.ppq = ppq;
|
|
||||||
}
|
|
||||||
if let Some(length) = args.length {
|
|
||||||
if let Some(phrase) = seq.phrase.as_mut() {
|
|
||||||
phrase.write().unwrap().length = length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if args.transport == Some(true) {
|
|
||||||
seq.transport = Some(Arc::new(RwLock::new(TransportToolbar::new(None))));
|
|
||||||
}
|
|
||||||
seq
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
impl Handle<Tui> for Sequencer<Tui> {
|
|
||||||
fn handle (&mut self, from: &Tui) -> 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,3 +3,40 @@ include!("lib.rs");
|
||||||
pub fn main () -> Usually<()> {
|
pub fn main () -> Usually<()> {
|
||||||
Tui::run(Arc::new(RwLock::new(crate::Sequencer::from_args()))).map(|_|())
|
Tui::run(Arc::new(RwLock::new(crate::Sequencer::from_args()))).map(|_|())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use tek_core::clap::{self, Parser};
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
pub struct SequencerCli {
|
||||||
|
/// Name of JACK client
|
||||||
|
#[arg(short, long)] name: Option<String>,
|
||||||
|
/// Pulses per quarter note (sequencer resolution; default: 96)
|
||||||
|
#[arg(short, long)] ppq: Option<usize>,
|
||||||
|
/// Default phrase duration (in pulses; default: 4 * PPQ = 1 bar)
|
||||||
|
#[arg(short, long)] length: Option<usize>,
|
||||||
|
/// Whether to include a transport toolbar (default: true)
|
||||||
|
#[arg(short, long)] transport: Option<bool>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sequencer<Tui> {
|
||||||
|
pub fn from_args () -> Self {
|
||||||
|
let args = SequencerCli::parse();
|
||||||
|
let mut seq = Self::new("");
|
||||||
|
if let Some(name) = args.name {
|
||||||
|
seq.name = Arc::new(RwLock::new(name.clone()));
|
||||||
|
}
|
||||||
|
if let Some(ppq) = args.ppq {
|
||||||
|
seq.ppq = ppq;
|
||||||
|
}
|
||||||
|
if let Some(length) = args.length {
|
||||||
|
if let Some(phrase) = seq.phrase.as_mut() {
|
||||||
|
phrase.write().unwrap().length = length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if args.transport == Some(true) {
|
||||||
|
seq.transport = Some(Arc::new(RwLock::new(TransportToolbar::new(None))));
|
||||||
|
}
|
||||||
|
seq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,203 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
impl Widget for Sequencer<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
self.horizontal_draw(to)?;
|
|
||||||
if self.focused && self.entered {
|
|
||||||
Corners(Style::default().green().not_dim()).draw(to)?;
|
|
||||||
}
|
|
||||||
Ok(Some(to.area()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: Engine> Sequencer<E> {
|
|
||||||
/// 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();
|
|
||||||
fill_seq_bg(&mut buffer, phrase.length, self.ppq)?;
|
|
||||||
fill_seq_fg(&mut buffer, &phrase)?;
|
|
||||||
self.buffer = buffer;
|
|
||||||
} else {
|
|
||||||
self.buffer = Default::default();
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nth_octave (index: u16) -> &'static str {
|
|
||||||
match index {
|
|
||||||
0 => "-1",
|
|
||||||
1 => "0",
|
|
||||||
2 => "1",
|
|
||||||
3 => "2",
|
|
||||||
4 => "3",
|
|
||||||
5 => "4",
|
|
||||||
6 => "5",
|
|
||||||
7 => "6",
|
|
||||||
8 => "7",
|
|
||||||
9 => "8",
|
|
||||||
10 => "9",
|
|
||||||
_ => unreachable!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn key_colors (index: u16) -> (Color, Color) {
|
|
||||||
match index % 6 {
|
|
||||||
0 => (Color::White, Color::Black),
|
|
||||||
1 => (Color::White, Color::Black),
|
|
||||||
2 => (Color::White, Color::White),
|
|
||||||
3 => (Color::Black, Color::White),
|
|
||||||
4 => (Color::Black, Color::White),
|
|
||||||
5 => (Color::Black, Color::White),
|
|
||||||
_ => unreachable!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(char_seq_bg(ppq, x as u16));
|
|
||||||
cell.set_fg(Color::Gray);
|
|
||||||
cell.modifier = Modifier::DIM;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn char_seq_bg (ppq: usize, x: u16) -> char {
|
|
||||||
if ppq == 0 {
|
|
||||||
'·'
|
|
||||||
} else if x % (4 * ppq as u16) == 0 {
|
|
||||||
'│'
|
|
||||||
} else if x % ppq as u16 == 0 {
|
|
||||||
'╎'
|
|
||||||
} else {
|
|
||||||
'·'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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);
|
|
||||||
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));
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
buffer
|
|
||||||
}
|
|
||||||
|
|
@ -1,241 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
impl Sequencer<Tui> {
|
|
||||||
|
|
||||||
const H_KEYS_OFFSET: usize = 5;
|
|
||||||
|
|
||||||
pub(crate) fn horizontal_draw <'a> (&self, to: &mut Tui) -> Usually<()> {
|
|
||||||
let area = to.area();
|
|
||||||
Split::down(|add|{
|
|
||||||
add(&SequenceName(&self))?;
|
|
||||||
add(&SequenceRange)?;
|
|
||||||
add(&SequenceLoopRange)?;
|
|
||||||
add(&SequenceNoteRange)?;
|
|
||||||
Ok(())
|
|
||||||
}).render(to.with_area(area.x(), area.y(), 10, area.h()))?;
|
|
||||||
let area = [area.x() + 10, area.y(), area.w().saturating_sub(10), area.h().min(66)];
|
|
||||||
Lozenge(Style::default().fg(Nord::BG2)).draw(to.with_rect(area))?;
|
|
||||||
let area = [area.x() + 1, area.y(), area.w().saturating_sub(1), area.h()];
|
|
||||||
Layers::new(|add|{
|
|
||||||
add(&SequenceKeys(&self))?;
|
|
||||||
add(&self.phrase.as_ref().map(|phrase|SequenceTimer(&self, phrase.clone())))?;
|
|
||||||
add(&SequenceNotes(&self))?;
|
|
||||||
add(&SequenceCursor(&self))?;
|
|
||||||
add(&SequenceZoom(&self))
|
|
||||||
}).render(to.with_rect(area))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const STYLE_LABEL: Option<Style> = Some(Style {
|
|
||||||
fg: Some(Color::Reset),
|
|
||||||
bg: None,
|
|
||||||
underline_color: None,
|
|
||||||
add_modifier: Modifier::empty(),
|
|
||||||
sub_modifier: Modifier::BOLD,
|
|
||||||
});
|
|
||||||
|
|
||||||
const STYLE_VALUE: Option<Style> = Some(Style {
|
|
||||||
fg: Some(Color::White),
|
|
||||||
bg: None,
|
|
||||||
underline_color: None,
|
|
||||||
add_modifier: Modifier::BOLD,
|
|
||||||
sub_modifier: Modifier::DIM,
|
|
||||||
});
|
|
||||||
|
|
||||||
struct SequenceName<'a>(&'a Sequencer<Tui>);
|
|
||||||
|
|
||||||
impl<'a> Widget for SequenceName<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let [x, y, ..] = to.area();
|
|
||||||
let frame = [x, y, 10, 4];
|
|
||||||
Lozenge(Style::default().fg(Nord::BG2)).draw(to.with_rect(frame))?;
|
|
||||||
to.blit(&"Name:", x + 1, y + 1, STYLE_LABEL)?;
|
|
||||||
to.blit(&*self.0.name.read().unwrap(), x + 1, y + 2, STYLE_VALUE)?;
|
|
||||||
Ok(Some(frame))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SequenceRange;
|
|
||||||
|
|
||||||
impl Widget for SequenceRange {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let [x, y, ..] = to.area();
|
|
||||||
let frame = [x, y, 10, 6];
|
|
||||||
Lozenge(Style::default().fg(Nord::BG2)).draw(to.with_rect(frame))?;
|
|
||||||
to.blit(&"Start: ", x + 1, y + 1, STYLE_LABEL)?;
|
|
||||||
to.blit(&" 1.1.1", x + 1, y + 2, STYLE_VALUE)?;
|
|
||||||
to.blit(&"End: ", x + 1, y + 3, STYLE_LABEL)?;
|
|
||||||
to.blit(&" 2.1.1", x + 1, y + 4, STYLE_VALUE)?;
|
|
||||||
Ok(Some(frame))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SequenceLoopRange;
|
|
||||||
|
|
||||||
impl Widget for SequenceLoopRange {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let [x, y, ..] = to.area();
|
|
||||||
let range = [x, y, 10, 7];
|
|
||||||
Lozenge(Style::default().fg(Nord::BG2)).draw(to.with_rect(range))?;
|
|
||||||
to.blit(&"Loop [ ]", x + 1, y + 1, STYLE_LABEL)?;
|
|
||||||
to.blit(&"From: ", x + 1, y + 2, STYLE_LABEL)?;
|
|
||||||
to.blit(&" 1.1.1", x + 1, y + 3, STYLE_VALUE)?;
|
|
||||||
to.blit(&"Length: ", x + 1, y + 4, STYLE_LABEL)?;
|
|
||||||
to.blit(&" 1.0.0", x + 1, y + 5, STYLE_VALUE)?;
|
|
||||||
Ok(Some(range))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SequenceNoteRange;
|
|
||||||
|
|
||||||
impl Widget for SequenceNoteRange {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let [x, y, ..] = to.area();
|
|
||||||
let range = [x, y, 10, 9];
|
|
||||||
Lozenge(Style::default().fg(Nord::BG2)).draw(to.with_rect(range))?;
|
|
||||||
to.blit(&"Notes: ", x + 1, y + 1, STYLE_LABEL)?;
|
|
||||||
to.blit(&"C#0-C#9 ", x + 1, y + 2, STYLE_VALUE)?;
|
|
||||||
to.blit(&"[ /2 ]", x + 1, y + 3, STYLE_LABEL)?;
|
|
||||||
to.blit(&"[ x2 ]", x + 1, y + 4, STYLE_LABEL)?;
|
|
||||||
to.blit(&"[ Rev ]", x + 1, y + 5, STYLE_LABEL)?;
|
|
||||||
to.blit(&"[ Inv ]", x + 1, y + 6, STYLE_LABEL)?;
|
|
||||||
to.blit(&"[ Dup ]", x + 1, y + 7, STYLE_LABEL)?;
|
|
||||||
Ok(Some(to.area()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SequenceKeys<'a>(&'a Sequencer<Tui>);
|
|
||||||
|
|
||||||
impl<'a> Widget for SequenceKeys<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let area = to.area();
|
|
||||||
if area.h() < 2 {
|
|
||||||
return Ok(Some(area))
|
|
||||||
}
|
|
||||||
let area = [area.x(), area.y() + 1, 5, area.h() - 2];
|
|
||||||
buffer_update(to.buffer(), area, &|cell, x, y|{
|
|
||||||
let y = y + self.0.note_axis.start as u16;
|
|
||||||
if x < self.0.keys.area.width && y < self.0.keys.area.height {
|
|
||||||
*cell = self.0.keys.get(x, y).clone()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok(Some(area))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SequenceNotes<'a>(&'a Sequencer<Tui>);
|
|
||||||
|
|
||||||
impl<'a> Widget for SequenceNotes<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let area = to.area();
|
|
||||||
if area.h() < 2 {
|
|
||||||
return Ok(Some(area))
|
|
||||||
}
|
|
||||||
let area = [
|
|
||||||
area.x() + Sequencer::H_KEYS_OFFSET as u16,
|
|
||||||
area.y() + 1,
|
|
||||||
area.w().saturating_sub(Sequencer::H_KEYS_OFFSET as u16),
|
|
||||||
area.h().saturating_sub(2),
|
|
||||||
];
|
|
||||||
to.buffer_update(area, &move |cell, x, y|{
|
|
||||||
let src_x = ((x as usize + self.0.time_axis.start) * self.0.time_axis.scale) as usize;
|
|
||||||
let src_y = (y as usize + self.0.note_axis.start) as usize;
|
|
||||||
if src_x < self.0.buffer.width && src_y < self.0.buffer.height - 1 {
|
|
||||||
let src = self.0.buffer.get(src_x, self.0.buffer.height - src_y);
|
|
||||||
src.map(|src|{
|
|
||||||
cell.set_symbol(src.symbol());
|
|
||||||
cell.set_fg(src.fg);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok(Some(area))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SequenceCursor<'a>(&'a Sequencer<Tui>);
|
|
||||||
|
|
||||||
impl<'a> Widget for SequenceCursor<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let area = to.area();
|
|
||||||
if let (Some(time), Some(note)) = (self.0.time_axis.point, self.0.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.0.style_focus())
|
|
||||||
} else {
|
|
||||||
Ok(Some([0,0,0,0]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SequenceZoom<'a>(&'a Sequencer<Tui>);
|
|
||||||
|
|
||||||
impl<'a> Widget for SequenceZoom<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let area = to.area();
|
|
||||||
let quant = ppq_to_name(self.0.time_axis.scale);
|
|
||||||
let quant_x = area.x() + area.w() - 1 - quant.len() as u16;
|
|
||||||
let quant_y = area.y() + area.h() - 2;
|
|
||||||
to.blit(&quant, quant_x, quant_y, self.0.style_focus())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SequenceTimer<'a>(&'a Sequencer<Tui>, Arc<RwLock<Phrase>>);
|
|
||||||
|
|
||||||
impl<'a> Widget for SequenceTimer<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let area = to.area();
|
|
||||||
let phrase = self.1.read().unwrap();
|
|
||||||
let (time0, time_z, now) = (
|
|
||||||
self.0.time_axis.start, self.0.time_axis.scale, self.0.now % phrase.length
|
|
||||||
);
|
|
||||||
let [x, _, width, _] = 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 = (time0 + x2) * time_z;
|
|
||||||
let next_step = (time0 + x2 + 1) * time_z;
|
|
||||||
let style = Sequencer::<Tui>::style_timer_step(now, step as usize, next_step as usize);
|
|
||||||
to.blit(&"-", x as u16, area.y(), Some(style))?;
|
|
||||||
}
|
|
||||||
return Ok(Some([area.x(), area.y(), area.w(), 1]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -260,3 +260,205 @@ impl Focusable<Tui> for TransportClock<Tui> {
|
||||||
self.focused = focused
|
self.focused = focused
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Handle<Tui> for TransportToolbar<Tui> {
|
||||||
|
fn handle (&mut self, from: &Tui) -> Perhaps<bool> {
|
||||||
|
Ok(None)
|
||||||
|
//Ok(
|
||||||
|
//from.key(KeyCode::Right).does(||self.focus_next())?
|
||||||
|
//||
|
||||||
|
//from.key(KeyCode::Left).does(||self.focus_prev())?
|
||||||
|
//||
|
||||||
|
//from.key(KeyCode::Char(' ')).does(||self.toggle_play())?
|
||||||
|
//)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handle<Tui> for TransportPlayPauseButton<Tui> {
|
||||||
|
fn handle (&mut self, from: &Tui) -> Perhaps<bool> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handle<Tui> for TransportBPM<Tui> {
|
||||||
|
fn handle (&mut self, from: &Tui) -> Perhaps<bool> {
|
||||||
|
//TransportFocus::BPM => {
|
||||||
|
//transport.timebase.bpm.fetch_add(1.0, Ordering::Relaxed);
|
||||||
|
//},
|
||||||
|
//TransportFocus::BPM => {
|
||||||
|
//transport.timebase.bpm.fetch_sub(1.0, Ordering::Relaxed);
|
||||||
|
//},
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handle<Tui> for TransportQuantize<Tui> {
|
||||||
|
fn handle (&mut self, from: &Tui) -> Perhaps<bool> {
|
||||||
|
//TransportFocus::Quant => {
|
||||||
|
//transport.quant.value = next_note_length(transport.quant)
|
||||||
|
//},
|
||||||
|
//TransportFocus::Quant => {
|
||||||
|
//transport.quant.value = prev_note_length(transport.quant);
|
||||||
|
//},
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handle<Tui> for TransportSync<Tui> {
|
||||||
|
fn handle (&mut self, from: &Tui) -> Perhaps<bool> {
|
||||||
|
//TransportFocus::Sync => {
|
||||||
|
//transport.sync.value = next_note_length(transport.sync)
|
||||||
|
//},
|
||||||
|
//TransportFocus::Sync => {
|
||||||
|
//transport.sync.value = prev_note_length(transport.sync);
|
||||||
|
//},
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handle<Tui> for TransportClock<Tui> {
|
||||||
|
fn handle (&mut self, from: &Tui) -> Perhaps<bool> {
|
||||||
|
//TransportFocus::Sync => {
|
||||||
|
//transport.sync.value = next_note_length(transport.sync)
|
||||||
|
//},
|
||||||
|
//TransportFocus::Sync => {
|
||||||
|
//transport.sync.value = prev_note_length(transport.sync);
|
||||||
|
//},
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CORNERS: Corners = Corners(NOT_DIM_GREEN);
|
||||||
|
|
||||||
|
impl Content for TransportToolbar<Tui> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
|
Split::right(|add|{
|
||||||
|
add(&self.playing)?;
|
||||||
|
add(&self.bpm)?;
|
||||||
|
add(&self.quant)?;
|
||||||
|
add(&self.sync)?;
|
||||||
|
add(&self.clock)?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Content for TransportPlayPauseButton<Tui> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
|
Layers::new(|add|{
|
||||||
|
//add(&self.focused.then_some(CORNERS))?;
|
||||||
|
add(&Styled(match self.value {
|
||||||
|
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.value {
|
||||||
|
Some(TransportState::Rolling) => "▶ PLAYING",
|
||||||
|
Some(TransportState::Starting) => "READY ...",
|
||||||
|
Some(TransportState::Stopped) => "⏹ STOPPED",
|
||||||
|
_ => unreachable!(),
|
||||||
|
}))?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for TransportBPM<Tui> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
area.expect_min(10, 1)?;
|
||||||
|
Ok(Some([area.x(), area.y(), 10, 1]))
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let area = to.area();
|
||||||
|
let [x, y, ..] = area;
|
||||||
|
let Self { value, focused, .. } = self;
|
||||||
|
to.blit(&"BPM", x, y, Some(NOT_DIM))?;
|
||||||
|
let bpm = format!("{}.{:03}", value, (value * 1000.0) % 1000.0);
|
||||||
|
to.blit(&bpm, x, y + 1, Some(NOT_DIM_BOLD))?;
|
||||||
|
let width = bpm.len() as u16;
|
||||||
|
let area = [x, y, (width + 2).max(10), 2];
|
||||||
|
if *focused {
|
||||||
|
let area = [area.x() - 1, area.y(), area.w() - 1, area.h() ];
|
||||||
|
CORNERS.draw(to)?;
|
||||||
|
to.fill_bg(area, COLOR_BG1);
|
||||||
|
}
|
||||||
|
Ok(Some(area))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for TransportQuantize<Tui> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
area.expect_min(10, 1)?;
|
||||||
|
Ok(Some([area.x(), area.y(), 10, 1]))
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let [x, y, ..] = to.area();
|
||||||
|
let Self { value, focused, .. } = self;
|
||||||
|
to.blit(&"QUANT", x, y, Some(NOT_DIM))?;
|
||||||
|
let name = ppq_to_name(*value as usize);
|
||||||
|
let width = name.len() as u16;
|
||||||
|
to.blit(&name, x, y + 1, Some(NOT_DIM_BOLD))?;
|
||||||
|
let area = [x, y, (width + 2).max(10), 2];
|
||||||
|
if *focused {
|
||||||
|
let area = [area.x() - 1, area.y(), area.w() - 1, area.h() ];
|
||||||
|
CORNERS.draw(to)?;
|
||||||
|
to.fill_bg(area, COLOR_BG1);
|
||||||
|
}
|
||||||
|
Ok(Some(area))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for TransportSync<Tui> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
area.expect_min(10, 1)?;
|
||||||
|
Ok(Some([area.x(), area.y(), 10, 1]))
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let [x, y, ..] = to.area();
|
||||||
|
let Self { value, focused, .. } = self;
|
||||||
|
to.blit(&"SYNC", x, y, Some(NOT_DIM))?;
|
||||||
|
let name = ppq_to_name(*value as usize);
|
||||||
|
let width = name.len() as u16;
|
||||||
|
to.blit(&name, x, y + 1, Some(NOT_DIM_BOLD))?;
|
||||||
|
let area = [x, y, (width + 2).max(10), 2];
|
||||||
|
if *focused {
|
||||||
|
let area = [area.x() - 1, area.y(), area.w() - 1, area.h() ];
|
||||||
|
CORNERS.draw(to)?;
|
||||||
|
to.fill_bg(area, COLOR_BG1);
|
||||||
|
}
|
||||||
|
Ok(Some(area))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for TransportClock<Tui> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
||||||
|
area.expect_min(10, 1)?;
|
||||||
|
Ok(Some([area.x(), area.y(), 20, 1]))
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
||||||
|
let [x, y, width, _] = to.area();
|
||||||
|
let Self { frame: _frame, pulse, ppq, usecs, focused, .. } = 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);
|
||||||
|
let timer = format!("{bars}.{beats}.{pulses:02}");
|
||||||
|
to.blit(&timer, x + width - timer.len() as u16 - 1, y + 0, Some(NOT_DIM))?;
|
||||||
|
let timer = format!("{minutes}:{seconds:02}:{msecs:03}");
|
||||||
|
to.blit(&timer, x + width - timer.len() as u16 - 1, y + 1, Some(NOT_DIM))?;
|
||||||
|
let area = to.area();
|
||||||
|
let area = [area.x(), area.y(), area.w() + 1, area.h()];
|
||||||
|
if *focused {
|
||||||
|
let area = [area.x() - 1, area.y(), area.w(), area.h() ];
|
||||||
|
CORNERS.draw(to)?;
|
||||||
|
to.fill_bg(area, COLOR_BG1);
|
||||||
|
}
|
||||||
|
Ok(Some(area))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
impl Handle<Tui> for TransportToolbar<Tui> {
|
|
||||||
fn handle (&mut self, from: &Tui) -> Perhaps<bool> {
|
|
||||||
Ok(None)
|
|
||||||
//Ok(
|
|
||||||
//from.key(KeyCode::Right).does(||self.focus_next())?
|
|
||||||
//||
|
|
||||||
//from.key(KeyCode::Left).does(||self.focus_prev())?
|
|
||||||
//||
|
|
||||||
//from.key(KeyCode::Char(' ')).does(||self.toggle_play())?
|
|
||||||
//)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Handle<Tui> for TransportPlayPauseButton<Tui> {
|
|
||||||
fn handle (&mut self, from: &Tui) -> Perhaps<bool> {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Handle<Tui> for TransportBPM<Tui> {
|
|
||||||
fn handle (&mut self, from: &Tui) -> Perhaps<bool> {
|
|
||||||
//TransportFocus::BPM => {
|
|
||||||
//transport.timebase.bpm.fetch_add(1.0, Ordering::Relaxed);
|
|
||||||
//},
|
|
||||||
//TransportFocus::BPM => {
|
|
||||||
//transport.timebase.bpm.fetch_sub(1.0, Ordering::Relaxed);
|
|
||||||
//},
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Handle<Tui> for TransportQuantize<Tui> {
|
|
||||||
fn handle (&mut self, from: &Tui) -> Perhaps<bool> {
|
|
||||||
//TransportFocus::Quant => {
|
|
||||||
//transport.quant.value = next_note_length(transport.quant)
|
|
||||||
//},
|
|
||||||
//TransportFocus::Quant => {
|
|
||||||
//transport.quant.value = prev_note_length(transport.quant);
|
|
||||||
//},
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Handle<Tui> for TransportSync<Tui> {
|
|
||||||
fn handle (&mut self, from: &Tui) -> Perhaps<bool> {
|
|
||||||
//TransportFocus::Sync => {
|
|
||||||
//transport.sync.value = next_note_length(transport.sync)
|
|
||||||
//},
|
|
||||||
//TransportFocus::Sync => {
|
|
||||||
//transport.sync.value = prev_note_length(transport.sync);
|
|
||||||
//},
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Handle<Tui> for TransportClock<Tui> {
|
|
||||||
fn handle (&mut self, from: &Tui) -> Perhaps<bool> {
|
|
||||||
//TransportFocus::Sync => {
|
|
||||||
//transport.sync.value = next_note_length(transport.sync)
|
|
||||||
//},
|
|
||||||
//TransportFocus::Sync => {
|
|
||||||
//transport.sync.value = prev_note_length(transport.sync);
|
|
||||||
//},
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
const CORNERS: Corners = Corners(NOT_DIM_GREEN);
|
|
||||||
|
|
||||||
impl Content for TransportToolbar<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
||||||
Split::right(|add|{
|
|
||||||
add(&self.playing)?;
|
|
||||||
add(&self.bpm)?;
|
|
||||||
add(&self.quant)?;
|
|
||||||
add(&self.sync)?;
|
|
||||||
add(&self.clock)?;
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Content for TransportPlayPauseButton<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
||||||
Layers::new(|add|{
|
|
||||||
//add(&self.focused.then_some(CORNERS))?;
|
|
||||||
add(&Styled(match self.value {
|
|
||||||
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.value {
|
|
||||||
Some(TransportState::Rolling) => "▶ PLAYING",
|
|
||||||
Some(TransportState::Starting) => "READY ...",
|
|
||||||
Some(TransportState::Stopped) => "⏹ STOPPED",
|
|
||||||
_ => unreachable!(),
|
|
||||||
}))?;
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for TransportBPM<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
area.expect_min(10, 1)?;
|
|
||||||
Ok(Some([area.x(), area.y(), 10, 1]))
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let area = to.area();
|
|
||||||
let [x, y, ..] = area;
|
|
||||||
let Self { value, focused, .. } = self;
|
|
||||||
to.blit(&"BPM", x, y, Some(NOT_DIM))?;
|
|
||||||
let bpm = format!("{}.{:03}", value, (value * 1000.0) % 1000.0);
|
|
||||||
to.blit(&bpm, x, y + 1, Some(NOT_DIM_BOLD))?;
|
|
||||||
let width = bpm.len() as u16;
|
|
||||||
let area = [x, y, (width + 2).max(10), 2];
|
|
||||||
if *focused {
|
|
||||||
let area = [area.x() - 1, area.y(), area.w() - 1, area.h() ];
|
|
||||||
CORNERS.draw(to)?;
|
|
||||||
to.fill_bg(area, COLOR_BG1);
|
|
||||||
}
|
|
||||||
Ok(Some(area))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for TransportQuantize<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
area.expect_min(10, 1)?;
|
|
||||||
Ok(Some([area.x(), area.y(), 10, 1]))
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let [x, y, ..] = to.area();
|
|
||||||
let Self { value, focused, .. } = self;
|
|
||||||
to.blit(&"QUANT", x, y, Some(NOT_DIM))?;
|
|
||||||
let name = ppq_to_name(*value as usize);
|
|
||||||
let width = name.len() as u16;
|
|
||||||
to.blit(&name, x, y + 1, Some(NOT_DIM_BOLD))?;
|
|
||||||
let area = [x, y, (width + 2).max(10), 2];
|
|
||||||
if *focused {
|
|
||||||
let area = [area.x() - 1, area.y(), area.w() - 1, area.h() ];
|
|
||||||
CORNERS.draw(to)?;
|
|
||||||
to.fill_bg(area, COLOR_BG1);
|
|
||||||
}
|
|
||||||
Ok(Some(area))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for TransportSync<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
area.expect_min(10, 1)?;
|
|
||||||
Ok(Some([area.x(), area.y(), 10, 1]))
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let [x, y, ..] = to.area();
|
|
||||||
let Self { value, focused, .. } = self;
|
|
||||||
to.blit(&"SYNC", x, y, Some(NOT_DIM))?;
|
|
||||||
let name = ppq_to_name(*value as usize);
|
|
||||||
let width = name.len() as u16;
|
|
||||||
to.blit(&name, x, y + 1, Some(NOT_DIM_BOLD))?;
|
|
||||||
let area = [x, y, (width + 2).max(10), 2];
|
|
||||||
if *focused {
|
|
||||||
let area = [area.x() - 1, area.y(), area.w() - 1, area.h() ];
|
|
||||||
CORNERS.draw(to)?;
|
|
||||||
to.fill_bg(area, COLOR_BG1);
|
|
||||||
}
|
|
||||||
Ok(Some(area))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for TransportClock<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, area: [u16;4]) -> Perhaps<[u16;4]> {
|
|
||||||
area.expect_min(10, 1)?;
|
|
||||||
Ok(Some([area.x(), area.y(), 20, 1]))
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
||||||
let [x, y, width, _] = to.area();
|
|
||||||
let Self { frame: _frame, pulse, ppq, usecs, focused, .. } = 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);
|
|
||||||
let timer = format!("{bars}.{beats}.{pulses:02}");
|
|
||||||
to.blit(&timer, x + width - timer.len() as u16 - 1, y + 0, Some(NOT_DIM))?;
|
|
||||||
let timer = format!("{minutes}:{seconds:02}:{msecs:03}");
|
|
||||||
to.blit(&timer, x + width - timer.len() as u16 - 1, y + 1, Some(NOT_DIM))?;
|
|
||||||
let area = to.area();
|
|
||||||
let area = [area.x(), area.y(), area.w() + 1, area.h()];
|
|
||||||
if *focused {
|
|
||||||
let area = [area.x() - 1, area.y(), area.w(), area.h() ];
|
|
||||||
CORNERS.draw(to)?;
|
|
||||||
to.fill_bg(area, COLOR_BG1);
|
|
||||||
}
|
|
||||||
Ok(Some(area))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue