From 7a8b59d024a7e1cc53f0c57faea76676b7b49d94 Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Fri, 5 Sep 2025 20:26:26 +0200 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 676f25af38011fe2e94fb6585cfd957ddc3995f3 Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Mon, 8 Sep 2025 20:04:34 +0200 Subject: [PATCH 6/7] dev commit: -add package bundler settings -use local tracker-engine --- Cargo.lock | 2 -- Cargo.toml | 8 +++++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 95a42fc..14705e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3001,8 +3001,6 @@ dependencies = [ [[package]] name = "torque-tracker-engine" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809ceef6db8d2bd9bf2a14b327efe8fd04d16827e3675a035f70d00503c2ae4f" dependencies = [ "dasp", "rtrb", diff --git a/Cargo.toml b/Cargo.toml index 8f60767..c58769e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ font8x8 = "0.3.1" wgpu = { version = "26.0.0", optional = true } ascii = "1.1.0" winit = "0.30.11" -torque-tracker-engine = "0.1.0" +torque-tracker-engine = { path = "../torque-tracker-engine" } smol = "2.0.2" paste = "1.0.15" cpal = "0.16.0" @@ -39,3 +39,9 @@ default = ["gpu_scaling", "accesskit"] [lints.clippy] uninlined_format_args = "allow" + +[package.metadata.bundle] +name = "Torque Tracker" +identifier = "ing.inkreas.torque-tracker" +copyright = "GPL-3.0" +category = "Music" From 327629fb87ca68c5f0b9db06c0220ddefe370078 Mon Sep 17 00:00:00 2001 From: Lucas Baumann Date: Tue, 9 Sep 2025 20:59:01 +0200 Subject: [PATCH 7/7] 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);