Skip to content

Commit

Permalink
AccessKit: expose popups in the accessibility tree
Browse files Browse the repository at this point in the history
This is the plumbing, where a `PopupWindow` directly becomes a child of wherever it's declared in, when show() is called. Further changes may be necessary to make this really useful, such as new roles or improved dialog handling. (see ticket for comments)

Fixes #7209
  • Loading branch information
tronical committed Jan 3, 2025
1 parent 30ae161 commit c31389d
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 13 deletions.
82 changes: 73 additions & 9 deletions internal/backends/winit/accesskit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use i_slint_core::accessibility::{
use i_slint_core::api::Window;
use i_slint_core::item_tree::{ItemTreeRc, ItemTreeRef, ItemTreeWeak};
use i_slint_core::items::{ItemRc, WindowItem};
use i_slint_core::lengths::ScaleFactor;
use i_slint_core::window::WindowInner;
use i_slint_core::lengths::{LogicalPoint, ScaleFactor};
use i_slint_core::window::{PopupWindowLocation, WindowInner};
use i_slint_core::SharedString;
use i_slint_core::{properties::PropertyTracker, window::WindowAdapter};

Expand Down Expand Up @@ -195,7 +195,11 @@ impl AccessKitAdapter {
let scale_factor = ScaleFactor::new(window.scale_factor());
let item = self.nodes.item_rc_for_node_id(cached_node.id)?;

let mut node = self.nodes.build_node_without_children(&item, scale_factor);
let mut node = self.nodes.build_node_without_children(
&item,
scale_factor,
Default::default(),
);

node.set_children(cached_node.children.clone());

Expand Down Expand Up @@ -314,20 +318,48 @@ impl NodeCollection {
&mut self,
item: ItemRc,
nodes: &mut Vec<(NodeId, Node)>,
popups: &[AccessiblePopup],
scale_factor: ScaleFactor,
window_position: LogicalPoint,
) -> NodeId {
let tracker = Box::pin(PropertyTracker::default());

let mut node =
tracker.as_ref().evaluate(|| self.build_node_without_children(&item, scale_factor));
let mut node = tracker
.as_ref()
.evaluate(|| self.build_node_without_children(&item, scale_factor, window_position));

let id = self.encode_item_node_id(&item);

let popup_child = popups.iter().find_map(|popup| {
if popup.parent_node != id {
return None;
}

let popup_item = ItemRc::new(popup.component.clone(), 0);
Some(self.build_node_for_item_recursively(
popup_item,
nodes,
popups,
scale_factor,
popup.location,
))
});

let children = i_slint_core::accessibility::accessible_descendents(&item)
.map(|child| self.build_node_for_item_recursively(child, nodes, scale_factor))
.map(|child| {
self.build_node_for_item_recursively(
child,
nodes,
popups,
scale_factor,
window_position,
)
})
.chain(popup_child)
.collect::<Vec<NodeId>>();

node.set_children(children.clone());

let id = self.encode_item_node_id(&item);
self.all_nodes.push(CachedNode { id, children, tracker });

nodes.push((id, node));
Expand All @@ -352,14 +384,35 @@ impl NodeCollection {

let root_item = ItemRc::new(window_inner.component(), 0);

let popups = window_inner
.active_popups()
.iter()
.filter_map(|popup| {
let PopupWindowLocation::ChildWindow(location) = popup.location else {
return None;
};

let parent_item = accessible_parent_for_item_rc(popup.parent_item.upgrade()?);
let parent_node = self.encode_item_node_id(if parent_item.is_accessible() {
&parent_item
} else {
&root_item
});

Some(AccessiblePopup { location, parent_node, component: popup.component.clone() })
})
.collect::<Vec<_>>();

self.all_nodes.clear();
let mut nodes = Vec::new();

let root_id = property_tracker.evaluate_as_dependency_root(|| {
self.build_node_for_item_recursively(
root_item,
&mut nodes,
&popups,
ScaleFactor::new(window.scale_factor()),
Default::default(),
)
});
self.root_node_id = root_id;
Expand All @@ -371,7 +424,12 @@ impl NodeCollection {
}
}

fn build_node_without_children(&self, item: &ItemRc, scale_factor: ScaleFactor) -> Node {
fn build_node_without_children(
&self,
item: &ItemRc,
scale_factor: ScaleFactor,
window_position: LogicalPoint,
) -> Node {
let is_checkable = item
.accessible_string_property(AccessibleStringProperty::Checkable)
.is_some_and(|x| x == "true");
Expand Down Expand Up @@ -426,7 +484,7 @@ impl NodeCollection {
}

let geometry = item.geometry();
let absolute_origin = item.map_to_window(geometry.origin);
let absolute_origin = item.map_to_window(geometry.origin) + window_position.to_vector();
let physical_origin = (absolute_origin * scale_factor).cast::<f64>();
let physical_size = (geometry.size * scale_factor).cast::<f64>();
node.set_bounds(accesskit::Rect {
Expand Down Expand Up @@ -596,3 +654,9 @@ impl DeferredAccessKitAction {
}
}
}

struct AccessiblePopup {
location: LogicalPoint,
parent_node: NodeId,
component: ItemTreeRc,
}
18 changes: 14 additions & 4 deletions internal/core/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,8 @@ impl crate::properties::PropertyDirtyHandler for WindowRedrawTracker {
}

/// This enum describes the different ways a popup can be rendered by the back-end.
enum PopupWindowLocation {
#[derive(Clone)]
pub enum PopupWindowLocation {
/// The popup is rendered in its own top-level window that is know to the windowing system.
TopLevel(Rc<dyn WindowAdapter>),
/// The popup is rendered as an embedded child window at the given position.
Expand All @@ -382,17 +383,20 @@ enum PopupWindowLocation {

/// This structure defines a graphical element that is designed to pop up from the surrounding
/// UI content, for example to show a context menu.
struct PopupWindow {
#[derive(Clone)]
pub struct PopupWindow {
/// The ID of the associated popup.
popup_id: NonZeroU32,
/// The location defines where the pop up is rendered.
location: PopupWindowLocation,
pub location: PopupWindowLocation,
/// The component that is responsible for providing the popup content.
component: ItemTreeRc,
pub component: ItemTreeRc,
/// Defines the close behaviour of the popup.
close_policy: PopupClosePolicy,
/// the item that had the focus in the parent window when the popup was opened
focus_item_in_parent: ItemWeak,
/// The item from where the Popup was invoked from
pub parent_item: ItemWeak,
}

#[pin_project::pin_project]
Expand Down Expand Up @@ -552,6 +556,11 @@ impl WindowInner {
self.component.borrow().upgrade()
}

/// Returns a slice of the active poppups.
pub fn active_popups(&self) -> core::cell::Ref<'_, [PopupWindow]> {
core::cell::Ref::map(self.active_popups.borrow(), |v| v.as_slice())
}

/// Receive a mouse event and pass it to the items of the component to
/// change their state.
///
Expand Down Expand Up @@ -1103,6 +1112,7 @@ impl WindowInner {
component: popup_componentrc.clone(),
close_policy,
focus_item_in_parent: focus_item,
parent_item: parent_item.downgrade(),
});

popup_id
Expand Down

0 comments on commit c31389d

Please sign in to comment.