cranpose_ui/layout/semantics_tree.rs
1use std::rc::Rc;
2
3use cranpose_core::NodeId;
4use cranpose_foundation::{SemanticsCustomAction, SemanticsWidgetRole};
5
6use super::{GeometryRect, LayoutState, Point, SemanticsDetails, Size};
7
8/// Discrete event callback reference produced during semantics extraction.
9#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct SemanticsCallback {
11 pub(super) node_id: NodeId,
12}
13
14impl SemanticsCallback {
15 /// Creates a callback that targets the node with `node_id`.
16 pub fn new(node_id: NodeId) -> Self {
17 Self { node_id }
18 }
19
20 /// Returns the id of the node whose action this callback triggers.
21 pub fn node_id(&self) -> NodeId {
22 self.node_id
23 }
24}
25
26/// Semantics action exposed to the input system.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub enum SemanticsAction {
29 /// Activates the node, as a tap or a keyboard or assistive technology click does.
30 Click {
31 /// Callback that identifies the node to activate.
32 handler: SemanticsCallback,
33 },
34}
35
36/// A text node's text in the semantics tree, shared with the node's modifier
37/// slices rather than copied out of them.
38#[derive(Clone, Debug)]
39pub struct SemanticsText(pub(super) Rc<crate::text::AnnotatedString>);
40
41impl SemanticsText {
42 /// The text the node shows.
43 pub fn as_str(&self) -> &str {
44 &self.0.text
45 }
46}
47
48impl PartialEq for SemanticsText {
49 fn eq(&self, other: &Self) -> bool {
50 self.as_str() == other.as_str()
51 }
52}
53
54impl Eq for SemanticsText {}
55
56impl From<&str> for SemanticsText {
57 fn from(text: &str) -> Self {
58 Self(Rc::new(crate::text::AnnotatedString::from(text)))
59 }
60}
61
62/// Semantic role describing how a node should participate in accessibility and hit testing.
63/// Roles are now derived from SemanticsConfiguration rather than widget types.
64#[derive(Clone, Debug, PartialEq, Eq)]
65pub enum SemanticsRole {
66 /// Generic container or layout node
67 Layout,
68 /// Subcomposition boundary
69 Subcompose,
70 /// Text content derived from the text node semantics payload.
71 Text {
72 /// The text the node shows.
73 value: SemanticsText,
74 },
75 /// Spacer (non-interactive)
76 Spacer,
77 /// Button (derived from the semantics `role`)
78 Button,
79 /// Unknown or unspecified role
80 Unknown,
81}
82
83/// A single node within the semantics tree.
84///
85/// Not `Eq`: a node now carries drawn-control bounds, which are floats. The
86/// tree is compared for "did anything a screen reader can see change", and
87/// that comparison is `PartialEq` everywhere else on the accessibility path
88/// (`AccessibilityRect`, `AccessibilityElement`) for the same reason.
89#[derive(Clone, Debug, PartialEq)]
90pub struct SemanticsNode {
91 /// Id of the runtime node this semantics node describes.
92 pub node_id: NodeId,
93 /// Where the node lies in the root's coordinates: its layout rect,
94 /// before graphics-layer transforms, as [`LayoutBox::rect`](super::LayoutBox::rect) holds it.
95 pub bounds: GeometryRect,
96 /// Where layout put the node inside its parent's content, from which a
97 /// tree update moves a node that did not change along with its parent.
98 pub placement: SemanticsPlacement,
99 /// Incarnation of the runtime node, incremented when its storage is recycled.
100 pub node_generation: u32,
101 /// Where this node sits in the tree (layout, text, subcomposition …).
102 pub role: SemanticsRole,
103 /// What kind of control this node is, as a screen reader announces it —
104 /// Compose's `Role`. Separate from [`SemanticsNode::role`] because a
105 /// clickable `Row` is structurally a layout node and semantically a button.
106 pub widget_role: Option<SemanticsWidgetRole>,
107 /// Actions that the input system and assistive technology can invoke on this node.
108 pub actions: Vec<SemanticsAction>,
109 /// Child semantics nodes, in layout order.
110 pub children: Vec<Self>,
111 /// Content description that a screen reader announces for the node, if one was set.
112 pub description: Option<String>,
113 /// Direct activation callback for keyboard and assistive technology.
114 pub on_click: Option<SemanticsCustomAction>,
115 /// Selection state, or `None` if the node is not selectable.
116 pub selected: Option<bool>,
117 /// Checked state, or `None` if the node is not a toggleable control.
118 pub toggled: Option<cranpose_foundation::ToggleableState>,
119 /// Whether the node accepts user interaction. `true` unless the node was disabled.
120 pub enabled: bool,
121 /// Whether a screen reader skips this node and everything under it.
122 pub hidden: bool,
123 /// Whether a screen reader takes this node and the text under it as one
124 /// stop.
125 pub merge_descendants: bool,
126 /// Where a screen reader visits this node among the ones beside it.
127 pub traversal_index: f32,
128 /// The text an editable field holds.
129 pub text: Option<String>,
130 /// Whether this node registered a focus target, so focus can land on it.
131 /// Compose's `SemanticsProperties.Focused` companion.
132 pub focusable: bool,
133 /// Whether focus sits on this node right now.
134 pub focused: bool,
135 /// The properties few nodes set, held only when the node sets one: read
136 /// them through [`SemanticsNode::details`] and write them through
137 /// [`SemanticsNode::set_details`].
138 pub details: Option<Box<SemanticsDetails>>,
139}
140
141impl Default for SemanticsNode {
142 fn default() -> Self {
143 Self {
144 node_id: NodeId::default(),
145 bounds: GeometryRect::EMPTY,
146 placement: SemanticsPlacement::default(),
147 node_generation: 0,
148 role: SemanticsRole::Unknown,
149 widget_role: None,
150 actions: Vec::new(),
151 children: Vec::new(),
152 description: None,
153 on_click: None,
154 selected: None,
155 toggled: None,
156 enabled: true,
157 hidden: false,
158 merge_descendants: false,
159 traversal_index: 0.0,
160 text: None,
161 focusable: false,
162 focused: false,
163 details: None,
164 }
165 }
166}
167
168/// Where layout put a semantics node: its offset inside its parent's
169/// content and where its own content starts inside it.
170#[derive(Clone, Copy, Debug, Default, PartialEq)]
171pub struct SemanticsPlacement {
172 pub(super) position: Point,
173 pub(super) content_offset: Point,
174}
175
176impl SemanticsPlacement {
177 pub(super) fn of(state: &LayoutState) -> Self {
178 Self {
179 position: state.position(),
180 content_offset: state.content_offset(),
181 }
182 }
183
184 pub(super) fn place(self, origin: Point) -> (Point, Point) {
185 let top_left = Point {
186 x: origin.x + self.position.x,
187 y: origin.y + self.position.y,
188 };
189 let content = Point {
190 x: top_left.x + self.content_offset.x,
191 y: top_left.y + self.content_offset.y,
192 };
193 (top_left, content)
194 }
195}
196
197/// Rooted semantics tree extracted after layout.
198///
199/// Two trees are equal when they hold the same nodes; what a tree keeps to
200/// bring itself up to date is not compared.
201#[derive(Clone, Debug)]
202pub struct SemanticsTree {
203 pub(super) full: SemanticsNode,
204 pub(super) modal: Option<Vec<usize>>,
205 pub(super) tracking: super::semantics_update::SemanticsTracking,
206}
207
208impl PartialEq for SemanticsTree {
209 fn eq(&self, other: &Self) -> bool {
210 self.full == other.full && self.modal == other.modal
211 }
212}
213
214impl SemanticsTree {
215 pub(super) fn new(full: SemanticsNode) -> Self {
216 let modal = top_modal_path(&full);
217 Self {
218 full,
219 modal,
220 tracking: super::semantics_update::SemanticsTracking::default(),
221 }
222 }
223
224 /// Returns the top visible modal subtree, or the full root when no modal is open.
225 pub fn root(&self) -> &SemanticsNode {
226 self.modal.as_deref().map_or(&self.full, |path| {
227 path.iter()
228 .try_fold(&self.full, |node, &index| node.children.get(index))
229 .unwrap_or(&self.full)
230 })
231 }
232}
233
234/// Whether a modal of `size` takes space; a zero-sized modal hides nothing.
235pub(super) fn modal_takes_space(size: Size) -> bool {
236 size.width > 0.0 && size.height > 0.0 && size.width.is_finite() && size.height.is_finite()
237}
238
239/// The path to the modal drawn on top: the last visible modal in document
240/// order, a modal inside another preferred to it.
241pub(super) fn top_modal_path(root: &SemanticsNode) -> Option<Vec<usize>> {
242 pub(super) fn search(node: &SemanticsNode, path: &mut Vec<usize>) -> bool {
243 if node.hidden {
244 return false;
245 }
246 for (index, child) in node.children.iter().enumerate().rev() {
247 path.push(index);
248 if search(child, path) {
249 return true;
250 }
251 path.pop();
252 }
253 node.details().is_modal
254 }
255 let mut path = Vec::new();
256 search(root, &mut path).then_some(path)
257}