mirror of
https://github.com/slint-ui/slint.git
synced 2025-11-03 13:23:00 +00:00
Initial AccessKit support
This change adds initial accessibility support for the winit backend through use of AccessKit.
This commit is contained in:
parent
ef82edcadb
commit
2e7cc49567
11 changed files with 542 additions and 5 deletions
|
|
@ -111,6 +111,7 @@ define_cargo_feature(backend-qt "Enable Qt based rendering backend" ON)
|
|||
|
||||
define_cargo_feature(experimental "Enable experimental features (no compatibility guarantees)" OFF)
|
||||
define_cargo_feature(gettext "Enable support of translations using gettext" OFF)
|
||||
define_cargo_feature(accessibility "Enable integration with operating system provided accessibility APIs" ON)
|
||||
|
||||
# Compat options
|
||||
option(SLINT_FEATURE_BACKEND_GL_ALL "This feature is an alias for SLINT_FEATURE_BACKEND_WINIT and SLINT_FEATURE_RENDERER_WINIT_FEMTOVG." OFF)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ renderer-winit-skia-opengl = ["i-slint-backend-selector/renderer-winit-skia-open
|
|||
renderer-winit-skia-vulkan = ["i-slint-backend-selector/renderer-winit-skia-vulkan"]
|
||||
renderer-winit-software = ["i-slint-backend-selector/renderer-winit-software"]
|
||||
gettext = ["i-slint-core/gettext-rs"]
|
||||
accessibility = ["i-slint-backend-selector/accessibility"]
|
||||
|
||||
experimental = ["i-slint-renderer-skia", "raw-window-handle"]
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ default = [
|
|||
"backend-winit",
|
||||
"renderer-winit-femtovg",
|
||||
"backend-qt",
|
||||
"accessibility",
|
||||
"compat-1-0",
|
||||
]
|
||||
|
||||
|
|
@ -65,6 +66,12 @@ software-renderer-systemfonts = ["i-slint-core/software-renderer-systemfonts"]
|
|||
## **Safety** : You must ensure that there is only one single thread that call into the Slint API
|
||||
unsafe-single-threaded = ["i-slint-core/unsafe-single-threaded"]
|
||||
|
||||
## Enable integration with operating system provided accessibility APIs
|
||||
##
|
||||
## Enabling this feature will try to expose the tree of UI elements to OS provided accessibility
|
||||
## APIs to support screen readers and other assistive technologies.
|
||||
accessibility = ["i-slint-backend-selector/accessibility"]
|
||||
|
||||
#! ### Backends
|
||||
|
||||
#! Slint needs a backend that will act as liaison between Slint and the OS.
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ renderer-winit-skia-vulkan = ["i-slint-backend-winit/renderer-winit-skia-vulkan"
|
|||
renderer-winit-software = ["i-slint-backend-winit/renderer-winit-software"]
|
||||
|
||||
rtti = ["i-slint-backend-winit?/rtti", "i-slint-backend-qt?/rtti"]
|
||||
accessibility = ["i-slint-backend-winit?/accessibility"]
|
||||
|
||||
[dependencies]
|
||||
i-slint-core = { version = "=1.0.3", path = "../../../internal/core", default-features = false }
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ renderer-winit-skia = ["i-slint-renderer-skia"]
|
|||
renderer-winit-skia-opengl = ["renderer-winit-skia", "i-slint-renderer-skia/opengl"]
|
||||
renderer-winit-skia-vulkan = ["renderer-winit-skia", "i-slint-renderer-skia/vulkan"]
|
||||
renderer-winit-software = ["softbuffer", "imgref", "rgb", "i-slint-core/software-renderer-systemfonts"]
|
||||
accessibility = ["accesskit", "accesskit_winit"]
|
||||
rtti = ["i-slint-core/rtti"]
|
||||
default = []
|
||||
|
||||
|
|
@ -49,6 +50,7 @@ winit = { version = "=0.28.6", default-features = false }
|
|||
instant = "0.1"
|
||||
raw-window-handle = { version = "0.5", features = ["alloc"] }
|
||||
scopeguard = { version = "1.1.0", default-features = false }
|
||||
send_wrapper = { workspace = true }
|
||||
|
||||
# For the FemtoVG renderer
|
||||
i-slint-renderer-femtovg = { version = "=1.0.3", path = "../../renderers/femtovg", optional = true }
|
||||
|
|
@ -64,11 +66,12 @@ rgb = { version = "0.8.27", optional = true }
|
|||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
web-sys = { version = "0.3", features=["HtmlInputElement", "HtmlCanvasElement", "Window", "Document", "Event", "KeyboardEvent", "InputEvent", "CompositionEvent", "DomStringMap", "ClipboardEvent", "DataTransfer"] }
|
||||
wasm-bindgen = { version = "0.2" }
|
||||
send_wrapper = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
glutin = { version = "0.30", optional = true, default-features = false, features = ["egl", "wgl"] }
|
||||
glutin-winit = { version = "0.3.0", optional = true, default-features = false, features = ["egl", "wgl"] }
|
||||
accesskit = { version = "0.11.0", optional = true }
|
||||
accesskit_winit = { version = "0.14.0", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
# For GL rendering
|
||||
|
|
|
|||
473
internal/backends/winit/accesskit.rs
Normal file
473
internal/backends/winit/accesskit.rs
Normal file
|
|
@ -0,0 +1,473 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
|
||||
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::pin::Pin;
|
||||
use std::rc::Weak;
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
|
||||
use accesskit::{
|
||||
Action, ActionRequest, CheckedState, Node, NodeBuilder, NodeId, Role, Tree, TreeUpdate,
|
||||
};
|
||||
use i_slint_core::accessibility::AccessibleStringProperty;
|
||||
use i_slint_core::item_tree::ItemWeak;
|
||||
use i_slint_core::items::{ItemRc, WindowItem};
|
||||
use i_slint_core::window::WindowInner;
|
||||
use i_slint_core::{component::ComponentRef, lengths::ScaleFactor};
|
||||
use i_slint_core::{properties::PropertyTracker, window::WindowAdapter};
|
||||
|
||||
use super::WinitWindowAdapter;
|
||||
|
||||
/// The AccessKit adapter tries to keep the given window adapter's item tree in sync with accesskit's node tree.
|
||||
///
|
||||
/// The entire item tree is mapped to accesskit's node tree. Any changes to an individual accessible item results
|
||||
/// in an access kit tree update with just changed nodes. Any changes in the tree structure result in a complete
|
||||
/// tree rebuild. This could be implemented more efficiently, but it requires encoding the item index, raw vrc pointer,
|
||||
/// and tree generation into the nodeid.
|
||||
///
|
||||
/// For unix it's necessary to inform accesskit about any changes to the position or size of the window, hence
|
||||
/// the `on_event` function that needs calling.
|
||||
///
|
||||
/// Similarly, when the window adapter is informed about a focus change, handle_focus_change must be called.
|
||||
/// Finally, when a component is destroyed, `unregister_component` must be called, which rebuilds the entire
|
||||
/// tree at the moment.
|
||||
///
|
||||
/// If we wanted to move this to corelib, `on_event` gets replaced with listening to the events sent from the
|
||||
/// platform adapter to the slint::Window. `handle_focus_change` is already internal to WindowInner, as well
|
||||
/// as `component_destroyed`. The `WindowInner` would own this `AccessKit`.
|
||||
pub struct AccessKitAdapter {
|
||||
inner: accesskit_winit::Adapter,
|
||||
window_adapter_weak: Weak<WinitWindowAdapter>,
|
||||
|
||||
node_classes: RefCell<accesskit::NodeClassSet>,
|
||||
tree_generation: Cell<usize>,
|
||||
all_nodes: RefCell<Vec<MappedNode>>,
|
||||
global_property_tracker: Pin<Box<PropertyTracker<AccessibilitiesPropertyTracker>>>,
|
||||
}
|
||||
|
||||
impl AccessKitAdapter {
|
||||
pub fn new(
|
||||
window_adapter_weak: Weak<WinitWindowAdapter>,
|
||||
winit_window: &winit::window::Window,
|
||||
) -> Self {
|
||||
let wrapped_window_adapter_weak =
|
||||
send_wrapper::SendWrapper::new(window_adapter_weak.clone());
|
||||
Self {
|
||||
inner: accesskit_winit::Adapter::with_action_handler(
|
||||
&winit_window,
|
||||
move || Self::build_initial_tree(wrapped_window_adapter_weak.clone()),
|
||||
Box::new(ActionForwarder::new(&window_adapter_weak)),
|
||||
),
|
||||
window_adapter_weak: window_adapter_weak.clone(),
|
||||
node_classes: RefCell::new(accesskit::NodeClassSet::new()),
|
||||
tree_generation: Cell::new(1),
|
||||
all_nodes: Default::default(),
|
||||
global_property_tracker: Box::pin(PropertyTracker::new_with_dirty_handler(
|
||||
AccessibilitiesPropertyTracker { window_adapter_weak: window_adapter_weak.clone() },
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_event(
|
||||
&self,
|
||||
window: &winit::window::Window,
|
||||
event: &winit::event::WindowEvent<'_>,
|
||||
) -> bool {
|
||||
match event {
|
||||
winit::event::WindowEvent::Focused(_) => {
|
||||
self.global_property_tracker.set_dirty();
|
||||
let win = self.window_adapter_weak.clone();
|
||||
i_slint_core::timers::Timer::single_shot(Default::default(), move || {
|
||||
if let Some(window_adapter) = win.upgrade() {
|
||||
window_adapter.accesskit_adapter.rebuild_tree_of_dirty_nodes();
|
||||
};
|
||||
});
|
||||
true // keep processing
|
||||
}
|
||||
_ => self.inner.on_event(window, event),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_focus_item_change(&self) {
|
||||
self.inner.update_if_active(|| TreeUpdate {
|
||||
nodes: vec![],
|
||||
tree: None,
|
||||
focus: self.focus_node(),
|
||||
})
|
||||
}
|
||||
|
||||
fn focus_node(&self) -> Option<NodeId> {
|
||||
let window_adapter = self.window_adapter_weak.upgrade()?;
|
||||
if !window_adapter.winit_window().has_focus() {
|
||||
return None;
|
||||
}
|
||||
let window_inner = WindowInner::from_pub(window_adapter.window());
|
||||
let focus_item = window_inner.focus_item.borrow().upgrade().or_else(|| {
|
||||
window_inner.try_component().map(|component_rc| ItemRc::new(component_rc, 0))
|
||||
})?;
|
||||
self.find_node_id_by_item_rc(focus_item)
|
||||
}
|
||||
|
||||
fn handle_request(&self, request: ActionRequest) {
|
||||
let Some(window_adapter) = self.window_adapter_weak.upgrade() else { return };
|
||||
match request.action {
|
||||
Action::Focus => {
|
||||
if let Some(item) = self.item_rc_for_node_id(request.target) {
|
||||
WindowInner::from_pub(window_adapter.window()).set_focus_item(&item);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_component<'a>(&self) {
|
||||
let win = self.window_adapter_weak.clone();
|
||||
i_slint_core::timers::Timer::single_shot(Default::default(), move || {
|
||||
if let Some(window_adapter) = win.upgrade() {
|
||||
let self_ = &window_adapter.accesskit_adapter;
|
||||
self_.inner.update_if_active(|| self_.build_new_tree())
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
pub fn unregister_component<'a>(&self, _component: ComponentRef) {
|
||||
let win = self.window_adapter_weak.clone();
|
||||
i_slint_core::timers::Timer::single_shot(Default::default(), move || {
|
||||
if let Some(window_adapter) = win.upgrade() {
|
||||
let self_ = &window_adapter.accesskit_adapter;
|
||||
self_.inner.update_if_active(|| self_.build_new_tree())
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
fn add_node(&self, node: MappedNode) -> NodeId {
|
||||
let index: usize = self.all_nodes.borrow().len();
|
||||
let id = NodeId(
|
||||
std::num::NonZeroU128::new(
|
||||
(index as u128) << usize::BITS
|
||||
| (self.tree_generation.get() as u128 & usize::MAX as u128),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
self.all_nodes.borrow_mut().push(node);
|
||||
id
|
||||
}
|
||||
|
||||
fn nodes_iter(&self) -> NodeIter<'_> {
|
||||
NodeIter {
|
||||
nodes: Some(std::cell::Ref::map(self.all_nodes.borrow(), |vec| &vec[..])),
|
||||
index: 0,
|
||||
tree_generation: self.tree_generation.get(),
|
||||
}
|
||||
}
|
||||
|
||||
fn item_rc_for_node_id(&self, id: NodeId) -> Option<ItemRc> {
|
||||
let index: usize = (id.0.get() >> usize::BITS) as _;
|
||||
let generation: usize = (id.0.get() & usize::MAX as u128) as _;
|
||||
if generation != self.tree_generation.get() {
|
||||
return None;
|
||||
}
|
||||
self.all_nodes.borrow().get(index).and_then(|cached_node| cached_node.item.upgrade())
|
||||
}
|
||||
|
||||
fn find_node_id_by_item_rc(&self, mut item: ItemRc) -> Option<NodeId> {
|
||||
while !item.is_accessible() {
|
||||
if let Some(parent) = item.parent_item() {
|
||||
item = parent;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.nodes_iter().find_map(|(id, cached_node)| {
|
||||
cached_node.item.upgrade().and_then(|cached_item| (cached_item.eq(&item)).then(|| id))
|
||||
})
|
||||
}
|
||||
|
||||
fn rebuild_tree_of_dirty_nodes(&self) {
|
||||
if !self.global_property_tracker.is_dirty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// It's possible that we may have been triggered by a timer, but in the meantime
|
||||
// the node tree has been emptied due to a tree structure change.
|
||||
if self.all_nodes.borrow().is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(window_adapter) = self.window_adapter_weak.upgrade() else { return };
|
||||
let window = window_adapter.window();
|
||||
|
||||
self.inner.update_if_active(|| {
|
||||
self.global_property_tracker.as_ref().evaluate_as_dependency_root(|| {
|
||||
let nodes = self.nodes_iter().filter_map(|(id, cached_node)| {
|
||||
cached_node.tracker.as_ref().evaluate_if_dirty(|| {
|
||||
let scale_factor = ScaleFactor::new(window.scale_factor());
|
||||
let item = cached_node.item.upgrade()?;
|
||||
|
||||
let mut builder = self.build_node_without_children(&item, scale_factor);
|
||||
|
||||
builder.set_children(cached_node.children.clone());
|
||||
|
||||
let node = builder.build(&mut self.node_classes.borrow_mut());
|
||||
|
||||
Some((id, node))
|
||||
})?
|
||||
});
|
||||
|
||||
let update =
|
||||
TreeUpdate { nodes: nodes.collect(), tree: None, focus: self.focus_node() };
|
||||
|
||||
update
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn build_node_for_item_recursively(
|
||||
&self,
|
||||
item: ItemRc,
|
||||
nodes: &mut Vec<(NodeId, Node)>,
|
||||
scale_factor: ScaleFactor,
|
||||
) -> NodeId {
|
||||
let tracker = Box::pin(PropertyTracker::default());
|
||||
|
||||
let mut builder =
|
||||
tracker.as_ref().evaluate(|| self.build_node_without_children(&item, scale_factor));
|
||||
|
||||
let children = i_slint_core::accessibility::accessible_descendents(&item)
|
||||
.map(|child| self.build_node_for_item_recursively(child, nodes, scale_factor))
|
||||
.collect::<Vec<NodeId>>();
|
||||
|
||||
builder.set_children(children.clone());
|
||||
|
||||
let id = self.add_node(MappedNode { item: item.downgrade(), children, tracker });
|
||||
let node = builder.build(&mut self.node_classes.borrow_mut());
|
||||
|
||||
nodes.push((id, node));
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
fn build_new_tree(&self) -> TreeUpdate {
|
||||
let Some(window_adapter) = self.window_adapter_weak.upgrade() else { return Default::default(); };
|
||||
let window = window_adapter.window();
|
||||
let window_inner = i_slint_core::window::WindowInner::from_pub(window);
|
||||
|
||||
let root_item = ItemRc::new(window_inner.component(), 0);
|
||||
|
||||
self.tree_generation.set(self.tree_generation.get() + 1);
|
||||
|
||||
self.all_nodes.borrow_mut().clear();
|
||||
let mut nodes = Vec::new();
|
||||
|
||||
let root_id = self.global_property_tracker.as_ref().evaluate_as_dependency_root(|| {
|
||||
self.build_node_for_item_recursively(
|
||||
root_item,
|
||||
&mut nodes,
|
||||
ScaleFactor::new(window.scale_factor()),
|
||||
)
|
||||
});
|
||||
|
||||
let update = TreeUpdate { nodes, tree: Some(Tree::new(root_id)), focus: self.focus_node() };
|
||||
update
|
||||
}
|
||||
|
||||
fn build_initial_tree(
|
||||
wrapped_window_adapter_weak: send_wrapper::SendWrapper<Weak<WinitWindowAdapter>>,
|
||||
) -> TreeUpdate {
|
||||
if wrapped_window_adapter_weak.valid() {
|
||||
return wrapped_window_adapter_weak
|
||||
.take()
|
||||
.upgrade()
|
||||
.map(|adapter| adapter.accesskit_adapter.build_new_tree())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
let update_from_main_thread = Arc::new((Mutex::new(None), Condvar::new()));
|
||||
|
||||
if let Err(_) = i_slint_core::api::invoke_from_event_loop({
|
||||
let update_from_main_thread = update_from_main_thread.clone();
|
||||
move || {
|
||||
let (lock, wait_condition) = &*update_from_main_thread;
|
||||
let mut update = lock.lock().unwrap();
|
||||
|
||||
*update = Some(Self::build_initial_tree(wrapped_window_adapter_weak));
|
||||
|
||||
wait_condition.notify_one();
|
||||
}
|
||||
}) {
|
||||
return Default::default();
|
||||
}
|
||||
|
||||
let (lock, wait_condition) = &*update_from_main_thread;
|
||||
let mut update = lock.lock().unwrap();
|
||||
while update.is_none() {
|
||||
update = wait_condition.wait(update).unwrap();
|
||||
}
|
||||
|
||||
return update.take().unwrap();
|
||||
}
|
||||
|
||||
fn build_node_without_children(&self, item: &ItemRc, scale_factor: ScaleFactor) -> NodeBuilder {
|
||||
let (role, label) = if let Some(window_item) = item.downcast::<WindowItem>() {
|
||||
(Role::Window, window_item.as_pin_ref().title().to_string())
|
||||
} else {
|
||||
(
|
||||
match item.accessible_role() {
|
||||
i_slint_core::items::AccessibleRole::None => Role::Unknown,
|
||||
i_slint_core::items::AccessibleRole::Button => Role::Button,
|
||||
i_slint_core::items::AccessibleRole::Checkbox => Role::CheckBox,
|
||||
i_slint_core::items::AccessibleRole::Combobox => Role::ComboBoxGrouping,
|
||||
i_slint_core::items::AccessibleRole::Slider => Role::Slider,
|
||||
i_slint_core::items::AccessibleRole::Spinbox => Role::SpinButton,
|
||||
i_slint_core::items::AccessibleRole::Tab => Role::Tab,
|
||||
i_slint_core::items::AccessibleRole::Text => Role::StaticText,
|
||||
},
|
||||
item.accessible_string_property(
|
||||
i_slint_core::accessibility::AccessibleStringProperty::Label,
|
||||
)
|
||||
.to_string(),
|
||||
)
|
||||
};
|
||||
|
||||
let mut builder = NodeBuilder::new(role);
|
||||
|
||||
builder.set_name(label);
|
||||
|
||||
let geometry = item.geometry();
|
||||
let absolute_origin = item.map_to_window(geometry.origin);
|
||||
let physical_origin = (absolute_origin * scale_factor).cast::<f64>();
|
||||
let physical_size = (geometry.size * scale_factor).cast::<f64>();
|
||||
builder.set_bounds(accesskit::Rect {
|
||||
x0: physical_origin.x,
|
||||
y0: physical_origin.y,
|
||||
x1: physical_origin.x + physical_size.width,
|
||||
y1: physical_origin.y + physical_size.height,
|
||||
});
|
||||
|
||||
if item.accessible_string_property(AccessibleStringProperty::Checkable) == "true" {
|
||||
builder.set_checked_state(
|
||||
if item.accessible_string_property(AccessibleStringProperty::Checked) == "true" {
|
||||
CheckedState::True
|
||||
} else {
|
||||
CheckedState::False
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
builder.set_description(
|
||||
item.accessible_string_property(AccessibleStringProperty::Description).to_string(),
|
||||
);
|
||||
|
||||
if matches!(
|
||||
role,
|
||||
Role::Button
|
||||
| Role::CheckBox
|
||||
| Role::ComboBoxGrouping
|
||||
| Role::Slider
|
||||
| Role::SpinButton
|
||||
| Role::Tab
|
||||
) {
|
||||
builder.add_action(Action::Focus);
|
||||
}
|
||||
|
||||
let min = item.accessible_string_property(AccessibleStringProperty::ValueMinimum);
|
||||
let max = item.accessible_string_property(AccessibleStringProperty::ValueMaximum);
|
||||
let step = item.accessible_string_property(AccessibleStringProperty::ValueStep);
|
||||
let value = item.accessible_string_property(AccessibleStringProperty::Value).to_string();
|
||||
|
||||
match (min.parse(), max.parse(), value.parse(), step.parse()) {
|
||||
(Ok(min), Ok(max), Ok(value), Ok(step)) => {
|
||||
builder.set_min_numeric_value(min);
|
||||
builder.set_max_numeric_value(max);
|
||||
builder.set_numeric_value(value);
|
||||
builder.set_numeric_value_step(step);
|
||||
}
|
||||
_ => {
|
||||
builder.set_value(value);
|
||||
}
|
||||
}
|
||||
|
||||
builder
|
||||
}
|
||||
}
|
||||
|
||||
struct NodeIter<'a> {
|
||||
nodes: Option<std::cell::Ref<'a, [MappedNode]>>,
|
||||
index: usize,
|
||||
tree_generation: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for NodeIter<'a> {
|
||||
type Item = (NodeId, std::cell::Ref<'a, MappedNode>);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.nodes.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(remaining_slice) = self.nodes.take() else { return None; };
|
||||
|
||||
if remaining_slice.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (head, tail) =
|
||||
std::cell::Ref::map_split(remaining_slice, |slice| (&slice[0], &slice[1..]));
|
||||
self.nodes.replace(tail);
|
||||
|
||||
let index = self.index;
|
||||
self.index += 1;
|
||||
let id = NodeId(
|
||||
std::num::NonZeroU128::new(
|
||||
(index as u128) << usize::BITS
|
||||
| (self.tree_generation as u128 & usize::MAX as u128),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
Some((id, head))
|
||||
}
|
||||
}
|
||||
|
||||
struct AccessibilitiesPropertyTracker {
|
||||
window_adapter_weak: Weak<WinitWindowAdapter>,
|
||||
}
|
||||
|
||||
impl i_slint_core::properties::PropertyDirtyHandler for AccessibilitiesPropertyTracker {
|
||||
fn notify(&self) {
|
||||
let win = self.window_adapter_weak.clone();
|
||||
i_slint_core::timers::Timer::single_shot(Default::default(), move || {
|
||||
if let Some(window_adapter) = win.upgrade() {
|
||||
window_adapter.accesskit_adapter.rebuild_tree_of_dirty_nodes();
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct MappedNode {
|
||||
item: ItemWeak,
|
||||
children: Vec<NodeId>,
|
||||
tracker: Pin<Box<PropertyTracker>>,
|
||||
}
|
||||
|
||||
struct ActionForwarder {
|
||||
wrapped_window_adapter_weak: send_wrapper::SendWrapper<Weak<WinitWindowAdapter>>,
|
||||
}
|
||||
|
||||
impl ActionForwarder {
|
||||
pub fn new(window_adapter: &Weak<WinitWindowAdapter>) -> Self {
|
||||
Self { wrapped_window_adapter_weak: send_wrapper::SendWrapper::new(window_adapter.clone()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl accesskit::ActionHandler for ActionForwarder {
|
||||
fn do_action(&self, request: ActionRequest) {
|
||||
let wrapped_window_adapter_weak = self.wrapped_window_adapter_weak.clone();
|
||||
i_slint_core::api::invoke_from_event_loop(move || {
|
||||
let Some(window_adapter) = wrapped_window_adapter_weak.take().upgrade() else { return };
|
||||
window_adapter.accesskit_adapter.handle_request(request)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
|
@ -7,5 +7,6 @@ fn main() {
|
|||
// Setup cfg aliases
|
||||
cfg_aliases! {
|
||||
enable_skia_renderer: { any(feature = "renderer-winit-skia", feature = "renderer-winit-skia-opengl", feature = "renderer-winit-skia-vulkan")},
|
||||
enable_accesskit: { all(feature = "accessibility", not(target_arch = "wasm32")) },
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -502,8 +502,17 @@ pub fn run() -> Result<(), corelib::platform::PlatformError> {
|
|||
match event {
|
||||
Event::WindowEvent { event, window_id } => {
|
||||
if let Some(window) = window_by_id(window_id) {
|
||||
#[cfg(not(enable_accesskit))]
|
||||
let process_event = true;
|
||||
#[cfg(enable_accesskit)]
|
||||
let process_event =
|
||||
window.accesskit_adapter.on_event(&window.winit_window(), &event);
|
||||
|
||||
if process_event {
|
||||
*inner_event_loop_error.borrow_mut() =
|
||||
process_window_event(window, event, &mut cursor_pos, &mut pressed).err();
|
||||
process_window_event(window, event, &mut cursor_pos, &mut pressed)
|
||||
.err();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ mod renderer {
|
|||
pub(crate) mod sw;
|
||||
}
|
||||
|
||||
#[cfg(enable_accesskit)]
|
||||
mod accesskit;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) mod wasm_input_helper;
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,11 @@ use crate::renderer::WinitCompatibleRenderer;
|
|||
use const_field_offset::FieldOffsets;
|
||||
|
||||
use corelib::component::ComponentRc;
|
||||
#[cfg(enable_accesskit)]
|
||||
use corelib::component::ComponentRef;
|
||||
use corelib::items::MouseCursor;
|
||||
#[cfg(enable_accesskit)]
|
||||
use corelib::items::{ItemRc, ItemRef};
|
||||
|
||||
use corelib::layout::Orientation;
|
||||
use corelib::lengths::{LogicalLength, LogicalSize};
|
||||
|
|
@ -118,6 +122,9 @@ pub struct WinitWindowAdapter {
|
|||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
virtual_keyboard_helper: RefCell<Option<super::wasm_input_helper::WasmInputHelper>>,
|
||||
|
||||
#[cfg(enable_accesskit)]
|
||||
pub accesskit_adapter: crate::accesskit::AccessKitAdapter,
|
||||
}
|
||||
|
||||
impl WinitWindowAdapter {
|
||||
|
|
@ -131,6 +138,8 @@ impl WinitWindowAdapter {
|
|||
)
|
||||
.and_then(|builder| R::new(builder))?;
|
||||
|
||||
let winit_window = Rc::new(winit_window);
|
||||
|
||||
let self_rc = Rc::new_cyclic(|self_weak| Self {
|
||||
window: OnceCell::with_value(corelib::api::Window::new(self_weak.clone() as _)),
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
|
@ -140,10 +149,15 @@ impl WinitWindowAdapter {
|
|||
dark_color_scheme: Default::default(),
|
||||
constraints: Default::default(),
|
||||
shown: Default::default(),
|
||||
winit_window: Rc::new(winit_window),
|
||||
winit_window: winit_window.clone(),
|
||||
renderer: Box::new(renderer),
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
virtual_keyboard_helper: Default::default(),
|
||||
#[cfg(enable_accesskit)]
|
||||
accesskit_adapter: crate::accesskit::AccessKitAdapter::new(
|
||||
self_weak.clone(),
|
||||
&*winit_window,
|
||||
),
|
||||
});
|
||||
|
||||
let id = self_rc.winit_window().id();
|
||||
|
|
@ -643,6 +657,25 @@ impl WindowAdapterInternal for WinitWindowAdapter {
|
|||
fn is_visible(&self) -> bool {
|
||||
self.winit_window().is_visible().unwrap_or(true)
|
||||
}
|
||||
|
||||
#[cfg(enable_accesskit)]
|
||||
fn handle_focus_change(&self, _old: Option<ItemRc>, _new: Option<ItemRc>) {
|
||||
self.accesskit_adapter.handle_focus_item_change();
|
||||
}
|
||||
|
||||
#[cfg(enable_accesskit)]
|
||||
fn register_component(&self) {
|
||||
self.accesskit_adapter.register_component();
|
||||
}
|
||||
|
||||
#[cfg(enable_accesskit)]
|
||||
fn unregister_component<'a>(
|
||||
&self,
|
||||
_component: ComponentRef,
|
||||
_: &mut dyn Iterator<Item = Pin<ItemRef<'a>>>,
|
||||
) {
|
||||
self.accesskit_adapter.unregister_component(_component);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WinitWindowAdapter {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ path = "lib.rs"
|
|||
|
||||
[features]
|
||||
|
||||
default = ["std", "backend-winit", "renderer-winit-femtovg", "backend-qt", "compat-1-0"]
|
||||
default = ["std", "backend-winit", "renderer-winit-femtovg", "backend-qt", "accessibility", "compat-1-0"]
|
||||
|
||||
## Mandatory feature:
|
||||
## This feature is required to keep the compatibility with Slint 1.0
|
||||
|
|
@ -81,6 +81,11 @@ renderer-winit-skia-vulkan = ["i-slint-backend-selector/renderer-winit-skia-vulk
|
|||
## Must be used in combination with `backend-winit`, `backend-winit-x11`, or `backend-winit-wayland`.
|
||||
renderer-winit-software = ["i-slint-backend-selector/renderer-winit-software"]
|
||||
|
||||
## Enable integration with operating system provided accessibility APIs (default: enabled)
|
||||
##
|
||||
## Enabling this feature will try to expose the tree of UI elements to OS provided accessibility
|
||||
## APIs to support screen readers and other assistive technologies.
|
||||
accessibility = ["i-slint-backend-selector/accessibility"]
|
||||
|
||||
[dependencies]
|
||||
i-slint-compiler = { version = "=1.0.3", path = "../compiler" }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue