1use std::collections::HashSet;
2
3use crate::{AppWindowId, NodeId, Rect};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6#[non_exhaustive]
7pub enum SemanticsRole {
8 Generic,
9 Window,
10 Panel,
11 Group,
12 Region,
17 Toolbar,
18 Heading,
19 Dialog,
20 AlertDialog,
21 Alert,
22 Status,
24 Log,
26 Button,
27 Link,
28 Image,
29 Checkbox,
30 Switch,
31 Slider,
32 SpinButton,
33 ProgressBar,
34 Meter,
35 ScrollBar,
36 Splitter,
37 ComboBox,
38 RadioGroup,
39 RadioButton,
40 TabList,
41 Tab,
42 TabPanel,
43 MenuBar,
44 Menu,
45 MenuItem,
46 MenuItemCheckbox,
47 MenuItemRadio,
48 Tooltip,
49 Text,
50 TextField,
51 List,
52 ListItem,
53 Separator,
54 ListBox,
55 ListBoxOption,
56 TreeItem,
57 Viewport,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61#[non_exhaustive]
62pub enum SemanticsOrientation {
63 Horizontal,
64 Vertical,
65}
66
67#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
68pub struct SemanticsActions {
69 pub focus: bool,
70 pub invoke: bool,
71 pub set_value: bool,
72 pub decrement: bool,
74 pub increment: bool,
76 pub scroll_by: bool,
77 pub set_text_selection: bool,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81#[non_exhaustive]
82pub enum SemanticsCheckedState {
83 False,
84 True,
85 Mixed,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89#[non_exhaustive]
90pub enum SemanticsPressedState {
91 False,
92 True,
93 Mixed,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98#[non_exhaustive]
99pub enum SemanticsInvalid {
100 True,
101 Grammar,
102 Spelling,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106#[non_exhaustive]
107pub enum SemanticsLive {
108 Off,
109 Polite,
110 Assertive,
111}
112
113#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
114pub struct SemanticsFlags {
115 pub focused: bool,
116 pub captured: bool,
117 pub disabled: bool,
118 pub read_only: bool,
119 pub hidden: bool,
124 pub visited: bool,
128 pub multiselectable: bool,
132 pub live: Option<SemanticsLive>,
136 pub live_atomic: bool,
139 pub selected: bool,
140 pub expanded: bool,
141 pub checked: Option<bool>,
145 pub checked_state: Option<SemanticsCheckedState>,
147 pub pressed_state: Option<SemanticsPressedState>,
149 pub required: bool,
151 pub invalid: Option<SemanticsInvalid>,
153 pub busy: bool,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct SemanticsInlineSpan {
161 pub range_utf8: (u32, u32),
163 pub role: SemanticsRole,
164 pub tag: Option<String>,
166}
167
168#[derive(Debug, Default, Clone, Copy, PartialEq)]
169pub struct SemanticsNumeric {
170 pub value: Option<f64>,
171 pub min: Option<f64>,
172 pub max: Option<f64>,
173 pub step: Option<f64>,
174 pub jump: Option<f64>,
175}
176
177#[derive(Debug, Default, Clone, Copy, PartialEq)]
178pub struct SemanticsScroll {
179 pub x: Option<f64>,
180 pub x_min: Option<f64>,
181 pub x_max: Option<f64>,
182 pub y: Option<f64>,
183 pub y_min: Option<f64>,
184 pub y_max: Option<f64>,
185}
186
187#[derive(Debug, Default, Clone, PartialEq)]
188pub struct SemanticsNodeExtra {
189 pub placeholder: Option<String>,
190 pub url: Option<String>,
191 pub role_description: Option<String>,
193 pub level: Option<u32>,
195 pub orientation: Option<SemanticsOrientation>,
196 pub numeric: SemanticsNumeric,
197 pub scroll: SemanticsScroll,
198}
199
200#[derive(Debug, Clone)]
201pub struct SemanticsNode {
202 pub id: NodeId,
203 pub parent: Option<NodeId>,
204 pub role: SemanticsRole,
205 pub bounds: Rect,
206 pub flags: SemanticsFlags,
207 pub test_id: Option<String>,
211 pub active_descendant: Option<NodeId>,
215 pub pos_in_set: Option<u32>,
220 pub set_size: Option<u32>,
225 pub label: Option<String>,
227 pub value: Option<String>,
229 pub extra: SemanticsNodeExtra,
230 pub text_selection: Option<(u32, u32)>,
234 pub text_composition: Option<(u32, u32)>,
239 pub actions: SemanticsActions,
241 pub labelled_by: Vec<NodeId>,
245 pub described_by: Vec<NodeId>,
249 pub controls: Vec<NodeId>,
253 pub inline_spans: Vec<SemanticsInlineSpan>,
255}
256
257#[derive(Debug, Clone)]
258pub struct SemanticsRoot {
259 pub root: NodeId,
260 pub visible: bool,
261 pub blocks_underlay_input: bool,
262 pub hit_testable: bool,
263 pub z_index: u32,
265}
266
267#[derive(Debug, Default, Clone)]
268pub struct SemanticsSnapshot {
269 pub window: AppWindowId,
270 pub roots: Vec<SemanticsRoot>,
271 pub barrier_root: Option<NodeId>,
273 pub focus_barrier_root: Option<NodeId>,
278 pub focus: Option<NodeId>,
279 pub captured: Option<NodeId>,
280 pub nodes: Vec<SemanticsNode>,
281}
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq)]
284pub enum SemanticsValidationField {
285 TextSelection,
286 TextComposition,
287 InlineSpan,
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
291pub enum SemanticsNumericField {
292 Value,
293 Min,
294 Max,
295 Step,
296 Jump,
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300pub enum SemanticsScrollField {
301 X,
302 XMin,
303 XMax,
304 Y,
305 YMin,
306 YMax,
307}
308
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
310pub enum SemanticsScrollAxis {
311 X,
312 Y,
313}
314
315#[derive(Debug, Clone)]
316pub enum SemanticsValidationErrorKind {
317 MissingValueForTextRange {
318 field: SemanticsValidationField,
319 },
320 RangeOutOfBounds {
321 field: SemanticsValidationField,
322 start: u32,
323 end: u32,
324 len: u32,
325 },
326 RangeNotCharBoundary {
327 field: SemanticsValidationField,
328 offset: u32,
329 },
330 InvalidRangeOrder {
331 field: SemanticsValidationField,
332 start: u32,
333 end: u32,
334 },
335 DuplicateNodeId {
336 id: NodeId,
337 },
338 MissingReferencedNode {
339 field: SemanticsReferenceField,
340 referenced: NodeId,
341 },
342 InvalidCollectionMetadata {
343 pos_in_set: Option<u32>,
344 set_size: Option<u32>,
345 },
346 InvalidHierarchyLevel {
347 level: u32,
348 },
349 NonFiniteNumeric {
350 field: SemanticsNumericField,
351 value: f64,
352 },
353 InvalidNumericBounds {
354 min: f64,
355 max: f64,
356 },
357 NumericValueOutOfBounds {
358 value: f64,
359 min: f64,
360 max: f64,
361 },
362 InvalidNumericStep {
363 step: f64,
364 },
365 InvalidNumericJump {
366 jump: f64,
367 },
368 NonFiniteScroll {
369 field: SemanticsScrollField,
370 value: f64,
371 },
372 InvalidScrollBounds {
373 axis: SemanticsScrollAxis,
374 min: f64,
375 max: f64,
376 },
377 ScrollValueOutOfBounds {
378 axis: SemanticsScrollAxis,
379 value: f64,
380 min: f64,
381 max: f64,
382 },
383}
384
385impl PartialEq for SemanticsValidationErrorKind {
386 fn eq(&self, other: &Self) -> bool {
387 use SemanticsValidationErrorKind::*;
388 match (self, other) {
389 (MissingValueForTextRange { field: a }, MissingValueForTextRange { field: b }) => {
390 a == b
391 }
392 (
393 RangeOutOfBounds {
394 field: a_field,
395 start: a_start,
396 end: a_end,
397 len: a_len,
398 },
399 RangeOutOfBounds {
400 field: b_field,
401 start: b_start,
402 end: b_end,
403 len: b_len,
404 },
405 ) => a_field == b_field && a_start == b_start && a_end == b_end && a_len == b_len,
406 (
407 RangeNotCharBoundary {
408 field: a_field,
409 offset: a_offset,
410 },
411 RangeNotCharBoundary {
412 field: b_field,
413 offset: b_offset,
414 },
415 ) => a_field == b_field && a_offset == b_offset,
416 (
417 InvalidRangeOrder {
418 field: a_field,
419 start: a_start,
420 end: a_end,
421 },
422 InvalidRangeOrder {
423 field: b_field,
424 start: b_start,
425 end: b_end,
426 },
427 ) => a_field == b_field && a_start == b_start && a_end == b_end,
428 (DuplicateNodeId { id: a }, DuplicateNodeId { id: b }) => a == b,
429 (
430 MissingReferencedNode {
431 field: a_field,
432 referenced: a_referenced,
433 },
434 MissingReferencedNode {
435 field: b_field,
436 referenced: b_referenced,
437 },
438 ) => a_field == b_field && a_referenced == b_referenced,
439 (
440 InvalidCollectionMetadata {
441 pos_in_set: a_pos_in_set,
442 set_size: a_set_size,
443 },
444 InvalidCollectionMetadata {
445 pos_in_set: b_pos_in_set,
446 set_size: b_set_size,
447 },
448 ) => a_pos_in_set == b_pos_in_set && a_set_size == b_set_size,
449 (InvalidHierarchyLevel { level: a }, InvalidHierarchyLevel { level: b }) => a == b,
450 (
451 NonFiniteNumeric {
452 field: a_field,
453 value: a_value,
454 },
455 NonFiniteNumeric {
456 field: b_field,
457 value: b_value,
458 },
459 ) => a_field == b_field && a_value.to_bits() == b_value.to_bits(),
460 (
461 InvalidNumericBounds {
462 min: a_min,
463 max: a_max,
464 },
465 InvalidNumericBounds {
466 min: b_min,
467 max: b_max,
468 },
469 ) => a_min.to_bits() == b_min.to_bits() && a_max.to_bits() == b_max.to_bits(),
470 (
471 NumericValueOutOfBounds {
472 value: a_value,
473 min: a_min,
474 max: a_max,
475 },
476 NumericValueOutOfBounds {
477 value: b_value,
478 min: b_min,
479 max: b_max,
480 },
481 ) => {
482 a_value.to_bits() == b_value.to_bits()
483 && a_min.to_bits() == b_min.to_bits()
484 && a_max.to_bits() == b_max.to_bits()
485 }
486 (InvalidNumericStep { step: a }, InvalidNumericStep { step: b }) => {
487 a.to_bits() == b.to_bits()
488 }
489 (InvalidNumericJump { jump: a }, InvalidNumericJump { jump: b }) => {
490 a.to_bits() == b.to_bits()
491 }
492 (
493 NonFiniteScroll {
494 field: a_field,
495 value: a_value,
496 },
497 NonFiniteScroll {
498 field: b_field,
499 value: b_value,
500 },
501 ) => a_field == b_field && a_value.to_bits() == b_value.to_bits(),
502 (
503 InvalidScrollBounds {
504 axis: a_axis,
505 min: a_min,
506 max: a_max,
507 },
508 InvalidScrollBounds {
509 axis: b_axis,
510 min: b_min,
511 max: b_max,
512 },
513 ) => {
514 a_axis == b_axis
515 && a_min.to_bits() == b_min.to_bits()
516 && a_max.to_bits() == b_max.to_bits()
517 }
518 (
519 ScrollValueOutOfBounds {
520 axis: a_axis,
521 value: a_value,
522 min: a_min,
523 max: a_max,
524 },
525 ScrollValueOutOfBounds {
526 axis: b_axis,
527 value: b_value,
528 min: b_min,
529 max: b_max,
530 },
531 ) => {
532 a_axis == b_axis
533 && a_value.to_bits() == b_value.to_bits()
534 && a_min.to_bits() == b_min.to_bits()
535 && a_max.to_bits() == b_max.to_bits()
536 }
537 _ => false,
538 }
539 }
540}
541
542impl Eq for SemanticsValidationErrorKind {}
543
544#[derive(Debug, Clone, Copy, PartialEq, Eq)]
545pub enum SemanticsReferenceField {
546 Root,
547 BarrierRoot,
548 FocusBarrierRoot,
549 Focus,
550 Captured,
551 Parent,
552 ActiveDescendant,
553 LabelledBy,
554 DescribedBy,
555 Controls,
556}
557
558#[derive(Debug, Clone, PartialEq, Eq)]
559pub struct SemanticsValidationError {
560 pub node: NodeId,
561 pub kind: SemanticsValidationErrorKind,
562}
563
564impl SemanticsNode {
565 pub fn validate(&self) -> Result<(), SemanticsValidationError> {
566 validate_text_ranges(
567 self.id,
568 self.value.as_deref(),
569 self.text_selection,
570 self.text_composition,
571 )?;
572 validate_inline_spans(self.id, self.value.as_deref(), &self.inline_spans)?;
573 validate_extra(self.id, &self.extra)?;
574 Ok(())
575 }
576}
577
578impl SemanticsSnapshot {
579 pub fn validate(&self) -> Result<(), SemanticsValidationError> {
580 let mut ids = HashSet::with_capacity(self.nodes.len());
581 for node in &self.nodes {
582 if !ids.insert(node.id) {
583 return Err(SemanticsValidationError {
584 node: node.id,
585 kind: SemanticsValidationErrorKind::DuplicateNodeId { id: node.id },
586 });
587 }
588 }
589
590 let check_ref = |node: NodeId,
591 field: SemanticsReferenceField,
592 referenced: NodeId,
593 ids: &HashSet<NodeId>|
594 -> Result<(), SemanticsValidationError> {
595 if ids.contains(&referenced) {
596 return Ok(());
597 }
598 Err(SemanticsValidationError {
599 node,
600 kind: SemanticsValidationErrorKind::MissingReferencedNode { field, referenced },
601 })
602 };
603
604 for root in &self.roots {
605 check_ref(root.root, SemanticsReferenceField::Root, root.root, &ids)?;
606 }
607 if let Some(barrier_root) = self.barrier_root {
608 check_ref(
609 barrier_root,
610 SemanticsReferenceField::BarrierRoot,
611 barrier_root,
612 &ids,
613 )?;
614 }
615 if let Some(focus_barrier_root) = self.focus_barrier_root {
616 check_ref(
617 focus_barrier_root,
618 SemanticsReferenceField::FocusBarrierRoot,
619 focus_barrier_root,
620 &ids,
621 )?;
622 }
623 if let Some(focus) = self.focus {
624 check_ref(focus, SemanticsReferenceField::Focus, focus, &ids)?;
625 }
626 if let Some(captured) = self.captured {
627 check_ref(captured, SemanticsReferenceField::Captured, captured, &ids)?;
628 }
629
630 for node in &self.nodes {
631 node.validate()?;
632
633 if node.pos_in_set.is_some() ^ node.set_size.is_some() {
634 return Err(SemanticsValidationError {
635 node: node.id,
636 kind: SemanticsValidationErrorKind::InvalidCollectionMetadata {
637 pos_in_set: node.pos_in_set,
638 set_size: node.set_size,
639 },
640 });
641 }
642 if let (Some(pos_in_set), Some(set_size)) = (node.pos_in_set, node.set_size)
643 && (pos_in_set == 0 || set_size == 0 || pos_in_set > set_size)
644 {
645 return Err(SemanticsValidationError {
646 node: node.id,
647 kind: SemanticsValidationErrorKind::InvalidCollectionMetadata {
648 pos_in_set: Some(pos_in_set),
649 set_size: Some(set_size),
650 },
651 });
652 }
653
654 if let Some(parent) = node.parent {
655 check_ref(node.id, SemanticsReferenceField::Parent, parent, &ids)?;
656 }
657 if let Some(active) = node.active_descendant {
658 check_ref(
659 node.id,
660 SemanticsReferenceField::ActiveDescendant,
661 active,
662 &ids,
663 )?;
664 }
665 for id in &node.labelled_by {
666 check_ref(node.id, SemanticsReferenceField::LabelledBy, *id, &ids)?;
667 }
668 for id in &node.described_by {
669 check_ref(node.id, SemanticsReferenceField::DescribedBy, *id, &ids)?;
670 }
671 for id in &node.controls {
672 check_ref(node.id, SemanticsReferenceField::Controls, *id, &ids)?;
673 }
674 }
675 Ok(())
676 }
677}
678
679fn validate_extra(
680 node: NodeId,
681 extra: &SemanticsNodeExtra,
682) -> Result<(), SemanticsValidationError> {
683 if let Some(level) = extra.level
684 && level == 0
685 {
686 return Err(SemanticsValidationError {
687 node,
688 kind: SemanticsValidationErrorKind::InvalidHierarchyLevel { level },
689 });
690 }
691
692 validate_numeric(node, extra.numeric)?;
693 validate_scroll(node, extra.scroll)?;
694 Ok(())
695}
696
697fn validate_numeric(
698 node: NodeId,
699 numeric: SemanticsNumeric,
700) -> Result<(), SemanticsValidationError> {
701 let check_finite =
702 |field: SemanticsNumericField, value: f64| -> Result<(), SemanticsValidationError> {
703 if value.is_finite() {
704 Ok(())
705 } else {
706 Err(SemanticsValidationError {
707 node,
708 kind: SemanticsValidationErrorKind::NonFiniteNumeric { field, value },
709 })
710 }
711 };
712
713 if let Some(value) = numeric.value {
714 check_finite(SemanticsNumericField::Value, value)?;
715 }
716 if let Some(min) = numeric.min {
717 check_finite(SemanticsNumericField::Min, min)?;
718 }
719 if let Some(max) = numeric.max {
720 check_finite(SemanticsNumericField::Max, max)?;
721 }
722 if let Some(step) = numeric.step {
723 check_finite(SemanticsNumericField::Step, step)?;
724 }
725 if let Some(jump) = numeric.jump {
726 check_finite(SemanticsNumericField::Jump, jump)?;
727 }
728
729 if let (Some(min), Some(max)) = (numeric.min, numeric.max)
730 && min > max
731 {
732 return Err(SemanticsValidationError {
733 node,
734 kind: SemanticsValidationErrorKind::InvalidNumericBounds { min, max },
735 });
736 }
737
738 if let Some(step) = numeric.step
739 && step <= 0.0
740 {
741 return Err(SemanticsValidationError {
742 node,
743 kind: SemanticsValidationErrorKind::InvalidNumericStep { step },
744 });
745 }
746 if let Some(jump) = numeric.jump
747 && jump <= 0.0
748 {
749 return Err(SemanticsValidationError {
750 node,
751 kind: SemanticsValidationErrorKind::InvalidNumericJump { jump },
752 });
753 }
754
755 if let (Some(value), Some(min), Some(max)) = (numeric.value, numeric.min, numeric.max)
756 && (value < min || value > max)
757 {
758 return Err(SemanticsValidationError {
759 node,
760 kind: SemanticsValidationErrorKind::NumericValueOutOfBounds { value, min, max },
761 });
762 }
763
764 Ok(())
765}
766
767fn validate_scroll(node: NodeId, scroll: SemanticsScroll) -> Result<(), SemanticsValidationError> {
768 const EPS: f64 = 1e-9;
769
770 let check_finite =
771 |field: SemanticsScrollField, value: f64| -> Result<(), SemanticsValidationError> {
772 if value.is_finite() {
773 Ok(())
774 } else {
775 Err(SemanticsValidationError {
776 node,
777 kind: SemanticsValidationErrorKind::NonFiniteScroll { field, value },
778 })
779 }
780 };
781
782 if let Some(x) = scroll.x {
783 check_finite(SemanticsScrollField::X, x)?;
784 }
785 if let Some(x_min) = scroll.x_min {
786 check_finite(SemanticsScrollField::XMin, x_min)?;
787 }
788 if let Some(x_max) = scroll.x_max {
789 check_finite(SemanticsScrollField::XMax, x_max)?;
790 }
791 if let Some(y) = scroll.y {
792 check_finite(SemanticsScrollField::Y, y)?;
793 }
794 if let Some(y_min) = scroll.y_min {
795 check_finite(SemanticsScrollField::YMin, y_min)?;
796 }
797 if let Some(y_max) = scroll.y_max {
798 check_finite(SemanticsScrollField::YMax, y_max)?;
799 }
800
801 if let (Some(min), Some(max)) = (scroll.x_min, scroll.x_max)
802 && min > max
803 {
804 return Err(SemanticsValidationError {
805 node,
806 kind: SemanticsValidationErrorKind::InvalidScrollBounds {
807 axis: SemanticsScrollAxis::X,
808 min,
809 max,
810 },
811 });
812 }
813 if let (Some(value), Some(min), Some(max)) = (scroll.x, scroll.x_min, scroll.x_max)
814 && (value < min - EPS || value > max + EPS)
815 {
816 return Err(SemanticsValidationError {
817 node,
818 kind: SemanticsValidationErrorKind::ScrollValueOutOfBounds {
819 axis: SemanticsScrollAxis::X,
820 value,
821 min,
822 max,
823 },
824 });
825 }
826
827 if let (Some(min), Some(max)) = (scroll.y_min, scroll.y_max)
828 && min > max
829 {
830 return Err(SemanticsValidationError {
831 node,
832 kind: SemanticsValidationErrorKind::InvalidScrollBounds {
833 axis: SemanticsScrollAxis::Y,
834 min,
835 max,
836 },
837 });
838 }
839 if let (Some(value), Some(min), Some(max)) = (scroll.y, scroll.y_min, scroll.y_max)
840 && (value < min - EPS || value > max + EPS)
841 {
842 return Err(SemanticsValidationError {
843 node,
844 kind: SemanticsValidationErrorKind::ScrollValueOutOfBounds {
845 axis: SemanticsScrollAxis::Y,
846 value,
847 min,
848 max,
849 },
850 });
851 }
852
853 Ok(())
854}
855
856fn validate_text_ranges(
857 node: NodeId,
858 value: Option<&str>,
859 text_selection: Option<(u32, u32)>,
860 text_composition: Option<(u32, u32)>,
861) -> Result<(), SemanticsValidationError> {
862 if text_selection.is_none() && text_composition.is_none() {
863 return Ok(());
864 }
865
866 let Some(value) = value else {
867 return Err(SemanticsValidationError {
868 node,
869 kind: SemanticsValidationErrorKind::MissingValueForTextRange {
870 field: if text_selection.is_some() {
871 SemanticsValidationField::TextSelection
872 } else {
873 SemanticsValidationField::TextComposition
874 },
875 },
876 });
877 };
878
879 let len_u32 = u32::try_from(value.len()).unwrap_or(u32::MAX);
880
881 let check_range = |field: SemanticsValidationField,
882 start: u32,
883 end: u32|
884 -> Result<(), SemanticsValidationError> {
885 if start > end {
886 return Err(SemanticsValidationError {
887 node,
888 kind: SemanticsValidationErrorKind::InvalidRangeOrder { field, start, end },
889 });
890 }
891 if start > len_u32 || end > len_u32 {
892 return Err(SemanticsValidationError {
893 node,
894 kind: SemanticsValidationErrorKind::RangeOutOfBounds {
895 field,
896 start,
897 end,
898 len: len_u32,
899 },
900 });
901 }
902
903 let start_usize = start as usize;
904 let end_usize = end as usize;
905 if !value.is_char_boundary(start_usize) {
906 return Err(SemanticsValidationError {
907 node,
908 kind: SemanticsValidationErrorKind::RangeNotCharBoundary {
909 field,
910 offset: start,
911 },
912 });
913 }
914 if !value.is_char_boundary(end_usize) {
915 return Err(SemanticsValidationError {
916 node,
917 kind: SemanticsValidationErrorKind::RangeNotCharBoundary { field, offset: end },
918 });
919 }
920 Ok(())
921 };
922
923 if let Some((anchor, focus)) = text_selection {
924 let (start, end) = if anchor <= focus {
925 (anchor, focus)
926 } else {
927 (focus, anchor)
928 };
929 check_range(SemanticsValidationField::TextSelection, start, end)?;
930 }
931
932 if let Some((start, end)) = text_composition {
933 check_range(SemanticsValidationField::TextComposition, start, end)?;
934 }
935
936 Ok(())
937}
938
939fn validate_inline_spans(
940 node: NodeId,
941 value: Option<&str>,
942 spans: &[SemanticsInlineSpan],
943) -> Result<(), SemanticsValidationError> {
944 if spans.is_empty() {
945 return Ok(());
946 }
947
948 let Some(value) = value else {
949 return Err(SemanticsValidationError {
950 node,
951 kind: SemanticsValidationErrorKind::MissingValueForTextRange {
952 field: SemanticsValidationField::InlineSpan,
953 },
954 });
955 };
956
957 let len_u32 = u32::try_from(value.len()).unwrap_or(u32::MAX);
958 for span in spans {
959 let (start, end) = span.range_utf8;
960 if start > end {
961 return Err(SemanticsValidationError {
962 node,
963 kind: SemanticsValidationErrorKind::InvalidRangeOrder {
964 field: SemanticsValidationField::InlineSpan,
965 start,
966 end,
967 },
968 });
969 }
970 if start > len_u32 || end > len_u32 {
971 return Err(SemanticsValidationError {
972 node,
973 kind: SemanticsValidationErrorKind::RangeOutOfBounds {
974 field: SemanticsValidationField::InlineSpan,
975 start,
976 end,
977 len: len_u32,
978 },
979 });
980 }
981
982 let start_usize = start as usize;
983 let end_usize = end as usize;
984 if !value.is_char_boundary(start_usize) {
985 return Err(SemanticsValidationError {
986 node,
987 kind: SemanticsValidationErrorKind::RangeNotCharBoundary {
988 field: SemanticsValidationField::InlineSpan,
989 offset: start,
990 },
991 });
992 }
993 if !value.is_char_boundary(end_usize) {
994 return Err(SemanticsValidationError {
995 node,
996 kind: SemanticsValidationErrorKind::RangeNotCharBoundary {
997 field: SemanticsValidationField::InlineSpan,
998 offset: end,
999 },
1000 });
1001 }
1002 }
1003
1004 Ok(())
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009 use super::*;
1010 use slotmap::KeyData;
1011
1012 fn node(id: u64) -> NodeId {
1013 NodeId::from(KeyData::from_ffi(id))
1014 }
1015
1016 fn snapshot_with_nodes(nodes: Vec<SemanticsNode>) -> SemanticsSnapshot {
1017 SemanticsSnapshot {
1018 window: AppWindowId::default(),
1019 roots: vec![SemanticsRoot {
1020 root: nodes.first().expect("at least one node").id,
1021 visible: true,
1022 blocks_underlay_input: false,
1023 hit_testable: true,
1024 z_index: 0,
1025 }],
1026 barrier_root: None,
1027 focus_barrier_root: None,
1028 focus: None,
1029 captured: None,
1030 nodes,
1031 }
1032 }
1033
1034 fn base_node(extra: SemanticsNodeExtra) -> SemanticsNode {
1035 SemanticsNode {
1036 id: node(1),
1037 parent: None,
1038 role: SemanticsRole::Slider,
1039 bounds: Rect::default(),
1040 flags: SemanticsFlags::default(),
1041 test_id: None,
1042 active_descendant: None,
1043 pos_in_set: None,
1044 set_size: None,
1045 label: None,
1046 value: None,
1047 extra,
1048 text_selection: None,
1049 text_composition: None,
1050 actions: SemanticsActions::default(),
1051 labelled_by: Vec::new(),
1052 described_by: Vec::new(),
1053 controls: Vec::new(),
1054 inline_spans: Vec::new(),
1055 }
1056 }
1057
1058 #[test]
1059 fn validates_utf8_char_boundaries_for_text_ranges() {
1060 let n = SemanticsNode {
1061 id: node(1),
1062 parent: None,
1063 role: SemanticsRole::TextField,
1064 bounds: Rect::default(),
1065 flags: SemanticsFlags::default(),
1066 test_id: None,
1067 active_descendant: None,
1068 pos_in_set: None,
1069 set_size: None,
1070 label: None,
1071 value: Some("😀".to_string()), extra: SemanticsNodeExtra::default(),
1073 text_selection: Some((0, 4)),
1074 text_composition: Some((0, 4)),
1075 actions: SemanticsActions::default(),
1076 labelled_by: Vec::new(),
1077 described_by: Vec::new(),
1078 controls: Vec::new(),
1079 inline_spans: Vec::new(),
1080 };
1081 n.validate().expect("valid ranges should pass");
1082
1083 let bad = SemanticsNode {
1084 text_selection: Some((0, 2)),
1085 ..n
1086 };
1087 let err = bad.validate().expect_err("non-boundary should fail");
1088 assert_eq!(err.node, node(1));
1089 assert!(matches!(
1090 err.kind,
1091 SemanticsValidationErrorKind::RangeNotCharBoundary {
1092 field: SemanticsValidationField::TextSelection,
1093 offset: 2
1094 }
1095 ));
1096 }
1097
1098 #[test]
1099 fn rejects_ranges_without_value() {
1100 let n = SemanticsNode {
1101 id: node(1),
1102 parent: None,
1103 role: SemanticsRole::TextField,
1104 bounds: Rect::default(),
1105 flags: SemanticsFlags::default(),
1106 test_id: None,
1107 active_descendant: None,
1108 pos_in_set: None,
1109 set_size: None,
1110 label: None,
1111 value: None,
1112 extra: SemanticsNodeExtra::default(),
1113 text_selection: Some((0, 0)),
1114 text_composition: None,
1115 actions: SemanticsActions::default(),
1116 labelled_by: Vec::new(),
1117 described_by: Vec::new(),
1118 controls: Vec::new(),
1119 inline_spans: Vec::new(),
1120 };
1121 let err = n.validate().expect_err("range without value should fail");
1122 assert!(matches!(
1123 err.kind,
1124 SemanticsValidationErrorKind::MissingValueForTextRange { .. }
1125 ));
1126 }
1127
1128 #[test]
1129 fn rejects_inline_spans_without_value() {
1130 let n = SemanticsNode {
1131 id: node(1),
1132 parent: None,
1133 role: SemanticsRole::Text,
1134 bounds: Rect::default(),
1135 flags: SemanticsFlags::default(),
1136 test_id: None,
1137 active_descendant: None,
1138 pos_in_set: None,
1139 set_size: None,
1140 label: None,
1141 value: None,
1142 extra: SemanticsNodeExtra::default(),
1143 text_selection: None,
1144 text_composition: None,
1145 actions: SemanticsActions::default(),
1146 labelled_by: Vec::new(),
1147 described_by: Vec::new(),
1148 controls: Vec::new(),
1149 inline_spans: vec![SemanticsInlineSpan {
1150 range_utf8: (0, 0),
1151 role: SemanticsRole::Link,
1152 tag: None,
1153 }],
1154 };
1155 let err = n
1156 .validate()
1157 .expect_err("inline spans without value should fail");
1158 assert!(matches!(
1159 err.kind,
1160 SemanticsValidationErrorKind::MissingValueForTextRange {
1161 field: SemanticsValidationField::InlineSpan
1162 }
1163 ));
1164 }
1165
1166 #[test]
1167 fn validates_extra_numeric_and_scroll_metadata() {
1168 let n = base_node(SemanticsNodeExtra {
1169 level: Some(1),
1170 numeric: SemanticsNumeric {
1171 value: Some(5.0),
1172 min: Some(0.0),
1173 max: Some(10.0),
1174 step: Some(1.0),
1175 jump: Some(5.0),
1176 },
1177 scroll: SemanticsScroll {
1178 x: Some(0.0),
1179 x_min: Some(0.0),
1180 x_max: Some(0.0),
1181 y: Some(10.0),
1182 y_min: Some(0.0),
1183 y_max: Some(10.0),
1184 },
1185 ..SemanticsNodeExtra::default()
1186 });
1187 n.validate().expect("valid extra metadata should pass");
1188 }
1189
1190 #[test]
1191 fn rejects_invalid_hierarchy_level() {
1192 let n = base_node(SemanticsNodeExtra {
1193 level: Some(0),
1194 ..SemanticsNodeExtra::default()
1195 });
1196 let err = n.validate().expect_err("level=0 should fail");
1197 assert!(matches!(
1198 err.kind,
1199 SemanticsValidationErrorKind::InvalidHierarchyLevel { level: 0 }
1200 ));
1201 }
1202
1203 #[test]
1204 fn rejects_non_finite_numeric_metadata() {
1205 let n = base_node(SemanticsNodeExtra {
1206 numeric: SemanticsNumeric {
1207 value: Some(f64::NAN),
1208 ..SemanticsNumeric::default()
1209 },
1210 ..SemanticsNodeExtra::default()
1211 });
1212 let err = n.validate().expect_err("NaN should fail");
1213 assert!(matches!(
1214 err.kind,
1215 SemanticsValidationErrorKind::NonFiniteNumeric {
1216 field: SemanticsNumericField::Value,
1217 ..
1218 }
1219 ));
1220 }
1221
1222 #[test]
1223 fn rejects_invalid_numeric_bounds() {
1224 let n = base_node(SemanticsNodeExtra {
1225 numeric: SemanticsNumeric {
1226 min: Some(10.0),
1227 max: Some(5.0),
1228 ..SemanticsNumeric::default()
1229 },
1230 ..SemanticsNodeExtra::default()
1231 });
1232 let err = n.validate().expect_err("min > max should fail");
1233 assert!(matches!(
1234 err.kind,
1235 SemanticsValidationErrorKind::InvalidNumericBounds { .. }
1236 ));
1237 }
1238
1239 #[test]
1240 fn rejects_numeric_value_out_of_bounds() {
1241 let n = base_node(SemanticsNodeExtra {
1242 numeric: SemanticsNumeric {
1243 value: Some(11.0),
1244 min: Some(0.0),
1245 max: Some(10.0),
1246 ..SemanticsNumeric::default()
1247 },
1248 ..SemanticsNodeExtra::default()
1249 });
1250 let err = n.validate().expect_err("out-of-bounds should fail");
1251 assert!(matches!(
1252 err.kind,
1253 SemanticsValidationErrorKind::NumericValueOutOfBounds { .. }
1254 ));
1255 }
1256
1257 #[test]
1258 fn rejects_non_positive_numeric_step_and_jump() {
1259 let step = base_node(SemanticsNodeExtra {
1260 numeric: SemanticsNumeric {
1261 step: Some(0.0),
1262 ..SemanticsNumeric::default()
1263 },
1264 ..SemanticsNodeExtra::default()
1265 });
1266 let err = step.validate().expect_err("step <= 0 should fail");
1267 assert!(matches!(
1268 err.kind,
1269 SemanticsValidationErrorKind::InvalidNumericStep { .. }
1270 ));
1271
1272 let jump = base_node(SemanticsNodeExtra {
1273 numeric: SemanticsNumeric {
1274 jump: Some(-1.0),
1275 ..SemanticsNumeric::default()
1276 },
1277 ..SemanticsNodeExtra::default()
1278 });
1279 let err = jump.validate().expect_err("jump <= 0 should fail");
1280 assert!(matches!(
1281 err.kind,
1282 SemanticsValidationErrorKind::InvalidNumericJump { .. }
1283 ));
1284 }
1285
1286 #[test]
1287 fn rejects_non_finite_scroll_metadata() {
1288 let n = base_node(SemanticsNodeExtra {
1289 scroll: SemanticsScroll {
1290 y: Some(f64::INFINITY),
1291 ..SemanticsScroll::default()
1292 },
1293 ..SemanticsNodeExtra::default()
1294 });
1295 let err = n.validate().expect_err("Infinity should fail");
1296 assert!(matches!(
1297 err.kind,
1298 SemanticsValidationErrorKind::NonFiniteScroll {
1299 field: SemanticsScrollField::Y,
1300 ..
1301 }
1302 ));
1303 }
1304
1305 #[test]
1306 fn rejects_invalid_scroll_bounds_and_value_out_of_bounds() {
1307 let bounds = base_node(SemanticsNodeExtra {
1308 scroll: SemanticsScroll {
1309 x_min: Some(10.0),
1310 x_max: Some(5.0),
1311 ..SemanticsScroll::default()
1312 },
1313 ..SemanticsNodeExtra::default()
1314 });
1315 let err = bounds.validate().expect_err("x_min > x_max should fail");
1316 assert!(matches!(
1317 err.kind,
1318 SemanticsValidationErrorKind::InvalidScrollBounds {
1319 axis: SemanticsScrollAxis::X,
1320 ..
1321 }
1322 ));
1323
1324 let oob = base_node(SemanticsNodeExtra {
1325 scroll: SemanticsScroll {
1326 y: Some(11.0),
1327 y_min: Some(0.0),
1328 y_max: Some(10.0),
1329 ..SemanticsScroll::default()
1330 },
1331 ..SemanticsNodeExtra::default()
1332 });
1333 let err = oob.validate().expect_err("y out-of-bounds should fail");
1334 assert!(matches!(
1335 err.kind,
1336 SemanticsValidationErrorKind::ScrollValueOutOfBounds {
1337 axis: SemanticsScrollAxis::Y,
1338 ..
1339 }
1340 ));
1341 }
1342
1343 #[test]
1344 fn validates_utf8_char_boundaries_for_inline_spans() {
1345 let n = SemanticsNode {
1346 id: node(1),
1347 parent: None,
1348 role: SemanticsRole::Text,
1349 bounds: Rect::default(),
1350 flags: SemanticsFlags::default(),
1351 test_id: None,
1352 active_descendant: None,
1353 pos_in_set: None,
1354 set_size: None,
1355 label: None,
1356 value: Some("😀".to_string()), extra: SemanticsNodeExtra::default(),
1358 text_selection: None,
1359 text_composition: None,
1360 actions: SemanticsActions::default(),
1361 labelled_by: Vec::new(),
1362 described_by: Vec::new(),
1363 controls: Vec::new(),
1364 inline_spans: vec![SemanticsInlineSpan {
1365 range_utf8: (0, 4),
1366 role: SemanticsRole::Link,
1367 tag: None,
1368 }],
1369 };
1370 n.validate()
1371 .expect("inline span on a utf-8 boundary should pass");
1372
1373 let bad = SemanticsNode {
1374 inline_spans: vec![SemanticsInlineSpan {
1375 range_utf8: (0, 2),
1376 role: SemanticsRole::Link,
1377 tag: None,
1378 }],
1379 ..n
1380 };
1381 let err = bad.validate().expect_err("non-boundary should fail");
1382 assert!(matches!(
1383 err.kind,
1384 SemanticsValidationErrorKind::RangeNotCharBoundary {
1385 field: SemanticsValidationField::InlineSpan,
1386 offset: 2
1387 }
1388 ));
1389 }
1390
1391 #[test]
1392 fn rejects_out_of_bounds_ranges() {
1393 let n = SemanticsNode {
1394 id: node(1),
1395 parent: None,
1396 role: SemanticsRole::TextField,
1397 bounds: Rect::default(),
1398 flags: SemanticsFlags::default(),
1399 test_id: None,
1400 active_descendant: None,
1401 pos_in_set: None,
1402 set_size: None,
1403 label: None,
1404 value: Some("abc".to_string()),
1405 extra: SemanticsNodeExtra::default(),
1406 text_selection: Some((0, 4)),
1407 text_composition: None,
1408 actions: SemanticsActions::default(),
1409 labelled_by: Vec::new(),
1410 described_by: Vec::new(),
1411 controls: Vec::new(),
1412 inline_spans: Vec::new(),
1413 };
1414 let err = n.validate().expect_err("oob should fail");
1415 assert!(matches!(
1416 err.kind,
1417 SemanticsValidationErrorKind::RangeOutOfBounds { .. }
1418 ));
1419 }
1420
1421 #[test]
1422 fn rejects_invalid_composition_order() {
1423 let n = SemanticsNode {
1424 id: node(1),
1425 parent: None,
1426 role: SemanticsRole::TextField,
1427 bounds: Rect::default(),
1428 flags: SemanticsFlags::default(),
1429 test_id: None,
1430 active_descendant: None,
1431 pos_in_set: None,
1432 set_size: None,
1433 label: None,
1434 value: Some("abc".to_string()),
1435 extra: SemanticsNodeExtra::default(),
1436 text_selection: None,
1437 text_composition: Some((2, 1)),
1438 actions: SemanticsActions::default(),
1439 labelled_by: Vec::new(),
1440 described_by: Vec::new(),
1441 controls: Vec::new(),
1442 inline_spans: Vec::new(),
1443 };
1444 let err = n.validate().expect_err("invalid order should fail");
1445 assert!(matches!(
1446 err.kind,
1447 SemanticsValidationErrorKind::InvalidRangeOrder {
1448 field: SemanticsValidationField::TextComposition,
1449 ..
1450 }
1451 ));
1452 }
1453
1454 #[test]
1455 fn rejects_duplicate_node_ids_in_snapshot() {
1456 let n1 = SemanticsNode {
1457 id: node(1),
1458 parent: None,
1459 role: SemanticsRole::Window,
1460 bounds: Rect::default(),
1461 flags: SemanticsFlags::default(),
1462 test_id: None,
1463 active_descendant: None,
1464 pos_in_set: None,
1465 set_size: None,
1466 label: None,
1467 value: None,
1468 extra: SemanticsNodeExtra::default(),
1469 text_selection: None,
1470 text_composition: None,
1471 actions: SemanticsActions::default(),
1472 labelled_by: Vec::new(),
1473 described_by: Vec::new(),
1474 controls: Vec::new(),
1475 inline_spans: Vec::new(),
1476 };
1477 let snap = snapshot_with_nodes(vec![n1.clone(), n1]);
1478 let err = snap.validate().expect_err("duplicate node ids should fail");
1479 assert!(matches!(
1480 err.kind,
1481 SemanticsValidationErrorKind::DuplicateNodeId { .. }
1482 ));
1483 }
1484
1485 #[test]
1486 fn rejects_missing_references() {
1487 let root = SemanticsNode {
1488 id: node(1),
1489 parent: None,
1490 role: SemanticsRole::Window,
1491 bounds: Rect::default(),
1492 flags: SemanticsFlags::default(),
1493 test_id: None,
1494 active_descendant: None,
1495 pos_in_set: None,
1496 set_size: None,
1497 label: None,
1498 value: None,
1499 extra: SemanticsNodeExtra::default(),
1500 text_selection: None,
1501 text_composition: None,
1502 actions: SemanticsActions::default(),
1503 labelled_by: Vec::new(),
1504 described_by: Vec::new(),
1505 controls: Vec::new(),
1506 inline_spans: Vec::new(),
1507 };
1508 let child = SemanticsNode {
1509 id: node(2),
1510 parent: Some(node(999)),
1511 role: SemanticsRole::Group,
1512 bounds: Rect::default(),
1513 flags: SemanticsFlags::default(),
1514 test_id: None,
1515 active_descendant: Some(node(998)),
1516 pos_in_set: None,
1517 set_size: None,
1518 label: None,
1519 value: None,
1520 extra: SemanticsNodeExtra::default(),
1521 text_selection: None,
1522 text_composition: None,
1523 actions: SemanticsActions::default(),
1524 labelled_by: vec![node(997)],
1525 described_by: vec![node(996)],
1526 controls: vec![node(995)],
1527 inline_spans: Vec::new(),
1528 };
1529
1530 let snap = snapshot_with_nodes(vec![root, child]);
1531 let err = snap.validate().expect_err("missing references should fail");
1532 assert!(matches!(
1533 err.kind,
1534 SemanticsValidationErrorKind::MissingReferencedNode { .. }
1535 ));
1536 }
1537
1538 #[test]
1539 fn rejects_invalid_collection_metadata() {
1540 let root = SemanticsNode {
1541 id: node(1),
1542 parent: None,
1543 role: SemanticsRole::Window,
1544 bounds: Rect::default(),
1545 flags: SemanticsFlags::default(),
1546 test_id: None,
1547 active_descendant: None,
1548 pos_in_set: None,
1549 set_size: None,
1550 label: None,
1551 value: None,
1552 extra: SemanticsNodeExtra::default(),
1553 text_selection: None,
1554 text_composition: None,
1555 actions: SemanticsActions::default(),
1556 labelled_by: Vec::new(),
1557 described_by: Vec::new(),
1558 controls: Vec::new(),
1559 inline_spans: Vec::new(),
1560 };
1561
1562 let bad_missing_peer = SemanticsNode {
1563 id: node(2),
1564 parent: Some(node(1)),
1565 role: SemanticsRole::ListItem,
1566 bounds: Rect::default(),
1567 flags: SemanticsFlags::default(),
1568 test_id: None,
1569 active_descendant: None,
1570 pos_in_set: Some(1),
1571 set_size: None,
1572 label: None,
1573 value: None,
1574 extra: SemanticsNodeExtra::default(),
1575 text_selection: None,
1576 text_composition: None,
1577 actions: SemanticsActions::default(),
1578 labelled_by: Vec::new(),
1579 described_by: Vec::new(),
1580 controls: Vec::new(),
1581 inline_spans: Vec::new(),
1582 };
1583 let snap = snapshot_with_nodes(vec![root.clone(), bad_missing_peer]);
1584 let err = snap.validate().expect_err("missing set_size should fail");
1585 assert!(matches!(
1586 err.kind,
1587 SemanticsValidationErrorKind::InvalidCollectionMetadata { .. }
1588 ));
1589
1590 let bad_bounds = SemanticsNode {
1591 id: node(2),
1592 parent: Some(node(1)),
1593 role: SemanticsRole::ListItem,
1594 bounds: Rect::default(),
1595 flags: SemanticsFlags::default(),
1596 test_id: None,
1597 active_descendant: None,
1598 pos_in_set: Some(2),
1599 set_size: Some(1),
1600 label: None,
1601 value: None,
1602 extra: SemanticsNodeExtra::default(),
1603 text_selection: None,
1604 text_composition: None,
1605 actions: SemanticsActions::default(),
1606 labelled_by: Vec::new(),
1607 described_by: Vec::new(),
1608 controls: Vec::new(),
1609 inline_spans: Vec::new(),
1610 };
1611 let snap = snapshot_with_nodes(vec![root, bad_bounds]);
1612 let err = snap
1613 .validate()
1614 .expect_err("pos_in_set > set_size should fail");
1615 assert!(matches!(
1616 err.kind,
1617 SemanticsValidationErrorKind::InvalidCollectionMetadata {
1618 pos_in_set: Some(2),
1619 set_size: Some(1),
1620 }
1621 ));
1622 }
1623}