1use crate::gpu::{Viewport, page::PageScene};
2use crate::{
3 date::DateField,
4 document::TextPosition,
5 editor::{CanvasEditor, Selection},
6 outline::ParagraphLayout,
7};
8use accesskit::{
9 Action, Affine, Node, NodeId, Rect, Role, TextDirection, TextSelection, TreeId, TreeInfo,
10 TreeUpdate,
11};
12use onestore::{ExGuid, page::text::EditError};
13use parley::Affinity;
14use std::collections::HashMap;
15
16pub const ROOT: NodeId = NodeId(0);
17/// The page in document coordinates; its transform is the viewport, so scrolling and zooming
18/// change only this node.
19const PAGE: NodeId = NodeId(1);
20
21struct Run {
22 id: NodeId,
23 node: Node,
24 /// Paragraph-relative source offset of each character boundary, except the end of a
25 /// paragraph break, which is the next paragraph's start.
26 offsets: Vec<u32>,
27}
28
29/// A paragraph's runs, in paragraph coordinates under a node at its origin, so reflow above
30/// it moves one node.
31struct Paragraph {
32 id: NodeId,
33 layout: u64,
34 source: usize,
35 origin: [f32; 2],
36 /// Its text's byte length in the field's value.
37 length: usize,
38 runs: Vec<Run>,
39}
40
41impl Paragraph {
42 fn node(&self) -> Node {
43 let mut node = Node::new(Role::GenericContainer);
44 node.set_transform(Affine::translate((
45 f64::from(self.origin[0]),
46 f64::from(self.origin[1]),
47 )));
48 node.set_children(self.runs.iter().map(|run| run.id).collect::<Vec<_>>());
49 node
50 }
51}
52
53struct Field {
54 outline: ExGuid,
55 id: NodeId,
56 node: Node,
57 paragraphs: Vec<Paragraph>,
58}
59
60/// The nodes last sent to assistive technology, so each update sends only what changed.
61pub struct Accessibility {
62 fields: Vec<Field>,
63 read_only: Vec<(NodeId, Node)>,
64 dates: Vec<(DateField, NodeId, Node)>,
65 next_id: u64,
66}
67
68impl Default for Accessibility {
69 fn default() -> Self {
70 Self {
71 fields: Vec::new(),
72 read_only: Vec::new(),
73 dates: Vec::new(),
74 next_id: 2,
75 }
76 }
77}
78
79impl Accessibility {
80 /// Retire published nodes without recycling their IDs; the next update sends the whole tree.
81 pub fn deactivate(&mut self) {
82 self.fields.clear();
83 self.read_only.clear();
84 self.dates.clear();
85 }
86
87 fn allocate(&mut self) -> NodeId {
88 let id = NodeId(self.next_id);
89 self.next_id = self
90 .next_id
91 .checked_add(1)
92 .expect("Accessibility node IDs exhausted");
93 id
94 }
95
96 pub fn read_only_for_node(&self, node: NodeId) -> Option<usize> {
97 self.read_only.iter().position(|(id, _)| *id == node)
98 }
99
100 pub fn date_for_node(&self, node: NodeId) -> Option<DateField> {
101 self.dates
102 .iter()
103 .find(|(_, id, _)| *id == node)
104 .map(|(field, ..)| *field)
105 }
106
107 pub fn outline_for_node(&self, node: NodeId) -> Option<ExGuid> {
108 self.fields
109 .iter()
110 .find(|field| field.id == node)
111 .map(|field| field.outline)
112 }
113
114 /// The nodes that changed since the last update. After an error, `deactivate` before the
115 /// next update.
116 pub fn update(
117 &mut self,
118 editor: &CanvasEditor,
119 scene: Option<&(PageScene, [f32; 2])>,
120 viewport: Viewport,
121 label: &str,
122 preview: Option<(ExGuid, [f32; 2])>,
123 read_only_focus: Option<usize>,
124 ) -> Result<TreeUpdate, EditError> {
125 let mut nodes = Vec::new();
126 let mut focus = ROOT;
127 let mut previous: HashMap<_, _> = self
128 .fields
129 .drain(..)
130 .map(|field| (field.outline, field))
131 .collect();
132 for (ordinal, outline) in editor
133 .visible_outlines()
134 .chain(editor.caret_outline())
135 .enumerate()
136 {
137 let (id, sent, old) = match previous.remove(&outline.id) {
138 Some(field) => (field.id, Some(field.node), field.paragraphs),
139 None => (self.allocate(), None, Vec::new()),
140 };
141 let layouts: Vec<_> = outline.layouts().collect();
142 // Only a paragraph with a successor ends its last run with the paragraph break.
143 let same = |old_index: usize, index: usize| {
144 old[old_index].layout == layouts[index].1.text.id()
145 && (old_index + 1 == old.len()) == (index + 1 == layouts.len())
146 };
147 let shorter = old.len().min(layouts.len());
148 let prefix = (0..shorter).take_while(|&i| same(i, i)).count();
149 let suffix = (1..=shorter - prefix)
150 .take_while(|&i| same(old.len() - i, layouts.len() - i))
151 .count();
152 let text = sent.as_ref().and_then(Node::value).unwrap_or_default();
153 let length = |paragraphs: &[Paragraph]| paragraphs.iter().map(|p| p.length).sum();
154 let mut value = text[..length(&old[..prefix])].to_owned();
155 let tail = &text[text.len() - length(&old[old.len() - suffix..])..];
156 let mut paragraphs = old;
157 let kept = paragraphs.split_off(paragraphs.len() - suffix);
158 let mut replaced = paragraphs.split_off(prefix).into_iter();
159 for (at, (source, shaped)) in layouts
160 .iter()
161 .enumerate()
162 .take(layouts.len() - suffix)
163 .skip(prefix)
164 {
165 let (id, old) = match replaced.next() {
166 Some(paragraph) => (paragraph.id, paragraph.runs),
167 None => (self.allocate(), Vec::new()),
168 };
169 let mut runs = Vec::new();
170 for (index, (node, offsets)) in runs_of(shaped, at + 1 < layouts.len())?
171 .into_iter()
172 .enumerate()
173 {
174 let id = match old.get(index) {
175 Some(old) if old.offsets == offsets && old.node.value() == node.value() => {
176 old.id
177 }
178 _ => self.allocate(),
179 };
180 runs.push(Run { id, node, offsets });
181 }
182 for index in 1..runs.len() {
183 if runs[index - 1].node.bounds().map(|r| r.y0)
184 == runs[index].node.bounds().map(|r| r.y0)
185 {
186 let (before, after) = runs.split_at_mut(index);
187 before[index - 1].node.set_next_on_line(after[0].id);
188 after[0].node.set_previous_on_line(before[index - 1].id);
189 }
190 }
191 for (index, run) in runs.iter().enumerate() {
192 if old
193 .get(index)
194 .is_none_or(|old| old.id != run.id || old.node != run.node)
195 {
196 nodes.push((run.id, run.node.clone()));
197 }
198 }
199 let start = value.len();
200 value.extend(runs.iter().filter_map(|run| run.node.value()));
201 let paragraph = Paragraph {
202 id,
203 layout: shaped.text.id(),
204 source: *source,
205 origin: shaped.origin,
206 length: value.len() - start,
207 runs,
208 };
209 nodes.push((id, paragraph.node()));
210 paragraphs.push(paragraph);
211 }
212 paragraphs.extend(kept);
213 value.push_str(tail);
214 for (paragraph, (source, shaped)) in paragraphs.iter_mut().zip(&layouts) {
215 paragraph.source = *source;
216 if paragraph.origin != shaped.origin {
217 paragraph.origin = shaped.origin;
218 nodes.push((paragraph.id, paragraph.node()));
219 }
220 }
221 let mut node = Node::new(Role::MultilineTextInput);
222 node.set_label(if outline.title {
223 "Page title".into()
224 } else if editor
225 .caret_outline()
226 .is_some_and(|caret| caret.id == outline.id)
227 {
228 "Text input".into()
229 } else {
230 format!("Text outline {}", ordinal + 1)
231 });
232 node.add_action(Action::Focus);
233 node.add_action(Action::SetTextSelection);
234 node.add_action(Action::ReplaceSelectedText);
235 node.add_action(Action::SetValue);
236 node.set_value(value);
237 node.set_children(
238 paragraphs
239 .iter()
240 .map(|paragraph| paragraph.id)
241 .collect::<Vec<_>>(),
242 );
243 let origin = preview
244 .filter(|(id, _)| *id == outline.id)
245 .map(|(_, origin)| origin)
246 .unwrap_or_else(|| outline.origin());
247 node.set_transform(Affine::translate((
248 f64::from(origin[0]),
249 f64::from(origin[1]),
250 )));
251 let bounds = outline.bounds();
252 node.set_bounds(Rect::new(0.0, 0.0, bounds.width(), bounds.height()));
253 if outline.id == editor.active_outline().id {
254 let Selection {
255 positions: [anchor, caret],
256 affinities,
257 } = editor.selection();
258 node.set_text_selection(TextSelection {
259 anchor: position(&layouts, &paragraphs, anchor, affinities[0])?,
260 focus: position(&layouts, &paragraphs, caret, affinities[1])?,
261 });
262 focus = id;
263 }
264 if sent.as_ref() != Some(&node) {
265 nodes.push((id, node.clone()));
266 }
267 self.fields.push(Field {
268 outline: outline.id,
269 id,
270 node,
271 paragraphs,
272 });
273 }
274 let mut children: Vec<_> = self.fields.iter().map(|field| field.id).collect();
275 let sent_read_only = std::mem::take(&mut self.read_only);
276 let sent_dates = std::mem::take(&mut self.dates);
277 if let Some((scene, offset)) = scene {
278 let rect = |[x0, y0, x1, y1]: [f32; 4]| {
279 Rect::new(
280 f64::from(x0 + offset[0]),
281 f64::from(y0 + offset[1]),
282 f64::from(x1 + offset[0]),
283 f64::from(y1 + offset[1]),
284 )
285 };
286 for (index, object) in scene.read_only(Some(editor)).enumerate() {
287 let mut node = Node::new(Role::Label);
288 node.set_value(object.message);
289 node.set_read_only();
290 node.add_action(Action::Focus);
291 node.set_bounds(rect(object.rect()));
292 let sent = sent_read_only.get(index);
293 let id = sent.map_or_else(|| self.allocate(), |(id, _)| *id);
294 if sent.is_none_or(|(_, sent)| *sent != node) {
295 nodes.push((id, node.clone()));
296 }
297 if read_only_focus == Some(index) {
298 focus = id;
299 }
300 children.push(id);
301 self.read_only.push((id, node));
302 }
303 for (index, (field, bounds)) in scene.date_fields(editor).enumerate() {
304 let mut node = Node::new(Role::Button);
305 node.set_label(super::DATE_LABELS[field as usize]);
306 node.set_value(
307 editor.date().unwrap().source().paragraphs[index]
308 .text()
309 .unwrap()
310 .text
311 .text(),
312 );
313 node.add_action(Action::Click);
314 node.set_bounds(rect(bounds));
315 let sent = sent_dates.get(index);
316 let id = sent.map_or_else(|| self.allocate(), |(_, id, _)| *id);
317 if sent.is_none_or(|(_, _, sent)| *sent != node) {
318 nodes.push((id, node.clone()));
319 }
320 children.push(id);
321 self.dates.push((field, id, node));
322 }
323 }
324 let mut page = Node::new(Role::GenericContainer);
325 page.set_transform(Affine::new([
326 f64::from(viewport.scale),
327 0.0,
328 0.0,
329 f64::from(viewport.scale),
330 f64::from(viewport.origin[0]),
331 f64::from(viewport.origin[1]),
332 ]));
333 page.set_children(children);
334 let mut root = Node::new(Role::Group);
335 root.set_label(label);
336 root.set_children(vec![PAGE]);
337 root.set_bounds(Rect::new(
338 0.0,
339 0.0,
340 viewport.size[0] as f64,
341 viewport.size[1] as f64,
342 ));
343 nodes.push((ROOT, root));
344 nodes.push((PAGE, page));
345 Ok(TreeUpdate {
346 nodes,
347 tree: Some(TreeInfo::new(ROOT)),
348 tree_id: TreeId::ROOT,
349 focus,
350 })
351 }
352
353 pub fn selection(
354 &self,
355 outline: ExGuid,
356 selection: TextSelection,
357 ) -> Result<Selection, EditError> {
358 let field = self
359 .fields
360 .iter()
361 .find(|field| field.outline == outline)
362 .ok_or(EditError::InvalidRange)?;
363 let position = |position: accesskit::TextPosition| {
364 let (index, paragraph, run) = field
365 .paragraphs
366 .iter()
367 .enumerate()
368 .find_map(|(index, paragraph)| {
369 let run = paragraph
370 .runs
371 .iter()
372 .position(|run| run.id == position.node)?;
373 Some((index, paragraph, run))
374 })
375 .ok_or(EditError::InvalidRange)?;
376 let offsets = &paragraph.runs[run].offsets;
377 let breaks = run + 1 == paragraph.runs.len() && index + 1 < field.paragraphs.len();
378 let count = offsets.len() + usize::from(breaks);
379 let character = position.character_index;
380 if character >= count {
381 return Err(EditError::InvalidRange);
382 }
383 let source = match offsets.get(character) {
384 Some(offset) => TextPosition {
385 paragraph: paragraph.source,
386 offset: *offset,
387 },
388 None => TextPosition {
389 paragraph: field.paragraphs[index + 1].source,
390 offset: 0,
391 },
392 };
393 let affinity = if character > 0 && character + 1 == count {
394 Affinity::Upstream
395 } else {
396 Affinity::Downstream
397 };
398 Ok::<_, EditError>((source, affinity))
399 };
400 let (anchor, anchor_affinity) = position(selection.anchor)?;
401 let (focus, focus_affinity) = position(selection.focus)?;
402 Ok(Selection {
403 positions: [anchor, focus],
404 affinities: [anchor_affinity, focus_affinity],
405 })
406 }
407}
408
409/// The accessible position of a source position in `layouts`, which `paragraphs` mirrors.
410fn position(
411 layouts: &[(usize, &ParagraphLayout)],
412 paragraphs: &[Paragraph],
413 mut position: TextPosition,
414 affinity: Affinity,
415) -> Result<accesskit::TextPosition, EditError> {
416 let index = layouts
417 .binary_search_by_key(&position.paragraph, |(source, _)| *source)
418 .map_err(|_| EditError::InvalidRange)?;
419 let shaped = layouts[index].1;
420 let projection = &shaped.projection;
421 let layout = &shaped.text;
422 let cursor = layout.cursor(
423 projection
424 .text()
425 .byte_offset(projection.visible_offset(position.offset)?)?,
426 affinity,
427 );
428 position.offset = projection.source_offset(
429 projection.text().utf16_offset(cursor.index())?,
430 onestore::page::text::Affinity::Downstream,
431 )?;
432 let caret = layout.caret(cursor, 0.0);
433 let mut positions = paragraphs[index]
434 .runs
435 .iter()
436 .filter(|run| run.node.bounds().is_some_and(|rect| rect.y0 == caret.y0))
437 .filter_map(|run| {
438 run.offsets
439 .iter()
440 .position(|offset| *offset == position.offset)
441 .map(|character_index| accesskit::TextPosition {
442 node: run.id,
443 character_index,
444 })
445 });
446 match affinity {
447 Affinity::Upstream => positions.next(),
448 Affinity::Downstream => positions.next_back(),
449 }
450 .ok_or(EditError::InvalidRange)
451}
452
453/// A paragraph's text runs in paragraph coordinates, each with its character boundaries'
454/// source offsets; `breaks` ends the last run with the paragraph break.
455fn runs_of(shaped: &ParagraphLayout, breaks: bool) -> Result<Vec<(Node, Vec<u32>)>, EditError> {
456 let mut runs = Vec::new();
457 let layout = &shaped.text;
458 let projection = &shaped.projection;
459 let text = projection.text().text();
460 let boundaries: Vec<_> = projection.source_boundaries().collect();
461 let source_offset = |byte| -> Result<u32, EditError> {
462 let index = boundaries
463 .binary_search_by_key(&byte, |(byte, _)| *byte)
464 .map_err(|_| EditError::InvalidRange)?;
465 Ok(boundaries[index].1)
466 };
467 for (line, bounds) in layout.lines() {
468 let line_start = runs.len();
469 let mut x = line.metrics().offset;
470 let mut logical_runs = Vec::new();
471 for run in line.runs() {
472 let advance = run.advance();
473 logical_runs.push((run, x));
474 x += advance;
475 }
476 logical_runs.sort_by_key(|(run, _)| run.text_range().start);
477 for (run, x) in logical_runs {
478 // AccessKit stores each character's byte length in a u8.
479 let mut characters = Vec::new();
480 for cluster in run.clusters() {
481 let range = cluster.text_range();
482 if range.is_empty() || range.start == text.len() {
483 continue;
484 }
485 let cluster_text = text.get(range.clone()).ok_or(EditError::InvalidRange)?;
486 if range.len() <= u8::MAX as usize {
487 characters.push((range, cluster.advance(), cluster.is_word_boundary()));
488 } else {
489 for (index, (offset, ch)) in cluster_text.char_indices().enumerate() {
490 characters.push((
491 range.start + offset..range.start + offset + ch.len_utf8(),
492 if index == 0 { cluster.advance() } else { 0.0 },
493 index == 0 && cluster.is_word_boundary(),
494 ));
495 }
496 }
497 }
498 let mut advance = 0.0;
499 for chunk in characters.chunks(256) {
500 let width: f32 = chunk.iter().map(|(_, width, _)| width).sum();
501 let left = x + if run.is_rtl() {
502 run.advance() - advance - width
503 } else {
504 advance
505 };
506 let mut node = Node::new(Role::TextRun);
507 node.set_bounds(Rect::new(
508 f64::from(left),
509 f64::from(bounds.top),
510 f64::from(left + width),
511 f64::from(bounds.top + bounds.height),
512 ));
513 node.set_text_direction(if run.is_rtl() {
514 TextDirection::RightToLeft
515 } else {
516 TextDirection::LeftToRight
517 });
518 node.set_font_size(run.font_size());
519 let start = chunk[0].0.start;
520 let end = chunk.last().unwrap().0.end;
521 node.set_value(text.get(start..end).ok_or(EditError::InvalidRange)?);
522 node.set_character_lengths(
523 chunk
524 .iter()
525 .map(|(range, _, _)| u8::try_from(range.len()).unwrap())
526 .collect::<Vec<_>>(),
527 );
528 let mut offset = 0.0;
529 node.set_character_positions(
530 chunk
531 .iter()
532 .map(|(_, width, _)| {
533 let position = offset;
534 offset += width;
535 position
536 })
537 .collect::<Vec<_>>(),
538 );
539 node.set_character_widths(
540 chunk.iter().map(|(_, width, _)| *width).collect::<Vec<_>>(),
541 );
542 node.set_word_starts(
543 chunk
544 .iter()
545 .enumerate()
546 .filter_map(|(index, (_, _, word))| word.then_some(index as u8))
547 .collect::<Vec<_>>(),
548 );
549 let offsets = chunk
550 .iter()
551 .map(|(range, _, _)| source_offset(range.start))
552 .chain(std::iter::once(source_offset(end)))
553 .collect::<Result<Vec<_>, _>>()?;
554 runs.push((node, offsets));
555 advance += width;
556 }
557 }
558 // Empty lines still need a text position for selection and insertion.
559 if runs.len() == line_start {
560 let mut node = Node::new(Role::TextRun);
561 node.set_value("");
562 node.set_character_lengths(Vec::<u8>::new());
563 node.set_character_positions(Vec::<f32>::new());
564 node.set_character_widths(Vec::<f32>::new());
565 node.set_text_direction(TextDirection::LeftToRight);
566 node.set_bounds(Rect::new(
567 0.0,
568 f64::from(bounds.top),
569 0.0,
570 f64::from(bounds.top + bounds.height),
571 ));
572 runs.push((node, vec![source_offset(bounds.source.start)?]));
573 }
574 }
575 if breaks {
576 let (node, _) = runs.last_mut().unwrap();
577 let mut value = node.value().unwrap().to_owned();
578 value.push('\n');
579 node.set_value(value);
580 let mut lengths = node.character_lengths().to_vec();
581 lengths.push(1);
582 node.set_character_lengths(lengths);
583 if let Some(rect) = node.bounds() {
584 let mut positions = node.character_positions().unwrap_or_default().to_vec();
585 positions.push(rect.width() as f32);
586 node.set_character_positions(positions);
587 let mut widths = node.character_widths().unwrap_or_default().to_vec();
588 widths.push(4.0);
589 node.set_character_widths(widths);
590 }
591 }
592 if !shaped.tags.is_empty() {
593 let descriptions = shaped
594 .tags
595 .iter()
596 .map(|tag| {
597 use crate::outline::{TagIcon, symbol_name};
598 let label = match tag.icon {
599 _ if !tag.label.is_empty() => &tag.label,
600 TagIcon::Symbol { shape, .. } => symbol_name(shape).unwrap_or("Tag"),
601 TagIcon::Task { .. } => "Outlook task",
602 };
603 let state = match tag.icon {
604 TagIcon::Symbol { checked: true, .. } => ", completed",
605 icon if icon.checkable() => ", incomplete",
606 _ => "",
607 };
608 format!(
609 "{label}{state}{}",
610 if tag.disabled { ", disabled" } else { "" }
611 )
612 })
613 .collect::<Vec<_>>()
614 .join("; ");
615 runs[0].0.set_description(descriptions);
616 }
617 Ok(runs)
618}
619
620#[cfg(test)]
621pub(super) mod tests {
622 use super::*;
623 use crate::{document::TextDocument, editor::TextOutline, layout::TextEngine};
624 use accesskit_consumer::{NodeRef, Tree, TreeState};
625 use onestore::document::Format;
626 use onestore::page::text::Paragraph;
627
628 /// Applies `update` to the platform's copy of the tree, as an adapter does.
629 pub(in crate::interaction) fn apply(tree: &mut Option<Tree>, update: TreeUpdate) -> &TreeState {
630 struct Ignore;
631 impl accesskit_consumer::TreeChangeHandler for Ignore {
632 fn node_added(&mut self, _: &NodeRef) {}
633 fn node_updated(&mut self, _: &NodeRef, _: &NodeRef) {}
634 fn focus_moved(&mut self, _: Option<&NodeRef>, _: Option<&NodeRef>) {}
635 fn node_removed(&mut self, _: &NodeRef) {}
636 }
637 match tree {
638 Some(tree) => tree.update_and_process_changes(update, &mut Ignore),
639 None => *tree = Some(Tree::new(update, true)),
640 }
641 tree.as_ref().unwrap().state()
642 }
643
644 /// Every node of `state`, depth first.
645 pub(in crate::interaction) fn nodes(state: &TreeState) -> Vec<NodeRef<'_>> {
646 let mut nodes = Vec::new();
647 let mut pending = vec![state.root()];
648 while let Some(node) = pending.pop() {
649 pending.extend(node.children().rev());
650 nodes.push(node);
651 }
652 nodes
653 }
654
655 /// `state`'s nodes and focus with IDs renumbered depth first, to compare trees whose IDs differ.
656 fn canonical(state: &TreeState) -> (Vec<Node>, NodeId) {
657 let nodes = nodes(state);
658 let order: HashMap<_, _> = nodes
659 .iter()
660 .enumerate()
661 .map(|(index, node)| (node.locate().0, NodeId(index as u64)))
662 .collect();
663 let id = |id: NodeId| order[&id];
664 let canonical = nodes
665 .iter()
666 .map(|node| {
667 let mut data = node.data().clone();
668 data.set_children(
669 data.children()
670 .iter()
671 .map(|child| id(*child))
672 .collect::<Vec<_>>(),
673 );
674 if let Some(next) = data.next_on_line() {
675 data.set_next_on_line(id(next));
676 }
677 if let Some(previous) = data.previous_on_line() {
678 data.set_previous_on_line(id(previous));
679 }
680 if let Some(&TextSelection { anchor, focus }) = data.text_selection() {
681 data.set_text_selection(TextSelection {
682 anchor: accesskit::TextPosition {
683 node: id(anchor.node),
684 ..anchor
685 },
686 focus: accesskit::TextPosition {
687 node: id(focus.node),
688 ..focus
689 },
690 });
691 }
692 data
693 })
694 .collect();
695 (canonical, id(state.focus_in_tree().locate().0))
696 }
697
698 #[test]
699 fn title_flow_updates_read_only_hit_and_accessibility_bounds() {
700 use onestore::page::{Page, PageObject, Title, Unsupported};
701 let mut engine = TextEngine::default();
702 let mut title = TextOutline::new(
703 &mut engine,
704 TextDocument::new(vec![Paragraph::new(
705 "Header".into(),
706 Format {
707 font: Some("Arial".into()),
708 font_size: Some(8.0),
709 line_spacing: Some(20.0),
710 ..Default::default()
711 },
712 )])
713 .unwrap(),
714 468.0,
715 [0.0; 2],
716 )
717 .unwrap()
718 .snapshot();
719 title.title = true;
720 let page = Page {
721 identity: None,
722 title: "Header".into(),
723 created: None,
724 margin_origin: [36.0, 14.4],
725 rtl: false,
726 color: None,
727 rule_lines: None,
728 definitions: Default::default(),
729 objects: vec![
730 PageObject::Title(Title {
731 id: Default::default(),
732 date: None,
733 layout: Default::default(),
734 outlines: vec![title],
735 }),
736 PageObject::Unsupported(Unsupported {
737 id: Default::default(),
738 jcid: 0xdead,
739 layout: onestore::document::Layout {
740 x: Some(120.0),
741 y: Some(80.0),
742 max_width: Some(200.0),
743 max_height: Some(60.0),
744 ..Default::default()
745 },
746 }),
747 ],
748 };
749 let (scene, mut editor) =
750 crate::gpu::page::PageScene::from_page(page, &mut engine).unwrap();
751 let scene = (scene, [30.0, 40.0]);
752 let viewport = Viewport {
753 size: [800, 600],
754 origin: [10.0, -20.0],
755 scale: 2.0,
756 };
757 let mut access = Accessibility::default();
758 let mut tree = None;
759 let mut identity = None;
760 for phase in 0..4 {
761 match phase {
762 1 => {
763 editor.select_all().unwrap();
764 editor.insert(&mut engine, "Header\u{000b}Second\u{000b}Third\u{000b}Fourth\u{000b}Fifth\u{000b}Sixth").unwrap();
765 }
766 2 => {
767 editor.undo(&mut engine).unwrap();
768 }
769 3 => {
770 editor.redo(&mut engine).unwrap();
771 }
772 _ => {}
773 }
774 let update = access
775 .update(&editor, Some(&scene), viewport, "Test", None, Some(0))
776 .unwrap();
777 let node = nodes(apply(&mut tree, update))
778 .into_iter()
779 .find(|node| node.role() == Role::Label)
780 .unwrap();
781 let id = node.locate().0;
782 if let Some(identity) = identity {
783 assert_eq!(id, identity);
784 }
785 identity = Some(id);
786 let y = if phase % 2 == 0 { 80.0 } else { 145.92 };
787 let expected = Rect::new(
788 310.0,
789 f64::from((y + 40.0) * 2.0 - 20.0),
790 710.0,
791 f64::from((y + 100.0) * 2.0 - 20.0),
792 );
793 let bounds = node.bounding_box().unwrap();
794 assert_eq!(bounds.x0, expected.x0);
795 assert_eq!(bounds.x1, expected.x1);
796 assert!((bounds.y0 - expected.y0).abs() < 0.001);
797 assert!((bounds.y1 - expected.y1).abs() < 0.001);
798 assert_eq!(
799 super::super::page_hit_test(&editor, Some(&scene), [155.0, y + 45.0], 1.0),
800 Some(super::super::Hit::ReadOnly(0))
801 );
802 if phase % 2 == 1 {
803 assert_ne!(
804 super::super::page_hit_test(&editor, Some(&scene), [155.0, 125.0], 1.0),
805 Some(super::super::Hit::ReadOnly(0))
806 );
807 }
808 }
809 }
810
811 #[test]
812 fn read_only_objects_keep_accessibility_identity_through_edits_and_view_changes() {
813 use crate::gpu::page::PageScene;
814 use onestore::page::{Page, PageObject, Unsupported};
815 let page = Page {
816 identity: None,
817 created: None,
818 title: String::new(),
819 margin_origin: [0.0; 2],
820 rtl: false,
821 color: None,
822 rule_lines: None,
823 definitions: Default::default(),
824 objects: vec![PageObject::Unsupported(Unsupported {
825 id: Default::default(),
826 jcid: 0xdead,
827 layout: onestore::document::Layout {
828 x: Some(10.0),
829 y: Some(20.0),
830 max_width: Some(200.0),
831 max_height: Some(60.0),
832 ..Default::default()
833 },
834 })],
835 };
836 let mut engine = TextEngine::default();
837 let (scene, mut editor) = PageScene::from_page(page, &mut engine).unwrap();
838 let scene = (scene, [30.0, 40.0]);
839 let mut access = Accessibility::default();
840 let mut tree = None;
841 let mut identity = None;
842 for scale in [1.0, 2.0, 0.5] {
843 editor.insert(&mut engine, "annotation ").unwrap();
844 let viewport = Viewport {
845 size: [800, 600],
846 origin: [-200.0, 10.0],
847 scale,
848 };
849 let update = access
850 .update(&editor, Some(&scene), viewport, "Test", None, None)
851 .unwrap();
852 let state = apply(&mut tree, update);
853 let node = nodes(state)
854 .into_iter()
855 .find(|node| node.role() == Role::Label)
856 .unwrap();
857 let id = node.locate().0;
858 if let Some(identity) = identity {
859 assert_eq!(id, identity);
860 }
861 identity = Some(id);
862 assert_eq!(node.data().value(), Some("Unsupported content\nRead-only"));
863 assert!(node.is_read_only());
864 assert!(node.data().supports_action(Action::Focus));
865 for action in [
866 Action::SetValue,
867 Action::ReplaceSelectedText,
868 Action::SetTextSelection,
869 ] {
870 assert!(!node.data().supports_action(action));
871 }
872 assert_eq!(
873 node.bounding_box(),
874 Some(Rect::new(
875 f64::from(40.0 * scale - 200.0),
876 f64::from(60.0 * scale + 10.0),
877 f64::from(240.0 * scale - 200.0),
878 f64::from(120.0 * scale + 10.0)
879 ))
880 );
881 assert_eq!(access.outline_for_node(id), None);
882 assert_eq!(
883 state.focus().unwrap().document_range().text(),
884 "annotation "
885 );
886 let focused = access
887 .update(&editor, Some(&scene), viewport, "Test", None, Some(0))
888 .unwrap();
889 assert_eq!(focused.focus, identity.unwrap());
890 assert_eq!(access.read_only_for_node(focused.focus), Some(0));
891 assert_eq!(
892 apply(&mut tree, focused)
893 .focus()
894 .unwrap()
895 .value()
896 .as_deref(),
897 Some("Unsupported content\nRead-only")
898 );
899 editor.undo(&mut engine).unwrap();
900 }
901 access.deactivate();
902 assert!(access.read_only.is_empty());
903 }
904
905 #[test]
906 fn tag_descriptions_preserve_plain_text_and_survive_edit_undo_and_cache_reuse() {
907 use onestore::page::{Definition, Outline};
908 use onestore::{
909 ExGuid,
910 document::{Kind, Layout, Tag},
911 };
912 use std::collections::BTreeMap;
913 let text = TextDocument::new(
914 ["task one", "task two", "question", "music"]
915 .into_iter()
916 .map(|text| Paragraph::new(text.into(), Format::default()))
917 .collect(),
918 )
919 .unwrap();
920 let mut paragraphs = text.nodes().to_vec();
921 let mut definitions = BTreeMap::new();
922 for (index, (shape, label, status)) in [
923 (3, Some("Rehearsal"), 0),
924 (3, Some("Rehearsal"), 1),
925 (15, None, 1),
926 (121, None, 3),
927 ]
928 .into_iter()
929 .enumerate()
930 {
931 let id = ExGuid {
932 n: index as u32 + 500,
933 ..ExGuid::default()
934 };
935 definitions.insert(
936 id,
937 Definition {
938 kind: Kind::TagDefinition {
939 label: label.map(str::to_owned),
940 action_type: None,
941 shape: Some(shape),
942 color: None,
943 highlight: None,
944 },
945 format: Format::default(),
946 },
947 );
948 paragraphs[index].text_mut().unwrap().tags.push(Tag {
949 definition: Some(id),
950 action_type: None,
951 shape: None,
952 property_status: None,
953 status,
954 created: None,
955 completed: None,
956 start: None,
957 due: None,
958 task_id: None,
959 extra_set: 0,
960 });
961 }
962 let mut engine = TextEngine::default();
963 let mut editor = CanvasEditor::from_outlines(
964 &mut engine,
965 vec![Outline {
966 title: false,
967 min_width: None,
968 id: ExGuid {
969 n: 900,
970 ..ExGuid::default()
971 },
972 layout: Layout {
973 max_width: Some(120.0),
974 ..Layout::default()
975 },
976 indents: vec![18.0, 0.0],
977 paragraphs,
978 unsupported: Vec::new(),
979 }],
980 definitions,
981 )
982 .unwrap();
983 let mut access = Accessibility::default();
984 let mut tree = None;
985 let viewport = Viewport {
986 size: [800, 600],
987 origin: [48.0; 2],
988 scale: 2.0,
989 };
990 for step in 0..4 {
991 if step == 2 {
992 editor.insert(&mut engine, "new ").unwrap();
993 }
994 if step == 3 {
995 editor.undo(&mut engine).unwrap();
996 }
997 let update = access
998 .update(&editor, None, viewport, "Test", None, None)
999 .unwrap();
1000 let state = apply(&mut tree, update);
1001 assert_eq!(
1002 nodes(state)
1003 .iter()
1004 .filter_map(|node| node.data().description())
1005 .collect::<Vec<_>>(),
1006 [
1007 "Rehearsal, incomplete",
1008 "Rehearsal, completed",
1009 "Question",
1010 "Music Note, disabled"
1011 ]
1012 );
1013 let text = state.focus().unwrap().document_range().text();
1014 assert_eq!(
1015 text,
1016 if step == 2 {
1017 "new task one\ntask two\nquestion\nmusic"
1018 } else {
1019 "task one\ntask two\nquestion\nmusic"
1020 }
1021 );
1022 }
1023 }
1024
1025 #[test]
1026 fn collapsed_paragraphs_keep_visible_accessibility_ranges_in_source_coordinates() {
1027 let document = TextDocument::new(
1028 [
1029 "root",
1030 "folded",
1031 "hidden",
1032 "last",
1033 "folded tail",
1034 "hidden tail",
1035 ]
1036 .into_iter()
1037 .map(|text| Paragraph::new(text.into(), Format::default()))
1038 .collect(),
1039 )
1040 .unwrap();
1041 let mut nodes = document.nodes().to_vec();
1042 for (child, parent, level) in [(1, 0, 2), (2, 1, 3), (4, 3, 2), (5, 4, 3)] {
1043 nodes[child].parent = Some(nodes[parent].id);
1044 nodes[child].level = level;
1045 }
1046 nodes[1].collapsed = true;
1047 nodes[4].collapsed = true;
1048 let mut engine = TextEngine::default();
1049 let mut editor =
1050 CanvasEditor::new(&mut engine, TextDocument::from_nodes(nodes).unwrap(), 180.0)
1051 .unwrap();
1052 let mut access = Accessibility::default();
1053 let mut tree = None;
1054 let viewport = Viewport {
1055 size: [900, 700],
1056 scale: 2.0,
1057 origin: [13.0, 19.0],
1058 };
1059 let update = access
1060 .update(&editor, None, viewport, "Test", None, None)
1061 .unwrap();
1062 let field = apply(&mut tree, update).focus().unwrap();
1063 assert_eq!(
1064 field.document_range().text(),
1065 "root\nfolded\nlast\nfolded tail"
1066 );
1067 assert_eq!(
1068 access.fields[0]
1069 .paragraphs
1070 .iter()
1071 .map(|paragraph| paragraph.source)
1072 .collect::<Vec<_>>(),
1073 [0, 1, 3, 4]
1074 );
1075 let source = access
1076 .selection(
1077 editor.active_outline().id,
1078 field.document_range().to_text_selection(),
1079 )
1080 .unwrap();
1081 assert_eq!(
1082 source.positions[0],
1083 TextPosition {
1084 paragraph: 0,
1085 offset: 0
1086 }
1087 );
1088 assert_eq!(
1089 source.positions[1],
1090 TextPosition {
1091 paragraph: 4,
1092 offset: 11
1093 }
1094 );
1095 editor.select(source).unwrap();
1096 let update = access
1097 .update(&editor, None, viewport, "Test", None, None)
1098 .unwrap();
1099 assert_eq!(
1100 apply(&mut tree, update)
1101 .focus()
1102 .unwrap()
1103 .text_selection()
1104 .unwrap()
1105 .text(),
1106 "root\nfolded\nlast\nfolded tail"
1107 );
1108 editor
1109 .select(
1110 [TextPosition {
1111 paragraph: 4,
1112 offset: 0,
1113 }; 2]
1114 .into(),
1115 )
1116 .unwrap();
1117 let update = access
1118 .update(&editor, None, viewport, "Test", None, None)
1119 .unwrap();
1120 let caret = editor.caret(1.0).unwrap();
1121 assert_eq!(caret.x0, 27.0);
1122 let rects = apply(&mut tree, update)
1123 .focus()
1124 .unwrap()
1125 .text_selection()
1126 .unwrap()
1127 .bounding_boxes();
1128 assert_eq!(rects.len(), 1);
1129 assert_eq!(rects[0].x0, 27.0 * 2.0 + 13.0);
1130 assert_eq!(rects[0].y0, caret.y0 * 2.0 + 19.0);
1131 }
1132
1133 #[test]
1134 fn consumer_reads_unicode_blank_paragraphs_and_hidden_fields() {
1135 let cases = [
1136 "",
1137 "a\n\nืฉืœื•ื\nend",
1138 "café e\u{301} ๐ŸŒณ ๐Ÿ‘ฉ‍๐Ÿ‘ฉ‍๐Ÿ‘ง‍๐Ÿ‘ฆ words wrap here",
1139 "\n\n",
1140 "a\nb\n",
1141 ];
1142 let mut engine = TextEngine::default();
1143 for text in cases
1144 .into_iter()
1145 .map(str::to_owned)
1146 .chain([format!("a{}", "\u{301}".repeat(300)), "x".repeat(600)])
1147 {
1148 for width in [24.0, 480.0, 10000.0] {
1149 let document = TextDocument::new(
1150 text.split('\n')
1151 .map(|line| {
1152 Paragraph::from_runs([
1153 (
1154 String::from("secret"),
1155 Format {
1156 hidden: Some(true),
1157 ..Format::default()
1158 },
1159 ),
1160 (line.to_owned(), Format::default()),
1161 ])
1162 })
1163 .collect(),
1164 )
1165 .unwrap();
1166 let mut editor = CanvasEditor::new(&mut engine, document, width).unwrap();
1167 let mut access = Accessibility::default();
1168 let mut tree = None;
1169 let viewport = Viewport {
1170 size: [800, 600],
1171 origin: [48.0; 2],
1172 scale: 2.0,
1173 };
1174 let update = access
1175 .update(&editor, None, viewport, "Test", None, None)
1176 .unwrap();
1177 for (_, node) in &update.nodes {
1178 if node.role() == Role::TextRun {
1179 assert_eq!(
1180 node.character_lengths()
1181 .iter()
1182 .map(|n| *n as usize)
1183 .sum::<usize>(),
1184 node.value().unwrap().len()
1185 );
1186 if let Some(positions) = node.character_positions() {
1187 assert_eq!(positions.len(), node.character_lengths().len());
1188 }
1189 if let Some(widths) = node.character_widths() {
1190 assert_eq!(widths.len(), node.character_lengths().len());
1191 }
1192 }
1193 }
1194 let field = apply(&mut tree, update).focus().unwrap();
1195 assert_eq!(field.document_range().text(), text, "width {width}");
1196 let selection = field.document_range().to_text_selection();
1197 let source_selection = access
1198 .selection(editor.active_outline().id, selection)
1199 .unwrap();
1200 editor.select(source_selection).unwrap();
1201 let selected = access
1202 .update(&editor, None, viewport, "Test", None, None)
1203 .unwrap();
1204 assert_eq!(
1205 apply(&mut tree, selected)
1206 .focus()
1207 .unwrap()
1208 .text_selection()
1209 .unwrap()
1210 .text(),
1211 text
1212 );
1213 let zoomed = access
1214 .update(
1215 &editor,
1216 None,
1217 Viewport {
1218 scale: 3.0,
1219 ..viewport
1220 },
1221 "Test",
1222 None,
1223 None,
1224 )
1225 .unwrap();
1226 assert_eq!(
1227 zoomed.nodes.iter().map(|(id, _)| *id).collect::<Vec<_>>(),
1228 [ROOT, PAGE]
1229 );
1230 apply(&mut tree, zoomed);
1231 access.deactivate();
1232 let update = access
1233 .update(&editor, None, viewport, "Test", None, None)
1234 .unwrap();
1235 let state = apply(&mut tree, update);
1236 assert!(
1237 access
1238 .selection(editor.active_outline().id, selection)
1239 .is_err()
1240 );
1241 let selection = *state.focus().unwrap().data().text_selection().unwrap();
1242 editor.insert(&mut engine, "replacement").unwrap();
1243 let update = access
1244 .update(&editor, None, viewport, "Test", None, None)
1245 .unwrap();
1246 apply(&mut tree, update);
1247 assert!(
1248 access
1249 .selection(editor.active_outline().id, selection)
1250 .is_err()
1251 );
1252 editor.undo(&mut engine).unwrap();
1253 let update = access
1254 .update(&editor, None, viewport, "Test", None, None)
1255 .unwrap();
1256 assert_eq!(
1257 apply(&mut tree, update)
1258 .focus()
1259 .unwrap()
1260 .document_range()
1261 .text(),
1262 text
1263 );
1264 }
1265 }
1266 }
1267
1268 #[test]
1269 fn caret_geometry_uses_visible_offsets_and_canvas_line_metrics() {
1270 let mut engine = TextEngine::default();
1271 let document = TextDocument::new(vec![
1272 Paragraph::from_runs([
1273 ("abc ".into(), Format::default()),
1274 (
1275 "hidden field".into(),
1276 Format {
1277 hidden: Some(true),
1278 ..Format::default()
1279 },
1280 ),
1281 ("e\u{301} ๐ŸŒณ words wrap here".into(), Format::default()),
1282 ]),
1283 Paragraph::new("".into(), Format::default()),
1284 Paragraph::new("abc ืฉืœื•ื def ุนุงู„ู… end".into(), Format::default()),
1285 Paragraph::new("last\nline\n".into(), Format::default()),
1286 ])
1287 .unwrap();
1288 let positions: Vec<_> = document
1289 .paragraphs()
1290 .enumerate()
1291 .flat_map(|(paragraph, text)| {
1292 text.text()
1293 .char_indices()
1294 .map(|(byte, _)| text.utf16_offset(byte).unwrap())
1295 .chain(std::iter::once(
1296 text.utf16_offset(text.text().len()).unwrap(),
1297 ))
1298 .map(move |offset| TextPosition { paragraph, offset })
1299 })
1300 .collect();
1301 let mut editor = CanvasEditor::new(&mut engine, document, 48.0).unwrap();
1302 let mut access = Accessibility::default();
1303 let mut tree = None;
1304 let viewport = Viewport {
1305 size: [800, 600],
1306 origin: [31.0, -57.0],
1307 scale: 2.5,
1308 };
1309 for (position, affinity) in positions.into_iter().flat_map(|position| {
1310 [Affinity::Upstream, Affinity::Downstream].map(|affinity| (position, affinity))
1311 }) {
1312 editor
1313 .select(Selection {
1314 positions: [position; 2],
1315 affinities: [affinity; 2],
1316 })
1317 .unwrap();
1318 let caret = editor.caret(0.0).unwrap();
1319 let update = access
1320 .update(&editor, None, viewport, "Test", None, None)
1321 .unwrap();
1322 let field = apply(&mut tree, update).focus().unwrap();
1323 assert_eq!(
1324 field.document_range().text(),
1325 "abc e\u{301} ๐ŸŒณ words wrap here\n\nabc ืฉืœื•ื def ุนุงู„ู… end\nlast\nline\n"
1326 );
1327 let rects = field.text_selection().unwrap().bounding_boxes();
1328 assert_eq!(rects.len(), 1, "{position:?}");
1329 let rect = rects[0];
1330 for (actual, expected) in [
1331 (rect.x0, caret.x0 * 2.5 + 31.0),
1332 (rect.y0, caret.y0 * 2.5 - 57.0),
1333 (rect.y1, caret.y1 * 2.5 - 57.0),
1334 ] {
1335 assert!(
1336 (actual - expected).abs() < 0.0001,
1337 "{position:?} {affinity:?}: {rect:?} vs {caret:?}"
1338 );
1339 }
1340 let selection = access
1341 .selection(
1342 editor.active_outline().id,
1343 field.text_selection().unwrap().to_text_selection(),
1344 )
1345 .unwrap();
1346 editor.select(selection).unwrap();
1347 assert_eq!(
1348 editor.caret(0.0).unwrap(),
1349 caret,
1350 "{position:?} {affinity:?}: AX selection moved the caret"
1351 );
1352 }
1353 }
1354 #[test]
1355 fn keyboard_and_accessibility_preserve_visual_caret_at_wraps() {
1356 use draw::edit::Movement;
1357 let mut engine = TextEngine::default();
1358 let document = TextDocument::new(vec![
1359 Paragraph::new(
1360 "first words wrap here and continue".into(),
1361 Format::default(),
1362 ),
1363 Paragraph::new("abc ืฉืœื•ื def ุนุงู„ู… end".into(), Format::default()),
1364 Paragraph::new("last\nline".into(), Format::default()),
1365 ])
1366 .unwrap();
1367 let mut editor = CanvasEditor::new(&mut engine, document, 72.0).unwrap();
1368 let mut access = Accessibility::default();
1369 let mut tree = None;
1370 let viewport = Viewport {
1371 size: [800, 600],
1372 origin: [0.0; 2],
1373 scale: 1.0,
1374 };
1375 for movement in [
1376 Movement::LineEnd,
1377 Movement::Right,
1378 Movement::Left,
1379 Movement::Down,
1380 Movement::LineStart,
1381 Movement::LineEnd,
1382 Movement::Down,
1383 Movement::Down,
1384 Movement::Down,
1385 Movement::LineEnd,
1386 Movement::Left,
1387 Movement::Left,
1388 Movement::Right,
1389 Movement::LineStart,
1390 Movement::Up,
1391 Movement::Right,
1392 Movement::LineEnd,
1393 ] {
1394 editor.move_selection(&mut engine, movement, false).unwrap();
1395 let caret = editor.caret(0.0).unwrap();
1396 let update = access
1397 .update(&editor, None, viewport, "Test", None, None)
1398 .unwrap();
1399 let field = apply(&mut tree, update).focus().unwrap();
1400 let selection = field.text_selection().unwrap();
1401 let rects = selection.bounding_boxes();
1402 assert_eq!(rects.len(), 1);
1403 let rect = rects[0];
1404 for (actual, expected) in [
1405 (rect.x0, caret.x0),
1406 (rect.y0, caret.y0),
1407 (rect.y1, caret.y1),
1408 ] {
1409 assert!(
1410 (actual - expected).abs() < 0.0001,
1411 "{movement:?}: {rect:?} vs {caret:?}"
1412 );
1413 }
1414 let source_selection = access
1415 .selection(editor.active_outline().id, selection.to_text_selection())
1416 .unwrap();
1417 editor.select(source_selection).unwrap();
1418 let restored = editor.caret(0.0).unwrap();
1419 assert_eq!(
1420 restored, caret,
1421 "{movement:?}: AX selection moved the caret"
1422 );
1423 }
1424 }
1425 #[test]
1426 fn cached_runs_match_reconstruction_through_editing_and_view_changes() {
1427 let mut engine = TextEngine::default();
1428 let document = TextDocument::new(
1429 [
1430 "one two three",
1431 "",
1432 "ืื‘ื’ ๆ—ฅๆœฌ ๐Ÿ‘ฉ๐Ÿฝ‍๐Ÿ’ป e\u{301}",
1433 "last paragraph",
1434 ]
1435 .into_iter()
1436 .map(|text| Paragraph::new(text.into(), Format::default()))
1437 .collect(),
1438 )
1439 .unwrap();
1440 let mut editor = CanvasEditor::new(&mut engine, document, 72.0).unwrap();
1441 let mut cached = Accessibility::default();
1442 let mut tree = None;
1443 let mut compare = |editor: &CanvasEditor, step: usize| {
1444 let viewport = Viewport {
1445 size: [800, 600],
1446 origin: [step as f32 * 0.125, -17.25],
1447 scale: 0.75 + (step % 5) as f32 * 0.5,
1448 };
1449 let preview = step
1450 .is_multiple_of(2)
1451 .then_some((editor.active_outline().id, [13.5, -7.25]));
1452 for repeat in 0..2 {
1453 let update = cached
1454 .update(editor, None, viewport, "Test", preview, None)
1455 .unwrap();
1456 let actual = apply(&mut tree, update);
1457 let mut rebuilt = Accessibility::default();
1458 let expected = Tree::new(
1459 rebuilt
1460 .update(editor, None, viewport, "Test", preview, None)
1461 .unwrap(),
1462 true,
1463 );
1464 assert_eq!(
1465 canonical(actual),
1466 canonical(expected.state()),
1467 "step {step}, repeat {repeat}"
1468 );
1469 let [actual, expected] =
1470 [(&cached, actual), (&rebuilt, expected.state())].map(|(access, state)| {
1471 let field = state.focus().unwrap();
1472 let selection = field.document_range().to_text_selection();
1473 access
1474 .selection(editor.active_outline().id, selection)
1475 .unwrap()
1476 });
1477 assert_eq!(actual, expected);
1478 let count = editor
1479 .outlines()
1480 .iter()
1481 .map(|o| o.layouts().count())
1482 .sum::<usize>();
1483 assert_eq!(
1484 cached
1485 .fields
1486 .iter()
1487 .map(|field| field.paragraphs.len())
1488 .sum::<usize>(),
1489 count
1490 );
1491 }
1492 };
1493 compare(&editor, 0);
1494 let mut seed = 0x923e_u64;
1495 let mut next = || {
1496 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
1497 (seed >> 32) as usize
1498 };
1499 for step in 1..=160 {
1500 let id = editor.outlines()[next() % editor.outlines().len()].id;
1501 editor.focus_outline(id).unwrap();
1502 let positions: Vec<_> = editor
1503 .active_outline()
1504 .document()
1505 .paragraphs()
1506 .enumerate()
1507 .flat_map(|(paragraph, text)| {
1508 (0..=text.text().len()).filter_map(move |byte| {
1509 text.utf16_offset(byte)
1510 .ok()
1511 .map(|offset| TextPosition { paragraph, offset })
1512 })
1513 })
1514 .collect();
1515 let anchor = positions[next() % positions.len()];
1516 let focus = if step % 3 == 0 {
1517 positions[next() % positions.len()]
1518 } else {
1519 anchor
1520 };
1521 editor.select([anchor, focus].into()).unwrap();
1522 match step % 10 {
1523 0 => editor.insert(&mut engine, "\n").unwrap(),
1524 1 => editor.insert(&mut engine, "abc ๐Ÿ‘ฉ๐Ÿฝ‍๐Ÿ’ป ืฉืœื•ื e\u{301}").unwrap(),
1525 2 => {
1526 editor.delete(&mut engine, true).unwrap();
1527 }
1528 3 => editor
1529 .resize(&mut engine, 48.0 + (step % 7) as f32 * 31.25)
1530 .unwrap(),
1531 4 => {
1532 editor
1533 .create_outline(&mut engine, [step as f32, -5.0], 97.0)
1534 .unwrap();
1535 editor.insert(&mut engine, "annotation").unwrap();
1536 }
1537 5 => {
1538 editor.undo(&mut engine).unwrap();
1539 }
1540 6 => {
1541 editor.redo(&mut engine).unwrap();
1542 }
1543 7 => editor.move_outline(id, [17.5, step as f32]).unwrap(),
1544 8 => {
1545 editor.compose(&mut engine, "ไปฎ\nๅ".into(), 1..1).unwrap();
1546 compare(&editor, step);
1547 editor.cancel_composition(&mut engine).unwrap();
1548 }
1549 _ => {}
1550 }
1551 compare(&editor, step);
1552 }
1553 while editor.undo(&mut engine).unwrap() {
1554 compare(&editor, 161);
1555 }
1556 while editor.redo(&mut engine).unwrap() {
1557 compare(&editor, 162);
1558 }
1559 }
1560 #[test]
1561 fn scrolling_sends_the_page_and_typing_sends_the_edited_paragraph() {
1562 let mut engine = TextEngine::default();
1563 let document = TextDocument::new(
1564 (0..200)
1565 .map(|index| Paragraph::new(format!("paragraph {index}"), Format::default()))
1566 .collect(),
1567 )
1568 .unwrap();
1569 let mut editor = CanvasEditor::new(&mut engine, document, 480.0).unwrap();
1570 let mut access = Accessibility::default();
1571 let mut tree = None;
1572 let mut viewport = Viewport {
1573 size: [800, 600],
1574 origin: [48.0; 2],
1575 scale: 2.0,
1576 };
1577 let update = access
1578 .update(&editor, None, viewport, "Test", None, None)
1579 .unwrap();
1580 apply(&mut tree, update);
1581 let ids = |update: &TreeUpdate| update.nodes.iter().map(|(id, _)| *id).collect::<Vec<_>>();
1582 for (scroll, zoom) in [(-300.0, 1.0), (150.0, 1.25)] {
1583 viewport.origin[1] += scroll;
1584 viewport.scale *= zoom;
1585 let update = access
1586 .update(&editor, None, viewport, "Test", None, None)
1587 .unwrap();
1588 assert_eq!(ids(&update), [ROOT, PAGE]);
1589 apply(&mut tree, update);
1590 }
1591 let field = access.fields[0].id;
1592 editor
1593 .select(
1594 [TextPosition {
1595 paragraph: 150,
1596 offset: 3,
1597 }; 2]
1598 .into(),
1599 )
1600 .unwrap();
1601 let update = access
1602 .update(&editor, None, viewport, "Test", None, None)
1603 .unwrap();
1604 assert_eq!(ids(&update), [field, ROOT, PAGE]);
1605 apply(&mut tree, update);
1606 editor.insert(&mut engine, "x").unwrap();
1607 let update = access
1608 .update(&editor, None, viewport, "Test", None, None)
1609 .unwrap();
1610 let paragraph = &access.fields[0].paragraphs[150];
1611 assert_eq!(
1612 ids(&update),
1613 [paragraph.runs[0].id, paragraph.id, field, ROOT, PAGE]
1614 );
1615 assert_eq!(update.nodes[0].1.value(), Some("parxagraph 150\n"));
1616 let state = apply(&mut tree, update);
1617 let expected = Tree::new(
1618 Accessibility::default()
1619 .update(&editor, None, viewport, "Test", None, None)
1620 .unwrap(),
1621 true,
1622 );
1623 assert_eq!(canonical(state), canonical(expected.state()));
1624 }
1625
1626 #[test]
1627 fn outlines_have_independent_accessibility_identity_and_selection() {
1628 let mut engine = TextEngine::default();
1629 let document =
1630 TextDocument::new(vec![Paragraph::new("lyrics".into(), Format::default())]).unwrap();
1631 let mut editor = CanvasEditor::new(&mut engine, document, 240.0).unwrap();
1632 let first = editor.active_outline().id;
1633 let second = editor
1634 .create_outline(&mut engine, [300.0, 40.0], 120.0)
1635 .unwrap();
1636 editor.insert(&mut engine, "annotation ๐ŸŒณ").unwrap();
1637 let mut access = Accessibility::default();
1638 let viewport = Viewport {
1639 size: [1000, 720],
1640 scale: 2.0,
1641 origin: [48.0; 2],
1642 };
1643 let update = access
1644 .update(&editor, None, viewport, "Test", None, None)
1645 .unwrap();
1646 let fields: Vec<_> = update
1647 .nodes
1648 .iter()
1649 .filter(|(_, node)| node.role() == Role::MultilineTextInput)
1650 .map(|(id, node)| (*id, node.value().unwrap().to_owned()))
1651 .collect();
1652 assert_eq!(
1653 fields
1654 .iter()
1655 .map(|(_, text)| text.as_str())
1656 .collect::<Vec<_>>(),
1657 ["lyrics", "annotation ๐ŸŒณ"]
1658 );
1659 assert_ne!(fields[0].0, fields[1].0);
1660 assert_eq!(access.outline_for_node(fields[0].0), Some(first));
1661 assert_eq!(access.outline_for_node(fields[1].0), Some(second));
1662 let mut tree = None;
1663 let field = apply(&mut tree, update).focus().unwrap();
1664 assert_eq!(field.document_range().text(), "annotation ๐ŸŒณ");
1665 let selection = field.document_range().to_text_selection();
1666 assert!(access.selection(first, selection).is_err());
1667 let source_selection = access.selection(second, selection).unwrap();
1668 editor.select(source_selection).unwrap();
1669 let preview = access
1670 .update(
1671 &editor,
1672 None,
1673 viewport,
1674 "Test",
1675 Some((second, [400.0, 50.0])),
1676 None,
1677 )
1678 .unwrap();
1679 let preview_field = apply(&mut tree, preview).focus().unwrap();
1680 let rect = preview_field.document_range().bounding_boxes()[0];
1681 assert_eq!(rect.x0, 848.0);
1682 assert_eq!(rect.y0, 148.0);
1683 assert_eq!(editor.active_outline().origin(), [300.0, 40.0]);
1684 editor.move_outline(second, [400.0, 50.0]).unwrap();
1685 let update = access
1686 .update(&editor, None, viewport, "Test", None, None)
1687 .unwrap();
1688 apply(&mut tree, update);
1689 assert_eq!(access.outline_for_node(fields[1].0), Some(second));
1690 assert!(access.selection(second, selection).is_ok());
1691 editor.undo(&mut engine).unwrap();
1692 editor.undo(&mut engine).unwrap();
1693 editor.undo(&mut engine).unwrap();
1694 let update = access
1695 .update(&editor, None, viewport, "Test", None, None)
1696 .unwrap();
1697 assert_eq!(
1698 nodes(apply(&mut tree, update))
1699 .iter()
1700 .filter(|node| node.role() == Role::MultilineTextInput)
1701 .count(),
1702 1
1703 );
1704 assert_eq!(access.outline_for_node(fields[1].0), None);
1705 assert!(access.selection(second, selection).is_err());
1706 assert_eq!(access.outline_for_node(fields[0].0), Some(first));
1707 }
1708
1709 #[test]
1710 fn the_end_of_a_wrapped_rtl_word_stays_on_its_line() {
1711 let mut engine = TextEngine::default();
1712 let text = "Fictitious: café, ๆฑไบฌ, ู…ุฑุญุจุง";
1713 let end = text.encode_utf16().count() as u32;
1714 let mut wrapped = false;
1715 for width in [60.0, 90.0, 120.0, 480.0] {
1716 let document =
1717 TextDocument::new(vec![Paragraph::new(text.into(), Format::default())]).unwrap();
1718 let mut editor = CanvasEditor::new(&mut engine, document, width).unwrap();
1719 let caret = |editor: &mut CanvasEditor, offset| {
1720 let position = TextPosition {
1721 paragraph: 0,
1722 offset,
1723 };
1724 editor.select([position; 2].into()).unwrap();
1725 editor.caret(0.0).unwrap()
1726 };
1727 let inside = caret(&mut editor, end - 2);
1728 let first = caret(&mut editor, 0);
1729 wrapped |= inside.y0 > first.y0;
1730 assert_eq!(caret(&mut editor, end).y0, inside.y0, "width {width}");
1731 editor.select_all().unwrap();
1732 let viewport = Viewport {
1733 size: [800, 600],
1734 origin: [0.0; 2],
1735 scale: 1.0,
1736 };
1737 Accessibility::default()
1738 .update(&editor, None, viewport, "Test", None, None)
1739 .unwrap();
1740 }
1741 assert!(wrapped);
1742 }
1743}