1use std::sync::Arc;
42
43use cosmic_text::Buffer;
44use unicode_segmentation::UnicodeSegmentation;
45
46use crate::display::{Clip, NO_CLIP};
47use crate::edit::EditStore;
48use crate::geom::{Rect, Size, Vec2};
49use crate::key::Key;
50use crate::scroll::ScrollStore;
51use crate::spec::NodeSpec;
52use crate::text::TextSystem;
53use crate::tree::{NIL, NodeContent, OriginId, Tree};
54use crate::value::{Handles, Value};
55use crate::window::{WindowButton, WindowRole};
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
65pub enum Role {
66 None,
68 Button,
69 Checkbox,
70 Radio,
71 Switch,
72 Slider,
73 Tab,
74 TabList,
75 Link,
76 Heading,
77 List,
78 ListItem,
79 Image,
80 Dialog,
81 Group,
82 Window,
85 TitleBar,
87 StaticText,
89 TextInput,
92 MultilineTextInput,
94 ScrollView,
96 Line,
101 RadioGroup,
108 Menu,
110 MenuItem,
113 Terminal,
116}
117
118impl Role {
119 pub fn name(self) -> &'static str {
121 match self {
122 Role::None => "none",
123 Role::Button => "button",
124 Role::Checkbox => "checkbox",
125 Role::Radio => "radio",
126 Role::Switch => "switch",
127 Role::Slider => "slider",
128 Role::Tab => "tab",
129 Role::TabList => "tabList",
130 Role::Link => "link",
131 Role::Heading => "heading",
132 Role::List => "list",
133 Role::ListItem => "listItem",
134 Role::Image => "image",
135 Role::Dialog => "dialog",
136 Role::Group => "group",
137 Role::RadioGroup => "radioGroup",
138 Role::Menu => "menu",
139 Role::MenuItem => "menuItem",
140 Role::Terminal => "terminal",
141 Role::Window => "window",
142 Role::TitleBar => "titleBar",
143 Role::StaticText => "staticText",
144 Role::TextInput => "textInput",
145 Role::MultilineTextInput => "multilineTextInput",
146 Role::ScrollView => "scrollView",
147 Role::Line => "line",
148 }
149 }
150
151 pub fn parse(name: &str) -> Option<Role> {
152 Role::ALL.iter().copied().find(|r| r.name() == name)
153 }
154
155 pub const ALL: [Role; 26] = [
156 Role::None,
157 Role::Button,
158 Role::Checkbox,
159 Role::Radio,
160 Role::Switch,
161 Role::Slider,
162 Role::Tab,
163 Role::TabList,
164 Role::Link,
165 Role::Heading,
166 Role::List,
167 Role::ListItem,
168 Role::Image,
169 Role::Dialog,
170 Role::Group,
171 Role::Window,
172 Role::TitleBar,
173 Role::StaticText,
174 Role::TextInput,
175 Role::MultilineTextInput,
176 Role::ScrollView,
177 Role::Line,
178 Role::RadioGroup,
179 Role::Menu,
180 Role::MenuItem,
181 Role::Terminal,
182 ];
183
184 pub fn is_control(self) -> bool {
186 matches!(
187 self,
188 Role::Button
189 | Role::Checkbox
190 | Role::Radio
191 | Role::Switch
192 | Role::Slider
193 | Role::Tab
194 | Role::MenuItem
195 | Role::Link
196 | Role::TextInput
197 | Role::MultilineTextInput
198 )
199 }
200
201 pub fn is_editor(self) -> bool {
203 matches!(self, Role::TextInput | Role::MultilineTextInput)
204 }
205
206 pub(crate) fn presentational(self) -> bool {
210 matches!(
211 self,
212 Role::Button
213 | Role::Checkbox
214 | Role::Radio
215 | Role::Switch
216 | Role::Slider
217 | Role::Tab
218 | Role::MenuItem
219 | Role::Link
220 | Role::Heading
221 | Role::Image
222 )
223 }
224}
225
226#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
231pub enum Live {
232 #[default]
234 Off,
235 Polite,
237 Assertive,
239}
240
241impl Live {
242 pub const ALL: &'static [Live] = &[Live::Off, Live::Polite, Live::Assertive];
244
245 pub fn name(self) -> &'static str {
247 match self {
248 Live::Off => "off",
249 Live::Polite => "polite",
250 Live::Assertive => "assertive",
251 }
252 }
253
254 pub fn from_index(i: usize) -> Live {
257 Self::ALL.get(i).copied().unwrap_or_default()
258 }
259}
260
261#[derive(Clone, Debug, PartialEq, Eq)]
267pub struct Announcement {
268 pub text: String,
269 pub live: Live,
272}
273
274#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
282pub enum Orientation {
283 Horizontal,
284 Vertical,
285}
286
287impl Orientation {
288 pub fn name(self) -> &'static str {
289 match self {
290 Orientation::Horizontal => "horizontal",
291 Orientation::Vertical => "vertical",
292 }
293 }
294
295 pub fn parse(name: &str) -> Option<Orientation> {
296 Orientation::ALL.iter().copied().find(|o| o.name() == name)
297 }
298
299 pub const ALL: [Orientation; 2] = [Orientation::Horizontal, Orientation::Vertical];
300}
301
302#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
306pub enum AccessAction {
307 Click,
312 Focus,
315 Blur,
316 SetValue,
320 Increment,
323 Decrement,
324 ScrollIntoView,
326 ScrollUp,
328 ScrollDown,
329 ScrollLeft,
330 ScrollRight,
331 SetTextSelection,
336 ReplaceSelectedText,
340}
341
342impl AccessAction {
343 pub const ALL: [AccessAction; 13] = [
344 AccessAction::Click,
345 AccessAction::Focus,
346 AccessAction::Blur,
347 AccessAction::SetValue,
348 AccessAction::Increment,
349 AccessAction::Decrement,
350 AccessAction::ScrollIntoView,
351 AccessAction::ScrollUp,
352 AccessAction::ScrollDown,
353 AccessAction::ScrollLeft,
354 AccessAction::ScrollRight,
355 AccessAction::SetTextSelection,
356 AccessAction::ReplaceSelectedText,
357 ];
358
359 pub fn bit(self) -> u32 {
361 1 << (self as u32)
362 }
363
364 pub fn name(self) -> &'static str {
365 match self {
366 AccessAction::Click => "click",
367 AccessAction::Focus => "focus",
368 AccessAction::Blur => "blur",
369 AccessAction::SetValue => "setValue",
370 AccessAction::Increment => "increment",
371 AccessAction::Decrement => "decrement",
372 AccessAction::ScrollIntoView => "scrollIntoView",
373 AccessAction::ScrollUp => "scrollUp",
374 AccessAction::ScrollDown => "scrollDown",
375 AccessAction::ScrollLeft => "scrollLeft",
376 AccessAction::ScrollRight => "scrollRight",
377 AccessAction::SetTextSelection => "setTextSelection",
378 AccessAction::ReplaceSelectedText => "replaceSelectedText",
379 }
380 }
381
382 pub fn parse(name: &str) -> Option<AccessAction> {
383 AccessAction::ALL.iter().copied().find(|a| a.name() == name)
384 }
385}
386
387#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
390pub struct TextPos {
391 pub run: Key,
392 pub character: usize,
393}
394
395impl TextPos {
396 pub fn to_value(self, h: Handles) -> Value {
398 Value::map([
399 ("run", (h.key)(self.run)),
400 ("character", Value::Int(self.character as i64)),
401 ])
402 }
403}
404
405#[derive(Clone, Debug, PartialEq)]
408pub struct AccessRequest {
409 pub key: Key,
410 pub action: AccessAction,
411 pub value: Option<String>,
414 pub anchor: Option<TextPos>,
417 pub focus: Option<TextPos>,
418}
419
420impl AccessRequest {
421 pub fn new(key: Key, action: AccessAction) -> Self {
422 AccessRequest {
423 key,
424 action,
425 value: None,
426 anchor: None,
427 focus: None,
428 }
429 }
430
431 pub fn with_value(mut self, value: impl Into<String>) -> Self {
432 self.value = Some(value.into());
433 self
434 }
435
436 pub fn with_selection(mut self, anchor: TextPos, focus: TextPos) -> Self {
437 self.anchor = Some(anchor);
438 self.focus = Some(focus);
439 self
440 }
441}
442
443#[derive(Clone, Copy, Debug, Default, PartialEq)]
445pub struct ScrollState {
446 pub x: f32,
447 pub y: f32,
448 pub max_x: f32,
449 pub max_y: f32,
450}
451
452impl ScrollState {
453 pub fn to_value(self) -> Value {
455 Value::map([
456 ("x", Value::float(self.x)),
457 ("y", Value::float(self.y)),
458 ("max_x", Value::float(self.max_x)),
459 ("max_y", Value::float(self.max_y)),
460 ])
461 }
462}
463
464#[derive(Clone, Debug, PartialEq)]
471pub struct AccessRun {
472 pub key: Key,
474 pub line: usize,
477 pub start: usize,
479 pub end: usize,
480 pub text: String,
481 pub rect: Rect,
485 pub char_lengths: Vec<u8>,
486 pub char_positions: Vec<f32>,
488 pub char_widths: Vec<f32>,
489 pub word_starts: Vec<u8>,
491 pub rtl: bool,
492}
493
494impl AccessRun {
495 pub fn to_value(&self, h: Handles) -> Value {
497 let bytes = |v: &[u8]| Value::list(v.iter().map(|b| Value::Int(*b as i64)));
498 Value::map([
499 ("key", (h.key)(self.key)),
500 ("line", Value::Int(self.line as i64)),
501 ("start", Value::Int(self.start as i64)),
502 ("end", Value::Int(self.end as i64)),
503 ("text", Value::Str(self.text.clone())),
504 ("rect", self.rect.to_value()),
505 ("char_lengths", bytes(&self.char_lengths)),
506 ("char_positions", Value::floats(&self.char_positions)),
507 ("char_widths", Value::floats(&self.char_widths)),
508 ("word_starts", bytes(&self.word_starts)),
509 ("rtl", Value::Bool(self.rtl)),
510 ])
511 }
512}
513
514pub const RUN_CHARS: usize = 200;
516
517#[derive(Clone, Debug, PartialEq)]
519pub struct AccessNode {
520 pub key: Key,
521 pub parent: Option<Key>,
523 pub origin: OriginId,
524 pub role: Role,
525 pub name: Option<String>,
529 pub description: Option<String>,
531 pub rect: Rect,
537 pub value: Option<String>,
546 pub caret: Option<usize>,
548 pub selection: Option<(usize, usize)>,
550 pub runs: Vec<AccessRun>,
552 pub anchor: Option<TextPos>,
555 pub focus: Option<TextPos>,
556 pub checked: Option<bool>,
558 pub mixed: bool,
561 pub selected: Option<bool>,
566 pub expanded: Option<bool>,
569 pub pos_in_set: Option<usize>,
574 pub orientation: Option<Orientation>,
578 pub set_size: Option<usize>,
582 pub number: Option<f32>,
586 pub min: Option<f32>,
587 pub max: Option<f32>,
588 pub step: Option<f32>,
590 pub focused: bool,
592 pub disabled: bool,
594 pub modal: bool,
599 pub scroll: Option<ScrollState>,
600 pub actions: u32,
602 pub live: Live,
608}
609
610impl AccessNode {
611 pub fn to_value(&self, h: Handles) -> Value {
618 Value::map([
619 ("key", (h.key)(self.key)),
620 ("parent", h.opt_key(self.parent)),
621 ("origin", Value::Int(self.origin.0 as i64)),
622 ("role", Value::str(self.role.name())),
623 ("name", Value::opt_str(&self.name)),
624 ("description", Value::opt_str(&self.description)),
625 ("rect", self.rect.to_value()),
626 ("value", Value::opt_str(&self.value)),
627 ("caret", Value::opt_usize(self.caret)),
628 (
629 "selection",
630 Value::opt(self.selection, |(a, b)| {
631 Value::list([Value::Int(a as i64), Value::Int(b as i64)])
632 }),
633 ),
634 ("anchor", Value::opt(self.anchor, |p| p.to_value(h))),
635 ("focus", Value::opt(self.focus, |p| p.to_value(h))),
636 ("runs", Value::list(self.runs.iter().map(|r| r.to_value(h)))),
637 ("checked", Value::opt_bool(self.checked)),
638 ("mixed", Value::Bool(self.mixed)),
639 ("selected", Value::opt_bool(self.selected)),
640 ("expanded", Value::opt_bool(self.expanded)),
641 ("pos_in_set", Value::opt_usize(self.pos_in_set)),
642 ("set_size", Value::opt_usize(self.set_size)),
643 (
644 "orientation",
645 Value::opt(self.orientation, |o| Value::str(o.name())),
646 ),
647 ("live", Value::str(self.live.name())),
648 ("value_now", Value::opt_float(self.number)),
649 ("value_min", Value::opt_float(self.min)),
650 ("value_max", Value::opt_float(self.max)),
651 ("value_step", Value::opt_float(self.step)),
652 ("focused", Value::Bool(self.focused)),
653 ("disabled", Value::Bool(self.disabled)),
654 ("modal", Value::Bool(self.modal)),
655 ("scroll", Value::opt(self.scroll, ScrollState::to_value)),
656 (
657 "actions",
658 Value::list(self.action_list().into_iter().map(|a| Value::str(a.name()))),
659 ),
660 ])
661 }
662
663 pub fn supports(&self, action: AccessAction) -> bool {
664 self.actions & action.bit() != 0
665 }
666
667 pub fn action_list(&self) -> Vec<AccessAction> {
669 AccessAction::ALL
670 .iter()
671 .copied()
672 .filter(|a| self.supports(*a))
673 .collect()
674 }
675
676 pub fn line_offset(&self, pos: TextPos) -> Option<(usize, usize)> {
679 let run = self.runs.iter().find(|r| r.key == pos.run)?;
680 let within = run
681 .text
682 .char_indices()
683 .nth(pos.character)
684 .map_or(run.text.len(), |(b, _)| b);
685 Some((run.line, run.start + within.min(run.end - run.start)))
686 }
687
688 pub fn text_pos(&self, line: usize, offset: usize) -> Option<TextPos> {
694 let chars_before =
695 |r: &AccessRun, at: usize| r.text[..r.text.floor_char_boundary(at)].chars().count();
696 let mut last = None;
697 for r in self.runs.iter().filter(|r| r.line == line) {
698 if offset >= r.start && offset < r.end {
699 return Some(TextPos {
700 run: r.key,
701 character: chars_before(r, offset - r.start),
702 });
703 }
704 last = Some(r);
705 }
706 let r = last?;
707 Some(TextPos {
708 run: r.key,
709 character: chars_before(r, (offset.max(r.start) - r.start).min(r.end - r.start)),
710 })
711 }
712}
713
714#[derive(Clone, Debug, Default, PartialEq)]
717pub struct AccessTree {
718 pub nodes: Vec<AccessNode>,
719 pub focus: Option<Key>,
721 pub hash: u64,
724}
725
726impl AccessTree {
727 pub fn to_value(&self, h: Handles) -> Value {
730 Value::map([
731 (
732 "nodes",
733 Value::list(self.nodes.iter().map(|n| n.to_value(h))),
734 ),
735 ("focus", h.opt_key(self.focus)),
736 ("hash", Value::Str(format!("{:016x}", self.hash))),
737 ])
738 }
739
740 pub fn get(&self, key: Key) -> Option<&AccessNode> {
741 self.nodes.iter().find(|n| n.key == key)
742 }
743
744 pub fn children(&self, key: Key) -> impl Iterator<Item = &AccessNode> {
746 self.nodes.iter().filter(move |n| n.parent == Some(key))
747 }
748
749 pub fn root(&self) -> Option<&AccessNode> {
751 self.nodes.first()
752 }
753}
754
755pub(crate) struct Semantic {
757 pub role: Role,
758 pub name: Option<String>,
759 pub presentational: bool,
761}
762
763pub(crate) fn derived_role(tree: &Tree, i: usize) -> Option<Role> {
766 let spec = &tree.specs[i];
767 if let Some(r) = spec.access().role {
768 return (r != Role::Line).then_some(r);
771 }
772 if i == 0 {
773 return Some(Role::Window);
774 }
775 match tree.content[i] {
776 NodeContent::Text(_) => return Some(Role::StaticText),
777 NodeContent::Edit(_) => return Some(Role::TextInput),
778 NodeContent::Image(..) => return Some(Role::Image),
779 NodeContent::Line(_) | NodeContent::Polygon(_) | NodeContent::Path(_) => {}
784 NodeContent::Cells(_) => return Some(Role::Terminal),
785 NodeContent::Fragment(_) => {}
791 NodeContent::Container => {}
792 }
793 match spec.window {
794 Some(WindowRole::Drag) => return Some(Role::TitleBar),
795 Some(WindowRole::Button(_)) => return Some(Role::Button),
796 None => {}
797 }
798 if spec.events().modal.is_some() {
799 return Some(Role::Dialog);
802 }
803 if spec.events().on_click.is_some() {
804 return Some(Role::Button);
805 }
806 if spec.layout.scroll_x || spec.layout.scroll_y {
807 return Some(Role::ScrollView);
808 }
809 if spec.events().on_key.is_some() || spec.focusable {
810 return Some(Role::Group);
813 }
814 if spec.access().live != Live::Off {
815 return Some(Role::Group);
819 }
820 None
821}
822
823pub(crate) fn is_custom_editor(tree: &Tree, i: usize) -> bool {
826 tree.specs[i].access().role.is_some_and(Role::is_editor)
827 && !matches!(tree.content[i], NodeContent::Edit(_))
828}
829
830pub(crate) fn focusable(tree: &Tree, i: usize) -> bool {
836 let spec = &tree.specs[i];
837 if spec.disabled || spec.access().role == Some(Role::None) || spec.window.is_some() {
838 return false;
839 }
840 if spec.focusable
841 || spec.events().on_key.is_some()
842 || matches!(tree.content[i], NodeContent::Edit(_))
843 {
844 return true;
845 }
846 match spec.access().role {
847 Some(r) => r.is_control(),
848 None => spec.events().on_click.is_some(),
849 }
850}
851
852pub(crate) fn semantic(
856 tree: &Tree,
857 text: &TextSystem,
858 edit: &EditStore,
859 title: Option<&str>,
860 i: usize,
861) -> Option<Semantic> {
862 let mut role = derived_role(tree, i)?;
863 let spec = &tree.specs[i];
864 if role == Role::TextInput
865 && let NodeContent::Edit(key) = tree.content[i]
866 && edit.is_multiline(key)
867 {
868 role = Role::MultilineTextInput;
869 }
870 let presentational = role.presentational()
884 || spec.access().live != Live::Off
885 || matches!(spec.window, Some(WindowRole::Button(_)))
886 || is_custom_editor(tree, i);
887 let name = match (&spec.access().label, spec.window) {
888 (Some(label), _) => Some(label.to_string()),
889 (None, Some(WindowRole::Button(b))) => Some(
890 match b {
891 WindowButton::Close => "Close",
892 WindowButton::Minimize => "Minimize",
893 WindowButton::Maximize => "Maximize",
894 }
895 .to_string(),
896 ),
897 (None, _) => match role {
898 Role::StaticText => match tree.content[i] {
899 NodeContent::Text(id) => Some(text.content(id).to_string()),
900 _ => None,
901 },
902 Role::Window => title.map(str::to_string),
906 _ if role.presentational() || spec.access().live != Live::Off => {
907 content_name(tree, text, i)
908 }
909 _ => None,
910 },
911 };
912 Some(Semantic {
913 role,
914 name,
915 presentational,
916 })
917}
918
919pub(crate) fn live_region_speaks(tree: &Tree, text: &TextSystem, i: usize) -> bool {
925 tree.specs[i].access().label.is_some() || content_name(tree, text, i).is_some()
926}
927
928fn content_name(tree: &Tree, text: &TextSystem, i: usize) -> Option<String> {
938 let end = tree.subtree_end(i);
939 let mut out = String::new();
940 let mut j = i;
941 while j < end {
942 if j > i && tree.specs[j].access().role == Some(Role::None) {
943 j = tree.subtree_end(j);
944 continue;
945 }
946 let at = j;
947 j += 1;
948 if let NodeContent::Text(id) = tree.content[at] {
949 let s = text.content(id).trim();
950 if s.is_empty() {
951 continue;
952 }
953 if !out.is_empty() {
954 out.push(' ');
955 }
956 out.push_str(s);
957 }
958 }
959 (!out.is_empty()).then_some(out)
960}
961
962pub(crate) struct Sources<'a> {
964 pub text: &'a TextSystem,
965 pub cells: &'a crate::cells::CellStore,
966 pub edit: &'a EditStore,
967 pub scroll: &'a ScrollStore,
968 pub title: Option<&'a str>,
969 pub focus: Option<Key>,
971 pub modal: Option<Key>,
973 pub viewport: Size,
974 pub scale: f32,
975 pub clips: &'a [Clip],
980}
981
982fn clip_of(src: &Sources<'_>, i: usize) -> Rect {
984 src.clips.get(i).map_or(NO_CLIP, |c| c.rect)
985}
986
987fn clipped(rect: Rect, clip: Rect) -> Rect {
994 let x0 = rect.x.max(clip.x);
995 let y0 = rect.y.max(clip.y);
996 let x1 = (rect.x + rect.w).min(clip.x + clip.w);
997 let y1 = (rect.y + rect.h).min(clip.y + clip.h);
998 let gone = |lo: f32, hi: f32, extent: f32| hi < lo || (hi == lo && extent > 0.0);
1002 if gone(x0, x1, rect.w) || gone(y0, y1, rect.h) {
1003 let x = rect.x.max(clip.x).min(clip.x + clip.w);
1006 let y = rect.y.max(clip.y).min(clip.y + clip.h);
1007 return Rect::new(x, y, 0.0, 0.0);
1008 }
1009 Rect::new(x0, y0, x1 - x0, y1 - y0)
1010}
1011
1012fn node_rect(tree: &Tree, src: &Sources<'_>, i: usize) -> Rect {
1015 if i == 0 {
1016 return Rect::new(0.0, 0.0, src.viewport.w, src.viewport.h);
1017 }
1018 let rect = Rect::from_pos_size(tree.pos[i], tree.size[i]);
1019 if let Some(c) = src.clips.get(i)
1023 && c.turned()
1024 {
1025 let inner = clipped(rect, c.inner);
1026 if inner.w <= 0.0 && inner.h <= 0.0 {
1027 return inner;
1028 }
1029 return clipped(c.transform.bounds(inner), c.rect);
1030 }
1031 clipped(rect, clip_of(src, i))
1032}
1033
1034fn edit_run_clip(tree: &Tree, src: &Sources<'_>, i: usize, edit_key: Key) -> Rect {
1039 let clip = clip_of(src, i);
1040 if src.edit.folds(edit_key) {
1041 return clip;
1042 }
1043 let pad = tree.specs[i].layout.padding;
1044 let across = Rect::new(
1045 tree.pos[i].x + pad.l,
1046 clip.y,
1047 (tree.size[i].w - pad.x()).max(0.0),
1048 clip.h,
1049 );
1050 clip.intersect(&across)
1051}
1052
1053fn clip_runs(runs: &mut [AccessRun], clip: Rect) {
1057 if clip == NO_CLIP {
1058 return;
1059 }
1060 for run in runs {
1061 let cut = clipped(run.rect, clip);
1062 let dx = run.rect.x - cut.x;
1063 if dx != 0.0 {
1064 for p in &mut run.char_positions {
1065 *p += dx;
1066 }
1067 }
1068 run.rect = cut;
1069 }
1070}
1071
1072pub(crate) fn inputs_hash(tree: &Tree, src: &Sources<'_>) -> Option<u64> {
1098 use std::hash::{Hash, Hasher};
1099 let mut h = rustc_hash::FxHasher::default();
1100 let f = |h: &mut rustc_hash::FxHasher, v: f32| v.to_bits().hash(h);
1101
1102 tree.len().hash(&mut h);
1103 src.focus.map(|k| k.0).hash(&mut h);
1104 src.modal.map(|k| k.0).hash(&mut h);
1105 src.title.hash(&mut h);
1106 f(&mut h, src.viewport.w);
1107 f(&mut h, src.viewport.h);
1108 f(&mut h, src.scale);
1109
1110 let mut skip_until = 0usize;
1111 let mut i = 0usize;
1112 while i < tree.len() {
1113 if i < skip_until {
1114 i += 1;
1115 continue;
1116 }
1117 let Some(sem) = semantic(tree, src.text, src.edit, src.title, i) else {
1118 i += 1;
1119 continue;
1120 };
1121 if sem.role == Role::None {
1122 skip_until = tree.subtree_end(i);
1123 i += 1;
1124 continue;
1125 }
1126 if is_custom_editor(tree, i) {
1127 return None;
1128 }
1129 let key = tree.keys[i];
1130 let spec = &tree.specs[i];
1131 let ax = spec.access();
1132
1133 i.hash(&mut h);
1135 key.0.hash(&mut h);
1136 tree.parent[i].hash(&mut h);
1137 tree.origins[i].0.hash(&mut h);
1138
1139 sem.role.hash(&mut h);
1141 sem.name.hash(&mut h);
1142 sem.presentational.hash(&mut h);
1143 ax.description.as_deref().hash(&mut h);
1144
1145 let rect = node_rect(tree, src, i);
1148 for v in [rect.x, rect.y, rect.w, rect.h] {
1149 f(&mut h, v);
1150 }
1151
1152 (src.focus == Some(key)).hash(&mut h);
1154 (src.modal == Some(key)).hash(&mut h);
1155 spec.disabled.hash(&mut h);
1156 ax.live.hash(&mut h);
1157 spec.window.hash(&mut h);
1158 spec.events().on_click.is_some().hash(&mut h);
1159 focusable(tree, i).hash(&mut h);
1160 crate::composite::orientation(tree, i).hash(&mut h);
1161 ax.expanded.hash(&mut h);
1162 ax.checked.hash(&mut h);
1163 ax.mixed.hash(&mut h);
1164 ax.selected.hash(&mut h);
1165 ax.value_text.as_deref().hash(&mut h);
1166 for v in [ax.value_now, ax.value_min, ax.value_max, ax.value_step] {
1167 v.is_some().hash(&mut h);
1168 f(&mut h, v.unwrap_or(0.0));
1169 }
1170
1171 match tree.content[i] {
1173 NodeContent::Edit(edit_key) => {
1174 let pad = spec.layout.padding;
1175 f(&mut h, tree.pos[i].x + pad.l);
1176 f(&mut h, tree.pos[i].y + pad.t);
1177 src.edit.text(edit_key).hash(&mut h);
1178 src.edit.placeholder(edit_key).hash(&mut h);
1179 src.edit.caret_and_selection(edit_key).hash(&mut h);
1180 src.edit.selection_cursors(edit_key).hash(&mut h);
1181 src.edit.version(edit_key).hash(&mut h);
1187 let clip = edit_run_clip(tree, src, i, edit_key);
1188 for v in [clip.x, clip.y, clip.w, clip.h] {
1189 f(&mut h, v);
1190 }
1191 }
1192 NodeContent::Cells(id) => src.cells.value(id).hash(&mut h),
1193 _ => {}
1194 }
1195
1196 if spec.layout.scroll_x || spec.layout.scroll_y {
1198 spec.layout.scroll_x.hash(&mut h);
1199 spec.layout.scroll_y.hash(&mut h);
1200 let off = src.scroll.drawn(key);
1201 let max = tree.scroll_max[i];
1202 for v in [off.x, off.y, max.x, max.y] {
1203 f(&mut h, v);
1204 }
1205 }
1206
1207 if sem.presentational {
1208 skip_until = tree.subtree_end(i);
1209 }
1210 i += 1;
1211 }
1212 Some(h.finish())
1213}
1214
1215pub(crate) fn build(tree: &Tree, src: &Sources<'_>) -> AccessTree {
1217 let mut out = AccessTree::default();
1218 if tree.is_empty() {
1219 return out;
1220 }
1221 let mut parents: Vec<Option<Key>> = vec![None; tree.len()];
1224 let mut skip_until = 0usize;
1225 let mut i = 0usize;
1226 while i < tree.len() {
1227 if i < skip_until {
1228 i += 1;
1229 continue;
1230 }
1231 let parent = tree.parent[i];
1232 let inherited = if parent == NIL {
1233 None
1234 } else {
1235 parents[parent as usize]
1236 };
1237 let Some(sem) = semantic(tree, src.text, src.edit, src.title, i) else {
1238 parents[i] = inherited;
1239 i += 1;
1240 continue;
1241 };
1242 if sem.role == Role::None {
1243 skip_until = tree.subtree_end(i);
1244 i += 1;
1245 continue;
1246 }
1247 let key = tree.keys[i];
1248 parents[i] = Some(key);
1249 let spec: &NodeSpec = &tree.specs[i];
1250 let rect = node_rect(tree, src, i);
1251 let mut node = AccessNode {
1252 key,
1253 parent: inherited,
1254 origin: tree.origins[i],
1255 role: sem.role,
1256 name: sem.name,
1257 description: spec.access().description.as_ref().map(|d| d.to_string()),
1258 rect,
1259 value: None,
1260 caret: None,
1261 selection: None,
1262 runs: Vec::new(),
1263 anchor: None,
1264 focus: None,
1265 checked: None,
1266 mixed: false,
1267 selected: None,
1268 expanded: None,
1269 orientation: crate::composite::orientation(tree, i),
1270 pos_in_set: None,
1271 set_size: None,
1272 number: None,
1273 min: None,
1274 max: None,
1275 step: None,
1276 focused: src.focus == Some(key),
1277 disabled: spec.disabled,
1278 modal: src.modal == Some(key),
1279 scroll: None,
1280 actions: 0,
1281 live: spec.access().live,
1282 };
1283 let mut actions = 0u32;
1284 match spec.window {
1285 Some(WindowRole::Button(_)) => actions |= AccessAction::Click.bit(),
1286 Some(WindowRole::Drag) => {}
1287 None => {
1288 if spec.events().on_click.is_some() && !spec.disabled {
1289 actions |= AccessAction::Click.bit();
1290 }
1291 }
1292 }
1293 if focusable(tree, i) {
1296 actions |= AccessAction::Focus.bit() | AccessAction::Blur.bit();
1297 }
1298 if let NodeContent::Edit(edit_key) = tree.content[i] {
1299 let pad = spec.layout.padding;
1300 let origin = Vec2::new(tree.pos[i].x + pad.l, tree.pos[i].y + pad.t);
1301 node.value = src.edit.text(edit_key);
1302 if node.description.is_none() {
1305 node.description = src.edit.placeholder(edit_key).map(str::to_string);
1306 }
1307 node.runs = src.edit.runs(edit_key, key, origin, src.scale);
1308 clip_runs(&mut node.runs, edit_run_clip(tree, src, i, edit_key));
1309 if let Some((caret, selection)) = src.edit.caret_and_selection(edit_key) {
1310 node.caret = Some(caret);
1311 node.selection = selection;
1312 }
1313 if let Some((anchor, focus)) = src.edit.selection_cursors(edit_key) {
1314 node.anchor = node.text_pos(anchor.0, anchor.1);
1315 node.focus = node.text_pos(focus.0, focus.1);
1316 }
1317 if !spec.disabled {
1318 actions |= AccessAction::Click.bit()
1319 | AccessAction::SetValue.bit()
1320 | AccessAction::SetTextSelection.bit()
1321 | AccessAction::ReplaceSelectedText.bit();
1322 }
1323 } else if is_custom_editor(tree, i) {
1324 custom_editor(tree, src, i, &mut node);
1325 if !spec.disabled {
1326 actions |= AccessAction::SetValue.bit()
1327 | AccessAction::SetTextSelection.bit()
1328 | AccessAction::ReplaceSelectedText.bit();
1329 }
1330 } else if let NodeContent::Cells(id) = tree.content[i] {
1331 node.value = Some(src.cells.value(id));
1333 }
1334 let ax = spec.access();
1337 node.expanded = ax.expanded;
1338 match sem.role {
1339 Role::Checkbox | Role::Radio | Role::Switch => {
1340 node.checked = Some(ax.checked);
1341 node.mixed = ax.mixed && sem.role == Role::Checkbox;
1342 }
1343 Role::Tab => node.selected = Some(ax.selected),
1349 Role::ListItem | Role::Link if ax.selected => node.selected = Some(true),
1350 Role::MenuItem if ax.checked => node.checked = Some(true),
1357 Role::Slider => {
1358 node.number = ax.value_now;
1359 node.min = ax.value_min;
1360 node.max = ax.value_max;
1361 node.step = ax.value_step;
1362 node.value = ax.value_text.as_deref().map(str::to_owned);
1368 if !spec.disabled {
1371 actions |= AccessAction::Increment.bit()
1372 | AccessAction::Decrement.bit()
1373 | AccessAction::SetValue.bit();
1374 }
1375 }
1376 _ => {}
1377 }
1378 if spec.layout.scroll_x || spec.layout.scroll_y {
1379 let off = src.scroll.drawn(key);
1380 let max = tree.scroll_max[i];
1381 node.scroll = Some(ScrollState {
1382 x: off.x,
1383 y: off.y,
1384 max_x: max.x,
1385 max_y: max.y,
1386 });
1387 if spec.layout.scroll_y {
1388 actions |= AccessAction::ScrollUp.bit() | AccessAction::ScrollDown.bit();
1389 }
1390 if spec.layout.scroll_x {
1391 actions |= AccessAction::ScrollLeft.bit() | AccessAction::ScrollRight.bit();
1392 }
1393 }
1394 if i != 0 {
1395 actions |= AccessAction::ScrollIntoView.bit();
1396 }
1397 node.actions = actions;
1398 if node.focused {
1399 out.focus = Some(key);
1400 }
1401 out.nodes.push(node);
1402 if sem.presentational {
1403 skip_until = tree.subtree_end(i);
1404 }
1405 i += 1;
1406 }
1407 set_positions(tree, &mut out);
1408 out.hash = hash_of(&out);
1409 out
1410}
1411
1412fn set_positions(tree: &Tree, out: &mut AccessTree) {
1425 let containers: Vec<(usize, Role)> = (0..tree.len())
1426 .filter_map(|i| {
1427 let item = crate::composite::item_role(tree.specs[i].access().role?)?;
1428 Some((i, item))
1429 })
1430 .collect();
1431 let mut items: Vec<usize> = Vec::new();
1432 for (container, item) in containers {
1433 crate::composite::items(tree, container, item, &mut items);
1434 if items.is_empty() {
1435 continue;
1436 }
1437 let size = items.len();
1438 let container = tree.keys[container];
1439 let keys: Vec<Key> = items.iter().map(|&i| tree.keys[i]).collect();
1440 for n in &mut out.nodes {
1441 if n.key == container {
1442 n.set_size = Some(size);
1443 } else if let Some(pos) = keys.iter().position(|k| *k == n.key) {
1444 n.pos_in_set = Some(pos);
1445 }
1446 }
1447 }
1448}
1449
1450pub(crate) fn lines_under(tree: &Tree, i: usize) -> Vec<usize> {
1456 let end = tree.subtree_end(i);
1457 let mut lines: Vec<usize> = Vec::new();
1458 let mut j = i + 1;
1459 while j < end {
1460 let spec = &tree.specs[j];
1461 if spec.access().role == Some(Role::None) {
1462 j = tree.subtree_end(j);
1463 continue;
1464 }
1465 if spec.access().role == Some(Role::Line) {
1466 lines.push(j);
1467 j = tree.subtree_end(j);
1468 continue;
1469 }
1470 j += 1;
1471 }
1472 lines
1473}
1474
1475fn custom_editor(tree: &Tree, src: &Sources<'_>, i: usize, node: &mut AccessNode) {
1480 let lines = lines_under(tree, i);
1481 let mut value = String::new();
1482 let mut run_no = 0usize;
1483 let mut caret: Option<(usize, usize)> = None;
1484 let mut anchor: Option<(usize, usize)> = None;
1485 for (ln, &l) in lines.iter().enumerate() {
1486 let last = ln + 1 == lines.len();
1487 let mut line_text = String::new();
1488 let mut texts: Vec<usize> = Vec::new();
1489 let lend = tree.subtree_end(l);
1490 let mut t = l;
1491 while t < lend {
1492 if tree.specs[t].access().role == Some(Role::None) {
1493 t = tree.subtree_end(t);
1494 continue;
1495 }
1496 if let NodeContent::Text(_) = tree.content[t] {
1497 texts.push(t);
1498 }
1499 t += 1;
1500 }
1501 for (k, &t) in texts.iter().enumerate() {
1502 let NodeContent::Text(id) = tree.content[t] else {
1503 continue;
1504 };
1505 let base = line_text.len();
1506 line_text.push_str(src.text.content(id));
1507 let newline = !last && k + 1 == texts.len();
1508 let first = node.runs.len();
1509 src.text.access_runs(
1510 id,
1511 RunSource {
1512 key: node.key,
1513 line: ln,
1514 byte_base: base,
1515 origin: tree.pos[t],
1516 scale: src.scale,
1517 newline_after_last: newline,
1518 },
1519 &mut run_no,
1520 &mut node.runs,
1521 );
1522 clip_runs(&mut node.runs[first..], clip_of(src, t));
1523 }
1524 if texts.is_empty() {
1525 node.runs.push(empty_run(
1527 node.key,
1528 run_no,
1529 ln,
1530 Rect::from_pos_size(tree.pos[l], tree.size[l]),
1531 !last,
1532 ));
1533 let at = node.runs.len() - 1;
1534 clip_runs(&mut node.runs[at..], clip_of(src, l));
1535 run_no += 1;
1536 }
1537 if let Some(c) = tree.specs[l].access().caret {
1538 caret = Some((ln, (c as usize).min(line_text.len())));
1539 }
1540 if let Some(a) = tree.specs[l].access().selection_anchor {
1541 anchor = Some((ln, (a as usize).min(line_text.len())));
1542 }
1543 if ln > 0 {
1544 value.push('\n');
1545 }
1546 value.push_str(&line_text);
1547 }
1548 node.value = Some(value);
1549 if let Some((line, offset)) = caret {
1550 node.focus = node.text_pos(line, offset);
1551 let anchor = anchor.unwrap_or((line, offset));
1552 node.anchor = node.text_pos(anchor.0, anchor.1);
1553 let flat = |(line, offset): (usize, usize)| {
1555 node.value
1556 .as_ref()
1557 .map(|v| v.split('\n').take(line).map(|l| l.len() + 1).sum::<usize>() + offset)
1558 };
1559 node.caret = flat((line, offset));
1560 if anchor != (line, offset)
1561 && let (Some(a), Some(c)) = (flat(anchor), node.caret)
1562 {
1563 node.selection = Some((a.min(c), a.max(c)));
1564 }
1565 }
1566}
1567
1568pub(crate) struct RunSource {
1571 pub key: Key,
1572 pub line: usize,
1574 pub byte_base: usize,
1577 pub origin: Vec2,
1579 pub scale: f32,
1580 pub newline_after_last: bool,
1583}
1584
1585fn run_key(key: Key, n: usize) -> Key {
1588 key.str("\u{1}run").index(n as u64)
1589}
1590
1591fn empty_run(key: Key, n: usize, line: usize, rect: Rect, newline: bool) -> AccessRun {
1592 AccessRun {
1593 key: run_key(key, n),
1594 line,
1595 start: 0,
1596 end: 0,
1597 text: if newline { "\n".into() } else { String::new() },
1598 rect: Rect::new(rect.x, rect.y, 0.0, rect.h),
1599 char_lengths: if newline { vec![1] } else { Vec::new() },
1600 char_positions: if newline { vec![0.0] } else { Vec::new() },
1601 char_widths: if newline { vec![0.0] } else { Vec::new() },
1602 word_starts: Vec::new(),
1603 rtl: false,
1604 }
1605}
1606
1607pub(crate) fn runs_of_buffer(
1612 buffer: &Buffer,
1613 src: RunSource,
1614 run_no: &mut usize,
1615 out: &mut Vec<AccessRun>,
1616) {
1617 let line_count = buffer.lines.len();
1618 let runs: Vec<_> = buffer.layout_runs().collect();
1619 for (r, run) in runs.iter().enumerate() {
1620 let last_of_line = runs.get(r + 1).is_none_or(|next| next.line_i != run.line_i);
1621 let newline = last_of_line && (run.line_i + 1 < line_count || src.newline_after_last);
1622 push_row_runs(
1623 &RowGlyphs {
1624 glyphs: run.glyphs,
1625 text: run.text,
1626 line: run.line_i,
1627 top: run.line_top,
1628 height: run.line_height,
1629 rtl: run.rtl,
1630 dx: 0.0,
1631 base: 0,
1632 newline,
1633 },
1634 &src,
1635 run_no,
1636 out,
1637 );
1638 }
1639}
1640
1641pub(crate) struct RowGlyphs<'a> {
1646 pub glyphs: &'a [cosmic_text::LayoutGlyph],
1647 pub text: &'a str,
1649 pub line: usize,
1651 pub top: f32,
1652 pub height: f32,
1653 pub rtl: bool,
1654 pub dx: f32,
1655 pub base: usize,
1656 pub newline: bool,
1658}
1659
1660pub(crate) fn push_row_runs(
1663 row: &RowGlyphs<'_>,
1664 src: &RunSource,
1665 run_no: &mut usize,
1666 out: &mut Vec<AccessRun>,
1667) {
1668 let scale = src.scale.max(f32::EPSILON);
1669 let newline = row.newline;
1670 {
1671 let (start, end) = row.glyphs.iter().fold((usize::MAX, 0usize), |(s, e), g| {
1673 (s.min(g.start), e.max(g.end))
1674 });
1675 let (start, end) = if row.glyphs.is_empty() {
1676 (0, 0)
1677 } else {
1678 (start, end)
1679 };
1680 let slice = &row.text[start..end];
1681 let x0 = row
1683 .glyphs
1684 .iter()
1685 .map(|g| g.x + row.dx)
1686 .fold(f32::INFINITY, f32::min);
1687 let x0 = if x0.is_finite() { x0 } else { 0.0 };
1688 let mut chars: Vec<(usize, u8, f32, f32)> = Vec::new(); for (rel, c) in slice.char_indices() {
1692 let idx = start + rel;
1693 let len = c.len_utf8();
1694 let (x, w) = match row.glyphs.iter().find(|g| g.start <= idx && idx < g.end) {
1695 Some(g) => {
1696 let span = (g.end - g.start).max(1) as f32;
1697 (
1698 g.x + row.dx + g.w * (idx - g.start) as f32 / span,
1699 g.w * len as f32 / span,
1700 )
1701 }
1702 None => chars.last().map_or((x0, 0.0), |&(_, _, x, w)| (x + w, 0.0)),
1703 };
1704 chars.push((idx, len as u8, x, w));
1705 }
1706 let line_end_x = chars.last().map_or(x0, |&(_, _, x, w)| x + w);
1707 if newline {
1708 chars.push((end, 1, line_end_x, 0.0));
1709 }
1710 let starts: Vec<usize> = slice
1712 .split_word_bound_indices()
1713 .filter(|(_, w)| !w.chars().all(char::is_whitespace))
1714 .map(|(b, _)| slice[..b].chars().count())
1715 .collect();
1716 let mut at = 0usize;
1718 loop {
1719 let chunk_end = (at + RUN_CHARS).min(chars.len());
1720 let chunk = &chars[at..chunk_end];
1721 let chunk_x = chunk.first().map_or(x0, |c| c.2);
1722 let chunk_end_x = chunk.last().map_or(chunk_x, |c| c.2 + c.3);
1723 let byte_start = chunk.first().map_or(end, |c| c.0);
1724 let byte_end = chunk
1725 .last()
1726 .map_or(end, |c| {
1727 if newline && c.0 == end {
1728 end
1729 } else {
1730 c.0 + c.1 as usize
1731 }
1732 })
1733 .min(end);
1734 let mut text = row.text[byte_start.min(end)..byte_end].to_string();
1735 if newline && chunk_end == chars.len() {
1736 text.push('\n');
1737 }
1738 out.push(AccessRun {
1739 key: run_key(src.key, *run_no),
1740 line: src.line + row.line,
1741 start: src.byte_base + row.base + byte_start.min(end),
1742 end: src.byte_base + row.base + byte_end,
1743 text,
1744 rect: Rect::new(
1745 src.origin.x + chunk_x / scale,
1746 src.origin.y + row.top / scale,
1747 (chunk_end_x - chunk_x) / scale,
1748 row.height / scale,
1749 ),
1750 char_lengths: chunk.iter().map(|c| c.1).collect(),
1751 char_positions: chunk.iter().map(|c| (c.2 - chunk_x) / scale).collect(),
1752 char_widths: chunk.iter().map(|c| c.3 / scale).collect(),
1753 word_starts: starts
1754 .iter()
1755 .filter(|&&s| s >= at && s < chunk_end)
1756 .map(|&s| (s - at) as u8)
1757 .collect(),
1758 rtl: row.rtl,
1759 });
1760 *run_no += 1;
1761 at = chunk_end;
1762 if at >= chars.len() {
1763 break;
1764 }
1765 }
1766 }
1767}
1768
1769fn hash_of(tree: &AccessTree) -> u64 {
1770 let mut h = crate::key::FNV_OFFSET;
1771 let mut mix = |bytes: &[u8]| h = crate::key::fnv(h, bytes);
1772 let mix_str = |mix: &mut dyn FnMut(&[u8]), s: &Option<String>| match s {
1773 Some(s) => {
1774 mix(&[1]);
1775 mix(s.as_bytes());
1776 mix(&[0]);
1777 }
1778 None => mix(&[0]),
1779 };
1780 let mix_f32 = |mix: &mut dyn FnMut(&[u8]), v: Option<f32>| match v {
1781 Some(v) => {
1782 mix(&[1]);
1783 mix(&v.to_bits().to_le_bytes());
1784 }
1785 None => mix(&[0]),
1786 };
1787 let mix_pos = |mix: &mut dyn FnMut(&[u8]), p: Option<TextPos>| match p {
1788 Some(p) => {
1789 mix(&p.run.0.to_le_bytes());
1790 mix(&p.character.to_le_bytes());
1791 }
1792 None => mix(&[0]),
1793 };
1794 for n in &tree.nodes {
1795 mix(&n.key.0.to_le_bytes());
1796 mix(&n.parent.map_or(0, |k| k.0).to_le_bytes());
1797 mix(&[n.role as u8]);
1798 mix_str(&mut mix, &n.name);
1799 mix_str(&mut mix, &n.description);
1800 for v in [n.rect.x, n.rect.y, n.rect.w, n.rect.h] {
1801 mix(&v.to_bits().to_le_bytes());
1802 }
1803 mix_str(&mut mix, &n.value);
1804 mix(&n.caret.unwrap_or(usize::MAX).to_le_bytes());
1805 let (a, b) = n.selection.unwrap_or((usize::MAX, usize::MAX));
1806 mix(&a.to_le_bytes());
1807 mix(&b.to_le_bytes());
1808 for r in &n.runs {
1809 mix(&r.key.0.to_le_bytes());
1810 mix(r.text.as_bytes());
1811 mix(&[0]);
1812 for v in [r.rect.x, r.rect.y, r.rect.w, r.rect.h] {
1813 mix(&v.to_bits().to_le_bytes());
1814 }
1815 for v in &r.char_positions {
1816 mix(&v.to_bits().to_le_bytes());
1817 }
1818 }
1819 mix_pos(&mut mix, n.anchor);
1820 mix_pos(&mut mix, n.focus);
1821 mix(&[n.checked.map_or(2, |c| c as u8), n.mixed as u8]);
1822 mix(&n.step.map_or(u32::MAX, f32::to_bits).to_le_bytes());
1823 mix(&[n.selected.map_or(2, |c| c as u8)]);
1824 mix(&[n.expanded.map_or(2, |c| c as u8)]);
1825 mix(&n.pos_in_set.unwrap_or(usize::MAX).to_le_bytes());
1826 mix(&n.set_size.unwrap_or(usize::MAX).to_le_bytes());
1827 mix_f32(&mut mix, n.number);
1828 mix_f32(&mut mix, n.min);
1829 mix_f32(&mut mix, n.max);
1830 mix(&[n.focused as u8, n.disabled as u8, n.modal as u8]);
1831 if let Some(s) = n.scroll {
1832 for v in [s.x, s.y, s.max_x, s.max_y] {
1833 mix(&v.to_bits().to_le_bytes());
1834 }
1835 }
1836 mix(&n.actions.to_le_bytes());
1837 mix(&[n.live as u8]);
1838 }
1839 mix(&tree.focus.map_or(0, |k| k.0).to_le_bytes());
1840 h
1841}
1842
1843pub type Label = Arc<str>;
1846
1847#[cfg(test)]
1848mod tests {
1849 use super::*;
1850
1851 #[test]
1852 fn role_names_round_trip() {
1853 for r in Role::ALL {
1854 assert_eq!(Role::parse(r.name()), Some(r));
1855 }
1856 assert_eq!(Role::parse("nope"), None);
1857 }
1858
1859 #[test]
1860 fn action_bits_are_distinct_and_named() {
1861 let mut seen = 0u32;
1862 for a in AccessAction::ALL {
1863 assert_eq!(seen & a.bit(), 0);
1864 seen |= a.bit();
1865 assert_eq!(AccessAction::parse(a.name()), Some(a));
1866 }
1867 }
1868
1869 #[test]
1870 fn run_keys_never_collide_with_node_keys() {
1871 let k = Key::ROOT.str("editor");
1872 assert_ne!(run_key(k, 0), k.index(0));
1873 assert_ne!(run_key(k, 0), k.str("run"));
1874 assert_ne!(run_key(k, 0), run_key(k, 1));
1875 }
1876}