diff --git a/Cargo.toml b/Cargo.toml index 14751e940..9d8ccabb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,12 +93,14 @@ wgpu.workspace = true fluent-bundle = { version = "0.16", optional = true } unic-langid = { version = "0.9", optional = true } sys-locale = {version = "0.3.2", optional = true } +accesskit = {version = "0.18.0"} [target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies] muda = { workspace = true } [target.'cfg(any(target_os = "linux"))'.dependencies] muda = { workspace = true, default-features = false, features = ["gtk"] } +accesskit_unix = "0.17.1" [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen-futures = { version = "0.4" } @@ -107,6 +109,7 @@ wgpu = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] clipboard-win = "5.4" +accesskit_windows = "0.29.1" [target.'cfg(target_os = "macos")'.dependencies] objc2 = { version = "0.6", default-features = false } @@ -116,6 +119,7 @@ objc2-app-kit = { version = "0.3", features = [ "NSResponder", "NSView" ] } +accesskit_macos = "0.19.0" [features] default = ["editor", "default-image-formats", "vger"] diff --git a/src/app.rs b/src/app.rs index 5b6dd4ecf..208c35363 100644 --- a/src/app.rs +++ b/src/app.rs @@ -136,6 +136,9 @@ pub(crate) enum AppUpdateEvent { ThemeChanged { theme: Theme, }, + AccessibilityAction { + request: accesskit::ActionRequest, + }, } pub(crate) fn add_app_update_event(event: AppUpdateEvent) { diff --git a/src/app_handle.rs b/src/app_handle.rs index 0bb313261..696998564 100644 --- a/src/app_handle.rs +++ b/src/app_handle.rs @@ -161,6 +161,14 @@ impl ApplicationHandle { window_handle.set_theme(Some(theme), false); } } + AppUpdateEvent::AccessibilityAction { request } => { + // Find which window contains the target node and handle the action + for (_, handle) in self.window_handles.iter_mut() { + if handle.handle_accessibility_action(&request) { + break; + } + } + } } } } @@ -213,6 +221,9 @@ impl ApplicationHandle { ) }); + // Process accessibility events first + window_handle.process_accessibility_event(&event); + match window_handle .event_reducer .reduce(window_handle.scale, &event) @@ -540,7 +551,7 @@ impl ApplicationHandle { } } let window_id = window.id(); - let window_handle = WindowHandle::new( + let mut window_handle = WindowHandle::new( window, self.gpu_resources.clone(), self.config.wgpu_features, @@ -549,6 +560,7 @@ impl ApplicationHandle { apply_default_theme, font_embolden, ); + window_handle.init_accessibility_with_event_loop(event_loop); self.window_handles.insert(window_id, window_handle); } diff --git a/src/context.rs b/src/context.rs index 9617037c2..0006b4ed4 100644 --- a/src/context.rs +++ b/src/context.rs @@ -642,17 +642,30 @@ impl<'a> StyleCx<'a> { let view_state = view_id.state(); { let mut view_state = view_state.borrow_mut(); - if !view_state.requested_changes.contains(ChangeFlags::STYLE) { + if !view_state.requested_changes.contains(ChangeFlags::STYLE) + && !view_state + .requested_changes + .contains(ChangeFlags::VIEW_STYLE) + { self.restore(); return; } view_state.requested_changes.remove(ChangeFlags::STYLE); } - let view_style = view.borrow().view_style(); let view_class = view.borrow().view_class(); { let mut view_state = view_state.borrow_mut(); + if view_state + .requested_changes + .contains(ChangeFlags::VIEW_STYLE) + { + view_state.requested_changes.remove(ChangeFlags::VIEW_STYLE); + if let Some(view_style) = view.borrow().view_style() { + let offest = view_state.view_style_offset; + view_state.style.set(offest, view_style); + } + } // Propagate style requests to children if needed. if view_state.request_style_recursive { @@ -670,7 +683,6 @@ impl<'a> StyleCx<'a> { let view_interact_state = self.get_interact_state(&view_id); self.disabled = view_interact_state.is_disabled; let (mut new_frame, classes_applied) = view_id.state().borrow_mut().compute_combined( - view_style, view_interact_state, self.window_state.screen_size_bp, view_class, @@ -702,13 +714,20 @@ impl<'a> StyleCx<'a> { self.window_state.focusable.remove(&view_id); } view_state.borrow_mut().computed_style = computed_style; - self.hidden |= view_id.is_hidden(); + let view_hidden = view_id.is_hidden(); + self.hidden |= view_hidden; // This is used by the `request_transition` and `style` methods below. self.current_view = view_id; { let mut view_state = view_state.borrow_mut(); + if view_hidden { + view_state.accessibility_node.set_hidden(); + } else { + view_state.accessibility_node.clear_hidden(); + } + // Extract the relevant layout properties so the content rect can be calculated // when painting. view_state.layout_props.read_explicit( @@ -731,6 +750,30 @@ impl<'a> StyleCx<'a> { if new_frame && !self.hidden { self.window_state.schedule_style(view_id); } + + if view_state.accessibility_props.read_explicit( + &self.direct, + &self.current, + &self.now, + &mut false, + ) { + view_id.request_accessibility_update(); + if let Some(role) = view_state.accessibility_props.role() { + view_state.accessibility_node.set_role(role); + } + if let Some(label) = view_state.accessibility_props.label() { + view_state.accessibility_node.set_label(label); + } + if let Some(value) = view_state.accessibility_props.value() { + view_state.accessibility_node.set_value(value); + } + if let Some(actions) = view_state.accessibility_props.actions() { + view_state.accessibility_node.clear_actions(); + for action in actions { + view_state.accessibility_node.add_action(action); + } + } + } } // If there's any changes to the Taffy style, request layout. let layout_style = view_state.borrow().layout_props.to_style(); @@ -849,6 +892,7 @@ impl<'a> StyleCx<'a> { } pub struct ComputeLayoutCx<'a> { + pub scale: f64, pub window_state: &'a mut WindowState, pub(crate) viewport: Rect, pub(crate) window_origin: Point, @@ -857,8 +901,9 @@ pub struct ComputeLayoutCx<'a> { } impl<'a> ComputeLayoutCx<'a> { - pub(crate) fn new(window_state: &'a mut WindowState, viewport: Rect) -> Self { + pub(crate) fn new(window_state: &'a mut WindowState, viewport: Rect, scale: f64) -> Self { Self { + scale, window_state, viewport, window_origin: Point::ZERO, @@ -977,7 +1022,19 @@ impl<'a> ComputeLayoutCx<'a> { let transform = view_state.borrow().transform; let layout_rect = transform.transform_rect_bbox(layout_rect); - view_state.borrow_mut().layout_rect = layout_rect; + { + let mut view_state_ref = view_state.borrow_mut(); + let scale = self.window_state.scale * self.scale; + view_state_ref.layout_rect = layout_rect; + view_state_ref + .accessibility_node + .set_bounds(accesskit::Rect { + x0: layout_rect.x0 * scale, + y0: layout_rect.y0 * scale, + x1: layout_rect.x1 * scale, + y1: layout_rect.y1 * scale, + }); + } self.restore(); diff --git a/src/id.rs b/src/id.rs index 01e23ff28..3640c2d0e 100644 --- a/src/id.rs +++ b/src/id.rs @@ -7,7 +7,7 @@ use std::{any::Any, cell::RefCell, rc::Rc}; use peniko::kurbo::{Insets, Point, Rect, Size}; -use slotmap::new_key_type; +use slotmap::{Key, new_key_type}; use taffy::{Display, Layout, NodeId, TaffyTree}; use winit::window::WindowId; @@ -82,6 +82,31 @@ impl ViewId { self.state().borrow().node } + /// Get the accessibility node for this ViewId by converting it directly + pub(crate) fn accessibility_node(&self) -> accesskit::NodeId { + // Use the ViewId's internal representation as the NodeId + // SlotMap keys have a data() method that returns the internal KeyData + // TODO: Find a better implementation + accesskit::NodeId(self.data().as_ffi()) + } + + /// Set accessibility children (called when children are modified) + pub(crate) fn set_accessibility_children(&self, children: Vec) { + self.state() + .borrow_mut() + .accessibility_node + .set_children(children); + } + + /// Request an accessibility tree update for this view's window + pub(crate) fn request_accessibility_update(&self) { + // Find the root window and request accessibility update + if let Some(root_id) = self.root() { + // The accessibility update will be triggered during the next update cycle + root_id.request_changes(crate::view_state::ChangeFlags::ACCESSIBILITY); + } + } + pub(crate) fn state(&self) -> Rc> { VIEW_STORAGE.with_borrow_mut(|s| { if !s.view_ids.contains_key(*self) { @@ -112,18 +137,26 @@ impl ViewId { /// Add a child View to this Id's list of children pub fn add_child(&self, child: Box) { + let child_id = child.id(); VIEW_STORAGE.with_borrow_mut(|s| { - let child_id = child.id(); s.children.entry(*self).unwrap().or_default().push(child_id); s.parent.insert(child_id, Some(*self)); s.views.insert(child_id, Rc::new(RefCell::new(child))); }); + + // Update accessibility children + let children = self.children(); + let accessibility_children: Vec = children + .into_iter() + .map(|child_id| child_id.accessibility_node()) + .collect(); + self.set_accessibility_children(accessibility_children); } /// Set the children views of this Id /// See also [`Self::set_children_vec`] pub fn set_children(&self, children: [V; N]) { - VIEW_STORAGE.with_borrow_mut(|s| { + let children_ids = VIEW_STORAGE.with_borrow_mut(|s| { let mut children_ids = Vec::new(); for child in children { let child_view = child.into_view(); @@ -133,14 +166,22 @@ impl ViewId { s.views .insert(child_view_id, Rc::new(RefCell::new(child_view.into_any()))); } - s.children.insert(*self, children_ids); + s.children.insert(*self, children_ids.clone()); + children_ids }); + + // Update accessibility children + let accessibility_children: Vec = children_ids + .into_iter() + .map(|child_id| child_id.accessibility_node()) + .collect(); + self.set_accessibility_children(accessibility_children); } /// Set the children views of this Id using a Vector /// See also [`Self::set_children`] pub fn set_children_vec(&self, children: Vec) { - VIEW_STORAGE.with_borrow_mut(|s| { + let children_ids = VIEW_STORAGE.with_borrow_mut(|s| { let mut children_ids = Vec::new(); for child in children { let child_view = child.into_view(); @@ -150,8 +191,16 @@ impl ViewId { s.views .insert(child_view_id, Rc::new(RefCell::new(child_view.into_any()))); } - s.children.insert(*self, children_ids); + s.children.insert(*self, children_ids.clone()); + children_ids }); + + // Update accessibility children + let accessibility_children: Vec = children_ids + .into_iter() + .map(|child_id| child_id.accessibility_node()) + .collect(); + self.set_accessibility_children(accessibility_children); } /// Set the view that should be associated with this Id @@ -174,11 +223,23 @@ impl ViewId { /// Set the Ids that should be used as the children of this Id pub fn set_children_ids(&self, children: Vec) { - VIEW_STORAGE.with_borrow_mut(|s| { + let should_update = VIEW_STORAGE.with_borrow_mut(|s| { if s.view_ids.contains_key(*self) { - s.children.insert(*self, children); + s.children.insert(*self, children.clone()); + true + } else { + false } }); + + if should_update { + // Update accessibility children + let accessibility_children: Vec = children + .into_iter() + .map(|child_id| child_id.accessibility_node()) + .collect(); + self.set_accessibility_children(accessibility_children); + } } /// Get the list of `ViewId`s that are associated with the children views of this `ViewId` @@ -381,6 +442,11 @@ impl ViewId { self.request_changes(ChangeFlags::STYLE) } + /// use this when you want the `view_style` method from the `View` trait to be rerun + pub fn request_view_style(&self) { + self.request_changes(ChangeFlags::VIEW_STYLE) + } + pub(crate) fn request_changes(&self, flags: ChangeFlags) { let state = self.state(); if state.borrow().requested_changes.contains(flags) { diff --git a/src/lib.rs b/src/lib.rs index 9ec4e249a..5d75ab6bc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -229,6 +229,7 @@ pub mod receiver_signal { pub use stream_signal::*; } +pub use accesskit; pub use app::{AppConfig, AppEvent, Application, launch, quit_app, reopen}; pub use clipboard::{Clipboard, ClipboardError}; pub use floem_reactive as reactive; diff --git a/src/style.rs b/src/style.rs index 0ff9ffaff..bfa437746 100644 --- a/src/style.rs +++ b/src/style.rs @@ -128,6 +128,7 @@ //! //! You can create custom extractors and embed them in your custom views so that you can get out any built in prop, or any of your custom props from the final combined style that is applied to your `View`. +use accesskit::{Action, Role}; use floem_reactive::{RwSignal, SignalGet, SignalUpdate as _, create_updater}; use floem_renderer::Renderer; use floem_renderer::text::{LineHeightValue, Weight}; @@ -3463,6 +3464,17 @@ impl StylePropValue for Margin { } } } +impl StylePropValue for Role { + fn debug_view(&self) -> Option> { + Some(text(format!("{self:?}")).into_any()) + } +} + +impl StylePropValue for Action { + fn debug_view(&self) -> Option> { + Some(text(format!("{self:?}")).into_any()) + } +} /// The value for a [`Style`] property #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -3628,6 +3640,10 @@ define_builtin_props!( Hidden set_hidden: bool { inherited } = false, Focusable focusable: bool { } = false, Draggable draggable: bool { } = false, + AccessibilityRole role: Option { } = None, + AccessibilityLabel label: Option {} = None, + AccessibilityValue value: Option {} = None, + AccessibilityActions actions: Option> {} = None, ); impl BuiltinStyle<'_> { @@ -3752,6 +3768,15 @@ impl LayoutProps { } } +prop_extractor!( + pub AccessibilityProps { + pub role: AccessibilityRole, + pub label: AccessibilityLabel, + pub value: AccessibilityValue, + pub actions: AccessibilityActions, + } +); + prop_extractor! { pub SelectionStyle { pub corner_radius: SelectionCornerRadius, diff --git a/src/view.rs b/src/view.rs index 14a04350f..e8adfa67a 100644 --- a/src/view.rs +++ b/src/view.rs @@ -257,6 +257,7 @@ pub fn recursively_layout_view(id: ViewId, cx: &mut LayoutCx) -> NodeId { pub trait View { fn id(&self) -> ViewId; + /// View style will be rerun only if you use `id.request_view_style()`. fn view_style(&self) -> Option