diff --git a/src/app.rs b/src/app.rs deleted file mode 100644 index 4bb2ea60..00000000 --- a/src/app.rs +++ /dev/null @@ -1,636 +0,0 @@ -use crate::*; - -/// Total application state. -/// -/// ``` -/// use tek::{HasTracks, HasScenes, TracksView, ScenesView}; -/// let mut app = tek::App::default(); -/// let _ = app.scenes_add_one(None, None).unwrap(); -/// let _ = app.update_clock(); -/// app.project.editor = Some(Default::default()); -/// //let _: Vec<_> = app.project.inputs_with_sizes().collect(); -/// //let _: Vec<_> = app.project.outputs_with_sizes().collect(); -/// let _: Vec<_> = app.project.tracks_with_sizes().collect(); -/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect(); -/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect(); -/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect(); -/// let _ = app.project.w(); -/// //let _ = app.project.w_sidebar(); -/// //let _ = app.project.w_tracks_area(); -/// let _ = app.project.h(); -/// //let _ = app.project.h_tracks_area(); -/// //let _ = app.project.h_inputs(); -/// //let _ = app.project.h_outputs(); -/// let _ = app.project.h_scenes(); -/// ``` -#[derive(Default, Debug)] -#[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(ItemColor)] -#[namespace(ItemTheme)] -#[namespace(MidiClip)] -#[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)] -#[namespace(Option get_opt_usize)] -#[namespace(PathBuf get_path_buf)] -#[namespace(Vec> get_vec_arc_str)] -#[namespace(bool get_bool)] -#[namespace(f64 get_f64)] -#[namespace(isize)] -#[namespace(u16 get_u16)] -#[namespace(u32)] -#[namespace(u8)] -#[namespace(usize get_usize)] -pub struct App { - /// Exit flag - pub exit: Exit, - /// Base color. - pub color: ItemTheme, - /// Must not be dropped for the duration of the process - pub jack: Jack<'static>, - /// Display size - pub size: Sizer, - /// Performance counter - pub perf: PerfModel, - /// Available view modes and input bindings - pub config: Arc, - /// Currently selected mode - pub modes: Arc>>>, - /// Undo history - pub history: Vec<(AppCommand, Option)>, - /// Dialog overlay - pub dialog: Dialog, - /// Contains all recently created clips. - pub pool: Pool, - /// Contains the currently edited musical arrangement - pub project: Project, - /// Error, if any - pub error: Arc>>>, - #[cfg(feature = "prof2")] - /// Tracing guard - pub guard: Option>>, -} - -impl App { - /// Create a new application instance from a backend, project, config, and mode - /// - /// ``` - /// let mut proj = tek::Project::default(); - /// proj.jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack"); - /// let mut conf = tek::Config::load(tek::Config::default(), "(mode hello)").unwrap(); - /// let tek = tek::App::new(None, proj, conf.into(), "hello"); - /// ``` - pub fn new ( - exit: Option, - project: Project, - config: Arc, - mode: impl AsRef - ) -> Self { - App { - exit: exit.unwrap_or_default(), - jack: project.jack.clone(), - color: ItemTheme::random(), - dialog: Dialog::welcome(), - modes: Arc::new(RwLock::new(vec![mode.as_ref().into()])), - config, - project, - ..Default::default() - } - } -} - -fn get_arc_str <'a> (_: &App, src: impl Language) -> Perhaps> { - Ok(src.src()?.map(|x|x.into())) -} - -fn get_u16 <'a> (state: &App, src: impl Language) -> Perhaps { - Ok(Some(match src.word()? { - Some(":w/sidebar") => state.project.w_sidebar(state.editor().is_some()), - Some(":h/sample-detail") => 6.max(state.size.h() as u16 * 3 / 9), - _ => return try_to_u16(src) - })) -} - -fn get_usize <'a> (state: &App, src: impl Language) -> Perhaps { - Ok(Some(match src.word()? { - Some(":scene-count") => state.scenes().len(), - Some(":track-count") => state.tracks().len(), - Some(":device-kind") => state.dialog.device_kind().unwrap_or(0), - Some(":device-kind/next") => state.dialog.device_kind_next().unwrap_or(0), - Some(":device-kind/prev") => state.dialog.device_kind_prev().unwrap_or(0), - _ => return try_to_usize(src) - })) -} - -fn get_f64 <'a> (_state: &App, _src: impl Language) -> Perhaps { - todo!() -} - -fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps { - src.word()?.map(|word|Ok(match word { - "Y" => true, - "N" => false, - ":mode/editor" => state.project.editor.is_some(), - ":focused/dialog" => !matches!(state.dialog, Dialog::None), - ":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.pool.mode, Some(PoolMode::Import(..))), - ":focused/pool/export" => matches!(state.pool.mode, Some(PoolMode::Export(..))), - ":focused/pool/rename" => matches!(state.pool.mode, Some(PoolMode::Rename(..))), - ":focused/pool/length" => matches!(state.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), - _ => return err!("not bool: {word}") - })).transpose() -} - -#[allow(unused)] -fn get_selection <'a> (state: &App, src: impl Language) -> Perhaps { - src.word()?.map(|word|Ok(match word { - ":select/scene" => state.selection().select_scene(state.tracks().len()), - ":select/scene/next" => state.selection().select_scene_next(state.scenes().len()), - ":select/scene/prev" => state.selection().select_scene_prev(), - ":select/track" => state.selection().select_track(state.tracks().len()), - ":select/track/next" => state.selection().select_track_next(state.tracks().len()), - ":select/track/prev" => state.selection().select_track_prev(), - _ => return err!("not selection: {word}") - })).transpose() -} - -fn get_color <'a> (_: &App, src: impl Language) -> Perhaps { - if let Some(expr) = src.expr()? { - match (expr.head()?, expr.tail()?) { - (Some("g"), Some(tail)) => { - let n = try_to_u8(tail.head().map_err(Into::into))? - .ok_or(LanguageError::domain("not gray"))?; - Ok(Some(Color::Rgb(n, n, n))) - }, - (Some("rgb"), Some(tail)) => { - let r = try_to_u8(tail.head().map_err(Into::into))? - .ok_or(LanguageError::domain("not red"))?; - let g = try_to_u8(tail.tail().head().map_err(Into::into))? - .ok_or(LanguageError::domain("not green"))?; - let b = try_to_u8(tail.tail().tail().head().map_err(Into::into))? - .ok_or(LanguageError::domain("not blue"))?; - Ok(Some(Color::Rgb(r, g, b))) - }, - (Some(_), _) => return err!("not a color expression: {expr}"), - (None, _) => return err!("not a color expression: {expr}"), - } - } else if let Ok(Some(sym)) = src.word() { - Ok(match sym { - ":color/bg" => Some(Color::Rgb(28, 32, 36)), - ":color/fg" => Some(Color::Rgb(98, 92, 96)), - _ => return err!("not a color: {sym}") - }) - } else { - return err!("not a color: {:?}", src.src()?) - } -} - -#[allow(unused)] -fn get_opt_u7 <'a> (state: &App, src: impl Language) -> Perhaps> { - eval_word(src, |word|Ok(match word { - - ":editor/pitch" => Some(( - state.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8 - ).into()), - - _ => return err!("unknown midi note: {word}") - - })) -} - -fn get_opt_usize <'a> (state: &App, src: impl Language) -> Perhaps> { - eval_word(src, |word|Ok(match word { - - ":selected/scene" => state.selection().scene(), - ":selected/track" => state.selection().track(), - - _ => return err!("unknown opt: {word}") - - })) -} - -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) -} - -fn get_opt_u32 <'a> (_: &App, _src: impl Language) -> Perhaps> { - Ok(None) -} - -fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps> { - todo!() -} - -fn get_opt_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps>> { - todo!() -} - -fn get_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps> { - todo!() -} - -fn get_opt_arc_str <'a> (_: &App, _: impl Language) -> Perhaps>> { - todo!() -} - -fn get_opt_bool <'a> (_: &App, _: impl Language) -> Perhaps> { - todo!() -} - -fn get_path_buf <'a> (_: &App, _: impl Language) -> Perhaps { - todo!() -} - -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!(Jack<'static>: |self: App|self.jack); -impl_has!(Pool: |self: App|self.pool); -impl_has!(Selection: |self: App|self.project.selection); -impl_as_ref!(Option: |self: App|self.project.as_ref()); -impl_as_mut!(Option: |self: App|self.project.as_mut()); - -primitive!(u8: try_to_u8); -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_command(self, input) -}); - -impl App { - - fn input_command (state: &mut Self, input: &TuiEvent) -> Usually<()> { - if let Some(bindings) = Self::input_command_find(state, input) { - return Self::input_command_exec(state, bindings) - } - Ok(()) - } - - fn input_command_find (state: &mut Self, input: &TuiEvent) -> Option>]>> { - for name in state.modes.read().iter().rev() { - let mode = state.config.get_mode(name).as_ref().map(Arc::clone); - if let Some(mode) = mode { - let binds = state.config.binds.clone(); - for id in mode.keys.iter() { - if let Some(event_map) = binds.find(id) - && let Some(bindings) = event_map.query(input) { - return Some(bindings.into()) - } - } - } - } - None - } - - fn input_command_exec (state: &mut Self, bindings: Arc<[Binding>]>) -> Usually<()> { - for binding in bindings.iter() { - for command in binding.commands.iter() { - if Self::command::(state, command)? { - return Ok(()) - } - } - } - Ok(()) - } - - fn command < - T: Dispatch + Clone + Into - > (state: &mut Self, src: &str) -> Usually where - Self: Namespace, - { - if let Some(command) = Namespace::::namespace(state, &src)? { - return Self::command_dispatch(state, command.into()) - } - Ok(false) - } - - fn command_dispatch (state: &mut Self, command: AppCommand) -> Usually where - Self: Namespace, - { - match command.clone().dispatch(state) { - Err(err) => { - state.history.push((command.into(), None)); - return Err(err) - }, - Ok(undo) => { - state.history.push((command.into(), undo.map(Into::into))); - return Ok(true) - } - } - } -} - -#[tek_proc::commands(AppCommand)] -impl App { - - /// Exit the application. - #[command(Exit = "exit")] - pub fn command_exit (&mut self) -> Perhaps { - self.exit.exit(); - Ok(None) - } - - /// Cancel current selection. - #[command(Cancel = "cancel")] - pub fn command_cancel (&mut self) -> Perhaps { - todo!("cancel") - } - - /// Confirm current selection. - #[command(Confirm = "confirm")] - pub fn command_confirm (&mut self) -> Perhaps { - Ok(match self.dialog.clone() { - Dialog::Menu(index, items) => { - for command in items[index].1.iter() { - App::command_dispatch(self, command.clone())?; - } - None - } - _ => todo!(), - }) - } - - /// Swap currently active modal dialog. - /// - /// ``` - /// let _ = tek::App::default().command_dialog(tek::Dialog::welcome()); - /// ``` - #[command(Dialog = "dialog")] - pub fn command_dialog (&mut self, dialog: Dialog) -> Perhaps { - let mut dialog = dialog.clone(); - std::mem::swap(&mut self.dialog, &mut dialog); - Ok(Some(AppCommand::Dialog { dialog })) - } - - #[command(Mode = "mode")] - pub fn command_mode (&mut self, command: ModeCommand) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } - - #[command(Project = "session")] - pub fn command_session (&mut self, command: ProjectCommand) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } - - #[command(Axis = "axis")] - pub fn command_axis (&mut self, command: AxisCommand) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } - - #[command(Browse = "browse")] - pub fn command_browse (&mut self, command: BrowseCommand) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } - - #[command(MidiClip = "clip")] - pub fn command_midi_clip (&mut self, command: MidiClipCommand) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } - - #[command(MidiEdit = "edit")] - pub fn command_midi_edit (&mut self, command: MidiEditCommand) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } - - #[command(Pool = "pool")] - pub fn command_pool (&mut self, command: PoolCommand) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } - - #[command(Scene = "scene")] - pub fn command_scene (&mut self, command: SceneCommand) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } - - #[command(Scenes = "scenes")] - pub fn command_scenes (&mut self, command: ScenesCommand) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } - - #[command(Track = "track")] - pub fn command_track (&mut self, command: TrackCommand) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } - - #[command(Tracks = "tracks")] - pub fn command_tracks (&mut self, command: TracksCommand) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } - -} - -/// A control axis. -/// -/// ``` -/// let axis = tek::ControlAxis::X; -/// ``` -#[derive(Debug, Copy, Clone)] pub enum ControlAxis { - X, XFine, - Y, YFine, - Z, ZFine, - I, IFine, -} - -#[tek_proc::commands(AxisCommand)] -impl App { - /// Increment a 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) => { - AppCommand::Dialog { dialog: self.dialog.menu_next() }.dispatch(self)?; - None - }, - _ => todo!() - }) - } - /// Decrement a 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) => { - AppCommand::Dialog { dialog: self.dialog.menu_prev() }.dispatch(self)?; - None - }, - _ => todo!() - }) - } -} - -impl_from!(AppCommand: |x: AxisCommand| AppCommand::Axis { command: x }); -impl_from!(AppCommand: |x: BrowseCommand| AppCommand::Browse { command: x }); -impl_from!(AppCommand: |x: MidiClipCommand| AppCommand::MidiClip { command: x }); -impl_from!(AppCommand: |x: MidiEditCommand| AppCommand::MidiEdit { command: x }); -impl_from!(AppCommand: |x: ModeCommand| AppCommand::Mode { command: x }); -impl_from!(AppCommand: |x: PoolCommand| AppCommand::Pool { command: x }); -impl_from!(AppCommand: |x: ProjectCommand| AppCommand::Project { command: x }); -impl_from!(AppCommand: |x: SceneCommand| AppCommand::Scene { command: x }); -impl_from!(AppCommand: |x: ScenesCommand| AppCommand::Scenes { command: x }); -impl_from!(AppCommand: |x: TrackCommand| AppCommand::Track { command: x }); -impl_from!(AppCommand: |x: TracksCommand| AppCommand::Tracks { command: x }); - -/// 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 { - 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. - fn width_inc (&mut self); - /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. - fn width_dec (&mut self); -} - -pub fn view_templates <'a> (state: &'a App) -> impl Draw + use<'a> { - let height = (state.config.modes.len() * 2) as u16; - draw(move |to: &mut Tui|{ - let mut index = 0; - state.config.modes.for_each(&mut |id: &str, profile: &Mode| { - let b = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; - let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); - let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); - let fg1 = Rgb(224, 192, 128); - let fg2 = Rgb(224, 128, 32); - let field_name = fg(fg1, name).align_w().full_w(); - let field_id = fg(fg2, id).align_e().full_w(); - let field_info = info.align_w().full_w(); - let _ = bg(b, south(above(field_name, field_id), field_info)) - .full_w().exact_h(2).push_y((2 * index) as u16).draw(to); - index += 1; - }); - Ok(Some(to.area().into())) - }).min_w(30).exact_h(height) -} - -pub fn draw_status_bar (to: &mut Tui, state: &App) -> Drawn { - bar(Split::South, 1u16, - dim(true, "Mode"), - bold(true, state.modes.read().get(0)) - ).push_x(1).draw(to) -} diff --git a/src/clock_view.rs b/src/clock_view.rs deleted file mode 100644 index 0cffcde3..00000000 --- a/src/clock_view.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::*; - -/// Contains memoized renders of clock values. -/// -/// Performance optimization. -#[derive(Debug)] pub struct ClockView { - pub sr: Memo, String>, - pub buf: Memo, String>, - pub lat: Memo, String>, - pub bpm: Memo, String>, - pub beat: Memo, String>, - pub time: Memo, String>, -} - -impl_default!(ClockView: { - let mut beat = String::with_capacity(16); - let _ = write!(beat, "{}", Self::BEAT_EMPTY); - let mut time = String::with_capacity(16); - let _ = write!(time, "{}", Self::TIME_EMPTY); - let mut bpm = String::with_capacity(16); - let _ = write!(bpm, "{}", Self::BPM_EMPTY); - Self { - beat: Memo::new(None, beat), - time: Memo::new(None, time), - bpm: Memo::new(None, bpm), - sr: Memo::new(None, String::with_capacity(16)), - buf: Memo::new(None, String::with_capacity(16)), - lat: Memo::new(None, String::with_capacity(16)), - } -}); - -impl ClockView { - pub const BEAT_EMPTY: &'static str = "-.-.--"; - pub const TIME_EMPTY: &'static str = "-.---s"; - pub const BPM_EMPTY: &'static str = "---.---"; - pub fn update_clock (cache: &Arc>, clock: &Clock, compact: bool) { - let rate = clock.timebase.sr.get(); - let chunk = clock.chunk.load(Relaxed) as f64; - let lat = chunk / rate * 1000.; - let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); - let mut cache = cache.try_write().unwrap(); - - cache.buf.update( - Some(chunk), rewrite!(buf, "{chunk}") - ); - - cache.lat.update( - Some(lat), rewrite!(buf, "{lat:.1}ms") - ); - - cache.sr.update( - Some((compact, rate)), |buf: &mut String,_,_|{ - buf.clear(); - if compact { - write!(buf, "{:.1}kHz", rate / 1000.) - } else { - write!(buf, "{:.0}Hz", rate) - } - } - ); - - if let Some(now) = clock.started.try_read().unwrap().as_ref().map(delta) { - let pulse = clock.timebase.usecs_to_pulse(now); - let time = now/1000000.; - let bpm = clock.timebase.bpm.get(); - cache.beat.update(Some(pulse), |buf, _, _|{ - buf.clear(); - clock.timebase.format_beats_1_to(buf, pulse) - }); - cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time)); - cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm)); - } else { - cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY)); - cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY)); - cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY)); - } - } -} diff --git a/src/config.rs b/src/config.rs index 2eb14ab3..0d1d5b19 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,13 +7,10 @@ use notify_debouncer_full::notify::{RecursiveMode, PollWatcher, Config as Notify /// ``` /// use ::{std::sync::Arc, tek::{Config, Modes, Mode}}; /// let config: Config = Default::default(); -/// Modes::add(&config.modes, stringify!( -/// (mode :menu (name Menu) -/// (info Mode selector.) (keys :axis/y :confirm) -/// (view (bg (g 0) (bsp/s :ports/out -/// (bsp/n :ports/in -/// (bg (g 30) (bsp/s (fixed/y 7 :logo) -/// (fill :dialog/menu)))))))))).unwrap(); +/// let mode = stringify!(mode :menu (name Menu) (info Mode selector.) (keys Y Confirm) +/// (view (bg (g 0) (bsp/s PortsOut (bsp/n PortsIn +/// (bg (g 30) (bsp/s (exact/y 7 Logo) (fill Dialog)))))))); +/// Modes::add(&config.modes, mode).unwrap(); /// let mode: Arc = config.get_mode(":menu").unwrap(); /// ``` #[derive(Default, Debug)] @@ -60,7 +57,7 @@ impl Config { if let Some(expr) = src.expr()? { match expr.head()? { Some("mode") => Modes::add(&config.as_ref().modes, expr.tail()), - Some("keys") => Binds::add(&config.as_ref().binds, expr.tail()), + Some("keys") => config.as_ref().binds.add(expr.tail()), Some("view") => Views::add(&config.as_ref().views, expr.tail()), _ => return Err(format!("Config::load: expected view/keys/mode expr, got: {src:?}").into()) }?; diff --git a/src/config_bind.rs b/src/config_bind.rs index 440feab3..76536114 100644 --- a/src/config_bind.rs +++ b/src/config_bind.rs @@ -8,49 +8,13 @@ pub struct Binds(pub Arc>>); impl Binds { - pub fn add <'a> (binds: &Binds, expr: impl Language) -> UsuallyRef<'a, ()> { + pub fn add <'a> (&self, expr: impl Language) -> UsuallyRef<'a, ()> { let name = expr.head()?.ok_or("bind: missing name")?; let body = expr.tail()?.unwrap_or(""); - binds.0.try_write().unwrap().insert(name.into(), { - let mut map: Bind> = Bind::new(); - body.each((), |_, item: &str|if matches!(item.expr().head(), Ok(Some("see"))) { - Binds::add_ref(&mut map, item) - } else if let Ok(Some(_word)) = item.expr().head().word() { - Binds::add_def(&mut map, item) - } else { - return err!("Binds::add: unexpected: {item:?}") - })?; - map.into() - }); + self.0.try_write().unwrap().insert(name.into(), Bind::load(body)?.into()); Ok(()) } - fn add_ref (_map: &mut Bind>, _item: &str) -> Usually<()> { - // TODO - Ok(()) - } - - fn add_def (map: &mut Bind>, item: &str) -> Usually<()> { - let expr = item.expr()?; - let head = expr.head()?; - if let Some(event) = TuiKey::from_dsl(&head)?.to_crossterm() { - let mut commands: Vec> = vec![]; - item.expr()?.tail()?.each((), |_, def|Ok(commands.push(def.into())))?; - map.add(TuiEvent(event), Binding { - commands: commands.into(), - condition: None, - description: None, - source: None - }); - Ok(()) - } else if Some(":char") == item.expr()?.head()? { - // TODO - Ok(()) - } else { - err!("Binds::add: invalid key: {:?}", item.expr()?.head()?) - } - } - pub fn clear (&self) { *self.0.write() = Default::default(); } @@ -86,7 +50,7 @@ pub struct Bind( pub BTreeMap>> ); -/// A sequence of zero or more commands (e.g. [AppCommand]), +/// A sequence of zero or more commands (e.g. [AppCmd]), /// optionally filtered by [Condition] to form layers. /// /// ``` @@ -124,7 +88,48 @@ impl Default for Binding { } } +impl Bind> { + pub fn load (body: impl Language) -> Usually { + let mut bind: Bind> = Bind::new(); + body.each((), |_, item: &str|if matches!(item.expr().head(), Ok(Some("see"))) { + bind.add_ref(item) + } else if let Ok(Some(_word)) = item.expr().head().word() { + bind.add_def(item) + } else { + return err!("Binds::add: unexpected: {item:?}") + })?; + Ok(bind) + } + + fn add_ref (&mut self, _item: &str) -> Usually<()> { + // TODO + Ok(()) + } + + fn add_def (&mut self, item: &str) -> Usually<()> { + let expr = item.expr()?; + let head = expr.head()?; + if let Some(event) = TuiKey::from_dsl(&head)?.to_crossterm() { + let mut commands: Vec> = vec![]; + item.expr()?.tail()?.each((), |_, def|Ok(commands.push(def.into())))?; + self.add(TuiEvent(event), Binding { + commands: commands.into(), + condition: None, + description: None, + source: None + }); + Ok(()) + } else if Some(":char") == item.expr()?.head()? { + // TODO + Ok(()) + } else { + err!("Binds::add: invalid key: {:?}", item.expr()?.head()?) + } + } +} + impl Bind { + /// Create a new event map pub fn new () -> Self { Default::default() @@ -156,5 +161,5 @@ impl Bind { impl_debug!(Condition |self, w| { write!(w, "*") }); -//take!(DeviceCommand|state: Project, iter|state.selected_device().as_ref() +//take!(DeviceCmd|state: Project, iter|state.selected_device().as_ref() //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); diff --git a/src/config_mode.rs b/src/config_mode.rs index c0fbfa2f..b4aa8970 100644 --- a/src/config_mode.rs +++ b/src/config_mode.rs @@ -5,7 +5,7 @@ use crate::*; /// ``` /// let mut modes = tek::Modes::default(); /// assert_eq!(modes.len(), 0); -/// let _ = modes.add(&":foo", &"(mode (name Foo) (info Bar))"); +/// let _ = modes.add(&"mode Foo (name Foo) (info Bar)").unwrap(); /// assert_eq!(modes.len(), 1); /// ``` #[derive(Default, Debug, Clone)] @@ -22,7 +22,7 @@ impl Modes { let body = expr.tail()?.ok_or("mode: missing body")?; let mode = Mode::default(); let mode = body.each(mode, |c, s|Mode::add(c, s))?; - self.0.try_write().unwrap().insert(name.into(), Arc::new(mode)); + self.0.write().insert(name.into(), Arc::new(mode)); Ok(()) } @@ -33,14 +33,14 @@ impl Modes { /// Run something for each mode. pub fn for_each (&self, mut ator: impl FnMut(&str, &Mode)->T) { - for (k, v) in self.0.try_read().unwrap().iter() { + for (k, v) in self.0.read().iter() { let _ = ator(k.as_ref(), v.as_ref()); } } /// Count modes. pub fn len (&self) -> usize { - self.0.try_read().unwrap().len() + self.0.read().len() } } @@ -86,7 +86,7 @@ impl Mode { let submode = Mode::default(); let submode = body.each(submode, |c,s|Mode::add(c,s))?; let modes = mode.as_mut().modes.clone(); - modes.0.try_write().unwrap().insert(name.into(), Arc::new(submode)); + modes.0.write().insert(name.into(), Arc::new(submode)); mode }, "keys" => { @@ -123,40 +123,9 @@ pub trait HasModeStack { impl HasModeStack for App { fn modes (&self) -> RwLockReadGuard<'_, Vec>> { - self.modes.try_read().unwrap() + self.modes.read() } fn modes_mut (&mut self) -> RwLockWriteGuard<'_, Vec>> { - self.modes.try_write().unwrap() - } -} - -impl ModeController for T where T: HasModeStack - + Namespace>> - + Namespace> - + Namespace -{} - -#[tek_proc::commands(ModeCommand)] -pub trait ModeController: HasModeStack - + Namespace>> - + Namespace> - + Namespace -{ - #[command(Reset = "reset")] - fn mode_reset (&mut self, modes: Vec>) -> Perhaps { - *self.modes_mut() = modes.into(); - Ok(None) - } - - #[command(Push = "push")] - fn mode_push (&mut self, mode: Arc) -> Perhaps { - self.modes_mut().push(mode); - Ok(Some(ModeCommand::Pop)) - } - - #[command(Pop = "pop")] - fn mode_pop (&mut self) -> Perhaps { - let mode = self.modes_mut().pop(); - Ok(mode.map(|mode|ModeCommand::Push { mode })) + self.modes.write() } } diff --git a/src/lv2.rs b/src/device_lv2.rs similarity index 91% rename from src/lv2.rs rename to src/device_lv2.rs index aafdc18b..8d378d77 100644 --- a/src/lv2.rs +++ b/src/device_lv2.rs @@ -155,18 +155,6 @@ impl Draw<'_, Tui> for Lv2 { break } } - draw_header(self, to, x, y, width); + draw_lv2_header(self, to, x, y, width); } } - - -fn draw_header (state: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) { - let style = Style::default().gray(); - let label1 = format!(" {}", state.name); - to.blit(&label1, x + 1, y, Some(style.white().bold())); - if let Some(ref path) = state.path { - let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]); - to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim())); - } - //Ok(Rect { x, y, width: w, height: 1 }) -} diff --git a/src/lv2_gui.rs b/src/device_lv2_gui.rs similarity index 100% rename from src/lv2_gui.rs rename to src/device_lv2_gui.rs diff --git a/src/vst2.rs b/src/device_vst2.rs similarity index 100% rename from src/vst2.rs rename to src/device_vst2.rs diff --git a/src/dialog.rs b/src/dialog.rs deleted file mode 100644 index e399687e..00000000 --- a/src/dialog.rs +++ /dev/null @@ -1,188 +0,0 @@ -use crate::*; - -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. -/// -/// ``` -/// let dialog: tek::Dialog = Default::default(); -/// ``` -#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog { - #[default] None, - Help(usize), - Menu(usize, Arc<[MenuItem]>), - Device(usize), - Message(Arc), - Browse(BrowseTarget, Arc), - Options, -} - -impl Dialog { - - /// ``` - /// let _ = tek::Dialog::welcome(); - /// ``` - pub fn welcome () -> Self { - Self::Menu(1, Arc::new([ - - MenuItem::new(" Resume session ", [ - AppCommand::Project { - command: ProjectCommand::LoadLast - } - ]), - - MenuItem::new(" Create session ", [ - AppCommand::Dialog { - dialog: Dialog::None - }, - AppCommand::Mode { - command: ModeCommand::Reset { - modes: vec!["Arranger".into()] - } - }, - AppCommand::Tracks { - command: TracksCommand::Add - }, - AppCommand::Scenes { - command: ScenesCommand::Add - } - ]), - - MenuItem::new(" Load session ", [ - AppCommand::Dialog { - dialog: Dialog::Browse( - BrowseTarget::LoadProject, - Browse::new(None).unwrap().into() - ) - //dialog: Dialog::Browse { - //title: "Load session".into(), - //filter: "\\.tek$", - //} - } - ]), - - MenuItem::new(" Exit ", [ - AppCommand::Exit - ]), - - ])) - } - - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().menu_selected(); - /// ``` - pub fn menu_selected (&self) -> Option { - if let Self::Menu(selected, _) = self { Some(*selected) } else { None } - } - - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().menu_next(); - /// ``` - pub fn menu_next (&self) -> Self { - match self { - Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.len()), items.clone()), - _ => Self::None - } - } - - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().menu_prev(); - /// ``` - pub fn menu_prev (&self) -> Self { - match self { - Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.len()), items.clone()), - _ => Self::None - } - } - - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().device_kind(); - /// ``` - pub fn device_kind (&self) -> Option { - if let Self::Device(index) = self { Some(*index) } else { None } - } - - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().device_kind_next(); - /// ``` - pub fn device_kind_next (&self) -> Option { - self.device_kind().map(|index|(index + 1) % device_kinds().len()) - } - - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().device_kind_prev(); - /// ``` - pub fn device_kind_prev (&self) -> Option { - self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) - } - - /// FIXME: implement - pub fn message (&self) -> Option<&str> { todo!() } - - /// FIXME: implement - pub fn browser (&self) -> Option<&Arc> { todo!() } - - /// FIXME: implement - pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() } - -} - -/// Increment a wrapping counter. -pub const fn wrap_inc (index: usize, count: usize) -> usize { - if count > 0 { (index + 1) % count } else { 0 } -} - -/// Decrement a wrapping counter. -pub const fn wrap_dec (index: usize, count: usize) -> usize { - if count > 0 { - let a = index.overflowing_sub(1).0; - let b = count.saturating_sub(1); - if a < b { a } else { b } - } else { - 0 - } -} diff --git a/src/field.rs b/src/field.rs deleted file mode 100644 index ca808cb3..00000000 --- a/src/field.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::*; - -pub fn field_h <'a, T: Screen> ( - _theme: ItemTheme, _head: impl Draw, _body: impl Draw -) -> impl Draw { -} - -pub fn field_v <'a, T: Screen> ( - _theme: ItemTheme, _head: impl Draw, _body: impl Draw -) -> impl Draw { -} - -pub fn view_sessions <'a> () -> impl Draw { - let h = 6; - let w = Some(30); - let f = Rgb(224, 192, 128); - draw(move |to: &mut Tui|{ - for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { - let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; - let y = (2 * index) as u16; - let h = 2; - bg(b, fg(f, *name).align_w()).full_w().exact_h(h).push_y(y).draw(to)?; - } - Ok(Some(to.area().into())) - }).min_w(w).exact_h(h) -} - -/// ``` -/// let bg = tengri::ratatui::style::Color::Red; -/// let fg = tengri::ratatui::style::Color::Green; -/// let _ = tek::view_wrap(bg, fg, "and then blue, too!"); -/// ``` -pub fn view_wrap <'a> (bg: Color, fg: Color, content: impl Draw) - -> impl Draw -{ - let left = fg_bg(bg, Reset, y_repeat("▐").exact_w(1)); - let right = fg_bg(bg, Reset, y_repeat("▌").exact_w(1)); - east(left, west(right, fg_bg(fg, bg, content))) -} - -/// ``` -/// let _ = tek::button_2("", "", true); -/// let _ = tek::button_2("", "", false); -/// ``` -pub fn button_2 <'a> (key: impl Draw, label: impl Draw, hide: bool) - -> impl Draw -{ - let c1 = tui_orange(); - let c2 = tui_g(0); - let c3 = tui_g(96); - let c4 = tui_g(255); - bold(true, fg_bg(c1, c2, - bar(Split::East, 1u16, fg(c2, "▐"), - bar(Split::East, 1u16, key, - bar(Split::East, 1u16, fg(c3, "▐"), - when(!hide, fg_bg(c4, c3, label))))))) -} - -/// ``` -/// let _ = tek::button_3("", "", "", true); -/// let _ = tek::button_3("", "", "", false); -/// ``` -pub fn button_3 <'a> ( - key: impl Draw, - label: impl Draw, - value: impl Draw, - editing: bool, -) -> impl Draw { - let c1 = tui_orange(); - let c2 = tui_g(0); - let c3 = tui_g(96); - let c4 = tui_g(255); - let c5 = tui_g(128); - bold(true, bar(Split::East, 3u16, - fg_bg(c1, c2, - bar(Split::East, 1u16, fg(c2, "▐"), - bar(Split::East, 1u16, key, fg(if editing { c5 } else { c3 }, "▐")))), - east( - when(!editing, east(fg_bg(c4, c3, label), fg_bg(c5, c3, "▐"),)), - east(fg_bg(tui_g(224), c5, value), fg_bg(c5, Reset, "▌"), )))) -} diff --git a/src/memo.rs b/src/memo.rs deleted file mode 100644 index 33691d80..00000000 --- a/src/memo.rs +++ /dev/null @@ -1,54 +0,0 @@ -#![allow(unused)] -use crate::*; - -#[macro_export] macro_rules! rewrite { - ($buf:ident, $($rest:tt)*) => { - |$buf,_,_|{ - $buf.clear(); - write!($buf, $($rest)*) - } - }; -} - -pub struct Memo { - value: T, - view: U, -} - -impl Memo { - pub fn new (value: T, view: U) -> Self { - Self { value, view } - } -} - -impl Debug for Memo { - fn fmt (&self, _: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { - todo!() - } -} - -impl Memo { - pub fn update (&mut self, newval: T, render: impl Fn(&mut U, &T, &T)->R) -> Option { - if newval != self.value { - let result = render(&mut self.view, &newval, &self.value); - self.value = newval; - return Some(result); - } - None - } -} - -pub trait Gettable { - /// Returns current value - fn get (&self) -> T; -} - -pub trait Mutable: Gettable { - /// Sets new value, returns old - fn set (&mut self, value: T) -> T; -} - -pub trait InteriorMutable: Gettable { - /// Sets new value, returns old - fn set (&self, value: T) -> T; -} diff --git a/src/menu_item.rs b/src/menu_item.rs deleted file mode 100644 index 73537a8d..00000000 --- a/src/menu_item.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::*; - -/// An item of a menu. -/// -/// ``` -/// let item: tek::MenuItem = Default::default(); -/// ``` -#[derive(Clone)] pub struct MenuItem( - pub Arc, - pub Arc<[AppCommand]>, -); - -impl MenuItem { - pub fn new (name: impl AsRef, actions: [AppCommand; 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 - //} -//} diff --git a/src/meter.rs b/src/meter.rs deleted file mode 100644 index dd3f3331..00000000 --- a/src/meter.rs +++ /dev/null @@ -1,115 +0,0 @@ -#![allow(unused)] -use crate::*; - -#[derive(Debug, Default)] pub enum MeteringMode { - #[default] Rms, - Log10, -} - -#[derive(Debug, Default, Clone)] -pub struct Log10Meter(pub f32); - -#[derive(Debug, Default, Clone)] -pub struct RmsMeter(pub f32); - -impl_draw!(|self: RmsMeter, to: Tui|{ - let XYWH(x, y, w, h) = to.area().into(); - let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); - let v = (signal * h as f32).ceil() as u16; - let y2 = y + h; - //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); - for y in y..(y + v) { - for x in x..(x + w) { - to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green())); - } - } - Ok(Some(to.area().into())) -}); - -impl_draw!(|self: Log10Meter, to: Tui| { - let XYWH(x, y, w, h) = to.area().into(); - let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); - let v = (signal * h as f32 / 100.0).ceil() as u16; - let y2 = y + h; - //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); - for y in y..(y + v) { - for x in x..(x + w) { - to.blit(&"▌", x, y2 - y, Some(Style::default().green())); - } - } - Ok(Some(to.area().into())) -}); - -fn draw_meters (meters: &[f32]) -> impl Draw + use<'_> { - bg(Black, iter_east_fixed(1, ||meters.iter(), |value, _index|{ - RmsMeter(*value).full_h() - }).exact_w(2)) -} - -pub fn to_log10 (samples: &[f32]) -> f32 { - let total: f32 = samples.iter().map(|x|x.abs()).sum(); - let count = samples.len() as f32; - 10. * (total / count).log10() -} - -pub fn to_rms (samples: &[f32]) -> f32 { - let sum = samples.iter() - .map(|s|*s) - .reduce(|sum, sample|sum + sample.abs()) - .unwrap_or(0.0); - (sum / samples.len() as f32).sqrt() -} - -/// ``` -/// let _ = tek::view_meter("", 0.0); -/// let _ = tek::view_meters(&[0.0, 0.0]); -/// ``` -pub fn view_meter <'a> (label: &'a str, value: f32) - -> impl Draw -{ - let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)); - let w = if value >= 0.0 { 13 } - else if value >= -1.0 { 12 } - else if value >= -2.0 { 11 } - else if value >= -3.0 { 10 } - else if value >= -4.0 { 9 } - else if value >= -6.0 { 8 } - else if value >= -9.0 { 7 } - else if value >= -12.0 { 6 } - else if value >= -15.0 { 5 } - else if value >= -20.0 { 4 } - else if value >= -25.0 { 3 } - else if value >= -30.0 { 2 } - else if value >= -40.0 { 1 } - else { 0 }; - let c = if value >= 0.0 { Red } - else if value >= -3.0 { Yellow } - else { Green }; - south!(f, bg(c, ()).exact_wh(w, 1)) -} - -pub fn view_meters (values: &[f32;2]) -> impl Draw + use<'_> { - let left = format!("L/{:>+9.3}", values[0]); - let right = format!("R/{:>+9.3}", values[1]); - south(left, right) -} - -#[cfg(test)] mod test_view_meter { - use super::*; - use proptest::prelude::*; - proptest! { - - #[test] fn proptest_view_meter ( - label in "\\PC*", value in f32::MIN..f32::MAX - ) { - let _ = view_meter(&label, value); - } - - #[test] fn proptest_view_meters ( - value1 in f32::MIN..f32::MAX, - value2 in f32::MIN..f32::MAX - ) { - let _ = view_meters(&[value1, value2]); - } - } -} diff --git a/src/midi_clip.rs b/src/midi_clip.rs index 848402fb..55e04a1e 100644 --- a/src/midi_clip.rs +++ b/src/midi_clip.rs @@ -131,50 +131,6 @@ pub type MidiData = Vec>; impl PartialEq for MidiClip { fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid } } impl Eq for MidiClip {} -impl HasMidiClip for Project { - fn clip (&self) -> Option>> { None } -} -impl HasMidiClip for App { - fn clip (&self) -> Option>> { self.project.clip() } -} - -impl> - + Namespace> -> MidiClipController for T {} - -#[tek_proc::commands(MidiClipCommand)] -pub trait MidiClipController: HasMidiClip - + 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!() - } - - #[command(SetColor = "color")] - fn clip_set_color (&mut self, color: Option) -> Perhaps { - Ok(self.clip().map(|clip|MidiClipCommand::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|MidiClipCommand::SetLoop { - looping: Some(clip.write().toggle_loop(looping)) - })) - } - -} 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 } } diff --git a/src/midi_clips.rs b/src/midi_clips.rs deleted file mode 100644 index c7b7e813..00000000 --- a/src/midi_clips.rs +++ /dev/null @@ -1 +0,0 @@ -use crate::*; diff --git a/src/midi_edit.rs b/src/midi_edit.rs index 7c7fb7f8..e8c11623 100644 --- a/src/midi_edit.rs +++ b/src/midi_edit.rs @@ -19,140 +19,17 @@ pub trait HasEditor: AsRef> + AsMut> { } } -impl HasEditor for T where T: - AsRef> + - AsMut> {} - -impl - + Namespace - + Namespace - + Namespace - + Namespace> - + Namespace>>> -> MidiEditController for T {} - -#[tek_proc::commands(MidiEditCommand)] -pub trait MidiEditController: HasEditor - + Namespace - + Namespace - + Namespace - + Namespace - + Namespace> - + Namespace>>> -{ - #[command(Toggle = "toggle")] - fn edit_toggle (&mut self) -> Perhaps { - if self.is_editing() { - *self.editor_mut() = None; - } else { - todo!() - } - Ok(Some(MidiEditCommand::Toggle)) - } - - #[command(Show = "show")] - fn edit_show (&mut self, clip: Option>>) -> Perhaps { - Ok(self.editor_mut().as_mut().map(|mut editor|{ - editor.set_clip(clip.as_ref()); - editor.redraw(); - None - }).flatten()) - } - - #[command(DeleteNote = "delete")] - fn edit_note_delete (&mut self) -> Perhaps { - Ok(self.editor_mut().as_mut().map(|editor|{ - editor.redraw(); - todo!() - }).flatten()) - } - - #[command(AppendNote = "append")] - fn edit_note_append (&mut self, advance: bool) -> Perhaps { - Ok(self.editor_mut().as_mut().map(|mut editor|{ - editor.put_note(advance); - editor.redraw(); - None - }).flatten()) - } - - #[command(SetNotePos = "note-pos")] - fn edit_note_set_pos (&mut self, pos: usize) -> Perhaps { - Ok(self.editor_mut().as_mut().map(|editor|{ - editor.set_note_pos((pos).min(127)); - editor.redraw(); - None - }).flatten()) - } - - #[command(SetNoteLen = "note-len")] - fn edit_note_set_len (&mut self, len: usize) -> Perhaps { - Ok(self.editor_mut().as_mut().map(|editor|{ - editor.set_note_len(len); - editor.redraw(); - None - }).flatten()) - } - - #[command(SetNoteScroll = "note-scroll")] - fn edit_note_set_scroll (&mut self, scroll: usize) -> Perhaps { - Ok(self.editor_mut().as_mut().map(|editor|{ - editor.set_note_lo((scroll).min(127)); - editor.redraw(); - None - }).flatten()) - } - - #[command(SetTimePos = "time-pos")] - fn edit_time_set_pos (&mut self, pos: usize) -> Perhaps { - Ok(self.editor_mut().as_mut().map(|editor|{ - editor.set_time_pos(pos); - editor.redraw(); - None - }).flatten()) - } - - #[command(SetTimeScroll = "time-scroll")] - fn edit_time_set_scroll (&mut self, scroll: usize) -> Perhaps { - Ok(self.editor_mut().as_mut().map(|editor|{ - editor.set_time_start(scroll); - editor.redraw(); - None - }).flatten()) - } - - #[command(SetTimeZoom = "time-zoom")] - fn edit_time_set_zoom (&mut self, zoom: usize) -> Perhaps { - Ok(self.editor_mut().as_mut().map(|editor|{ - editor.set_time_zoom(zoom); - editor.redraw(); - None - }).flatten()) - } - - #[command(SetTimeLock = "time-lock")] - fn edit_time_set_lock (&mut self, lock: bool) -> Perhaps { - Ok(self.editor_mut().as_mut().map(|editor|{ - editor.set_time_lock(lock); - editor.redraw(); - None - }).flatten()) - } - - // TODO: 1-9 seek markers that by default start every 8th of the clip -} +impl HasEditor for T where T: AsRef> + AsMut> {} /// Contains state for viewing and editing a clip. /// /// ``` -/// use std::sync::{Arc, RwLock}; +/// use std::sync::Arc; +/// use parking_lot::RwLock; /// let clip = tek::MidiClip::stop_all(); -/// let mut editor = tek::MidiEditor { -/// mode: tek::PianoHorizontal::new(Some(&Arc::new(RwLock::new(clip)))), -/// size: Default::default(), -/// //keys: Default::default(), -/// }; +/// let mode = tek::PianoHorizontal::new(Some(&Arc::new(RwLock::new(clip)))); +/// let mut editor = tek::MidiEditor { mode, size: Default::default() }; +/// /// let _ = editor.put_note(true); /// let _ = editor.put_note(false); /// let _ = editor.clip_status(); @@ -275,8 +152,8 @@ impl MidiEditor { /// use tek::{*, tengri::*}; /// /// struct Test(Option); -/// impl_as_ref_opt!(MidiEditor: |self: Test|self.0.as_ref()); -/// impl_as_mut_opt!(MidiEditor: |self: Test|self.0.as_mut()); +/// impl_as_ref!(Option: |self: Test|&self.0); +/// impl_as_mut!(Option: |self: Test|&mut self.0); /// /// let mut host = Test(Some(MidiEditor::default())); /// let _ = host.editor(); @@ -427,7 +304,7 @@ impl App { && slot.is_none() && let Some(track) = self.project.tracks.get_mut(track) { - let (_index, clip) = self.pool.add_new_clip(); + 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(); @@ -450,7 +327,7 @@ impl App { std::mem::swap(&mut swapped, slot); } if let Some(clip) = swapped { - self.pool.delete_clip(&clip.try_read().unwrap()); + self.project.pool.delete_clip(&clip.try_read().unwrap()); } } } diff --git a/src/octave.rs b/src/midi_octave.rs similarity index 100% rename from src/octave.rs rename to src/midi_octave.rs diff --git a/src/piano.rs b/src/midi_piano.rs similarity index 100% rename from src/piano.rs rename to src/midi_piano.rs diff --git a/src/point.rs b/src/midi_point.rs similarity index 100% rename from src/point.rs rename to src/midi_point.rs diff --git a/src/range.rs b/src/midi_range.rs similarity index 100% rename from src/range.rs rename to src/midi_range.rs diff --git a/src/midi_seq.rs b/src/midi_seq.rs index f40bb97d..ddec83ce 100644 --- a/src/midi_seq.rs +++ b/src/midi_seq.rs @@ -18,7 +18,7 @@ pub trait HasSequencer: AsRef + AsMut { /// /// let mut clip = tek::MidiClip::new("clip", true, 1, None, None); /// clip.set_length(96); -/// clip.toggle_loop(); +/// clip.toggle_loop(None); /// clip.record_event(12, ::tengri::midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }); /// assert!(clip.contains_note_on(36.into(), 6, 18)); /// assert_eq!(&clip.notes, &clip.duplicate().notes); diff --git a/src/mix.rs b/src/mix.rs deleted file mode 100644 index 7e4d5574..00000000 --- a/src/mix.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::*; - -#[derive(Debug, Default)] -pub enum MixingMode { - #[default] Summing, - Average, -} - -pub fn mix_summing ( - buffer: &mut [Vec], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>, -) -> bool { - let channels = buffer.len(); - for index in 0..frames { - if let Some(frame) = next() { - for (channel, sample) in frame.iter().enumerate() { - let channel = channel % channels; - buffer[channel][index] += sample * gain; - } - } else { - return false - } - } - true -} - -pub fn mix_average ( - buffer: &mut [Vec], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>, -) -> bool { - let channels = buffer.len(); - for index in 0..frames { - if let Some(frame) = next() { - for (channel, sample) in frame.iter().enumerate() { - let channel = channel % channels; - let value = buffer[channel][index]; - buffer[channel][index] = (value + sample * gain) / 2.0; - } - } else { - return false - } - } - true -} diff --git a/src/plugin.rs b/src/plugin.rs deleted file mode 100644 index c7b7e813..00000000 --- a/src/plugin.rs +++ /dev/null @@ -1 +0,0 @@ -use crate::*; diff --git a/src/pool.rs b/src/pool.rs deleted file mode 100644 index f5348355..00000000 --- a/src/pool.rs +++ /dev/null @@ -1,493 +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 - + Namespace - + Namespace> - + Namespace - + Namespace - + Namespace - + Namespace -> PoolController for T {} - -#[tek_proc::commands(PoolCommand = "pool")] -pub trait PoolController: HasPool - + Namespace - + Namespace - + Namespace> - + Namespace - + Namespace - + Namespace - + Namespace -{ - - #[command(Show = "show")] - /// Toggle visibility of pool - fn show (&mut self, visible: bool) -> Perhaps { - self.pool_mut().visible = visible; - Ok(Some(PoolCommand::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) - } - - ///// Import from file - //#[command(Browse = "browse")] - //fn browse (&mut self, command: BrowseCommand) -> Perhaps { - //Ok(if let Some(browse) = self.pool_mut().browse.as_mut() { - //command.act(browse)?.map(|command|PoolCommand::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(PoolCommand::Add { index, clip }.dispatch(self)?) - } - - ///// Export to file - //#[command(Export = "export")] - //fn export (&mut self, command: BrowseCommand) -> Perhaps { - //Ok(if let Some(browse) = self.pool_mut().browse.as_mut() { - //command.act(browse)?.map(|command|PoolCommand::Export{command}) - //} else { - //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(PoolCommand::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(PoolCommand::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(PoolCommand::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(PoolCommand::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(PoolCommand::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(PoolCommand::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|PoolCommand::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(PoolCommand::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) - } - -} - -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/port.rs b/src/port.rs deleted file mode 100644 index 54cc6a7a..00000000 --- a/src/port.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::*; -def_sizes_iter!(PortsSizes => Arc, [Connect]); -impl_has!(Vec: |self: Project| self.midi_ins); -impl_has!(Vec: |self: Project| self.midi_outs); -impl_has!(Vec: |self: App|self.project.midi_ins); -impl_has!(Vec: |self: App|self.project.midi_outs); - -pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { - track.map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) -} - -pub fn view_midi_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { - track.map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs)) -} - -pub fn view_audio_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { - track.map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins())) -} - -pub fn view_audio_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { - track.map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) -} - -pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) - -> impl Draw + use<'a, T> -{ - let ins = ports.len() as u16; - let frame = Outer(true, Style::default().fg(g(96))); - border(true, frame, field_v(theme, title, iter_south({ - move||ports.iter().enumerate().map(|(index, port)|{ - east!(" ", index, " ", port.port_name()).align_w().full_h() - }) - })).exact_wh(20, 1 + ins)).exact_wh(20, 1 + ins) -} - -pub fn view_io_ports <'a, T: PortsSizes<'a>> ( - fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a -) -> impl Draw + 'a { - type Item<'a> = (usize, &'a Arc, &'a [Connect], usize, usize); - iter(items, - move|(_index, name, connections, y, y2): Item<'a>, _| south( - bold(true, fg_bg(fg, bg, east(" 󰣲 ", name).align_w())).full_h(), - iter(||connections.iter(), move|connect: &'a Connect, index|{ - bold(false, fg_bg(fg, bg, &connect.info)).exact_h(1).align_w().push_y(index as u16) - }) - ).exact_h((y2 - y) as u16).push_y(y as u16)) -} - -pub struct Junction(T); - -impl Draw for Junction { - fn draw (&self, to: &mut Tui) -> Drawn { - T::KIND.draw(to) - } -} - -#[tek_proc::command(AudioInput)] -#[tek_proc::keyword(AudioInput)] -#[derive(Debug)] -pub enum AudioInputCommand { - Close, - Connect(Arc), -} - -#[tek_proc::command(AudioOutput)] -#[tek_proc::keyword(AudioOutput)] -#[derive(Debug)] -pub enum AudioOutputCommand { - Close, - Connect(Arc), -} - -#[tek_proc::command(MidiInput)] -#[tek_proc::keyword(MidiInput)] -#[derive(Debug)] -pub enum MidiInputCommand { - Close, - Connect(Arc), -} - -#[tek_proc::command(MidiOutput)] -#[tek_proc::keyword(MidiOutput)] -#[derive(Debug)] -pub enum MidiOutputCommand { - Close, - Connect(Arc), -} diff --git a/src/project.rs b/src/proj.rs similarity index 87% rename from src/project.rs rename to src/proj.rs index 3ac54d71..43669459 100644 --- a/src/project.rs +++ b/src/proj.rs @@ -1,38 +1,9 @@ use crate::*; - -impl AsRef> for App { - fn as_ref (&self) -> &Option { - todo!() - } -} - -impl AsMut> for App { - fn as_mut (&mut self) -> &mut Option { - todo!() - } -} - +impl_as_ref!(Option: |self: App|todo!()); +impl_as_mut!(Option: |self: App|todo!()); pub trait HasProject: AsRef> + AsMut> {} - impl> + AsMut>> HasProject for T {} -impl ProjectController for T {} - -#[tek_proc::commands(ProjectCommand)] -pub trait ProjectController: HasProject { - - #[command(LoadLast = "load-last")] - fn load_last (&mut self) -> Perhaps { - todo!() - } - - #[command(Load = "load")] - fn load (&mut self) -> Perhaps { - todo!() - } - -} - /// Arranger. /// /// ``` @@ -55,6 +26,8 @@ pub struct Project { pub size_inner: Sizer, /// Selected UI element pub selection: Selection, + /// Contains all recently created clips. + pub pool: Pool, /// Source of time #[cfg(feature = "clock")] pub clock: Clock, /// Allows one MIDI clip to be edited @@ -137,7 +110,6 @@ impl Project { } } -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); @@ -147,6 +119,15 @@ 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!() } +} pub fn swap_value ( target: &mut T, value: &T, returned: impl Fn(T)->U diff --git a/src/clock.rs b/src/proj_clock.rs similarity index 83% rename from src/clock.rs rename to src/proj_clock.rs index cf919b75..3b9f0adf 100644 --- a/src/clock.rs +++ b/src/proj_clock.rs @@ -57,82 +57,7 @@ pub trait HasClock: AsRef + AsMut { } } -impl - + Namespace - + Namespace> -> ClockController for T {} - -#[tek_proc::commands(ClockCommand = "clock")] -pub trait ClockController: HasClock - + Namespace - + Namespace - + Namespace> -{ - #[command(SeekUsec = "usec")] - fn seek_usec (&mut self, usec: f64) -> Perhaps { - self.clock().playhead.update_from_usec(usec); - Ok(None) - } - - #[command(SeekSample = "sample")] - fn seek_sample (&mut self, sample: f64) -> Perhaps { - self.clock().playhead.update_from_sample(sample); - Ok(None) - } - - #[command(SeekPulse = "pulse")] - fn seek_pulse (&mut self, pulse: f64) -> Perhaps { - self.clock().playhead.update_from_pulse(pulse); - Ok(None) - } - - #[command(SetBpm = "bpm")] - fn set_bpm (&mut self, bpm: f64) -> Perhaps { - Ok(Some(ClockCommand::SetBpm { - bpm: self.clock().timebase().bpm.set(bpm) - })) - } - - #[command(SetQuant = "quant")] - fn set_quant (&mut self, quant: f64) -> Perhaps { - Ok(Some(ClockCommand::SetQuant { - quant: self.clock().quant.set(quant) - })) - } - - #[command(SetSync = "sync")] - fn set_sync (&mut self, sync: f64) -> Perhaps { - Ok(Some(ClockCommand::SetSync { - sync: self.clock().sync.set(sync) - })) - } - - #[command(Play = "play")] - fn play (&mut self, position: Option) -> Perhaps { - self.clock().play_from(position)?; - Ok(None) /* TODO Some(Pause(previousPosition)) */ - } - - #[command(Pause = "pause")] - fn pause (&mut self, position: Option) -> Perhaps { - self.clock().pause_at(position)?; - Ok(None) - } - - #[command(TogglePlayback = "toggle")] - fn toggle (&mut self, position: u32) -> Perhaps { - Ok(if self.clock().is_rolling() { - self.clock().pause_at(Some(position))?; - None - } else { - self.clock().play_from(Some(position))?; - None - }) - } -} - -//impl Act for ClockCommand { +//impl Act for ClockCmd { //fn act (&self, state: &mut T) -> Perhaps { //self.act(state.clock_mut()) // awesome //} @@ -449,22 +374,6 @@ impl_time_unit!(Pulse); impl_time_unit!(Bpm); impl_time_unit!(LaunchSync); -/// ``` -/// let x = ""; -/// let _ = tek::view_status(None, x.as_ref(), x.as_ref(), x.as_ref()); -/// let _ = tek::view_status(Some("".into()), x.as_ref(), x.as_ref(), x.as_ref()); -/// ``` -pub fn view_status <'a> (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw { - let theme = ItemTheme::G[96]; - let sr = field_h(theme, "SR", sr); - let buf = field_h(theme, "Buf", buf); - let lat = field_h(theme, "Lat", lat); - bg(Black, east!(above( - sel.map(|sel|field_h(theme, "Selected", sel)).align_w().full_wh(), - east!(sr, buf, lat).align_e().full_wh(), - ))) -} - /// ``` /// let _ = tek::button_play_pause(true, true); /// let _ = tek::button_play_pause(true, false); @@ -485,3 +394,80 @@ pub fn button_play_pause <'a> (playing: bool, compact: bool) -> impl Draw { ) ) } + +/// Contains memoized renders of clock values. +/// +/// Performance optimization. +#[derive(Debug)] pub struct ClockView { + pub sr: Memo, String>, + pub buf: Memo, String>, + pub lat: Memo, String>, + pub bpm: Memo, String>, + pub beat: Memo, String>, + pub time: Memo, String>, +} + +impl_default!(ClockView: { + let mut beat = String::with_capacity(16); + let _ = write!(beat, "{}", Self::BEAT_EMPTY); + let mut time = String::with_capacity(16); + let _ = write!(time, "{}", Self::TIME_EMPTY); + let mut bpm = String::with_capacity(16); + let _ = write!(bpm, "{}", Self::BPM_EMPTY); + Self { + beat: Memo::new(None, beat), + time: Memo::new(None, time), + bpm: Memo::new(None, bpm), + sr: Memo::new(None, String::with_capacity(16)), + buf: Memo::new(None, String::with_capacity(16)), + lat: Memo::new(None, String::with_capacity(16)), + } +}); + +impl ClockView { + pub const BEAT_EMPTY: &'static str = "-.-.--"; + pub const TIME_EMPTY: &'static str = "-.---s"; + pub const BPM_EMPTY: &'static str = "---.---"; + pub fn update_clock (cache: &Arc>, clock: &Clock, compact: bool) { + let rate = clock.timebase.sr.get(); + let chunk = clock.chunk.load(Relaxed) as f64; + let lat = chunk / rate * 1000.; + let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); + let mut cache = cache.try_write().unwrap(); + + cache.buf.update( + Some(chunk), rewrite!(buf, "{chunk}") + ); + + cache.lat.update( + Some(lat), rewrite!(buf, "{lat:.1}ms") + ); + + cache.sr.update( + Some((compact, rate)), |buf: &mut String,_,_|{ + buf.clear(); + if compact { + write!(buf, "{:.1}kHz", rate / 1000.) + } else { + write!(buf, "{:.0}Hz", rate) + } + } + ); + + if let Some(now) = clock.started.try_read().unwrap().as_ref().map(delta) { + let pulse = clock.timebase.usecs_to_pulse(now); + let time = now/1000000.; + let bpm = clock.timebase.bpm.get(); + cache.beat.update(Some(pulse), |buf, _, _|{ + buf.clear(); + clock.timebase.format_beats_1_to(buf, pulse) + }); + cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time)); + cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm)); + } else { + cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY)); + cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY)); + cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY)); + } + } +} diff --git a/src/device.rs b/src/proj_device.rs similarity index 91% rename from src/device.rs rename to src/proj_device.rs index 8dd6bae4..52aa013c 100644 --- a/src/device.rs +++ b/src/proj_device.rs @@ -99,7 +99,7 @@ fn tek_jack_event (state: &mut App, event: JackEvent) { #[tek_proc::command(Device)] #[tek_proc::keyword(Device)] #[derive(Debug)] -pub enum DeviceCommand {} +pub enum DeviceCmd {} impl Device { pub fn name (&self) -> &str { @@ -198,15 +198,3 @@ pub trait HasDevices: AsRef> + AsMut> { self.as_mut() } } - -pub fn view_device <'a> (state: &'a App) -> impl Draw { - let selected = state.dialog.device_kind().unwrap(); - south( - bold(true, "Add device"), - iter_south(move||device_kinds().iter().enumerate().map(move|(i, _label)|{ - let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; - let l = if i == selected { "[ " } else { " " }; - let r = if i == selected { " ]" } else { " " }; - bg(b, east(l, west(r, "FIXME device name"))).full_w() - }))) -} diff --git a/src/proj_mixer.rs b/src/proj_mixer.rs new file mode 100644 index 00000000..0a7346bf --- /dev/null +++ b/src/proj_mixer.rs @@ -0,0 +1,95 @@ +use crate::*; + +#[derive(Debug, Default)] +pub enum MixingMode { + #[default] Summing, + Average, +} + +pub fn mix_summing ( + buffer: &mut [Vec], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>, +) -> bool { + let channels = buffer.len(); + for index in 0..frames { + if let Some(frame) = next() { + for (channel, sample) in frame.iter().enumerate() { + let channel = channel % channels; + buffer[channel][index] += sample * gain; + } + } else { + return false + } + } + true +} + +pub fn mix_average ( + buffer: &mut [Vec], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>, +) -> bool { + let channels = buffer.len(); + for index in 0..frames { + if let Some(frame) = next() { + for (channel, sample) in frame.iter().enumerate() { + let channel = channel % channels; + let value = buffer[channel][index]; + buffer[channel][index] = (value + sample * gain) / 2.0; + } + } else { + return false + } + } + true +} + +#[derive(Debug, Default)] pub enum MeteringMode { + #[default] Rms, + Log10, +} + +#[derive(Debug, Default, Clone)] +pub struct Log10Meter(pub f32); + +#[derive(Debug, Default, Clone)] +pub struct RmsMeter(pub f32); + +impl_draw!(|self: RmsMeter, to: Tui|{ + let XYWH(x, y, w, h) = to.area().into(); + let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); + let v = (signal * h as f32).ceil() as u16; + let y2 = y + h; + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); + for y in y..(y + v) { + for x in x..(x + w) { + to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green())); + } + } + Ok(Some(to.area().into())) +}); + +impl_draw!(|self: Log10Meter, to: Tui| { + let XYWH(x, y, w, h) = to.area().into(); + let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); + let v = (signal * h as f32 / 100.0).ceil() as u16; + let y2 = y + h; + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); + for y in y..(y + v) { + for x in x..(x + w) { + to.blit(&"▌", x, y2 - y, Some(Style::default().green())); + } + } + Ok(Some(to.area().into())) +}); + +pub fn to_log10 (samples: &[f32]) -> f32 { + let total: f32 = samples.iter().map(|x|x.abs()).sum(); + let count = samples.len() as f32; + 10. * (total / count).log10() +} + +pub fn to_rms (samples: &[f32]) -> f32 { + let sum = samples.iter() + .map(|s|*s) + .reduce(|sum, sample|sum + sample.abs()) + .unwrap_or(0.0); + (sum / samples.len() as f32).sqrt() +} diff --git a/src/proj_pool.rs b/src/proj_pool.rs new file mode 100644 index 00000000..3827d062 --- /dev/null +++ b/src/proj_pool.rs @@ -0,0 +1,236 @@ +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_port.rs b/src/proj_port.rs new file mode 100644 index 00000000..5df64797 --- /dev/null +++ b/src/proj_port.rs @@ -0,0 +1,46 @@ +use crate::*; +def_sizes_iter!(PortsSizes => Arc, [Connect]); +impl_has!(Vec: |self: Project| self.midi_ins); +impl_has!(Vec: |self: Project| self.midi_outs); +impl_has!(Vec: |self: App|self.project.midi_ins); +impl_has!(Vec: |self: App|self.project.midi_outs); + +pub struct Junction(T); + +impl Draw for Junction { + fn draw (&self, to: &mut Tui) -> Drawn { + T::KIND.draw(to) + } +} + +#[tek_proc::command(AudioInput)] +#[tek_proc::keyword(AudioInput)] +#[derive(Debug)] +pub enum AudioInputCmd { + Close, + Connect(Arc), +} + +#[tek_proc::command(AudioOutput)] +#[tek_proc::keyword(AudioOutput)] +#[derive(Debug)] +pub enum AudioOutputCmd { + Close, + Connect(Arc), +} + +#[tek_proc::command(MidiInput)] +#[tek_proc::keyword(MidiInput)] +#[derive(Debug)] +pub enum MidiInputCmd { + Close, + Connect(Arc), +} + +#[tek_proc::command(MidiOutput)] +#[tek_proc::keyword(MidiOutput)] +#[derive(Debug)] +pub enum MidiOutputCmd { + Close, + Connect(Arc), +} diff --git a/src/scene.rs b/src/proj_scene.rs similarity index 53% rename from src/scene.rs rename to src/proj_scene.rs index d7925caf..fc948d6c 100644 --- a/src/scene.rs +++ b/src/proj_scene.rs @@ -67,55 +67,6 @@ pub trait HasScene: AsRefOpt + AsMutOpt { } } -impl - + Namespace> - + Namespace -> SceneController for T {} - -#[tek_proc::commands(SceneCommand)] -pub trait SceneController: HasScene - + Namespace - + Namespace> - + Namespace -{ - #[command(SetSize = "size")] - fn scene_set_size (&mut self, _size: usize) -> Perhaps - where Self: Namespace - { - todo!() - } - - #[command(SetZoom = "zoom")] - fn scene_set_zoom (&mut self, _size: usize) -> Perhaps - where Self: Namespace - { - todo!() - } - - #[command(SetName = "name")] - fn scene_set_name (&mut self, name: Arc) -> Perhaps - where Self: Namespace> - { - Ok(self.scene_mut().map(|scene|swap_value( - &mut scene.name, - &name, - |name|SceneCommand::SetName { name } - )).transpose()?.flatten()) - } - - #[command(SetColor = "color")] - fn scene_set_color (&mut self, color: ItemTheme) -> Perhaps - where Self: Namespace - { - Ok(self.scene_mut().map(|scene|swap_value( - &mut scene.color, - &color, - |color|SceneCommand::SetColor { color } - )).transpose()?.flatten()) - } -} - pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T); def_sizes_iter!(ScenesSizes => Scene); @@ -170,59 +121,6 @@ pub trait HasScenes: AsRef> + AsMut> { } } -impl - + Namespace - + Namespace> - + Namespace>> -> ScenesController for T {} - -#[tek_proc::commands(ScenesCommand)] -pub trait ScenesController: HasScenes - + HasTracks - + HasTrackScroll - + HasSelection - + HasJack<'static> - + Namespace - + Namespace> - + Namespace>> -{ - #[command(Add = "add")] - fn scenes_add (&mut self) -> Perhaps - where Self: HasScenes + HasJack<'static> - { - let (_index, _) = self.scenes_add_one(None, None)?; - Ok(None) - } - // TODO - - #[command(Select = "select")] - fn scene_select (&mut self) -> Perhaps { - let scenes = self.scenes().len(); - let select = self.selection_mut().select_scene(scenes); - *self.selection_mut() = select; - Ok(None) - } - - #[command(Next = "next")] - fn scene_select_next (&mut self) -> Perhaps { - let scenes = self.scenes().len(); - let select = self.selection().select_scene_next(scenes); - *self.selection_mut() = select; - Ok(None) - } - - #[command(Prev = "prev")] - fn scene_select_prev (&mut self) -> Perhaps { - let select = self.selection_mut().select_scene_prev(); - *self.selection_mut() = select; - Ok(None) - } -} - pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync { fn h_scenes (&self) -> u16; fn w_side (&self) -> u16; @@ -280,30 +178,6 @@ impl ScenesView for Project { } } -pub fn view_scene_name <'a> ( - select: &Selection, - editor: Option<&'a MidiEditor>, - index: usize, - scene: &Scene, - editing: bool -) -> impl Draw { - let h = if select.scene() == Some(index) && let Some(_editor) = editor { - 7 - } else { - Scene::DEFAULT_HEIGHT as u16 - }; - let a = east!("·s", index, " ", fg(g(255), bold(true, &scene.name))).align_w().full_w(); - let b = when(select.scene() == Some(index) && editing, south( - editor.as_ref().map(|e|e.clip_status()), - editor.as_ref().map(|e|e.edit_status())).align_nw().full_wh()); - let c = if select.scene() == Some(index) { - scene.color.light.term - } else { - scene.color.base.term - }; - bg(c, south(a, b).align_nw().exact_h(h)) -} - pub trait HasSceneScroll: HasScenes { fn scene_scroll (&self) -> usize; } @@ -319,51 +193,3 @@ impl HasSceneScroll for App { self.project.scene_scroll() } } - -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]) - } -} - -pub fn view_scene_bg ( - theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize -) -> (Color, Color) { - let mut outline = theme.base.term; - (if select.track() == Some(track_index) && select.scene() == Some(scene_index) { - outline = theme.lighter.term; - theme.light.term - } else if select.track() == Some(track_index) || select.scene() == Some(scene_index) { - outline = theme.darkest.term; - theme.base.term - } else { - theme.dark.term - }, outline) -} - -pub fn view_scene_w ( - track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor> -) -> u16 { - if select.track() == Some(track_index) && let Some(editor) = editor { - (editor.size.w() as usize).max(24).max(track.width) as u16 - } else { - track.width as u16 - } -} - -pub fn view_scene_y ( - select: &Selection, scene_index: usize, editor: Option<&MidiEditor> -) -> u16 { - if select.scene() == Some(scene_index) && let Some(editor) = editor { - editor.size.h().max(12) - } else { - Scene::DEFAULT_HEIGHT as u16 - } -} - -pub fn view_scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool { - editing && select.track() == Some(track_index) && select.scene() == Some(scene_index) -} diff --git a/src/select.rs b/src/proj_select.rs similarity index 100% rename from src/select.rs rename to src/proj_select.rs diff --git a/src/track.rs b/src/proj_track.rs similarity index 78% rename from src/track.rs rename to src/proj_track.rs index 11e37cdc..f44f218e 100644 --- a/src/track.rs +++ b/src/proj_track.rs @@ -58,7 +58,7 @@ impl Track { ).exact_w((x2 - x1) as u16) })) } - pub fn stop (&mut self) -> Perhaps { + pub fn stop (&mut self) -> Perhaps { self.sequencer.enqueue_next(None); Ok(None) } @@ -85,71 +85,6 @@ pub trait HasTrack: AsRefOpt + AsMutOpt { } } -impl - + Namespace> - + Namespace - + Namespace> -> TrackController for T {} - -#[tek_proc::commands(TrackCommand)] -pub trait TrackController: HasTrack - + Namespace - + Namespace> - + Namespace - + Namespace> -{ - #[command(Stop = "track/stop")] - fn track_stop (&mut self) -> Perhaps { - self.track_mut().map(|track|track.sequencer.enqueue_next(None)); - Ok(None) - } - #[command(SetMute = "track/mute")] - fn track_set_mute (&mut self, _mute: Option) -> Perhaps { - todo!() - } - #[command(SetSolo = "track/solo")] - fn track_set_solo (&mut self, _solo: Option) -> Perhaps { - todo!() - } - #[command(SetSize = "track/size")] - fn track_set_size (&mut self, _size: usize) -> Perhaps { - todo!() - } - #[command(SetZoom = "track/zoom")] - fn track_set_zoom (&mut self, _zoom: usize) -> Perhaps { - todo!() - } - #[command(SetName = "track/name")] - fn track_set_name (&mut self, name: Arc) -> Perhaps { - self.track_mut() - .map(|track|swap_value(&mut track.name, &name, |name|TrackCommand::SetName { name })) - .transpose() - .map(Option::flatten) - } - #[command(SetColor = "track/color")] - fn track_set_color (&mut self, color: ItemTheme) -> Perhaps { - self.track_mut() - .map(|track|swap_value(&mut track.color, &color, |color|TrackCommand::SetColor { color })) - .transpose() - .map(Option::flatten) - } - #[command(SetRec = "track/rec")] - fn track_set_rec (&mut self, rec: Option) -> Perhaps { - self.track_mut() - .map(|track|toggle_bool(&mut track.sequencer.recording, &rec, |rec|TrackCommand::SetRec { rec })) - .transpose() - .map(Option::flatten) - } - #[command(SetMon = "track/mon")] - fn track_set_mon (&mut self, mon: Option) -> Perhaps { - self.track_mut() - .map(|track|toggle_bool(&mut track.sequencer.monitoring, &mon, |mon|TrackCommand::SetMon { mon })) - .transpose() - .map(Option::flatten) - } -} - pub(crate) fn track_width (_index: usize, track: &Track) -> u16 { track.width as u16 } @@ -260,90 +195,6 @@ pub trait HasTracks: AsRef> + AsMut> + HasClock + HasTrack } } -impl - + Namespace - + Namespace> - + Namespace> - + Namespace>> - + Namespace> - + Namespace>> - + Namespace>>>>> -> TracksController for T {} - -#[tek_proc::commands(TracksCommand)] -pub trait TracksController: HasTracks - + HasScenes - + HasSelection - + HasJack<'static> - + Namespace - + Namespace> - + Namespace> - + Namespace>> - + Namespace> - + Namespace>> - + Namespace>>>>> -{ - #[command(Stop = "stop")] - /// Stop all playing clips - fn tracks_stop_all (&mut self) -> Perhaps { - for track in self.tracks_mut().iter_mut() { - track.sequencer.enqueue_next(None); - } - Ok(None) - } - - #[command(Launch = "launch")] - /// Launch multiple clips - 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()); - } - } else { - for track in self.tracks_mut().iter_mut() { - track.sequencer.enqueue_next(None); - } - } - Ok(None) - } - - #[command(Add = "add")] - fn tracks_add (&mut self) -> Perhaps - where Self: HasScenes + HasJack<'static> - { - let (_index, _) = self.tracks_add_one(None, None, [].into(), [].into())?; - Ok(None) - } - - #[command(Select = "select")] - fn track_select (&mut self) -> Perhaps { - let tracks = self.tracks().len(); - let select = self.selection_mut().select_track(tracks); - *self.selection_mut() = select; - Ok(None) - } - - #[command(Next = "next")] - fn track_select_next (&mut self) -> Perhaps { - let tracks = self.tracks().len(); - let select = self.selection().select_track_next(tracks); - *self.selection_mut() = select; - Ok(None) - } - - #[command(Prev = "prev")] - fn track_select_prev (&mut self) -> Perhaps { - let select = self.selection_mut().select_track_prev(); - *self.selection_mut() = select; - Ok(None) - } -} - impl< T: HasTracks + ScenesView + HasMidiIns + HasMidiOuts + HasTrack > TracksView for T {} @@ -696,31 +547,6 @@ impl HasTrackScroll for App { } } -fn view_track_row_section <'a> ( - _theme: ItemTheme, - button: impl Draw, - button_add: impl Draw, - content: impl Draw, -) -> impl Draw { - bar( - Split::West, - 4u16, - button_add.align_ne().full_wh(), - bar( - Split::East, - 20u16, - button.full_wh().align_nw(), - content.align_c().full_wh() - ) - ) -} - -pub fn view_track_header <'a> (theme: ItemTheme, content: impl Draw) - -> impl Draw -{ - bg(theme.darker.term, content.align_e().full_w()).exact_w(12) -} - //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 diff --git a/src/sampler.rs b/src/smpl.rs similarity index 59% rename from src/sampler.rs rename to src/smpl.rs index 63947297..5337ec7c 100644 --- a/src/sampler.rs +++ b/src/smpl.rs @@ -8,81 +8,6 @@ pub(crate) use symphonia::{ }, }; -pub trait HasSampler: AsRef + AsMut { - fn sampler (&self) -> &Sampler { - self.as_ref() - } - fn sampler_mut (&mut self) -> &mut Sampler { - self.as_mut() - } -} - -impl + AsMut> HasSampler for T {} - -#[tek_proc::commands(SamplerCommand = "sampler")] -pub trait SamplerController: HasSampler - + Namespace -{ - - #[command(RecordToggle = "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 _ = SamplerCommand::RecordFinish.dispatch(self)?; - // autoslice: continue recording at next slot - if recording != Some(slot) { - SamplerCommand::RecordBegin { slot }.dispatch(self) - } else { - Ok(None) - } - } - - #[command(RecordBegin = "rec-begin")] - fn record_begin (&mut self, slot: usize) -> Perhaps { - let sampler = self.sampler_mut(); - sampler.recording = Some(( - slot, - Some(Arc::new(RwLock::new(Sample::new( - "Sample", 0, 0, vec![vec![]; sampler.audio_ins.len()] - )))) - )); - Ok(None) - } - - #[command(RecordFinish = "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]); - sample - }); // TODO: undo - Ok(None) - } - - #[command(RecordCancel = "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))); - } - Ok(None) - } - - #[command(StopSample = "sample-stop")] - fn sample_stop (&mut self, slot: usize) -> Perhaps { - todo!(); - //Ok(None) - } -} - /// Plays [Voice]s from [Sample]s. /// /// ``` @@ -133,6 +58,17 @@ pub trait SamplerController: HasSampler #[cfg(feature = "meter")] pub output_meters: Vec, } +pub trait HasSampler: AsRef + AsMut { + fn sampler (&self) -> &Sampler { + self.as_ref() + } + fn sampler_mut (&mut self) -> &mut Sampler { + self.as_mut() + } +} + +impl + AsMut> HasSampler for T {} + impl_audio!(Sampler: sampler_jack_process); pub(crate) fn sampler_jack_process (state: &mut Sampler, _: &Client, scope: &ProcessScope) -> Control { @@ -314,84 +250,6 @@ impl Sampler { } } -fn draw_list_item (sample: &Option>>) -> String { - if let Some(sample) = sample { - let sample = sample.try_read().unwrap(); - format!("{:8}", sample.name) - //format!("{:8} {:3} {:6}-{:6}/{:6}", - //sample.name, - //sample.gain, - //sample.start, - //sample.end, - //sample.channels[0].len() - //) - } else { - String::from("........") - } -} - -fn draw_viewer (sample: Option<&Arc>>) -> impl Draw + use<'_> { - let min_db = -64.0; - draw(move|to: &mut Tui|{ - let xywh = to.area().into(); - let XYWH(x, y, width, height) = xywh; - let area = Rect { x, y, width, height }; - if let Some(sample) = &sample { - let sample = sample.try_read().unwrap(); - let start = sample.start as f64; - let end = sample.end as f64; - let length = end - start; - let step = length / width as f64; - let mut t = start; - let mut lines = vec![]; - while t < end { - let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; - let total: f32 = chunk.iter().map(|x|x.abs()).sum(); - let count = chunk.len() as f32; - let meter = 10. * (total / count).log10(); - let x = t as f64; - let y = meter as f64; - lines.push(Line::new(x, min_db, x, y, Color::Green)); - t += step / 2.; - } - to.buffer().map(|buf|{ - Canvas::default() - .x_bounds([sample.start as f64, sample.end as f64]) - .y_bounds([min_db, 0.]) - .paint(|ctx| { - for line in lines.iter() { - ctx.draw(line); - } - //FIXME: proportions - //let text = "press record to finish sampling"; - //ctx.print( - //(width - text.len() as u16) as f64 / 2.0, - //height as f64 / 2.0, - //text.red() - //); - }) - .render(area, buf); - }); - } else { - to.buffer().map(|buf|{ - Canvas::default() - .x_bounds([0.0, width as f64]) - .y_bounds([0.0, height as f64]) - .paint(|_ctx| { - //let text = "press record to begin sampling"; - //ctx.print( - //(width - text.len() as u16) as f64 / 2.0, - //height as f64 / 2.0, - //text.red() - //); - }) - .render(area, buf); - }); - } - Ok(Some(xywh)) - }) -} - /// Create [Voice]s from [Sample]s in response to MIDI input. fn sampler_midi_in ( samples: &SampleKit<128>, voices: &Arc>>, RawMidi { time, bytes }: RawMidi @@ -411,71 +269,6 @@ fn sampler_midi_in ( } } -fn draw_sample ( - to: &mut Tui, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool -) -> Usually { - let style = if focus { Style::default().green() } else { Style::default() }; - if focus { - to.blit(&"🬴", x+1, y, Some(style.bold())); - } - let label1 = format!("{:3} {:12}", - note.map(|n|n.to_string()).unwrap_or(String::default()), - sample.name); - let label2 = format!("{:>6} {:>6} +0.0", - sample.start, - sample.end); - to.blit(&label1, x+2, y, Some(style.bold())); - to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); - Ok(label1.len() + label2.len() + 4) -} - -pub fn view_sample_info (sample: Option<&Arc>>) -> impl Draw + use<'_> { - when(sample.is_some(), draw(move|to: &mut Tui|{ - let sample = sample.unwrap().try_read().unwrap(); - let theme = sample.color; - east!( - field_h(theme, "Name", format!("{:<10}", sample.name.clone())), - field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())), - field_h(theme, "Start", format!("{:<8}", sample.start)), - field_h(theme, "End", format!("{:<8}", sample.end)), - field_h(theme, "Trans", "0"), - field_h(theme, "Gain", format!("{}", sample.gain)), - ).draw(to) - })) -} - -pub fn view_sample_info_v (sample: Option<&Arc>>) -> impl Draw + use<'_> { - let a = draw(move|to: &mut Tui|{ - let sample = sample.unwrap().try_read().unwrap(); - let theme = sample.color; - south!( - field_h(theme, "Name ", format!("{:<10}", sample.name.clone())) .align_w().full_w(), - field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())).align_w().full_w(), - field_h(theme, "Start ", format!("{:<8}", sample.start)) .align_w().full_w(), - field_h(theme, "End ", format!("{:<8}", sample.end)) .align_w().full_w(), - field_h(theme, "Trans ", "0") .align_w().full_w(), - field_h(theme, "Gain ", format!("{}", sample.gain)) .align_w().full_w(), - ).exact_w(20).draw(to) - }); - - let b = draw(|to: &mut Tui|fg(Red, south!( - bold(true, "× No sample."), - "[r] record", - "[Shift-F9] import", - )).draw(to)); - - either(sample.is_some(), a, b) -} - -pub fn view_sample_status (sample: Option<&Arc>>) -> impl Draw { - bold(true, fg(g(224), sample - .map(|sample|{ - let sample = sample.try_read().unwrap(); - format!("Sample {}-{}", sample.start, sample.end) - }) - .unwrap_or_else(||"No sample".to_string()))) -} - /// Get the first sampler of the active track impl Project { pub fn sampler (&self) -> Option<&Sampler> { diff --git a/src/sample_add.rs b/src/smpl_add.rs similarity index 100% rename from src/sample_add.rs rename to src/smpl_add.rs diff --git a/src/sample_kit.rs b/src/smpl_kit.rs similarity index 100% rename from src/sample_kit.rs rename to src/smpl_kit.rs diff --git a/src/sample.rs b/src/smpl_sample.rs similarity index 100% rename from src/sample.rs rename to src/smpl_sample.rs diff --git a/src/sample_voice.rs b/src/smpl_voice.rs similarity index 100% rename from src/sample_voice.rs rename to src/smpl_voice.rs diff --git a/src/tek.rs b/src/tek.rs index bd2dfb92..e85d0dfd 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -1,5 +1,34 @@ #![allow(clippy::unit_arg)] +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::*; +mod midi_point; pub use self::midi_point::*; +mod midi_range; pub use self::midi_range::*; + +mod smpl; pub use self::smpl::*; +mod smpl_sample; pub use self::smpl_sample::*; +mod smpl_add; pub use self::smpl_add::*; +mod smpl_kit; pub use self::smpl_kit::*; +mod smpl_voice; pub use self::smpl_voice::*; + +mod midi_clip; pub use self::midi_clip::*; +mod midi_edit; pub use self::midi_edit::*; +mod midi_mon; pub use self::midi_mon::*; +mod midi_play; pub use self::midi_play::*; +mod midi_rec; pub use self::midi_rec::*; +mod midi_seq; pub use self::midi_seq::*; + +mod time_base; pub use self::time_base::*; +mod time_moment; pub use self::time_moment::*; +mod time_ticker; pub use self::time_ticker::*; + /// Define a type alias for iterators of sized items (columns). macro_rules! def_sizes_iter { ($Type:ident => $($Item:ty),+) => { @@ -8,57 +37,24 @@ macro_rules! def_sizes_iter { } } -mod app; pub use self::app::*; -mod browse; pub use self::browse::*; -mod cli; pub use self::cli::*; -mod clock; pub use self::clock::*; -mod clock_view; pub use self::clock_view::*; -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 deps; pub use self::deps::*; -mod device; pub use self::device::*; -mod dialog; pub use self::dialog::*; -mod field; pub use self::field::*; -mod memo; pub use self::memo::*; -mod menu_item; pub use self::menu_item::*; -mod meter; pub use self::meter::*; -mod mix; pub use self::mix::*; -mod moment; pub use self::moment::*; -mod octave; pub use self::octave::*; -mod piano; pub use self::piano::*; -mod point; pub use self::point::*; -mod pool; pub use self::pool::*; -mod port; pub use self::port::*; -mod project; pub use self::project::*; -mod range; pub use self::range::*; -mod sampler; pub use self::sampler::*; -mod sample; pub use self::sample::*; -mod sample_add; pub use self::sample_add::*; -mod sample_kit; pub use self::sample_kit::*; -mod sample_voice; pub use self::sample_voice::*; -mod scene; pub use self::scene::*; -mod select; pub use self::select::*; -mod midi_clip; pub use self::midi_clip::*; -mod midi_clips; pub use self::midi_clips::*; -mod midi_edit; pub use self::midi_edit::*; -mod midi_mon; pub use self::midi_mon::*; -mod midi_play; pub use self::midi_play::*; -mod midi_rec; pub use self::midi_rec::*; -mod midi_seq; pub use self::midi_seq::*; -mod ticker; pub use self::ticker::*; -mod timebase; pub use self::timebase::*; -mod track; pub use self::track::*; +mod proj; pub use self::proj::*; +mod proj_clock; pub use self::proj_clock::*; +mod proj_device; pub use self::proj_device::*; +mod proj_mixer; pub use self::proj_mixer::*; +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::*; -#[cfg(feature = "lv2")] mod lv2; -#[cfg(feature = "lv2")] pub use self::lv2::*; +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::*; -#[cfg(feature = "lv2_gui")] mod lv2_gui; -#[cfg(feature = "lv2_gui")] pub use self::lv2_gui::*; - -#[cfg(feature = "vst2")] mod vst2; -#[cfg(feature = "vst2")] pub use self::vst2::*; +#[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::*; +#[cfg(feature = "vst2")] mod device_vst2; #[cfg(feature = "vst2")] pub use self::device_vst2::*; /// Command-line entrypoint. #[allow(unused)] @@ -102,3 +98,689 @@ fn run_new_plain (config: Config) -> Usually<()> { Ok(app) })?) } + +/// Total application state. +/// +/// ``` +/// use tek::{HasTracks, HasScenes, TracksView, ScenesView}; +/// let mut app = tek::App::default(); +/// let _ = app.scenes_add_one(None, None).unwrap(); +/// let _ = app.update_clock(); +/// app.project.editor = Some(Default::default()); +/// //let _: Vec<_> = app.project.inputs_with_sizes().collect(); +/// //let _: Vec<_> = app.project.outputs_with_sizes().collect(); +/// let _: Vec<_> = app.project.tracks_with_sizes().collect(); +/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect(); +/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect(); +/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect(); +/// let _ = app.project.w(); +/// //let _ = app.project.w_sidebar(); +/// //let _ = app.project.w_tracks_area(); +/// let _ = app.project.h(); +/// //let _ = app.project.h_tracks_area(); +/// //let _ = app.project.h_inputs(); +/// //let _ = app.project.h_outputs(); +/// let _ = app.project.h_scenes(); +/// ``` +#[derive(Default, Debug)] +#[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(ItemColor)] +#[namespace(ItemTheme)] +#[namespace(MidiClip)] +#[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)] +#[namespace(Option get_opt_usize)] +#[namespace(PathBuf get_path_buf)] +#[namespace(Vec> get_vec_arc_str)] +#[namespace(bool get_bool)] +#[namespace(f64 get_f64)] +#[namespace(isize)] +#[namespace(u16 get_u16)] +#[namespace(u32)] +#[namespace(u8)] +#[namespace(usize get_usize)] +pub struct App { + /// Exit flag + pub exit: Exit, + /// Base color. + pub color: ItemTheme, + /// Must not be dropped for the duration of the process + pub jack: Jack<'static>, + /// Display size + pub size: Sizer, + /// Performance counter + pub perf: PerfModel, + /// Available view modes and input bindings + pub config: Arc, + /// Currently selected mode + pub modes: Arc>>>, + /// Undo history + pub history: Vec<(AppCmd, Option)>, + /// Dialog overlay + pub dialog: Dialog, + /// Contains the currently edited musical arrangement + pub project: Project, + /// Error, if any + pub error: Arc>>>, + #[cfg(feature = "prof2")] + /// Tracing guard + pub guard: Option>>, +} + +impl App { + /// Create a new application instance from a backend, project, config, and mode + /// + /// ``` + /// let mut proj = tek::Project::default(); + /// proj.jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack"); + /// let config = "(mode hi (name Hello) (info World))"; + /// let mut conf = tek::Config::load(tek::Config::default(), config).unwrap(); + /// let tek = tek::App::new(None, proj, conf.into(), "hello"); + /// ``` + pub fn new ( + exit: Option, + project: Project, + config: Arc, + mode: impl AsRef + ) -> Self { + App { + exit: exit.unwrap_or_default(), + jack: project.jack.clone(), + color: ItemTheme::random(), + dialog: Dialog::welcome(), + modes: Arc::new(RwLock::new(vec![mode.as_ref().into()])), + config, + project, + ..Default::default() + } + } +} + +fn get_arc_str <'a> (_: &App, src: impl Language) -> Perhaps> { + Ok(src.src()?.map(|x|x.into())) +} + +fn get_u16 <'a> (state: &App, src: impl Language) -> Perhaps { + Ok(Some(match src.word()? { + Some(":w/sidebar") => state.project.w_sidebar(state.editor().is_some()), + Some(":h/sample-detail") => 6.max(state.size.h() as u16 * 3 / 9), + _ => return try_to_u16(src) + })) +} + +fn get_usize <'a> (state: &App, src: impl Language) -> Perhaps { + Ok(Some(match src.word()? { + Some(":scene-count") => state.scenes().len(), + Some(":track-count") => state.tracks().len(), + Some(":device-kind") => state.dialog.device_kind().unwrap_or(0), + Some(":device-kind/next") => state.dialog.device_kind_next().unwrap_or(0), + Some(":device-kind/prev") => state.dialog.device_kind_prev().unwrap_or(0), + _ => return try_to_usize(src) + })) +} + +fn get_f64 <'a> (_state: &App, _src: impl Language) -> Perhaps { + todo!() +} + +fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps { + src.word()?.map(|word|Ok(match word { + "Y" => true, + "N" => false, + ":mode/editor" => state.project.editor.is_some(), + ":focused/dialog" => !matches!(state.dialog, Dialog::None), + ":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), + _ => return err!("not bool: {word}") + })).transpose() +} + +#[allow(unused)] +fn get_selection <'a> (state: &App, src: impl Language) -> Perhaps { + src.word()?.map(|word|Ok(match word { + ":select/scene" => state.selection().select_scene(state.tracks().len()), + ":select/scene/next" => state.selection().select_scene_next(state.scenes().len()), + ":select/scene/prev" => state.selection().select_scene_prev(), + ":select/track" => state.selection().select_track(state.tracks().len()), + ":select/track/next" => state.selection().select_track_next(state.tracks().len()), + ":select/track/prev" => state.selection().select_track_prev(), + _ => return err!("not selection: {word}") + })).transpose() +} + +fn get_color <'a> (_: &App, src: impl Language) -> Perhaps { + if let Some(expr) = src.expr()? { + match (expr.head()?, expr.tail()?) { + (Some("g"), Some(tail)) => { + let n = try_to_u8(tail.head().map_err(Into::into))? + .ok_or(LanguageError::domain("not gray"))?; + Ok(Some(Color::Rgb(n, n, n))) + }, + (Some("rgb"), Some(tail)) => { + let r = try_to_u8(tail.head().map_err(Into::into))? + .ok_or(LanguageError::domain("not red"))?; + let g = try_to_u8(tail.tail().head().map_err(Into::into))? + .ok_or(LanguageError::domain("not green"))?; + let b = try_to_u8(tail.tail().tail().head().map_err(Into::into))? + .ok_or(LanguageError::domain("not blue"))?; + Ok(Some(Color::Rgb(r, g, b))) + }, + (Some(_), _) => return err!("not a color expression: {expr}"), + (None, _) => return err!("not a color expression: {expr}"), + } + } else if let Ok(Some(sym)) = src.word() { + Ok(match sym { + ":color/bg" => Some(Color::Rgb(28, 32, 36)), + ":color/fg" => Some(Color::Rgb(98, 92, 96)), + _ => return err!("not a color: {sym}") + }) + } else { + return err!("not a color: {:?}", src.src()?) + } +} + +#[allow(unused)] +fn get_opt_u7 <'a> (state: &App, src: impl Language) -> Perhaps> { + eval_word(src, |word|Ok(match word { + + ":editor/pitch" => Some(( + state.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8 + ).into()), + + _ => return err!("unknown midi note: {word}") + + })) +} + +fn get_opt_usize <'a> (state: &App, src: impl Language) -> Perhaps> { + eval_word(src, |word|Ok(match word { + + ":selected/scene" => state.selection().scene(), + ":selected/track" => state.selection().track(), + + _ => return err!("unknown opt: {word}") + + })) +} + +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) +} + +fn get_opt_u32 <'a> (_: &App, _src: impl Language) -> Perhaps> { + Ok(None) +} + +fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps> { + todo!() +} + +fn get_opt_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps>> { + todo!() +} + +fn get_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps> { + todo!() +} + +fn get_opt_arc_str <'a> (_: &App, _: impl Language) -> Perhaps>> { + todo!() +} + +fn get_opt_bool <'a> (_: &App, _: impl Language) -> Perhaps> { + todo!() +} + +fn get_path_buf <'a> (_: &App, _: impl Language) -> Perhaps { + todo!() +} + +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!(Jack<'static>: |self: App|self.jack); +impl_has!(Pool: |self: App|self.project.pool); +impl_has!(Selection: |self: App|self.project.selection); +impl_as_ref!(Option: |self: App|self.project.as_ref()); +impl_as_mut!(Option: |self: App|self.project.as_mut()); + +primitive!(u8: try_to_u8); +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) +}); + +impl App { + + fn input_cmd (state: &mut Self, input: &TuiEvent) -> Usually<()> { + if let Some(bindings) = Self::input_cmd_find(state, input) { + return Self::input_cmd_exec(state, bindings) + } + Ok(()) + } + + fn input_cmd_find (state: &mut Self, input: &TuiEvent) -> Option>]>> { + for name in state.modes.read().iter().rev() { + let mode = state.config.get_mode(name).as_ref().map(Arc::clone); + if let Some(mode) = mode { + let binds = state.config.binds.clone(); + for id in mode.keys.iter() { + if let Some(event_map) = binds.find(id) + && let Some(bindings) = event_map.query(input) { + return Some(bindings.into()) + } + } + } + } + None + } + + fn input_cmd_exec (state: &mut Self, bindings: Arc<[Binding>]>) -> Usually<()> { + for binding in bindings.iter() { + for cmd in binding.commands.iter() { + if Self::cmd::(state, cmd)? { + return Ok(()) + } + } + } + Ok(()) + } + + fn cmd < + T: Dispatch + Clone + Into + > (state: &mut Self, src: &str) -> Usually where + Self: Namespace, + { + if let Some(cmd) = Namespace::::namespace(state, &src)? { + return Self::cmd_dispatch(state, cmd.into()) + } + Ok(false) + } + + fn cmd_dispatch (state: &mut Self, cmd: AppCmd) -> Usually where + Self: Namespace, + { + match cmd.clone().dispatch(state) { + Err(err) => { + state.history.push((cmd.into(), None)); + return Err(err) + }, + Ok(undo) => { + state.history.push((cmd.into(), undo.map(Into::into))); + return Ok(true) + } + } + } +} + +/// A control axis. +/// +/// ``` +/// let axis = tek::ControlAxis::X; +/// ``` +#[derive(Debug, Copy, Clone)] pub enum ControlAxis { + X, XFine, + Y, YFine, + Z, ZFine, + 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 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. + fn width_inc (&mut self); + /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. + 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. +/// +/// ``` +/// let dialog: tek::Dialog = Default::default(); +/// ``` +#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog { + #[default] None, + Help(usize), + Menu(usize, Arc<[MenuItem]>), + Device(usize), + Message(Arc), + Browse(BrowseTarget, Arc), + Options, +} + +impl Dialog { + + /// ``` + /// let _ = tek::Dialog::welcome(); + /// ``` + pub fn welcome () -> Self { + Self::Menu(1, Arc::new([ + + MenuItem::new(" Resume session ", [ + AppCmd::Project { + command: ProjectCmd::LoadLast + } + ]), + + MenuItem::new(" Create session ", [ + AppCmd::Dialog { + dialog: Dialog::None + }, + AppCmd::Mode { + command: ModeCmd::Reset { + modes: vec!["Arranger".into()] + } + }, + AppCmd::Tracks { + command: TracksCmd::Add + }, + AppCmd::Scenes { + command: ScenesCmd::Add + } + ]), + + MenuItem::new(" Load session ", [ + AppCmd::Dialog { + dialog: Dialog::Browse( + BrowseTarget::LoadProject, + Browse::new(None).unwrap().into() + ) + //dialog: Dialog::Browse { + //title: "Load session".into(), + //filter: "\\.tek$", + //} + } + ]), + + MenuItem::new(" Exit ", [ + AppCmd::Exit + ]), + + ])) + } + + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().menu_selected(); + /// ``` + pub fn menu_selected (&self) -> Option { + if let Self::Menu(selected, _) = self { Some(*selected) } else { None } + } + + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().menu_next(); + /// ``` + pub fn menu_next (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.len()), items.clone()), + _ => Self::None + } + } + + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().menu_prev(); + /// ``` + pub fn menu_prev (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.len()), items.clone()), + _ => Self::None + } + } + + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().device_kind(); + /// ``` + pub fn device_kind (&self) -> Option { + if let Self::Device(index) = self { Some(*index) } else { None } + } + + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().device_kind_next(); + /// ``` + pub fn device_kind_next (&self) -> Option { + self.device_kind().map(|index|(index + 1) % device_kinds().len()) + } + + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().device_kind_prev(); + /// ``` + pub fn device_kind_prev (&self) -> Option { + self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) + } + + /// FIXME: implement + pub fn message (&self) -> Option<&str> { todo!() } + + /// FIXME: implement + pub fn browser (&self) -> Option<&Arc> { todo!() } + + /// FIXME: implement + pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() } + +} + +/// Increment a wrapping counter. +pub const fn wrap_inc (index: usize, count: usize) -> usize { + if count > 0 { (index + 1) % count } else { 0 } +} + +/// Decrement a wrapping counter. +pub const fn wrap_dec (index: usize, count: usize) -> usize { + if count > 0 { + let a = index.overflowing_sub(1).0; + let b = count.saturating_sub(1); + if a < b { a } else { b } + } else { + 0 + } +} + +/// An item of a menu. +/// +/// ``` +/// let item: tek::MenuItem = Default::default(); +/// ``` +#[derive(Clone)] pub struct MenuItem( + pub Arc, + pub Arc<[AppCmd]>, +); + +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 + //} +//} diff --git a/src/browse.rs b/src/tek_browse.rs similarity index 76% rename from src/browse.rs rename to src/tek_browse.rs index be3bb3ec..c61646af 100644 --- a/src/browse.rs +++ b/src/tek_browse.rs @@ -1,50 +1,5 @@ use crate::*; -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 } - } -} - -impl + - Namespace + - Namespace> -> BrowseController for T {} - -#[tek_proc::commands(BrowseCommand = "browse")] -pub trait BrowseController: - Namespace + - Namespace + - Namespace> -{ - /// Toggle visibility of browser - #[command(Show = "show")] - fn show (&mut self) -> Perhaps { - todo!() - } - /// Set current directory - #[command(Chdir = "chdir")] - fn chdir (&mut self, _path: PathBuf) -> Perhaps { - todo!() - } - /// Set current filter - #[command(SetSearch = "set-search")] - fn set_search (&mut self, _filter: Arc) -> Perhaps { - todo!() - } - /// Set selected item - #[command(SetCursor = "set-cursor")] - fn set_cursor (&mut self, _index: usize) -> Perhaps { - todo!() - } -} - /// Browses for files to load/save. /// /// ``` @@ -179,13 +134,13 @@ pub fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { Ok((subdirs, files)) } -pub fn view_browse_title (state: &App) -> impl Draw { - field_v(ItemTheme::default(), match state.dialog.browser_target().unwrap() { - BrowseTarget::SaveProject => "Save project:", - BrowseTarget::LoadProject => "Load project:", - BrowseTarget::ImportSample(_) => "Import sample:", - BrowseTarget::ExportSample(_) => "Export sample:", - BrowseTarget::ImportClip(_) => "Import clip:", - BrowseTarget::ExportClip(_) => "Export clip:", - }, fg(g(96), x_repeat("🭻")).exact_h(1)).align_w().full_w() +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/cli.rs b/src/tek_cli.rs similarity index 100% rename from src/cli.rs rename to src/tek_cli.rs diff --git a/src/tek_ctrl.rs b/src/tek_ctrl.rs new file mode 100644 index 00000000..3495b25e --- /dev/null +++ b/src/tek_ctrl.rs @@ -0,0 +1,822 @@ +use crate::*; + +impl_from!(AppCmd: |x: AxisCmd| AppCmd::Axis { command: x }); +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: ProjectCmd| AppCmd::Project { command: x }); +impl_from!(AppCmd: |x: SceneCmd| AppCmd::Scene { command: x }); +impl_from!(AppCmd: |x: ScenesCmd| AppCmd::Scenes { command: x }); +impl_from!(AppCmd: |x: TrackCmd| AppCmd::Track { command: x }); +impl_from!(AppCmd: |x: TracksCmd| AppCmd::Tracks { command: x }); + +#[tek_proc::commands(AppCmd)] impl App { + /// Exit the application. + #[command(Exit = "exit")] pub fn cmd_exit (&mut self) -> Perhaps { + self.exit.exit(); + Ok(None) + } + /// Cancel current selection. + #[command(Cancel = "cancel")] pub fn cmd_cancel (&mut self) -> Perhaps { + todo!("cancel") + } + /// Confirm current selection. + #[command(Confirm = "confirm")] pub fn cmd_confirm (&mut self) -> Perhaps { + Ok(match self.dialog.clone() { + Dialog::Menu(index, items) => { + for command in items[index].1.iter() { + App::cmd_dispatch(self, command.clone())?; + } + None + } + _ => todo!(), + }) + } + /// Swap currently active modal dialog. + /// + /// ``` + /// let _ = tek::App::default().cmd_dialog(tek::Dialog::welcome()); + /// ``` + #[command(Dialog = "dialog")] pub fn cmd_dialog (&mut self, dialog: Dialog) -> Perhaps { + let mut dialog = dialog.clone(); + std::mem::swap(&mut self.dialog, &mut dialog); + Ok(Some(AppCmd::Dialog { dialog })) + } + #[command(Mode = "mode")] pub fn cmd_mode (&mut self, command: ModeCmd) -> Perhaps { + Ok(command.dispatch(self)?.map(Into::into)) + } + #[command(Project = "session")] pub fn cmd_session (&mut self, command: ProjectCmd) -> Perhaps { + Ok(command.dispatch(self)?.map(Into::into)) + } + #[command(Axis = "axis")] pub fn cmd_axis (&mut self, command: AxisCmd) -> Perhaps { + Ok(command.dispatch(self)?.map(Into::into)) + } + #[command(Browse = "browse")] pub fn cmd_browse (&mut self, command: BrowseCmd) -> Perhaps { + Ok(command.dispatch(self)?.map(Into::into)) + } + #[command(MidiClip = "clip")] pub fn cmd_midi_clip (&mut self, command: MidiClipCmd) -> Perhaps { + Ok(command.dispatch(self)?.map(Into::into)) + } + #[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(Scene = "scene")] pub fn cmd_scene (&mut self, command: SceneCmd) -> Perhaps { + Ok(command.dispatch(self)?.map(Into::into)) + } + #[command(Scenes = "scenes")] pub fn cmd_scenes (&mut self, command: ScenesCmd) -> Perhaps { + Ok(command.dispatch(self)?.map(Into::into)) + } + #[command(Track = "track")] pub fn cmd_track (&mut self, command: TrackCmd) -> Perhaps { + Ok(command.dispatch(self)?.map(Into::into)) + } + #[command(Tracks = "tracks")] pub fn cmd_tracks (&mut self, command: TracksCmd) -> Perhaps { + Ok(command.dispatch(self)?.map(Into::into)) + } +} + +impl ProjectControl for T {} + +#[tek_proc::commands(ProjectCmd)] pub trait ProjectControl: HasProject { + #[command(LoadLast = "load-last")] fn load_last (&mut self) -> Perhaps { + todo!() + } + #[command(Load = "load")] fn load (&mut self) -> Perhaps { + todo!() + } +} + +impl + Namespace + Namespace>> ClockControl for T {} + +#[tek_proc::commands(ClockCmd = "clock")] pub trait ClockControl: HasClock + + Namespace + + Namespace + + Namespace> +{ + #[command(SeekUsec = "usec")] fn seek_usec (&mut self, usec: f64) -> Perhaps { + self.clock().playhead.update_from_usec(usec); + Ok(None) + } + #[command(SeekSample = "sample")] fn seek_sample (&mut self, sample: f64) -> Perhaps { + self.clock().playhead.update_from_sample(sample); + Ok(None) + } + #[command(SeekPulse = "pulse")] fn seek_pulse (&mut self, pulse: f64) -> Perhaps { + self.clock().playhead.update_from_pulse(pulse); + Ok(None) + } + #[command(SetBpm = "bpm")] fn set_bpm (&mut self, bpm: f64) -> Perhaps { + Ok(Some(ClockCmd::SetBpm { + bpm: self.clock().timebase().bpm.set(bpm) + })) + } + #[command(SetQuant = "quant")] fn set_quant (&mut self, quant: f64) -> Perhaps { + Ok(Some(ClockCmd::SetQuant { + quant: self.clock().quant.set(quant) + })) + } + #[command(SetSync = "sync")] fn set_sync (&mut self, sync: f64) -> Perhaps { + Ok(Some(ClockCmd::SetSync { + sync: self.clock().sync.set(sync) + })) + } + #[command(Play = "play")] fn play (&mut self, position: Option) -> Perhaps { + self.clock().play_from(position)?; + Ok(None) /* TODO Some(Pause(previousPosition)) */ + } + #[command(Pause = "pause")] fn pause (&mut self, position: Option) -> Perhaps { + self.clock().pause_at(position)?; + Ok(None) + } + #[command(TogglePlayback = "toggle")] fn toggle (&mut self, position: u32) -> Perhaps { + Ok(if self.clock().is_rolling() { + self.clock().pause_at(Some(position))?; + None + } else { + self.clock().play_from(Some(position))?; + None + }) + } +} + +impl + + Namespace + + Namespace> + + Namespace> + + Namespace>> + + Namespace> + + Namespace>> + + Namespace>>>>> +> TracksControl for T {} + +#[tek_proc::commands(TracksCmd)] pub trait TracksControl: HasTracks + + HasScenes + + HasSelection + + HasJack<'static> + + Namespace + + Namespace> + + Namespace> + + Namespace>> + + Namespace> + + Namespace>> + + Namespace>>>>> +{ + /// Stop all playing clips + #[command(Stop = "stop")] fn tracks_stop_all (&mut self) -> Perhaps { + for track in self.tracks_mut().iter_mut() { + track.sequencer.enqueue_next(None); + } + Ok(None) + } + /// Launch multiple clips + #[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()); + } + } else { + for track in self.tracks_mut().iter_mut() { + track.sequencer.enqueue_next(None); + } + } + Ok(None) + } + #[command(Add = "add")] fn tracks_add (&mut self) -> Perhaps + where Self: HasScenes + HasJack<'static> + { + let (_index, _) = self.tracks_add_one(None, None, [].into(), [].into())?; + Ok(None) + } + #[command(Select = "select")] fn track_select (&mut self) -> Perhaps { + let tracks = self.tracks().len(); + let select = self.selection_mut().select_track(tracks); + *self.selection_mut() = select; + Ok(None) + } + #[command(Next = "next")] fn track_select_next (&mut self) -> Perhaps { + let tracks = self.tracks().len(); + let select = self.selection().select_track_next(tracks); + *self.selection_mut() = select; + Ok(None) + } + #[command(Prev = "prev")] fn track_select_prev (&mut self) -> Perhaps { + let select = self.selection_mut().select_track_prev(); + *self.selection_mut() = select; + Ok(None) + } +} + +impl + + Namespace> + + Namespace + + Namespace> +> TrackControl for T {} + +#[tek_proc::commands(TrackCmd)] pub trait TrackControl: HasTrack + + Namespace + + Namespace> + + Namespace + + Namespace> +{ + #[command(Stop = "track/stop")] + fn track_stop (&mut self) -> Perhaps { + self.track_mut().map(|track|track.sequencer.enqueue_next(None)); + Ok(None) + } + #[command(SetMute = "track/mute")] + fn track_set_mute (&mut self, _mute: Option) -> Perhaps { + todo!() + } + #[command(SetSolo = "track/solo")] + fn track_set_solo (&mut self, _solo: Option) -> Perhaps { + todo!() + } + #[command(SetSize = "track/size")] + fn track_set_size (&mut self, _size: usize) -> Perhaps { + todo!() + } + #[command(SetZoom = "track/zoom")] + fn track_set_zoom (&mut self, _zoom: usize) -> Perhaps { + todo!() + } + #[command(SetName = "track/name")] + fn track_set_name (&mut self, name: Arc) -> Perhaps { + self.track_mut() + .map(|track|swap_value(&mut track.name, &name, |name|TrackCmd::SetName { name })) + .transpose() + .map(Option::flatten) + } + #[command(SetColor = "track/color")] + fn track_set_color (&mut self, color: ItemTheme) -> Perhaps { + self.track_mut() + .map(|track|swap_value(&mut track.color, &color, |color|TrackCmd::SetColor { color })) + .transpose() + .map(Option::flatten) + } + #[command(SetRec = "track/rec")] + fn track_set_rec (&mut self, rec: Option) -> Perhaps { + self.track_mut() + .map(|track|toggle_bool(&mut track.sequencer.recording, &rec, |rec|TrackCmd::SetRec { rec })) + .transpose() + .map(Option::flatten) + } + #[command(SetMon = "track/mon")] + fn track_set_mon (&mut self, mon: Option) -> Perhaps { + self.track_mut() + .map(|track|toggle_bool(&mut track.sequencer.monitoring, &mon, |mon|TrackCmd::SetMon { mon })) + .transpose() + .map(Option::flatten) + } +} + +impl + + Namespace + + Namespace> + + Namespace>> +> ScenesControl for T {} + +#[tek_proc::commands(ScenesCmd)] pub trait ScenesControl: HasScenes + + HasTracks + + HasTrackScroll + + HasSelection + + HasJack<'static> + + Namespace + + Namespace> + + Namespace>> +{ + #[command(Add = "add")] fn scenes_add (&mut self) -> Perhaps { + let (_index, _) = self.scenes_add_one(None, None)?; + Ok(None) + } + #[command(Select = "select")] fn scene_select (&mut self) -> Perhaps { + let scenes = self.scenes().len(); + let select = self.selection_mut().select_scene(scenes); + *self.selection_mut() = select; + Ok(None) + } + #[command(Next = "next")] fn scene_select_next (&mut self) -> Perhaps { + let scenes = self.scenes().len(); + let select = self.selection().select_scene_next(scenes); + *self.selection_mut() = select; + Ok(None) + } + #[command(Prev = "prev")] fn scene_select_prev (&mut self) -> Perhaps { + let select = self.selection_mut().select_scene_prev(); + *self.selection_mut() = select; + Ok(None) + } +} + +impl + + Namespace> + + Namespace +> SceneControl for T {} + +#[tek_proc::commands(SceneCmd)] pub trait SceneControl: HasScene + + Namespace + + Namespace> + + Namespace +{ + #[command(SetSize = "size")] fn scene_set_size (&mut self, _size: usize) -> Perhaps { + todo!() + } + #[command(SetZoom = "zoom")] fn scene_set_zoom (&mut self, _size: usize) -> Perhaps { + todo!() + } + #[command(SetName = "name")] fn scene_set_name (&mut self, name: Arc) -> Perhaps { + Ok(self.scene_mut().map(|scene|swap_value( + &mut scene.name, + &name, + |name|SceneCmd::SetName { name } + )).transpose()?.flatten()) + } + #[command(SetColor = "color")] fn scene_set_color (&mut self, color: ItemTheme) -> Perhaps { + Ok(self.scene_mut().map(|scene|swap_value( + &mut scene.color, + &color, + |color|SceneCmd::SetColor { color } + )).transpose()?.flatten()) + } +} + +#[tek_proc::commands(SamplerCmd = "sampler")] pub trait SamplerControl: HasSampler + + Namespace +{ + + #[command(RecordToggle = "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)?; + // autoslice: continue recording at next slot + if recording != Some(slot) { + SamplerCmd::RecordBegin { slot }.dispatch(self) + } else { + Ok(None) + } + } + + #[command(RecordBegin = "rec-begin")] fn record_begin (&mut self, slot: usize) -> Perhaps { + let sampler = self.sampler_mut(); + sampler.recording = Some(( + slot, + Some(Arc::new(RwLock::new(Sample::new( + "Sample", 0, 0, vec![vec![]; sampler.audio_ins.len()] + )))) + )); + Ok(None) + } + #[command(RecordFinish = "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]); + sample + }); // TODO: undo + Ok(None) + } + #[command(RecordCancel = "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))); + } + Ok(None) + } + #[command(StopSample = "sample-stop")] fn sample_stop (&mut self, slot: usize) -> Perhaps { + todo!(); + //Ok(None) + } +} + +impl + + Namespace + + Namespace + + Namespace + + Namespace> + + Namespace>>> +> MidiEditControl for T {} + +#[tek_proc::commands(MidiEditCmd)] pub trait MidiEditControl: HasEditor + + Namespace + + Namespace + + Namespace + + Namespace + + Namespace> + + Namespace>>> +{ + #[command(Toggle = "toggle")] fn edit_toggle (&mut self) -> Perhaps { + if self.is_editing() { + *self.editor_mut() = None; + } else { + todo!() + } + Ok(Some(MidiEditCmd::Toggle)) + } + #[command(Show = "show")] fn edit_show (&mut self, clip: Option>>) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|mut editor|{ + editor.set_clip(clip.as_ref()); + editor.redraw(); + None + }).flatten()) + } + #[command(DeleteNote = "delete")] fn edit_note_delete (&mut self) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|editor|{ + editor.redraw(); + todo!() + }).flatten()) + } + #[command(AppendNote = "append")] fn edit_note_append (&mut self, advance: bool) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|mut editor|{ + editor.put_note(advance); + editor.redraw(); + None + }).flatten()) + } + #[command(SetNotePos = "note-pos")] fn edit_note_set_pos (&mut self, pos: usize) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|editor|{ + editor.set_note_pos((pos).min(127)); + editor.redraw(); + None + }).flatten()) + } + #[command(SetNoteLen = "note-len")] fn edit_note_set_len (&mut self, len: usize) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|editor|{ + editor.set_note_len(len); + editor.redraw(); + None + }).flatten()) + } + #[command(SetNoteScroll = "note-scroll")] fn edit_note_set_scroll (&mut self, scroll: usize) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|editor|{ + editor.set_note_lo((scroll).min(127)); + editor.redraw(); + None + }).flatten()) + } + #[command(SetTimePos = "time-pos")] fn edit_time_set_pos (&mut self, pos: usize) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|editor|{ + editor.set_time_pos(pos); + editor.redraw(); + None + }).flatten()) + } + #[command(SetTimeScroll = "time-scroll")] fn edit_time_set_scroll (&mut self, scroll: usize) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|editor|{ + editor.set_time_start(scroll); + editor.redraw(); + None + }).flatten()) + } + #[command(SetTimeZoom = "time-zoom")] fn edit_time_set_zoom (&mut self, zoom: usize) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|editor|{ + editor.set_time_zoom(zoom); + editor.redraw(); + None + }).flatten()) + } + #[command(SetTimeLock = "time-lock")] fn edit_time_set_lock (&mut self, lock: bool) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|editor|{ + editor.set_time_lock(lock); + editor.redraw(); + None + }).flatten()) + } + // TODO: 1-9 seek markers that by default start every 8th of the clip +} + +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}) + //} 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 +{} + +#[tek_proc::commands(ModeCmd)] pub trait ModeControl: HasModeStack + + Namespace>> + + Namespace> + + Namespace +{ + #[command(Reset = "reset")] fn mode_reset (&mut self, modes: Vec>) -> Perhaps { + *self.modes_mut() = modes.into(); + Ok(None) + } + #[command(Push = "push")] fn mode_push (&mut self, mode: Arc) -> Perhaps { + self.modes_mut().push(mode); + Ok(Some(ModeCmd::Pop)) + } + #[command(Pop = "pop")] fn mode_pop (&mut self) -> Perhaps { + let mode = self.modes_mut().pop(); + Ok(mode.map(|mode|ModeCmd::Push { mode })) + } +} + +impl> + Namespace> > MidiClipControl for T {} +#[tek_proc::commands(MidiClipCmd)] pub trait MidiClipControl: HasClips + + 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!() + } + #[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)) + })) + } + /// 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)) + })) + } +} +impl + Namespace + Namespace>> BrowseControl for T {} +#[tek_proc::commands(BrowseCmd = "browse")] pub trait BrowseControl: + Namespace + + Namespace + + Namespace> +{ + /// Toggle visibility of browser + #[command(Show = "show")] fn show (&mut self) -> Perhaps { + todo!() + } + /// Set current directory + #[command(Chdir = "chdir")] fn chdir (&mut self, _path: PathBuf) -> Perhaps { + todo!() + } + /// Set current filter + #[command(SetSearch = "set-search")] fn set_search (&mut self, _filter: Arc) -> Perhaps { + todo!() + } + /// Set selected item + #[command(SetCursor = "set-cursor")] fn set_cursor (&mut self, _index: usize) -> Perhaps { + todo!() + } +} + +#[tek_proc::commands(AxisCmd)] +impl App { + /// Increment a 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) => { + AppCmd::Dialog { dialog: self.dialog.menu_next() }.dispatch(self)?; + None + }, + _ => todo!() + }) + } + /// Decrement a 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) => { + AppCmd::Dialog { dialog: self.dialog.menu_prev() }.dispatch(self)?; + None + }, + _ => todo!() + }) + } +} diff --git a/src/tek_view.rs b/src/tek_view.rs new file mode 100644 index 00000000..ffb6ca74 --- /dev/null +++ b/src/tek_view.rs @@ -0,0 +1,553 @@ +use crate::*; + +pub fn draw_sample ( + to: &mut Tui, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool +) -> Usually { + let style = if focus { Style::default().green() } else { Style::default() }; + if focus { + to.blit(&"🬴", x+1, y, Some(style.bold())); + } + let label1 = format!("{:3} {:12}", + note.map(|n|n.to_string()).unwrap_or(String::default()), + sample.name); + let label2 = format!("{:>6} {:>6} +0.0", + sample.start, + sample.end); + to.blit(&label1, x+2, y, Some(style.bold())); + to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); + Ok(label1.len() + label2.len() + 4) +} + +pub fn draw_sample_list_item (sample: &Option>>) -> String { + if let Some(sample) = sample { + let sample = sample.try_read().unwrap(); + format!("{:8}", sample.name) + //format!("{:8} {:3} {:6}-{:6}/{:6}", + //sample.name, + //sample.gain, + //sample.start, + //sample.end, + //sample.channels[0].len() + //) + } else { + String::from("........") + } +} + +pub fn draw_sample_viewer (sample: Option<&Arc>>) -> impl Draw + use<'_> { + let min_db = -64.0; + draw(move|to: &mut Tui|{ + let xywh = to.area().into(); + let XYWH(x, y, width, height) = xywh; + let area = Rect { x, y, width, height }; + if let Some(sample) = &sample { + let sample = sample.try_read().unwrap(); + let start = sample.start as f64; + let end = sample.end as f64; + let length = end - start; + let step = length / width as f64; + let mut t = start; + let mut lines = vec![]; + while t < end { + let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; + let total: f32 = chunk.iter().map(|x|x.abs()).sum(); + let count = chunk.len() as f32; + let meter = 10. * (total / count).log10(); + let x = t as f64; + let y = meter as f64; + lines.push(Line::new(x, min_db, x, y, Color::Green)); + t += step / 2.; + } + to.buffer().map(|buf|{ + Canvas::default() + .x_bounds([sample.start as f64, sample.end as f64]) + .y_bounds([min_db, 0.]) + .paint(|ctx| { + for line in lines.iter() { + ctx.draw(line); + } + //FIXME: proportions + //let text = "press record to finish sampling"; + //ctx.print( + //(width - text.len() as u16) as f64 / 2.0, + //height as f64 / 2.0, + //text.red() + //); + }) + .render(area, buf); + }); + } else { + to.buffer().map(|buf|{ + Canvas::default() + .x_bounds([0.0, width as f64]) + .y_bounds([0.0, height as f64]) + .paint(|_ctx| { + //let text = "press record to begin sampling"; + //ctx.print( + //(width - text.len() as u16) as f64 / 2.0, + //height as f64 / 2.0, + //text.red() + //); + }) + .render(area, buf); + }); + } + Ok(Some(xywh)) + }) +} + +pub fn draw_meters (meters: &[f32]) -> impl Draw + use<'_> { + bg(Black, iter_east_fixed(1, ||meters.iter(), |value, _index|{ + RmsMeter(*value).full_h() + }).exact_w(2)) +} + +#[cfg(feature = "lv2")] +pub fn draw_lv2_header (state: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) { + let style = Style::default().gray(); + let label1 = format!(" {}", state.name); + to.blit(&label1, x + 1, y, Some(style.white().bold())); + if let Some(ref path) = state.path { + let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]); + to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim())); + } + //Ok(Rect { x, y, width: w, height: 1 }) +} + +pub fn draw_status_bar (to: &mut Tui, state: &App) -> Drawn { + bar(Split::South, 1u16, + dim(true, "Mode"), + bold(true, state.modes.read().get(0)) + ).push_x(1).draw(to) +} + +/// ``` +/// let x = ""; +/// let _ = tek::view_status(None, x.as_ref(), x.as_ref(), x.as_ref()); +/// let _ = tek::view_status(Some("".into()), x.as_ref(), x.as_ref(), x.as_ref()); +/// ``` +pub fn view_status <'a> (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw { + let theme = ItemTheme::G[96]; + let sr = field_h(theme, "SR", sr); + let buf = field_h(theme, "Buf", buf); + let lat = field_h(theme, "Lat", lat); + bg(Black, east!(above( + sel.map(|sel|field_h(theme, "Selected", sel)).align_w().full_wh(), + east!(sr, buf, lat).align_e().full_wh(), + ))) +} + +pub fn view_device <'a> (state: &'a App) -> impl Draw { + let selected = state.dialog.device_kind().unwrap(); + south( + bold(true, "Add device"), + iter_south(move||device_kinds().iter().enumerate().map(move|(i, _label)|{ + let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; + let l = if i == selected { "[ " } else { " " }; + let r = if i == selected { " ]" } else { " " }; + bg(b, east(l, west(r, "FIXME device name"))).full_w() + }))) +} + +pub fn view_track_row_section <'a> ( + _theme: ItemTheme, + button: impl Draw, + button_add: impl Draw, + content: impl Draw, +) -> impl Draw { + bar( + Split::West, + 4u16, + button_add.align_ne().full_wh(), + bar( + Split::East, + 20u16, + button.full_wh().align_nw(), + content.align_c().full_wh() + ) + ) +} + +pub fn view_track_header <'a> (theme: ItemTheme, content: impl Draw) + -> impl Draw +{ + bg(theme.darker.term, content.align_e().full_w()).exact_w(12) +} + +pub fn view_scene_name <'a> ( + select: &Selection, + editor: Option<&'a MidiEditor>, + index: usize, + scene: &Scene, + editing: bool +) -> impl Draw { + let h = if select.scene() == Some(index) && let Some(_editor) = editor { + 7 + } else { + Scene::DEFAULT_HEIGHT as u16 + }; + let a = east!("·s", index, " ", fg(g(255), bold(true, &scene.name))).align_w().full_w(); + let b = when(select.scene() == Some(index) && editing, south( + editor.as_ref().map(|e|e.clip_status()), + editor.as_ref().map(|e|e.edit_status())).align_nw().full_wh()); + let c = if select.scene() == Some(index) { + scene.color.light.term + } else { + scene.color.base.term + }; + bg(c, south(a, b).align_nw().exact_h(h)) +} + +pub fn view_scene_name_theme (scene: &Scene, track_index: usize) -> (Arc, 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]) + } +} + +pub fn view_scene_bg ( + theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize +) -> (Color, Color) { + let mut outline = theme.base.term; + (if select.track() == Some(track_index) && select.scene() == Some(scene_index) { + outline = theme.lighter.term; + theme.light.term + } else if select.track() == Some(track_index) || select.scene() == Some(scene_index) { + outline = theme.darkest.term; + theme.base.term + } else { + theme.dark.term + }, outline) +} + +pub fn view_scene_w ( + track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor> +) -> u16 { + if select.track() == Some(track_index) && let Some(editor) = editor { + (editor.size.w() as usize).max(24).max(track.width) as u16 + } else { + track.width as u16 + } +} + +pub fn view_scene_y ( + select: &Selection, scene_index: usize, editor: Option<&MidiEditor> +) -> u16 { + if select.scene() == Some(scene_index) && let Some(editor) = editor { + editor.size.h().max(12) + } else { + Scene::DEFAULT_HEIGHT as u16 + } +} + +pub fn view_scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool { + editing && select.track() == Some(track_index) && select.scene() == Some(scene_index) +} + +pub fn view_templates <'a> (state: &'a App) -> impl Draw + use<'a> { + let height = (state.config.modes.len() * 2) as u16; + draw(move |to: &mut Tui|{ + let mut index = 0; + state.config.modes.for_each(&mut |id: &str, profile: &Mode| { + let b = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; + let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); + let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); + let fg1 = Rgb(224, 192, 128); + let fg2 = Rgb(224, 128, 32); + let field_name = fg(fg1, name).align_w().full_w(); + let field_id = fg(fg2, id).align_e().full_w(); + let field_info = info.align_w().full_w(); + let _ = bg(b, south(above(field_name, field_id), field_info)) + .full_w().exact_h(2).push_y((2 * index) as u16).draw(to); + index += 1; + }); + Ok(Some(to.area().into())) + }).min_w(30).exact_h(height) +} + +pub fn view_browse_title (state: &App) -> impl Draw { + field_v(ItemTheme::default(), match state.dialog.browser_target().unwrap() { + BrowseTarget::SaveProject => "Save project:", + BrowseTarget::LoadProject => "Load project:", + BrowseTarget::ImportSample(_) => "Import sample:", + BrowseTarget::ExportSample(_) => "Export sample:", + BrowseTarget::ImportClip(_) => "Import clip:", + BrowseTarget::ExportClip(_) => "Export clip:", + }, fg(g(96), x_repeat("🭻")).exact_h(1)).align_w().full_w() +} + +/// ``` +/// let _ = tek::view_meter("", 0.0); +/// let _ = tek::view_meters(&[0.0, 0.0]); +/// ``` +pub fn view_meter <'a> (label: &'a str, value: f32) + -> impl Draw +{ + let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)); + let w = if value >= 0.0 { 13 } + else if value >= -1.0 { 12 } + else if value >= -2.0 { 11 } + else if value >= -3.0 { 10 } + else if value >= -4.0 { 9 } + else if value >= -6.0 { 8 } + else if value >= -9.0 { 7 } + else if value >= -12.0 { 6 } + else if value >= -15.0 { 5 } + else if value >= -20.0 { 4 } + else if value >= -25.0 { 3 } + else if value >= -30.0 { 2 } + else if value >= -40.0 { 1 } + else { 0 }; + let c = if value >= 0.0 { Red } + else if value >= -3.0 { Yellow } + else { Green }; + south!(f, bg(c, ()).exact_wh(w, 1)) +} + +pub fn view_meters (values: &[f32;2]) -> impl Draw + use<'_> { + let left = format!("L/{:>+9.3}", values[0]); + let right = format!("R/{:>+9.3}", values[1]); + south(left, right) +} + +pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) +} + +pub fn view_midi_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs)) +} + +pub fn view_audio_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins())) +} + +pub fn view_audio_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) +} + +pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) + -> impl Draw + use<'a, T> +{ + let ins = ports.len() as u16; + let frame = Outer(true, Style::default().fg(g(96))); + border(true, frame, field_v(theme, title, iter_south({ + move||ports.iter().enumerate().map(|(index, port)|{ + east!(" ", index, " ", port.port_name()).align_w().full_h() + }) + })).exact_wh(20, 1 + ins)).exact_wh(20, 1 + ins) +} + +pub fn view_io_ports <'a, T: PortsSizes<'a>> ( + fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a +) -> impl Draw + 'a { + type Item<'a> = (usize, &'a Arc, &'a [Connect], usize, usize); + iter(items, + move|(_index, name, connections, y, y2): Item<'a>, _| south( + bold(true, fg_bg(fg, bg, east(" 󰣲 ", name).align_w())).full_h(), + iter(||connections.iter(), move|connect: &'a Connect, index|{ + bold(false, fg_bg(fg, bg, &connect.info)).exact_h(1).align_w().push_y(index as u16) + }) + ).exact_h((y2 - y) as u16).push_y(y as u16)) +} + +pub fn view_sample_info (sample: Option<&Arc>>) -> impl Draw + use<'_> { + when(sample.is_some(), draw(move|to: &mut Tui|{ + let sample = sample.unwrap().try_read().unwrap(); + let theme = sample.color; + east!( + field_h(theme, "Name", format!("{:<10}", sample.name.clone())), + field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())), + field_h(theme, "Start", format!("{:<8}", sample.start)), + field_h(theme, "End", format!("{:<8}", sample.end)), + field_h(theme, "Trans", "0"), + field_h(theme, "Gain", format!("{}", sample.gain)), + ).draw(to) + })) +} + +pub fn view_sample_info_v (sample: Option<&Arc>>) -> impl Draw + use<'_> { + let a = draw(move|to: &mut Tui|{ + let sample = sample.unwrap().try_read().unwrap(); + let theme = sample.color; + south!( + field_h(theme, "Name ", format!("{:<10}", sample.name.clone())) .align_w().full_w(), + field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())).align_w().full_w(), + field_h(theme, "Start ", format!("{:<8}", sample.start)) .align_w().full_w(), + field_h(theme, "End ", format!("{:<8}", sample.end)) .align_w().full_w(), + field_h(theme, "Trans ", "0") .align_w().full_w(), + field_h(theme, "Gain ", format!("{}", sample.gain)) .align_w().full_w(), + ).exact_w(20).draw(to) + }); + + let b = draw(|to: &mut Tui|fg(Red, south!( + bold(true, "× No sample."), + "[r] record", + "[Shift-F9] import", + )).draw(to)); + + either(sample.is_some(), a, b) +} + +pub fn view_sample_status (sample: Option<&Arc>>) -> impl Draw { + bold(true, fg(g(224), sample + .map(|sample|{ + let sample = sample.try_read().unwrap(); + format!("Sample {}-{}", sample.start, sample.end) + }) + .unwrap_or_else(||"No sample".to_string()))) +} + +/// ``` +/// let _ = tek::button_2("", "", true); +/// let _ = tek::button_2("", "", false); +/// ``` +pub fn button_2 <'a> (key: impl Draw, label: impl Draw, hide: bool) + -> impl Draw +{ + let c1 = tui_orange(); + let c2 = tui_g(0); + let c3 = tui_g(96); + let c4 = tui_g(255); + bold(true, fg_bg(c1, c2, + bar(Split::East, 1u16, fg(c2, "▐"), + bar(Split::East, 1u16, key, + bar(Split::East, 1u16, fg(c3, "▐"), + when(!hide, fg_bg(c4, c3, label))))))) +} + +/// ``` +/// let _ = tek::button_3("", "", "", true); +/// let _ = tek::button_3("", "", "", false); +/// ``` +pub fn button_3 <'a> ( + key: impl Draw, + label: impl Draw, + value: impl Draw, + editing: bool, +) -> impl Draw { + let c1 = tui_orange(); + let c2 = tui_g(0); + let c3 = tui_g(96); + let c4 = tui_g(255); + let c5 = tui_g(128); + bold(true, bar(Split::East, 3u16, + fg_bg(c1, c2, + bar(Split::East, 1u16, fg(c2, "▐"), + bar(Split::East, 1u16, key, fg(if editing { c5 } else { c3 }, "▐")))), + east( + when(!editing, east(fg_bg(c4, c3, label), fg_bg(c5, c3, "▐"),)), + east(fg_bg(tui_g(224), c5, value), fg_bg(c5, Reset, "▌"), )))) +} + +pub fn field_h <'a, T: Screen> ( + _theme: ItemTheme, _head: impl Draw, _body: impl Draw +) -> impl Draw { +} + +pub fn field_v <'a, T: Screen> ( + _theme: ItemTheme, _head: impl Draw, _body: impl Draw +) -> impl Draw { +} + +pub fn view_sessions <'a> () -> impl Draw { + let h = 6; + let w = Some(30); + let f = Rgb(224, 192, 128); + draw(move |to: &mut Tui|{ + for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { + let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; + let y = (2 * index) as u16; + let h = 2; + bg(b, fg(f, *name).align_w()).full_w().exact_h(h).push_y(y).draw(to)?; + } + Ok(Some(to.area().into())) + }).min_w(w).exact_h(h) +} + +/// ``` +/// let bg = tengri::ratatui::style::Color::Red; +/// let fg = tengri::ratatui::style::Color::Green; +/// let _ = tek::view_wrap(bg, fg, "and then blue, too!"); +/// ``` +pub fn view_wrap <'a> (bg: Color, fg: Color, content: impl Draw) + -> impl Draw +{ + let left = fg_bg(bg, Reset, y_repeat("▐").exact_w(1)); + let right = fg_bg(bg, Reset, y_repeat("▌").exact_w(1)); + east(left, west(right, fg_bg(fg, bg, content))) +} + +#[macro_export] macro_rules! rewrite { + ($buf:ident, $($rest:tt)*) => { + |$buf,_,_|{ + $buf.clear(); + write!($buf, $($rest)*) + } + }; +} + +pub struct Memo { + value: T, + view: U, +} + +impl Memo { + pub fn new (value: T, view: U) -> Self { + Self { value, view } + } +} + +impl Debug for Memo { + fn fmt (&self, _: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + todo!() + } +} + +impl Memo { + pub fn update (&mut self, newval: T, render: impl Fn(&mut U, &T, &T)->R) -> Option { + if newval != self.value { + let result = render(&mut self.view, &newval, &self.value); + self.value = newval; + return Some(result); + } + None + } +} + +pub trait Gettable { + /// Returns current value + fn get (&self) -> T; +} + +pub trait Mutable: Gettable { + /// Sets new value, returns old + fn set (&mut self, value: T) -> T; +} + +pub trait InteriorMutable: Gettable { + /// Sets new value, returns old + fn set (&self, value: T) -> T; +} + +#[cfg(test)] mod test_view { + use super::*; + use proptest::prelude::*; + proptest! { + + #[test] fn proptest_view_meter ( + label in "\\PC*", value in f32::MIN..f32::MAX + ) { + let _ = view_meter(&label, value); + } + + #[test] fn proptest_view_meters ( + value1 in f32::MIN..f32::MAX, + value2 in f32::MIN..f32::MAX + ) { + let _ = view_meters(&[value1, value2]); + } + } +} diff --git a/src/timebase.rs b/src/time_base.rs similarity index 100% rename from src/timebase.rs rename to src/time_base.rs diff --git a/src/moment.rs b/src/time_moment.rs similarity index 100% rename from src/moment.rs rename to src/time_moment.rs diff --git a/src/ticker.rs b/src/time_ticker.rs similarity index 100% rename from src/ticker.rs rename to src/time_ticker.rs