app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit a6f3c8dee48ab9adf52c067975593228ea80a21b
parent edea0bec57287f95dc080863ea0076eef9ad690a
Author: triesap <triesap@radroots.dev>
Date:   Wed, 21 Jan 2026 19:56:26 +0000

ui: add input modality tracking

- add input modality state and accessors

- add wasm listener to update html data-input

- add unit tests for modality defaults and set

- add wasm web-sys deps for listeners

Diffstat:
Mcrates/ui-core/Cargo.toml | 12++++++++++++
Acrates/ui-core/src/input.rs | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/ui-core/src/lib.rs | 8++++++++
3 files changed, 138 insertions(+), 0 deletions(-)

diff --git a/crates/ui-core/Cargo.toml b/crates/ui-core/Cargo.toml @@ -10,3 +10,15 @@ rust-version.workspace = true crate-type = ["rlib"] [dependencies] + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = { workspace = true } +web-sys = { workspace = true, features = [ + "Document", + "Element", + "EventTarget", + "HtmlElement", + "KeyboardEvent", + "PointerEvent", + "Window" +] } diff --git a/crates/ui-core/src/input.rs b/crates/ui-core/src/input.rs @@ -0,0 +1,118 @@ +use core::fmt; +use core::sync::atomic::{AtomicU8, Ordering}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RadrootsAppUiInputModality { + Keyboard, + Pointer, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RadrootsAppUiInputModalityError { + WindowMissing, + DocumentMissing, + RootMissing, + ListenerFailed, +} + +impl fmt::Display for RadrootsAppUiInputModalityError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RadrootsAppUiInputModalityError::WindowMissing => { + write!(f, "input_modality_window_missing") + } + RadrootsAppUiInputModalityError::DocumentMissing => { + write!(f, "input_modality_document_missing") + } + RadrootsAppUiInputModalityError::RootMissing => { + write!(f, "input_modality_root_missing") + } + RadrootsAppUiInputModalityError::ListenerFailed => { + write!(f, "input_modality_listener_failed") + } + } + } +} + +static RADROOTS_APP_UI_INPUT_MODE: AtomicU8 = AtomicU8::new(0); + +pub fn radroots_app_ui_input_modality_get() -> Option<RadrootsAppUiInputModality> { + match RADROOTS_APP_UI_INPUT_MODE.load(Ordering::Relaxed) { + 1 => Some(RadrootsAppUiInputModality::Keyboard), + 2 => Some(RadrootsAppUiInputModality::Pointer), + _ => None, + } +} + +pub fn radroots_app_ui_input_modality_set(modality: RadrootsAppUiInputModality) { + let value = match modality { + RadrootsAppUiInputModality::Keyboard => 1, + RadrootsAppUiInputModality::Pointer => 2, + }; + RADROOTS_APP_UI_INPUT_MODE.store(value, Ordering::Relaxed); +} + +#[cfg(target_arch = "wasm32")] +pub fn radroots_app_ui_input_modality_attach() -> Result<(), RadrootsAppUiInputModalityError> { + use wasm_bindgen::closure::Closure; + use wasm_bindgen::JsCast; + + let window = web_sys::window().ok_or(RadrootsAppUiInputModalityError::WindowMissing)?; + let document = window + .document() + .ok_or(RadrootsAppUiInputModalityError::DocumentMissing)?; + let root = document + .document_element() + .ok_or(RadrootsAppUiInputModalityError::RootMissing)?; + + let root_keyboard = root.clone(); + let keydown = Closure::wrap(Box::new(move |_event: web_sys::KeyboardEvent| { + radroots_app_ui_input_modality_set(RadrootsAppUiInputModality::Keyboard); + let _ = root_keyboard.set_attribute("data-input", "keyboard"); + }) as Box<dyn FnMut(_)>); + document + .add_event_listener_with_callback("keydown", keydown.as_ref().unchecked_ref()) + .map_err(|_| RadrootsAppUiInputModalityError::ListenerFailed)?; + keydown.forget(); + + let root_pointer = root.clone(); + let pointerdown = Closure::wrap(Box::new(move |_event: web_sys::PointerEvent| { + radroots_app_ui_input_modality_set(RadrootsAppUiInputModality::Pointer); + let _ = root_pointer.set_attribute("data-input", "pointer"); + }) as Box<dyn FnMut(_)>); + document + .add_event_listener_with_callback("pointerdown", pointerdown.as_ref().unchecked_ref()) + .map_err(|_| RadrootsAppUiInputModalityError::ListenerFailed)?; + pointerdown.forget(); + + Ok(()) +} + +#[cfg(not(target_arch = "wasm32"))] +pub fn radroots_app_ui_input_modality_attach() -> Result<(), RadrootsAppUiInputModalityError> { + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::{ + radroots_app_ui_input_modality_get, + radroots_app_ui_input_modality_set, + RadrootsAppUiInputModality, + }; + + #[test] + fn input_modality_defaults_to_none() { + let current = radroots_app_ui_input_modality_get(); + assert!(current.is_none()); + } + + #[test] + fn input_modality_set_roundtrips() { + radroots_app_ui_input_modality_set(RadrootsAppUiInputModality::Keyboard); + assert_eq!( + radroots_app_ui_input_modality_get(), + Some(RadrootsAppUiInputModality::Keyboard) + ); + } +} diff --git a/crates/ui-core/src/lib.rs b/crates/ui-core/src/lib.rs @@ -5,9 +5,17 @@ extern crate alloc; mod id; mod event; +mod input; pub use event::{ radroots_app_ui_compose_event_handlers, radroots_app_ui_compose_event_handlers_unchecked, }; pub use id::{RadrootsAppUiId, RadrootsAppUiIdSequence}; +pub use input::{ + radroots_app_ui_input_modality_attach, + radroots_app_ui_input_modality_get, + radroots_app_ui_input_modality_set, + RadrootsAppUiInputModality, + RadrootsAppUiInputModalityError, +};