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/// How a focusable node responds to pointer focus.
52///
53/// `FocusPolicy` only changes pointer-driven focus assignment. Keyboard focus,
54/// accessibility focus, and semantic activation still work for focusable nodes.
55///
56/// # Example
57///
58/// A toolbar button can run its action without taking focus from an editor:
59///
60/// ```rust
61/// use fission_ir::semantics::{FocusPolicy, Role};
62/// use fission_ir::Semantics;
63///
64/// let semantics = Semantics {
65/// role: Role::Button,
66/// focusable: true,
67/// focus_policy: FocusPolicy::PreserveCurrentOnPointer,
68/// ..Semantics::default()
69/// };
70/// ```
71#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
72pub enum FocusPolicy {
73 /// Pointer-down focuses this node when it is focusable. This is the normal
74 /// behavior for buttons, text inputs, and other controls.
75 #[default]
76 FocusOnPointer,
77 /// Pointer-down keeps the currently focused node focused while still letting
78 /// this node receive pointer state and activation actions.
79 PreserveCurrentOnPointer,
80}
81
82/// What user interaction triggers an action.
83///
84/// Each [`ActionEntry`] pairs an `ActionTrigger` with an action ID so the event
85/// system knows which callback to invoke for a given input gesture.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
87pub enum ActionTrigger {
88 /// Primary activation: tap, click, or Enter key.
89 Default,
90 /// The user began dragging this node.
91 DragStart,
92 /// The drag position changed (fires continuously).
93 DragUpdate,
94 /// The user released the drag.
95 DragEnd,
96 /// The pointer entered the node's hit area.
97 HoverEnter,
98 /// The pointer left the node's hit area.
99 HoverExit,
100 /// A semantic cursor request applied while the pointer hovers this node.
101 ///
102 /// This is metadata, not a dispatched reducer action.
103 HoverCursor,
104 /// The node received keyboard focus.
105 Focus,
106 /// The node lost keyboard focus.
107 Blur,
108 /// A pointer-down happened outside the active text field.
109 TapOutside,
110 /// The node's value changed (for example, a slider moved).
111 Change,
112 /// Reserved legacy numeric text-change trigger.
113 ///
114 /// Fission 0.11 `TextInput` never emits this trigger and shells must not
115 /// interpret it. It remains in place to preserve serialized IR enum
116 /// discriminants for the variants that follow it.
117 #[deprecated(
118 since = "0.11.0",
119 note = "TextInput uses TextChanged and carries live edits in ActionInput"
120 )]
121 NumberChange,
122 /// Text editing was explicitly completed by the current input method.
123 EditingComplete,
124 /// The user submitted a text field.
125 Submit,
126 /// The caret or selection anchor position changed in a text field.
127 CursorChange,
128 /// A dragged payload was dropped onto this node.
129 Drop,
130 /// A drag entered this node's hit area (for drop targets).
131 DragEnter,
132 /// A drag left this node's hit area (for drop targets).
133 DragLeave,
134 /// Right-click or secondary mouse button.
135 SecondaryClick,
136 /// A text field changed.
137 ///
138 /// The bound action payload remains unchanged. The edited value, widget
139 /// identity, caret, and anchor are delivered as runtime action input.
140 TextChanged,
141 /// An interactive viewport began a pan or zoom gesture.
142 ViewportInteractionStart,
143 /// An interactive viewport's camera changed during a gesture.
144 ViewportInteractionUpdate,
145 /// An interactive viewport gesture ended; configured inertia may continue.
146 ViewportInteractionEnd,
147}
148
149#[cfg(test)]
150mod tests {
151 use super::ActionTrigger;
152
153 #[test]
154 fn text_changed_round_trips_through_ir_serialization() {
155 let encoded = serde_json::to_string(&ActionTrigger::TextChanged).unwrap();
156 assert_eq!(encoded, "\"TextChanged\"");
157 let decoded: ActionTrigger = serde_json::from_str(&encoded).unwrap();
158 assert_eq!(decoded, ActionTrigger::TextChanged);
159 }
160
161 #[test]
162 #[allow(deprecated)]
163 fn existing_action_trigger_discriminants_remain_stable() {
164 assert_eq!(ActionTrigger::Change as u8, 10);
165 assert_eq!(ActionTrigger::NumberChange as u8, 11);
166 assert_eq!(ActionTrigger::EditingComplete as u8, 12);
167 assert_eq!(ActionTrigger::SecondaryClick as u8, 18);
168 assert_eq!(ActionTrigger::TextChanged as u8, 19);
169 assert_eq!(ActionTrigger::ViewportInteractionStart as u8, 20);
170 assert_eq!(ActionTrigger::ViewportInteractionUpdate as u8, 21);
171 assert_eq!(ActionTrigger::ViewportInteractionEnd as u8, 22);
172 }
173}
174
175impl Default for ActionTrigger {
176 fn default() -> Self {
177 ActionTrigger::Default
178 }
179}
180
181/// Semantic cursor requests that shells map onto platform cursor icons.
182#[repr(u8)]
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
184pub enum MouseCursor {
185 #[default]
186 Default = 0,
187 Pointer = 1,
188 Text = 2,
189 Crosshair = 3,
190 Move = 4,
191 NotAllowed = 5,
192 Grab = 6,
193 Grabbing = 7,
194 Wait = 8,
195 Help = 9,
196 VerticalText = 10,
197}
198
199impl MouseCursor {
200 pub fn from_repr(value: u128) -> Option<Self> {
201 match value {
202 0 => Some(Self::Default),
203 1 => Some(Self::Pointer),
204 2 => Some(Self::Text),
205 3 => Some(Self::Crosshair),
206 4 => Some(Self::Move),
207 5 => Some(Self::NotAllowed),
208 6 => Some(Self::Grab),
209 7 => Some(Self::Grabbing),
210 8 => Some(Self::Wait),
211 9 => Some(Self::Help),
212 10 => Some(Self::VerticalText),
213 _ => None,
214 }
215 }
216}
217
218/// Preferred software keyboard / input modality for a text field.
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
220pub enum TextInputType {
221 #[default]
222 Text,
223 Multiline,
224 Number,
225 EmailAddress,
226 Url,
227 Phone,
228 Name,
229}
230
231/// Preferred action for the return/submit key on software keyboards.
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
233pub enum TextInputAction {
234 #[default]
235 Done,
236 Go,
237 Search,
238 Send,
239 Next,
240 Previous,
241 Continue,
242 Join,
243 Route,
244 EmergencyCall,
245 Newline,
246}
247
248/// Automatic capitalization strategy for inserted text.
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
250pub enum TextCapitalization {
251 #[default]
252 None,
253 Characters,
254 Words,
255 Sentences,
256}
257
258/// Whether the framework should enforce `max_length` during editing.
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
260pub enum MaxLengthEnforcement {
261 None,
262 #[default]
263 Enforced,
264}
265
266/// Structured formatter primitives applied to inserted text.
267#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
268pub enum InputFormatter {
269 DigitsOnly,
270 AsciiOnly,
271 InternalLowercase,
272 Uppercase,
273 TrimWhitespace,
274 SingleLine,
275}
276
277/// A single action binding: a trigger, an action ID, and optional payload.
278///
279/// When the event system detects the input described by `trigger`, it dispatches
280/// the action identified by `action_id`. If the action carries data (e.g., drag
281/// coordinates), `payload_data` holds the serialized payload.
282///
283/// # Example
284///
285/// ```rust
286/// use fission_ir::semantics::{ActionEntry, ActionTrigger};
287///
288/// let entry = ActionEntry {
289/// trigger: ActionTrigger::Default,
290/// action_id: 42,
291/// payload_data: None,
292/// };
293/// ```
294#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
295pub struct ActionEntry {
296 /// Which input gesture triggers this action.
297 pub trigger: ActionTrigger,
298 /// The raw 128-bit action ID dispatched to the widget's action handler.
299 pub action_id: u128,
300 /// Optional serialized payload. `None` for actions with no data.
301 pub payload_data: Option<Vec<u8>>,
302}
303
304/// Canvas-specific semantic target used by the shared gesture controller.
305///
306/// This keeps stable document identity and geometry in backend-neutral IR while
307/// action payloads remain entirely application-defined.
308#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
309pub struct CanvasTarget {
310 pub canvas_id: u128,
311 pub kind: CanvasTargetKind,
312 pub selection_policy: CanvasSelectionPolicy,
313 pub snap_spacing: Option<f32>,
314 pub snap_threshold: f32,
315}
316
317impl std::hash::Hash for CanvasTarget {
318 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
319 self.canvas_id.hash(state);
320 self.kind.hash(state);
321 self.selection_policy.hash(state);
322 self.snap_spacing.map(f32::to_bits).hash(state);
323 self.snap_threshold.to_bits().hash(state);
324 }
325}
326
327/// Declarative selection behavior requested by an infinite canvas.
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
329pub enum CanvasSelectionPolicy {
330 None,
331 Single,
332 Toggle,
333 Marquee,
334}
335
336#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
337pub enum CanvasTargetKind {
338 Node {
339 node_id: u128,
340 bounds: [f32; 4],
341 },
342 ResizeHandle {
343 node_id: u128,
344 handle: u8,
345 bounds: [f32; 4],
346 },
347 Edge {
348 edge_id: u128,
349 points: Vec<[f32; 2]>,
350 cubic: bool,
351 hit_tolerance: f32,
352 },
353 Marquee,
354}
355
356impl std::hash::Hash for CanvasTargetKind {
357 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
358 std::mem::discriminant(self).hash(state);
359 match self {
360 Self::Node { node_id, bounds } => {
361 node_id.hash(state);
362 bounds.iter().for_each(|value| value.to_bits().hash(state));
363 }
364 Self::ResizeHandle {
365 node_id,
366 handle,
367 bounds,
368 } => {
369 node_id.hash(state);
370 handle.hash(state);
371 bounds.iter().for_each(|value| value.to_bits().hash(state));
372 }
373 Self::Edge {
374 edge_id,
375 points,
376 cubic,
377 hit_tolerance,
378 } => {
379 edge_id.hash(state);
380 for point in points {
381 point[0].to_bits().hash(state);
382 point[1].to_bits().hash(state);
383 }
384 cubic.hash(state);
385 hit_tolerance.to_bits().hash(state);
386 }
387 Self::Marquee => {}
388 }
389 }
390}
391
392impl ActionEntry {
393 /// Creates a non-dispatched cursor request consumed by hover handling.
394 pub fn hover_cursor(cursor: MouseCursor) -> Self {
395 Self {
396 trigger: ActionTrigger::HoverCursor,
397 action_id: cursor as u128,
398 payload_data: None,
399 }
400 }
401
402 /// Returns the semantic cursor encoded by this entry, if any.
403 pub fn as_hover_cursor(&self) -> Option<MouseCursor> {
404 (self.trigger == ActionTrigger::HoverCursor)
405 .then(|| MouseCursor::from_repr(self.action_id))
406 .flatten()
407 }
408}
409
410/// Accessibility and interaction metadata for a node.
411///
412/// `Semantics` is the IR's way of describing *what a node means* rather than how it
413/// looks or where it is positioned. It is consumed by:
414///
415/// * Assistive technology (screen readers, switch control) via the accessibility tree.
416/// * The event/focus system, which uses `focusable`, `actions`, and `disabled` to
417/// route input.
418/// * The drag-and-drop subsystem, which reads `draggable` and `drag_payload`.
419///
420/// Most fields default to "inert" values (see [`Default`] impl), so you only need to
421/// set the fields that matter for a given widget.
422///
423/// # Example
424///
425/// ```rust
426/// use fission_ir::Semantics;
427/// use fission_ir::semantics::Role;
428///
429/// let sem = Semantics {
430/// role: Role::Button,
431/// label: Some("Submit".into()),
432/// focusable: true,
433/// ..Semantics::default()
434/// };
435/// ```
436#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
437pub struct Semantics {
438 /// The accessibility role. Defaults to [`Role::Generic`].
439 pub role: Role,
440 /// A human-readable label for assistive technology (e.g., "Close" for a button).
441 pub label: Option<String>,
442 /// Stable semantic identifier for tooling and automation.
443 pub identifier: Option<String>,
444 /// The current value as a string (e.g., the text in an input field).
445 pub value: Option<String>,
446 /// The set of actions this node responds to.
447 pub actions: ActionSet,
448 /// Structured InfiniteCanvas target metadata for contextual gesture input.
449 #[serde(default)]
450 pub canvas_target: Option<CanvasTarget>,
451 /// Optional raw action dispatch scope inherited by descendant actions.
452 #[serde(default)]
453 pub action_scope_id: Option<u128>,
454 /// Whether this node can receive keyboard focus.
455 pub focusable: bool,
456 /// How pointer-down should affect focus for this node.
457 #[serde(default)]
458 pub focus_policy: FocusPolicy,
459 /// Whether this text input supports multiple lines.
460 pub multiline: bool,
461 /// Whether the value should be obscured (password fields).
462 pub masked: bool,
463 /// An optional input mask that restricts which characters are accepted.
464 pub input_mask: Option<InputMask>,
465 /// The byte range of IME pre-edit (composition) text, if any.
466 pub ime_preedit_range: Option<(usize, usize)>,
467 /// The active byte range within [`Semantics::ime_preedit_range`], if the
468 /// platform IME exposes a pre-edit cursor or marked sub-range.
469 #[serde(default)]
470 pub ime_preedit_cursor_range: Option<(usize, usize)>,
471 /// Editable or selectable text selection as byte offsets `(anchor, focus)`.
472 #[serde(default)]
473 pub text_selection: Option<(usize, usize)>,
474 /// Whether this read-only text node supports pointer/keyboard selection.
475 #[serde(default)]
476 pub selectable_text: bool,
477 /// Whether this node can open a framework-managed context menu.
478 #[serde(default)]
479 pub context_menu: bool,
480 /// For checkboxes, radios, and switches: `Some(true)` = checked or selected,
481 /// `Some(false)` = unchecked or unselected, and `None` = no checked state.
482 pub checked: Option<bool>,
483 /// Whether the node is disabled (grayed out, non-interactive).
484 pub disabled: bool,
485 /// Whether the node can be focused and selected but not edited.
486 pub read_only: bool,
487 /// Whether this node should receive focus automatically when mounted.
488 pub autofocus: bool,
489 /// Whether this node can be dragged.
490 pub draggable: bool,
491 /// Whether the node scrolls horizontally.
492 pub scrollable_x: bool,
493 /// Whether the node scrolls vertically.
494 pub scrollable_y: bool,
495 /// Minimum value for range inputs (sliders).
496 pub min_value: Option<f32>,
497 /// Maximum value for range inputs (sliders).
498 pub max_value: Option<f32>,
499 /// Current numeric value for range inputs (sliders).
500 pub current_value: Option<f32>,
501 /// When `true`, this node creates a new focus scope (like a dialog or panel).
502 pub is_focus_scope: bool,
503 /// When `true`, Tab traversal does not leave this subtree.
504 pub is_focus_barrier: bool,
505 /// Serialized payload attached to a drag operation.
506 pub drag_payload: Option<Vec<u8>>,
507 /// An identifier for hero/shared-element transitions.
508 pub hero_tag: Option<String>,
509 /// Explicit tab order index. InternalLower values receive focus first. `None` means
510 /// the node follows document order.
511 pub focus_index: Option<i32>,
512 /// Preferred keyboard/input modality for text entry.
513 pub text_input_type: TextInputType,
514 /// Preferred submit/return key action.
515 pub text_input_action: TextInputAction,
516 /// Automatic capitalization strategy for inserted text.
517 pub text_capitalization: TextCapitalization,
518 /// Maximum number of Unicode scalar values allowed in the field.
519 pub max_length: Option<usize>,
520 /// Whether `max_length` should be enforced during editing.
521 pub max_length_enforcement: MaxLengthEnforcement,
522 /// Structured input formatters applied to inserted text.
523 pub input_formatters: Vec<InputFormatter>,
524 /// Hint to the platform IME whether autocorrect should be enabled.
525 pub autocorrect: bool,
526 /// Hint to the platform IME whether suggestions should be enabled.
527 pub enable_suggestions: bool,
528 /// Hint to the platform IME whether spell checking should be enabled.
529 pub spell_check: bool,
530 /// Hint to the platform IME whether smart dashes should be enabled.
531 pub smart_dashes: bool,
532 /// Hint to the platform IME whether smart quotes should be enabled.
533 pub smart_quotes: bool,
534 /// Platform autofill categories associated with this field.
535 pub autofill_hints: Vec<String>,
536 /// Extra padding to keep around the caret/selection when auto-scrolling `[left, right, top, bottom]`.
537 pub scroll_padding: Option<[f32; 4]>,
538 /// When true, Tab key inserts spaces instead of moving focus.
539 pub capture_tab: bool,
540 /// When true, Enter copies leading whitespace from the current line.
541 pub auto_indent: bool,
542}
543
544impl std::hash::Hash for Semantics {
545 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
546 self.role.hash(state);
547 self.label.hash(state);
548 self.identifier.hash(state);
549 self.value.hash(state);
550 self.actions.hash(state);
551 self.canvas_target.hash(state);
552 self.action_scope_id.hash(state);
553 self.focusable.hash(state);
554 self.focus_policy.hash(state);
555 self.multiline.hash(state);
556 self.masked.hash(state);
557 self.input_mask.hash(state);
558 self.ime_preedit_range.hash(state);
559 self.ime_preedit_cursor_range.hash(state);
560 self.text_selection.hash(state);
561 self.selectable_text.hash(state);
562 self.context_menu.hash(state);
563 self.checked.hash(state);
564 self.disabled.hash(state);
565 self.read_only.hash(state);
566 self.autofocus.hash(state);
567 self.draggable.hash(state);
568 self.scrollable_x.hash(state);
569 self.scrollable_y.hash(state);
570 self.min_value.map(|f| f.to_bits()).hash(state);
571 self.max_value.map(|f| f.to_bits()).hash(state);
572 self.current_value.map(|f| f.to_bits()).hash(state);
573 self.is_focus_scope.hash(state);
574 self.is_focus_barrier.hash(state);
575 self.drag_payload.hash(state);
576 self.hero_tag.hash(state);
577 self.focus_index.hash(state);
578 self.text_input_type.hash(state);
579 self.text_input_action.hash(state);
580 self.text_capitalization.hash(state);
581 self.max_length.hash(state);
582 self.max_length_enforcement.hash(state);
583 self.input_formatters.hash(state);
584 self.autocorrect.hash(state);
585 self.enable_suggestions.hash(state);
586 self.spell_check.hash(state);
587 self.smart_dashes.hash(state);
588 self.smart_quotes.hash(state);
589 self.autofill_hints.hash(state);
590 self.scroll_padding
591 .map(|padding| padding.map(f32::to_bits))
592 .hash(state);
593 self.capture_tab.hash(state);
594 self.auto_indent.hash(state);
595 }
596}
597
598impl Default for Semantics {
599 fn default() -> Self {
600 Self {
601 role: Role::Generic,
602 label: None,
603 identifier: None,
604 value: None,
605 actions: ActionSet::default(),
606 canvas_target: None,
607 action_scope_id: None,
608 focusable: false,
609 focus_policy: FocusPolicy::FocusOnPointer,
610 multiline: false,
611 masked: false,
612 input_mask: None,
613 ime_preedit_range: None,
614 ime_preedit_cursor_range: None,
615 text_selection: None,
616 selectable_text: false,
617 context_menu: false,
618 checked: None,
619 disabled: false,
620 read_only: false,
621 autofocus: false,
622 draggable: false,
623 scrollable_x: false,
624 scrollable_y: false,
625 min_value: None,
626 max_value: None,
627 current_value: None,
628 is_focus_scope: false,
629 is_focus_barrier: false,
630 drag_payload: None,
631 hero_tag: None,
632 focus_index: None,
633 text_input_type: TextInputType::Text,
634 text_input_action: TextInputAction::Done,
635 text_capitalization: TextCapitalization::None,
636 max_length: None,
637 max_length_enforcement: MaxLengthEnforcement::Enforced,
638 input_formatters: Vec::new(),
639 autocorrect: true,
640 enable_suggestions: true,
641 spell_check: true,
642 smart_dashes: true,
643 smart_quotes: true,
644 autofill_hints: Vec::new(),
645 scroll_padding: None,
646 capture_tab: false,
647 auto_indent: false,
648 }
649 }
650}
651
652/// A collection of [`ActionEntry`]s attached to a semantics node.
653///
654/// `ActionSet` is a simple wrapper around a `Vec<ActionEntry>`. It exists as a
655/// named type so that serialization and hashing are straightforward.
656#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
657pub struct ActionSet {
658 /// The action entries. Order does not matter for dispatch; the event system
659 /// matches on [`ActionTrigger`].
660 pub entries: Vec<ActionEntry>,
661}
662
663/// Restricts which characters a text input accepts.
664///
665/// Apply an `InputMask` to a [`Semantics`] node to filter keystrokes before they
666/// reach the text editing logic.
667#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
668pub enum InputMask {
669 /// Accept only ASCII digits (`0`-`9`).
670 Numeric,
671 /// Accept only ASCII letters and digits (`a`-`z`, `A`-`Z`, `0`-`9`).
672 Alphanumeric,
673}
674
675impl InputMask {
676 /// Returns `true` if `ch` is accepted by this mask.
677 ///
678 /// # Example
679 ///
680 /// ```rust
681 /// use fission_ir::semantics::InputMask;
682 /// assert!(InputMask::Numeric.is_valid_char('5'));
683 /// assert!(!InputMask::Numeric.is_valid_char('a'));
684 /// ```
685 pub fn is_valid_char(&self, ch: char) -> bool {
686 match self {
687 InputMask::Numeric => ch.is_ascii_digit(),
688 InputMask::Alphanumeric => ch.is_ascii_alphanumeric(),
689 }
690 }
691}