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    /// A semantic cursor request applied while the pointer hovers this node.
64    ///
65    /// This is metadata, not a dispatched reducer action.
66    HoverCursor,
67    /// The node received keyboard focus.
68    Focus,
69    /// The node lost keyboard focus.
70    Blur,
71    /// A pointer-down happened outside the active text field.
72    TapOutside,
73    /// The node's value changed (sliders, text inputs, etc.).
74    Change,
75    /// Text editing was explicitly completed by the current input method.
76    EditingComplete,
77    /// The user submitted a text field.
78    Submit,
79    /// The caret or selection anchor position changed in a text field.
80    CursorChange,
81    /// A dragged payload was dropped onto this node.
82    Drop,
83    /// A drag entered this node's hit area (for drop targets).
84    DragEnter,
85    /// A drag left this node's hit area (for drop targets).
86    DragLeave,
87    /// Right-click or secondary mouse button.
88    SecondaryClick,
89}
90
91impl Default for ActionTrigger {
92    fn default() -> Self {
93        ActionTrigger::Default
94    }
95}
96
97/// Semantic cursor requests that shells map onto platform cursor icons.
98#[repr(u8)]
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
100pub enum MouseCursor {
101    #[default]
102    Default = 0,
103    Pointer = 1,
104    Text = 2,
105    Crosshair = 3,
106    Move = 4,
107    NotAllowed = 5,
108    Grab = 6,
109    Grabbing = 7,
110    Wait = 8,
111    Help = 9,
112    VerticalText = 10,
113}
114
115impl MouseCursor {
116    pub fn from_repr(value: u128) -> Option<Self> {
117        match value {
118            0 => Some(Self::Default),
119            1 => Some(Self::Pointer),
120            2 => Some(Self::Text),
121            3 => Some(Self::Crosshair),
122            4 => Some(Self::Move),
123            5 => Some(Self::NotAllowed),
124            6 => Some(Self::Grab),
125            7 => Some(Self::Grabbing),
126            8 => Some(Self::Wait),
127            9 => Some(Self::Help),
128            10 => Some(Self::VerticalText),
129            _ => None,
130        }
131    }
132}
133
134/// Preferred software keyboard / input modality for a text field.
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
136pub enum TextInputType {
137    #[default]
138    Text,
139    Multiline,
140    Number,
141    EmailAddress,
142    Url,
143    Phone,
144    Name,
145}
146
147/// Preferred action for the return/submit key on software keyboards.
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
149pub enum TextInputAction {
150    #[default]
151    Done,
152    Go,
153    Search,
154    Send,
155    Next,
156    Previous,
157    Continue,
158    Join,
159    Route,
160    EmergencyCall,
161    Newline,
162}
163
164/// Automatic capitalization strategy for inserted text.
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
166pub enum TextCapitalization {
167    #[default]
168    None,
169    Characters,
170    Words,
171    Sentences,
172}
173
174/// Whether the framework should enforce `max_length` during editing.
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
176pub enum MaxLengthEnforcement {
177    None,
178    #[default]
179    Enforced,
180}
181
182/// Structured formatter primitives applied to inserted text.
183#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
184pub enum InputFormatter {
185    DigitsOnly,
186    AsciiOnly,
187    Lowercase,
188    Uppercase,
189    TrimWhitespace,
190    SingleLine,
191}
192
193/// A single action binding: a trigger, an action ID, and optional payload.
194///
195/// When the event system detects the input described by `trigger`, it dispatches
196/// the action identified by `action_id`. If the action carries data (e.g., drag
197/// coordinates), `payload_data` holds the serialized payload.
198///
199/// # Example
200///
201/// ```rust
202/// use fission_ir::semantics::{ActionEntry, ActionTrigger};
203///
204/// let entry = ActionEntry {
205///     trigger: ActionTrigger::Default,
206///     action_id: 42,
207///     payload_data: None,
208/// };
209/// ```
210#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
211pub struct ActionEntry {
212    /// Which input gesture triggers this action.
213    pub trigger: ActionTrigger,
214    /// The raw 128-bit action ID dispatched to the widget's action handler.
215    pub action_id: u128,
216    /// Optional serialized payload. `None` for actions with no data.
217    pub payload_data: Option<Vec<u8>>,
218}
219
220impl ActionEntry {
221    /// Creates a non-dispatched cursor request consumed by hover handling.
222    pub fn hover_cursor(cursor: MouseCursor) -> Self {
223        Self {
224            trigger: ActionTrigger::HoverCursor,
225            action_id: cursor as u128,
226            payload_data: None,
227        }
228    }
229
230    /// Returns the semantic cursor encoded by this entry, if any.
231    pub fn as_hover_cursor(&self) -> Option<MouseCursor> {
232        (self.trigger == ActionTrigger::HoverCursor)
233            .then(|| MouseCursor::from_repr(self.action_id))
234            .flatten()
235    }
236}
237
238/// Accessibility and interaction metadata for a node.
239///
240/// `Semantics` is the IR's way of describing *what a node means* rather than how it
241/// looks or where it is positioned. It is consumed by:
242///
243/// * Assistive technology (screen readers, switch control) via the accessibility tree.
244/// * The event/focus system, which uses `focusable`, `actions`, and `disabled` to
245///   route input.
246/// * The drag-and-drop subsystem, which reads `draggable` and `drag_payload`.
247///
248/// Most fields default to "inert" values (see [`Default`] impl), so you only need to
249/// set the fields that matter for a given widget.
250///
251/// # Example
252///
253/// ```rust
254/// use fission_ir::Semantics;
255/// use fission_ir::semantics::Role;
256///
257/// let sem = Semantics {
258///     role: Role::Button,
259///     label: Some("Submit".into()),
260///     focusable: true,
261///     ..Semantics::default()
262/// };
263/// ```
264#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
265pub struct Semantics {
266    /// The accessibility role. Defaults to [`Role::Generic`].
267    pub role: Role,
268    /// A human-readable label for assistive technology (e.g., "Close" for a button).
269    pub label: Option<String>,
270    /// Stable semantic identifier for tooling and automation.
271    pub identifier: Option<String>,
272    /// The current value as a string (e.g., the text in an input field).
273    pub value: Option<String>,
274    /// The set of actions this node responds to.
275    pub actions: ActionSet,
276    /// Optional raw action dispatch scope inherited by descendant actions.
277    #[serde(default)]
278    pub action_scope_id: Option<u128>,
279    /// Whether this node can receive keyboard focus.
280    pub focusable: bool,
281    /// Whether this text input supports multiple lines.
282    pub multiline: bool,
283    /// Whether the value should be obscured (password fields).
284    pub masked: bool,
285    /// An optional input mask that restricts which characters are accepted.
286    pub input_mask: Option<InputMask>,
287    /// The byte range of IME pre-edit (composition) text, if any.
288    pub ime_preedit_range: Option<(usize, usize)>,
289    /// For checkboxes and switches: `Some(true)` = checked, `Some(false)` = unchecked,
290    /// `None` = not a toggle.
291    pub checked: Option<bool>,
292    /// Whether the node is disabled (grayed out, non-interactive).
293    pub disabled: bool,
294    /// Whether the node can be focused and selected but not edited.
295    pub read_only: bool,
296    /// Whether this node should receive focus automatically when mounted.
297    pub autofocus: bool,
298    /// Whether this node can be dragged.
299    pub draggable: bool,
300    /// Whether the node scrolls horizontally.
301    pub scrollable_x: bool,
302    /// Whether the node scrolls vertically.
303    pub scrollable_y: bool,
304    /// Minimum value for range inputs (sliders).
305    pub min_value: Option<f32>,
306    /// Maximum value for range inputs (sliders).
307    pub max_value: Option<f32>,
308    /// Current numeric value for range inputs (sliders).
309    pub current_value: Option<f32>,
310    /// When `true`, this node creates a new focus scope (like a dialog or panel).
311    pub is_focus_scope: bool,
312    /// When `true`, Tab traversal does not leave this subtree.
313    pub is_focus_barrier: bool,
314    /// Serialized payload attached to a drag operation.
315    pub drag_payload: Option<Vec<u8>>,
316    /// An identifier for hero/shared-element transitions.
317    pub hero_tag: Option<String>,
318    /// Explicit tab order index. Lower values receive focus first. `None` means
319    /// the node follows document order.
320    pub focus_index: Option<i32>,
321    /// Preferred keyboard/input modality for text entry.
322    pub text_input_type: TextInputType,
323    /// Preferred submit/return key action.
324    pub text_input_action: TextInputAction,
325    /// Automatic capitalization strategy for inserted text.
326    pub text_capitalization: TextCapitalization,
327    /// Maximum number of Unicode scalar values allowed in the field.
328    pub max_length: Option<usize>,
329    /// Whether `max_length` should be enforced during editing.
330    pub max_length_enforcement: MaxLengthEnforcement,
331    /// Structured input formatters applied to inserted text.
332    pub input_formatters: Vec<InputFormatter>,
333    /// Hint to the platform IME whether autocorrect should be enabled.
334    pub autocorrect: bool,
335    /// Hint to the platform IME whether suggestions should be enabled.
336    pub enable_suggestions: bool,
337    /// Hint to the platform IME whether spell checking should be enabled.
338    pub spell_check: bool,
339    /// Hint to the platform IME whether smart dashes should be enabled.
340    pub smart_dashes: bool,
341    /// Hint to the platform IME whether smart quotes should be enabled.
342    pub smart_quotes: bool,
343    /// Platform autofill categories associated with this field.
344    pub autofill_hints: Vec<String>,
345    /// Extra padding to keep around the caret/selection when auto-scrolling `[left, right, top, bottom]`.
346    pub scroll_padding: Option<[f32; 4]>,
347    /// When true, Tab key inserts spaces instead of moving focus.
348    pub capture_tab: bool,
349    /// When true, Enter copies leading whitespace from the current line.
350    pub auto_indent: bool,
351}
352
353impl std::hash::Hash for Semantics {
354    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
355        self.role.hash(state);
356        self.label.hash(state);
357        self.identifier.hash(state);
358        self.value.hash(state);
359        self.actions.hash(state);
360        self.action_scope_id.hash(state);
361        self.focusable.hash(state);
362        self.multiline.hash(state);
363        self.masked.hash(state);
364        self.input_mask.hash(state);
365        self.ime_preedit_range.hash(state);
366        self.checked.hash(state);
367        self.disabled.hash(state);
368        self.read_only.hash(state);
369        self.autofocus.hash(state);
370        self.draggable.hash(state);
371        self.scrollable_x.hash(state);
372        self.scrollable_y.hash(state);
373        self.min_value.map(|f| f.to_bits()).hash(state);
374        self.max_value.map(|f| f.to_bits()).hash(state);
375        self.current_value.map(|f| f.to_bits()).hash(state);
376        self.is_focus_scope.hash(state);
377        self.is_focus_barrier.hash(state);
378        self.drag_payload.hash(state);
379        self.hero_tag.hash(state);
380        self.focus_index.hash(state);
381        self.text_input_type.hash(state);
382        self.text_input_action.hash(state);
383        self.text_capitalization.hash(state);
384        self.max_length.hash(state);
385        self.max_length_enforcement.hash(state);
386        self.input_formatters.hash(state);
387        self.autocorrect.hash(state);
388        self.enable_suggestions.hash(state);
389        self.spell_check.hash(state);
390        self.smart_dashes.hash(state);
391        self.smart_quotes.hash(state);
392        self.autofill_hints.hash(state);
393        self.scroll_padding
394            .map(|padding| padding.map(f32::to_bits))
395            .hash(state);
396        self.capture_tab.hash(state);
397        self.auto_indent.hash(state);
398    }
399}
400
401impl Default for Semantics {
402    fn default() -> Self {
403        Self {
404            role: Role::Generic,
405            label: None,
406            identifier: None,
407            value: None,
408            actions: ActionSet::default(),
409            action_scope_id: None,
410            focusable: false,
411            multiline: false,
412            masked: false,
413            input_mask: None,
414            ime_preedit_range: None,
415            checked: None,
416            disabled: false,
417            read_only: false,
418            autofocus: false,
419            draggable: false,
420            scrollable_x: false,
421            scrollable_y: false,
422            min_value: None,
423            max_value: None,
424            current_value: None,
425            is_focus_scope: false,
426            is_focus_barrier: false,
427            drag_payload: None,
428            hero_tag: None,
429            focus_index: None,
430            text_input_type: TextInputType::Text,
431            text_input_action: TextInputAction::Done,
432            text_capitalization: TextCapitalization::None,
433            max_length: None,
434            max_length_enforcement: MaxLengthEnforcement::Enforced,
435            input_formatters: Vec::new(),
436            autocorrect: true,
437            enable_suggestions: true,
438            spell_check: true,
439            smart_dashes: true,
440            smart_quotes: true,
441            autofill_hints: Vec::new(),
442            scroll_padding: None,
443            capture_tab: false,
444            auto_indent: false,
445        }
446    }
447}
448
449/// A collection of [`ActionEntry`]s attached to a semantics node.
450///
451/// `ActionSet` is a simple wrapper around a `Vec<ActionEntry>`. It exists as a
452/// named type so that serialization and hashing are straightforward.
453#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
454pub struct ActionSet {
455    /// The action entries. Order does not matter for dispatch; the event system
456    /// matches on [`ActionTrigger`].
457    pub entries: Vec<ActionEntry>,
458}
459
460/// Restricts which characters a text input accepts.
461///
462/// Apply an `InputMask` to a [`Semantics`] node to filter keystrokes before they
463/// reach the text editing logic.
464#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
465pub enum InputMask {
466    /// Accept only ASCII digits (`0`-`9`).
467    Numeric,
468    /// Accept only ASCII letters and digits (`a`-`z`, `A`-`Z`, `0`-`9`).
469    Alphanumeric,
470}
471
472impl InputMask {
473    /// Returns `true` if `ch` is accepted by this mask.
474    ///
475    /// # Example
476    ///
477    /// ```rust
478    /// use fission_ir::semantics::InputMask;
479    /// assert!(InputMask::Numeric.is_valid_char('5'));
480    /// assert!(!InputMask::Numeric.is_valid_char('a'));
481    /// ```
482    pub fn is_valid_char(&self, ch: char) -> bool {
483        match self {
484            InputMask::Numeric => ch.is_ascii_digit(),
485            InputMask::Alphanumeric => ch.is_ascii_alphanumeric(),
486        }
487    }
488}