mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 04:06:45 +01:00
1099 lines
41 KiB
Rust
1099 lines
41 KiB
Rust
use crate::*;
|
|
|
|
impl Widget for TransportTui {
|
|
type Engine = Tui;
|
|
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
|
TransportView(&self, Default::default()).layout(to)
|
|
}
|
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
|
TransportView(&self, Default::default()).render(to)
|
|
}
|
|
}
|
|
|
|
pub struct TransportView<'a, T: TransportViewState>(pub &'a T);
|
|
|
|
pub trait TransportViewState: Send + Sync {
|
|
fn focus (&self) -> TransportFocus;
|
|
fn is_focused (&self) -> bool;
|
|
fn transport_state (&self) -> Option<TransportState>;
|
|
fn bpm_value (&self) -> f64;
|
|
fn sync_value (&self) -> f64;
|
|
fn format_beat (&self) -> String;
|
|
fn format_msu (&self) -> String;
|
|
}
|
|
|
|
impl TransportViewState for TransportTui {
|
|
fn focus (&self) -> TransportFocus {
|
|
self.focus
|
|
}
|
|
fn is_focused (&self) -> bool {
|
|
self.focused
|
|
}
|
|
fn transport_state (&self) -> Option<TransportState> {
|
|
*self.playing().read().unwrap()
|
|
}
|
|
fn bpm_value (&self) -> f64 {
|
|
self.bpm().get()
|
|
}
|
|
fn sync_value (&self) -> f64 {
|
|
self.sync().get()
|
|
}
|
|
fn format_beat (&self) -> String {
|
|
self.current().format_beat()
|
|
}
|
|
fn format_msu (&self) -> String {
|
|
self.current().usec.format_msu()
|
|
}
|
|
}
|
|
|
|
impl<'a, T: TransportViewState> Content for TransportView<'a, T> {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
let state = self.0;
|
|
lay!(
|
|
state.focus().wrap(state.is_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.focus().wrap(state.is_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.focus().wrap(state.is_focused(), TransportFocus::Sync, &Outset::X(1u16, row! {
|
|
"SYNC ", pulses_to_name(state.sync_value() as usize)
|
|
}))
|
|
).align_w().fill_x(),
|
|
|
|
state.focus().wrap(state.is_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)
|
|
)
|
|
}
|
|
}
|
|
|
|
impl TransportViewState for SequencerTui {
|
|
fn focus (&self) -> TransportFocus {
|
|
self.focus
|
|
}
|
|
fn is_focused (&self) -> bool {
|
|
self.focused
|
|
}
|
|
fn transport_state (&self) -> Option<TransportState> {
|
|
*self.playing().read().unwrap()
|
|
}
|
|
fn bpm_value (&self) -> f64 {
|
|
self.bpm().get()
|
|
}
|
|
fn sync_value (&self) -> f64 {
|
|
self.sync().get()
|
|
}
|
|
fn format_beat (&self) -> String {
|
|
self.current().format_beat()
|
|
}
|
|
fn format_msu (&self) -> String {
|
|
self.current().usec.format_msu()
|
|
}
|
|
}
|
|
|
|
impl PhrasesViewState for SequencerTui {
|
|
fn focused (&self) -> bool {
|
|
todo!()
|
|
}
|
|
fn entered (&self) -> bool {
|
|
todo!()
|
|
}
|
|
fn phrases (&self) -> Vec<Arc<RwLock<Phrase>>> {
|
|
todo!()
|
|
}
|
|
fn phrase (&self) -> usize {
|
|
todo!()
|
|
}
|
|
fn mode (&self) -> Option<&PhrasesMode> {
|
|
&self.mode
|
|
}
|
|
}
|
|
|
|
impl PhraseViewState for SequencerTui {
|
|
fn focused (&self) -> bool {
|
|
todo!()
|
|
}
|
|
fn entered (&self) -> bool {
|
|
todo!()
|
|
}
|
|
fn keys (&self) -> &Buffer {
|
|
todo!()
|
|
}
|
|
fn phrase (&self) -> &Option<Arc<RwLock<Phrase>>> {
|
|
todo!()
|
|
}
|
|
fn buffer (&self) -> &BigBuffer {
|
|
todo!()
|
|
}
|
|
fn note_len (&self) -> usize {
|
|
todo!()
|
|
}
|
|
fn note_axis (&self) -> &RwLock<FixedAxis<usize>> {
|
|
todo!()
|
|
}
|
|
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>> {
|
|
todo!()
|
|
}
|
|
fn size (&self) -> &Measure<Tui> {
|
|
todo!()
|
|
}
|
|
fn now (&self) -> &Arc<Pulse> {
|
|
todo!()
|
|
}
|
|
}
|
|
|
|
/// 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> {
|
|
Split::up(
|
|
1,
|
|
widget(&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(self.focused() == ArrangerFocus::Arranger)))),
|
|
widget(&self.size),
|
|
widget(&format!("[{}] Arranger", if self.entered {
|
|
"■"
|
|
} else {
|
|
" "
|
|
}))
|
|
.fg(TuiTheme::title_fg(self.focused() == ArrangerFocus::Arranger))
|
|
.push_x(1),
|
|
),
|
|
Split::right(
|
|
self.splits[1],
|
|
widget(&PhrasesView(self)),
|
|
widget(&PhraseView(self)),
|
|
)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> {
|
|
let mut widths = vec![];
|
|
let mut total = 0;
|
|
for track in tracks.iter() {
|
|
let width = track.width;
|
|
widths.push((width, total));
|
|
total += width;
|
|
}
|
|
widths.push((0, total));
|
|
widths
|
|
}
|
|
|
|
pub fn arranger_content_vertical (
|
|
view: &ArrangerTui,
|
|
factor: usize
|
|
) -> impl Widget<Engine = Tui> + use<'_> {
|
|
let timebase = view.timebase();
|
|
let current = view.current();
|
|
let tracks = view.tracks();
|
|
let scenes = view.scenes();
|
|
let cols = track_widths(tracks);
|
|
let rows = ArrangerScene::ppqs(scenes, factor);
|
|
let bg = view.color;
|
|
let clip_bg = TuiTheme::border_bg();
|
|
let sep_fg = TuiTheme::separator_fg(false);
|
|
let header_h = 3u16;//5u16;
|
|
let scenes_w = 3 + ArrangerScene::longest_name(scenes) as u16; // x of 1st track
|
|
let arrangement = Layers::new(move |add|{
|
|
let rows: &[(usize, usize)] = rows.as_ref();
|
|
let cols: &[(usize, usize)] = cols.as_ref();
|
|
let any_size = |_|Ok(Some([0,0]));
|
|
// column separators
|
|
add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{
|
|
let style = Some(Style::default().fg(sep_fg));
|
|
Ok(for x in cols.iter().map(|col|col.1) {
|
|
let x = scenes_w + to.area().x() + x as u16;
|
|
for y in to.area().y()..to.area().y2() { to.blit(&"▎", x, y, style); }
|
|
})
|
|
}))?;
|
|
// row separators
|
|
add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{
|
|
Ok(for y in rows.iter().map(|row|row.1) {
|
|
let y = to.area().y() + (y / PPQ) as u16 + 1;
|
|
if y >= to.buffer.area.height { break }
|
|
for x in to.area().x()..to.area().x2().saturating_sub(2) {
|
|
if x < to.buffer.area.x && y < to.buffer.area.y {
|
|
let cell = to.buffer.get_mut(x, y);
|
|
cell.modifier = Modifier::UNDERLINED;
|
|
cell.underline_color = sep_fg;
|
|
}
|
|
}
|
|
})
|
|
}))?;
|
|
// track titles
|
|
let header = row!((track, w) in tracks.iter().zip(cols.iter().map(|col|col.0))=>{
|
|
// name and width of track
|
|
let name = track.name().read().unwrap();
|
|
let max_w = w.saturating_sub(1).min(name.len()).max(2);
|
|
let name = format!("▎{}", &name[0..max_w]);
|
|
let name = TuiStyle::bold(name, true);
|
|
// beats elapsed
|
|
let elapsed = if let Some((_, Some(phrase))) = track.phrase().as_ref() {
|
|
let length = phrase.read().unwrap().length;
|
|
let elapsed = track.pulses_since_start().unwrap();
|
|
let elapsed = timebase.format_beats_1_short(
|
|
(elapsed as usize % length) as f64
|
|
);
|
|
format!("▎+{elapsed:>}")
|
|
} else {
|
|
String::from("▎")
|
|
};
|
|
// beats until switchover
|
|
let until_next = track.next_phrase().as_ref().map(|(t, _)|{
|
|
let target = t.pulse.get();
|
|
let current = current.pulse.get();
|
|
if target > current {
|
|
let remaining = target - current;
|
|
format!("▎-{:>}", timebase.format_beats_0_short(remaining))
|
|
} else {
|
|
String::new()
|
|
}
|
|
}).unwrap_or(String::from("▎"));
|
|
// name of active MIDI input
|
|
let input = format!("▎>{}", track.midi_ins().get(0)
|
|
.map(|port|port.short_name())
|
|
.transpose()?
|
|
.unwrap_or("(none)".into()));
|
|
// name of active MIDI output
|
|
let output = format!("▎<{}", track.midi_outs().get(0)
|
|
.map(|port|port.short_name())
|
|
.transpose()?
|
|
.unwrap_or("(none)".into()));
|
|
col!(name, /*input, output,*/ until_next, elapsed)
|
|
.min_xy(w as u16, header_h)
|
|
.bg(track.color().rgb)
|
|
.push_x(scenes_w)
|
|
});
|
|
// tracks and scenes
|
|
let content = col!(
|
|
// scenes:
|
|
(scene, pulses) in scenes.iter().zip(rows.iter().map(|row|row.0)) => {
|
|
let height = 1.max((pulses / PPQ) as u16);
|
|
let playing = scene.is_playing(tracks);
|
|
Stack::right(move |add| {
|
|
// scene title:
|
|
add(&row!(
|
|
if playing { "▶ " } else { " " },
|
|
TuiStyle::bold(scene.name.read().unwrap().as_str(), true),
|
|
).fixed_xy(scenes_w, height).bg(scene.color.rgb))?;
|
|
// clip per track:
|
|
Ok(for (track, w) in cols.iter().map(|col|col.0).enumerate() {
|
|
add(&Layers::new(move |add|{
|
|
let mut bg = clip_bg;
|
|
match (tracks.get(track), scene.clips.get(track)) {
|
|
(Some(track), Some(Some(phrase))) => {
|
|
let name = &(phrase as &Arc<RwLock<Phrase>>).read().unwrap().name;
|
|
let name = format!("{}", name);
|
|
let max_w = name.len().min((w as usize).saturating_sub(2));
|
|
let color = phrase.read().unwrap().color;
|
|
add(&name.as_str()[0..max_w].push_x(1).fixed_x(w as u16))?;
|
|
bg = color.dark.rgb;
|
|
if let Some((_, Some(ref playing))) = track.phrase() {
|
|
if *playing.read().unwrap() == *phrase.read().unwrap() {
|
|
bg = color.light.rgb
|
|
}
|
|
};
|
|
},
|
|
_ => {}
|
|
};
|
|
add(&Background(bg))
|
|
}).fixed_xy(w as u16, height))?;
|
|
})
|
|
}).fixed_y(height)
|
|
}
|
|
).fixed_y((view.size.h() as u16).saturating_sub(header_h));
|
|
// full grid with header and footer
|
|
add(&col!(header, content))?;
|
|
// cursor
|
|
add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{
|
|
let area = to.area();
|
|
let focused = view.is_focused();
|
|
let selected = view.selected;
|
|
let get_track_area = |t: usize| [
|
|
scenes_w + area.x() + cols[t].1 as u16, area.y(),
|
|
cols[t].0 as u16, area.h(),
|
|
];
|
|
let get_scene_area = |s: usize| [
|
|
area.x(), header_h + area.y() + (rows[s].1 / PPQ) as u16,
|
|
area.w(), (rows[s].0 / PPQ) as u16
|
|
];
|
|
let get_clip_area = |t: usize, s: usize| [
|
|
scenes_w + area.x() + cols[t].1 as u16,
|
|
header_h + area.y() + (rows[s].1/PPQ) as u16,
|
|
cols[t].0 as u16,
|
|
(rows[s].0 / PPQ) 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 {
|
|
ArrangerSelection::Mix => area,
|
|
ArrangerSelection::Track(t) => {
|
|
track_area = Some(get_track_area(t));
|
|
area
|
|
},
|
|
ArrangerSelection::Scene(s) => {
|
|
scene_area = Some(get_scene_area(s));
|
|
area
|
|
},
|
|
ArrangerSelection::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
|
|
},
|
|
};
|
|
let bg = TuiTheme::border_bg();
|
|
if let Some([x, y, width, height]) = track_area {
|
|
to.fill_fg([x, y, 1, height], bg);
|
|
to.fill_fg([x + width, y, 1, height], bg);
|
|
}
|
|
if let Some([_, y, _, height]) = scene_area {
|
|
to.fill_ul([area.x(), y - 1, area.w(), 1], bg);
|
|
to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg);
|
|
}
|
|
Ok(if focused {
|
|
to.render_in(if let Some(clip_area) = clip_area { clip_area }
|
|
else if let Some(track_area) = track_area { track_area.clip_h(header_h) }
|
|
else if let Some(scene_area) = scene_area { scene_area.clip_w(scenes_w) }
|
|
else { area.clip_w(scenes_w).clip_h(header_h) }, &CORNERS)?
|
|
})
|
|
}))
|
|
}).bg(bg.rgb);
|
|
let color = TuiTheme::title_fg(view.is_focused());
|
|
let size = format!("{}x{}", view.size.w(), view.size.h());
|
|
let lower_right = TuiStyle::fg(size, color).pull_x(1).align_se().fill_xy();
|
|
lay!(arrangement, lower_right)
|
|
}
|
|
|
|
pub fn arranger_content_horizontal (
|
|
view: &ArrangerTui,
|
|
) -> impl Widget<Engine = Tui> + use<'_> {
|
|
let focused = view.is_focused();
|
|
let _tracks = view.tracks();
|
|
lay!(
|
|
focused.then_some(Background(TuiTheme::border_bg())),
|
|
row!(
|
|
// name
|
|
CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
|
todo!()
|
|
//let Self(tracks, selected) = self;
|
|
//let yellow = Some(Style::default().yellow().bold().not_dim());
|
|
//let white = Some(Style::default().white().bold().not_dim());
|
|
//let area = to.area();
|
|
//let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()];
|
|
//let offset = 0; // track scroll offset
|
|
//for y in 0..area.h() {
|
|
//if y == 0 {
|
|
//to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?;
|
|
//} else if y % 2 == 0 {
|
|
//let index = (y as usize - 2) / 2 + offset;
|
|
//if let Some(track) = tracks.get(index) {
|
|
//let selected = selected.track() == Some(index);
|
|
//let style = if selected { yellow } else { white };
|
|
//to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?;
|
|
//to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?;
|
|
//}
|
|
//}
|
|
//}
|
|
//Ok(Some(area))
|
|
}),
|
|
// monitor
|
|
CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
|
todo!()
|
|
//let Self(tracks) = self;
|
|
//let mut area = to.area();
|
|
//let on = Some(Style::default().not_dim().green().bold());
|
|
//let off = Some(DIM);
|
|
//area.x += 1;
|
|
//for y in 0..area.h() {
|
|
//if y == 0 {
|
|
////" MON ".blit(to.buffer, area.x, area.y + y, style2)?;
|
|
//} else if y % 2 == 0 {
|
|
//let index = (y as usize - 2) / 2;
|
|
//if let Some(track) = tracks.get(index) {
|
|
//let style = if track.monitoring { on } else { off };
|
|
//to.blit(&" MON ", area.x(), area.y() + y, style)?;
|
|
//} else {
|
|
//area.height = y;
|
|
//break
|
|
//}
|
|
//}
|
|
//}
|
|
//area.width = 4;
|
|
//Ok(Some(area))
|
|
}),
|
|
// record
|
|
CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
|
todo!()
|
|
//let Self(tracks) = self;
|
|
//let mut area = to.area();
|
|
//let on = Some(Style::default().not_dim().red().bold());
|
|
//let off = Some(Style::default().dim());
|
|
//area.x += 1;
|
|
//for y in 0..area.h() {
|
|
//if y == 0 {
|
|
////" REC ".blit(to.buffer, area.x, area.y + y, style2)?;
|
|
//} else if y % 2 == 0 {
|
|
//let index = (y as usize - 2) / 2;
|
|
//if let Some(track) = tracks.get(index) {
|
|
//let style = if track.recording { on } else { off };
|
|
//to.blit(&" REC ", area.x(), area.y() + y, style)?;
|
|
//} else {
|
|
//area.height = y;
|
|
//break
|
|
//}
|
|
//}
|
|
//}
|
|
//area.width = 4;
|
|
//Ok(Some(area))
|
|
}),
|
|
// overdub
|
|
CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
|
todo!()
|
|
//let Self(tracks) = self;
|
|
//let mut area = to.area();
|
|
//let on = Some(Style::default().not_dim().yellow().bold());
|
|
//let off = Some(Style::default().dim());
|
|
//area.x = area.x + 1;
|
|
//for y in 0..area.h() {
|
|
//if y == 0 {
|
|
////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?;
|
|
//} else if y % 2 == 0 {
|
|
//let index = (y as usize - 2) / 2;
|
|
//if let Some(track) = tracks.get(index) {
|
|
//to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub {
|
|
//on
|
|
//} else {
|
|
//off
|
|
//})?;
|
|
//} else {
|
|
//area.height = y;
|
|
//break
|
|
//}
|
|
//}
|
|
//}
|
|
//area.width = 4;
|
|
//Ok(Some(area))
|
|
}),
|
|
// erase
|
|
CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
|
todo!()
|
|
//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))
|
|
}),
|
|
// gain
|
|
CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
|
todo!()
|
|
//let Self(tracks) = self;
|
|
//let mut area = to.area();
|
|
//let off = Some(Style::default().dim());
|
|
//area.x = area.x() + 1;
|
|
//for y in 0..area.h() {
|
|
//if y == 0 {
|
|
////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?;
|
|
//} else if y % 2 == 0 {
|
|
//let index = (y as usize - 2) / 2;
|
|
//if let Some(_) = tracks.get(index) {
|
|
//to.blit(&" +0.0 ", area.x(), area.y() + y, off)?;
|
|
//} else {
|
|
//area.height = y;
|
|
//break
|
|
//}
|
|
//}
|
|
//}
|
|
//area.width = 7;
|
|
//Ok(Some(area))
|
|
}),
|
|
// scenes
|
|
CustomWidget::new(|_|{todo!()}, |to: &mut TuiOutput|{
|
|
let [x, y, _, height] = to.area();
|
|
let mut x2 = 0;
|
|
Ok(for (scene_index, scene) in view.scenes().iter().enumerate() {
|
|
let active_scene = view.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 = view.selected.track() == Some(i);
|
|
if let Some(clip) = clip {
|
|
let y2 = y + 2 + i as u16 * 2;
|
|
let label = format!("{}", clip.read().unwrap().name);
|
|
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;
|
|
})
|
|
}),
|
|
)
|
|
)
|
|
}
|
|
|
|
impl TransportViewState for ArrangerTui {
|
|
fn focus (&self) -> TransportFocus {
|
|
self.focus
|
|
}
|
|
fn is_focused (&self) -> bool {
|
|
self.focused
|
|
}
|
|
fn transport_state (&self) -> Option<TransportState> {
|
|
*self.playing().read().unwrap()
|
|
}
|
|
fn bpm_value (&self) -> f64 {
|
|
self.bpm().get()
|
|
}
|
|
fn sync_value (&self) -> f64 {
|
|
self.sync().get()
|
|
}
|
|
fn format_beat (&self) -> String {
|
|
self.current().format_beat()
|
|
}
|
|
fn format_msu (&self) -> String {
|
|
self.current().usec.format_msu()
|
|
}
|
|
}
|
|
|
|
impl PhrasesViewState for ArrangerTui {
|
|
fn focused (&self) -> bool {
|
|
todo!()
|
|
}
|
|
fn entered (&self) -> bool {
|
|
todo!()
|
|
}
|
|
fn phrases (&self) -> Vec<Arc<RwLock<Phrase>>> {
|
|
todo!()
|
|
}
|
|
fn phrase (&self) -> usize {
|
|
todo!()
|
|
}
|
|
fn mode (&self) -> Option<&PhrasesMode> {
|
|
&self.mode
|
|
}
|
|
}
|
|
|
|
impl PhraseViewState for ArrangerTui {
|
|
fn focused (&self) -> bool {
|
|
todo!()
|
|
}
|
|
fn entered (&self) -> bool {
|
|
todo!()
|
|
}
|
|
fn keys (&self) -> &Buffer {
|
|
todo!()
|
|
}
|
|
fn phrase (&self) -> &Option<Arc<RwLock<Phrase>>> {
|
|
todo!()
|
|
}
|
|
fn buffer (&self) -> &BigBuffer {
|
|
todo!()
|
|
}
|
|
fn note_len (&self) -> usize {
|
|
todo!()
|
|
}
|
|
fn note_axis (&self) -> &RwLock<FixedAxis<usize>> {
|
|
todo!()
|
|
}
|
|
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>> {
|
|
todo!()
|
|
}
|
|
fn size (&self) -> &Measure<Tui> {
|
|
todo!()
|
|
}
|
|
fn now (&self) -> &Arc<Pulse> {
|
|
todo!()
|
|
}
|
|
}
|
|
|
|
impl Widget for PhrasesTui {
|
|
type Engine = Tui;
|
|
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
|
PhrasesView(&self).layout(to)
|
|
}
|
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
|
PhrasesView(&self).render(to)
|
|
}
|
|
}
|
|
|
|
pub trait PhrasesViewState {
|
|
fn focused (&self) -> bool;
|
|
fn entered (&self) -> bool;
|
|
fn phrases (&self) -> Vec<Arc<RwLock<Phrase>>>;
|
|
fn phrase (&self) -> usize;
|
|
fn mode (&self) -> Option<&PhrasesMode>;
|
|
}
|
|
|
|
impl PhrasesViewState for PhrasesTui {
|
|
fn focused (&self) -> bool {
|
|
todo!()
|
|
}
|
|
fn entered (&self) -> bool {
|
|
todo!()
|
|
}
|
|
fn phrases (&self) -> Vec<Arc<RwLock<Phrase>>> {
|
|
todo!()
|
|
}
|
|
fn phrase (&self) -> usize {
|
|
todo!()
|
|
}
|
|
fn mode (&self) -> Option<&PhrasesMode> {
|
|
&self.mode
|
|
}
|
|
}
|
|
|
|
pub struct PhrasesView<'a, T: PhrasesViewState>(&'a T);
|
|
|
|
// 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.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(),
|
|
)
|
|
}
|
|
}
|
|
|
|
pub struct PhraseView<'a, T: PhraseViewState>(pub &'a T);
|
|
|
|
pub trait PhraseViewState: Send + Sync {
|
|
fn focused (&self) -> bool;
|
|
fn entered (&self) -> bool;
|
|
fn keys (&self) -> &Buffer;
|
|
fn phrase (&self) -> &Option<Arc<RwLock<Phrase>>>;
|
|
fn buffer (&self) -> &BigBuffer;
|
|
fn note_len (&self) -> usize;
|
|
fn note_axis (&self) -> &RwLock<FixedAxis<usize>>;
|
|
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>>;
|
|
fn size (&self) -> &Measure<Tui>;
|
|
fn now (&self) -> &Arc<Pulse>;
|
|
}
|
|
|
|
impl PhraseViewState for PhraseTui {
|
|
fn focused (&self) -> bool {
|
|
self.focused
|
|
}
|
|
fn entered (&self) -> bool {
|
|
self.entered
|
|
}
|
|
fn keys (&self) -> &Buffer {
|
|
&self.keys
|
|
}
|
|
fn phrase (&self) -> &Option<Arc<RwLock<Phrase>>> {
|
|
&self.phrase
|
|
}
|
|
fn buffer (&self) -> &BigBuffer {
|
|
&self.buffer
|
|
}
|
|
fn note_len (&self) -> usize {
|
|
self.note_len
|
|
}
|
|
fn note_axis (&self) -> &RwLock<FixedAxis<usize>> {
|
|
&self.note_axis
|
|
}
|
|
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>> {
|
|
&self.time_axis
|
|
}
|
|
fn size (&self) -> &Measure<Tui> {
|
|
&self.size
|
|
}
|
|
fn now (&self) -> &Arc<Pulse> {
|
|
&self.now
|
|
}
|
|
}
|
|
|
|
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.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(),
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Colors of piano keys
|
|
const KEY_COLORS: [(Color, Color);6] = [
|
|
(Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)),
|
|
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
|
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
|
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
|
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
|
|
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
|
|
];
|
|
|
|
pub(crate) fn keys_vert () -> Buffer {
|
|
let area = [0, 0, 5, 64];
|
|
let mut buffer = Buffer::empty(Rect {
|
|
x: area.x(), y: area.y(), width: area.w(), height: area.h()
|
|
});
|
|
buffer_update(&mut buffer, area, &|cell, x, y| {
|
|
let y = 63 - y;
|
|
match x {
|
|
0 => {
|
|
cell.set_char('▀');
|
|
let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize];
|
|
cell.set_fg(fg);
|
|
cell.set_bg(bg);
|
|
},
|
|
1 => {
|
|
cell.set_char('▀');
|
|
cell.set_fg(Color::White);
|
|
cell.set_bg(Color::White);
|
|
},
|
|
2 => if y % 6 == 0 {
|
|
cell.set_char('C');
|
|
},
|
|
3 => if y % 6 == 0 {
|
|
cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]);
|
|
},
|
|
_ => {}
|
|
}
|
|
});
|
|
buffer
|
|
}
|
|
|
|
const NTH_OCTAVE: [&'static str; 11] = [
|
|
"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8",
|
|
];
|
|
|
|
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(),
|
|
"]"
|
|
)),
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Displays and edits phrase length.
|
|
pub struct PhraseLength {
|
|
/// Pulses per beat (quaver)
|
|
pub ppq: usize,
|
|
/// Beats per bar
|
|
pub bpb: usize,
|
|
/// Length of phrase in pulses
|
|
pub pulses: usize,
|
|
/// Selected subdivision
|
|
pub focus: Option<PhraseLengthFocus>,
|
|
}
|
|
|
|
impl PhraseLength {
|
|
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
|
|
Self { ppq: PPQ, bpb: 4, pulses, focus }
|
|
}
|
|
pub fn bars (&self) -> usize {
|
|
self.pulses / (self.bpb * self.ppq)
|
|
}
|
|
pub fn beats (&self) -> usize {
|
|
(self.pulses % (self.bpb * self.ppq)) / self.ppq
|
|
}
|
|
pub fn ticks (&self) -> usize {
|
|
self.pulses % self.ppq
|
|
}
|
|
pub fn bars_string (&self) -> String {
|
|
format!("{}", self.bars())
|
|
}
|
|
pub fn beats_string (&self) -> String {
|
|
format!("{}", self.beats())
|
|
}
|
|
pub fn ticks_string (&self) -> String {
|
|
format!("{:>02}", self.ticks())
|
|
}
|
|
}
|
|
|
|
impl Widget for PhraseTui {
|
|
type Engine = Tui;
|
|
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
|
PhraseView(&self, Default::default()).layout(to)
|
|
}
|
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
|
PhraseView(&self, Default::default()).render(to)
|
|
}
|
|
}
|