tek/src/conf/view.rs
2026-10-05 02:26:01 +03:00

1417 lines
54 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use crate::*;
impl_debug!(<S: Screen, T> View<S, T>: |self, w| { write!(w, "View({})", self.source) });
impl_display!(<S: Screen, T> View<S, T>: |self, w| { write!(w, "View({})", self.source) });
impl_default!(ClockView: {
let mut beat = String::with_capacity(16);
let _ = write!(beat, "{}", Self::BEAT_EMPTY);
let mut time = String::with_capacity(16);
let _ = write!(time, "{}", Self::TIME_EMPTY);
let mut bpm = String::with_capacity(16);
let _ = write!(bpm, "{}", Self::BPM_EMPTY);
Self {
beat: Memo::new(None, beat),
time: Memo::new(None, time),
bpm: Memo::new(None, bpm),
sr: Memo::new(None, String::with_capacity(16)),
buf: Memo::new(None, String::with_capacity(16)),
lat: Memo::new(None, String::with_capacity(16)),
}
});
/// Custom view definition is a boxed closure emitting a [Draw]able from state `S`.
pub struct View<S: Screen, T> {
pub source: Arc<str>,
pub render: Arc<Box<dyn Fn(&T, &mut S)->Drawn<S::Unit> + Send + Sync>>
}
/// The [Draw] implementation for [App] handles the loaded view,
/// which is defined in terms of [dizzle] DSL.
///
/// If there is an error, the error is displayed. FIXME: overlay it
/// Then, every top-level form of the DSL description is rendered.
impl Draw<Tui> for App {
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
#[cfg(feature = "prof2")] profiling::scope!("App::draw");
self.perf.cycle(&mut |_|{
draw_error(to, self.error.as_ref())?;
draw_mode_stack(to, &self)?;
view_debug(&self.perf, &self.size).draw(to)?;
#[cfg(feature = "prof2")] profiling::finish_frame!();
Ok(Some(to.area().into()))
})
}
}
pub fn draw_error (to: &mut Tui, error: &RwLock<Option<Arc<str>>>) -> Usually<()> {
if let Some(e) = error.read().as_ref() {
e.as_ref().align_c().draw(to)?;
}
Ok(())
}
pub fn view_debug (perf: &PerfModel, size: &Sizer) -> impl Draw<Tui> {
Split::Above.bar(0,
Split::West.bar(16,
format!("{}/{} {} ",
perf.used.load(Relaxed),
perf.window.load(Relaxed),
perf.clock.raw() / 10000000).align_e(),
format!("{}x{} ",
size.0.load(Relaxed),
size.1.load(Relaxed)).align_w(),
).align_se(),
()
//Split::East.bar(16,
//format!("{}", self.modes.read()[0]),
//format!("{}", self.modes.read()[0])
//).align_sw()
)
}
/// ```
/// let x = "";
/// let _ = tek::view_status(None, x.as_ref(), x.as_ref(), x.as_ref());
/// let _ = tek::view_status(Some("".into()), x.as_ref(), x.as_ref(), x.as_ref());
/// ```
pub fn view_status <'a> (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw<Tui> {
let theme = ItemTheme::G[96];
let sr = field_h(theme, "SR", sr);
let buf = field_h(theme, "Buf", buf);
let lat = field_h(theme, "Lat", lat);
bg(Black, east!(above(
sel.map(|sel|field_h(theme, "Selected", sel)).align_w().full_wh(),
east!(sr, buf, lat).align_e().full_wh(),
)))
}
pub fn draw_status_bar (to: &mut Tui, state: &App) -> Drawn<u16> {
bar(Split::South, 1u16,
dim(true, "Mode"),
bold(true, state.mode_stack().read().get(0))
).push_x(1).draw(to)
}
pub fn draw_mode_stack (to: &mut Tui, state: &App) -> Usually<()> {
for name in state.mode_stack().read().iter() {
let mut error = false;
if let Some(mode) = state.modes_get(name).as_ref().map(Arc::clone) {
for (index, view) in mode.view.iter().enumerate() {
match (view.render)(state, to) {
Ok(None) => {},
Ok(Some(XYWH(.., w, h))) => {
state.size.0.store(w as usize, Relaxed);
state.size.1.store(h as usize, Relaxed);
},
Err(e) => {
*state.error.try_write().unwrap() = Some(format!(
"Mode: {:?}\n\nLayer: #{index}\n\nError: {e}\n\nSource:\n{}",
&mode.name,
&view.source
).into());
error = true;
break;
}
}
}
if error {
break
} else {
*state.error.try_write().unwrap() = None;
}
}
}
Ok(())
}
pub fn view_menu (app: &App) -> impl Draw<Tui> {
if let Dialog::Menu(selected, items) = &app.dialog {
Some(iter_south(move||items.iter().enumerate().map(move|(index, MenuItem(item, _))|{
let f = if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) };
let b = if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) };
fg_bg(f, b, item).exact_h(2).align_x()
})).exact_w(20).align_x())
} else {
None
}
}
pub fn view_history_list (app: &App) -> impl Draw<Tui> {
let border = Outer(true, Style::default().fg(Color::Rgb(0, 192, 0)));
let background = xy_repeat(" ");
below(border, above(background, draw(|to: &mut Tui|{
let area = to.area();
let XYWH(x0, y0, w, h) = area;
let mut y = 0;
for (done, undo) in app.history.iter().rev() {
to.clip(XYWH(x0 + 1, y0 + y, w, 1), &|to|format!("{done:?}").draw(to))?;
y += 1;
if y >= h {
break
}
}
Ok(Some(area))
})))
}
pub fn view_config (app: &App) -> impl Draw<Tui> {
macro_rules! next_line { ($h:ident, $y:ident, $n:expr) => { $y += $n; if $y > $h { break } }; }
let border = Outer(true, Style::default().fg(Color::Rgb(0, 192, 0)));
let background = xy_repeat(" ");
below(border, above(background, draw(|to: &mut Tui|{
let area = to.area();
let XYWH(x0, y0, w, h) = area;
let mut y = 0;
for key in app.stack.read().iter().rev() {
if let Some(mode) = app.modes_get(key) {
to.clip(XYWH(x0 + 1, y0 + y, w, 1), &|to|Split::East.bar(16,
key,
mode.info.get(0)).draw(to))?;
next_line!(h, y, 1);
for (ctrl, data) in mode.axis.iter() {
to.clip(XYWH(x0 + 3, y0 + y, w, 1), &|to|Split::East.bar(4,
format!("{ctrl:?}"),
format!("{data:?}")).draw(to))?;
next_line!(h, y, 1);
}
for bind in mode.keys.iter() {
to.clip(XYWH(x0 + 3, y0 + y, w, 1), &|to|bind.draw(to))?;
next_line!(h, y, 1);
if let Some(bind) = app.binds_find(bind) {
for (key, binding) in bind.0.iter() {
to.clip(XYWH(x0 + 5, y0 + y, 40, 1), &|to|Split::East.bar(40,
format!("{}", key), //binding.description.get(0)
"").draw(to))?;
next_line!(h, y, 1);
}
}
}
next_line!(h, y, 1);
}
}
Ok(Some(area))
})))
}
/// ```
/// let _ = tek::button_play_pause(true, true);
/// let _ = tek::button_play_pause(true, false);
/// let _ = tek::button_play_pause(false, true);
/// let _ = tek::button_play_pause(false, false);
/// ```
pub fn button_play_pause <'a> (playing: bool, compact: bool) -> impl Draw<Tui> {
bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
either(compact,
draw(move|to: &mut Tui|either(playing,
fg(Rgb(0, 255, 0), " PLAYING "),
fg(Rgb(255, 128, 0), " STOPPED "),
).exact_w(9).draw(to)),
draw(move|to: &mut Tui|either(playing,
fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",)),
).exact_w(5).draw(to)),
)
)
}
impl Draw<Tui> for PianoHorizontal {
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
south(
east(
format!("{}x{}", self.size.w(), self.size.h()).exact_w(5),
view_piano_h_timeline(self)
),
east(
view_piano_h_keys(self),
self.size.of(below(
view_piano_h_notes(self).full_wh(),
view_piano_h_cursor(self).full_wh()
))
),
).draw(to)
}
}
pub fn view_piano_h_notes (state: &PianoHorizontal) -> impl Draw<Tui> {
let time_start = state.get_time_start();
let note_lo = state.get_note_lo();
let note_hi = state.get_note_hi();
let buffer = state.buffer.clone();
draw(move|to: &mut Tui|{
let xywh = to.area().into();
to.buffer().map(|to|{
let XYWH(x0, y0, w, _h) = xywh;
let source = buffer.read();
//if h as usize != note_axis {
//panic!("area height mismatch: {h} <> {note_axis}");
//}
for (area_x, screen_x) in (x0..x0+w).enumerate() {
for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) {
let source_x = time_start + area_x;
let source_y = note_hi - area_y;
// TODO: enable loop rollover:
//let source_x = (time_start + area_x) % source.width.max(1);
//let source_y = (note_hi - area_y) % source.height.max(1);
let is_in_x = source_x < source.width;
let is_in_y = source_y < source.height;
if is_in_x && is_in_y {
if let Some(source_cell) = source.get(source_x, source_y) {
if let Some(cell) = to.cell_mut(
ratatui::prelude::Position::from((screen_x, screen_y))
) {
*cell = source_cell.clone();
}
}
}
}
}
});
Ok(Some(xywh))
})
}
pub fn view_piano_h_cursor (state: &PianoHorizontal) -> impl Draw<Tui> {
let note_hi = state.get_note_hi();
let note_lo = state.get_note_lo();
let note_pos = state.get_note_pos();
let note_len = state.get_note_len();
let time_pos = state.get_time_pos();
let time_start = state.get_time_start();
let time_zoom = state.get_time_zoom();
let style = Some(Style::default().fg(state.color.lightest.term));
draw(move|to: &mut Tui|{
let xywh = to.area().into();
let XYWH(x0, y0, w, _h) = xywh;
for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
if note == note_pos {
for x in 0..w {
let screen_x = x0 + x;
let time_1 = time_start + x as usize * time_zoom;
let time_2 = time_1 + time_zoom;
if time_1 <= time_pos && time_pos < time_2 {
to.blit(&"█", screen_x, screen_y, style);
let tail = note_len as u16 / time_zoom as u16;
for x_tail in (screen_x + 1)..(screen_x + tail) {
to.blit(&"▂", x_tail, screen_y, style);
}
break
}
}
break
}
}
Ok(Some(xywh))
})
}
pub fn view_piano_h_keys (state: &PianoHorizontal) -> impl Draw<Tui> {
let color = state.color;
let note_lo = state.get_note_lo();
let note_hi = state.get_note_hi();
let note_pos = state.get_note_pos();
let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0)));
let off_style = Some(Style::default().fg(g(255)));
let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.term).bold());
draw(move|to: &mut Tui|{
let xywh = to.area().into();
let XYWH(x, y0, _w, _h) = xywh;
for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
to.blit(&to_key(note), x, screen_y, key_style);
if note > 127 {
continue
}
if note == note_pos {
to.blit(&format!("{:<5}", note_pitch_to_name(note)), x, screen_y, on_style)
} else {
to.blit(&note_pitch_to_name(note), x, screen_y, off_style)
};
}
Ok(Some(xywh))
}).exact_w(state.keys_width).full_h()
}
pub fn view_piano_h_timeline (state: &PianoHorizontal) -> impl Draw<Tui> + '_ {
draw(move|to: &mut Tui|{
let xywh = to.area().into();
let XYWH(x, y, w, _h) = xywh;
let style = Some(Style::default().dim());
let length = state.clip.read().as_ref().map(|p|p.read().length).unwrap_or(1);
for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) {
let t = area_x as usize * state.time_zoom().load(Relaxed);
if t < length {
to.blit(&"|", screen_x, y, style);
}
}
Ok(Some(xywh))
}).exact_h(1).full_w()
}
impl Track {
pub fn per <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: &'a (impl Fn(usize, &'a Track)->T + Send + Sync + 'a)
) -> impl Draw<Tui> {
iter_east(move||tracks().map(|(index, track, x1, x2): (usize, &Track, usize, usize)|{
fg_bg(
track.color.lightest.term,
track.color.base.term,
callback(index, track)
).exact_w((x2 - x1) as u16)
}))
}
}
impl OctaveVertical {
pub fn tui (&self) -> impl Draw<Tui> {
east!(
fg_bg(self.color(0), self.color(1), "▙"),
fg_bg(self.color(2), self.color(3), "▙"),
fg_bg(self.color(4), self.color(5), "▌"),
fg_bg(self.color(6), self.color(7), "▟"),
fg_bg(self.color(8), self.color(9), "▟"),
fg_bg(self.color(10), self.color(11), "▟"),
)
}
}
impl Draw<Tui> for MidiEditor {
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
self.autoscroll();
/*self.autozoom();*/
self.size.of(&self.mode).draw(to)
}
}
impl<T> TracksView for T where T:
HasTracks + HasScenes + ScenesView + HasMidiIns + HasMidiOuts + HasTrack
{}
fn iter_tracks_with_sizes <'t, T> (state: &'t T) -> impl TracksSizes<'t> where
T: HasTracks + HasEditor + HasSelection + HasClipsSize + ?Sized
{
let _editor_width = state.editor().as_ref().map(|e|e.size.w());
let _active_track = state.selection().track();
let mut x = 0;
let w = state.clips_size().w() as usize;
state.tracks().iter().enumerate().map_while(move |(index, track)|{
let width = track.width.max(8);
if x + width < w {
let data = (index, track, x, x + width);
x += width;
Some(data)
} else {
None
}
})
}
pub trait TracksView:
HasTracks + HasScenes + ScenesView + HasMidiIns + HasMidiOuts + HasTrack
{
/// Iterate over tracks with their corresponding sizes.
fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
iter_tracks_with_sizes(self)
}
fn view_track_column <'t, S, D, F> (&'t self, f: F) -> impl Draw<S> where
S: Screen,
D: Draw<S> + 't,
F: Fn(usize, &'t Track, &Selection, Option<&MidiEditor>)->D + Copy + 't
{
let select = self.selection();
let editor = self.editor().as_ref();
iter_east(move||self.tracks_with_sizes()
.map(move|(i, t, _x1, _x2)|f(i, t, select, editor)))
}
fn view_track_names_cells (&self) -> impl Draw<Tui> {
self.view_track_column(|i, t, select, editor|{
let b = if select.track() == Some(i) { t.color.light } else { t.color.base }.term;
bg(b, south(east!(
"·t", i, " ", fg(Rgb(255, 255, 255), bold(true, &t.name))
).align_nw().full_w(), "")).exact_wh(
view_track_w(select, i, t, editor),
2
)
})
}
fn view_track_output_cells (&self) -> impl Draw<Tui> {
self.view_track_column(|i, t, select, editor|{
let f = Rgb(255, 255, 255);
let b = t.color.dark.term;
iter_south(move||t.sequencer.midi_outs.iter().map(move|port: &MidiOutput|{
fg(f, bg(b, east!("·o", i, " ", port.port_name()).full_w().align_w()).exact_h(1))
})).full_h().align_nw().exact_w(
view_track_w(select, i, t, editor)
)
})
}
fn view_track_device_cells (&self, height: u16) -> impl Draw<Tui> + '_ {
self.view_track_column(move|i, t, select, editor|bg(
t.color.dark.term,
iter_south(move||(0..height).map(|_|fg_bg(
ItemTheme::G[32].lightest.term,
ItemTheme::G[32].dark.term,
" · --".align_nw()
).exact_wh(t.width as u16, 2))).align_nw()
).exact_wh(
view_track_w(select, i, t, editor),
height + 1
))
}
fn view_track_input_cells (&self, height: u16) -> impl Draw<Tui> {
self.view_track_column(move|i, t, select, editor|south(
bg(t.color.base.term,
east!(
either(t.sequencer.monitoring, fg(Green, "●mon "), "·mon "),
either(t.sequencer.recording, fg(Red, "●rec "), "·rec "),
either(t.sequencer.overdub, fg(Yellow, "●dub "), "·dub "),
).align_w().full_w()),
iter_south(move||t.sequencer.midi_ins.iter().map(move|port|fg_bg(
Rgb(255, 255, 255), t.color.dark.term, east!(
"·i", i, " ", port.port_name()
).align_w().full_w()
)))).align_nw().exact_wh(view_track_w(select, i, t, editor), height + 1)
).align_w()
}
fn view_input_cells (&self) -> impl Draw<Tui> {
draw(move|to: &mut Tui|{
for (_index, track, x1, _x2) in self.tracks_with_sizes() {
let _ = south(
bg(track.color.dark.term, east!(
either(track.sequencer.monitoring, fg(Green, "mon "), "mon "),
either(track.sequencer.recording, fg(Red, "rec "), "rec "),
either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "),
).exact_w(track.width as u16)).align_w().push_x(x1 as u16),
draw(move |to: &mut Tui|{
for (index, port) in self.midi_ins().as_slice().iter().enumerate() {
let _ = east(
east(
" ● ",
bold(true, fg(Rgb(255,255,255), port.port_name()))
).align_w().exact_w(20),
west(
().exact_w(4),
bg(track.color.darker.term, east!(
either(track.sequencer.monitoring, fg(Green, " ● "), " · "),
either(track.sequencer.recording, fg(Red, " ● "), " · "),
either(track.sequencer.overdub, fg(Yellow, " ● "), " · "),
).exact_w(track.width as u16).align_w())
)
).push_x(index as u16 * 10).exact_h(1).draw(to)?;
}
Ok(None)
})
).draw(to)?;
}
Ok(Some(to.area()))
})
}
fn tracks_devices_height (&self) -> u16 { self.tracks().iter().fold(2, |h, t|h.max(t.devices.len() * 2)) as u16 }
fn tracks_inputs_height (&self) -> u16 { self.tracks().iter().fold(0, |h, t|h.max(t.sequencer.midi_ins.len())) as u16 }
fn outputs_height (&self) -> u16 { self.midi_outs().iter().fold(1, |h, o|h + o.connections.len()) as u16 }
fn view_track_add (&self) -> impl Draw<Tui> { button_2("T", "+", false) }
fn view_scene_add (&self) -> impl Draw<Tui> { button_2("S", "+", false) }
fn view_track_output_add (&self) -> impl Draw<Tui> { button_2("O", "+", false) }
fn view_track_input_add (&self) -> impl Draw<Tui> { button_2("I", "+", false) }
fn view_track_device_add (&self) -> impl Draw<Tui> { button_2("D", "+", false) }
fn view_track_names_add (&self) -> impl Draw<Tui> { south(self.view_track_add(), self.view_scene_add()) }
fn view_track_count (&self) -> impl Draw<Tui> { view_n_of_m("t", "rack", self.selection().track(), self.tracks().len()); }
fn view_scene_count (&self) -> impl Draw<Tui> { view_n_of_m("s", "cene", self.selection().scene(), self.scenes().len()); }
fn view_track_input_count (&self) -> impl Draw<Tui> { button_2("i", "nput", false) }
fn view_track_output_count (&self) -> impl Draw<Tui> { button_3("o", "utput", 0, false) }
fn view_track_names_count (&self) -> impl Draw<Tui> { south(self.view_track_count(), self.view_scene_count()) }
fn view_track_device_count (&self) -> impl Draw<Tui> { button_3("d", "evice", self.track().map(|t|t.devices.len()).unwrap_or(0), false).min_h(2) }
/// Draw outputs per track
fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw<Tui> {
view_track_row_section(theme, self.view_track_output_count(), self.view_track_output_add(),
self.view_track_output_cells())
}
/// Draw inputs per track
fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw<Tui> {
view_track_row_section(theme, self.view_track_input_count(), self.view_track_input_add(),
self.view_track_input_cells(self.tracks_inputs_height()))
}
/// Draw devices of track
fn view_track_devices (&self, theme: ItemTheme) -> impl Draw<Tui> {
view_track_row_section(theme, self.view_track_device_count(), self.view_track_device_add(),
self.view_track_device_cells(self.tracks_devices_height()))
}
/// Draw name of each track
fn view_track_names (&self, theme: ItemTheme) -> impl Draw<Tui> {
view_track_row_section(theme, self.view_track_names_count(), self.view_track_names_add(),
self.view_track_names_cells())
}
fn view_inputs (&self) -> impl Draw<Tui> + '_ {
let title_1 = button_3("i", "nput ", self.midi_ins().len(), false).align_w().exact_wh(20, 1);
let title_2 = self.view_track_input_add().exact_wh(4, 1);
east(title_1, west(title_2, self.view_input_cells()))
}
fn view_outputs (&self, theme: ItemTheme) -> impl Draw<Tui> {
let height = self.outputs_height();
let list = self.view_outputs_list(height);
view_track_row_section(theme, list, self.view_track_output_add(),
bg(theme.darker.term, self.view_outputs_grid().align_w().full_w())).exact_h(height)
}
fn view_outputs_list (&self, height: u16) -> impl Draw<Tui> {
south(
button_3("o", "utput", self.midi_outs().len(), false).align_w().full_w().exact_h(1),
bg(Color::Rgb(0, 0, 0), draw(|to: &mut Tui|{
for (_index, port) in self.midi_outs().iter().enumerate() {
east(
east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name())))
.align_w(),
east!(port.port().get_connections().len(), "/", port.connections.len(), " ")
.align_e().full_w().exact_h(1)).full_w().draw(to)?;
for (index, conn) in port.connections.iter().enumerate() {
east!(" c", index, conn.info()).align_w().full_w().exact_h(1).draw(to)?;
}
}
todo!();
})).align_nw().full_wh().exact_h(height.minus(1)).min_h(1)
)
}
fn view_outputs_grid (&self) -> impl Draw<Tui> {
draw(|to: &mut Tui|{
for (index, track, _x1, _x2) in self.tracks_with_sizes() {
let _ = draw(|to: &mut Tui|{
east(either(true, fg(Green, "play "), "play "),
either(false, fg(Yellow, "solo "), "solo ")).align_w().exact_h(1).draw(to)?;
for (_index, port) in self.midi_outs().iter().enumerate() {
east(either(true, fg(Green, " ● "), " · "),
either(false, fg(Yellow, " ● "), " · ")).align_w().exact_h(1).draw(to)?;
for (_index, _conn) in port.connections.iter().enumerate() {
"".full_w().exact_h(1).draw(to)?;
}
}
todo!()
}).exact_w(track_width(index, track)).draw(to)?;
}
todo!()
})
}
}
fn view_n_of_m (key: &str, name: &str, index: Option<usize>, count: usize) -> impl Draw<Tui> {
button_3(key, name, east(index, count), false)
}
pub trait ScenesView: HasScenes + HasEditor + HasSelection + HasClipsSize + Send + Sync {
fn h_scenes (&self) -> u16;
fn w_side (&self) -> u16;
fn w_mid (&self) -> u16;
fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> {
iter_scenes_with_sizes(self)
}
fn view_scenes_names (&self) -> impl Draw<Tui> where Self: Sized {
let select = self.selection();
let editor = self.editor();
let editing = self.is_editing();
iter_south(move||self.scenes_with_sizes().map(move|(index, scene, ..)|{
view_scene_name(select, editor.as_ref(), index, scene, editing)
}))
}
}
fn iter_scenes_with_sizes <'s, T> (state: &'s T) -> impl ScenesSizes<'s> where T:
HasScenes + HasEditor + HasSelection + HasClipsSize + ?Sized
{
let mut y = 0;
let skip = state.scenes_scroll();
let selected = state.selection();
let editor = state.editor();
let clips_size = state.clips_size();
state.scenes().iter().enumerate().skip(skip).map_while(move|(s, scene)|{
let height = if selected.scene() == Some(s)
&& let Some(editor) = editor
{
editor.size.h() as usize
} else {
Scene::DEFAULT_HEIGHT
};
if y + height <= clips_size.h() as usize {
let data = (s, scene, y, y + height);
y += height;
Some(data)
} else {
None
}
})
}
pub fn view_modes <'a, S: HasModeStack> (state: &'a S) -> impl Draw<Tui> + 'a {
draw(move|to: &mut Tui|{
let XYWH(x, y, w, h) = to.area();
let mut x_next = x;
let mut w_used = 0u16;
for item in state.mode_stack().read().iter() {
let area = XYWH(x_next, y, w.minus(w_used), h);
if let Some(used) = to.draw(area, item)? {
let used_w = used.w() + 1;
x_next = x_next.plus(used_w);
w_used = w_used.plus(used_w);
if w_used >= w {
break;
}
}
}
Ok(Some(XYWH(x, y, w_used, h)))
})
}
pub fn view_track_w (
select: &Selection, track_index: usize, track: &Track, editor: Option<&MidiEditor>
) -> u16 {
if select.track() == Some(track_index) && let Some(editor) = editor {
(editor.size.w() as usize).max(20).max(track.width) as u16
} else {
track.width as u16
}
}
pub(crate) fn track_width (_index: usize, track: &Track) -> u16 {
track.width as u16
}
pub fn draw_sample (
to: &mut Tui, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
) -> Usually<usize> {
let style = if focus { Style::default().green() } else { Style::default() };
if focus {
to.blit(&"🬴", x+1, y, Some(style.bold()));
}
let label1 = format!("{:3} {:12}",
note.map(|n|n.to_string()).unwrap_or(String::default()),
sample.name);
let label2 = format!("{:>6} {:>6} +0.0",
sample.start,
sample.end);
to.blit(&label1, x+2, y, Some(style.bold()));
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
Ok(label1.len() + label2.len() + 4)
}
pub fn draw_sample_list_item (sample: &Option<Arc<RwLock<Sample>>>) -> String {
if let Some(sample) = sample {
let sample = sample.try_read().unwrap();
format!("{:8}", sample.name)
//format!("{:8} {:3} {:6}-{:6}/{:6}",
//sample.name,
//sample.gain,
//sample.start,
//sample.end,
//sample.channels[0].len()
//)
} else {
String::from("........")
}
}
pub fn draw_sample_viewer (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
let min_db = -64.0;
draw(move|to: &mut Tui|{
let xywh = to.area().into();
let XYWH(x, y, width, height) = xywh;
let area = Rect { x, y, width, height };
if let Some(sample) = &sample {
let sample = sample.try_read().unwrap();
let start = sample.start as f64;
let end = sample.end as f64;
let length = end - start;
let step = length / width as f64;
let mut t = start;
let mut lines = vec![];
while t < end {
let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)];
let total: f32 = chunk.iter().map(|x|x.abs()).sum();
let count = chunk.len() as f32;
let meter = 10. * (total / count).log10();
let x = t as f64;
let y = meter as f64;
lines.push(Line::new(x, min_db, x, y, Color::Green));
t += step / 2.;
}
to.buffer().map(|buf|{
Canvas::default()
.x_bounds([sample.start as f64, sample.end as f64])
.y_bounds([min_db, 0.])
.paint(|ctx| {
for line in lines.iter() {
ctx.draw(line);
}
//FIXME: proportions
//let text = "press record to finish sampling";
//ctx.print(
//(width - text.len() as u16) as f64 / 2.0,
//height as f64 / 2.0,
//text.red()
//);
})
.render(area, buf);
});
} else {
to.buffer().map(|buf|{
Canvas::default()
.x_bounds([0.0, width as f64])
.y_bounds([0.0, height as f64])
.paint(|_ctx| {
//let text = "press record to begin sampling";
//ctx.print(
//(width - text.len() as u16) as f64 / 2.0,
//height as f64 / 2.0,
//text.red()
//);
})
.render(area, buf);
});
}
Ok(Some(xywh))
})
}
pub fn draw_meters (meters: &[f32]) -> impl Draw<Tui> + use<'_> {
bg(Black, iter_east_fixed(1, ||meters.iter(), |value, _index|{
RmsMeter(*value).full_h()
}).exact_w(2))
}
#[cfg(feature = "lv2")]
pub fn draw_lv2_header (state: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) {
let style = Style::default().gray();
let label1 = format!(" {}", state.name);
to.blit(&label1, x + 1, y, Some(style.white().bold()));
if let Some(ref path) = state.path {
let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]);
to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
}
//Ok(Rect { x, y, width: w, height: 1 })
}
pub fn view_device <'a> (state: &'a App) -> impl Draw<Tui> {
let selected = state.dialog.device_kind().unwrap();
south(
bold(true, "Add device"),
iter_south(move||device_kinds().iter().enumerate().map(move|(i, _label)|{
let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) };
let l = if i == selected { "[ " } else { " " };
let r = if i == selected { " ]" } else { " " };
bg(b, east(l, west(r, "FIXME device name"))).full_w()
})))
}
pub fn view_track_row_section <'a> (
_theme: ItemTheme,
button: impl Draw<Tui>,
button_add: impl Draw<Tui>,
content: impl Draw<Tui>,
) -> impl Draw<Tui> {
Split::West.bar(4u16, button_add.align_ne().full_wh(),
Split::East.bar(20u16, button.full_wh().align_nw(),
content.align_c().full_wh()))
}
pub fn view_track_header <'a> (theme: ItemTheme, content: impl Draw<Tui>)
-> impl Draw<Tui>
{
bg(theme.darker.term, content.align_e().full_w()).exact_w(12)
}
pub fn view_scene_name <'a> (
select: &Selection,
editor: Option<&'a MidiEditor>,
index: usize,
scene: &Scene,
editing: bool
) -> impl Draw<Tui> {
let h = if select.scene() == Some(index) && let Some(editor) = editor {
editor.size.h() as u16
} else {
Scene::DEFAULT_HEIGHT as u16
};
let a = east!("·s", index, " ", fg(g(255), bold(true, &scene.name))).align_nw().full_w();
let s = select.scene() == Some(index) && editing;
let b = when(s, south(editor.map(view_clip_info), editor.map(view_edit_info)).full_wh().align_se());
let c = if select.scene() == Some(index) {
scene.color.light.term
} else {
scene.color.base.term
};
bg(c, above(a, b.push_y(1)).align_nw().exact_h(h))
}
/// Draw clips per scene
pub fn view_scenes_clips <S: TracksView + ScenesView> (state: &S) -> impl Draw<Tui> {
let select = state.selection();
let editor = state.editor();
let editing = state.is_editing();
let sized = move|it|state.with_clips_size(true, it).align_c();
let tracks = move|it|iter_east(move||state.tracks_with_sizes().map(it));
let scenes = move|it|iter_south(move||state.scenes_with_sizes().map(it));
sized(tracks(move|(track_index, track, _, _)|{
let w = view_track_w(select, track_index, track, editor.as_ref()) as u16;
scenes(move|(scene_index, scene, _, _)|{
let (name, theme): (Arc<str>, ItemTheme) = view_scene_name_theme(scene, track_index);
let f = theme.lightest.term;
let (b, o) = view_scene_bg(theme, select, track_index, scene_index);
let is_sel = view_scene_sel(select, track_index, scene_index, editing);
let border = Outer(true, Style::default().fg(o)).full_wh();
let title = below(fg_bg(o, b, "".full_wh()),
fg_bg(f, b, bold(true, name)).align_nw().full_wh());
above(border, above(title, when(is_sel, editor).full_wh()).full_wh())
.exact_wh(w, view_scene_y(select, scene_index, editor.as_ref()),)
}).exact_w(w)
}))
}
pub fn view_scene_bg (
theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize
) -> (Color, Color) {
let mut outline = theme.base.term;
(if select.track() == Some(track_index) && select.scene() == Some(scene_index) {
outline = theme.lighter.term;
theme.light.term
} else if select.track() == Some(track_index) || select.scene() == Some(scene_index) {
outline = theme.darkest.term;
theme.base.term
} else {
theme.dark.term
}, outline)
}
pub fn view_scene_sel (
select: &Selection, track_index: usize, scene_index: usize, editing: bool
) -> bool {
editing && select.track() == Some(track_index) && select.scene() == Some(scene_index)
}
pub fn view_scene_y (
select: &Selection, scene_index: usize, editor: Option<&MidiEditor>
) -> u16 {
if select.scene() == Some(scene_index) && let Some(editor) = editor {
editor.size.h().max(13)
} else {
Scene::DEFAULT_HEIGHT as u16
}
}
pub fn view_clip_info (editor: &MidiEditor) -> impl Draw<Tui> + '_ {
let (
_color, name, length, looped
) = if let Some(clip) = editor.mode.clip.read().as_ref().map(|p|p.read()) {
(clip.color, clip.name.clone(), clip.length, clip.looped)
} else {
(ItemTheme::G[64], String::new().into(), 0, false)
};
south!(
south(
button_2("f", "name ", false),
fg_bg(Rgb(255, 255, 255), Rgb(16, 16, 16), format!("{name} ").origin_e().pad_w(1).full_w())
).origin_w().full_w(),
east(
button_2("l", "ength ", false),
fg(Rgb(255, 255, 255), format!("{length} ")).origin_e().full_w()
).origin_w().full_w(),
east(
button_2("r", "epeat ", false),
fg(Rgb(255, 255, 255), format!("{looped} ")).origin_e().full_w()
).origin_w().full_w(),
)
}
pub fn view_edit_info (editor: &MidiEditor) -> impl Draw<Tui> + '_ {
let (_color, length) = if let Some(clip) = editor.mode.clip.read().as_ref().map(|p|p.read()) {
(clip.color, clip.length)
} else { (ItemTheme::G[64], 0) };
let time_pos = editor.get_time_pos();
let time_zoom = editor.get_time_zoom();
let time_lock = if editor.get_time_lock() { "[lock]" } else { " " };
let note_pos = editor.get_note_pos();
let note_name = format!("{:4}", note_pitch_to_name(note_pos));
let note_pos = format!("{:>3}", note_pos);
let note_len = format!("{:>4}", editor.get_note_len());
south!(
east(
button_2("t", "ime ", false),
fg(Rgb(255, 255, 255), format!("{length} /{time_zoom} +{time_pos} ")).origin_e().pad_w(1).full_w()
).origin_w().full_w(),
east(
button_2("z", "lock ", false),
fg(Rgb(255, 255, 255), format!("{time_lock}")).origin_e().full_w()
).origin_w().full_w(),
south(
button_2("x", "note ", false),
fg_bg(Rgb(255, 255, 255), Rgb(16, 16, 16), format!("{note_name} {note_pos} {note_len}").origin_e().full_w())
).origin_w().full_w(),
)
}
pub fn view_scene_name_theme (scene: &Scene, track_index: usize) -> (Arc<str>, ItemTheme) {
scene.clips
.get(track_index)
.as_ref()
.map(|clip|clip.as_ref().map(|clip|{
let clip = clip.read();
(format!(" ⏹ {}", &clip.name).into(), clip.color)
}))
.flatten()
.unwrap_or_else(||{
(" ⏹ -- ".into(), ItemTheme::G[32])
})
}
pub fn view_selection <'a> (state: &'a App) -> impl Draw<Tui> + use<'a> {
format!("{:?}", state.selection())
}
pub fn view_templates <'a> (state: &'a App) -> impl Draw<Tui> + use<'a> {
let config: &Config = state.config();
let height = (config.modes_count() * 2) as u16;
draw(move |to: &mut Tui|{
let mut index = 0;
config.modes_for_each(&mut |id: &str, profile: &Mode| {
let b = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) };
let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or("<no name>");
let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or("<no info>");
let fg1 = Rgb(224, 192, 128);
let fg2 = Rgb(224, 128, 32);
let field_name = fg(fg1, name).align_w().full_w();
let field_id = fg(fg2, id).align_e().full_w();
let field_info = info.align_w().full_w();
let _ = bg(b, south(above(field_name, field_id), field_info))
.full_w().exact_h(2).push_y((2 * index) as u16).draw(to);
index += 1;
});
Ok(Some(to.area().into()))
}).min_w(30).exact_h(height)
}
pub fn view_browse_title (state: &App) -> impl Draw<Tui> {
field_v(ItemTheme::default(), match state.dialog.browser_target().unwrap() {
BrowseTarget::SaveProject => "Save project:",
BrowseTarget::LoadProject => "Load project:",
BrowseTarget::ImportSample(_) => "Import sample:",
BrowseTarget::ExportSample(_) => "Export sample:",
BrowseTarget::ImportClip(_) => "Import clip:",
BrowseTarget::ExportClip(_) => "Export clip:",
}, fg(g(96), x_repeat("🭻")).exact_h(1)).align_w().full_w()
}
/// ```
/// let _ = tek::view_meter("", 0.0);
/// let _ = tek::view_meters(&[0.0, 0.0]);
/// ```
pub fn view_meter <'a> (label: &'a str, value: f32)
-> impl Draw<Tui>
{
let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value));
let w = if value >= 0.0 { 13 }
else if value >= -1.0 { 12 }
else if value >= -2.0 { 11 }
else if value >= -3.0 { 10 }
else if value >= -4.0 { 9 }
else if value >= -6.0 { 8 }
else if value >= -9.0 { 7 }
else if value >= -12.0 { 6 }
else if value >= -15.0 { 5 }
else if value >= -20.0 { 4 }
else if value >= -25.0 { 3 }
else if value >= -30.0 { 2 }
else if value >= -40.0 { 1 }
else { 0 };
let c = if value >= 0.0 { Red }
else if value >= -3.0 { Yellow }
else { Green };
south!(f, bg(c, ()).exact_wh(w, 1))
}
pub fn view_meters (values: &[f32;2]) -> impl Draw<Tui> + use<'_> {
let left = format!("L/{:>+9.3}", values[0]);
let right = format!("R/{:>+9.3}", values[1]);
south(left, right)
}
pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
track.map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins))
}
pub fn view_midi_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
track.map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs))
}
pub fn view_audio_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
track.map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins()))
}
pub fn view_audio_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
track.map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs()))
}
pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T])
-> impl Draw<Tui> + use<'a, T>
{
let ins = ports.len() as u16;
let frame = Outer(true, Style::default().fg(g(96)));
border(true, frame, field_v(theme, title, iter_south({
move||ports.iter().enumerate().map(|(index, port)|{
east!(" ", index, " ", port.port_name()).align_w().full_h()
})
})).exact_wh(20, 1 + ins)).exact_wh(20, 1 + ins)
}
pub fn view_io_ports <'a, T: PortsSizes<'a>> (
fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a
) -> impl Draw<Tui> + 'a {
type Item<'a> = (usize, &'a Arc<str>, &'a [Connect], usize, usize);
iter(items,
move|(_index, name, connections, y, y2): Item<'a>, _| south(
bold(true, fg_bg(fg, bg, east(" 󰣲 ", name).align_w())).full_h(),
iter(||connections.iter(), move|connect: &'a Connect, index|{
bold(false, fg_bg(fg, bg, &connect.info)).exact_h(1).align_w().push_y(index as u16)
})
).exact_h((y2 - y) as u16).push_y(y as u16))
}
pub fn view_sample_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
when(sample.is_some(), draw(move|to: &mut Tui|{
let sample = sample.unwrap().try_read().unwrap();
let theme = sample.color;
east!(
field_h(theme, "Name", format!("{:<10}", sample.name.clone())),
field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())),
field_h(theme, "Start", format!("{:<8}", sample.start)),
field_h(theme, "End", format!("{:<8}", sample.end)),
field_h(theme, "Trans", "0"),
field_h(theme, "Gain", format!("{}", sample.gain)),
).draw(to)
}))
}
pub fn view_sample_info_v (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
let a = draw(move|to: &mut Tui|{
let sample = sample.unwrap().try_read().unwrap();
let theme = sample.color;
south!(
field_h(theme, "Name ", format!("{:<10}", sample.name.clone())) .align_w().full_w(),
field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())).align_w().full_w(),
field_h(theme, "Start ", format!("{:<8}", sample.start)) .align_w().full_w(),
field_h(theme, "End ", format!("{:<8}", sample.end)) .align_w().full_w(),
field_h(theme, "Trans ", "0") .align_w().full_w(),
field_h(theme, "Gain ", format!("{}", sample.gain)) .align_w().full_w(),
).exact_w(20).draw(to)
});
let b = draw(|to: &mut Tui|fg(Red, south!(
bold(true, "× No sample."),
"[r] record",
"[Shift-F9] import",
)).draw(to));
either(sample.is_some(), a, b)
}
pub fn view_sample_status (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> {
bold(true, fg(g(224), sample
.map(|sample|{
let sample = sample.try_read().unwrap();
format!("Sample {}-{}", sample.start, sample.end)
})
.unwrap_or_else(||"No sample".to_string())))
}
/// ```
/// let _ = tek::button_2("", "", true);
/// let _ = tek::button_2("", "", false);
/// ```
pub fn button_2 <'a> (
key: impl Draw<Tui>, label: impl Draw<Tui>, hide: bool
) -> impl Draw<Tui> {
let c1 = tui_orange();
let c2 = tui_g(0);
let c3 = tui_g(96);
let c4 = tui_g(255);
bold(true, fg_bg(c1, c2,
bar(Split::East, 1u16, fg(c2, "▐"),
bar(Split::East, 1u16, key,
bar(Split::East, 1u16, fg(c3, "▐"),
when(!hide, fg_bg(c4, c3, label)))))))
}
/// ```
/// let _ = tek::button_3("", "", "", true);
/// let _ = tek::button_3("", "", "", false);
/// ```
pub fn button_3 <'a> (
key: impl Draw<Tui>, label: impl Draw<Tui>, value: impl Draw<Tui>, editing: bool,
) -> impl Draw<Tui> {
let c1 = tui_orange();
let c2 = tui_g(0);
let c3 = tui_g(96);
let c4 = tui_g(255);
let c5 = tui_g(128);
bold(true, bar(Split::East, 3u16,
fg_bg(c1, c2, bar(Split::East, 1u16, fg(c2, "▐"),
bar(Split::East, 1u16, key, fg(if editing { c5 } else { c3 }, "▐")))),
east(when(!editing, east(fg_bg(c4, c3, label), fg_bg(c5, c3, "▐"),)),
east(fg_bg(tui_g(224), c5, value), fg_bg(c5, Reset, "▌"), ))))
}
pub fn field_h <'a, T: Screen> (
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
) -> impl Draw<T> {
}
pub fn field_v <'a, T: Screen> (
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
) -> impl Draw<T> {
}
pub fn view_sessions <'a> () -> impl Draw<Tui> {
let h = 6;
let w = Some(30);
let f = Rgb(224, 192, 128);
draw(move |to: &mut Tui|{
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) };
let y = (2 * index) as u16;
let h = 2;
bg(b, fg(f, *name).align_w()).full_w().exact_h(h).push_y(y).draw(to)?;
}
Ok(Some(to.area().into()))
}).min_w(w).exact_h(h)
}
/// ```
/// let bg = tengri::ratatui::style::Color::Red;
/// let fg = tengri::ratatui::style::Color::Green;
/// let _ = tek::view_wrap(bg, fg, "and then blue, too!");
/// ```
pub fn view_wrap <'a> (bg: Color, fg: Color, content: impl Draw<Tui>)
-> impl Draw<Tui>
{
let left = fg_bg(bg, Reset, y_repeat("▐").exact_w(1));
let right = fg_bg(bg, Reset, y_repeat("▌").exact_w(1));
east(left, west(right, fg_bg(fg, bg, content)))
}
pub fn redraw_piano (state: &PianoHorizontal) -> BigBuffer {
state.clip.map_inner(|clip|{
let mut buffer = BigBuffer::from(state.buffer_size(&clip));
let time_zoom = state.get_time_zoom().max(1);
state.time_len().store(clip.length, Relaxed);
let note_len = state.get_note_len();
let note_pos = state.get_note_pos();
let time_pos = state.get_time_pos();
draw_piano_bg(&mut buffer, &clip, time_zoom, note_len, note_pos, time_pos);
draw_piano_fg(&mut buffer, &clip, time_zoom);
buffer
}).unwrap_or_default()
}
/// Draw the piano roll background.
pub fn draw_piano_bg (
buf: &mut BigBuffer,
clip: &MidiClip,
zoom: usize,
note_len: usize,
note_point: usize,
time_point: usize
) {
for (y, note) in (0..=127).rev().enumerate() {
for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) {
let cell = buf.get_mut(x, y).unwrap();
if note == (127-note_point) || time == time_point {
cell.set_bg(Rgb(0,0,0));
} else {
cell.set_bg(clip.color.darkest.term);
}
if time % 384 == 0 {
cell.set_fg(clip.color.darker.term);
cell.set_char('│');
} else if time % 96 == 0 {
cell.set_fg(clip.color.dark.term);
cell.set_char('╎');
} else if time % note_len == 0 {
cell.set_fg(clip.color.darker.term);
cell.set_char('┊');
} else if (127 - note) % 12 == 0 {
cell.set_fg(clip.color.darker.term);
cell.set_char('=');
} else if (127 - note) % 6 == 0 {
cell.set_fg(clip.color.darker.term);
cell.set_char('-');
} else {
cell.set_fg(clip.color.darker.term);
cell.set_char('·');
}
}
}
}
/// Draw the piano roll foreground.
pub fn draw_piano_fg (
buf: &mut BigBuffer,
clip: &MidiClip,
zoom: usize
) {
let style = Style::default().fg(clip.color.base.term);//.bg(Rgb(0, 0, 0));
let mut notes_on = [false;128];
for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() {
for (_y, note) in (0..=127).rev().enumerate() {
if let Some(cell) = buf.get_mut(x, note) {
if notes_on[note] {
cell.set_char('▂');
cell.set_style(style);
}
}
}
let time_end = time_start + zoom;
for time in time_start..time_end.min(clip.length) {
if let Some(notes) = clip.notes.get(time) {
for event in notes.iter() {
match event {
MidiMessage::NoteOn { key, .. } => {
let note = key.as_int() as usize;
if let Some(cell) = buf.get_mut(x, note) {
cell.set_char('█');
cell.set_style(style);
}
notes_on[note] = true
},
MidiMessage::NoteOff { key, .. } => {
notes_on[key.as_int() as usize] = false
},
_ => {}
}
}
}
}
}
}
#[macro_export] macro_rules! rewrite {
($buf:ident, $($rest:tt)*) => {
|$buf,_,_|{
$buf.clear();
write!($buf, $($rest)*)
}
};
}
pub struct Memo<T, U> { value: T, view: U, }
impl<T, U> Memo<T, U> { pub fn new (value: T, view: U) -> Self { Self { value, view } } }
impl<T: PartialEq + Debug, U: Debug> Debug for Memo<T, U> {
fn fmt (&self, _: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
todo!()
}
}
impl<T: PartialEq, U> Memo<T, U> {
pub fn update <R> (&mut self, newval: T, render: impl Fn(&mut U, &T, &T)->R) -> Option<R> {
if newval != self.value {
let result = render(&mut self.view, &newval, &self.value);
self.value = newval;
return Some(result);
}
None
}
}
/// Contains memoized renders of clock values.
///
/// Performance optimization.
#[derive(Debug)] pub struct ClockView {
pub sr: Memo<Option<(bool, f64)>, String>,
pub buf: Memo<Option<f64>, String>,
pub lat: Memo<Option<f64>, String>,
pub bpm: Memo<Option<f64>, String>,
pub beat: Memo<Option<f64>, String>,
pub time: Memo<Option<f64>, String>,
}
impl ClockView {
pub const BEAT_EMPTY: &'static str = "-.-.--";
pub const TIME_EMPTY: &'static str = "-.---s";
pub const BPM_EMPTY: &'static str = "---.---";
pub fn update_clock (cache: &Arc<RwLock<Self>>, clock: &Clock, compact: bool) {
let rate = clock.timebase.sr.get();
let chunk = clock.chunk.load(Relaxed) as f64;
let lat = chunk / rate * 1000.;
let delta = |start: &Moment|clock.global.usec.get() - start.usec.get();
let mut cache = cache.try_write().unwrap();
cache.buf.update(
Some(chunk), rewrite!(buf, "{chunk}")
);
cache.lat.update(
Some(lat), rewrite!(buf, "{lat:.1}ms")
);
cache.sr.update(
Some((compact, rate)), |buf: &mut String,_,_|{
buf.clear();
if compact {
write!(buf, "{:.1}kHz", rate / 1000.)
} else {
write!(buf, "{:.0}Hz", rate)
}
}
);
if let Some(now) = clock.started.try_read().unwrap().as_ref().map(delta) {
let pulse = clock.timebase.usecs_to_pulse(now);
let time = now/1000000.;
let bpm = clock.timebase.bpm.get();
cache.beat.update(Some(pulse), |buf, _, _|{
buf.clear();
clock.timebase.format_beats_1_to(buf, pulse)
});
cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time));
cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm));
} else {
cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY));
cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY));
cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY));
}
}
}
#[cfg(test)] mod test_view {
use super::*;
use proptest::prelude::*;
proptest! {
#[test] fn proptest_view_meter (
label in "\\PC*", value in f32::MIN..f32::MAX
) {
let _ = view_meter(&label, value);
}
#[test] fn proptest_view_meters (
value1 in f32::MIN..f32::MAX,
value2 in f32::MIN..f32::MAX
) {
let _ = view_meters(&[value1, value2]);
}
}
}
pub fn print_config_views (config: &Config) {
use ::ansi_term::Color::*;
config.views_for_each(|k, v|{
println!("{} {} {}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}")), v.source);
});
}