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.caret_and_selection(edit_key).hash(&mut h);
1179 src.edit.selection_cursors(edit_key).hash(&mut h);
1180 src.edit.version(edit_key).hash(&mut h);
1186 let clip = edit_run_clip(tree, src, i, edit_key);
1187 for v in [clip.x, clip.y, clip.w, clip.h] {
1188 f(&mut h, v);
1189 }
1190 }
1191 NodeContent::Cells(id) => src.cells.value(id).hash(&mut h),
1192 _ => {}
1193 }
1194
1195 if spec.layout.scroll_x || spec.layout.scroll_y {
1197 spec.layout.scroll_x.hash(&mut h);
1198 spec.layout.scroll_y.hash(&mut h);
1199 let off = src.scroll.drawn(key);
1200 let max = tree.scroll_max[i];
1201 for v in [off.x, off.y, max.x, max.y] {
1202 f(&mut h, v);
1203 }
1204 }
1205
1206 if sem.presentational {
1207 skip_until = tree.subtree_end(i);
1208 }
1209 i += 1;
1210 }
1211 Some(h.finish())
1212}
1213
1214pub(crate) fn build(tree: &Tree, src: &Sources<'_>) -> AccessTree {
1216 let mut out = AccessTree::default();
1217 if tree.is_empty() {
1218 return out;
1219 }
1220 let mut parents: Vec<Option<Key>> = vec![None; tree.len()];
1223 let mut skip_until = 0usize;
1224 let mut i = 0usize;
1225 while i < tree.len() {
1226 if i < skip_until {
1227 i += 1;
1228 continue;
1229 }
1230 let parent = tree.parent[i];
1231 let inherited = if parent == NIL {
1232 None
1233 } else {
1234 parents[parent as usize]
1235 };
1236 let Some(sem) = semantic(tree, src.text, src.edit, src.title, i) else {
1237 parents[i] = inherited;
1238 i += 1;
1239 continue;
1240 };
1241 if sem.role == Role::None {
1242 skip_until = tree.subtree_end(i);
1243 i += 1;
1244 continue;
1245 }
1246 let key = tree.keys[i];
1247 parents[i] = Some(key);
1248 let spec: &NodeSpec = &tree.specs[i];
1249 let rect = node_rect(tree, src, i);
1250 let mut node = AccessNode {
1251 key,
1252 parent: inherited,
1253 origin: tree.origins[i],
1254 role: sem.role,
1255 name: sem.name,
1256 description: spec.access().description.as_ref().map(|d| d.to_string()),
1257 rect,
1258 value: None,
1259 caret: None,
1260 selection: None,
1261 runs: Vec::new(),
1262 anchor: None,
1263 focus: None,
1264 checked: None,
1265 mixed: false,
1266 selected: None,
1267 expanded: None,
1268 orientation: crate::composite::orientation(tree, i),
1269 pos_in_set: None,
1270 set_size: None,
1271 number: None,
1272 min: None,
1273 max: None,
1274 step: None,
1275 focused: src.focus == Some(key),
1276 disabled: spec.disabled,
1277 modal: src.modal == Some(key),
1278 scroll: None,
1279 actions: 0,
1280 live: spec.access().live,
1281 };
1282 let mut actions = 0u32;
1283 match spec.window {
1284 Some(WindowRole::Button(_)) => actions |= AccessAction::Click.bit(),
1285 Some(WindowRole::Drag) => {}
1286 None => {
1287 if spec.events().on_click.is_some() && !spec.disabled {
1288 actions |= AccessAction::Click.bit();
1289 }
1290 }
1291 }
1292 if focusable(tree, i) {
1295 actions |= AccessAction::Focus.bit() | AccessAction::Blur.bit();
1296 }
1297 if let NodeContent::Edit(edit_key) = tree.content[i] {
1298 let pad = spec.layout.padding;
1299 let origin = Vec2::new(tree.pos[i].x + pad.l, tree.pos[i].y + pad.t);
1300 node.value = src.edit.text(edit_key);
1301 node.runs = src.edit.runs(edit_key, key, origin, src.scale);
1302 clip_runs(&mut node.runs, edit_run_clip(tree, src, i, edit_key));
1303 if let Some((caret, selection)) = src.edit.caret_and_selection(edit_key) {
1304 node.caret = Some(caret);
1305 node.selection = selection;
1306 }
1307 if let Some((anchor, focus)) = src.edit.selection_cursors(edit_key) {
1308 node.anchor = node.text_pos(anchor.0, anchor.1);
1309 node.focus = node.text_pos(focus.0, focus.1);
1310 }
1311 if !spec.disabled {
1312 actions |= AccessAction::Click.bit()
1313 | AccessAction::SetValue.bit()
1314 | AccessAction::SetTextSelection.bit()
1315 | AccessAction::ReplaceSelectedText.bit();
1316 }
1317 } else if is_custom_editor(tree, i) {
1318 custom_editor(tree, src, i, &mut node);
1319 if !spec.disabled {
1320 actions |= AccessAction::SetValue.bit()
1321 | AccessAction::SetTextSelection.bit()
1322 | AccessAction::ReplaceSelectedText.bit();
1323 }
1324 } else if let NodeContent::Cells(id) = tree.content[i] {
1325 node.value = Some(src.cells.value(id));
1327 }
1328 let ax = spec.access();
1331 node.expanded = ax.expanded;
1332 match sem.role {
1333 Role::Checkbox | Role::Radio | Role::Switch => {
1334 node.checked = Some(ax.checked);
1335 node.mixed = ax.mixed && sem.role == Role::Checkbox;
1336 }
1337 Role::Tab => node.selected = Some(ax.selected),
1343 Role::ListItem | Role::Link if ax.selected => node.selected = Some(true),
1344 Role::MenuItem if ax.checked => node.checked = Some(true),
1351 Role::Slider => {
1352 node.number = ax.value_now;
1353 node.min = ax.value_min;
1354 node.max = ax.value_max;
1355 node.step = ax.value_step;
1356 node.value = ax.value_text.as_deref().map(str::to_owned);
1362 if !spec.disabled {
1365 actions |= AccessAction::Increment.bit()
1366 | AccessAction::Decrement.bit()
1367 | AccessAction::SetValue.bit();
1368 }
1369 }
1370 _ => {}
1371 }
1372 if spec.layout.scroll_x || spec.layout.scroll_y {
1373 let off = src.scroll.drawn(key);
1374 let max = tree.scroll_max[i];
1375 node.scroll = Some(ScrollState {
1376 x: off.x,
1377 y: off.y,
1378 max_x: max.x,
1379 max_y: max.y,
1380 });
1381 if spec.layout.scroll_y {
1382 actions |= AccessAction::ScrollUp.bit() | AccessAction::ScrollDown.bit();
1383 }
1384 if spec.layout.scroll_x {
1385 actions |= AccessAction::ScrollLeft.bit() | AccessAction::ScrollRight.bit();
1386 }
1387 }
1388 if i != 0 {
1389 actions |= AccessAction::ScrollIntoView.bit();
1390 }
1391 node.actions = actions;
1392 if node.focused {
1393 out.focus = Some(key);
1394 }
1395 out.nodes.push(node);
1396 if sem.presentational {
1397 skip_until = tree.subtree_end(i);
1398 }
1399 i += 1;
1400 }
1401 set_positions(tree, &mut out);
1402 out.hash = hash_of(&out);
1403 out
1404}
1405
1406fn set_positions(tree: &Tree, out: &mut AccessTree) {
1419 let containers: Vec<(usize, Role)> = (0..tree.len())
1420 .filter_map(|i| {
1421 let item = crate::composite::item_role(tree.specs[i].access().role?)?;
1422 Some((i, item))
1423 })
1424 .collect();
1425 let mut items: Vec<usize> = Vec::new();
1426 for (container, item) in containers {
1427 crate::composite::items(tree, container, item, &mut items);
1428 if items.is_empty() {
1429 continue;
1430 }
1431 let size = items.len();
1432 let container = tree.keys[container];
1433 let keys: Vec<Key> = items.iter().map(|&i| tree.keys[i]).collect();
1434 for n in &mut out.nodes {
1435 if n.key == container {
1436 n.set_size = Some(size);
1437 } else if let Some(pos) = keys.iter().position(|k| *k == n.key) {
1438 n.pos_in_set = Some(pos);
1439 }
1440 }
1441 }
1442}
1443
1444pub(crate) fn lines_under(tree: &Tree, i: usize) -> Vec<usize> {
1450 let end = tree.subtree_end(i);
1451 let mut lines: Vec<usize> = Vec::new();
1452 let mut j = i + 1;
1453 while j < end {
1454 let spec = &tree.specs[j];
1455 if spec.access().role == Some(Role::None) {
1456 j = tree.subtree_end(j);
1457 continue;
1458 }
1459 if spec.access().role == Some(Role::Line) {
1460 lines.push(j);
1461 j = tree.subtree_end(j);
1462 continue;
1463 }
1464 j += 1;
1465 }
1466 lines
1467}
1468
1469fn custom_editor(tree: &Tree, src: &Sources<'_>, i: usize, node: &mut AccessNode) {
1474 let lines = lines_under(tree, i);
1475 let mut value = String::new();
1476 let mut run_no = 0usize;
1477 let mut caret: Option<(usize, usize)> = None;
1478 let mut anchor: Option<(usize, usize)> = None;
1479 for (ln, &l) in lines.iter().enumerate() {
1480 let last = ln + 1 == lines.len();
1481 let mut line_text = String::new();
1482 let mut texts: Vec<usize> = Vec::new();
1483 let lend = tree.subtree_end(l);
1484 let mut t = l;
1485 while t < lend {
1486 if tree.specs[t].access().role == Some(Role::None) {
1487 t = tree.subtree_end(t);
1488 continue;
1489 }
1490 if let NodeContent::Text(_) = tree.content[t] {
1491 texts.push(t);
1492 }
1493 t += 1;
1494 }
1495 for (k, &t) in texts.iter().enumerate() {
1496 let NodeContent::Text(id) = tree.content[t] else {
1497 continue;
1498 };
1499 let base = line_text.len();
1500 line_text.push_str(src.text.content(id));
1501 let newline = !last && k + 1 == texts.len();
1502 let first = node.runs.len();
1503 src.text.access_runs(
1504 id,
1505 RunSource {
1506 key: node.key,
1507 line: ln,
1508 byte_base: base,
1509 origin: tree.pos[t],
1510 scale: src.scale,
1511 newline_after_last: newline,
1512 },
1513 &mut run_no,
1514 &mut node.runs,
1515 );
1516 clip_runs(&mut node.runs[first..], clip_of(src, t));
1517 }
1518 if texts.is_empty() {
1519 node.runs.push(empty_run(
1521 node.key,
1522 run_no,
1523 ln,
1524 Rect::from_pos_size(tree.pos[l], tree.size[l]),
1525 !last,
1526 ));
1527 let at = node.runs.len() - 1;
1528 clip_runs(&mut node.runs[at..], clip_of(src, l));
1529 run_no += 1;
1530 }
1531 if let Some(c) = tree.specs[l].access().caret {
1532 caret = Some((ln, (c as usize).min(line_text.len())));
1533 }
1534 if let Some(a) = tree.specs[l].access().selection_anchor {
1535 anchor = Some((ln, (a as usize).min(line_text.len())));
1536 }
1537 if ln > 0 {
1538 value.push('\n');
1539 }
1540 value.push_str(&line_text);
1541 }
1542 node.value = Some(value);
1543 if let Some((line, offset)) = caret {
1544 node.focus = node.text_pos(line, offset);
1545 let anchor = anchor.unwrap_or((line, offset));
1546 node.anchor = node.text_pos(anchor.0, anchor.1);
1547 let flat = |(line, offset): (usize, usize)| {
1549 node.value
1550 .as_ref()
1551 .map(|v| v.split('\n').take(line).map(|l| l.len() + 1).sum::<usize>() + offset)
1552 };
1553 node.caret = flat((line, offset));
1554 if anchor != (line, offset)
1555 && let (Some(a), Some(c)) = (flat(anchor), node.caret)
1556 {
1557 node.selection = Some((a.min(c), a.max(c)));
1558 }
1559 }
1560}
1561
1562pub(crate) struct RunSource {
1565 pub key: Key,
1566 pub line: usize,
1568 pub byte_base: usize,
1571 pub origin: Vec2,
1573 pub scale: f32,
1574 pub newline_after_last: bool,
1577}
1578
1579fn run_key(key: Key, n: usize) -> Key {
1582 key.str("\u{1}run").index(n as u64)
1583}
1584
1585fn empty_run(key: Key, n: usize, line: usize, rect: Rect, newline: bool) -> AccessRun {
1586 AccessRun {
1587 key: run_key(key, n),
1588 line,
1589 start: 0,
1590 end: 0,
1591 text: if newline { "\n".into() } else { String::new() },
1592 rect: Rect::new(rect.x, rect.y, 0.0, rect.h),
1593 char_lengths: if newline { vec![1] } else { Vec::new() },
1594 char_positions: if newline { vec![0.0] } else { Vec::new() },
1595 char_widths: if newline { vec![0.0] } else { Vec::new() },
1596 word_starts: Vec::new(),
1597 rtl: false,
1598 }
1599}
1600
1601pub(crate) fn runs_of_buffer(
1606 buffer: &Buffer,
1607 src: RunSource,
1608 run_no: &mut usize,
1609 out: &mut Vec<AccessRun>,
1610) {
1611 let line_count = buffer.lines.len();
1612 let runs: Vec<_> = buffer.layout_runs().collect();
1613 for (r, run) in runs.iter().enumerate() {
1614 let last_of_line = runs.get(r + 1).is_none_or(|next| next.line_i != run.line_i);
1615 let newline = last_of_line && (run.line_i + 1 < line_count || src.newline_after_last);
1616 push_row_runs(
1617 &RowGlyphs {
1618 glyphs: run.glyphs,
1619 text: run.text,
1620 line: run.line_i,
1621 top: run.line_top,
1622 height: run.line_height,
1623 rtl: run.rtl,
1624 dx: 0.0,
1625 base: 0,
1626 newline,
1627 },
1628 &src,
1629 run_no,
1630 out,
1631 );
1632 }
1633}
1634
1635pub(crate) struct RowGlyphs<'a> {
1640 pub glyphs: &'a [cosmic_text::LayoutGlyph],
1641 pub text: &'a str,
1643 pub line: usize,
1645 pub top: f32,
1646 pub height: f32,
1647 pub rtl: bool,
1648 pub dx: f32,
1649 pub base: usize,
1650 pub newline: bool,
1652}
1653
1654pub(crate) fn push_row_runs(
1657 row: &RowGlyphs<'_>,
1658 src: &RunSource,
1659 run_no: &mut usize,
1660 out: &mut Vec<AccessRun>,
1661) {
1662 let scale = src.scale.max(f32::EPSILON);
1663 let newline = row.newline;
1664 {
1665 let (start, end) = row.glyphs.iter().fold((usize::MAX, 0usize), |(s, e), g| {
1667 (s.min(g.start), e.max(g.end))
1668 });
1669 let (start, end) = if row.glyphs.is_empty() {
1670 (0, 0)
1671 } else {
1672 (start, end)
1673 };
1674 let slice = &row.text[start..end];
1675 let x0 = row
1677 .glyphs
1678 .iter()
1679 .map(|g| g.x + row.dx)
1680 .fold(f32::INFINITY, f32::min);
1681 let x0 = if x0.is_finite() { x0 } else { 0.0 };
1682 let mut chars: Vec<(usize, u8, f32, f32)> = Vec::new(); for (rel, c) in slice.char_indices() {
1686 let idx = start + rel;
1687 let len = c.len_utf8();
1688 let (x, w) = match row.glyphs.iter().find(|g| g.start <= idx && idx < g.end) {
1689 Some(g) => {
1690 let span = (g.end - g.start).max(1) as f32;
1691 (
1692 g.x + row.dx + g.w * (idx - g.start) as f32 / span,
1693 g.w * len as f32 / span,
1694 )
1695 }
1696 None => chars.last().map_or((x0, 0.0), |&(_, _, x, w)| (x + w, 0.0)),
1697 };
1698 chars.push((idx, len as u8, x, w));
1699 }
1700 let line_end_x = chars.last().map_or(x0, |&(_, _, x, w)| x + w);
1701 if newline {
1702 chars.push((end, 1, line_end_x, 0.0));
1703 }
1704 let starts: Vec<usize> = slice
1706 .split_word_bound_indices()
1707 .filter(|(_, w)| !w.chars().all(char::is_whitespace))
1708 .map(|(b, _)| slice[..b].chars().count())
1709 .collect();
1710 let mut at = 0usize;
1712 loop {
1713 let chunk_end = (at + RUN_CHARS).min(chars.len());
1714 let chunk = &chars[at..chunk_end];
1715 let chunk_x = chunk.first().map_or(x0, |c| c.2);
1716 let chunk_end_x = chunk.last().map_or(chunk_x, |c| c.2 + c.3);
1717 let byte_start = chunk.first().map_or(end, |c| c.0);
1718 let byte_end = chunk
1719 .last()
1720 .map_or(end, |c| {
1721 if newline && c.0 == end {
1722 end
1723 } else {
1724 c.0 + c.1 as usize
1725 }
1726 })
1727 .min(end);
1728 let mut text = row.text[byte_start.min(end)..byte_end].to_string();
1729 if newline && chunk_end == chars.len() {
1730 text.push('\n');
1731 }
1732 out.push(AccessRun {
1733 key: run_key(src.key, *run_no),
1734 line: src.line + row.line,
1735 start: src.byte_base + row.base + byte_start.min(end),
1736 end: src.byte_base + row.base + byte_end,
1737 text,
1738 rect: Rect::new(
1739 src.origin.x + chunk_x / scale,
1740 src.origin.y + row.top / scale,
1741 (chunk_end_x - chunk_x) / scale,
1742 row.height / scale,
1743 ),
1744 char_lengths: chunk.iter().map(|c| c.1).collect(),
1745 char_positions: chunk.iter().map(|c| (c.2 - chunk_x) / scale).collect(),
1746 char_widths: chunk.iter().map(|c| c.3 / scale).collect(),
1747 word_starts: starts
1748 .iter()
1749 .filter(|&&s| s >= at && s < chunk_end)
1750 .map(|&s| (s - at) as u8)
1751 .collect(),
1752 rtl: row.rtl,
1753 });
1754 *run_no += 1;
1755 at = chunk_end;
1756 if at >= chars.len() {
1757 break;
1758 }
1759 }
1760 }
1761}
1762
1763fn hash_of(tree: &AccessTree) -> u64 {
1764 let mut h = crate::key::FNV_OFFSET;
1765 let mut mix = |bytes: &[u8]| h = crate::key::fnv(h, bytes);
1766 let mix_str = |mix: &mut dyn FnMut(&[u8]), s: &Option<String>| match s {
1767 Some(s) => {
1768 mix(&[1]);
1769 mix(s.as_bytes());
1770 mix(&[0]);
1771 }
1772 None => mix(&[0]),
1773 };
1774 let mix_f32 = |mix: &mut dyn FnMut(&[u8]), v: Option<f32>| match v {
1775 Some(v) => {
1776 mix(&[1]);
1777 mix(&v.to_bits().to_le_bytes());
1778 }
1779 None => mix(&[0]),
1780 };
1781 let mix_pos = |mix: &mut dyn FnMut(&[u8]), p: Option<TextPos>| match p {
1782 Some(p) => {
1783 mix(&p.run.0.to_le_bytes());
1784 mix(&p.character.to_le_bytes());
1785 }
1786 None => mix(&[0]),
1787 };
1788 for n in &tree.nodes {
1789 mix(&n.key.0.to_le_bytes());
1790 mix(&n.parent.map_or(0, |k| k.0).to_le_bytes());
1791 mix(&[n.role as u8]);
1792 mix_str(&mut mix, &n.name);
1793 mix_str(&mut mix, &n.description);
1794 for v in [n.rect.x, n.rect.y, n.rect.w, n.rect.h] {
1795 mix(&v.to_bits().to_le_bytes());
1796 }
1797 mix_str(&mut mix, &n.value);
1798 mix(&n.caret.unwrap_or(usize::MAX).to_le_bytes());
1799 let (a, b) = n.selection.unwrap_or((usize::MAX, usize::MAX));
1800 mix(&a.to_le_bytes());
1801 mix(&b.to_le_bytes());
1802 for r in &n.runs {
1803 mix(&r.key.0.to_le_bytes());
1804 mix(r.text.as_bytes());
1805 mix(&[0]);
1806 for v in [r.rect.x, r.rect.y, r.rect.w, r.rect.h] {
1807 mix(&v.to_bits().to_le_bytes());
1808 }
1809 for v in &r.char_positions {
1810 mix(&v.to_bits().to_le_bytes());
1811 }
1812 }
1813 mix_pos(&mut mix, n.anchor);
1814 mix_pos(&mut mix, n.focus);
1815 mix(&[n.checked.map_or(2, |c| c as u8), n.mixed as u8]);
1816 mix(&n.step.map_or(u32::MAX, f32::to_bits).to_le_bytes());
1817 mix(&[n.selected.map_or(2, |c| c as u8)]);
1818 mix(&[n.expanded.map_or(2, |c| c as u8)]);
1819 mix(&n.pos_in_set.unwrap_or(usize::MAX).to_le_bytes());
1820 mix(&n.set_size.unwrap_or(usize::MAX).to_le_bytes());
1821 mix_f32(&mut mix, n.number);
1822 mix_f32(&mut mix, n.min);
1823 mix_f32(&mut mix, n.max);
1824 mix(&[n.focused as u8, n.disabled as u8, n.modal as u8]);
1825 if let Some(s) = n.scroll {
1826 for v in [s.x, s.y, s.max_x, s.max_y] {
1827 mix(&v.to_bits().to_le_bytes());
1828 }
1829 }
1830 mix(&n.actions.to_le_bytes());
1831 mix(&[n.live as u8]);
1832 }
1833 mix(&tree.focus.map_or(0, |k| k.0).to_le_bytes());
1834 h
1835}
1836
1837pub type Label = Arc<str>;
1840
1841#[cfg(test)]
1842mod tests {
1843 use super::*;
1844
1845 #[test]
1846 fn role_names_round_trip() {
1847 for r in Role::ALL {
1848 assert_eq!(Role::parse(r.name()), Some(r));
1849 }
1850 assert_eq!(Role::parse("nope"), None);
1851 }
1852
1853 #[test]
1854 fn action_bits_are_distinct_and_named() {
1855 let mut seen = 0u32;
1856 for a in AccessAction::ALL {
1857 assert_eq!(seen & a.bit(), 0);
1858 seen |= a.bit();
1859 assert_eq!(AccessAction::parse(a.name()), Some(a));
1860 }
1861 }
1862
1863 #[test]
1864 fn run_keys_never_collide_with_node_keys() {
1865 let k = Key::ROOT.str("editor");
1866 assert_ne!(run_key(k, 0), k.index(0));
1867 assert_ne!(run_key(k, 0), k.str("run"));
1868 assert_ne!(run_key(k, 0), run_key(k, 1));
1869 }
1870}