Skip to main content

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}