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> {
691 let mut last = None;
692 for r in self.runs.iter().filter(|r| r.line == line) {
693 if offset >= r.start && offset < r.end {
694 return Some(TextPos {
695 run: r.key,
696 character: r.text[..offset - r.start].chars().count(),
697 });
698 }
699 last = Some(r);
700 }
701 let r = last?;
702 Some(TextPos {
703 run: r.key,
704 character: r.text[..(offset.max(r.start) - r.start).min(r.end - r.start)]
705 .chars()
706 .count(),
707 })
708 }
709}
710
711#[derive(Clone, Debug, Default, PartialEq)]
714pub struct AccessTree {
715 pub nodes: Vec<AccessNode>,
716 pub focus: Option<Key>,
718 pub hash: u64,
721}
722
723impl AccessTree {
724 pub fn to_value(&self, h: Handles) -> Value {
727 Value::map([
728 (
729 "nodes",
730 Value::list(self.nodes.iter().map(|n| n.to_value(h))),
731 ),
732 ("focus", h.opt_key(self.focus)),
733 ("hash", Value::Str(format!("{:016x}", self.hash))),
734 ])
735 }
736
737 pub fn get(&self, key: Key) -> Option<&AccessNode> {
738 self.nodes.iter().find(|n| n.key == key)
739 }
740
741 pub fn children(&self, key: Key) -> impl Iterator<Item = &AccessNode> {
743 self.nodes.iter().filter(move |n| n.parent == Some(key))
744 }
745
746 pub fn root(&self) -> Option<&AccessNode> {
748 self.nodes.first()
749 }
750}
751
752pub(crate) struct Semantic {
754 pub role: Role,
755 pub name: Option<String>,
756 pub presentational: bool,
758}
759
760pub(crate) fn derived_role(tree: &Tree, i: usize) -> Option<Role> {
763 let spec = &tree.specs[i];
764 if let Some(r) = spec.access().role {
765 return (r != Role::Line).then_some(r);
768 }
769 if i == 0 {
770 return Some(Role::Window);
771 }
772 match tree.content[i] {
773 NodeContent::Text(_) => return Some(Role::StaticText),
774 NodeContent::Edit(_) => return Some(Role::TextInput),
775 NodeContent::Image(..) => return Some(Role::Image),
776 NodeContent::Line(_) | NodeContent::Polygon(_) | NodeContent::Path(_) => {}
781 NodeContent::Cells(_) => return Some(Role::Terminal),
782 NodeContent::Fragment(_) => {}
788 NodeContent::Container => {}
789 }
790 match spec.window {
791 Some(WindowRole::Drag) => return Some(Role::TitleBar),
792 Some(WindowRole::Button(_)) => return Some(Role::Button),
793 None => {}
794 }
795 if spec.events().modal.is_some() {
796 return Some(Role::Dialog);
799 }
800 if spec.events().on_click.is_some() {
801 return Some(Role::Button);
802 }
803 if spec.layout.scroll_x || spec.layout.scroll_y {
804 return Some(Role::ScrollView);
805 }
806 if spec.events().on_key.is_some() || spec.focusable {
807 return Some(Role::Group);
810 }
811 if spec.access().live != Live::Off {
812 return Some(Role::Group);
816 }
817 None
818}
819
820pub(crate) fn is_custom_editor(tree: &Tree, i: usize) -> bool {
823 tree.specs[i].access().role.is_some_and(Role::is_editor)
824 && !matches!(tree.content[i], NodeContent::Edit(_))
825}
826
827pub(crate) fn focusable(tree: &Tree, i: usize) -> bool {
833 let spec = &tree.specs[i];
834 if spec.disabled || spec.access().role == Some(Role::None) || spec.window.is_some() {
835 return false;
836 }
837 if spec.focusable
838 || spec.events().on_key.is_some()
839 || matches!(tree.content[i], NodeContent::Edit(_))
840 {
841 return true;
842 }
843 match spec.access().role {
844 Some(r) => r.is_control(),
845 None => spec.events().on_click.is_some(),
846 }
847}
848
849pub(crate) fn semantic(
853 tree: &Tree,
854 text: &TextSystem,
855 edit: &EditStore,
856 title: Option<&str>,
857 i: usize,
858) -> Option<Semantic> {
859 let mut role = derived_role(tree, i)?;
860 let spec = &tree.specs[i];
861 if role == Role::TextInput
862 && let NodeContent::Edit(key) = tree.content[i]
863 && edit.is_multiline(key)
864 {
865 role = Role::MultilineTextInput;
866 }
867 let presentational = role.presentational()
881 || spec.access().live != Live::Off
882 || matches!(spec.window, Some(WindowRole::Button(_)))
883 || is_custom_editor(tree, i);
884 let name = match (&spec.access().label, spec.window) {
885 (Some(label), _) => Some(label.to_string()),
886 (None, Some(WindowRole::Button(b))) => Some(
887 match b {
888 WindowButton::Close => "Close",
889 WindowButton::Minimize => "Minimize",
890 WindowButton::Maximize => "Maximize",
891 }
892 .to_string(),
893 ),
894 (None, _) => match role {
895 Role::StaticText => match tree.content[i] {
896 NodeContent::Text(id) => Some(text.content(id).to_string()),
897 _ => None,
898 },
899 Role::Window => title.map(str::to_string),
903 _ if role.presentational() || spec.access().live != Live::Off => {
904 content_name(tree, text, i)
905 }
906 _ => None,
907 },
908 };
909 Some(Semantic {
910 role,
911 name,
912 presentational,
913 })
914}
915
916pub(crate) fn live_region_speaks(tree: &Tree, text: &TextSystem, i: usize) -> bool {
922 tree.specs[i].access().label.is_some() || content_name(tree, text, i).is_some()
923}
924
925fn content_name(tree: &Tree, text: &TextSystem, i: usize) -> Option<String> {
935 let end = tree.subtree_end(i);
936 let mut out = String::new();
937 let mut j = i;
938 while j < end {
939 if j > i && tree.specs[j].access().role == Some(Role::None) {
940 j = tree.subtree_end(j);
941 continue;
942 }
943 let at = j;
944 j += 1;
945 if let NodeContent::Text(id) = tree.content[at] {
946 let s = text.content(id).trim();
947 if s.is_empty() {
948 continue;
949 }
950 if !out.is_empty() {
951 out.push(' ');
952 }
953 out.push_str(s);
954 }
955 }
956 (!out.is_empty()).then_some(out)
957}
958
959pub(crate) struct Sources<'a> {
961 pub text: &'a TextSystem,
962 pub cells: &'a crate::cells::CellStore,
963 pub edit: &'a EditStore,
964 pub scroll: &'a ScrollStore,
965 pub title: Option<&'a str>,
966 pub focus: Option<Key>,
968 pub modal: Option<Key>,
970 pub viewport: Size,
971 pub scale: f32,
972 pub clips: &'a [Clip],
977}
978
979fn clip_of(src: &Sources<'_>, i: usize) -> Rect {
981 src.clips.get(i).map_or(NO_CLIP, |c| c.rect)
982}
983
984fn clipped(rect: Rect, clip: Rect) -> Rect {
991 let x0 = rect.x.max(clip.x);
992 let y0 = rect.y.max(clip.y);
993 let x1 = (rect.x + rect.w).min(clip.x + clip.w);
994 let y1 = (rect.y + rect.h).min(clip.y + clip.h);
995 let gone = |lo: f32, hi: f32, extent: f32| hi < lo || (hi == lo && extent > 0.0);
999 if gone(x0, x1, rect.w) || gone(y0, y1, rect.h) {
1000 let x = rect.x.max(clip.x).min(clip.x + clip.w);
1003 let y = rect.y.max(clip.y).min(clip.y + clip.h);
1004 return Rect::new(x, y, 0.0, 0.0);
1005 }
1006 Rect::new(x0, y0, x1 - x0, y1 - y0)
1007}
1008
1009fn node_rect(tree: &Tree, src: &Sources<'_>, i: usize) -> Rect {
1012 if i == 0 {
1013 Rect::new(0.0, 0.0, src.viewport.w, src.viewport.h)
1014 } else {
1015 clipped(
1016 Rect::from_pos_size(tree.pos[i], tree.size[i]),
1017 clip_of(src, i),
1018 )
1019 }
1020}
1021
1022fn edit_run_clip(tree: &Tree, src: &Sources<'_>, i: usize, edit_key: Key) -> Rect {
1027 let clip = clip_of(src, i);
1028 if src.edit.folds(edit_key) {
1029 return clip;
1030 }
1031 let pad = tree.specs[i].layout.padding;
1032 let across = Rect::new(
1033 tree.pos[i].x + pad.l,
1034 clip.y,
1035 (tree.size[i].w - pad.x()).max(0.0),
1036 clip.h,
1037 );
1038 clip.intersect(&across)
1039}
1040
1041fn clip_runs(runs: &mut [AccessRun], clip: Rect) {
1045 if clip == NO_CLIP {
1046 return;
1047 }
1048 for run in runs {
1049 let cut = clipped(run.rect, clip);
1050 let dx = run.rect.x - cut.x;
1051 if dx != 0.0 {
1052 for p in &mut run.char_positions {
1053 *p += dx;
1054 }
1055 }
1056 run.rect = cut;
1057 }
1058}
1059
1060pub(crate) fn inputs_hash(tree: &Tree, src: &Sources<'_>) -> Option<u64> {
1086 use std::hash::{Hash, Hasher};
1087 let mut h = rustc_hash::FxHasher::default();
1088 let f = |h: &mut rustc_hash::FxHasher, v: f32| v.to_bits().hash(h);
1089
1090 tree.len().hash(&mut h);
1091 src.focus.map(|k| k.0).hash(&mut h);
1092 src.modal.map(|k| k.0).hash(&mut h);
1093 src.title.hash(&mut h);
1094 f(&mut h, src.viewport.w);
1095 f(&mut h, src.viewport.h);
1096 f(&mut h, src.scale);
1097
1098 let mut skip_until = 0usize;
1099 let mut i = 0usize;
1100 while i < tree.len() {
1101 if i < skip_until {
1102 i += 1;
1103 continue;
1104 }
1105 let Some(sem) = semantic(tree, src.text, src.edit, src.title, i) else {
1106 i += 1;
1107 continue;
1108 };
1109 if sem.role == Role::None {
1110 skip_until = tree.subtree_end(i);
1111 i += 1;
1112 continue;
1113 }
1114 if is_custom_editor(tree, i) {
1115 return None;
1116 }
1117 let key = tree.keys[i];
1118 let spec = &tree.specs[i];
1119 let ax = spec.access();
1120
1121 i.hash(&mut h);
1123 key.0.hash(&mut h);
1124 tree.parent[i].hash(&mut h);
1125 tree.origins[i].0.hash(&mut h);
1126
1127 sem.role.hash(&mut h);
1129 sem.name.hash(&mut h);
1130 sem.presentational.hash(&mut h);
1131 ax.description.as_deref().hash(&mut h);
1132
1133 let rect = node_rect(tree, src, i);
1136 for v in [rect.x, rect.y, rect.w, rect.h] {
1137 f(&mut h, v);
1138 }
1139
1140 (src.focus == Some(key)).hash(&mut h);
1142 (src.modal == Some(key)).hash(&mut h);
1143 spec.disabled.hash(&mut h);
1144 ax.live.hash(&mut h);
1145 spec.window.hash(&mut h);
1146 spec.events().on_click.is_some().hash(&mut h);
1147 focusable(tree, i).hash(&mut h);
1148 crate::composite::orientation(tree, i).hash(&mut h);
1149 ax.expanded.hash(&mut h);
1150 ax.checked.hash(&mut h);
1151 ax.mixed.hash(&mut h);
1152 ax.selected.hash(&mut h);
1153 ax.value_text.as_deref().hash(&mut h);
1154 for v in [ax.value_now, ax.value_min, ax.value_max, ax.value_step] {
1155 v.is_some().hash(&mut h);
1156 f(&mut h, v.unwrap_or(0.0));
1157 }
1158
1159 match tree.content[i] {
1161 NodeContent::Edit(edit_key) => {
1162 let pad = spec.layout.padding;
1163 f(&mut h, tree.pos[i].x + pad.l);
1164 f(&mut h, tree.pos[i].y + pad.t);
1165 src.edit.text(edit_key).hash(&mut h);
1166 src.edit.caret_and_selection(edit_key).hash(&mut h);
1167 src.edit.selection_cursors(edit_key).hash(&mut h);
1168 src.edit.version(edit_key).hash(&mut h);
1174 let clip = edit_run_clip(tree, src, i, edit_key);
1175 for v in [clip.x, clip.y, clip.w, clip.h] {
1176 f(&mut h, v);
1177 }
1178 }
1179 NodeContent::Cells(id) => src.cells.value(id).hash(&mut h),
1180 _ => {}
1181 }
1182
1183 if spec.layout.scroll_x || spec.layout.scroll_y {
1185 spec.layout.scroll_x.hash(&mut h);
1186 spec.layout.scroll_y.hash(&mut h);
1187 let off = src.scroll.drawn(key);
1188 let max = tree.scroll_max[i];
1189 for v in [off.x, off.y, max.x, max.y] {
1190 f(&mut h, v);
1191 }
1192 }
1193
1194 if sem.presentational {
1195 skip_until = tree.subtree_end(i);
1196 }
1197 i += 1;
1198 }
1199 Some(h.finish())
1200}
1201
1202pub(crate) fn build(tree: &Tree, src: &Sources<'_>) -> AccessTree {
1204 let mut out = AccessTree::default();
1205 if tree.is_empty() {
1206 return out;
1207 }
1208 let mut parents: Vec<Option<Key>> = vec![None; tree.len()];
1211 let mut skip_until = 0usize;
1212 let mut i = 0usize;
1213 while i < tree.len() {
1214 if i < skip_until {
1215 i += 1;
1216 continue;
1217 }
1218 let parent = tree.parent[i];
1219 let inherited = if parent == NIL {
1220 None
1221 } else {
1222 parents[parent as usize]
1223 };
1224 let Some(sem) = semantic(tree, src.text, src.edit, src.title, i) else {
1225 parents[i] = inherited;
1226 i += 1;
1227 continue;
1228 };
1229 if sem.role == Role::None {
1230 skip_until = tree.subtree_end(i);
1231 i += 1;
1232 continue;
1233 }
1234 let key = tree.keys[i];
1235 parents[i] = Some(key);
1236 let spec: &NodeSpec = &tree.specs[i];
1237 let rect = node_rect(tree, src, i);
1238 let mut node = AccessNode {
1239 key,
1240 parent: inherited,
1241 origin: tree.origins[i],
1242 role: sem.role,
1243 name: sem.name,
1244 description: spec.access().description.as_ref().map(|d| d.to_string()),
1245 rect,
1246 value: None,
1247 caret: None,
1248 selection: None,
1249 runs: Vec::new(),
1250 anchor: None,
1251 focus: None,
1252 checked: None,
1253 mixed: false,
1254 selected: None,
1255 expanded: None,
1256 orientation: crate::composite::orientation(tree, i),
1257 pos_in_set: None,
1258 set_size: None,
1259 number: None,
1260 min: None,
1261 max: None,
1262 step: None,
1263 focused: src.focus == Some(key),
1264 disabled: spec.disabled,
1265 modal: src.modal == Some(key),
1266 scroll: None,
1267 actions: 0,
1268 live: spec.access().live,
1269 };
1270 let mut actions = 0u32;
1271 match spec.window {
1272 Some(WindowRole::Button(_)) => actions |= AccessAction::Click.bit(),
1273 Some(WindowRole::Drag) => {}
1274 None => {
1275 if spec.events().on_click.is_some() && !spec.disabled {
1276 actions |= AccessAction::Click.bit();
1277 }
1278 }
1279 }
1280 if focusable(tree, i) {
1283 actions |= AccessAction::Focus.bit() | AccessAction::Blur.bit();
1284 }
1285 if let NodeContent::Edit(edit_key) = tree.content[i] {
1286 let pad = spec.layout.padding;
1287 let origin = Vec2::new(tree.pos[i].x + pad.l, tree.pos[i].y + pad.t);
1288 node.value = src.edit.text(edit_key);
1289 node.runs = src.edit.runs(edit_key, key, origin, src.scale);
1290 clip_runs(&mut node.runs, edit_run_clip(tree, src, i, edit_key));
1291 if let Some((caret, selection)) = src.edit.caret_and_selection(edit_key) {
1292 node.caret = Some(caret);
1293 node.selection = selection;
1294 }
1295 if let Some((anchor, focus)) = src.edit.selection_cursors(edit_key) {
1296 node.anchor = node.text_pos(anchor.0, anchor.1);
1297 node.focus = node.text_pos(focus.0, focus.1);
1298 }
1299 if !spec.disabled {
1300 actions |= AccessAction::Click.bit()
1301 | AccessAction::SetValue.bit()
1302 | AccessAction::SetTextSelection.bit()
1303 | AccessAction::ReplaceSelectedText.bit();
1304 }
1305 } else if is_custom_editor(tree, i) {
1306 custom_editor(tree, src, i, &mut node);
1307 if !spec.disabled {
1308 actions |= AccessAction::SetValue.bit()
1309 | AccessAction::SetTextSelection.bit()
1310 | AccessAction::ReplaceSelectedText.bit();
1311 }
1312 } else if let NodeContent::Cells(id) = tree.content[i] {
1313 node.value = Some(src.cells.value(id));
1315 }
1316 let ax = spec.access();
1319 node.expanded = ax.expanded;
1320 match sem.role {
1321 Role::Checkbox | Role::Radio | Role::Switch => {
1322 node.checked = Some(ax.checked);
1323 node.mixed = ax.mixed && sem.role == Role::Checkbox;
1324 }
1325 Role::Tab => node.selected = Some(ax.selected),
1331 Role::ListItem | Role::Link if ax.selected => node.selected = Some(true),
1332 Role::MenuItem if ax.checked => node.checked = Some(true),
1339 Role::Slider => {
1340 node.number = ax.value_now;
1341 node.min = ax.value_min;
1342 node.max = ax.value_max;
1343 node.step = ax.value_step;
1344 node.value = ax.value_text.as_deref().map(str::to_owned);
1350 if !spec.disabled {
1353 actions |= AccessAction::Increment.bit()
1354 | AccessAction::Decrement.bit()
1355 | AccessAction::SetValue.bit();
1356 }
1357 }
1358 _ => {}
1359 }
1360 if spec.layout.scroll_x || spec.layout.scroll_y {
1361 let off = src.scroll.drawn(key);
1362 let max = tree.scroll_max[i];
1363 node.scroll = Some(ScrollState {
1364 x: off.x,
1365 y: off.y,
1366 max_x: max.x,
1367 max_y: max.y,
1368 });
1369 if spec.layout.scroll_y {
1370 actions |= AccessAction::ScrollUp.bit() | AccessAction::ScrollDown.bit();
1371 }
1372 if spec.layout.scroll_x {
1373 actions |= AccessAction::ScrollLeft.bit() | AccessAction::ScrollRight.bit();
1374 }
1375 }
1376 if i != 0 {
1377 actions |= AccessAction::ScrollIntoView.bit();
1378 }
1379 node.actions = actions;
1380 if node.focused {
1381 out.focus = Some(key);
1382 }
1383 out.nodes.push(node);
1384 if sem.presentational {
1385 skip_until = tree.subtree_end(i);
1386 }
1387 i += 1;
1388 }
1389 set_positions(tree, &mut out);
1390 out.hash = hash_of(&out);
1391 out
1392}
1393
1394fn set_positions(tree: &Tree, out: &mut AccessTree) {
1407 let containers: Vec<(usize, Role)> = (0..tree.len())
1408 .filter_map(|i| {
1409 let item = crate::composite::item_role(tree.specs[i].access().role?)?;
1410 Some((i, item))
1411 })
1412 .collect();
1413 let mut items: Vec<usize> = Vec::new();
1414 for (container, item) in containers {
1415 crate::composite::items(tree, container, item, &mut items);
1416 if items.is_empty() {
1417 continue;
1418 }
1419 let size = items.len();
1420 let container = tree.keys[container];
1421 let keys: Vec<Key> = items.iter().map(|&i| tree.keys[i]).collect();
1422 for n in &mut out.nodes {
1423 if n.key == container {
1424 n.set_size = Some(size);
1425 } else if let Some(pos) = keys.iter().position(|k| *k == n.key) {
1426 n.pos_in_set = Some(pos);
1427 }
1428 }
1429 }
1430}
1431
1432pub(crate) fn lines_under(tree: &Tree, i: usize) -> Vec<usize> {
1438 let end = tree.subtree_end(i);
1439 let mut lines: Vec<usize> = Vec::new();
1440 let mut j = i + 1;
1441 while j < end {
1442 let spec = &tree.specs[j];
1443 if spec.access().role == Some(Role::None) {
1444 j = tree.subtree_end(j);
1445 continue;
1446 }
1447 if spec.access().role == Some(Role::Line) {
1448 lines.push(j);
1449 j = tree.subtree_end(j);
1450 continue;
1451 }
1452 j += 1;
1453 }
1454 lines
1455}
1456
1457fn custom_editor(tree: &Tree, src: &Sources<'_>, i: usize, node: &mut AccessNode) {
1462 let lines = lines_under(tree, i);
1463 let mut value = String::new();
1464 let mut run_no = 0usize;
1465 let mut caret: Option<(usize, usize)> = None;
1466 let mut anchor: Option<(usize, usize)> = None;
1467 for (ln, &l) in lines.iter().enumerate() {
1468 let last = ln + 1 == lines.len();
1469 let mut line_text = String::new();
1470 let mut texts: Vec<usize> = Vec::new();
1471 let lend = tree.subtree_end(l);
1472 let mut t = l;
1473 while t < lend {
1474 if tree.specs[t].access().role == Some(Role::None) {
1475 t = tree.subtree_end(t);
1476 continue;
1477 }
1478 if let NodeContent::Text(_) = tree.content[t] {
1479 texts.push(t);
1480 }
1481 t += 1;
1482 }
1483 for (k, &t) in texts.iter().enumerate() {
1484 let NodeContent::Text(id) = tree.content[t] else {
1485 continue;
1486 };
1487 let base = line_text.len();
1488 line_text.push_str(src.text.content(id));
1489 let newline = !last && k + 1 == texts.len();
1490 let first = node.runs.len();
1491 src.text.access_runs(
1492 id,
1493 RunSource {
1494 key: node.key,
1495 line: ln,
1496 byte_base: base,
1497 origin: tree.pos[t],
1498 scale: src.scale,
1499 newline_after_last: newline,
1500 },
1501 &mut run_no,
1502 &mut node.runs,
1503 );
1504 clip_runs(&mut node.runs[first..], clip_of(src, t));
1505 }
1506 if texts.is_empty() {
1507 node.runs.push(empty_run(
1509 node.key,
1510 run_no,
1511 ln,
1512 Rect::from_pos_size(tree.pos[l], tree.size[l]),
1513 !last,
1514 ));
1515 let at = node.runs.len() - 1;
1516 clip_runs(&mut node.runs[at..], clip_of(src, l));
1517 run_no += 1;
1518 }
1519 if let Some(c) = tree.specs[l].access().caret {
1520 caret = Some((ln, (c as usize).min(line_text.len())));
1521 }
1522 if let Some(a) = tree.specs[l].access().selection_anchor {
1523 anchor = Some((ln, (a as usize).min(line_text.len())));
1524 }
1525 if ln > 0 {
1526 value.push('\n');
1527 }
1528 value.push_str(&line_text);
1529 }
1530 node.value = Some(value);
1531 if let Some((line, offset)) = caret {
1532 node.focus = node.text_pos(line, offset);
1533 let anchor = anchor.unwrap_or((line, offset));
1534 node.anchor = node.text_pos(anchor.0, anchor.1);
1535 let flat = |(line, offset): (usize, usize)| {
1537 node.value
1538 .as_ref()
1539 .map(|v| v.split('\n').take(line).map(|l| l.len() + 1).sum::<usize>() + offset)
1540 };
1541 node.caret = flat((line, offset));
1542 if anchor != (line, offset)
1543 && let (Some(a), Some(c)) = (flat(anchor), node.caret)
1544 {
1545 node.selection = Some((a.min(c), a.max(c)));
1546 }
1547 }
1548}
1549
1550pub(crate) struct RunSource {
1553 pub key: Key,
1554 pub line: usize,
1556 pub byte_base: usize,
1559 pub origin: Vec2,
1561 pub scale: f32,
1562 pub newline_after_last: bool,
1565}
1566
1567fn run_key(key: Key, n: usize) -> Key {
1570 key.str("\u{1}run").index(n as u64)
1571}
1572
1573fn empty_run(key: Key, n: usize, line: usize, rect: Rect, newline: bool) -> AccessRun {
1574 AccessRun {
1575 key: run_key(key, n),
1576 line,
1577 start: 0,
1578 end: 0,
1579 text: if newline { "\n".into() } else { String::new() },
1580 rect: Rect::new(rect.x, rect.y, 0.0, rect.h),
1581 char_lengths: if newline { vec![1] } else { Vec::new() },
1582 char_positions: if newline { vec![0.0] } else { Vec::new() },
1583 char_widths: if newline { vec![0.0] } else { Vec::new() },
1584 word_starts: Vec::new(),
1585 rtl: false,
1586 }
1587}
1588
1589pub(crate) fn runs_of_buffer(
1594 buffer: &Buffer,
1595 src: RunSource,
1596 run_no: &mut usize,
1597 out: &mut Vec<AccessRun>,
1598) {
1599 let line_count = buffer.lines.len();
1600 let runs: Vec<_> = buffer.layout_runs().collect();
1601 for (r, run) in runs.iter().enumerate() {
1602 let last_of_line = runs.get(r + 1).is_none_or(|next| next.line_i != run.line_i);
1603 let newline = last_of_line && (run.line_i + 1 < line_count || src.newline_after_last);
1604 push_row_runs(
1605 &RowGlyphs {
1606 glyphs: run.glyphs,
1607 text: run.text,
1608 line: run.line_i,
1609 top: run.line_top,
1610 height: run.line_height,
1611 rtl: run.rtl,
1612 dx: 0.0,
1613 base: 0,
1614 newline,
1615 },
1616 &src,
1617 run_no,
1618 out,
1619 );
1620 }
1621}
1622
1623pub(crate) struct RowGlyphs<'a> {
1628 pub glyphs: &'a [cosmic_text::LayoutGlyph],
1629 pub text: &'a str,
1631 pub line: usize,
1633 pub top: f32,
1634 pub height: f32,
1635 pub rtl: bool,
1636 pub dx: f32,
1637 pub base: usize,
1638 pub newline: bool,
1640}
1641
1642pub(crate) fn push_row_runs(
1645 row: &RowGlyphs<'_>,
1646 src: &RunSource,
1647 run_no: &mut usize,
1648 out: &mut Vec<AccessRun>,
1649) {
1650 let scale = src.scale.max(f32::EPSILON);
1651 let newline = row.newline;
1652 {
1653 let (start, end) = row.glyphs.iter().fold((usize::MAX, 0usize), |(s, e), g| {
1655 (s.min(g.start), e.max(g.end))
1656 });
1657 let (start, end) = if row.glyphs.is_empty() {
1658 (0, 0)
1659 } else {
1660 (start, end)
1661 };
1662 let slice = &row.text[start..end];
1663 let x0 = row
1665 .glyphs
1666 .iter()
1667 .map(|g| g.x + row.dx)
1668 .fold(f32::INFINITY, f32::min);
1669 let x0 = if x0.is_finite() { x0 } else { 0.0 };
1670 let mut chars: Vec<(usize, u8, f32, f32)> = Vec::new(); for (rel, c) in slice.char_indices() {
1674 let idx = start + rel;
1675 let len = c.len_utf8();
1676 let (x, w) = match row.glyphs.iter().find(|g| g.start <= idx && idx < g.end) {
1677 Some(g) => {
1678 let span = (g.end - g.start).max(1) as f32;
1679 (
1680 g.x + row.dx + g.w * (idx - g.start) as f32 / span,
1681 g.w * len as f32 / span,
1682 )
1683 }
1684 None => chars.last().map_or((x0, 0.0), |&(_, _, x, w)| (x + w, 0.0)),
1685 };
1686 chars.push((idx, len as u8, x, w));
1687 }
1688 let line_end_x = chars.last().map_or(x0, |&(_, _, x, w)| x + w);
1689 if newline {
1690 chars.push((end, 1, line_end_x, 0.0));
1691 }
1692 let starts: Vec<usize> = slice
1694 .split_word_bound_indices()
1695 .filter(|(_, w)| !w.chars().all(char::is_whitespace))
1696 .map(|(b, _)| slice[..b].chars().count())
1697 .collect();
1698 let mut at = 0usize;
1700 loop {
1701 let chunk_end = (at + RUN_CHARS).min(chars.len());
1702 let chunk = &chars[at..chunk_end];
1703 let chunk_x = chunk.first().map_or(x0, |c| c.2);
1704 let chunk_end_x = chunk.last().map_or(chunk_x, |c| c.2 + c.3);
1705 let byte_start = chunk.first().map_or(end, |c| c.0);
1706 let byte_end = chunk
1707 .last()
1708 .map_or(end, |c| {
1709 if newline && c.0 == end {
1710 end
1711 } else {
1712 c.0 + c.1 as usize
1713 }
1714 })
1715 .min(end);
1716 let mut text = row.text[byte_start.min(end)..byte_end].to_string();
1717 if newline && chunk_end == chars.len() {
1718 text.push('\n');
1719 }
1720 out.push(AccessRun {
1721 key: run_key(src.key, *run_no),
1722 line: src.line + row.line,
1723 start: src.byte_base + row.base + byte_start.min(end),
1724 end: src.byte_base + row.base + byte_end,
1725 text,
1726 rect: Rect::new(
1727 src.origin.x + chunk_x / scale,
1728 src.origin.y + row.top / scale,
1729 (chunk_end_x - chunk_x) / scale,
1730 row.height / scale,
1731 ),
1732 char_lengths: chunk.iter().map(|c| c.1).collect(),
1733 char_positions: chunk.iter().map(|c| (c.2 - chunk_x) / scale).collect(),
1734 char_widths: chunk.iter().map(|c| c.3 / scale).collect(),
1735 word_starts: starts
1736 .iter()
1737 .filter(|&&s| s >= at && s < chunk_end)
1738 .map(|&s| (s - at) as u8)
1739 .collect(),
1740 rtl: row.rtl,
1741 });
1742 *run_no += 1;
1743 at = chunk_end;
1744 if at >= chars.len() {
1745 break;
1746 }
1747 }
1748 }
1749}
1750
1751fn hash_of(tree: &AccessTree) -> u64 {
1752 let mut h = crate::key::FNV_OFFSET;
1753 let mut mix = |bytes: &[u8]| h = crate::key::fnv(h, bytes);
1754 let mix_str = |mix: &mut dyn FnMut(&[u8]), s: &Option<String>| match s {
1755 Some(s) => {
1756 mix(&[1]);
1757 mix(s.as_bytes());
1758 mix(&[0]);
1759 }
1760 None => mix(&[0]),
1761 };
1762 let mix_f32 = |mix: &mut dyn FnMut(&[u8]), v: Option<f32>| match v {
1763 Some(v) => {
1764 mix(&[1]);
1765 mix(&v.to_bits().to_le_bytes());
1766 }
1767 None => mix(&[0]),
1768 };
1769 let mix_pos = |mix: &mut dyn FnMut(&[u8]), p: Option<TextPos>| match p {
1770 Some(p) => {
1771 mix(&p.run.0.to_le_bytes());
1772 mix(&p.character.to_le_bytes());
1773 }
1774 None => mix(&[0]),
1775 };
1776 for n in &tree.nodes {
1777 mix(&n.key.0.to_le_bytes());
1778 mix(&n.parent.map_or(0, |k| k.0).to_le_bytes());
1779 mix(&[n.role as u8]);
1780 mix_str(&mut mix, &n.name);
1781 mix_str(&mut mix, &n.description);
1782 for v in [n.rect.x, n.rect.y, n.rect.w, n.rect.h] {
1783 mix(&v.to_bits().to_le_bytes());
1784 }
1785 mix_str(&mut mix, &n.value);
1786 mix(&n.caret.unwrap_or(usize::MAX).to_le_bytes());
1787 let (a, b) = n.selection.unwrap_or((usize::MAX, usize::MAX));
1788 mix(&a.to_le_bytes());
1789 mix(&b.to_le_bytes());
1790 for r in &n.runs {
1791 mix(&r.key.0.to_le_bytes());
1792 mix(r.text.as_bytes());
1793 mix(&[0]);
1794 for v in [r.rect.x, r.rect.y, r.rect.w, r.rect.h] {
1795 mix(&v.to_bits().to_le_bytes());
1796 }
1797 for v in &r.char_positions {
1798 mix(&v.to_bits().to_le_bytes());
1799 }
1800 }
1801 mix_pos(&mut mix, n.anchor);
1802 mix_pos(&mut mix, n.focus);
1803 mix(&[n.checked.map_or(2, |c| c as u8), n.mixed as u8]);
1804 mix(&n.step.map_or(u32::MAX, f32::to_bits).to_le_bytes());
1805 mix(&[n.selected.map_or(2, |c| c as u8)]);
1806 mix(&[n.expanded.map_or(2, |c| c as u8)]);
1807 mix(&n.pos_in_set.unwrap_or(usize::MAX).to_le_bytes());
1808 mix(&n.set_size.unwrap_or(usize::MAX).to_le_bytes());
1809 mix_f32(&mut mix, n.number);
1810 mix_f32(&mut mix, n.min);
1811 mix_f32(&mut mix, n.max);
1812 mix(&[n.focused as u8, n.disabled as u8, n.modal as u8]);
1813 if let Some(s) = n.scroll {
1814 for v in [s.x, s.y, s.max_x, s.max_y] {
1815 mix(&v.to_bits().to_le_bytes());
1816 }
1817 }
1818 mix(&n.actions.to_le_bytes());
1819 mix(&[n.live as u8]);
1820 }
1821 mix(&tree.focus.map_or(0, |k| k.0).to_le_bytes());
1822 h
1823}
1824
1825pub type Label = Arc<str>;
1828
1829#[cfg(test)]
1830mod tests {
1831 use super::*;
1832
1833 #[test]
1834 fn role_names_round_trip() {
1835 for r in Role::ALL {
1836 assert_eq!(Role::parse(r.name()), Some(r));
1837 }
1838 assert_eq!(Role::parse("nope"), None);
1839 }
1840
1841 #[test]
1842 fn action_bits_are_distinct_and_named() {
1843 let mut seen = 0u32;
1844 for a in AccessAction::ALL {
1845 assert_eq!(seen & a.bit(), 0);
1846 seen |= a.bit();
1847 assert_eq!(AccessAction::parse(a.name()), Some(a));
1848 }
1849 }
1850
1851 #[test]
1852 fn run_keys_never_collide_with_node_keys() {
1853 let k = Key::ROOT.str("editor");
1854 assert_ne!(run_key(k, 0), k.index(0));
1855 assert_ne!(run_key(k, 0), k.str("run"));
1856 assert_ne!(run_key(k, 0), run_key(k, 1));
1857 }
1858}