From f81f16b47bb8306904e753ceff085ee7d431e755 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 24 Aug 2025 02:42:30 +0300 Subject: [PATCH] working main menu --- crates/app/app.rs | 64 ++---------------------------- crates/app/app_bind.rs | 11 +++++- crates/app/app_deps.rs | 17 ++++---- crates/app/app_menu.rs | 88 ++++++++++++++++++++++++++++++++++++++++++ crates/app/app_view.rs | 13 +++++-- crates/cli/tek.rs | 15 ++++--- tek.edn | 2 +- 7 files changed, 130 insertions(+), 80 deletions(-) create mode 100644 crates/app/app_menu.rs diff --git a/crates/app/app.rs b/crates/app/app.rs index ad4e0a47..60f387f0 100644 --- a/crates/app/app.rs +++ b/crates/app/app.rs @@ -2,10 +2,11 @@ #![feature(adt_const_params, associated_type_defaults, if_let_guard, impl_trait_in_assoc_type, type_alias_impl_trait, trait_alias, type_changing_struct_update, closure_lifetime_binder)] #[cfg(test)] mod app_test; -mod app_deps; pub use self::app_deps::*; -mod app_jack; pub use self::app_jack::*; mod app_bind; pub use self::app_bind::*; mod app_data; pub use self::app_data::*; +mod app_deps; pub use self::app_deps::*; +mod app_jack; pub use self::app_jack::*; +mod app_menu; pub use self::app_menu::*; mod app_view; pub use self::app_view::*; /// Total state #[derive(Default, Debug)] @@ -31,23 +32,6 @@ pub struct App { /// Contains the currently edited musical arrangement pub project: Arrangement, } -#[derive(Debug, Clone, Default, PartialEq)] -pub struct Axis { - min: usize, - max: usize, - step: usize, -} -/// Various possible dialog modes. -#[derive(Debug, Clone, Default, PartialEq)] -pub enum Dialog { - #[default] None, - Help(usize), - Menu(usize, Arc<[Arc]>), - Device(usize), - Message(Arc), - Browse(BrowseTarget, Arc), - Options, -} has!(Jack<'static>: |self: App|self.jack); has!(Pool: |self: App|self.pool); has!(Dialog: |self: App|self.dialog); @@ -67,48 +51,6 @@ maybe_has!(Scene: |self: App| { MaybeHas::::get(&self.project) }; impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.inner_size } } impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } } impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } -impl Dialog { - pub fn welcome () -> Self { - Self::Menu(0, [ - "Continue session".into(), - "Load old session".into(), - "Begin new session".into(), - ].into()) - } - pub fn menu_next (&self) -> Self { - match self { - Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.len()), items.clone()), - _ => Self::None - } - } - pub fn menu_prev (&self) -> Self { - match self { - Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.len()), items.clone()), - _ => Self::None - } - } - pub fn menu_selected (&self) -> Option { - if let Self::Menu(selected, _) = self { Some(*selected) } else { None } - } - pub fn device_kind (&self) -> Option { - if let Self::Device(index) = self { Some(*index) } else { None } - } - pub fn device_kind_next (&self) -> Option { - self.device_kind().map(|index|(index + 1) % device_kinds().len()) - } - pub fn device_kind_prev (&self) -> Option { - self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) - } - pub fn message (&self) -> Option<&str> { - todo!() - } - pub fn browser (&self) -> Option<&Arc> { - todo!() - } - pub fn browser_target (&self) -> Option<&BrowseTarget> { - todo!() - } -} impl App { pub fn editor_focused (&self) -> bool { false diff --git a/crates/app/app_bind.rs b/crates/app/app_bind.rs index 6abdd449..c0c4a7a7 100644 --- a/crates/app/app_bind.rs +++ b/crates/app/app_bind.rs @@ -49,8 +49,15 @@ impl Default for AppCommand { fn default () -> Self { Self::Nop } } def_command!(AppCommand: |app: App| { Nop => Ok(None), - Confirm => todo!(), - Cancel => todo!(), // TODO delegate: + Confirm => Ok(match &app.dialog { + Dialog::Menu(index, items) => { + let callback = items.0[*index].1.clone(); + callback(app)?; + None + }, + _ => todo!(), + }), + Cancel => todo!(), // TODO delegate: Inc { axis: Axis } => Ok(match (&app.dialog, axis) { (Dialog::None, _) => todo!(), (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_next() } diff --git a/crates/app/app_deps.rs b/crates/app/app_deps.rs index 40bdf08f..8084c703 100644 --- a/crates/app/app_deps.rs +++ b/crates/app/app_deps.rs @@ -3,14 +3,17 @@ pub use ::{ tek_config::*, tek_device::{self, *}, tengri::{ - Usually, Perhaps, Has, MaybeHas, has, maybe_has, + Usually, Perhaps, Has, MaybeHas, has, maybe_has, impl_debug, dsl::*, input::*, output::*, tui::*, - tui::ratatui, - tui::ratatui::prelude::buffer::Cell, - tui::ratatui::prelude::Color::{self, *}, - tui::ratatui::prelude::{Style, Stylize, Buffer, Modifier}, - tui::crossterm, - tui::crossterm::event::{Event, KeyCode::{self, *}}, + tui::{ + ratatui::{ + self, prelude::{Style, Stylize, Buffer, Modifier, buffer::Cell, Color::{self, *}} + }, + crossterm::{ + self, + event::{Event, KeyCode::{self, *}}, + }, + } }, std::{ path::{Path, PathBuf}, diff --git a/crates/app/app_menu.rs b/crates/app/app_menu.rs new file mode 100644 index 00000000..b4bd5374 --- /dev/null +++ b/crates/app/app_menu.rs @@ -0,0 +1,88 @@ +use crate::*; + +/// Various possible dialog modes. +#[derive(Debug, Clone, Default, PartialEq)] +pub enum Dialog { + #[default] None, + Help(usize), + Menu(usize, MenuItems), + Device(usize), + Message(Arc), + Browse(BrowseTarget, Arc), + Options, +} + +#[derive(Debug, Clone, Default, PartialEq)] +pub struct MenuItems(pub Arc<[MenuItem]>); + +impl AsRef> for MenuItems { + fn as_ref (&self) -> &Arc<[MenuItem]> { + &self.0 + } +} + +#[derive(Clone)] +pub struct MenuItem( + /// Label + pub Arc, + /// Callback + pub ArcUsually<()> + Send + Sync>> +); + +impl Default for MenuItem { + fn default () -> Self { Self("".into(), Arc::new(Box::new(|_|Ok(())))) } +} + +impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); + +impl PartialEq for MenuItem { + fn eq (&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Dialog { + pub fn welcome () -> Self { + Self::Menu(1, MenuItems([ + MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), + MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ + app.dialog = Dialog::None; + app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); + })))), + MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), + ].into())) + } + pub fn menu_next (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()), + _ => Self::None + } + } + pub fn menu_prev (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()), + _ => Self::None + } + } + pub fn menu_selected (&self) -> Option { + if let Self::Menu(selected, _) = self { Some(*selected) } else { None } + } + pub fn device_kind (&self) -> Option { + if let Self::Device(index) = self { Some(*index) } else { None } + } + pub fn device_kind_next (&self) -> Option { + self.device_kind().map(|index|(index + 1) % device_kinds().len()) + } + pub fn device_kind_prev (&self) -> Option { + self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) + } + pub fn message (&self) -> Option<&str> { + todo!() + } + pub fn browser (&self) -> Option<&Arc> { + todo!() + } + pub fn browser_target (&self) -> Option<&BrowseTarget> { + todo!() + } +} diff --git a/crates/app/app_view.rs b/crates/app/app_view.rs index 43ac442f..ff11b2fb 100644 --- a/crates/app/app_view.rs +++ b/crates/app/app_view.rs @@ -86,18 +86,25 @@ impl<'t> DslNs<'t, Box>> for App { ":dialog/menu" => Box::new(if let Dialog::Menu(selected, items) = &app.dialog { let items = items.clone(); let selected = *selected; - Some(Fill::xy(Align::c(Tui::bg(Red, Fill::x(Stack::south(move|add|{ - for (index, item) in items.iter().enumerate() { + Some(Fill::x(Stack::south(move|add|{ + for (index, MenuItem(item, _)) in items.0.iter().enumerate() { add(&Tui::fg_bg( if selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, if selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, Fixed::y(2, Align::n(Fill::x(item))) )); } - })))))) + }))) } else { None }), + ":logo" => Box::new(Fixed::xy(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), Stack::south(|add|{ + add(&Fixed::y(1, "")); + add(&Fixed::y(1, "")); + add(&Fixed::y(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~")); + add(&Fixed::y(1, Bsp::e("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~")))); + add(&Fixed::y(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~")); + }))))), ":templates" => Box::new({ let modes = app.config.modes.clone(); let height = (modes.read().unwrap().len() * 2) as u16; diff --git a/crates/cli/tek.rs b/crates/cli/tek.rs index 255a8d8c..3562ecab 100644 --- a/crates/cli/tek.rs +++ b/crates/cli/tek.rs @@ -115,12 +115,15 @@ impl Cli { /// CLI header const HEADER: &'static str = r#" -~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~~~~~~ -~ ~ ║ ~ ╟─╌ ~╟─< ~ v0.3.0-rc.0 "no, i insist that i am not a dj ~ -~ ~ ╨ ~ ╙──╜ ╨ ╜ ~ 2025, summer, the nose of the cat. J ~ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - On first run, Tek will create configuration and state dirs. ~ - On subsequent runs, Tek should resume from where you left off. ~"#; +~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ ~~~~~~ ~ ~~~ + ~~ ║ ~ ╟─╌ ~╟─< ~ v0.3.0, 2025 sum(m)er @ the nose of the cat. ~ +~~~ ╨ ~ ╙──╜ ╨ ╜ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ ~~ ~~ ~ ~~ + On first run, Tek will create configuration and state dirs: + * [x] ~/.config/tek - config + * [ ] ~/.local/share/tek - projects + * [ ] ~/.local/lib/tek - plugins + * [ ] ~/.cache/tek - cache +~"#; #[cfg(test)] #[test] fn test_cli () { use clap::CommandFactory; diff --git a/tek.edn b/tek.edn index 5066d1b1..e943d2d6 100644 --- a/tek.edn +++ b/tek.edn @@ -12,7 +12,7 @@ (mode :menu (keys :axis/y :confirm) :menu) (keys :confirm (@enter confirm)) (view :menu (bg (g 40) (bsp/s :ports/out (bsp/n :ports/in - (bg (g 30) (fill/xy (align/c :dialog/menu))))))) + (bg (g 30) (bsp/s (fixed/y 7 :logo) (fill/xy :dialog/menu))))))) (view :ports/out (fill/x (fixed/y 3 (bsp/a (fill/x (align/w (text L-AUDIO-OUT))) (bsp/a (text MIDI-OUT) (fill/x (align/e (text AUDIO-OUT R)))))))) (view :ports/in (fill/x (fixed/y 3 (bsp/a (fill/x (align/w (text L-AUDIO-IN)))