Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Copy link
Copy Markdown
Contributor

@charlescgs charlescgs Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we make it optional behind an "accesskit" feature flag the similar way localization was done?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes that would be good. I think I will add it to the default features though


[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" }
Expand All @@ -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 }
Expand All @@ -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"]
Expand Down
3 changes: 3 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ pub(crate) enum AppUpdateEvent {
ThemeChanged {
theme: Theme,
},
AccessibilityAction {
request: accesskit::ActionRequest,
},
}

pub(crate) fn add_app_update_event(event: AppUpdateEvent) {
Expand Down
14 changes: 13 additions & 1 deletion src/app_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
}
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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);
}

Expand Down
69 changes: 63 additions & 6 deletions src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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();
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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();

Expand Down
82 changes: 74 additions & 8 deletions src/id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<accesskit::NodeId>) {
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<RefCell<ViewState>> {
VIEW_STORAGE.with_borrow_mut(|s| {
if !s.view_ids.contains_key(*self) {
Expand Down Expand Up @@ -112,18 +137,26 @@ impl ViewId {

/// Add a child View to this Id's list of children
pub fn add_child(&self, child: Box<dyn View>) {
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<accesskit::NodeId> = 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<const N: usize, V: IntoView>(&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();
Expand All @@ -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<accesskit::NodeId> = 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<impl IntoView>) {
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();
Expand All @@ -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<accesskit::NodeId> = 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
Expand All @@ -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<ViewId>) {
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<accesskit::NodeId> = 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`
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading