Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial support for popups with the AccessKit backend #7265

Merged
merged 3 commits into from
Jan 3, 2025
Merged
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
141 changes: 103 additions & 38 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 @@ -226,6 +230,18 @@ impl AccessKitAdapter {
}
}

fn accessible_parent_for_item_rc(mut item: ItemRc) -> ItemRc {
while !item.is_accessible() {
if let Some(parent) = item.parent_item() {
item = parent;
} else {
break;
}
}

item
}

struct NodeCollection {
next_component_id: u32,
components_by_id: HashMap<u32, ItemTreeWeak>,
Expand All @@ -235,19 +251,7 @@ struct NodeCollection {
}

impl NodeCollection {
fn accessible_parent_for_item_rc(&self, mut item: ItemRc) -> ItemRc {
while !item.is_accessible() {
if let Some(parent) = item.parent_item() {
item = parent;
} else {
break;
}
}

item
}

fn focus_node(&self, window_adapter_weak: &Weak<WinitWindowAdapter>) -> NodeId {
fn focus_node(&mut self, window_adapter_weak: &Weak<WinitWindowAdapter>) -> NodeId {
window_adapter_weak
.upgrade()
.filter(|window_adapter| {
Expand All @@ -260,7 +264,7 @@ impl NodeCollection {
.borrow()
.upgrade()
.map(|focus_item| {
let parent = self.accessible_parent_for_item_rc(focus_item);
let parent = accessible_parent_for_item_rc(focus_item);
parent
.accessible_string_property(AccessibleStringProperty::DelegateFocus)
.and_then(|s| s.parse::<usize>().ok())
Expand All @@ -274,7 +278,7 @@ impl NodeCollection {
.try_component()
.map(|component_rc| ItemRc::new(component_rc, 0))
})
.and_then(|focus_item| self.find_node_id_by_item_rc(focus_item))
.map(|focus_item| self.find_node_id_by_item_rc(focus_item))
})
.unwrap_or_else(|| self.root_node_id)
}
Expand All @@ -286,47 +290,76 @@ impl NodeCollection {
Some(ItemRc::new(component, index))
}

fn find_node_id_by_item_rc(&self, mut item: ItemRc) -> Option<NodeId> {
item = self.accessible_parent_for_item_rc(item);
fn find_node_id_by_item_rc(&mut self, mut item: ItemRc) -> NodeId {
item = accessible_parent_for_item_rc(item);

self.encode_item_node_id(&item)
}

fn encode_item_node_id(&self, item: &ItemRc) -> Option<NodeId> {
fn encode_item_node_id(&mut self, item: &ItemRc) -> NodeId {
let component = item.item_tree();
let component_ptr = ItemTreeRef::as_ptr(ItemTreeRc::borrow(component));
let component_id = *(self.component_ids.get(&component_ptr)?);
let component_id = match self.component_ids.get(&component_ptr) {
Some(&component_id) => component_id,
None => {
let component_id = self.next_component_id;
self.next_component_id += 1;
self.component_ids.insert(component_ptr, component_id);
self.components_by_id.insert(component_id, ItemTreeRc::downgrade(component));
component_id
}
};

let index = item.index();
Some(NodeId((component_id as u64) << u32::BITS | (index as u64 & u32::MAX as u64)))
NodeId((component_id as u64) << u32::BITS | (index as u64 & u32::MAX as u64))
}

fn build_node_for_item_recursively(
&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 component = item.item_tree();
let component_ptr = ItemTreeRef::as_ptr(ItemTreeRc::borrow(component));
if !self.component_ids.contains_key(&component_ptr) {
let component_id = self.next_component_id;
self.next_component_id += 1;
self.component_ids.insert(component_ptr, component_id);
self.components_by_id.insert(component_id, ItemTreeRc::downgrade(component));
}

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

nodes.push((id, node));
Expand All @@ -351,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 @@ -370,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 @@ -425,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 @@ -595,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
Loading