mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
336 lines
14 KiB
Rust
336 lines
14 KiB
Rust
use crate::*;
|
|
|
|
impl<'a, T: TransportViewState> Content for TransportView<'a, T> {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
let state = self.0;
|
|
let focused = state.transport_focused();
|
|
lay!(
|
|
state.transport_selected().wrap(focused, TransportFocus::PlayPause, &Styled(
|
|
None,
|
|
match state.transport_state() {
|
|
Some(TransportState::Rolling) => "▶ PLAYING",
|
|
Some(TransportState::Starting) => "READY ...",
|
|
Some(TransportState::Stopped) => "⏹ STOPPED",
|
|
_ => unreachable!(),
|
|
}
|
|
).min_xy(11, 2).push_x(1)).align_x().fill_x(),
|
|
|
|
row!(
|
|
state.transport_selected().wrap(focused, TransportFocus::Bpm, &Outset::X(1u16, {
|
|
let bpm = state.bpm_value();
|
|
row! { "BPM ", format!("{}.{:03}", bpm as usize, (bpm * 1000.0) % 1000.0) }
|
|
})),
|
|
//let quant = state.focus().wrap(state.focused(), TransportFocus::Quant, &Outset::X(1u16, row! {
|
|
//"QUANT ", ppq_to_name(state.0.quant as usize)
|
|
//})),
|
|
state.transport_selected().wrap(focused, TransportFocus::Sync, &Outset::X(1u16, row! {
|
|
"SYNC ", pulses_to_name(state.sync_value() as usize)
|
|
}))
|
|
).align_w().fill_x(),
|
|
|
|
state.transport_selected().wrap(focused, TransportFocus::Clock, &{
|
|
let time1 = state.format_beat();
|
|
let time2 = state.format_msu();
|
|
row!("B" ,time1.as_str(), " T", time2.as_str()).outset_x(1)
|
|
}).align_e().fill_x(),
|
|
|
|
).fill_x().bg(Color::Rgb(40, 50, 30))
|
|
}
|
|
}
|
|
|
|
impl Content for SequencerTui {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
col!(
|
|
widget(&TransportView(self)),
|
|
Split::right(20,
|
|
widget(&PhrasesView(self)),
|
|
widget(&PhraseView(self)),
|
|
).min_y(20)
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Display mode of arranger
|
|
#[derive(Clone, PartialEq)]
|
|
pub enum ArrangerMode {
|
|
/// Tracks are rows
|
|
Horizontal,
|
|
/// Tracks are columns
|
|
Vertical(usize),
|
|
}
|
|
|
|
/// Arranger display mode can be cycled
|
|
impl ArrangerMode {
|
|
/// Cycle arranger display mode
|
|
pub fn to_next (&mut self) {
|
|
*self = match self {
|
|
Self::Horizontal => Self::Vertical(1),
|
|
Self::Vertical(1) => Self::Vertical(2),
|
|
Self::Vertical(2) => Self::Vertical(2),
|
|
Self::Vertical(0) => Self::Horizontal,
|
|
Self::Vertical(_) => Self::Vertical(0),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Layout for standalone arranger app.
|
|
impl Content for ArrangerTui {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
let arranger_focused = self.arranger_focused();
|
|
Split::up(
|
|
1,
|
|
TransportView(self),
|
|
Split::down(
|
|
self.splits[0],
|
|
lay!(
|
|
Layers::new(move |add|{
|
|
match self.mode {
|
|
ArrangerMode::Horizontal =>
|
|
add(&arranger_content_horizontal(self))?,
|
|
ArrangerMode::Vertical(factor) =>
|
|
add(&arranger_content_vertical(self, factor))?
|
|
};
|
|
add(&self.size)
|
|
})
|
|
.grow_y(1)
|
|
.border(Lozenge(Style::default()
|
|
.bg(TuiTheme::border_bg())
|
|
.fg(TuiTheme::border_fg(arranger_focused)))),
|
|
widget(&self.size),
|
|
widget(&format!("[{}] Arranger", if self.entered {
|
|
"■"
|
|
} else {
|
|
" "
|
|
}))
|
|
.fg(TuiTheme::title_fg(arranger_focused))
|
|
.push_x(1),
|
|
),
|
|
Split::right(
|
|
self.splits[1],
|
|
PhrasesView(self),
|
|
PhraseView(self),
|
|
)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
// TODO: Display phrases always in order of appearance
|
|
impl<'a, T: PhrasesViewState> Content for PhrasesView<'a, T> {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
let focused = self.0.phrases_focused();
|
|
let entered = self.0.entered();
|
|
let phrases = self.0.phrases();
|
|
let selected_phrase = self.0.phrase();
|
|
let mode = self.0.mode();
|
|
let content = col!(
|
|
(i, phrase) in phrases.iter().enumerate() => Layers::new(|add|{
|
|
let Phrase { ref name, color, length, .. } = *phrase.read().unwrap();
|
|
let mut length = PhraseLength::new(length, None);
|
|
if let Some(PhrasesMode::Length(phrase, new_length, focus)) = mode {
|
|
if focused && i == *phrase {
|
|
length.pulses = *new_length;
|
|
length.focus = Some(*focus);
|
|
}
|
|
}
|
|
let length = length.align_e().fill_x();
|
|
let row1 = lay!(format!(" {i}").align_w().fill_x(), length).fill_x();
|
|
let mut row2 = format!(" {name}");
|
|
if let Some(PhrasesMode::Rename(phrase, _)) = mode {
|
|
if focused && i == *phrase {
|
|
row2 = format!("{row2}▄");
|
|
}
|
|
};
|
|
let row2 = TuiStyle::bold(row2, true);
|
|
add(&col!(row1, row2).fill_x().bg(color.base.rgb))?;
|
|
if focused && i == selected_phrase {
|
|
add(&CORNERS)?;
|
|
}
|
|
Ok(())
|
|
})
|
|
);
|
|
let border_color = if focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)};
|
|
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
|
|
let content = content.fill_xy().bg(Color::Rgb(28, 35, 25)).border(border);
|
|
let title_color = if focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)};
|
|
let upper_left = format!("[{}] Phrases", if entered {"■"} else {" "});
|
|
let upper_right = format!("({})", self.0.phrases().len());
|
|
lay!(
|
|
content,
|
|
TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(),
|
|
TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(),
|
|
)
|
|
}
|
|
}
|
|
|
|
impl<'a, T: PhraseViewState> Content for PhraseView<'a, T> {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
let phrase = self.0.phrase();
|
|
let size = self.0.size();
|
|
let focused = self.0.phrase_focused();
|
|
let entered = self.0.entered();
|
|
let keys = self.0.keys();
|
|
let buffer = self.0.buffer();
|
|
let note_len = self.0.note_len();
|
|
let note_axis = self.0.note_axis();
|
|
let time_axis = self.0.time_axis();
|
|
let FixedAxis { start: note_start, point: note_point, clamp: note_clamp }
|
|
= *note_axis.read().unwrap();
|
|
let ScaledAxis { start: time_start, point: time_point, clamp: time_clamp, scale: time_scale }
|
|
= *time_axis.read().unwrap();
|
|
//let color = Color::Rgb(0,255,0);
|
|
//let color = phrase.as_ref().map(|p|p.read().unwrap().color.base.rgb).unwrap_or(color);
|
|
let keys = CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{
|
|
Ok(if to.area().h() >= 2 {
|
|
to.buffer_update(to.area().set_w(5), &|cell, x, y|{
|
|
let y = y + (note_start / 2) as u16;
|
|
if x < keys.area.width && y < keys.area.height {
|
|
*cell = keys.get(x, y).clone()
|
|
}
|
|
});
|
|
})
|
|
}).fill_y();
|
|
let notes_bg_null = Color::Rgb(28, 35, 25);
|
|
let notes = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{
|
|
let area = to.area();
|
|
let h = area.h() as usize;
|
|
size.set_wh(area.w(), h);
|
|
let mut axis = note_axis.write().unwrap();
|
|
if let Some(point) = axis.point {
|
|
if point.saturating_sub(axis.start) > (h * 2).saturating_sub(1) {
|
|
axis.start += 2;
|
|
}
|
|
}
|
|
Ok(if to.area().h() >= 2 {
|
|
let area = to.area();
|
|
to.buffer_update(area, &move |cell, x, y|{
|
|
cell.set_bg(notes_bg_null);
|
|
let src_x = (x as usize + time_start) * time_scale;
|
|
let src_y = y as usize + note_start / 2;
|
|
if src_x < buffer.width && src_y < buffer.height - 1 {
|
|
buffer.get(src_x, buffer.height - src_y - 2).map(|src|{
|
|
cell.set_symbol(src.symbol());
|
|
cell.set_fg(src.fg);
|
|
cell.set_bg(src.bg);
|
|
});
|
|
}
|
|
});
|
|
})
|
|
}).fill_x();
|
|
let cursor = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{
|
|
Ok(if focused && entered {
|
|
let area = to.area();
|
|
if let (Some(time), Some(note)) = (time_point, note_point) {
|
|
let x1 = area.x() + (time / time_scale) as u16;
|
|
let x2 = x1 + (note_len / time_scale) as u16;
|
|
let y = area.y() + note.saturating_sub(note_start) as u16 / 2;
|
|
let c = if note % 2 == 0 { "▀" } else { "▄" };
|
|
for x in x1..x2 {
|
|
to.blit(&c, x, y, Some(Style::default().fg(Color::Rgb(0,255,0))));
|
|
}
|
|
}
|
|
})
|
|
});
|
|
let playhead_inactive = Style::default().fg(Color::Rgb(255,255,255)).bg(Color::Rgb(40,50,30));
|
|
let playhead_active = playhead_inactive.clone().yellow().bold().not_dim();
|
|
let playhead = CustomWidget::new(
|
|
|to:[u16;2]|Ok(Some(to.clip_h(1))),
|
|
move|to: &mut TuiOutput|{
|
|
if let Some(_) = phrase {
|
|
let now = self.0.now().get() as usize; // TODO FIXME: self.now % phrase.read().unwrap().length;
|
|
let time_clamp = time_clamp
|
|
.expect("time_axis of sequencer expected to be clamped");
|
|
for x in 0..(time_clamp/time_scale).saturating_sub(time_start) {
|
|
let this_step = time_start + (x + 0) * time_scale;
|
|
let next_step = time_start + (x + 1) * time_scale;
|
|
let x = to.area().x() + x as u16;
|
|
let active = this_step <= now && now < next_step;
|
|
let character = if active { "|" } else { "·" };
|
|
let style = if active { playhead_active } else { playhead_inactive };
|
|
to.blit(&character, x, to.area.y(), Some(style));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
).push_x(6).align_sw();
|
|
let border_color = if focused{Color::Rgb(100, 110, 40)}else{Color::Rgb(70, 80, 50)};
|
|
let title_color = if focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)};
|
|
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
|
|
let note_area = lay!(notes, cursor).fill_x();
|
|
let piano_roll = row!(keys, note_area).fill_x();
|
|
let content = piano_roll.bg(Color::Rgb(40, 50, 30)).border(border);
|
|
let content = lay!(content, playhead);
|
|
let mut upper_left = format!("[{}] Sequencer", if entered {"■"} else {" "});
|
|
if let Some(phrase) = phrase {
|
|
upper_left = format!("{upper_left}: {}", phrase.read().unwrap().name);
|
|
}
|
|
let mut lower_right = format!("┤{}├", size.format());
|
|
lower_right = format!("┤Zoom: {}├─{lower_right}", pulses_to_name(time_scale));
|
|
//lower_right = format!("Zoom: {} (+{}:{}*{}|{})",
|
|
//pulses_to_name(time_scale),
|
|
//time_start, time_point.unwrap_or(0),
|
|
//time_scale, time_clamp.unwrap_or(0),
|
|
//);
|
|
if focused && entered {
|
|
lower_right = format!("┤Note: {} {}├─{lower_right}",
|
|
note_axis.read().unwrap().point.unwrap(),
|
|
pulses_to_name(note_len));
|
|
//lower_right = format!("Note: {} (+{}:{}|{}) {upper_right}",
|
|
//pulses_to_name(*note_len),
|
|
//note_start,
|
|
//note_point.unwrap_or(0),
|
|
//note_clamp.unwrap_or(0),
|
|
//);
|
|
}
|
|
let upper_right = if let Some(phrase) = phrase {
|
|
format!("┤Length: {}├", phrase.read().unwrap().length)
|
|
} else {
|
|
String::new()
|
|
};
|
|
lay!(
|
|
content,
|
|
TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(),
|
|
TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(),
|
|
TuiStyle::fg(lower_right.to_string(), title_color).pull_x(1).align_se().fill_xy(),
|
|
)
|
|
}
|
|
}
|
|
|
|
impl Content for PhraseLength {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
Layers::new(move|add|{
|
|
match self.focus {
|
|
None => add(&row!(
|
|
" ", self.bars_string(),
|
|
".", self.beats_string(),
|
|
".", self.ticks_string(),
|
|
" "
|
|
)),
|
|
Some(PhraseLengthFocus::Bar) => add(&row!(
|
|
"[", self.bars_string(),
|
|
"]", self.beats_string(),
|
|
".", self.ticks_string(),
|
|
" "
|
|
)),
|
|
Some(PhraseLengthFocus::Beat) => add(&row!(
|
|
" ", self.bars_string(),
|
|
"[", self.beats_string(),
|
|
"]", self.ticks_string(),
|
|
" "
|
|
)),
|
|
Some(PhraseLengthFocus::Tick) => add(&row!(
|
|
" ", self.bars_string(),
|
|
".", self.beats_string(),
|
|
"[", self.ticks_string(),
|
|
"]"
|
|
)),
|
|
}
|
|
})
|
|
}
|
|
}
|