From 7a8b59d024a7e1cc53f0c57faea76676b7b49d94 Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Fri, 5 Sep 2025 20:26:26 +0200 Subject: [PATCH 01/19] add accessability. can send an empty Tree TODO: full tree + event handling --- Cargo.lock | 308 ++++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 7 +- src/app.rs | 128 +++++++++++++++++++--- 3 files changed, 426 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf20ce6..95a42fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,96 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2187590a23ab1e3df8681afdf0987c48504d80291f002fcdb651f0ef5e25169" +[[package]] +name = "accesskit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c0690ad6e6f9597b8439bd3c95e8c6df5cd043afd950c6d68f3b37df641e27c" + +[[package]] +name = "accesskit_atspi_common" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb511e093896d3cae0efba40322087dff59ea322308a3e6edf70f28d22f2607" +dependencies = [ + "accesskit", + "accesskit_consumer", + "atspi-common", + "serde", + "thiserror 1.0.69", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec27574c1baeb7747c802a194566b46b602461e81dc4957949580ea8da695038" +dependencies = [ + "accesskit", + "hashbrown", +] + +[[package]] +name = "accesskit_macos" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf962bfd305aed21133d06128ab3f4a6412031a5b8505534d55af869788af272" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2abbfb16144cca5bb2ea6acad5865b7c1e70d4fa171ceba1a52ea8e78a7515f4" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4cd727229c389e32c1a78fe9f74dc62d7c9fb6eac98cfa1a17efde254fb2d98" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown", + "static_assertions", + "windows 0.61.3", + "windows-core 0.61.2", +] + +[[package]] +name = "accesskit_winit" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "822493d0e54e6793da77525bb7235a19e4fef8418194aaf25a988bc93740d683" +dependencies = [ + "accesskit", + "accesskit_macos", + "accesskit_unix", + "accesskit_windows", + "raw-window-handle", + "winit", +] + [[package]] name = "ahash" version = "0.8.12" @@ -305,6 +395,56 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atspi" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83247582e7508838caf5f316c00791eee0e15c0bf743e6880585b867e16815c" +dependencies = [ + "atspi-common", + "atspi-connection", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33dfc05e7cdf90988a197803bf24f5788f94f7c94a69efa95683e8ffe76cfdfb" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-connection" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4193d51303d8332304056ae0004714256b46b6635a5c556109b319c0d3784938" +dependencies = [ + "atspi-common", + "atspi-proxies", + "futures-lite", + "zbus", +] + +[[package]] +name = "atspi-proxies" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2eebcb9e7e76f26d0bcfd6f0295e1cd1e6f33bedbc5698a971db8dc43d7751c" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -2142,6 +2282,16 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +[[package]] +name = "quick-xml" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quick-xml" version = "0.37.5" @@ -2832,6 +2982,8 @@ dependencies = [ name = "torque-tracker" version = "0.1.1" dependencies = [ + "accesskit", + "accesskit_winit", "ascii", "cpal", "font8x8", @@ -3147,7 +3299,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" dependencies = [ "proc-macro2", - "quick-xml", + "quick-xml 0.37.5", "quote", ] @@ -3383,6 +3535,28 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + [[package]] name = "windows-core" version = "0.54.0" @@ -3399,13 +3573,37 @@ version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.58.0", + "windows-interface 0.58.0", "windows-result 0.2.0", - "windows-strings", + "windows-strings 0.1.0", "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.0", + "windows-interface 0.59.1", + "windows-link", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.58.0" @@ -3417,6 +3615,17 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-interface" version = "0.58.0" @@ -3428,6 +3637,33 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -3446,6 +3682,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-strings" version = "0.1.0" @@ -3456,6 +3701,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -3554,6 +3808,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -3930,6 +4193,30 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zbus-lockstep" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29e96e38ded30eeab90b6ba88cb888d70aef4e7489b6cd212c5e5b5ec38045b6" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6821851fa840b708b4cbbaf6241868cabc85a2dc22f426361b0292bfc0b836" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + [[package]] name = "zbus_macros" version = "5.8.0" @@ -3957,6 +4244,19 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zbus_xml" +version = "5.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589e9a02bfafb9754bb2340a9e3b38f389772684c63d9637e76b1870377bec29" +dependencies = [ + "quick-xml 0.36.2", + "serde", + "static_assertions", + "zbus_names", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.26" diff --git a/Cargo.toml b/Cargo.toml index 94d3b25..8f60767 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,14 +23,19 @@ triple_buffer = "8.1.1" softbuffer = { version="0.4.6", optional = true } rfd = "0.15.4" symphonia = "0.5.4" +accesskit_winit = { version = "0.29.0", optional = true } +accesskit = { version = "0.21.0", optional = true } [features] # needs to be one, but not both # less artifacts than software scaling. also probably faster gpu_scaling = ["dep:wgpu"] soft_scaling = ["dep:softbuffer"] +# accessability isn't an optional feature of course, but i want to make this progream compatible with embedded at some point. +# To not make this harder than necessary it is done like this +accesskit = ["dep:accesskit_winit", "dep:accesskit"] -default = ["gpu_scaling"] +default = ["gpu_scaling", "accesskit"] [lints.clippy] uninlined_format_args = "allow" diff --git a/src/app.rs b/src/app.rs index 611fc98..5c9aa42 100644 --- a/src/app.rs +++ b/src/app.rs @@ -19,7 +19,7 @@ use winit::{ event::{Modifiers, WindowEvent}, event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy}, keyboard::{Key, NamedKey}, - window::{Window, WindowAttributes}, + window::WindowAttributes, }; use cpal::{ @@ -66,6 +66,8 @@ pub enum GlobalEvent { GoToPage(PagesEnum), // Needed because only in the main app i know which pattern is selected, so i know what to play Playback(PlaybackType), + #[cfg(feature = "accesskit")] + Accesskit(accesskit_winit::WindowEvent), CloseRequested, CloseApp, ConstRedraw, @@ -83,6 +85,10 @@ impl Clone for GlobalEvent { GlobalEvent::CloseApp => GlobalEvent::CloseApp, GlobalEvent::ConstRedraw => GlobalEvent::ConstRedraw, GlobalEvent::Playback(playback_type) => GlobalEvent::Playback(*playback_type), + #[cfg(feature = "accesskit")] + GlobalEvent::Accesskit(window_event) => { + todo!("https://github.com/AccessKit/accesskit/issues/610") + } } } } @@ -99,11 +105,21 @@ impl Debug for GlobalEvent { GlobalEvent::CloseApp => debug.field("CloseApp", &""), GlobalEvent::ConstRedraw => debug.field("ConstRedraw", &""), GlobalEvent::Playback(playback_type) => debug.field("Playback", &playback_type), + #[cfg(feature = "accesskit")] + GlobalEvent::Accesskit(window_event) => debug.field("Accesskit", &window_event), }; debug.finish() } } +#[cfg(feature = "accesskit")] +impl From for GlobalEvent { + fn from(value: accesskit_winit::Event) -> Self { + // ignore window id, because i only have one window + Self::Accesskit(value.window_event) + } +} + #[derive(Clone, Copy, Debug)] pub enum PlaybackType { Stop, @@ -165,8 +181,16 @@ impl EventQueue<'_> { } } +/// window with all the additional stuff +struct Window { + window: Arc, + render_backend: RenderBackend, + #[cfg(feature = "accesskit")] + adapter: accesskit_winit::Adapter, +} + pub struct App { - window_gpu: Option<(Arc, RenderBackend)>, + window: Option, draw_buffer: DrawBuffer, modifiers: Modifiers, ui_pages: AllPages, @@ -236,7 +260,7 @@ impl ApplicationHandler for App { fn suspended(&mut self, _: &ActiveEventLoop) { // my window and GPU state have been invalidated - self.window_gpu = None; + self.window = None; } fn window_event( @@ -247,7 +271,7 @@ impl ApplicationHandler for App { ) { // destructure so i don't have to always type self. let Self { - window_gpu, + window, draw_buffer, modifiers, ui_pages, @@ -259,10 +283,24 @@ impl ApplicationHandler for App { audio_stream: _, } = self; - // panic is fine because when i get a window_event a window exists - let (window, render_backend) = window_gpu.as_mut().unwrap(); - // don't want the window to be mut + // i won't get a window_event without having a window, so unwrap is fine + let window = window.as_mut().unwrap(); + #[cfg(not(feature = "accesskit"))] + let Window { + window, + render_backend, + } = window; + #[cfg(feature = "accesskit")] + let Window { + window, + render_backend, + adapter, + } = window; + // don't want window to be mutable let window = window.as_ref(); + + #[cfg(feature = "accesskit")] + adapter.process_event(window, &event); // limit the pages and widgets to only push events and not read or pop let event_queue = &mut EventQueue(event_queue); @@ -287,6 +325,9 @@ impl ApplicationHandler for App { header.draw(draw_buffer); ui_pages.draw(draw_buffer); dialog_manager.draw(draw_buffer); + #[cfg(feature = "accesskit")] + adapter + .update_if_active(|| Self::produce_full_tree(ui_pages, header, dialog_manager)); // notify the windowing system that drawing is done and the new buffer is about to be pushed window.pre_present_notify(); // push the framebuffer into GPU/softbuffer and render it onto the screen @@ -412,6 +453,27 @@ impl ApplicationHandler for App { .expect("buffer full. either increase size or retry somehow") } } + #[cfg(feature = "accesskit")] + GlobalEvent::Accesskit(window_event) => { + use accesskit_winit::WindowEvent; + match window_event { + WindowEvent::InitialTreeRequested => { + // there probably should always be a window + // make sure we always respond to this event. I hope it can't be send when there is no window + let window = self.window.as_mut().unwrap(); + window.adapter.update_if_active(|| { + Self::produce_full_tree( + &self.ui_pages, + &self.header, + &self.dialog_manager, + ) + }); + } + WindowEvent::ActionRequested(action_request) => todo!(), + // i don't have any extra state for accessability so i don't need to cleanup anything + WindowEvent::AccessibilityDeactivated => (), + } + } } } @@ -429,7 +491,7 @@ impl ApplicationHandler for App { impl App { pub fn new(proxy: EventLoopProxy) -> Self { Self { - window_gpu: None, + window: None, draw_buffer: DrawBuffer::new(), modifiers: Modifiers::default(), ui_pages: AllPages::new(proxy.clone()), @@ -455,8 +517,8 @@ impl App { /// tries to request a redraw. if there currently is no window this fails fn try_request_redraw(&self) -> Result<(), ()> { - if let Some((window, _)) = &self.window_gpu { - window.request_redraw(); + if let Some(window) = &self.window { + window.window.request_redraw(); Ok(()) } else { Err(()) @@ -464,16 +526,34 @@ impl App { } fn build_window(&mut self, event_loop: &ActiveEventLoop) { - self.window_gpu.get_or_insert_with(|| { + self.window.get_or_insert_with(|| { let mut attributes = WindowAttributes::default(); attributes.active = true; attributes.resizable = true; attributes.resize_increments = None; attributes.title = String::from("Torque Tracker"); + // for accesskit + attributes.visible = false; let window = Arc::new(event_loop.create_window(attributes).unwrap()); let render_backend = RenderBackend::new(window.clone(), Palette::CAMOUFLAGE); - (window, render_backend) + + #[cfg(feature = "accesskit")] + let adapter = { + accesskit_winit::Adapter::with_event_loop_proxy( + event_loop, + &window, + self.event_loop_proxy.clone(), + ) + }; + + window.set_visible(true); + Window { + window, + render_backend, + #[cfg(feature = "accesskit")] + adapter, + } }); } @@ -570,6 +650,30 @@ impl App { // lastly kill the audio stream drop(stream); } + + #[cfg(feature = "accesskit")] + fn produce_full_tree( + pages: &AllPages, + header: &Header, + dialogs: &DialogManager, + ) -> accesskit::TreeUpdate { + use accesskit::TreeUpdate; + use accesskit::{Node, NodeId, Role, Tree}; + const ROOT_ID: NodeId = NodeId(0); + + let tree = Tree { + root: ROOT_ID, + toolkit_name: Some(String::from("Torque Tracker Custom")), + toolkit_version: None, + }; + let mut root_node = Node::new(Role::Window); + let nodes = vec![(ROOT_ID, root_node)]; + TreeUpdate { + nodes, + tree: Some(tree), + focus: ROOT_ID, + } + } } impl Drop for App { From 877af1a6586c539f71dc7b90f0f96e18f555c0bb Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Sat, 6 Sep 2025 12:47:54 +0200 Subject: [PATCH 02/19] basic main menu accessability working. still need to get rid of "group" somehow --- src/app.rs | 21 ++++++++- src/ui/dialog.rs | 9 ++++ src/ui/dialog/confirm.rs | 8 ++++ src/ui/dialog/page_menu.rs | 80 ++++++++++++++++++++++++++++++---- src/ui/dialog/slider_dialog.rs | 8 ++++ src/ui/header.rs | 14 ++++++ 6 files changed, 129 insertions(+), 11 deletions(-) diff --git a/src/app.rs b/src/app.rs index 5c9aa42..d6dbfac 100644 --- a/src/app.rs +++ b/src/app.rs @@ -667,11 +667,22 @@ impl App { toolkit_version: None, }; let mut root_node = Node::new(Role::Window); - let nodes = vec![(ROOT_ID, root_node)]; + let mut nodes = Vec::new(); + let mut focused = None; + let header_id = header.build_tree(&mut nodes); + root_node.push_child(header_id); + + if let Some(dialog) = dialogs.active_dialog() { + let resp = dialog.build_tree(&mut nodes); + root_node.push_child(resp.root); + focused = Some(resp.selected); + } + + nodes.push((ROOT_ID, root_node)); TreeUpdate { nodes, tree: Some(tree), - focus: ROOT_ID, + focus: focused.unwrap_or(ROOT_ID), } } } @@ -697,3 +708,9 @@ pub fn run() { event_loop.run_app(&mut app).unwrap(); } + +#[cfg(feature = "accesskit")] +pub struct AccessResponse { + pub root: accesskit::NodeId, + pub selected: accesskit::NodeId, +} diff --git a/src/ui/dialog.rs b/src/ui/dialog.rs index e8a2148..7394d7d 100644 --- a/src/ui/dialog.rs +++ b/src/ui/dialog.rs @@ -24,6 +24,11 @@ pub trait Dialog { modifiers: &Modifiers, events: &mut EventQueue<'_>, ) -> DialogResponse; + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::app::AccessResponse; } pub struct DialogManager { @@ -45,6 +50,10 @@ impl DialogManager { } } + pub fn active_dialog(&self) -> Option<&dyn Dialog> { + self.stack.last().map(|d| d.as_ref()) + } + pub fn is_active(&self) -> bool { !self.stack.is_empty() } diff --git a/src/ui/dialog/confirm.rs b/src/ui/dialog/confirm.rs index 2b06edb..accd340 100644 --- a/src/ui/dialog/confirm.rs +++ b/src/ui/dialog/confirm.rs @@ -111,4 +111,12 @@ impl Dialog for ConfirmDialog { StandardResponse::None => DialogResponse::None, } } + + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::app::AccessResponse { + todo!() + } } diff --git a/src/ui/dialog/page_menu.rs b/src/ui/dialog/page_menu.rs index 225eb11..bc91398 100644 --- a/src/ui/dialog/page_menu.rs +++ b/src/ui/dialog/page_menu.rs @@ -30,10 +30,12 @@ enum Menu { pub struct PageMenu { name: &'static str, rect: CharRect, - selected: usize, + selected: u8, pressed: bool, buttons: &'static [(&'static str, Action)], sub_menu: Option>, + #[cfg(feature = "accesskit")] + node_id: accesskit::NodeId, } impl Dialog for PageMenu { @@ -58,7 +60,7 @@ impl Dialog for PageMenu { 1, ); for (num, (name, _)) in self.buttons.iter().enumerate() { - let text_color = match self.selected == num { + let text_color = match usize::from(self.selected) == num { true => 11, false => 0, }; @@ -70,11 +72,12 @@ impl Dialog for PageMenu { Self::BACKGROUND_COLOR, ); let top = top + (3 * num) + 4; - let (top_left, bot_right) = - match (self.pressed || self.sub_menu.is_some()) && self.selected == num { - true => (Self::BOTRIGHT_COLOR, Self::TOPLEFT_COLOR), - false => (Self::TOPLEFT_COLOR, Self::BOTRIGHT_COLOR), - }; + let (top_left, bot_right) = match (self.pressed || self.sub_menu.is_some()) + && usize::from(self.selected) == num + { + true => (Self::BOTRIGHT_COLOR, Self::TOPLEFT_COLOR), + false => (Self::TOPLEFT_COLOR, Self::BOTRIGHT_COLOR), + }; let rect = CharRect::new(top, top + 2, left + 2, left + self.rect.width() - 2); draw_buffer.draw_out_border(rect, top_left, bot_right, 1); Self::draw_button_corners(rect, draw_buffer); @@ -110,7 +113,7 @@ impl Dialog for PageMenu { return DialogResponse::RequestRedraw; } else if self.pressed { self.pressed = false; - match &self.buttons[self.selected].1 { + match &self.buttons[usize::from(self.selected)].1 { Action::Menu(menu) => { let menu = match menu { Menu::File => Self::file(), @@ -144,7 +147,7 @@ impl Dialog for PageMenu { self.pressed = false; return DialogResponse::RequestRedraw; } else if key_event.logical_key == Key::Named(NamedKey::ArrowDown) - && self.selected < self.buttons.len() - 1 + && usize::from(self.selected) < self.buttons.len() - 1 { self.selected += 1; self.pressed = false; @@ -154,6 +157,50 @@ impl Dialog for PageMenu { DialogResponse::None } + + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::app::AccessResponse { + use accesskit::{Action, Node, NodeId, Role}; + + use crate::app::AccessResponse; + let mut root = Node::new(Role::Dialog); + root.set_label(self.name); + let mut selected = NodeId(u64::from(self.selected) + self.node_id.0 + 1); + + // root of the sub_menu. Will be set as the child of the selected button + let sub_menu = self.sub_menu.as_ref().map(|m| { + let resp = m.build_tree(tree); + + selected = resp.selected; + + resp.root + }); + + for (num, (name, _)) in self.buttons.iter().enumerate() { + let mut node = Node::new(Role::Button); + node.add_action(Action::Click); + node.set_keyboard_shortcut("Enter"); + node.set_label(*name); + if usize::from(self.selected) == num { + node.set_selected(true); + if let Some(id) = sub_menu { + node.push_child(id); + } + } + let id = NodeId(u64::try_from(num).unwrap() + self.node_id.0 + 1); + tree.push((id, node)); + root.push_child(id); + } + + tree.push((self.node_id, root)); + AccessResponse { + root: self.node_id, + selected, + } + } } impl PageMenu { @@ -165,6 +212,7 @@ impl PageMenu { pos: CharPosition, width: usize, buttons: &'static [(&'static str, Action)], + #[cfg(feature = "accesskit")] node_id: accesskit::NodeId, ) -> Self { let rect = CharRect::new( pos.y(), @@ -180,6 +228,8 @@ impl PageMenu { pressed: false, buttons, sub_menu: None, + #[cfg(feature = "accesskit")] + node_id, } } @@ -227,6 +277,8 @@ impl PageMenu { ("Settings Menu...", Action::Menu(Menu::Settings)), ("Help! (F1)", Action::Page(PagesEnum::Help)), ], + #[cfg(feature = "accesskit")] + accesskit::NodeId(1_000_000_000), ) } @@ -247,6 +299,8 @@ impl PageMenu { Action::Event(GlobalEvent::CloseRequested), ), ], + #[cfg(feature = "accesskit")] + accesskit::NodeId(1_000_000_100), ) } @@ -278,6 +332,8 @@ impl PageMenu { ("Driver Screen (Shift-F5)", Action::NotYetImplemented), ("Calculate Length (Ctrl-P)", Action::NotYetImplemented), ], + #[cfg(feature = "accesskit")] + accesskit::NodeId(1_000_000_200), ) } @@ -293,6 +349,8 @@ impl PageMenu { ), ("Sample Library (Ctrl-F3)", Action::NotYetImplemented), ], + #[cfg(feature = "accesskit")] + accesskit::NodeId(1_000_000_300), ) } @@ -305,6 +363,8 @@ impl PageMenu { ("Instrument List (F4)", Action::NotYetImplemented), ("Instrument Library (Ctrl-F4)", Action::NotYetImplemented), ], + #[cfg(feature = "accesskit")] + accesskit::NodeId(1_000_000_400), ) } @@ -339,6 +399,8 @@ impl PageMenu { Action::NotYetImplemented, ), ], + #[cfg(feature = "accesskit")] + accesskit::NodeId(1_000_000_500), ) } } diff --git a/src/ui/dialog/slider_dialog.rs b/src/ui/dialog/slider_dialog.rs index e975b91..9cf890b 100644 --- a/src/ui/dialog/slider_dialog.rs +++ b/src/ui/dialog/slider_dialog.rs @@ -59,6 +59,14 @@ impl Dialog for SliderDialog { StandardResponse::None => DialogResponse::None, } } + + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::app::AccessResponse { + todo!() + } } impl SliderDialog { diff --git a/src/ui/header.rs b/src/ui/header.rs index a6cd090..8affc8f 100644 --- a/src/ui/header.rs +++ b/src/ui/header.rs @@ -186,4 +186,18 @@ impl Header { // TODO: Not actually constant as it changes between Sample and Instrument mode buffer.draw_string("Sample", CharPosition::new(43, 3), 0, 2); } + + /// Returns the ID of the header root. This should be added as a child of the outer node + #[cfg(feature = "accesskit")] + pub fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> accesskit::NodeId { + use accesskit::{Node, NodeId, Role}; + const HEADER_ID: NodeId = NodeId(1); + let root = Node::new(Role::Header); + + tree.push((HEADER_ID, root)); + HEADER_ID + } } From 36b5a3e38ed758e9bb5b75e6b4e8acb2f51030cf Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Sun, 7 Sep 2025 14:38:42 +0200 Subject: [PATCH 03/19] remove WidgetList macro --- src/ui/dialog/confirm.rs | 84 +++++---- src/ui/pages.rs | 75 --------- src/ui/pages/song_directory_config_page.rs | 187 ++++++++++++--------- src/ui/widgets.rs | 37 ++-- 4 files changed, 168 insertions(+), 215 deletions(-) diff --git a/src/ui/dialog/confirm.rs b/src/ui/dialog/confirm.rs index accd340..e0fb77b 100644 --- a/src/ui/dialog/confirm.rs +++ b/src/ui/dialog/confirm.rs @@ -4,34 +4,26 @@ use crate::{ app::GlobalEvent, coordinates::{CharPosition, CharRect}, draw_buffer::DrawBuffer, - ui::{ - pages::create_widget_list, - widgets::{NextWidget, StandardResponse, WidgetResponse, button::Button}, - }, + ui::widgets::{NextWidget, StandardResponse, Widget, WidgetResponse, button::Button}, }; use super::{Dialog, DialogResponse}; -create_widget_list!( - response: Option; - WidgetList - { - ok: Button>, - cancel: Button> - } -); - pub struct ConfirmDialog { text: &'static str, text_pos: CharPosition, // computed from the string length rect: CharRect, - widgets: WidgetList, + ok: Button>, + cancel: Button>, + selected: u8, } impl ConfirmDialog { const OK_RECT: CharRect = CharRect::new(29, 31, 41, 50); const CANCEL_RECT: CharRect = CharRect::new(29, 31, 30, 39); + const OK: u8 = 0; + const CANCEL: u8 = 1; pub fn new( text: &'static str, ok_event: fn() -> Option, @@ -42,33 +34,31 @@ impl ConfirmDialog { Self { text, text_pos: CharPosition::new(40 - per_side + 5, 27), - widgets: WidgetList { - selected: WidgetList::OK, - ok: Button::new( - " Ok", - Self::OK_RECT, - NextWidget { - left: Some(WidgetList::CANCEL), - right: Some(WidgetList::CANCEL), - tab: Some(WidgetList::CANCEL), - shift_tab: Some(WidgetList::CANCEL), - ..Default::default() - }, - ok_event, - ), - cancel: Button::new( - "Cancel", - Self::CANCEL_RECT, - NextWidget { - left: Some(WidgetList::OK), - right: Some(WidgetList::OK), - tab: Some(WidgetList::OK), - shift_tab: Some(WidgetList::OK), - ..Default::default() - }, - cancel_event, - ), - }, + selected: Self::OK, + ok: Button::new( + " Ok", + Self::OK_RECT, + NextWidget { + left: Some(Self::CANCEL), + right: Some(Self::CANCEL), + tab: Some(Self::CANCEL), + shift_tab: Some(Self::CANCEL), + ..Default::default() + }, + ok_event, + ), + cancel: Button::new( + "Cancel", + Self::CANCEL_RECT, + NextWidget { + left: Some(Self::OK), + right: Some(Self::OK), + tab: Some(Self::OK), + shift_tab: Some(Self::OK), + ..Default::default() + }, + cancel_event, + ), rect: CharRect::new(25, 32, 40 - per_side, 40 + per_side), } } @@ -79,7 +69,8 @@ impl Dialog for ConfirmDialog { draw_buffer.draw_rect(2, self.rect); draw_buffer.draw_out_border(self.rect, 3, 3, 2); draw_buffer.draw_string(self.text, self.text_pos, 0, 2); - self.widgets.draw_widgets(draw_buffer); + self.ok.draw(draw_buffer, self.selected == Self::OK); + self.cancel.draw(draw_buffer, self.selected == Self::CANCEL); } fn process_input( @@ -92,8 +83,11 @@ impl Dialog for ConfirmDialog { return DialogResponse::Close; } - let WidgetResponse { standard, extra } = - self.widgets.process_input(key_event, modifiers, events); + let WidgetResponse { standard, extra } = match self.selected { + Self::OK => self.ok.process_input(modifiers, key_event, events), + Self::CANCEL => self.cancel.process_input(modifiers, key_event, events), + _ => unreachable!(), + }; if let Some(global_option) = extra { if let Some(global) = global_option { events.push(global); @@ -104,7 +98,7 @@ impl Dialog for ConfirmDialog { match standard { StandardResponse::SwitchFocus(next) => { - self.widgets.selected = next; + self.selected = next; DialogResponse::RequestRedraw } StandardResponse::RequestRedraw => DialogResponse::RequestRedraw, diff --git a/src/ui/pages.rs b/src/ui/pages.rs index 10b234a..9a2a098 100644 --- a/src/ui/pages.rs +++ b/src/ui/pages.rs @@ -35,81 +35,6 @@ pub trait Page { ) -> PageResponse; } -/// creates a struct called WidgetList with all the specified fields. -/// -/// inserts a const index for every field in WidgetList into the specified struct -/// as well as a function to query for the Widgets from a those indices. -/// -/// Needs at least one fields to work. If it is less, just write from hand. -macro_rules! create_widget_list { - (@function rsp: $response:ty; $($name:ident),*) => { - fn get_widget_mut(&mut self, idx: usize) -> &mut dyn crate::ui::widgets::Widget { - paste::paste! ( - $(if idx == Self::[<$name:upper>] { &mut self.$name } else)* - { panic!("invalid index {:?}", idx) } - ) - } - fn get_widget(&self, idx: usize) -> &dyn crate::ui::widgets::Widget { - paste::paste! ( - $(if idx == Self::[<$name:upper>] { &self.$name } else)* - { panic!("invalid index {:?}", idx) } - ) - } - }; - // inital with more than one name - (response: $response:ty; $list_name: ident { $name:ident: $type:ty, $($n:ident: $t:ty),* }) => ( - struct $list_name { - selected: usize, - $name: $type, - $($n: $t),* - } - - impl $list_name { - paste::paste!( - const [<$name:upper>]: usize = 0; - ); - const INDEX_RANGE: std::ops::Range = 0..Self::WIDGET_COUNT; - - pub fn draw_widgets(&self, draw_buffer: &mut crate::draw_buffer::DrawBuffer) { - for widget in Self::INDEX_RANGE { - let is_selected = widget == self.selected; - self.get_widget(widget).draw(draw_buffer, is_selected); - } - } - - pub fn process_input( - &mut self, - key_event: &winit::event::KeyEvent, - modifiers: &winit::event::Modifiers, - events: &mut crate::app::EventQueue<'_>, - ) -> WidgetResponse<$response> { - self.get_widget_mut(self.selected).process_input(modifiers, key_event, events) - } - - crate::ui::pages::create_widget_list!($($n),* ; $name); - crate::ui::pages::create_widget_list!(@function rsp: $response; $name, $($n),*); - } - ); - // last name - ($name:ident ; $prev:ident) => ( - // const $name: usize = $num; - paste::paste!( - const [<$name:upper>]: usize = Self::[<$prev:upper>] + 1usize; - const WIDGET_COUNT: usize = Self::[<$name:upper>] + 1usize; - ); - ); - // loop over names - ($name:ident, $($n:ident),+ ; $prev:ident) => ( - // const $name: usize = $num; - paste::paste!( - const [<$name:upper>]: usize = Self::[<$prev:upper>] + 1; - ); - crate::ui::pages::create_widget_list!($($n),+ ; $name); - ); -} - -pub(crate) use create_widget_list; - pub enum PageResponse { RequestRedraw, None, diff --git a/src/ui/pages/song_directory_config_page.rs b/src/ui/pages/song_directory_config_page.rs index 4641137..29b6c44 100644 --- a/src/ui/pages/song_directory_config_page.rs +++ b/src/ui/pages/song_directory_config_page.rs @@ -6,7 +6,9 @@ use crate::{ app::{EventQueue, GlobalEvent, send_song_op}, coordinates::{CharPosition, CharRect}, draw_buffer::DrawBuffer, - ui::widgets::{NextWidget, StandardResponse, WidgetResponse, slider::Slider, text_in::TextIn}, + ui::widgets::{ + NextWidget, StandardResponse, Widget, WidgetResponse, slider::Slider, text_in::TextIn, + }, }; use super::{Page, PageResponse}; @@ -39,43 +41,56 @@ pub enum SDCChange { // Seperation(i16), } -super::create_widget_list!( - response: (); - WidgetList - { - song_name: TextIn<()>, - initial_tempo: Slider<31, 255, ()>, - initial_speed: Slider<1, 255, ()>, - global_volume: Slider<0, 128, ()> - // mixing_volume: Slider<0, 128, ()>, - // seperation: Slider<0, 128, ()>, - - // old_effects: Toggle, - // compatible_gxx: Toggle, - - // instruments: ToggleButton, - // samples: ToggleButton, - - // stereo: ToggleButton, - // mono: ToggleButton, - - // linear_slides: ToggleButton, - // amiga_slides: ToggleButton, - - // module_path: TextInScroll<()>, - // sample_path: TextInScroll<()>, - // instrument_path: TextInScroll<()>, - // save: Button<()> - } -); +// super::create_widget_list!( +// response: (); +// WidgetList +// { +// song_name: TextIn<()>, +// initial_tempo: Slider<31, 255, ()>, +// initial_speed: Slider<1, 255, ()>, +// global_volume: Slider<0, 128, ()> +// // mixing_volume: Slider<0, 128, ()>, +// // seperation: Slider<0, 128, ()>, + +// // old_effects: Toggle, +// // compatible_gxx: Toggle, + +// // instruments: ToggleButton, +// // samples: ToggleButton, + +// // stereo: ToggleButton, +// // mono: ToggleButton, + +// // linear_slides: ToggleButton, +// // amiga_slides: ToggleButton, + +// // module_path: TextInScroll<()>, +// // sample_path: TextInScroll<()>, +// // instrument_path: TextInScroll<()>, +// // save: Button<()> +// } +// ); pub struct SongDirectoryConfigPage { - widgets: WidgetList, + // widgets: WidgetList, + song_name: TextIn<()>, + initial_tempo: Slider<31, 255, ()>, + initial_speed: Slider<1, 255, ()>, + global_volume: Slider<0, 128, ()>, + selected: u8, } impl Page for SongDirectoryConfigPage { fn draw(&mut self, draw_buffer: &mut DrawBuffer) { - self.widgets.draw_widgets(draw_buffer); + // self.widgets.draw_widgets(draw_buffer); + self.song_name + .draw(draw_buffer, Self::SONG_NAME == self.selected); + self.initial_tempo + .draw(draw_buffer, Self::INITIAL_TEMPO == self.selected); + self.initial_speed + .draw(draw_buffer, Self::INITIAL_SPEED == self.selected); + self.global_volume + .draw(draw_buffer, Self::GLOBAL_VOLUME == self.selected); } fn draw_constant(&mut self, draw_buffer: &mut DrawBuffer) { @@ -147,37 +162,45 @@ impl Page for SongDirectoryConfigPage { key_event: &winit::event::KeyEvent, events: &mut EventQueue<'_>, ) -> PageResponse { - match self - .widgets - .process_input(key_event, modifiers, events) - .standard - { - StandardResponse::SwitchFocus(next) => { - self.widgets.selected = next; - PageResponse::RequestRedraw - } - StandardResponse::RequestRedraw => PageResponse::RequestRedraw, - StandardResponse::None => PageResponse::None, - } + let resp = match self.selected { + Self::SONG_NAME => self.song_name.process_input(modifiers, key_event, events), + Self::INITIAL_TEMPO => self + .initial_tempo + .process_input(modifiers, key_event, events), + Self::INITIAL_SPEED => self + .initial_speed + .process_input(modifiers, key_event, events), + Self::GLOBAL_VOLUME => self + .global_volume + .process_input(modifiers, key_event, events), + _ => unreachable!(), + }; + + resp.standard.to_page_resp(&mut self.selected) } } impl SongDirectoryConfigPage { + const SONG_NAME: u8 = 0; + const INITIAL_TEMPO: u8 = 1; + const INITIAL_SPEED: u8 = 2; + const GLOBAL_VOLUME: u8 = 3; + pub fn ui_change(&mut self, change: SDCChange) -> PageResponse { match change { - SDCChange::SetSongName(s) => match self.widgets.song_name.set_string(s) { + SDCChange::SetSongName(s) => match self.song_name.set_string(s) { Ok(_) => PageResponse::RequestRedraw, Err(_) => PageResponse::None, }, - SDCChange::InitialTempo(n) => match self.widgets.initial_tempo.try_set(n) { + SDCChange::InitialTempo(n) => match self.initial_tempo.try_set(n) { Ok(_) => PageResponse::RequestRedraw, Err(_) => PageResponse::None, }, - SDCChange::InitialSpeed(n) => match self.widgets.initial_speed.try_set(n) { + SDCChange::InitialSpeed(n) => match self.initial_speed.try_set(n) { Ok(_) => PageResponse::RequestRedraw, Err(_) => PageResponse::None, }, - SDCChange::GlobalVolume(n) => match self.widgets.global_volume.try_set(n) { + SDCChange::GlobalVolume(n) => match self.global_volume.try_set(n) { Ok(_) => PageResponse::RequestRedraw, Err(_) => PageResponse::None, }, @@ -197,8 +220,8 @@ impl SongDirectoryConfigPage { CharPosition::new(17, 16), 25, NextWidget { - down: Some(WidgetList::INITIAL_TEMPO), - tab: Some(WidgetList::INITIAL_TEMPO), + down: Some(Self::INITIAL_TEMPO), + tab: Some(Self::INITIAL_TEMPO), ..Default::default() }, |s| println!("new song name: {}", s), @@ -208,10 +231,10 @@ impl SongDirectoryConfigPage { CharPosition::new(17, 19), 32, NextWidget { - up: Some(WidgetList::SONG_NAME), - shift_tab: Some(WidgetList::SONG_NAME), - down: Some(WidgetList::INITIAL_SPEED), - tab: Some(WidgetList::INITIAL_SPEED), + up: Some(Self::SONG_NAME), + shift_tab: Some(Self::SONG_NAME), + down: Some(Self::INITIAL_SPEED), + tab: Some(Self::INITIAL_SPEED), ..Default::default() }, |n| GlobalEvent::Page(super::PageEvent::Sdc(SDCChange::InitialTempo(n))), @@ -226,10 +249,10 @@ impl SongDirectoryConfigPage { CharPosition::new(17, 20), 32, NextWidget { - up: Some(WidgetList::INITIAL_TEMPO), - shift_tab: Some(WidgetList::INITIAL_TEMPO), - down: Some(WidgetList::GLOBAL_VOLUME), - tab: Some(WidgetList::GLOBAL_VOLUME), + up: Some(Self::INITIAL_TEMPO), + shift_tab: Some(Self::INITIAL_TEMPO), + down: Some(Self::GLOBAL_VOLUME), + tab: Some(Self::GLOBAL_VOLUME), ..Default::default() }, |n| GlobalEvent::Page(super::PageEvent::Sdc(SDCChange::InitialSpeed(n))), @@ -244,10 +267,10 @@ impl SongDirectoryConfigPage { CharPosition::new(17, 23), 16, NextWidget { - up: Some(WidgetList::INITIAL_SPEED), - shift_tab: Some(WidgetList::INITIAL_SPEED), - // down: Some(WidgetList::MIXING_VOLUME), - // tab: Some(WidgetList::MIXING_VOLUME), + up: Some(Self::INITIAL_SPEED), + shift_tab: Some(Self::INITIAL_SPEED), + // down: Some(Self::MIXING_VOLUME), + // tab: Some(Self::MIXING_VOLUME), ..Default::default() }, |n| GlobalEvent::Page(super::PageEvent::Sdc(SDCChange::GlobalVolume(n))), @@ -458,27 +481,25 @@ impl SongDirectoryConfigPage { // }, // ); Self { - widgets: WidgetList { - song_name, - initial_tempo, - initial_speed, - global_volume, - // mixing_volume, - // seperation, - // old_effects, - // compatible_gxx, - // instruments, - // samples, - // stereo, - // mono, - // linear_slides, - // amiga_slides, - // module_path, - // sample_path, - // instrument_path, - // save, - selected: WidgetList::SONG_NAME, - }, + song_name, + initial_tempo, + initial_speed, + global_volume, + // mixing_volume, + // seperation, + // old_effects, + // compatible_gxx, + // instruments, + // samples, + // stereo, + // mono, + // linear_slides, + // amiga_slides, + // module_path, + // sample_path, + // instrument_path, + // save, + selected: Self::SONG_NAME, } } } diff --git a/src/ui/widgets.rs b/src/ui/widgets.rs index 47181ce..abd39de 100644 --- a/src/ui/widgets.rs +++ b/src/ui/widgets.rs @@ -10,7 +10,7 @@ use winit::{ keyboard::{Key, ModifiersState, NamedKey}, }; -use crate::{app::EventQueue, draw_buffer::DrawBuffer}; +use crate::{app::EventQueue, draw_buffer::DrawBuffer, ui::pages::PageResponse}; pub(crate) trait Widget { type Response; @@ -47,7 +47,7 @@ impl WidgetResponse { } } - pub fn next_widget(value: usize) -> Self { + pub fn next_widget(value: u8) -> Self { Self { standard: StandardResponse::SwitchFocus(value), extra: None, @@ -58,7 +58,7 @@ impl WidgetResponse { // SwitchFocus also has to request a redraw #[derive(Debug, Default)] pub enum StandardResponse { - SwitchFocus(usize), + SwitchFocus(u8), RequestRedraw, // GlobalEvent(GlobalEvent), #[default] @@ -74,14 +74,27 @@ impl From for WidgetResponse { } } +impl StandardResponse { + pub fn to_page_resp(self, selected: &mut u8) -> PageResponse { + match self { + StandardResponse::SwitchFocus(s) => { + *selected = s; + PageResponse::RequestRedraw + } + StandardResponse::RequestRedraw => PageResponse::RequestRedraw, + StandardResponse::None => PageResponse::None, + } + } +} + #[derive(Debug, Default)] pub struct NextWidget { - pub left: Option, - pub right: Option, - pub up: Option, - pub down: Option, - pub tab: Option, - pub shift_tab: Option, + pub left: Option, + pub right: Option, + pub up: Option, + pub down: Option, + pub tab: Option, + pub shift_tab: Option, } impl NextWidget { @@ -97,10 +110,10 @@ impl NextWidget { #[expect( non_local_definitions, - reason = "this is only valid with these specific Option not in general" + reason = "this is only valid with these specific Option not in general" )] - impl From> for WidgetResponse { - fn from(value: Option) -> Self { + impl From> for WidgetResponse { + fn from(value: Option) -> Self { Self { standard: match value { Some(num) => StandardResponse::SwitchFocus(num), From dbfa677e16e976af7b426c5b8fa348c6caf71de4 Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Sun, 7 Sep 2025 16:34:48 +0200 Subject: [PATCH 04/19] add accessability to widgets and pages --- src/app.rs | 11 +++- src/ui/dialog/confirm.rs | 21 ++++++- src/ui/dialog/slider_dialog.rs | 10 +++- src/ui/pages.rs | 14 +++++ src/ui/pages/help_page.rs | 8 +++ src/ui/pages/order_list.rs | 20 +++++++ src/ui/pages/pattern.rs | 8 +++ src/ui/pages/sample_list.rs | 8 +++ src/ui/pages/song_directory_config_page.rs | 68 +++++++++++++++++++--- src/ui/widgets.rs | 3 + src/ui/widgets/button.rs | 21 ++++++- src/ui/widgets/slider.rs | 18 ++++++ src/ui/widgets/text_in.rs | 33 +++++++++++ src/ui/widgets/text_in_scroll.rs | 5 ++ src/ui/widgets/toggle.rs | 5 ++ src/ui/widgets/toggle_button.rs | 7 ++- 16 files changed, 244 insertions(+), 16 deletions(-) diff --git a/src/app.rs b/src/app.rs index d6dbfac..030fde3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -668,21 +668,26 @@ impl App { }; let mut root_node = Node::new(Role::Window); let mut nodes = Vec::new(); - let mut focused = None; + root_node.set_label("Torque Tracker"); + root_node.set_language("English"); let header_id = header.build_tree(&mut nodes); root_node.push_child(header_id); + let resp = pages.build_tree(&mut nodes); + let mut focused = resp.selected; + root_node.push_child(resp.root); + if let Some(dialog) = dialogs.active_dialog() { let resp = dialog.build_tree(&mut nodes); root_node.push_child(resp.root); - focused = Some(resp.selected); + focused = resp.selected; } nodes.push((ROOT_ID, root_node)); TreeUpdate { nodes, tree: Some(tree), - focus: focused.unwrap_or(ROOT_ID), + focus: focused, } } } diff --git a/src/ui/dialog/confirm.rs b/src/ui/dialog/confirm.rs index e0fb77b..04c6609 100644 --- a/src/ui/dialog/confirm.rs +++ b/src/ui/dialog/confirm.rs @@ -20,10 +20,12 @@ pub struct ConfirmDialog { } impl ConfirmDialog { + const DIALOG_ID: u64 = 600_000_000; + const OK_RECT: CharRect = CharRect::new(29, 31, 41, 50); const CANCEL_RECT: CharRect = CharRect::new(29, 31, 30, 39); - const OK: u8 = 0; - const CANCEL: u8 = 1; + const OK: u8 = 1; + const CANCEL: u8 = 2; pub fn new( text: &'static str, ok_event: fn() -> Option, @@ -46,6 +48,8 @@ impl ConfirmDialog { ..Default::default() }, ok_event, + #[cfg(feature = "accesskit")] + accesskit::NodeId(Self::DIALOG_ID + u64::from(Self::OK) * 20), ), cancel: Button::new( "Cancel", @@ -58,6 +62,8 @@ impl ConfirmDialog { ..Default::default() }, cancel_event, + #[cfg(feature = "accesskit")] + accesskit::NodeId(Self::DIALOG_ID + u64::from(Self::CANCEL) * 20), ), rect: CharRect::new(25, 32, 40 - per_side, 40 + per_side), } @@ -111,6 +117,15 @@ impl Dialog for ConfirmDialog { &self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, ) -> crate::app::AccessResponse { - todo!() + use accesskit::{Node, Role}; + + use crate::app::AccessResponse; + + let mut root_node = Node::new(Role::Dialog); + + AccessResponse { + root: todo!(), + selected: todo!(), + } } } diff --git a/src/ui/dialog/slider_dialog.rs b/src/ui/dialog/slider_dialog.rs index 9cf890b..077f62d 100644 --- a/src/ui/dialog/slider_dialog.rs +++ b/src/ui/dialog/slider_dialog.rs @@ -70,12 +70,20 @@ impl Dialog for SliderDialog { } impl SliderDialog { + const NODE_ID: u64 = 400_000_000; pub fn new( inital_char: char, range: RangeInclusive, return_event: fn(i16) -> GlobalEvent, ) -> Self { - let mut text_in = TextIn::new(CharPosition::new(45, 26), 3, NextWidget::default(), |_| {}); + let mut text_in = TextIn::new( + CharPosition::new(45, 26), + 3, + NextWidget::default(), + |_| {}, + #[cfg(feature = "accesskit")] + (accesskit::NodeId(Self::NODE_ID + 20), "value"), + ); text_in.set_string(inital_char.to_string()).unwrap(); Self { text: text_in, diff --git a/src/ui/pages.rs b/src/ui/pages.rs index 9a2a098..c97c576 100644 --- a/src/ui/pages.rs +++ b/src/ui/pages.rs @@ -33,6 +33,12 @@ pub trait Page { // please give me reborrowing for custom structs rustc :3 events: &mut EventQueue<'_>, ) -> PageResponse; + + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::app::AccessResponse; } pub enum PageResponse { @@ -230,4 +236,12 @@ impl AllPages { PageResponse::None } } + + #[cfg(feature = "accesskit")] + pub fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::app::AccessResponse { + self.get_page().build_tree(tree) + } } diff --git a/src/ui/pages/help_page.rs b/src/ui/pages/help_page.rs index 195c56f..34b69f7 100644 --- a/src/ui/pages/help_page.rs +++ b/src/ui/pages/help_page.rs @@ -19,6 +19,14 @@ impl Page for HelpPage { ) -> PageResponse { PageResponse::None } + + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::app::AccessResponse { + todo!() + } } impl HelpPage { diff --git a/src/ui/pages/order_list.rs b/src/ui/pages/order_list.rs index 441a1f7..1a181e9 100644 --- a/src/ui/pages/order_list.rs +++ b/src/ui/pages/order_list.rs @@ -55,6 +55,8 @@ pub struct OrderListPage { } impl OrderListPage { + const PAGE_ID: u64 = 11_000_000_000; + pub fn new() -> Self { Self { cursor: Cursor::Order, @@ -85,6 +87,11 @@ impl OrderListPage { let vol = u8::try_from(vol).unwrap(); send_song_op(SongOperation::SetVolume(idx, vol)); }, + #[cfg(feature = "accesskit")] + ( + accesskit::NodeId(Self::PAGE_ID + u64::try_from(idx).unwrap()), + format!("Volume Channel {idx}").into_boxed_str(), + ), ) }), pan: array::from_fn(|idx| { @@ -110,6 +117,11 @@ impl OrderListPage { let pan = Pan::Value(u8::try_from(pan).unwrap()); send_song_op(SongOperation::SetPan(idx, pan)); }, + #[cfg(feature = "accesskit")] + ( + accesskit::NodeId(Self::PAGE_ID + u64::try_from(idx).unwrap()), + format!("Pan Channel {idx}").into_boxed_str(), + ), ) }), } @@ -543,4 +555,12 @@ impl Page for OrderListPage { } PageResponse::None } + + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::app::AccessResponse { + todo!() + } } diff --git a/src/ui/pages/pattern.rs b/src/ui/pages/pattern.rs index 3c883ad..ebe2304 100644 --- a/src/ui/pages/pattern.rs +++ b/src/ui/pages/pattern.rs @@ -649,4 +649,12 @@ impl Page for PatternPage { PageResponse::None } + + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::app::AccessResponse { + todo!() + } } diff --git a/src/ui/pages/sample_list.rs b/src/ui/pages/sample_list.rs index a9102e8..8552b1d 100644 --- a/src/ui/pages/sample_list.rs +++ b/src/ui/pages/sample_list.rs @@ -363,4 +363,12 @@ impl Page for SampleList { PageResponse::None } + + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::app::AccessResponse { + todo!() + } } diff --git a/src/ui/pages/song_directory_config_page.rs b/src/ui/pages/song_directory_config_page.rs index 29b6c44..48a0474 100644 --- a/src/ui/pages/song_directory_config_page.rs +++ b/src/ui/pages/song_directory_config_page.rs @@ -6,9 +6,7 @@ use crate::{ app::{EventQueue, GlobalEvent, send_song_op}, coordinates::{CharPosition, CharRect}, draw_buffer::DrawBuffer, - ui::widgets::{ - NextWidget, StandardResponse, Widget, WidgetResponse, slider::Slider, text_in::TextIn, - }, + ui::widgets::{NextWidget, Widget, slider::Slider, text_in::TextIn}, }; use super::{Page, PageResponse}; @@ -178,13 +176,49 @@ impl Page for SongDirectoryConfigPage { resp.standard.to_page_resp(&mut self.selected) } + + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::app::AccessResponse { + use accesskit::{Node, NodeId, Role}; + + use crate::app::AccessResponse; + + let mut root_node = Node::new(Role::Menu); + let nodes = [ + NodeId(Self::SONG_NAME_ID), + NodeId(Self::INITIAL_TEMPO_ID), + NodeId(Self::INITIAL_SPEED_ID), + NodeId(Self::GLOBAL_VOLUME_ID), + ]; + root_node.set_children(nodes); + root_node.set_label("Song Directory Config Page"); + + self.song_name.build_tree(tree); + self.initial_tempo.build_tree(tree); + self.initial_speed.build_tree(tree); + self.global_volume.build_tree(tree); + + tree.push((NodeId(Self::PAGE_ID), root_node)); + AccessResponse { + root: NodeId(Self::PAGE_ID), + selected: nodes[usize::from(self.selected - 1)], + } + } } impl SongDirectoryConfigPage { - const SONG_NAME: u8 = 0; - const INITIAL_TEMPO: u8 = 1; - const INITIAL_SPEED: u8 = 2; - const GLOBAL_VOLUME: u8 = 3; + const PAGE_ID: u64 = 12_000_000_000; + const SONG_NAME: u8 = 1; + const SONG_NAME_ID: u64 = Self::PAGE_ID + Self::SONG_NAME as u64 * 20; + const INITIAL_TEMPO: u8 = 2; + const INITIAL_TEMPO_ID: u64 = Self::PAGE_ID + Self::INITIAL_TEMPO as u64 * 20; + const INITIAL_SPEED: u8 = 3; + const INITIAL_SPEED_ID: u64 = Self::PAGE_ID + Self::INITIAL_SPEED as u64 * 20; + const GLOBAL_VOLUME: u8 = 4; + const GLOBAL_VOLUME_ID: u64 = Self::PAGE_ID + Self::GLOBAL_VOLUME as u64 * 20; pub fn ui_change(&mut self, change: SDCChange) -> PageResponse { match change { @@ -225,6 +259,11 @@ impl SongDirectoryConfigPage { ..Default::default() }, |s| println!("new song name: {}", s), + #[cfg(feature = "accesskit")] + ( + accesskit::NodeId(Self::PAGE_ID + u64::from(Self::SONG_NAME) * 20), + "Song Name", + ), ); let initial_tempo = Slider::new( 125, @@ -243,6 +282,11 @@ impl SongDirectoryConfigPage { NonZero::new(u8::try_from(value).unwrap()).unwrap(), )); }, + #[cfg(feature = "accesskit")] + ( + accesskit::NodeId(Self::PAGE_ID + u64::from(Self::INITIAL_TEMPO) * 20), + "Initial Tempo".into(), + ), ); let initial_speed = Slider::new( 6, @@ -261,6 +305,11 @@ impl SongDirectoryConfigPage { NonZero::new(u8::try_from(value).unwrap()).unwrap(), )); }, + #[cfg(feature = "accesskit")] + ( + accesskit::NodeId(Self::PAGE_ID + u64::from(Self::INITIAL_SPEED) * 20), + "Initial Speed".into(), + ), ); let global_volume = Slider::new( 128, @@ -275,6 +324,11 @@ impl SongDirectoryConfigPage { }, |n| GlobalEvent::Page(super::PageEvent::Sdc(SDCChange::GlobalVolume(n))), |value| send_song_op(SongOperation::SetGlobalVol(u8::try_from(value).unwrap())), + #[cfg(feature = "accesskit")] + ( + accesskit::NodeId(Self::PAGE_ID + u64::from(Self::GLOBAL_VOLUME) * 20), + "Global Volume".into(), + ), ); // let mixing_volume = Slider::new( // 48, diff --git a/src/ui/widgets.rs b/src/ui/widgets.rs index abd39de..809bfc6 100644 --- a/src/ui/widgets.rs +++ b/src/ui/widgets.rs @@ -22,6 +22,9 @@ pub(crate) trait Widget { key_event: &KeyEvent, events: &mut EventQueue<'_>, ) -> WidgetResponse; + + #[cfg(feature = "accesskit")] + fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>); } #[derive(Debug)] diff --git a/src/ui/widgets/button.rs b/src/ui/widgets/button.rs index 53deb40..bff32b3 100644 --- a/src/ui/widgets/button.rs +++ b/src/ui/widgets/button.rs @@ -14,6 +14,8 @@ pub struct Button { pressed: bool, next_widget: NextWidget, callback: fn() -> R, + #[cfg(feature = "accesskit")] + node_id: accesskit::NodeId, } impl Widget for Button { @@ -57,13 +59,28 @@ impl Widget for Button { } WidgetResponse::default() } + + #[cfg(feature = "accesskit")] + fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>) { + use accesskit::{Node, Role}; + + let mut node = Node::new(Role::Button); + node.set_label(self.text); + tree.push((self.node_id, node)); + } } impl Button { const TOPLEFT_COLOR: u8 = 3; const BOTRIGHT_COLOR: u8 = 1; - pub fn new(text: &'static str, rect: CharRect, next_widget: NextWidget, cb: fn() -> R) -> Self { + pub fn new( + text: &'static str, + rect: CharRect, + next_widget: NextWidget, + cb: fn() -> R, + #[cfg(feature = "accesskit")] node_id: accesskit::NodeId, + ) -> Self { // is 3 rows high, because bot and top are inclusive assert!( rect.bot() - rect.top() >= 2, @@ -75,6 +92,8 @@ impl Button { callback: cb, pressed: false, next_widget, + #[cfg(feature = "accesskit")] + node_id, } } diff --git a/src/ui/widgets/slider.rs b/src/ui/widgets/slider.rs index ea9a06f..625ebc7 100644 --- a/src/ui/widgets/slider.rs +++ b/src/ui/widgets/slider.rs @@ -111,6 +111,8 @@ pub struct Slider { next_widget: NextWidget, dialog_return: fn(i16) -> GlobalEvent, callback: Box R + Send>, + #[cfg(feature = "accesskit")] + access: (accesskit::NodeId, Box), } impl Widget for Slider { @@ -244,6 +246,19 @@ impl Widget for Slider { WidgetResponse::default() } + + #[cfg(feature = "accesskit")] + fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>) { + use accesskit::{Node, Role}; + + let mut node = Node::new(Role::Slider); + node.set_numeric_value(self.number.inner as f64); + node.set_min_numeric_value(MIN as f64); + node.set_max_numeric_value(MAX as f64); + node.set_label(self.access.1.clone()); + + tree.push((self.access.0, node)); + } } impl Slider { @@ -255,6 +270,7 @@ impl Slider { next_widget: NextWidget, dialog_return: fn(i16) -> GlobalEvent, callback: impl Fn(i16) -> R + Send + 'static, + #[cfg(feature = "accesskit")] access: (accesskit::NodeId, Box), ) -> Self { assert!(MIN <= MAX, "MIN must be less than or equal to MAX"); // panic is fine, because this object only is generated with compile time values @@ -275,6 +291,8 @@ impl Slider { next_widget, dialog_return, callback: Box::new(callback), + #[cfg(feature = "accesskit")] + access, } } diff --git a/src/ui/widgets/text_in.rs b/src/ui/widgets/text_in.rs index d163e39..d6194f5 100644 --- a/src/ui/widgets/text_in.rs +++ b/src/ui/widgets/text_in.rs @@ -20,6 +20,8 @@ pub struct TextIn { next_widget: NextWidget, callback: Box R + Send>, cursor_pos: usize, + #[cfg(feature = "accesskit")] + access: (accesskit::NodeId, &'static str), } impl Widget for TextIn { @@ -125,6 +127,34 @@ impl Widget for TextIn { } WidgetResponse::default() } + + #[cfg(feature = "accesskit")] + fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>) { + use accesskit::{Node, NodeId, Role, TextDirection, TextPosition, TextSelection}; + + let mut root_node = Node::new(Role::TextInput); + root_node.set_label(self.access.1); + let mut text_node = Node::new(Role::TextRun); + let text_node_id = NodeId(self.access.0.0 + 1); + text_node.set_text_direction(TextDirection::LeftToRight); + text_node + .set_character_lengths(std::iter::repeat_n(1, self.text.len()).collect::>()); + text_node.set_value(self.text.as_str()); + text_node.set_text_selection(TextSelection { + anchor: TextPosition { + node: text_node_id, + character_index: self.cursor_pos, + }, + focus: TextPosition { + node: text_node_id, + character_index: self.cursor_pos + 1, + }, + }); + root_node.push_child(text_node_id); + + tree.push((self.access.0, root_node)); + tree.push((text_node_id, text_node)); + } } impl TextIn { @@ -133,6 +163,7 @@ impl TextIn { width: usize, next_widget: NextWidget, cb: impl Fn(&str) -> R + Send + 'static, + #[cfg(feature = "accesskit")] access: (accesskit::NodeId, &'static str), ) -> Self { assert!(pos.x() + width < WINDOW_SIZE.0); // right and left keys are used in the widget itself. doeesnt make sense to put NextWidget there @@ -146,6 +177,8 @@ impl TextIn { next_widget, callback: Box::new(cb), cursor_pos: 0, + #[cfg(feature = "accesskit")] + access, } } diff --git a/src/ui/widgets/text_in_scroll.rs b/src/ui/widgets/text_in_scroll.rs index 7b76d8a..26b6f22 100644 --- a/src/ui/widgets/text_in_scroll.rs +++ b/src/ui/widgets/text_in_scroll.rs @@ -132,6 +132,11 @@ impl Widget for TextInScroll { } WidgetResponse::default() } + + #[cfg(feature = "accesskit")] + fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>) { + todo!() // probably very similar / the same as the regular text in + } } impl TextInScroll { diff --git a/src/ui/widgets/toggle.rs b/src/ui/widgets/toggle.rs index ccf5744..3e06bdc 100644 --- a/src/ui/widgets/toggle.rs +++ b/src/ui/widgets/toggle.rs @@ -56,6 +56,11 @@ impl Widget for Toggle { self.next_widget.process_key_event(key_event, modifiers) } } + + #[cfg(feature = "accesskit")] + fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>) { + todo!() + } } impl Toggle { diff --git a/src/ui/widgets/toggle_button.rs b/src/ui/widgets/toggle_button.rs index 548171a..9a5ae38 100644 --- a/src/ui/widgets/toggle_button.rs +++ b/src/ui/widgets/toggle_button.rs @@ -34,6 +34,11 @@ impl Widget for ToggleButton { }); WidgetResponse { standard, extra } } + + #[cfg(feature = "accesskit")] + fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>) { + todo!() + } } impl ToggleButton { @@ -45,7 +50,7 @@ impl ToggleButton { state: Rc>, cb: fn(T) -> R, ) -> Self { - let button = Button::new(text, rect, next_widget, || ()); + let button = Button::new(text, rect, next_widget, || (), todo!()); Self { button, variant, From 2dad7274a0429f05fbd156bfff0bc6b73c04cd41 Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Sat, 6 Sep 2025 20:00:58 +0200 Subject: [PATCH 05/19] add positions to main menu. add accessability to Slider Dialog --- src/app.rs | 27 +++++++++++++++++++++------ src/coordinates.rs | 12 ++++++++++++ src/gpu.rs | 4 ++++ src/render.rs | 11 +++++++++++ src/ui/dialog/page_menu.rs | 3 ++- src/ui/dialog/slider_dialog.rs | 24 +++++++++++++++++++++++- 6 files changed, 73 insertions(+), 8 deletions(-) diff --git a/src/app.rs b/src/app.rs index 030fde3..998b85c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,6 +14,8 @@ use torque_tracker_engine::{ project::song::{Song, SongOperation}, }; use triple_buffer::triple_buffer; +#[cfg(feature = "accesskit")] +use winit::dpi::PhysicalSize; use winit::{ application::ApplicationHandler, event::{Modifiers, WindowEvent}, @@ -186,7 +188,7 @@ struct Window { window: Arc, render_backend: RenderBackend, #[cfg(feature = "accesskit")] - adapter: accesskit_winit::Adapter, + adapter: (accesskit_winit::Adapter, accesskit::Affine), } pub struct App { @@ -300,7 +302,7 @@ impl ApplicationHandler for App { let window = window.as_ref(); #[cfg(feature = "accesskit")] - adapter.process_event(window, &event); + adapter.0.process_event(window, &event); // limit the pages and widgets to only push events and not read or pop let event_queue = &mut EventQueue(event_queue); @@ -326,8 +328,9 @@ impl ApplicationHandler for App { ui_pages.draw(draw_buffer); dialog_manager.draw(draw_buffer); #[cfg(feature = "accesskit")] - adapter - .update_if_active(|| Self::produce_full_tree(ui_pages, header, dialog_manager)); + adapter.0.update_if_active(|| { + Self::produce_full_tree(ui_pages, header, dialog_manager, adapter.1) + }); // notify the windowing system that drawing is done and the new buffer is about to be pushed window.pre_present_notify(); // push the framebuffer into GPU/softbuffer and render it onto the screen @@ -461,11 +464,12 @@ impl ApplicationHandler for App { // there probably should always be a window // make sure we always respond to this event. I hope it can't be send when there is no window let window = self.window.as_mut().unwrap(); - window.adapter.update_if_active(|| { + window.adapter.0.update_if_active(|| { Self::produce_full_tree( &self.ui_pages, &self.header, &self.dialog_manager, + window.adapter.1, ) }); } @@ -548,11 +552,12 @@ impl App { }; window.set_visible(true); + let size = render_backend.get_size(); Window { window, render_backend, #[cfg(feature = "accesskit")] - adapter, + adapter: (adapter, create_transform(size)), } }); } @@ -656,6 +661,7 @@ impl App { pages: &AllPages, header: &Header, dialogs: &DialogManager, + transform: accesskit::Affine, ) -> accesskit::TreeUpdate { use accesskit::TreeUpdate; use accesskit::{Node, NodeId, Role, Tree}; @@ -672,6 +678,7 @@ impl App { root_node.set_language("English"); let header_id = header.build_tree(&mut nodes); root_node.push_child(header_id); + root_node.set_transform(transform); let resp = pages.build_tree(&mut nodes); let mut focused = resp.selected; @@ -719,3 +726,11 @@ pub struct AccessResponse { pub root: accesskit::NodeId, pub selected: accesskit::NodeId, } + +#[cfg(feature = "accesskit")] +fn create_transform(size: winit::dpi::PhysicalSize) -> accesskit::Affine { + accesskit::Affine::scale_non_uniform( + size.width as f64 / crate::coordinates::WINDOW_SIZE_CHARS.0 as f64, + size.height as f64 / crate::coordinates::WINDOW_SIZE_CHARS.1 as f64, + ) +} diff --git a/src/coordinates.rs b/src/coordinates.rs index 6468eef..4da17c6 100644 --- a/src/coordinates.rs +++ b/src/coordinates.rs @@ -80,6 +80,18 @@ impl From for CharRect { } } +#[cfg(feature = "accesskit")] +impl From for accesskit::Rect { + fn from(value: CharRect) -> Self { + accesskit::Rect { + x0: value.left as f64, + y0: value.top as f64, + x1: value.right as f64, + y1: value.bot as f64, + } + } +} + /// PixelRect as well as CharRect uses all values inclusive, meaning the borders are included #[derive(Debug, Clone, Copy)] pub struct PixelRect { diff --git a/src/gpu.rs b/src/gpu.rs index f18f51c..1092863 100644 --- a/src/gpu.rs +++ b/src/gpu.rs @@ -391,4 +391,8 @@ impl GPUState { Ok(()) } + + pub fn size(&self) -> winit::dpi::PhysicalSize { + self.size + } } diff --git a/src/render.rs b/src/render.rs index cdddb85..e1a1f60 100644 --- a/src/render.rs +++ b/src/render.rs @@ -43,6 +43,10 @@ impl RenderBackend { Err(e) => eprint!("{:?}", e), } } + + pub fn get_size(&self) -> PhysicalSize { + self.backend.size() + } } #[cfg(feature = "soft_scaling")] @@ -100,4 +104,11 @@ impl RenderBackend { } buffer.present().unwrap(); } + + pub fn get_size(&self) -> PhysicalSize { + PhysicalSize { + width: self.width, + height: self.height, + } + } } diff --git a/src/ui/dialog/page_menu.rs b/src/ui/dialog/page_menu.rs index bc91398..e7588a6 100644 --- a/src/ui/dialog/page_menu.rs +++ b/src/ui/dialog/page_menu.rs @@ -163,11 +163,12 @@ impl Dialog for PageMenu { &self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, ) -> crate::app::AccessResponse { - use accesskit::{Action, Node, NodeId, Role}; + use accesskit::{Action, Node, NodeId, Rect, Role}; use crate::app::AccessResponse; let mut root = Node::new(Role::Dialog); root.set_label(self.name); + root.set_bounds(dbg!(Rect::from(self.rect))); let mut selected = NodeId(u64::from(self.selected) + self.node_id.0 + 1); // root of the sub_menu. Will be set as the child of the selected button diff --git a/src/ui/dialog/slider_dialog.rs b/src/ui/dialog/slider_dialog.rs index 077f62d..b7debb1 100644 --- a/src/ui/dialog/slider_dialog.rs +++ b/src/ui/dialog/slider_dialog.rs @@ -65,7 +65,29 @@ impl Dialog for SliderDialog { &self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, ) -> crate::app::AccessResponse { - todo!() + use accesskit::{Node, NodeId, Role}; + + use crate::app::AccessResponse; + + const ROOT_ID: NodeId = NodeId(400_000_000); + const TEXT_ID: NodeId = NodeId(400_000_001); + + let mut root_node = Node::new(Role::Dialog); + root_node.set_label("Slider Dialog"); + root_node.push_child(TEXT_ID); + + let mut text_node = Node::new(Role::NumberInput); + text_node.set_min_numeric_value(*self.range.start() as f64); + text_node.set_max_numeric_value(*self.range.end() as f64); + text_node.set_value(self.text.get_str()); + text_node.set_label("Set Value"); + + tree.push((ROOT_ID, root_node)); + tree.push((TEXT_ID, text_node)); + AccessResponse { + root: ROOT_ID, + selected: TEXT_ID, + } } } From 7da210e48b93b8d4ece1e056775ef65343b961f8 Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Tue, 9 Sep 2025 20:59:01 +0200 Subject: [PATCH 06/19] almost fix the text_in access --- src/ui/widgets/text_in.rs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/ui/widgets/text_in.rs b/src/ui/widgets/text_in.rs index d6194f5..ea8857c 100644 --- a/src/ui/widgets/text_in.rs +++ b/src/ui/widgets/text_in.rs @@ -130,6 +130,8 @@ impl Widget for TextIn { #[cfg(feature = "accesskit")] fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>) { + use std::iter::repeat_n; + use accesskit::{Node, NodeId, Role, TextDirection, TextPosition, TextSelection}; let mut root_node = Node::new(Role::TextInput); @@ -137,17 +139,30 @@ impl Widget for TextIn { let mut text_node = Node::new(Role::TextRun); let text_node_id = NodeId(self.access.0.0 + 1); text_node.set_text_direction(TextDirection::LeftToRight); - text_node - .set_character_lengths(std::iter::repeat_n(1, self.text.len()).collect::>()); + text_node.set_character_lengths(repeat_n(1, self.text.len()).collect::>()); + // text_node.set_character_widths(repeat_n(1., self.text.len()).collect::>()); + // text_node.set_character_positions( + // (0..self.text.len()) + // .map(|n| n as f32) + // .collect::>(), + // ); text_node.set_value(self.text.as_str()); - text_node.set_text_selection(TextSelection { + // text_node.set_word_lengths( + // self.text + // .as_str() + // .split_whitespace() + // .map(|w| u8::try_from(w.len()).unwrap()) + // .collect::>(), + // ); + root_node.set_text_selection(TextSelection { anchor: TextPosition { node: text_node_id, character_index: self.cursor_pos, }, focus: TextPosition { node: text_node_id, - character_index: self.cursor_pos + 1, + character_index: self.cursor_pos, + // character_index: self.text.as_str().len().min(self.cursor_pos + 1), }, }); root_node.push_child(text_node_id); From 776a6d05a080355ccaff12b2afa475ec497b9a0d Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Tue, 30 Sep 2025 18:21:01 +0200 Subject: [PATCH 07/19] allow fallback scaling backend --- Cargo.toml | 9 ++++--- src/app.rs | 8 +++++- src/gpu.rs | 29 ++++++++++++--------- src/palettes.rs | 1 + src/render.rs | 67 +++++++++++++++++++++++++++++++++++++++++-------- 5 files changed, 88 insertions(+), 26 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8f60767..3dc3d5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,15 +27,18 @@ accesskit_winit = { version = "0.29.0", optional = true } accesskit = { version = "0.21.0", optional = true } [features] -# needs to be one, but not both -# less artifacts than software scaling. also probably faster +# at least one of {gpu/soft}_scaling needs to be enabled. If both are active gpu_scaling is prefered and +# softwave scaling is only used if the gpu initialisation fails. + +# gpu_scaling: less artifacts than software scaling. also probably faster gpu_scaling = ["dep:wgpu"] soft_scaling = ["dep:softbuffer"] + # accessability isn't an optional feature of course, but i want to make this progream compatible with embedded at some point. # To not make this harder than necessary it is done like this accesskit = ["dep:accesskit_winit", "dep:accesskit"] -default = ["gpu_scaling", "accesskit"] +default = ["soft_scaling", "gpu_scaling", "accesskit"] [lints.clippy] uninlined_format_args = "allow" diff --git a/src/app.rs b/src/app.rs index 998b85c..9bd72d6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -34,9 +34,15 @@ use crate::{ ui::pages::{order_list::OrderListPageEvent, pattern::PatternPageEvent}, }; +#[cfg(all(feature = "gpu_scaling", feature = "soft_scaling"))] +use super::render::BothRenderBackend as RenderBackend; +#[cfg(all(feature = "gpu_scaling", not(feature = "soft_scaling")))] +use super::render::GPURenderBackend as RenderBackend; +#[cfg(all(not(feature = "gpu_scaling"), feature = "soft_scaling"))] +use super::render::SoftRenderBackend as RenderBackend; + use super::{ draw_buffer::DrawBuffer, - render::RenderBackend, ui::{ dialog::{ Dialog, DialogManager, DialogResponse, confirm::ConfirmDialog, page_menu::PageMenu, diff --git a/src/gpu.rs b/src/gpu.rs index 1092863..e910877 100644 --- a/src/gpu.rs +++ b/src/gpu.rs @@ -46,11 +46,13 @@ impl GPUState { depth_or_array_layers: 1, }; - pub async fn new(window: std::sync::Arc) -> Self { + // this should never panic to allow using the software scaling if it's enabled + pub async fn new(window: std::sync::Arc) -> Result { let size = window.inner_size(); let instance = Instance::default(); - let surface: Surface<'static> = instance.create_surface(window).unwrap(); + let surface: Surface<'static> = + instance.create_surface(window).map_err(|e| e.to_string())?; let adapter = instance .request_adapter(&RequestAdapterOptions { @@ -61,7 +63,7 @@ impl GPUState { force_fallback_adapter: false, }) .await - .unwrap(); + .map_err(|e| e.to_string())?; let (device, queue) = adapter .request_device(&DeviceDescriptor { @@ -74,19 +76,22 @@ impl GPUState { trace: wgpu::Trace::Off, }) .await - .unwrap(); + .map_err(|e| e.to_string())?; let surface_caps = surface.get_capabilities(&adapter); - let surface_format = *surface_caps - .formats - .iter() - .find(|f| !f.is_srgb()) - .unwrap_or(&surface_caps.formats[0]); + let surface_format = { + let surface_opt = surface_caps.formats.iter().find(|f| !f.is_srgb()); + match (surface_opt, surface_caps.formats.first()) { + (Some(f), _) => f, + (None, Some(f)) => f, + (None, None) => return Err(String::from("No surface capabilites available")), + } + }; let config = SurfaceConfiguration { usage: TextureUsages::RENDER_ATTACHMENT, - format: surface_format, + format: *surface_format, width: size.width, height: size.height, present_mode: surface_caps.present_modes[0], @@ -267,7 +272,7 @@ impl GPUState { cache: None, }); - Self { + Ok(Self { surface, device, queue, @@ -277,7 +282,7 @@ impl GPUState { diffuse_bind_group, streaming_texture: window_texture, color_map, - } + }) } /// on next render the new palette will be used diff --git a/src/palettes.rs b/src/palettes.rs index 3cd874d..c7c8d12 100644 --- a/src/palettes.rs +++ b/src/palettes.rs @@ -57,6 +57,7 @@ impl From for RGBA8 { } #[repr(transparent)] +#[derive(Clone, Copy)] pub struct Palette(pub [Color; PALETTE_SIZE]); // only needed, because its easier to set u8 color values than u10 by hand, so i store them in u8 and convert to RGB10A2 diff --git a/src/render.rs b/src/render.rs index e1a1f60..5e62042 100644 --- a/src/render.rs +++ b/src/render.rs @@ -7,24 +7,71 @@ use crate::{ palettes::{Palette, RGB8}, }; -#[cfg(all(feature = "gpu_scaling", feature = "soft_scaling"))] -compile_error!("it's impossible to have both gpu and software scaling enabled"); - #[cfg(not(any(feature = "gpu_scaling", feature = "soft_scaling")))] compile_error!("at least one of gpu_scaling or soft_scaling needs to be active"); +#[cfg(all(feature = "gpu_scaling", feature = "soft_scaling"))] +pub enum BothRenderBackend { + GPU(GPURenderBackend), + Soft(SoftRenderBackend), +} + +#[cfg(all(feature = "gpu_scaling", feature = "soft_scaling"))] +impl BothRenderBackend { + pub fn new(window: Arc, palette: Palette) -> Self { + match GPURenderBackend::try_new(window.clone(), palette) { + Ok(b) => Self::GPU(b), + Err(e) => { + eprintln!( + "GPU render backend creation failed: {e}.\n\nFalling back to software scaling" + ); + Self::Soft(SoftRenderBackend::new(window, palette)) + } + } + } + + pub fn resize(&mut self, size: PhysicalSize) { + match self { + BothRenderBackend::GPU(b) => b.resize(size), + BothRenderBackend::Soft(b) => b.resize(size), + } + } + + pub fn render( + &mut self, + frame_buffer: &[[u8; WINDOW_SIZE.0]; WINDOW_SIZE.1], + event_loop: &ActiveEventLoop, + ) { + match self { + BothRenderBackend::GPU(b) => b.render(frame_buffer, event_loop), + BothRenderBackend::Soft(b) => b.render(frame_buffer, event_loop), + } + } + + pub fn get_size(&self) -> PhysicalSize { + match self { + BothRenderBackend::GPU(b) => b.get_size(), + BothRenderBackend::Soft(b) => b.get_size(), + } + } +} + #[cfg(feature = "gpu_scaling")] -pub struct RenderBackend { +pub struct GPURenderBackend { backend: crate::gpu::GPUState, } #[cfg(feature = "gpu_scaling")] -impl RenderBackend { - pub fn new(window: Arc, palette: Palette) -> Self { - let mut backend = smol::block_on(crate::gpu::GPUState::new(window)); +impl GPURenderBackend { + fn try_new(window: Arc, palette: Palette) -> Result { + let mut backend = smol::block_on(crate::gpu::GPUState::new(window))?; backend.queue_palette_update(palette.into()); - Self { backend } + Ok(Self { backend }) + } + + pub fn new(window: Arc, palette: Palette) -> Self { + Self::try_new(window, palette).unwrap() } pub fn resize(&mut self, size: PhysicalSize) { @@ -50,7 +97,7 @@ impl RenderBackend { } #[cfg(feature = "soft_scaling")] -pub struct RenderBackend { +pub struct SoftRenderBackend { backend: softbuffer::Surface, Arc>, width: u32, height: u32, @@ -58,7 +105,7 @@ pub struct RenderBackend { } #[cfg(feature = "soft_scaling")] -impl RenderBackend { +impl SoftRenderBackend { pub fn new(window: Arc, palette: Palette) -> Self { let size = window.inner_size(); let context = softbuffer::Context::new(window.clone()).unwrap(); From efac889bef834a51355397e869d3aafe2d9d567e Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Tue, 30 Sep 2025 21:32:48 +0200 Subject: [PATCH 08/19] also send stream timestamp through the engine channel --- Cargo.lock | 22 ++++++++------------ Cargo.toml | 7 ++++++- src/app.rs | 61 ++++++++++++++++++++++++++++++++---------------------- 3 files changed, 51 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 813b61c..ccb58a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2415,6 +2415,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rt-write-lock" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79ce7f6e4b55e98feea9066f403ac0866c1d16d602d7522367151771d2800a15" + [[package]] name = "rtrb" version = "0.3.2" @@ -2581,9 +2587,9 @@ dependencies = [ [[package]] name = "simple-left-right" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d14c606977d5f05c85d6c9d30da1d49567dd92c16e0bc693644659bcfbe8b05" +checksum = "a3a8aae2755857e854bf0aaa2024e240c39c75228789afe556f552a52dbb9d74" [[package]] name = "slab" @@ -3026,7 +3032,6 @@ dependencies = [ "softbuffer", "symphonia", "torque-tracker-engine", - "triple_buffer", "wgpu", "winit", ] @@ -3038,10 +3043,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809ceef6db8d2bd9bf2a14b327efe8fd04d16827e3675a035f70d00503c2ae4f" dependencies = [ "dasp", + "rt-write-lock", "rtrb", "rtsan-standalone", "simple-left-right", - "triple_buffer", ] [[package]] @@ -3075,15 +3080,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "triple_buffer" -version = "8.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420466259f9fa5decc654c490b9ab538400e5420df8237f84ecbe20368bcf72b" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "ttf-parser" version = "0.25.1" diff --git a/Cargo.toml b/Cargo.toml index 3dc3d5c..b7938d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,17 +11,22 @@ categories = ["multimedia::audio"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +# my font font8x8 = "0.3.1" wgpu = { version = "26.0.0", optional = true } +# ascii strings ascii = "1.1.0" winit = "0.30.11" torque-tracker-engine = "0.1.0" smol = "2.0.2" +# macro shit paste = "1.0.15" +# audio output cpal = "0.16.0" -triple_buffer = "8.1.1" softbuffer = { version="0.4.6", optional = true } +# cross platform file dialogs rfd = "0.15.4" +# audio file loading symphonia = "0.5.4" accesskit_winit = { version = "0.29.0", optional = true } accesskit = { version = "0.21.0", optional = true } diff --git a/src/app.rs b/src/app.rs index 9bd72d6..9b81b1f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -13,7 +13,6 @@ use torque_tracker_engine::{ manager::{AudioManager, OutputConfig, PlaybackSettings, ToWorkerMsg}, project::song::{Song, SongOperation}, }; -use triple_buffer::triple_buffer; #[cfg(feature = "accesskit")] use winit::dpi::PhysicalSize; use winit::{ @@ -54,8 +53,6 @@ use super::{ pub static EXECUTOR: smol::Executor = smol::Executor::new(); /// Song data -/// -/// Be careful about locking order with AUDIO_OUTPUT_COMMS to not deadlock pub static SONG_MANAGER: LazyLock> = LazyLock::new(|| Mutex::new(AudioManager::new(Song::default()))); /// Sender for Song changes @@ -207,7 +204,6 @@ pub struct App { header: Header, event_loop_proxy: EventLoopProxy, worker_threads: Option, - // needed here because it isn't send. This Option should be synchronized with AUDIO_OUTPUT_COMMS audio_stream: Option<( cpal::Stream, smol::Task<()>, @@ -570,6 +566,8 @@ impl App { // TODO: make this configurable fn start_audio_stream(&mut self) { + use cpal::StreamInstant; + assert!(self.audio_stream.is_none()); let host = cpal::default_host(); let device = host.default_output_device().unwrap(); @@ -587,21 +585,25 @@ impl App { (config, buffer_size) }; let mut guard = SONG_MANAGER.lock_blocking(); - let (mut worker, buffer_time, status, stream_send) = - guard.get_callback::(OutputConfig { - buffer_size, - channel_count: NonZero::new(config.channels).unwrap(), - sample_rate: NonZero::new(config.sample_rate.0).unwrap(), - }); + let (mut worker, buffer_time, status, stream_send) = guard + .get_callback::( + OutputConfig { + buffer_size, + channel_count: NonZero::new(config.channels).unwrap(), + sample_rate: NonZero::new(config.sample_rate.0).unwrap(), + }, + cpal::OutputStreamTimestamp { + callback: cpal::StreamInstant::new(0, 0), + playback: cpal::StreamInstant::new(0, 0), + }, + ); // keep the guard as short as possible to not block the async threads drop(guard); - let (mut timestamp_send, recv) = triple_buffer(&None); let stream = device .build_output_stream( &config, move |data, info| { - worker(data); - timestamp_send.write(Some(info.timestamp())); + worker(data, info.timestamp()); }, |err| eprintln!("audio stream err: {err:?}"), None, @@ -613,39 +615,48 @@ impl App { let buffer_time = buffer_time; let mut status_recv = status; // maybe also send the timestamp every second or so - let mut timestamp_recv = recv; - let mut old_status: Option = None; - let mut old_timestamp: Option = None; + let mut old_playback = None; + let mut old_timestamp = OutputStreamTimestamp { + callback: StreamInstant::new(0, 0), + playback: StreamInstant::new(0, 0), + }; loop { - let status = *status_recv.get(); + smol::Timer::after(buffer_time).await; + let Some(read) = status_recv.try_get() else { + // we had a lock for way too long, so now we are behing and have to wait for the writer to finish + // writing once. Then locking will succeed again. + continue; + }; + let playback = read.0; + let timestamp = read.1; // only react on status changes. could at some point be made more granular - if status != old_status { - old_status = status; + if playback != old_playback { + old_playback = playback; // println!("playback status: {status:?}"); - let pos = status.map(|s| s.position); + let pos = playback.map(|s| s.position); proxy .send_event(GlobalEvent::Header(HeaderEvent::SetPlayback(pos))) .unwrap(); - let pos = status.map(|s| (s.position.pattern, s.position.row)); + let pos = playback.map(|s| (s.position.pattern, s.position.row)); proxy .send_event(GlobalEvent::Page(PageEvent::Pattern( PatternPageEvent::PlaybackPosition(pos), ))) .unwrap(); // does a map flatten. idk why it's called and_then - let pos = status.and_then(|s| s.position.order); + let pos = playback.and_then(|s| s.position.order); proxy .send_event(GlobalEvent::Page(PageEvent::OrderList( OrderListPageEvent::SetPlayback(pos), ))) .unwrap(); } - let timestamp = *timestamp_recv.read(); + + // also check for changed timestamp if timestamp != old_timestamp { - // TODO: maybe send it somewhere old_timestamp = timestamp; + // maybe at some point use the data } - smol::Timer::after(buffer_time).await; } }); self.audio_stream = Some((stream, task, stream_send)); From e609af81546ebc27018259c5f2655eadf7c457bf Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Thu, 2 Oct 2025 11:03:32 +0200 Subject: [PATCH 09/19] upgrade wgpu --- Cargo.lock | 56 +++++++++++++++++++++++++++++++----------------------- Cargo.toml | 2 +- src/gpu.rs | 1 + 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ccb58a7..fe66901 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -475,9 +475,6 @@ name = "bitflags" version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" -dependencies = [ - "serde", -] [[package]] name = "block" @@ -1086,6 +1083,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "font8x8" version = "0.3.1" @@ -1317,7 +1320,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -1325,6 +1328,9 @@ name = "hashbrown" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +dependencies = [ + "foldhash 0.2.0", +] [[package]] name = "hermit-abi" @@ -1664,9 +1670,9 @@ dependencies = [ [[package]] name = "naga" -version = "26.0.0" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916cbc7cb27db60be930a4e2da243cf4bc39569195f22fd8ee419cd31d5b662c" +checksum = "12b2e757b11b47345d44e7760e45458339bc490463d9548cd8651c53ae523153" dependencies = [ "arrayvec", "bit-set", @@ -1675,7 +1681,7 @@ dependencies = [ "cfg_aliases", "codespan-reporting", "half", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "hexf-parse", "indexmap", "libm", @@ -3376,16 +3382,16 @@ dependencies = [ [[package]] name = "wgpu" -version = "26.0.1" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70b6ff82bbf6e9206828e1a3178e851f8c20f1c9028e74dd3a8090741ccd5798" +checksum = "3a355f55850044d897fdaa72199509ff08baaa0c387c4c80599decb5ee86790b" dependencies = [ "arrayvec", "bitflags 2.9.4", "cfg-if", "cfg_aliases", "document-features", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "js-sys", "log", "naga", @@ -3405,17 +3411,18 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "26.0.1" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f62f1053bd28c2268f42916f31588f81f64796e2ff91b81293515017ca8bd9" +checksum = "893764e276cdafec946c7f394f044e283bc8f1e445ab3fea8ad3b6dbc10c0322" dependencies = [ "arrayvec", "bit-set", "bit-vec", "bitflags 2.9.4", + "bytemuck", "cfg_aliases", "document-features", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "indexmap", "log", "naga", @@ -3436,36 +3443,36 @@ dependencies = [ [[package]] name = "wgpu-core-deps-apple" -version = "26.0.0" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18ae5fbde6a4cbebae38358aa73fcd6e0f15c6144b67ef5dc91ded0db125dbdf" +checksum = "0772ae958e9be0c729561d5e3fd9a19679bcdfb945b8b1a1969d9bfe8056d233" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-emscripten" -version = "26.0.0" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7670e390f416006f746b4600fdd9136455e3627f5bd763abf9a65daa216dd2d" +checksum = "b06ac3444a95b0813ecfd81ddb2774b66220b264b3e2031152a4a29fda4da6b5" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-windows-linux-android" -version = "26.0.0" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "720a5cb9d12b3d337c15ff0e24d3e97ed11490ff3f7506e7f3d98c68fa5d6f14" +checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-hal" -version = "26.0.4" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df2c64ac282a91ad7662c90bc4a77d4a2135bc0b2a2da5a4d4e267afc034b9e" +checksum = "a753c3dc95e69be3aacfe9c871c5fa2cfa9e35748cdc87de7ba5fc1735b61604" dependencies = [ "android_system_properties", "arrayvec", @@ -3482,7 +3489,7 @@ dependencies = [ "gpu-alloc", "gpu-allocator", "gpu-descriptor", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "js-sys", "khronos-egl", "libc", @@ -3492,6 +3499,7 @@ dependencies = [ "naga", "ndk-sys", "objc", + "once_cell", "ordered-float", "parking_lot", "portable-atomic", @@ -3511,9 +3519,9 @@ dependencies = [ [[package]] name = "wgpu-types" -version = "26.0.0" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca7a8d8af57c18f57d393601a1fb159ace8b2328f1b6b5f80893f7d672c9ae2" +checksum = "d67453b02f7adc33c452d17da1c2cad813448221df1547bce9dd4b02d3558538" dependencies = [ "bitflags 2.9.4", "bytemuck", diff --git a/Cargo.toml b/Cargo.toml index b7938d8..5bc3add 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ categories = ["multimedia::audio"] [dependencies] # my font font8x8 = "0.3.1" -wgpu = { version = "26.0.0", optional = true } +wgpu = { version = "27.0.0", optional = true } # ascii strings ascii = "1.1.0" winit = "0.30.11" diff --git a/src/gpu.rs b/src/gpu.rs index e910877..862b7f4 100644 --- a/src/gpu.rs +++ b/src/gpu.rs @@ -74,6 +74,7 @@ impl GPUState { // so i don't need a lot of allocations and they don't have to be that fast memory_hints: wgpu::MemoryHints::MemoryUsage, trace: wgpu::Trace::Off, + experimental_features: wgpu::ExperimentalFeatures::disabled(), }) .await .map_err(|e| e.to_string())?; From 203e062b3a1ca56874c323e3e52561b4b668ae56 Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Tue, 30 Sep 2025 22:16:17 +0200 Subject: [PATCH 10/19] reorganize modules --- Cargo.lock | 289 +++---- src/.DS_Store | Bin 6148 -> 0 bytes src/EK-MAC.itf | Bin 2050 -> 0 bytes src/app.rs | 753 ----------------- src/{ui => }/dialog/confirm.rs | 10 +- src/{ui/dialog.rs => dialog/mod.rs} | 4 +- src/{ui => }/dialog/page_menu.rs | 8 +- src/{ui => }/dialog/slider_dialog.rs | 8 +- src/draw_buffer.rs | 4 +- src/{ui => }/header.rs | 3 +- src/main.rs | 757 +++++++++++++++++- src/{ui => }/pages/help_page.rs | 4 +- src/{ui/pages.rs => pages/mod.rs} | 8 +- src/{ui => }/pages/order_list.rs | 13 +- src/{ui => }/pages/pattern.rs | 7 +- src/{ui => }/pages/sample_list.rs | 10 +- .../pages/song_directory_config_page.rs | 9 +- src/{ => render}/gpu.rs | 4 +- src/{render.rs => render/mod.rs} | 17 +- src/{ => render}/palettes.rs | 0 src/{ => render}/shader.wgsl | 0 src/ui/.DS_Store | Bin 6148 -> 0 bytes src/ui/mod.rs | 4 - src/{ui => }/widgets/button.rs | 2 +- src/{ui/widgets.rs => widgets/mod.rs} | 2 +- src/{ui => }/widgets/slider.rs | 4 +- src/{ui => }/widgets/text_in.rs | 2 +- src/{ui => }/widgets/text_in_scroll.rs | 4 +- src/{ui => }/widgets/toggle.rs | 2 +- src/{ui => }/widgets/toggle_button.rs | 2 +- 30 files changed, 960 insertions(+), 970 deletions(-) delete mode 100644 src/.DS_Store delete mode 100644 src/EK-MAC.itf delete mode 100644 src/app.rs rename src/{ui => }/dialog/confirm.rs (94%) rename src/{ui/dialog.rs => dialog/mod.rs} (95%) rename src/{ui => }/dialog/page_menu.rs (98%) rename src/{ui => }/dialog/slider_dialog.rs (94%) rename src/{ui => }/header.rs (98%) rename src/{ui => }/pages/help_page.rs (86%) rename src/{ui/pages.rs => pages/mod.rs} (98%) rename src/{ui => }/pages/order_list.rs (98%) rename src/{ui => }/pages/pattern.rs (99%) rename src/{ui => }/pages/sample_list.rs (98%) rename src/{ui => }/pages/song_directory_config_page.rs (99%) rename src/{ => render}/gpu.rs (99%) rename src/{render.rs => render/mod.rs} (93%) rename src/{ => render}/palettes.rs (100%) rename src/{ => render}/shader.wgsl (100%) delete mode 100644 src/ui/.DS_Store delete mode 100644 src/ui/mod.rs rename src/{ui => }/widgets/button.rs (99%) rename src/{ui/widgets.rs => widgets/mod.rs} (98%) rename src/{ui => }/widgets/slider.rs (99%) rename src/{ui => }/widgets/text_in.rs (99%) rename src/{ui => }/widgets/text_in_scroll.rs (99%) rename src/{ui => }/widgets/toggle.rs (99%) rename src/{ui => }/widgets/toggle_button.rs (95%) diff --git a/Cargo.lock b/Cargo.lock index fe66901..1f771be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,15 +20,15 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "accesskit" -version = "0.21.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c0690ad6e6f9597b8439bd3c95e8c6df5cd043afd950c6d68f3b37df641e27c" +checksum = "cf203f9d3bd8f29f98833d1fbef628df18f759248a547e7e01cfbf63cda36a99" [[package]] name = "accesskit_atspi_common" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb511e093896d3cae0efba40322087dff59ea322308a3e6edf70f28d22f2607" +checksum = "29f73a9b855b6f4af4962a94553ef0c092b80cf5e17038724d5e30945d036f69" dependencies = [ "accesskit", "accesskit_consumer", @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "accesskit_consumer" -version = "0.30.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec27574c1baeb7747c802a194566b46b602461e81dc4957949580ea8da695038" +checksum = "bdd06f5fea9819250fffd4debf926709f3593ac22f8c1541a2573e5ee0ca01cd" dependencies = [ "accesskit", "hashbrown 0.15.5", @@ -50,9 +50,9 @@ dependencies = [ [[package]] name = "accesskit_macos" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf962bfd305aed21133d06128ab3f4a6412031a5b8505534d55af869788af272" +checksum = "93fbaf15815f39084e0cb24950c232f0e3634702c2dfbf182ae3b4919a4a1d45" dependencies = [ "accesskit", "accesskit_consumer", @@ -64,9 +64,9 @@ dependencies = [ [[package]] name = "accesskit_unix" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2abbfb16144cca5bb2ea6acad5865b7c1e70d4fa171ceba1a52ea8e78a7515f4" +checksum = "64926a930368d52d95422b822ede15014c04536cabaa2394f99567a1f4788dc6" dependencies = [ "accesskit", "accesskit_atspi_common", @@ -82,9 +82,9 @@ dependencies = [ [[package]] name = "accesskit_windows" -version = "0.29.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4cd727229c389e32c1a78fe9f74dc62d7c9fb6eac98cfa1a17efde254fb2d98" +checksum = "792991159fa9ba57459de59e12e918bb90c5346fea7d40ac1a11f8632b41e63a" dependencies = [ "accesskit", "accesskit_consumer", @@ -96,9 +96,9 @@ dependencies = [ [[package]] name = "accesskit_winit" -version = "0.29.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822493d0e54e6793da77525bb7235a19e4fef8418194aaf25a988bc93740d683" +checksum = "cd9db0ea66997e3f4eae4a5f2c6b6486cf206642639ee629dbbb860ace1dec87" dependencies = [ "accesskit", "accesskit_macos", @@ -298,7 +298,7 @@ dependencies = [ "polling", "rustix 1.1.2", "slab", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -367,7 +367,7 @@ dependencies = [ "rustix 1.1.2", "signal-hook-registry", "slab", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -493,11 +493,11 @@ dependencies = [ [[package]] name = "block2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.2", + "objc2 0.6.3", ] [[package]] @@ -521,18 +521,18 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.10.1" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", @@ -573,9 +573,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.39" +version = "1.2.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" dependencies = [ "find-msvc-tools", "jobserver", @@ -901,9 +901,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.9.4", - "block2 0.6.1", + "block2 0.6.2", "libc", - "objc2 0.6.2", + "objc2 0.6.3", ] [[package]] @@ -1035,7 +1035,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -1073,9 +1073,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "foldhash" @@ -1305,13 +1305,14 @@ dependencies = [ [[package]] name = "half" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "e54c115d4f30f52c67202f079c5f9d8b49db4691f460fdb0b4c2e838261b2ba5" dependencies = [ "cfg-if", "crunchy", "num-traits", + "zerocopy", ] [[package]] @@ -1534,9 +1535,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.176" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" @@ -1545,7 +1546,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -1562,7 +1563,7 @@ checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags 2.9.4", "libc", - "redox_syscall 0.5.17", + "redox_syscall 0.5.18", ] [[package]] @@ -1597,11 +1598,10 @@ checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -1817,9 +1817,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" dependencies = [ "objc2-encode", ] @@ -1842,29 +1842,29 @@ dependencies = [ [[package]] name = "objc2-app-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.9.4", - "block2 0.6.1", - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "block2 0.6.2", + "objc2 0.6.3", + "objc2-foundation 0.3.2", ] [[package]] name = "objc2-audio-toolbox" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" dependencies = [ "bitflags 2.9.4", "libc", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-core-audio", "objc2-core-audio-types", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", ] [[package]] @@ -1893,24 +1893,24 @@ dependencies = [ [[package]] name = "objc2-core-audio" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca44961e888e19313b808f23497073e3f6b3c22bb485056674c8b49f3b025c82" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" dependencies = [ "dispatch2", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-core-audio-types", "objc2-core-foundation", ] [[package]] name = "objc2-core-audio-types" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ "bitflags 2.9.4", - "objc2 0.6.2", + "objc2 0.6.3", ] [[package]] @@ -1927,13 +1927,13 @@ dependencies = [ [[package]] name = "objc2-core-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.9.4", "dispatch2", - "objc2 0.6.2", + "objc2 0.6.3", ] [[package]] @@ -1981,12 +1981,12 @@ dependencies = [ [[package]] name = "objc2-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.9.4", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-core-foundation", ] @@ -2133,9 +2133,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -2143,15 +2143,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.17", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -2226,7 +2226,7 @@ dependencies = [ "hermit-abi", "pin-project-lite", "rustix 1.1.2", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -2384,9 +2384,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags 2.9.4", ] @@ -2404,14 +2404,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" dependencies = [ "ashpd", - "block2 0.6.1", + "block2 0.6.2", "dispatch2", "js-sys", "log", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "pollster", "raw-window-handle", "urlencoding", @@ -2421,12 +2421,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "rt-write-lock" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79ce7f6e4b55e98feea9066f403ac0866c1d16d602d7522367151771d2800a15" - [[package]] name = "rtrb" version = "0.3.2" @@ -2492,7 +2486,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -2689,7 +2683,7 @@ dependencies = [ "objc2-foundation 0.2.2", "objc2-quartz-core", "raw-window-handle", - "redox_syscall 0.5.17", + "redox_syscall 0.5.18", "rustix 0.38.44", "tiny-xlib", "wasm-bindgen", @@ -2712,9 +2706,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -2893,7 +2887,7 @@ dependencies = [ "getrandom", "once_cell", "rustix 1.1.2", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -2995,18 +2989,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.6" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap", "toml_datetime", @@ -3016,9 +3010,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow", ] @@ -3049,10 +3043,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809ceef6db8d2bd9bf2a14b327efe8fd04d16827e3675a035f70d00503c2ae4f" dependencies = [ "dasp", - "rt-write-lock", "rtrb", "rtsan-standalone", "simple-left-right", + "triple_buffer", ] [[package]] @@ -3086,6 +3080,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "triple_buffer" +version = "8.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420466259f9fa5decc654c490b9ab538400e5420df8237f84ecbe20368bcf72b" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "ttf-parser" version = "0.25.1" @@ -3117,9 +3120,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "url" @@ -3382,9 +3385,9 @@ dependencies = [ [[package]] name = "wgpu" -version = "27.0.0" +version = "27.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a355f55850044d897fdaa72199509ff08baaa0c387c4c80599decb5ee86790b" +checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77" dependencies = [ "arrayvec", "bitflags 2.9.4", @@ -3411,9 +3414,9 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "27.0.0" +version = "27.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "893764e276cdafec946c7f394f044e283bc8f1e445ab3fea8ad3b6dbc10c0322" +checksum = "e3d654c0b6c6335edfca18c11bdaed964def641b8e9997d3a495a2ff4077c922" dependencies = [ "arrayvec", "bit-set", @@ -3470,9 +3473,9 @@ dependencies = [ [[package]] name = "wgpu-hal" -version = "27.0.0" +version = "27.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a753c3dc95e69be3aacfe9c871c5fa2cfa9e35748cdc87de7ba5fc1735b61604" +checksum = "2618a2d6b8a5964ecc1ac32a5db56cb3b1e518725fcd773fd9a782e023453f2b" dependencies = [ "android_system_properties", "arrayvec", @@ -3519,9 +3522,9 @@ dependencies = [ [[package]] name = "wgpu-types" -version = "27.0.0" +version = "27.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d67453b02f7adc33c452d17da1c2cad813448221df1547bce9dd4b02d3558538" +checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" dependencies = [ "bitflags 2.9.4", "bytemuck", @@ -3553,7 +3556,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -3633,8 +3636,8 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement 0.60.1", - "windows-interface 0.59.2", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", @@ -3664,9 +3667,9 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.1" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -3686,9 +3689,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.2" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -3703,9 +3706,9 @@ checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" @@ -3796,16 +3799,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.4", + "windows-targets 0.53.5", ] [[package]] name = "windows-sys" -version = "0.61.1" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -3841,19 +3844,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.4" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.2.0", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -3879,9 +3882,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -3897,9 +3900,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -3915,9 +3918,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -3927,9 +3930,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -3945,9 +3948,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -3963,9 +3966,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -3981,9 +3984,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -3999,9 +4002,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winit" diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index b1cfadb2097359b2aba3539acdd85980c9483749..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKJ5Iwu5S=k8EYYN-+=c_>1|~8+QZ9f(BtVMgG%1nXQ6MEZ;5eLs1M%h)lwi_Q zpf}RY^UluOm0#iUh=|U&tC`4DL~6L9yjmEW?VFFR%FG0)cE&^BZ9J#Dmr?ca3FB6> zmFM;2Z1IEd`L1u<<+@*@uD*M}-oJf*+#EPvzdF0Oc{)0;DxlD)02QDDRDcRl0Vsf; zZB}0fGExC5Kn1=Pue#1$2w0{=<@ zowSQ~j#tXwI(j+lwFQ0;KNxDg9Kl;L&|5JU){3u=>WV#MzeyYdosPWIf&39LU1(I` HFBJF!k?|&# diff --git a/src/EK-MAC.itf b/src/EK-MAC.itf deleted file mode 100644 index 17b0b60d7d31f6bdb89cdd63dd5efaaf5635f906..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2050 zcmZ`)ziT5$6n=`$+7}CHNj1I**#Qw8NU?VpmgQJhi3zT*Jr^Q`!7)>qCAd^Etb|DE z;&63;L#nH;NTn)O;9Q9mDFg>8gWwt$%PQaZX3bUi&9ZOaoA+zp%)S)~@QdFUfBcw< zzrAfp%cg037E7FYrCGLWR&l0$bDN2nJkO0t2dpVlx;HOf35*n`UO83)Wg#oRt5SCuF%J($+wyA_*ahltD>OJOr#mgqcH3g+xS<)0D*rywod2>(4P?Ao%x| z{(v%V<8$5S^n)T2;XVYv&RtF=xT`6rsZ#2(E(!7+s+k$;h9i+_;HPm`{)sX?pfa)E z5%Bq0fpbN!aOcT8?mvnO@P+b~&1LX(sy9Uha$LxukNr!=2-1}SF8YfM#bY6ZR1ZWe z06+Ba)F1aBcyHAoj!=2j1MzwBcmd&-=SMT@gJA!n6{wX*0fJmVkV@|lZU?Sp_u*WW zfk=`GeGz8F&vrHs*ao3Aoq$$6Z`n@E&sw!@D{or`6yPb`X`g=Q_@ofF> z4%lu*L)OjShXa5zf^&lNck9`hj-k4x?89DD-@Q6sQg-lYcZZZ%9>2OH-PwI~0P>P^ zsVfe43j%b^ferULK=(LA#ycD${SF75=zxQz4w-K_z}6fh>xM&Q({Kp7phEX!a%8Ie zs#-o!e{}_idJAN&Kz<5rP65B4kS`S?_ll4D8krkKlOzVkFF)u+c5m*lv9IxyCtqPd z^WSc6YUDi_etC$#C~=a!*(`0B*JWN;PEfwLF8boL F{{dFP4jcdg diff --git a/src/app.rs b/src/app.rs deleted file mode 100644 index 9b81b1f..0000000 --- a/src/app.rs +++ /dev/null @@ -1,753 +0,0 @@ -use std::{ - collections::VecDeque, - fmt::Debug, - num::NonZero, - sync::{Arc, LazyLock, OnceLock}, - thread::JoinHandle, - time::Duration, -}; - -use smol::{channel::Sender, lock::Mutex}; -use torque_tracker_engine::{ - audio_processing::playback::PlaybackStatus, - manager::{AudioManager, OutputConfig, PlaybackSettings, ToWorkerMsg}, - project::song::{Song, SongOperation}, -}; -#[cfg(feature = "accesskit")] -use winit::dpi::PhysicalSize; -use winit::{ - application::ApplicationHandler, - event::{Modifiers, WindowEvent}, - event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy}, - keyboard::{Key, NamedKey}, - window::WindowAttributes, -}; - -use cpal::{ - BufferSize, OutputStreamTimestamp, SupportedBufferSize, - traits::{DeviceTrait, HostTrait}, -}; - -use crate::{ - palettes::Palette, - ui::pages::{order_list::OrderListPageEvent, pattern::PatternPageEvent}, -}; - -#[cfg(all(feature = "gpu_scaling", feature = "soft_scaling"))] -use super::render::BothRenderBackend as RenderBackend; -#[cfg(all(feature = "gpu_scaling", not(feature = "soft_scaling")))] -use super::render::GPURenderBackend as RenderBackend; -#[cfg(all(not(feature = "gpu_scaling"), feature = "soft_scaling"))] -use super::render::SoftRenderBackend as RenderBackend; - -use super::{ - draw_buffer::DrawBuffer, - ui::{ - dialog::{ - Dialog, DialogManager, DialogResponse, confirm::ConfirmDialog, page_menu::PageMenu, - }, - header::{Header, HeaderEvent}, - pages::{AllPages, PageEvent, PageResponse, PagesEnum}, - }, -}; - -pub static EXECUTOR: smol::Executor = smol::Executor::new(); -/// Song data -pub static SONG_MANAGER: LazyLock> = - LazyLock::new(|| Mutex::new(AudioManager::new(Song::default()))); -/// Sender for Song changes -pub static SONG_OP_SEND: OnceLock> = OnceLock::new(); - -/// shorter function name -pub fn send_song_op(op: SongOperation) { - SONG_OP_SEND.get().unwrap().send_blocking(op).unwrap(); -} - -pub enum GlobalEvent { - OpenDialog(Box Box + Send>), - Page(PageEvent), - Header(HeaderEvent), - /// also closes all dialogs - GoToPage(PagesEnum), - // Needed because only in the main app i know which pattern is selected, so i know what to play - Playback(PlaybackType), - #[cfg(feature = "accesskit")] - Accesskit(accesskit_winit::WindowEvent), - CloseRequested, - CloseApp, - ConstRedraw, -} - -impl Clone for GlobalEvent { - fn clone(&self) -> Self { - // TODO: make this really clone, once the Dialogs are an enum instead of Box dyn - match self { - GlobalEvent::OpenDialog(_) => panic!("TODO: don't clone this"), - GlobalEvent::Page(page_event) => GlobalEvent::Page(page_event.clone()), - GlobalEvent::Header(header_event) => GlobalEvent::Header(header_event.clone()), - GlobalEvent::GoToPage(pages_enum) => GlobalEvent::GoToPage(*pages_enum), - GlobalEvent::CloseRequested => GlobalEvent::CloseRequested, - GlobalEvent::CloseApp => GlobalEvent::CloseApp, - GlobalEvent::ConstRedraw => GlobalEvent::ConstRedraw, - GlobalEvent::Playback(playback_type) => GlobalEvent::Playback(*playback_type), - #[cfg(feature = "accesskit")] - GlobalEvent::Accesskit(window_event) => { - todo!("https://github.com/AccessKit/accesskit/issues/610") - } - } - } -} - -impl Debug for GlobalEvent { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut debug = f.debug_struct("GlobalEvent"); - match self { - GlobalEvent::OpenDialog(_) => debug.field("OpenDialog", &"closure"), - GlobalEvent::Page(page_event) => debug.field("Page", page_event), - GlobalEvent::Header(header_event) => debug.field("Header", header_event), - GlobalEvent::GoToPage(pages_enum) => debug.field("GoToPage", pages_enum), - GlobalEvent::CloseRequested => debug.field("CloseRequested", &""), - GlobalEvent::CloseApp => debug.field("CloseApp", &""), - GlobalEvent::ConstRedraw => debug.field("ConstRedraw", &""), - GlobalEvent::Playback(playback_type) => debug.field("Playback", &playback_type), - #[cfg(feature = "accesskit")] - GlobalEvent::Accesskit(window_event) => debug.field("Accesskit", &window_event), - }; - debug.finish() - } -} - -#[cfg(feature = "accesskit")] -impl From for GlobalEvent { - fn from(value: accesskit_winit::Event) -> Self { - // ignore window id, because i only have one window - Self::Accesskit(value.window_event) - } -} - -#[derive(Clone, Copy, Debug)] -pub enum PlaybackType { - Stop, - Song, - Pattern, - FromOrder, - FromCursor, -} - -struct WorkerThreads { - handles: [JoinHandle<()>; 2], - close_msg: [Sender<()>; 2], -} - -impl WorkerThreads { - fn new() -> Self { - let (send1, recv1) = smol::channel::unbounded(); - let thread1 = std::thread::Builder::new() - .name("Background Worker 1".into()) - .spawn(Self::worker_task(recv1)) - .unwrap(); - let (send2, recv2) = smol::channel::unbounded(); - let thread2 = std::thread::Builder::new() - .name("Background Worker 2".into()) - .spawn(Self::worker_task(recv2)) - .unwrap(); - - Self { - handles: [thread1, thread2], - close_msg: [send1, send2], - } - } - - fn worker_task(recv: smol::channel::Receiver<()>) -> impl FnOnce() + Send + 'static { - move || { - smol::block_on(EXECUTOR.run(async { recv.recv().await.unwrap() })); - } - } - - /// prepares the closing of the threads by signalling them to stop - fn send_close(&mut self) { - _ = self.close_msg[0].send_blocking(()); - _ = self.close_msg[1].send_blocking(()); - } - - fn close_all(mut self) { - self.send_close(); - let [handle1, handle2] = self.handles; - handle1.join().unwrap(); - handle2.join().unwrap(); - } -} - -pub struct EventQueue<'a>(&'a mut VecDeque); - -impl EventQueue<'_> { - pub fn push(&mut self, event: GlobalEvent) { - self.0.push_back(event); - } -} - -/// window with all the additional stuff -struct Window { - window: Arc, - render_backend: RenderBackend, - #[cfg(feature = "accesskit")] - adapter: (accesskit_winit::Adapter, accesskit::Affine), -} - -pub struct App { - window: Option, - draw_buffer: DrawBuffer, - modifiers: Modifiers, - ui_pages: AllPages, - event_queue: VecDeque, - dialog_manager: DialogManager, - header: Header, - event_loop_proxy: EventLoopProxy, - worker_threads: Option, - audio_stream: Option<( - cpal::Stream, - smol::Task<()>, - torque_tracker_engine::manager::StreamSend, - )>, -} - -impl ApplicationHandler for App { - fn new_events(&mut self, _: &ActiveEventLoop, start_cause: winit::event::StartCause) { - if start_cause == winit::event::StartCause::Init { - LazyLock::force(&SONG_MANAGER); - self.worker_threads = Some(WorkerThreads::new()); - let (send, recv) = smol::channel::unbounded(); - SONG_OP_SEND.get_or_init(|| send); - EXECUTOR - .spawn(async move { - while let Ok(op) = recv.recv().await { - let mut manager = SONG_MANAGER.lock().await; - // if there is no active channel the buffer isn't used, so it doesn't matter that it's wrong - let buffer_time = manager.last_buffer_time(); - // spin loop to lock the song - let mut song = loop { - if let Some(song) = manager.try_edit_song() { - break song; - } - // smol mutex lock is held across await point - smol::Timer::after(buffer_time).await; - }; - // apply the received op - song.apply_operation(op).unwrap(); - // try to get more ops. This avoids repeated locking of the song when a lot of operations are - // in queue - while let Ok(op) = recv.try_recv() { - song.apply_operation(op).unwrap(); - } - drop(song); - } - }) - .detach(); - // spawn a task to collect sample garbage every 10 seconds - EXECUTOR - .spawn(async { - loop { - let mut lock = SONG_MANAGER.lock().await; - lock.collect_garbage(); - drop(lock); - smol::Timer::after(Duration::from_secs(10)).await; - } - }) - .detach(); - self.start_audio_stream(); - } - } - - fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { - self.build_window(event_loop); - } - - fn suspended(&mut self, _: &ActiveEventLoop) { - // my window and GPU state have been invalidated - self.window = None; - } - - fn window_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - _: winit::window::WindowId, - event: WindowEvent, - ) { - // destructure so i don't have to always type self. - let Self { - window, - draw_buffer, - modifiers, - ui_pages, - event_queue, - dialog_manager, - header, - event_loop_proxy: _, - worker_threads: _, - audio_stream: _, - } = self; - - // i won't get a window_event without having a window, so unwrap is fine - let window = window.as_mut().unwrap(); - #[cfg(not(feature = "accesskit"))] - let Window { - window, - render_backend, - } = window; - #[cfg(feature = "accesskit")] - let Window { - window, - render_backend, - adapter, - } = window; - // don't want window to be mutable - let window = window.as_ref(); - - #[cfg(feature = "accesskit")] - adapter.0.process_event(window, &event); - // limit the pages and widgets to only push events and not read or pop - let event_queue = &mut EventQueue(event_queue); - - match event { - WindowEvent::CloseRequested => Self::close_requested(event_queue), - WindowEvent::Resized(physical_size) => { - render_backend.resize(physical_size); - window.request_redraw(); - } - WindowEvent::ScaleFactorChanged { - scale_factor: _, - inner_size_writer: _, - } => { - // window_state.resize(**new_inner_size); - // due to a version bump in winit i dont know anymore how to handle this event so i just ignore it for know and see if it makes problems in the future - // i have yet only received this event on linux wayland, not macos - println!("Window Scale Factor Changed"); - } - WindowEvent::RedrawRequested => { - // draw the new frame buffer - // TODO: split redraw header and redraw page. As soon as header gets a spectrometer this becomes important - header.draw(draw_buffer); - ui_pages.draw(draw_buffer); - dialog_manager.draw(draw_buffer); - #[cfg(feature = "accesskit")] - adapter.0.update_if_active(|| { - Self::produce_full_tree(ui_pages, header, dialog_manager, adapter.1) - }); - // notify the windowing system that drawing is done and the new buffer is about to be pushed - window.pre_present_notify(); - // push the framebuffer into GPU/softbuffer and render it onto the screen - render_backend.render(&draw_buffer.framebuffer, event_loop); - } - WindowEvent::KeyboardInput { - device_id: _, - event, - is_synthetic, - } => { - if is_synthetic { - return; - } - - if event.state.is_pressed() { - if event.logical_key == Key::Named(NamedKey::F5) { - self.event_queue - .push_back(GlobalEvent::Playback(PlaybackType::Song)); - return; - } else if event.logical_key == Key::Named(NamedKey::F6) { - if modifiers.state().shift_key() { - self.event_queue - .push_back(GlobalEvent::Playback(PlaybackType::FromOrder)); - } else { - self.event_queue - .push_back(GlobalEvent::Playback(PlaybackType::Pattern)); - } - return; - } else if event.logical_key == Key::Named(NamedKey::F8) { - self.event_queue - .push_back(GlobalEvent::Playback(PlaybackType::Stop)); - return; - } - } - // key_event didn't start or stop the song, so process normally - if let Some(dialog) = dialog_manager.active_dialog_mut() { - match dialog.process_input(&event, modifiers, event_queue) { - DialogResponse::Close => { - dialog_manager.close_dialog(); - // if i close a pop_up i need to redraw the const part of the page as the pop-up overlapped it probably - ui_pages.request_draw_const(); - window.request_redraw(); - } - DialogResponse::RequestRedraw => window.request_redraw(), - DialogResponse::None => (), - } - } else { - if event.state.is_pressed() && event.logical_key == Key::Named(NamedKey::Escape) - { - event_queue.push(GlobalEvent::OpenDialog(Box::new(|| { - Box::new(PageMenu::main()) - }))); - } - - match ui_pages.process_key_event(&self.modifiers, &event, event_queue) { - PageResponse::RequestRedraw => window.request_redraw(), - PageResponse::None => (), - } - } - } - // not sure if i need it just to make sure i always have all current modifiers to be used with keyboard events - WindowEvent::ModifiersChanged(new_modifiers) => *modifiers = new_modifiers, - - _ => (), - } - - while let Some(event) = self.event_queue.pop_front() { - self.user_event(event_loop, event); - } - } - - /// i may need to add the ability for events to add events to the event queue, but that should be possible - fn user_event(&mut self, event_loop: &ActiveEventLoop, event: GlobalEvent) { - let event_queue = &mut EventQueue(&mut self.event_queue); - match event { - GlobalEvent::OpenDialog(dialog) => { - self.dialog_manager.open_dialog(dialog()); - _ = self.try_request_redraw(); - } - GlobalEvent::Page(c) => match self.ui_pages.process_page_event(c, event_queue) { - PageResponse::RequestRedraw => _ = self.try_request_redraw(), - PageResponse::None => (), - }, - GlobalEvent::Header(header_event) => { - self.header.process_event(header_event); - _ = self.try_request_redraw(); - } - GlobalEvent::GoToPage(pages_enum) => { - self.dialog_manager.close_all(); - self.ui_pages.switch_page(pages_enum); - _ = self.try_request_redraw(); - } - GlobalEvent::CloseApp => event_loop.exit(), - GlobalEvent::CloseRequested => Self::close_requested(event_queue), - GlobalEvent::ConstRedraw => { - self.ui_pages.request_draw_const(); - _ = self.try_request_redraw(); - } - GlobalEvent::Playback(playback_type) => { - let msg = match playback_type { - PlaybackType::Song => Some(ToWorkerMsg::Playback(PlaybackSettings::Order { - idx: 0, - should_loop: true, - })), - PlaybackType::Stop => Some(ToWorkerMsg::StopPlayback), - PlaybackType::Pattern => { - Some(ToWorkerMsg::Playback(self.header.play_current_pattern())) - } - PlaybackType::FromOrder => { - Some(ToWorkerMsg::Playback(self.header.play_current_order())) - } - PlaybackType::FromCursor => None, - }; - - if let Some(msg) = msg { - self.audio_stream - .as_mut() - .expect( - "audio stream should always be active, should still handle this error", - ) - .2 - .try_msg_worker(msg) - .expect("buffer full. either increase size or retry somehow") - } - } - #[cfg(feature = "accesskit")] - GlobalEvent::Accesskit(window_event) => { - use accesskit_winit::WindowEvent; - match window_event { - WindowEvent::InitialTreeRequested => { - // there probably should always be a window - // make sure we always respond to this event. I hope it can't be send when there is no window - let window = self.window.as_mut().unwrap(); - window.adapter.0.update_if_active(|| { - Self::produce_full_tree( - &self.ui_pages, - &self.header, - &self.dialog_manager, - window.adapter.1, - ) - }); - } - WindowEvent::ActionRequested(action_request) => todo!(), - // i don't have any extra state for accessability so i don't need to cleanup anything - WindowEvent::AccessibilityDeactivated => (), - } - } - } - } - - fn exiting(&mut self, _: &ActiveEventLoop) { - if let Some(workers) = self.worker_threads.take() { - // wait for all the threads to close - workers.close_all(); - } - if self.audio_stream.is_some() { - self.close_audio_stream(); - } - } -} - -impl App { - pub fn new(proxy: EventLoopProxy) -> Self { - Self { - window: None, - draw_buffer: DrawBuffer::new(), - modifiers: Modifiers::default(), - ui_pages: AllPages::new(proxy.clone()), - dialog_manager: DialogManager::new(), - header: Header::default(), - event_loop_proxy: proxy, - worker_threads: None, - audio_stream: None, - event_queue: VecDeque::with_capacity(3), - } - } - - // TODO: should this be its own function?? or is there something better - fn close_requested(events: &mut EventQueue<'_>) { - events.push(GlobalEvent::OpenDialog(Box::new(|| { - Box::new(ConfirmDialog::new( - "Close Torque Tracker?", - || Some(GlobalEvent::CloseApp), - || None, - )) - }))); - } - - /// tries to request a redraw. if there currently is no window this fails - fn try_request_redraw(&self) -> Result<(), ()> { - if let Some(window) = &self.window { - window.window.request_redraw(); - Ok(()) - } else { - Err(()) - } - } - - fn build_window(&mut self, event_loop: &ActiveEventLoop) { - self.window.get_or_insert_with(|| { - let mut attributes = WindowAttributes::default(); - attributes.active = true; - attributes.resizable = true; - attributes.resize_increments = None; - attributes.title = String::from("Torque Tracker"); - // for accesskit - attributes.visible = false; - - let window = Arc::new(event_loop.create_window(attributes).unwrap()); - let render_backend = RenderBackend::new(window.clone(), Palette::CAMOUFLAGE); - - #[cfg(feature = "accesskit")] - let adapter = { - accesskit_winit::Adapter::with_event_loop_proxy( - event_loop, - &window, - self.event_loop_proxy.clone(), - ) - }; - - window.set_visible(true); - let size = render_backend.get_size(); - Window { - window, - render_backend, - #[cfg(feature = "accesskit")] - adapter: (adapter, create_transform(size)), - } - }); - } - - // TODO: make this configurable - fn start_audio_stream(&mut self) { - use cpal::StreamInstant; - - assert!(self.audio_stream.is_none()); - let host = cpal::default_host(); - let device = host.default_output_device().unwrap(); - let default_config = device.default_output_config().unwrap(); - let (config, buffer_size) = { - let mut config = default_config.config(); - let buffer_size = { - let default = default_config.buffer_size(); - match default { - SupportedBufferSize::Unknown => 1024, - SupportedBufferSize::Range { min, max } => u32::min(u32::max(1024, *min), *max), - } - }; - config.buffer_size = BufferSize::Fixed(buffer_size); - (config, buffer_size) - }; - let mut guard = SONG_MANAGER.lock_blocking(); - let (mut worker, buffer_time, status, stream_send) = guard - .get_callback::( - OutputConfig { - buffer_size, - channel_count: NonZero::new(config.channels).unwrap(), - sample_rate: NonZero::new(config.sample_rate.0).unwrap(), - }, - cpal::OutputStreamTimestamp { - callback: cpal::StreamInstant::new(0, 0), - playback: cpal::StreamInstant::new(0, 0), - }, - ); - // keep the guard as short as possible to not block the async threads - drop(guard); - let stream = device - .build_output_stream( - &config, - move |data, info| { - worker(data, info.timestamp()); - }, - |err| eprintln!("audio stream err: {err:?}"), - None, - ) - .unwrap(); - // spawn a task to process the audio playback status updates - let proxy = self.event_loop_proxy.clone(); - let task = EXECUTOR.spawn(async move { - let buffer_time = buffer_time; - let mut status_recv = status; - // maybe also send the timestamp every second or so - let mut old_playback = None; - let mut old_timestamp = OutputStreamTimestamp { - callback: StreamInstant::new(0, 0), - playback: StreamInstant::new(0, 0), - }; - loop { - smol::Timer::after(buffer_time).await; - let Some(read) = status_recv.try_get() else { - // we had a lock for way too long, so now we are behing and have to wait for the writer to finish - // writing once. Then locking will succeed again. - continue; - }; - let playback = read.0; - let timestamp = read.1; - // only react on status changes. could at some point be made more granular - if playback != old_playback { - old_playback = playback; - // println!("playback status: {status:?}"); - let pos = playback.map(|s| s.position); - proxy - .send_event(GlobalEvent::Header(HeaderEvent::SetPlayback(pos))) - .unwrap(); - let pos = playback.map(|s| (s.position.pattern, s.position.row)); - proxy - .send_event(GlobalEvent::Page(PageEvent::Pattern( - PatternPageEvent::PlaybackPosition(pos), - ))) - .unwrap(); - // does a map flatten. idk why it's called and_then - let pos = playback.and_then(|s| s.position.order); - proxy - .send_event(GlobalEvent::Page(PageEvent::OrderList( - OrderListPageEvent::SetPlayback(pos), - ))) - .unwrap(); - } - - // also check for changed timestamp - if timestamp != old_timestamp { - old_timestamp = timestamp; - // maybe at some point use the data - } - } - }); - self.audio_stream = Some((stream, task, stream_send)); - } - - fn close_audio_stream(&mut self) { - let (stream, task, mut stream_send) = self.audio_stream.take().unwrap(); - // stop playback - _ = stream_send.try_msg_worker(ToWorkerMsg::StopPlayback); - _ = stream_send.try_msg_worker(ToWorkerMsg::StopLiveNote); - // kill the task. using `cancel` doesn't make sense because it doesn't finishe anyways - drop(task); - // lastly kill the audio stream - drop(stream); - } - - #[cfg(feature = "accesskit")] - fn produce_full_tree( - pages: &AllPages, - header: &Header, - dialogs: &DialogManager, - transform: accesskit::Affine, - ) -> accesskit::TreeUpdate { - use accesskit::TreeUpdate; - use accesskit::{Node, NodeId, Role, Tree}; - const ROOT_ID: NodeId = NodeId(0); - - let tree = Tree { - root: ROOT_ID, - toolkit_name: Some(String::from("Torque Tracker Custom")), - toolkit_version: None, - }; - let mut root_node = Node::new(Role::Window); - let mut nodes = Vec::new(); - root_node.set_label("Torque Tracker"); - root_node.set_language("English"); - let header_id = header.build_tree(&mut nodes); - root_node.push_child(header_id); - root_node.set_transform(transform); - - let resp = pages.build_tree(&mut nodes); - let mut focused = resp.selected; - root_node.push_child(resp.root); - - if let Some(dialog) = dialogs.active_dialog() { - let resp = dialog.build_tree(&mut nodes); - root_node.push_child(resp.root); - focused = resp.selected; - } - - nodes.push((ROOT_ID, root_node)); - TreeUpdate { - nodes, - tree: Some(tree), - focus: focused, - } - } -} - -impl Drop for App { - fn drop(&mut self) { - if self.audio_stream.is_some() { - self.close_audio_stream(); - } - } -} - -pub fn run() { - let event_loop = winit::event_loop::EventLoop::::with_user_event() - .build() - .unwrap(); - event_loop.set_control_flow(ControlFlow::Wait); - // i don't need any raw device events. Keyboard and Mouse coming as window events are enough - event_loop.listen_device_events(winit::event_loop::DeviceEvents::Never); - let event_loop_proxy = event_loop.create_proxy(); - let mut app = App::new(event_loop_proxy); - app.header.draw_constant(&mut app.draw_buffer); - - event_loop.run_app(&mut app).unwrap(); -} - -#[cfg(feature = "accesskit")] -pub struct AccessResponse { - pub root: accesskit::NodeId, - pub selected: accesskit::NodeId, -} - -#[cfg(feature = "accesskit")] -fn create_transform(size: winit::dpi::PhysicalSize) -> accesskit::Affine { - accesskit::Affine::scale_non_uniform( - size.width as f64 / crate::coordinates::WINDOW_SIZE_CHARS.0 as f64, - size.height as f64 / crate::coordinates::WINDOW_SIZE_CHARS.1 as f64, - ) -} diff --git a/src/ui/dialog/confirm.rs b/src/dialog/confirm.rs similarity index 94% rename from src/ui/dialog/confirm.rs rename to src/dialog/confirm.rs index 04c6609..85478f2 100644 --- a/src/ui/dialog/confirm.rs +++ b/src/dialog/confirm.rs @@ -1,10 +1,10 @@ use winit::keyboard::{Key, NamedKey}; use crate::{ - app::GlobalEvent, + GlobalEvent, coordinates::{CharPosition, CharRect}, draw_buffer::DrawBuffer, - ui::widgets::{NextWidget, StandardResponse, Widget, WidgetResponse, button::Button}, + widgets::{NextWidget, StandardResponse, Widget, WidgetResponse, button::Button}, }; use super::{Dialog, DialogResponse}; @@ -83,7 +83,7 @@ impl Dialog for ConfirmDialog { &mut self, key_event: &winit::event::KeyEvent, modifiers: &winit::event::Modifiers, - events: &mut crate::app::EventQueue<'_>, + events: &mut crate::EventQueue<'_>, ) -> DialogResponse { if key_event.logical_key == Key::Named(NamedKey::Escape) && modifiers.state().is_empty() { return DialogResponse::Close; @@ -116,10 +116,10 @@ impl Dialog for ConfirmDialog { fn build_tree( &self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, - ) -> crate::app::AccessResponse { + ) -> crate::AccessResponse { use accesskit::{Node, Role}; - use crate::app::AccessResponse; + use crate::AccessResponse; let mut root_node = Node::new(Role::Dialog); diff --git a/src/ui/dialog.rs b/src/dialog/mod.rs similarity index 95% rename from src/ui/dialog.rs rename to src/dialog/mod.rs index 7394d7d..82adf0b 100644 --- a/src/ui/dialog.rs +++ b/src/dialog/mod.rs @@ -4,7 +4,7 @@ pub mod slider_dialog; use winit::event::{KeyEvent, Modifiers}; -use crate::{app::EventQueue, draw_buffer::DrawBuffer}; +use crate::{EventQueue, draw_buffer::DrawBuffer}; pub enum DialogResponse { RequestRedraw, @@ -28,7 +28,7 @@ pub trait Dialog { fn build_tree( &self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, - ) -> crate::app::AccessResponse; + ) -> crate::AccessResponse; } pub struct DialogManager { diff --git a/src/ui/dialog/page_menu.rs b/src/dialog/page_menu.rs similarity index 98% rename from src/ui/dialog/page_menu.rs rename to src/dialog/page_menu.rs index e7588a6..3069924 100644 --- a/src/ui/dialog/page_menu.rs +++ b/src/dialog/page_menu.rs @@ -1,10 +1,10 @@ use winit::keyboard::{Key, NamedKey}; use crate::{ - app::{EventQueue, GlobalEvent, PlaybackType}, + EventQueue, GlobalEvent, PlaybackType, coordinates::{CharPosition, CharRect, FONT_SIZE, PixelRect}, draw_buffer::DrawBuffer, - ui::pages::PagesEnum, + pages::PagesEnum, }; use super::{Dialog, DialogResponse}; @@ -162,10 +162,10 @@ impl Dialog for PageMenu { fn build_tree( &self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, - ) -> crate::app::AccessResponse { + ) -> crate::AccessResponse { use accesskit::{Action, Node, NodeId, Rect, Role}; - use crate::app::AccessResponse; + use crate::AccessResponse; let mut root = Node::new(Role::Dialog); root.set_label(self.name); root.set_bounds(dbg!(Rect::from(self.rect))); diff --git a/src/ui/dialog/slider_dialog.rs b/src/dialog/slider_dialog.rs similarity index 94% rename from src/ui/dialog/slider_dialog.rs rename to src/dialog/slider_dialog.rs index b7debb1..ee385fe 100644 --- a/src/ui/dialog/slider_dialog.rs +++ b/src/dialog/slider_dialog.rs @@ -6,10 +6,10 @@ use winit::{ }; use crate::{ - app::{EventQueue, GlobalEvent}, + EventQueue, GlobalEvent, coordinates::{CharPosition, CharRect}, draw_buffer::DrawBuffer, - ui::widgets::{NextWidget, StandardResponse, Widget, text_in::TextIn}, + widgets::{NextWidget, StandardResponse, Widget, text_in::TextIn}, }; use super::{Dialog, DialogResponse}; @@ -64,10 +64,10 @@ impl Dialog for SliderDialog { fn build_tree( &self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, - ) -> crate::app::AccessResponse { + ) -> crate::AccessResponse { use accesskit::{Node, NodeId, Role}; - use crate::app::AccessResponse; + use crate::AccessResponse; const ROOT_ID: NodeId = NodeId(400_000_000); const TEXT_ID: NodeId = NodeId(400_000_001); diff --git a/src/draw_buffer.rs b/src/draw_buffer.rs index c90b40c..6ea614b 100644 --- a/src/draw_buffer.rs +++ b/src/draw_buffer.rs @@ -1,5 +1,3 @@ -use crate::palettes; - use super::coordinates::{CharPosition, CharRect, FONT_SIZE, PixelRect, WINDOW_SIZE}; use font8x8::UnicodeFonts; @@ -221,7 +219,7 @@ impl DrawBuffer { } pub fn show_colors(&mut self) { - for i in 0..palettes::PALETTE_SIZE as u8 { + for i in 0..crate::render::palettes::PALETTE_SIZE as u8 { self.draw_rect(i, CharPosition::new(i as usize, 5).into()); } } diff --git a/src/ui/header.rs b/src/header.rs similarity index 98% rename from src/ui/header.rs rename to src/header.rs index 8affc8f..1718e49 100644 --- a/src/ui/header.rs +++ b/src/header.rs @@ -2,8 +2,7 @@ use std::{io::Write, str::from_utf8}; use font8x8::UnicodeFonts; use torque_tracker_engine::{ - audio_processing::playback::PlaybackPosition, manager::PlaybackSettings, - project::pattern::Pattern, + PlaybackSettings, audio_processing::playback::PlaybackPosition, project::pattern::Pattern, }; use crate::{ diff --git a/src/main.rs b/src/main.rs index 4604bc7..f7f864a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,757 @@ -pub mod app; pub mod coordinates; +pub mod dialog; pub mod draw_buffer; -#[cfg(feature = "gpu_scaling")] -pub mod gpu; -pub mod palettes; +pub mod header; +pub mod pages; pub mod render; -pub mod ui; +pub mod widgets; + +use std::{ + collections::VecDeque, + fmt::Debug, + num::NonZero, + sync::{Arc, LazyLock, OnceLock}, + thread::JoinHandle, + time::Duration, +}; + +use smol::{channel::Sender, lock::Mutex}; +use torque_tracker_engine::{ + AudioManager, OutputConfig, PlaybackSettings, ToWorkerMsg, + audio_processing::playback::PlaybackStatus, + project::song::{Song, SongOperation}, +}; +#[cfg(feature = "accesskit")] +use winit::dpi::PhysicalSize; +use winit::{ + application::ApplicationHandler, + event::{Modifiers, WindowEvent}, + event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy}, + keyboard::{Key, NamedKey}, + window::WindowAttributes, +}; + +use cpal::{ + BufferSize, OutputStreamTimestamp, SupportedBufferSize, + traits::{DeviceTrait, HostTrait}, +}; + +use pages::{ + AllPages, PageEvent, PageResponse, PagesEnum, order_list::OrderListPageEvent, + pattern::PatternPageEvent, +}; + +use { + dialog::{Dialog, DialogManager, DialogResponse, confirm::ConfirmDialog, page_menu::PageMenu}, + draw_buffer::DrawBuffer, + header::{Header, HeaderEvent}, +}; + +#[cfg(all(feature = "gpu_scaling", feature = "soft_scaling"))] +use render::BothRenderBackend as RenderBackend; +#[cfg(all(feature = "gpu_scaling", not(feature = "soft_scaling")))] +use render::GPURenderBackend as RenderBackend; +#[cfg(all(not(feature = "gpu_scaling"), feature = "soft_scaling"))] +use render::SoftRenderBackend as RenderBackend; + +pub static EXECUTOR: smol::Executor = smol::Executor::new(); +/// Song data +pub static SONG_MANAGER: LazyLock> = + LazyLock::new(|| Mutex::new(AudioManager::new(Song::default()))); +/// Sender for Song changes +pub static SONG_OP_SEND: OnceLock> = OnceLock::new(); + +/// shorter function name +pub fn send_song_op(op: SongOperation) { + SONG_OP_SEND.get().unwrap().send_blocking(op).unwrap(); +} + +pub enum GlobalEvent { + OpenDialog(Box Box + Send>), + Page(PageEvent), + Header(HeaderEvent), + /// also closes all dialogs + GoToPage(PagesEnum), + // Needed because only in the main app i know which pattern is selected, so i know what to play + Playback(PlaybackType), + #[cfg(feature = "accesskit")] + Accesskit(accesskit_winit::WindowEvent), + CloseRequested, + CloseApp, + ConstRedraw, +} + +impl Clone for GlobalEvent { + fn clone(&self) -> Self { + // TODO: make this really clone, once the Dialogs are an enum instead of Box dyn + match self { + GlobalEvent::OpenDialog(_) => panic!("TODO: don't clone this"), + GlobalEvent::Page(page_event) => GlobalEvent::Page(page_event.clone()), + GlobalEvent::Header(header_event) => GlobalEvent::Header(header_event.clone()), + GlobalEvent::GoToPage(pages_enum) => GlobalEvent::GoToPage(*pages_enum), + GlobalEvent::CloseRequested => GlobalEvent::CloseRequested, + GlobalEvent::CloseApp => GlobalEvent::CloseApp, + GlobalEvent::ConstRedraw => GlobalEvent::ConstRedraw, + GlobalEvent::Playback(playback_type) => GlobalEvent::Playback(*playback_type), + #[cfg(feature = "accesskit")] + GlobalEvent::Accesskit(window_event) => { + todo!("https://github.com/AccessKit/accesskit/issues/610") + } + } + } +} + +impl Debug for GlobalEvent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut debug = f.debug_struct("GlobalEvent"); + match self { + GlobalEvent::OpenDialog(_) => debug.field("OpenDialog", &"closure"), + GlobalEvent::Page(page_event) => debug.field("Page", page_event), + GlobalEvent::Header(header_event) => debug.field("Header", header_event), + GlobalEvent::GoToPage(pages_enum) => debug.field("GoToPage", pages_enum), + GlobalEvent::CloseRequested => debug.field("CloseRequested", &""), + GlobalEvent::CloseApp => debug.field("CloseApp", &""), + GlobalEvent::ConstRedraw => debug.field("ConstRedraw", &""), + GlobalEvent::Playback(playback_type) => debug.field("Playback", &playback_type), + #[cfg(feature = "accesskit")] + GlobalEvent::Accesskit(window_event) => debug.field("Accesskit", &window_event), + }; + debug.finish() + } +} + +#[cfg(feature = "accesskit")] +impl From for GlobalEvent { + fn from(value: accesskit_winit::Event) -> Self { + // ignore window id, because i only have one window + Self::Accesskit(value.window_event) + } +} + +#[derive(Clone, Copy, Debug)] +pub enum PlaybackType { + Stop, + Song, + Pattern, + FromOrder, + FromCursor, +} + +struct WorkerThreads { + handles: [JoinHandle<()>; 2], + close_msg: [Sender<()>; 2], +} + +impl WorkerThreads { + fn new() -> Self { + let (send1, recv1) = smol::channel::unbounded(); + let thread1 = std::thread::Builder::new() + .name("Background Worker 1".into()) + .spawn(Self::worker_task(recv1)) + .unwrap(); + let (send2, recv2) = smol::channel::unbounded(); + let thread2 = std::thread::Builder::new() + .name("Background Worker 2".into()) + .spawn(Self::worker_task(recv2)) + .unwrap(); + + Self { + handles: [thread1, thread2], + close_msg: [send1, send2], + } + } + + fn worker_task(recv: smol::channel::Receiver<()>) -> impl FnOnce() + Send + 'static { + move || { + smol::block_on(EXECUTOR.run(async { recv.recv().await.unwrap() })); + } + } + + /// prepares the closing of the threads by signalling them to stop + fn send_close(&mut self) { + _ = self.close_msg[0].send_blocking(()); + _ = self.close_msg[1].send_blocking(()); + } + + fn close_all(mut self) { + self.send_close(); + let [handle1, handle2] = self.handles; + handle1.join().unwrap(); + handle2.join().unwrap(); + } +} + +pub struct EventQueue<'a>(&'a mut VecDeque); + +impl EventQueue<'_> { + pub fn push(&mut self, event: GlobalEvent) { + self.0.push_back(event); + } +} + +/// window with all the additional stuff +struct Window { + window: Arc, + render_backend: RenderBackend, + #[cfg(feature = "accesskit")] + adapter: (accesskit_winit::Adapter, accesskit::Affine), +} + +pub struct App { + window: Option, + draw_buffer: DrawBuffer, + modifiers: Modifiers, + ui_pages: AllPages, + event_queue: VecDeque, + dialog_manager: DialogManager, + header: Header, + event_loop_proxy: EventLoopProxy, + worker_threads: Option, + audio_stream: Option<( + cpal::Stream, + smol::Task<()>, + torque_tracker_engine::StreamSend, + )>, +} + +impl ApplicationHandler for App { + fn new_events(&mut self, _: &ActiveEventLoop, start_cause: winit::event::StartCause) { + if start_cause == winit::event::StartCause::Init { + LazyLock::force(&SONG_MANAGER); + self.worker_threads = Some(WorkerThreads::new()); + let (send, recv) = smol::channel::unbounded(); + SONG_OP_SEND.get_or_init(|| send); + EXECUTOR + .spawn(async move { + while let Ok(op) = recv.recv().await { + let mut manager = SONG_MANAGER.lock().await; + // if there is no active channel the buffer isn't used, so it doesn't matter that it's wrong + let buffer_time = manager.last_buffer_time(); + // spin loop to lock the song + let mut song = loop { + if let Some(song) = manager.try_edit_song() { + break song; + } + // smol mutex lock is held across await point + smol::Timer::after(buffer_time).await; + }; + // apply the received op + song.apply_operation(op).unwrap(); + // try to get more ops. This avoids repeated locking of the song when a lot of operations are + // in queue + while let Ok(op) = recv.try_recv() { + song.apply_operation(op).unwrap(); + } + drop(song); + } + }) + .detach(); + // spawn a task to collect sample garbage every 10 seconds + EXECUTOR + .spawn(async { + loop { + let mut lock = SONG_MANAGER.lock().await; + lock.collect_garbage(); + drop(lock); + smol::Timer::after(Duration::from_secs(10)).await; + } + }) + .detach(); + self.start_audio_stream(); + } + } + + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + self.build_window(event_loop); + } + + fn suspended(&mut self, _: &ActiveEventLoop) { + // my window and GPU state have been invalidated + self.window = None; + } + + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + _: winit::window::WindowId, + event: WindowEvent, + ) { + // destructure so i don't have to always type self. + let Self { + window, + draw_buffer, + modifiers, + ui_pages, + event_queue, + dialog_manager, + header, + event_loop_proxy: _, + worker_threads: _, + audio_stream: _, + } = self; + + // i won't get a window_event without having a window, so unwrap is fine + let window = window.as_mut().unwrap(); + #[cfg(not(feature = "accesskit"))] + let Window { + window, + render_backend, + } = window; + #[cfg(feature = "accesskit")] + let Window { + window, + render_backend, + adapter, + } = window; + // don't want window to be mutable + let window = window.as_ref(); + + #[cfg(feature = "accesskit")] + adapter.0.process_event(window, &event); + // limit the pages and widgets to only push events and not read or pop + let event_queue = &mut EventQueue(event_queue); + + match event { + WindowEvent::CloseRequested => Self::close_requested(event_queue), + WindowEvent::Resized(physical_size) => { + render_backend.resize(physical_size); + window.request_redraw(); + } + WindowEvent::ScaleFactorChanged { + scale_factor: _, + inner_size_writer: _, + } => { + // window_state.resize(**new_inner_size); + // due to a version bump in winit i dont know anymore how to handle this event so i just ignore it for know and see if it makes problems in the future + // i have yet only received this event on linux wayland, not macos + println!("Window Scale Factor Changed"); + } + WindowEvent::RedrawRequested => { + // draw the new frame buffer + // TODO: split redraw header and redraw page. As soon as header gets a spectrometer this becomes important + header.draw(draw_buffer); + ui_pages.draw(draw_buffer); + dialog_manager.draw(draw_buffer); + #[cfg(feature = "accesskit")] + adapter.0.update_if_active(|| { + Self::produce_full_tree(ui_pages, header, dialog_manager, adapter.1) + }); + // notify the windowing system that drawing is done and the new buffer is about to be pushed + window.pre_present_notify(); + // push the framebuffer into GPU/softbuffer and render it onto the screen + render_backend.render(&draw_buffer.framebuffer, event_loop); + } + WindowEvent::KeyboardInput { + device_id: _, + event, + is_synthetic, + } => { + if is_synthetic { + return; + } + + if event.state.is_pressed() { + if event.logical_key == Key::Named(NamedKey::F5) { + self.event_queue + .push_back(GlobalEvent::Playback(PlaybackType::Song)); + return; + } else if event.logical_key == Key::Named(NamedKey::F6) { + if modifiers.state().shift_key() { + self.event_queue + .push_back(GlobalEvent::Playback(PlaybackType::FromOrder)); + } else { + self.event_queue + .push_back(GlobalEvent::Playback(PlaybackType::Pattern)); + } + return; + } else if event.logical_key == Key::Named(NamedKey::F8) { + self.event_queue + .push_back(GlobalEvent::Playback(PlaybackType::Stop)); + return; + } + } + // key_event didn't start or stop the song, so process normally + if let Some(dialog) = dialog_manager.active_dialog_mut() { + match dialog.process_input(&event, modifiers, event_queue) { + DialogResponse::Close => { + dialog_manager.close_dialog(); + // if i close a pop_up i need to redraw the const part of the page as the pop-up overlapped it probably + ui_pages.request_draw_const(); + window.request_redraw(); + } + DialogResponse::RequestRedraw => window.request_redraw(), + DialogResponse::None => (), + } + } else { + if event.state.is_pressed() && event.logical_key == Key::Named(NamedKey::Escape) + { + event_queue.push(GlobalEvent::OpenDialog(Box::new(|| { + Box::new(PageMenu::main()) + }))); + } + + match ui_pages.process_key_event(&self.modifiers, &event, event_queue) { + PageResponse::RequestRedraw => window.request_redraw(), + PageResponse::None => (), + } + } + } + // not sure if i need it just to make sure i always have all current modifiers to be used with keyboard events + WindowEvent::ModifiersChanged(new_modifiers) => *modifiers = new_modifiers, + + _ => (), + } + + while let Some(event) = self.event_queue.pop_front() { + self.user_event(event_loop, event); + } + } + + /// i may need to add the ability for events to add events to the event queue, but that should be possible + fn user_event(&mut self, event_loop: &ActiveEventLoop, event: GlobalEvent) { + let event_queue = &mut EventQueue(&mut self.event_queue); + match event { + GlobalEvent::OpenDialog(dialog) => { + self.dialog_manager.open_dialog(dialog()); + _ = self.try_request_redraw(); + } + GlobalEvent::Page(c) => match self.ui_pages.process_page_event(c, event_queue) { + PageResponse::RequestRedraw => _ = self.try_request_redraw(), + PageResponse::None => (), + }, + GlobalEvent::Header(header_event) => { + self.header.process_event(header_event); + _ = self.try_request_redraw(); + } + GlobalEvent::GoToPage(pages_enum) => { + self.dialog_manager.close_all(); + self.ui_pages.switch_page(pages_enum); + _ = self.try_request_redraw(); + } + GlobalEvent::CloseApp => event_loop.exit(), + GlobalEvent::CloseRequested => Self::close_requested(event_queue), + GlobalEvent::ConstRedraw => { + self.ui_pages.request_draw_const(); + _ = self.try_request_redraw(); + } + GlobalEvent::Playback(playback_type) => { + let msg = match playback_type { + PlaybackType::Song => Some(ToWorkerMsg::Playback(PlaybackSettings::Order { + idx: 0, + should_loop: true, + })), + PlaybackType::Stop => Some(ToWorkerMsg::StopPlayback), + PlaybackType::Pattern => { + Some(ToWorkerMsg::Playback(self.header.play_current_pattern())) + } + PlaybackType::FromOrder => { + Some(ToWorkerMsg::Playback(self.header.play_current_order())) + } + PlaybackType::FromCursor => None, + }; + + if let Some(msg) = msg { + self.audio_stream + .as_mut() + .expect( + "audio stream should always be active, should still handle this error", + ) + .2 + .try_msg_worker(msg) + .expect("buffer full. either increase size or retry somehow") + } + } + #[cfg(feature = "accesskit")] + GlobalEvent::Accesskit(window_event) => { + use accesskit_winit::WindowEvent; + match window_event { + WindowEvent::InitialTreeRequested => { + // there probably should always be a window + // make sure we always respond to this event. I hope it can't be send when there is no window + let window = self.window.as_mut().unwrap(); + window.adapter.0.update_if_active(|| { + Self::produce_full_tree( + &self.ui_pages, + &self.header, + &self.dialog_manager, + window.adapter.1, + ) + }); + } + WindowEvent::ActionRequested(action_request) => todo!(), + // i don't have any extra state for accessability so i don't need to cleanup anything + WindowEvent::AccessibilityDeactivated => (), + } + } + } + } + + fn exiting(&mut self, _: &ActiveEventLoop) { + if let Some(workers) = self.worker_threads.take() { + // wait for all the threads to close + workers.close_all(); + } + if self.audio_stream.is_some() { + self.close_audio_stream(); + } + } +} + +impl App { + pub fn new(proxy: EventLoopProxy) -> Self { + Self { + window: None, + draw_buffer: DrawBuffer::new(), + modifiers: Modifiers::default(), + ui_pages: AllPages::new(proxy.clone()), + dialog_manager: DialogManager::new(), + header: Header::default(), + event_loop_proxy: proxy, + worker_threads: None, + audio_stream: None, + event_queue: VecDeque::with_capacity(3), + } + } + + // TODO: should this be its own function?? or is there something better + fn close_requested(events: &mut EventQueue<'_>) { + events.push(GlobalEvent::OpenDialog(Box::new(|| { + Box::new(ConfirmDialog::new( + "Close Torque Tracker?", + || Some(GlobalEvent::CloseApp), + || None, + )) + }))); + } + + /// tries to request a redraw. if there currently is no window this fails + fn try_request_redraw(&self) -> Result<(), ()> { + if let Some(window) = &self.window { + window.window.request_redraw(); + Ok(()) + } else { + Err(()) + } + } + + fn build_window(&mut self, event_loop: &ActiveEventLoop) { + self.window.get_or_insert_with(|| { + let mut attributes = WindowAttributes::default(); + attributes.active = true; + attributes.resizable = true; + attributes.resize_increments = None; + attributes.title = String::from("Torque Tracker"); + // for accesskit + attributes.visible = false; + + let window = Arc::new(event_loop.create_window(attributes).unwrap()); + let render_backend = + RenderBackend::new(window.clone(), render::palettes::Palette::CAMOUFLAGE); + + #[cfg(feature = "accesskit")] + let adapter = { + accesskit_winit::Adapter::with_event_loop_proxy( + event_loop, + &window, + self.event_loop_proxy.clone(), + ) + }; + + window.set_visible(true); + let size = render_backend.get_size(); + Window { + window, + render_backend, + #[cfg(feature = "accesskit")] + adapter: (adapter, create_transform(size)), + } + }); + } + + // TODO: make this configurable + fn start_audio_stream(&mut self) { + use cpal::StreamInstant; + + assert!(self.audio_stream.is_none()); + let host = cpal::default_host(); + let device = host.default_output_device().unwrap(); + let default_config = device.default_output_config().unwrap(); + let (config, buffer_size) = { + let mut config = default_config.config(); + let buffer_size = { + let default = default_config.buffer_size(); + match default { + SupportedBufferSize::Unknown => 1024, + SupportedBufferSize::Range { min, max } => u32::min(u32::max(1024, *min), *max), + } + }; + config.buffer_size = BufferSize::Fixed(buffer_size); + (config, buffer_size) + }; + let mut guard = SONG_MANAGER.lock_blocking(); + let (mut worker, buffer_time, status, stream_send) = guard + .get_callback::( + OutputConfig { + buffer_size, + channel_count: NonZero::new(config.channels).unwrap(), + sample_rate: NonZero::new(config.sample_rate.0).unwrap(), + }, + cpal::OutputStreamTimestamp { + callback: cpal::StreamInstant::new(0, 0), + playback: cpal::StreamInstant::new(0, 0), + }, + ); + // keep the guard as short as possible to not block the async threads + drop(guard); + let stream = device + .build_output_stream( + &config, + move |data, info| { + worker(data, info.timestamp()); + }, + |err| eprintln!("audio stream err: {err:?}"), + None, + ) + .unwrap(); + // spawn a task to process the audio playback status updates + let proxy = self.event_loop_proxy.clone(); + let task = EXECUTOR.spawn(async move { + let buffer_time = buffer_time; + let mut status_recv = status; + // maybe also send the timestamp every second or so + let mut old_playback = None; + let mut old_timestamp = OutputStreamTimestamp { + callback: StreamInstant::new(0, 0), + playback: StreamInstant::new(0, 0), + }; + loop { + smol::Timer::after(buffer_time).await; + let Some(read) = status_recv.try_get() else { + // we had a lock for way too long, so now we are behing and have to wait for the writer to finish + // writing once. Then locking will succeed again. + continue; + }; + let playback = read.0; + let timestamp = read.1; + // only react on status changes. could at some point be made more granular + if playback != old_playback { + old_playback = playback; + // println!("playback status: {status:?}"); + let pos = playback.map(|s| s.position); + proxy + .send_event(GlobalEvent::Header(HeaderEvent::SetPlayback(pos))) + .unwrap(); + let pos = playback.map(|s| (s.position.pattern, s.position.row)); + proxy + .send_event(GlobalEvent::Page(PageEvent::Pattern( + PatternPageEvent::PlaybackPosition(pos), + ))) + .unwrap(); + // does a map flatten. idk why it's called and_then + let pos = playback.and_then(|s| s.position.order); + proxy + .send_event(GlobalEvent::Page(PageEvent::OrderList( + OrderListPageEvent::SetPlayback(pos), + ))) + .unwrap(); + } + + // also check for changed timestamp + if timestamp != old_timestamp { + old_timestamp = timestamp; + // maybe at some point use the data + } + } + }); + self.audio_stream = Some((stream, task, stream_send)); + } + + fn close_audio_stream(&mut self) { + let (stream, task, mut stream_send) = self.audio_stream.take().unwrap(); + // stop playback + _ = stream_send.try_msg_worker(ToWorkerMsg::StopPlayback); + _ = stream_send.try_msg_worker(ToWorkerMsg::StopLiveNote); + // kill the task. using `cancel` doesn't make sense because it doesn't finishe anyways + drop(task); + // lastly kill the audio stream + drop(stream); + } + + #[cfg(feature = "accesskit")] + fn produce_full_tree( + pages: &AllPages, + header: &Header, + dialogs: &DialogManager, + transform: accesskit::Affine, + ) -> accesskit::TreeUpdate { + use accesskit::TreeUpdate; + use accesskit::{Node, NodeId, Role, Tree}; + const ROOT_ID: NodeId = NodeId(0); + + let tree = Tree { + root: ROOT_ID, + toolkit_name: Some(String::from("Torque Tracker Custom")), + toolkit_version: None, + }; + let mut root_node = Node::new(Role::Window); + let mut nodes = Vec::new(); + root_node.set_label("Torque Tracker"); + root_node.set_language("English"); + let header_id = header.build_tree(&mut nodes); + root_node.push_child(header_id); + root_node.set_transform(transform); + + let resp = pages.build_tree(&mut nodes); + let mut focused = resp.selected; + root_node.push_child(resp.root); + + if let Some(dialog) = dialogs.active_dialog() { + let resp = dialog.build_tree(&mut nodes); + root_node.push_child(resp.root); + focused = resp.selected; + } + + nodes.push((ROOT_ID, root_node)); + TreeUpdate { + nodes, + tree: Some(tree), + focus: focused, + } + } +} + +impl Drop for App { + fn drop(&mut self) { + if self.audio_stream.is_some() { + self.close_audio_stream(); + } + } +} + +#[cfg(feature = "accesskit")] +pub struct AccessResponse { + pub root: accesskit::NodeId, + pub selected: accesskit::NodeId, +} + +#[cfg(feature = "accesskit")] +fn create_transform(size: winit::dpi::PhysicalSize) -> accesskit::Affine { + accesskit::Affine::scale_non_uniform( + size.width as f64 / crate::coordinates::WINDOW_SIZE_CHARS.0 as f64, + size.height as f64 / crate::coordinates::WINDOW_SIZE_CHARS.1 as f64, + ) +} fn main() { - app::run(); + let event_loop = winit::event_loop::EventLoop::::with_user_event() + .build() + .unwrap(); + event_loop.set_control_flow(ControlFlow::Wait); + // i don't need any raw device events. Keyboard and Mouse coming as window events are enough + event_loop.listen_device_events(winit::event_loop::DeviceEvents::Never); + let event_loop_proxy = event_loop.create_proxy(); + let mut app = App::new(event_loop_proxy); + app.header.draw_constant(&mut app.draw_buffer); + + event_loop.run_app(&mut app).unwrap(); } diff --git a/src/ui/pages/help_page.rs b/src/pages/help_page.rs similarity index 86% rename from src/ui/pages/help_page.rs rename to src/pages/help_page.rs index 34b69f7..72a711d 100644 --- a/src/ui/pages/help_page.rs +++ b/src/pages/help_page.rs @@ -1,4 +1,4 @@ -use crate::{app::EventQueue, coordinates::CharRect, draw_buffer::DrawBuffer}; +use crate::{EventQueue, coordinates::CharRect, draw_buffer::DrawBuffer}; use super::{Page, PageResponse}; @@ -24,7 +24,7 @@ impl Page for HelpPage { fn build_tree( &self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, - ) -> crate::app::AccessResponse { + ) -> crate::AccessResponse { todo!() } } diff --git a/src/ui/pages.rs b/src/pages/mod.rs similarity index 98% rename from src/ui/pages.rs rename to src/pages/mod.rs index c97c576..2414ba3 100644 --- a/src/ui/pages.rs +++ b/src/pages/mod.rs @@ -16,10 +16,10 @@ use winit::{ }; use crate::{ - app::{EventQueue, GlobalEvent}, + EventQueue, GlobalEvent, coordinates::{CharPosition, CharRect, WINDOW_SIZE_CHARS}, draw_buffer::DrawBuffer, - ui::pages::sample_list::SampleListEvent, + pages::sample_list::SampleListEvent, }; pub trait Page { @@ -38,7 +38,7 @@ pub trait Page { fn build_tree( &self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, - ) -> crate::app::AccessResponse; + ) -> crate::AccessResponse; } pub enum PageResponse { @@ -241,7 +241,7 @@ impl AllPages { pub fn build_tree( &self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, - ) -> crate::app::AccessResponse { + ) -> crate::AccessResponse { self.get_page().build_tree(tree) } } diff --git a/src/ui/pages/order_list.rs b/src/pages/order_list.rs similarity index 98% rename from src/ui/pages/order_list.rs rename to src/pages/order_list.rs index 1a181e9..facbecd 100644 --- a/src/ui/pages/order_list.rs +++ b/src/pages/order_list.rs @@ -1,17 +1,16 @@ use std::str::from_utf8; use std::{array, io::Write}; -use torque_tracker_engine::channel::Pan; -use torque_tracker_engine::project::song::SongOperation; +use torque_tracker_engine::project::song::{Pan, SongOperation}; use torque_tracker_engine::{file::impulse_format::header::PatternOrder, project::song::Song}; use winit::keyboard::{Key, ModifiersState, NamedKey}; -use crate::app::{EventQueue, GlobalEvent, send_song_op}; -use crate::ui::header::HeaderEvent; -use crate::ui::widgets::{NextWidget, StandardResponse, Widget}; +use crate::header::HeaderEvent; +use crate::widgets::{NextWidget, StandardResponse, Widget}; +use crate::{EventQueue, GlobalEvent, send_song_op}; use crate::{ coordinates::{CharPosition, CharRect}, - ui::widgets::slider::Slider, + widgets::slider::Slider, }; use super::{Page, PageEvent, PageResponse}; @@ -560,7 +559,7 @@ impl Page for OrderListPage { fn build_tree( &self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, - ) -> crate::app::AccessResponse { + ) -> crate::AccessResponse { todo!() } } diff --git a/src/ui/pages/pattern.rs b/src/pages/pattern.rs similarity index 99% rename from src/ui/pages/pattern.rs rename to src/pages/pattern.rs index ebe2304..2e34932 100644 --- a/src/ui/pages/pattern.rs +++ b/src/pages/pattern.rs @@ -12,9 +12,10 @@ use winit::{ }; use crate::{ - app::{EXECUTOR, EventQueue, GlobalEvent, SONG_MANAGER, send_song_op}, + EXECUTOR, EventQueue, GlobalEvent, SONG_MANAGER, coordinates::{CharPosition, CharRect}, - ui::header::HeaderEvent, + header::HeaderEvent, + send_song_op, }; use super::{Page, PageResponse}; @@ -654,7 +655,7 @@ impl Page for PatternPage { fn build_tree( &self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, - ) -> crate::app::AccessResponse { + ) -> crate::AccessResponse { todo!() } } diff --git a/src/ui/pages/sample_list.rs b/src/pages/sample_list.rs similarity index 98% rename from src/ui/pages/sample_list.rs rename to src/pages/sample_list.rs index 8552b1d..4b495dc 100644 --- a/src/ui/pages/sample_list.rs +++ b/src/pages/sample_list.rs @@ -15,13 +15,11 @@ use torque_tracker_engine::{ use winit::keyboard::{Key, NamedKey}; use crate::{ - app::{EXECUTOR, EventQueue, GlobalEvent, SONG_OP_SEND}, + EXECUTOR, EventQueue, GlobalEvent, SONG_OP_SEND, coordinates::{CharPosition, CharRect}, draw_buffer::DrawBuffer, - ui::{ - header::HeaderEvent, - pages::{Page, PageEvent, PageResponse, pattern::PatternPageEvent}, - }, + header::HeaderEvent, + pages::{Page, PageEvent, PageResponse, pattern::PatternPageEvent}, }; #[derive(Debug, Clone)] @@ -368,7 +366,7 @@ impl Page for SampleList { fn build_tree( &self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, - ) -> crate::app::AccessResponse { + ) -> crate::AccessResponse { todo!() } } diff --git a/src/ui/pages/song_directory_config_page.rs b/src/pages/song_directory_config_page.rs similarity index 99% rename from src/ui/pages/song_directory_config_page.rs rename to src/pages/song_directory_config_page.rs index 48a0474..95ef3e4 100644 --- a/src/ui/pages/song_directory_config_page.rs +++ b/src/pages/song_directory_config_page.rs @@ -3,10 +3,11 @@ use std::num::NonZero; use torque_tracker_engine::project::song::SongOperation; use crate::{ - app::{EventQueue, GlobalEvent, send_song_op}, + EventQueue, GlobalEvent, coordinates::{CharPosition, CharRect}, draw_buffer::DrawBuffer, - ui::widgets::{NextWidget, Widget, slider::Slider, text_in::TextIn}, + send_song_op, + widgets::{NextWidget, Widget, slider::Slider, text_in::TextIn}, }; use super::{Page, PageResponse}; @@ -181,10 +182,10 @@ impl Page for SongDirectoryConfigPage { fn build_tree( &self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, - ) -> crate::app::AccessResponse { + ) -> crate::AccessResponse { use accesskit::{Node, NodeId, Role}; - use crate::app::AccessResponse; + use crate::AccessResponse; let mut root_node = Node::new(Role::Menu); let nodes = [ diff --git a/src/gpu.rs b/src/render/gpu.rs similarity index 99% rename from src/gpu.rs rename to src/render/gpu.rs index 862b7f4..432cbc5 100644 --- a/src/gpu.rs +++ b/src/render/gpu.rs @@ -13,9 +13,9 @@ use wgpu::{ }; use winit::window::Window; -use crate::palettes::{PALETTE_SIZE, Palette, RGB10A2}; +use super::palettes::{PALETTE_SIZE, Palette, RGB10A2}; -use super::coordinates::WINDOW_SIZE; +use crate::coordinates::WINDOW_SIZE; pub struct GPUState { surface: Surface<'static>, diff --git a/src/render.rs b/src/render/mod.rs similarity index 93% rename from src/render.rs rename to src/render/mod.rs index 5e62042..380bc4c 100644 --- a/src/render.rs +++ b/src/render/mod.rs @@ -1,11 +1,13 @@ +#[cfg(feature = "gpu_scaling")] +pub mod gpu; +pub mod palettes; + use std::sync::Arc; use winit::{dpi::PhysicalSize, event_loop::ActiveEventLoop, window::Window}; -use crate::{ - coordinates::WINDOW_SIZE, - palettes::{Palette, RGB8}, -}; +use crate::coordinates::WINDOW_SIZE; +use palettes::{Palette, RGB8}; #[cfg(not(any(feature = "gpu_scaling", feature = "soft_scaling")))] compile_error!("at least one of gpu_scaling or soft_scaling needs to be active"); @@ -58,13 +60,13 @@ impl BothRenderBackend { #[cfg(feature = "gpu_scaling")] pub struct GPURenderBackend { - backend: crate::gpu::GPUState, + backend: gpu::GPUState, } #[cfg(feature = "gpu_scaling")] impl GPURenderBackend { fn try_new(window: Arc, palette: Palette) -> Result { - let mut backend = smol::block_on(crate::gpu::GPUState::new(window))?; + let mut backend = smol::block_on(gpu::GPUState::new(window))?; backend.queue_palette_update(palette.into()); Ok(Self { backend }) @@ -96,12 +98,13 @@ impl GPURenderBackend { } } +// softscaling is small enough to not have its own file / module #[cfg(feature = "soft_scaling")] pub struct SoftRenderBackend { backend: softbuffer::Surface, Arc>, width: u32, height: u32, - palette: Palette, + palette: Palette, } #[cfg(feature = "soft_scaling")] diff --git a/src/palettes.rs b/src/render/palettes.rs similarity index 100% rename from src/palettes.rs rename to src/render/palettes.rs diff --git a/src/shader.wgsl b/src/render/shader.wgsl similarity index 100% rename from src/shader.wgsl rename to src/render/shader.wgsl diff --git a/src/ui/.DS_Store b/src/ui/.DS_Store deleted file mode 100644 index 134c854a99e0cf05c3d86792859fd16f72d2ae37..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKy-veG4EB`*K`>N^fiW!itdk0<;7Lq(-R(h=C@Y(a}OMiwcZ(0TPOx__#!mUGy?d+**#4yE`k|H@#tNA>(iK7X06z(e9QxTxna zxo&!t^Vr&1KhQjC`;>E;?^Vs`mGdz%28;n?;HMZs&1MN!1#L72i~(a{$^d^KLMUUT zSP1%02L|5)0NXHo!Mwv0V5k@=7J^uTI0*$xsM8k1NjU7G`bCO`poEjt=ELd9PCFFm zPsjeDcPAGK+Gq?I162kVd*>C9SQEo8wv=LXV&< o>{kd*Loo5B7`|MJH=$l&4|xKN6bnIEAoe5RX|TZ<_*Diz0rO^W8UO$Q diff --git a/src/ui/mod.rs b/src/ui/mod.rs deleted file mode 100644 index ae379c3..0000000 --- a/src/ui/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod dialog; -pub mod header; -pub mod pages; -pub mod widgets; diff --git a/src/ui/widgets/button.rs b/src/widgets/button.rs similarity index 99% rename from src/ui/widgets/button.rs rename to src/widgets/button.rs index bff32b3..8c4f619 100644 --- a/src/ui/widgets/button.rs +++ b/src/widgets/button.rs @@ -1,7 +1,7 @@ use winit::keyboard::{Key, NamedKey}; use crate::{ - app::EventQueue, + EventQueue, coordinates::{CharPosition, CharRect}, draw_buffer::DrawBuffer, }; diff --git a/src/ui/widgets.rs b/src/widgets/mod.rs similarity index 98% rename from src/ui/widgets.rs rename to src/widgets/mod.rs index 809bfc6..865dfdd 100644 --- a/src/ui/widgets.rs +++ b/src/widgets/mod.rs @@ -10,7 +10,7 @@ use winit::{ keyboard::{Key, ModifiersState, NamedKey}, }; -use crate::{app::EventQueue, draw_buffer::DrawBuffer, ui::pages::PageResponse}; +use crate::{EventQueue, draw_buffer::DrawBuffer, pages::PageResponse}; pub(crate) trait Widget { type Response; diff --git a/src/ui/widgets/slider.rs b/src/widgets/slider.rs similarity index 99% rename from src/ui/widgets/slider.rs rename to src/widgets/slider.rs index 625ebc7..3017faf 100644 --- a/src/ui/widgets/slider.rs +++ b/src/widgets/slider.rs @@ -6,10 +6,10 @@ use std::{ use winit::keyboard::{Key, NamedKey}; use crate::{ - app::{EventQueue, GlobalEvent}, + EventQueue, GlobalEvent, coordinates::{CharPosition, CharRect, FONT_SIZE, PixelRect, WINDOW_SIZE_CHARS}, + dialog::slider_dialog::SliderDialog, draw_buffer::DrawBuffer, - ui::dialog::slider_dialog::SliderDialog, }; use super::{NextWidget, StandardResponse, Widget, WidgetResponse}; diff --git a/src/ui/widgets/text_in.rs b/src/widgets/text_in.rs similarity index 99% rename from src/ui/widgets/text_in.rs rename to src/widgets/text_in.rs index ea8857c..c8eafe4 100644 --- a/src/ui/widgets/text_in.rs +++ b/src/widgets/text_in.rs @@ -3,7 +3,7 @@ use font8x8::UnicodeFonts; use winit::keyboard::{Key, NamedKey}; use crate::{ - app::EventQueue, + EventQueue, coordinates::{CharPosition, WINDOW_SIZE}, draw_buffer::DrawBuffer, }; diff --git a/src/ui/widgets/text_in_scroll.rs b/src/widgets/text_in_scroll.rs similarity index 99% rename from src/ui/widgets/text_in_scroll.rs rename to src/widgets/text_in_scroll.rs index 26b6f22..6359e01 100644 --- a/src/ui/widgets/text_in_scroll.rs +++ b/src/widgets/text_in_scroll.rs @@ -3,10 +3,10 @@ use font8x8::UnicodeFonts; use winit::keyboard::{Key, NamedKey}; use crate::{ - app::EventQueue, + EventQueue, coordinates::{CharPosition, WINDOW_SIZE}, draw_buffer::DrawBuffer, - ui::widgets::StandardResponse, + widgets::StandardResponse, }; use super::{NextWidget, Widget, WidgetResponse}; diff --git a/src/ui/widgets/toggle.rs b/src/widgets/toggle.rs similarity index 99% rename from src/ui/widgets/toggle.rs rename to src/widgets/toggle.rs index 3e06bdc..b90323c 100644 --- a/src/ui/widgets/toggle.rs +++ b/src/widgets/toggle.rs @@ -1,7 +1,7 @@ use winit::keyboard::{Key, NamedKey}; use crate::{ - app::EventQueue, + EventQueue, coordinates::{CharPosition, CharRect, WINDOW_SIZE}, draw_buffer::DrawBuffer, }; diff --git a/src/ui/widgets/toggle_button.rs b/src/widgets/toggle_button.rs similarity index 95% rename from src/ui/widgets/toggle_button.rs rename to src/widgets/toggle_button.rs index 9a5ae38..9b2cfc9 100644 --- a/src/ui/widgets/toggle_button.rs +++ b/src/widgets/toggle_button.rs @@ -1,6 +1,6 @@ use std::{cell::Cell, rc::Rc}; -use crate::{app::EventQueue, coordinates::CharRect, draw_buffer::DrawBuffer}; +use crate::{EventQueue, coordinates::CharRect, draw_buffer::DrawBuffer}; use super::{NextWidget, Widget, WidgetResponse, button::Button}; From 46961870893352321e15934a33eb59400bd64a60 Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Fri, 3 Oct 2025 10:27:47 +0200 Subject: [PATCH 11/19] simplify UI widgets again, now that each page is custom anyway --- src/dialog/confirm.rs | 110 ++++---- src/dialog/slider_dialog.rs | 17 +- src/draw_buffer.rs | 3 +- src/main.rs | 4 +- src/pages/order_list.rs | 84 +++--- src/pages/song_directory_config_page.rs | 119 ++++++--- src/widgets/button.rs | 33 +-- src/widgets/mod.rs | 124 ++++----- src/widgets/slider.rs | 39 +-- src/widgets/text_in.rs | 330 +++++++++++++++++------- src/widgets/text_in_scroll.rs | 217 ---------------- src/widgets/toggle.rs | 19 +- src/widgets/toggle_button.rs | 30 +-- 13 files changed, 543 insertions(+), 586 deletions(-) delete mode 100644 src/widgets/text_in_scroll.rs diff --git a/src/dialog/confirm.rs b/src/dialog/confirm.rs index 85478f2..3b7a24f 100644 --- a/src/dialog/confirm.rs +++ b/src/dialog/confirm.rs @@ -4,7 +4,7 @@ use crate::{ GlobalEvent, coordinates::{CharPosition, CharRect}, draw_buffer::DrawBuffer, - widgets::{NextWidget, StandardResponse, Widget, WidgetResponse, button::Button}, + widgets::{NextWidget, Widget, WidgetResponse, button::Button}, }; use super::{Dialog, DialogResponse}; @@ -14,8 +14,8 @@ pub struct ConfirmDialog { text_pos: CharPosition, // computed from the string length rect: CharRect, - ok: Button>, - cancel: Button>, + ok: (Button, Option), + cancel: (Button, Option), selected: u8, } @@ -28,8 +28,8 @@ impl ConfirmDialog { const CANCEL: u8 = 2; pub fn new( text: &'static str, - ok_event: fn() -> Option, - cancel_event: fn() -> Option, + ok_event: Option, + cancel_event: Option, ) -> Self { let width = (text.len() + 10).max(22); let per_side = width / 2; @@ -37,33 +37,37 @@ impl ConfirmDialog { text, text_pos: CharPosition::new(40 - per_side + 5, 27), selected: Self::OK, - ok: Button::new( - " Ok", - Self::OK_RECT, - NextWidget { - left: Some(Self::CANCEL), - right: Some(Self::CANCEL), - tab: Some(Self::CANCEL), - shift_tab: Some(Self::CANCEL), - ..Default::default() - }, + ok: ( + Button::new( + " Ok", + Self::OK_RECT, + NextWidget { + left: Some(Self::CANCEL), + right: Some(Self::CANCEL), + tab: Some(Self::CANCEL), + shift_tab: Some(Self::CANCEL), + ..Default::default() + }, + #[cfg(feature = "accesskit")] + accesskit::NodeId(Self::DIALOG_ID + u64::from(Self::OK) * 20), + ), ok_event, - #[cfg(feature = "accesskit")] - accesskit::NodeId(Self::DIALOG_ID + u64::from(Self::OK) * 20), ), - cancel: Button::new( - "Cancel", - Self::CANCEL_RECT, - NextWidget { - left: Some(Self::OK), - right: Some(Self::OK), - tab: Some(Self::OK), - shift_tab: Some(Self::OK), - ..Default::default() - }, + cancel: ( + Button::new( + "Cancel", + Self::CANCEL_RECT, + NextWidget { + left: Some(Self::OK), + right: Some(Self::OK), + tab: Some(Self::OK), + shift_tab: Some(Self::OK), + ..Default::default() + }, + #[cfg(feature = "accesskit")] + accesskit::NodeId(Self::DIALOG_ID + u64::from(Self::CANCEL) * 20), + ), cancel_event, - #[cfg(feature = "accesskit")] - accesskit::NodeId(Self::DIALOG_ID + u64::from(Self::CANCEL) * 20), ), rect: CharRect::new(25, 32, 40 - per_side, 40 + per_side), } @@ -75,8 +79,10 @@ impl Dialog for ConfirmDialog { draw_buffer.draw_rect(2, self.rect); draw_buffer.draw_out_border(self.rect, 3, 3, 2); draw_buffer.draw_string(self.text, self.text_pos, 0, 2); - self.ok.draw(draw_buffer, self.selected == Self::OK); - self.cancel.draw(draw_buffer, self.selected == Self::CANCEL); + self.ok.0.draw(draw_buffer, self.selected == Self::OK); + self.cancel + .0 + .draw(draw_buffer, self.selected == Self::CANCEL); } fn process_input( @@ -89,26 +95,38 @@ impl Dialog for ConfirmDialog { return DialogResponse::Close; } - let WidgetResponse { standard, extra } = match self.selected { - Self::OK => self.ok.process_input(modifiers, key_event, events), - Self::CANCEL => self.cancel.process_input(modifiers, key_event, events), + let response = match self.selected { + Self::OK => { + let resp = self.ok.0.process_input(modifiers, key_event, events); + if resp == WidgetResponse::RequestRedraw(true) { + if let Some(event) = self.ok.1.take() { + events.push(event); + } + return DialogResponse::Close; + } + resp + } + Self::CANCEL => { + let resp = self.cancel.0.process_input(modifiers, key_event, events); + if resp == WidgetResponse::RequestRedraw(true) { + if let Some(event) = self.cancel.1.take() { + events.push(event); + } + return DialogResponse::Close; + } + resp + } _ => unreachable!(), }; - if let Some(global_option) = extra { - if let Some(global) = global_option { - events.push(global); - } - // if there is a response i also want to close myself - return DialogResponse::Close; - } - match standard { - StandardResponse::SwitchFocus(next) => { - self.selected = next; + match response { + WidgetResponse::SwitchFocus(i) => { + self.selected = i; DialogResponse::RequestRedraw } - StandardResponse::RequestRedraw => DialogResponse::RequestRedraw, - StandardResponse::None => DialogResponse::None, + // always false here, because otherwise we return earlier, but it doesn't matter + WidgetResponse::RequestRedraw(_) => DialogResponse::RequestRedraw, + WidgetResponse::None => DialogResponse::None, } } diff --git a/src/dialog/slider_dialog.rs b/src/dialog/slider_dialog.rs index ee385fe..90be49b 100644 --- a/src/dialog/slider_dialog.rs +++ b/src/dialog/slider_dialog.rs @@ -9,13 +9,13 @@ use crate::{ EventQueue, GlobalEvent, coordinates::{CharPosition, CharRect}, draw_buffer::DrawBuffer, - widgets::{NextWidget, StandardResponse, Widget, text_in::TextIn}, + widgets::{NextWidget, Widget, WidgetResponse, text_in::TextIn}, }; use super::{Dialog, DialogResponse}; pub struct SliderDialog { - text: TextIn<()>, + text: TextIn, range: RangeInclusive, return_event: fn(i16) -> GlobalEvent, } @@ -48,15 +48,11 @@ impl Dialog for SliderDialog { } } - match self - .text - .process_input(modifiers, key_event, events) - .standard - { + match self.text.process_input(modifiers, key_event, events) { // cant switch focus as this is the only widget - StandardResponse::SwitchFocus(_) => DialogResponse::None, - StandardResponse::RequestRedraw => DialogResponse::RequestRedraw, - StandardResponse::None => DialogResponse::None, + WidgetResponse::SwitchFocus(_) => DialogResponse::None, + WidgetResponse::RequestRedraw(_) => DialogResponse::RequestRedraw, + WidgetResponse::None => DialogResponse::None, } } @@ -102,7 +98,6 @@ impl SliderDialog { CharPosition::new(45, 26), 3, NextWidget::default(), - |_| {}, #[cfg(feature = "accesskit")] (accesskit::NodeId(Self::NODE_ID + 20), "value"), ); diff --git a/src/draw_buffer.rs b/src/draw_buffer.rs index 6ea614b..642a0c6 100644 --- a/src/draw_buffer.rs +++ b/src/draw_buffer.rs @@ -65,10 +65,11 @@ impl DrawBuffer { &mut self, string: &str, position: CharPosition, - lenght: usize, + lenght: u8, fg_color: u8, bg_color: u8, ) { + let lenght = usize::from(lenght); if string.len() > lenght { self.draw_string(&string[..=lenght], position, fg_color, bg_color) } else { diff --git a/src/main.rs b/src/main.rs index f7f864a..7acf801 100644 --- a/src/main.rs +++ b/src/main.rs @@ -518,8 +518,8 @@ impl App { events.push(GlobalEvent::OpenDialog(Box::new(|| { Box::new(ConfirmDialog::new( "Close Torque Tracker?", - || Some(GlobalEvent::CloseApp), - || None, + Some(GlobalEvent::CloseApp), + None, )) }))); } diff --git a/src/pages/order_list.rs b/src/pages/order_list.rs index facbecd..eb6f0ec 100644 --- a/src/pages/order_list.rs +++ b/src/pages/order_list.rs @@ -1,12 +1,13 @@ use std::str::from_utf8; use std::{array, io::Write}; +use symphonia::core::conv::IntoSample; use torque_tracker_engine::project::song::{Pan, SongOperation}; use torque_tracker_engine::{file::impulse_format::header::PatternOrder, project::song::Song}; use winit::keyboard::{Key, ModifiersState, NamedKey}; use crate::header::HeaderEvent; -use crate::widgets::{NextWidget, StandardResponse, Widget}; +use crate::widgets::{NextWidget, Widget, WidgetResponse}; use crate::{EventQueue, GlobalEvent, send_song_op}; use crate::{ coordinates::{CharPosition, CharRect}, @@ -17,8 +18,8 @@ use super::{Page, PageEvent, PageResponse}; #[derive(Debug, Clone)] pub enum OrderListPageEvent { - SetVolumeCurrent(i16), - SetPanCurrent(i16), + SetVolumeCurrent(u8), + SetPanCurrent(u8), SetPlayback(Option), } @@ -49,8 +50,8 @@ pub struct OrderListPage { order_draw: u16, order_playback: Option, pattern_order: [PatternOrder; Song::MAX_ORDERS], - volume: [Slider<0, 64, ()>; 64], - pan: [Slider<0, 64, ()>; 64], + volume: [Slider<0, 64>; 64], + pan: [Slider<0, 64>; 64], } impl OrderListPage { @@ -78,14 +79,9 @@ impl OrderListPage { NextWidget::default(), |value| { GlobalEvent::Page(PageEvent::OrderList( - OrderListPageEvent::SetVolumeCurrent(value), + OrderListPageEvent::SetVolumeCurrent(value.try_into().unwrap()), )) }, - move |vol| { - // impossible to trigger as long as the slider is correct - let vol = u8::try_from(vol).unwrap(); - send_song_op(SongOperation::SetVolume(idx, vol)); - }, #[cfg(feature = "accesskit")] ( accesskit::NodeId(Self::PAGE_ID + u64::try_from(idx).unwrap()), @@ -107,15 +103,9 @@ impl OrderListPage { NextWidget::default(), |value| { GlobalEvent::Page(PageEvent::OrderList(OrderListPageEvent::SetPanCurrent( - value, + u8::try_from(value).unwrap(), ))) }, - move |pan| { - // surround and disabled not supported yet - // This panic can't be triggered as long as the slider is correct. - let pan = Pan::Value(u8::try_from(pan).unwrap()); - send_song_op(SongOperation::SetPan(idx, pan)); - }, #[cfg(feature = "accesskit")] ( accesskit::NodeId(Self::PAGE_ID + u64::try_from(idx).unwrap()), @@ -136,8 +126,9 @@ impl OrderListPage { Cursor::VolPan(c) => c, }; self.volume[usize::from(cursor - 1)] - .try_set(vol) + .try_set(i16::from(vol)) .expect("the value was created from the slider, so it has to fit."); + send_song_op(SongOperation::SetVolume(cursor - 1, vol)); } OrderListPageEvent::SetPanCurrent(pan) => { let cursor = match self.cursor { @@ -147,8 +138,9 @@ impl OrderListPage { Cursor::VolPan(c) => c, }; self.pan[usize::from(cursor - 1)] - .try_set(pan) - .expect("the event was created from the slider, so has to fit.") + .try_set(i16::from(pan)) + .expect("the event was created from the slider, so has to fit."); + send_song_op(SongOperation::SetPan(cursor - 1, Pan::Value(pan))); } OrderListPageEvent::SetPlayback(o) => self.order_playback = o, }; @@ -533,22 +525,40 @@ impl Page for OrderListPage { } match self.mode { - Mode::Panning => match self.pan[usize::from(c - 1)] - .process_input(modifiers, key_event, events) - .standard - { - StandardResponse::SwitchFocus(_) => return PageResponse::None, - StandardResponse::RequestRedraw => return PageResponse::RequestRedraw, - StandardResponse::None => return PageResponse::None, - }, - Mode::Volume => match self.volume[usize::from(c - 1)] - .process_input(modifiers, key_event, events) - .standard - { - StandardResponse::SwitchFocus(_) => return PageResponse::None, - StandardResponse::RequestRedraw => return PageResponse::RequestRedraw, - StandardResponse::None => return PageResponse::None, - }, + Mode::Panning => { + let slider = &mut self.pan[usize::from(c - 1)]; + match slider.process_input(modifiers, key_event, events) { + // i catch these earlier + WidgetResponse::SwitchFocus(_) => return PageResponse::None, + WidgetResponse::RequestRedraw(changed) => { + if changed { + send_song_op(SongOperation::SetPan( + c, + Pan::Value(u8::try_from(slider.get()).unwrap()), + )); + } + return PageResponse::RequestRedraw; + } + WidgetResponse::None => return PageResponse::None, + } + } + Mode::Volume => { + let slider = &mut self.volume[usize::from(c - 1)]; + match slider.process_input(modifiers, key_event, events) { + // i catch these earlier + WidgetResponse::SwitchFocus(_) => return PageResponse::None, + WidgetResponse::RequestRedraw(changed) => { + if changed { + send_song_op(SongOperation::SetVolume( + c, + u8::try_from(slider.get()).unwrap(), + )); + } + return PageResponse::RequestRedraw; + } + WidgetResponse::None => return PageResponse::None, + } + } } } } diff --git a/src/pages/song_directory_config_page.rs b/src/pages/song_directory_config_page.rs index 95ef3e4..be21542 100644 --- a/src/pages/song_directory_config_page.rs +++ b/src/pages/song_directory_config_page.rs @@ -7,7 +7,11 @@ use crate::{ coordinates::{CharPosition, CharRect}, draw_buffer::DrawBuffer, send_song_op, - widgets::{NextWidget, Widget, slider::Slider, text_in::TextIn}, + widgets::{ + NextWidget, Widget, WidgetResponse, + slider::Slider, + text_in::{TextIn, TextInScroll}, + }, }; use super::{Page, PageResponse}; @@ -72,10 +76,11 @@ pub enum SDCChange { pub struct SongDirectoryConfigPage { // widgets: WidgetList, - song_name: TextIn<()>, - initial_tempo: Slider<31, 255, ()>, - initial_speed: Slider<1, 255, ()>, - global_volume: Slider<0, 128, ()>, + song_name: TextIn, + initial_tempo: Slider<31, 255>, + initial_speed: Slider<1, 255>, + global_volume: Slider<0, 128>, + instrument_path: TextInScroll, selected: u8, } @@ -90,6 +95,8 @@ impl Page for SongDirectoryConfigPage { .draw(draw_buffer, Self::INITIAL_SPEED == self.selected); self.global_volume .draw(draw_buffer, Self::GLOBAL_VOLUME == self.selected); + self.instrument_path + .draw(draw_buffer, Self::INSTRUMENT_PATH == self.selected); } fn draw_constant(&mut self, draw_buffer: &mut DrawBuffer) { @@ -162,20 +169,29 @@ impl Page for SongDirectoryConfigPage { events: &mut EventQueue<'_>, ) -> PageResponse { let resp = match self.selected { - Self::SONG_NAME => self.song_name.process_input(modifiers, key_event, events), + Self::SONG_NAME => self + .song_name + .process_input(modifiers, key_event, events) + .on_change(|| self.song_name_changed()), Self::INITIAL_TEMPO => self .initial_tempo - .process_input(modifiers, key_event, events), + .process_input(modifiers, key_event, events) + .on_change(|| self.initial_tempo_changed()), Self::INITIAL_SPEED => self .initial_speed - .process_input(modifiers, key_event, events), + .process_input(modifiers, key_event, events) + .on_change(|| self.initial_tempo_changed()), Self::GLOBAL_VOLUME => self .global_volume + .process_input(modifiers, key_event, events) + .on_change(|| self.global_volume_changed()), + Self::INSTRUMENT_PATH => self + .instrument_path .process_input(modifiers, key_event, events), _ => unreachable!(), }; - resp.standard.to_page_resp(&mut self.selected) + resp.to_page_resp(&mut self.selected) } #[cfg(feature = "accesskit")] @@ -220,22 +236,39 @@ impl SongDirectoryConfigPage { const INITIAL_SPEED_ID: u64 = Self::PAGE_ID + Self::INITIAL_SPEED as u64 * 20; const GLOBAL_VOLUME: u8 = 4; const GLOBAL_VOLUME_ID: u64 = Self::PAGE_ID + Self::GLOBAL_VOLUME as u64 * 20; + const INSTRUMENT_PATH: u8 = 5; pub fn ui_change(&mut self, change: SDCChange) -> PageResponse { match change { - SDCChange::SetSongName(s) => match self.song_name.set_string(s) { + SDCChange::SetSongName(s) => match self + .song_name + .set_string(s) + .map(|_| self.song_name_changed()) + { Ok(_) => PageResponse::RequestRedraw, Err(_) => PageResponse::None, }, - SDCChange::InitialTempo(n) => match self.initial_tempo.try_set(n) { + SDCChange::InitialTempo(n) => match self + .initial_tempo + .try_set(n) + .map(|_| self.initial_tempo_changed()) + { Ok(_) => PageResponse::RequestRedraw, Err(_) => PageResponse::None, }, - SDCChange::InitialSpeed(n) => match self.initial_speed.try_set(n) { + SDCChange::InitialSpeed(n) => match self + .initial_speed + .try_set(n) + .map(|_| self.initial_speed_changed()) + { Ok(_) => PageResponse::RequestRedraw, Err(_) => PageResponse::None, }, - SDCChange::GlobalVolume(n) => match self.global_volume.try_set(n) { + SDCChange::GlobalVolume(n) => match self + .global_volume + .try_set(n) + .map(|_| self.global_volume_changed()) + { Ok(_) => PageResponse::RequestRedraw, Err(_) => PageResponse::None, }, @@ -250,6 +283,28 @@ impl SongDirectoryConfigPage { } } + fn song_name_changed(&self) { + println!("song name: {}", self.song_name.get_str()) + } + + fn initial_tempo_changed(&self) { + send_song_op(SongOperation::SetInitialTempo( + NonZero::new(u8::try_from(self.initial_tempo.get()).unwrap()).unwrap(), + )); + } + + fn initial_speed_changed(&self) { + send_song_op(SongOperation::SetInitialSpeed( + NonZero::new(u8::try_from(self.initial_speed.get()).unwrap()).unwrap(), + )); + } + + fn global_volume_changed(&self) { + send_song_op(SongOperation::SetGlobalVol( + u8::try_from(self.global_volume.get()).unwrap(), + )); + } + pub fn new() -> Self { let song_name = TextIn::new( CharPosition::new(17, 16), @@ -259,7 +314,6 @@ impl SongDirectoryConfigPage { tab: Some(Self::INITIAL_TEMPO), ..Default::default() }, - |s| println!("new song name: {}", s), #[cfg(feature = "accesskit")] ( accesskit::NodeId(Self::PAGE_ID + u64::from(Self::SONG_NAME) * 20), @@ -278,11 +332,6 @@ impl SongDirectoryConfigPage { ..Default::default() }, |n| GlobalEvent::Page(super::PageEvent::Sdc(SDCChange::InitialTempo(n))), - |value| { - send_song_op(SongOperation::SetInitialTempo( - NonZero::new(u8::try_from(value).unwrap()).unwrap(), - )); - }, #[cfg(feature = "accesskit")] ( accesskit::NodeId(Self::PAGE_ID + u64::from(Self::INITIAL_TEMPO) * 20), @@ -301,11 +350,6 @@ impl SongDirectoryConfigPage { ..Default::default() }, |n| GlobalEvent::Page(super::PageEvent::Sdc(SDCChange::InitialSpeed(n))), - |value| { - send_song_op(SongOperation::SetInitialSpeed( - NonZero::new(u8::try_from(value).unwrap()).unwrap(), - )); - }, #[cfg(feature = "accesskit")] ( accesskit::NodeId(Self::PAGE_ID + u64::from(Self::INITIAL_SPEED) * 20), @@ -320,11 +364,11 @@ impl SongDirectoryConfigPage { up: Some(Self::INITIAL_SPEED), shift_tab: Some(Self::INITIAL_SPEED), // down: Some(Self::MIXING_VOLUME), + down: Some(Self::INSTRUMENT_PATH), // tab: Some(Self::MIXING_VOLUME), ..Default::default() }, |n| GlobalEvent::Page(super::PageEvent::Sdc(SDCChange::GlobalVolume(n))), - |value| send_song_op(SongOperation::SetGlobalVol(u8::try_from(value).unwrap())), #[cfg(feature = "accesskit")] ( accesskit::NodeId(Self::PAGE_ID + u64::from(Self::GLOBAL_VOLUME) * 20), @@ -511,18 +555,17 @@ impl SongDirectoryConfigPage { // }, // |text| println!("Sample path set to {text}"), // ); - // let instrument_path = TextInScroll::new( - // CharPosition::new(13, 44), - // 64, - // NextWidget { - // up: Some(WidgetList::MODULE_PATH), - // shift_tab: Some(WidgetList::MODULE_PATH), - // down: Some(WidgetList::SAVE), - // tab: Some(WidgetList::SAVE), - // ..Default::default() - // }, - // |text| println!("Instrument path set to {text}"), - // ); + let instrument_path = TextInScroll::new( + CharPosition::new(13, 44), + 64, + NextWidget { + up: Some(Self::GLOBAL_VOLUME), + // shift_tab: Some(Self::MODULE_PATH), + // down: Some(Self::SAVE), + // tab: Some(Self::SAVE), + ..Default::default() + }, + ); // let save = Button::new( // "Save all Preferences", @@ -552,7 +595,7 @@ impl SongDirectoryConfigPage { // amiga_slides, // module_path, // sample_path, - // instrument_path, + instrument_path, // save, selected: Self::SONG_NAME, } diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 8c4f619..c93ca24 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -6,20 +6,18 @@ use crate::{ draw_buffer::DrawBuffer, }; -use super::{NextWidget, StandardResponse, Widget, WidgetResponse}; +use super::{NextWidget, Widget, WidgetResponse}; -pub struct Button { +pub struct Button { text: &'static str, rect: CharRect, pressed: bool, next_widget: NextWidget, - callback: fn() -> R, #[cfg(feature = "accesskit")] node_id: accesskit::NodeId, } -impl Widget for Button { - type Response = R; +impl Widget for Button { fn draw(&self, buffer: &mut DrawBuffer, selected: bool) { self.draw_overwrite_pressed(buffer, selected, false); } @@ -29,35 +27,26 @@ impl Widget for Button { modifiers: &winit::event::Modifiers, key_event: &winit::event::KeyEvent, _: &mut EventQueue<'_>, - ) -> WidgetResponse { + ) -> WidgetResponse { if key_event.logical_key == Key::Named(NamedKey::Space) || key_event.logical_key == Key::Named(NamedKey::Enter) { if !key_event.repeat { if key_event.state.is_pressed() { self.pressed = true; - return WidgetResponse::default(); + return WidgetResponse::None; } else { - // only call the callback on a release event if the button was pressed in before - // meaning if the user pressed the key, then changed focus to another button and then releases - // no button should be triggered - let response = if self.pressed { - Some((self.callback)()) - } else { - None - }; + let pressed = self.pressed; self.pressed = false; - return WidgetResponse { - standard: StandardResponse::RequestRedraw, - extra: response, - }; + + return WidgetResponse::RequestRedraw(pressed); } } // if focus is changed stop being pressed } else { return self.next_widget.process_key_event(key_event, modifiers); } - WidgetResponse::default() + WidgetResponse::None } #[cfg(feature = "accesskit")] @@ -70,7 +59,7 @@ impl Widget for Button { } } -impl Button { +impl Button { const TOPLEFT_COLOR: u8 = 3; const BOTRIGHT_COLOR: u8 = 1; @@ -78,7 +67,6 @@ impl Button { text: &'static str, rect: CharRect, next_widget: NextWidget, - cb: fn() -> R, #[cfg(feature = "accesskit")] node_id: accesskit::NodeId, ) -> Self { // is 3 rows high, because bot and top are inclusive @@ -89,7 +77,6 @@ impl Button { Button { text, rect, - callback: cb, pressed: false, next_widget, #[cfg(feature = "accesskit")] diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 865dfdd..5e77561 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -1,7 +1,7 @@ pub mod button; pub mod slider; pub mod text_in; -pub mod text_in_scroll; +// pub mod text_in_scroll; pub mod toggle; pub mod toggle_button; @@ -13,81 +13,92 @@ use winit::{ use crate::{EventQueue, draw_buffer::DrawBuffer, pages::PageResponse}; pub(crate) trait Widget { - type Response; fn draw(&self, draw_buffer: &mut DrawBuffer, selected: bool); - /// returns a Some(usize) if the next widget gets selected + /// true if the widget data was changed by the input fn process_input( &mut self, modifiers: &Modifiers, key_event: &KeyEvent, events: &mut EventQueue<'_>, - ) -> WidgetResponse; + ) -> WidgetResponse; #[cfg(feature = "accesskit")] fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>); } -#[derive(Debug)] -pub struct WidgetResponse { - pub standard: StandardResponse, - pub extra: Option, -} +// #[derive(Debug)] +// pub struct WidgetResponse { +// pub standard: StandardResponse, +// pub extra: Option, +// } -impl Default for WidgetResponse { - fn default() -> Self { - Self { - standard: StandardResponse::default(), - extra: None, - } - } -} +// impl Default for WidgetResponse { +// fn default() -> Self { +// Self { +// standard: StandardResponse::default(), +// extra: None, +// } +// } +// } -impl WidgetResponse { - pub fn request_redraw() -> Self { - Self { - standard: StandardResponse::RequestRedraw, - extra: None, - } - } +// impl WidgetResponse { +// pub fn request_redraw() -> Self { +// Self { +// standard: StandardResponse::RequestRedraw, +// extra: None, +// } +// } - pub fn next_widget(value: u8) -> Self { - Self { - standard: StandardResponse::SwitchFocus(value), - extra: None, - } - } -} +// pub fn next_widget(value: u8) -> Self { +// Self { +// standard: StandardResponse::SwitchFocus(value), +// extra: None, +// } +// } +// } // SwitchFocus also has to request a redraw -#[derive(Debug, Default)] -pub enum StandardResponse { +#[derive(Debug, Default, PartialEq, Eq)] +pub enum WidgetResponse { SwitchFocus(u8), - RequestRedraw, + /// true if the redraw is because the widget data was changed. + /// This could be true even if the data didn't change. This makes the implementation of some widgets easier. + /// It is still useful to keep other parts of the system in sync. + /// + /// A redraw request could otherwise be made because for example a internal cursor changed + RequestRedraw(bool), // GlobalEvent(GlobalEvent), #[default] None, } -impl From for WidgetResponse { - fn from(value: StandardResponse) -> Self { - Self { - standard: value, - extra: None, - } - } -} +// impl From for WidgetResponse { +// fn from(value: StandardResponse) -> Self { +// Self { +// standard: value, +// extra: None, +// } +// } +// } -impl StandardResponse { +impl WidgetResponse { pub fn to_page_resp(self, selected: &mut u8) -> PageResponse { match self { - StandardResponse::SwitchFocus(s) => { + WidgetResponse::SwitchFocus(s) => { *selected = s; PageResponse::RequestRedraw } - StandardResponse::RequestRedraw => PageResponse::RequestRedraw, - StandardResponse::None => PageResponse::None, + WidgetResponse::RequestRedraw(_) => PageResponse::RequestRedraw, + WidgetResponse::None => PageResponse::None, } } + + pub fn on_change(self, f: impl FnOnce()) -> Self { + if self == WidgetResponse::RequestRedraw(true) { + f(); + } + self + } } #[derive(Debug, Default)] @@ -102,27 +113,20 @@ pub struct NextWidget { impl NextWidget { /// supposed to be called from Widgets, that own a NextWidget after catching their custom KeyEvents to pick a return - pub fn process_key_event( - &self, - key_event: &KeyEvent, - modifiers: &Modifiers, - ) -> WidgetResponse { + pub fn process_key_event(&self, key_event: &KeyEvent, modifiers: &Modifiers) -> WidgetResponse { if !key_event.state.is_pressed() { - return WidgetResponse::default(); + return WidgetResponse::None; } #[expect( non_local_definitions, reason = "this is only valid with these specific Option not in general" )] - impl From> for WidgetResponse { + impl From> for WidgetResponse { fn from(value: Option) -> Self { - Self { - standard: match value { - Some(num) => StandardResponse::SwitchFocus(num), - None => StandardResponse::None, - }, - extra: None, + match value { + Some(num) => WidgetResponse::SwitchFocus(num), + None => WidgetResponse::None, } } } @@ -148,7 +152,7 @@ impl NextWidget { self.tab.into() } } else { - WidgetResponse::default() + WidgetResponse::None } } } diff --git a/src/widgets/slider.rs b/src/widgets/slider.rs index 3017faf..30151fa 100644 --- a/src/widgets/slider.rs +++ b/src/widgets/slider.rs @@ -12,7 +12,7 @@ use crate::{ draw_buffer::DrawBuffer, }; -use super::{NextWidget, StandardResponse, Widget, WidgetResponse}; +use super::{NextWidget, Widget, WidgetResponse}; #[derive(Debug)] pub struct BoundNumber { @@ -104,19 +104,18 @@ impl BoundNumber { /// Slider needs more Space then is specified in Rect as it draws the current value with an offset of 2 right to the box. /// currently this always draws 3 chars, but this can only take values between -99 and 999. If values outside of that are needed, this implementation needs to change -pub struct Slider { +#[derive(Debug)] +pub struct Slider { number: BoundNumber, position: CharPosition, width: usize, next_widget: NextWidget, dialog_return: fn(i16) -> GlobalEvent, - callback: Box R + Send>, #[cfg(feature = "accesskit")] access: (accesskit::NodeId, Box), } -impl Widget for Slider { - type Response = R; +impl Widget for Slider { fn draw(&self, draw_buffer: &mut DrawBuffer, selected: bool) { const BACKGROUND_COLOR: u8 = 0; const CURSOR_COLOR: u8 = 2; @@ -178,7 +177,7 @@ impl Widget for Slider { modifiers: &winit::event::Modifiers, key_event: &winit::event::KeyEvent, event: &mut EventQueue<'_>, - ) -> WidgetResponse { + ) -> WidgetResponse { if !key_event.state.is_pressed() { return WidgetResponse::default(); } @@ -225,10 +224,7 @@ impl Widget for Slider { }; self.number += amount * direction; - return WidgetResponse { - standard: StandardResponse::RequestRedraw, - extra: Some((self.callback)(*self.number)), - }; + return WidgetResponse::RequestRedraw(true); } // set the value directly, by opening a pop-up } else if let Key::Character(text) = &key_event.logical_key { @@ -237,14 +233,14 @@ impl Widget for Slider { if first_char.is_ascii_digit() { let dialog = SliderDialog::new(first_char, MIN..=MAX, self.dialog_return); event.push(GlobalEvent::OpenDialog(Box::new(|| Box::new(dialog)))); - return WidgetResponse::default(); + return WidgetResponse::None; } } } else { return self.next_widget.process_key_event(key_event, modifiers); } - WidgetResponse::default() + WidgetResponse::None } #[cfg(feature = "accesskit")] @@ -261,7 +257,7 @@ impl Widget for Slider { } } -impl Slider { +impl Slider { /// next_widget left and right must be None, because they cant be called pub fn new( inital_value: i16, @@ -269,7 +265,6 @@ impl Slider { width: usize, next_widget: NextWidget, dialog_return: fn(i16) -> GlobalEvent, - callback: impl Fn(i16) -> R + Send + 'static, #[cfg(feature = "accesskit")] access: (accesskit::NodeId, Box), ) -> Self { assert!(MIN <= MAX, "MIN must be less than or equal to MAX"); @@ -290,24 +285,16 @@ impl Slider { width, next_widget, dialog_return, - callback: Box::new(callback), #[cfg(feature = "accesskit")] access, } } - pub fn try_set(&mut self, value: i16) -> Result { - self.number.try_set(value).map(|_| (self.callback)(value)) + pub fn try_set(&mut self, value: i16) -> Result<(), ()> { + self.number.try_set(value) } -} -impl Debug for Slider { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Slider") - .field("number", &self.number) - .field("position", &self.position) - .field("width", &self.width) - .field("next_widget", &self.next_widget) - .finish_non_exhaustive() + pub fn get(&self) -> i16 { + self.number.inner } } diff --git a/src/widgets/text_in.rs b/src/widgets/text_in.rs index c8eafe4..d467d04 100644 --- a/src/widgets/text_in.rs +++ b/src/widgets/text_in.rs @@ -8,24 +8,22 @@ use crate::{ draw_buffer::DrawBuffer, }; -use super::{NextWidget, StandardResponse, Widget, WidgetResponse}; +use super::{NextWidget, Widget, WidgetResponse}; /// text has max_len of the rect that was given, because the text_in cannot scroll /// use text_in_scroll for that /// i only allow Ascii characters as i can only render ascii -pub struct TextIn { +pub struct TextIn { pos: CharPosition, - width: usize, + width: u8, text: AsciiString, next_widget: NextWidget, - callback: Box R + Send>, cursor_pos: usize, #[cfg(feature = "accesskit")] access: (accesskit::NodeId, &'static str), } -impl Widget for TextIn { - type Response = R; +impl Widget for TextIn { fn draw(&self, draw_buffer: &mut DrawBuffer, selected: bool) { draw_buffer.draw_string_length(self.text.as_str(), self.pos, self.width, 2, 0); // draw the cursor by overdrawing a letter @@ -50,82 +48,17 @@ impl Widget for TextIn { &mut self, modifiers: &winit::event::Modifiers, key_event: &winit::event::KeyEvent, - _: &mut EventQueue<'_>, - ) -> WidgetResponse { - if !key_event.state.is_pressed() { - return WidgetResponse::default(); - } - - if let Key::Character(str) = &key_event.logical_key { - let mut char_iter = str.chars(); - // why would i get a character event if there wasnt a char?? so i unwrap - let first_char = char_iter.next().unwrap(); - - if let Ok(ascii_char) = AsciiChar::from_ascii(first_char) { - self.insert_char(ascii_char); - } - // make sure i only got one char. dont know why i should get more than one - // if this ever panics switch to a loop implementation above - assert!(char_iter.next().is_none()); - return WidgetResponse::request_redraw(); - } else if key_event.logical_key == Key::Named(NamedKey::Space) { - self.insert_char(AsciiChar::Space); - return WidgetResponse::request_redraw(); - } else if key_event.logical_key == Key::Named(NamedKey::ArrowLeft) - && modifiers.state().is_empty() - { - // when moving left from 0 i dont need to redraw - if self.cursor_pos > 0 { - self.cursor_pos -= 1; - return WidgetResponse::request_redraw(); - } - } else if key_event.logical_key == Key::Named(NamedKey::ArrowRight) - && modifiers.state().is_empty() - { - // check that the cursor doesnt go away from the string - if self.text.len() > self.cursor_pos { - self.cursor_pos += 1; - return WidgetResponse::request_redraw(); - } - // backspace and delete keys - // entf on German Keyboards - } else if key_event.logical_key == Key::Named(NamedKey::Delete) { - // cant delete if im outside the text, also includes text empty - if self.cursor_pos < self.text.len() { - let _ = self.text.remove(self.cursor_pos); - return WidgetResponse { - standard: StandardResponse::RequestRedraw, - extra: Some((self.callback)(self.text.as_str())), - }; - } - } else if key_event.logical_key == Key::Named(NamedKey::Backspace) { - // super + backspace clears the string - if modifiers.state().super_key() { - self.text.clear(); - self.cursor_pos = 0; - return WidgetResponse { - standard: StandardResponse::RequestRedraw, - extra: Some((self.callback)(self.text.as_str())), - }; - // if string is empty we cant remove from it - } else if modifiers.state().is_empty() && !self.text.is_empty() { - // if cursor at position 0 backspace starts to behave like delete, no idea why original is like that - if self.cursor_pos == 0 { - let _ = self.text.remove(0); - } else { - let _ = self.text.remove(self.cursor_pos - 1); - self.cursor_pos -= 1; - } - return WidgetResponse { - standard: StandardResponse::RequestRedraw, - extra: Some((self.callback)(self.text.as_str())), - }; - } - // next widget - } else { - return self.next_widget.process_key_event(key_event, modifiers); - } - WidgetResponse::default() + _events: &mut EventQueue<'_>, + ) -> WidgetResponse { + process_input( + &mut self.text, + self.width, + &self.next_widget, + &mut self.cursor_pos, + None, + modifiers, + key_event, + ) } #[cfg(feature = "accesskit")] @@ -172,15 +105,14 @@ impl Widget for TextIn { } } -impl TextIn { +impl TextIn { pub fn new( pos: CharPosition, - width: usize, + width: u8, next_widget: NextWidget, - cb: impl Fn(&str) -> R + Send + 'static, #[cfg(feature = "accesskit")] access: (accesskit::NodeId, &'static str), ) -> Self { - assert!(pos.x() + width < WINDOW_SIZE.0); + assert!(pos.x() + usize::from(width) < WINDOW_SIZE.0); // right and left keys are used in the widget itself. doeesnt make sense to put NextWidget there assert!(next_widget.right.is_none()); assert!(next_widget.left.is_none()); @@ -188,9 +120,8 @@ impl TextIn { TextIn { pos, width, - text: AsciiString::with_capacity(width), // allows to never allocate or deallocate in TextIn + text: AsciiString::with_capacity(usize::from(width)), // allows to never allocate or deallocate in TextIn next_widget, - callback: Box::new(cb), cursor_pos: 0, #[cfg(feature = "accesskit")] access, @@ -198,25 +129,234 @@ impl TextIn { } // not tested - pub fn set_string(&mut self, new_str: String) -> Result> { + pub fn set_string(&mut self, new_str: String) -> Result<(), ascii::FromAsciiError> { self.text = AsciiString::from_ascii(new_str)?; - self.text.truncate(self.width); + self.text.truncate(usize::from(self.width)); self.cursor_pos = self.text.len(); - Ok((self.callback)(self.text.as_str())) + Ok(()) } pub fn get_str(&self) -> &str { self.text.as_str() } - fn insert_char(&mut self, char: AsciiChar) -> R { - if self.cursor_pos < self.width { + fn insert_char(&mut self, char: AsciiChar) { + if self.cursor_pos < usize::from(self.width) { self.cursor_pos += 1; } self.text.insert(self.cursor_pos - 1, char); - self.text.truncate(self.width); + self.text.truncate(usize::from(self.width)); + } +} + +pub struct TextInScroll { + pos: CharPosition, + width: u8, + text: AsciiString, + next_widget: NextWidget, + cursor_pos: usize, + scroll_offset: usize, +} + +impl Widget for TextInScroll { + fn draw(&self, draw_buffer: &mut DrawBuffer, selected: bool) { + draw_buffer.draw_string_length( + &self.text.as_str()[self.scroll_offset..], + self.pos, + self.width, + 2, + 0, + ); + + if selected { + let cursor_char_pos = + self.pos + CharPosition::new(self.cursor_pos - self.scroll_offset, 0); + if self.cursor_pos < self.text.len() { + draw_buffer.draw_char( + font8x8::BASIC_FONTS + .get(self.text[self.cursor_pos].into()) + .unwrap(), + cursor_char_pos, + 0, + 3, + ); + } else { + draw_buffer.draw_rect(3, cursor_char_pos.into()); + } + } + } + + fn process_input( + &mut self, + modifiers: &winit::event::Modifiers, + key_event: &winit::event::KeyEvent, + _events: &mut EventQueue<'_>, + ) -> WidgetResponse { + process_input( + &mut self.text, + self.width, + &self.next_widget, + &mut self.cursor_pos, + Some(&mut self.scroll_offset), + modifiers, + key_event, + ) + } + + #[cfg(feature = "accesskit")] + fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>) { + todo!() + } +} + +impl TextInScroll { + pub fn new(pos: CharPosition, width: u8, next_widget: NextWidget) -> Self { + assert!(next_widget.right.is_none()); + assert!(next_widget.left.is_none()); + assert!(pos.x() + usize::from(width) < WINDOW_SIZE.0); + + Self { + pos, + width, + text: AsciiString::new(), + next_widget, + cursor_pos: 0, + scroll_offset: 0, + } + } + + pub fn get_str(&self) -> &str { + self.text.as_str() + } +} + +pub fn process_input( + text: &mut AsciiString, + width: u8, + next: &NextWidget, + cursor_pos: &mut usize, + scroll_offset: Option<&mut usize>, + modifiers: &winit::event::Modifiers, + key_event: &winit::event::KeyEvent, +) -> WidgetResponse { + /// returns true if the cursor moved + fn move_cursor_left( + cursor_pos: &mut usize, + scroll_offset: Option<&mut usize>, + ) -> WidgetResponse { + if *cursor_pos > 0 { + *cursor_pos -= 1; + if let Some(scroll) = scroll_offset + && *cursor_pos < *scroll + { + *scroll -= 1; + } + // redraw, but no state change + WidgetResponse::RequestRedraw(false) + } else { + WidgetResponse::None + } + } - (self.callback)(self.text.as_str()) + /// returns true if the cursor moved + fn move_cursor_right( + text: &AsciiString, + cursor_pos: &mut usize, + width: u8, + scroll_offset: Option<&mut usize>, + ) -> WidgetResponse { + if *cursor_pos < text.len() { + *cursor_pos += 1; + if let Some(scroll) = scroll_offset + && *cursor_pos > *scroll + usize::from(width) + { + *scroll += 1; + } + // redraw, but no state change + WidgetResponse::RequestRedraw(false) + } else { + WidgetResponse::None + } + } + + fn insert_char( + text: &mut AsciiString, + width: u8, + cursor_pos: &mut usize, + char: AsciiChar, + scroll_offset: Option<&mut usize>, + ) { + if scroll_offset.is_some() { + text.insert(*cursor_pos, char); + move_cursor_right(text, cursor_pos, width, scroll_offset); + } else { + if *cursor_pos < usize::from(width) { + *cursor_pos += 1; + } + text.insert(*cursor_pos - 1, char); + text.truncate(usize::from(width)); + } + } + + if !key_event.state.is_pressed() { + return WidgetResponse::None; + } + + if let Key::Character(str) = &key_event.logical_key { + let mut char_iter = str.chars(); + let first_char = char_iter.next().unwrap(); + assert!(char_iter.next().is_none()); + + if let Ok(ascii_char) = AsciiChar::from_ascii(first_char) { + insert_char(text, width, cursor_pos, ascii_char, scroll_offset); + WidgetResponse::RequestRedraw(true) + } else { + WidgetResponse::None + } + } else if key_event.logical_key == Key::Named(NamedKey::Space) { + insert_char(text, width, cursor_pos, AsciiChar::Space, scroll_offset); + return WidgetResponse::RequestRedraw(true); + } else if key_event.logical_key == Key::Named(NamedKey::ArrowLeft) + && modifiers.state().is_empty() + { + move_cursor_left(cursor_pos, scroll_offset) + } else if key_event.logical_key == Key::Named(NamedKey::ArrowRight) + && modifiers.state().is_empty() + { + move_cursor_right(text, cursor_pos, width, scroll_offset) + // entf key on german keyboard + } else if key_event.logical_key == Key::Named(NamedKey::Delete) { + // can't delete if cursor is at the front of the text or the text is empty + if *cursor_pos < text.len() { + _ = text.remove(*cursor_pos); + WidgetResponse::RequestRedraw(true) + } else { + WidgetResponse::None + } + } else if key_event.logical_key == Key::Named(NamedKey::Backspace) { + // super + backspace clears the string + // if the text is already empty we don't need to do anything + if modifiers.state().super_key() && !text.is_empty() { + text.clear(); + *cursor_pos = 0; + if let Some(scroll) = scroll_offset { + *scroll = 0; + } + WidgetResponse::RequestRedraw(true) + } else if modifiers.state().is_empty() && !text.is_empty() { + if *cursor_pos == 0 { + _ = text.remove(0); + } else { + _ = text.remove(*cursor_pos - 1); + move_cursor_left(cursor_pos, scroll_offset); + } + WidgetResponse::RequestRedraw(true) + } else { + WidgetResponse::None + } + } else { + // next widget select + next.process_key_event(key_event, modifiers) } } diff --git a/src/widgets/text_in_scroll.rs b/src/widgets/text_in_scroll.rs deleted file mode 100644 index 6359e01..0000000 --- a/src/widgets/text_in_scroll.rs +++ /dev/null @@ -1,217 +0,0 @@ -use ascii::{AsciiChar, AsciiString}; -use font8x8::UnicodeFonts; -use winit::keyboard::{Key, NamedKey}; - -use crate::{ - EventQueue, - coordinates::{CharPosition, WINDOW_SIZE}, - draw_buffer::DrawBuffer, - widgets::StandardResponse, -}; - -use super::{NextWidget, Widget, WidgetResponse}; - -pub struct TextInScroll { - pos: CharPosition, - width: usize, - text: AsciiString, - next_widget: NextWidget, - callback: Box R>, - cursor_pos: usize, - scroll_offset: usize, -} - -impl Widget for TextInScroll { - type Response = R; - fn draw(&self, draw_buffer: &mut DrawBuffer, selected: bool) { - draw_buffer.draw_string_length( - &self.text.as_str()[self.scroll_offset..], - self.pos, - self.width, - 2, - 0, - ); - // draw the cursor by overdrawing a letter - if selected { - let cursor_char_pos = - self.pos + CharPosition::new(self.cursor_pos - self.scroll_offset, 0); - if self.cursor_pos < self.text.len() { - draw_buffer.draw_char( - font8x8::BASIC_FONTS - .get(self.text[self.cursor_pos].into()) - .unwrap(), - cursor_char_pos, - 0, - 3, - ); - } else { - draw_buffer.draw_rect(3, cursor_char_pos.into()); - } - } - } - - fn process_input( - &mut self, - modifiers: &winit::event::Modifiers, - key_event: &winit::event::KeyEvent, - _: &mut EventQueue<'_>, - ) -> WidgetResponse { - if !key_event.state.is_pressed() { - return WidgetResponse::default(); - } - - if let Key::Character(str) = &key_event.logical_key { - let mut char_iter = str.chars(); - // why would i get a character event if there wasnt a char?? so i unwrap - let first_char = char_iter.next().unwrap(); - - if let Ok(ascii_char) = AsciiChar::from_ascii(first_char) { - self.insert_char(ascii_char); - } - // make sure i only got one char. dont know why i should get more than one - // if this ever panics switch to a loop implementation above - assert!(char_iter.next().is_none()); - return WidgetResponse { - standard: StandardResponse::RequestRedraw, - extra: Some((self.callback)(self.text.as_str())), - }; - } else if key_event.logical_key == Key::Named(NamedKey::Space) { - self.insert_char(AsciiChar::Space); - return WidgetResponse::request_redraw(); - } else if key_event.logical_key == Key::Named(NamedKey::ArrowLeft) - && modifiers.state().is_empty() - { - if self.move_cursor_left() { - return WidgetResponse::request_redraw(); - } - } else if key_event.logical_key == Key::Named(NamedKey::ArrowRight) - && modifiers.state().is_empty() - { - if self.move_cursor_right() { - return WidgetResponse::request_redraw(); - } - // backspace and delete keys - // entf on German Keyboards - } else if key_event.logical_key == Key::Named(NamedKey::Delete) { - // cant delete if i'm outside the text. also includes text empty - if self.cursor_pos < self.text.len() { - let _ = self.text.remove(self.cursor_pos); - return WidgetResponse { - standard: StandardResponse::RequestRedraw, - extra: Some((self.callback)(self.text.as_str())), - }; - } - } else if key_event.logical_key == Key::Named(NamedKey::Backspace) { - // super + backspace clears the string - if modifiers.state().super_key() { - self.text.clear(); - self.cursor_pos = 0; - self.scroll_offset = 0; - return WidgetResponse { - standard: StandardResponse::RequestRedraw, - extra: Some((self.callback)(self.text.as_str())), - }; - // if string is empty we cant remove from it - } else if modifiers.state().is_empty() && !self.text.is_empty() { - // if cursor at position 0 backspace starts to behave like delete, no idea why original is like that - if self.cursor_pos == 0 { - let _ = self.text.remove(0); - } else { - let _ = self.text.remove(self.cursor_pos - 1); - self.move_cursor_left(); - } - - return WidgetResponse { - standard: StandardResponse::RequestRedraw, - extra: Some((self.callback)(self.text.as_str())), - }; - } - // next widget - } else { - return self.next_widget.process_key_event(key_event, modifiers); - } - WidgetResponse::default() - } - - #[cfg(feature = "accesskit")] - fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>) { - todo!() // probably very similar / the same as the regular text in - } -} - -impl TextInScroll { - pub fn new( - pos: CharPosition, - width: usize, - next_widget: NextWidget, - cb: impl Fn(&str) -> R + 'static, - ) -> Self { - assert!(pos.x() + width < WINDOW_SIZE.0); - // right and left keys are used in the widget itself. doeesnt make sense to put NextWidget there - assert!(next_widget.right.is_none()); - assert!(next_widget.left.is_none()); - - Self { - pos, - width, - text: AsciiString::new(), // size completely unknown, so i don't allocate - next_widget, - callback: Box::new(cb), - cursor_pos: 0, - scroll_offset: 0, - } - } - - pub fn set_string<'a>( - &mut self, - new_str: &'a str, - ) -> Result> { - self.text = AsciiString::from_ascii(new_str)?; - self.text.truncate(self.width); - if self.cursor_pos > self.text.len() { - self.cursor_pos = self.text.len(); - } - // never tested could be buggy - self.scroll_offset = self.text.len().saturating_sub(self.width); - - Ok((self.callback)(self.text.as_str())) - } - - fn insert_char(&mut self, char: AsciiChar) -> R { - self.text.insert(self.cursor_pos, char); - - self.move_cursor_right(); - - (self.callback)(self.text.as_str()) - } - - // returns if it moved - fn move_cursor_left(&mut self) -> bool { - if self.cursor_pos > 0 { - self.cursor_pos -= 1; - if self.cursor_pos < self.scroll_offset { - self.scroll_offset -= 1; - } - true - } else { - false - } - } - - // returns if it moved - fn move_cursor_right(&mut self) -> bool { - if self.cursor_pos < self.text.len() { - self.cursor_pos += 1; - if self.cursor_pos > self.scroll_offset + self.width { - self.scroll_offset += 1; - } - true - } else { - false - } - } - - pub fn get_str(&self) -> &str { - self.text.as_str() - } -} diff --git a/src/widgets/toggle.rs b/src/widgets/toggle.rs index b90323c..f885e6c 100644 --- a/src/widgets/toggle.rs +++ b/src/widgets/toggle.rs @@ -6,19 +6,17 @@ use crate::{ draw_buffer::DrawBuffer, }; -use super::{NextWidget, StandardResponse, Widget, WidgetResponse}; +use super::{NextWidget, Widget, WidgetResponse}; -pub struct Toggle { +pub struct Toggle { pos: CharPosition, width: usize, state: usize, next_widget: NextWidget, variants: &'static [(T, &'static str)], - cb: Box R>, } -impl Widget for Toggle { - type Response = R; +impl Widget for Toggle { fn draw(&self, draw_buffer: &mut DrawBuffer, selected: bool) { let str = self.variants[self.state].1; draw_buffer.draw_rect( @@ -42,16 +40,13 @@ impl Widget for Toggle { modifiers: &winit::event::Modifiers, key_event: &winit::event::KeyEvent, _: &mut EventQueue<'_>, - ) -> WidgetResponse { + ) -> WidgetResponse { if key_event.logical_key == Key::Named(NamedKey::Space) && modifiers.state().is_empty() && key_event.state.is_pressed() { self.next(); - WidgetResponse { - standard: StandardResponse::RequestRedraw, - extra: Some((*self.cb)(self.variants[self.state].0)), - } + WidgetResponse::RequestRedraw(true) } else { self.next_widget.process_key_event(key_event, modifiers) } @@ -63,13 +58,12 @@ impl Widget for Toggle { } } -impl Toggle { +impl Toggle { pub fn new( pos: CharPosition, width: usize, next_widget: NextWidget, variants: &'static [(T, &'static str)], - cb: impl Fn(T) -> R + 'static, ) -> Self { assert!(pos.x() + width < WINDOW_SIZE.0); @@ -79,7 +73,6 @@ impl Toggle { state: 0, next_widget, variants, - cb: Box::new(cb), } } diff --git a/src/widgets/toggle_button.rs b/src/widgets/toggle_button.rs index 9b2cfc9..ec4ee87 100644 --- a/src/widgets/toggle_button.rs +++ b/src/widgets/toggle_button.rs @@ -5,16 +5,14 @@ use crate::{EventQueue, coordinates::CharRect, draw_buffer::DrawBuffer}; use super::{NextWidget, Widget, WidgetResponse, button::Button}; // dont need to store a callback as it gets pushed into the inner button callback -pub struct ToggleButton { - button: Button<()>, +pub struct ToggleButton { + button: Button, variant: T, - cb: fn(T) -> R, state: Rc>, } -impl Widget for ToggleButton { - type Response = R; +impl Widget for ToggleButton { fn draw(&self, draw_buffer: &mut DrawBuffer, selected: bool) { self.button .draw_overwrite_pressed(draw_buffer, selected, self.variant == self.state.get()) @@ -25,14 +23,8 @@ impl Widget for ToggleButton { modifiers: &winit::event::Modifiers, key_event: &winit::event::KeyEvent, event: &mut EventQueue<'_>, - ) -> WidgetResponse { - let WidgetResponse { standard, extra } = - self.button.process_input(modifiers, key_event, event); - let extra = extra.map(|_| { - self.state.set(self.variant); - (self.cb)(self.variant) - }); - WidgetResponse { standard, extra } + ) -> WidgetResponse { + self.button.process_input(modifiers, key_event, event) } #[cfg(feature = "accesskit")] @@ -41,20 +33,24 @@ impl Widget for ToggleButton { } } -impl ToggleButton { +impl ToggleButton { pub fn new( text: &'static str, rect: CharRect, next_widget: NextWidget, variant: T, state: Rc>, - cb: fn(T) -> R, ) -> Self { - let button = Button::new(text, rect, next_widget, || (), todo!()); + let button = Button::new( + text, + rect, + next_widget, + #[cfg(feature = "accesskit")] + todo!(), + ); Self { button, variant, - cb, state, } } From 219c278aa19286f3d0ae34a2f3134ed8d567af9e Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Sun, 5 Oct 2025 10:15:31 +0200 Subject: [PATCH 12/19] allow changing sample names --- src/pages/sample_list.rs | 602 +++++++++++++++++++++++---------------- 1 file changed, 354 insertions(+), 248 deletions(-) diff --git a/src/pages/sample_list.rs b/src/pages/sample_list.rs index 4b495dc..c20ea89 100644 --- a/src/pages/sample_list.rs +++ b/src/pages/sample_list.rs @@ -1,10 +1,12 @@ use std::{ - io::{Cursor, Write}, + io::{self, Write}, iter::zip, num::NonZero, str::from_utf8, }; +use ascii::{AsciiChar, AsciiString}; +use font8x8::UnicodeFonts; use torque_tracker_engine::{ project::{ note_event::Note, @@ -20,6 +22,7 @@ use crate::{ draw_buffer::DrawBuffer, header::HeaderEvent, pages::{Page, PageEvent, PageResponse, pattern::PatternPageEvent}, + widgets::{NextWidget, WidgetResponse, text_in}, }; #[derive(Debug, Clone)] @@ -28,10 +31,17 @@ pub enum SampleListEvent { SelectSample(u8), } +#[derive(PartialEq, Eq, Copy, Clone)] +enum Cursor { + Name(u8), + Play, +} + pub struct SampleList { - selected: u8, + selected_sample: u8, + cursor: Cursor, sample_view: u8, - samples: [Option<(String, SampleMetaData)>; Song::MAX_SAMPLES_INSTR], + samples: [Option<(AsciiString, SampleMetaData)>; Song::MAX_SAMPLES_INSTR], event_proxy: winit::event_loop::EventLoopProxy, } @@ -39,10 +49,11 @@ impl SampleList { const SAMPLE_VIEW_COUNT: u8 = 34; pub fn new(event_proxy: winit::event_loop::EventLoopProxy) -> Self { Self { - selected: 0, + selected_sample: 0, samples: [const { None }; Song::MAX_SAMPLES_INSTR], sample_view: 0, event_proxy, + cursor: Cursor::Name(0), } } @@ -59,8 +70,12 @@ impl SampleList { PageResponse::RequestRedraw } SampleListEvent::SetSample(idx, name, meta) => { + let name = name + .chars() + .flat_map(|c| AsciiChar::from_ascii(c).ok()) + .collect::(); self.samples[usize::from(idx)] = Some((name, meta)); - if self.selected == idx { + if self.selected_sample == idx { self.send_to_header(events); } PageResponse::RequestRedraw @@ -68,64 +83,275 @@ impl SampleList { } } - fn select_sample(&mut self, selected: u8) { - self.selected = selected; - self.sample_view = if self.selected < self.sample_view { - self.selected - } else if self.selected > self.sample_view + Self::SAMPLE_VIEW_COUNT { - self.selected - Self::SAMPLE_VIEW_COUNT + fn select_sample(&mut self, sample: u8) { + self.selected_sample = sample; + self.sample_view = if self.selected_sample < self.sample_view { + self.selected_sample + } else if self.selected_sample > self.sample_view + Self::SAMPLE_VIEW_COUNT { + self.selected_sample - Self::SAMPLE_VIEW_COUNT } else { self.sample_view }; } fn send_to_header(&self, events: &mut EventQueue<'_>) { - let name: Box = self.samples[usize::from(self.selected)] + let name: Box = self.samples[usize::from(self.selected_sample)] .as_ref() .map(|(n, _)| Box::from(n.as_str())) .unwrap_or(Box::from("")); events.push(GlobalEvent::Header(HeaderEvent::SetSample( - self.selected, + self.selected_sample, name, ))); } fn send_to_pattern(&self, events: &mut EventQueue<'_>) { events.push(GlobalEvent::Page(PageEvent::Pattern( - PatternPageEvent::SetSampleInstr(self.selected), + PatternPageEvent::SetSampleInstr(self.selected_sample), ))); } + + // exists so that this montrosity doesn't sit in the middle of the keyboard input code + fn load_audio_file(&mut self) { + let dialog = rfd::AsyncFileDialog::new() + // TODO: figure out which formats i support and sync it with the symphonia features + // .add_filter("supported audio formats", &["wav"]) + .pick_file(); + let proxy = self.event_proxy.clone(); + let idx = self.selected_sample; + EXECUTOR + .spawn(async move { + let file = dialog.await; + let Some(file) = file else { + return; + }; + let file_name = file.file_name(); + // HOW TO SYMPHONIA: https://github.com/pdeljanov/Symphonia/blob/master/symphonia/examples/basic-interleaved.rs + // IO is not async as symphonia doesn't support async IO. + // This is fine as i have two background threads and don't + // do IO that often. + let Ok(file) = std::fs::File::open(file.path()) else { + eprintln!("error opening file"); + return; + }; + let mss = + symphonia::core::io::MediaSourceStream::new(Box::new(file), Default::default()); + let probe = symphonia::default::get_probe(); + let Ok(probed) = probe.format( + // TODO: add file extension to the hint + &symphonia::core::probe::Hint::new(), + mss, + &Default::default(), + &Default::default(), + ) else { + eprintln!("format error"); + return; + }; + let mut format = probed.format; + let Some(track) = format + .tracks() + .iter() + .find(|t| t.codec_params.codec != symphonia::core::codecs::CODEC_TYPE_NULL) + else { + eprintln!("no decodable track found"); + return; + }; + let Ok(mut decoder) = + symphonia::default::get_codecs().make(&track.codec_params, &Default::default()) + else { + eprintln!("no decoder found"); + return; + }; + let track_id = track.id; + let Some(sample_rate) = track.codec_params.sample_rate else { + eprintln!("no sample rate"); + return; + }; + let Some(sample_rate) = NonZero::new(sample_rate) else { + eprintln!("sample rate = 0"); + return; + }; + let mut buf = Vec::new(); + // i don't know yet. after the first iteration of the loop this is set + let mut stereo: Option = None; + loop { + let packet = format.next_packet(); + let packet = match packet { + Ok(p) => p, + // this is used as a end of stream signal. don't ask me why + Err(symphonia::core::errors::Error::IoError(e)) + if e.kind() == std::io::ErrorKind::UnexpectedEof => + { + break; + } + Err(e) => { + eprintln!("decoding error: {e:?}"); + return; + } + }; + + if packet.track_id() != track_id { + continue; + } + match decoder.decode(&packet) { + Ok(audio_buf) => { + fn append_to_buf( + buf: &mut Vec, + in_buf: &symphonia::core::audio::AudioBuffer, + stereo: &mut Option, + ) where + T: symphonia::core::sample::Sample, + f32: symphonia::core::conv::FromSample, + { + use symphonia::core::{ + audio::{Channels, Signal}, + conv::FromSample, + }; + if in_buf + .spec() + .channels + .contains(Channels::FRONT_LEFT | Channels::FRONT_RIGHT) + { + // stereo + plus maybe other channels that i ignore + assert!(stereo.is_none() || *stereo == Some(true)); + *stereo = Some(true); + let left = in_buf.chan(0); + let right = in_buf.chan(1); + assert!(left.len() == right.len()); + let iter = zip(left, right).flat_map(|(l, r)| { + [f32::from_sample(*l), f32::from_sample(*r)] + }); + buf.extend(iter); + } else if in_buf.spec().channels.contains(Channels::FRONT_LEFT) { + // assert not + assert!(stereo.is_none() || *stereo == Some(false)); + *stereo = Some(false); + buf.extend( + in_buf + .chan(0) + .iter() + .map(|sample| f32::from_sample(*sample)), + ); + } else { + eprintln!("no usable channel in sample data") + } + } + use symphonia::core::audio::AudioBufferRef; + match audio_buf { + AudioBufferRef::U8(d) => append_to_buf(&mut buf, &d, &mut stereo), + AudioBufferRef::U16(d) => append_to_buf(&mut buf, &d, &mut stereo), + AudioBufferRef::U24(d) => append_to_buf(&mut buf, &d, &mut stereo), + AudioBufferRef::U32(d) => append_to_buf(&mut buf, &d, &mut stereo), + AudioBufferRef::S8(d) => append_to_buf(&mut buf, &d, &mut stereo), + AudioBufferRef::S16(d) => append_to_buf(&mut buf, &d, &mut stereo), + AudioBufferRef::S24(d) => append_to_buf(&mut buf, &d, &mut stereo), + AudioBufferRef::S32(d) => append_to_buf(&mut buf, &d, &mut stereo), + AudioBufferRef::F32(d) => append_to_buf(&mut buf, &d, &mut stereo), + AudioBufferRef::F64(d) => append_to_buf(&mut buf, &d, &mut stereo), + } + } + Err(symphonia::core::errors::Error::DecodeError(_)) => (), + Err(_) => break, + } + } + // hopefully both of these compile to a memcopy... + let sample = if stereo.unwrap() { + Sample::new_stereo_interpolated(buf) + } else { + Sample::new_mono(buf) + }; + // TODO: get the real metadata / sane defaults / configurable + let meta = SampleMetaData { + default_volume: 32, + global_volume: 32, + default_pan: None, + vibrato_speed: 0, + vibrato_depth: 0, + vibrato_rate: 0, + vibrato_waveform: Default::default(), + sample_rate, + base_note: Note::new(64).unwrap(), + }; + // send to UI + proxy + .send_event(GlobalEvent::Page(PageEvent::SampleList( + SampleListEvent::SetSample(idx, file_name, meta), + ))) + .unwrap(); + drop(proxy); + // send to playback + let operation = SongOperation::SetSample(idx, meta, sample); + SONG_OP_SEND.get().unwrap().send(operation).await.unwrap(); + }) + .detach(); + } } impl Page for SampleList { fn draw(&mut self, draw_buffer: &mut DrawBuffer) { - // samples - { - const BASE_POS: CharPosition = CharPosition::new(2, 13); - let mut buf = [0; 2]; - for (i, n) in - (self.sample_view..=self.sample_view + Self::SAMPLE_VIEW_COUNT).enumerate() - { - // number - let mut curse: Cursor<&mut [u8]> = Cursor::new(&mut buf); - write!(curse, "{:02}", n).unwrap(); - let str = from_utf8(&buf).unwrap(); - draw_buffer.draw_string(str, BASE_POS + CharPosition::new(0, i), 0, 2); + // samples + play buttons + const SAMPLE_BASE_POS: CharPosition = CharPosition::new(2, 13); + const PLAY_BASE_POS: CharPosition = CharPosition::new(31, 13); + let mut buf = [0; 2]; + for (i, n) in (self.sample_view..=self.sample_view + Self::SAMPLE_VIEW_COUNT).enumerate() { + // number + let mut curse: io::Cursor<&mut [u8]> = io::Cursor::new(&mut buf); + write!(curse, "{:02}", n).unwrap(); + let str = from_utf8(&buf).unwrap(); + draw_buffer.draw_string(str, SAMPLE_BASE_POS + CharPosition::new(0, i), 0, 2); - // name - let name = self.samples[usize::from(n)] - .as_ref() - .map(|(n, _)| n.as_str()) - .unwrap_or(""); - let background_color = if self.selected == n { 14 } else { 0 }; - draw_buffer.draw_string_length( - name, - BASE_POS + CharPosition::new(3, i), - 24, - 6, - background_color, - ); + // name + let name = self.samples[usize::from(n)].as_ref().map(|(n, _)| n); + let selected = self.selected_sample == n; + let background_color = if selected { 14 } else { 0 }; + let name_pos = SAMPLE_BASE_POS + CharPosition::new(3, i); + draw_buffer.draw_string_length( + name.unwrap_or(&AsciiString::new()).as_str(), + name_pos, + 24, + 6, + background_color, + ); + // if selected draw the text cursor by replacing one char + if selected + && let Cursor::Name(text_cursor) = self.cursor + && let Some(name) = name + { + let cursor_char_pos = name_pos + CharPosition::new(text_cursor.into(), 0); + if usize::from(text_cursor) < name.len() { + draw_buffer.draw_char( + font8x8::BASIC_FONTS + .get(name[usize::from(text_cursor)].into()) + .unwrap(), + cursor_char_pos, + 0, + 3, + ); + } else { + draw_buffer.draw_rect(3, cursor_char_pos.into()); + } } + + // play button + let (fg_color, bg_color) = match (selected, name.is_some(), self.cursor == Cursor::Play) + { + // row not selected + (false, false, _) => (7, 0), + (false, true, _) => (6, 0), + // row selected, sample inactive + (true, false, false) => (7, 14), + (true, false, true) => (0, 6), + // row selected, sample active + (true, true, false) => (6, 14), + (true, true, true) => (0, 3), + }; + draw_buffer.show_colors(); + draw_buffer.draw_string( + "Play", + PLAY_BASE_POS + CharPosition::new(0, i), + fg_color, + bg_color, + ); } } @@ -139,225 +365,105 @@ impl Page for SampleList { key_event: &winit::event::KeyEvent, events: &mut EventQueue<'_>, ) -> PageResponse { + // TODO: remove this once buttons exist on this page if !key_event.state.is_pressed() { return PageResponse::None; } - if key_event.logical_key == Key::Named(NamedKey::ArrowUp) && modifiers.state().is_empty() { - if let Some(s) = self.selected.checked_sub(1) { - self.select_sample(s); - self.send_to_header(events); - self.send_to_pattern(events); - return PageResponse::RequestRedraw; - } - } else if key_event.logical_key == Key::Named(NamedKey::ArrowDown) - && modifiers.state().is_empty() - { - if self.selected + 1 < 100 { - self.select_sample(self.selected + 1); - self.send_to_header(events); - self.send_to_pattern(events); - return PageResponse::RequestRedraw; - } - } else if key_event.logical_key == Key::Named(NamedKey::Enter) - && modifiers.state().is_empty() - { - let dialog = rfd::AsyncFileDialog::new() - // TODO: figure out which formats i support and sync it with the symphonia features - // .add_filter("supported audio formats", &["wav"]) - .pick_file(); - let proxy = self.event_proxy.clone(); - let idx = self.selected; - EXECUTOR - .spawn(async move { - let file = dialog.await; - let Some(file) = file else { - return; - }; - let file_name = file.file_name(); - // HOW TO SYMPHONIA: https://github.com/pdeljanov/Symphonia/blob/master/symphonia/examples/basic-interleaved.rs - // IO is not async as symphonia doesn't support async IO. - // This is fine as i have two background threads and don't - // do IO that often. - let Ok(file) = std::fs::File::open(file.path()) else { - eprintln!("error opening file"); - return; - }; - let mss = symphonia::core::io::MediaSourceStream::new( - Box::new(file), - Default::default(), + match self.cursor { + Cursor::Name(mut text_cursor) => { + if key_event.logical_key == Key::Named(NamedKey::Tab) { + if modifiers.state().shift_key() { + // TODO: shift aroung to one of the buttons + } else { + self.cursor = Cursor::Play; + } + return PageResponse::RequestRedraw; + } + if let Some(sample) = &mut self.samples[usize::from(self.selected_sample)] { + let mut text_cursor = usize::from(text_cursor); + // text_editing + let resp = text_in::process_input( + &mut sample.0, + 24, + &NextWidget::default(), + &mut text_cursor, + None, + modifiers, + key_event, ); - let probe = symphonia::default::get_probe(); - let Ok(probed) = probe.format( - // TODO: add file extension to the hint - &symphonia::core::probe::Hint::new(), - mss, - &Default::default(), - &Default::default(), - ) else { - eprintln!("format error"); - return; - }; - let mut format = probed.format; - let Some(track) = format - .tracks() - .iter() - .find(|t| t.codec_params.codec != symphonia::core::codecs::CODEC_TYPE_NULL) - else { - eprintln!("no decodable track found"); - return; - }; - let Ok(mut decoder) = symphonia::default::get_codecs() - .make(&track.codec_params, &Default::default()) - else { - eprintln!("no decoder found"); - return; - }; - let track_id = track.id; - let Some(sample_rate) = track.codec_params.sample_rate else { - eprintln!("no sample rate"); - return; - }; - let Some(sample_rate) = NonZero::new(sample_rate) else { - eprintln!("sample rate = 0"); - return; - }; - let mut buf = Vec::new(); - // i don't know yet. after the first iteration of the loop this is set - let mut stereo: Option = None; - loop { - let packet = format.next_packet(); - let packet = match packet { - Ok(p) => p, - // this is used as a end of stream signal. don't ask me why - Err(symphonia::core::errors::Error::IoError(e)) - if e.kind() == std::io::ErrorKind::UnexpectedEof => - { - break; - } - Err(e) => { - eprintln!("decoding error: {e:?}"); - return; - } - }; - - if packet.track_id() != track_id { - continue; + let text_cursor = u8::try_from(text_cursor).expect( + "process input has increased the cursor outside of the text bounds", + ); + match resp { + // no next widget specified + WidgetResponse::SwitchFocus(_) => unreachable!(), + // need to update the header + WidgetResponse::RequestRedraw(true) => { + self.send_to_header(events); + self.cursor = Cursor::Name(text_cursor); + // data changed, so early return redraw + return PageResponse::RequestRedraw; } - match decoder.decode(&packet) { - Ok(audio_buf) => { - fn append_to_buf( - buf: &mut Vec, - in_buf: &symphonia::core::audio::AudioBuffer, - stereo: &mut Option, - ) where - T: symphonia::core::sample::Sample, - f32: symphonia::core::conv::FromSample, - { - use symphonia::core::{ - audio::{Channels, Signal}, - conv::FromSample, - }; - if in_buf - .spec() - .channels - .contains(Channels::FRONT_LEFT | Channels::FRONT_RIGHT) - { - // stereo + plus maybe other channels that i ignore - assert!(stereo.is_none() || *stereo == Some(true)); - *stereo = Some(true); - let left = in_buf.chan(0); - let right = in_buf.chan(1); - assert!(left.len() == right.len()); - let iter = zip(left, right).flat_map(|(l, r)| { - [f32::from_sample(*l), f32::from_sample(*r)] - }); - buf.extend(iter); - } else if in_buf.spec().channels.contains(Channels::FRONT_LEFT) - { - // assert not - assert!(stereo.is_none() || *stereo == Some(false)); - *stereo = Some(false); - buf.extend( - in_buf - .chan(0) - .iter() - .map(|sample| f32::from_sample(*sample)), - ); - } else { - eprintln!("no usable channel in sample data") - } - } - use symphonia::core::audio::AudioBufferRef; - match audio_buf { - AudioBufferRef::U8(d) => { - append_to_buf(&mut buf, &d, &mut stereo) - } - AudioBufferRef::U16(d) => { - append_to_buf(&mut buf, &d, &mut stereo) - } - AudioBufferRef::U24(d) => { - append_to_buf(&mut buf, &d, &mut stereo) - } - AudioBufferRef::U32(d) => { - append_to_buf(&mut buf, &d, &mut stereo) - } - AudioBufferRef::S8(d) => { - append_to_buf(&mut buf, &d, &mut stereo) - } - AudioBufferRef::S16(d) => { - append_to_buf(&mut buf, &d, &mut stereo) - } - AudioBufferRef::S24(d) => { - append_to_buf(&mut buf, &d, &mut stereo) - } - AudioBufferRef::S32(d) => { - append_to_buf(&mut buf, &d, &mut stereo) - } - AudioBufferRef::F32(d) => { - append_to_buf(&mut buf, &d, &mut stereo) - } - AudioBufferRef::F64(d) => { - append_to_buf(&mut buf, &d, &mut stereo) - } - } - } - Err(symphonia::core::errors::Error::DecodeError(_)) => (), - Err(_) => break, + WidgetResponse::RequestRedraw(false) => { + // cursor movement, so early return + self.cursor = Cursor::Name(text_cursor); + // here the header doesn't have to be updated, because only the + // cursor position changed + return PageResponse::RequestRedraw; } + WidgetResponse::None => (), } - // hopefully both of these compile to a memcopy... - let sample = if stereo.unwrap() { - Sample::new_stereo_interpolated(buf) + } + } + Cursor::Play => { + if key_event.logical_key == Key::Named(NamedKey::Tab) { + if modifiers.state().shift_key() { + // set the text_cursor to the end, because i came from the right + let name_len = self.samples[usize::from(self.selected_sample)] + .as_ref() + .map(|(s, _)| s.len()) + .unwrap_or(0); + self.cursor = Cursor::Name(name_len.try_into().unwrap()); + return PageResponse::RequestRedraw; } else { - Sample::new_mono(buf) - }; - // TODO: get the real metadata / sane defaults / configurable - let meta = SampleMetaData { - default_volume: 32, - global_volume: 32, - default_pan: None, - vibrato_speed: 0, - vibrato_depth: 0, - vibrato_rate: 0, - vibrato_waveform: Default::default(), - sample_rate, - base_note: Note::new(64).unwrap(), - }; - // send to UI - proxy - .send_event(GlobalEvent::Page(PageEvent::SampleList( - SampleListEvent::SetSample(idx, file_name, meta), - ))) - .unwrap(); - drop(proxy); - // send to playback - let operation = SongOperation::SetSample(idx, meta, sample); - SONG_OP_SEND.get().unwrap().send(operation).await.unwrap(); - }) - .detach(); + // TODO: move to one of the sample controls + return PageResponse::None; + } + } + // trigger a oneshot playback of the selected sample + } + } + + // if this matches the cursor is in the sample list + if matches!(self.cursor, Cursor::Play | Cursor::Name(_)) { + if key_event.logical_key == Key::Named(NamedKey::ArrowUp) + && modifiers.state().is_empty() + { + if let Some(s) = self.selected_sample.checked_sub(1) { + self.select_sample(s); + self.send_to_header(events); + self.send_to_pattern(events); + return PageResponse::RequestRedraw; + } + } else if key_event.logical_key == Key::Named(NamedKey::ArrowDown) + && modifiers.state().is_empty() + { + if self.selected_sample + 1 < 100 { + self.select_sample(self.selected_sample + 1); + self.send_to_header(events); + self.send_to_pattern(events); + return PageResponse::RequestRedraw; + } + } else if key_event.logical_key == Key::Named(NamedKey::Enter) + && modifiers.state().is_empty() + { + self.load_audio_file(); + } + // TODO: add PageUp and PageDown + } else { + todo!("other UI elements that are per sample") } - // TODO: add PageUp and PageDown PageResponse::None } From f14ba8f8e0a00173029d0026d138840d1752487e Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Sun, 5 Oct 2025 15:18:37 +0200 Subject: [PATCH 13/19] use smaller numbers for coordinates --- src/coordinates.rs | 93 +++++++++++++++++----------------------- src/dialog/confirm.rs | 2 +- src/dialog/page_menu.rs | 47 ++++++++++++-------- src/draw_buffer.rs | 85 +++++++++++++++++------------------- src/header.rs | 2 +- src/main.rs | 4 +- src/pages/mod.rs | 7 +-- src/pages/order_list.rs | 18 ++++---- src/pages/pattern.rs | 18 +++++--- src/pages/sample_list.rs | 5 ++- src/render/gpu.rs | 5 ++- src/render/mod.rs | 7 ++- src/widgets/slider.rs | 27 +++++++----- src/widgets/text_in.rs | 49 ++++++++++----------- src/widgets/toggle.rs | 10 ++--- 15 files changed, 195 insertions(+), 184 deletions(-) diff --git a/src/coordinates.rs b/src/coordinates.rs index 4da17c6..c7b914e 100644 --- a/src/coordinates.rs +++ b/src/coordinates.rs @@ -1,22 +1,24 @@ use std::ops::{Add, RangeInclusive}; /// font size in pixel. font is a square -pub const FONT_SIZE: usize = 8; +pub const FONT_SIZE: u8 = 8; +pub const FONT_SIZE16: u16 = FONT_SIZE as u16; /// window size in characters -pub const WINDOW_SIZE_CHARS: (usize, usize) = (80, 50); +pub const WINDOW_SIZE_CHARS: (u8, u8) = (80, 50); /// window size in pixel -pub const WINDOW_SIZE: (usize, usize) = ( - FONT_SIZE * WINDOW_SIZE_CHARS.0, - FONT_SIZE * WINDOW_SIZE_CHARS.1, +pub const WINDOW_SIZE: (u16, u16) = ( + // TODO: use from once const traits are available + FONT_SIZE16 * WINDOW_SIZE_CHARS.0 as u16, + FONT_SIZE16 * WINDOW_SIZE_CHARS.1 as u16, ); /// CharRect as well as PixelRect uses all values inclusive, meaning the borders are included #[derive(Debug, Clone, Copy)] pub struct CharRect { - top: usize, - bot: usize, - left: usize, - right: usize, + top: u8, + bot: u8, + left: u8, + right: u8, } impl CharRect { @@ -24,7 +26,7 @@ impl CharRect { pub const PAGE_AREA: Self = Self::new(12, WINDOW_SIZE_CHARS.1 - 1, 0, WINDOW_SIZE_CHARS.0 - 1); pub const HEADER_AREA: Self = Self::new(0, 10, 0, WINDOW_SIZE_CHARS.0 - 1); - pub const fn new(top: usize, bot: usize, left: usize, right: usize) -> Self { + pub const fn new(top: u8, bot: u8, left: u8, right: u8) -> Self { assert!(top <= bot, "top needs to be smaller than bot"); assert!(left <= right, "left needs to be smaller than right"); assert!(bot < WINDOW_SIZE_CHARS.1, "lower than window bounds"); @@ -38,16 +40,16 @@ impl CharRect { } } - pub const fn top(self) -> usize { + pub const fn top(self) -> u8 { self.top } - pub const fn bot(self) -> usize { + pub const fn bot(self) -> u8 { self.bot } - pub const fn right(self) -> usize { + pub const fn right(self) -> u8 { self.right } - pub const fn left(self) -> usize { + pub const fn left(self) -> u8 { self.left } @@ -58,25 +60,18 @@ impl CharRect { } } - pub const fn width(self) -> usize { + pub const fn width(self) -> u8 { self.right - self.left } - pub const fn height(self) -> usize { + pub const fn height(self) -> u8 { self.bot - self.top } } -/// uncheck conversion, because CharPosition is a safe type impl From for CharRect { fn from(value: CharPosition) -> Self { Self::new(value.y, value.y, value.x, value.x) - // Self { - // top: value.y, - // bot: value.y, - // left: value.x, - // right: value.x, - // } } } @@ -95,14 +90,14 @@ impl From for accesskit::Rect { /// PixelRect as well as CharRect uses all values inclusive, meaning the borders are included #[derive(Debug, Clone, Copy)] pub struct PixelRect { - top: usize, - bot: usize, - right: usize, - left: usize, + top: u16, + bot: u16, + right: u16, + left: u16, } impl PixelRect { - pub const fn new(top: usize, bot: usize, right: usize, left: usize) -> Self { + pub const fn new(top: u16, bot: u16, right: u16, left: u16) -> Self { assert!(top <= bot, "top needs to be smaller than bot"); assert!(left <= right, "left needs to be smaller than right"); assert!(bot < WINDOW_SIZE.1, "lower than window bounds"); @@ -116,50 +111,42 @@ impl PixelRect { } } - pub const fn vertical_range(&self) -> RangeInclusive { + pub const fn vertical_range(&self) -> RangeInclusive { RangeInclusive::new(self.top, self.bot) } - pub const fn horizontal_range(&self) -> RangeInclusive { + pub const fn horizontal_range(&self) -> RangeInclusive { RangeInclusive::new(self.left, self.right) } - pub const fn top(&self) -> usize { + pub const fn top(&self) -> u16 { self.top } - pub const fn bot(&self) -> usize { + pub const fn bot(&self) -> u16 { self.bot } - pub const fn right(&self) -> usize { + pub const fn right(&self) -> u16 { self.right } - pub const fn left(&self) -> usize { + pub const fn left(&self) -> u16 { self.left } } -/// unchecked conversion because CharRect is a safe type impl From for PixelRect { fn from(value: CharRect) -> Self { Self::new( - value.top * FONT_SIZE, - (value.bot * FONT_SIZE) + FONT_SIZE - 1, - (value.right * FONT_SIZE) + FONT_SIZE - 1, - value.left * FONT_SIZE, + u16::from(value.top) * FONT_SIZE16, + u16::from(value.bot) * FONT_SIZE16 + FONT_SIZE16 - 1, + u16::from(value.right) * FONT_SIZE16 + FONT_SIZE16 - 1, + u16::from(value.left) * FONT_SIZE16, ) - // Self { - // top: value.top * FONT_SIZE, - // bot: (value.bot * FONT_SIZE) + FONT_SIZE - 1, - // right: (value.right * FONT_SIZE) + FONT_SIZE - 1, - // left: value.left * FONT_SIZE, - // } } } -/// unchecked conversion, because CharPosition is a safe type impl From for PixelRect { fn from(value: CharPosition) -> Self { Self::from(CharRect::from(value)) @@ -168,24 +155,24 @@ impl From for PixelRect { #[derive(Debug, Clone, Copy)] pub struct CharPosition { - x: usize, - y: usize, + x: u8, + y: u8, } impl CharPosition { #[track_caller] - pub const fn new(x: usize, y: usize) -> Self { + pub const fn new(x: u8, y: u8) -> Self { assert!(y < WINDOW_SIZE_CHARS.1); assert!(x < WINDOW_SIZE_CHARS.0); Self { x, y } } - pub const fn x(&self) -> usize { + pub const fn x(&self) -> u8 { self.x } - pub const fn y(&self) -> usize { + pub const fn y(&self) -> u8 { self.y } } @@ -199,11 +186,11 @@ impl Add for CharPosition { } } -impl Add<(usize, usize)> for CharPosition { +impl Add<(u8, u8)> for CharPosition { type Output = Self; #[track_caller] - fn add(self, rhs: (usize, usize)) -> Self::Output { + fn add(self, rhs: (u8, u8)) -> Self::Output { Self::new(self.x + rhs.0, self.y + rhs.1) } } diff --git a/src/dialog/confirm.rs b/src/dialog/confirm.rs index 3b7a24f..33afd0f 100644 --- a/src/dialog/confirm.rs +++ b/src/dialog/confirm.rs @@ -31,7 +31,7 @@ impl ConfirmDialog { ok_event: Option, cancel_event: Option, ) -> Self { - let width = (text.len() + 10).max(22); + let width = (u8::try_from(text.len()).unwrap() + 10).max(22); let per_side = width / 2; Self { text, diff --git a/src/dialog/page_menu.rs b/src/dialog/page_menu.rs index 3069924..de347c9 100644 --- a/src/dialog/page_menu.rs +++ b/src/dialog/page_menu.rs @@ -2,7 +2,7 @@ use winit::keyboard::{Key, NamedKey}; use crate::{ EventQueue, GlobalEvent, PlaybackType, - coordinates::{CharPosition, CharRect, FONT_SIZE, PixelRect}, + coordinates::{CharPosition, CharRect, FONT_SIZE16, PixelRect}, draw_buffer::DrawBuffer, pages::PagesEnum, }; @@ -60,7 +60,8 @@ impl Dialog for PageMenu { 1, ); for (num, (name, _)) in self.buttons.iter().enumerate() { - let text_color = match usize::from(self.selected) == num { + let num = u8::try_from(num).unwrap(); + let text_color = match self.selected == num { true => 11, false => 0, }; @@ -72,12 +73,11 @@ impl Dialog for PageMenu { Self::BACKGROUND_COLOR, ); let top = top + (3 * num) + 4; - let (top_left, bot_right) = match (self.pressed || self.sub_menu.is_some()) - && usize::from(self.selected) == num - { - true => (Self::BOTRIGHT_COLOR, Self::TOPLEFT_COLOR), - false => (Self::TOPLEFT_COLOR, Self::BOTRIGHT_COLOR), - }; + let (top_left, bot_right) = + match (self.pressed || self.sub_menu.is_some()) && self.selected == num { + true => (Self::BOTRIGHT_COLOR, Self::TOPLEFT_COLOR), + false => (Self::TOPLEFT_COLOR, Self::BOTRIGHT_COLOR), + }; let rect = CharRect::new(top, top + 2, left + 2, left + self.rect.width() - 2); draw_buffer.draw_out_border(rect, top_left, bot_right, 1); Self::draw_button_corners(rect, draw_buffer); @@ -211,13 +211,14 @@ impl PageMenu { const fn new( name: &'static str, pos: CharPosition, - width: usize, + width: u8, buttons: &'static [(&'static str, Action)], #[cfg(feature = "accesskit")] node_id: accesskit::NodeId, ) -> Self { let rect = CharRect::new( pos.y(), - pos.y() + 5 + (3 * buttons.len()), + // TODO: const trait + pos.y() + 5 + (3 * buttons.len() as u8), pos.x(), pos.x() + width + 3, ); @@ -235,19 +236,29 @@ impl PageMenu { } fn draw_button_corners(rect: CharRect, draw_buffer: &mut DrawBuffer) { - let framebuffer = &mut draw_buffer.framebuffer; + // let framebuffer = &mut draw_buffer.framebuffer; let pixel_rect = PixelRect::from(rect); // draw top right corner - for y in 0..FONT_SIZE { - for x in y..FONT_SIZE { - framebuffer[pixel_rect.top() + y][pixel_rect.right() - FONT_SIZE + x + 1] = - Self::BOTRIGHT_COLOR; + for y in 0..FONT_SIZE16 { + for x in y..FONT_SIZE16 { + draw_buffer.set( + pixel_rect.top() + y, + pixel_rect.right() - FONT_SIZE16 + x + 1, + Self::BOTRIGHT_COLOR, + ); + // framebuffer[pixel_rect.top() + y][pixel_rect.right() - FONT_SIZE + x + 1] = + // Self::BOTRIGHT_COLOR; } } // draw botleft corner - for y in 0..FONT_SIZE { - for x in 0..(FONT_SIZE - y) { - framebuffer[pixel_rect.bot() - y][pixel_rect.left() + x] = Self::BOTRIGHT_COLOR; + for y in 0..FONT_SIZE16 { + for x in 0..(FONT_SIZE16 - y) { + draw_buffer.set( + pixel_rect.bot() - y, + pixel_rect.left() + x, + Self::BOTRIGHT_COLOR, + ); + // framebuffer[pixel_rect.bot() - y][pixel_rect.left() + x] = Self::BOTRIGHT_COLOR; } } } diff --git a/src/draw_buffer.rs b/src/draw_buffer.rs index 642a0c6..afeeb5e 100644 --- a/src/draw_buffer.rs +++ b/src/draw_buffer.rs @@ -1,14 +1,16 @@ +use crate::coordinates::FONT_SIZE16; + use super::coordinates::{CharPosition, CharRect, FONT_SIZE, PixelRect, WINDOW_SIZE}; use font8x8::UnicodeFonts; pub struct DrawBuffer { - pub framebuffer: Box<[[u8; WINDOW_SIZE.0]; WINDOW_SIZE.1]>, + pub framebuffer: Box<[[u8; WINDOW_SIZE.0 as usize]; WINDOW_SIZE.1 as usize]>, } impl Default for DrawBuffer { fn default() -> Self { Self { - framebuffer: Box::new([[0; WINDOW_SIZE.0]; WINDOW_SIZE.1]), + framebuffer: Box::new([[0; WINDOW_SIZE.0 as usize]; WINDOW_SIZE.1 as usize]), } } } @@ -16,10 +18,8 @@ impl Default for DrawBuffer { impl DrawBuffer { pub const BACKGROUND_COLOR: u8 = 2; - pub fn new() -> Self { - Self { - framebuffer: Box::new([[0; WINDOW_SIZE.0]; WINDOW_SIZE.1]), - } + pub fn set(&mut self, y: u16, x: u16, color: u8) { + self.framebuffer[usize::from(y)][usize::from(x)] = color; } pub fn draw_char( @@ -29,15 +29,16 @@ impl DrawBuffer { fg_color: u8, bg_color: u8, ) { - // this is the top_left pixel - let position = (position.x() * FONT_SIZE, position.y() * FONT_SIZE); + let pixel_pos = PixelRect::from(CharRect::from(position)); + let position = (pixel_pos.left(), pixel_pos.top()); for (y, line) in char_data.iter().enumerate() { + let y = u16::try_from(y).unwrap(); for x in 0..8 { let color = match (line >> x) & 1 == 1 { true => fg_color, false => bg_color, }; - self.framebuffer[position.1 + y][position.0 + x] = color; + self.set(position.1 + y, position.0 + x, color); } } } @@ -52,7 +53,7 @@ impl DrawBuffer { for (num, char) in string.char_indices() { self.draw_char( font8x8::BASIC_FONTS.get(char).unwrap(), - position + (num, 0), + position + (u8::try_from(num).unwrap(), 0), fg_color, bg_color, ); @@ -69,9 +70,9 @@ impl DrawBuffer { fg_color: u8, bg_color: u8, ) { - let lenght = usize::from(lenght); - if string.len() > lenght { - self.draw_string(&string[..=lenght], position, fg_color, bg_color) + let ulen = usize::from(lenght); + if string.len() > ulen { + self.draw_string(&string[..=ulen], position, fg_color, bg_color) } else { self.draw_string(string, position, fg_color, bg_color); self.draw_rect( @@ -79,7 +80,7 @@ impl DrawBuffer { CharRect::new( position.y(), position.y(), - position.x() + string.len(), + position.x() + u8::try_from(string.len()).unwrap(), position.x() + lenght, ), ); @@ -92,22 +93,23 @@ impl DrawBuffer { char_rect: CharRect, top_left_color: u8, bot_right_color: u8, - thickness: usize, + thickness: u8, ) { assert!(thickness <= FONT_SIZE); assert!(thickness > 0); let pixel_rect = PixelRect::from(char_rect); + let thickness = u16::from(thickness); for x in pixel_rect.left()..=pixel_rect.right() { for y in 0..thickness { - self.framebuffer[pixel_rect.top() + y][x] = top_left_color; - self.framebuffer[pixel_rect.bot() - y][x] = bot_right_color; + self.set(pixel_rect.top() + y, x, top_left_color); + self.set(pixel_rect.bot() - y, x, bot_right_color); } } for y in pixel_rect.top()..=pixel_rect.bot() { for x in 0..thickness { - self.framebuffer[y][pixel_rect.right() - x] = bot_right_color; - self.framebuffer[y][pixel_rect.left() + x] = top_left_color; + self.set(y, pixel_rect.right() - x, bot_right_color); + self.set(y, pixel_rect.left() + x, top_left_color); } } } @@ -118,45 +120,42 @@ impl DrawBuffer { background_color: u8, top_left_color: u8, bot_right_color: u8, - thickness: usize, + thickness: u8, ) { assert!(thickness < FONT_SIZE); assert!(thickness > 0); - // needs to be between 0 and FONT_SIZE - // const BOX_THICKNESS: usize = 1; - // const SPACE_FROM_BORDER: usize = FONT_SIZE - BOX_THICKNESS; - let space_from_border = FONT_SIZE - thickness; + let space_from_border = FONT_SIZE16 - u16::from(thickness); let pixel_rect = PixelRect::from(char_rect); // all pixel lines except those in top and bottom char line - for y in (pixel_rect.top() + FONT_SIZE)..=(pixel_rect.bot() - FONT_SIZE) { + for y in (pixel_rect.top() + FONT_SIZE16)..=(pixel_rect.bot() - FONT_SIZE16) { // left side foreground - for x in (pixel_rect.left() + space_from_border)..(pixel_rect.left() + FONT_SIZE) { - self.framebuffer[y][x] = top_left_color; + for x in (pixel_rect.left() + space_from_border)..(pixel_rect.left() + FONT_SIZE16) { + self.set(y, x, top_left_color); } // left side background for x in pixel_rect.left()..(pixel_rect.left() + space_from_border) { - self.framebuffer[y][x] = background_color; + self.set(y, x, background_color); } // need the plus ones, as the '..' would need to be exclusive on the low and inclusive on the high, which i dont know how to do for x in - (pixel_rect.right() - FONT_SIZE + 1)..(pixel_rect.right() - space_from_border + 1) + (pixel_rect.right() - FONT_SIZE16 + 1)..(pixel_rect.right() - space_from_border + 1) { - self.framebuffer[y][x] = bot_right_color; + self.set(y, x, bot_right_color); } // right side background for x in (pixel_rect.right() - space_from_border + 1)..=pixel_rect.right() { - self.framebuffer[y][x] = background_color; + self.set(y, x, background_color); } } // top char line - for y in pixel_rect.top()..(pixel_rect.top() + FONT_SIZE) { + for y in pixel_rect.top()..(pixel_rect.top() + FONT_SIZE16) { if y < pixel_rect.top() + space_from_border { for x in pixel_rect.horizontal_range() { - self.framebuffer[y][x] = background_color; + self.set(y, x, background_color); } } else { for x in pixel_rect.left()..=pixel_rect.right() { @@ -173,17 +172,17 @@ impl DrawBuffer { bot_right_color }; - self.framebuffer[y][x] = color; + self.set(y, x, color); } } } // bottom char line - for y in (pixel_rect.bot() - FONT_SIZE + 1)..=pixel_rect.bot() { + for y in (pixel_rect.bot() - FONT_SIZE16 + 1)..=pixel_rect.bot() { // does the top 'SPACE_FROM_BORDER' rows in background color if y > pixel_rect.bot() - space_from_border { for x in pixel_rect.horizontal_range() { - self.framebuffer[y][x] = background_color; + self.set(y, x, background_color); } } else { for x in pixel_rect.horizontal_range() { @@ -197,7 +196,8 @@ impl DrawBuffer { bot_right_color }; - self.framebuffer[y][x] = color; + // self.framebuffer[y][x] = color; + self.set(y, x, color); } } } @@ -209,19 +209,14 @@ impl DrawBuffer { } pub fn draw_pixel_rect(&mut self, color: u8, rect: PixelRect) { - for line in &mut self.framebuffer[rect.top()..=rect.bot()] { - line[rect.left()..=rect.right()].fill(color); + for line in &mut self.framebuffer[usize::from(rect.top())..=usize::from(rect.bot())] { + line[usize::from(rect.left())..=usize::from(rect.right())].fill(color); } } - /// for debugging. draws a pixel in the middle of the char - fn mark_char(&mut self, position: CharPosition) { - self.framebuffer[(position.y() + 4) * WINDOW_SIZE.0][position.x() + 4] = 1; - } - pub fn show_colors(&mut self) { for i in 0..crate::render::palettes::PALETTE_SIZE as u8 { - self.draw_rect(i, CharPosition::new(i as usize, 5).into()); + self.draw_rect(i, CharPosition::new(i, 5).into()); } } } diff --git a/src/header.rs b/src/header.rs index 1718e49..a7d1c5d 100644 --- a/src/header.rs +++ b/src/header.rs @@ -144,7 +144,7 @@ impl Header { let char_color = if char.is_ascii_digit() { 3 } else { 0 }; draw_buffer.draw_char( font8x8::BASIC_FONTS.get(char).unwrap(), - CharPosition::new(2 + index, 9), + CharPosition::new(2 + u8::try_from(index).unwrap(), 9), char_color, 2, ); diff --git a/src/main.rs b/src/main.rs index 7acf801..96c07de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -478,7 +478,7 @@ impl ApplicationHandler for App { ) }); } - WindowEvent::ActionRequested(action_request) => todo!(), + WindowEvent::ActionRequested(_) => todo!(), // i don't have any extra state for accessability so i don't need to cleanup anything WindowEvent::AccessibilityDeactivated => (), } @@ -501,7 +501,7 @@ impl App { pub fn new(proxy: EventLoopProxy) -> Self { Self { window: None, - draw_buffer: DrawBuffer::new(), + draw_buffer: DrawBuffer::default(), modifiers: Modifiers::default(), ui_pages: AllPages::new(proxy.clone()), dialog_manager: DialogManager::new(), diff --git a/src/pages/mod.rs b/src/pages/mod.rs index 2414ba3..bc9e1a8 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -160,19 +160,20 @@ impl AllPages { pub fn draw_constant(&mut self, draw_buffer: &mut DrawBuffer) { // draw page title let title = self.get_title(); + let title_len = u8::try_from(title.len()).expect("title doesn't fit on the page"); let middle = WINDOW_SIZE_CHARS.0 / 2; - let str_start = middle - (title.len() / 2); + let str_start = middle - (title_len / 2); draw_buffer.draw_string(title, CharPosition::new(str_start, 11), 0, 2); const DOTTED: [u8; 8] = [0, 0, 0, 0b01010101, 0, 0, 0, 0]; draw_buffer.draw_rect(2, CharRect::new(11, 11, str_start - 1, str_start - 1)); draw_buffer.draw_rect( 2, - CharRect::new(11, 11, str_start + title.len(), str_start + title.len()), + CharRect::new(11, 11, str_start + title_len, str_start + title_len), ); for x in 1..=(str_start - 2) { draw_buffer.draw_char(DOTTED, CharPosition::new(x, 11), 1, 2); } - for x in (str_start + title.len() + 1)..=(WINDOW_SIZE_CHARS.0 - 2) { + for x in (str_start + title_len + 1)..=(WINDOW_SIZE_CHARS.0 - 2) { draw_buffer.draw_char(DOTTED, CharPosition::new(x, 11), 1, 2); } // draw page const diff --git a/src/pages/order_list.rs b/src/pages/order_list.rs index eb6f0ec..246d23f 100644 --- a/src/pages/order_list.rs +++ b/src/pages/order_list.rs @@ -1,7 +1,6 @@ use std::str::from_utf8; use std::{array, io::Write}; -use symphonia::core::conv::IntoSample; use torque_tracker_engine::project::song::{Pan, SongOperation}; use torque_tracker_engine::{file::impulse_format::header::PatternOrder, project::song::Song}; use winit::keyboard::{Key, ModifiersState, NamedKey}; @@ -66,12 +65,12 @@ impl OrderListPage { pattern_order: [PatternOrder::EndOfSong; Song::MAX_ORDERS], order_playback: None, volume: array::from_fn(|idx| { + let idx = u8::try_from(idx).unwrap(); let pos = if idx >= 32 { CharPosition::new(65, 15 + idx - 32) } else { CharPosition::new(31, 15 + idx) }; - let idx = u8::try_from(idx).unwrap(); Slider::new( 64, pos, @@ -84,18 +83,18 @@ impl OrderListPage { }, #[cfg(feature = "accesskit")] ( - accesskit::NodeId(Self::PAGE_ID + u64::try_from(idx).unwrap()), + accesskit::NodeId(Self::PAGE_ID + u64::from(idx)), format!("Volume Channel {idx}").into_boxed_str(), ), ) }), pan: array::from_fn(|idx| { + let idx = u8::try_from(idx).unwrap(); let pos = if idx >= 32 { CharPosition::new(65, 15 + idx - 32) } else { CharPosition::new(31, 15 + idx) }; - let idx = u8::try_from(idx).unwrap(); Slider::new( 32, pos, @@ -108,7 +107,7 @@ impl OrderListPage { }, #[cfg(feature = "accesskit")] ( - accesskit::NodeId(Self::PAGE_ID + u64::try_from(idx).unwrap()), + accesskit::NodeId(Self::PAGE_ID + u64::from(idx)), format!("Pan Channel {idx}").into_boxed_str(), ), ) @@ -233,6 +232,7 @@ impl Page for OrderListPage { const ORDER_BASE_POS: CharPosition = CharPosition::new(2, 15); let mut buf = [0; 3]; for (pos, order) in (self.order_draw..=self.order_draw + 31).enumerate() { + let pos = u8::try_from(pos).unwrap(); // row index let mut curse: std::io::Cursor<&mut [u8]> = std::io::Cursor::new(&mut buf); write!(&mut curse, "{:03}", order).unwrap(); @@ -306,8 +306,8 @@ impl Page for OrderListPage { .unwrap(), ORDER_BASE_POS + CharPosition::new( - 4 + usize::from(self.order_cursor.digit), - usize::from(self.order_cursor.order - self.order_draw), + 4 + self.order_cursor.digit, + u8::try_from(self.order_cursor.order - self.order_draw).unwrap(), ), 0, 3, @@ -318,9 +318,9 @@ impl Page for OrderListPage { let mut curse: std::io::Cursor<&mut [u8]> = std::io::Cursor::new(&mut buf); write!(&mut curse, "{:02}", c).unwrap(); let pos = if c <= 32 { - CHANNEL_BASE_LEFT + CharPosition::new(0, usize::from(c - 1)) + CHANNEL_BASE_LEFT + CharPosition::new(0, c - 1) } else { - CHANNEL_BASE_RIGHT + CharPosition::new(0, usize::from(c - 33)) + CHANNEL_BASE_RIGHT + CharPosition::new(0, c - 33) }; draw_buffer.draw_string(CHANNEL, pos, 3, 2); draw_buffer.draw_string( diff --git a/src/pages/pattern.rs b/src/pages/pattern.rs index 2e34932..4732ccd 100644 --- a/src/pages/pattern.rs +++ b/src/pages/pattern.rs @@ -135,7 +135,7 @@ impl PatternPage { /// how many rows the cursor is moved when pressing pageup/down // TODO: make configurable const PAGE_AS_ROWS: u16 = 16; - const CHANNEL_WIDTH: usize = 14; + const CHANNEL_WIDTH: u8 = 14; // TODO: make configurable const ROW_HIGHTLIGHT_MINOR: u16 = 4; @@ -295,7 +295,7 @@ impl Page for PatternPage { }; draw_buffer.draw_string( from_utf8(&buf).unwrap(), - BASE_POS + CharPosition::new(0, index), + BASE_POS + CharPosition::new(0, u8::try_from(index).unwrap()), text_color, 2, ); @@ -312,7 +312,7 @@ impl Page for PatternPage { draw_buffer.draw_string( from_utf8(&buf).unwrap(), - BASE_POS + (index * Self::CHANNEL_WIDTH, 0), + BASE_POS + (u8::try_from(index).unwrap() * Self::CHANNEL_WIDTH, 0), 3, 1, ); @@ -405,7 +405,11 @@ impl Page for PatternPage { }) .map(|e| (*e).into()) .unwrap_or_default(); - let pos = EVENT_BASE_POS + (c_idx * Self::CHANNEL_WIDTH, r_idx); + let pos = EVENT_BASE_POS + + ( + u8::try_from(c_idx).unwrap() * Self::CHANNEL_WIDTH, + u8::try_from(r_idx).unwrap(), + ); draw_buffer.draw_char(view.note1, pos, 6, background_color); draw_buffer.draw_char(view.note2, pos + (1, 0), FOREGROUND, background_color); draw_buffer.draw_char(view.octave, pos + (2, 0), FOREGROUND, background_color); @@ -431,8 +435,8 @@ impl Page for PatternPage { assert!(self.cursor_position.0.channel >= self.draw_position.channel); assert!(self.cursor_position.0.row >= self.draw_position.row); let c_idx = self.cursor_position.0.channel - self.draw_position.channel; - let r_idx = self.cursor_position.0.row - self.draw_position.row; - let pos = EVENT_BASE_POS + (c_idx as usize * Self::CHANNEL_WIDTH, r_idx as usize); + let r_idx = u8::try_from(self.cursor_position.0.row - self.draw_position.row).unwrap(); + let pos = EVENT_BASE_POS + (c_idx * Self::CHANNEL_WIDTH, r_idx); match self.cursor_position.1 { InEventPosition::Note => draw_buffer.draw_char(view.note1, pos, 0, 3), InEventPosition::Octave => draw_buffer.draw_char(view.octave, pos + (2, 0), 0, 3), @@ -450,7 +454,7 @@ impl Page for PatternPage { draw_buffer.draw_rect(2, CharRect::PAGE_AREA); // draw channel headers const parts - for index in 0..Self::DRAWN_CHANNELS as usize { + for index in 0..Self::DRAWN_CHANNELS { const BASE_POS: CharPosition = CharPosition::new(5, 14); let pos = BASE_POS + (index * Self::CHANNEL_WIDTH, 0); draw_buffer.draw_rect(1, (pos + (11, 0)).into()); diff --git a/src/pages/sample_list.rs b/src/pages/sample_list.rs index c20ea89..282f3d3 100644 --- a/src/pages/sample_list.rs +++ b/src/pages/sample_list.rs @@ -294,6 +294,7 @@ impl Page for SampleList { const PLAY_BASE_POS: CharPosition = CharPosition::new(31, 13); let mut buf = [0; 2]; for (i, n) in (self.sample_view..=self.sample_view + Self::SAMPLE_VIEW_COUNT).enumerate() { + let i = u8::try_from(i).unwrap(); // number let mut curse: io::Cursor<&mut [u8]> = io::Cursor::new(&mut buf); write!(curse, "{:02}", n).unwrap(); @@ -317,7 +318,7 @@ impl Page for SampleList { && let Cursor::Name(text_cursor) = self.cursor && let Some(name) = name { - let cursor_char_pos = name_pos + CharPosition::new(text_cursor.into(), 0); + let cursor_char_pos = name_pos + CharPosition::new(text_cursor, 0); if usize::from(text_cursor) < name.len() { draw_buffer.draw_char( font8x8::BASIC_FONTS @@ -371,7 +372,7 @@ impl Page for SampleList { } match self.cursor { - Cursor::Name(mut text_cursor) => { + Cursor::Name(text_cursor) => { if key_event.logical_key == Key::Named(NamedKey::Tab) { if modifiers.state().shift_key() { // TODO: shift aroung to one of the buttons diff --git a/src/render/gpu.rs b/src/render/gpu.rs index 432cbc5..b8c7af8 100644 --- a/src/render/gpu.rs +++ b/src/render/gpu.rs @@ -16,6 +16,8 @@ use winit::window::Window; use super::palettes::{PALETTE_SIZE, Palette, RGB10A2}; use crate::coordinates::WINDOW_SIZE; +// use crate::coordinates::WINDOW_SIZE as WINDOW_SIZE16; +// const WINDOW_SIZE: (usize, usize) = (WINDOW_SIZE16.0 as usize, WINDOW_SIZE16.1 as usize); pub struct GPUState { surface: Surface<'static>, @@ -325,12 +327,11 @@ impl GPUState { pub fn render( &mut self, - framebuffer: &[[u8; WINDOW_SIZE.0]; WINDOW_SIZE.1], + framebuffer: &[[u8; WINDOW_SIZE.0 as usize]; WINDOW_SIZE.1 as usize], ) -> Result<(), SurfaceError> { // SAFETY: let framebuffer = framebuffer.as_flattened(); - assert!(framebuffer.len() == WINDOW_SIZE.0 * WINDOW_SIZE.1); const BYTES_PER_ROW: Option = Some(WINDOW_SIZE.0 as u32); const ROWS_PER_IMAGE: Option = Some(WINDOW_SIZE.1 as u32); diff --git a/src/render/mod.rs b/src/render/mod.rs index 380bc4c..4e54642 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -6,12 +6,17 @@ use std::sync::Arc; use winit::{dpi::PhysicalSize, event_loop::ActiveEventLoop, window::Window}; -use crate::coordinates::WINDOW_SIZE; +use crate::coordinates::WINDOW_SIZE as WINDOW_SIZE16; +const WINDOW_SIZE: (usize, usize) = (WINDOW_SIZE16.0 as usize, WINDOW_SIZE16.1 as usize); use palettes::{Palette, RGB8}; #[cfg(not(any(feature = "gpu_scaling", feature = "soft_scaling")))] compile_error!("at least one of gpu_scaling or soft_scaling needs to be active"); +#[expect( + clippy::large_enum_variant, + reason = "this doesn't get moved a lot, so it doesn't matter. If it's not in the enum i also don't box it" +)] #[cfg(all(feature = "gpu_scaling", feature = "soft_scaling"))] pub enum BothRenderBackend { GPU(GPURenderBackend), diff --git a/src/widgets/slider.rs b/src/widgets/slider.rs index 30151fa..2272866 100644 --- a/src/widgets/slider.rs +++ b/src/widgets/slider.rs @@ -7,7 +7,7 @@ use winit::keyboard::{Key, NamedKey}; use crate::{ EventQueue, GlobalEvent, - coordinates::{CharPosition, CharRect, FONT_SIZE, PixelRect, WINDOW_SIZE_CHARS}, + coordinates::{CharPosition, CharRect, FONT_SIZE16, PixelRect, WINDOW_SIZE_CHARS}, dialog::slider_dialog::SliderDialog, draw_buffer::DrawBuffer, }; @@ -108,7 +108,7 @@ impl BoundNumber { pub struct Slider { number: BoundNumber, position: CharPosition, - width: usize, + width: u8, next_widget: NextWidget, dialog_return: fn(i16) -> GlobalEvent, #[cfg(feature = "accesskit")] @@ -121,7 +121,7 @@ impl Widget for Slider { const CURSOR_COLOR: u8 = 2; const CURSOR_SELECTED_COLOR: u8 = 3; - const CURSOR_WIDTH: usize = 4; + const CURSOR_WIDTH: u16 = 4; draw_buffer.draw_string( &format!("{:03}", *self.number), @@ -145,28 +145,33 @@ impl Widget for Slider { } else { // shift value scale. this is the new MAX // MIN -> MAX => 0 -> (MAX-MIN) - let num_possible_values = usize::from(MAX.abs_diff(MIN)); + let num_possible_values = MAX.abs_diff(MIN); // shift value scale as shown below // MIN -> MAX => 0 -> (MAX-MIN) - let value = usize::from(self.number.abs_diff(MIN)); + let value = self.number.abs_diff(MIN); // first + 1 makes it have a border on the left side // rest mostly stole from original source code - 1 + value * (self.width * FONT_SIZE + 1) / num_possible_values + 1 + value * (u16::from(self.width) * FONT_SIZE16 + 1) / num_possible_values }; let color = match selected { true => CURSOR_SELECTED_COLOR, false => CURSOR_COLOR, }; + let self_pixel_rect = PixelRect::from(CharRect::from(self.position)); let cursor_pixel_rect = PixelRect::new( // +1 to have a space above - (self.position.y() * FONT_SIZE) + 1, + // (self.position.y() * FONT_SIZE16) + 1, + self_pixel_rect.top() + 1, // -2 to make if have a 1. not go into the next line and 2. have a empty row below - (self.position.y() * FONT_SIZE) + (FONT_SIZE - 2), - (self.position.x() * FONT_SIZE) + cursor_pos + CURSOR_WIDTH, - (self.position.x() * FONT_SIZE) + cursor_pos, + self_pixel_rect.bot() - 2, + // (self.position.y() * FONT_SIZE16) + (FONT_SIZE16 - 2), + self_pixel_rect.left() + cursor_pos + CURSOR_WIDTH, + // (self.position.x() * FONT_SIZE16) + cursor_pos + CURSOR_WIDTH, + self_pixel_rect.left() + cursor_pos, + // (self.position.x() * FONT_SIZE16) + cursor_pos, ); draw_buffer.draw_pixel_rect(color, cursor_pixel_rect); @@ -262,7 +267,7 @@ impl Slider { pub fn new( inital_value: i16, position: CharPosition, - width: usize, + width: u8, next_widget: NextWidget, dialog_return: fn(i16) -> GlobalEvent, #[cfg(feature = "accesskit")] access: (accesskit::NodeId, Box), diff --git a/src/widgets/text_in.rs b/src/widgets/text_in.rs index d467d04..5bac6c8 100644 --- a/src/widgets/text_in.rs +++ b/src/widgets/text_in.rs @@ -4,7 +4,7 @@ use winit::keyboard::{Key, NamedKey}; use crate::{ EventQueue, - coordinates::{CharPosition, WINDOW_SIZE}, + coordinates::{CharPosition, WINDOW_SIZE_CHARS}, draw_buffer::DrawBuffer, }; @@ -18,7 +18,8 @@ pub struct TextIn { width: u8, text: AsciiString, next_widget: NextWidget, - cursor_pos: usize, + // fixed width, so text length is also fixed + cursor_pos: u8, #[cfg(feature = "accesskit")] access: (accesskit::NodeId, &'static str), } @@ -29,11 +30,10 @@ impl Widget for TextIn { // draw the cursor by overdrawing a letter if selected { let cursor_char_pos = self.pos + CharPosition::new(self.cursor_pos, 0); - if self.cursor_pos < self.text.len() { + let upos = usize::from(self.cursor_pos); + if upos < self.text.len() { draw_buffer.draw_char( - font8x8::BASIC_FONTS - .get(self.text[self.cursor_pos].into()) - .unwrap(), + font8x8::BASIC_FONTS.get(self.text[upos].into()).unwrap(), cursor_char_pos, 0, 3, @@ -50,15 +50,18 @@ impl Widget for TextIn { key_event: &winit::event::KeyEvent, _events: &mut EventQueue<'_>, ) -> WidgetResponse { - process_input( + let mut pos = usize::from(self.cursor_pos); + let res = process_input( &mut self.text, self.width, &self.next_widget, - &mut self.cursor_pos, + &mut pos, None, modifiers, key_event, - ) + ); + self.cursor_pos = u8::try_from(pos).unwrap(); + res } #[cfg(feature = "accesskit")] @@ -87,14 +90,15 @@ impl Widget for TextIn { // .map(|w| u8::try_from(w.len()).unwrap()) // .collect::>(), // ); + let character_index = usize::from(self.cursor_pos); root_node.set_text_selection(TextSelection { anchor: TextPosition { node: text_node_id, - character_index: self.cursor_pos, + character_index, }, focus: TextPosition { node: text_node_id, - character_index: self.cursor_pos, + character_index, // character_index: self.text.as_str().len().min(self.cursor_pos + 1), }, }); @@ -112,7 +116,7 @@ impl TextIn { next_widget: NextWidget, #[cfg(feature = "accesskit")] access: (accesskit::NodeId, &'static str), ) -> Self { - assert!(pos.x() + usize::from(width) < WINDOW_SIZE.0); + assert!(pos.x() + width < WINDOW_SIZE_CHARS.0); // right and left keys are used in the widget itself. doeesnt make sense to put NextWidget there assert!(next_widget.right.is_none()); assert!(next_widget.left.is_none()); @@ -129,10 +133,11 @@ impl TextIn { } // not tested + // TODO: can panic if the string is too long pub fn set_string(&mut self, new_str: String) -> Result<(), ascii::FromAsciiError> { self.text = AsciiString::from_ascii(new_str)?; self.text.truncate(usize::from(self.width)); - self.cursor_pos = self.text.len(); + self.cursor_pos = u8::try_from(self.text.len()).unwrap(); Ok(()) } @@ -140,14 +145,6 @@ impl TextIn { pub fn get_str(&self) -> &str { self.text.as_str() } - - fn insert_char(&mut self, char: AsciiChar) { - if self.cursor_pos < usize::from(self.width) { - self.cursor_pos += 1; - } - self.text.insert(self.cursor_pos - 1, char); - self.text.truncate(usize::from(self.width)); - } } pub struct TextInScroll { @@ -170,8 +167,12 @@ impl Widget for TextInScroll { ); if selected { - let cursor_char_pos = - self.pos + CharPosition::new(self.cursor_pos - self.scroll_offset, 0); + let cursor_char_pos = self.pos + + CharPosition::new( + // this minus should make this diff be less then self.width + u8::try_from(self.cursor_pos - self.scroll_offset).unwrap(), + 0, + ); if self.cursor_pos < self.text.len() { draw_buffer.draw_char( font8x8::BASIC_FONTS @@ -214,7 +215,7 @@ impl TextInScroll { pub fn new(pos: CharPosition, width: u8, next_widget: NextWidget) -> Self { assert!(next_widget.right.is_none()); assert!(next_widget.left.is_none()); - assert!(pos.x() + usize::from(width) < WINDOW_SIZE.0); + assert!(pos.x() + width < WINDOW_SIZE_CHARS.0); Self { pos, diff --git a/src/widgets/toggle.rs b/src/widgets/toggle.rs index f885e6c..8948c2b 100644 --- a/src/widgets/toggle.rs +++ b/src/widgets/toggle.rs @@ -2,7 +2,7 @@ use winit::keyboard::{Key, NamedKey}; use crate::{ EventQueue, - coordinates::{CharPosition, CharRect, WINDOW_SIZE}, + coordinates::{CharPosition, CharRect, WINDOW_SIZE_CHARS}, draw_buffer::DrawBuffer, }; @@ -10,7 +10,7 @@ use super::{NextWidget, Widget, WidgetResponse}; pub struct Toggle { pos: CharPosition, - width: usize, + width: u8, state: usize, next_widget: NextWidget, variants: &'static [(T, &'static str)], @@ -24,7 +24,7 @@ impl Widget for Toggle { CharRect::new( self.pos.y(), self.pos.y(), - self.pos.x() + str.len(), + self.pos.x() + u8::try_from(str.len()).unwrap(), self.pos.x() + self.width, ), ); @@ -61,11 +61,11 @@ impl Widget for Toggle { impl Toggle { pub fn new( pos: CharPosition, - width: usize, + width: u8, next_widget: NextWidget, variants: &'static [(T, &'static str)], ) -> Self { - assert!(pos.x() + width < WINDOW_SIZE.0); + assert!(pos.x() + width < WINDOW_SIZE_CHARS.0); Self { pos, From c047cfeaa68babacb49ab063d25063047acd312e Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Mon, 6 Oct 2025 15:41:51 +0200 Subject: [PATCH 14/19] rewrite main menu, to only allow one nesting --- Cargo.toml | 2 + src/dialog/mod.rs | 1 + src/dialog/page_menu.rs | 249 +++++++++++++++++++++++----------------- src/main.rs | 4 +- 4 files changed, 150 insertions(+), 106 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5bc3add..4f7746d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,3 +47,5 @@ default = ["soft_scaling", "gpu_scaling", "accesskit"] [lints.clippy] uninlined_format_args = "allow" +new_without_default = "allow" + diff --git a/src/dialog/mod.rs b/src/dialog/mod.rs index 82adf0b..f3a0cc9 100644 --- a/src/dialog/mod.rs +++ b/src/dialog/mod.rs @@ -6,6 +6,7 @@ use winit::event::{KeyEvent, Modifiers}; use crate::{EventQueue, draw_buffer::DrawBuffer}; +#[derive(PartialEq, Eq)] pub enum DialogResponse { RequestRedraw, // should also close all Dialogs diff --git a/src/dialog/page_menu.rs b/src/dialog/page_menu.rs index de347c9..227a014 100644 --- a/src/dialog/page_menu.rs +++ b/src/dialog/page_menu.rs @@ -19,6 +19,7 @@ enum Action { } // Main missing, because it cant be opened from a menu +#[derive(Clone, Copy)] enum Menu { File, Playback, @@ -33,13 +34,65 @@ pub struct PageMenu { selected: u8, pressed: bool, buttons: &'static [(&'static str, Action)], - sub_menu: Option>, #[cfg(feature = "accesskit")] node_id: accesskit::NodeId, } -impl Dialog for PageMenu { - fn draw(&self, draw_buffer: &mut DrawBuffer) { +impl PageMenu { + const BACKGROUND_COLOR: u8 = 2; + const TOPLEFT_COLOR: u8 = 3; + const BOTRIGHT_COLOR: u8 = 1; + const fn new( + name: &'static str, + pos: CharPosition, + width: u8, + buttons: &'static [(&'static str, Action)], + #[cfg(feature = "accesskit")] node_id: accesskit::NodeId, + ) -> Self { + let rect = CharRect::new( + pos.y(), + // TODO: const trait + pos.y() + 5 + (3 * buttons.len() as u8), + pos.x(), + pos.x() + width + 3, + ); + + Self { + name, + rect, + selected: 0, + pressed: false, + buttons, + #[cfg(feature = "accesskit")] + node_id, + } + } + fn draw_button_corners(rect: CharRect, draw_buffer: &mut DrawBuffer) { + // let framebuffer = &mut draw_buffer.framebuffer; + let pixel_rect = PixelRect::from(rect); + // draw top right corner + for y in 0..FONT_SIZE16 { + for x in y..FONT_SIZE16 { + draw_buffer.set( + pixel_rect.top() + y, + pixel_rect.right() - FONT_SIZE16 + x + 1, + Self::BOTRIGHT_COLOR, + ); + } + } + // draw botleft corner + for y in 0..FONT_SIZE16 { + for x in 0..(FONT_SIZE16 - y) { + draw_buffer.set( + pixel_rect.bot() - y, + pixel_rect.left() + x, + Self::BOTRIGHT_COLOR, + ); + } + } + } + + fn draw(&self, has_child: bool, draw_buffer: &mut DrawBuffer) { let top_left = self.rect.top_left(); let top = top_left.y(); let left = top_left.x(); @@ -73,69 +126,42 @@ impl Dialog for PageMenu { Self::BACKGROUND_COLOR, ); let top = top + (3 * num) + 4; - let (top_left, bot_right) = - match (self.pressed || self.sub_menu.is_some()) && self.selected == num { - true => (Self::BOTRIGHT_COLOR, Self::TOPLEFT_COLOR), - false => (Self::TOPLEFT_COLOR, Self::BOTRIGHT_COLOR), - }; + let (top_left, bot_right) = match (self.pressed || has_child) && self.selected == num { + true => (Self::BOTRIGHT_COLOR, Self::TOPLEFT_COLOR), + false => (Self::TOPLEFT_COLOR, Self::BOTRIGHT_COLOR), + }; let rect = CharRect::new(top, top + 2, left + 2, left + self.rect.width() - 2); draw_buffer.draw_out_border(rect, top_left, bot_right, 1); Self::draw_button_corners(rect, draw_buffer); } - if let Some(sub) = self.sub_menu.as_ref() { - sub.draw(draw_buffer); - } } fn process_input( &mut self, key_event: &winit::event::KeyEvent, - modifiers: &winit::event::Modifiers, - event: &mut EventQueue<'_>, - ) -> DialogResponse { - if key_event.state.is_pressed() && key_event.logical_key == Key::Named(NamedKey::Escape) { - if self.sub_menu.is_some() { - self.sub_menu = None; - event.push(GlobalEvent::ConstRedraw); - return DialogResponse::RequestRedraw; - } else { - return DialogResponse::Close; - } - } - - if let Some(sub) = self.sub_menu.as_mut() { - return sub.process_input(key_event, modifiers, event); - } - + events: &mut EventQueue<'_>, + ) -> (DialogResponse, Option) { if key_event.logical_key == Key::Named(NamedKey::Enter) { if key_event.state.is_pressed() { self.pressed = true; - return DialogResponse::RequestRedraw; + return (DialogResponse::RequestRedraw, None); } else if self.pressed { self.pressed = false; match &self.buttons[usize::from(self.selected)].1 { Action::Menu(menu) => { - let menu = match menu { - Menu::File => Self::file(), - Menu::Playback => Self::playback(), - Menu::Sample => Self::sample(), - Menu::Instrument => Self::instrument(), - Menu::Settings => Self::settings(), - }; - self.sub_menu = Some(Box::new(menu)); - return DialogResponse::RequestRedraw; + return (DialogResponse::RequestRedraw, Some(*menu)); } Action::Page(page) => { - event.push(GlobalEvent::GoToPage(*page)); - return DialogResponse::Close; + events.push(GlobalEvent::GoToPage(*page)); + return (DialogResponse::Close, None); } Action::NotYetImplemented => { - println!("Not yet implementes"); - return DialogResponse::RequestRedraw; + eprintln!("Not yet implementes"); + return (DialogResponse::RequestRedraw, None); } Action::Event(global_event) => { - event.push(global_event.clone()); - return DialogResponse::Close; + events.push(global_event.clone()); + return (DialogResponse::Close, None); } } } @@ -145,17 +171,17 @@ impl Dialog for PageMenu { if key_event.logical_key == Key::Named(NamedKey::ArrowUp) && self.selected > 0 { self.selected -= 1; self.pressed = false; - return DialogResponse::RequestRedraw; + return (DialogResponse::RequestRedraw, None); } else if key_event.logical_key == Key::Named(NamedKey::ArrowDown) && usize::from(self.selected) < self.buttons.len() - 1 { self.selected += 1; self.pressed = false; - return DialogResponse::RequestRedraw; + return (DialogResponse::RequestRedraw, None); } } - DialogResponse::None + (DialogResponse::None, None) } #[cfg(feature = "accesskit")] @@ -169,16 +195,16 @@ impl Dialog for PageMenu { let mut root = Node::new(Role::Dialog); root.set_label(self.name); root.set_bounds(dbg!(Rect::from(self.rect))); - let mut selected = NodeId(u64::from(self.selected) + self.node_id.0 + 1); + let selected = NodeId(u64::from(self.selected) + self.node_id.0 + 1); // root of the sub_menu. Will be set as the child of the selected button - let sub_menu = self.sub_menu.as_ref().map(|m| { - let resp = m.build_tree(tree); + // let sub_menu = self.sub_menu.as_ref().map(|m| { + // let resp = m.build_tree(tree); - selected = resp.selected; + // selected = resp.selected; - resp.root - }); + // resp.root + // }); for (num, (name, _)) in self.buttons.iter().enumerate() { let mut node = Node::new(Role::Button); @@ -187,9 +213,9 @@ impl Dialog for PageMenu { node.set_label(*name); if usize::from(self.selected) == num { node.set_selected(true); - if let Some(id) = sub_menu { - node.push_child(id); - } + // if let Some(id) = sub_menu { + // node.push_child(id); + // } } let id = NodeId(u64::try_from(num).unwrap() + self.node_id.0 + 1); tree.push((id, node)); @@ -204,65 +230,80 @@ impl Dialog for PageMenu { } } -impl PageMenu { - const BACKGROUND_COLOR: u8 = 2; - const TOPLEFT_COLOR: u8 = 3; - const BOTRIGHT_COLOR: u8 = 1; - const fn new( - name: &'static str, - pos: CharPosition, - width: u8, - buttons: &'static [(&'static str, Action)], - #[cfg(feature = "accesskit")] node_id: accesskit::NodeId, - ) -> Self { - let rect = CharRect::new( - pos.y(), - // TODO: const trait - pos.y() + 5 + (3 * buttons.len() as u8), - pos.x(), - pos.x() + width + 3, - ); +pub struct MainMenu { + main: PageMenu, + child: Option, +} - Self { - name, - rect, - selected: 0, - pressed: false, - buttons, - sub_menu: None, - #[cfg(feature = "accesskit")] - node_id, +impl Dialog for MainMenu { + fn draw(&self, draw_buffer: &mut DrawBuffer) { + self.main.draw(self.child.is_some(), draw_buffer); + if let Some(child) = &self.child { + // the child never has a child + child.draw(false, draw_buffer); } } - fn draw_button_corners(rect: CharRect, draw_buffer: &mut DrawBuffer) { - // let framebuffer = &mut draw_buffer.framebuffer; - let pixel_rect = PixelRect::from(rect); - // draw top right corner - for y in 0..FONT_SIZE16 { - for x in y..FONT_SIZE16 { - draw_buffer.set( - pixel_rect.top() + y, - pixel_rect.right() - FONT_SIZE16 + x + 1, - Self::BOTRIGHT_COLOR, - ); - // framebuffer[pixel_rect.top() + y][pixel_rect.right() - FONT_SIZE + x + 1] = - // Self::BOTRIGHT_COLOR; + fn process_input( + &mut self, + key_event: &winit::event::KeyEvent, + _modifiers: &winit::event::Modifiers, + events: &mut EventQueue<'_>, + ) -> DialogResponse { + // check for close dialog + if key_event.state.is_pressed() && key_event.logical_key == Key::Named(NamedKey::Escape) { + if self.child.is_some() { + self.child = None; + events.push(GlobalEvent::ConstRedraw); + return DialogResponse::RequestRedraw; + } else { + return DialogResponse::Close; } } - // draw botleft corner - for y in 0..FONT_SIZE16 { - for x in 0..(FONT_SIZE16 - y) { - draw_buffer.set( - pixel_rect.bot() - y, - pixel_rect.left() + x, - Self::BOTRIGHT_COLOR, + + if let Some(child) = &mut self.child { + let (resp, open_child) = child.process_input(key_event, events); + assert!(open_child.is_none(), "the child can't open a child"); + resp + } else { + let (resp, open_child) = self.main.process_input(key_event, events); + if let Some(open_child) = open_child { + assert!( + resp == DialogResponse::RequestRedraw, + "child open is always in combination with redraw" ); - // framebuffer[pixel_rect.bot() - y][pixel_rect.left() + x] = Self::BOTRIGHT_COLOR; + let child = match open_child { + Menu::File => PageMenu::file(), + Menu::Playback => PageMenu::playback(), + Menu::Sample => PageMenu::sample(), + Menu::Instrument => PageMenu::instrument(), + Menu::Settings => PageMenu::settings(), + }; + self.child = Some(child); } + resp } } + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + _tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::AccessResponse { + todo!("look at PageMenu commented out stuff"); + } +} + +impl MainMenu { + pub fn new() -> Self { + Self { + main: PageMenu::main(), + child: None, + } + } +} + +impl PageMenu { pub const fn main() -> Self { Self::new( "Main Menu", diff --git a/src/main.rs b/src/main.rs index 96c07de..f460836 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,7 +42,7 @@ use pages::{ }; use { - dialog::{Dialog, DialogManager, DialogResponse, confirm::ConfirmDialog, page_menu::PageMenu}, + dialog::{Dialog, DialogManager, DialogResponse, confirm::ConfirmDialog, page_menu::MainMenu}, draw_buffer::DrawBuffer, header::{Header, HeaderEvent}, }; @@ -386,7 +386,7 @@ impl ApplicationHandler for App { if event.state.is_pressed() && event.logical_key == Key::Named(NamedKey::Escape) { event_queue.push(GlobalEvent::OpenDialog(Box::new(|| { - Box::new(PageMenu::main()) + Box::new(MainMenu::new()) }))); } From 4f1c6daa1e31a5a721ffd392d2b6bdf799d94266 Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Wed, 8 Oct 2025 22:32:52 +0200 Subject: [PATCH 15/19] new audio API and accesskit bump --- Cargo.toml | 2 +- src/main.rs | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4f7746d..7e38a1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ softbuffer = { version="0.4.6", optional = true } rfd = "0.15.4" # audio file loading symphonia = "0.5.4" -accesskit_winit = { version = "0.29.0", optional = true } +accesskit_winit = { version = "0.29.1", optional = true } accesskit = { version = "0.21.0", optional = true } [features] diff --git a/src/main.rs b/src/main.rs index f460836..96ee14e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,11 +18,8 @@ use std::{ use smol::{channel::Sender, lock::Mutex}; use torque_tracker_engine::{ AudioManager, OutputConfig, PlaybackSettings, ToWorkerMsg, - audio_processing::playback::PlaybackStatus, project::song::{Song, SongOperation}, }; -#[cfg(feature = "accesskit")] -use winit::dpi::PhysicalSize; use winit::{ application::ApplicationHandler, event::{Modifiers, WindowEvent}, @@ -94,9 +91,7 @@ impl Clone for GlobalEvent { GlobalEvent::ConstRedraw => GlobalEvent::ConstRedraw, GlobalEvent::Playback(playback_type) => GlobalEvent::Playback(*playback_type), #[cfg(feature = "accesskit")] - GlobalEvent::Accesskit(window_event) => { - todo!("https://github.com/AccessKit/accesskit/issues/610") - } + GlobalEvent::Accesskit(w) => GlobalEvent::Accesskit(w.clone()), } } } @@ -595,6 +590,7 @@ impl App { buffer_size, channel_count: NonZero::new(config.channels).unwrap(), sample_rate: NonZero::new(config.sample_rate.0).unwrap(), + interpolation: torque_tracker_engine::audio_processing::Interpolation::Linear, }, cpal::OutputStreamTimestamp { callback: cpal::StreamInstant::new(0, 0), From 113975a958ff822d47f9c0c1ab1c2aa1aacc14f7 Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Thu, 9 Oct 2025 18:20:06 +0200 Subject: [PATCH 16/19] don't clone GlobalEvents --- src/dialog/page_menu.rs | 20 ++++++++++++-------- src/main.rs | 18 ------------------ 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/src/dialog/page_menu.rs b/src/dialog/page_menu.rs index 227a014..bbace5f 100644 --- a/src/dialog/page_menu.rs +++ b/src/dialog/page_menu.rs @@ -11,9 +11,9 @@ use super::{Dialog, DialogResponse}; enum Action { Menu(Menu), - // TODO: maybe fold this into the more general event variant Page(PagesEnum), - Event(GlobalEvent), + Playback(PlaybackType), + Event(fn() -> GlobalEvent), // TODO: should be removed when it's all implemented NotYetImplemented, } @@ -160,7 +160,11 @@ impl PageMenu { return (DialogResponse::RequestRedraw, None); } Action::Event(global_event) => { - events.push(global_event.clone()); + events.push(global_event()); + return (DialogResponse::Close, None); + } + Action::Playback(p) => { + events.push(GlobalEvent::Playback(*p)); return (DialogResponse::Close, None); } } @@ -349,7 +353,7 @@ impl PageMenu { ("Message Log (Ctrl-F11)", Action::NotYetImplemented), ( "Quit (Ctrl-Q)", - Action::Event(GlobalEvent::CloseRequested), + Action::Event(|| GlobalEvent::CloseRequested), ), ], #[cfg(feature = "accesskit")] @@ -366,20 +370,20 @@ impl PageMenu { ("Show Infopage (F5)", Action::NotYetImplemented), ( "Play Song (Ctrl-F5)", - Action::Event(GlobalEvent::Playback(PlaybackType::Song)), + Action::Playback(PlaybackType::Song), ), ( "Play Pattern (F6)", - Action::Event(GlobalEvent::Playback(PlaybackType::Pattern)), + Action::Playback(PlaybackType::Pattern), ), ( "Play from Order (Shift-F6)", - Action::Event(GlobalEvent::Playback(PlaybackType::FromOrder)), + Action::Playback(PlaybackType::FromOrder), ), ("Play from Mark/Cursor (F7)", Action::NotYetImplemented), ( "Stop (F8)", - Action::Event(GlobalEvent::Playback(PlaybackType::Stop)), + Action::Playback(PlaybackType::Stop), ), ("Reinit Soundcard (Ctrl-I)", Action::NotYetImplemented), ("Driver Screen (Shift-F5)", Action::NotYetImplemented), diff --git a/src/main.rs b/src/main.rs index 96ee14e..20f082c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -78,24 +78,6 @@ pub enum GlobalEvent { ConstRedraw, } -impl Clone for GlobalEvent { - fn clone(&self) -> Self { - // TODO: make this really clone, once the Dialogs are an enum instead of Box dyn - match self { - GlobalEvent::OpenDialog(_) => panic!("TODO: don't clone this"), - GlobalEvent::Page(page_event) => GlobalEvent::Page(page_event.clone()), - GlobalEvent::Header(header_event) => GlobalEvent::Header(header_event.clone()), - GlobalEvent::GoToPage(pages_enum) => GlobalEvent::GoToPage(*pages_enum), - GlobalEvent::CloseRequested => GlobalEvent::CloseRequested, - GlobalEvent::CloseApp => GlobalEvent::CloseApp, - GlobalEvent::ConstRedraw => GlobalEvent::ConstRedraw, - GlobalEvent::Playback(playback_type) => GlobalEvent::Playback(*playback_type), - #[cfg(feature = "accesskit")] - GlobalEvent::Accesskit(w) => GlobalEvent::Accesskit(w.clone()), - } - } -} - impl Debug for GlobalEvent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut debug = f.debug_struct("GlobalEvent"); From 5ef1c8df20ce9e2e3f9a2b62beb040fa26f0fa7a Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Thu, 9 Oct 2025 18:30:10 +0200 Subject: [PATCH 17/19] remove allocs from dialogs --- src/dialog/confirm.rs | 12 +++---- src/dialog/mod.rs | 76 ++++++++++++++++++++++++++++++++++++----- src/dialog/page_menu.rs | 1 + src/main.rs | 24 +++++++------ src/widgets/slider.rs | 4 ++- 5 files changed, 91 insertions(+), 26 deletions(-) diff --git a/src/dialog/confirm.rs b/src/dialog/confirm.rs index 33afd0f..2adfc39 100644 --- a/src/dialog/confirm.rs +++ b/src/dialog/confirm.rs @@ -14,8 +14,8 @@ pub struct ConfirmDialog { text_pos: CharPosition, // computed from the string length rect: CharRect, - ok: (Button, Option), - cancel: (Button, Option), + ok: (Button, Option GlobalEvent>), + cancel: (Button, Option GlobalEvent>), selected: u8, } @@ -28,8 +28,8 @@ impl ConfirmDialog { const CANCEL: u8 = 2; pub fn new( text: &'static str, - ok_event: Option, - cancel_event: Option, + ok_event: Option GlobalEvent>, + cancel_event: Option GlobalEvent>, ) -> Self { let width = (u8::try_from(text.len()).unwrap() + 10).max(22); let per_side = width / 2; @@ -100,7 +100,7 @@ impl Dialog for ConfirmDialog { let resp = self.ok.0.process_input(modifiers, key_event, events); if resp == WidgetResponse::RequestRedraw(true) { if let Some(event) = self.ok.1.take() { - events.push(event); + events.push(event()); } return DialogResponse::Close; } @@ -110,7 +110,7 @@ impl Dialog for ConfirmDialog { let resp = self.cancel.0.process_input(modifiers, key_event, events); if resp == WidgetResponse::RequestRedraw(true) { if let Some(event) = self.cancel.1.take() { - events.push(event); + events.push(event()); } return DialogResponse::Close; } diff --git a/src/dialog/mod.rs b/src/dialog/mod.rs index f3a0cc9..ccccebf 100644 --- a/src/dialog/mod.rs +++ b/src/dialog/mod.rs @@ -4,7 +4,11 @@ pub mod slider_dialog; use winit::event::{KeyEvent, Modifiers}; -use crate::{EventQueue, draw_buffer::DrawBuffer}; +use crate::{ + EventQueue, + dialog::{confirm::ConfirmDialog, page_menu::MainMenu, slider_dialog::SliderDialog}, + draw_buffer::DrawBuffer, +}; #[derive(PartialEq, Eq)] pub enum DialogResponse { @@ -33,7 +37,66 @@ pub trait Dialog { } pub struct DialogManager { - stack: Vec>, + stack: Vec, +} + +pub enum DialogEnum { + Main(MainMenu), + Slider(SliderDialog), + Confirm(ConfirmDialog), +} + +impl DialogEnum { + fn get_mut(&mut self) -> &mut dyn Dialog { + match self { + DialogEnum::Main(d) => d, + DialogEnum::Slider(d) => d, + DialogEnum::Confirm(d) => d, + } + } + + fn get(&self) -> &dyn Dialog { + match self { + DialogEnum::Main(d) => d, + DialogEnum::Slider(d) => d, + DialogEnum::Confirm(d) => d, + } + } +} + +impl Dialog for DialogEnum { + fn draw(&self, draw_buffer: &mut DrawBuffer) { + match self { + DialogEnum::Main(d) => d.draw(draw_buffer), + DialogEnum::Slider(d) => d.draw(draw_buffer), + DialogEnum::Confirm(d) => d.draw(draw_buffer), + } + } + + fn process_input( + &mut self, + key_event: &KeyEvent, + modifiers: &Modifiers, + events: &mut EventQueue<'_>, + ) -> DialogResponse { + match self { + DialogEnum::Main(d) => d.process_input(key_event, modifiers, events), + DialogEnum::Slider(d) => d.process_input(key_event, modifiers, events), + DialogEnum::Confirm(d) => d.process_input(key_event, modifiers, events), + } + } + + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::AccessResponse { + match self { + DialogEnum::Main(d) => d.build_tree(tree), + DialogEnum::Slider(d) => d.build_tree(tree), + DialogEnum::Confirm(d) => d.build_tree(tree), + } + } } impl DialogManager { @@ -45,21 +108,18 @@ impl DialogManager { } pub fn active_dialog_mut(&mut self) -> Option<&mut dyn Dialog> { - match self.stack.last_mut() { - Some(dialog) => Some(dialog.as_mut()), - None => None, - } + self.stack.last_mut().map(|d| d.get_mut()) } pub fn active_dialog(&self) -> Option<&dyn Dialog> { - self.stack.last().map(|d| d.as_ref()) + self.stack.last().map(|d| d.get()) } pub fn is_active(&self) -> bool { !self.stack.is_empty() } - pub fn open_dialog(&mut self, dialog: Box) { + pub fn open_dialog(&mut self, dialog: DialogEnum) { self.stack.push(dialog); } diff --git a/src/dialog/page_menu.rs b/src/dialog/page_menu.rs index bbace5f..befc778 100644 --- a/src/dialog/page_menu.rs +++ b/src/dialog/page_menu.rs @@ -13,6 +13,7 @@ enum Action { Menu(Menu), Page(PagesEnum), Playback(PlaybackType), + // const data. Allows Recursing this inside the GlobalEvent Event(fn() -> GlobalEvent), // TODO: should be removed when it's all implemented NotYetImplemented, diff --git a/src/main.rs b/src/main.rs index 20f082c..22a1d2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,8 +38,10 @@ use pages::{ pattern::PatternPageEvent, }; +use crate::dialog::DialogEnum; + use { - dialog::{Dialog, DialogManager, DialogResponse, confirm::ConfirmDialog, page_menu::MainMenu}, + dialog::{DialogManager, DialogResponse, confirm::ConfirmDialog, page_menu::MainMenu}, draw_buffer::DrawBuffer, header::{Header, HeaderEvent}, }; @@ -64,7 +66,7 @@ pub fn send_song_op(op: SongOperation) { } pub enum GlobalEvent { - OpenDialog(Box Box + Send>), + OpenDialog(DialogEnum), Page(PageEvent), Header(HeaderEvent), /// also closes all dialogs @@ -362,9 +364,9 @@ impl ApplicationHandler for App { } else { if event.state.is_pressed() && event.logical_key == Key::Named(NamedKey::Escape) { - event_queue.push(GlobalEvent::OpenDialog(Box::new(|| { - Box::new(MainMenu::new()) - }))); + event_queue.push(GlobalEvent::OpenDialog(dialog::DialogEnum::Main( + MainMenu::new(), + ))); } match ui_pages.process_key_event(&self.modifiers, &event, event_queue) { @@ -389,7 +391,7 @@ impl ApplicationHandler for App { let event_queue = &mut EventQueue(&mut self.event_queue); match event { GlobalEvent::OpenDialog(dialog) => { - self.dialog_manager.open_dialog(dialog()); + self.dialog_manager.open_dialog(dialog); _ = self.try_request_redraw(); } GlobalEvent::Page(c) => match self.ui_pages.process_page_event(c, event_queue) { @@ -492,13 +494,13 @@ impl App { // TODO: should this be its own function?? or is there something better fn close_requested(events: &mut EventQueue<'_>) { - events.push(GlobalEvent::OpenDialog(Box::new(|| { - Box::new(ConfirmDialog::new( + events.push(GlobalEvent::OpenDialog(DialogEnum::Confirm( + ConfirmDialog::new( "Close Torque Tracker?", - Some(GlobalEvent::CloseApp), + Some(|| GlobalEvent::CloseApp), None, - )) - }))); + ), + ))); } /// tries to request a redraw. if there currently is no window this fails diff --git a/src/widgets/slider.rs b/src/widgets/slider.rs index 2272866..265e4ff 100644 --- a/src/widgets/slider.rs +++ b/src/widgets/slider.rs @@ -237,7 +237,9 @@ impl Widget for Slider { if let Some(first_char) = chars.next() { if first_char.is_ascii_digit() { let dialog = SliderDialog::new(first_char, MIN..=MAX, self.dialog_return); - event.push(GlobalEvent::OpenDialog(Box::new(|| Box::new(dialog)))); + event.push(GlobalEvent::OpenDialog(crate::dialog::DialogEnum::Slider( + dialog, + ))); return WidgetResponse::None; } } From 927aa8908ef06ef3b39182d9af3fe2614c4a4f90 Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Fri, 10 Oct 2025 12:18:49 +0200 Subject: [PATCH 18/19] accesskit events --- src/main.rs | 56 ++++++++++++++++++++++++++++++++++++++++++------ src/pages/mod.rs | 5 +++++ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index 22a1d2c..1468a1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,8 @@ use std::{ time::Duration, }; +#[cfg(feature = "accesskit")] +use accesskit::NodeId; use smol::{channel::Sender, lock::Mutex}; use torque_tracker_engine::{ AudioManager, OutputConfig, PlaybackSettings, ToWorkerMsg, @@ -457,8 +459,28 @@ impl ApplicationHandler for App { ) }); } - WindowEvent::ActionRequested(_) => todo!(), - // i don't have any extra state for accessability so i don't need to cleanup anything + WindowEvent::ActionRequested(event) => { + match event.target.0 / Self::ID_LEVEL_1 { + Self::HEADER_ID => todo!("decide if the header should be interactive"), + Self::PAGE_ID => { + let event = { + let mut event = event; + // information about lvl 1 was processed, so remove it + event.target.0 -= Self::PAGE_ID * Self::ID_LEVEL_1; + event + }; + match self.ui_pages.process_accesskit_event(event) { + PageResponse::RequestRedraw => { + self.try_request_redraw().unwrap() + } + PageResponse::None => (), + } + } + Self::DIALOG_ID => (), + // should i ignore invalid IDs? probably not as that would be a bug either in accesskit or my code + _ => unreachable!(), + } + } // i don't have any extra state for accessability so i don't need to cleanup anything WindowEvent::AccessibilityDeactivated => (), } } @@ -656,8 +678,27 @@ impl App { // lastly kill the audio stream drop(stream); } +} + +#[cfg(feature = "accesskit")] +impl App { + // Node ID Layout: + // xxx_xxx_xxx_xxx_xxx + // In Widget lvl 4 + // In Page/Dialog/Header lvl 3 + // Which Page/Header/Dialog lvl 2 + // Page, Dialog or Header lvl 1 + // Root + const ROOT_ID: NodeId = NodeId(1_000_000_000_000); + pub const ID_LEVEL_1: u64 = 1_000_000_000; + pub const ID_LEVEL_2: u64 = 1_000_000; + pub const ID_LEVEL_3: u64 = 1_000; + pub const ID_LEVEL_4: u64 = 1; // do i need this? + + pub const HEADER_ID: u64 = 1; + pub const PAGE_ID: u64 = 2; + pub const DIALOG_ID: u64 = 3; - #[cfg(feature = "accesskit")] fn produce_full_tree( pages: &AllPages, header: &Header, @@ -665,11 +706,10 @@ impl App { transform: accesskit::Affine, ) -> accesskit::TreeUpdate { use accesskit::TreeUpdate; - use accesskit::{Node, NodeId, Role, Tree}; - const ROOT_ID: NodeId = NodeId(0); + use accesskit::{Node, Role, Tree}; let tree = Tree { - root: ROOT_ID, + root: Self::ROOT_ID, toolkit_name: Some(String::from("Torque Tracker Custom")), toolkit_version: None, }; @@ -691,13 +731,15 @@ impl App { focused = resp.selected; } - nodes.push((ROOT_ID, root_node)); + nodes.push((Self::ROOT_ID, root_node)); TreeUpdate { nodes, tree: Some(tree), focus: focused, } } + + fn process_accesskit_event(&mut self, event: accesskit::ActionRequest) {} } impl Drop for App { diff --git a/src/pages/mod.rs b/src/pages/mod.rs index bc9e1a8..aeb3c1e 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -245,4 +245,9 @@ impl AllPages { ) -> crate::AccessResponse { self.get_page().build_tree(tree) } + + #[cfg(feature = "accesskit")] + pub fn process_accesskit_event(&mut self, event: accesskit::ActionRequest) -> PageResponse { + todo!() + } } From 6a63059c63e908de74040566bc591a8e6d20b94b Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Fri, 10 Oct 2025 13:10:33 +0200 Subject: [PATCH 19/19] dev commit --- Cargo.lock | 22 +++++++++------------- Cargo.toml | 4 +++- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1f771be..8c1a448 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2421,6 +2421,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rt-write-lock" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79ce7f6e4b55e98feea9066f403ac0866c1d16d602d7522367151771d2800a15" + [[package]] name = "rtrb" version = "0.3.2" @@ -3038,15 +3044,14 @@ dependencies = [ [[package]] name = "torque-tracker-engine" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809ceef6db8d2bd9bf2a14b327efe8fd04d16827e3675a035f70d00503c2ae4f" +version = "0.2.0" +source = "git+https://github.com/luca3s/torque-tracker-engine.git?branch=dev#6769b8159285a0b3ba6139f22e484d66bd7a0b97" dependencies = [ "dasp", + "rt-write-lock", "rtrb", "rtsan-standalone", "simple-left-right", - "triple_buffer", ] [[package]] @@ -3080,15 +3085,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "triple_buffer" -version = "8.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420466259f9fa5decc654c490b9ab538400e5420df8237f84ecbe20368bcf72b" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "ttf-parser" version = "0.25.1" diff --git a/Cargo.toml b/Cargo.toml index 7e38a1a..4eb4af3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,9 @@ wgpu = { version = "27.0.0", optional = true } # ascii strings ascii = "1.1.0" winit = "0.30.11" -torque-tracker-engine = "0.1.0" +# torque-tracker-engine = "0.1.0" +# torque-tracker-engine = { git = "https://tangled.org/@inkreas.ing/torque-tracker-engine.git", branch = "dev" } +torque-tracker-engine = { git = "https://github.com/luca3s/torque-tracker-engine.git", branch = "dev" } smol = "2.0.2" # macro shit paste = "1.0.15"