Skip to main content

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}