fission_ir/semantics.rs
1//! Accessibility and interaction semantics.
2//!
3//! The [`Semantics`] struct describes what a node *means* to assistive technology
4//! and to the event system. It carries a [`Role`] (button, text input, slider, ...),
5//! an optional human-readable label, a set of [`ActionEntry`]s that map input
6//! triggers to framework actions, and flags for focus, drag-and-drop, scrollability,
7//! and more.
8//!
9//! Semantics nodes appear in the IR as `Op::Semantics(semantics)`.
10
11use serde::{Deserialize, Serialize};
12
13/// The accessibility role of a node.
14///
15/// Roles tell screen readers and other assistive technology what kind of control a
16/// node represents. Choose the most specific role that applies.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub enum Role {
19 /// A clickable button that triggers an action.
20 Button,
21 /// A read-only text label.
22 Text,
23 /// An editable text field (single or multi-line).
24 TextInput,
25 /// A raster or vector image.
26 Image,
27 /// A toggle that is either checked or unchecked.
28 Checkbox,
29 /// A toggle switch (on/off).
30 Switch,
31 /// A modal or non-modal dialog overlay.
32 Dialog,
33 /// A continuous range input (e.g., volume control).
34 Slider,
35 /// A generic form input that does not fit the other roles.
36 Input,
37 /// A scrollable list container.
38 List,
39 /// An individual item inside a [`List`](Role::List).
40 ListItem,
41 /// A node with no specific semantic role. The default.
42 Generic,
43}
44
45/// What user interaction triggers an action.
46///
47/// Each [`ActionEntry`] pairs an `ActionTrigger` with an action ID so the event
48/// system knows which callback to invoke for a given input gesture.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
50pub enum ActionTrigger {
51 /// Primary activation: tap, click, or Enter key.
52 Default,
53 /// The user began dragging this node.
54 DragStart,
55 /// The drag position changed (fires continuously).
56 DragUpdate,
57 /// The user released the drag.
58 DragEnd,
59 /// The pointer entered the node's hit area.
60 HoverEnter,
61 /// The pointer left the node's hit area.
62 HoverExit,
63 /// The node received keyboard focus.
64 Focus,
65 /// The node lost keyboard focus.
66 Blur,
67 /// The node's value changed (sliders, text inputs, etc.).
68 Change,
69 /// The caret or selection anchor position changed in a text field.
70 CursorChange,
71 /// A dragged payload was dropped onto this node.
72 Drop,
73 /// A drag entered this node's hit area (for drop targets).
74 DragEnter,
75 /// A drag left this node's hit area (for drop targets).
76 DragLeave,
77 /// Right-click or secondary mouse button.
78 SecondaryClick,
79}
80
81impl Default for ActionTrigger {
82 fn default() -> Self {
83 ActionTrigger::Default
84 }
85}
86
87/// A single action binding: a trigger, an action ID, and optional payload.
88///
89/// When the event system detects the input described by `trigger`, it dispatches
90/// the action identified by `action_id`. If the action carries data (e.g., drag
91/// coordinates), `payload_data` holds the serialized payload.
92///
93/// # Example
94///
95/// ```rust
96/// use fission_ir::semantics::{ActionEntry, ActionTrigger};
97///
98/// let entry = ActionEntry {
99/// trigger: ActionTrigger::Default,
100/// action_id: 42,
101/// payload_data: None,
102/// };
103/// ```
104#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
105pub struct ActionEntry {
106 /// Which input gesture triggers this action.
107 pub trigger: ActionTrigger,
108 /// The raw 128-bit action ID dispatched to the widget's action handler.
109 pub action_id: u128,
110 /// Optional serialized payload. `None` for actions with no data.
111 pub payload_data: Option<Vec<u8>>,
112}
113
114/// Accessibility and interaction metadata for a node.
115///
116/// `Semantics` is the IR's way of describing *what a node means* rather than how it
117/// looks or where it is positioned. It is consumed by:
118///
119/// * Assistive technology (screen readers, switch control) via the accessibility tree.
120/// * The event/focus system, which uses `focusable`, `actions`, and `disabled` to
121/// route input.
122/// * The drag-and-drop subsystem, which reads `draggable` and `drag_payload`.
123///
124/// Most fields default to "inert" values (see [`Default`] impl), so you only need to
125/// set the fields that matter for a given widget.
126///
127/// # Example
128///
129/// ```rust
130/// use fission_ir::Semantics;
131/// use fission_ir::semantics::Role;
132///
133/// let sem = Semantics {
134/// role: Role::Button,
135/// label: Some("Submit".into()),
136/// focusable: true,
137/// ..Semantics::default()
138/// };
139/// ```
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141pub struct Semantics {
142 /// The accessibility role. Defaults to [`Role::Generic`].
143 pub role: Role,
144 /// A human-readable label for assistive technology (e.g., "Close" for a button).
145 pub label: Option<String>,
146 /// The current value as a string (e.g., the text in an input field).
147 pub value: Option<String>,
148 /// The set of actions this node responds to.
149 pub actions: ActionSet,
150 /// Whether this node can receive keyboard focus.
151 pub focusable: bool,
152 /// Whether this text input supports multiple lines.
153 pub multiline: bool,
154 /// Whether the value should be obscured (password fields).
155 pub masked: bool,
156 /// An optional input mask that restricts which characters are accepted.
157 pub input_mask: Option<InputMask>,
158 /// The byte range of IME pre-edit (composition) text, if any.
159 pub ime_preedit_range: Option<(usize, usize)>,
160 /// For checkboxes and switches: `Some(true)` = checked, `Some(false)` = unchecked,
161 /// `None` = not a toggle.
162 pub checked: Option<bool>,
163 /// Whether the node is disabled (grayed out, non-interactive).
164 pub disabled: bool,
165 /// Whether this node can be dragged.
166 pub draggable: bool,
167 /// Whether the node scrolls horizontally.
168 pub scrollable_x: bool,
169 /// Whether the node scrolls vertically.
170 pub scrollable_y: bool,
171 /// Minimum value for range inputs (sliders).
172 pub min_value: Option<f32>,
173 /// Maximum value for range inputs (sliders).
174 pub max_value: Option<f32>,
175 /// Current numeric value for range inputs (sliders).
176 pub current_value: Option<f32>,
177 /// When `true`, this node creates a new focus scope (like a dialog or panel).
178 pub is_focus_scope: bool,
179 /// When `true`, Tab traversal does not leave this subtree.
180 pub is_focus_barrier: bool,
181 /// Serialized payload attached to a drag operation.
182 pub drag_payload: Option<Vec<u8>>,
183 /// An identifier for hero/shared-element transitions.
184 pub hero_tag: Option<String>,
185 /// Explicit tab order index. Lower values receive focus first. `None` means
186 /// the node follows document order.
187 pub focus_index: Option<i32>,
188 /// When true, Tab key inserts spaces instead of moving focus.
189 pub capture_tab: bool,
190 /// When true, Enter copies leading whitespace from the current line.
191 pub auto_indent: bool,
192}
193
194impl std::hash::Hash for Semantics {
195 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
196 self.role.hash(state);
197 self.label.hash(state);
198 self.value.hash(state);
199 self.actions.hash(state);
200 self.focusable.hash(state);
201 self.multiline.hash(state);
202 self.masked.hash(state);
203 self.input_mask.hash(state);
204 self.ime_preedit_range.hash(state);
205 self.checked.hash(state);
206 self.disabled.hash(state);
207 self.draggable.hash(state);
208 self.scrollable_x.hash(state);
209 self.scrollable_y.hash(state);
210 self.min_value.map(|f| f.to_bits()).hash(state);
211 self.max_value.map(|f| f.to_bits()).hash(state);
212 self.current_value.map(|f| f.to_bits()).hash(state);
213 self.is_focus_scope.hash(state);
214 self.is_focus_barrier.hash(state);
215 self.drag_payload.hash(state);
216 self.hero_tag.hash(state);
217 self.focus_index.hash(state);
218 self.capture_tab.hash(state);
219 self.auto_indent.hash(state);
220 }
221}
222
223impl Default for Semantics {
224 fn default() -> Self {
225 Self {
226 role: Role::Generic,
227 label: None,
228 value: None,
229 actions: ActionSet::default(),
230 focusable: false,
231 multiline: false,
232 masked: false,
233 input_mask: None,
234 ime_preedit_range: None,
235 checked: None,
236 disabled: false,
237 draggable: false,
238 scrollable_x: false,
239 scrollable_y: false,
240 min_value: None,
241 max_value: None,
242 current_value: None,
243 is_focus_scope: false,
244 is_focus_barrier: false,
245 drag_payload: None,
246 hero_tag: None,
247 focus_index: None,
248 capture_tab: false,
249 auto_indent: false,
250 }
251 }
252}
253
254/// A collection of [`ActionEntry`]s attached to a semantics node.
255///
256/// `ActionSet` is a simple wrapper around a `Vec<ActionEntry>`. It exists as a
257/// named type so that serialization and hashing are straightforward.
258#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
259pub struct ActionSet {
260 /// The action entries. Order does not matter for dispatch; the event system
261 /// matches on [`ActionTrigger`].
262 pub entries: Vec<ActionEntry>,
263}
264
265/// Restricts which characters a text input accepts.
266///
267/// Apply an `InputMask` to a [`Semantics`] node to filter keystrokes before they
268/// reach the text editing logic.
269#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
270pub enum InputMask {
271 /// Accept only ASCII digits (`0`-`9`).
272 Numeric,
273 /// Accept only ASCII letters and digits (`a`-`z`, `A`-`Z`, `0`-`9`).
274 Alphanumeric,
275}
276
277impl InputMask {
278 /// Returns `true` if `ch` is accepted by this mask.
279 ///
280 /// # Example
281 ///
282 /// ```rust
283 /// use fission_ir::semantics::InputMask;
284 /// assert!(InputMask::Numeric.is_valid_char('5'));
285 /// assert!(!InputMask::Numeric.is_valid_char('a'));
286 /// ```
287 pub fn is_valid_char(&self, ch: char) -> bool {
288 match self {
289 InputMask::Numeric => ch.is_ascii_digit(),
290 InputMask::Alphanumeric => ch.is_ascii_alphanumeric(),
291 }
292 }
293}