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 navigational link.
22 Link,
23 /// An actionable item inside a menu.
24 MenuItem,
25 /// A read-only text label.
26 Text,
27 /// An editable text field (single or multi-line).
28 TextInput,
29 /// A raster or vector image.
30 Image,
31 /// A toggle that is either checked or unchecked.
32 Checkbox,
33 /// A one-of-many selectable option in a radio group.
34 Radio,
35 /// A toggle switch (on/off).
36 Switch,
37 /// A modal or non-modal dialog overlay.
38 Dialog,
39 /// A continuous range input (e.g., volume control).
40 Slider,
41 /// A generic form input that does not fit the other roles.
42 Input,
43 /// A scrollable list container.
44 List,
45 /// An individual item inside a [`List`](Role::List).
46 ListItem,
47 /// A node with no specific semantic role. The default.
48 Generic,
49}
50
51/// Where a hyperlink should open its destination.
52///
53/// Shells map the standard variants to the host's native navigation model. HTML
54/// renderers use the corresponding `_self`, `_blank`, `_parent`, and `_top`
55/// targets, while [`Named`](Self::Named) preserves an application-provided
56/// browsing-context name.
57#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
58pub enum LinkTarget {
59 /// Reuse the active browsing context or native application window.
60 #[default]
61 Current,
62 /// Open a new browser tab/window or the closest native equivalent.
63 NewWindow,
64 /// Navigate the parent browsing context.
65 Parent,
66 /// Navigate the top-level browsing context.
67 Top,
68 /// Navigate a named browsing context.
69 Named(String),
70}
71
72impl LinkTarget {
73 /// Returns the HTML `target` value represented by this target.
74 pub fn as_html_target(&self) -> &str {
75 match self {
76 Self::Current => "_self",
77 Self::NewWindow => "_blank",
78 Self::Parent => "_parent",
79 Self::Top => "_top",
80 Self::Named(name) => name,
81 }
82 }
83}
84
85/// Declarative hyperlink metadata understood by every shell.
86///
87/// This lives on semantic nodes rather than on one concrete `Link` widget so
88/// application and third-party widgets can expose genuine navigation without
89/// inheriting Fission's visual link treatment.
90#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
91pub struct Hyperlink {
92 /// Destination URL or logical application route.
93 pub href: String,
94 /// Browsing context in which the destination opens.
95 #[serde(default)]
96 pub target: LinkTarget,
97 /// Optional HTML relationship tokens.
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub rel: Option<String>,
100 /// Optional download filename; presence requests download behavior.
101 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub download: Option<String>,
103}
104
105impl Hyperlink {
106 pub fn new(href: impl Into<String>) -> Self {
107 Self {
108 href: href.into(),
109 target: LinkTarget::Current,
110 rel: None,
111 download: None,
112 }
113 }
114
115 pub fn target(mut self, target: LinkTarget) -> Self {
116 self.target = target;
117 self
118 }
119
120 pub fn rel(mut self, rel: impl Into<String>) -> Self {
121 self.rel = Some(rel.into());
122 self
123 }
124
125 pub fn download(mut self, filename: impl Into<String>) -> Self {
126 self.download = Some(filename.into());
127 self
128 }
129}
130
131/// Action requested from an HTML popover invocation target.
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
133pub enum PopoverAction {
134 /// Toggle the target popover's visibility.
135 #[default]
136 Toggle,
137 /// Show the target popover.
138 Show,
139 /// Hide the target popover.
140 Hide,
141}
142
143impl PopoverAction {
144 pub fn as_html_action(self) -> &'static str {
145 match self {
146 Self::Toggle => "toggle",
147 Self::Show => "show",
148 Self::Hide => "hide",
149 }
150 }
151}
152
153/// Declarative relationship between an invoker and an HTML popover.
154#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
155pub struct PopoverTarget {
156 /// DOM id of the popover controlled by this invoker.
157 pub id: String,
158 /// Visibility operation requested on activation.
159 #[serde(default)]
160 pub action: PopoverAction,
161}
162
163/// How a focusable node responds to pointer focus.
164///
165/// `FocusPolicy` only changes pointer-driven focus assignment. Keyboard focus,
166/// accessibility focus, and semantic activation still work for focusable nodes.
167///
168/// # Example
169///
170/// A toolbar button can run its action without taking focus from an editor:
171///
172/// ```rust
173/// use fission_ir::semantics::{FocusPolicy, Role};
174/// use fission_ir::Semantics;
175///
176/// let semantics = Semantics {
177/// role: Role::Button,
178/// focusable: true,
179/// focus_policy: FocusPolicy::PreserveCurrentOnPointer,
180/// ..Semantics::default()
181/// };
182/// ```
183#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
184pub enum FocusPolicy {
185 /// Pointer-down focuses this node when it is focusable. This is the normal
186 /// behavior for buttons, text inputs, and other controls.
187 #[default]
188 FocusOnPointer,
189 /// Pointer-down keeps the currently focused node focused while still letting
190 /// this node receive pointer state and activation actions.
191 PreserveCurrentOnPointer,
192}
193
194/// What user interaction triggers an action.
195///
196/// Each [`ActionEntry`] pairs an `ActionTrigger` with an action ID so the event
197/// system knows which callback to invoke for a given input gesture.
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
199pub enum ActionTrigger {
200 /// Primary activation: tap, click, or Enter key.
201 Default,
202 /// The user began dragging this node.
203 DragStart,
204 /// The drag position changed (fires continuously).
205 DragUpdate,
206 /// The user released the drag.
207 DragEnd,
208 /// The pointer entered the node's hit area.
209 HoverEnter,
210 /// The pointer left the node's hit area.
211 HoverExit,
212 /// A semantic cursor request applied while the pointer hovers this node.
213 ///
214 /// This is metadata, not a dispatched reducer action.
215 HoverCursor,
216 /// The node received keyboard focus.
217 Focus,
218 /// The node lost keyboard focus.
219 Blur,
220 /// A pointer-down happened outside the active text field.
221 TapOutside,
222 /// The node's value changed (for example, a slider moved).
223 Change,
224 /// Reserved legacy numeric text-change trigger.
225 ///
226 /// Fission 0.11 `TextInput` never emits this trigger and shells must not
227 /// interpret it. It remains in place to preserve serialized IR enum
228 /// discriminants for the variants that follow it.
229 #[deprecated(
230 since = "0.11.0",
231 note = "TextInput uses TextChanged and carries live edits in ActionInput"
232 )]
233 NumberChange,
234 /// Text editing was explicitly completed by the current input method.
235 EditingComplete,
236 /// The user submitted a text field.
237 Submit,
238 /// The caret or selection anchor position changed in a text field.
239 CursorChange,
240 /// A dragged payload was dropped onto this node.
241 Drop,
242 /// A drag entered this node's hit area (for drop targets).
243 DragEnter,
244 /// A drag left this node's hit area (for drop targets).
245 DragLeave,
246 /// Right-click or secondary mouse button.
247 SecondaryClick,
248 /// A text field changed.
249 ///
250 /// The bound action payload remains unchanged. The edited value, widget
251 /// identity, caret, and anchor are delivered as runtime action input.
252 TextChanged,
253 /// An interactive viewport began a pan or zoom gesture.
254 ViewportInteractionStart,
255 /// An interactive viewport's camera changed during a gesture.
256 ViewportInteractionUpdate,
257 /// An interactive viewport gesture ended; configured inertia may continue.
258 ViewportInteractionEnd,
259 /// A text field's validation state was requested or changed.
260 Validation,
261}
262
263#[cfg(test)]
264mod tests {
265 use super::ActionTrigger;
266
267 #[test]
268 fn text_changed_round_trips_through_ir_serialization() {
269 let encoded = serde_json::to_string(&ActionTrigger::TextChanged).unwrap();
270 assert_eq!(encoded, "\"TextChanged\"");
271 let decoded: ActionTrigger = serde_json::from_str(&encoded).unwrap();
272 assert_eq!(decoded, ActionTrigger::TextChanged);
273 }
274
275 #[test]
276 #[allow(deprecated)]
277 fn existing_action_trigger_discriminants_remain_stable() {
278 assert_eq!(ActionTrigger::Change as u8, 10);
279 assert_eq!(ActionTrigger::NumberChange as u8, 11);
280 assert_eq!(ActionTrigger::EditingComplete as u8, 12);
281 assert_eq!(ActionTrigger::SecondaryClick as u8, 18);
282 assert_eq!(ActionTrigger::TextChanged as u8, 19);
283 assert_eq!(ActionTrigger::ViewportInteractionStart as u8, 20);
284 assert_eq!(ActionTrigger::ViewportInteractionUpdate as u8, 21);
285 assert_eq!(ActionTrigger::ViewportInteractionEnd as u8, 22);
286 }
287}
288
289impl Default for ActionTrigger {
290 fn default() -> Self {
291 ActionTrigger::Default
292 }
293}
294
295/// Semantic cursor requests that shells map onto platform cursor icons.
296#[repr(u8)]
297#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
298pub enum MouseCursor {
299 #[default]
300 Default = 0,
301 Pointer = 1,
302 Text = 2,
303 Crosshair = 3,
304 Move = 4,
305 NotAllowed = 5,
306 Grab = 6,
307 Grabbing = 7,
308 Wait = 8,
309 Help = 9,
310 VerticalText = 10,
311}
312
313impl MouseCursor {
314 pub fn from_repr(value: u128) -> Option<Self> {
315 match value {
316 0 => Some(Self::Default),
317 1 => Some(Self::Pointer),
318 2 => Some(Self::Text),
319 3 => Some(Self::Crosshair),
320 4 => Some(Self::Move),
321 5 => Some(Self::NotAllowed),
322 6 => Some(Self::Grab),
323 7 => Some(Self::Grabbing),
324 8 => Some(Self::Wait),
325 9 => Some(Self::Help),
326 10 => Some(Self::VerticalText),
327 _ => None,
328 }
329 }
330}
331
332/// Preferred software keyboard / input modality for a text field.
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
334pub enum TextInputType {
335 #[default]
336 Text,
337 Multiline,
338 Number,
339 EmailAddress,
340 Url,
341 Phone,
342 Name,
343}
344
345/// Editable multiline wrapping and submission behavior.
346#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
347pub enum TextWrapMode {
348 /// Wrap visually without inserting line breaks into submitted text.
349 #[default]
350 Soft,
351 /// Wrap visually and allow HTML textarea targets to submit hard line breaks.
352 Hard,
353 /// Do not wrap; overflow scrolls horizontally.
354 NoWrap,
355}
356
357/// Preferred action for the return/submit key on software keyboards.
358#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
359pub enum TextInputAction {
360 #[default]
361 Done,
362 Go,
363 Search,
364 Send,
365 Next,
366 Previous,
367 Continue,
368 Join,
369 Route,
370 EmergencyCall,
371 Newline,
372}
373
374/// Automatic capitalization strategy for inserted text.
375#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
376pub enum TextCapitalization {
377 #[default]
378 None,
379 Characters,
380 Words,
381 Sentences,
382}
383
384/// Whether the framework should enforce `max_length` during editing.
385#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
386pub enum MaxLengthEnforcement {
387 None,
388 Enforced,
389 /// Allow the active composing value to exceed the limit and enforce it
390 /// once the input method commits.
391 #[default]
392 AfterComposition,
393}
394
395/// Structured formatter primitives applied to inserted text.
396#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
397pub enum InputFormatter {
398 DigitsOnly,
399 AsciiOnly,
400 InternalLowercase,
401 Uppercase,
402 TrimWhitespace,
403 SingleLine,
404}
405
406/// Marks a semantic subtree as one coordinated read-only text selection region.
407#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
408pub struct SelectionRegionSemantics {
409 /// Excluded regions prevent an ancestor region from selecting this subtree.
410 pub excluded: bool,
411 /// Text inserted between selectable descendants when copying or exposing
412 /// the region as one accessibility value.
413 pub separator: String,
414}
415
416/// Declarative validity of an editable field.
417#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
418pub enum TextFieldValidationState {
419 #[default]
420 Unvalidated,
421 Valid,
422 Invalid,
423}
424
425/// A single action binding: a trigger, an action ID, and optional payload.
426///
427/// When the event system detects the input described by `trigger`, it dispatches
428/// the action identified by `action_id`. If the action carries data (e.g., drag
429/// coordinates), `payload_data` holds the serialized payload.
430///
431/// # Example
432///
433/// ```rust
434/// use fission_ir::semantics::{ActionEntry, ActionTrigger};
435///
436/// let entry = ActionEntry {
437/// trigger: ActionTrigger::Default,
438/// action_id: 42,
439/// payload_data: None,
440/// };
441/// ```
442#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
443pub struct ActionEntry {
444 /// Which input gesture triggers this action.
445 pub trigger: ActionTrigger,
446 /// The raw 128-bit action ID dispatched to the widget's action handler.
447 pub action_id: u128,
448 /// Optional serialized payload. `None` for actions with no data.
449 pub payload_data: Option<Vec<u8>>,
450}
451
452/// Canvas-specific semantic target used by the shared gesture controller.
453///
454/// This keeps stable document identity and geometry in backend-neutral IR while
455/// action payloads remain entirely application-defined.
456#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
457pub struct CanvasTarget {
458 pub canvas_id: u128,
459 pub kind: CanvasTargetKind,
460 pub selection_policy: CanvasSelectionPolicy,
461 pub snap_spacing: Option<f32>,
462 pub snap_threshold: f32,
463}
464
465impl std::hash::Hash for CanvasTarget {
466 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
467 self.canvas_id.hash(state);
468 self.kind.hash(state);
469 self.selection_policy.hash(state);
470 self.snap_spacing.map(f32::to_bits).hash(state);
471 self.snap_threshold.to_bits().hash(state);
472 }
473}
474
475/// Declarative selection behavior requested by an infinite canvas.
476#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
477pub enum CanvasSelectionPolicy {
478 None,
479 Single,
480 Toggle,
481 Marquee,
482}
483
484#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
485pub enum CanvasTargetKind {
486 Node {
487 node_id: u128,
488 bounds: [f32; 4],
489 },
490 ResizeHandle {
491 node_id: u128,
492 handle: u8,
493 bounds: [f32; 4],
494 },
495 Edge {
496 edge_id: u128,
497 points: Vec<[f32; 2]>,
498 cubic: bool,
499 hit_tolerance: f32,
500 },
501 Marquee,
502}
503
504impl std::hash::Hash for CanvasTargetKind {
505 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
506 std::mem::discriminant(self).hash(state);
507 match self {
508 Self::Node { node_id, bounds } => {
509 node_id.hash(state);
510 bounds.iter().for_each(|value| value.to_bits().hash(state));
511 }
512 Self::ResizeHandle {
513 node_id,
514 handle,
515 bounds,
516 } => {
517 node_id.hash(state);
518 handle.hash(state);
519 bounds.iter().for_each(|value| value.to_bits().hash(state));
520 }
521 Self::Edge {
522 edge_id,
523 points,
524 cubic,
525 hit_tolerance,
526 } => {
527 edge_id.hash(state);
528 for point in points {
529 point[0].to_bits().hash(state);
530 point[1].to_bits().hash(state);
531 }
532 cubic.hash(state);
533 hit_tolerance.to_bits().hash(state);
534 }
535 Self::Marquee => {}
536 }
537 }
538}
539
540impl ActionEntry {
541 /// Creates a non-dispatched cursor request consumed by hover handling.
542 pub fn hover_cursor(cursor: MouseCursor) -> Self {
543 Self {
544 trigger: ActionTrigger::HoverCursor,
545 action_id: cursor as u128,
546 payload_data: None,
547 }
548 }
549
550 /// Returns the semantic cursor encoded by this entry, if any.
551 pub fn as_hover_cursor(&self) -> Option<MouseCursor> {
552 (self.trigger == ActionTrigger::HoverCursor)
553 .then(|| MouseCursor::from_repr(self.action_id))
554 .flatten()
555 }
556}
557
558/// Accessibility and interaction metadata for a node.
559///
560/// `Semantics` is the IR's way of describing *what a node means* rather than how it
561/// looks or where it is positioned. It is consumed by:
562///
563/// * Assistive technology (screen readers, switch control) via the accessibility tree.
564/// * The event/focus system, which uses `focusable`, `actions`, and `disabled` to
565/// route input.
566/// * The drag-and-drop subsystem, which reads `draggable` and `drag_payload`.
567///
568/// Most fields default to "inert" values (see [`Default`] impl), so you only need to
569/// set the fields that matter for a given widget.
570///
571/// # Example
572///
573/// ```rust
574/// use fission_ir::Semantics;
575/// use fission_ir::semantics::Role;
576///
577/// let sem = Semantics {
578/// role: Role::Button,
579/// label: Some("Submit".into()),
580/// focusable: true,
581/// ..Semantics::default()
582/// };
583/// ```
584#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
585pub struct Semantics {
586 /// The accessibility role. Defaults to [`Role::Generic`].
587 pub role: Role,
588 /// A human-readable label for assistive technology (e.g., "Close" for a button).
589 pub label: Option<String>,
590 /// Stable semantic identifier for tooling and automation.
591 pub identifier: Option<String>,
592 /// The current value as a string (e.g., the text in an input field).
593 pub value: Option<String>,
594 /// Optional hyperlink destination for this semantic region.
595 #[serde(default, skip_serializing_if = "Option::is_none")]
596 pub hyperlink: Option<Hyperlink>,
597 /// Optional HTML popover invocation metadata.
598 #[serde(default, skip_serializing_if = "Option::is_none")]
599 pub popover_target: Option<PopoverTarget>,
600 /// The set of actions this node responds to.
601 pub actions: ActionSet,
602 /// Structured InfiniteCanvas target metadata for contextual gesture input.
603 #[serde(default)]
604 pub canvas_target: Option<CanvasTarget>,
605 /// Optional raw action dispatch scope inherited by descendant actions.
606 #[serde(default)]
607 pub action_scope_id: Option<u128>,
608 /// Whether this node can receive keyboard focus.
609 pub focusable: bool,
610 /// How pointer-down should affect focus for this node.
611 #[serde(default)]
612 pub focus_policy: FocusPolicy,
613 /// Whether this text input supports multiple lines.
614 pub multiline: bool,
615 /// Editable multiline wrapping and submission behavior.
616 #[serde(default)]
617 pub text_wrap_mode: TextWrapMode,
618 /// Whether the value should be obscured (password fields).
619 pub masked: bool,
620 /// An optional input mask that restricts which characters are accepted.
621 pub input_mask: Option<InputMask>,
622 /// The byte range of IME pre-edit (composition) text, if any.
623 pub ime_preedit_range: Option<(usize, usize)>,
624 /// The active byte range within [`Semantics::ime_preedit_range`], if the
625 /// platform IME exposes a pre-edit cursor or marked sub-range.
626 #[serde(default)]
627 pub ime_preedit_cursor_range: Option<(usize, usize)>,
628 /// Editable or selectable text selection as byte offsets `(anchor, focus)`.
629 #[serde(default)]
630 pub text_selection: Option<(usize, usize)>,
631 /// Whether this read-only text node supports pointer/keyboard selection.
632 #[serde(default)]
633 pub selectable_text: bool,
634 /// Coordinated selection metadata for this semantic subtree.
635 #[serde(default, skip_serializing_if = "Option::is_none")]
636 pub selection_region: Option<SelectionRegionSemantics>,
637 /// Whether this node can open a framework-managed context menu.
638 #[serde(default)]
639 pub context_menu: bool,
640 /// For checkboxes, radios, and switches: `Some(true)` = checked or selected,
641 /// `Some(false)` = unchecked or unselected, and `None` = no checked state.
642 pub checked: Option<bool>,
643 /// Whether the node is disabled (grayed out, non-interactive).
644 pub disabled: bool,
645 /// Whether the node can be focused and selected but not edited.
646 pub read_only: bool,
647 /// Whether this node should receive focus automatically when mounted.
648 pub autofocus: bool,
649 /// Whether this node can be dragged.
650 pub draggable: bool,
651 /// Whether the node scrolls horizontally.
652 pub scrollable_x: bool,
653 /// Whether the node scrolls vertically.
654 pub scrollable_y: bool,
655 /// Minimum value for range inputs (sliders).
656 pub min_value: Option<f32>,
657 /// Maximum value for range inputs (sliders).
658 pub max_value: Option<f32>,
659 /// Current numeric value for range inputs (sliders).
660 pub current_value: Option<f32>,
661 /// When `true`, this node creates a new focus scope (like a dialog or panel).
662 pub is_focus_scope: bool,
663 /// When `true`, Tab traversal does not leave this subtree.
664 pub is_focus_barrier: bool,
665 /// Serialized payload attached to a drag operation.
666 pub drag_payload: Option<Vec<u8>>,
667 /// An identifier for hero/shared-element transitions.
668 pub hero_tag: Option<String>,
669 /// Explicit tab order index. InternalLower values receive focus first. `None` means
670 /// the node follows document order.
671 pub focus_index: Option<i32>,
672 /// Preferred keyboard/input modality for text entry.
673 pub text_input_type: TextInputType,
674 /// Preferred submit/return key action.
675 pub text_input_action: TextInputAction,
676 /// Automatic capitalization strategy for inserted text.
677 pub text_capitalization: TextCapitalization,
678 /// Maximum number of user-perceived grapheme clusters allowed in the field.
679 pub max_length: Option<usize>,
680 /// Whether `max_length` should be enforced during editing.
681 pub max_length_enforcement: MaxLengthEnforcement,
682 /// Structured input formatters applied to inserted text.
683 pub input_formatters: Vec<InputFormatter>,
684 /// Name submitted by semantic form targets.
685 #[serde(default)]
686 pub text_field_name: Option<String>,
687 /// Logical form membership for coordinated validation/submission.
688 #[serde(default)]
689 pub text_form_id: Option<String>,
690 /// Autofill session/group shared by related fields.
691 #[serde(default)]
692 pub autofill_group: Option<String>,
693 /// Whether a non-empty value is required.
694 #[serde(default)]
695 pub required: bool,
696 /// Minimum number of user-perceived graphemes.
697 #[serde(default)]
698 pub min_length: Option<usize>,
699 /// Pattern constraint for targets with pattern-validation support.
700 #[serde(default)]
701 pub validation_pattern: Option<String>,
702 /// Application-authoritative field validity.
703 #[serde(default)]
704 pub validation_state: TextFieldValidationState,
705 /// Accessible validation message, independent of visual decoration.
706 #[serde(default)]
707 pub validation_message: Option<String>,
708 /// Hint to the platform IME whether autocorrect should be enabled.
709 pub autocorrect: bool,
710 /// Hint to the platform IME whether suggestions should be enabled.
711 pub enable_suggestions: bool,
712 /// Hint to the platform IME whether spell checking should be enabled.
713 pub spell_check: bool,
714 /// Hint to the platform IME whether smart dashes should be enabled.
715 pub smart_dashes: bool,
716 /// Hint to the platform IME whether smart quotes should be enabled.
717 pub smart_quotes: bool,
718 /// Platform autofill categories associated with this field.
719 pub autofill_hints: Vec<String>,
720 /// Extra padding to keep around the caret/selection when auto-scrolling `[left, right, top, bottom]`.
721 pub scroll_padding: Option<[f32; 4]>,
722 /// When true, Tab key inserts spaces instead of moving focus.
723 pub capture_tab: bool,
724 /// When true, Enter copies leading whitespace from the current line.
725 pub auto_indent: bool,
726}
727
728impl std::hash::Hash for Semantics {
729 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
730 self.role.hash(state);
731 self.label.hash(state);
732 self.identifier.hash(state);
733 self.value.hash(state);
734 self.hyperlink.hash(state);
735 self.popover_target.hash(state);
736 self.actions.hash(state);
737 self.canvas_target.hash(state);
738 self.action_scope_id.hash(state);
739 self.focusable.hash(state);
740 self.focus_policy.hash(state);
741 self.multiline.hash(state);
742 self.text_wrap_mode.hash(state);
743 self.masked.hash(state);
744 self.input_mask.hash(state);
745 self.ime_preedit_range.hash(state);
746 self.ime_preedit_cursor_range.hash(state);
747 self.text_selection.hash(state);
748 self.selectable_text.hash(state);
749 self.selection_region.hash(state);
750 self.context_menu.hash(state);
751 self.checked.hash(state);
752 self.disabled.hash(state);
753 self.read_only.hash(state);
754 self.autofocus.hash(state);
755 self.draggable.hash(state);
756 self.scrollable_x.hash(state);
757 self.scrollable_y.hash(state);
758 self.min_value.map(|f| f.to_bits()).hash(state);
759 self.max_value.map(|f| f.to_bits()).hash(state);
760 self.current_value.map(|f| f.to_bits()).hash(state);
761 self.is_focus_scope.hash(state);
762 self.is_focus_barrier.hash(state);
763 self.drag_payload.hash(state);
764 self.hero_tag.hash(state);
765 self.focus_index.hash(state);
766 self.text_input_type.hash(state);
767 self.text_input_action.hash(state);
768 self.text_capitalization.hash(state);
769 self.max_length.hash(state);
770 self.max_length_enforcement.hash(state);
771 self.input_formatters.hash(state);
772 self.text_field_name.hash(state);
773 self.text_form_id.hash(state);
774 self.autofill_group.hash(state);
775 self.required.hash(state);
776 self.min_length.hash(state);
777 self.validation_pattern.hash(state);
778 self.validation_state.hash(state);
779 self.validation_message.hash(state);
780 self.autocorrect.hash(state);
781 self.enable_suggestions.hash(state);
782 self.spell_check.hash(state);
783 self.smart_dashes.hash(state);
784 self.smart_quotes.hash(state);
785 self.autofill_hints.hash(state);
786 self.scroll_padding
787 .map(|padding| padding.map(f32::to_bits))
788 .hash(state);
789 self.capture_tab.hash(state);
790 self.auto_indent.hash(state);
791 }
792}
793
794impl Default for Semantics {
795 fn default() -> Self {
796 Self {
797 role: Role::Generic,
798 label: None,
799 identifier: None,
800 value: None,
801 hyperlink: None,
802 popover_target: None,
803 actions: ActionSet::default(),
804 canvas_target: None,
805 action_scope_id: None,
806 focusable: false,
807 focus_policy: FocusPolicy::FocusOnPointer,
808 multiline: false,
809 text_wrap_mode: TextWrapMode::Soft,
810 masked: false,
811 input_mask: None,
812 ime_preedit_range: None,
813 ime_preedit_cursor_range: None,
814 text_selection: None,
815 selectable_text: false,
816 selection_region: None,
817 context_menu: false,
818 checked: None,
819 disabled: false,
820 read_only: false,
821 autofocus: false,
822 draggable: false,
823 scrollable_x: false,
824 scrollable_y: false,
825 min_value: None,
826 max_value: None,
827 current_value: None,
828 is_focus_scope: false,
829 is_focus_barrier: false,
830 drag_payload: None,
831 hero_tag: None,
832 focus_index: None,
833 text_input_type: TextInputType::Text,
834 text_input_action: TextInputAction::Done,
835 text_capitalization: TextCapitalization::None,
836 max_length: None,
837 max_length_enforcement: MaxLengthEnforcement::AfterComposition,
838 input_formatters: Vec::new(),
839 text_field_name: None,
840 text_form_id: None,
841 autofill_group: None,
842 required: false,
843 min_length: None,
844 validation_pattern: None,
845 validation_state: TextFieldValidationState::Unvalidated,
846 validation_message: None,
847 autocorrect: true,
848 enable_suggestions: true,
849 spell_check: true,
850 smart_dashes: true,
851 smart_quotes: true,
852 autofill_hints: Vec::new(),
853 scroll_padding: None,
854 capture_tab: false,
855 auto_indent: false,
856 }
857 }
858}
859
860/// A collection of [`ActionEntry`]s attached to a semantics node.
861///
862/// `ActionSet` is a simple wrapper around a `Vec<ActionEntry>`. It exists as a
863/// named type so that serialization and hashing are straightforward.
864#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
865pub struct ActionSet {
866 /// The action entries. Order does not matter for dispatch; the event system
867 /// matches on [`ActionTrigger`].
868 pub entries: Vec<ActionEntry>,
869}
870
871/// Restricts which characters a text input accepts.
872///
873/// Apply an `InputMask` to a [`Semantics`] node to filter keystrokes before they
874/// reach the text editing logic.
875#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
876pub enum InputMask {
877 /// Accept only ASCII digits (`0`-`9`).
878 Numeric,
879 /// Accept only ASCII letters and digits (`a`-`z`, `A`-`Z`, `0`-`9`).
880 Alphanumeric,
881}
882
883impl InputMask {
884 /// Returns `true` if `ch` is accepted by this mask.
885 ///
886 /// # Example
887 ///
888 /// ```rust
889 /// use fission_ir::semantics::InputMask;
890 /// assert!(InputMask::Numeric.is_valid_char('5'));
891 /// assert!(!InputMask::Numeric.is_valid_char('a'));
892 /// ```
893 pub fn is_valid_char(&self, ch: char) -> bool {
894 match self {
895 InputMask::Numeric => ch.is_ascii_digit(),
896 InputMask::Alphanumeric => ch.is_ascii_alphanumeric(),
897 }
898 }
899}