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 Rect::new(0.0, 0.0, src.viewport.w, src.viewport.h)
1017 } else {
1018 clipped(
1019 Rect::from_pos_size(tree.pos[i], tree.size[i]),
1020 clip_of(src, i),
1021 )
1022 }
1023}
1024
1025fn edit_run_clip(tree: &Tree, src: &Sources<'_>, i: usize, edit_key: Key) -> Rect {
1030 let clip = clip_of(src, i);
1031 if src.edit.folds(edit_key) {
1032 return clip;
1033 }
1034 let pad = tree.specs[i].layout.padding;
1035 let across = Rect::new(
1036 tree.pos[i].x + pad.l,
1037 clip.y,
1038 (tree.size[i].w - pad.x()).max(0.0),
1039 clip.h,
1040 );
1041 clip.intersect(&across)
1042}
1043
1044fn clip_runs(runs: &mut [AccessRun], clip: Rect) {
1048 if clip == NO_CLIP {
1049 return;
1050 }
1051 for run in runs {
1052 let cut = clipped(run.rect, clip);
1053 let dx = run.rect.x - cut.x;
1054 if dx != 0.0 {
1055 for p in &mut run.char_positions {
1056 *p += dx;
1057 }
1058 }
1059 run.rect = cut;
1060 }
1061}
1062
1063pub(crate) fn inputs_hash(tree: &Tree, src: &Sources<'_>) -> Option<u64> {
1089 use std::hash::{Hash, Hasher};
1090 let mut h = rustc_hash::FxHasher::default();
1091 let f = |h: &mut rustc_hash::FxHasher, v: f32| v.to_bits().hash(h);
1092
1093 tree.len().hash(&mut h);
1094 src.focus.map(|k| k.0).hash(&mut h);
1095 src.modal.map(|k| k.0).hash(&mut h);
1096 src.title.hash(&mut h);
1097 f(&mut h, src.viewport.w);
1098 f(&mut h, src.viewport.h);
1099 f(&mut h, src.scale);
1100
1101 let mut skip_until = 0usize;
1102 let mut i = 0usize;
1103 while i < tree.len() {
1104 if i < skip_until {
1105 i += 1;
1106 continue;
1107 }
1108 let Some(sem) = semantic(tree, src.text, src.edit, src.title, i) else {
1109 i += 1;
1110 continue;
1111 };
1112 if sem.role == Role::None {
1113 skip_until = tree.subtree_end(i);
1114 i += 1;
1115 continue;
1116 }
1117 if is_custom_editor(tree, i) {
1118 return None;
1119 }
1120 let key = tree.keys[i];
1121 let spec = &tree.specs[i];
1122 let ax = spec.access();
1123
1124 i.hash(&mut h);
1126 key.0.hash(&mut h);
1127 tree.parent[i].hash(&mut h);
1128 tree.origins[i].0.hash(&mut h);
1129
1130 sem.role.hash(&mut h);
1132 sem.name.hash(&mut h);
1133 sem.presentational.hash(&mut h);
1134 ax.description.as_deref().hash(&mut h);
1135
1136 let rect = node_rect(tree, src, i);
1139 for v in [rect.x, rect.y, rect.w, rect.h] {
1140 f(&mut h, v);
1141 }
1142
1143 (src.focus == Some(key)).hash(&mut h);
1145 (src.modal == Some(key)).hash(&mut h);
1146 spec.disabled.hash(&mut h);
1147 ax.live.hash(&mut h);
1148 spec.window.hash(&mut h);
1149 spec.events().on_click.is_some().hash(&mut h);
1150 focusable(tree, i).hash(&mut h);
1151 crate::composite::orientation(tree, i).hash(&mut h);
1152 ax.expanded.hash(&mut h);
1153 ax.checked.hash(&mut h);
1154 ax.mixed.hash(&mut h);
1155 ax.selected.hash(&mut h);
1156 ax.value_text.as_deref().hash(&mut h);
1157 for v in [ax.value_now, ax.value_min, ax.value_max, ax.value_step] {
1158 v.is_some().hash(&mut h);
1159 f(&mut h, v.unwrap_or(0.0));
1160 }
1161
1162 match tree.content[i] {
1164 NodeContent::Edit(edit_key) => {
1165 let pad = spec.layout.padding;
1166 f(&mut h, tree.pos[i].x + pad.l);
1167 f(&mut h, tree.pos[i].y + pad.t);
1168 src.edit.text(edit_key).hash(&mut h);
1169 src.edit.caret_and_selection(edit_key).hash(&mut h);
1170 src.edit.selection_cursors(edit_key).hash(&mut h);
1171 src.edit.version(edit_key).hash(&mut h);
1177 let clip = edit_run_clip(tree, src, i, edit_key);
1178 for v in [clip.x, clip.y, clip.w, clip.h] {
1179 f(&mut h, v);
1180 }
1181 }
1182 NodeContent::Cells(id) => src.cells.value(id).hash(&mut h),
1183 _ => {}
1184 }
1185
1186 if spec.layout.scroll_x || spec.layout.scroll_y {
1188 spec.layout.scroll_x.hash(&mut h);
1189 spec.layout.scroll_y.hash(&mut h);
1190 let off = src.scroll.drawn(key);
1191 let max = tree.scroll_max[i];
1192 for v in [off.x, off.y, max.x, max.y] {
1193 f(&mut h, v);
1194 }
1195 }
1196
1197 if sem.presentational {
1198 skip_until = tree.subtree_end(i);
1199 }
1200 i += 1;
1201 }
1202 Some(h.finish())
1203}
1204
1205pub(crate) fn build(tree: &Tree, src: &Sources<'_>) -> AccessTree {
1207 let mut out = AccessTree::default();
1208 if tree.is_empty() {
1209 return out;
1210 }
1211 let mut parents: Vec<Option<Key>> = vec![None; tree.len()];
1214 let mut skip_until = 0usize;
1215 let mut i = 0usize;
1216 while i < tree.len() {
1217 if i < skip_until {
1218 i += 1;
1219 continue;
1220 }
1221 let parent = tree.parent[i];
1222 let inherited = if parent == NIL {
1223 None
1224 } else {
1225 parents[parent as usize]
1226 };
1227 let Some(sem) = semantic(tree, src.text, src.edit, src.title, i) else {
1228 parents[i] = inherited;
1229 i += 1;
1230 continue;
1231 };
1232 if sem.role == Role::None {
1233 skip_until = tree.subtree_end(i);
1234 i += 1;
1235 continue;
1236 }
1237 let key = tree.keys[i];
1238 parents[i] = Some(key);
1239 let spec: &NodeSpec = &tree.specs[i];
1240 let rect = node_rect(tree, src, i);
1241 let mut node = AccessNode {
1242 key,
1243 parent: inherited,
1244 origin: tree.origins[i],
1245 role: sem.role,
1246 name: sem.name,
1247 description: spec.access().description.as_ref().map(|d| d.to_string()),
1248 rect,
1249 value: None,
1250 caret: None,
1251 selection: None,
1252 runs: Vec::new(),
1253 anchor: None,
1254 focus: None,
1255 checked: None,
1256 mixed: false,
1257 selected: None,
1258 expanded: None,
1259 orientation: crate::composite::orientation(tree, i),
1260 pos_in_set: None,
1261 set_size: None,
1262 number: None,
1263 min: None,
1264 max: None,
1265 step: None,
1266 focused: src.focus == Some(key),
1267 disabled: spec.disabled,
1268 modal: src.modal == Some(key),
1269 scroll: None,
1270 actions: 0,
1271 live: spec.access().live,
1272 };
1273 let mut actions = 0u32;
1274 match spec.window {
1275 Some(WindowRole::Button(_)) => actions |= AccessAction::Click.bit(),
1276 Some(WindowRole::Drag) => {}
1277 None => {
1278 if spec.events().on_click.is_some() && !spec.disabled {
1279 actions |= AccessAction::Click.bit();
1280 }
1281 }
1282 }
1283 if focusable(tree, i) {
1286 actions |= AccessAction::Focus.bit() | AccessAction::Blur.bit();
1287 }
1288 if let NodeContent::Edit(edit_key) = tree.content[i] {
1289 let pad = spec.layout.padding;
1290 let origin = Vec2::new(tree.pos[i].x + pad.l, tree.pos[i].y + pad.t);
1291 node.value = src.edit.text(edit_key);
1292 node.runs = src.edit.runs(edit_key, key, origin, src.scale);
1293 clip_runs(&mut node.runs, edit_run_clip(tree, src, i, edit_key));
1294 if let Some((caret, selection)) = src.edit.caret_and_selection(edit_key) {
1295 node.caret = Some(caret);
1296 node.selection = selection;
1297 }
1298 if let Some((anchor, focus)) = src.edit.selection_cursors(edit_key) {
1299 node.anchor = node.text_pos(anchor.0, anchor.1);
1300 node.focus = node.text_pos(focus.0, focus.1);
1301 }
1302 if !spec.disabled {
1303 actions |= AccessAction::Click.bit()
1304 | AccessAction::SetValue.bit()
1305 | AccessAction::SetTextSelection.bit()
1306 | AccessAction::ReplaceSelectedText.bit();
1307 }
1308 } else if is_custom_editor(tree, i) {
1309 custom_editor(tree, src, i, &mut node);
1310 if !spec.disabled {
1311 actions |= AccessAction::SetValue.bit()
1312 | AccessAction::SetTextSelection.bit()
1313 | AccessAction::ReplaceSelectedText.bit();
1314 }
1315 } else if let NodeContent::Cells(id) = tree.content[i] {
1316 node.value = Some(src.cells.value(id));
1318 }
1319 let ax = spec.access();
1322 node.expanded = ax.expanded;
1323 match sem.role {
1324 Role::Checkbox | Role::Radio | Role::Switch => {
1325 node.checked = Some(ax.checked);
1326 node.mixed = ax.mixed && sem.role == Role::Checkbox;
1327 }
1328 Role::Tab => node.selected = Some(ax.selected),
1334 Role::ListItem | Role::Link if ax.selected => node.selected = Some(true),
1335 Role::MenuItem if ax.checked => node.checked = Some(true),
1342 Role::Slider => {
1343 node.number = ax.value_now;
1344 node.min = ax.value_min;
1345 node.max = ax.value_max;
1346 node.step = ax.value_step;
1347 node.value = ax.value_text.as_deref().map(str::to_owned);
1353 if !spec.disabled {
1356 actions |= AccessAction::Increment.bit()
1357 | AccessAction::Decrement.bit()
1358 | AccessAction::SetValue.bit();
1359 }
1360 }
1361 _ => {}
1362 }
1363 if spec.layout.scroll_x || spec.layout.scroll_y {
1364 let off = src.scroll.drawn(key);
1365 let max = tree.scroll_max[i];
1366 node.scroll = Some(ScrollState {
1367 x: off.x,
1368 y: off.y,
1369 max_x: max.x,
1370 max_y: max.y,
1371 });
1372 if spec.layout.scroll_y {
1373 actions |= AccessAction::ScrollUp.bit() | AccessAction::ScrollDown.bit();
1374 }
1375 if spec.layout.scroll_x {
1376 actions |= AccessAction::ScrollLeft.bit() | AccessAction::ScrollRight.bit();
1377 }
1378 }
1379 if i != 0 {
1380 actions |= AccessAction::ScrollIntoView.bit();
1381 }
1382 node.actions = actions;
1383 if node.focused {
1384 out.focus = Some(key);
1385 }
1386 out.nodes.push(node);
1387 if sem.presentational {
1388 skip_until = tree.subtree_end(i);
1389 }
1390 i += 1;
1391 }
1392 set_positions(tree, &mut out);
1393 out.hash = hash_of(&out);
1394 out
1395}
1396
1397fn set_positions(tree: &Tree, out: &mut AccessTree) {
1410 let containers: Vec<(usize, Role)> = (0..tree.len())
1411 .filter_map(|i| {
1412 let item = crate::composite::item_role(tree.specs[i].access().role?)?;
1413 Some((i, item))
1414 })
1415 .collect();
1416 let mut items: Vec<usize> = Vec::new();
1417 for (container, item) in containers {
1418 crate::composite::items(tree, container, item, &mut items);
1419 if items.is_empty() {
1420 continue;
1421 }
1422 let size = items.len();
1423 let container = tree.keys[container];
1424 let keys: Vec<Key> = items.iter().map(|&i| tree.keys[i]).collect();
1425 for n in &mut out.nodes {
1426 if n.key == container {
1427 n.set_size = Some(size);
1428 } else if let Some(pos) = keys.iter().position(|k| *k == n.key) {
1429 n.pos_in_set = Some(pos);
1430 }
1431 }
1432 }
1433}
1434
1435pub(crate) fn lines_under(tree: &Tree, i: usize) -> Vec<usize> {
1441 let end = tree.subtree_end(i);
1442 let mut lines: Vec<usize> = Vec::new();
1443 let mut j = i + 1;
1444 while j < end {
1445 let spec = &tree.specs[j];
1446 if spec.access().role == Some(Role::None) {
1447 j = tree.subtree_end(j);
1448 continue;
1449 }
1450 if spec.access().role == Some(Role::Line) {
1451 lines.push(j);
1452 j = tree.subtree_end(j);
1453 continue;
1454 }
1455 j += 1;
1456 }
1457 lines
1458}
1459
1460fn custom_editor(tree: &Tree, src: &Sources<'_>, i: usize, node: &mut AccessNode) {
1465 let lines = lines_under(tree, i);
1466 let mut value = String::new();
1467 let mut run_no = 0usize;
1468 let mut caret: Option<(usize, usize)> = None;
1469 let mut anchor: Option<(usize, usize)> = None;
1470 for (ln, &l) in lines.iter().enumerate() {
1471 let last = ln + 1 == lines.len();
1472 let mut line_text = String::new();
1473 let mut texts: Vec<usize> = Vec::new();
1474 let lend = tree.subtree_end(l);
1475 let mut t = l;
1476 while t < lend {
1477 if tree.specs[t].access().role == Some(Role::None) {
1478 t = tree.subtree_end(t);
1479 continue;
1480 }
1481 if let NodeContent::Text(_) = tree.content[t] {
1482 texts.push(t);
1483 }
1484 t += 1;
1485 }
1486 for (k, &t) in texts.iter().enumerate() {
1487 let NodeContent::Text(id) = tree.content[t] else {
1488 continue;
1489 };
1490 let base = line_text.len();
1491 line_text.push_str(src.text.content(id));
1492 let newline = !last && k + 1 == texts.len();
1493 let first = node.runs.len();
1494 src.text.access_runs(
1495 id,
1496 RunSource {
1497 key: node.key,
1498 line: ln,
1499 byte_base: base,
1500 origin: tree.pos[t],
1501 scale: src.scale,
1502 newline_after_last: newline,
1503 },
1504 &mut run_no,
1505 &mut node.runs,
1506 );
1507 clip_runs(&mut node.runs[first..], clip_of(src, t));
1508 }
1509 if texts.is_empty() {
1510 node.runs.push(empty_run(
1512 node.key,
1513 run_no,
1514 ln,
1515 Rect::from_pos_size(tree.pos[l], tree.size[l]),
1516 !last,
1517 ));
1518 let at = node.runs.len() - 1;
1519 clip_runs(&mut node.runs[at..], clip_of(src, l));
1520 run_no += 1;
1521 }
1522 if let Some(c) = tree.specs[l].access().caret {
1523 caret = Some((ln, (c as usize).min(line_text.len())));
1524 }
1525 if let Some(a) = tree.specs[l].access().selection_anchor {
1526 anchor = Some((ln, (a as usize).min(line_text.len())));
1527 }
1528 if ln > 0 {
1529 value.push('\n');
1530 }
1531 value.push_str(&line_text);
1532 }
1533 node.value = Some(value);
1534 if let Some((line, offset)) = caret {
1535 node.focus = node.text_pos(line, offset);
1536 let anchor = anchor.unwrap_or((line, offset));
1537 node.anchor = node.text_pos(anchor.0, anchor.1);
1538 let flat = |(line, offset): (usize, usize)| {
1540 node.value
1541 .as_ref()
1542 .map(|v| v.split('\n').take(line).map(|l| l.len() + 1).sum::<usize>() + offset)
1543 };
1544 node.caret = flat((line, offset));
1545 if anchor != (line, offset)
1546 && let (Some(a), Some(c)) = (flat(anchor), node.caret)
1547 {
1548 node.selection = Some((a.min(c), a.max(c)));
1549 }
1550 }
1551}
1552
1553pub(crate) struct RunSource {
1556 pub key: Key,
1557 pub line: usize,
1559 pub byte_base: usize,
1562 pub origin: Vec2,
1564 pub scale: f32,
1565 pub newline_after_last: bool,
1568}
1569
1570fn run_key(key: Key, n: usize) -> Key {
1573 key.str("\u{1}run").index(n as u64)
1574}
1575
1576fn empty_run(key: Key, n: usize, line: usize, rect: Rect, newline: bool) -> AccessRun {
1577 AccessRun {
1578 key: run_key(key, n),
1579 line,
1580 start: 0,
1581 end: 0,
1582 text: if newline { "\n".into() } else { String::new() },
1583 rect: Rect::new(rect.x, rect.y, 0.0, rect.h),
1584 char_lengths: if newline { vec![1] } else { Vec::new() },
1585 char_positions: if newline { vec![0.0] } else { Vec::new() },
1586 char_widths: if newline { vec![0.0] } else { Vec::new() },
1587 word_starts: Vec::new(),
1588 rtl: false,
1589 }
1590}
1591
1592pub(crate) fn runs_of_buffer(
1597 buffer: &Buffer,
1598 src: RunSource,
1599 run_no: &mut usize,
1600 out: &mut Vec<AccessRun>,
1601) {
1602 let line_count = buffer.lines.len();
1603 let runs: Vec<_> = buffer.layout_runs().collect();
1604 for (r, run) in runs.iter().enumerate() {
1605 let last_of_line = runs.get(r + 1).is_none_or(|next| next.line_i != run.line_i);
1606 let newline = last_of_line && (run.line_i + 1 < line_count || src.newline_after_last);
1607 push_row_runs(
1608 &RowGlyphs {
1609 glyphs: run.glyphs,
1610 text: run.text,
1611 line: run.line_i,
1612 top: run.line_top,
1613 height: run.line_height,
1614 rtl: run.rtl,
1615 dx: 0.0,
1616 base: 0,
1617 newline,
1618 },
1619 &src,
1620 run_no,
1621 out,
1622 );
1623 }
1624}
1625
1626pub(crate) struct RowGlyphs<'a> {
1631 pub glyphs: &'a [cosmic_text::LayoutGlyph],
1632 pub text: &'a str,
1634 pub line: usize,
1636 pub top: f32,
1637 pub height: f32,
1638 pub rtl: bool,
1639 pub dx: f32,
1640 pub base: usize,
1641 pub newline: bool,
1643}
1644
1645pub(crate) fn push_row_runs(
1648 row: &RowGlyphs<'_>,
1649 src: &RunSource,
1650 run_no: &mut usize,
1651 out: &mut Vec<AccessRun>,
1652) {
1653 let scale = src.scale.max(f32::EPSILON);
1654 let newline = row.newline;
1655 {
1656 let (start, end) = row.glyphs.iter().fold((usize::MAX, 0usize), |(s, e), g| {
1658 (s.min(g.start), e.max(g.end))
1659 });
1660 let (start, end) = if row.glyphs.is_empty() {
1661 (0, 0)
1662 } else {
1663 (start, end)
1664 };
1665 let slice = &row.text[start..end];
1666 let x0 = row
1668 .glyphs
1669 .iter()
1670 .map(|g| g.x + row.dx)
1671 .fold(f32::INFINITY, f32::min);
1672 let x0 = if x0.is_finite() { x0 } else { 0.0 };
1673 let mut chars: Vec<(usize, u8, f32, f32)> = Vec::new(); for (rel, c) in slice.char_indices() {
1677 let idx = start + rel;
1678 let len = c.len_utf8();
1679 let (x, w) = match row.glyphs.iter().find(|g| g.start <= idx && idx < g.end) {
1680 Some(g) => {
1681 let span = (g.end - g.start).max(1) as f32;
1682 (
1683 g.x + row.dx + g.w * (idx - g.start) as f32 / span,
1684 g.w * len as f32 / span,
1685 )
1686 }
1687 None => chars.last().map_or((x0, 0.0), |&(_, _, x, w)| (x + w, 0.0)),
1688 };
1689 chars.push((idx, len as u8, x, w));
1690 }
1691 let line_end_x = chars.last().map_or(x0, |&(_, _, x, w)| x + w);
1692 if newline {
1693 chars.push((end, 1, line_end_x, 0.0));
1694 }
1695 let starts: Vec<usize> = slice
1697 .split_word_bound_indices()
1698 .filter(|(_, w)| !w.chars().all(char::is_whitespace))
1699 .map(|(b, _)| slice[..b].chars().count())
1700 .collect();
1701 let mut at = 0usize;
1703 loop {
1704 let chunk_end = (at + RUN_CHARS).min(chars.len());
1705 let chunk = &chars[at..chunk_end];
1706 let chunk_x = chunk.first().map_or(x0, |c| c.2);
1707 let chunk_end_x = chunk.last().map_or(chunk_x, |c| c.2 + c.3);
1708 let byte_start = chunk.first().map_or(end, |c| c.0);
1709 let byte_end = chunk
1710 .last()
1711 .map_or(end, |c| {
1712 if newline && c.0 == end {
1713 end
1714 } else {
1715 c.0 + c.1 as usize
1716 }
1717 })
1718 .min(end);
1719 let mut text = row.text[byte_start.min(end)..byte_end].to_string();
1720 if newline && chunk_end == chars.len() {
1721 text.push('\n');
1722 }
1723 out.push(AccessRun {
1724 key: run_key(src.key, *run_no),
1725 line: src.line + row.line,
1726 start: src.byte_base + row.base + byte_start.min(end),
1727 end: src.byte_base + row.base + byte_end,
1728 text,
1729 rect: Rect::new(
1730 src.origin.x + chunk_x / scale,
1731 src.origin.y + row.top / scale,
1732 (chunk_end_x - chunk_x) / scale,
1733 row.height / scale,
1734 ),
1735 char_lengths: chunk.iter().map(|c| c.1).collect(),
1736 char_positions: chunk.iter().map(|c| (c.2 - chunk_x) / scale).collect(),
1737 char_widths: chunk.iter().map(|c| c.3 / scale).collect(),
1738 word_starts: starts
1739 .iter()
1740 .filter(|&&s| s >= at && s < chunk_end)
1741 .map(|&s| (s - at) as u8)
1742 .collect(),
1743 rtl: row.rtl,
1744 });
1745 *run_no += 1;
1746 at = chunk_end;
1747 if at >= chars.len() {
1748 break;
1749 }
1750 }
1751 }
1752}
1753
1754fn hash_of(tree: &AccessTree) -> u64 {
1755 let mut h = crate::key::FNV_OFFSET;
1756 let mut mix = |bytes: &[u8]| h = crate::key::fnv(h, bytes);
1757 let mix_str = |mix: &mut dyn FnMut(&[u8]), s: &Option<String>| match s {
1758 Some(s) => {
1759 mix(&[1]);
1760 mix(s.as_bytes());
1761 mix(&[0]);
1762 }
1763 None => mix(&[0]),
1764 };
1765 let mix_f32 = |mix: &mut dyn FnMut(&[u8]), v: Option<f32>| match v {
1766 Some(v) => {
1767 mix(&[1]);
1768 mix(&v.to_bits().to_le_bytes());
1769 }
1770 None => mix(&[0]),
1771 };
1772 let mix_pos = |mix: &mut dyn FnMut(&[u8]), p: Option<TextPos>| match p {
1773 Some(p) => {
1774 mix(&p.run.0.to_le_bytes());
1775 mix(&p.character.to_le_bytes());
1776 }
1777 None => mix(&[0]),
1778 };
1779 for n in &tree.nodes {
1780 mix(&n.key.0.to_le_bytes());
1781 mix(&n.parent.map_or(0, |k| k.0).to_le_bytes());
1782 mix(&[n.role as u8]);
1783 mix_str(&mut mix, &n.name);
1784 mix_str(&mut mix, &n.description);
1785 for v in [n.rect.x, n.rect.y, n.rect.w, n.rect.h] {
1786 mix(&v.to_bits().to_le_bytes());
1787 }
1788 mix_str(&mut mix, &n.value);
1789 mix(&n.caret.unwrap_or(usize::MAX).to_le_bytes());
1790 let (a, b) = n.selection.unwrap_or((usize::MAX, usize::MAX));
1791 mix(&a.to_le_bytes());
1792 mix(&b.to_le_bytes());
1793 for r in &n.runs {
1794 mix(&r.key.0.to_le_bytes());
1795 mix(r.text.as_bytes());
1796 mix(&[0]);
1797 for v in [r.rect.x, r.rect.y, r.rect.w, r.rect.h] {
1798 mix(&v.to_bits().to_le_bytes());
1799 }
1800 for v in &r.char_positions {
1801 mix(&v.to_bits().to_le_bytes());
1802 }
1803 }
1804 mix_pos(&mut mix, n.anchor);
1805 mix_pos(&mut mix, n.focus);
1806 mix(&[n.checked.map_or(2, |c| c as u8), n.mixed as u8]);
1807 mix(&n.step.map_or(u32::MAX, f32::to_bits).to_le_bytes());
1808 mix(&[n.selected.map_or(2, |c| c as u8)]);
1809 mix(&[n.expanded.map_or(2, |c| c as u8)]);
1810 mix(&n.pos_in_set.unwrap_or(usize::MAX).to_le_bytes());
1811 mix(&n.set_size.unwrap_or(usize::MAX).to_le_bytes());
1812 mix_f32(&mut mix, n.number);
1813 mix_f32(&mut mix, n.min);
1814 mix_f32(&mut mix, n.max);
1815 mix(&[n.focused as u8, n.disabled as u8, n.modal as u8]);
1816 if let Some(s) = n.scroll {
1817 for v in [s.x, s.y, s.max_x, s.max_y] {
1818 mix(&v.to_bits().to_le_bytes());
1819 }
1820 }
1821 mix(&n.actions.to_le_bytes());
1822 mix(&[n.live as u8]);
1823 }
1824 mix(&tree.focus.map_or(0, |k| k.0).to_le_bytes());
1825 h
1826}
1827
1828pub type Label = Arc<str>;
1831
1832#[cfg(test)]
1833mod tests {
1834 use super::*;
1835
1836 #[test]
1837 fn role_names_round_trip() {
1838 for r in Role::ALL {
1839 assert_eq!(Role::parse(r.name()), Some(r));
1840 }
1841 assert_eq!(Role::parse("nope"), None);
1842 }
1843
1844 #[test]
1845 fn action_bits_are_distinct_and_named() {
1846 let mut seen = 0u32;
1847 for a in AccessAction::ALL {
1848 assert_eq!(seen & a.bit(), 0);
1849 seen |= a.bit();
1850 assert_eq!(AccessAction::parse(a.name()), Some(a));
1851 }
1852 }
1853
1854 #[test]
1855 fn run_keys_never_collide_with_node_keys() {
1856 let k = Key::ROOT.str("editor");
1857 assert_ne!(run_key(k, 0), k.index(0));
1858 assert_ne!(run_key(k, 0), k.str("run"));
1859 assert_ne!(run_key(k, 0), run_key(k, 1));
1860 }
1861}