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