tek/src/tek_view.rs
i do not exist 6aff4e55a6 open editor
2026-09-23 07:49:38 +03:00

1700 lines
70 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::*;
/// 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 |_|{
self.draw_error(to)?;
self.draw_modes(to)?;
self.draw_debug(to)?;
#[cfg(feature = "prof2")] profiling::finish_frame!();
Ok(Some(to.area().into()))
//})
}
}
impl App {
#[allow(unused)] fn draw_debug (&self, to: &mut Tui) -> Drawn<u16> {
Split::Above.bar(0,
Split::West.bar(16,
format!("{}/{} {} ",
self.perf.used.load(Relaxed),
self.perf.window.load(Relaxed),
self.perf.clock.raw() / 10000000).align_e(),
format!("{}x{} ",
self.size.0.load(Relaxed),
self.size.1.load(Relaxed)).align_w(),
).align_se(),
Split::East.bar(16,
format!("{}", self.modes.read()[0]),
format!("{}", self.modes.read()[0])
).align_sw()
).draw(to)
}
fn draw_error (&self, to: &mut Tui) -> Usually<()> {
if let Some(e) = self.error.try_read().unwrap().as_ref() {
e.as_ref().align_c().draw(to)?;
}
Ok(())
}
fn draw_modes (&self, to: &mut Tui) -> Usually<()> {
for name in self.modes.read().iter() {
let mut error = false;
if let Some(mode) = self.config.get_mode(name).as_ref().map(Arc::clone) {
for (index, view) in mode.view.iter().enumerate() {
match (view.render)(self, to) {
Ok(None) => {},
Ok(Some(XYWH(.., w, h))) => {
self.size.0.store(w as usize, Relaxed);
self.size.1.store(h as usize, Relaxed);
},
Err(e) => {
let message = format!(
"Mode: {:?}\n\nLayer: #{index}\n\nError: {e}\n\nSource:\n{}",
&mode.name,
&view.source
);
*self.error.try_write().unwrap() = Some(message.into());
error = true;
break;
}
}
}
if error {
break
} else {
*self.error.try_write().unwrap() = None;
}
}
}
Ok(())
}
}
/// Collection of custom view definitions.
#[derive(Default, Clone, Debug)]
pub struct Views(pub Arc<RwLock<BTreeMap<
Arc<str>,
Arc<View<Tui, App>>
>>>);
impl Views {
/// Load custom view definition.
pub fn add <'a> (views: &Views, expr: impl Language) -> UsuallyRef<'a, ()> {
views.0.write().insert(
expr.head()?.ok_or("view: missing name")?.into(),
View::new(expr.tail()?.ok_or("view: missing body")?)?.into()
);
Ok(())
}
pub fn clear (&self) {
*self.0.try_write().unwrap() = Default::default();
}
pub fn get (&self, name: impl AsRef<str>) -> Option<Arc<View<Tui, App>>> {
self.0.try_read().map(|views|views.get(name.as_ref()).cloned()).flatten()
}
pub fn for_each <T> (&self, cb: impl Fn(&Arc<str>, &Arc<View<Tui, App>>)->T) -> () {
for (k, v) in self.0.read().iter() {
cb(k, v);
}
}
}
/// 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>>
}
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 View<Tui, App> {
pub fn new (source: impl AsRef<str>) -> Usually<Self> {
Ok(Self {
source: source.as_ref().into(),
render: Self::compile(source)?
})
}
fn boxed <F: Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync + 'static> (f: F)
-> Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync + 'static>
{
Box::new(f)
}
fn compile (source: impl AsRef<str>) ->
Usually<Arc<Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync>>>
{
let source = source.as_ref();
let layer = if let Ok(Some(expr)) = source.expr() {
Self::compile_expr(expr.into())?
} else if let Ok(Some(word)) = source.word() {
Self::compile_word(word.into())?
} else {
return err!("not word/expr:\n{source:?}")
};
Ok(Arc::new(Box::new(move|state, screen|layer(state, screen))))
}
fn compile_expr (expr: Arc<str>) ->
Usually<Arc<Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync>>>
{
macro_rules! arg {
($src:expr, $head:expr, $index:expr, $name:expr) => {
$src.nth($index)?
.ok_or_else(||Box::<dyn Error>::from(format!(
"{}: no arg #{} ({}) in {}", $head, $index, $name, $src
)))?
};
}
Ok(Arc::new(if let Some(head) = expr.head()? && let Some(ns) = head.split('/').next() {
match ns {
"when" => {
let cond = Arc::from(arg!(expr, head, 1, "condition"));
let cond = move|state: &App|state.namespace(&cond)?.ok_or_else(||Box::<dyn Error>::from("when: no condition value"));
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
Self::boxed(move|state, screen|{
when(
cond(state)?,
draw(|screen|thunk(state, screen))
).draw(screen)
})
},
"either" => {
let cond = Arc::from(arg!(expr, head, 1, "condition"));
let cond = move|state: &App|state.namespace(&cond)?.ok_or_else(||Box::<dyn Error>::from("either: no condition value"));
let a = Self::compile(arg!(expr, head, 2, "content A"))?;
let b = Self::compile(arg!(expr, head, 3, "content B"))?;
Self::boxed(move|state, screen|{
either(
cond(state)?,
draw(|screen|a(state, screen)),
draw(|screen|b(state, screen)),
).draw(screen)
})
},
"bsp" | "split" | "stack" => {
let split = head.split('/').skip(1).next();
let split = match split {
Some("n") => Split::North,
Some("s") => Split::South,
Some("e") => Split::East,
Some("w") => Split::West,
Some("a") => Split::Above,
Some("b") => Split::Below,
_ => return err!("invalid split: {split:?}")
};
let a = Self::compile(arg!(expr, head, 1, "content A"))?;
let b = Self::compile(arg!(expr, head, 2, "content B"))?;
Self::boxed(move|state, screen|split.stack(
draw(|screen|a(state, screen)),
draw(|screen|b(state, screen)),
).draw(screen))
},
"bar" => {
let split = head.split('/').skip(1).next();
let split = match split {
Some("n") => Split::North,
Some("s") => Split::South,
Some("e") => Split::East,
Some("w") => Split::West,
Some("a") => Split::Above,
Some("b") => Split::Below,
_ => return err!("invalid split: {split:?}")
};
let size = Arc::from(arg!(expr, head, 1, "size"));
let size = move|state: &App|state.namespace(&size)?.ok_or_else(||Box::<dyn Error>::from("bar: no size"));
let a = Self::compile(arg!(expr, head, 2, "content A"))?;
let b = Self::compile(arg!(expr, head, 3, "content B"))?;
Self::boxed(move|state, screen|split.bar(
size(state)?,
draw(|screen|a(state, screen)),
draw(|screen|b(state, screen)),
).draw(screen))
},
"align" => {
let azimuth = head.split('/').skip(1).next();
let azimuth = match azimuth {
Some("n") => Azimuth::N,
Some("s") => Azimuth::S,
Some("e") => Azimuth::E,
Some("w") => Azimuth::W,
Some("ne") => Azimuth::NE,
Some("se") => Azimuth::SE,
Some("nw") => Azimuth::NW,
Some("sw") => Azimuth::SW,
Some("c") => Azimuth::C,
Some("x") => Azimuth::X,
Some("y") => Azimuth::Y,
_ => return err!("invalid azimuth: {azimuth:?}")
};
let thunk = Self::compile(arg!(expr, head, 1, "content"))?;
Self::boxed(move|state, screen|{
Align(
Some(azimuth),
draw(|screen|thunk(state, screen))
).draw(screen)
})
},
"full" | "fill" => {
let thunk = Self::compile(arg!(expr, head, 1, "content"))?;
match head.split('/').skip(1).next() {
Some("w") | Some("x") => Self::boxed(move|state, screen|{
Full::W(draw(|screen|thunk(state, screen))).draw(screen)
}),
Some("h") | Some("y") => Self::boxed(move|state, screen|{
Full::H(draw(|screen|thunk(state, screen))).draw(screen)
}),
Some("wh") | Some("xy") | None => Self::boxed(move|state, screen|{
Full::WH(draw(|screen|thunk(state, screen))).draw(screen)
}),
_ => unreachable!()
}
},
"exact" | "min" | "max" | "push" | "pull" | "pad" => {
match head.split('/').skip(1).next() {
Some("w") | Some("x") => {
let value = Arc::from(arg!(expr, head, 1, "value"));
let value = move|state: &App|state.namespace(&value);
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
match ns {
"exact" => Self::boxed(move|state, screen|Exact::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"push" => Self::boxed(move|state, screen|Push::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"pull" => Self::boxed(move|state, screen|Pull::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"pad" => Self::boxed(move|state, screen|Pad::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"min" => Self::boxed(move|state, screen|Min::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"max" => Self::boxed(move|state, screen|Max::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
_ => unreachable!()
}
},
Some("h") | Some("y") => {
let value = Arc::from(arg!(expr, head, 1, "value"));
let value = move|state: &App|state.namespace(&value);
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
match ns {
"exact" => Self::boxed(move|state, screen|Exact::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"push" => Self::boxed(move|state, screen|Push::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"pull" => Self::boxed(move|state, screen|Pull::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"pad" => Self::boxed(move|state, screen|Pad::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"min" => Self::boxed(move|state, screen|Min::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"max" => Self::boxed(move|state, screen|Max::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
_ => unreachable!()
}
},
Some("wh") | Some("xy") | None => {
let value1 = Arc::from(arg!(expr, head, 1, "value"));
let value1 = move|state: &App|state.namespace(&value1);
let value2 = Arc::from(arg!(expr, head, 2, "value"));
let value2 = move|state: &App|state.namespace(&value2);
let thunk = Self::compile(arg!(expr, head, 3, "content"))?;
match ns {
"exact" => Self::boxed(move|state, screen|Exact::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
"push" => Self::boxed(move|state, screen|Push::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
"pull" => Self::boxed(move|state, screen|Pull::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
"pad" => Self::boxed(move|state, screen|Pad::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
"min" => Self::boxed(move|state, screen|Min::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
"max" => Self::boxed(move|state, screen|Max::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
_ => unreachable!()
}
},
_ => unreachable!()
}
},
"fg" | "bg" => {
let color = Arc::from(arg!(expr, head, 1, "color"));
let color = move|state: &App|state.namespace(&color);
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
match ns {
"fg" => Self::boxed(move|state, screen|fg(
color(state)?.unwrap_or_default(), draw(|screen|thunk(state, screen))
).draw(screen)),
"bg" => Self::boxed(move|state, screen|bg(
color(state)?.unwrap_or_default(), draw(|screen|thunk(state, screen))
).draw(screen)),
_ => unreachable!()
}
},
"text" => {
let text: Arc<str> = Arc::from(expr.tail()?.unwrap_or_default());
Self::boxed(move|_state, screen|{
text.draw(screen)
})
},
_ => return err!("compile_expr: unexpected: {expr:?}")
}
} else {
return err!("compile_expr: invalid expression: {expr:?}")
}))
}
fn compile_word (word: Arc<str>)
-> Usually<Arc<Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync>>>
{
macro_rules! draw {
(|$state:ident|$body:expr)=>{Self::boxed(move|$state, to|$body.draw(to))};
($const:expr)=>{Self::boxed(move|_, to|$const.draw(to))};
()=>{Self::boxed(move|_, __|Ok(None))};
}
Ok(Arc::new(match word.split("/").next() {
Some("Beat") => draw!(),
Some("Bpm") => draw!(),
Some("BrowseTitle") => draw!(|app|view_browse_title(app)),
Some("Debug") => Self::boxed(move|_, to|format!("[{:?}]", to.area()).exact_h(1).draw(to)),
Some("Device") => draw!(|app|view_device(app)),
Some("Menu") => draw!(|app|{
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
}
}),
Some("Editor") => draw!("TODO editor"),
Some("MetersIn") => draw!(|___|bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h())),
Some("MetersOut") => draw!(|___|bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h())),
Some("ModeList") => draw!(|app|app.modes.read().get(0)), // TODO all
Some("ScenesClips") => draw!(|app|view_scenes_clips(app)),
Some("ScenesNames") => draw!(|app|app.view_scenes_names()),
Some("Sessions") => draw!(|___|view_sessions()),
Some("Templates") => draw!(|app|view_templates(app)),
Some("Time") => draw!(),
Some("Tracks") => draw!("TODO tracks"),
Some("TracksDevices") => draw!(|app|app.project.view_track_devices(app.color.clone())),
Some("TracksDevicesAdd") => draw!(|app|app.project.view_track_device_add()),
Some("TracksDevicesCells") => draw!(|app|app.project.view_track_device_cells(app.tracks_devices_height())),
Some("TracksDevicesCount") => draw!(|app|app.project.view_track_device_count()),
Some("TracksInputs") => draw!(|app|app.project.view_track_inputs(app.color.clone())),
Some("TracksInputsAdd") => draw!(|app|app.project.view_track_input_add()),
Some("TracksInputsCells") => draw!(|app|app.project.view_track_input_cells(app.tracks_inputs_height())),
Some("TracksInputsCount") => draw!(|app|app.project.view_track_input_count()),
Some("TracksNames") => draw!(|app|app.project.view_track_names(app.color.clone())),
Some("TracksNamesAdd") => draw!(|app|app.project.view_track_names_add()),
Some("TracksNamesCells") => draw!(|app|app.project.view_track_names_cells()),
Some("TracksNamesCount") => draw!(|app|app.project.view_track_names_count()),
Some("TracksOutputs") => draw!(|app|app.project.view_track_outputs(app.color.clone(), 0)),
Some("TracksOutputsAdd") => draw!(|app|app.project.view_track_output_add()),
Some("TracksOutputsCells") => draw!(|app|app.project.view_track_output_cells()),
Some("TracksOutputsCount") => draw!(|app|app.project.view_track_output_count()),
Some(_) => Self::boxed(move|state, to|if let Some(view) = state.config.get_view(word.as_ref()) {
(view.render)(state, to)
} else {
fg(Color::Rgb(128, 32, 32), format!("undefined: {word:?}")).draw(to)
}),
_ => unreachable!()
}))
}
}
/// ```
/// 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 HasClipsSize for App { fn clips_size (&self) -> &Sizer { &self.project.size_inner } }
impl HasClipsSize for Project { fn clips_size (&self) -> &Sizer { &self.size_inner } }
pub trait HasClipsSize {
/// Size of clips area.
fn clips_size (&self) -> &Sizer;
/// Draw content annotated with value of [Self::clips_size].
fn with_clips_size (&self, show: bool, content: impl Draw<Tui>) -> impl Draw<Tui> {
let size = self.clips_size();
above(
when(show, fg(Green, east!(size.w() as usize, "x", size.h() as usize)).align_se()),
size.of(content.align_c().full_wh())
)
}
}
impl MidiEditor {
pub fn clip_status (&self) -> impl Draw<Tui> + '_ {
let (
_color, name, length, looped
) = if let Some(clip) = self.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!(
east(button_2("f2", "name ", false), fg(Rgb(255, 255, 255), format!("{name} "))
.origin_e().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(),
).exact_w(20)
}
pub fn edit_status (&self) -> impl Draw<Tui> + '_ {
let (_color, length) = if let Some(clip) = self.mode.clip.read().as_ref().map(|p|p.read()) {
(clip.color, clip.length)
} else { (ItemTheme::G[64], 0) };
let time_pos = self.get_time_pos();
let time_zoom = self.get_time_zoom();
let time_lock = if self.get_time_lock() { "[lock]" } else { " " };
let note_pos = self.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}", self.get_note_len());
south!(
east(button_2("t", "ime ", false),
fg(Rgb(255, 255, 255), format!("{length} /{time_zoom} +{time_pos} ")).origin_e().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(),
east(button_2("x", "note ", false),
fg(Rgb(255, 255, 255), format!("{note_name} {note_pos} {note_len}")).origin_e().full_w()).origin_w().full_w(),
).exact_w(20)
}
}
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 + ScenesView + HasMidiIns + HasMidiOuts + HasTrack
{}
pub trait TracksView:
HasTracks + ScenesView + HasMidiIns + HasMidiOuts + HasTrack
{
/// Iterate over tracks with their corresponding sizes.
fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
let _editor_width = self.editor().as_ref().map(|e|e.size.w());
let _active_track = self.selection().track();
let mut x = 0;
let w = self.clips_size().w() as usize;
self.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 + Self::TRACK_SPACING;
Some(data)
} else {
None
}
})
}
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 view_outputs (&self, theme: ItemTheme) -> impl Draw<Tui> {
let height = self.outputs_height();
let list = 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 - 1).min_h(1)
);
view_track_row_section(theme, list, self.view_track_output_add(),
bg(theme.darker.term, 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!()
}).align_w().full_w())).exact_h(height)
}
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 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()
)
}
/// 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())
)
}
fn view_track_devices (&self, theme: ItemTheme) -> impl Draw<Tui> {
let height = self.tracks_devices_height();
let btn1 = self.view_track_device_count();
let btn2 = self.view_track_device_add();
view_track_row_section(theme, btn1, btn2, self.view_track_device_cells(height))
}
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_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: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync {
fn h_scenes (&self) -> u16;
fn w_side (&self) -> u16;
fn w_mid (&self) -> u16;
fn view_scenes_names (&self) -> impl Draw<Tui> {
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 scenes_with_sizes (&self) -> impl ScenesSizes<'_> {
let mut y = 0;
self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{
let height = if self.selection().scene() == Some(s) && self.editor().is_some() {
8
} else {
Scene::DEFAULT_HEIGHT
};
if y + height <= self.clips_size().h() as usize {
let data = (s, scene, y, y + height);
y += height;
Some(data)
} else {
None
}
})
}
}
impl ScenesView for App {
fn w_mid (&self) -> u16 {
(self.size.w() as u16).saturating_sub(self.w_side())
}
fn w_side (&self) -> u16 {
20
}
fn h_scenes (&self) -> u16 {
(self.size.h() as u16).saturating_sub(20)
}
}
impl ScenesView for Project {
fn h_scenes (&self) -> u16 {
(self.size.h() as u16).saturating_sub(20)
}
fn w_side (&self) -> u16 {
(self.size.w() as u16 * 2 / 10).max(20)
}
fn w_mid (&self) -> u16 {
(self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40)
}
}
/// Draw clips per scene
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);
below(
Outer(true, Style::default().fg(o)).full_wh(),
below(
below(
fg_bg(o, b, "".full_wh()),
fg_bg(f, b, bold(true, name)).align_nw().full_wh(),
),
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_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(24).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 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(12)
} else {
Scene::DEFAULT_HEIGHT 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 draw_status_bar (to: &mut Tui, state: &App) -> Drawn<u16> {
bar(Split::South, 1u16,
dim(true, "Mode"),
bold(true, state.modes.read().get(0))
).push_x(1).draw(to)
}
/// ```
/// 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 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> {
bar(
Split::West,
4u16,
button_add.align_ne().full_wh(),
bar(
Split::East,
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 {
7
} else {
Scene::DEFAULT_HEIGHT as u16
};
let a = east!("·s", index, " ", fg(g(255), bold(true, &scene.name))).align_w().full_w();
let b = when(select.scene() == Some(index) && editing, south(
editor.as_ref().map(|e|e.clip_status()),
editor.as_ref().map(|e|e.edit_status())).align_nw().full_wh());
let c = if select.scene() == Some(index) {
scene.color.light.term
} else {
scene.color.base.term
};
bg(c, south(a, b).align_nw().exact_h(h))
}
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_templates <'a> (state: &'a App) -> impl Draw<Tui> + use<'a> {
let height = (state.config.modes.len() * 2) as u16;
draw(move |to: &mut Tui|{
let mut index = 0;
state.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)))
}
impl PianoHorizontal {
/// Draw the piano roll background.
///
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
pub fn draw_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.
///
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
pub fn draw_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) {
for event in clip.notes[time].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
}
}
pub trait Gettable<T> {
/// Returns current value
fn get (&self) -> T;
}
pub trait Mutable<T>: Gettable<T> {
/// Sets new value, returns old
fn set (&mut self, value: T) -> T;
}
pub trait InteriorMutable<T>: Gettable<T> {
/// Sets new value, returns old
fn set (&self, value: T) -> T;
}
#[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 per_track <'a, T: Draw<'_, Tui> + 'a, U: TracksSizes<'a>> (
//tracks: impl Fn() -> U + Send + Sync + 'a,
//callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
//) -> impl Draw<'_, Tui> + 'a {
//per_track_top(tracks, move|index, track|callback(index, track).full_h().align_y())
//}
//pub fn per_track_top <'a, T: Draw<'_, Tui> + 'a, U: TracksSizes<'a>> (
//tracks: impl Fn() -> U + Send + Sync + 'a,
//callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
//) -> impl Draw<'_, Tui> + 'a {
//bg(Reset, iter_east(||tracks()
//.map(move|(index, track, x1, x2): (usize, &'a Track, usize, usize)|{
//fg_bg(
//track.color.lightest.term,
//track.color.base.term,
//callback(index, track)
//).exact_w((x2 - x1) as u16)
//})).align_x())
//}
//fn tui (&self) -> impl Draw<Tui> {
//iter_south_fixed(1, ||self.tui_entries(), |entry, _index|entry.origin_w().full_w())
//}
//impl<'a> Iterator for EntriesIterator<'a, Tui> {
//type Item = impl Draw<Tui>;
//fn next (&mut self) -> Option<Self::Item> {
//let dirs = self.browser.dirs.len();
//let files = self.browser.files.len();
//let index = self.index;
//if self.index < dirs {
//self.index += 1;
//Some(bold(true, self.browser.dirs[index].1.as_str()))
//} else if self.index < dirs + files {
//self.index += 1;
//Some(bold(false, self.browser.files[index - dirs].1.as_str()))
//} else {
//None
//}
//}
//}
//impl<'a> PoolView<'a> {
//fn tui (&self) -> impl Draw<'_, Tui> {
//let Self(pool) = self;
////let color = self.1.clip().map(|c|c.try_read().unwrap().color).unwrap_or_else(||g(32).into());
////let on_bg = |x|x;//below(Repeat(" "), bg(color.darkest.term, x));
////let border = |x|x;//Outer(Style::default().fg(color.dark.term).bg(color.darkest.term)).enclose(x);
////let height = pool.clips.try_read().unwrap().len() as u16;
//iter(
//||pool.clips.clone().into_iter(),
//move|clip: Arc<RwLock<MidiClip>>, i: usize|{
//let MidiClip { ref name, color, length, .. } = *clip.try_read().unwrap();
//let item_height = 1;
//let _item_offset = i as u16 * item_height;
//let selected = i == pool.clip_index();
//let b = if selected { color.light.term } else { color.base.term };
//let f = color.lightest.term;
//let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
//let length = if false { String::default() } else { format!("{length} ") };
//bg(b, below!(
//fg(f, bold(selected, name)).origin_w().full_w(),
//fg(f, bold(selected, length)).origin_e().full_w(),
//when(selected, bold(true, fg(g(255), "▶"))).origin_w().full_w(),
//when(selected, bold(true, fg(g(255), "◀"))).origin_e().full_w(),
//)).exact_h(1)
//}).origin_n().full_h().exact_w(20)
//}
//}
//impl ClipLength {
//fn tui (&self) -> impl Draw<'_, Tui> {
//use ClipLengthFocus::*;
//let bars = format!("{}", self.bars());
//let beats = format!("{}", self.beats());
//let ticks = format!("{:>02}", self.ticks());
//match self.focus {
//None => east!(" ", bars, ".", beats, ".", ticks),
//Some(Bar) => east!("[", bars, "]", beats, ".", ticks),
//Some(Beat) => east!(" ", bars, "[", beats, "]", ticks),
//Some(Tick) => east!(" ", bars, ".", beats, "[", ticks),
//}
//}
//}