From ca774cca417c3b0d97a3c1ab4e831847c24293d5 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Mon, 21 Sep 2026 01:44:52 +0300 Subject: [PATCH] wip: more shrunkening --- src/config_view.rs | 340 -------- src/deps.rs | 2 + src/midi_clip.rs | 98 --- src/midi_edit.rs | 214 ++--- src/midi_octave.rs | 15 +- src/midi_piano.rs | 245 +----- src/midi_play.rs | 8 +- src/proj.rs | 295 ++++++- src/proj_clock.rs | 21 - src/proj_pool.rs | 236 ----- src/proj_scene.rs | 129 +-- src/proj_select.rs | 35 +- src/proj_track.rs | 378 +------- src/tek.rs | 391 +++++---- src/{config_bind.rs => tek_bind.rs} | 0 src/tek_browse.rs | 146 ---- src/{config.rs => tek_config.rs} | 0 src/tek_ctrl.rs | 554 ++++++------ src/{config_mode.rs => tek_mode.rs} | 0 src/tek_view.rs | 1242 ++++++++++++++++++++++++++- tengri | 2 +- 21 files changed, 2160 insertions(+), 2191 deletions(-) delete mode 100644 src/config_view.rs delete mode 100644 src/proj_pool.rs rename src/{config_bind.rs => tek_bind.rs} (100%) delete mode 100644 src/tek_browse.rs rename src/{config.rs => tek_config.rs} (100%) rename src/{config_mode.rs => tek_mode.rs} (100%) diff --git a/src/config_view.rs b/src/config_view.rs deleted file mode 100644 index dd1ed8ff..00000000 --- a/src/config_view.rs +++ /dev/null @@ -1,340 +0,0 @@ -use crate::*; - -/// 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> (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) -> 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" => { - 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 = 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! 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|app.view_scenes_clips()), - 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!() - })) - } -} diff --git a/src/deps.rs b/src/deps.rs index 67dfeb42..0e6ae476 100644 --- a/src/deps.rs +++ b/src/deps.rs @@ -41,6 +41,8 @@ pub(crate) use ::{ RwLock, RwLockReadGuard, RwLockWriteGuard, + ArcRwLockReadGuard, + ArcRwLockWriteGuard, RawRwLock, } }, diff --git a/src/midi_clip.rs b/src/midi_clip.rs index 55e04a1e..fd347a4c 100644 --- a/src/midi_clip.rs +++ b/src/midi_clip.rs @@ -1,18 +1,5 @@ use crate::*; -pub type Clips = Vec>>; - -pub trait HasClips { - /// Active clip slot (immutable). Use this to access or modify current clip. - fn clip (&self) -> Option>>; - /// Active clip slot (mutable). Use this to place or remove a clip at current position. - fn clip_mut (&self) -> &mut Option>>; - /// Clip list (immutable). Use this to access or modify more than one clip. - fn clips <'a> (&'a self) -> RwLockReadGuard<'a, Clips>; - /// Clip list (mutable). Use this to define or delete new clips. - fn clips_mut <'a> (&'a self) -> RwLockWriteGuard<'a, Clips>; -} - /// A MIDI sequence. /// /// ``` @@ -130,88 +117,3 @@ const MIDI_PANIC: MidiMessage = MidiMessage::Controller { pub type MidiData = Vec>; impl PartialEq for MidiClip { fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid } } impl Eq for MidiClip {} - - -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) -> impl Draw { - 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 ClipsView for T {} - -pub trait ClipsView: TracksView + ScenesView { - - /// Draw clips per scene - fn view_scenes_clips (&self) -> impl Draw { - let select = self.selection(); - let editor = self.editor(); - let editing = self.is_editing(); - self.with_clips_size(true, iter_east(move||self.tracks_with_sizes() - .map(move|(track_index, track, _, _)|iter_south(move||self.scenes_with_sizes() - .map(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_selected = 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_selected, editor).full_wh() - ).full_wh() - ).exact_wh( - view_scene_w(track, select, track_index, editor.as_ref()), - view_scene_y(select, scene_index, editor.as_ref()), - ) - }) - ) - .exact_w(track.width as u16)) - )).align_c() - } - -} - -pub fn get_opt_arc_rwlock_midiclip <'a> (state: &App, src: impl Language) - -> Perhaps>>> -{ - eval_word(src, |word|Ok(match word { - - ":selected" if let Selection::TrackClip { - track, scene - } = state.selection() => { - state.scenes()[*scene].clips[*track].clone() - }, - - //":selected-or-create" if let Selection::TrackClip { - //track, scene - //} = state.selection() => Some(if let Some(Some(clip)) = state.scenes()[*scene].clips.get(*track) { - //clip.clone() - //} else { - //let clip: Arc> = Arc::new(RwLock::new(Default::default())); - //state.scenes()[*scene].clips[*track] = Some(clip.clone()); - //clip - //}), - - _ => return err!("not a clip: {word}") - - })) -} - -pub fn get_opt_vec_opt_arc_rwlock_midiclip <'a> (_: &App, _: impl Language) - -> Perhaps>>>>> -{ - todo!() -} diff --git a/src/midi_edit.rs b/src/midi_edit.rs index e8c11623..2703e8fc 100644 --- a/src/midi_edit.rs +++ b/src/midi_edit.rs @@ -26,9 +26,10 @@ impl HasEditor for T where T: AsRef> + AsMut usize { self.clip().as_ref().map(|p|p.try_read().unwrap().length).unwrap_or(1) } - fn note_length (&self) -> usize { self.get_note_len() } - fn note_pos (&self) -> usize { self.get_note_pos() } - fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 } - fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 } - fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) } - fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) } - fn note_len (&self) -> usize { self.get_note_len() } - fn note_len_next (&self) -> usize { self.get_note_len() + 1 } - fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) } - fn note_range (&self) -> usize { self.get_note_axis() } - fn note_range_next (&self) -> usize { self.get_note_axis() + 1 } - fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) } - fn time_zoom (&self) -> usize { self.get_time_zoom() } - fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 } + fn clip_length (&self) -> usize { + self.mode.clip.map_inner(|p|p.length).unwrap_or(1) + } + fn note_length (&self) -> usize { + self.get_note_len() + } + fn note_pos (&self) -> usize { + self.get_note_pos() + } + fn note_pos_next (&self) -> usize { + self.get_note_pos() + 1 + } + fn note_pos_next_octave (&self) -> usize { + self.get_note_pos() + 12 + } + fn note_pos_prev (&self) -> usize { + self.get_note_pos().saturating_sub(1) + } + fn note_pos_prev_octave (&self) -> usize { + self.get_note_pos().saturating_sub(12) + } + fn note_len (&self) -> usize { + self.get_note_len() + } + fn note_len_next (&self) -> usize { + self.get_note_len() + 1 + } + fn note_len_prev (&self) -> usize { + self.get_note_len().saturating_sub(1) + } + fn note_range (&self) -> usize { + self.get_note_axis() + } + fn note_range_next (&self) -> usize { + self.get_note_axis() + 1 + } + fn note_range_prev (&self) -> usize { + self.get_note_axis().saturating_sub(1) + } + fn time_zoom (&self) -> usize { + self.get_time_zoom() + } + fn time_zoom_next (&self) -> usize { + self.get_time_zoom() + 1 + } fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 } fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } @@ -113,39 +144,6 @@ impl MidiEditor { self.get_time_pos().overflowing_sub(1) .0.min(self.clip_length().saturating_sub(1)) } - pub fn clip_status (&self) -> impl Draw + '_ { - let (_color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.try_read().unwrap()) { - (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 + '_ { - let (_color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.try_read().unwrap()) { - (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) - } } /// ``` @@ -170,12 +168,6 @@ impl MidiRange for T {} pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync { fn buffer_size (&self, clip: &MidiClip) -> (usize, usize); fn redraw (&self); - fn clip (&self) -> &Option>>; - fn clip_mut (&mut self) -> &mut Option>>; - fn set_clip (&mut self, clip: Option<&Arc>>) { - *self.clip_mut() = clip.cloned(); - self.redraw(); - } /// Make sure cursor is within note range fn autoscroll (&self) { let note_pos = self.get_note_pos().min(127); @@ -240,19 +232,6 @@ impl std::fmt::Debug for MidiEditor { } } -impl_from!(MidiEditor: |clip: &Arc>| { - let model = Self::from(Some(clip.clone())); - model.redraw(); - model -}); - -impl_from!(MidiEditor: |clip: Option>>| { - let mut model = Self::default(); - *model.clip_mut() = clip; - model.redraw(); - model -}); - impl NotePoint for MidiEditor { fn note_len (&self) -> &AtomicUsize { self.mode.note_len() } fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() } @@ -263,31 +242,15 @@ impl TimePoint for MidiEditor { } impl MidiViewer for MidiEditor { - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } - fn redraw (&self) { self.mode.redraw() } - fn clip (&self) -> &Option>> { self.mode.clip() } - fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } - fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } -} - -impl Draw for MidiEditor { - fn draw (&self, to: &mut Tui) -> Drawn { - self.autoscroll(); - /*self.autozoom();*/ - self.size.of(&self.mode).draw(to) + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { + self.mode.buffer_size(clip) + } + fn redraw (&self) { + self.mode.redraw() } } impl App { - /// Is a MIDI editor currently focused? - /// - /// ``` - /// tek::App::default().editor_focused(); - /// ``` - pub fn editor_focused (&self) -> bool { - false - } - /// Toggle MIDI editor. /// /// ``` @@ -295,39 +258,50 @@ impl App { /// ``` pub fn toggle_editor (&mut self, value: Option) { //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); - let value = value.unwrap_or_else(||!self.editor().is_some()); - if value { - // Create new clip in pool when entering empty cell - if let Selection::TrackClip { track, scene } = *self.selection() - && let Some(scene) = self.project.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && slot.is_none() - && let Some(track) = self.project.tracks.get_mut(track) - { - let (_index, clip) = self.project.pool.add_new_clip(); - // autocolor: new clip colors from scene and track color - let color = track.color.base.mix(scene.color.base, 0.5); - clip.try_write().unwrap().color = ItemColor::random_near(color, 0.2).into(); - if let Some(editor) = &mut self.project.editor { - editor.set_clip(Some(&clip)); - } - *slot = Some(clip.clone()); - //Some(clip) - } else { - //None + if value.unwrap_or_else(||!self.editor().is_some()) { + self.toggle_editor_on() + } else { + self.toggle_editor_off() + } + } + + fn toggle_editor_on (&mut self) { + // Create new clip in pool when entering empty cell + if let Selection::TrackClip { track: track_index, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && scene.clips.get(track_index).is_none() + && let Some(track) = self.project.tracks.get_mut(track_index) + { + let color = track.color.base.mix(scene.color.base, 0.5); + let color = ItemColor::random_near(color, 0.2).into(); + let clip: MidiClip = MidiClip { color, ..Default::default() }; + let clip = self.project.clips.append(Some(clip)); + // autocolor: new clip colors from scene and track color + if let Some(editor) = &mut self.project.editor { + *editor.mode.clip.write() = clip.0.read().clone(); + editor.mode.color = clip.0.read().as_ref().map(|p|p.read().color) + .unwrap_or(ItemTheme::G[64]); } - } else if let Selection::TrackClip { track, scene } = *self.selection() + *scene.clips.slot(track_index).write() = clip.0.read().clone(); + //Some(clip) + } else { + //None + } + } + + fn toggle_editor_off (&mut self) { + if let Selection::TrackClip { track, scene } = *self.selection() && let Some(scene) = self.project.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && let Some(clip) = slot.as_mut() { // Remove clip from arrangement when exiting empty clip editor let mut swapped = None; - if clip.try_read().unwrap().count_midi_messages() == 0 { - std::mem::swap(&mut swapped, slot); + if let Some(count) = scene.clips.slot(track).read().as_ref().map(|c|c.read().count_midi_messages()) + && count == 0 + { + std::mem::swap(&mut swapped, &mut scene.clips.slot(track).write()); } if let Some(clip) = swapped { - self.project.pool.delete_clip(&clip.try_read().unwrap()); + self.project.clips.remove(&*clip.read()); } } } diff --git a/src/midi_octave.rs b/src/midi_octave.rs index 21124e98..5d3d15ed 100644 --- a/src/midi_octave.rs +++ b/src/midi_octave.rs @@ -12,25 +12,12 @@ pub struct OctaveVertical { } impl OctaveVertical { - fn color (&self, pitch: usize) -> Color { + pub fn color (&self, pitch: usize) -> Color { let pitch = pitch % 12; self.colors[if self.on[pitch] { 2 } else { match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } }] } } -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_default!(OctaveVertical: Self { on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] }); diff --git a/src/midi_piano.rs b/src/midi_piano.rs index a2e9d8f4..f9fc695e 100644 --- a/src/midi_piano.rs +++ b/src/midi_piano.rs @@ -5,245 +5,34 @@ use crate::*; /// ``` /// let piano = tek::PianoHorizontal::default(); /// ``` -#[derive(Clone, Default)] -pub struct PianoHorizontal { - pub clip: Option>>, +#[derive(Clone)] pub struct PianoHorizontal { + pub clip: ClipSlot, /// Buffer where the whole clip is rerendered on change pub buffer: Arc>, /// Size of actual notes area - pub size: Sizer, + pub size: Sizer, /// The display window - pub range: MidiSelection, + pub range: MidiSelection, /// The note cursor - pub point: MidiCursor, + pub point: MidiCursor, /// The highlight color palette - pub color: ItemTheme, + pub color: ItemTheme, /// Width of the keyboard pub keys_width: u16, } impl_has!(Sizer: |self: PianoHorizontal| self.size); -impl Draw for PianoHorizontal { - fn draw (&self, to: &mut Tui) -> Drawn { - south( - east( - format!("{}x{}", self.size.w(), self.size.h()).exact_w(5), - self.timeline() - ), - east( - self.keys(), - self.size.of(below(self.notes().full_wh(), self.cursor().full_wh())) - ), - ).draw(to) - } -} - -impl PianoHorizontal { - - pub fn new (clip: Option<&Arc>>) -> Self { +impl Default for PianoHorizontal { + fn default () -> Self { let size = Sizer::default(); let mut range = MidiSelection::from((12, true)); range.time_axis = size.0.clone(); range.note_axis = size.1.clone(); - let piano = Self { - keys_width: 5, - size, - range, - buffer: RwLock::new(Default::default()).into(), - point: MidiCursor::default(), - clip: clip.cloned(), - color: clip.as_ref().map(|p|p.try_read().unwrap().color).unwrap_or(ItemTheme::G[64]), - }; + let piano = Self { keys_width: 5, size, range, ..Default::default() }; piano.redraw(); piano } - - /// Draw the piano roll background. - /// - /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ - 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: █▄ █▄ █▄ - 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 - }, - _ => {} - } - } - } - - } - } - - fn notes (&self) -> impl Draw { - let time_start = self.get_time_start(); - let note_lo = self.get_note_lo(); - let note_hi = self.get_note_hi(); - let buffer = self.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.try_read().unwrap(); - //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)) - }) - } - - fn cursor (&self) -> impl Draw { - let note_hi = self.get_note_hi(); - let note_lo = self.get_note_lo(); - let note_pos = self.get_note_pos(); - let note_len = self.get_note_len(); - let time_pos = self.get_time_pos(); - let time_start = self.get_time_start(); - let time_zoom = self.get_time_zoom(); - let style = Some(Style::default().fg(self.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)) - }) - } - - fn keys (&self) -> impl Draw { - let state = self; - 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(self.keys_width).full_h() - } - - fn timeline (&self) -> 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 = self.clip.as_ref().map(|p|p.try_read().unwrap().length).unwrap_or(1); - for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { - let t = area_x as usize * self.time_zoom().load(Relaxed); - if t < length { - to.blit(&"|", screen_x, y, style); - } - } - Ok(Some(xywh)) - }).exact_h(1).full_w() - } } impl TimeRange for PianoHorizontal { @@ -269,15 +58,12 @@ impl TimePoint for PianoHorizontal { } impl MidiViewer for PianoHorizontal { - fn clip (&self) -> &Option>> { &self.clip } - fn clip_mut (&mut self) -> &mut Option>> { &mut self.clip } /// Determine the required space to render the clip. fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { (clip.length / self.range.time_zoom().load(Relaxed), 128) } fn redraw (&self) { - *self.buffer.try_write().unwrap() = if let Some(clip) = self.clip.as_ref() { - let clip = clip.try_read().unwrap(); + *self.buffer.write() = self.clip.map_inner(|clip|{ let buf_size = self.buffer_size(&clip); let mut buffer = BigBuffer::from(buf_size); let time_zoom = self.get_time_zoom(); @@ -285,20 +71,13 @@ impl MidiViewer for PianoHorizontal { PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); buffer - } else { - Default::default() - } - } - fn set_clip (&mut self, clip: Option<&Arc>>) { - *self.clip_mut() = clip.cloned(); - self.color = clip.map(|p|p.try_read().unwrap().color).unwrap_or(ItemTheme::G[64]); - self.redraw(); + }).unwrap_or_default() } } impl std::fmt::Debug for PianoHorizontal { fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - let buffer = self.buffer.try_read().unwrap(); + let buffer = self.buffer.read(); f.debug_struct("PianoHorizontal") .field("time_zoom", &self.range.time_zoom) .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) diff --git a/src/midi_play.rs b/src/midi_play.rs index 0ceaa90d..fc00b079 100644 --- a/src/midi_play.rs +++ b/src/midi_play.rs @@ -19,7 +19,7 @@ pub trait HasPlayClip: HasClock { fn pulses_since_start_looped (&self) -> Option<(f64, f64)> { if let Some((started, Some(clip))) = self.play_clip().as_ref() { let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); - let length = clip.try_read().unwrap().length.max(1); // prevent div0 on empty clip + let length = clip.read().length.max(1); // prevent div0 on empty clip let times = (elapsed as usize / length) as f64; let elapsed = (elapsed as usize % length) as f64; return Some((times, elapsed)) @@ -34,7 +34,7 @@ pub trait HasPlayClip: HasClock { fn play_status (&self) -> impl Draw { let (name, color): (Arc, ItemTheme) = if let Some((_, Some(clip))) = self.play_clip() { - let MidiClip { ref name, color, .. } = *clip.try_read().unwrap(); + let MidiClip { ref name, color, .. } = *clip.read(); (name.clone(), color) } else { ("".into(), ItemTheme::G[64].into()) @@ -55,7 +55,7 @@ pub trait HasPlayClip: HasClock { let mut color = ItemTheme::G[64]; let clock = self.clock(); if let Some((t, Some(clip))) = self.next_clip() { - let clip = clip.try_read().unwrap(); + let clip = clip.read(); name = clip.name.clone(); color = clip.color.clone(); time = { @@ -69,7 +69,7 @@ pub trait HasPlayClip: HasClock { } }.into() } else if let Some((t, Some(clip))) = self.play_clip() { - let clip = clip.try_read().unwrap(); + let clip = clip.read(); if clip.looped { name = clip.name.clone(); color = clip.color.clone(); diff --git a/src/proj.rs b/src/proj.rs index 43669459..c6298f32 100644 --- a/src/proj.rs +++ b/src/proj.rs @@ -3,14 +3,12 @@ impl_as_ref!(Option: |self: App|todo!()); impl_as_mut!(Option: |self: App|todo!()); pub trait HasProject: AsRef> + AsMut> {} impl> + AsMut>> HasProject for T {} - /// Arranger. /// /// ``` /// let arranger = tek::Project::default(); /// ``` -#[derive(Default, Debug)] -pub struct Project { +#[derive(Default, Debug)] pub struct Project { /// JACK client handle. pub jack: Jack<'static>, /// Project name. @@ -27,33 +25,33 @@ pub struct Project { /// Selected UI element pub selection: Selection, /// Contains all recently created clips. - pub pool: Pool, + #[cfg(feature = "sequencer")] pub clips: Pool, + /// Contains all recently created clips. + #[cfg(feature = "sampler")] pub samples: Pool, /// Source of time - #[cfg(feature = "clock")] pub clock: Clock, + #[cfg(feature = "clock")] pub clock: Clock, /// Allows one MIDI clip to be edited - #[cfg(feature = "editor")] pub editor: Option, + #[cfg(feature = "editor")] pub editor: Option, /// List of global midi inputs - #[cfg(feature = "port")] pub midi_ins: Vec, + #[cfg(feature = "port")] pub midi_ins: Vec, /// List of global midi outputs - #[cfg(feature = "port")] pub midi_outs: Vec, + #[cfg(feature = "port")] pub midi_outs: Vec, /// List of global audio inputs - #[cfg(feature = "port")] pub audio_ins: Vec, + #[cfg(feature = "port")] pub audio_ins: Vec, /// List of global audio outputs - #[cfg(feature = "port")] pub audio_outs: Vec, + #[cfg(feature = "port")] pub audio_outs: Vec, /// Last track number (to avoid duplicate port names) - #[cfg(feature = "track")] pub track_last: usize, + #[cfg(feature = "track")] pub track_last: usize, /// List of tracks - #[cfg(feature = "track")] pub tracks: Vec, + #[cfg(feature = "track")] pub tracks: Vec, /// Scroll offset of tracks - #[cfg(feature = "track")] pub track_scroll: usize, + #[cfg(feature = "track")] pub track_scroll: usize, /// List of scenes - #[cfg(feature = "scene")] pub scenes: Vec, + #[cfg(feature = "scene")] pub scenes: Vec, /// Scroll offset of scenes - #[cfg(feature = "scene")] pub scene_scroll: usize, + #[cfg(feature = "scene")] pub scene_scroll: usize, } - impl Project { - /// Create a new arrangement. pub fn new ( jack: &Jack<'static>, @@ -109,7 +107,7 @@ impl Project { //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } } - +impl HasJack<'static> for Project { fn jack (&self) -> &Jack<'static> { &self.jack } } impl_has!(Jack<'static>: |self: Project| self.jack); impl_has!(Sizer: |self: Project| self.size); impl_has!(Clock: |self: Project| self.clock); @@ -119,16 +117,21 @@ impl_as_ref_opt!(Scene: |self: Project| self.selected_scene()); impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut()); impl_as_ref!(Option: |self: Project| &self.editor); impl_as_mut!(Option: |self: Project| &mut self.editor); -impl HasJack<'static> for Project { - fn jack (&self) -> &Jack<'static> { &self.jack } -} -impl HasClips for Project { - fn clip (&self) -> Option>> { todo!() } - fn clip_mut (&self) -> &mut Option>> { todo!() } - fn clips (&self) -> RwLockReadGuard<'_, Clips> { todo!() } - fn clips_mut (&self) -> RwLockWriteGuard<'_, Clips> { todo!() } -} - +//impl HasClip for Project { + //fn clip (&self) -> Option>>>> { + //match self.selection { + //Selection::Pool(index) => + //self.clips.read(index), + //Selection::TrackClip { track, scene } if let Some(scene) = self.scenes.get(scene) => + //scene.clips.read(track), + //_ => + //None + //} + //} + //fn clip_mut (&self) -> RwLockWriteGuard<'_, Option>>> { + //self.as_ref().0.write() + //} +//} pub fn swap_value ( target: &mut T, value: &T, returned: impl Fn(T)->U ) -> Perhaps { @@ -140,7 +143,6 @@ pub fn swap_value ( Ok(Some(returned(value))) } } - pub fn toggle_bool ( target: &mut bool, value: &Option, returned: impl Fn(Option)->U ) -> Perhaps { @@ -152,3 +154,238 @@ pub fn toggle_bool ( Ok(Some(returned(Some(value)))) } } +#[derive(Default, Debug)] pub struct Slot(pub Arc>>>>); +pub type SampleSlot = Slot; +pub type ClipSlot = Slot; +impl Clone for Slot { fn clone (&self) -> Self { Slot(self.0.clone()) } } +impl Slot { + pub fn new (value: impl Into>) -> Self { + Self(Arc::new(RwLock::new(value.into().map(|value|Arc::new(RwLock::new(value)))))) + } + pub fn read (&self) -> ArcRwLockReadGuard>>> { + self.0.read_arc() + } + pub fn write (&self) -> ArcRwLockWriteGuard>>> { + self.0.write_arc() + } + pub fn set_to (&mut self, value: Self) { + self.0 = value.0; + } + pub fn set (&self, value: Option) -> Option>> { + let mut value = value.map(|value|Arc::new(RwLock::new(value))); + std::mem::swap(&mut*self.0.write(), &mut value); + value + } + pub fn map_inner (&self, cb: impl FnOnce(&T)->U) -> Option { + self.read().as_ref().map(|x|cb(&*x.read())) + } +} +impl Slot { + pub fn eval <'a> (state: &App, src: impl Language) -> Perhaps { + eval_word(src, |word|Ok(match word { + + ":selected" if let Selection::TrackClip { track, scene } = state.selection() => { + state.scenes()[*scene].clips.slot(*track) + }, + + //":selected-or-create" if let Selection::TrackClip { + //track, scene + //} = state.selection() => Some(if let Some(Some(clip)) = state.scenes()[*scene].clips.get(*track) { + //clip.clone() + //} else { + //let clip: Arc> = Arc::new(RwLock::new(Default::default())); + //state.scenes()[*scene].clips[*track] = Some(clip.clone()); + //clip + //}), + + _ => return err!("not a clip: {word}") + + })) + } +} +impl Slot { + pub fn eval <'a> (_: &App, _: impl Language) -> Perhaps { + todo!() + } +} +#[derive(Default, Debug)] pub struct Pool(pub Arc>>>); +impl Clone for Pool { fn clone (&self) -> Self { Pool(self.0.clone()) } } +pub type SamplePool = Pool; +pub type ClipPool = Pool; +impl Pool { + pub fn slot (&self, index: usize) -> Slot { + let mut state = self.0.write(); + while index >= state.len() { + state.push(Slot::new(None)); + } + state[index].clone() + } + pub fn delete (&self, index: usize) -> Option>> { + let mut none = None; + let mut slot = self.slot(index).write(); + std::mem::swap(&mut none, &mut slot); + none + } + pub fn len (&self) -> usize { + self.0.read().len() + } + pub fn get (&self, index: usize) -> Option>>>> { + self.0.read_arc().get(index).map(|clip|clip.read()) + } + pub fn set (&self, index: usize, value: Option) { + self.write()[index] = Slot::new(value); + } + pub fn read (&self) -> ArcRwLockReadGuard>> { + self.0.read_arc() + } + pub fn write (&self) -> ArcRwLockWriteGuard>> { + self.0.write_arc() + } + pub fn append (&self, value: Option) -> Slot { + let slot = Slot::new(value); + self.0.write().push(slot.clone()); + slot + } +} +impl Pool { + pub fn eval <'a> (_: &App, _: impl Language) -> Perhaps { + todo!() + } + pub fn remove (&self, filter: &MidiClip) { + let iter = self.0.write(); + iter.retain(|x|if let Some(x) = &*x.0.read() { x!=filter } else { true }) + } +} +impl Pool { + pub fn eval <'a> (_: &App, _: impl Language) -> Perhaps { + todo!() + } +} +impl From>> for Pool { + fn from (values: Vec>) -> Self { + Self(Arc::new(RwLock::new(values.into_iter().map(Slot::new).collect()))) + } +} +/// Modes for clip pool +#[derive(Debug, Clone)] pub enum PoolMode { + /// Renaming a pattern + Rename(usize, Arc), + /// Editing the length of a pattern + Length(usize, usize, ClipLengthFocus), + /// Load clip from disk + Import(usize, Browse), + /// Save clip to disk + Export(usize, Browse), +} +/// Focused field of `ClipLength` +#[derive(Copy, Clone, Debug)] pub enum ClipLengthFocus { + /// Editing the number of bars + Bar, + /// Editing the number of beats + Beat, + /// Editing the number of ticks + Tick, +} +impl ClipLengthFocus { + pub fn next (&mut self) { + use ClipLengthFocus::*; + *self = match self { Bar => Beat, Beat => Tick, Tick => Bar, } + } + pub fn prev (&mut self) { + use ClipLengthFocus::*; + *self = match self { Bar => Tick, Beat => Bar, Tick => Beat, } + } +} +/// Displays and edits clip length. +#[derive(Clone, Debug, Default)] pub struct ClipLength { + /// Pulses per beat (quaver) + pub ppq: usize, + /// Beats per bar + pub bpb: usize, + /// Length of clip in pulses + pub pulses: usize, + /// Selected subdivision + pub focus: Option, +} +impl ClipLength { + pub fn _new (pulses: usize, focus: Option) -> Self { + Self { ppq: PPQ, bpb: 4, pulses, focus } + } + pub fn bars (&self) -> usize { + self.pulses / (self.bpb * self.ppq) + } + pub fn beats (&self) -> usize { + (self.pulses % (self.bpb * self.ppq)) / self.ppq + } + pub fn ticks (&self) -> usize { + self.pulses % self.ppq + } +} +//impl_from!(Pool: |clip:&Arc>|{ + //let model = Self::default(); + //model.clips.try_write().unwrap().push(clip.clone()); + //model.clip.store(1, Relaxed); + //model +//}); +//impl Pool { + //pub fn clip_index (&self) -> usize { + //self.clip.load(Relaxed) + //} + //pub fn set_clip_index (&self, value: usize) { + //self.clip.store(value, Relaxed); + //} + //pub fn mode (&self) -> &Option { + //&self.mode + //} + //pub fn mode_mut (&mut self) -> &mut Option { + //&mut self.mode + //} + //pub fn begin_import (&mut self) -> Usually<()> { + //*self.mode_mut() = Some(PoolMode::Import( + //self.clip_index(), + //Browse::new(None)? + //)); + //Ok(()) + //} + //pub fn begin_export (&mut self) -> Usually<()> { + //*self.mode_mut() = Some(PoolMode::Export( + //self.clip_index(), + //Browse::new(None)? + //)); + //Ok(()) + //} + //pub fn new_clip (&self) -> MidiClip { + //MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random())) + //} + //pub fn cloned_clip (&self) -> MidiClip { + //let index = self.clip_index(); + //let mut clip = self.clips.read()[index].read().duplicate(); + //clip.color = ItemTheme::random_near(clip.color, 0.25); + //clip + //} + //pub fn add_new_clip (&self) -> (usize, Arc>) { + //let clip = Arc::new(RwLock::new(self.new_clip())); + //let index = { + //let mut clips = self.clips.try_write().unwrap(); + //clips.push(clip.clone()); + //clips.len().saturating_sub(1) + //}; + //self.clip.store(index, Relaxed); + //(index, clip) + //} + //pub fn delete_clip (&mut self, clip: &MidiClip) -> bool { + //let index = self.clips.try_read().unwrap().iter().position(|x|*x.try_read().unwrap()==*clip); + //if let Some(index) = index { + //self.clips.try_write().unwrap().remove(index); + //return true + //} + //false + //} + //fn _clip_new (&self) -> MidiClip { self.new_clip() } + //fn _clip_cloned (&self) -> MidiClip { self.cloned_clip() } + //fn _clip_index_current (&self) -> usize { 0 } + //fn _clip_index_after (&self) -> usize { 0 } + //fn _clip_index_previous (&self) -> usize { 0 } + //fn _clip_index_next (&self) -> usize { 0 } + //fn _color_random (&self) -> ItemColor { ItemColor::random() } +//} diff --git a/src/proj_clock.rs b/src/proj_clock.rs index 3b9f0adf..27a78bf1 100644 --- a/src/proj_clock.rs +++ b/src/proj_clock.rs @@ -374,27 +374,6 @@ impl_time_unit!(Pulse); impl_time_unit!(Bpm); impl_time_unit!(LaunchSync); -/// ``` -/// 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)), - ) - ) -} - /// Contains memoized renders of clock values. /// /// Performance optimization. diff --git a/src/proj_pool.rs b/src/proj_pool.rs deleted file mode 100644 index 3827d062..00000000 --- a/src/proj_pool.rs +++ /dev/null @@ -1,236 +0,0 @@ -use crate::*; - -/// A clip pool. -/// -/// ``` -/// let pool = tek::Pool::default(); -/// ``` -#[derive(Debug)] pub struct Pool { - pub visible: bool, - /// Selected clip - pub clip: AtomicUsize, - /// Mode switch - pub mode: Option, - /// Embedded file browse - #[cfg(feature = "browse")] pub browse: Option, - /// Collection of MIDI clips. - #[cfg(feature = "clip")] pub clips: Arc>>>>, - /// Collection of sound samples. - #[cfg(feature = "sampler")] pub samples: Arc>>>>, -} - -impl HasClips for Pool { - fn clip (&self) -> Option>> { - self.clips.read().get(self.clip_index()).map(|c|c.clone()) - } - fn clip_mut (&self) -> &mut Option>> { - todo!() - } - fn clips <'a> (&'a self) -> RwLockReadGuard<'a, Clips> { - self.clips.read() - } - fn clips_mut <'a> (&'a self) -> RwLockWriteGuard<'a, Clips> { - self.clips.write() - } -} - -/// Displays and edits clip length. -#[derive(Clone, Debug, Default)] pub struct ClipLength { - /// Pulses per beat (quaver) - pub ppq: usize, - /// Beats per bar - pub bpb: usize, - /// Length of clip in pulses - pub pulses: usize, - /// Selected subdivision - pub focus: Option, -} - -/// Some sort of wrapper again? -pub struct PoolView<'a>(pub &'a Pool); - -/// Modes for clip pool -#[derive(Debug, Clone)] -pub enum PoolMode { - /// Renaming a pattern - Rename(usize, Arc), - /// Editing the length of a pattern - Length(usize, usize, ClipLengthFocus), - /// Load clip from disk - Import(usize, Browse), - /// Save clip to disk - Export(usize, Browse), -} - -/// Focused field of `ClipLength` -#[derive(Copy, Clone, Debug)] -pub enum ClipLengthFocus { - /// Editing the number of bars - Bar, - /// Editing the number of beats - Beat, - /// Editing the number of ticks - Tick, -} - -impl + AsMut> HasPool for T {} - -pub trait HasPool: AsRef + AsMut { - fn pool (&self) -> &Pool { - self.as_ref() - } - fn pool_mut (&mut self) -> &mut Pool { - self.as_mut() - } -} - -impl_from!(Pool: |clip:&Arc>|{ - let model = Self::default(); - model.clips.try_write().unwrap().push(clip.clone()); - model.clip.store(1, Relaxed); - model -}); - -impl_default!(Pool: Self { - browse: None, - clip: 0.into(), - clips: Arc::from(RwLock::from(vec![])), - mode: None, - samples: Arc::from(RwLock::from(vec![])), - visible: true, -}); - -impl Pool { - pub fn clip_index (&self) -> usize { - self.clip.load(Relaxed) - } - pub fn set_clip_index (&self, value: usize) { - self.clip.store(value, Relaxed); - } - pub fn mode (&self) -> &Option { - &self.mode - } - pub fn mode_mut (&mut self) -> &mut Option { - &mut self.mode - } - pub fn begin_import (&mut self) -> Usually<()> { - *self.mode_mut() = Some(PoolMode::Import( - self.clip_index(), - Browse::new(None)? - )); - Ok(()) - } - pub fn begin_export (&mut self) -> Usually<()> { - *self.mode_mut() = Some(PoolMode::Export( - self.clip_index(), - Browse::new(None)? - )); - Ok(()) - } - pub fn new_clip (&self) -> MidiClip { - MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random())) - } - pub fn cloned_clip (&self) -> MidiClip { - let index = self.clip_index(); - let mut clip = self.clips()[index].try_read().unwrap().duplicate(); - clip.color = ItemTheme::random_near(clip.color, 0.25); - clip - } - pub fn add_new_clip (&self) -> (usize, Arc>) { - let clip = Arc::new(RwLock::new(self.new_clip())); - let index = { - let mut clips = self.clips.try_write().unwrap(); - clips.push(clip.clone()); - clips.len().saturating_sub(1) - }; - self.clip.store(index, Relaxed); - (index, clip) - } - pub fn delete_clip (&mut self, clip: &MidiClip) -> bool { - let index = self.clips.try_read().unwrap().iter().position(|x|*x.try_read().unwrap()==*clip); - if let Some(index) = index { - self.clips.try_write().unwrap().remove(index); - return true - } - false - } -} - -impl ClipLengthFocus { - pub fn next (&mut self) { - use ClipLengthFocus::*; - *self = match self { Bar => Beat, Beat => Tick, Tick => Bar, } - } - pub fn prev (&mut self) { - use ClipLengthFocus::*; - *self = match self { Bar => Tick, Beat => Bar, Tick => Beat, } - } -} - -impl ClipLength { - pub fn _new (pulses: usize, focus: Option) -> Self { - Self { ppq: PPQ, bpb: 4, pulses, focus } - } - pub fn bars (&self) -> usize { - self.pulses / (self.bpb * self.ppq) - } - pub fn beats (&self) -> usize { - (self.pulses % (self.bpb * self.ppq)) / self.ppq - } - pub fn ticks (&self) -> usize { - self.pulses % self.ppq - } -} - -impl Pool { - fn _clip_new (&self) -> MidiClip { self.new_clip() } - fn _clip_cloned (&self) -> MidiClip { self.cloned_clip() } - fn _clip_index_current (&self) -> usize { 0 } - fn _clip_index_after (&self) -> usize { 0 } - fn _clip_index_previous (&self) -> usize { 0 } - fn _clip_index_next (&self) -> usize { 0 } - fn _color_random (&self) -> ItemColor { ItemColor::random() } -} - -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), - //} - //} -} diff --git a/src/proj_scene.rs b/src/proj_scene.rs index fc948d6c..06f65e74 100644 --- a/src/proj_scene.rs +++ b/src/proj_scene.rs @@ -13,46 +13,38 @@ use super::*; /// Identifying color of scene pub color: ItemTheme, /// Clips in scene, one per track - pub clips: Vec>>>, + pub clips: Pool, } impl Scene { pub const DEFAULT_HEIGHT: usize = 2; - - /// Get currently playing clip, if any - pub fn clip (&self, index: usize) -> Option<&Arc>> { - if let Some(Some(clip)) = self.clips.get(index) { - Some(clip) - } else { - None - } - } - /// Get pulse length of the longest clip in the scene pub fn pulses (&self) -> usize { - self.clips.iter().fold(0, |a, p|{ - a.max(p.as_ref().map(|q|q.try_read().unwrap().length).unwrap_or(0)) + self.clips.read().iter().fold(0, |a, p|{ + a.max(p.read().as_ref().map(|q|q.read().length).unwrap_or(0)) }) } - /// True if all clips in scene are currently playing on given tracks. pub fn is_playing (&self, tracks: &[Track]) -> bool { - self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() - .all(|(track_index, clip)|match clip { - Some(c) => tracks - .get(track_index) - .map(|track|{ - if let Some((_, Some(clip))) = track.sequencer().play_clip() { - *clip.try_read().unwrap() == *c.try_read().unwrap() - } else { - false - } - }) - .unwrap_or(false), - None => true - }) + // Not "playing" if there are no clips + if !self.clips.read().iter().any(|clip|clip.read().is_some()) { + return false + } + // Playing if at least 1 clip is playing + self.clips.read().iter().enumerate().all(|(track_index, clip)|match &*clip.read() { + Some(c) => tracks + .get(track_index) + .map(|track|{ + if let Some((_, Some(clip))) = track.sequencer().play_clip() { + *clip.read() == *c.read() + } else { + false + } + }) + .unwrap_or(false), + None => true + }) } - } @@ -111,8 +103,8 @@ pub trait HasScenes: AsRef> + AsMut> { Self: HasTracks { let scene = Scene { - name: name.map_or_else(||self.scenes_default_name(), |x|x.to_string().into()), - clips: vec![None;self.tracks().len()], + name: name.map_or_else(||self.scenes_default_name(), |x|x.to_string().into()), + clips: Pool::from(vec![None;self.tracks().len()]), color: color.unwrap_or_else(ItemTheme::random), }; self.scenes_mut().push(scene); @@ -121,75 +113,6 @@ pub trait HasScenes: AsRef> + AsMut> { } } -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 { - 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) - } -} - -pub trait HasSceneScroll: HasScenes { - fn scene_scroll (&self) -> usize; -} - -impl HasSceneScroll for Project { - fn scene_scroll (&self) -> usize { - self.scene_scroll - } -} - -impl HasSceneScroll for App { - fn scene_scroll (&self) -> usize { - self.project.scene_scroll() - } -} +pub trait HasSceneScroll: HasScenes { fn scene_scroll (&self) -> usize; } +impl HasSceneScroll for Project { fn scene_scroll (&self) -> usize { self.scene_scroll } } +impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } diff --git a/src/proj_select.rs b/src/proj_select.rs index e061294b..8b3700b8 100644 --- a/src/proj_select.rs +++ b/src/proj_select.rs @@ -13,39 +13,29 @@ pub trait HasSelection: AsRef + AsMut { fn selection_mut (&mut self) -> &mut Selection { self.as_mut() } - /// Get the active track - #[cfg(feature = "track")] - fn selected_track (&self) -> Option<&Track> where Self: HasTracks { + #[cfg(feature = "track")] fn selected_track (&self) -> Option<&Track> where Self: HasTracks { let index = self.selection().track()?; self.tracks().get(index) } - /// Get a mutable reference to the active track - #[cfg(feature = "track")] - fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks { + #[cfg(feature = "track")] fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks { let index = self.selection().track()?; self.tracks_mut().get_mut(index) } - /// Get the active scene - #[cfg(feature = "scene")] - fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes { + #[cfg(feature = "scene")] fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes { let index = self.selection().scene()?; self.scenes().get(index) } - /// Get a mutable reference to the active scene - #[cfg(feature = "scene")] - fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes { + #[cfg(feature = "scene")] fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes { let index = self.selection().scene()?; self.scenes_mut().get_mut(index) } - /// Get the active clip - #[cfg(feature = "clip")] - fn selected_clip (&self) -> Option>> where Self: HasScenes + HasTracks { - self.selected_scene()?.clips.get(self.selection().track()?)?.clone() + #[cfg(feature = "clip")] fn selected_clip (&self) -> Option> where Self: HasScenes + HasTracks { + self.selected_scene()?.clips.read().get(self.selection().track()?).cloned() } } @@ -65,7 +55,11 @@ pub enum Selection { #[cfg(feature = "scene")] Scene(usize), /// A track is selected. #[cfg(feature = "track")] Track(usize), - /// A clip (track × scene) is selected. + /// A clip is selected. + #[cfg(feature = "track")] Clip(usize), + /// A sample is selected. + #[cfg(feature = "track")] Sample(usize), + /// A clip slot (track × scene) is selected. #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, /// A track's MIDI input connection is selected. #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, @@ -90,9 +84,10 @@ impl Selection { #[cfg(feature = "track")] Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()), TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { - (Some(_), Some(s)) => match s.clip(*track) { - Some(clip) => format!("T{track} S{scene} C{}", &clip.try_read().unwrap().name), - None => format!("T{track} S{scene}: Empty") + (Some(_), Some(s)) => match &s.clips.get(*track) { + None => format!("T{track} S{scene}: Empty"), + Some(clip) => format!("T{track} S{scene} C{}", + clip.as_ref().map(|clip|clip.read().name.clone()).unwrap_or_default()), }, _ => format!("T{track} S{scene}: Empty"), }, diff --git a/src/proj_track.rs b/src/proj_track.rs index f44f218e..ff637da3 100644 --- a/src/proj_track.rs +++ b/src/proj_track.rs @@ -40,24 +40,30 @@ impl Track { ..Default::default() }) } + pub fn new_added ( + jack: &Jack<'static>, + clock: Option<&Clock>, + name: &impl AsRef, + color: Option, + clip: Option<&Arc>>, + mins: Arc<[Connect]>, + mouts: Arc<[Connect]>, + ) -> Usually { + let name = name.as_ref(); + Ok(Self { + name: name.into(), + width: (name.len() + 2).max(12), + color: color.unwrap_or_else(ItemTheme::random), + sequencer: Sequencer::new(name, jack, clock, clip, mins.as_ref(), mouts.as_ref())?, + ..Default::default() + }) + } pub fn audio_ins (&self) -> &[AudioInput] { self.devices.first().map(|x|x.audio_ins()).unwrap_or_default() } pub fn audio_outs (&self) -> &[AudioOutput] { self.devices.last().map(|x|x.audio_outs()).unwrap_or_default() } - 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) - })) - } pub fn stop (&mut self) -> Perhaps { self.sequencer.enqueue_next(None); Ok(None) @@ -165,26 +171,13 @@ pub trait HasTracks: AsRef> + AsMut> + HasClock + HasTrack |x|x.to_string().into() ); *self.tracks_last_mut() += 1; - let track = Track { - width: (name.len() + 2).max(12), - color: color.unwrap_or_else(ItemTheme::random), - sequencer: Sequencer::new( - &name, - self.jack(), - Some(self.clock()), - None, - mins.as_ref(), - mouts.as_ref() - )?, - name, - ..Default::default() - }; + let track = Track::new_added(self.jack(), Some(self.clock()), &name, color, None, mins, mouts)?; self.tracks_mut().push(track); let len = self.tracks().len(); let index = len - 1; for scene in self.scenes_mut().iter_mut() { while scene.clips.len() < len { - scene.clips.push(None); + scene.clips.append(None); } } Ok((index, &mut self.tracks_mut()[index])) @@ -195,316 +188,6 @@ pub trait HasTracks: AsRef> + AsMut> + HasClock + HasTrack } } -impl< - T: HasTracks + ScenesView + HasMidiIns + HasMidiOuts + HasTrack -> TracksView for T {} - -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 - } - }) - } - - /// 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_track_names_add (&self) -> impl Draw { - south(self.view_track_add(), self.view_scene_add()) - } - - fn view_track_names_count (&self) -> impl Draw { - south(self.view_track_count(), self.view_scene_count()) - } - - fn view_track_names_cells <'a> (&'a self) -> impl Draw { - iter_east(||self.tracks_with_sizes().map(|(index, track, _x1, _x2)|{ - let b = if self.selection().track() == Some(index) { - track.color.light.term - } else { - track.color.base.term - }; - bg(b, south( - east!( - "·t", - index, - " ", - fg(Rgb(255, 255, 255), bold(true, &track.name)) - ) - .align_nw() - .full_w(), - "" - )) - .exact_w(track_width(index, track)) - .exact_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() - ) - } - - fn view_track_output_cells <'a> (&'a self) -> impl Draw { - iter_east(move||self.tracks_with_sizes().map(move|(index, track, _x1, _x2)|{ - let f = Rgb(255, 255, 255); - let b = track.color.dark.term; - iter_south(move||track.sequencer.midi_outs.iter().map(move|port: &MidiOutput|{ - fg(f, bg(b, east!("·o", index, " ", port.port_name()).full_w().align_w()).exact_h(1)) - })) - .full_h() - .align_nw() - .exact_w(track_width(index, track)) - })) - } - - 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) - } - - fn view_track_devices (&self, theme: ItemTheme) -> impl Draw { - 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_track_device_cells (&self, height: u16) -> impl Draw { - iter_east(move||self.tracks_with_sizes() - .enumerate() - .map(move|(index, (_, track, _x1, _x2))|bg( - track.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(track.width as u16, 2))) - .align_nw() - ) - .exact_wh( - Some(track_width(index, track)), - Some(height + 1), - ))) - } - - fn tracks_devices_height (&self) -> u16 { - let mut h = 2; - for track in self.tracks().iter() { - h = h.max(track.devices.len() * 2); - } - h as u16 - } - - 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 tracks_inputs_height (&self) -> u16 { - let mut height = 0u16; - for track in self.tracks().iter() { - height = height.max(track.sequencer.midi_ins.len() as u16); - } - height - } - - /// 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()) - ) - } - - fn view_track_input_cells (&self, height: u16) -> impl Draw { - iter_east(move||self - .tracks_with_sizes() - .map(move|(index, track, _x1, _x2)|{ - south( - bg(track.color.base.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 "), - ).align_w().full_w()), - iter_south(move||track.sequencer.midi_ins.iter().map(move|port|fg_bg( - Rgb(255, 255, 255), - track.color.dark.term, - east!("·i", index, " ", port.port_name()).align_w().full_w())))) - .align_nw() - .exact_wh(track_width(index, track), 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 outputs_height (&self) -> u16 { - let mut h = 1; - for output in self.midi_outs().iter() { - h += 1 + output.connections.len(); - } - h as u16 - } - - fn view_outputs (&self, theme: ItemTheme) -> impl Draw { - 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 view_track_count (&self) -> impl Draw { - button_3("t", "rack ", east( - self.selection().track().map(|track|east(track, "/")), - self.tracks().len() - ), false) - } - - fn view_scene_count (&self) -> impl Draw { - button_3("s", "cene ", east( - self.selection().scene().map(|scene|east(scene, "/")), - self.scenes().len() - ), false) - } - - 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_input_count (&self) -> impl Draw { - button_2("i", "nput", false) - } - - fn view_track_output_count (&self) -> impl Draw { - button_3("o", "utput", 0, false) - //draw(|to: &mut Tui|{ - //for port in self.midi_outs().iter() { - //let _ = port.port_name().align_w().full_w().draw(to)?; - //} - //Ok(Some(XYWH(0, 0, 0, 0))) - //})), - } - - 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) - } - -} - pub trait HasTrackScroll { fn track_scroll (&self) -> usize; fn tracks_last (&self) -> usize; @@ -546,24 +229,3 @@ impl HasTrackScroll for App { &mut self.project.track_last } } - - //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()) - //} diff --git a/src/tek.rs b/src/tek.rs index e85d0dfd..7850a7d2 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -2,10 +2,6 @@ mod deps; pub use self::deps::*; -mod config; pub use self::config::*; -mod config_bind; pub use self::config_bind::*; -mod config_mode; pub use self::config_mode::*; -mod config_view; pub use self::config_view::*; mod midi_octave; pub use self::midi_octave::*; mod midi_piano; pub use self::midi_piano::*; @@ -45,12 +41,13 @@ mod proj_port; pub use self::proj_port::*; mod proj_scene; pub use self::proj_scene::*; mod proj_select; pub use self::proj_select::*; mod proj_track; pub use self::proj_track::*; -mod proj_pool; pub use self::proj_pool::*; -mod tek_browse; pub use self::tek_browse::*; -mod tek_cli; pub use self::tek_cli::*; -mod tek_ctrl; pub use self::tek_ctrl::*; -mod tek_view; pub use self::tek_view::*; +mod tek_bind; pub use self::tek_bind::*; +mod tek_cli; pub use self::tek_cli::*; +mod tek_config; pub use self::tek_config::*; +mod tek_ctrl; pub use self::tek_ctrl::*; +mod tek_mode; pub use self::tek_mode::*; +mod tek_view; pub use self::tek_view::*; #[cfg(feature = "lv2")] mod device_lv2; #[cfg(feature = "lv2")] pub use self::device_lv2::*; #[cfg(feature = "lv2_gui")] mod device_lv2_gui; #[cfg(feature = "lv2_gui")] pub use self::device_lv2_gui::*; @@ -126,16 +123,18 @@ fn run_new_plain (config: Config) -> Usually<()> { #[namespace(Arc<[Connect]> get_arc_array_connect)] #[namespace(Arc get_arc_str)] #[namespace(Color get_color)] -#[namespace(ControlAxis get_axis)] -#[namespace(Dialog get_dialog)] +#[namespace(ControlAxis ControlAxis::get_axis)] +#[namespace(Dialog Dialog::get_dialog)] #[namespace(ItemColor)] #[namespace(ItemTheme)] #[namespace(MidiClip)] +#[namespace(ClipSlot ClipSlot::eval)] +#[namespace(ClipPool ClipPool::eval)] +#[namespace(SampleSlot SampleSlot::eval)] +#[namespace(SamplePool SamplePool::eval)] #[namespace(Option> get_opt_arc_array_connect)] #[namespace(Option> get_opt_arc_str)] #[namespace(Option get_opt_itemtheme)] -#[namespace(Option>> get_opt_arc_rwlock_midiclip)] -#[namespace(Option>>>> get_opt_vec_opt_arc_rwlock_midiclip)] #[namespace(Option get_opt_bool)] #[namespace(Option get_opt_u16)] #[namespace(Option get_opt_u32)] @@ -242,14 +241,14 @@ fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps { ":focused/message" => matches!(state.dialog, Dialog::Message(..)), ":focused/add_device" => matches!(state.dialog, Dialog::Device(..)), ":focused/browser" => state.dialog.browser().is_some(), - ":focused/pool/import" => matches!(state.project.pool.mode, Some(PoolMode::Import(..))), - ":focused/pool/export" => matches!(state.project.pool.mode, Some(PoolMode::Export(..))), - ":focused/pool/rename" => matches!(state.project.pool.mode, Some(PoolMode::Rename(..))), - ":focused/pool/length" => matches!(state.project.pool.mode, Some(PoolMode::Length(..))), - ":focused/clip" => !state.editor_focused() && matches!(state.selection(), Selection::TrackClip{..}), - ":focused/track" => !state.editor_focused() && matches!(state.selection(), Selection::Track(..)), - ":focused/scene" => !state.editor_focused() && matches!(state.selection(), Selection::Scene(..)), - ":focused/mix" => !state.editor_focused() && matches!(state.selection(), Selection::Mix), + //":focused/pool/import" => matches!(state.project.pool.mode, Some(PoolMode::Import(..))), + //":focused/pool/export" => matches!(state.project.pool.mode, Some(PoolMode::Export(..))), + //":focused/pool/rename" => matches!(state.project.pool.mode, Some(PoolMode::Rename(..))), + //":focused/pool/length" => matches!(state.project.pool.mode, Some(PoolMode::Length(..))), + ":focused/clip" => matches!(state.selection(), Selection::TrackClip{..}), + ":focused/track" => matches!(state.selection(), Selection::Track(..)), + ":focused/scene" => matches!(state.selection(), Selection::Scene(..)), + ":focused/mix" => matches!(state.selection(), Selection::Mix), _ => return err!("not bool: {word}") })).transpose() } @@ -322,17 +321,6 @@ fn get_opt_usize <'a> (state: &App, src: impl Language) -> Perhaps })) } -fn get_axis <'a> (_: &App, src: impl Language) -> Perhaps { - eval_word(src, |word|Ok(match word { - - "X" => ControlAxis::X, "XFine" | "X2" => ControlAxis::XFine, - "Y" => ControlAxis::Y, "YFine" | "Y2" => ControlAxis::YFine, - "Z" => ControlAxis::Z, "ZFine" | "Z2" => ControlAxis::ZFine, - - _ => return err!("unknown axis {word}") - - })) -} fn get_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps> { Ok(None) @@ -370,11 +358,16 @@ fn get_vec_arc_str <'a> (_: &App, _: impl Language) -> Perhaps>> { todo!() } -impl_has!(Clock: |self: App|self.project.clock); -impl_has!(Dialog: |self: App|self.dialog); +impl_has!(Sizer: |self: App|self.size); +impl_has!(Dialog: |self: App|self.dialog); impl_has!(Jack<'static>: |self: App|self.jack); -impl_has!(Pool: |self: App|self.project.pool); -impl_has!(Selection: |self: App|self.project.selection); + +impl_has!(Clock: |self: App|self.project.clock); +impl_has!(Pool: |self: App|self.project.clips); +impl_has!(Pool: |self: App|self.project.samples); +impl_has!(Selection: |self: App|self.project.selection); + +impl_as_ref!(Slot: |self: App|todo!()); impl_as_ref!(Option: |self: App|self.project.as_ref()); impl_as_mut!(Option: |self: App|self.project.as_mut()); @@ -383,21 +376,6 @@ primitive!(u16: try_to_u16); primitive!(usize: try_to_usize); primitive!(isize: try_to_isize); -impl HasClips for App { - fn clip (&self) -> Option>> { - self.project.clip() - } - fn clip_mut (&self) -> &mut Option>> { - self.project.clip_mut() - } - fn clips <'a> (&'a self) -> RwLockReadGuard<'a, Clips> { - self.project.clips() - } - fn clips_mut <'a> (&'a self) -> RwLockWriteGuard<'a, Clips> { - self.project.clips_mut() - } -} - tui_keys!(self: App, input { #[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!"); Self::input_cmd(self, input) @@ -478,86 +456,20 @@ impl App { I, IFine, } -/// 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 |_|{ - self.draw_error(to)?; - self.draw_modes(to)?; - self.draw_debug(to)?; - #[cfg(feature = "prof2")] profiling::finish_frame!(); - Ok(Some(to.area().into())) - //}) +impl ControlAxis { + fn get_axis <'a> (_: &App, src: impl Language) -> Perhaps { + eval_word(src, |word|Ok(match word { + + "X" => ControlAxis::X, "XFine" | "X2" => ControlAxis::XFine, + "Y" => ControlAxis::Y, "YFine" | "Y2" => ControlAxis::YFine, + "Z" => ControlAxis::Z, "ZFine" | "Z2" => ControlAxis::ZFine, + + _ => return err!("unknown axis {word}") + + })) } } -impl App { - 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(()) - } - - #[allow(unused)] fn draw_debug (&self, to: &mut Tui) -> Drawn { - 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) - } -} - -impl_has!(Sizer: |self: App|self.size); - pub trait HasWidth { const MIN_WIDTH: usize; /// Increment track width. @@ -566,46 +478,6 @@ pub trait HasWidth { fn width_dec (&mut self); } -pub fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps { - - eval_word(&src, |word|Ok(match word { - - ":none" => Dialog::None, - - ":options" => Dialog::Options, - - ":device" => Dialog::Device(0), - ":device/prev" => Dialog::Device(0), - ":device/next" => Dialog::Device(0), - - ":help" => Dialog::Help(0), - ":save" => Dialog::Browse( - BrowseTarget::SaveProject, - Browse::new(None).unwrap().into()), - ":load" => Dialog::Browse( - BrowseTarget::LoadProject, - Browse::new(None).unwrap().into()), - - ":import/clip" => Dialog::Browse( - BrowseTarget::ImportClip(Default::default()), - Browse::new(None).unwrap().into()), - ":export/clip" => Dialog::Browse( - BrowseTarget::ExportClip(Default::default()), - Browse::new(None).unwrap().into()), - - ":import/sample" => Dialog::Browse( - BrowseTarget::ImportSample(Default::default()), - Browse::new(None).unwrap().into()), - ":export/sample" => Dialog::Browse( - BrowseTarget::ExportSample(Default::default()), - Browse::new(None).unwrap().into()), - - _ => return Err(format!("not a dialog: {}", src.src()?.unwrap_or("")).into()) - - })) - -} - /// Various possible dialog modes. /// /// ``` @@ -623,6 +495,46 @@ pub fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps { impl Dialog { + fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps { + + eval_word(&src, |word|Ok(match word { + + ":none" => Dialog::None, + + ":options" => Dialog::Options, + + ":device" => Dialog::Device(0), + ":device/prev" => Dialog::Device(0), + ":device/next" => Dialog::Device(0), + + ":help" => Dialog::Help(0), + ":save" => Dialog::Browse( + BrowseTarget::SaveProject, + Browse::new(None).unwrap().into()), + ":load" => Dialog::Browse( + BrowseTarget::LoadProject, + Browse::new(None).unwrap().into()), + + ":import/clip" => Dialog::Browse( + BrowseTarget::ImportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":export/clip" => Dialog::Browse( + BrowseTarget::ExportClip(Default::default()), + Browse::new(None).unwrap().into()), + + ":import/sample" => Dialog::Browse( + BrowseTarget::ImportSample(Default::default()), + Browse::new(None).unwrap().into()), + ":export/sample" => Dialog::Browse( + BrowseTarget::ExportSample(Default::default()), + Browse::new(None).unwrap().into()), + + _ => return Err(format!("not a dialog: {}", src.src()?.unwrap_or("")).into()) + + })) + + } + /// ``` /// let _ = tek::Dialog::welcome(); /// ``` @@ -758,29 +670,142 @@ pub const fn wrap_dec (index: usize, count: usize) -> usize { /// ``` /// let item: tek::MenuItem = Default::default(); /// ``` -#[derive(Clone)] pub struct MenuItem( - pub Arc, - pub Arc<[AppCmd]>, -); - +#[derive(Clone)] pub struct MenuItem(pub Arc, pub Arc<[AppCmd]>); +impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); +impl_default!(MenuItem: Self::new("", [])); +impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } } impl MenuItem { pub fn new (name: impl AsRef, actions: [AppCmd; N]) -> Self { Self(name.as_ref().into(), Arc::new(actions)) } } -impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); - -impl_default!(MenuItem: Self::new("", [])); - -impl PartialEq for MenuItem { - fn eq (&self, other: &Self) -> bool { - self.0 == other.0 - } -} - //impl AsRef> for MenuItems { //fn as_ref (&self) -> &Arc<[MenuItem]> { //&self.0 //} //} + +/// Browses for files to load/save. +/// +/// ``` +/// let browse = tek::Browse::default(); +/// ``` +#[derive(Debug, Clone, Default, PartialEq)] pub struct Browse { + pub cwd: PathBuf, + pub dirs: Vec<(OsString, String)>, + pub files: Vec<(OsString, String)>, + pub filter: String, + pub index: usize, + pub scroll: usize, + pub size: Sizer, +} + +//pub(crate) struct EntriesIterator<'a, S: Screen> { + //pub browser: &'a Browse, + //pub offset: usize, + //pub length: usize, + //pub index: usize, + //_screen: std::marker::PhantomData +//} + +#[derive(Clone, Debug)] pub enum BrowseTarget { + SaveProject, + LoadProject, + ImportSample(Arc>>), + ExportSample(Arc>>), + ImportClip(Arc>>), + ExportClip(Arc>>), +} + +// Commands supported by [Browse] +//#[derive(Debug, Clone, PartialEq)] +//pub enum BrowseCommand { + //Begin, + //Cancel, + //Confirm, + //Select(usize), + //Chdir(PathBuf), + //Filter(Arc), +//} +impl Browse { + pub fn new (cwd: Option) -> Usually { + let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; + let mut dirs = vec![]; + let mut files = vec![]; + for entry in std::fs::read_dir(&cwd)? { + let entry = entry?; + let name = entry.file_name(); + let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); + let meta = entry.metadata()?; + if meta.is_dir() { + dirs.push((name, format!("📁 {decoded}"))); + } else if meta.is_file() { + files.push((name, format!("📄 {decoded}"))); + } + } + Ok(Self { cwd, dirs, files, ..Default::default() }) + } + pub fn to_chdir (&self) -> Usually { Self::new(Some(self.path())) } + pub fn len (&self) -> usize { self.dirs.len() + self.files.len() } + pub fn is_dir (&self) -> bool { self.index < self.dirs.len() } + pub fn is_file (&self) -> bool { self.index >= self.dirs.len() } + pub fn path (&self) -> PathBuf { + self.cwd.join(if self.is_dir() { + &self.dirs[self.index].0 + } else if self.is_file() { + &self.files[self.index - self.dirs.len()].0 + } else { + unreachable!() + }) + } + //fn tui_entries (&self) -> EntriesIterator<'_, Tui> { + //EntriesIterator { + //offset: 0, + //index: 0, + //length: self.dirs.len() + self.files.len(), + //browser: self, + //_screen: Default::default(), + //} + //} +} + +impl PartialEq for BrowseTarget { + fn eq (&self, other: &Self) -> bool { + match self { + Self::ImportSample(_) => false, + Self::ExportSample(_) => false, + Self::ImportClip(_) => false, + Self::ExportClip(_) => false, + #[allow(unused)] t => matches!(other, t) + } + } +} + +pub fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { + let (mut subdirs, mut files) = std::fs::read_dir(dir)? + .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ + let entry = entry.expect("failed to read drectory entry"); + let meta = entry.metadata().expect("failed to read entry metadata"); + if meta.is_file() { + files.push(entry.file_name()); + } else if meta.is_dir() { + subdirs.push(entry.file_name()); + } + (subdirs, files) + }); + subdirs.sort(); + files.sort(); + Ok((subdirs, files)) +} + +impl App { + /// Return reference to content browser if open. + /// + /// ``` + /// assert_eq!(tek::App::default().browser(), None); + /// ``` + pub fn browser (&self) -> Option<&Browse> { + if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } + } +} diff --git a/src/config_bind.rs b/src/tek_bind.rs similarity index 100% rename from src/config_bind.rs rename to src/tek_bind.rs diff --git a/src/tek_browse.rs b/src/tek_browse.rs deleted file mode 100644 index c61646af..00000000 --- a/src/tek_browse.rs +++ /dev/null @@ -1,146 +0,0 @@ -use crate::*; - -/// Browses for files to load/save. -/// -/// ``` -/// let browse = tek::Browse::default(); -/// ``` -#[derive(Debug, Clone, Default, PartialEq)] pub struct Browse { - pub cwd: PathBuf, - pub dirs: Vec<(OsString, String)>, - pub files: Vec<(OsString, String)>, - pub filter: String, - pub index: usize, - pub scroll: usize, - pub size: Sizer, -} - -//pub(crate) struct EntriesIterator<'a, S: Screen> { - //pub browser: &'a Browse, - //pub offset: usize, - //pub length: usize, - //pub index: usize, - //_screen: std::marker::PhantomData -//} - -#[derive(Clone, Debug)] pub enum BrowseTarget { - SaveProject, - LoadProject, - ImportSample(Arc>>), - ExportSample(Arc>>), - ImportClip(Arc>>), - ExportClip(Arc>>), -} - -// Commands supported by [Browse] -//#[derive(Debug, Clone, PartialEq)] -//pub enum BrowseCommand { - //Begin, - //Cancel, - //Confirm, - //Select(usize), - //Chdir(PathBuf), - //Filter(Arc), -//} -impl Browse { - pub fn new (cwd: Option) -> Usually { - let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; - let mut dirs = vec![]; - let mut files = vec![]; - for entry in std::fs::read_dir(&cwd)? { - let entry = entry?; - let name = entry.file_name(); - let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); - let meta = entry.metadata()?; - if meta.is_dir() { - dirs.push((name, format!("📁 {decoded}"))); - } else if meta.is_file() { - files.push((name, format!("📄 {decoded}"))); - } - } - Ok(Self { cwd, dirs, files, ..Default::default() }) - } - pub fn to_chdir (&self) -> Usually { Self::new(Some(self.path())) } - pub fn len (&self) -> usize { self.dirs.len() + self.files.len() } - pub fn is_dir (&self) -> bool { self.index < self.dirs.len() } - pub fn is_file (&self) -> bool { self.index >= self.dirs.len() } - pub fn path (&self) -> PathBuf { - self.cwd.join(if self.is_dir() { - &self.dirs[self.index].0 - } else if self.is_file() { - &self.files[self.index - self.dirs.len()].0 - } else { - unreachable!() - }) - } - //fn tui (&self) -> impl Draw { - //iter_south_fixed(1, ||self.tui_entries(), |entry, _index|entry.origin_w().full_w()) - //} - //fn tui_entries (&self) -> EntriesIterator<'_, Tui> { - //EntriesIterator { - //offset: 0, - //index: 0, - //length: self.dirs.len() + self.files.len(), - //browser: self, - //_screen: Default::default(), - //} - //} -} - -//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 PartialEq for BrowseTarget { - fn eq (&self, other: &Self) -> bool { - match self { - Self::ImportSample(_) => false, - Self::ExportSample(_) => false, - Self::ImportClip(_) => false, - Self::ExportClip(_) => false, - #[allow(unused)] t => matches!(other, t) - } - } -} - -pub fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { - let (mut subdirs, mut files) = std::fs::read_dir(dir)? - .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ - let entry = entry.expect("failed to read drectory entry"); - let meta = entry.metadata().expect("failed to read entry metadata"); - if meta.is_file() { - files.push(entry.file_name()); - } else if meta.is_dir() { - subdirs.push(entry.file_name()); - } - (subdirs, files) - }); - subdirs.sort(); - files.sort(); - Ok((subdirs, files)) -} - -impl App { - /// Return reference to content browser if open. - /// - /// ``` - /// assert_eq!(tek::App::default().browser(), None); - /// ``` - pub fn browser (&self) -> Option<&Browse> { - if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } - } -} diff --git a/src/config.rs b/src/tek_config.rs similarity index 100% rename from src/config.rs rename to src/tek_config.rs diff --git a/src/tek_ctrl.rs b/src/tek_ctrl.rs index 3495b25e..b3d22a13 100644 --- a/src/tek_ctrl.rs +++ b/src/tek_ctrl.rs @@ -5,7 +5,7 @@ impl_from!(AppCmd: |x: BrowseCmd| AppCmd::Browse { command: x }); impl_from!(AppCmd: |x: MidiClipCmd| AppCmd::MidiClip { command: x }); impl_from!(AppCmd: |x: MidiEditCmd| AppCmd::MidiEdit { command: x }); impl_from!(AppCmd: |x: ModeCmd| AppCmd::Mode { command: x }); -impl_from!(AppCmd: |x: PoolCmd| AppCmd::Pool { command: x }); +//impl_from!(AppCmd: |x: PoolCmd| AppCmd::Pool { command: x }); impl_from!(AppCmd: |x: ProjectCmd| AppCmd::Project { command: x }); impl_from!(AppCmd: |x: SceneCmd| AppCmd::Scene { command: x }); impl_from!(AppCmd: |x: ScenesCmd| AppCmd::Scenes { command: x }); @@ -62,9 +62,9 @@ impl_from!(AppCmd: |x: TracksCmd| AppCmd::Tracks { command: x }); #[command(MidiEdit = "edit")] pub fn cmd_midi_edit (&mut self, command: MidiEditCmd) -> Perhaps { Ok(command.dispatch(self)?.map(Into::into)) } - #[command(Pool = "pool")] pub fn cmd_pool (&mut self, command: PoolCmd) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } + //#[command(Pool = "pool")] pub fn cmd_pool (&mut self, command: PoolCmd) -> Perhaps { + //Ok(command.dispatch(self)?.map(Into::into)) + //} #[command(Scene = "scene")] pub fn cmd_scene (&mut self, command: SceneCmd) -> Perhaps { Ok(command.dispatch(self)?.map(Into::into)) } @@ -90,12 +90,11 @@ impl ProjectControl for T {} } } -impl + Namespace + Namespace>> ClockControl for T {} - +impl ClockControl for T where T: HasClock + + Namespace + Namespace + Namespace> +{} #[tek_proc::commands(ClockCmd = "clock")] pub trait ClockControl: HasClock - + Namespace - + Namespace - + Namespace> + + Namespace + Namespace + Namespace> { #[command(SeekUsec = "usec")] fn seek_usec (&mut self, usec: f64) -> Perhaps { self.clock().playhead.update_from_usec(usec); @@ -153,7 +152,7 @@ impl>> + Namespace> + Namespace>> - + Namespace>>>>> + + Namespace > TracksControl for T {} #[tek_proc::commands(TracksCmd)] pub trait TracksControl: HasTracks @@ -166,7 +165,7 @@ impl>> + Namespace> + Namespace>> - + Namespace>>>>> + + Namespace { /// Stop all playing clips #[command(Stop = "stop")] fn tracks_stop_all (&mut self) -> Perhaps { @@ -176,12 +175,12 @@ impl>>>> - ) -> Perhaps { + #[command(Launch = "launch")] fn tracks_launch (&mut self, clips: Option) + -> Perhaps + { if let Some(clips) = clips { - for (clip, track) in clips.iter().zip(self.tracks_mut()) { - track.sequencer.enqueue_next(clip.as_ref()); + for (clip, track) in clips.0.read().iter().zip(self.tracks_mut()) { + track.sequencer.enqueue_next(clip); } } else { for track in self.tracks_mut().iter_mut() { @@ -322,15 +321,11 @@ impl - + Namespace> - + Namespace + + Namespace + Namespace> + Namespace > SceneControl for T {} #[tek_proc::commands(SceneCmd)] pub trait SceneControl: HasScene - + Namespace - + Namespace> - + Namespace + + Namespace + Namespace> + Namespace { #[command(SetSize = "size")] fn scene_set_size (&mut self, _size: usize) -> Perhaps { todo!() @@ -357,23 +352,20 @@ impl { - - #[command(RecordToggle = "rec-toggle")] - fn record_toggle (&mut self, slot: usize) -> Perhaps + #[command(RecToggle = "rec-toggle")] fn record_toggle (&mut self, slot: usize) -> Perhaps where Self: Sized { let sampler = self.sampler_mut(); let recording = sampler.recording.as_ref().map(|x|x.0); - let _ = SamplerCmd::RecordFinish.dispatch(self)?; + let _ = SamplerCmd::RecFinish.dispatch(self)?; // autoslice: continue recording at next slot if recording != Some(slot) { - SamplerCmd::RecordBegin { slot }.dispatch(self) + SamplerCmd::RecBegin { slot }.dispatch(self) } else { Ok(None) } } - - #[command(RecordBegin = "rec-begin")] fn record_begin (&mut self, slot: usize) -> Perhaps { + #[command(RecBegin = "rec-begin")] fn record_begin (&mut self, slot: usize) -> Perhaps { let sampler = self.sampler_mut(); sampler.recording = Some(( slot, @@ -383,7 +375,7 @@ impl Perhaps { + #[command(RecFinish = "rec-finish")] fn record_finish (&mut self) -> Perhaps { let sampler = self.sampler_mut(); let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{ std::mem::swap(sample, &mut sampler.samples.0[*index]); @@ -391,14 +383,14 @@ impl Perhaps { + #[command(RecCancel = "rec-cancel")] fn record_cancel (&mut self) -> Perhaps { self.sampler_mut().recording = None; Ok(None) } #[command(PlaySample = "sample-play")] fn sample_play (&mut self, slot: usize) -> Perhaps { let sampler = self.sampler_mut(); if let Some(ref sample) = sampler.samples.0[slot] { - sampler.voices.try_write().unwrap().push(Sample::play(sample, 0, &u7::from(128))); + sampler.voices.write().push(Sample::play(sample, 0, &u7::from(128))); } Ok(None) } @@ -414,7 +406,7 @@ impl + Namespace + Namespace> - + Namespace>>> + + Namespace > MidiEditControl for T {} #[tek_proc::commands(MidiEditCmd)] pub trait MidiEditControl: HasEditor @@ -423,7 +415,7 @@ impl + Namespace + Namespace> - + Namespace>>> + + Namespace { #[command(Toggle = "toggle")] fn edit_toggle (&mut self) -> Perhaps { if self.is_editing() { @@ -433,9 +425,9 @@ impl>>) -> Perhaps { - Ok(self.editor_mut().as_mut().map(|mut editor|{ - editor.set_clip(clip.as_ref()); + #[command(Show = "show")] fn edit_show (&mut self, clip: ClipSlot) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|editor|{ + editor.mode.clip.0 = clip.0; editor.redraw(); None }).flatten()) @@ -447,7 +439,7 @@ impl Perhaps { - Ok(self.editor_mut().as_mut().map(|mut editor|{ + Ok(self.editor_mut().as_mut().map(|editor|{ editor.put_note(advance); editor.redraw(); None @@ -505,230 +497,227 @@ impl - + Namespace - + Namespace> - + Namespace - + Namespace - + Namespace - + Namespace -> PoolControl for T {} +//impl + //+ Namespace + //+ Namespace> + //+ Namespace + //+ Namespace + //+ Namespace + //+ Namespace +//> PoolControl for T {} -#[tek_proc::commands(PoolCmd = "pool")] pub trait PoolControl: HasPool - + Namespace - + Namespace - + Namespace> - + Namespace - + Namespace - + Namespace - + Namespace -{ - /// Toggle visibility of pool - #[command(Show = "show")] fn show (&mut self, visible: bool) -> Perhaps { - self.pool_mut().visible = visible; - Ok(Some(PoolCmd::Show { visible: !visible })) - } - /// Set selected clip - #[command(Select = "select")] fn select (&mut self, index: usize) -> Perhaps { - self.pool_mut().set_clip_index(index); - Ok(None) - } - /// Delete a clip from the pool - #[command(Delete = "delete")] fn delete (&mut self, index: usize) -> Perhaps { - let clip = self.pool_mut().clips_mut().remove(index).try_read().unwrap().clone(); - Ok(Some(PoolCmd::Add { index, clip })) - } - /// Switch places of two clips in pool - #[command(Swap = "swap")] fn swap (&mut self, index: usize, other: usize) -> Perhaps { - self.pool_mut().clips_mut().swap(index, other); - Ok(Some(PoolCmd::Swap { index, other })) - } - /// Add clip to pool - #[command(Add = "add")] fn clip_add (&mut self, index: usize, clip: MidiClip) -> Perhaps { - let mut index = index; - let clip = Arc::new(RwLock::new(clip.clone())); - let mut clips = self.pool_mut().clips_mut(); - if index >= clips.len() { - index = clips.len(); - clips.push(clip) - } else { - clips.insert(index, clip); - } - Ok(Some(PoolCmd::Delete { index })) - } - /// Set name of clip - #[command(SetName = "set-name")] fn clip_set_name (&mut self, index: usize, name: Arc) -> Perhaps { - let clip = &mut self.pool_mut().clips_mut()[index]; - let old_name = clip.try_read().unwrap().name.clone(); - clip.try_write().unwrap().name = name.clone(); - Ok(Some(PoolCmd::SetName { index, name: old_name })) - } - /// Set length of clip - #[command(SetLength = "set-length")] fn clip_set_length (&mut self, index: usize, length: usize) -> Perhaps { - let clip = &mut self.pool_mut().clips_mut()[index]; - let old_len = clip.try_read().unwrap().length; - clip.try_write().unwrap().length = length; - Ok(Some(PoolCmd::SetLength { index, length: old_len })) - } - /// Set color of clip - #[command(SetColor = "set-color")] fn clip_set_color (&mut self, index: usize, color: ItemColor) -> Perhaps { - let mut color = ItemTheme::from(color); - std::mem::swap(&mut color, &mut self.pool().clips()[index].try_write().unwrap().color); - Ok(Some(PoolCmd::SetColor { index, color: color.base })) - } - /// Enter crop mode - #[command(CropBegin = "crop/begin")] fn crop_begin (&mut self) -> Perhaps { - let index = self.pool().clip_index(); - let length = self.pool().clips()[index].try_read().unwrap().length; - *self.pool_mut().mode_mut() = Some(PoolMode::Length(index, length, ClipLengthFocus::Bar)); - Ok(None) - } - /// Exit crop mode, discard - #[command(CropCancel = "crop/cancel")] fn crop_cancel (&mut self) -> Perhaps { - if let Some(PoolMode::Length(..)) = self.pool_mut().mode_mut().clone() { - *self.pool_mut().mode_mut() = None; - } - Ok(None) - } - #[command(CropSet = "crop/set")] fn crop_set (&mut self, _length: usize) -> Perhaps { - if let Some(PoolMode::Length(clip, ref mut length, ref mut _focus)) - = self.pool_mut().mode_mut().clone() - { - let old_length; - { - let clip = self.pool().clips()[clip].clone();//.try_write().unwrap(); - old_length = Some(clip.try_read().unwrap().length); - clip.try_write().unwrap().length = *length; - } - *self.pool_mut().mode_mut() = None; - return Ok(old_length.map(|l|PoolCmd::CropSet { _length: l })) - } - Ok(None) - } - #[command(CropNext = "crop/next")] fn crop_next (&mut self) -> Perhaps { - if let Some(PoolMode::Length( - _clip, ref mut _length, ref mut focus - )) = self.pool_mut().mode_mut().clone() { - focus.next() - }; - Ok(None) - } - #[command(CropPrev = "crop/prev")] fn crop_prev (&mut self) -> Perhaps { - if let Some(PoolMode::Length( - _clip, ref mut _length, ref mut focus - )) = self.pool_mut().mode_mut().clone() { - focus.prev() - }; - Ok(None) - } - #[command(CropInc = "crop/inc")] fn crop_inc (&mut self) -> Perhaps { - if let Some(PoolMode::Length( - _clip, ref mut length, ref mut focus - )) = self.pool_mut().mode_mut().clone() { - match focus { - ClipLengthFocus::Bar => { *length += 4 * PPQ }, - ClipLengthFocus::Beat => { *length += PPQ }, - ClipLengthFocus::Tick => { *length += 1 }, - } - } - Ok(None) - } - #[command(CropDec = "crop/dec")] fn crop_dec (&mut self) -> Perhaps { - if let Some(PoolMode::Length( - _clip, ref mut length, ref mut focus - )) = self.pool_mut().mode_mut().clone() { - match focus { - ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) }, - ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) }, - ClipLengthFocus::Tick => { *length = length.saturating_sub(1) }, - } - } - Ok(None) - } - /// Enter rename mode - #[command(RenameBegin = "rename/begin")] fn rename_begin (&mut self) -> Perhaps { - let index = self.pool().clip_index(); - let name = self.pool().clips()[index].try_read().unwrap().name.clone(); - *self.pool_mut().mode_mut() = Some(PoolMode::Rename(index, name)); - Ok(None) - } - /// Exit rename mode, discard - #[command(RenameCancel = "rename/cancel")] fn rename_cancel (&mut self) -> Perhaps { - if let Some(PoolMode::Rename(clip, ref mut old_name)) = self.pool_mut().mode_mut().clone() { - self.pool().clips()[clip].try_write().unwrap().name = old_name.clone().into(); - } - Ok(None) - } - /// Exit rename mode, update name - #[command(RenameConfirm = "rename/confirm")] fn rename_confirm (&mut self) -> Perhaps { - Ok(if let Some(PoolMode::Rename(_clip, ref mut old_name)) = self.pool_mut().mode_mut().clone() { - let old_name = old_name.clone(); - *self.pool_mut().mode_mut() = None; - Some(PoolCmd::RenameSet { value: old_name }) - } else { - None - }) - } - #[command(RenameSet = "rename/set")] fn rename_set (&mut self, value: Arc) -> Perhaps { - if let Some(PoolMode::Rename(clip, ref mut _old_name)) = self.pool_mut().mode_mut().clone() { - self.pool().clips()[clip].try_write().unwrap().name = value.clone(); - } - Ok(None) - } - ///// Import from file - //#[command(Browse = "browse")] - //fn browse (&mut self, command: BrowseCmd) -> Perhaps { - //Ok(if let Some(browse) = self.pool_mut().browse.as_mut() { - //command.act(browse)?.map(|command|PoolCmd::Browse{command}) +//#[tek_proc::commands(PolCmd = "pool")] pub trait PoolControl: HasPool + //+ Namespace + //+ Namespace + //+ Namespace> + //+ Namespace + //+ Namespace + //+ Namespace + //+ Namespace +//{ + ///// Toggle visibility of pool + //#[command(Show = "show")] fn show (&mut self, visible: bool) -> Perhaps { + //self.pool_mut().visible = visible; + //Ok(Some(PoolCmd::Show { visible: !visible })) + //} + ///// Set selected clip + //#[command(Select = "select")] fn select (&mut self, index: usize) -> Perhaps { + //self.pool_mut().set_clip_index(index); + //Ok(None) + //} + ///// Delete a clip from the pool + //#[command(Delete = "delete")] fn delete (&mut self, index: usize) -> Perhaps { + //let clip = self.pool_mut().remove(index).read().clone(); + //Ok(Some(PoolCmd::Add { index, clip })) + //} + ///// Switch places of two clips in pool + //#[command(Swap = "swap")] fn swap (&mut self, index: usize, other: usize) -> Perhaps { + //self.pool_mut().swap(index, other); + //Ok(Some(PoolCmd::Swap { index, other })) + //} + ///// Add clip to pool + //#[command(Add = "add")] fn clip_add (&mut self, index: usize, clip: MidiClip) -> Perhaps { + //let mut index = index; + //let clip = Arc::new(RwLock::new(clip.clone())); + //let mut clips = self.pool_mut().clips_mut(); + //if index >= clips.len() { + //index = clips.len(); + //clips.push(clip) + //} else { + //clips.insert(index, clip); + //} + //Ok(Some(PoolCmd::Delete { index })) + //} + ///// Set name of clip + //#[command(SetName = "set-name")] fn clip_set_name (&mut self, index: usize, name: Arc) -> Perhaps { + //Ok(self.pool_mut().clips.read().get(index).map(|clip|{ + //let old_name = clip.read().name.clone(); + //clip.write().name = name.clone(); + //PoolCmd::SetName { index, name: old_name } + //})) + //} + ///// Set length of clip + //#[command(SetLength = "set-length")] fn clip_set_length (&mut self, index: usize, length: usize) -> Perhaps { + //Ok(self.pool_mut().clips.read().get(index).map(|clip|{ + //let old_len = clip.read().length; + //clip.write().length = length; + //PoolCmd::SetLength { index, length: old_len } + //})) + //} + ///// Set color of clip + //#[command(SetColor = "set-color")] fn clip_set_color (&mut self, index: usize, color: ItemColor) -> Perhaps { + //let mut color = ItemTheme::from(color); + //std::mem::swap(&mut color, &mut self.pool().clips.read()[index].write().color); + //Ok(Some(PoolCmd::SetColor { index, color: color.base })) + //} + ///// Enter crop mode + //#[command(CropBegin = "crop/begin")] fn crop_begin (&mut self) -> Perhaps { + //let index = self.pool().clip_index(); + //let length = self.pool().clips.read()[index].read().length; + //*self.pool_mut().mode_mut() = Some(PoolMode::Length(index, length, ClipLengthFocus::Bar)); + //Ok(None) + //} + ///// Exit crop mode, discard + //#[command(CropCancel = "crop/cancel")] fn crop_cancel (&mut self) -> Perhaps { + //if let Some(PoolMode::Length(..)) = self.pool_mut().mode_mut().clone() { + //*self.pool_mut().mode_mut() = None; + //} + //Ok(None) + //} + //#[command(CropSet = "crop/set")] fn crop_set (&mut self, _length: usize) -> Perhaps { + //if let Some(PoolMode::Length(clip, ref mut length, ref mut _focus)) + //= self.pool_mut().mode_mut().clone() + //{ + //let old_length; + //{ + //let clip = self.pool().clips.read()[clip].clone();//.write(); + //old_length = Some(clip.read().length); + //clip.write().length = *length; + //} + //*self.pool_mut().mode_mut() = None; + //return Ok(old_length.map(|l|PoolCmd::CropSet { _length: l })) + //} + //Ok(None) + //} + //#[command(CropNext = "crop/next")] fn crop_next (&mut self) -> Perhaps { + //if let Some(PoolMode::Length( + //_clip, ref mut _length, ref mut focus + //)) = self.pool_mut().mode_mut().clone() { + //focus.next() + //}; + //Ok(None) + //} + //#[command(CropPrev = "crop/prev")] fn crop_prev (&mut self) -> Perhaps { + //if let Some(PoolMode::Length( + //_clip, ref mut _length, ref mut focus + //)) = self.pool_mut().mode_mut().clone() { + //focus.prev() + //}; + //Ok(None) + //} + //#[command(CropInc = "crop/inc")] fn crop_inc (&mut self) -> Perhaps { + //if let Some(PoolMode::Length( + //_clip, ref mut length, ref mut focus + //)) = self.pool_mut().mode_mut().clone() { + //match focus { + //ClipLengthFocus::Bar => { *length += 4 * PPQ }, + //ClipLengthFocus::Beat => { *length += PPQ }, + //ClipLengthFocus::Tick => { *length += 1 }, + //} + //} + //Ok(None) + //} + //#[command(CropDec = "crop/dec")] fn crop_dec (&mut self) -> Perhaps { + //if let Some(PoolMode::Length( + //_clip, ref mut length, ref mut focus + //)) = self.pool_mut().mode_mut().clone() { + //match focus { + //ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) }, + //ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) }, + //ClipLengthFocus::Tick => { *length = length.saturating_sub(1) }, + //} + //} + //Ok(None) + //} + ///// Enter rename mode + //#[command(RenameBegin = "rename/begin")] fn rename_begin (&mut self) -> Perhaps { + //let index = self.pool().clip_index(); + //let name = self.pool().clips.read()[index].read().name.clone(); + //*self.pool_mut().mode_mut() = Some(PoolMode::Rename(index, name)); + //Ok(None) + //} + ///// Exit rename mode, discard + //#[command(RenameCancel = "rename/cancel")] fn rename_cancel (&mut self) -> Perhaps { + //if let Some(PoolMode::Rename(clip, ref mut old_name)) = self.pool_mut().mode_mut().clone() { + //self.pool().clips.read()[clip].write().name = old_name.clone().into(); + //} + //Ok(None) + //} + ///// Exit rename mode, update name + //#[command(RenameConfirm = "rename/confirm")] fn rename_confirm (&mut self) -> Perhaps { + //Ok(if let Some(PoolMode::Rename(_clip, ref mut old_name)) = self.pool_mut().mode_mut().clone() { + //let old_name = old_name.clone(); + //*self.pool_mut().mode_mut() = None; + //Some(PoolCmd::RenameSet { value: old_name }) //} else { //None //}) //} - /// Import clip to pool from file - #[command(Import = "import")] - fn clip_import (&mut self, index: usize, path: PathBuf) -> Perhaps - where Self: Sized - { - let bytes = std::fs::read(&path)?; - let smf = Smf::parse(bytes.as_slice())?; - let mut t = 0u32; - let mut events = vec![]; - for track in smf.tracks.iter() { - for event in track.iter() { - t += event.delta.as_int(); - if let TrackEventKind::Midi { channel, message } = event.kind { - events.push((t, channel.as_int(), message)); - } - } - } - let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None); - for event in events.iter() { - clip.notes[event.0 as usize].push(event.2); - } - Ok(PoolCmd::Add { index, clip }.dispatch(self)?) - } - ///// Export to file - //#[command(Export = "export")] - //fn export (&mut self, command: BrowseCmd) -> Perhaps { - //Ok(if let Some(browse) = self.pool_mut().browse.as_mut() { - //command.act(browse)?.map(|command|PoolCmd::Export{command}) - //} else { - //None - //}) + //#[command(RenameSet = "rename/set")] fn rename_set (&mut self, value: Arc) -> Perhaps { + //if let Some(PoolMode::Rename(clip, ref mut _old_name)) = self.pool_mut().mode_mut().clone() { + //self.pool().clips.read()[clip].write().name = value.clone(); + //} + //Ok(None) //} -} + /////// Import from file + ////#[command(Browse = "browse")] + ////fn browse (&mut self, command: BrowseCmd) -> Perhaps { + ////Ok(if let Some(browse) = self.pool_mut().browse.as_mut() { + ////command.act(browse)?.map(|command|PoolCmd::Browse{command}) + ////} else { + ////None + ////}) + ////} + ///// Import clip to pool from file + //#[command(Import = "import")] + //fn clip_import (&mut self, index: usize, path: PathBuf) -> Perhaps + //where Self: Sized + //{ + //let bytes = std::fs::read(&path)?; + //let smf = Smf::parse(bytes.as_slice())?; + //let mut t = 0u32; + //let mut events = vec![]; + //for track in smf.tracks.iter() { + //for event in track.iter() { + //t += event.delta.as_int(); + //if let TrackEventKind::Midi { channel, message } = event.kind { + //events.push((t, channel.as_int(), message)); + //} + //} + //} + //let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None); + //for event in events.iter() { + //clip.notes[event.0 as usize].push(event.2); + //} + //Ok(PoolCmd::Add { index, clip }.dispatch(self)?) + //} + /////// Export to file + ////#[command(Export = "export")] + ////fn export (&mut self, command: BrowseCmd) -> Perhaps { + ////Ok(if let Some(browse) = self.pool_mut().browse.as_mut() { + ////command.act(browse)?.map(|command|PoolCmd::Export{command}) + ////} else { + ////None + ////}) + ////} +//} impl ModeControl for T where T: HasModeStack - + Namespace>> - + Namespace> - + Namespace + + Namespace>> + Namespace> + Namespace {} - #[tek_proc::commands(ModeCmd)] pub trait ModeControl: HasModeStack - + Namespace>> - + Namespace> - + Namespace + + Namespace>> + Namespace> + Namespace { #[command(Reset = "reset")] fn mode_reset (&mut self, modes: Vec>) -> Perhaps { *self.modes_mut() = modes.into(); @@ -744,36 +733,46 @@ impl ModeControl for T where T: HasModeStack } } -impl> + Namespace> > MidiClipControl for T {} -#[tek_proc::commands(MidiClipCmd)] pub trait MidiClipControl: HasClips - + Namespace> - + Namespace> +pub trait HasClip { + fn clip (&self) -> RwLockReadGuard<'_, Option>>>; + fn clip_mut (&self) -> RwLockWriteGuard<'_, Option>>>; +} +impl>> HasClip for T { + fn clip (&self) -> RwLockReadGuard<'_, Option>>> { + self.as_ref().0.read() + } + fn clip_mut (&self) -> RwLockWriteGuard<'_, Option>>> { + self.as_ref().0.write() + } +} + +impl MidiClipControl for T where T: HasClip + + Namespace> + Namespace> {} +#[tek_proc::commands(MidiClipCmd)] pub trait MidiClipControl: HasClip + + Namespace> + Namespace> { /// Create a clip at the current position if it does not already exist. #[command(Upsert = "upsert")] fn clip_upsert (&mut self) -> Perhaps { - let clip = self.clip(); - //let old = self.scenes[scene].clips[track].clone(); - //self.scenes[scene].clips[track] = clip; - //old - todo!() + if self.clip().is_some() { + Ok(None) + } else { + *self.clip_mut() = Some(Default::default()); + Ok(None) + } } #[command(SetColor = "color")] fn clip_set_color (&mut self, color: Option) -> Perhaps { - Ok(self.clip().map(|clip|MidiClipCmd::SetColor { - color: Some(clip.write().set_color(color)) - })) + Ok(self.clip().as_ref().map(|clip|MidiClipCmd::SetColor { color: Some(clip.write().set_color(color)) })) } /// Toggle looping for the active clip #[command(SetLoop = "loop")] fn clip_toggle_loop (&mut self, looping: Option) -> Perhaps { - Ok(self.clip().map(|clip|MidiClipCmd::SetLoop { - looping: Some(clip.write().toggle_loop(looping)) - })) + Ok(self.clip().as_ref().map(|clip|MidiClipCmd::SetLoop { looping: Some(clip.write().toggle_loop(looping)) })) } } -impl + Namespace + Namespace>> BrowseControl for T {} + +impl BrowseControl for T where T: + Namespace + Namespace + Namespace> {} #[tek_proc::commands(BrowseCmd = "browse")] pub trait BrowseControl: - Namespace + - Namespace + - Namespace> + Namespace + Namespace + Namespace> { /// Toggle visibility of browser #[command(Show = "show")] fn show (&mut self) -> Perhaps { @@ -793,11 +792,9 @@ impl + Namespace + Namespace>> BrowseContr } } -#[tek_proc::commands(AxisCmd)] -impl App { - /// Increment a given data axis. - #[command(Inc = "inc")] - pub fn inc (&mut self, axis: ControlAxis) -> Perhaps { +#[tek_proc::commands(AxisCmd)] impl App { + /// Increment the given data axis. + #[command(Inc = "inc")] pub fn inc (&mut self, axis: ControlAxis) -> Perhaps { Ok(match (&self.dialog, axis) { (Dialog::None, _) => todo!(), (Dialog::Menu(_, _), ControlAxis::Y) => { @@ -807,9 +804,8 @@ impl App { _ => todo!() }) } - /// Decrement a given data axis. - #[command(Dec = "dec")] - pub fn dec (&mut self, axis: ControlAxis) -> Perhaps { + /// Decrement the given data axis. + #[command(Dec = "dec")] pub fn dec (&mut self, axis: ControlAxis) -> Perhaps { Ok(match (&self.dialog, axis) { (Dialog::None, _) => None, (Dialog::Menu(_, _), ControlAxis::Y) => { diff --git a/src/config_mode.rs b/src/tek_mode.rs similarity index 100% rename from src/config_mode.rs rename to src/tek_mode.rs diff --git a/src/tek_view.rs b/src/tek_view.rs index ffb6ca74..4b3c846c 100644 --- a/src/tek_view.rs +++ b/src/tek_view.rs @@ -1,5 +1,1070 @@ 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 |_|{ + 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 { + 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, + Arc> +>>>); + +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) -> 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" => { + 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 = 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! 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|app.view_scenes_clips()), + 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 { + 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 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) -> impl Draw { + 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 + '_ { + 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 + '_ { + 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 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 +{} + +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 + } + }) + } + + /// 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_track_names_add (&self) -> impl Draw { + south(self.view_track_add(), self.view_scene_add()) + } + + fn view_track_names_count (&self) -> impl Draw { + south(self.view_track_count(), self.view_scene_count()) + } + + fn view_track_names_cells <'a> (&'a self) -> impl Draw { + iter_east(||self.tracks_with_sizes().map(|(index, track, _x1, _x2)|{ + let b = if self.selection().track() == Some(index) { + track.color.light.term + } else { + track.color.base.term + }; + bg(b, south( + east!( + "·t", + index, + " ", + fg(Rgb(255, 255, 255), bold(true, &track.name)) + ) + .align_nw() + .full_w(), + "" + )) + .exact_w(track_width(index, track)) + .exact_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() + ) + } + + fn view_track_output_cells <'a> (&'a self) -> impl Draw { + iter_east(move||self.tracks_with_sizes().map(move|(index, track, _x1, _x2)|{ + let f = Rgb(255, 255, 255); + let b = track.color.dark.term; + iter_south(move||track.sequencer.midi_outs.iter().map(move|port: &MidiOutput|{ + fg(f, bg(b, east!("·o", index, " ", port.port_name()).full_w().align_w()).exact_h(1)) + })) + .full_h() + .align_nw() + .exact_w(track_width(index, track)) + })) + } + + 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) + } + + fn view_track_devices (&self, theme: ItemTheme) -> impl Draw { + 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_track_device_cells (&self, height: u16) -> impl Draw { + iter_east(move||self.tracks_with_sizes() + .enumerate() + .map(move|(index, (_, track, _x1, _x2))|bg( + track.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(track.width as u16, 2))) + .align_nw() + ) + .exact_wh( + Some(track_width(index, track)), + Some(height + 1), + ))) + } + + fn tracks_devices_height (&self) -> u16 { + let mut h = 2; + for track in self.tracks().iter() { + h = h.max(track.devices.len() * 2); + } + h as u16 + } + + 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 tracks_inputs_height (&self) -> u16 { + let mut height = 0u16; + for track in self.tracks().iter() { + height = height.max(track.sequencer.midi_ins.len() as u16); + } + height + } + + /// 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()) + ) + } + + fn view_track_input_cells (&self, height: u16) -> impl Draw { + iter_east(move||self + .tracks_with_sizes() + .map(move|(index, track, _x1, _x2)|{ + south( + bg(track.color.base.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 "), + ).align_w().full_w()), + iter_south(move||track.sequencer.midi_ins.iter().map(move|port|fg_bg( + Rgb(255, 255, 255), + track.color.dark.term, + east!("·i", index, " ", port.port_name()).align_w().full_w())))) + .align_nw() + .exact_wh(track_width(index, track), 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 outputs_height (&self) -> u16 { + let mut h = 1; + for output in self.midi_outs().iter() { + h += 1 + output.connections.len(); + } + h as u16 + } + + fn view_outputs (&self, theme: ItemTheme) -> impl Draw { + 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 view_track_count (&self) -> impl Draw { + button_3("t", "rack ", east( + self.selection().track().map(|track|east(track, "/")), + self.tracks().len() + ), false) + } + + fn view_scene_count (&self) -> impl Draw { + button_3("s", "cene ", east( + self.selection().scene().map(|scene|east(scene, "/")), + self.scenes().len() + ), false) + } + + 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_input_count (&self) -> impl Draw { + button_2("i", "nput", false) + } + + fn view_track_output_count (&self) -> impl Draw { + button_3("o", "utput", 0, false) + //draw(|to: &mut Tui|{ + //for port in self.midi_outs().iter() { + //let _ = port.port_name().align_w().full_w().draw(to)?; + //} + //Ok(Some(XYWH(0, 0, 0, 0))) + //})), + } + + 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) + } + +} + +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 { + 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) + } +} + +impl ClipsView for T {} + +pub trait ClipsView: TracksView + ScenesView { + + /// Draw clips per scene + fn view_scenes_clips (&self) -> impl Draw { + let select = self.selection(); + let editor = self.editor(); + let editing = self.is_editing(); + self.with_clips_size(true, iter_east(move||self.tracks_with_sizes() + .map(move|(track_index, track, _, _)|iter_south(move||self.scenes_with_sizes() + .map(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_selected = 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_selected, editor).full_wh() + ).full_wh() + ).exact_wh( + view_scene_w(track, select, track_index, editor.as_ref()), + view_scene_y(select, scene_index, editor.as_ref()), + ) + }) + ) + .exact_w(track.width as u16)) + )).align_c() + } + +} + pub fn draw_sample ( to: &mut Tui, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool ) -> Usually { @@ -199,12 +1264,17 @@ pub fn view_scene_name <'a> ( } pub fn view_scene_name_theme (scene: &Scene, track_index: usize) -> (Arc, ItemTheme) { - if let Some(Some(clip)) = &scene.clips.get(track_index) { - let clip = clip.try_read().unwrap(); - (format!(" ⏹ {}", &clip.name).into(), clip.color) - } else { - (" ⏹ -- ".into(), ItemTheme::G[32]) - } + 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_scene_bg ( @@ -480,6 +1550,82 @@ pub fn view_wrap <'a> (bg: Color, fg: Color, content: impl Draw) 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,_,_|{ @@ -551,3 +1697,87 @@ pub trait InteriorMutable: Gettable { } } } + + //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 { + //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; + //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), + //} + //} +//} diff --git a/tengri b/tengri index 79f9cfcb..7237121a 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 79f9cfcbf93db626d031ab86abbbdf5c29d80a48 +Subproject commit 7237121a508556ee98714a6af387e325a371e368