diff --git a/Cargo.lock b/Cargo.lock index b1a63742..4959d05a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2396,7 +2396,6 @@ dependencies = [ "rand 0.8.5", "symphonia", "tengri", - "tengri_proc", "toml", "uuid", "wavers", @@ -2458,17 +2457,6 @@ dependencies = [ "tengri_dsl", ] -[[package]] -name = "tengri_proc" -version = "0.14.0" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", - "tengri_core", -] - [[package]] name = "tengri_tui" version = "0.14.0" diff --git a/Justfile b/Justfile index b783e089..dd5824e4 100644 --- a/Justfile +++ b/Justfile @@ -1,45 +1,24 @@ export RUSTFLAGS := "--cfg procmacro2_semver_exempt -Zmacro-backtrace -Clink-arg=-fuse-ld=mold" export RUST_BACKTRACE := "1" -debug := "reset && cargo run --" -release := "reset && cargo run --release --" -name := "-n tek" -bpm := "-b 174" -midi-in := "-i 'Midi-Bridge:.*nanoKEY.*:.*capture.*'" -midi-out := "-o 'Midi-Bridge:.*playback.*'" -audio-in := "-l 'Komplete Audio 6 Pro:capture_AUX1' -r 'Komplete Audio 6 Pro:capture_AUX1'" -audio-out := "-L 'Komplete Audio 6 Pro:playback_AUX1' -R 'Komplete Audio 6 Pro:playback_AUX1'" -firefox-in := "-l 'Firefox:output_FL*' -r 'Firefox:output_FR*'" +default: + just -l + +cloc: + for src in {cli,edn/src,input/src,jack/src,midi/src,output/src,plugin/src,sampler/src,tek/src,time/src,tui/src}; do echo; echo $src; cloc --quiet $src; done + +bacon: + bacon -s + +check: + reset && cargo check + +test: + cargo test --workspace --exclude jack + covfig := "CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' RUSTDOCFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='cov/cargo-test-%p-%m.profraw'" grcov-binary := "--binary-path ./target/coverage/deps/" grcov-ignore := "--ignore-not-existing --ignore '../*' --ignore \"/*\" --ignore 'target/*'" - -default: - just -l -bacon: - bacon -s -check: - reset && cargo check -build: - reset && cargo build -run: - {{debug}} -run-init: - rm -rf ~/.config/tek && {{debug}} -release: - {{release}} -build-release: - time cargo build -j4 --release -tui: - cargo run --example tui -cloc: - for src in {cli,edn/src,input/src,jack/src,midi/src,output/src,plugin/src,sampler/src,tek/src,time/src,tui/src}; do echo; echo $src; cloc --quiet $src; done -test: - cargo test --workspace --exclude jack -prof: - CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph -- -doc: - cargo doc -j4 --workspace --document-private-items cov: {{covfig}} time cargo test -j4 --workspace --exclude jack --profile coverage rm -rf target/coverage/html || true @@ -51,6 +30,28 @@ llcov: time cargo llvm-cov --workspace --exclude jack --profile coverage --no-report time cargo llvm-cov --workspace --exclude jack --profile coverage --no-report --doc time cargo llvm-cov report --doctests --html #--output-path target/coverage/html + +build: + reset && cargo build + +debug := "reset && cargo run --" +run: + {{debug}} +run-init: + rm -rf ~/.config/tek && {{debug}} + +prof: + CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph -- + +doc: + cargo doc -j4 --workspace --document-private-items + +release := "reset && cargo run --release --" +release: + {{release}} +build-release: + time cargo build -j4 --release + amend: git commit --amend push: @@ -62,11 +63,18 @@ fpush: ftpush: git push --tags -fu codeberg && git push --tags -fu origin +name := "-n tek" +bpm := "-b 174" clock: {{debug}} {{name}} {{bpm}} clock clock-release: {{release}} {{name}} {{bpm}} clock +midi-in := "-i 'Midi-Bridge:.*nanoKEY.*:.*capture.*'" +midi-out := "-o 'Midi-Bridge:.*playback.*'" +audio-in := "-l 'Komplete Audio 6 Pro:capture_AUX1' -r 'Komplete Audio 6 Pro:capture_AUX1'" +audio-out := "-L 'Komplete Audio 6 Pro:playback_AUX1' -R 'Komplete Audio 6 Pro:playback_AUX1'" +firefox-in := "-l 'Firefox:output_FL*' -r 'Firefox:output_FR*'" arranger: {{debug}} {{name}} {{bpm}} arranger arranger-ext: diff --git a/bacon.toml b/bacon.toml index 53b4f1ff..5a611b08 100644 --- a/bacon.toml +++ b/bacon.toml @@ -1,5 +1,5 @@ # https://dystroy.org/bacon/config/ -default_job = "check" +default_job = "test" env.CARGO_TERM_COLOR = "always" [keybindings] c = "job:check" @@ -24,7 +24,7 @@ command = ["cargo", "clippy", "--all-targets"] need_stdout = false watch = ["tek", "deps"] [jobs.test] -command = ["cargo", "test"] +command = ["cargo", "test", "--workspace", "--exclude", "jack"] need_stdout = true watch = ["tek", "deps"] [jobs.nextest] diff --git a/deps/rust-jack b/deps/rust-jack index 4cbf155d..764a38a8 160000 --- a/deps/rust-jack +++ b/deps/rust-jack @@ -1 +1 @@ -Subproject commit 4cbf155d8ed222c140c11770474832ddfa52bcd7 +Subproject commit 764a38a880ab4749ea60aa7e53cd814b858e606c diff --git a/deps/tengri b/deps/tengri index a4dbf882..ca862b98 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit a4dbf88220f75ccaf9d14cc2e4fb7c00479e3940 +Subproject commit ca862b9802524ee1713c0088c7a525ad07f370a0 diff --git a/tek/Cargo.toml b/tek/Cargo.toml index f29213a0..656b182b 100644 --- a/tek/Cargo.toml +++ b/tek/Cargo.toml @@ -14,9 +14,6 @@ path = "tek_cli.rs" rustflags = ["-C", "link-arg=-fuse-ld=mold"] [dependencies] -tengri = { workspace = true } -tengri_proc = { workspace = true } - atomic_float = { workspace = true } backtrace = { workspace = true } clap = { workspace = true, optional = true } @@ -27,6 +24,7 @@ midly = { workspace = true } palette = { workspace = true } rand = { workspace = true } symphonia = { workspace = true, optional = true } +tengri = { workspace = true } toml = { workspace = true } uuid = { workspace = true, optional = true } wavers = { workspace = true, optional = true } diff --git a/tek/device/arranger.rs b/tek/device/arranger.rs index 0963df93..dd277a25 100644 --- a/tek/device/arranger.rs +++ b/tek/device/arranger.rs @@ -84,7 +84,7 @@ maybe_has!(Scene: |self: Arrangement| //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); //take!(ClipCommand |state: Arrangement, iter|state.selected_clip().as_ref() //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); -#[tengri_proc::expose] impl Arrangement { +impl Arrangement { fn selected_midi_in (&self) -> Option { todo!() } fn selected_midi_out (&self) -> Option { todo!() } fn selected_device (&self) -> Option { todo!() } diff --git a/tek/device/arranger/arranger_clip.rs b/tek/device/arranger/arranger_clip.rs index e85e6839..8ddcc485 100644 --- a/tek/device/arranger/arranger_clip.rs +++ b/tek/device/arranger/arranger_clip.rs @@ -1,6 +1,5 @@ use crate::*; -#[tengri_proc::expose] impl MidiClip { fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } fn _todo_bool_stub_ (&self) -> bool { todo!() } diff --git a/tek/device/arranger/arranger_scenes.rs b/tek/device/arranger/arranger_scenes.rs index 61e2841d..d8383fa3 100644 --- a/tek/device/arranger/arranger_scenes.rs +++ b/tek/device/arranger/arranger_scenes.rs @@ -56,7 +56,6 @@ pub trait AddScene: HasScenes + HasTracks { } } -#[tengri_proc::expose] impl Scene { fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } fn _todo_usize_stub_ (&self) -> usize { todo!() } diff --git a/tek/device/arranger/arranger_tracks.rs b/tek/device/arranger/arranger_tracks.rs index 3f002a11..8a293c7a 100644 --- a/tek/device/arranger/arranger_tracks.rs +++ b/tek/device/arranger/arranger_tracks.rs @@ -121,7 +121,6 @@ impl> HasTrack for T { } } -#[tengri_proc::expose] impl Track { fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } fn _todo_usize_stub_ (&self) -> usize { todo!() } diff --git a/tek/device/arranger/arranger_view.rs b/tek/device/arranger/arranger_view.rs index dcc87a92..10114eb2 100644 --- a/tek/device/arranger/arranger_view.rs +++ b/tek/device/arranger/arranger_view.rs @@ -14,8 +14,8 @@ impl Arrangement { pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw + Layout + '_ { Bsp::s(self.view_inputs_header(), Thunk::new(|to: &mut TuiOut|{ - for port in self.midi_ins().iter() { - to.place(&self.view_inputs_row(port)) + for (index, port) in self.midi_ins().iter().enumerate() { + to.place(&Push::x(index as u16 * 10, self.view_inputs_row(port))) } })) } @@ -25,12 +25,12 @@ impl Arrangement { Fixed::x(20, Align::w( button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), Bsp::w(Fixed::x(4, button_2("I", "+", false)), - Thunk::new(|to: &mut TuiOut|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&Tui::bg(track.color.dark.rgb, Align::w(Fixed::x(track.width as u16, row!( + Thunk::new(|to: &mut TuiOut|for (_index, track, x1, _x2) in self.tracks_with_sizes() { + to.place(&Push::x(x1 as u16, Tui::bg(track.color.dark.rgb, Align::w(Fixed::x(track.width as u16, row!( Either::new(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "), Either::new(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "), Either::new(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "), - ))))) + )))))) })))) } @@ -180,8 +180,8 @@ pub trait TracksView: button_2("S", "+", false)); view_track_row_section(theme, button, button_2, Tui::bg(theme.darker.rgb, Fixed::y(2, Thunk::new(|to: &mut TuiOut|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&Fixed::x(self.track_width(index, track), + for (index, track, x1, _x2) in self.tracks_with_sizes() { + to.place(&Push::x(x1 as u16, Fixed::x(self.track_width(index, track), Tui::bg(if selected.track() == Some(index) { track.color.light.rgb } else { @@ -189,7 +189,7 @@ pub trait TracksView: }, Bsp::s(Fill::x(Align::nw(Bsp::e( format!("ยทt{index:02} "), Tui::fg(Rgb(255, 255, 255), Tui::bold(true, &track.name)) - ))), ""))) );}})))) + ))), ""))) ));}})))) } fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw + Layout { view_track_row_section(theme, @@ -288,7 +288,7 @@ pub trait ScenesView: }; let a = Fill::x(Align::w(Bsp::e(format!("ยทs{index:02} "), Tui::fg(Tui::g(255), Tui::bold(true, &scene.name))))); - let b = When(self.selection().scene() == Some(index) && self.is_editing(), + let b = When::new(self.selection().scene() == Some(index) && self.is_editing(), Fill::xy(Align::nw(Bsp::s( self.editor().as_ref().map(|e|e.clip_status()), self.editor().as_ref().map(|e|e.edit_status()))))); @@ -363,7 +363,7 @@ pub trait ClipsView: Tui::fg_bg(outline, bg, Fill::xy("")), Fill::xy(Align::nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))), ), - Fill::xy(When(self.selection().track() == Some(track_index) + Fill::xy(When::new(self.selection().track() == Some(track_index) && self.selection().scene() == Some(scene_index) && self.is_editing(), self.editor()))))))); }) diff --git a/tek/device/browse/browse_api.rs b/tek/device/browse/browse_api.rs index 21f78b1e..ddbb04d5 100644 --- a/tek/device/browse/browse_api.rs +++ b/tek/device/browse/browse_api.rs @@ -1,6 +1,5 @@ use crate::*; -#[tengri_proc::expose] impl Browse { fn _todo_stub_path_buf (&self) -> PathBuf { todo!() diff --git a/tek/device/clock/clock_api.rs b/tek/device/clock/clock_api.rs index 9235869a..faf93ace 100644 --- a/tek/device/clock/clock_api.rs +++ b/tek/device/clock/clock_api.rs @@ -1,6 +1,5 @@ use crate::*; -#[tengri_proc::expose] impl Clock { fn _todo_provide_u32 (&self) -> u32 { todo!() diff --git a/tek/device/editor/editor_api.rs b/tek/device/editor/editor_api.rs index 9f22a6d9..2da18f81 100644 --- a/tek/device/editor/editor_api.rs +++ b/tek/device/editor/editor_api.rs @@ -24,7 +24,7 @@ def_command!(MidiEditCommand: |editor: MidiEditor| { // TODO: 1-9 seek markers that by default start every 8th of the clip }); -#[tengri_proc::expose] impl MidiEditor { +impl MidiEditor { fn _todo_opt_clip_stub (&self) -> Option>> { todo!() } diff --git a/tek/device/pool/pool_api.rs b/tek/device/pool/pool_api.rs index 24e435b6..001b90d9 100644 --- a/tek/device/pool/pool_api.rs +++ b/tek/device/pool/pool_api.rs @@ -1,6 +1,5 @@ use crate::*; -#[tengri_proc::expose] impl Pool { fn _todo_usize_ (&self) -> usize { todo!() } fn _todo_bool_ (&self) -> bool { todo!() } diff --git a/tek/device/sampler/sampler_api.rs b/tek/device/sampler/sampler_api.rs index f3f8ee05..b7308d36 100644 --- a/tek/device/sampler/sampler_api.rs +++ b/tek/device/sampler/sampler_api.rs @@ -56,7 +56,6 @@ def_command!(FileBrowserCommand: |sampler: Sampler|{ //("filter" [f: Arc] Some(Self::Filter(f.expect("no filter"))))) }); -#[tengri_proc::expose] impl Sampler { fn sample_selected (&self) -> usize { (self.get_note_pos() as u8).into() diff --git a/tek/device/sampler/sampler_view.rs b/tek/device/sampler/sampler_view.rs index 20c4ab13..b6331dbd 100644 --- a/tek/device/sampler/sampler_view.rs +++ b/tek/device/sampler/sampler_view.rs @@ -3,10 +3,10 @@ use crate::*; impl Sampler { pub fn view_grid (&self) -> impl Draw + Layout + use<'_> { - let cells_x = 8u16; - let cells_y = 8u16; - let cell_width = 10u16; - let cell_height = 2u16; + //let cells_x = 8u16; + //let cells_y = 8u16; + //let cell_width = 10u16; + //let cell_height = 2u16; //let width = cells_x * cell_width; //let height = cells_y * cell_height; //let cols = Map::east( @@ -222,7 +222,7 @@ fn draw_viewer (sample: Option<&Arc>>) -> impl Draw + Lay } fn draw_info (sample: Option<&Arc>>) -> impl Draw + Layout + use<'_> { - When(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{ + When::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{ let sample = sample.unwrap().read().unwrap(); let theme = sample.color; to.place(&row!( diff --git a/tek/tek.edn b/tek/tek.edn index 30849ccb..225def70 100644 --- a/tek/tek.edn +++ b/tek/tek.edn @@ -34,7 +34,10 @@ (@enter confirm)) (view :menu (bg (g 0) (bsp/s :ports/out - (bsp/n :ports/in (bg (g 30) (bsp/s (fixed/y 7 :logo) (fill/xy :dialog/menu))))))) + (bsp/n :ports/in (bg (g 30) (bsp/s (fixed/y 7 :logo) (fill :dialog/menu))))))) +(view :menu (bsp/s + (push/y 4 (fixed/xy 20 2 (bg (g 0) :debug))) + (fixed 20 2 (bg (g 20) (push/x 2 :debug))))) (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)))))))) @@ -42,7 +45,7 @@ (fill/x (align/w (text L-AUDIO-IN))) (bsp/a (text MIDI-IN) (fill/x (align/e (text AUDIO-IN-R)))))))) (view :browse (bsp/s - (padding/xy 3 1 :browse-title) + (padding 3 1 :browse-title) (enclose (fg (g 96)) browser))) (keys :help (@f1 dialog :help)) @@ -84,86 +87,113 @@ (mode :length (keys :rename)) (mode :clip (keys :clip)) (mode :track (keys :track)) (mode :scene (keys :scene)) (mode :mix (keys :mix)) (keys :clock :arranger :global) :arranger) + (view :arranger (bsp/n :status (bsp/w :meters/output (bsp/e :meters/input :arrangement)))) + (view :arrangement (bsp/n :tracks/inputs (bsp/s :tracks/outputs (bsp/s :tracks/names (bsp/s :tracks/devices - (fill/xy (either :mode/editor (bsp/e :scenes/names :editor) :scenes))))))) + (fill (either :mode/editor (bsp/e :scenes/names :editor) :scenes))))))) + (keys :arranger (see :color :launch :scenes :tracks) (@tab project/edit) (@enter project/edit) (@shift/I project/input/add) (@shift/O project/output/add) (@shift/S project/scene/add) (@shift/T project/track/add) (@shift/D dialog/show :dialog/device)) + (keys :tracks (@t select :select/track) (@left select :select/track/dec) (@right select :select/track/inc)) + (keys :scenes (@s select :select/scene) (@up select :select/scene/dec) (@down select :select/scene/inc)) + (keys :track (see :color :launch :axis/z :axis/z2 :delete) (@r toggle :rec) (@m toggle :mon) (@p toggle :play) (@P toggle :solo)) (keys :scene (see :color :launch :axis/z :axis/z2 :delete)) + (keys :clip (see :color :launch :axis/z :axis/z2 :delete) (@l toggle :loop)) + (mode :groovebox (name Groovebox) (info A sequencer with built-in sampler.) (mode browse (keys :browse)) (mode rename (keys :pool-rename)) (mode length (keys :pool-length)) (keys :clock :editor :sampler :global) (view :groovebox)) + (view :groovebox (bsp/w :meters/output (bsp/e :meters/input (bsp/w :groove/meta :groove/editor)))) + (view :groove/meta (fill/y (align/n (stack/s :midi-ins/status :midi-outs/status :audio-ins/status :audio-outs/status :pool)))) + (view :groove/editor (bsp/n :groove/sample :groove/sequence)) + (view :groove/sample (fixed/y :h-sample-detail (bsp/e (fill/y (fixed/x 20 (align/nw :sample-status))) :sample-viewer))) + (view :groove/sequence (bsp/e (fill/y (align/n (bsp/s :status/v :editor-status))) (bsp/e :samples/keys :editor))) + (mode :sampler (name Sampler) (info A sampling soundboard.) (keys :sampler :global) (view :sampler)) + (view :sampler (bsp/s (fixed/y 1 :transport) - (bsp/n (fixed/y 1 :status) (fill/xy :samples/grid)))) + (bsp/n (fixed/y 1 :status) (fill :samples/grid)))) + (keys :sampler (see :sampler/directions :sampler/record :sampler/play)) + (keys :sampler/record (@r sampler/record/toggle :sample/selected) (@shift/R sampler/record/back)) + (keys :sampler/play (@p sampler/play/sample :sample/selected) (@P sampler/stop/sample :sample/selected)) + (keys :sampler/import-export (@shift/f6 dialog :dialog/export/sample) (@shift/f9 dialog :dialog/import/sample)) + (keys :sampler/directions (@up sampler/select :sample/above) (@down sampler/select :sample/below) (@left sampler/select :sample/to/left) (@right sampler/select :sample/to/right)) + (mode :sequencer (name Sequencer) (info A MIDI sequencer.) (mode browse (keys :browse)) (mode rename (keys :pool/rename)) (mode length (keys :pool/length)) (keys :editor :clock :global) (view :sequencer)) + (view :sequencer (bsp/s (fixed/y 1 :transport) - (bsp/n (fixed/y 1 :status) (fill/xy (bsp/a (fill/xy (align/e :pool)) :editor))))) + (bsp/n (fixed/y 1 :status) (fill (bsp/a (fill/xy (align/e :pool)) :editor))))) + (keys :editor (see :editor/view :editor/note)) + (keys :editor/view (see :axis/x :axis/x2 :axis/z :axis/z2) (@z toggle :lock)) + (keys :editor/note (see :axis/i :axis/i2 :axis/y :page) (@a editor/append :true) (@enter editor/append :false) (@del editor/delete/note) (@shift/del editor/delete/note)) + (keys :pool (see :axis-y :axis-w :axis/z2 :color :delete) (@n rename/begin) (@t length/begin) (@m import/begin) (@x export/begin) (@shift/A clip/add :after :new/clip) (@shift/D clip/add :after :cloned/clip)) + (keys :sequencer (see :color :launch) (@shift/I input/add) (@shift/O output/add)) diff --git a/tek/tek.rs b/tek/tek.rs index 7ff10600..8f7a31c1 100644 --- a/tek/tek.rs +++ b/tek/tek.rs @@ -13,15 +13,11 @@ clippy::unit_arg )] -mod deps; pub use self::deps::*; - +mod deps; pub use self::deps::*; mod config; pub use self::config::*; mod device; pub use self::device::*; mod engine; pub use self::engine::*; -mod tek_bind; pub use self::tek_bind::*; -mod tek_menu; pub use self::tek_menu::*; - #[cfg(test)] mod test; /// Total state @@ -76,21 +72,19 @@ audio!( |self, event|{ use JackEvent::*; match event { - SampleRate(sr) => { - self.clock().timebase.sr.set(sr as f64); - }, - PortRegistration(id, true) => { + SampleRate(sr) => { self.clock().timebase.sr.set(sr as f64); }, + PortRegistration(_id, true) => { //let port = self.jack().port_by_id(id); //println!("\rport add: {id} {port:?}"); //println!("\rport add: {id}"); }, - PortRegistration(id, false) => { + PortRegistration(_id, false) => { /*println!("\rport del: {id}")*/ }, - PortsConnected(a, b, true) => { /*println!("\rport conn: {a} {b}")*/ }, - PortsConnected(a, b, false) => { /*println!("\rport disc: {a} {b}")*/ }, - ClientRegistration(id, true) => {}, - ClientRegistration(id, false) => {}, + PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, + PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, + ClientRegistration(_id, true) => {}, + ClientRegistration(_id, false) => {}, ThreadInit => {}, XRun => {}, GraphReorder => {}, @@ -99,13 +93,144 @@ audio!( } ); +tui_draw!(|self: App, to|to.place(&self.content())); +content!(TuiOut: |self: App|Thunk::new(|to: &mut TuiOut|{ + for (index, dsl) in self.mode.view.iter().enumerate() { + self.view(to, dsl).unwrap_or_else(|e|panic!("render #{index} failed ({e}): {dsl}")); + } +})); +impl View for App { + fn view_expr <'a> (&'a self, to: &mut TuiOut, expr: &'a impl DslExpr) -> Usually<()> { + if evaluate_output_expression(self, to, expr)? + || evaluate_output_expression_tui(self, to, expr)? { + Ok(()) + } else { + Err(format!("App::view_expr: unexpected: {expr:?}").into()) + } + } + fn view_word <'a> (&'a self, to: &mut TuiOut, dsl: &'a impl DslExpr) -> Usually<()> { + let mut frags = dsl.src()?.unwrap().split("/"); + match frags.next() { + Some(":logo") => to.place(&view_logo()), + Some(":status") => to.place(&Fixed::y(1, "TODO: Status Bar")), + Some(":meters") => match frags.next() { + Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::y(Align::s("Input Meters")))), + Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::y(Align::s("Output Meters")))), + _ => panic!() + }, + Some(":tracks") => match frags.next() { + None => to.place(&"TODO tracks"), + Some("names") => to.place(&self.project.view_track_names(self.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Names")))), + Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Inputs")))), + Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Devices")))), + Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Outputs")))), + _ => panic!() + }, + Some(":scenes") => match frags.next() { + None => to.place(&"TODO scenes"), + Some(":scenes/names") => to.place(&"TODO Scene Names"), + _ => panic!() + }, + Some(":editor") => to.place(&"TODO Editor"), + Some(":dialog") => match frags.next() { + Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &self.dialog { + let items = items.clone(); + let selected = selected; + Some(Fill::xy(Thunk::new(move|to: &mut TuiOut|{ + for (index, MenuItem(item, _)) in items.0.iter().enumerate() { + to.place(&Push::y((2 * index) as u16, + 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 + }), + _ => unimplemented!("App::view_word: {dsl:?} ({frags:?})"), + }, + Some(":templates") => to.place(&{ + let modes = self.config.modes.clone(); + let height = (modes.read().unwrap().len() * 2) as u16; + Fixed::y(height, Min::x(30, Thunk::new(move |to: &mut TuiOut|{ + for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { + let bg = 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 = Fill::x(Align::w(Tui::fg(fg1, name))); + let field_id = Fill::x(Align::e(Tui::fg(fg2, id))); + let field_info = Fill::x(Align::w(info)); + to.place(&Push::y((2 * index) as u16, + Fixed::y(2, Fill::x(Tui::bg(bg, Bsp::s( + Bsp::a(field_name, field_id), field_info)))))); + } + }))) + }), + Some(":sessions") => to.place(&Fixed::y(6, Min::x(30, Thunk::new(|to: &mut TuiOut|{ + let fg = Rgb(224, 192, 128); + for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { + let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; + to.place(&Push::y((2 * index) as u16, + &Fixed::y(2, Fill::x(Tui::bg(bg, Align::w(Tui::fg(fg, name))))))); + } + })))), + Some(":browse/title") => to.place(&Fill::x(Align::w(FieldV(Default::default(), + match self.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:", + }, Shrink::x(3, Fixed::y(1, Tui::fg(Tui::g(96), RepeatH("๐Ÿญป")))))))), + Some(":device") => { + let selected = self.dialog.device_kind().unwrap(); + to.place(&Bsp::s(Tui::bold(true, "Add device"), Map::south(1, + move||device_kinds().iter(), + move|_label: &&'static str, i|{ + let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; + let lb = if i == selected { "[ " } else { " " }; + let rb = if i == selected { " ]" } else { " " }; + Fill::x(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) }))) + }, + Some(":debug") => to.place(&Fixed::y(1, format!("[{:?}]", to.area()))), + Some(_) => { + let views = self.config.views.read().unwrap(); + if let Some(dsl) = views.get(dsl.src()?.unwrap()) { + let dsl = dsl.clone(); + std::mem::drop(views); + self.view(to, &dsl)? + } else { + unimplemented!("{dsl:?}"); + } + }, + _ => unreachable!() + } + Ok(()) + } +} +fn view_logo () -> impl Draw + Layout { + Fixed::xy(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), col!{ + Fixed::y(1, ""), + Fixed::y(1, ""), + Fixed::y(1, "~~ โ•“โ”€โ•ฅโ”€โ•– โ•“โ”€โ”€โ•– โ•ฅ โ•– ~~~~~~~~~~~~"), + Fixed::y(1, Bsp::e("~~~~ โ•‘ ~ โ•Ÿโ”€โ•Œ ~โ•Ÿโ”€< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), + Fixed::y(1, "~~~~ โ•จ ~ โ•™โ”€โ”€โ•œ โ•จ โ•œ ~~~~~~~~~~~~"), + }))) +} + +// Allow source to be read as Literal string dsl_ns!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); -dsl_ns!(App: ItemTheme {}); - +// Provide boolean values. dsl_ns!(App: bool { + // TODO literal = ... word = |app| { ":mode/editor" => app.project.editor.is_some(), ":focused/dialog" => !matches!(app.dialog, Dialog::None), @@ -127,6 +252,9 @@ dsl_ns!(App: bool { }; }); +// TODO: provide colors here +dsl_ns!(App: ItemTheme {}); + dsl_ns!(App: Dialog { word = |app| { ":dialog/none" => Dialog::None, @@ -239,10 +367,6 @@ dsl_ns!(App: isize { }); }); -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 HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } has!(Jack<'static>: |self: App|self.jack); has!(Pool: |self: App|self.pool); has!(Dialog: |self: App|self.dialog); @@ -260,280 +384,24 @@ maybe_has!(Track: |self: App| { MaybeHas::::get(&self.project) }; maybe_has!(Scene: |self: App| { MaybeHas::::get(&self.project) }; { MaybeHas::::get_mut(&mut 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 HasJack<'static> for App { + fn jack (&self) -> &Jack<'static> { &self.jack } +} impl ScenesView for App { - fn w_side (&self) -> u16 { 20 } - fn w_mid (&self) -> u16 { (self.width() as u16).saturating_sub(self.w_side()) } - fn h_scenes (&self) -> u16 { (self.height() as u16).saturating_sub(20) } + fn w_side (&self) -> u16 { 20 } + fn w_mid (&self) -> u16 { (self.width() as u16).saturating_sub(self.w_side()) } + fn h_scenes (&self) -> u16 { (self.height() as u16).saturating_sub(20) } } -impl Draw for App { - fn draw (&self, to: &mut TuiOut) { - for (index, dsl) in self.mode.view.iter().enumerate() { - to.place(&Align::nw(Push::y(1 + index as u16 * 2, dsl.src().unwrap()))); - //let _ = self.view(to, dsl).expect("render failed"); - } - } -} - -impl App { - - fn view <'a> (&'a self, to: &mut TuiOut, dsl: impl Dsl + 'a) -> Usually<()> { - if let Ok(Some(expr)) = dsl.expr() { - self.view_expr(to, expr) - } else if let Ok(Some(word)) = dsl.word() { - self.view_word(to, word) - } else { - panic!("{dsl:?}: invalid") - } - } - - fn view_expr <'a> (&'a self, to: &mut TuiOut, expr: impl DslExpr + 'a) -> Usually<()> { - let head = expr.head()?; let args = expr.tail(); - let mut frags = head.src()?.unwrap_or_default().split("/"); - let arg0 = args.head(); let tail0 = args.tail(); - let arg1 = tail0.head(); let tail1 = tail0.tail(); - let arg2 = tail1.head(); let tail2 = tail1.tail(); - let arg3 = tail2.head(); let tail3 = tail2.tail(); - match frags.next() { - - Some("text") => to.place(&frags.next()), - - Some("when") => to.place(&When::new( - self.from(arg0?)?.unwrap(), - Thunk::new(move|to: &mut TuiOut|self.view(to, arg1).unwrap()) - )), - - Some("either") => to.place(&Either::new( - self.from(arg0?)?.unwrap(), - Thunk::new(move|to: &mut TuiOut|self.view(to, arg1).unwrap()), - Thunk::new(move|to: &mut TuiOut|self.view(to, arg2).unwrap()) - )), - - Some("fg") => { - let arg0 = arg0?.expect("fg: expected arg 0 (color)"); - to.place(&Tui::fg( - DslNs::::from(self, arg0)?.unwrap_or_else(||panic!("fg: {arg0:?}: not a color")), - Thunk::new(move|to: &mut TuiOut|self.view(to, arg1).unwrap()), - )) - }, - - Some("bg") => { - let arg0 = arg0?.expect("bg: expected arg 0 (color)"); - to.place(&Tui::bg( - DslNs::::from(self, arg0)?.unwrap_or_else(||panic!("bg: {arg0:?}: not a color")), - Thunk::new(move|to: &mut TuiOut|self.view(to, arg1).unwrap()), - )) - }, - - Some("bsp") => to.place(&{ - let a = Thunk::new(move|to: &mut TuiOut|self.view(to, arg0).unwrap()); - let b = Thunk::new(move|to: &mut TuiOut|self.view(to, arg1).unwrap()); - match frags.next() { - Some("n") => Bsp::n(a, b), - Some("s") => Bsp::s(a, b), - Some("e") => Bsp::e(a, b), - Some("w") => Bsp::w(a, b), - Some("a") => Bsp::a(a, b), - Some("b") => Bsp::b(a, b), - frag => unimplemented!("bsp/{frag:?}") - } - }), - - Some("align") => to.place(&{ - let a = Thunk::new(move|to: &mut TuiOut|self.view(to, arg0).unwrap()); - match frags.next() { - Some("n") => Align::n(a), - Some("s") => Align::s(a), - Some("e") => Align::e(a), - Some("w") => Align::w(a), - Some("x") => Align::x(a), - Some("y") => Align::y(a), - Some("c") => Align::c(a), - frag => unimplemented!("align/{frag:?}") - } - }), - - Some("fill") => to.place(&{ - let a = Thunk::new(move|to: &mut TuiOut|self.view(to, arg0).unwrap()); - match frags.next() { - Some("x") => Fill::X(a), - Some("y") => Fill::Y(a), - Some("xy") => Fill::XY(a), - frag => unimplemented!("fill/{frag:?}") - } - }), - - Some("fixed") => to.place(&{ - let axis = frags.next(); - let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") => arg2, _ => panic!() }; - let cb = Thunk::new(move|to: &mut TuiOut|self.view(to, arg).unwrap()); - match axis { - Some("x") => Fixed::X(self.from(arg0?)?.unwrap(), cb), - Some("y") => Fixed::Y(self.from(arg0?)?.unwrap(), cb), - Some("xy") => Fixed::XY(self.from(arg0?)?.unwrap(), self.from(arg1?)?.unwrap(), cb), - frag => unimplemented!("fixed/{frag:?} ({expr:?}) ({head:?}) ({:?})", - head.src()?.unwrap_or_default().split("/").next()) - } - }), - - Some("min") => to.place(&{ - let c = match frags.next() { - Some("x") | Some("y") => arg1, Some("xy") => arg2, _ => panic!() - }; - let cb = Thunk::new(move|to: &mut TuiOut|self.view(to, c).unwrap()); - match frags.next() { - Some("x") => Min::X(self.from(arg0?)?.unwrap(), cb), - Some("y") => Min::Y(self.from(arg0?)?.unwrap(), cb), - Some("xy") => Min::XY(self.from(arg0?)?.unwrap(), self.from(arg1?)?.unwrap(), cb), - frag => unimplemented!("min/{frag:?}") - } - }), - - Some("max") => to.place(&{ - let c = match frags.next() { - Some("x") | Some("y") => arg1, Some("xy") => arg2, _ => panic!() - }; - let cb = Thunk::new(move|to: &mut TuiOut|self.view(to, c).unwrap()); - match frags.next() { - Some("x") => Max::X(self.from(arg0?)?.unwrap(), cb), - Some("y") => Max::Y(self.from(arg0?)?.unwrap(), cb), - Some("xy") => Max::XY(self.from(arg0?)?.unwrap(), self.from(arg1?)?.unwrap(), cb), - frag => unimplemented!("max/{frag:?}") - } - }), - - _ => panic!("unexpected: {expr:?}") - - }; - Ok(()) - } - - fn view_word <'a> (&'a self, to: &mut TuiOut, dsl: impl DslExpr + 'a) -> Usually<()> { - let mut frags = dsl.src()?.unwrap().split("/"); - match frags.next() { - - Some(":logo") => to.place(&Fixed::xy(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), col!{ - Fixed::y(1, ""), - Fixed::y(1, ""), - Fixed::y(1, "~~ โ•“โ”€โ•ฅโ”€โ•– โ•“โ”€โ”€โ•– โ•ฅ โ•– ~~~~~~~~~~~~"), - Fixed::y(1, Bsp::e("~~~~ โ•‘ ~ โ•Ÿโ”€โ•Œ ~โ•Ÿโ”€< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), - Fixed::y(1, "~~~~ โ•จ ~ โ•™โ”€โ”€โ•œ โ•จ โ•œ ~~~~~~~~~~~~"), - })))), - - Some(":status") => to.place(&"TODO: Status Bar"), - - Some(":meters") => match frags.next() { - Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::y(Align::s("Input Meters")))), - Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::y(Align::s("Output Meters")))), - _ => panic!() - }, - - Some(":tracks") => match frags.next() { - None => to.place(&"TODO tracks"), - Some("names") => to.place(&self.project.view_track_names(self.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Names")))), - Some("names") => to.place(&self.project.view_track_names(self.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Names")))), - Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Inputs")))), - Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Devices")))), - Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Outputs")))), - _ => panic!() - }, - - Some(":scenes") => match frags.next() { - None => to.place(&"TODO scenes"), - Some(":scenes/names") => to.place(&"TODO Scene Names"), - _ => panic!() - }, - - Some(":editor") => to.place(&"TODO Editor"), - - Some(":dialog") => match frags.next() { - Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &self.dialog { - let items = items.clone(); - let selected = selected; - Some(Fill::xy(Thunk::new(move|to: &mut TuiOut|{ - for (index, MenuItem(item, _)) in items.0.iter().enumerate() { - to.place(&Push::y((2 * index) as u16, - 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 - }), - _ => unimplemented!("App::view_word: {dsl:?} ({frags:?})"), - }, - - Some(":templates") => to.place(&{ - let modes = self.config.modes.clone(); - let height = (modes.read().unwrap().len() * 2) as u16; - Fixed::y(height, Min::x(30, Thunk::new(move |to: &mut TuiOut|{ - for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { - let bg = 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 = Fill::x(Align::w(Tui::fg(fg1, name))); - let field_id = Fill::x(Align::e(Tui::fg(fg2, id))); - let field_info = Fill::x(Align::w(info)); - to.place(&Push::y((2 * index) as u16, - Fixed::y(2, Fill::x(Tui::bg(bg, Bsp::s( - Bsp::a(field_name, field_id), field_info)))))); - } - }))) - }), - - Some(":sessions") => to.place(&Fixed::y(6, Min::x(30, Thunk::new(|to: &mut TuiOut|{ - let fg = Rgb(224, 192, 128); - for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { - let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; - to.place(&Push::y((2 * index) as u16, - &Fixed::y(2, Fill::x(Tui::bg(bg, Align::w(Tui::fg(fg, name))))))); - } - })))), - - Some(":browse/title") => to.place(&Fill::x(Align::w(FieldV(Default::default(), - match self.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:", - }, Shrink::x(3, Fixed::y(1, Tui::fg(Tui::g(96), RepeatH("๐Ÿญป")))))))), - - Some(":device") => { - let selected = self.dialog.device_kind().unwrap(); - to.place(&Bsp::s(Tui::bold(true, "Add device"), Map::south(1, - move||device_kinds().iter(), - move|label: &&'static str, i|{ - let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; - let lb = if i == selected { "[ " } else { " " }; - let rb = if i == selected { " ]" } else { " " }; - Fill::x(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) }))) - }, - - Some(_) => { - let views = self.config.views.read().unwrap(); - if let Some(dsl) = views.get(dsl.src()?.unwrap()) { - let dsl = dsl.clone(); - std::mem::drop(views); - self.view(to, dsl)? - } else { - unimplemented!("{dsl:?}"); - } - }, - - _ => unreachable!() - - } - Ok(()) - } - -} impl App { @@ -627,6 +495,323 @@ impl App { } } +/// 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!() + } +} + +handle!(TuiIn:|self: App, input|{ + let mut commands = vec![]; + for id in self.mode.keys.iter() { + if let Some(event_map) = self.config.binds.clone().read().unwrap().get(id.as_ref()) { + if let Some(bindings) = event_map.query(input.event()) { + for binding in bindings { + for command in binding.commands.iter() { + if let Some(command) = self.from(command)? as Option { + commands.push(command) + } + } + } + } + } + } + for command in commands.into_iter() { + let result = command.execute(self); + match result { + Ok(undo) => { + self.history.push((command, undo)); + }, + Err(e) => { + self.history.push((command, None)); + return Err(e) + } + } + } + Ok(None) +}); + +#[derive(Debug, Copy, Clone)] +pub enum Axis { X, Y, Z, I } + +impl<'a> DslNs<'a, AppCommand> for App {} +impl<'a> DslNsExprs<'a, AppCommand> for App {} +impl<'a> DslNsWords<'a, AppCommand> for App { + dsl_words!('a |app| -> AppCommand { + "x/inc" => AppCommand::Inc { axis: Axis::X }, + "x/dec" => AppCommand::Dec { axis: Axis::X }, + "y/inc" => AppCommand::Inc { axis: Axis::Y }, + "y/dec" => AppCommand::Dec { axis: Axis::Y }, + "confirm" => AppCommand::Confirm, + "cancel" => AppCommand::Cancel, + }); +} + +impl Default for AppCommand { fn default () -> Self { Self::Nop } } + +def_command!(AppCommand: |app: App| { + Nop => Ok(None), + 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() } + .execute(app)?, + _ => todo!() + }), + Dec { axis: Axis } => Ok(match (&app.dialog, axis) { + (Dialog::None, _) => None, + (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_prev() } + .execute(app)?, + _ => todo!() + }), + SetDialog { dialog: Dialog } => { + swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) + }, +}); + +pub fn wrap_inc (index: usize, count: usize) -> usize { + if count > 0 { (index + 1) % count } else { 0 } +} + +pub fn wrap_dec (index: usize, count: usize) -> usize { + if count > 0 { index.overflowing_sub(1).0.min(count.saturating_sub(1)) } else { 0 } +} + +impl Dialog { +} + + //AppCommand => { + //("x/inc" / + //("stop-all") => todo!(),//app.project.stop_all(), + //("enqueue", clip: Option>>) => todo!(), + //("history", delta: isize) => todo!(), + //("zoom", zoom: usize) => todo!(), + //("select", selection: Selection) => todo!(), + //("dialog" / command: DialogCommand) => todo!(), + //("project" / command: ArrangementCommand) => todo!(), + //("clock" / command: ClockCommand) => todo!(), + //("sampler" / command: SamplerCommand) => todo!(), + //("pool" / command: PoolCommand) => todo!(), + //("edit" / editor: MidiEditCommand) => todo!(), + //}; + + //DialogCommand; + + //ArrangementCommand; + + //ClockCommand; + + //SamplerCommand; + + //PoolCommand; + + //MidiEditCommand; + + +//take!(DialogCommand |state: App, iter|Take::take(&state.dialog, iter)); +//#[derive(Clone, Debug)] +//pub enum DialogCommand { + //Open { dialog: Dialog }, + //Close +//} + +//impl Command> for DialogCommand { + //fn execute (self, state: &mut Option) -> Perhaps { + //match self { + //Self::Open { dialog } => { + //*state = Some(dialog); + //}, + //Self::Close => { + //*state = None; + //} + //}; + //Ok(None) + //} +//} + +//dsl!(DialogCommand: |self: Dialog, iter|todo!()); +//Dsl::take(&mut self.dialog, iter)); + +//#[tengri_proc::command(Option)]//Nope. +//impl DialogCommand { + //fn open (dialog: &mut Option, new: Dialog) -> Perhaps { + //*dialog = Some(new); + //Ok(None) + //} + //fn close (dialog: &mut Option) -> Perhaps { + //*dialog = None; + //Ok(None) + //} +//} +// +//dsl_bind!(AppCommand: App { + //enqueue = |app, clip: Option>>| { todo!() }; + //history = |app, delta: isize| { todo!() }; + //zoom = |app, zoom: usize| { todo!() }; + //stop_all = |app| { app.tracks_stop_all(); Ok(None) }; + ////dialog = |app, command: DialogCommand| + ////Ok(command.delegate(&mut app.dialog, |c|Self::Dialog{command: c})?); + //project = |app, command: ArrangementCommand| + //Ok(command.delegate(&mut app.project, |c|Self::Project{command: c})?); + //clock = |app, command: ClockCommand| + //Ok(command.execute(app.clock_mut())?.map(|c|Self::Clock{command: c})); + //sampler = |app, command: SamplerCommand| + //Ok(app.project.sampler_mut().map(|s|command.delegate(s, |command|Self::Sampler{command})) + //.transpose()?.flatten()); + //pool = |app, command: PoolCommand| { + //let undo = command.clone().delegate(&mut app.pool, |command|AppCommand::Pool{command})?; + //// update linked editor after pool action + //match command { + //// autoselect: automatically load selected clip in editor + //PoolCommand::Select { .. } | + //// autocolor: update color in all places simultaneously + //PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => { + //let clip = app.pool.clip().clone(); + //app.editor_mut().map(|editor|editor.set_clip(clip.as_ref())) + //}, + //_ => None + //}; + //Ok(undo) + //}; + //select = |app, selection: Selection| { + //*app.project.selection_mut() = selection; + ////todo! + ////if let Some(ref mut editor) = app.editor_mut() { + ////editor.set_clip(match selection { + ////Selection::TrackClip { track, scene } if let Some(Some(Some(clip))) = app + ////.project + ////.scenes.get(scene) + ////.map(|s|s.clips.get(track)) + ////=> + ////Some(clip), + ////_ => + ////None + ////}); + ////} + //Ok(None) + ////("select" [t: usize, s: usize] Some(match (t.expect("no track"), s.expect("no scene")) { + ////(0, 0) => Self::Select(Selection::Mix), + ////(t, 0) => Self::Select(Selection::Track(t)), + ////(0, s) => Self::Select(Selection::Scene(s)), + ////(t, s) => Self::Select(Selection::TrackClip { track: t, scene: s }) }))) + //// autoedit: load focused clip in editor. + //}; + ////fn color (app: &mut App, theme: ItemTheme) -> Perhaps { + ////Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme})) + ////} + ////fn launch (app: &mut App) -> Perhaps { + ////app.project.launch(); + ////Ok(None) + ////} + //toggle_editor = |app, value: bool|{ app.toggle_editor(Some(value)); Ok(None) }; + //editor = |app, command: MidiEditCommand| Ok(if let Some(editor) = app.editor_mut() { + //let undo = command.clone().delegate(editor, |command|AppCommand::Editor{command})?; + //// update linked sampler after editor action + //app.project.sampler_mut().map(|sampler|match command { + //// autoselect: automatically select sample in sampler + //MidiEditCommand::SetNotePos { pos } => { sampler.set_note_pos(pos); }, + //_ => {} + //}); + //undo + //} else { + //None + //}); +//}); +//take!(ClockCommand |state: App, iter|Take::take(state.clock(), iter)); +//take!(MidiEditCommand |state: App, iter|Ok(state.editor().map(|x|Take::take(x, iter)).transpose()?.flatten())); +//take!(PoolCommand |state: App, iter|Take::take(&state.pool, iter)); +//take!(SamplerCommand |state: App, iter|Ok(state.project.sampler().map(|x|Take::take(x, iter)).transpose()?.flatten())); +//take!(ArrangementCommand |state: App, iter|Take::take(&state.project, iter)); + //pub fn view_nil (_: &App) -> TuiCb { //|to|to.place(&Fill::xy("ยท")) //} diff --git a/tek/tek_bind.rs b/tek/tek_bind.rs index b6b02d01..c7b7e813 100644 --- a/tek/tek_bind.rs +++ b/tek/tek_bind.rs @@ -1,231 +1 @@ use crate::*; - -handle!(TuiIn:|self: App, input|{ - let mut commands = vec![]; - for id in self.mode.keys.iter() { - if let Some(event_map) = self.config.binds.clone().read().unwrap().get(id.as_ref()) { - if let Some(bindings) = event_map.query(input.event()) { - for binding in bindings { - for command in binding.commands.iter() { - if let Some(command) = self.from(command)? as Option { - commands.push(command) - } - } - } - } - } - } - for command in commands.into_iter() { - let result = command.execute(self); - match result { - Ok(undo) => { - self.history.push((command, undo)); - }, - Err(e) => { - self.history.push((command, None)); - return Err(e) - } - } - } - Ok(None) -}); - -#[derive(Debug, Copy, Clone)] -pub enum Axis { X, Y, Z, I } - -impl<'a> DslNs<'a, AppCommand> for App {} -impl<'a> DslNsExprs<'a, AppCommand> for App {} -impl<'a> DslNsWords<'a, AppCommand> for App { - dsl_words!('a |app| -> AppCommand { - "x/inc" => AppCommand::Inc { axis: Axis::X }, - "x/dec" => AppCommand::Dec { axis: Axis::X }, - "y/inc" => AppCommand::Inc { axis: Axis::Y }, - "y/dec" => AppCommand::Dec { axis: Axis::Y }, - "confirm" => AppCommand::Confirm, - "cancel" => AppCommand::Cancel, - }); -} - -impl Default for AppCommand { fn default () -> Self { Self::Nop } } - -def_command!(AppCommand: |app: App| { - Nop => Ok(None), - 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() } - .execute(app)?, - _ => todo!() - }), - Dec { axis: Axis } => Ok(match (&app.dialog, axis) { - (Dialog::None, _) => None, - (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_prev() } - .execute(app)?, - _ => todo!() - }), - SetDialog { dialog: Dialog } => { - swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) - }, -}); - -pub fn wrap_inc (index: usize, count: usize) -> usize { - if count > 0 { (index + 1) % count } else { 0 } -} - -pub fn wrap_dec (index: usize, count: usize) -> usize { - if count > 0 { index.overflowing_sub(1).0.min(count.saturating_sub(1)) } else { 0 } -} - -impl Dialog { -} - - //AppCommand => { - //("x/inc" / - //("stop-all") => todo!(),//app.project.stop_all(), - //("enqueue", clip: Option>>) => todo!(), - //("history", delta: isize) => todo!(), - //("zoom", zoom: usize) => todo!(), - //("select", selection: Selection) => todo!(), - //("dialog" / command: DialogCommand) => todo!(), - //("project" / command: ArrangementCommand) => todo!(), - //("clock" / command: ClockCommand) => todo!(), - //("sampler" / command: SamplerCommand) => todo!(), - //("pool" / command: PoolCommand) => todo!(), - //("edit" / editor: MidiEditCommand) => todo!(), - //}; - - //DialogCommand; - - //ArrangementCommand; - - //ClockCommand; - - //SamplerCommand; - - //PoolCommand; - - //MidiEditCommand; - - -//take!(DialogCommand |state: App, iter|Take::take(&state.dialog, iter)); -//#[derive(Clone, Debug)] -//pub enum DialogCommand { - //Open { dialog: Dialog }, - //Close -//} - -//impl Command> for DialogCommand { - //fn execute (self, state: &mut Option) -> Perhaps { - //match self { - //Self::Open { dialog } => { - //*state = Some(dialog); - //}, - //Self::Close => { - //*state = None; - //} - //}; - //Ok(None) - //} -//} - -//dsl!(DialogCommand: |self: Dialog, iter|todo!()); -//Dsl::take(&mut self.dialog, iter)); - -//#[tengri_proc::command(Option)]//Nope. -//impl DialogCommand { - //fn open (dialog: &mut Option, new: Dialog) -> Perhaps { - //*dialog = Some(new); - //Ok(None) - //} - //fn close (dialog: &mut Option) -> Perhaps { - //*dialog = None; - //Ok(None) - //} -//} -// -//dsl_bind!(AppCommand: App { - //enqueue = |app, clip: Option>>| { todo!() }; - //history = |app, delta: isize| { todo!() }; - //zoom = |app, zoom: usize| { todo!() }; - //stop_all = |app| { app.tracks_stop_all(); Ok(None) }; - ////dialog = |app, command: DialogCommand| - ////Ok(command.delegate(&mut app.dialog, |c|Self::Dialog{command: c})?); - //project = |app, command: ArrangementCommand| - //Ok(command.delegate(&mut app.project, |c|Self::Project{command: c})?); - //clock = |app, command: ClockCommand| - //Ok(command.execute(app.clock_mut())?.map(|c|Self::Clock{command: c})); - //sampler = |app, command: SamplerCommand| - //Ok(app.project.sampler_mut().map(|s|command.delegate(s, |command|Self::Sampler{command})) - //.transpose()?.flatten()); - //pool = |app, command: PoolCommand| { - //let undo = command.clone().delegate(&mut app.pool, |command|AppCommand::Pool{command})?; - //// update linked editor after pool action - //match command { - //// autoselect: automatically load selected clip in editor - //PoolCommand::Select { .. } | - //// autocolor: update color in all places simultaneously - //PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => { - //let clip = app.pool.clip().clone(); - //app.editor_mut().map(|editor|editor.set_clip(clip.as_ref())) - //}, - //_ => None - //}; - //Ok(undo) - //}; - //select = |app, selection: Selection| { - //*app.project.selection_mut() = selection; - ////todo! - ////if let Some(ref mut editor) = app.editor_mut() { - ////editor.set_clip(match selection { - ////Selection::TrackClip { track, scene } if let Some(Some(Some(clip))) = app - ////.project - ////.scenes.get(scene) - ////.map(|s|s.clips.get(track)) - ////=> - ////Some(clip), - ////_ => - ////None - ////}); - ////} - //Ok(None) - ////("select" [t: usize, s: usize] Some(match (t.expect("no track"), s.expect("no scene")) { - ////(0, 0) => Self::Select(Selection::Mix), - ////(t, 0) => Self::Select(Selection::Track(t)), - ////(0, s) => Self::Select(Selection::Scene(s)), - ////(t, s) => Self::Select(Selection::TrackClip { track: t, scene: s }) }))) - //// autoedit: load focused clip in editor. - //}; - ////fn color (app: &mut App, theme: ItemTheme) -> Perhaps { - ////Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme})) - ////} - ////fn launch (app: &mut App) -> Perhaps { - ////app.project.launch(); - ////Ok(None) - ////} - //toggle_editor = |app, value: bool|{ app.toggle_editor(Some(value)); Ok(None) }; - //editor = |app, command: MidiEditCommand| Ok(if let Some(editor) = app.editor_mut() { - //let undo = command.clone().delegate(editor, |command|AppCommand::Editor{command})?; - //// update linked sampler after editor action - //app.project.sampler_mut().map(|sampler|match command { - //// autoselect: automatically select sample in sampler - //MidiEditCommand::SetNotePos { pos } => { sampler.set_note_pos(pos); }, - //_ => {} - //}); - //undo - //} else { - //None - //}); -//}); -//take!(ClockCommand |state: App, iter|Take::take(state.clock(), iter)); -//take!(MidiEditCommand |state: App, iter|Ok(state.editor().map(|x|Take::take(x, iter)).transpose()?.flatten())); -//take!(PoolCommand |state: App, iter|Take::take(&state.pool, iter)); -//take!(SamplerCommand |state: App, iter|Ok(state.project.sampler().map(|x|Take::take(x, iter)).transpose()?.flatten())); -//take!(ArrangementCommand |state: App, iter|Take::take(&state.project, iter)); diff --git a/tek/tek_menu.rs b/tek/tek_menu.rs deleted file mode 100644 index b4bd5374..00000000 --- a/tek/tek_menu.rs +++ /dev/null @@ -1,88 +0,0 @@ -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/tek/test.rs b/tek/test.rs index 6ca6190c..71f604be 100644 --- a/tek/test.rs +++ b/tek/test.rs @@ -1,23 +1,21 @@ use crate::*; -#[cfg(test)] #[test] fn test_model () -> Usually<()> { +#[cfg(test)] #[test] fn test_app () -> Usually<()> { let mut app = App::default(); - let _ = app.clip(); - let _ = app.toggle_loop(); - //let _ = app.tracks_add(8, None, &[], &[])?; - //let _ = app.track_add_focus()?; - let _ = app.scene_longest(); - let _ = app.scene(); - let _ = app.scene_mut(); let _ = app.scene_add(None, None)?; - let _ = app.scene_add_focus()?; - let scene = app.scene_del(0); + let _ = app.update_clock(); + Ok(()) +} + +#[cfg(test)] #[test] fn test_track () -> Usually<()> { + let track = Track::default(); + Ok(()) +} + +#[cfg(test)] #[test] fn test_scene () -> Usually<()> { let scene = Scene::default(); let _ = scene.pulses(); let _ = scene.is_playing(&[]); - let _ = app.view_transport(); - let _ = app.view_status(); - let _ = app.update_clock(); Ok(()) } @@ -27,8 +25,8 @@ use crate::*; let _ = button_2("", "", false); let _ = button_3("", "", "", true); let _ = button_3("", "", "", false); - let _ = heading("", "", 0, "", true); - let _ = heading("", "", 0, "", false); + //let _ = heading("", "", 0, "", true); + //let _ = heading("", "", 0, "", false); let _ = wrap(Reset, Reset, ""); } @@ -56,29 +54,29 @@ use crate::*; #[cfg(test)] #[test] fn test_view_iter () { let mut app = App::default(); - app.editor = Some(Default::default()); - let _: Vec<_> = app.inputs_with_sizes().collect(); - let _: Vec<_> = app.outputs_with_sizes().collect(); - let _: Vec<_> = app.tracks_with_sizes().collect(); - let _: Vec<_> = app.scenes_with_sizes(true, 10, 10).collect(); + 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(); } #[cfg(test)] #[test] fn test_view_sizes () { let app = App::default(); - let _ = app.w(); - let _ = app.w_sidebar(); - let _ = app.w_tracks_area(); - let _ = app.h(); - let _ = app.h_tracks_area(); - let _ = app.h_inputs(); - let _ = app.h_outputs(); - let _ = app.h_scenes(); + 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(); } #[cfg(test)] #[test] fn test_midi_edit () { - let editor = MidiEditor::default(); + let _editor = MidiEditor::default(); let mut editor = MidiEditor { mode: PianoHorizontal::new(Some(&Arc::new(RwLock::new(MidiClip::stop_all())))), size: Default::default(), @@ -89,12 +87,13 @@ use crate::*; let _ = editor.clip_status(); let _ = editor.edit_status(); struct TestEditorHost(Option); - has_editor!(|self: TestEditorHost|{ - editor = self.0; - editor_w = 0; - editor_h = 0; - is_editing = false; - }); + has!(Option: |self: TestEditorHost|self.0); + //has_editor!(|self: TestEditorHost|{ + //editor = self.0; + //editor_w = 0; + //editor_h = 0; + //is_editing = false; + //}); let mut host = TestEditorHost(Some(editor)); let _ = host.editor(); let _ = host.editor_mut();