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 for App { fn draw (&self, to: &mut Tui) -> Drawn { #[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>>) -> 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 { 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() ) } pub fn draw_mode_stack (to: &mut Tui, state: &App) -> Usually<()> { for name in state.modes.read().iter() { let mut error = false; if let Some(mode) = state.config.get_mode(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) => { let message = format!( "Mode: {:?}\n\nLayer: #{index}\n\nError: {e}\n\nSource:\n{}", &mode.name, &view.source ); *state.error.try_write().unwrap() = Some(message.into()); error = true; break; } } } if error { break } else { *state.error.try_write().unwrap() = None; } } } Ok(()) } /// Collection of custom view definitions. #[derive(Default, Clone, Debug)] pub struct Views(pub Arc, Arc> >>>); impl Views { /// Load custom view definition. pub fn add <'a> (&self, expr: impl Language) -> UsuallyRef<'a, ()> { self.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) -> Option>> { self.0.try_read().map(|views|views.get(name.as_ref()).cloned()).flatten() } pub fn for_each (&self, cb: impl Fn(&Arc, &Arc>)->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 { pub source: Arc, pub render: ArcDrawn + Send + Sync>> } impl_debug!( View |self, w| { write!(w, "View({})", self.source) }); impl_display!( View |self, w| { write!(w, "View({})", self.source) }); impl View { pub fn new (source: impl AsRef) -> Usually { Ok(Self { source: source.as_ref().into(), render: Self::compile(source)? }) } fn boxed Drawn + Send + Sync + 'static> (f: F) -> BoxDrawn + Send + Sync + 'static> { Box::new(f) } fn compile (source: impl AsRef) -> UsuallyDrawn + 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) -> UsuallyDrawn + Send + Sync>>> { macro_rules! arg { ($src:expr, $head:expr, $index:expr, $name:expr) => { $src.nth($index)? .ok_or_else(||Box::::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::::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::::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::::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" | "shrink" | "expand" => { 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 { "shrink" => Self::boxed(move|state, screen|Shrink::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), "expand" => Self::boxed(move|state, screen|Expand::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), "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 { "shrink" => Self::boxed(move|state, screen|Shrink::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), "expand" => Self::boxed(move|state, screen|Expand::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), "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 { "shrink" => Self::boxed(move|state, screen|Shrink::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)), "expand" => Self::boxed(move|state, screen|Expand::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)), "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 = 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) -> UsuallyDrawn + Send + Sync>>> { macro_rules! view { (|$state:ident|$body:expr)=>{Box::new(move|$state, to|$body.draw(to))}; ($const:expr)=>{Box::new(move|_, to|$const.draw(to))}; ()=>{Box::new(move|_, __|Ok(None))}; } Ok(Arc::new(match word.split("/").next() { Some("Beat") => view!(), Some("Bpm") => view!(), Some("BrowseTitle") => view!(|app|view_browse_title(app)), Some("Config") => view!(|app|view_config(app)), Some("Debug") => Box::new(move|_, to|format!("[{:?}]", to.area()).exact_h(1).draw(to)), Some("Device") => view!(|app|view_device(app)), Some("Menu") => view!(|app|view_menu(app)), Some("Editor") => view!(|app|app.editor()), Some("MetersIn") => view!(|___|bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h())), Some("MetersOut") => view!(|___|bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h())), Some("ModeList") => view!(|app|view_modes(app)), Some("HistoryCount") => view!(|app|app.history.len()), Some("HistoryList") => view!(|app|view_history_list(app)), Some("ScenesClips") => view!(|app|view_scenes_clips(app)), Some("ScenesNames") => view!(|app|app.view_scenes_names()), Some("Sessions") => view!(|___|view_sessions()), Some("Selected") => view!(|app|view_selection(app)), Some("Templates") => view!(|app|view_templates(app)), Some("Time") => view!(), Some("Tracks") => view!("TODO tracks"), Some("TracksDevices") => view!(|app|app.project.view_track_devices(app.color.clone())), Some("TracksDevicesAdd") => view!(|app|app.project.view_track_device_add()), Some("TracksDevicesCells") => view!(|app|app.project.view_track_device_cells(app.tracks_devices_height())), Some("TracksDevicesCount") => view!(|app|app.project.view_track_device_count()), Some("TracksInputs") => view!(|app|app.project.view_track_inputs(app.color.clone())), Some("TracksInputsAdd") => view!(|app|app.project.view_track_input_add()), Some("TracksInputsCells") => view!(|app|app.project.view_track_input_cells(app.tracks_inputs_height())), Some("TracksInputsCount") => view!(|app|app.project.view_track_input_count()), Some("TracksNames") => view!(|app|app.project.view_track_names(app.color.clone())), Some("TracksNamesAdd") => view!(|app|app.project.view_track_names_add()), Some("TracksNamesCells") => view!(|app|app.project.view_track_names_cells()), Some("TracksNamesCount") => view!(|app|app.project.view_track_names_count()), Some("TracksOutputs") => view!(|app|app.project.view_track_outputs(app.color.clone(), 0)), Some("TracksOutputsAdd") => view!(|app|app.project.view_track_output_add()), Some("TracksOutputsCells") => view!(|app|app.project.view_track_output_cells()), Some("TracksOutputsCount") => view!(|app|app.project.view_track_output_count()), Some(_) => Box::new(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!() })) } } pub fn view_menu (app: &App) -> impl Draw { 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 { above( Outer(true, Style::default().fg(Color::Rgb(0, 192, 0))), draw(|to: &mut Tui|{ let area = to.area(); let XYWH(x0, mut y0, w, h) = area; for (done, undo) in app.history.iter().rev() { to.clip(XYWH(x0 + 1, y0, w, 1), &|to|format!("{done:?}").draw(to))?; y0 += 1; } Ok(Some(area)) }) ) } pub fn view_config (app: &App) -> impl Draw { above( Outer(true, Style::default().fg(Color::Rgb(0, 192, 0))), draw(|to: &mut Tui|{ let area = to.area(); let XYWH(x0, mut y0, w, h) = area; for key in app.modes.read().iter().rev() { if let Some(mode) = app.config.modes.get(key) { to.clip(XYWH(x0 + 1, y0, w, 1), &|to|Split::East.bar(16, key, mode.info.get(0)).draw(to))?; y0 += 1; for bind in mode.keys.iter() { to.clip(XYWH(x0 + 3, y0, w, 1), &|to|bind.draw(to))?; y0 += 1; if let Some(bind) = app.config.binds.find(bind) { for (key, binding) in bind.0.iter() { to.clip(XYWH(x0 + 5, y0, 40, 1), &|to|Split::East.bar(20, format!("{:?}", key.0), "").draw(to))?; y0 += 1; } } } y0 += 1; } } //for (index, (key, mode)) in app.config.modes.0.read().iter().enumerate() { //to.clip(XYWH(x0, y0 + index as u16, w, 1), &|to|Split::East.bar(16, //key, mode.info.get(0), //).draw(to))?; //} Ok(Some(area)) }) ) } pub fn view_config_mode (mode: &Mode) -> impl Draw { } /// ``` /// 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 { 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 for PianoHorizontal { fn draw (&self, to: &mut Tui) -> Drawn { 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 { 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 { 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 { 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(¬e_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 + '_ { 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 + 'a, U: TracksSizes<'a>> ( tracks: impl Fn() -> U + Send + Sync + 'a, callback: &'a (impl Fn(usize, &'a Track)->T + Send + Sync + 'a) ) -> impl Draw { 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 { 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 for MidiEditor { fn draw (&self, to: &mut Tui) -> Drawn { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode).draw(to) } } impl TracksView for T where T: HasTracks + 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 + 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 where S: Screen, D: Draw + '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 { 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 { 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 + '_ { 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 { 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 { 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 { button_2("T", "+", false) } fn view_scene_add (&self) -> impl Draw { button_2("S", "+", false) } fn view_track_output_add (&self) -> impl Draw { button_2("O", "+", false) } fn view_track_input_add (&self) -> impl Draw { button_2("I", "+", false) } fn view_track_device_add (&self) -> impl Draw { button_2("D", "+", false) } fn view_track_names_add (&self) -> impl Draw { south(self.view_track_add(), self.view_scene_add()) } fn view_track_count (&self) -> impl Draw { view_n_of_m("t", "rack", self.selection().track(), self.tracks().len()); } fn view_scene_count (&self) -> impl Draw { view_n_of_m("s", "cene", self.selection().scene(), self.scenes().len()); } fn view_track_input_count (&self) -> impl Draw { button_2("i", "nput", false) } fn view_track_output_count (&self) -> impl Draw { button_3("o", "utput", 0, false) } fn view_track_names_count (&self) -> impl Draw { south(self.view_track_count(), self.view_scene_count()) } fn view_track_device_count (&self) -> impl Draw { 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 { 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 { 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 { 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 { 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 + '_ { 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 { 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 { 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 { 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, count: usize) -> impl Draw { 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 scenes_with_sizes (&self) -> impl ScenesSizes<'_> { iter_scenes_with_sizes(self) } fn view_scenes_names (&self) -> impl Draw 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 + HasSceneScroll + HasEditor + HasSelection + HasClipsSize + ?Sized { let mut y = 0; let skip = state.scene_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 } }) } fn view_modes <'a, S: HasModeStack> (state: &'a S) -> impl Draw + '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.modes().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 { 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>>) -> 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>>) -> impl Draw + 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 + 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 { 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 { 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 { 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, button_add: impl Draw, content: impl Draw, ) -> impl Draw { 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) -> impl Draw { 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 { 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 fn view_scenes_clips (state: &S) -> impl Draw { 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, 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 + '_ { 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 + '_ { 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, 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 + use<'a> { format!("{:?}", state.selection()) } pub fn view_templates <'a> (state: &'a App) -> impl Draw + 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(""); let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); 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 { 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 { 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 + 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 { 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 { 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 { 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 { 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 + 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 + 'a { type Item<'a> = (usize, &'a Arc, &'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>>) -> impl Draw + 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>>) -> impl Draw + 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>>) -> impl Draw { 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, label: impl Draw, hide: bool ) -> impl Draw { 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, label: impl Draw, value: impl Draw, editing: bool, ) -> impl Draw { 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, _body: impl Draw ) -> impl Draw { } pub fn field_v <'a, T: Screen> ( _theme: ItemTheme, _head: impl Draw, _body: impl Draw ) -> impl Draw { } pub fn view_sessions <'a> () -> impl Draw { 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) -> impl Draw { 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 { value: T, view: U, } impl Memo { pub fn new (value: T, view: U) -> Self { Self { value, view } } } impl Debug for Memo { fn fmt (&self, _: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { todo!() } } impl Memo { pub fn update (&mut self, newval: T, render: impl Fn(&mut U, &T, &T)->R) -> Option { 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, String>, pub buf: Memo, String>, pub lat: Memo, String>, pub bpm: Memo, String>, pub beat: Memo, String>, pub time: Memo, String>, } 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)), } }); 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>, 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]); } } } //impl<'a> Iterator for EntriesIterator<'a, Tui> { //type Item = impl Draw; //fn next (&mut self) -> Option { //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>, 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), //} //} //}