1use crate::{
2 date::PageDate,
3 document::{DocumentEdit, TextDocument, TextPosition, descendants, leaves},
4 layout::{LayoutError, TextEngine},
5 outline::{Arrange, OutlineLayout, ParagraphLayout, outline_layout, visible_paragraphs},
6};
7use draw::edit::{self, Movement, SelectionUnit};
8use onestore::ExGuid;
9use onestore::document::{Format, Kind};
10use onestore::op::PageOp;
11use onestore::page::text::{EditError, Paragraph};
12use onestore::page::{
13 Definition, Outline, Page, PageObject, PageParagraph, ParagraphContent, Title,
14};
15use parley::{
16 Affinity, BoundingBox,
17 editing::{Cursor, Selection as ParagraphSelection},
18};
19use std::{
20 collections::{BTreeMap, BTreeSet},
21 fmt,
22 ops::Range,
23};
24
25pub const DEFAULT_OUTLINE_WIDTH: f32 = 468.0;
26
27mod clip;
28mod equation;
29#[cfg(test)]
30mod evidence;
31mod format;
32mod html;
33mod ink;
34mod link;
35mod markdown;
36mod ops;
37pub(crate) mod page;
38mod recording;
39mod space;
40mod table;
41pub use clip::Clip;
42pub use format::{
43 Alignment, BULLET_LIBRARY, DefaultFont, FormatState, Formatting, ListStyle, NUMBER_LIBRARY,
44 NoteTag, Toggle,
45};
46pub use html::html_pieces;
47pub use ink::{FAVORITES, Pen};
48pub use link::{Link, shown_urls};
49pub use page::ReadOnlyObject;
50
51#[derive(Debug)]
52pub enum EditorError {
53 Edit(EditError),
54 Layout(LayoutError),
55 InvalidGeometry,
56}
57
58impl fmt::Display for EditorError {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 match self {
61 Self::Edit(error) => error.fmt(f),
62 Self::Layout(error) => error.fmt(f),
63 Self::InvalidGeometry => {
64 f.write_str("The page contains invalid object dimensions or positions.")
65 }
66 }
67 }
68}
69
70impl std::error::Error for EditorError {}
71
72impl From<EditError> for EditorError {
73 fn from(error: EditError) -> Self {
74 Self::Edit(error)
75 }
76}
77
78impl From<LayoutError> for EditorError {
79 fn from(error: LayoutError) -> Self {
80 Self::Layout(error)
81 }
82}
83
84#[derive(Clone, Copy, Debug, PartialEq)]
85pub struct Selection {
86 pub positions: [TextPosition; 2],
87 pub affinities: [Affinity; 2],
88}
89
90/// A piece of pasted content, in the order it comes.
91pub enum Piece {
92 Clip(Clip),
93 /// An encoded picture and its size in points.
94 Picture(Vec<u8>, [f32; 2]),
95 /// A picture still on its way, which ends the paragraph and goes in where
96 /// [`CanvasEditor::paste_pieces`] says once it arrives.
97 Awaited,
98}
99
100/// Where an awaited picture goes: before paragraph `before` of outline `outline`.
101#[derive(Clone, Copy, Debug, PartialEq)]
102pub struct Awaited {
103 outline: ExGuid,
104 before: ExGuid,
105}
106
107/// A new picture of encoded `bytes`, `size` points large.
108pub fn picture(bytes: Vec<u8>, size: [f32; 2]) -> Result<onestore::page::Image, EditError> {
109 Ok(onestore::page::Image {
110 id: onestore::page::text::new_id()?,
111 layout: onestore::document::Layout {
112 max_width: Some(size[0]),
113 max_height: Some(size[1]),
114 ..Default::default()
115 },
116 size: Some(size),
117 bytes: Some(bytes.into()),
118 display: None,
119 alt: None,
120 background: false,
121 printout: None,
122 tags: Vec::new(),
123 link: None,
124 text: None,
125 })
126}
127
128/// What OneNote 2010's Ctrl+A selects beyond text: the focused outline as an object, then every
129/// body outline on the page.
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub enum Whole {
132 Outline,
133 Page,
134}
135
136impl From<[TextPosition; 2]> for Selection {
137 fn from(positions: [TextPosition; 2]) -> Self {
138 Self {
139 positions,
140 affinities: [Affinity::Downstream; 2],
141 }
142 }
143}
144
145pub struct CanvasEditor {
146 pub(crate) objects: Vec<page::Content>,
147 date: Option<PageDate>,
148 outlines: Vec<TextOutline>,
149 pub(crate) definitions: BTreeMap<ExGuid, Definition>,
150 header: PageHeader,
151 active: Focus,
152 undo: Vec<History>,
153 redo: Vec<History>,
154 composition: Option<Composition>,
155 preferred_x: Option<f32>,
156 /// Formatting chosen at a caret for the text typed there next, until an edit.
157 pending: Option<(ExGuid, TextPosition, onestore::document::Format)>,
158 /// What `take_ops` hands over next.
159 ops: Result<Vec<PageOp>, onestore::Error>,
160 /// The page as stored when the editor last read it and the ops `take_ops` handed out
161 /// since, which `refresh` compares a changed stored page with; none once the editor
162 /// holds an edit that could not be stored.
163 stored: Option<(Page, Vec<PageOp>)>,
164 /// What Select All widened to, holding until the selection is moved or set, and while the
165 /// focused outline and its selection stay as they were then.
166 whole: Option<(Whole, ExGuid, Selection)>,
167 /// The outline, caret and undo depth a run of typing left, which typing there joins as
168 /// OneNote 2010 does until the caret moves or another kind of edit comes between.
169 typing: Option<(ExGuid, TextPosition, usize)>,
170 recording: Option<recording::Live>,
171 /// Drawings being dragged and how far, shown there until dropped.
172 ink_drag: Option<(Vec<ExGuid>, [f32; 2])>,
173 /// The paragraph each outline holding only pictures or files shows after them, by outline,
174 /// and whether it is stored: the first op that reaches it stores it, and undoing back to it
175 /// empty and alone removes it again, as OneNote 2010's undo does.
176 provisional: BTreeMap<ExGuid, (PageParagraph, bool)>,
177 /// What new outlines' text is set in; `PageView::replace` hands it to the next page's
178 /// editor.
179 pub default_font: DefaultFont,
180 /// The paragraph styles the page's theme gives, by stored name: new text takes its `p`,
181 /// and Enter a NextStyle the page lacks from here.
182 pub styles: BTreeMap<String, Definition>,
183 /// OneNote 2010's gallery styles, which Markdown shortcuts apply where the page's theme
184 /// gives none; none turns the shortcuts off.
185 pub markdown: Option<BTreeMap<String, Definition>>,
186 /// The outline, caret and undo depth a paragraph's Markdown marker left, where Backspace
187 /// takes it back.
188 formatted: Option<(ExGuid, TextPosition, usize)>,
189}
190
191/// Imported page state the editable content does not carry.
192#[derive(Default)]
193struct PageHeader {
194 title: String,
195 identity: Option<[u8; 16]>,
196 created: Option<u64>,
197 margin_origin: [f32; 2],
198 rtl: bool,
199 color: Option<u32>,
200 rule_lines: Option<onestore::page::RuleLines>,
201 areas: Vec<page::TitleArea>,
202}
203
204enum Focus {
205 Outline(usize),
206 Draft {
207 index: usize,
208 outline: Box<TextOutline>,
209 },
210 Caret {
211 outline: Box<TextOutline>,
212 index: usize,
213 },
214}
215
216#[derive(Clone)]
217pub struct TextOutline {
218 pub id: onestore::ExGuid,
219 pub title: bool,
220 min_width: Option<f32>,
221 layout: onestore::document::Layout,
222 document: TextDocument,
223 pub(crate) indents: Vec<f32>,
224 shaped: OutlineLayout,
225 selection: Selection,
226}
227
228impl TextOutline {
229 /// A single plain paragraph containing only ASCII spaces or no text.
230 pub fn is_empty(&self) -> bool {
231 let [node] = self.document.nodes() else {
232 return false;
233 };
234 node.lists.is_empty()
235 && node.tags.is_empty()
236 && !node.collapsed
237 && node.text().is_some_and(|text| {
238 text.tags.is_empty() && text.text.text().bytes().all(|byte| byte == b' ')
239 })
240 }
241
242 /// Tests an outline-local point against the provisional paragraph band.
243 pub fn contains_extension(&self, point: [f32; 2]) -> bool {
244 !self.title
245 && (0.0..=self.shaped.size[0]).contains(&point[0])
246 && point[1] > self.shaped.size[1]
247 && point[1] <= self.shaped.size[1] + 27.0
248 }
249
250 pub fn new(
251 engine: &mut TextEngine,
252 document: TextDocument,
253 width: f32,
254 position: [f32; 2],
255 ) -> Result<Self, EditorError> {
256 if !position.iter().all(|value| value.is_finite()) {
257 return Err(EditError::InvalidRange.into());
258 }
259 let indents = vec![18.0, 0.0, 27.0, 27.0];
260 let fixed_width = document.nodes().len() != 1
261 || document.nodes()[0]
262 .text()
263 .is_none_or(|text| !text.text.text().is_empty());
264 let shaped = OutlineLayout::flow(
265 document.nodes().iter(),
266 &indents,
267 width,
268 fixed_width,
269 0,
270 None,
271 &mut |paragraph, previous, width, indents| {
272 ParagraphLayout::shape(
273 engine,
274 paragraph,
275 previous,
276 width,
277 indents,
278 &BTreeMap::new(),
279 )
280 },
281 )?;
282 Ok(Self {
283 id: onestore::page::text::new_id()?,
284 title: false,
285 min_width: None,
286 layout: onestore::document::Layout {
287 x: Some(position[0]),
288 y: Some(position[1]),
289 max_width: Some(width),
290 width_set_by_user: Some(fixed_width),
291 ..Default::default()
292 },
293 document,
294 indents,
295 shaped,
296 selection: [TextPosition {
297 paragraph: 0,
298 offset: 0,
299 }; 2]
300 .into(),
301 })
302 }
303
304 pub fn from_outline(
305 engine: &mut TextEngine,
306 outline: &Outline,
307 definitions: &BTreeMap<ExGuid, Definition>,
308 ) -> Result<Self, EditorError> {
309 if [outline.layout.x, outline.layout.y]
310 .into_iter()
311 .flatten()
312 .any(|value| !value.is_finite())
313 {
314 return Err(EditError::InvalidRange.into());
315 }
316 if outline
317 .min_width
318 .is_some_and(|width| !width.is_finite() || width <= 0.0)
319 {
320 return Err(LayoutError::InvalidWidth.into());
321 }
322 let shaped = if outline.title {
323 if outline
324 .layout
325 .max_height
326 .is_some_and(|height| !height.is_finite() || height < 0.0)
327 {
328 return Err(LayoutError::InvalidSpacing.into());
329 }
330 outline_layout(
331 outline,
332 engine,
333 definitions,
334 outline
335 .layout
336 .reserved_width
337 .or(outline.layout.max_width)
338 .unwrap_or(crate::outline::TITLE_WIDTH),
339 )?
340 } else {
341 outline.layout(engine, definitions)?
342 };
343 let document = TextDocument::from_nodes(outline.paragraphs.clone())?;
344 // The caret needs a paragraph of text to stand in; a page gives an outline holding
345 // none a provisional one (`page::build`).
346 if document.text_nodes().next().is_none() {
347 return Err(EditError::InvalidStructure.into());
348 }
349 Ok(Self {
350 id: outline.id,
351 title: outline.title,
352 min_width: outline.min_width,
353 layout: outline.layout.clone(),
354 indents: outline.indents.clone(),
355 document,
356 shaped,
357 selection: [TextPosition {
358 paragraph: 0,
359 offset: 0,
360 }; 2]
361 .into(),
362 })
363 }
364
365 pub fn snapshot(&self) -> Outline {
366 Outline {
367 id: self.id,
368 title: self.title,
369 min_width: self.min_width,
370 layout: self.layout.clone(),
371 indents: self.indents.clone(),
372 paragraphs: self.document.nodes().to_vec(),
373 unsupported: Vec::new(),
374 }
375 }
376
377 pub fn document(&self) -> &TextDocument {
378 &self.document
379 }
380
381 pub fn layout(&self) -> &onestore::document::Layout {
382 &self.layout
383 }
384
385 pub fn shaped(&self) -> &OutlineLayout {
386 &self.shaped
387 }
388
389 pub fn origin(&self) -> [f32; 2] {
390 crate::origin(&self.layout)
391 }
392
393 pub fn wrap_width(&self) -> f32 {
394 self.layout
395 .reserved_width
396 .or(self.layout.max_width)
397 .unwrap_or(crate::outline::TITLE_WIDTH)
398 }
399
400 /// Native typing room expands the interactive bounds without changing the wrap constraint.
401 pub fn bounds(&self) -> BoundingBox {
402 let [x, y] = self.origin().map(f64::from);
403 let width = if self.layout.width_set_by_user == Some(true) {
404 self.shaped.size[0]
405 } else {
406 let text_width = if self.shaped.tables.is_empty() {
407 Some(self.shaped.size[0])
408 } else {
409 Some(self.shaped.widths[1]).filter(|width| *width > f32::NEG_INFINITY)
410 };
411 text_width.map_or(self.shaped.size[0], |width| {
412 (width + 36.0)
413 .max(72.0)
414 .min(self.wrap_width())
415 .max(self.shaped.size[0])
416 })
417 }
418 .max(self.min_width.unwrap_or(0.0));
419 BoundingBox {
420 x0: x,
421 y0: y,
422 x1: x + f64::from(width),
423 y1: y + f64::from(if self.title {
424 self.shaped.size[1].max(self.layout.max_height.unwrap_or(0.0))
425 } else {
426 self.shaped.size[1]
427 }),
428 }
429 }
430
431 fn geometry(&self) -> OutlineGeometry<'_> {
432 OutlineGeometry {
433 source: self,
434 shaped: &self.shaped,
435 }
436 }
437
438 pub fn layouts(&self) -> impl Iterator<Item = (usize, &ParagraphLayout)> {
439 self.geometry().layouts()
440 }
441
442 fn visible_index(&self, source: usize) -> Result<usize, EditError> {
443 self.geometry().visible_index(source)
444 }
445
446 fn source_index(&self, visible: usize) -> usize {
447 self.geometry().source_index(visible)
448 }
449
450 fn next_sibling_start(&self, index: usize) -> Option<TextPosition> {
451 self.geometry().next_sibling_start(index)
452 }
453
454 pub fn paragraph_layout(&self, source: usize) -> Result<&ParagraphLayout, EditError> {
455 self.geometry().paragraph_layout(source)
456 }
457
458 /// The shown paragraphs with their source text: those a collapsed parent hides have no
459 /// layout and are left out.
460 fn shown(&self) -> impl Iterator<Item = (usize, &Paragraph)> {
461 self.layouts()
462 .filter_map(|(index, _)| Some((index, self.document.paragraph(index)?)))
463 }
464
465 /// The outline's text as platform text input sees it: the shown paragraphs joined by
466 /// `\n`, addressed in UTF-16 units by `utf16_offset` and `utf16_position`.
467 pub fn shown_text(&self) -> String {
468 self.shown()
469 .map(|(_, paragraph)| paragraph.text())
470 .collect::<Vec<_>>()
471 .join("\n")
472 }
473
474 /// All the outline's shown text.
475 pub(crate) fn whole(&self) -> Selection {
476 let paragraph = self.source_index(self.shaped.paragraphs.len() - 1);
477 let last = self.document.paragraph(paragraph).unwrap();
478 [
479 TextPosition {
480 paragraph: 0,
481 offset: 0,
482 },
483 TextPosition {
484 paragraph,
485 offset: last
486 .utf16_offset(last.text().len())
487 .expect("paragraph text fits UTF-16 offsets"),
488 },
489 ]
490 .into()
491 }
492
493 /// The caret at `focus`, in the outline's coordinates.
494 pub fn caret_at(
495 &self,
496 focus: TextPosition,
497 affinity: Affinity,
498 width: f32,
499 ) -> Result<BoundingBox, EditError> {
500 self.geometry().caret_at(focus, affinity, width)
501 }
502
503 /// Highlight rectangles of `selection` in the outline, in its coordinates.
504 pub fn range_rects(&self, selection: Selection) -> Result<Vec<BoundingBox>, EditError> {
505 self.geometry().range_rects(selection)
506 }
507
508 /// Where a mark under UTF-16 `range` of paragraph `index` runs, outline-local: each
509 /// line's left, right and baseline.
510 #[cfg(test)]
511 pub(crate) fn underlines(
512 &self,
513 index: usize,
514 range: Range<u32>,
515 ) -> Result<Vec<[f32; 3]>, EditError> {
516 self.geometry().underlines(index, range)
517 }
518
519 /// Where `position` falls in `shown_text`; a hidden paragraph falls at the start of the
520 /// next shown one.
521 pub fn utf16_offset(&self, position: TextPosition) -> Result<u32, EditError> {
522 let mut offset = 0;
523 for (index, paragraph) in self.shown() {
524 if index == position.paragraph {
525 return Ok(offset + position.offset);
526 }
527 if index > position.paragraph {
528 return Ok(offset);
529 }
530 offset += paragraph.utf16_offset(paragraph.text().len())? + 1;
531 }
532 Ok(offset.saturating_sub(1))
533 }
534
535 /// The source position at `offset` in `shown_text`, clamped to its end.
536 pub fn utf16_position(&self, offset: u32) -> Result<TextPosition, EditError> {
537 let mut rest = offset;
538 let mut end = None;
539 for (index, paragraph) in self.shown() {
540 let length = paragraph.utf16_offset(paragraph.text().len())?;
541 if rest <= length {
542 return Ok(TextPosition {
543 paragraph: index,
544 offset: rest,
545 });
546 }
547 rest -= length + 1;
548 end = Some(TextPosition {
549 paragraph: index,
550 offset: length,
551 });
552 }
553 end.ok_or(EditError::InvalidRange)
554 }
555}
556
557#[derive(Clone, Copy)]
558pub(crate) struct OutlineGeometry<'a> {
559 pub(crate) source: &'a TextOutline,
560 pub(crate) shaped: &'a OutlineLayout,
561}
562
563struct Navigation {
564 selection: Selection,
565 preferred_x: Option<f32>,
566 boundary: bool,
567}
568
569impl<'a> OutlineGeometry<'a> {
570 pub(crate) fn layouts(self) -> impl Iterator<Item = (usize, &'a ParagraphLayout)> + 'a {
571 let mut shaped = self.shaped.paragraphs.iter().peekable();
572 self.source
573 .document
574 .text_nodes()
575 .enumerate()
576 .filter_map(move |(index, node)| {
577 if shaped
578 .peek()
579 .is_some_and(|paragraph| paragraph.id == node.id)
580 {
581 Some((index, shaped.next().unwrap()))
582 } else {
583 None
584 }
585 })
586 }
587
588 /// Hidden leaves have no layout; matching IDs avoid searching the common flat case.
589 fn visible_index(self, source: usize) -> Result<usize, EditError> {
590 let id = self
591 .source
592 .document
593 .leaf(source)
594 .ok_or(EditError::InvalidRange)?
595 .2
596 .id;
597 if self
598 .shaped
599 .paragraphs
600 .get(source)
601 .is_some_and(|paragraph| paragraph.id == id)
602 {
603 return Ok(source);
604 }
605 self.shaped
606 .paragraphs
607 .iter()
608 .position(|paragraph| paragraph.id == id)
609 .ok_or(EditError::InvalidRange)
610 }
611
612 fn source_index(self, visible: usize) -> usize {
613 let id = self.shaped.paragraphs[visible].id;
614 if self
615 .source
616 .document
617 .leaf(visible)
618 .is_some_and(|(_, _, node)| node.id == id)
619 {
620 return visible;
621 }
622 self.source
623 .document
624 .text_nodes()
625 .position(|paragraph| paragraph.id == id)
626 .unwrap()
627 }
628
629 /// Including a sibling's start lets whole-paragraph deletion take the break.
630 fn next_sibling_start(self, index: usize) -> Option<TextPosition> {
631 let next =
632 (index + 1 < self.shaped.paragraphs.len()).then(|| self.source_index(index + 1))?;
633 let (container, local, _) = self.source.document.leaf(self.source_index(index))?;
634 let (next_container, next_local, _) = self.source.document.leaf(next)?;
635 (container == next_container && next_local == local + 1).then_some(TextPosition {
636 paragraph: next,
637 offset: 0,
638 })
639 }
640
641 fn paragraph_layout(self, source: usize) -> Result<&'a ParagraphLayout, EditError> {
642 Ok(&self.shaped.paragraphs[self.visible_index(source)?])
643 }
644
645 pub(crate) fn caret_at(
646 self,
647 focus: TextPosition,
648 affinity: Affinity,
649 width: f32,
650 ) -> Result<BoundingBox, EditError> {
651 let paragraph = self.paragraph_layout(focus.paragraph)?;
652 let cursor = paragraph.cursor(focus.offset, affinity)?;
653 let mut rect = paragraph.text.caret(cursor, width);
654 rect.x0 += f64::from(paragraph.origin[0]);
655 rect.x1 += f64::from(paragraph.origin[0]);
656 rect.y0 += f64::from(paragraph.origin[1]);
657 rect.y1 += f64::from(paragraph.origin[1]);
658 Ok(rect)
659 }
660
661 pub(crate) fn range_rects(self, selection: Selection) -> Result<Vec<BoundingBox>, EditError> {
662 let [anchor, focus] = selection.positions;
663 if anchor == focus {
664 return Ok(Vec::new());
665 }
666 let (start, end, affinities) = if anchor < focus {
667 (anchor, focus, selection.affinities)
668 } else {
669 (
670 focus,
671 anchor,
672 [selection.affinities[1], selection.affinities[0]],
673 )
674 };
675 let mut rectangles = Vec::new();
676 for (visible, (index, paragraph)) in self
677 .layouts()
678 .enumerate()
679 .filter(|(_, (index, _))| *index >= start.paragraph && *index <= end.paragraph)
680 {
681 let source = self.source.document.paragraph(index).unwrap();
682 let first = if index == start.paragraph {
683 paragraph.cursor(start.offset, affinities[0])?
684 } else {
685 paragraph.cursor(0, Affinity::Downstream)?
686 };
687 let last = if index == end.paragraph {
688 paragraph.cursor(end.offset, affinities[1])?
689 } else {
690 paragraph.cursor(
691 source.utf16_offset(source.text().len())?,
692 Affinity::Upstream,
693 )?
694 };
695 let mut local = paragraph
696 .text
697 .selection(ParagraphSelection::new(first, last));
698 if index < end.paragraph {
699 local.push(paragraph.text.caret(last, 4.0));
700 }
701 for mut rect in local {
702 rect.x0 += f64::from(paragraph.origin[0]);
703 rect.x1 += f64::from(paragraph.origin[0]);
704 rect.y0 += f64::from(paragraph.origin[1]);
705 rect.y1 += f64::from(paragraph.origin[1]);
706 if let Some(cell) = self.shaped.paragraph_cell(visible) {
707 let Some(clipped) = cell.clip(rect) else {
708 continue;
709 };
710 rect = clipped;
711 }
712 rectangles.push(rect);
713 }
714 }
715 Ok(rectangles)
716 }
717
718 pub(crate) fn underlines(
719 self,
720 index: usize,
721 range: Range<u32>,
722 ) -> Result<Vec<[f32; 3]>, EditError> {
723 let paragraph = self.paragraph_layout(index)?;
724 let selection = ParagraphSelection::new(
725 paragraph.cursor(range.start, Affinity::Downstream)?,
726 paragraph.cursor(range.end, Affinity::Upstream)?,
727 );
728 let [x, y] = paragraph.origin;
729 Ok(paragraph
730 .text
731 .underlines(selection)
732 .into_iter()
733 .map(|[left, right, baseline]| [left + x, right + x, baseline + y])
734 .collect())
735 }
736
737 pub(crate) fn selection_at(
738 self,
739 x: f32,
740 y: f32,
741 unit: SelectionUnit,
742 ) -> Result<Selection, EditError> {
743 if !x.is_finite() || !y.is_finite() {
744 return Err(EditError::InvalidRange);
745 }
746 let index = self.paragraph_at(x, y)?;
747 let paragraph = &self.shaped.paragraphs[index];
748 let layout = &paragraph.text.shaped;
749 let point = [x - paragraph.origin[0], y - paragraph.origin[1]];
750 let local = edit::selection_at(
751 layout,
752 paragraph.text.hit_test(point[0], point[1]),
753 point[0],
754 unit,
755 );
756 let position = |cursor: Cursor| -> Result<TextPosition, EditError> {
757 let visible = paragraph.projection.text().utf16_offset(cursor.index())?;
758 Ok(TextPosition {
759 paragraph: self.source_index(index),
760 offset: paragraph
761 .projection
762 .source_offset(visible, crate::affinity(cursor.affinity()))?,
763 })
764 };
765 let mut selection = Selection {
766 positions: [position(local.anchor())?, position(local.focus())?],
767 affinities: [local.anchor().affinity(), local.focus().affinity()],
768 };
769 if unit == SelectionUnit::Paragraph
770 && let Some(next) = self.next_sibling_start(index)
771 {
772 selection.positions[1] = next;
773 selection.affinities[1] = Affinity::Downstream;
774 }
775 Ok(selection)
776 }
777
778 fn column(self, x: f32, current: ExGuid) -> Result<Vec<usize>, EditError> {
779 let paragraphs = self
780 .shaped
781 .paragraphs
782 .iter()
783 .enumerate()
784 .map(|(index, paragraph)| (paragraph.id, index))
785 .collect::<BTreeMap<_, _>>();
786 let tables = self
787 .shaped
788 .tables
789 .iter()
790 .map(|table| (table.id, table))
791 .collect::<BTreeMap<_, _>>();
792 let mut result = Vec::new();
793 let mut pending = vec![visible_paragraphs(self.source.document.nodes().iter())];
794 while let Some(nodes) = pending.last_mut() {
795 let Some(node) = nodes.next() else {
796 pending.pop();
797 continue;
798 };
799 match &node.content {
800 onestore::page::ParagraphContent::Text(_) => {
801 result.push(
802 *paragraphs
803 .get(&node.id)
804 .ok_or(EditError::InvalidStructure)?,
805 );
806 }
807 onestore::page::ParagraphContent::Table(table) => {
808 let layout = tables.get(&table.id).ok_or(EditError::InvalidStructure)?;
809 let cells = layout
810 .cells
811 .get(..table.columns.len())
812 .ok_or(EditError::InvalidStructure)?;
813 let (first, last) = cells
814 .first()
815 .zip(cells.last())
816 .ok_or(EditError::InvalidStructure)?;
817 let x = x.clamp(first.rect[0], last.rect[2]);
818 let column = cells
819 .iter()
820 .enumerate()
821 .min_by(|(_, a), (_, b)| {
822 let distance = |cell: &crate::outline::CellLayout| {
823 (f64::from(x) - f64::from(x.clamp(cell.rect[0], cell.rect[2])))
824 .abs()
825 };
826 distance(a).total_cmp(&distance(b))
827 })
828 .ok_or(EditError::InvalidStructure)?
829 .0;
830 for row in table.rows.iter().rev() {
831 let cell = row
832 .cells
833 .iter()
834 .find(|cell| {
835 leaves(&cell.paragraphs, None)
836 .any(|(_, _, node)| node.id == current)
837 })
838 .unwrap_or(&row.cells[column]);
839 pending.push(visible_paragraphs(cell.paragraphs.iter()));
840 }
841 }
842 onestore::page::ParagraphContent::Image(_)
843 | onestore::page::ParagraphContent::Attachment(_)
844 | onestore::page::ParagraphContent::Ink(_)
845 | onestore::page::ParagraphContent::Unsupported(_) => {}
846 }
847 }
848 Ok(result)
849 }
850
851 fn navigate(
852 self,
853 mut selection: Selection,
854 movement: Movement,
855 extend: bool,
856 mut preferred_x: Option<f32>,
857 ) -> Result<Navigation, EditError> {
858 let [anchor, focus] = selection.positions;
859 if matches!(
860 movement,
861 Movement::ParagraphStart
862 | Movement::ParagraphEnd
863 | Movement::DocumentStart
864 | Movement::DocumentEnd
865 ) {
866 let current = self.visible_index(focus.paragraph)?;
867 let end = matches!(movement, Movement::ParagraphEnd | Movement::DocumentEnd);
868 let current_end = self.source.document.paragraph(focus.paragraph).unwrap();
869 let current_end = current_end.utf16_offset(current_end.text().len())?;
870 let index = match movement {
871 Movement::DocumentStart => 0,
872 Movement::DocumentEnd => self.shaped.paragraphs.len() - 1,
873 Movement::ParagraphStart if focus.offset == 0 => current.saturating_sub(1),
874 Movement::ParagraphEnd if focus.offset == current_end => {
875 (current + 1).min(self.shaped.paragraphs.len() - 1)
876 }
877 _ => current,
878 };
879 let source = self.source_index(index);
880 let paragraph = self.source.document.paragraph(source).unwrap();
881 let position = TextPosition {
882 paragraph: source,
883 offset: if end {
884 paragraph.utf16_offset(paragraph.text().len())?
885 } else {
886 0
887 },
888 };
889 let affinity = if end {
890 Affinity::Upstream
891 } else {
892 Affinity::Downstream
893 };
894 if !extend {
895 selection.positions[0] = position;
896 selection.affinities[0] = affinity;
897 }
898 selection.positions[1] = position;
899 selection.affinities[1] = affinity;
900 return Ok(Navigation {
901 selection,
902 preferred_x: None,
903 boundary: false,
904 });
905 }
906 let horizontal = matches!(
907 movement,
908 Movement::Left | Movement::Right | Movement::WordLeft | Movement::WordRight
909 );
910 let left = matches!(movement, Movement::Left | Movement::WordLeft);
911 if horizontal && !extend && anchor.paragraph != focus.paragraph {
912 let index = usize::from(if left { anchor > focus } else { anchor < focus });
913 let selection = Selection {
914 positions: [selection.positions[index]; 2],
915 affinities: [selection.affinities[index]; 2],
916 };
917 return Ok(Navigation {
918 selection,
919 preferred_x: None,
920 boundary: false,
921 });
922 }
923 let mut index = self.visible_index(focus.paragraph)?;
924 let paragraph = &self.shaped.paragraphs[index];
925 let cursor = paragraph.cursor(focus.offset, selection.affinities[1])?;
926 let mut next;
927 let boundary;
928 if matches!(movement, Movement::Up | Movement::Down) {
929 let caret = paragraph.text.caret(cursor, 1.0);
930 let x = preferred_x.unwrap_or(caret.x0 as f32 + paragraph.origin[0]);
931 let line = paragraph
932 .text
933 .lines()
934 .position(|(_, line)| f64::from(line.top) == caret.y0)
935 .unwrap();
936 let up = matches!(movement, Movement::Up);
937 let target = if up {
938 line.checked_sub(1)
939 .and_then(|line| paragraph.text.lines().nth(line))
940 } else {
941 paragraph.text.lines().nth(line + 1)
942 }
943 .map(|(_, line)| line);
944 let target = if target.is_some() {
945 target
946 } else {
947 let neighbor = if self.shaped.tables.is_empty() {
948 if up {
949 index.checked_sub(1)
950 } else {
951 (index + 1 < self.shaped.paragraphs.len()).then_some(index + 1)
952 }
953 } else {
954 let column = self.column(x, paragraph.id)?;
955 let current = column
956 .iter()
957 .position(|candidate| *candidate == index)
958 .ok_or(EditError::InvalidStructure)?;
959 if up {
960 current.checked_sub(1).map(|i| column[i])
961 } else {
962 column.get(current + 1).copied()
963 }
964 };
965 neighbor.and_then(|neighbor| {
966 index = neighbor;
967 let paragraph = &self.shaped.paragraphs[index];
968 if up {
969 paragraph.text.lines().last()
970 } else {
971 paragraph.text.lines().next()
972 }
973 .map(|(_, line)| line)
974 })
975 };
976 boundary = target.is_none();
977 next = target
978 .map(|line| {
979 self.shaped.paragraphs[index].text.hit_test(
980 x - self.shaped.paragraphs[index].origin[0],
981 line.top + line.height * 0.5,
982 )
983 })
984 .unwrap_or(cursor);
985 preferred_x = Some(x);
986 } else {
987 let local = if anchor.paragraph == focus.paragraph {
988 ParagraphSelection::new(
989 paragraph.cursor(anchor.offset, selection.affinities[0])?,
990 cursor,
991 )
992 } else {
993 cursor.into()
994 };
995 next = edit::step(&paragraph.text.shaped, local, movement, extend).focus();
996 if horizontal && next == cursor && (extend || anchor == focus) {
997 let neighbor = if left {
998 index.checked_sub(1)
999 } else {
1000 (index + 1 < self.shaped.paragraphs.len()).then_some(index + 1)
1001 };
1002 if let Some(neighbor) = neighbor {
1003 index = neighbor;
1004 let paragraph = &self.shaped.paragraphs[index];
1005 let line = if left {
1006 paragraph.text.lines().last()
1007 } else {
1008 paragraph.text.lines().next()
1009 }
1010 .unwrap()
1011 .1;
1012 next = paragraph.text.hit_test(
1013 if left { f32::MAX } else { -1.0 },
1014 line.top + line.height * 0.5,
1015 );
1016 if matches!(movement, Movement::WordLeft | Movement::WordRight) {
1017 next = edit::word_cursor(&paragraph.text.shaped, next, left);
1018 }
1019 }
1020 }
1021 boundary = !extend
1022 && anchor == focus
1023 && next == cursor
1024 && self.source_index(index) == focus.paragraph
1025 && matches!(movement, Movement::Left | Movement::Right);
1026 }
1027 if !matches!(movement, Movement::Up | Movement::Down) {
1028 preferred_x = None;
1029 }
1030 let paragraph = &self.shaped.paragraphs[index];
1031 let visible = paragraph.projection.text().utf16_offset(next.index())?;
1032 let position = TextPosition {
1033 paragraph: self.source_index(index),
1034 offset: paragraph
1035 .projection
1036 .source_offset(visible, crate::affinity(next.affinity()))?,
1037 };
1038 if !extend {
1039 selection.positions[0] = position;
1040 selection.affinities[0] = next.affinity();
1041 }
1042 selection.positions[1] = position;
1043 selection.affinities[1] = next.affinity();
1044 Ok(Navigation {
1045 selection,
1046 preferred_x,
1047 boundary,
1048 })
1049 }
1050
1051 fn paragraph_at(self, x: f32, y: f32) -> Result<usize, EditError> {
1052 if self.shaped.tables.is_empty() {
1053 return Ok(self
1054 .shaped
1055 .paragraphs
1056 .partition_point(|paragraph| paragraph.origin[1] <= y)
1057 .saturating_sub(1));
1058 }
1059 let paragraphs = self
1060 .shaped
1061 .paragraphs
1062 .iter()
1063 .enumerate()
1064 .map(|(index, paragraph)| (paragraph.id, (index, paragraph)))
1065 .collect::<BTreeMap<_, _>>();
1066 let tables = self
1067 .shaped
1068 .tables
1069 .iter()
1070 .map(|table| (table.id, table))
1071 .collect::<BTreeMap<_, _>>();
1072 let mut nodes = self.source.document.nodes();
1073 loop {
1074 let mut target = None;
1075 for node in visible_paragraphs(nodes.iter()) {
1076 let top = match &node.content {
1077 onestore::page::ParagraphContent::Text(_) => {
1078 paragraphs
1079 .get(&node.id)
1080 .ok_or(EditError::InvalidStructure)?
1081 .1
1082 .origin[1]
1083 }
1084 onestore::page::ParagraphContent::Table(table) => {
1085 tables
1086 .get(&table.id)
1087 .and_then(|layout| layout.cells.first())
1088 .ok_or(EditError::InvalidStructure)?
1089 .rect[1]
1090 - 1.86
1091 }
1092 // Objects hold no caret; text around them takes the hit.
1093 onestore::page::ParagraphContent::Image(_)
1094 | onestore::page::ParagraphContent::Attachment(_)
1095 | onestore::page::ParagraphContent::Ink(_)
1096 | onestore::page::ParagraphContent::Unsupported(_) => continue,
1097 };
1098 if target.is_none() || top <= y {
1099 target = Some(node);
1100 }
1101 if top > y {
1102 break;
1103 }
1104 }
1105 let target = target.ok_or(EditError::InvalidStructure)?;
1106 let onestore::page::ParagraphContent::Table(table) = &target.content else {
1107 return paragraphs
1108 .get(&target.id)
1109 .map(|(index, _)| *index)
1110 .ok_or(EditError::InvalidStructure);
1111 };
1112 let layout = tables.get(&table.id).ok_or(EditError::InvalidStructure)?;
1113 let (first, last) = layout
1114 .cells
1115 .first()
1116 .zip(layout.cells.last())
1117 .ok_or(EditError::InvalidStructure)?;
1118 let x = x.clamp(first.rect[0], last.rect[2]);
1119 let y = y.clamp(first.rect[1], last.rect[3]);
1120 let cell = layout
1121 .cells
1122 .iter()
1123 .map(|cell| {
1124 let dx = f64::from(x) - f64::from(x.clamp(cell.rect[0], cell.rect[2]));
1125 let dy = f64::from(y) - f64::from(y.clamp(cell.rect[1], cell.rect[3]));
1126 (cell, dy.abs(), dx.abs())
1127 })
1128 .min_by(|a, b| a.1.total_cmp(&b.1).then(a.2.total_cmp(&b.2)))
1129 .ok_or(EditError::InvalidStructure)?
1130 .0;
1131 nodes = self.source.document.container(Some(cell.id))?;
1132 }
1133 }
1134}
1135
1136struct Composition {
1137 original: TextChange,
1138 range: Range<TextPosition>,
1139}
1140
1141impl ParagraphLayout {
1142 fn cursor(&self, source: u32, affinity: Affinity) -> Result<Cursor, EditError> {
1143 let visible = self.projection.visible_offset(source)?;
1144 let byte = self.projection.text().byte_offset(visible)?;
1145 Ok(self.text.cursor(byte, affinity))
1146 }
1147}
1148
1149#[derive(Clone)]
1150struct TextChange {
1151 edit: DocumentEdit,
1152 selection: Selection,
1153 positions: Vec<Placement>,
1154}
1155
1156#[derive(Clone)]
1157struct Placement {
1158 id: ExGuid,
1159 position: [Option<f32>; 2],
1160}
1161
1162#[derive(Clone)]
1163enum RestoreFocus {
1164 Outline(ExGuid),
1165 Caret {
1166 source: Box<Outline>,
1167 selection: Selection,
1168 index: usize,
1169 },
1170}
1171
1172enum History {
1173 Date(Box<PageDate>),
1174 /// The page's colour and rule lines before a change to either.
1175 Paper {
1176 color: Option<u32>,
1177 rule_lines: Option<onestore::page::RuleLines>,
1178 },
1179 Draft {
1180 outlines: Box<[TextOutline; 2]>,
1181 index: usize,
1182 caret: Box<Outline>,
1183 selection: Selection,
1184 restore_caret: bool,
1185 },
1186 Text {
1187 outline: onestore::ExGuid,
1188 change: Box<TextChange>,
1189 },
1190 /// A page object's position: an outline, picture or ink drawing.
1191 Position {
1192 object: onestore::ExGuid,
1193 position: [Option<f32>; 2],
1194 },
1195 /// Parts `outline`'s paragraphs from root node `at` into outline `part` at `position`.
1196 Split {
1197 outline: ExGuid,
1198 at: usize,
1199 part: ExGuid,
1200 position: [f32; 2],
1201 },
1202 /// Returns `part`'s paragraphs to the end of `outline` and removes it: each root of
1203 /// `part` under the parent it had, the outline when `None`.
1204 Join {
1205 outline: ExGuid,
1206 part: ExGuid,
1207 parents: Vec<(ExGuid, Option<ExGuid>)>,
1208 },
1209 Layout {
1210 outline: ExGuid,
1211 layout: onestore::document::Layout,
1212 },
1213 Image {
1214 image: ExGuid,
1215 layout: onestore::document::Layout,
1216 },
1217 /// Restores the picture at `index` in paint order, or removes the one there when `None`.
1218 Picture {
1219 index: usize,
1220 image: Option<Box<onestore::page::Image>>,
1221 },
1222 /// Restores the file on the page at `index` in paint order, or removes the one there
1223 /// when `None`.
1224 File {
1225 index: usize,
1226 file: Option<Box<page::Content>>,
1227 },
1228 /// Restores the drawing on the page at `index` in paint order, or removes the one there
1229 /// when `None`.
1230 Ink {
1231 index: usize,
1232 ink: Option<Box<onestore::page::Ink>>,
1233 },
1234 /// Gives a picture or file on the page back the note tags it had.
1235 Tags {
1236 object: ExGuid,
1237 tags: Vec<onestore::document::Tag>,
1238 },
1239 /// Gives a drawing back the strokes it had.
1240 Strokes {
1241 ink: ExGuid,
1242 strokes: Vec<onestore::page::InkStroke>,
1243 },
1244 Remove {
1245 outline: onestore::ExGuid,
1246 focus: RestoreFocus,
1247 },
1248 Insert {
1249 index: usize,
1250 outline: Box<TextOutline>,
1251 focus: RestoreFocus,
1252 },
1253 Restore {
1254 index: usize,
1255 source: Box<Outline>,
1256 selection: Selection,
1257 focus: RestoreFocus,
1258 },
1259 /// One undo step of several edits, undone last first; `page` when they edited OneNote's
1260 /// page selection, which undoing selects again.
1261 Group {
1262 entries: Vec<History>,
1263 page: bool,
1264 },
1265}
1266
1267impl CanvasEditor {
1268 pub fn new(
1269 engine: &mut TextEngine,
1270 document: TextDocument,
1271 width: f32,
1272 ) -> Result<Self, EditorError> {
1273 let outline = TextOutline::new(engine, document, width, [0.0; 2])?;
1274 let empty = outline.is_empty();
1275 let mut editor = Self::from_text_outlines(vec![outline], BTreeMap::new(), None)?;
1276 if empty {
1277 editor.active = Focus::Caret {
1278 outline: Box::new(editor.outlines.pop().unwrap()),
1279 index: 0,
1280 };
1281 }
1282 Ok(editor)
1283 }
1284
1285 pub fn from_page(mut page: Page, engine: &mut TextEngine) -> Result<Self, EditorError> {
1286 let stored = page.clone();
1287 let page::Import {
1288 objects,
1289 mut outlines,
1290 date,
1291 areas,
1292 provisional,
1293 } = page::build(&mut page, engine, true)?;
1294 let needs_caret = outlines.is_empty();
1295 if needs_caret {
1296 let x = objects
1297 .iter()
1298 .filter_map(|object| match object {
1299 page::Content::ReadOnly(object) => Some(object.rect()[2]),
1300 page::Content::File { .. } => object.file().map(|(_, rect)| rect[2]),
1301 page::Content::Ink(ink) => page::ink_bounds(ink).map(|b| b[2]),
1302 _ => None,
1303 })
1304 .fold(0.0_f32, f32::max);
1305 outlines.push(TextOutline::new(
1306 engine,
1307 TextDocument::new(vec![Paragraph::new(String::new(), Default::default())])?,
1308 240.0,
1309 [if x > 0.0 { x + 24.0 } else { 0.0 }, 0.0],
1310 )?);
1311 }
1312 let mut editor = Self::from_text_outlines(outlines, page.definitions, date)?;
1313 if needs_caret {
1314 editor.active = Focus::Caret {
1315 outline: Box::new(editor.outlines.pop().unwrap()),
1316 index: 0,
1317 };
1318 }
1319 editor.objects = objects;
1320 editor.provisional = provisional
1321 .into_iter()
1322 .map(|(outline, blank)| (outline, (blank, false)))
1323 .collect();
1324 editor.header = PageHeader {
1325 title: page.title,
1326 identity: page.identity,
1327 created: page.created,
1328 margin_origin: page.margin_origin,
1329 rtl: page.rtl,
1330 color: page.color,
1331 rule_lines: page.rule_lines,
1332 areas,
1333 };
1334 let mut ids = BTreeSet::new();
1335 if !editor.object_layouts().all(|(id, _)| ids.insert(id)) {
1336 return Err(EditError::InvalidStructure.into());
1337 }
1338 editor.stored = Some((stored, Vec::new()));
1339 Ok(editor)
1340 }
1341
1342 /// Shows `page`, the stored page after a change made elsewhere. What the change did not
1343 /// reach stays as it is, history included: the page as stored before it (as last read,
1344 /// with the ops handed out since) is compared with `page`; outlines the change reached
1345 /// show anew with the caret and selection kept by paragraph identity, and history entries
1346 /// editing them are dropped. False when the change reached nothing shown. Marked text
1347 /// must be committed or cancelled first.
1348 pub fn refresh(&mut self, page: Page, engine: &mut TextEngine) -> Result<bool, EditorError> {
1349 let known = self.stored.take().and_then(|(mut stored, sent)| {
1350 sent.iter()
1351 .try_for_each(|op| onestore::op::predict(&mut stored, op))
1352 .ok()
1353 .map(|()| stored)
1354 });
1355 // Without that, the editor's page stands in, which storage may normalize apart.
1356 let known = match known {
1357 Some(known) => known,
1358 None => self.page()?,
1359 };
1360 self.stored = Some((page.clone(), Vec::new()));
1361 if known == page {
1362 return Ok(false);
1363 }
1364 fn outlines(page: &Page) -> BTreeMap<ExGuid, &Outline> {
1365 page.objects
1366 .iter()
1367 .flat_map(|object| match object {
1368 PageObject::Outline(outline) => std::slice::from_ref(outline),
1369 PageObject::Title(title) => title.outlines.as_slice(),
1370 _ => &[],
1371 })
1372 .map(|outline| (outline.id, outline))
1373 .collect()
1374 }
1375 let (before, after) = (outlines(&known), outlines(&page));
1376 let changed: BTreeSet<ExGuid> = before
1377 .keys()
1378 .chain(after.keys())
1379 .filter(|id| before.get(id) != after.get(id))
1380 .copied()
1381 .collect();
1382 // Everything but outline content: the page's objects, geometry and date.
1383 let frame = |page: &Page| {
1384 let hollow = |outline: &Outline| Outline {
1385 id: outline.id,
1386 title: false,
1387 min_width: None,
1388 layout: Default::default(),
1389 indents: Vec::new(),
1390 paragraphs: Vec::new(),
1391 unsupported: Vec::new(),
1392 };
1393 let objects: Vec<PageObject> = page
1394 .objects
1395 .iter()
1396 .map(|object| match object {
1397 PageObject::Outline(outline) => PageObject::Outline(hollow(outline)),
1398 PageObject::Title(title) => PageObject::Title(Title {
1399 id: title.id,
1400 date: title.date,
1401 layout: title.layout.clone(),
1402 outlines: title.outlines.iter().map(hollow).collect(),
1403 }),
1404 object => object.clone(),
1405 })
1406 .collect();
1407 (page.created, page.margin_origin, objects)
1408 };
1409 let objects_changed = frame(&known) != frame(&page);
1410 let date_changed = known.created != page.created
1411 || self
1412 .date
1413 .as_ref()
1414 .is_some_and(|date| changed.contains(&date.source().id));
1415 let mut fresh = Self::from_page(page, engine)?;
1416 let shown = self.active_outline();
1417 let (id, selection) = (shown.id, shown.selection);
1418 let reached = changed.contains(&id);
1419 let document = reached.then(|| shown.document.clone());
1420 let mut own: BTreeMap<ExGuid, TextOutline> = std::mem::take(&mut self.outlines)
1421 .into_iter()
1422 .map(|outline| (outline.id, outline))
1423 .collect();
1424 for outline in &mut fresh.outlines {
1425 if !changed.contains(&outline.id)
1426 && let Some(kept) = own.remove(&outline.id)
1427 {
1428 *outline = kept;
1429 match self.provisional.remove(&outline.id) {
1430 Some(provisional) => fresh.provisional.insert(outline.id, provisional),
1431 None => fresh.provisional.remove(&outline.id),
1432 };
1433 }
1434 }
1435 let position = fresh.outlines.iter().position(|outline| outline.id == id);
1436 match (
1437 std::mem::replace(&mut self.active, Focus::Outline(0)),
1438 position,
1439 ) {
1440 (Focus::Caret { outline, index }, _) => {
1441 fresh.active = Focus::Caret {
1442 outline,
1443 index: index.min(fresh.outlines.len()),
1444 };
1445 }
1446 (Focus::Draft { outline, .. }, Some(index)) if !reached => {
1447 fresh.active = Focus::Draft { index, outline };
1448 }
1449 (_, Some(index)) if !reached => fresh.active = Focus::Outline(index),
1450 (_, Some(index)) => {
1451 fresh.active = Focus::Outline(index);
1452 let document = document.expect("a reached outline's document was kept");
1453 let mapped = Selection {
1454 positions: selection.positions.map(|position| {
1455 follow(&document, &fresh.outlines[index].document, position)
1456 }),
1457 affinities: selection.affinities,
1458 };
1459 let _ = fresh.select(mapped);
1460 self.pending = self.pending.take().filter(|(_, at, _)| {
1461 follow(&document, &fresh.active_outline().document, *at) == *at
1462 });
1463 }
1464 (_, None) => self.pending = None,
1465 }
1466 if !date_changed {
1467 fresh.date = self.date.take();
1468 }
1469 if !objects_changed {
1470 fresh.objects = std::mem::take(&mut self.objects);
1471 }
1472 for (id, definition) in std::mem::take(&mut self.definitions) {
1473 fresh.definitions.entry(id).or_insert(definition);
1474 }
1475 // Undoing an outline's creation focuses the outline focused before, if it is there.
1476 let gone = |focus: &RestoreFocus| matches!(focus, RestoreFocus::Outline(id) if !fresh.outlines.iter().any(|o| o.id == *id));
1477 let reaches_one = |history: &History| match history {
1478 History::Date(_) => date_changed,
1479 History::Paper { .. } => false,
1480 History::Image { .. }
1481 | History::Tags { .. }
1482 | History::Picture { .. }
1483 | History::File { .. }
1484 | History::Ink { .. }
1485 | History::Strokes { .. } => objects_changed,
1486 History::Draft { outlines, .. } => changed.contains(&outlines[0].id),
1487 History::Text { outline, change } => {
1488 changed.contains(outline)
1489 || change
1490 .positions
1491 .iter()
1492 .any(|placement| objects_changed || changed.contains(&placement.id))
1493 }
1494 History::Position { object, .. } => changed.contains(object) || objects_changed,
1495 History::Layout { outline, .. } => changed.contains(outline),
1496 History::Split { outline, part, .. } | History::Join { outline, part, .. } => {
1497 changed.contains(outline) || changed.contains(part) || objects_changed
1498 }
1499 History::Remove { outline, focus } => changed.contains(outline) || gone(focus),
1500 History::Insert { outline, focus, .. } => changed.contains(&outline.id) || gone(focus),
1501 History::Restore { source, focus, .. } => changed.contains(&source.id) || gone(focus),
1502 History::Group { .. } => unreachable!("groups do not nest"),
1503 };
1504 let reaches = |history: &History| match history {
1505 History::Group { entries, .. } => entries.iter().any(reaches_one),
1506 history => reaches_one(history),
1507 };
1508 // A typing run goes on while the change leaves its undo step alone.
1509 let run = self
1510 .typing
1511 .filter(|_| self.undo.last().is_some_and(|last| !reaches(last)));
1512 fresh.undo = std::mem::take(&mut self.undo)
1513 .into_iter()
1514 .filter(|history| !reaches(history))
1515 .collect();
1516 fresh.typing = run.map(|(outline, caret, _)| (outline, caret, fresh.undo.len()));
1517 fresh.redo = std::mem::take(&mut self.redo)
1518 .into_iter()
1519 .filter(|history| !reaches(history))
1520 .collect();
1521 fresh.pending = self.pending.take();
1522 fresh.preferred_x = self.preferred_x;
1523 fresh.ops = std::mem::replace(&mut self.ops, Ok(Vec::new()));
1524 fresh.stored = self.stored.take();
1525 fresh.default_font = std::mem::take(&mut self.default_font);
1526 fresh.styles = std::mem::take(&mut self.styles);
1527 fresh.markdown = self.markdown.take();
1528 *self = fresh;
1529 Ok(true)
1530 }
1531
1532 /// Lays every outline out again, keeping its text and selection, as after fonts arrived
1533 /// that draw characters the old layout had no face for.
1534 pub fn relayout(&mut self, engine: &mut TextEngine) -> Result<(), EditorError> {
1535 for outline in &mut self.outlines {
1536 outline.shaped =
1537 TextOutline::from_outline(engine, &outline.snapshot(), &self.definitions)?.shaped;
1538 }
1539 Ok(())
1540 }
1541
1542 /// Rebuilds the stored page, restoring the title areas and read-only objects import split up.
1543 pub fn page(&self) -> Result<Page, EditorError> {
1544 let mut objects: Vec<PageObject> = Vec::new();
1545 for content in &self.objects {
1546 let mut outline = match content {
1547 page::Content::Editable(id) => {
1548 match self.outlines.iter().find(|outline| outline.id == *id) {
1549 Some(outline) => {
1550 let mut outline = outline.snapshot();
1551 if let Some((blank, false)) = self.provisional.get(id) {
1552 outline.paragraphs.retain(|node| node.id != blank.id);
1553 }
1554 outline
1555 }
1556 None => continue,
1557 }
1558 }
1559 page::Content::Outline { source, .. } => source.clone(),
1560 page::Content::Date { .. } => self
1561 .date
1562 .as_ref()
1563 .ok_or(EditError::InvalidStructure)?
1564 .source()
1565 .clone(),
1566 page::Content::Image(image) => {
1567 objects.push(PageObject::Image(image.clone()));
1568 continue;
1569 }
1570 page::Content::File { source, .. } => {
1571 objects.push(PageObject::Attachment(source.clone()));
1572 continue;
1573 }
1574 page::Content::Ink(ink) => {
1575 objects.push(PageObject::Ink(ink.clone()));
1576 continue;
1577 }
1578 page::Content::ReadOnly(object) => {
1579 objects.push(object.source.clone());
1580 continue;
1581 }
1582 };
1583 let area = self
1584 .header
1585 .areas
1586 .iter()
1587 .find_map(|area| Some((area, *area.origins.get(&outline.id)?)));
1588 let Some((area, origin)) = area else {
1589 objects.push(PageObject::Outline(outline));
1590 continue;
1591 };
1592 [outline.layout.x, outline.layout.y] = origin;
1593 match objects.last_mut() {
1594 Some(PageObject::Title(title)) if title.id == area.id => {
1595 title.outlines.push(outline)
1596 }
1597 _ => objects.push(PageObject::Title(Title {
1598 id: area.id,
1599 date: area.date,
1600 layout: area.layout.clone(),
1601 outlines: vec![outline],
1602 })),
1603 }
1604 }
1605 objects.extend(
1606 self.outlines
1607 .iter()
1608 .filter(|outline| !self.has_page_outline(outline.id))
1609 .map(|outline| PageObject::Outline(outline.snapshot())),
1610 );
1611 // Definitions stay for undo after their last paragraph lets go; the page, as
1612 // OneNote stores it, holds only those its paragraphs reference.
1613 let mut referenced = BTreeSet::new();
1614 for object in &objects {
1615 let tags = match object {
1616 PageObject::Image(image) => image.tags.as_slice(),
1617 PageObject::Attachment(file) => file.tags.as_slice(),
1618 _ => &[],
1619 };
1620 referenced.extend(tags.iter().filter_map(|tag| tag.definition));
1621 let outlines = match object {
1622 PageObject::Outline(outline) => std::slice::from_ref(outline),
1623 PageObject::Title(title) => title.outlines.as_slice(),
1624 _ => &[],
1625 };
1626 for (_, _, node) in outlines
1627 .iter()
1628 .flat_map(|outline| descendants(&outline.paragraphs, None))
1629 {
1630 referenced.extend(ops::references(node));
1631 }
1632 }
1633 let mut definitions = self.definitions.clone();
1634 definitions.retain(|id, _| referenced.contains(id));
1635 Ok(Page {
1636 title: self.header.title.clone(),
1637 identity: self.header.identity,
1638 created: self
1639 .date
1640 .as_ref()
1641 .map(PageDate::timestamp)
1642 .or(self.header.created),
1643 margin_origin: self.header.margin_origin,
1644 rtl: self.header.rtl,
1645 color: self.header.color,
1646 rule_lines: self.header.rule_lines,
1647 objects,
1648 definitions,
1649 })
1650 }
1651
1652 /// Whether this outline occupies a slot in the imported page's paint order.
1653 pub fn has_page_outline(&self, id: ExGuid) -> bool {
1654 self.objects
1655 .iter()
1656 .any(|object| matches!(object, page::Content::Editable(candidate) if *candidate == id))
1657 }
1658
1659 pub fn object_layouts(&self) -> impl Iterator<Item = (ExGuid, &onestore::document::Layout)> {
1660 self.outlines
1661 .iter()
1662 .map(|outline| (outline.id, &outline.layout))
1663 .chain(
1664 self.date
1665 .iter()
1666 .map(|date| (date.source().id, &date.source().layout)),
1667 )
1668 .chain(self.objects.iter().filter_map(page::Content::layout))
1669 }
1670
1671 fn object_layout_mut(&mut self, id: ExGuid) -> Option<&mut onestore::document::Layout> {
1672 self.outlines
1673 .iter_mut()
1674 .map(|outline| (outline.id, &mut outline.layout))
1675 .chain(
1676 self.objects
1677 .iter_mut()
1678 .filter_map(page::Content::layout_mut),
1679 )
1680 .find_map(|(candidate, layout)| (candidate == id).then_some(layout))
1681 }
1682
1683 fn header_bottom(&self, title: ExGuid, text_bottom: f32) -> f32 {
1684 self.objects
1685 .iter()
1686 .filter_map(|object| match object {
1687 page::Content::Outline {
1688 source,
1689 layout,
1690 below_title,
1691 } if *below_title == Some(title) => {
1692 Some(text_bottom + source.layout.y.unwrap_or(0.0) + layout.size[1])
1693 }
1694 page::Content::Date { below_title } if *below_title == Some(title) => {
1695 self.date.as_ref().map(|date| {
1696 text_bottom + date.source().layout.y.unwrap_or(0.0) + date.layout().size[1]
1697 })
1698 }
1699 _ => None,
1700 })
1701 .fold(text_bottom, f32::max)
1702 }
1703
1704 /// Where the caret goes on leaving `title`: the first body line under its header, on
1705 /// OneNote's 18-point grid.
1706 fn body_start_of(&self, title: &TextOutline) -> [f32; 2] {
1707 let bottom = self.header_bottom(title.id, title.bounds().y1 as f32);
1708 [
1709 title.origin()[0],
1710 ((((f64::from(bottom) - 14.4) / 18.0).ceil() + 1.0) * 18.0 + 14.4) as f32,
1711 ]
1712 }
1713
1714 /// The page's colour (View, Page Color), COLORREF.
1715 pub fn page_color(&self) -> Option<u32> {
1716 self.header.color
1717 }
1718
1719 /// The page's notebook-management identity, which internal links name.
1720 pub fn identity(&self) -> Option<[u8; 16]> {
1721 self.header.identity
1722 }
1723
1724 /// The page's rule lines (View, Rule Lines).
1725 pub fn rule_lines(&self) -> Option<onestore::page::RuleLines> {
1726 self.header.rule_lines
1727 }
1728
1729 /// Where the page's first body line starts, in points, under its title.
1730 pub fn body_start(&self) -> Option<[f32; 2]> {
1731 let title = self
1732 .visible_outlines()
1733 .chain(self.caret_outline())
1734 .find(|outline| outline.title)?;
1735 Some(self.body_start_of(title))
1736 }
1737
1738 pub fn leave_title(&mut self, engine: &mut TextEngine) -> Result<(), EditorError> {
1739 let title = self.active_outline();
1740 if !title.title {
1741 return Ok(());
1742 }
1743 let position = self.body_start_of(title);
1744 if let Some((id, origin)) = self
1745 .outlines()
1746 .iter()
1747 .rev()
1748 .find(|outline| {
1749 let bounds = outline.bounds();
1750 let [x, y] = position.map(f64::from);
1751 !outline.title
1752 && x >= bounds.x0
1753 && x <= bounds.x1
1754 && y >= bounds.y0
1755 && y <= bounds.y1
1756 })
1757 .map(|outline| (outline.id, outline.origin()))
1758 {
1759 self.focus_outline(id)?;
1760 self.select_at(position[0] - origin[0], position[1] - origin[1], false)?;
1761 } else {
1762 self.place_caret(engine, position, DEFAULT_OUTLINE_WIDTH)?;
1763 }
1764 Ok(())
1765 }
1766
1767 fn title_flow(&self, height: f32) -> Result<Vec<Placement>, EditorError> {
1768 let title = self.active_outline();
1769 if !title.title {
1770 return Ok(Vec::new());
1771 }
1772 // Native flow uses text height even when the stored title bounds are taller.
1773 let old = self.header_bottom(title.id, title.origin()[1] + title.shaped.size[1]);
1774 let new = self.header_bottom(title.id, title.origin()[1] + height);
1775 if !old.is_finite() || !new.is_finite() {
1776 return Err(EditorError::InvalidGeometry);
1777 }
1778 if new <= old {
1779 return Ok(Vec::new());
1780 }
1781 let left = title.origin()[0] - 36.0;
1782 let right = title.origin()[0] + title.wrap_width() + 36.0;
1783 let destination = new + 11.52;
1784 let bodies = || {
1785 self.outlines
1786 .iter()
1787 .filter(|outline| !outline.title)
1788 .map(|outline| {
1789 let rect = outline.bounds();
1790 (
1791 outline.id,
1792 &outline.layout,
1793 [
1794 rect.x0 as f32,
1795 rect.y0 as f32,
1796 rect.x1 as f32,
1797 rect.y1 as f32,
1798 ],
1799 )
1800 })
1801 .chain(self.objects.iter().filter_map(|object| match object {
1802 page::Content::Image(image) if !image.background => {
1803 let x = image.layout.x.unwrap_or(0.0);
1804 let y = image.layout.y.unwrap_or(0.0);
1805 let [width, height] = crate::outline::image_size(image)?;
1806 Some((image.id, &image.layout, [x, y, x + width, y + height]))
1807 }
1808 page::Content::ReadOnly(object)
1809 if !matches!(object.source, onestore::page::PageObject::Title(_)) =>
1810 {
1811 Some((object.source.id(), object.source.layout(), object.rect()))
1812 }
1813 _ => None,
1814 }))
1815 };
1816 let Some(first) = bodies()
1817 .filter(|(_, _, rect)| {
1818 rect[1] >= old + 10.8
1819 && rect[1] < destination
1820 && rect[0] <= right
1821 && rect[2] >= left
1822 })
1823 .map(|(_, _, rect)| rect[1])
1824 .min_by(f32::total_cmp)
1825 else {
1826 return Ok(Vec::new());
1827 };
1828 let delta = destination - first;
1829 bodies()
1830 .filter(|(_, _, rect)| rect[1] >= first)
1831 .map(|(id, layout, rect)| {
1832 let y = rect[1] + delta;
1833 if !y.is_finite() || !(rect[3] + delta).is_finite() {
1834 return Err(EditorError::InvalidGeometry);
1835 }
1836 Ok(Placement {
1837 id,
1838 position: [layout.x, Some(y)],
1839 })
1840 })
1841 .collect()
1842 }
1843
1844 #[cfg(test)]
1845 pub(crate) fn from_outlines(
1846 engine: &mut TextEngine,
1847 outlines: Vec<Outline>,
1848 definitions: BTreeMap<ExGuid, Definition>,
1849 ) -> Result<Self, EditorError> {
1850 let outlines = outlines
1851 .iter()
1852 .map(|outline| TextOutline::from_outline(engine, outline, &definitions))
1853 .collect::<Result<_, _>>()?;
1854 Self::from_text_outlines(outlines, definitions, None)
1855 }
1856
1857 pub fn from_text_outlines(
1858 outlines: Vec<TextOutline>,
1859 definitions: BTreeMap<ExGuid, Definition>,
1860 date: Option<PageDate>,
1861 ) -> Result<Self, EditorError> {
1862 if outlines.is_empty() {
1863 return Err(EditError::InvalidRange.into());
1864 }
1865 let mut ids = BTreeSet::new();
1866 for (outline_id, paragraphs) in outlines
1867 .iter()
1868 .map(|outline| (outline.id, outline.document.nodes()))
1869 .chain(
1870 date.iter()
1871 .map(|date| (date.source().id, date.source().paragraphs.as_slice())),
1872 )
1873 {
1874 if !ids.insert(outline_id) {
1875 return Err(EditError::InvalidStructure.into());
1876 }
1877 crate::document::validate_nodes(paragraphs, &mut ids)?;
1878 }
1879 Ok(Self {
1880 outlines,
1881 definitions,
1882 objects: Vec::new(),
1883 date,
1884 header: PageHeader::default(),
1885 active: Focus::Outline(0),
1886 undo: Vec::new(),
1887 redo: Vec::new(),
1888 composition: None,
1889 preferred_x: None,
1890 pending: None,
1891 ops: Ok(Vec::new()),
1892 stored: None,
1893 whole: None,
1894 typing: None,
1895 recording: None,
1896 ink_drag: None,
1897 provisional: BTreeMap::new(),
1898 default_font: DefaultFont::default(),
1899 styles: BTreeMap::new(),
1900 markdown: None,
1901 formatted: None,
1902 })
1903 }
1904
1905 pub fn date(&self) -> Option<&PageDate> {
1906 self.date.as_ref()
1907 }
1908
1909 pub fn change_date(
1910 &mut self,
1911 engine: &mut TextEngine,
1912 timestamp: u64,
1913 mut text: [String; 2],
1914 ) -> Result<bool, EditorError> {
1915 let date = self.date.as_ref().ok_or(EditError::InvalidRange)?;
1916 if timestamp == date.timestamp() {
1917 return Ok(false);
1918 }
1919 let mut source = date.source().clone();
1920 for ((field, _), paragraph) in date.fields().zip(&mut source.paragraphs) {
1921 let format = paragraph.text().unwrap().text.format_at(0)?.clone();
1922 paragraph.text_mut().unwrap().text =
1923 Paragraph::new(std::mem::take(&mut text[field as usize]), format);
1924 }
1925 let updated = PageDate::new(timestamp, source, engine, &self.definitions)?;
1926 self.finish_composition();
1927 self.undo
1928 .push(History::Date(Box::new(self.date.replace(updated).unwrap())));
1929 self.redo.clear();
1930 self.record(Ok(self.date_ops()));
1931 Ok(true)
1932 }
1933
1934 /// Gives the page `color` (View, Page Color), `rule_lines` (View, Rule Lines) and, when
1935 /// given, `art` in place of its background pictures, as one undo step; false when it had
1936 /// them.
1937 pub fn set_paper(
1938 &mut self,
1939 color: Option<u32>,
1940 rule_lines: Option<onestore::page::RuleLines>,
1941 art: Option<Vec<onestore::page::Image>>,
1942 ) -> bool {
1943 let backgrounds: Vec<usize> = (0..self.objects.len())
1944 .filter(|&index| {
1945 self.objects[index]
1946 .picture()
1947 .is_some_and(|image| image.background)
1948 })
1949 .collect();
1950 let art = art.filter(|art| {
1951 art.len() != backgrounds.len()
1952 || art.iter().zip(&backgrounds).any(|(new, &index)| {
1953 let shown = self.objects[index].picture().unwrap();
1954 (&new.layout, new.size, &new.bytes) != (&shown.layout, shown.size, &shown.bytes)
1955 })
1956 });
1957 let paper = (color, rule_lines) != (self.header.color, self.header.rule_lines);
1958 if !paper && art.is_none() {
1959 return false;
1960 }
1961 self.finish_composition();
1962 let depth = self.undo.len();
1963 if paper {
1964 let entry = self.paper(color, rule_lines);
1965 self.undo.push(entry);
1966 }
1967 if let Some(art) = art {
1968 for index in backgrounds.into_iter().rev() {
1969 let image = self.remove_picture(index);
1970 self.undo.push(History::Picture {
1971 index,
1972 image: Some(Box::new(image)),
1973 });
1974 }
1975 // Art lies under everything else on the page.
1976 for image in art.into_iter().rev() {
1977 self.add_picture(0, image);
1978 self.undo.push(History::Picture {
1979 index: 0,
1980 image: None,
1981 });
1982 }
1983 }
1984 self.group(depth, false);
1985 self.redo.clear();
1986 true
1987 }
1988
1989 /// Puts page-level picture `image` at `index` in paint order, recording its op.
1990 fn add_picture(&mut self, index: usize, image: onestore::page::Image) {
1991 let object = PageObject::Image(image.clone());
1992 self.objects.insert(index, page::Content::Image(image));
1993 let before = self.successor(object.id());
1994 self.record(Ok(vec![PageOp::Add { object, before }]));
1995 }
1996
1997 /// Takes the page-level picture at `index` in paint order off the page, recording its op.
1998 fn remove_picture(&mut self, index: usize) -> onestore::page::Image {
1999 let image = self.objects[index].picture().unwrap().clone();
2000 self.objects.remove(index);
2001 self.record(Ok(vec![PageOp::Delete { object: image.id }]));
2002 image
2003 }
2004
2005 /// Puts a file on the page, `file` being its `Content::File`, at `index` in paint order,
2006 /// recording its op.
2007 fn add_file(&mut self, index: usize, file: page::Content) {
2008 let object = PageObject::Attachment(file.file().unwrap().0.clone());
2009 self.objects.insert(index, file);
2010 let before = self.successor(object.id());
2011 self.record(Ok(vec![PageOp::Add { object, before }]));
2012 }
2013
2014 /// Takes the file at `index` in paint order off the page, recording its op.
2015 fn remove_file(&mut self, index: usize) -> page::Content {
2016 let file = self.objects.remove(index);
2017 let object = file.file().unwrap().0.id;
2018 self.record(Ok(vec![PageOp::Delete { object }]));
2019 file
2020 }
2021
2022 /// Shows `color` and `rule_lines`, recording the ops that store them; the history entry
2023 /// restoring what the page had.
2024 fn paper(
2025 &mut self,
2026 color: Option<u32>,
2027 rule_lines: Option<onestore::page::RuleLines>,
2028 ) -> History {
2029 let mut ops = Vec::new();
2030 if color != self.header.color {
2031 ops.push(PageOp::Color(color));
2032 }
2033 if rule_lines != self.header.rule_lines {
2034 ops.push(PageOp::RuleLines(rule_lines));
2035 }
2036 self.record(Ok(ops));
2037 History::Paper {
2038 color: std::mem::replace(&mut self.header.color, color),
2039 rule_lines: std::mem::replace(&mut self.header.rule_lines, rule_lines),
2040 }
2041 }
2042
2043 /// Stored content; arrow-created paragraphs appear only in `visible_outlines`.
2044 pub fn outlines(&self) -> &[TextOutline] {
2045 &self.outlines
2046 }
2047 pub fn visible_outlines(&self) -> impl DoubleEndedIterator<Item = &TextOutline> {
2048 self.outlines
2049 .iter()
2050 .enumerate()
2051 .map(|(index, outline)| match &self.active {
2052 Focus::Draft {
2053 index: active,
2054 outline,
2055 } if index == *active => outline.as_ref(),
2056 _ => outline,
2057 })
2058 }
2059
2060 pub fn active_outline(&self) -> &TextOutline {
2061 match &self.active {
2062 Focus::Outline(index) => &self.outlines[*index],
2063 Focus::Caret { outline, .. } | Focus::Draft { outline, .. } => outline,
2064 }
2065 }
2066
2067 fn active_outline_mut(&mut self) -> &mut TextOutline {
2068 match &mut self.active {
2069 Focus::Outline(index) => &mut self.outlines[*index],
2070 Focus::Caret { outline, .. } | Focus::Draft { outline, .. } => outline,
2071 }
2072 }
2073
2074 /// The caret a run of typing left while typing there still joins the run: the word
2075 /// before it is still being written.
2076 pub fn typing(&self) -> Option<(ExGuid, TextPosition)> {
2077 let (outline, at, depth) = self.typing?;
2078 (outline == self.active_outline().id
2079 && self.selection().positions == [at; 2]
2080 && depth == self.undo.len())
2081 .then_some((outline, at))
2082 }
2083
2084 /// Provisional input geometry outside the document's outline collection.
2085 pub fn caret_outline(&self) -> Option<&TextOutline> {
2086 match &self.active {
2087 Focus::Caret { outline, .. } => Some(outline),
2088 Focus::Outline(_) | Focus::Draft { .. } => None,
2089 }
2090 }
2091
2092 pub fn place_caret(
2093 &mut self,
2094 engine: &mut TextEngine,
2095 position: [f32; 2],
2096 width: f32,
2097 ) -> Result<(), EditorError> {
2098 let document = TextDocument::from_nodes(vec![self.body_paragraph()?])?;
2099 let outline = TextOutline::new(engine, document, width, position)?;
2100 self.finish_composition();
2101 self.active = Focus::Caret {
2102 outline: Box::new(outline),
2103 index: self.outlines.len(),
2104 };
2105 self.preferred_x = None;
2106 Ok(())
2107 }
2108
2109 /// The format a paragraph style gives text, which clearing formatting returns to.
2110 fn style_format(&self, style: Option<ExGuid>) -> Result<onestore::document::Format, EditError> {
2111 Ok(match style {
2112 Some(id) => self
2113 .definitions
2114 .get(&id)
2115 .filter(|definition| {
2116 matches!(definition.kind, onestore::document::Kind::Style { .. })
2117 })
2118 .ok_or(EditError::InvalidStructure)?
2119 .format
2120 .clone(),
2121 None => Default::default(),
2122 })
2123 }
2124
2125 /// A new outline's first paragraph as OneNote 2010 starts one: in the page's `p` quick
2126 /// style of the Default font, which a page whose `p` is in another font gains beside it.
2127 fn body_paragraph(&mut self) -> Result<PageParagraph, EditError> {
2128 let definition = match self.styles.get("p") {
2129 Some(themed) => themed.clone(),
2130 None => self.default_font.body_style(),
2131 };
2132 let known = self.definitions.iter().find(|(_, known)| {
2133 matches!(&known.kind, Kind::Style { name: Some(name), .. } if name == "p")
2134 && known.format.font == definition.format.font
2135 && known.format.font_size == definition.format.font_size
2136 });
2137 let style = match known {
2138 Some((id, _)) => *id,
2139 None => {
2140 let id = onestore::page::text::new_id()?;
2141 self.definitions.insert(id, definition.clone());
2142 id
2143 }
2144 };
2145 let mut node = crate::document::node(
2146 Paragraph::new(String::new(), definition.format),
2147 onestore::document::Format::default(),
2148 )?;
2149 node.style = Some(style);
2150 Ok(node)
2151 }
2152
2153 /// An empty paragraph in `base`'s style, whose runs keep `base`'s language as OneNote's do.
2154 fn blank_paragraph(&self, base: &PageParagraph) -> Result<PageParagraph, EditError> {
2155 let mut format = self.style_format(base.style)?;
2156 format.language = format.language.or(base
2157 .text()
2158 .and_then(|text| text.text.spans()[0].format.language));
2159 let mut node =
2160 crate::document::node(Paragraph::new(String::new(), format), base.format.clone())?;
2161 node.level = base.level;
2162 node.style = base.style;
2163 Ok(node)
2164 }
2165
2166 pub fn select_below(
2167 &mut self,
2168 engine: &mut TextEngine,
2169 id: ExGuid,
2170 point: [f32; 2],
2171 ) -> Result<bool, EditorError> {
2172 if point.iter().any(|coordinate| !coordinate.is_finite()) {
2173 return Err(EditError::InvalidRange.into());
2174 }
2175 self.finish_composition();
2176 let Some(index) = self.outlines.iter().position(|outline| outline.id == id) else {
2177 return Ok(false);
2178 };
2179 let outline = &self.outlines[index];
2180 if !outline.contains_extension(point) {
2181 return Ok(false);
2182 }
2183 let draft = Box::new(match &self.active {
2184 Focus::Draft {
2185 outline,
2186 index: active,
2187 } if *active == index => (**outline).clone(),
2188 _ => outline.clone(),
2189 });
2190 let previous = std::mem::replace(
2191 &mut self.active,
2192 Focus::Draft {
2193 outline: draft,
2194 index,
2195 },
2196 );
2197 let preferred_x = self.preferred_x;
2198 let result = (|| {
2199 // Tiny source font sizes must not make one click generate unbounded paragraphs.
2200 for _ in 0..256 {
2201 if self.active_outline().shaped.size[1] >= point[1] {
2202 break;
2203 }
2204 let outline = self.active_outline();
2205 let count = outline.document.nodes().len();
2206 let height = outline.shaped.size[1];
2207 let last = &outline.document.nodes()[count - 1];
2208 let node = PageParagraph {
2209 parent: last.parent,
2210 ..self.blank_paragraph(last)?
2211 };
2212 self.apply(
2213 engine,
2214 DocumentEdit {
2215 columns: BTreeMap::new(),
2216 container: None,
2217 range: count..count,
2218 replacement: vec![node],
2219 },
2220 outline.selection,
2221 false,
2222 None,
2223 )?;
2224 if self.active_outline().shaped.size[1] <= height {
2225 return Err(EditError::InvalidStructure.into());
2226 }
2227 }
2228 if self.active_outline().shaped.size[1] < point[1] {
2229 return Err(EditError::TextTooLong.into());
2230 }
2231 self.select_at(point[0], point[1], false)?;
2232 Ok(true)
2233 })();
2234 if result.is_err() {
2235 self.active = previous;
2236 self.preferred_x = preferred_x;
2237 }
2238 result
2239 }
2240
2241 pub fn focus_outline(&mut self, id: onestore::ExGuid) -> Result<(), EditError> {
2242 if self.active_outline().id != id && !self.outlines.iter().any(|outline| outline.id == id) {
2243 return Err(EditError::InvalidRange);
2244 }
2245 self.finish_composition();
2246 if let Some(index) = self.outlines.iter().position(|outline| outline.id == id)
2247 && !matches!(self.active, Focus::Draft { index: active, .. } if active == index)
2248 {
2249 self.active = Focus::Outline(index);
2250 }
2251 self.preferred_x = None;
2252 Ok(())
2253 }
2254
2255 pub fn create_outline(
2256 &mut self,
2257 engine: &mut TextEngine,
2258 position: [f32; 2],
2259 width: f32,
2260 ) -> Result<onestore::ExGuid, EditorError> {
2261 let document = TextDocument::from_nodes(vec![self.body_paragraph()?])?;
2262 let outline = TextOutline::new(engine, document, width, position)?;
2263 let id = outline.id;
2264 self.finish_composition();
2265 self.undo.push(History::Remove {
2266 outline: id,
2267 focus: match &self.active {
2268 Focus::Outline(index) | Focus::Draft { index, .. } => {
2269 RestoreFocus::Outline(self.outlines[*index].id)
2270 }
2271 Focus::Caret { outline, index } => RestoreFocus::Caret {
2272 source: Box::new(outline.snapshot()),
2273 selection: outline.selection,
2274 index: *index,
2275 },
2276 },
2277 });
2278 self.redo.clear();
2279 self.active = Focus::Outline(self.outlines.len());
2280 self.outlines.push(outline);
2281 self.record(self.outline_ops(id, None));
2282 self.preferred_x = None;
2283 Ok(id)
2284 }
2285
2286 pub fn move_outline(
2287 &mut self,
2288 id: onestore::ExGuid,
2289 position: [f32; 2],
2290 ) -> Result<(), EditError> {
2291 if !position.iter().all(|value| value.is_finite()) {
2292 return Err(EditError::InvalidRange);
2293 }
2294 let outline = if self.active_outline().id == id {
2295 self.active_outline()
2296 } else {
2297 self.outlines
2298 .iter()
2299 .find(|outline| outline.id == id)
2300 .ok_or(EditError::InvalidRange)?
2301 };
2302 if outline.title {
2303 return Err(EditError::UnsupportedContent);
2304 }
2305 if position == outline.origin() {
2306 return Ok(());
2307 }
2308 self.finish_composition();
2309 if self.caret_outline().is_some_and(|outline| outline.id == id) {
2310 let outline = self.active_outline_mut();
2311 outline.layout.x = Some(position[0]);
2312 outline.layout.y = Some(position[1]);
2313 self.preferred_x = None;
2314 return Ok(());
2315 }
2316 let index = self
2317 .outlines
2318 .iter()
2319 .position(|outline| outline.id == id)
2320 .unwrap();
2321 let layout = &mut self.outlines[index].layout;
2322 let previous = [layout.x, layout.y];
2323 self.undo.push(History::Position {
2324 object: id,
2325 position: previous,
2326 });
2327 self.redo.clear();
2328 layout.x = Some(position[0]);
2329 layout.y = Some(position[1]);
2330 self.record(self.placement_ops(&Placement {
2331 id,
2332 position: previous,
2333 }));
2334 self.active = Focus::Outline(index);
2335 self.preferred_x = None;
2336 Ok(())
2337 }
2338
2339 /// Origin and size of a picture or file on the page the user can select; OneNote passes
2340 /// clicks through backgrounds.
2341 pub fn image_placement(&self, id: ExGuid) -> Option<([f32; 2], [f32; 2])> {
2342 if let Some((_, [x0, y0, x1, y1])) = self.page_file(id) {
2343 return Some(([x0, y0], [x1 - x0, y1 - y0]));
2344 }
2345 if let Some(image) = self.image(id) {
2346 return Some((
2347 crate::origin(&image.layout),
2348 crate::outline::image_size(image)?,
2349 ));
2350 }
2351 let (outline, ..) = self.outline_picture(id)?;
2352 let outline = self.outlines.iter().find(|item| item.id == outline)?;
2353 let [x0, y0, x1, y1] = outline.shaped.objects.iter().find(|o| o.id == id)?.rect;
2354 let origin = outline.origin();
2355 Some(([origin[0] + x0, origin[1] + y0], [x1 - x0, y1 - y0]))
2356 }
2357
2358 /// Whether the picture sits in an outline's flow, where only its size can change.
2359 pub(crate) fn image_in_outline(&self, id: ExGuid) -> bool {
2360 self.outline_picture(id).is_some()
2361 }
2362
2363 /// A picture or file held as a paragraph: its outline, the paragraph's container and index
2364 /// there, and the paragraph.
2365 fn outline_picture(
2366 &self,
2367 id: ExGuid,
2368 ) -> Option<(ExGuid, Option<ExGuid>, usize, &PageParagraph)> {
2369 self.outlines.iter().find_map(|outline| {
2370 descendants(outline.document.nodes(), None).find_map(|(container, index, node)| {
2371 holds(&node.content, id).then_some((outline.id, container, index, node))
2372 })
2373 })
2374 }
2375
2376 /// The address picture `id` links to, in an outline or on the page.
2377 pub(crate) fn picture_link(&self, id: ExGuid) -> Option<&str> {
2378 self.picture(id)?.link.as_deref()
2379 }
2380
2381 pub(crate) fn picture(&self, id: ExGuid) -> Option<&onestore::page::Image> {
2382 self.image(id)
2383 .or_else(|| match &self.outline_picture(id)?.3.content {
2384 ParagraphContent::Image(image) => Some(image),
2385 _ => None,
2386 })
2387 }
2388
2389 /// The file a paragraph holds or the page shows, which the host opens and saves.
2390 pub(crate) fn attachment(&self, id: ExGuid) -> Option<&onestore::page::Attachment> {
2391 if let Some((file, _)) = self.page_file(id) {
2392 return Some(file);
2393 }
2394 match &self.outline_picture(id)?.3.content {
2395 ParagraphContent::Attachment(file) => Some(file),
2396 _ => None,
2397 }
2398 }
2399
2400 /// A file on the page and where its column lies, in page coordinates.
2401 fn page_file(&self, id: ExGuid) -> Option<(&onestore::page::Attachment, [f32; 4])> {
2402 self.objects
2403 .iter()
2404 .filter_map(page::Content::file)
2405 .find(|(file, _)| file.id == id)
2406 }
2407
2408 /// Where a file draws with its name, in page coordinates.
2409 pub(crate) fn attachment_rect(&self, id: ExGuid) -> Option<[f32; 4]> {
2410 if let Some((_, rect)) = self.page_file(id) {
2411 return Some(rect);
2412 }
2413 let (outline, ..) = self.outline_picture(id)?;
2414 let outline = self.outlines.iter().find(|item| item.id == outline)?;
2415 let bounds = outline.shaped.objects.iter().find(|o| o.id == id)?.bounds();
2416 Some(crate::translated(bounds, outline.origin()))
2417 }
2418
2419 /// Replaces a picture paragraph through the outline's text history.
2420 fn edit_outline_picture(
2421 &mut self,
2422 engine: &mut TextEngine,
2423 id: ExGuid,
2424 replacement: Option<PageParagraph>,
2425 ) -> Result<(), EditorError> {
2426 let (outline, container, index, _) =
2427 self.outline_picture(id).ok_or(EditError::InvalidRange)?;
2428 self.focus_outline(outline)?;
2429 let selection = self.selection();
2430 self.commit(
2431 engine,
2432 DocumentEdit {
2433 columns: BTreeMap::new(),
2434 container,
2435 range: index..index + 1,
2436 replacement: replacement.into_iter().collect(),
2437 },
2438 selection,
2439 )
2440 }
2441
2442 fn image(&self, id: ExGuid) -> Option<&onestore::page::Image> {
2443 self.objects.iter().find_map(|object| match object {
2444 page::Content::Image(image) if image.id == id && !image.background => Some(image),
2445 _ => None,
2446 })
2447 }
2448
2449 fn object_tags_mut(&mut self, id: ExGuid) -> Option<&mut Vec<onestore::document::Tag>> {
2450 self.objects.iter_mut().find_map(|object| match object {
2451 page::Content::Image(image) if image.id == id => Some(&mut image.tags),
2452 page::Content::File { source, .. } if source.id == id => Some(&mut source.tags),
2453 _ => None,
2454 })
2455 }
2456
2457 /// Gives picture or file `id` on the page `tags`, returning those it had.
2458 fn swap_object_tags(
2459 &mut self,
2460 id: ExGuid,
2461 tags: Vec<onestore::document::Tag>,
2462 ) -> Vec<onestore::document::Tag> {
2463 let previous = std::mem::replace(self.object_tags_mut(id).unwrap(), tags);
2464 let tags = self.object_tags_mut(id).unwrap().clone();
2465 self.record(Ok(ops::tags(id, tags, &self.definitions)));
2466 previous
2467 }
2468
2469 /// Gives picture or file `id` on the page `tags`, as one undo step.
2470 pub(super) fn set_object_tags(&mut self, id: ExGuid, tags: Vec<onestore::document::Tag>) {
2471 let previous = self.swap_object_tags(id, tags);
2472 self.undo.push(History::Tags {
2473 object: id,
2474 tags: previous,
2475 });
2476 self.redo.clear();
2477 }
2478
2479 fn image_mut(&mut self, id: ExGuid) -> Option<&mut onestore::page::Image> {
2480 self.objects.iter_mut().find_map(|object| match object {
2481 page::Content::Image(image) if image.id == id && !image.background => Some(image),
2482 _ => None,
2483 })
2484 }
2485
2486 /// Moves a picture or a file on the page, and resizes a picture when `size` differs from
2487 /// the stored size. A picture in an outline's flow keeps its place.
2488 pub fn place_image(
2489 &mut self,
2490 engine: &mut TextEngine,
2491 id: ExGuid,
2492 origin: [f32; 2],
2493 size: [f32; 2],
2494 ) -> Result<(), EditorError> {
2495 if !origin.iter().chain(&size).all(|value| value.is_finite())
2496 || size.iter().any(|value| *value <= 0.0)
2497 {
2498 return Err(EditError::InvalidRange.into());
2499 }
2500 if let Some((.., node)) = self.outline_picture(id) {
2501 let mut node = node.clone();
2502 let onestore::page::ParagraphContent::Image(image) = &mut node.content else {
2503 return Ok(());
2504 };
2505 if crate::outline::image_size(image) == Some(size) {
2506 return Ok(());
2507 }
2508 [image.layout.max_width, image.layout.max_height] = size.map(Some);
2509 image.layout.width_set_by_user = Some(true);
2510 return self.edit_outline_picture(engine, id, Some(node));
2511 }
2512 if let Some((file, _)) = self.page_file(id) {
2513 let previous = [file.layout.x, file.layout.y];
2514 if previous == origin.map(Some) {
2515 return Ok(());
2516 }
2517 self.finish_composition();
2518 let layout = self.object_layout_mut(id).unwrap();
2519 [layout.x, layout.y] = origin.map(Some);
2520 self.undo.push(History::Position {
2521 object: id,
2522 position: previous,
2523 });
2524 self.redo.clear();
2525 self.record(self.placement_ops(&Placement {
2526 id,
2527 position: previous,
2528 }));
2529 return Ok(());
2530 }
2531 let image = self.image(id).ok_or(EditError::InvalidRange)?;
2532 let mut layout = image.layout.clone();
2533 [layout.x, layout.y] = origin.map(Some);
2534 if crate::outline::image_size(image) != Some(size) {
2535 [layout.max_width, layout.max_height] = size.map(Some);
2536 layout.width_set_by_user = Some(true);
2537 }
2538 if layout == image.layout {
2539 return Ok(());
2540 }
2541 self.finish_composition();
2542 let previous = std::mem::replace(&mut self.image_mut(id).unwrap().layout, layout);
2543 self.undo.push(History::Image {
2544 image: id,
2545 layout: previous,
2546 });
2547 self.redo.clear();
2548 let image = self.image(id).unwrap();
2549 self.record(Ok(ops::picture_layout(image)));
2550 Ok(())
2551 }
2552
2553 /// Leaves a picture as OneNote's Left and Right arrows do: the caret goes to the end of the
2554 /// text outline before it in page order, or to the start of the one after. False when there
2555 /// is none.
2556 pub(crate) fn step_from_image(
2557 &mut self,
2558 engine: &mut TextEngine,
2559 id: ExGuid,
2560 forward: bool,
2561 ) -> Result<bool, EditorError> {
2562 if let Some((outline, ..)) = self.outline_picture(id) {
2563 // Within an outline, the caret goes to the text beside the picture.
2564 let source = self
2565 .outlines
2566 .iter()
2567 .find(|item| item.id == outline)
2568 .unwrap();
2569 let mut leaf = 0;
2570 let mut before = None;
2571 let mut after = None;
2572 let mut passed = false;
2573 for (_, _, node) in descendants(source.document.nodes(), None) {
2574 match &node.content {
2575 content if holds(content, id) => passed = true,
2576 onestore::page::ParagraphContent::Text(text) => {
2577 let end = text.text.utf16_offset(text.text.text().len())?;
2578 if passed {
2579 after = after.or(Some((leaf, 0)));
2580 } else {
2581 before = Some((leaf, end));
2582 }
2583 leaf += 1;
2584 }
2585 _ => {}
2586 }
2587 }
2588 let Some((paragraph, offset)) = (if forward { after } else { before }) else {
2589 return Ok(false);
2590 };
2591 self.focus_outline(outline)?;
2592 self.select(Selection {
2593 positions: [TextPosition { paragraph, offset }; 2],
2594 affinities: [Affinity::Downstream; 2],
2595 })?;
2596 return Ok(true);
2597 }
2598 let index = self
2599 .objects
2600 .iter()
2601 .position(|object| {
2602 matches!(object, page::Content::Image(image) if image.id == id)
2603 || object.file().is_some_and(|(file, _)| file.id == id)
2604 })
2605 .ok_or(EditError::InvalidRange)?;
2606 // An emptied outline keeps its slot for undo after leaving `outlines`.
2607 let editable = |object: &page::Content| match object {
2608 page::Content::Editable(outline)
2609 if self.outlines.iter().any(|item| item.id == *outline) =>
2610 {
2611 Some(*outline)
2612 }
2613 _ => None,
2614 };
2615 let outline = if forward {
2616 self.objects[index + 1..].iter().find_map(editable)
2617 } else {
2618 self.objects[..index].iter().rev().find_map(editable)
2619 };
2620 let Some(outline) = outline else {
2621 return Ok(false);
2622 };
2623 self.focus_outline(outline)?;
2624 let edge = if forward {
2625 Movement::DocumentStart
2626 } else {
2627 Movement::DocumentEnd
2628 };
2629 self.move_selection(engine, edge, false)?;
2630 Ok(true)
2631 }
2632
2633 /// Moves the caret from the title to where OneNote 2010 puts what the title can't take
2634 /// (lab, 2026-09-30 and 2026-10-02): the end of an outline where the body starts, a new
2635 /// one there on a page without body content, and otherwise a caret on blank page two grid
2636 /// rows below the content, which is the one case it returns false.
2637 fn leave_title_for_body(&mut self, engine: &mut TextEngine) -> Result<bool, EditorError> {
2638 let start = self.body_start_of(self.active_outline());
2639 let first = self
2640 .outlines
2641 .iter()
2642 .find(|outline| !outline.title && outline.origin() == start)
2643 .map(|outline| outline.id);
2644 if let Some(id) = first {
2645 self.focus_outline(id)?;
2646 self.move_selection(engine, Movement::DocumentEnd, false)?;
2647 return Ok(true);
2648 }
2649 let Some(bottom) = self.body_bottom() else {
2650 self.place_caret(engine, start, DEFAULT_OUTLINE_WIDTH)?;
2651 return Ok(true);
2652 };
2653 let margin = self.margin_origin();
2654 let row = ((bottom - margin[1]) / 18.0).floor() + 2.0;
2655 self.place_caret(
2656 engine,
2657 [start[0], margin[1] + row * 18.0],
2658 DEFAULT_OUTLINE_WIDTH,
2659 )?;
2660 Ok(false)
2661 }
2662
2663 /// Puts `image` at the caret as OneNote 2010 pastes and inserts a picture: in an outline
2664 /// as [`Self::insert_attachment`] puts a file, and on blank page at the caret with the
2665 /// caret moving to the grid row below it, where typing starts a new outline. From the
2666 /// title it goes where [`Self::leave_title_for_body`] puts the caret, in the flow there
2667 /// unless on blank page.
2668 pub fn insert_picture(
2669 &mut self,
2670 engine: &mut TextEngine,
2671 mut image: onestore::page::Image,
2672 ) -> Result<(), EditorError> {
2673 if self.active_outline().title && self.leave_title_for_body(engine)? {
2674 return self.insert_in_flow(engine, ParagraphContent::Image(image));
2675 }
2676 let margin = self.margin_origin();
2677 let Focus::Caret { outline, .. } = &self.active else {
2678 return self.insert_in_flow(engine, ParagraphContent::Image(image));
2679 };
2680 let [x, y] = outline.origin();
2681 [image.layout.x, image.layout.y] = [Some(x), Some(y)];
2682 image.layout.width_set_by_user = Some(false);
2683 let height = image.layout.max_height.unwrap_or_default();
2684 self.finish_composition();
2685 let index = self.objects.len();
2686 self.add_picture(index, image);
2687 self.undo.push(History::Picture { index, image: None });
2688 self.redo.clear();
2689 let row = ((y + height - margin[1]) / 18.0).floor() + 1.0;
2690 self.place_caret(engine, [x, margin[1] + row * 18.0], DEFAULT_OUTLINE_WIDTH)
2691 }
2692
2693 /// Pastes clips and pictures in order, as one undo step, as OneNote 2010 pastes a web
2694 /// page's text with its pictures; the places of the awaited ones, in order.
2695 pub fn paste_pieces(
2696 &mut self,
2697 engine: &mut TextEngine,
2698 pieces: Vec<Piece>,
2699 ) -> Result<Vec<Awaited>, EditorError> {
2700 let depth = self.undo.len();
2701 let mut result = Ok(());
2702 let mut awaited = Vec::new();
2703 for piece in pieces {
2704 result = match piece {
2705 Piece::Clip(clip) => self.paste_clip(engine, clip),
2706 Piece::Picture(bytes, size) => picture(bytes, size)
2707 .map_err(EditorError::from)
2708 .and_then(|image| self.insert_picture(engine, image)),
2709 Piece::Awaited => self.enter(engine, false).and_then(|()| {
2710 let caret = self.selection().positions[1];
2711 let outline = self.active_outline();
2712 let (.., node) = outline
2713 .document
2714 .leaf(caret.paragraph)
2715 .ok_or(EditError::InvalidRange)?;
2716 awaited.push(Awaited {
2717 outline: outline.id,
2718 before: node.id,
2719 });
2720 Ok(())
2721 }),
2722 };
2723 if result.is_err() {
2724 break;
2725 }
2726 }
2727 self.group(depth, false);
2728 result.map(|()| awaited)
2729 }
2730
2731 /// Puts an awaited picture in its place as an undo step of its own, keeping the focused
2732 /// outline's selection; false where the place has gone.
2733 pub fn insert_awaited(
2734 &mut self,
2735 engine: &mut TextEngine,
2736 at: Awaited,
2737 image: onestore::page::Image,
2738 ) -> Result<bool, EditorError> {
2739 let Some(outline) = self
2740 .outlines
2741 .iter()
2742 .find(|outline| outline.id == at.outline)
2743 else {
2744 return Ok(false);
2745 };
2746 let Some((container, index, node)) = descendants(outline.document.nodes(), None)
2747 .find(|(.., node)| node.id == at.before)
2748 .map(|(container, index, node)| (container, index, node.clone()))
2749 else {
2750 return Ok(false);
2751 };
2752 let picture = PageParagraph {
2753 id: onestore::page::text::new_id()?,
2754 content: ParagraphContent::Image(image),
2755 style: None,
2756 lists: Vec::new(),
2757 tags: Vec::new(),
2758 collapsed: false,
2759 ..node
2760 };
2761 // A caret on blank page is let go; picture paragraphs take no text positions.
2762 let focused = match self.active {
2763 Focus::Outline(index) => Some((self.outlines[index].id, self.selection())),
2764 _ => None,
2765 };
2766 self.focus_outline(at.outline)?;
2767 let selection = self.selection();
2768 self.commit(
2769 engine,
2770 DocumentEdit {
2771 columns: BTreeMap::new(),
2772 container,
2773 range: index..index,
2774 replacement: vec![picture],
2775 },
2776 selection,
2777 )?;
2778 if let Some((id, selection)) = focused {
2779 self.focus_outline(id)?;
2780 self.select(selection)?;
2781 }
2782 Ok(true)
2783 }
2784
2785 /// The lowest edge of the page's body content, in page points; none on an empty page.
2786 fn body_bottom(&self) -> Option<f32> {
2787 let outlines = self
2788 .outlines
2789 .iter()
2790 .filter(|outline| !outline.title)
2791 .map(|outline| outline.bounds().y1 as f32);
2792 let objects = self
2793 .objects
2794 .iter()
2795 .filter(|object| !object.picture().is_some_and(|image| image.background))
2796 .filter_map(|object| {
2797 let (_, layout) = object.layout()?;
2798 Some(layout.y? + layout.max_height?)
2799 });
2800 outlines.chain(objects).reduce(f32::max)
2801 }
2802
2803 /// Attaches `file` at the caret as OneNote 2010 does (`corpus/attachment-insert`): the
2804 /// caret's paragraph splits there, the file takes the place of an empty first half, and the
2805 /// caret starts the second. A caret on blank page puts the file on the page there
2806 /// (`corpus/attachment-floating`).
2807 pub fn insert_attachment(
2808 &mut self,
2809 engine: &mut TextEngine,
2810 mut file: onestore::page::Attachment,
2811 ) -> Result<(), EditorError> {
2812 if self.active_outline().title && self.leave_title_for_body(engine)? {
2813 return self.insert_in_flow(engine, ParagraphContent::Attachment(file));
2814 }
2815 if let Focus::Caret { outline, .. } = &self.active {
2816 [file.layout.x, file.layout.y] = outline.origin().map(Some);
2817 let layout = Box::new(crate::outline::page_file(engine, &file)?);
2818 let [x0, y0, x1, y1] = layout.bounds();
2819 [file.layout.max_width, file.layout.max_height] = [Some(x1 - x0), Some(y1 - y0)];
2820 file.layout.width_set_by_user = Some(false);
2821 self.finish_composition();
2822 let index = self.objects.len();
2823 self.add_file(
2824 index,
2825 page::Content::File {
2826 source: file,
2827 layout,
2828 },
2829 );
2830 self.undo.push(History::File { index, file: None });
2831 self.redo.clear();
2832 return Ok(());
2833 }
2834 self.insert_in_flow(engine, ParagraphContent::Attachment(file))
2835 }
2836
2837 /// Splits the caret's paragraph around `content`, which takes the place of an empty first
2838 /// half; the caret starts the second.
2839 fn insert_in_flow(
2840 &mut self,
2841 engine: &mut TextEngine,
2842 content: ParagraphContent,
2843 ) -> Result<(), EditorError> {
2844 let content = match self.across(engine, |editor, engine| {
2845 editor.insert_in_flow(engine, content.clone())
2846 })? {
2847 Some(()) => return Ok(()),
2848 None => content,
2849 };
2850 let [anchor, focus] = self.selection().positions;
2851 let (start, end) = (anchor.min(focus), anchor.max(focus));
2852 let mut format = self.typing_format(start)?;
2853 link::unlinked(&mut format);
2854 let mut edit = self
2855 .active_outline()
2856 .document
2857 .replace(start..end, vec![Paragraph::new(String::new(), format); 2])?;
2858 let [head, tail, ..] = &mut edit.replacement[..] else {
2859 unreachable!("a split holds both halves")
2860 };
2861 let node = PageParagraph {
2862 id: onestore::page::text::new_id()?,
2863 content,
2864 style: None,
2865 lists: Vec::new(),
2866 tags: Vec::new(),
2867 collapsed: false,
2868 ..head.clone()
2869 };
2870 let empty = head.text().unwrap().text.text().is_empty();
2871 if empty {
2872 tail.tags = std::mem::take(&mut head.tags);
2873 tail.text_mut().unwrap().tags = std::mem::take(&mut head.text_mut().unwrap().tags);
2874 }
2875 edit.replacement.insert(1, node);
2876 if empty {
2877 edit.replacement.remove(0);
2878 }
2879 let caret = TextPosition {
2880 paragraph: start.paragraph + usize::from(!empty),
2881 offset: 0,
2882 };
2883 self.commit(
2884 engine,
2885 edit,
2886 Selection {
2887 positions: [caret; 2],
2888 affinities: [Affinity::Upstream; 2],
2889 },
2890 )
2891 }
2892
2893 pub fn remove_image(&mut self, engine: &mut TextEngine, id: ExGuid) -> Result<(), EditorError> {
2894 if self.outline_picture(id).is_some() {
2895 return self.edit_outline_picture(engine, id, None);
2896 }
2897 if let Some(index) = self
2898 .objects
2899 .iter()
2900 .position(|object| object.file().is_some_and(|(file, _)| file.id == id))
2901 {
2902 self.finish_composition();
2903 let file = self.remove_file(index);
2904 self.undo.push(History::File {
2905 index,
2906 file: Some(Box::new(file)),
2907 });
2908 self.redo.clear();
2909 return Ok(());
2910 }
2911 let index = self
2912 .objects
2913 .iter()
2914 .position(|object| {
2915 matches!(object, page::Content::Image(image) if image.id == id && !image.background)
2916 })
2917 .ok_or(EditError::InvalidRange)?;
2918 self.finish_composition();
2919 let image = self.remove_picture(index);
2920 self.undo.push(History::Picture {
2921 index,
2922 image: Some(Box::new(image)),
2923 });
2924 self.redo.clear();
2925 Ok(())
2926 }
2927
2928 pub fn selection(&self) -> Selection {
2929 self.active_outline().selection
2930 }
2931
2932 /// OneNote's 18 pt placement grid passes through this point.
2933 pub fn margin_origin(&self) -> [f32; 2] {
2934 self.header.margin_origin
2935 }
2936
2937 /// Whether the page runs right to left, which OneNote 2010 opens scrolled to its right
2938 /// end.
2939 pub fn rtl(&self) -> bool {
2940 self.header.rtl
2941 }
2942
2943 pub fn resize(&mut self, engine: &mut TextEngine, width: f32) -> Result<(), EditorError> {
2944 let outline = self.active_outline();
2945 if outline.layout.reserved_width.or(outline.layout.max_width) == Some(width)
2946 && outline.layout.width_set_by_user == Some(true)
2947 {
2948 return Ok(());
2949 }
2950 let previous = outline.layout.clone();
2951 let resized = self.preview_resize(engine, width)?;
2952 self.finish_composition();
2953 if let Focus::Draft { index, .. } = self.active {
2954 self.active = Focus::Outline(index);
2955 }
2956 let id = resized.id;
2957 *self.active_outline_mut() = resized;
2958 if self.caret_outline().is_none() {
2959 self.record(ops::layout_ops(
2960 id,
2961 &previous,
2962 &self.active_outline().layout,
2963 ));
2964 self.undo.push(History::Layout {
2965 outline: id,
2966 layout: previous,
2967 });
2968 self.redo.clear();
2969 }
2970 self.preferred_x = None;
2971 Ok(())
2972 }
2973
2974 pub fn preview_resize(
2975 &self,
2976 engine: &mut TextEngine,
2977 width: f32,
2978 ) -> Result<TextOutline, EditorError> {
2979 let outline = match &self.active {
2980 Focus::Draft { index, .. } => &self.outlines[*index],
2981 _ => self.active_outline(),
2982 };
2983 if outline.title {
2984 return Err(EditError::UnsupportedContent.into());
2985 }
2986 if !width.is_finite() || width <= 0.0 {
2987 return Err(LayoutError::InvalidWidth.into());
2988 }
2989 let width = width.max(outline.shaped.table_width());
2990 let shaped = OutlineLayout::flow(
2991 outline.document.nodes().iter(),
2992 &outline.indents,
2993 width,
2994 true,
2995 0,
2996 None,
2997 &mut |paragraph, previous, width, indents| {
2998 ParagraphLayout::shape(
2999 engine,
3000 paragraph,
3001 previous,
3002 width,
3003 indents,
3004 &self.definitions,
3005 )
3006 },
3007 )?;
3008 let mut layout = outline.layout.clone();
3009 layout.max_width = Some(width);
3010 layout.reserved_width = None;
3011 layout.width_set_by_user = Some(true);
3012 Ok(TextOutline {
3013 id: outline.id,
3014 title: outline.title,
3015 min_width: outline.min_width,
3016 layout,
3017 document: outline.document.clone(),
3018 indents: outline.indents.clone(),
3019 shaped,
3020 selection: outline.selection,
3021 })
3022 }
3023
3024 pub fn select(&mut self, selection: Selection) -> Result<(), EditError> {
3025 for position in selection.positions {
3026 self.active_outline().visible_index(position.paragraph)?;
3027 self.active_outline()
3028 .document
3029 .paragraph(position.paragraph)
3030 .ok_or(EditError::InvalidRange)?
3031 .byte_offset(position.offset)?;
3032 }
3033 self.finish_composition();
3034 self.preferred_x = None;
3035 self.whole = None;
3036 self.active_outline_mut().selection = selection;
3037 Ok(())
3038 }
3039
3040 pub fn select_all(&mut self) -> Result<(), EditError> {
3041 self.select(self.active_outline().whole())
3042 }
3043
3044 /// Selects the smallest unit strictly holding the selection, as OneNote 2010's Ctrl+A widens
3045 /// it: the paragraph, its subtree, each enclosing table cell, row and table, then a body
3046 /// outline as an object (the title's text instead), then every body outline on the page.
3047 pub fn widen_selection(&mut self) -> Result<(), EditError> {
3048 match self.whole() {
3049 Some(Whole::Page) => return Ok(()),
3050 Some(Whole::Outline) => return self.select_page(),
3051 None => {}
3052 }
3053 let outline = self.active_outline();
3054 let [anchor, focus] = outline.selection.positions;
3055 let (start, end) = (anchor.min(focus), anchor.max(focus));
3056 let last_shown = |range: Range<usize>| {
3057 let paragraph = range
3058 .rev()
3059 .find(|&paragraph| outline.visible_index(paragraph).is_ok())?;
3060 let text = outline.document.paragraph(paragraph)?;
3061 Some(TextPosition {
3062 paragraph,
3063 offset: text.utf16_offset(text.text().len()).ok()?,
3064 })
3065 };
3066 let first = |paragraph| TextPosition {
3067 paragraph,
3068 offset: 0,
3069 };
3070 let own = outline
3071 .next_sibling_start(outline.visible_index(start.paragraph)?)
3072 .or_else(|| last_shown(start.paragraph..start.paragraph + 1));
3073 let units = std::iter::once((first(start.paragraph), own))
3074 .chain(
3075 outline
3076 .document
3077 .enclosing(start.paragraph)
3078 .into_iter()
3079 .map(|range| (first(range.start), last_shown(range))),
3080 )
3081 .chain(outline.title.then(|| {
3082 (
3083 first(0),
3084 last_shown(0..outline.document.paragraphs().count()),
3085 )
3086 }))
3087 .filter_map(|(unit_start, unit_end)| Some([unit_start, unit_end?]))
3088 .find(|&unit| unit[0] <= start && end <= unit[1] && unit != [start, end]);
3089 let (id, title) = (outline.id, outline.title);
3090 match units {
3091 Some(unit) => self.select(unit.into()),
3092 None if title => self.select_page(),
3093 None => self.select_outline(id),
3094 }
3095 }
3096
3097 /// Selects outline `id` as an object, as a click on its handle does.
3098 pub(crate) fn select_outline(&mut self, id: ExGuid) -> Result<(), EditError> {
3099 self.focus_outline(id)?;
3100 let whole = self.active_outline().whole();
3101 self.select(whole)?;
3102 self.whole = Some((Whole::Outline, id, whole));
3103 Ok(())
3104 }
3105
3106 /// What Select All has selected beyond text, if the selection is still what it chose.
3107 pub fn whole(&self) -> Option<Whole> {
3108 let (whole, id, selection) = self.whole?;
3109 let outline = self.active_outline();
3110 (outline.id == id && outline.selection == selection && self.composition.is_none())
3111 .then_some(whole)
3112 }
3113
3114 fn page_selected(&self) -> bool {
3115 self.whole() == Some(Whole::Page)
3116 }
3117
3118 /// Selects every body outline, focusing the last in page order as OneNote does: arrows
3119 /// leave the selection at its ends, and typing replaces its text.
3120 fn select_page(&mut self) -> Result<(), EditError> {
3121 let Some(last) = self.outlines.iter().rev().find(|outline| !outline.title) else {
3122 return Ok(());
3123 };
3124 let (id, whole) = (last.id, last.whole());
3125 self.focus_outline(id)?;
3126 self.select(whole)?;
3127 self.whole = Some((Whole::Page, id, whole));
3128 Ok(())
3129 }
3130
3131 /// Makes the history entries past `depth` one undo step, `page` when they edited OneNote's
3132 /// page selection; false when there are none. Groups among them are flattened into it.
3133 fn group(&mut self, depth: usize, mut page: bool) -> bool {
3134 let mut entries = Vec::new();
3135 for entry in self.undo.split_off(depth) {
3136 match entry {
3137 History::Group {
3138 entries: inner,
3139 page: selected,
3140 } => {
3141 page |= selected;
3142 entries.extend(inner);
3143 }
3144 entry => entries.push(entry),
3145 }
3146 }
3147 match entries.len() {
3148 0 => return false,
3149 1 if !page => self.undo.extend(entries),
3150 _ => self.undo.push(History::Group { entries, page }),
3151 }
3152 true
3153 }
3154
3155 /// Runs `edit`, an edit of OneNote's page selection, as one undo step.
3156 fn grouped<T>(
3157 &mut self,
3158 edit: impl FnOnce(&mut Self) -> Result<T, EditorError>,
3159 ) -> Result<T, EditorError> {
3160 let depth = self.undo.len();
3161 let result = edit(self);
3162 self.group(depth, true);
3163 result
3164 }
3165
3166 /// Runs `edit` once a selection crossing a table's edge is deleted, as OneNote 2010 deletes
3167 /// one before what replaces it goes in at its start, in one undo step; `None` for any
3168 /// other selection.
3169 fn across<T>(
3170 &mut self,
3171 engine: &mut TextEngine,
3172 edit: impl FnOnce(&mut Self, &mut TextEngine) -> Result<T, EditorError>,
3173 ) -> Result<Option<T>, EditorError> {
3174 let [anchor, focus] = self.active_outline().selection.positions;
3175 if !self
3176 .active_outline()
3177 .document
3178 .crosses_table(anchor.min(focus)..anchor.max(focus))?
3179 {
3180 return Ok(None);
3181 }
3182 let depth = self.undo.len();
3183 let result = self.delete(engine, false).and_then(|_| edit(self, engine));
3184 self.group(depth, false);
3185 result.map(Some)
3186 }
3187
3188 /// Runs `edit`, typing or a backspace at a caret, as part of the typing run the last undo
3189 /// step holds if the caret is where that run left it and nothing came between; OneNote 2010
3190 /// undoes a run whole however long it pauses.
3191 fn typed<T>(
3192 &mut self,
3193 edit: impl FnOnce(&mut Self) -> Result<T, EditorError>,
3194 ) -> Result<T, EditorError> {
3195 let [anchor, focus] = self.selection().positions;
3196 let caret = match self.marked_range() {
3197 Some(marked) => Some(marked.start),
3198 None => (anchor == focus).then_some(focus),
3199 };
3200 let joins = self.typing.take().is_some_and(|(outline, at, depth)| {
3201 outline == self.active_outline().id
3202 && caret == Some(at)
3203 && depth == self.undo.len()
3204 && self.pending.is_none()
3205 });
3206 let depth = self.undo.len() - usize::from(joins);
3207 let result = edit(self);
3208 if !self.group(depth, false) {
3209 return result;
3210 }
3211 if result.is_ok() {
3212 let caret = self.selection().positions[1];
3213 self.typing = Some((self.active_outline().id, caret, self.undo.len()));
3214 }
3215 result
3216 }
3217
3218 /// Removes the page selection's outlines, last first, but its last when `keep`, whose
3219 /// text stays selected to be replaced. Deleting the page selection leaves a caret where
3220 /// its first outline began, as OneNote 2010's does.
3221 fn remove_page(&mut self, engine: &mut TextEngine, keep: bool) -> Result<(), EditorError> {
3222 self.finish_composition();
3223 self.whole = None;
3224 let mut body = (0..self.outlines.len())
3225 .filter(|&index| !self.outlines[index].title)
3226 .collect::<Vec<_>>();
3227 let Some(&first) = body.first() else {
3228 return Ok(());
3229 };
3230 let kept = if keep { body.pop() } else { None };
3231 let caret = TextOutline::new(
3232 engine,
3233 TextDocument::new(vec![Paragraph::new(String::new(), Default::default())])?,
3234 DEFAULT_OUTLINE_WIDTH,
3235 self.outlines[first].origin(),
3236 )?;
3237 let index = self.outlines.len() - body.len();
3238 let focus = RestoreFocus::Caret {
3239 source: Box::new(caret.snapshot()),
3240 selection: caret.selection,
3241 index,
3242 };
3243 for &removed in body.iter().rev() {
3244 let outline = self.outlines.remove(removed);
3245 self.record(Ok(vec![PageOp::Delete { object: outline.id }]));
3246 self.undo.push(History::Insert {
3247 index: removed,
3248 outline: Box::new(outline),
3249 focus: focus.clone(),
3250 });
3251 }
3252 self.redo.clear();
3253 self.preferred_x = None;
3254 match kept {
3255 Some(kept) => {
3256 self.active = Focus::Outline(kept - body.len());
3257 self.select_all()?;
3258 }
3259 None => {
3260 self.active = Focus::Caret {
3261 outline: Box::new(caret),
3262 index,
3263 }
3264 }
3265 }
3266 Ok(())
3267 }
3268
3269 /// Moves every outline of the page selection by `delta`, as one undo step keeping it.
3270 pub fn move_page(&mut self, delta: [f32; 2]) -> Result<(), EditorError> {
3271 let moves = self
3272 .outlines
3273 .iter()
3274 .filter(|outline| !outline.title)
3275 .map(|outline| {
3276 let [x, y] = outline.origin();
3277 (outline.id, [x + delta[0], y + delta[1]])
3278 })
3279 .collect::<Vec<_>>();
3280 self.grouped(|editor| {
3281 for (id, position) in moves {
3282 editor.move_outline(id, position)?;
3283 }
3284 Ok(editor.select_page()?)
3285 })
3286 }
3287
3288 pub(crate) fn select_at(&mut self, x: f32, y: f32, extend: bool) -> Result<(), EditError> {
3289 let mut selection = self.selection_at(x, y, SelectionUnit::Grapheme)?;
3290 if extend {
3291 selection.positions[0] = self.selection().positions[0];
3292 selection.affinities[0] = self.selection().affinities[0];
3293 }
3294 self.select(selection)
3295 }
3296
3297 pub fn selection_at(
3298 &self,
3299 x: f32,
3300 y: f32,
3301 unit: SelectionUnit,
3302 ) -> Result<Selection, EditError> {
3303 self.active_outline().geometry().selection_at(x, y, unit)
3304 }
3305
3306 pub fn caret(&self, width: f32) -> Result<BoundingBox, EditError> {
3307 let selection = self.active_outline().selection;
3308 self.caret_at(selection.positions[1], selection.affinities[1], width)
3309 }
3310
3311 /// The caret at `focus` in the active outline, in its coordinates.
3312 pub fn caret_at(
3313 &self,
3314 focus: TextPosition,
3315 affinity: Affinity,
3316 width: f32,
3317 ) -> Result<BoundingBox, EditError> {
3318 self.active_outline().caret_at(focus, affinity, width)
3319 }
3320
3321 pub fn move_selection(
3322 &mut self,
3323 engine: &mut TextEngine,
3324 movement: Movement,
3325 extend: bool,
3326 ) -> Result<(), EditorError> {
3327 if self.prepare_navigation(movement, extend)? {
3328 return Ok(());
3329 }
3330 self.move_selection_prepared(engine, movement, extend)
3331 }
3332
3333 pub(crate) fn prepare_navigation(
3334 &mut self,
3335 movement: Movement,
3336 extend: bool,
3337 ) -> Result<bool, EditorError> {
3338 self.finish_composition();
3339 self.whole = None;
3340 self.typing = None;
3341 Ok(!extend && movement == Movement::Right && self.leave_equation()?)
3342 }
3343
3344 #[cfg(feature = "interaction")]
3345 pub(crate) fn move_selection_in(
3346 &mut self,
3347 shaped: &OutlineLayout,
3348 movement: Movement,
3349 extend: bool,
3350 ) -> Result<(), EditorError> {
3351 let navigation = OutlineGeometry {
3352 source: self.active_outline(),
3353 shaped,
3354 }
3355 .navigate(self.selection(), movement, extend, self.preferred_x)?;
3356 self.preferred_x = navigation.preferred_x;
3357 self.active_outline_mut().selection = navigation.selection;
3358 Ok(())
3359 }
3360
3361 pub(crate) fn move_selection_prepared(
3362 &mut self,
3363 engine: &mut TextEngine,
3364 movement: Movement,
3365 extend: bool,
3366 ) -> Result<(), EditorError> {
3367 let [anchor, focus] = self.active_outline().selection.positions;
3368 if !extend
3369 && self.caret_outline().is_some()
3370 && self
3371 .active_outline()
3372 .document
3373 .paragraphs()
3374 .next()
3375 .unwrap()
3376 .text()
3377 .is_empty()
3378 && let Some(step) = match movement {
3379 Movement::Left => Some([-18.0, 0.0]),
3380 Movement::Right => Some([18.0, 0.0]),
3381 Movement::Up => Some([0.0, -18.0]),
3382 Movement::Down => Some([0.0, 18.0]),
3383 _ => None,
3384 }
3385 {
3386 let origin = self.active_outline().origin();
3387 let position = [origin[0] + step[0], origin[1] + step[1]];
3388 let center_y = position[1] + self.caret(1.0)?.height() as f32 * 0.5;
3389 let target = self
3390 .outlines
3391 .iter()
3392 .rev()
3393 .find(|outline| {
3394 let rect = outline.bounds();
3395 let bottom = rect.y1
3396 + if matches!(movement, Movement::Up) {
3397 36.0
3398 } else {
3399 0.0
3400 };
3401 (rect.x0..=rect.x1).contains(&f64::from(position[0]))
3402 && (rect.y0..=bottom).contains(&f64::from(center_y))
3403 })
3404 .map(|outline| (outline.id, outline.origin()));
3405 if let Some((id, origin)) = target {
3406 let x = position[0]
3407 + if step[0] == 0.0 {
3408 self.preferred_x.unwrap_or(0.0)
3409 } else {
3410 0.0
3411 }
3412 - origin[0];
3413 self.focus_outline(id)?;
3414 self.select_at(x, center_y - origin[1], false)?;
3415 self.preferred_x = (step[0] == 0.0).then_some(x);
3416 } else {
3417 let outline = self.active_outline_mut();
3418 outline.layout.x = Some(position[0]);
3419 outline.layout.y = Some(position[1]);
3420 if step[0] != 0.0 {
3421 self.preferred_x = None;
3422 }
3423 }
3424 return Ok(());
3425 }
3426 let navigation = self.active_outline().geometry().navigate(
3427 self.selection(),
3428 movement,
3429 extend,
3430 self.preferred_x,
3431 )?;
3432 if navigation.boundary
3433 && matches!(movement, Movement::Up | Movement::Down)
3434 && !extend
3435 && anchor == focus
3436 && !self.active_outline().title
3437 {
3438 let x = navigation
3439 .preferred_x
3440 .expect("vertical navigation retains its column");
3441 let outline = self.active_outline();
3442 let last = outline.document.nodes().len() - 1;
3443 let up = matches!(movement, Movement::Up);
3444 let base_node = &outline.document.nodes()[if up { 0 } else { last }];
3445 let node = self.blank_paragraph(base_node)?;
3446 let trailing = outline
3447 .document
3448 .nodes()
3449 .iter()
3450 .rev()
3451 .take_while(|node| {
3452 node.text()
3453 .is_some_and(|text| text.text.text().bytes().all(|byte| byte == b' '))
3454 })
3455 .count();
3456 let position = if up {
3457 Some([
3458 outline.origin()[0] + x,
3459 (((f64::from(outline.origin()[1]) / 18.0).ceil() - 3.0) * 18.0 + 14.4) as f32,
3460 ])
3461 } else if outline.origin()[0] >= 180.0 && trailing >= 2 {
3462 let node = &outline.document.nodes()[last.saturating_sub(trailing)];
3463 let bottom = match &node.content {
3464 onestore::page::ParagraphContent::Text(_) => {
3465 let paragraph = outline
3466 .shaped
3467 .paragraphs
3468 .iter()
3469 .find(|paragraph| paragraph.id == node.id)
3470 .ok_or(EditError::InvalidStructure)?;
3471 f64::from(paragraph.origin[1]) + f64::from(paragraph.text.height())
3472 }
3473 onestore::page::ParagraphContent::Table(table) => {
3474 let cell = outline
3475 .shaped
3476 .tables
3477 .iter()
3478 .find(|layout| layout.id == table.id)
3479 .and_then(|layout| layout.cells.last())
3480 .ok_or(EditError::InvalidStructure)?;
3481 f64::from(cell.rect[3]) + 1.68
3482 }
3483 onestore::page::ParagraphContent::Image(onestore::page::Image {
3484 id, ..
3485 })
3486 | onestore::page::ParagraphContent::Attachment(onestore::page::Attachment {
3487 id,
3488 ..
3489 })
3490 | onestore::page::ParagraphContent::Ink(onestore::page::Ink { id, .. })
3491 | onestore::page::ParagraphContent::Unsupported(
3492 onestore::page::Unsupported { id, .. },
3493 ) => outline
3494 .shaped
3495 .objects
3496 .iter()
3497 .find(|object| object.id == *id)
3498 .map(|object| f64::from(object.bottom))
3499 .ok_or(EditError::InvalidStructure)?,
3500 } + f64::from(outline.origin()[1]);
3501 let cell = (bottom - 14.4) / 18.0;
3502 // Stored coordinates can straddle a grid boundary by one f32 ULP.
3503 let grid = if (cell - cell.round()).abs()
3504 <= f64::from(f32::EPSILON) * cell.abs().max(1.0)
3505 {
3506 cell.round()
3507 } else {
3508 cell.ceil()
3509 };
3510 Some([outline.origin()[0], ((grid + 3.0) * 18.0 + 14.4) as f32])
3511 } else {
3512 None
3513 };
3514 if let Some(position) = position {
3515 let outline = TextOutline::new(
3516 engine,
3517 TextDocument::from_nodes(vec![node])?,
3518 DEFAULT_OUTLINE_WIDTH,
3519 position,
3520 )?;
3521 self.active = Focus::Caret {
3522 outline: Box::new(outline),
3523 index: self.outlines.len(),
3524 };
3525 self.preferred_x = Some(if up { 0.0 } else { x });
3526 return Ok(());
3527 }
3528 let edit = DocumentEdit {
3529 columns: BTreeMap::new(),
3530 container: None,
3531 range: last + 1..last + 1,
3532 replacement: vec![PageParagraph {
3533 parent: base_node.parent,
3534 ..node
3535 }],
3536 };
3537 let paragraph = outline.document.paragraphs().count();
3538 let previous = if let Focus::Outline(index) = self.active {
3539 let draft = self.outlines[index].clone();
3540 Some(std::mem::replace(
3541 &mut self.active,
3542 Focus::Draft {
3543 index,
3544 outline: Box::new(draft),
3545 },
3546 ))
3547 } else {
3548 None
3549 };
3550 if let Err(error) = self.apply(
3551 engine,
3552 edit,
3553 [TextPosition {
3554 paragraph,
3555 offset: 0,
3556 }; 2]
3557 .into(),
3558 false,
3559 None,
3560 ) {
3561 if let Some(previous) = previous {
3562 self.active = previous;
3563 }
3564 return Err(error);
3565 }
3566 self.preferred_x = Some(x);
3567 return Ok(());
3568 }
3569 if navigation.boundary && matches!(movement, Movement::Left | Movement::Right) {
3570 let left = movement == Movement::Left;
3571 let caret = self.caret(1.0)?;
3572 let origin = self.active_outline().origin();
3573 let x = caret.x0 + f64::from(origin[0]);
3574 let y = (caret.y0 + caret.y1) * 0.5 + f64::from(origin[1]);
3575 let target = self
3576 .outlines
3577 .iter()
3578 .filter(|outline| outline.id != self.active_outline().id)
3579 .filter_map(|outline| {
3580 let rect = outline.bounds();
3581 let distance = if left { x - rect.x1 } else { rect.x0 - x };
3582 ((rect.y0..rect.y1).contains(&y) && distance > 0.0).then_some((
3583 distance,
3584 outline.id,
3585 outline.origin(),
3586 ))
3587 })
3588 .min_by(|a, b| a.0.total_cmp(&b.0));
3589 if let Some((_, id, origin)) = target {
3590 self.focus_outline(id)?;
3591 self.select_at(
3592 if left { f32::MAX } else { -1.0 },
3593 y as f32 - origin[1],
3594 false,
3595 )?;
3596 return Ok(());
3597 }
3598 }
3599 self.preferred_x = navigation.preferred_x;
3600 self.active_outline_mut().selection = navigation.selection;
3601 Ok(())
3602 }
3603
3604 #[cfg(test)]
3605 pub(crate) fn selection_rects(&self) -> Result<Vec<BoundingBox>, EditError> {
3606 self.active_outline()
3607 .range_rects(self.active_outline().selection)
3608 }
3609
3610 pub fn replace(
3611 &mut self,
3612 engine: &mut TextEngine,
3613 replacement: Vec<Paragraph>,
3614 ) -> Result<(), EditorError> {
3615 self.take_objects()?;
3616 let start = self.active_outline().selection.positions[0]
3617 .min(self.active_outline().selection.positions[1]);
3618 let end = self.active_outline().selection.positions[0]
3619 .max(self.active_outline().selection.positions[1]);
3620 let last = replacement.last().ok_or(EditError::InvalidRange)?;
3621 let offset = last.utf16_offset(last.text().len())?;
3622 let offset = if replacement.len() == 1 {
3623 start
3624 .offset
3625 .checked_add(offset)
3626 .ok_or(EditError::TextTooLong)?
3627 } else {
3628 offset
3629 };
3630 let caret = TextPosition {
3631 paragraph: start
3632 .paragraph
3633 .checked_add(replacement.len() - 1)
3634 .ok_or(EditError::TextTooLong)?,
3635 offset,
3636 };
3637 let edit = self
3638 .active_outline()
3639 .document
3640 .replace(start..end, replacement)?;
3641 self.commit(
3642 engine,
3643 edit,
3644 Selection {
3645 positions: [caret; 2],
3646 affinities: [Affinity::Upstream; 2],
3647 },
3648 )
3649 }
3650
3651 /// Replaces the selection with `text` in the formatting of the text it replaces, as a
3652 /// spelling correction does.
3653 pub(crate) fn correct(
3654 &mut self,
3655 engine: &mut TextEngine,
3656 text: &str,
3657 ) -> Result<(), EditorError> {
3658 let [anchor, focus] = self.selection().positions;
3659 let end = anchor.max(focus);
3660 let format = self
3661 .active_outline()
3662 .document
3663 .paragraph(end.paragraph)
3664 .ok_or(EditError::InvalidRange)?
3665 .format_at(end.offset)?
3666 .clone();
3667 self.replace(engine, vec![Paragraph::new(text.to_owned(), format)])
3668 }
3669
3670 /// Types `text` at the selection; a space ends a typed URL, which becomes a link. A line
3671 /// of text joins the typing run before it, and may complete a Markdown shortcut.
3672 pub fn insert(&mut self, engine: &mut TextEngine, text: &str) -> Result<(), EditorError> {
3673 if text.is_empty() || text.contains('\n') {
3674 return self.insert_text(engine, text);
3675 }
3676 self.typed(|editor| editor.insert_text(engine, text))?;
3677 self.markdown(engine, text)
3678 }
3679
3680 /// Insert Symbol: `symbol` at the selection, as an undo step that neither joins the
3681 /// typing before it nor takes the typing after.
3682 pub fn insert_symbol(
3683 &mut self,
3684 engine: &mut TextEngine,
3685 symbol: char,
3686 ) -> Result<(), EditorError> {
3687 self.typing = None;
3688 self.insert(engine, symbol.encode_utf8(&mut [0; 4]))?;
3689 self.typing = None;
3690 Ok(())
3691 }
3692
3693 fn insert_text(&mut self, engine: &mut TextEngine, text: &str) -> Result<(), EditorError> {
3694 if self.page_selected() {
3695 return self.grouped(|editor| {
3696 editor.remove_page(engine, true)?;
3697 editor.insert_text(engine, text)
3698 });
3699 }
3700 self.leave_link_code()?;
3701 let start = self.active_outline().selection.positions[0]
3702 .min(self.active_outline().selection.positions[1]);
3703 let format = self.typing_format(start)?;
3704 let replacement = text
3705 .split('\n')
3706 .map(|line| Paragraph::new(line.to_owned(), format.clone()))
3707 .collect();
3708 self.replace(engine, replacement)?;
3709 if matches!(text, " " | "\t") {
3710 self.link_typed_url(engine, start)?;
3711 self.build_typed_equation(engine, start)?;
3712 }
3713 Ok(())
3714 }
3715
3716 /// Pastes plain text as OneNote does: lines become plain Calibri 11 paragraphs without style
3717 /// or list between the halves of the caret's paragraph (`evidence/structural-edits/xml/c7-*`),
3718 /// an empty half dropped; from the title they go where [`Self::leave_title_for_body`] puts
3719 /// the caret.
3720 /// Pasted runs take the clipboard's `language`, an LCID, not the caret's run's as typing
3721 /// does; Windows derives it from the keyboard language at copy time.
3722 pub fn paste(
3723 &mut self,
3724 engine: &mut TextEngine,
3725 text: &str,
3726 language: u32,
3727 ) -> Result<(), EditorError> {
3728 if self.page_selected() {
3729 return self.grouped(|editor| {
3730 editor.remove_page(engine, true)?;
3731 editor.paste(engine, text, language)
3732 });
3733 }
3734 self.take_objects()?;
3735 let lines = text
3736 .split('\n')
3737 .map(|line| line.strip_suffix('\r').unwrap_or(line))
3738 .collect::<Vec<_>>();
3739 if lines.len() > 1 && self.active_outline().title {
3740 self.leave_title_for_body(engine)?;
3741 }
3742 let last = lines[lines.len() - 1];
3743 let [anchor, focus] = self.active_outline().selection.positions;
3744 let (start, end) = (anchor.min(focus), anchor.max(focus));
3745 if lines.len() == 1 {
3746 let mut format = self.typing_format(start)?;
3747 format.language = Some(language);
3748 return self.replace(engine, vec![Paragraph::new(last.to_owned(), format)]);
3749 }
3750 if let Some(pasted) = self.across(engine, |editor, engine| {
3751 editor.paste(engine, text, language)
3752 })? {
3753 return Ok(pasted);
3754 }
3755 let edge = Paragraph::new(String::new(), self.typing_format(start)?);
3756 let pasted = Format {
3757 font: Some("Calibri".into()),
3758 font_size: Some(11.0),
3759 language: Some(language),
3760 ..Format::default()
3761 };
3762 let mut edit = self.active_outline().document.replace(
3763 start..end,
3764 std::iter::once(edge.clone())
3765 .chain(
3766 lines
3767 .iter()
3768 .map(|line| Paragraph::new((*line).to_owned(), pasted.clone())),
3769 )
3770 .chain([edge])
3771 .collect(),
3772 )?;
3773 for node in &mut edit.replacement[1..=lines.len()] {
3774 node.style = None;
3775 node.lists.clear();
3776 }
3777 let head = clip::drop_empty_halves(&mut edit, lines.len(), true);
3778 let caret = TextPosition {
3779 paragraph: start.paragraph + lines.len() - usize::from(head),
3780 offset: u32::try_from(last.encode_utf16().count())
3781 .map_err(|_| EditError::TextTooLong)?,
3782 };
3783 self.commit(
3784 engine,
3785 edit,
3786 Selection {
3787 positions: [caret; 2],
3788 affinities: [Affinity::Upstream; 2],
3789 },
3790 )
3791 }
3792
3793 /// Enter as OneNote 2010 does (`evidence/structural-edits`): the new paragraph takes the
3794 /// level, lists and character formatting at the caret but no note tags. At a paragraph's
3795 /// start its tags stay with its text below; an empty paragraph opens a plain one above and
3796 /// leaves the list it ends; a heading continues as body text. Unlike OneNote, a to-do list
3797 /// continues as lists do: the new paragraph takes unchecked copies of the check boxes, and
3798 /// an empty item loses them.
3799 fn split(&mut self, engine: &mut TextEngine) -> Result<(), EditorError> {
3800 if let Some(split) = self.across(engine, Self::split)? {
3801 return Ok(split);
3802 }
3803 let [anchor, focus] = self.active_outline().selection.positions;
3804 let (start, end) = (anchor.min(focus), anchor.max(focus));
3805 let mut format = self.typing_format(start)?;
3806 // A link or equation ends with its paragraph.
3807 link::unlinked(&mut format);
3808 if format.math == Some(true) {
3809 let text = self.active_outline().document.paragraph(start.paragraph);
3810 format = equation::text_after(text.ok_or(EditError::InvalidRange)?);
3811 }
3812 let document = &self.active_outline().document;
3813 let mut edit = document.replace(
3814 start..end,
3815 vec![Paragraph::new(String::new(), format.clone()); 2],
3816 )?;
3817 let nodes = document.container(edit.container)?;
3818 let next = nodes
3819 .get(crate::document::subtree_end(nodes, edit.range.start))
3820 .filter(|next| {
3821 let node = &nodes[edit.range.start];
3822 next.parent == node.parent && next.level == node.level
3823 });
3824 let [head, tail, ..] = &mut edit.replacement[..] else {
3825 unreachable!("a split holds both halves")
3826 };
3827 let empty = |node: &PageParagraph| node.text().unwrap().text.text().is_empty();
3828 if start.offset == 0 {
3829 tail.tags = std::mem::take(&mut head.tags);
3830 tail.text_mut().unwrap().tags = std::mem::take(&mut head.text_mut().unwrap().tags);
3831 }
3832 // Enter at a paragraph's end gives the new one its style's NextStyle, as OneNote 2010
3833 // follows a heading with Normal.
3834 let following = match head.style.and_then(|id| self.definitions.get(&id)) {
3835 Some(Definition {
3836 kind: Kind::Style { name, next },
3837 ..
3838 }) => next.clone().filter(|next| Some(next) != name.as_ref()),
3839 _ => None,
3840 };
3841 if empty(head) && empty(tail) {
3842 head.lists.clear();
3843 if next.is_none_or(|next| next.lists.is_empty()) {
3844 tail.lists.clear();
3845 }
3846 self.end_checks(tail);
3847 } else {
3848 if start.offset == 0 {
3849 self.continue_checks(tail, head);
3850 } else {
3851 self.continue_checks(head, tail);
3852 }
3853 if empty(tail)
3854 && let Some(following) = following
3855 {
3856 tail.style = self.style_named(&following)?;
3857 // The heading's run formatting carries over under the body style (`c9-h1-*`).
3858 let run = format.over(&self.style_format(head.style)?);
3859 tail.text_mut().unwrap().text =
3860 Paragraph::new(String::new(), run.inherit(&self.style_format(tail.style)?));
3861 }
3862 }
3863 let caret = TextPosition {
3864 paragraph: start.paragraph + 1,
3865 offset: 0,
3866 };
3867 self.commit(
3868 engine,
3869 edit,
3870 Selection {
3871 positions: [caret; 2],
3872 affinities: [Affinity::Upstream; 2],
3873 },
3874 )
3875 }
3876
3877 /// Tab or Shift+Tab at the start of the selected paragraphs; see [`crate::document::indent`].
3878 /// A default bullet or number steps to the style OneNote gives its new depth.
3879 pub fn indent(&mut self, engine: &mut TextEngine, outdent: bool) -> Result<bool, EditorError> {
3880 let outline = self.active_outline();
3881 if outline.title {
3882 return Ok(false);
3883 }
3884 let selection = outline.selection;
3885 let [anchor, focus] = selection.positions;
3886 let start = anchor.min(focus);
3887 let end = anchor.max(focus);
3888 let (container, local_start, _) = outline
3889 .document
3890 .leaf(start.paragraph)
3891 .ok_or(EditError::InvalidRange)?;
3892 let (end_container, local_end, _) = outline
3893 .document
3894 .leaf(end.paragraph)
3895 .ok_or(EditError::InvalidRange)?;
3896 // OneNote 2010's Tab leaves a selection crossing a table's edge alone.
3897 if container != end_container {
3898 return Ok(false);
3899 }
3900 let range = local_start
3901 ..local_end + usize::from(end.offset != 0 || start.paragraph == end.paragraph);
3902 let nodes = outline.document.container(container)?;
3903 let Some(mut edit) = crate::document::indent(nodes, container, range, outdent) else {
3904 return Ok(false);
3905 };
3906 let levels = nodes[edit.range.clone()]
3907 .iter()
3908 .map(|node| node.level)
3909 .collect::<Vec<_>>();
3910 for (node, level) in edit.replacement.iter_mut().zip(levels) {
3911 if node.level == level {
3912 continue;
3913 }
3914 for list in &mut node.lists {
3915 let Some(definition) = self
3916 .definitions
3917 .get(list)
3918 .and_then(|definition| format::nested_list(definition, node.level > level))
3919 else {
3920 continue;
3921 };
3922 *list = onestore::page::text::new_id()?;
3923 self.definitions.insert(*list, definition);
3924 }
3925 }
3926 self.commit(engine, edit, selection)?;
3927 Ok(true)
3928 }
3929
3930 /// Alt+Shift+Up or Down: swaps the selected paragraphs, with their children, and the
3931 /// sibling above or below with its children. Does nothing past the first or last sibling.
3932 pub(crate) fn move_paragraphs(
3933 &mut self,
3934 engine: &mut TextEngine,
3935 up: bool,
3936 ) -> Result<bool, EditorError> {
3937 use crate::document::{previous_sibling, subtree_end};
3938 let outline = self.active_outline();
3939 if outline.title {
3940 return Ok(false);
3941 }
3942 let selection = outline.selection;
3943 let [anchor, focus] = selection.positions;
3944 let document = &outline.document;
3945 let (container, first, _) = document
3946 .leaf(anchor.min(focus).paragraph)
3947 .ok_or(EditError::InvalidRange)?;
3948 let (end_container, last, _) = document
3949 .leaf(anchor.max(focus).paragraph)
3950 .ok_or(EditError::InvalidRange)?;
3951 if container != end_container {
3952 return Ok(false);
3953 }
3954 let nodes = document.container(container)?;
3955 let parent = nodes[first].parent;
3956 let mut end = first;
3957 while end <= last {
3958 if nodes[end].parent != parent {
3959 return Ok(false);
3960 }
3961 end = subtree_end(nodes, end);
3962 }
3963 let (range, [moved, other]) = if up {
3964 let Some(sibling) = previous_sibling(nodes, first, &BTreeSet::new()) else {
3965 return Ok(false);
3966 };
3967 (sibling..end, [first..end, sibling..first])
3968 } else {
3969 if nodes.get(end).is_none_or(|node| node.parent != parent) {
3970 return Ok(false);
3971 }
3972 let next = subtree_end(nodes, end);
3973 (first..next, [first..end, end..next])
3974 };
3975 let passed = leaves(&nodes[other.clone()], None).count();
3976 let replacement = if up {
3977 [&nodes[moved], &nodes[other]].concat()
3978 } else {
3979 [&nodes[other], &nodes[moved]].concat()
3980 };
3981 let mut moved = selection;
3982 for position in &mut moved.positions {
3983 position.paragraph = if up {
3984 position.paragraph - passed
3985 } else {
3986 position.paragraph + passed
3987 };
3988 }
3989 let edit = DocumentEdit {
3990 columns: BTreeMap::new(),
3991 container,
3992 range,
3993 replacement,
3994 };
3995 self.commit(engine, edit, moved)?;
3996 Ok(true)
3997 }
3998
3999 /// Deletes the selection or the character beside the caret. Backspace at a paragraph's start
4000 /// first removes its list, then outdents it, then joins it to the paragraph above, whose
4001 /// properties win; Delete at its end joins the paragraph below (`evidence/structural-edits/
4002 /// xml/c4-*`, `c5b-*`). Joins pass over a collapsed paragraph's hidden children.
4003 pub fn delete(&mut self, engine: &mut TextEngine, backward: bool) -> Result<bool, EditorError> {
4004 if self.page_selected() {
4005 self.grouped(|editor| editor.remove_page(engine, false))?;
4006 return Ok(true);
4007 }
4008 self.take_objects()?;
4009 let selection = self.active_outline().selection;
4010 let [anchor, focus] = selection.positions;
4011 let mut range = anchor.min(focus)..anchor.max(focus);
4012 let at_caret = range.is_empty();
4013 if backward
4014 && at_caret
4015 && self.formatted.take() == Some((self.active_outline().id, focus, self.undo.len()))
4016 {
4017 return self.undo(engine);
4018 }
4019 if at_caret {
4020 let outline = self.active_outline();
4021 let paragraph = outline.paragraph_layout(focus.paragraph)?;
4022 let cursor = paragraph.cursor(focus.offset, selection.affinities[1])?;
4023 let Some(cluster) =
4024 cursor.logical_clusters(&paragraph.text.shaped)[usize::from(!backward)]
4025 else {
4026 let (container, local, node) = outline
4027 .document
4028 .leaf(focus.paragraph)
4029 .ok_or(EditError::InvalidRange)?;
4030 if backward && !node.lists.is_empty() {
4031 let item = PageParagraph {
4032 lists: Vec::new(),
4033 ..node.clone()
4034 };
4035 let edit = DocumentEdit {
4036 columns: BTreeMap::new(),
4037 container,
4038 range: local..local + 1,
4039 replacement: vec![item],
4040 };
4041 self.commit(engine, edit, selection)?;
4042 return Ok(true);
4043 }
4044 if backward && node.level > 1 {
4045 return self.indent(engine, true);
4046 }
4047 let visible = outline.visible_index(focus.paragraph)?;
4048 let Some(neighbor) = (if backward {
4049 visible.checked_sub(1)
4050 } else {
4051 Some(visible + 1).filter(|next| *next < outline.shaped.paragraphs.len())
4052 }) else {
4053 return Ok(false);
4054 };
4055 let neighbor = outline.source_index(neighbor);
4056 let [upper, lower] = if backward {
4057 [neighbor, focus.paragraph]
4058 } else {
4059 [focus.paragraph, neighbor]
4060 };
4061 let (_, _, top) = outline
4062 .document
4063 .leaf(upper)
4064 .ok_or(EditError::InvalidRange)?;
4065 let base = self.style_format(top.style)?;
4066 let Some(edit) = outline.document.join(upper, lower, &base)? else {
4067 return Ok(false);
4068 };
4069 let text = outline.document.paragraph(upper).unwrap();
4070 let caret = TextPosition {
4071 paragraph: upper,
4072 offset: text.utf16_offset(text.text().len())?,
4073 };
4074 self.commit(
4075 engine,
4076 edit,
4077 Selection {
4078 positions: [caret; 2],
4079 affinities: [Affinity::Upstream; 2],
4080 },
4081 )?;
4082 return Ok(true);
4083 };
4084 let visible = paragraph.projection.text();
4085 let bytes = cluster.text_range();
4086 range.start.offset = paragraph.projection.source_offset(
4087 visible.utf16_offset(bytes.start)?,
4088 onestore::page::text::Affinity::Downstream,
4089 )?;
4090 range.end.offset = paragraph.projection.source_offset(
4091 visible.utf16_offset(bytes.end)?,
4092 onestore::page::text::Affinity::Upstream,
4093 )?;
4094 }
4095 if backward && at_caret {
4096 self.typed(|editor| editor.delete_range(engine, range))
4097 } else {
4098 self.delete_range(engine, range)
4099 }
4100 }
4101
4102 pub fn delete_to(
4103 &mut self,
4104 engine: &mut TextEngine,
4105 movement: Movement,
4106 ) -> Result<bool, EditorError> {
4107 if self.page_selected() {
4108 return self.delete(engine, false);
4109 }
4110 self.finish_composition();
4111 let original = self.selection();
4112 let (preferred_x, typing) = (self.preferred_x, self.typing);
4113 let [caret, focus] = original.positions;
4114 let collapsed = caret == focus;
4115 if collapsed {
4116 let result = self.move_selection(engine, movement, true);
4117 if let Err(error) = result {
4118 self.active_outline_mut().selection = original;
4119 (self.preferred_x, self.typing) = (preferred_x, typing);
4120 return Err(error);
4121 }
4122 }
4123 let [anchor, focus] = self.selection().positions;
4124 self.active_outline_mut().selection = original;
4125 (self.preferred_x, self.typing) = (preferred_x, typing);
4126 let range = anchor.min(focus)..anchor.max(focus);
4127 if collapsed && range.end == caret && range.start.paragraph == caret.paragraph {
4128 self.typed(|editor| editor.delete_range(engine, range))
4129 } else {
4130 self.delete_range(engine, range)
4131 }
4132 }
4133
4134 fn delete_range(
4135 &mut self,
4136 engine: &mut TextEngine,
4137 range: Range<TextPosition>,
4138 ) -> Result<bool, EditorError> {
4139 if range.is_empty() {
4140 return Ok(false);
4141 }
4142 let caret = range.start;
4143 let format = self
4144 .active_outline()
4145 .document
4146 .paragraph(caret.paragraph)
4147 .unwrap()
4148 .format_at(caret.offset)?
4149 .clone();
4150 let edit = self
4151 .active_outline()
4152 .document
4153 .replace(range, vec![Paragraph::new(String::new(), format)])?;
4154 self.commit(
4155 engine,
4156 edit,
4157 Selection {
4158 positions: [caret; 2],
4159 affinities: [Affinity::Upstream; 2],
4160 },
4161 )?;
4162 Ok(true)
4163 }
4164
4165 pub fn can_undo(&self) -> bool {
4166 !self.undo.is_empty()
4167 }
4168
4169 pub fn can_redo(&self) -> bool {
4170 !self.redo.is_empty()
4171 }
4172
4173 /// How many steps Undo and Redo hold.
4174 pub fn history_depth(&self) -> [usize; 2] {
4175 [self.undo.len(), self.redo.len()]
4176 }
4177
4178 /// Takes up `parked`, the editor this page had when it was last left, history and all,
4179 /// in place of this one: what storage changed since reaches it as `refresh` would.
4180 pub(crate) fn resume(
4181 &mut self,
4182 mut parked: CanvasEditor,
4183 engine: &mut TextEngine,
4184 ) -> Result<(), EditorError> {
4185 if let Some((page, _)) = &self.stored {
4186 parked.refresh(page.clone(), engine)?;
4187 parked.default_font = std::mem::take(&mut self.default_font);
4188 parked.markdown = self.markdown.take();
4189 *self = parked;
4190 }
4191 Ok(())
4192 }
4193
4194 pub fn undo(&mut self, engine: &mut TextEngine) -> Result<bool, EditorError> {
4195 if self.composition.is_some() {
4196 return self.cancel_composition(engine);
4197 }
4198 let Some(history) = self.undo.pop() else {
4199 return Ok(false);
4200 };
4201 match self.apply_history(engine, history) {
4202 Ok(inverse) => {
4203 self.unstore_provisional();
4204 let page = matches!(inverse, History::Group { page: true, .. });
4205 self.redo.push(inverse);
4206 if page {
4207 self.select_page()?;
4208 }
4209 Ok(true)
4210 }
4211 Err((history, error)) => {
4212 self.undo.push(history);
4213 Err(error)
4214 }
4215 }
4216 }
4217
4218 /// Removes from storage each stored provisional paragraph an undo left empty and the only
4219 /// text of its outline, which shows it still.
4220 fn unstore_provisional(&mut self) {
4221 let removed: Vec<ExGuid> = self
4222 .provisional
4223 .iter()
4224 .filter(|(outline, (blank, stored))| {
4225 let Some(outline) = self.outlines.iter().find(|item| item.id == **outline) else {
4226 return false;
4227 };
4228 let mut leaves = crate::document::leaves(outline.document.nodes(), None);
4229 *stored
4230 && matches!(
4231 (leaves.next(), leaves.next()),
4232 (Some((None, _, node)), None) if node.id == blank.id
4233 && node.tags.is_empty()
4234 && node.lists.is_empty()
4235 && node.text().is_some_and(|text| {
4236 text.text.text().is_empty() && text.tags.is_empty()
4237 })
4238 )
4239 })
4240 .map(|(outline, _)| *outline)
4241 .collect();
4242 for outline in removed {
4243 let (blank, _) = &self.provisional[&outline];
4244 self.record(Ok(vec![PageOp::Delete { object: blank.id }]));
4245 self.provisional.get_mut(&outline).unwrap().1 = false;
4246 }
4247 }
4248
4249 pub fn redo(&mut self, engine: &mut TextEngine) -> Result<bool, EditorError> {
4250 self.finish_composition();
4251 let Some(history) = self.redo.pop() else {
4252 return Ok(false);
4253 };
4254 match self.apply_history(engine, history) {
4255 Ok(inverse) => {
4256 self.undo.push(inverse);
4257 Ok(true)
4258 }
4259 Err((history, error)) => {
4260 self.redo.push(history);
4261 Err(error)
4262 }
4263 }
4264 }
4265
4266 fn apply_history(
4267 &mut self,
4268 engine: &mut TextEngine,
4269 history: History,
4270 ) -> Result<History, (History, EditorError)> {
4271 self.pending = None;
4272 self.typing = None;
4273 let valid = match &history {
4274 History::Date(date) => self
4275 .date
4276 .as_ref()
4277 .is_some_and(|current| current.source().id == date.source().id),
4278 History::Paper { .. } => true,
4279 History::Draft {
4280 outlines, index, ..
4281 } => {
4282 *index <= self.outlines.len()
4283 && self
4284 .outlines
4285 .iter()
4286 .position(|item| item.id == outlines[0].id)
4287 .is_none_or(|found| found == *index)
4288 }
4289 History::Text { outline, .. } | History::Layout { outline, .. } => {
4290 self.outlines.iter().any(|item| item.id == *outline)
4291 }
4292 History::Position { object, .. } => self.object_layout_mut(*object).is_some(),
4293 History::Split {
4294 outline, at, part, ..
4295 } => {
4296 self.outlines
4297 .iter()
4298 .find(|item| item.id == *outline)
4299 .is_some_and(|item| (1..item.document.nodes().len()).contains(at))
4300 && !self.outlines.iter().any(|item| item.id == *part)
4301 }
4302 History::Join { outline, part, .. } => [outline, part]
4303 .iter()
4304 .all(|id| self.outlines.iter().any(|item| item.id == **id)),
4305 History::Image { image, .. } => self.image(*image).is_some(),
4306 History::Tags { object, .. } => self.object_tags_mut(*object).is_some(),
4307 History::Picture {
4308 index,
4309 image: Some(_),
4310 } => *index <= self.objects.len(),
4311 History::Picture { index, image: None } => self
4312 .objects
4313 .get(*index)
4314 .and_then(page::Content::picture)
4315 .is_some(),
4316 History::File {
4317 index,
4318 file: Some(_),
4319 } => *index <= self.objects.len(),
4320 History::File { index, file: None } => self
4321 .objects
4322 .get(*index)
4323 .and_then(page::Content::file)
4324 .is_some(),
4325 History::Ink {
4326 index,
4327 ink: Some(_),
4328 } => *index <= self.objects.len(),
4329 History::Ink { index, ink: None } => {
4330 matches!(self.objects.get(*index), Some(page::Content::Ink(_)))
4331 }
4332 History::Strokes { ink, .. } => self.has_ink(*ink),
4333 History::Remove { outline, focus } => {
4334 self.outlines.iter().any(|item| item.id == *outline)
4335 && match focus {
4336 RestoreFocus::Outline(focus) => {
4337 outline != focus && self.outlines.iter().any(|item| item.id == *focus)
4338 }
4339 RestoreFocus::Caret { .. } => true,
4340 }
4341 }
4342 History::Insert { index, .. } | History::Restore { index, .. } => {
4343 *index <= self.outlines.len()
4344 }
4345 History::Group { .. } => true,
4346 };
4347 if !valid {
4348 return Err((history, EditError::InvalidRange.into()));
4349 }
4350 let inverse = match history {
4351 History::Date(date) => {
4352 let shown = self.date.replace(*date).unwrap();
4353 self.record(Ok(self.date_ops()));
4354 History::Date(Box::new(shown))
4355 }
4356 History::Paper { color, rule_lines } => self.paper(color, rule_lines),
4357 History::Draft {
4358 outlines,
4359 index,
4360 caret,
4361 selection,
4362 restore_caret,
4363 } => {
4364 let draft = if restore_caret {
4365 match TextOutline::from_outline(engine, &caret, &self.definitions) {
4366 Ok(mut draft) => {
4367 draft.selection = selection;
4368 Some(draft)
4369 }
4370 Err(error) => {
4371 return Err((
4372 History::Draft {
4373 outlines,
4374 index,
4375 caret,
4376 selection,
4377 restore_caret,
4378 },
4379 error,
4380 ));
4381 }
4382 }
4383 } else {
4384 None
4385 };
4386 let outline = outlines[usize::from(!restore_caret)].clone();
4387 let id = outline.id;
4388 let stored = self
4389 .outlines
4390 .get(index)
4391 .is_some_and(|item| item.id == id)
4392 .then(|| self.outlines.remove(index));
4393 self.active = if outline.is_empty() {
4394 Focus::Caret {
4395 outline: Box::new(outline),
4396 index,
4397 }
4398 } else {
4399 self.outlines.insert(index, outline);
4400 match draft {
4401 Some(outline) => Focus::Draft {
4402 index,
4403 outline: Box::new(outline),
4404 },
4405 None => Focus::Outline(index),
4406 }
4407 };
4408 self.record(self.outline_ops(id, stored.as_ref()));
4409 History::Draft {
4410 outlines,
4411 index,
4412 caret,
4413 selection,
4414 restore_caret: !restore_caret,
4415 }
4416 }
4417 History::Text { outline, change } => {
4418 let index = self
4419 .outlines
4420 .iter()
4421 .position(|item| item.id == outline)
4422 .unwrap();
4423 let previous = std::mem::replace(&mut self.active, Focus::Outline(index));
4424 let inverse = match self.apply(
4425 engine,
4426 change.edit.clone(),
4427 change.selection,
4428 false,
4429 Some(&change.positions),
4430 ) {
4431 Ok(change) => change,
4432 Err(error) => {
4433 self.active = previous;
4434 return Err((History::Text { outline, change }, error));
4435 }
4436 };
4437 self.record(self.change_ops(&self.outlines[index], &inverse));
4438 History::Text {
4439 outline,
4440 change: Box::new(inverse),
4441 }
4442 }
4443 History::Position { object, position } => {
4444 let layout = self.object_layout_mut(object).unwrap();
4445 let previous = [layout.x, layout.y];
4446 [layout.x, layout.y] = position;
4447 if let Some(index) = self.outlines.iter().position(|item| item.id == object) {
4448 self.active = Focus::Outline(index);
4449 }
4450 self.record(self.placement_ops(&Placement {
4451 id: object,
4452 position: previous,
4453 }));
4454 History::Position {
4455 object,
4456 position: previous,
4457 }
4458 }
4459 History::Split {
4460 outline,
4461 at,
4462 part,
4463 position,
4464 } => match self.split_outline(engine, outline, at, part, position) {
4465 Ok(parents) => History::Join {
4466 outline,
4467 part,
4468 parents,
4469 },
4470 Err(error) => {
4471 return Err((
4472 History::Split {
4473 outline,
4474 at,
4475 part,
4476 position,
4477 },
4478 error,
4479 ));
4480 }
4481 },
4482 History::Join {
4483 outline,
4484 part,
4485 parents,
4486 } => match self.join_outline(engine, outline, part, &parents) {
4487 Ok((at, position)) => History::Split {
4488 outline,
4489 at,
4490 part,
4491 position,
4492 },
4493 Err(error) => {
4494 return Err((
4495 History::Join {
4496 outline,
4497 part,
4498 parents,
4499 },
4500 error,
4501 ));
4502 }
4503 },
4504 History::Layout { outline, layout } => {
4505 let index = self
4506 .outlines
4507 .iter()
4508 .position(|item| item.id == outline)
4509 .unwrap();
4510 let mut source = self.outlines[index].snapshot();
4511 let previous = std::mem::replace(&mut source.layout, layout);
4512 let mut resized =
4513 match TextOutline::from_outline(engine, &source, &self.definitions) {
4514 Ok(resized) => resized,
4515 Err(error) => {
4516 return Err((
4517 History::Layout {
4518 outline,
4519 layout: source.layout,
4520 },
4521 error,
4522 ));
4523 }
4524 };
4525 resized.selection = self.outlines[index].selection;
4526 self.outlines[index] = resized;
4527 self.record(ops::layout_ops(
4528 outline,
4529 &previous,
4530 &self.outlines[index].layout,
4531 ));
4532 self.active = Focus::Outline(index);
4533 History::Layout {
4534 outline,
4535 layout: previous,
4536 }
4537 }
4538 History::Tags { object, tags } => History::Tags {
4539 object,
4540 tags: self.swap_object_tags(object, tags),
4541 },
4542 History::Image { image, layout } => {
4543 let previous =
4544 std::mem::replace(&mut self.image_mut(image).unwrap().layout, layout);
4545 self.record(Ok(ops::picture_layout(self.image(image).unwrap())));
4546 History::Image {
4547 image,
4548 layout: previous,
4549 }
4550 }
4551 History::Picture { index, image } => History::Picture {
4552 index,
4553 image: match image {
4554 Some(image) => {
4555 self.add_picture(index, *image);
4556 None
4557 }
4558 None => Some(Box::new(self.remove_picture(index))),
4559 },
4560 },
4561 History::File { index, file } => History::File {
4562 index,
4563 file: match file {
4564 Some(file) => {
4565 self.add_file(index, *file);
4566 None
4567 }
4568 None => Some(Box::new(self.remove_file(index))),
4569 },
4570 },
4571 History::Ink { index, ink } => History::Ink {
4572 index,
4573 ink: match ink {
4574 Some(ink) => {
4575 self.add_ink(index, *ink);
4576 None
4577 }
4578 None => Some(Box::new(self.remove_ink(index))),
4579 },
4580 },
4581 History::Strokes { ink, strokes } => self.set_strokes(ink, strokes),
4582 History::Remove { outline, focus } => {
4583 let index = self
4584 .outlines
4585 .iter()
4586 .position(|item| item.id == outline)
4587 .unwrap();
4588 let next_focus = match &focus {
4589 RestoreFocus::Caret {
4590 source,
4591 selection,
4592 index,
4593 } => match TextOutline::from_outline(engine, source, &self.definitions) {
4594 Ok(mut outline) => {
4595 outline.selection = *selection;
4596 Focus::Caret {
4597 outline: Box::new(outline),
4598 index: *index,
4599 }
4600 }
4601 Err(error) => return Err((History::Remove { outline, focus }, error)),
4602 },
4603 RestoreFocus::Outline(id) => {
4604 let previous = self
4605 .outlines
4606 .iter()
4607 .position(|outline| outline.id == *id)
4608 .unwrap();
4609 Focus::Outline(previous - usize::from(previous > index))
4610 }
4611 };
4612 let outline = Box::new(self.outlines.remove(index));
4613 self.record(Ok(vec![PageOp::Delete { object: outline.id }]));
4614 self.active = next_focus;
4615 History::Insert {
4616 index,
4617 outline,
4618 focus,
4619 }
4620 }
4621 History::Restore {
4622 index,
4623 source,
4624 selection,
4625 focus,
4626 } => {
4627 let mut outline =
4628 match TextOutline::from_outline(engine, &source, &self.definitions) {
4629 Ok(outline) => outline,
4630 Err(error) => {
4631 return Err((
4632 History::Restore {
4633 index,
4634 source,
4635 selection,
4636 focus,
4637 },
4638 error,
4639 ));
4640 }
4641 };
4642 outline.selection = selection;
4643 return self.apply_history(
4644 engine,
4645 History::Insert {
4646 index,
4647 outline: Box::new(outline),
4648 focus,
4649 },
4650 );
4651 }
4652 History::Insert {
4653 index,
4654 outline,
4655 focus,
4656 } => {
4657 let id = outline.id;
4658 self.outlines.insert(index, *outline);
4659 self.record(self.outline_ops(id, None));
4660 self.active = Focus::Outline(index);
4661 History::Remove { outline: id, focus }
4662 }
4663 History::Group { mut entries, page } => {
4664 let mut inverses = Vec::with_capacity(entries.len());
4665 while let Some(entry) = entries.pop() {
4666 match self.apply_history(engine, entry) {
4667 Ok(inverse) => inverses.push(inverse),
4668 Err((entry, error)) => {
4669 entries.push(entry);
4670 return Err((History::Group { entries, page }, error));
4671 }
4672 }
4673 }
4674 History::Group {
4675 entries: inverses,
4676 page,
4677 }
4678 }
4679 };
4680 self.preferred_x = None;
4681 Ok(inverse)
4682 }
4683
4684 pub fn marked_range(&self) -> Option<Range<TextPosition>> {
4685 self.composition
4686 .as_ref()
4687 .map(|composition| composition.range.clone())
4688 }
4689
4690 pub fn compose(
4691 &mut self,
4692 engine: &mut TextEngine,
4693 text: String,
4694 selected: Range<u32>,
4695 ) -> Result<(), EditorError> {
4696 if selected.start > selected.end {
4697 return Err(EditError::InvalidRange.into());
4698 }
4699 if self.page_selected() {
4700 self.grouped(|editor| editor.remove_page(engine, true))?;
4701 }
4702 let range = self.marked_range().unwrap_or_else(|| {
4703 self.active_outline().selection.positions[0]
4704 .min(self.active_outline().selection.positions[1])
4705 ..self.active_outline().selection.positions[0]
4706 .max(self.active_outline().selection.positions[1])
4707 });
4708 let positions = [
4709 inserted_position(range.start, &text, selected.start)?,
4710 inserted_position(range.start, &text, selected.end)?,
4711 ];
4712 let end = inserted_position(
4713 range.start,
4714 &text,
4715 text.encode_utf16()
4716 .count()
4717 .try_into()
4718 .map_err(|_| EditError::TextTooLong)?,
4719 )?;
4720 let format = self.typing_format(range.start)?;
4721 let replacement = text
4722 .split('\n')
4723 .map(|part| Paragraph::new(part.to_owned(), format.clone()))
4724 .collect();
4725 let mut edit = self
4726 .active_outline()
4727 .document
4728 .replace(range.clone(), replacement)?;
4729 self.own_lists(&mut edit)?;
4730 self.link_notes(&mut edit)?;
4731 self.fit_columns(engine, &mut edit)?;
4732 let inverse = self.apply(
4733 engine,
4734 edit,
4735 Selection {
4736 positions,
4737 affinities: [Affinity::Downstream; 2],
4738 },
4739 false,
4740 None,
4741 )?;
4742 let original = if let Some(composition) = self.composition.take() {
4743 let mut original = composition.original;
4744 // Nodes this edit reaches past what the composition changed are still as they were.
4745 let changed = original.edit.range.len();
4746 original
4747 .edit
4748 .replacement
4749 .extend(inverse.edit.replacement.iter().skip(changed).cloned());
4750 original.edit.range = inverse.edit.range;
4751 for (id, widths) in inverse.edit.columns {
4752 original.edit.columns.entry(id).or_insert(widths);
4753 }
4754 for position in inverse.positions {
4755 if !original
4756 .positions
4757 .iter()
4758 .any(|original| original.id == position.id)
4759 {
4760 original.positions.push(position);
4761 }
4762 }
4763 original
4764 } else {
4765 inverse
4766 };
4767 self.composition = Some(Composition {
4768 original,
4769 range: range.start..end,
4770 });
4771 Ok(())
4772 }
4773
4774 pub fn finish_composition(&mut self) {
4775 if let Some(composition) = self.composition.take() {
4776 self.record_change(composition.original);
4777 }
4778 }
4779
4780 pub fn cancel_composition(&mut self, engine: &mut TextEngine) -> Result<bool, EditorError> {
4781 let Some(composition) = &self.composition else {
4782 return Ok(false);
4783 };
4784 let original = composition.original.clone();
4785 self.apply(
4786 engine,
4787 original.edit,
4788 original.selection,
4789 false,
4790 Some(&original.positions),
4791 )?;
4792 self.composition = None;
4793 Ok(true)
4794 }
4795
4796 pub fn commit_text(
4797 &mut self,
4798 engine: &mut TextEngine,
4799 text: String,
4800 ) -> Result<(), EditorError> {
4801 if self.composition.is_some() {
4802 let end = text
4803 .encode_utf16()
4804 .count()
4805 .try_into()
4806 .map_err(|_| EditError::TextTooLong)?;
4807 let lines = text.contains('\n');
4808 let commit = |editor: &mut Self| {
4809 editor.compose(engine, text, end..end)?;
4810 editor.finish_composition();
4811 Ok(())
4812 };
4813 if lines {
4814 commit(self)
4815 } else {
4816 self.typed(commit)
4817 }
4818 } else {
4819 self.insert(engine, &text)
4820 }
4821 }
4822
4823 fn commit(
4824 &mut self,
4825 engine: &mut TextEngine,
4826 mut edit: DocumentEdit,
4827 selection: Selection,
4828 ) -> Result<(), EditorError> {
4829 self.own_lists(&mut edit)?;
4830 self.link_notes(&mut edit)?;
4831 self.fit_columns(engine, &mut edit)?;
4832 let inverse = self.apply(engine, edit, selection, true, None)?;
4833 self.record_change(inverse);
4834 Ok(())
4835 }
4836
4837 /// Gives each paragraph an edit adds its own copy of the lists it carries, as OneNote keeps
4838 /// a list node per paragraph.
4839 fn own_lists(&mut self, edit: &mut DocumentEdit) -> Result<(), EditError> {
4840 let existing = descendants(
4841 &self.active_outline().document.container(edit.container)?[edit.range.clone()],
4842 None,
4843 )
4844 .map(|(_, _, node)| node.id)
4845 .collect::<BTreeSet<_>>();
4846 let mut pending = edit.replacement.iter_mut().collect::<Vec<_>>();
4847 while let Some(node) = pending.pop() {
4848 if !existing.contains(&node.id) {
4849 for list in &mut node.lists {
4850 let definition = self
4851 .definitions
4852 .get(list)
4853 .ok_or(EditError::InvalidStructure)?
4854 .clone();
4855 *list = onestore::page::text::new_id()?;
4856 self.definitions.insert(*list, definition);
4857 }
4858 }
4859 if let ParagraphContent::Table(table) = &mut node.content {
4860 pending.extend(
4861 table
4862 .rows
4863 .iter_mut()
4864 .flat_map(|row| &mut row.cells)
4865 .flat_map(|cell| &mut cell.paragraphs),
4866 );
4867 }
4868 }
4869 Ok(())
4870 }
4871
4872 fn record_change(&mut self, change: TextChange) {
4873 self.pending = None;
4874 if let Focus::Draft { index, .. } = self.active {
4875 let Focus::Draft { mut outline, .. } =
4876 std::mem::replace(&mut self.active, Focus::Outline(index))
4877 else {
4878 unreachable!()
4879 };
4880 let mut caret = outline.snapshot();
4881 crate::document::container_mut(&mut caret.paragraphs, change.edit.container)
4882 .expect("inverse edit container exists")
4883 .splice(change.edit.range, change.edit.replacement);
4884 let mut columns = change.edit.columns;
4885 crate::document::swap_columns(&mut caret.paragraphs, &mut columns);
4886 let last_selected = outline.selection.positions[0]
4887 .paragraph
4888 .max(outline.selection.positions[1].paragraph);
4889 let mut count = outline.document.nodes().len();
4890 let mut selected_end = 0;
4891 let selected_root = outline
4892 .document
4893 .nodes()
4894 .iter()
4895 .position(|node| {
4896 selected_end += leaves(std::slice::from_ref(node), None).count();
4897 selected_end > last_selected
4898 })
4899 .expect("selection addresses a text leaf");
4900 let original_ids = self.outlines[index]
4901 .document
4902 .nodes()
4903 .iter()
4904 .map(|node| node.id)
4905 .collect::<BTreeSet<_>>();
4906 while count > selected_root + 1 {
4907 let node = &outline.document.nodes()[count - 1];
4908 if node.text().is_none_or(|text| !text.text.text().is_empty())
4909 || original_ids.contains(&node.id)
4910 {
4911 break;
4912 }
4913 count -= 1;
4914 }
4915 if count < outline.document.nodes().len() {
4916 let edit = DocumentEdit {
4917 columns: BTreeMap::new(),
4918 container: None,
4919 range: count..outline.document.nodes().len(),
4920 replacement: Vec::new(),
4921 };
4922 let relayout = outline
4923 .shaped
4924 .relayout(
4925 outline.document.nodes(),
4926 &edit,
4927 edit.range.clone(),
4928 &outline.indents,
4929 outline.wrap_width(),
4930 outline.layout.width_set_by_user == Some(true),
4931 &mut |_, _, _, _| Err(LayoutError::UnsupportedContent),
4932 )
4933 .expect("removing trailing text lays nothing out anew");
4934 outline
4935 .document
4936 .apply(edit)
4937 .expect("removing a provisional suffix preserves a valid document");
4938 outline.shaped.commit(relayout);
4939 }
4940 if outline.document == self.outlines[index].document {
4941 self.outlines[index].selection = outline.selection;
4942 return;
4943 }
4944 let versions = Box::new([self.outlines[index].clone(), (*outline).clone()]);
4945 if outline.is_empty() {
4946 self.outlines.remove(index);
4947 self.active = Focus::Caret { outline, index };
4948 } else {
4949 self.outlines[index] = *outline;
4950 }
4951 self.record(self.outline_ops(versions[0].id, Some(&versions[0])));
4952 self.undo.push(History::Draft {
4953 outlines: versions,
4954 index,
4955 caret: Box::new(caret),
4956 selection: change.selection,
4957 restore_caret: true,
4958 });
4959 } else if self.caret_outline().is_some() {
4960 if self.active_outline().is_empty() {
4961 return;
4962 }
4963 let Focus::Caret { index, .. } = self.active else {
4964 unreachable!()
4965 };
4966 let Focus::Caret { outline, .. } =
4967 std::mem::replace(&mut self.active, Focus::Outline(index))
4968 else {
4969 unreachable!();
4970 };
4971 let mut source = outline.snapshot();
4972 source.paragraphs = change.edit.replacement;
4973 let id = outline.id;
4974 self.undo.push(History::Remove {
4975 outline: id,
4976 focus: RestoreFocus::Caret {
4977 source: Box::new(source),
4978 selection: change.selection,
4979 index,
4980 },
4981 });
4982 self.outlines.insert(index, *outline);
4983 self.record(self.outline_ops(id, None));
4984 } else if self.active_outline().is_empty() && !self.active_outline().title {
4985 let Focus::Outline(index) = self.active else {
4986 unreachable!()
4987 };
4988 let outline = self.outlines.remove(index);
4989 self.record(Ok(vec![PageOp::Delete { object: outline.id }]));
4990 let mut source = outline.snapshot();
4991 source
4992 .paragraphs
4993 .splice(change.edit.range, change.edit.replacement);
4994 self.undo.push(History::Restore {
4995 index,
4996 source: Box::new(source),
4997 selection: change.selection,
4998 focus: RestoreFocus::Caret {
4999 source: Box::new(outline.snapshot()),
5000 selection: outline.selection,
5001 index,
5002 },
5003 });
5004 self.active = Focus::Caret {
5005 outline: Box::new(outline),
5006 index,
5007 };
5008 } else {
5009 self.record(self.change_ops(self.active_outline(), &change));
5010 self.undo.push(History::Text {
5011 outline: self.active_outline().id,
5012 change: Box::new(change),
5013 });
5014 }
5015 self.redo.clear();
5016 }
5017
5018 fn apply(
5019 &mut self,
5020 engine: &mut TextEngine,
5021 edit: DocumentEdit,
5022 selection: Selection,
5023 finish_composition: bool,
5024 positions: Option<&[Placement]>,
5025 ) -> Result<TextChange, EditorError> {
5026 let outline = self.active_outline();
5027 outline.document.validate_edit(&edit)?;
5028 let range = match edit.container {
5029 None => edit.range.clone(),
5030 Some(cell) => {
5031 let root = outline.document.root(cell)?;
5032 root..root + 1
5033 }
5034 };
5035 let relayout = outline.shaped.relayout(
5036 outline.document.nodes(),
5037 &edit,
5038 range,
5039 &outline.indents,
5040 outline.wrap_width(),
5041 outline.layout.width_set_by_user == Some(true),
5042 &mut |node, previous, width, indents| {
5043 ParagraphLayout::shape(engine, node, previous, width, indents, &self.definitions)
5044 },
5045 )?;
5046 let [replaced, ..] = outline.shaped.pieces(relayout.range.clone());
5047 for position in selection.positions {
5048 let (node, before) = outline
5049 .document
5050 .edited_leaf(&edit, position.paragraph)
5051 .ok_or(EditError::InvalidRange)?;
5052 node.text().unwrap().text.byte_offset(position.offset)?;
5053 // A paragraph the edit leaves keeps its layout unless that layout is replaced.
5054 if !relayout
5055 .segment
5056 .paragraphs
5057 .iter()
5058 .any(|paragraph| paragraph.id == node.id)
5059 && !before
5060 .and_then(|before| outline.visible_index(before).ok())
5061 .is_some_and(|index| !replaced.contains(&index))
5062 {
5063 return Err(EditError::InvalidRange.into());
5064 }
5065 }
5066 let placements = match positions {
5067 Some(positions) => positions.to_vec(),
5068 None => self.title_flow(relayout.size[1])?,
5069 };
5070 let previous_positions = placements
5071 .iter()
5072 .map(|placement| {
5073 let layout = self
5074 .object_layouts()
5075 .find_map(|(id, layout)| (id == placement.id).then_some(layout))
5076 .ok_or(EditError::InvalidStructure)?;
5077 if placement
5078 .position
5079 .iter()
5080 .flatten()
5081 .any(|value| !value.is_finite())
5082 {
5083 return Err(EditorError::InvalidGeometry);
5084 }
5085 Ok(Placement {
5086 id: placement.id,
5087 position: [layout.x, layout.y],
5088 })
5089 })
5090 .collect::<Result<Vec<_>, EditorError>>()?;
5091 if finish_composition && self.composition.is_some() {
5092 // Committing marked text can remove provisional paragraphs from the validated view.
5093 self.finish_composition();
5094 return self.apply(engine, edit, selection, false, positions);
5095 }
5096 let outline = self.active_outline_mut();
5097 let inverse = outline.document.splice(edit);
5098 outline.shaped.commit(relayout);
5099 self.preferred_x = None;
5100 let previous_selection =
5101 std::mem::replace(&mut self.active_outline_mut().selection, selection);
5102 for placement in placements {
5103 let layout = self
5104 .object_layout_mut(placement.id)
5105 .expect("validated object position");
5106 [layout.x, layout.y] = placement.position;
5107 }
5108 Ok(TextChange {
5109 edit: inverse,
5110 selection: previous_selection,
5111 positions: previous_positions,
5112 })
5113 }
5114}
5115
5116/// Whether `content` is picture or file `id`.
5117fn holds(content: &ParagraphContent, id: ExGuid) -> bool {
5118 match content {
5119 ParagraphContent::Image(image) => image.id == id,
5120 ParagraphContent::Attachment(file) => file.id == id,
5121 _ => false,
5122 }
5123}
5124
5125/// Follows paragraph IDs and UTF-16 through edits; deleted paragraphs clamp to their surviving index.
5126pub fn follow(old: &TextDocument, new: &TextDocument, position: TextPosition) -> TextPosition {
5127 let found = old.leaf(position.paragraph).and_then(|(_, _, node)| {
5128 let paragraph = new
5129 .text_nodes()
5130 .position(|candidate| candidate.id == node.id)?;
5131 Some((paragraph, node, new.leaf(paragraph)?.2))
5132 });
5133 let Some((paragraph, before, after)) = found else {
5134 let count = new.text_nodes().count();
5135 return TextPosition {
5136 paragraph: position.paragraph.min(count.saturating_sub(1)),
5137 offset: 0,
5138 };
5139 };
5140 let units = |text: &str| text.encode_utf16().count() as u32;
5141 let (a, b) = (
5142 before.text().unwrap().text.text(),
5143 after.text().unwrap().text.text(),
5144 );
5145 let prefix = units(
5146 &a[..a
5147 .char_indices()
5148 .zip(b.chars())
5149 .find(|((_, x), y)| x != y)
5150 .map_or(a.len().min(b.len()), |((at, _), _)| at)],
5151 );
5152 let suffix = a
5153 .chars()
5154 .rev()
5155 .zip(b.chars().rev())
5156 .take_while(|(x, y)| x == y)
5157 .map(|(x, _)| x.len_utf16() as u32)
5158 .scan(0, |sum, units| {
5159 *sum += units;
5160 Some(*sum)
5161 })
5162 .take_while(|sum| prefix + sum <= units(a).min(units(b)))
5163 .last()
5164 .unwrap_or(0);
5165 let (old_length, new_length) = (units(a), units(b));
5166 let offset = if position.offset <= prefix {
5167 position.offset
5168 } else if position.offset >= old_length - suffix {
5169 new_length - (old_length - position.offset)
5170 } else {
5171 new_length - suffix
5172 };
5173 TextPosition { paragraph, offset }
5174}
5175
5176fn inserted_position(
5177 mut position: TextPosition,
5178 text: &str,
5179 offset: u32,
5180) -> Result<TextPosition, EditError> {
5181 let mut units = 0_u32;
5182 for character in text.chars() {
5183 if units == offset {
5184 return Ok(position);
5185 }
5186 units = units
5187 .checked_add(character.len_utf16() as u32)
5188 .ok_or(EditError::TextTooLong)?;
5189 if units > offset {
5190 return Err(EditError::InvalidRange);
5191 }
5192 if character == '\n' {
5193 position.paragraph = position
5194 .paragraph
5195 .checked_add(1)
5196 .ok_or(EditError::TextTooLong)?;
5197 position.offset = 0;
5198 } else {
5199 position.offset = position
5200 .offset
5201 .checked_add(character.len_utf16() as u32)
5202 .ok_or(EditError::TextTooLong)?;
5203 }
5204 }
5205 if units == offset {
5206 Ok(position)
5207 } else {
5208 Err(EditError::InvalidRange)
5209 }
5210}
5211
5212#[cfg(test)]
5213mod tests {
5214 use super::*;
5215 use onestore::document::Format;
5216
5217 fn alternate_shape(
5218 engine: &mut TextEngine,
5219 editor: &CanvasEditor,
5220 width: f32,
5221 ) -> OutlineLayout {
5222 let outline = editor.active_outline();
5223 OutlineLayout::flow(
5224 outline.document.nodes().iter(),
5225 &outline.indents,
5226 width,
5227 true,
5228 0,
5229 None,
5230 &mut |node, previous, width, indents| {
5231 ParagraphLayout::shape(engine, node, previous, width, indents, &editor.definitions)
5232 },
5233 )
5234 .unwrap()
5235 }
5236
5237 #[test]
5238 fn alternate_geometry_keeps_source_utf16_and_blank_paragraphs() {
5239 let mut engine = TextEngine::default();
5240 let document = TextDocument::new(vec![
5241 Paragraph::from_runs([
5242 ("alpha ".into(), Format::default()),
5243 (
5244 "😀 e\u{301} ".into(),
5245 Format {
5246 bold: Some(true),
5247 ..Format::default()
5248 },
5249 ),
5250 ("omega".into(), Format::default()),
5251 ]),
5252 Paragraph::new(String::new(), Format::default()),
5253 Paragraph::new(String::new(), Format::default()),
5254 Paragraph::new("last".into(), Format::default()),
5255 Paragraph::new("left אבג right 😀".into(), Format::default()),
5256 ])
5257 .unwrap();
5258 let mut editor = CanvasEditor::new(&mut engine, document.clone(), 1000.0).unwrap();
5259 let selection = [
5260 TextPosition {
5261 paragraph: 4,
5262 offset: 5,
5263 },
5264 TextPosition {
5265 paragraph: 0,
5266 offset: 6,
5267 },
5268 ]
5269 .into();
5270 editor.select(selection).unwrap();
5271 let page = editor.page().unwrap();
5272 let canonical = layout_snapshot(&editor.active_outline().shaped);
5273 let ids = editor
5274 .active_outline()
5275 .layouts()
5276 .map(|(index, paragraph)| (index, paragraph.id))
5277 .collect::<Vec<_>>();
5278 let text = "alpha 😀 e\u{301} omega\n\n\nlast\nleft אבג right 😀";
5279 assert_eq!(editor.active_outline().shown_text(), text);
5280 let mut heights = Vec::new();
5281 for width in [70.0, 230.0] {
5282 let shaped = alternate_shape(&mut engine, &editor, width);
5283 let geometry = OutlineGeometry {
5284 source: editor.active_outline(),
5285 shaped: &shaped,
5286 };
5287 assert_eq!(
5288 geometry
5289 .layouts()
5290 .map(|(index, paragraph)| (index, paragraph.id))
5291 .collect::<Vec<_>>(),
5292 ids
5293 );
5294 for index in [1, 2] {
5295 let paragraph = geometry.paragraph_layout(index).unwrap();
5296 assert!(paragraph.projection.text().text().is_empty());
5297 assert_eq!(paragraph.text.lines().count(), 1);
5298 }
5299 for (index, paragraph) in geometry.layouts() {
5300 for (byte, offset) in paragraph.projection.source_boundaries() {
5301 for affinity in [Affinity::Upstream, Affinity::Downstream] {
5302 let cursor = paragraph.cursor(offset, affinity).unwrap();
5303 if cursor.index() != byte {
5304 continue;
5305 }
5306 let position = TextPosition {
5307 paragraph: index,
5308 offset,
5309 };
5310 let caret = geometry.caret_at(position, affinity, 1.0).unwrap();
5311 let hit = geometry
5312 .selection_at(
5313 caret.x0 as f32 + 0.01,
5314 ((caret.y0 + caret.y1) * 0.5) as f32,
5315 SelectionUnit::Grapheme,
5316 )
5317 .unwrap();
5318 assert_eq!(hit.positions[1].paragraph, index);
5319 // Bidi and soft-wrap boundaries can share a point with another source caret.
5320 let recovered = geometry
5321 .caret_at(hit.positions[1], hit.affinities[1], 1.0)
5322 .unwrap();
5323 assert!(
5324 (caret.x0 - recovered.x0).abs() < 0.05,
5325 "{position:?} {affinity:?} -> {hit:?}"
5326 );
5327 assert!(
5328 (caret.y0 - recovered.y0).abs() < 0.05,
5329 "{position:?} {affinity:?} -> {hit:?}"
5330 );
5331 let source = document.paragraph(index).unwrap();
5332 source.byte_offset(hit.positions[1].offset).unwrap();
5333 let flat = geometry.source.utf16_offset(position).unwrap();
5334 assert_eq!(geometry.source.utf16_position(flat).unwrap(), position);
5335 }
5336 }
5337 }
5338 assert!(matches!(
5339 geometry.selection_at(f32::NAN, 0.0, SelectionUnit::Grapheme),
5340 Err(EditError::InvalidRange)
5341 ));
5342 heights.push(shaped.size[1]);
5343 assert_eq!(editor.page().unwrap(), page);
5344 assert_eq!(layout_snapshot(&editor.active_outline().shaped), canonical);
5345 assert_eq!(editor.selection(), selection);
5346 assert_eq!(editor.history_depth(), [0, 0]);
5347 assert!(editor.take_ops().unwrap().is_empty());
5348 }
5349 assert!(heights[0] > heights[1]);
5350 assert_eq!(editor.active_outline().document, document);
5351 assert_eq!(
5352 editor.active_outline().shown_text().encode_utf16().count(),
5353 text.encode_utf16().count()
5354 );
5355 }
5356
5357 #[test]
5358 fn alternate_range_and_marked_geometry_use_display_lines() {
5359 let mut engine = TextEngine::default();
5360 let document = TextDocument::new(vec![
5361 Paragraph::from_runs([
5362 ("alpha 😀 ".into(), Format::default()),
5363 (
5364 "a rich run with enough words to wrap several times in a narrow column and twice in a wider column".into(),
5365 Format { bold: Some(true), ..Format::default() },
5366 ),
5367 ]),
5368 Paragraph::new(String::new(), Format::default()),
5369 Paragraph::new("last".into(), Format::default()),
5370 ]).unwrap();
5371 let page = CanvasEditor::new(&mut engine, document, 1000.0)
5372 .unwrap()
5373 .page()
5374 .unwrap();
5375 let selection: Selection = [
5376 TextPosition {
5377 paragraph: 2,
5378 offset: 4,
5379 },
5380 TextPosition {
5381 paragraph: 0,
5382 offset: 6,
5383 },
5384 ]
5385 .into();
5386 let on_lines = |shaped: &OutlineLayout, rectangles: &[BoundingBox]| {
5387 assert!(!rectangles.is_empty());
5388 for rect in rectangles {
5389 assert!(shaped.paragraphs.iter().any(|paragraph| {
5390 paragraph.text.lines().any(|(_, line)| {
5391 let top = f64::from(paragraph.origin[1] + line.top);
5392 (rect.y0 - top).abs() < 0.001
5393 && (rect.y1 - top - f64::from(line.height)).abs() < 0.001
5394 })
5395 }));
5396 }
5397 };
5398 for commit in [false, true] {
5399 let mut editor = CanvasEditor::from_page(page.clone(), &mut engine).unwrap();
5400 editor.insert(&mut engine, "!").unwrap();
5401 editor.undo(&mut engine).unwrap();
5402 editor.take_ops().unwrap();
5403 editor.select(selection).unwrap();
5404 let original = editor.page().unwrap();
5405 let canonical = editor.selection_rects().unwrap();
5406 let mut counts = Vec::new();
5407 for width in [70.0, 230.0] {
5408 let shaped = alternate_shape(&mut engine, &editor, width);
5409 let geometry = OutlineGeometry {
5410 source: editor.active_outline(),
5411 shaped: &shaped,
5412 };
5413 let rectangles = geometry.range_rects(selection).unwrap();
5414 on_lines(&shaped, &rectangles);
5415 assert_ne!(rectangles, canonical);
5416 let blank = geometry
5417 .caret_at(
5418 TextPosition {
5419 paragraph: 1,
5420 offset: 0,
5421 },
5422 Affinity::Downstream,
5423 4.0,
5424 )
5425 .unwrap();
5426 assert!(rectangles.contains(&blank));
5427 counts.push(rectangles.len());
5428 }
5429 assert!(counts[0] > counts[1]);
5430 assert_eq!(editor.history_depth(), [0, 1]);
5431 assert!(editor.take_ops().unwrap().is_empty());
5432 let preview =
5433 "marked 😀 preview with several words wrapping onto other lines\nsecond line";
5434 let end = preview.encode_utf16().count().try_into().unwrap();
5435 editor
5436 .compose(&mut engine, preview.into(), end..end)
5437 .unwrap();
5438 let marked = editor.marked_range().unwrap();
5439 let preview_page = editor.page().unwrap();
5440 let preview_selection = editor.selection();
5441 let canonical = layout_snapshot(&editor.active_outline().shaped);
5442 for width in [70.0, 230.0] {
5443 let shaped = alternate_shape(&mut engine, &editor, width);
5444 let geometry = OutlineGeometry {
5445 source: editor.active_outline(),
5446 shaped: &shaped,
5447 };
5448 on_lines(
5449 &shaped,
5450 &geometry
5451 .range_rects(Selection {
5452 positions: [marked.start, marked.end],
5453 affinities: [Affinity::Downstream, Affinity::Upstream],
5454 })
5455 .unwrap(),
5456 );
5457 for (index, paragraph) in geometry.layouts() {
5458 let source = geometry.source.document.paragraph(index).unwrap();
5459 let range = if index == marked.start.paragraph {
5460 marked.start.offset
5461 } else {
5462 0
5463 }..if index == marked.end.paragraph {
5464 marked.end.offset
5465 } else {
5466 source.utf16_offset(source.text().len()).unwrap()
5467 };
5468 for [_, _, baseline] in geometry.underlines(index, range).unwrap() {
5469 assert!(paragraph.text.lines().any(|(_, line)| {
5470 (baseline - paragraph.origin[1] - line.baseline).abs() < 0.001
5471 }));
5472 }
5473 }
5474 assert_eq!(editor.page().unwrap(), preview_page);
5475 assert_eq!(editor.selection(), preview_selection);
5476 assert_eq!(editor.marked_range(), Some(marked.clone()));
5477 assert_eq!(editor.history_depth(), [0, 1]);
5478 assert_eq!(layout_snapshot(&editor.active_outline().shaped), canonical);
5479 assert!(editor.take_ops().unwrap().is_empty());
5480 }
5481 if commit {
5482 editor.finish_composition();
5483 assert!(editor.marked_range().is_none());
5484 assert_eq!(editor.history_depth(), [1, 0]);
5485 assert!(!editor.take_ops().unwrap().is_empty());
5486 let committed = editor.page().unwrap();
5487 editor.undo(&mut engine).unwrap();
5488 assert_eq!(editor.page().unwrap(), original);
5489 assert_eq!(editor.selection(), selection);
5490 editor.redo(&mut engine).unwrap();
5491 assert_eq!(editor.page().unwrap(), committed);
5492 } else {
5493 assert!(editor.cancel_composition(&mut engine).unwrap());
5494 assert_eq!(editor.page().unwrap(), original);
5495 assert_eq!(editor.selection(), selection);
5496 assert_eq!(editor.history_depth(), [0, 1]);
5497 assert!(editor.take_ops().unwrap().is_empty());
5498 editor.redo(&mut engine).unwrap();
5499 assert!(
5500 editor
5501 .active_outline()
5502 .document
5503 .paragraph(0)
5504 .unwrap()
5505 .text()
5506 .starts_with("!alpha")
5507 );
5508 }
5509 }
5510 }
5511
5512 #[test]
5513 fn display_title_height_never_changes_canonical_placements() {
5514 let mut engine = TextEngine::default();
5515 let mut title = TextOutline::new(
5516 &mut engine,
5517 TextDocument::new(vec![Paragraph::new("Header".into(), Format::default())]).unwrap(),
5518 468.0,
5519 [0.0; 2],
5520 )
5521 .unwrap()
5522 .snapshot();
5523 title.title = true;
5524 title.layout.max_width = None;
5525 title.layout.width_set_by_user = None;
5526 let body = TextOutline::new(
5527 &mut engine,
5528 TextDocument::new(vec![Paragraph::new("Anchor".into(), Format::default())]).unwrap(),
5529 72.0,
5530 [120.0, 80.0],
5531 )
5532 .unwrap()
5533 .snapshot();
5534 let page = Page {
5535 title: "Header".into(),
5536 identity: None,
5537 created: None,
5538 margin_origin: [36.0, 14.4],
5539 rtl: false,
5540 color: None,
5541 rule_lines: None,
5542 definitions: BTreeMap::new(),
5543 objects: vec![
5544 PageObject::Title(Title {
5545 id: onestore::page::text::new_id().unwrap(),
5546 date: None,
5547 layout: Default::default(),
5548 outlines: vec![title],
5549 }),
5550 PageObject::Outline(body),
5551 ],
5552 };
5553 let text = "A long title must wrap independently of its canonical page and must never move the stored body according to a reader column. ".repeat(4);
5554 let mut results = Vec::new();
5555 let mut heights = Vec::new();
5556 for width in [70.0, 230.0] {
5557 let mut editor = CanvasEditor::from_page(page.clone(), &mut engine).unwrap();
5558 let original = editor.page().unwrap();
5559 let original_shape = layout_snapshot(&editor.active_outline().shaped);
5560 let shaped = alternate_shape(&mut engine, &editor, width);
5561 OutlineGeometry {
5562 source: editor.active_outline(),
5563 shaped: &shaped,
5564 }
5565 .caret_at(
5566 TextPosition {
5567 paragraph: 0,
5568 offset: 0,
5569 },
5570 Affinity::Downstream,
5571 1.0,
5572 )
5573 .unwrap();
5574 assert_eq!(editor.page().unwrap(), original);
5575 assert_eq!(editor.history_depth(), [0, 0]);
5576 assert!(editor.take_ops().unwrap().is_empty());
5577 editor.select_all().unwrap();
5578 editor.insert(&mut engine, &text).unwrap();
5579 let shaped = alternate_shape(&mut engine, &editor, width);
5580 let geometry = OutlineGeometry {
5581 source: editor.active_outline(),
5582 shaped: &shaped,
5583 };
5584 geometry
5585 .range_rects(editor.active_outline().whole())
5586 .unwrap();
5587 heights.push(shaped.size[1]);
5588 let edited = editor.page().unwrap();
5589 let edited_shape = layout_snapshot(&editor.active_outline().shaped);
5590 let edit_ops = editor.take_ops().unwrap();
5591 assert!(!edit_ops.is_empty());
5592 assert_eq!(editor.history_depth(), [1, 0]);
5593 editor.undo(&mut engine).unwrap();
5594 assert_eq!(editor.page().unwrap(), original);
5595 assert_eq!(
5596 layout_snapshot(&editor.active_outline().shaped),
5597 original_shape
5598 );
5599 let undo_ops = editor.take_ops().unwrap();
5600 assert_eq!(editor.history_depth(), [0, 1]);
5601 editor.redo(&mut engine).unwrap();
5602 assert_eq!(editor.page().unwrap(), edited);
5603 assert_eq!(
5604 layout_snapshot(&editor.active_outline().shaped),
5605 edited_shape
5606 );
5607 assert_eq!(editor.history_depth(), [1, 0]);
5608 results.push((
5609 edited,
5610 edited_shape,
5611 edit_ops,
5612 undo_ops,
5613 editor.take_ops().unwrap(),
5614 ));
5615 }
5616 assert!(heights[0] > heights[1]);
5617 assert_eq!(results[0], results[1]);
5618 }
5619
5620 #[test]
5621 fn alternate_navigation_follows_display_lines_and_keeps_source_anchors() {
5622 let mut engine = TextEngine::default();
5623 let document = TextDocument::new(vec![
5624 Paragraph::from_runs([
5625 ("alpha 😀 e\u{301} ".into(), Format::default()),
5626 (
5627 "many words cross several display lines while the canonical outline stays wide and unchanged ".repeat(3),
5628 Format { bold: Some(true), ..Format::default() },
5629 ),
5630 ("אבג end".into(), Format::default()),
5631 ]),
5632 Paragraph::new(String::new(), Format::default()),
5633 Paragraph::new(String::new(), Format::default()),
5634 Paragraph::new("last".into(), Format::default()),
5635 ]).unwrap();
5636 let mut editor = CanvasEditor::new(&mut engine, document, 2000.0).unwrap();
5637 let first = TextPosition {
5638 paragraph: 0,
5639 offset: 0,
5640 };
5641 let source_selection: Selection = [first; 2].into();
5642 let original = editor.page().unwrap();
5643 let canonical = layout_snapshot(&editor.active_outline().shaped);
5644 for width in [70.0, 230.0] {
5645 let shaped = alternate_shape(&mut engine, &editor, width);
5646 let geometry = OutlineGeometry {
5647 source: editor.active_outline(),
5648 shaped: &shaped,
5649 };
5650 let paragraph = geometry.paragraph_layout(0).unwrap();
5651 let caret = geometry.caret_at(first, Affinity::Downstream, 1.0).unwrap();
5652 let (_, second_line) = paragraph.text.lines().nth(1).unwrap();
5653 let hit = geometry
5654 .selection_at(
5655 caret.x0 as f32,
5656 paragraph.origin[1] + second_line.top + second_line.height * 0.5,
5657 SelectionUnit::Grapheme,
5658 )
5659 .unwrap();
5660 let down = geometry
5661 .navigate(source_selection, Movement::Down, false, None)
5662 .unwrap();
5663 assert_eq!(down.selection, hit);
5664 assert_eq!(down.selection.positions[1].paragraph, 0);
5665 assert_eq!(down.preferred_x, Some(caret.x0 as f32));
5666 assert!(!down.boundary);
5667 let extending = Selection {
5668 positions: [first, down.selection.positions[1]],
5669 affinities: [Affinity::Downstream, down.selection.affinities[1]],
5670 };
5671 let next = geometry
5672 .navigate(extending, Movement::Down, true, down.preferred_x)
5673 .unwrap();
5674 assert_eq!(next.selection.positions[0], first);
5675 assert_eq!(next.selection.affinities[0], Affinity::Downstream);
5676 assert_eq!(next.preferred_x, down.preferred_x);
5677 let next_caret = geometry
5678 .caret_at(
5679 next.selection.positions[1],
5680 next.selection.affinities[1],
5681 1.0,
5682 )
5683 .unwrap();
5684 let (_, third_line) = paragraph.text.lines().nth(2).unwrap();
5685 assert!(
5686 (next_caret.y0 - f64::from(paragraph.origin[1] + third_line.top)).abs() < 0.001
5687 );
5688 for movement in [Movement::LineStart, Movement::LineEnd] {
5689 let line = geometry
5690 .navigate(down.selection, movement, false, down.preferred_x)
5691 .unwrap();
5692 let line_caret = geometry
5693 .caret_at(
5694 line.selection.positions[1],
5695 line.selection.affinities[1],
5696 1.0,
5697 )
5698 .unwrap();
5699 assert!(
5700 (line_caret.y0 - f64::from(paragraph.origin[1] + second_line.top)).abs()
5701 < 0.001
5702 );
5703 assert_eq!(line.preferred_x, None);
5704 assert!(!line.boundary);
5705 }
5706 let last = TextPosition {
5707 paragraph: 3,
5708 offset: 4,
5709 };
5710 let reversed: Selection = [last, first].into();
5711 for (movement, expected) in [
5712 (Movement::Left, first),
5713 (Movement::WordLeft, first),
5714 (Movement::Right, last),
5715 (Movement::WordRight, last),
5716 ] {
5717 let collapsed = geometry
5718 .navigate(reversed, movement, false, Some(15.0))
5719 .unwrap();
5720 assert_eq!(collapsed.selection.positions, [expected; 2]);
5721 assert_eq!(collapsed.preferred_x, None);
5722 assert!(!collapsed.boundary);
5723 }
5724 for movement in [
5725 Movement::Left,
5726 Movement::Right,
5727 Movement::WordLeft,
5728 Movement::WordRight,
5729 Movement::ParagraphStart,
5730 Movement::ParagraphEnd,
5731 Movement::DocumentStart,
5732 Movement::DocumentEnd,
5733 ] {
5734 let moved = geometry.navigate(reversed, movement, true, None).unwrap();
5735 assert_eq!(moved.selection.positions[0], last);
5736 assert_eq!(moved.selection.affinities[0], reversed.affinities[0]);
5737 for position in moved.selection.positions {
5738 geometry
5739 .source
5740 .document
5741 .paragraph(position.paragraph)
5742 .unwrap()
5743 .byte_offset(position.offset)
5744 .unwrap();
5745 }
5746 }
5747 assert_eq!(editor.page().unwrap(), original);
5748 assert_eq!(layout_snapshot(&editor.active_outline().shaped), canonical);
5749 assert_eq!(editor.selection(), source_selection);
5750 assert_eq!(editor.history_depth(), [0, 0]);
5751 assert!(editor.take_ops().unwrap().is_empty());
5752 }
5753 editor
5754 .compose(&mut engine, "marked 😀\npreview".into(), 0..0)
5755 .unwrap();
5756 let marked = editor.marked_range();
5757 let preview = editor.page().unwrap();
5758 let selection = editor.selection();
5759 for width in [70.0, 230.0] {
5760 let shaped = alternate_shape(&mut engine, &editor, width);
5761 let geometry = OutlineGeometry {
5762 source: editor.active_outline(),
5763 shaped: &shaped,
5764 };
5765 for movement in [
5766 Movement::Up,
5767 Movement::Down,
5768 Movement::LineStart,
5769 Movement::LineEnd,
5770 ] {
5771 geometry.navigate(selection, movement, true, None).unwrap();
5772 }
5773 assert_eq!(editor.marked_range(), marked);
5774 assert_eq!(editor.page().unwrap(), preview);
5775 assert_eq!(editor.selection(), selection);
5776 assert_eq!(editor.history_depth(), [0, 0]);
5777 assert!(editor.take_ops().unwrap().is_empty());
5778 }
5779 editor.cancel_composition(&mut engine).unwrap();
5780 assert_eq!(editor.page().unwrap(), original);
5781 }
5782
5783 #[test]
5784 fn alternate_navigation_boundaries_do_not_create_spatial_content() {
5785 let mut engine = TextEngine::default();
5786 for title in [false, true] {
5787 for text in ["", "alpha 😀\nlast"] {
5788 let mut outline = TextOutline::new(
5789 &mut engine,
5790 TextDocument::new(
5791 text.split('\n')
5792 .map(|part| Paragraph::new(part.into(), Format::default()))
5793 .collect(),
5794 )
5795 .unwrap(),
5796 300.0,
5797 [220.0, 180.0],
5798 )
5799 .unwrap();
5800 outline.title = title;
5801 let mut editor =
5802 CanvasEditor::from_text_outlines(vec![outline], BTreeMap::new(), None).unwrap();
5803 let original = editor.page().unwrap();
5804 let source = editor.active_outline().snapshot();
5805 let canonical = layout_snapshot(&editor.active_outline().shaped);
5806 for width in [70.0, 230.0] {
5807 let shaped = alternate_shape(&mut engine, &editor, width);
5808 let geometry = OutlineGeometry {
5809 source: editor.active_outline(),
5810 shaped: &shaped,
5811 };
5812 let start = TextPosition {
5813 paragraph: 0,
5814 offset: 0,
5815 };
5816 let end = geometry
5817 .navigate([start; 2].into(), Movement::DocumentEnd, false, None)
5818 .unwrap()
5819 .selection;
5820 for (movement, selection) in
5821 [(Movement::Up, [start; 2].into()), (Movement::Down, end)]
5822 {
5823 for extend in [false, true] {
5824 let edge = geometry
5825 .navigate(selection, movement, extend, Some(11.0))
5826 .unwrap();
5827 assert!(edge.boundary);
5828 assert_eq!(edge.selection.positions, selection.positions);
5829 let focus = selection.positions[1];
5830 let affinity = geometry
5831 .paragraph_layout(focus.paragraph)
5832 .unwrap()
5833 .cursor(focus.offset, selection.affinities[1])
5834 .unwrap()
5835 .affinity();
5836 assert_eq!(edge.selection.affinities[1], affinity);
5837 assert_eq!(
5838 edge.selection.affinities[0],
5839 if extend {
5840 selection.affinities[0]
5841 } else {
5842 affinity
5843 }
5844 );
5845 assert_eq!(edge.preferred_x, Some(11.0));
5846 }
5847 }
5848 assert_eq!(
5849 geometry
5850 .navigate(
5851 [start; 2].into(),
5852 Movement::DocumentStart,
5853 false,
5854 Some(11.0)
5855 )
5856 .unwrap()
5857 .selection
5858 .positions,
5859 [start; 2]
5860 );
5861 assert_eq!(editor.active_outline().snapshot(), source);
5862 assert_eq!(editor.page().unwrap(), original);
5863 assert_eq!(layout_snapshot(&editor.active_outline().shaped), canonical);
5864 assert_eq!(editor.history_depth(), [0, 0]);
5865 assert!(editor.take_ops().unwrap().is_empty());
5866 }
5867 }
5868 }
5869 }
5870
5871 fn table_editor(engine: &mut TextEngine) -> CanvasEditor {
5872 use onestore::page::text::new_id;
5873 use onestore::page::{ParagraphContent, Table, TableCell, TableColumn, TableRow};
5874 let mut editor = CanvasEditor::new(
5875 engine,
5876 TextDocument::new(vec![Paragraph::new("Left".into(), Format::default())]).unwrap(),
5877 180.0,
5878 )
5879 .unwrap();
5880 let first = editor.active_outline().document.nodes()[0].clone();
5881 let wrapper = PageParagraph {
5882 id: new_id().unwrap(),
5883 parent: None,
5884 level: 1,
5885 style: None,
5886 format: Format::default(),
5887 lists: Vec::new(),
5888 tags: Vec::new(),
5889 media: Default::default(),
5890 collapsed: false,
5891 content: ParagraphContent::Table(Table {
5892 id: new_id().unwrap(),
5893 columns: vec![
5894 TableColumn {
5895 width: 72.0,
5896 locked: true
5897 };
5898 2
5899 ],
5900 rows: [["Left", "Right"], ["Below", "Last"]]
5901 .into_iter()
5902 .enumerate()
5903 .map(|(row, texts)| TableRow {
5904 id: new_id().unwrap(),
5905 cells: texts
5906 .into_iter()
5907 .enumerate()
5908 .map(|(column, text)| TableCell {
5909 id: new_id().unwrap(),
5910 layout: Default::default(),
5911 indents: vec![18.0, 0.0, 27.0],
5912 shading: None,
5913 paragraphs: vec![if row == 0 && column == 0 {
5914 first.clone()
5915 } else {
5916 crate::document::node(
5917 Paragraph::new(text.into(), Format::default()),
5918 Format::default(),
5919 )
5920 .unwrap()
5921 }],
5922 unsupported: Vec::new(),
5923 })
5924 .collect(),
5925 })
5926 .collect(),
5927 borders: Some(true),
5928 layout: Default::default(),
5929 tags: Vec::new(),
5930 }),
5931 };
5932 editor
5933 .commit(
5934 engine,
5935 DocumentEdit {
5936 columns: BTreeMap::new(),
5937 container: None,
5938 range: 0..1,
5939 replacement: vec![wrapper],
5940 },
5941 [TextPosition {
5942 paragraph: 1,
5943 offset: 0,
5944 }; 2]
5945 .into(),
5946 )
5947 .unwrap();
5948 editor
5949 }
5950
5951 #[test]
5952 fn structured_construction_and_resize_preserve_cells_and_enclose_locked_columns() {
5953 let mut engine = TextEngine::default();
5954 let mut source = table_editor(&mut engine).active_outline().snapshot();
5955 let onestore::page::ParagraphContent::Table(table) = &mut source.paragraphs[0].content
5956 else {
5957 panic!()
5958 };
5959 table.rows[0].cells[0].paragraphs[0]
5960 .text_mut()
5961 .unwrap()
5962 .text = Paragraph::new(String::new(), Format::default());
5963 source.paragraphs.push(
5964 crate::document::node(
5965 Paragraph::new(
5966 "Text below the table must use the same width during drag and after release."
5967 .into(),
5968 Format::default(),
5969 ),
5970 Format::default(),
5971 )
5972 .unwrap(),
5973 );
5974 let document = TextDocument::from_nodes(source.paragraphs.clone()).unwrap();
5975 let mut editor = CanvasEditor::new(&mut engine, document.clone(), 180.0).unwrap();
5976 let imported = TextOutline::from_outline(&mut engine, &source, &BTreeMap::new()).unwrap();
5977 let cells = |outline: &TextOutline| {
5978 outline
5979 .shaped
5980 .tables
5981 .iter()
5982 .flat_map(|table| {
5983 table
5984 .cells
5985 .iter()
5986 .map(move |cell| (table.id, table.borders, cell.id, cell.rect))
5987 })
5988 .collect::<Vec<_>>()
5989 };
5990 assert_eq!(editor.active_outline().layout.width_set_by_user, Some(true));
5991 assert_eq!(editor.active_outline().shaped.size, imported.shaped.size);
5992 assert_eq!(cells(editor.active_outline()), cells(&imported));
5993 for (actual, expected) in editor
5994 .active_outline()
5995 .shaped
5996 .paragraphs
5997 .iter()
5998 .zip(&imported.shaped.paragraphs)
5999 {
6000 assert_eq!(actual.id, expected.id);
6001 assert_eq!(actual.origin, expected.origin);
6002 assert_eq!(actual.text.height(), expected.text.height());
6003 }
6004 let minimum = 2.0 * (72.0 + 4.98) - 1.83;
6005 let preview = editor.preview_resize(&mut engine, 36.0).unwrap();
6006 assert!((preview.bounds().width() as f32 - minimum).abs() < 0.0001);
6007 assert_eq!(preview.document, document);
6008 assert_eq!(cells(&preview), cells(&imported));
6009 assert!((preview.wrap_width() - minimum).abs() < 0.0001);
6010 let final_width_preview = editor
6011 .preview_resize(&mut engine, preview.bounds().width() as f32)
6012 .unwrap();
6013 assert_eq!(preview.shaped.size, final_width_preview.shaped.size);
6014 assert_eq!(
6015 preview
6016 .shaped
6017 .paragraphs
6018 .last()
6019 .unwrap()
6020 .text
6021 .lines()
6022 .count(),
6023 final_width_preview
6024 .shaped
6025 .paragraphs
6026 .last()
6027 .unwrap()
6028 .text
6029 .lines()
6030 .count()
6031 );
6032 assert_eq!(editor.active_outline().bounds().width(), 180.0);
6033 assert!(editor.undo.is_empty());
6034 editor.resize(&mut engine, 36.0).unwrap();
6035 assert_eq!(editor.active_outline().bounds(), preview.bounds());
6036 editor.undo(&mut engine).unwrap();
6037 assert_eq!(editor.active_outline().bounds().width(), 180.0);
6038 editor.redo(&mut engine).unwrap();
6039 assert_eq!(editor.active_outline().bounds(), preview.bounds());
6040 assert_eq!(editor.active_outline().document, document);
6041 }
6042
6043 #[test]
6044 fn nested_table_extents_remain_reachable_beyond_the_parent_columns() {
6045 use onestore::page::ParagraphContent;
6046 let mut engine = TextEngine::default();
6047 let mut source = table_editor(&mut engine).active_outline().snapshot();
6048 let mut nested = table_editor(&mut engine).active_outline().document.nodes()[0].clone();
6049 let ParagraphContent::Table(table) = &mut nested.content else {
6050 panic!()
6051 };
6052 table.columns[0].width = 240.0;
6053 table.columns[1].width = 240.0;
6054 let ParagraphContent::Table(table) = &mut source.paragraphs[0].content else {
6055 panic!()
6056 };
6057 table.rows[0].cells[1].paragraphs = vec![nested];
6058 source.layout.width_set_by_user = Some(false);
6059 let mut editor =
6060 CanvasEditor::from_outlines(&mut engine, vec![source], BTreeMap::new()).unwrap();
6061 let paragraph = editor.active_outline().paragraph_layout(2).unwrap();
6062 let point = [
6063 paragraph.origin[0] + 1.0,
6064 paragraph.origin[1] + paragraph.text.height() * 0.5,
6065 ];
6066 assert!(point[0] > 180.0);
6067 assert!(f64::from(point[0]) < editor.active_outline().bounds().x1);
6068 assert_eq!(
6069 editor
6070 .selection_at(point[0], point[1], SelectionUnit::Grapheme)
6071 .unwrap()
6072 .positions[1]
6073 .paragraph,
6074 2
6075 );
6076 let preview = editor.preview_resize(&mut engine, 36.0).unwrap();
6077 assert!(preview.bounds().x1 > f64::from(point[0]));
6078 editor
6079 .select(
6080 [TextPosition {
6081 paragraph: 2,
6082 offset: 0,
6083 }; 2]
6084 .into(),
6085 )
6086 .unwrap();
6087 editor.insert(&mut engine, "!").unwrap();
6088 assert!(editor.active_outline().bounds().x1 > f64::from(point[0]));
6089 editor.undo(&mut engine).unwrap();
6090 assert!(editor.active_outline().bounds().x1 > f64::from(point[0]));
6091 }
6092
6093 #[test]
6094 fn automatic_table_bounds_add_typing_room_only_to_root_text() {
6095 let mut engine = TextEngine::default();
6096 let mut source = table_editor(&mut engine).active_outline().snapshot();
6097 source.layout.width_set_by_user = Some(false);
6098 let outline = TextOutline::from_outline(&mut engine, &source, &BTreeMap::new()).unwrap();
6099 assert_eq!(outline.bounds().width(), f64::from(outline.shaped.size[0]));
6100 source.paragraphs.push(
6101 crate::document::node(
6102 Paragraph::new(
6103 "A root paragraph needs room to continue typing".into(),
6104 Format::default(),
6105 ),
6106 Format::default(),
6107 )
6108 .unwrap(),
6109 );
6110 let outline = TextOutline::from_outline(&mut engine, &source, &BTreeMap::new()).unwrap();
6111 let paragraph = outline.shaped.paragraphs.last().unwrap();
6112 let expected = (paragraph.origin[0] + paragraph.text.shaped.width() + 36.0)
6113 .clamp(72.0, 180.0)
6114 .max(outline.shaped.size[0]);
6115 assert_eq!(outline.bounds().width(), f64::from(expected));
6116 assert!(outline.bounds().width() > f64::from(outline.shaped.size[0]));
6117 }
6118
6119 #[test]
6120 fn cell_mouse_targets_and_paragraph_selection_follow_columns() {
6121 let mut engine = TextEngine::default();
6122 let mut editor = table_editor(&mut engine);
6123 editor
6124 .select(
6125 [TextPosition {
6126 paragraph: 0,
6127 offset: 4,
6128 }; 2]
6129 .into(),
6130 )
6131 .unwrap();
6132 editor.insert(&mut engine, "\nSecond\nThird").unwrap();
6133 for source in 0..6 {
6134 let paragraph = editor.active_outline().paragraph_layout(source).unwrap();
6135 let (_, line) = paragraph.text.lines().next().unwrap();
6136 let x = paragraph.origin[0] + 1.0;
6137 let y = paragraph.origin[1] + line.top + line.height * 0.5;
6138 for unit in [
6139 SelectionUnit::Grapheme,
6140 SelectionUnit::Word,
6141 SelectionUnit::Paragraph,
6142 ] {
6143 let selection = editor.selection_at(x, y, unit).unwrap();
6144 assert_eq!(selection.positions[0].paragraph, source);
6145 if unit == SelectionUnit::Paragraph {
6146 let end = if source < 2 {
6147 TextPosition {
6148 paragraph: source + 1,
6149 offset: 0,
6150 }
6151 } else {
6152 let text = editor
6153 .active_outline()
6154 .document
6155 .paragraphs()
6156 .nth(source)
6157 .unwrap();
6158 TextPosition {
6159 paragraph: source,
6160 offset: text.utf16_offset(text.text().len()).unwrap(),
6161 }
6162 };
6163 assert_eq!(selection.positions[1], end);
6164 }
6165 }
6166 }
6167 let right = editor.active_outline().shaped.tables[0].cells[1].rect;
6168 for x in [right[0] + 0.5, right[2] - 0.5, f32::MAX] {
6169 let selection = editor
6170 .selection_at(x, right[3] - 1.0, SelectionUnit::Grapheme)
6171 .unwrap();
6172 assert_eq!(selection.positions[1].paragraph, 3);
6173 }
6174 let left = editor.active_outline().shaped.tables[0].cells[0].rect;
6175 let selection = editor
6176 .selection_at(-f32::MAX, left[3] - 1.0, SelectionUnit::Grapheme)
6177 .unwrap();
6178 assert_eq!(selection.positions[1].paragraph, 2);
6179 let history = editor.undo.len();
6180 let before = editor.active_outline().document.clone();
6181 editor
6182 .select_at(right[0] + 5.0, right[1] + 8.0, false)
6183 .unwrap();
6184 editor
6185 .select_at(left[0] + 5.0, left[3] - 2.0, true)
6186 .unwrap();
6187 assert_eq!(editor.selection().positions[0].paragraph, 3);
6188 assert_eq!(editor.selection().positions[1].paragraph, 2);
6189 assert!(!editor.selection_rects().unwrap().is_empty());
6190 assert_eq!(editor.active_outline().document, before);
6191 assert_eq!(editor.undo.len(), history);
6192 }
6193
6194 #[test]
6195 fn nested_table_mouse_targets_resolve_the_source_cell() {
6196 use onestore::page::ParagraphContent;
6197 use onestore::page::text::new_id;
6198 let mut engine = TextEngine::default();
6199 let nested = table_editor(&mut engine).active_outline().document.nodes()[0].clone();
6200 let mut editor = table_editor(&mut engine);
6201 let mut wrapper = editor.active_outline().document.nodes()[0].clone();
6202 let ParagraphContent::Table(table) = &mut wrapper.content else {
6203 panic!()
6204 };
6205 table.columns[0].width = 180.0;
6206 let cell = &mut table.rows[0].cells[0];
6207 cell.paragraphs.push(nested);
6208 let mut tail = cell.paragraphs[0].clone();
6209 tail.id = new_id().unwrap();
6210 tail.text_mut().unwrap().id = new_id().unwrap();
6211 cell.paragraphs.push(tail);
6212 let before = crate::document::node(
6213 Paragraph::new("Before".into(), Format::default()),
6214 Format::default(),
6215 )
6216 .unwrap();
6217 let after = crate::document::node(
6218 Paragraph::new("After".into(), Format::default()),
6219 Format::default(),
6220 )
6221 .unwrap();
6222 editor
6223 .commit(
6224 &mut engine,
6225 DocumentEdit {
6226 columns: BTreeMap::new(),
6227 container: None,
6228 range: 0..1,
6229 replacement: vec![before, wrapper, after],
6230 },
6231 [TextPosition {
6232 paragraph: 0,
6233 offset: 0,
6234 }; 2]
6235 .into(),
6236 )
6237 .unwrap();
6238 assert_eq!(editor.active_outline().document.paragraphs().count(), 11);
6239 for source in 0..11 {
6240 let paragraph = editor.active_outline().paragraph_layout(source).unwrap();
6241 let (_, line) = paragraph.text.lines().next().unwrap();
6242 let selection = editor
6243 .selection_at(
6244 paragraph.origin[0] + 1.0,
6245 paragraph.origin[1] + line.height * 0.5,
6246 SelectionUnit::Grapheme,
6247 )
6248 .unwrap();
6249 assert_eq!(selection.positions[0].paragraph, source);
6250 }
6251 let paragraph = editor.active_outline().paragraph_layout(1).unwrap();
6252 let selection = editor
6253 .selection_at(
6254 paragraph.origin[0] + 1.0,
6255 paragraph.origin[1] + 5.0,
6256 SelectionUnit::Paragraph,
6257 )
6258 .unwrap();
6259 assert_eq!(
6260 selection.positions[1],
6261 TextPosition {
6262 paragraph: 1,
6263 offset: 4
6264 }
6265 );
6266 let original = editor.active_outline().document.clone();
6267 editor
6268 .select(
6269 [TextPosition {
6270 paragraph: 10,
6271 offset: 2,
6272 }; 2]
6273 .into(),
6274 )
6275 .unwrap();
6276 assert!(editor.indent(&mut engine, false).unwrap());
6277 assert_eq!(editor.active_outline().document.nodes()[2].level, 2);
6278 editor.undo(&mut engine).unwrap();
6279 assert_eq!(editor.active_outline().document, original);
6280 for (position, backward) in [
6281 (
6282 TextPosition {
6283 paragraph: 0,
6284 offset: 6,
6285 },
6286 false,
6287 ),
6288 (
6289 TextPosition {
6290 paragraph: 10,
6291 offset: 0,
6292 },
6293 true,
6294 ),
6295 ] {
6296 editor.select([position; 2].into()).unwrap();
6297 assert!(!editor.delete(&mut engine, backward).unwrap());
6298 assert_eq!(editor.active_outline().document, original);
6299 }
6300 }
6301
6302 #[test]
6303 fn cell_delete_boundaries_and_local_indentation_preserve_neighbors() {
6304 let mut engine = TextEngine::default();
6305 let mut editor = table_editor(&mut engine);
6306 let original = editor.active_outline().document.clone();
6307 let history = editor.undo.len();
6308 for paragraph in 0..4 {
6309 let text = original.paragraphs().nth(paragraph).unwrap();
6310 for (backward, offset) in [
6311 (true, 0),
6312 (false, text.utf16_offset(text.text().len()).unwrap()),
6313 ] {
6314 let selection = [TextPosition { paragraph, offset }; 2].into();
6315 editor.select(selection).unwrap();
6316 assert!(!editor.delete(&mut engine, backward).unwrap());
6317 assert_eq!(editor.active_outline().document, original);
6318 assert_eq!(editor.selection(), selection);
6319 assert_eq!(editor.undo.len(), history);
6320 }
6321 }
6322 editor
6323 .select(
6324 [TextPosition {
6325 paragraph: 2,
6326 offset: 2,
6327 }; 2]
6328 .into(),
6329 )
6330 .unwrap();
6331 editor.insert(&mut engine, "\n").unwrap();
6332 let split = editor.active_outline().document.clone();
6333 for backward in [true, false] {
6334 let position = if backward {
6335 TextPosition {
6336 paragraph: 3,
6337 offset: 0,
6338 }
6339 } else {
6340 TextPosition {
6341 paragraph: 2,
6342 offset: 2,
6343 }
6344 };
6345 let selection = [position; 2].into();
6346 editor.select(selection).unwrap();
6347 assert!(editor.delete(&mut engine, backward).unwrap());
6348 assert_eq!(editor.active_outline().document, original);
6349 editor.undo(&mut engine).unwrap();
6350 assert_eq!(editor.active_outline().document, split);
6351 assert_eq!(editor.selection(), selection);
6352 }
6353 let selection = [
6354 TextPosition {
6355 paragraph: 2,
6356 offset: 1,
6357 },
6358 TextPosition {
6359 paragraph: 3,
6360 offset: 0,
6361 },
6362 ]
6363 .into();
6364 editor.select(selection).unwrap();
6365 assert!(editor.indent(&mut engine, false).unwrap());
6366 assert_eq!(
6367 editor
6368 .active_outline()
6369 .document
6370 .text_nodes()
6371 .map(|node| node.level)
6372 .collect::<Vec<_>>(),
6373 [1, 1, 2, 1, 1]
6374 );
6375 assert_eq!(editor.selection(), selection);
6376 editor.undo(&mut engine).unwrap();
6377 assert_eq!(editor.active_outline().document, split);
6378 editor.redo(&mut engine).unwrap();
6379 assert!(editor.indent(&mut engine, true).unwrap());
6380 assert_eq!(editor.active_outline().document, split);
6381 }
6382
6383 #[test]
6384 fn overflowing_cell_caret_moves_to_the_next_rows_visual_column() {
6385 use onestore::page::ParagraphContent;
6386 let mut engine = TextEngine::default();
6387 let mut source = table_editor(&mut engine).active_outline().snapshot();
6388 let ParagraphContent::Table(table) = &mut source.paragraphs[0].content else {
6389 panic!()
6390 };
6391 table.rows[0].cells[0].paragraphs[0]
6392 .text_mut()
6393 .unwrap()
6394 .text = Paragraph::new(
6395 "M".into(),
6396 Format {
6397 font_size: Some(130.0),
6398 ..Default::default()
6399 },
6400 );
6401 let mut editor =
6402 CanvasEditor::from_outlines(&mut engine, vec![source], BTreeMap::new()).unwrap();
6403 let original = editor.active_outline().document.clone();
6404 editor
6405 .select(
6406 [
6407 TextPosition {
6408 paragraph: 0,
6409 offset: 0,
6410 },
6411 TextPosition {
6412 paragraph: 0,
6413 offset: 1,
6414 },
6415 ]
6416 .into(),
6417 )
6418 .unwrap();
6419 let [_, _, right, _] = editor
6420 .active_outline()
6421 .shaped
6422 .paragraph_cell(0)
6423 .unwrap()
6424 .text_bounds();
6425 let selection = editor.selection_rects().unwrap();
6426 assert!(!selection.is_empty());
6427 assert!(selection.iter().all(|rect| rect.x1 <= f64::from(right)));
6428 for extend in [false, true] {
6429 editor
6430 .select(
6431 [TextPosition {
6432 paragraph: 0,
6433 offset: 1,
6434 }; 2]
6435 .into(),
6436 )
6437 .unwrap();
6438 let caret = editor.caret(1.0).unwrap();
6439 assert!(
6440 caret.x0 > f64::from(editor.active_outline().shaped.tables[0].cells[0].rect[2])
6441 );
6442 editor
6443 .move_selection(&mut engine, Movement::Down, extend)
6444 .unwrap();
6445 assert_eq!(editor.selection().positions[1].paragraph, 3);
6446 editor
6447 .move_selection(&mut engine, Movement::Up, extend)
6448 .unwrap();
6449 assert_eq!(
6450 editor.selection().positions[1],
6451 TextPosition {
6452 paragraph: 1,
6453 offset: 5
6454 }
6455 );
6456 assert_eq!(editor.active_outline().document, original);
6457 assert!(editor.undo.is_empty());
6458 }
6459 }
6460
6461 #[test]
6462 fn table_vertical_arrows_preserve_columns_and_cell_paragraphs() {
6463 let mut engine = TextEngine::default();
6464 let mut editor = table_editor(&mut engine);
6465 let original = editor.active_outline().document.clone();
6466 for column in 0..2 {
6467 editor
6468 .select(
6469 [TextPosition {
6470 paragraph: column,
6471 offset: 2,
6472 }; 2]
6473 .into(),
6474 )
6475 .unwrap();
6476 editor
6477 .move_selection(&mut engine, Movement::Down, false)
6478 .unwrap();
6479 assert_eq!(editor.selection().positions[1].paragraph, column + 2);
6480 editor
6481 .move_selection(&mut engine, Movement::Up, false)
6482 .unwrap();
6483 assert_eq!(
6484 editor.selection().positions[1],
6485 TextPosition {
6486 paragraph: column,
6487 offset: 2
6488 }
6489 );
6490 }
6491 editor
6492 .select(
6493 [TextPosition {
6494 paragraph: 0,
6495 offset: 2,
6496 }; 2]
6497 .into(),
6498 )
6499 .unwrap();
6500 editor.insert(&mut engine, "\n").unwrap();
6501 editor
6502 .select(
6503 [TextPosition {
6504 paragraph: 0,
6505 offset: 1,
6506 }; 2]
6507 .into(),
6508 )
6509 .unwrap();
6510 editor
6511 .move_selection(&mut engine, Movement::Down, false)
6512 .unwrap();
6513 assert_eq!(editor.selection().positions[1].paragraph, 1);
6514 editor
6515 .move_selection(&mut engine, Movement::Down, false)
6516 .unwrap();
6517 assert_eq!(editor.selection().positions[1].paragraph, 3);
6518 editor
6519 .select(
6520 [TextPosition {
6521 paragraph: 2,
6522 offset: 2,
6523 }; 2]
6524 .into(),
6525 )
6526 .unwrap();
6527 editor
6528 .move_selection(&mut engine, Movement::Down, false)
6529 .unwrap();
6530 assert_eq!(editor.selection().positions[1].paragraph, 4);
6531 editor
6532 .move_selection(&mut engine, Movement::Up, false)
6533 .unwrap();
6534 assert_eq!(
6535 editor.selection().positions[1],
6536 TextPosition {
6537 paragraph: 2,
6538 offset: 2
6539 }
6540 );
6541 editor.undo(&mut engine).unwrap();
6542 assert_eq!(editor.active_outline().document, original);
6543 }
6544
6545 #[test]
6546 fn table_edges_create_provisional_space_and_restore_it_with_history() {
6547 let mut engine = TextEngine::default();
6548 let mut editor = table_editor(&mut engine);
6549 editor.active_outline_mut().layout.x = Some(120.0);
6550 editor.active_outline_mut().layout.y = Some(120.0);
6551 let source = editor.active_outline().document.clone();
6552 editor
6553 .select(
6554 [TextPosition {
6555 paragraph: 3,
6556 offset: 4,
6557 }; 2]
6558 .into(),
6559 )
6560 .unwrap();
6561 let history = editor.undo.len();
6562 editor
6563 .move_selection(&mut engine, Movement::Down, false)
6564 .unwrap();
6565 assert_eq!(editor.outlines()[0].document, source);
6566 assert_eq!(editor.undo.len(), history);
6567 assert_eq!(
6568 editor.selection().positions,
6569 [TextPosition {
6570 paragraph: 4,
6571 offset: 0
6572 }; 2]
6573 );
6574 let draft = editor.active_outline().document.clone();
6575 let caret = editor.selection();
6576 editor.insert(&mut engine, "X").unwrap();
6577 let committed = editor.active_outline().document.clone();
6578 assert_eq!(committed.nodes().len(), 2);
6579 assert_eq!(committed.paragraphs().last().unwrap().text(), "X");
6580 for _ in 0..2 {
6581 editor.undo(&mut engine).unwrap();
6582 assert_eq!(editor.outlines()[0].document, source);
6583 assert_eq!(editor.active_outline().document, draft);
6584 assert_eq!(editor.selection(), caret);
6585 editor.redo(&mut engine).unwrap();
6586 assert_eq!(editor.active_outline().document, committed);
6587 }
6588 editor.undo(&mut engine).unwrap();
6589 editor
6590 .select(
6591 [TextPosition {
6592 paragraph: 0,
6593 offset: 0,
6594 }; 2]
6595 .into(),
6596 )
6597 .unwrap();
6598 editor
6599 .move_selection(&mut engine, Movement::Up, false)
6600 .unwrap();
6601 assert_eq!(editor.caret_outline().unwrap().origin(), [120.0, 86.4]);
6602 assert_eq!(editor.outlines()[0].document, source);
6603 editor.insert(&mut engine, "X").unwrap();
6604 assert_eq!(editor.outlines().len(), 2);
6605 editor.undo(&mut engine).unwrap();
6606 assert!(editor.caret_outline().is_some());
6607 assert_eq!(editor.outlines().len(), 1);
6608 assert_eq!(editor.outlines()[0].document, source);
6609 }
6610
6611 #[test]
6612 fn editing_a_cell_trims_only_provisional_root_paragraphs() {
6613 let mut engine = TextEngine::default();
6614 for compose in [false, true] {
6615 let mut editor = table_editor(&mut engine);
6616 let source = editor.active_outline().document.clone();
6617 editor
6618 .select(
6619 [TextPosition {
6620 paragraph: 3,
6621 offset: 4,
6622 }; 2]
6623 .into(),
6624 )
6625 .unwrap();
6626 for _ in 0..2 {
6627 editor
6628 .move_selection(&mut engine, Movement::Down, false)
6629 .unwrap();
6630 }
6631 for _ in 0..2 {
6632 editor
6633 .move_selection(&mut engine, Movement::Up, false)
6634 .unwrap();
6635 }
6636 assert_eq!(editor.selection().positions[1].paragraph, 3);
6637 let draft = editor.active_outline().document.clone();
6638 assert_eq!(draft.nodes().len(), 3);
6639 let selection = editor.selection();
6640 if compose {
6641 editor
6642 .compose(&mut engine, "temporary".into(), 9..9)
6643 .unwrap();
6644 editor.cancel_composition(&mut engine).unwrap();
6645 assert_eq!(editor.active_outline().document, draft);
6646 editor.compose(&mut engine, "marked".into(), 6..6).unwrap();
6647 editor.commit_text(&mut engine, "X".into()).unwrap();
6648 } else {
6649 editor.insert(&mut engine, "X").unwrap();
6650 }
6651 let after = editor.active_outline().document.clone();
6652 assert_eq!(after.nodes().len(), 1);
6653 assert_eq!(after.paragraphs().nth(3).unwrap().text(), "LastX");
6654 let fresh = editor
6655 .active_outline()
6656 .snapshot()
6657 .layout(&mut engine, &BTreeMap::new())
6658 .unwrap();
6659 assert_eq!(editor.active_outline().shaped.size, fresh.size);
6660 assert_eq!(editor.active_outline().shaped.paragraphs.len(), 4);
6661 editor.undo(&mut engine).unwrap();
6662 assert_eq!(editor.outlines()[0].document, source);
6663 assert_eq!(editor.active_outline().document, draft);
6664 assert_eq!(editor.selection(), selection);
6665 editor.redo(&mut engine).unwrap();
6666 assert_eq!(editor.active_outline().document, after);
6667 }
6668 }
6669
6670 #[test]
6671 fn editing_during_cell_composition_keeps_table_flow_and_history() {
6672 let mut engine = TextEngine::default();
6673 let mut editor = table_editor(&mut engine);
6674 let source = editor.active_outline().document.clone();
6675 editor
6676 .select(
6677 [TextPosition {
6678 paragraph: 3,
6679 offset: 4,
6680 }; 2]
6681 .into(),
6682 )
6683 .unwrap();
6684 for _ in 0..2 {
6685 editor
6686 .move_selection(&mut engine, Movement::Down, false)
6687 .unwrap();
6688 }
6689 for _ in 0..2 {
6690 editor
6691 .move_selection(&mut engine, Movement::Up, false)
6692 .unwrap();
6693 }
6694 let draft = editor.active_outline().document.clone();
6695 editor.compose(&mut engine, "a".into(), 1..1).unwrap();
6696 editor.insert(&mut engine, "b").unwrap();
6697 assert_eq!(
6698 editor
6699 .active_outline()
6700 .document
6701 .paragraphs()
6702 .last()
6703 .unwrap()
6704 .text(),
6705 "Lastab"
6706 );
6707 let fresh = editor
6708 .active_outline()
6709 .snapshot()
6710 .layout(&mut engine, &BTreeMap::new())
6711 .unwrap();
6712 assert_eq!(editor.active_outline().shaped.size, fresh.size);
6713 assert_eq!(
6714 editor.active_outline().shaped.paragraphs.len(),
6715 fresh.paragraphs.len()
6716 );
6717 // The committed composition and the typing after it are one run.
6718 editor.undo(&mut engine).unwrap();
6719 assert_eq!(editor.outlines()[0].document, source);
6720 assert_eq!(editor.active_outline().document, draft);
6721 }
6722
6723 /// OneNote 2010's undo steps (Windows 7 lab, 2026-09-29): typing, backspaces and word
6724 /// backspaces within a paragraph are one step however long the pauses; a caret move, Enter,
6725 /// a backspace joining paragraphs, Delete, paste and a format chosen at the caret each end it.
6726 #[test]
6727 fn typing_runs_undo_whole_until_the_caret_moves_or_another_edit() {
6728 let mut engine = TextEngine::default();
6729 let source =
6730 TextDocument::new(vec![Paragraph::new("Base".into(), Format::default())]).unwrap();
6731 let mut editor = CanvasEditor::new(&mut engine, source, 240.0).unwrap();
6732 let at = |paragraph, offset| [TextPosition { paragraph, offset }; 2].into();
6733 let text = |editor: &CanvasEditor| editor.active_outline().shown_text();
6734 editor.select(at(0, 4)).unwrap();
6735 editor.insert(&mut engine, " quick").unwrap();
6736 editor.insert(&mut engine, " brown").unwrap();
6737 editor.delete(&mut engine, true).unwrap();
6738 editor.delete_to(&mut engine, Movement::WordLeft).unwrap();
6739 editor.commit_text(&mut engine, "fox".into()).unwrap();
6740 editor.select(editor.selection()).unwrap();
6741 editor.insert(&mut engine, " jumps").unwrap();
6742 assert_eq!(text(&editor), "Base quick fox jumps");
6743 assert!(editor.undo(&mut engine).unwrap());
6744 assert_eq!(text(&editor), "Base");
6745 assert_eq!(editor.selection().positions, [at(0, 4).positions[0]; 2]);
6746 assert!(!editor.can_undo());
6747 assert!(editor.redo(&mut engine).unwrap());
6748 assert_eq!(text(&editor), "Base quick fox jumps");
6749
6750 let mut shown = vec!["Base".to_owned(), text(&editor)];
6751 for movement in [Movement::Left, Movement::Right] {
6752 editor.move_selection(&mut engine, movement, false).unwrap();
6753 }
6754 editor.insert(&mut engine, "!").unwrap();
6755 editor.insert(&mut engine, "?").unwrap();
6756 shown.push(text(&editor));
6757 editor.enter(&mut engine, false).unwrap();
6758 shown.push(text(&editor));
6759 editor.insert(&mut engine, "ab").unwrap();
6760 shown.push(text(&editor));
6761 editor
6762 .move_selection(&mut engine, Movement::Left, false)
6763 .unwrap();
6764 editor.delete(&mut engine, false).unwrap();
6765 shown.push(text(&editor));
6766 editor.insert(&mut engine, "c").unwrap();
6767 shown.push(text(&editor));
6768 editor
6769 .format(&mut engine, Formatting::Toggle(Toggle::Bold))
6770 .unwrap();
6771 editor.insert(&mut engine, "d").unwrap();
6772 editor.insert(&mut engine, "e").unwrap();
6773 shown.push(text(&editor));
6774 editor.select(at(1, 0)).unwrap();
6775 editor.delete(&mut engine, true).unwrap();
6776 shown.push(text(&editor));
6777 editor.insert(&mut engine, "f").unwrap();
6778 assert_eq!(text(&editor), "Base quick fox jumps!?facde");
6779 while editor.undo(&mut engine).unwrap() {
6780 assert_eq!(text(&editor), shown.pop().unwrap());
6781 }
6782 assert!(shown.is_empty());
6783 }
6784
6785 #[test]
6786 fn a_symbol_is_an_undo_step_between_typing_runs() {
6787 let mut engine = TextEngine::default();
6788 let source =
6789 TextDocument::new(vec![Paragraph::new("Cost".into(), Format::default())]).unwrap();
6790 let mut editor = CanvasEditor::new(&mut engine, source, 240.0).unwrap();
6791 let text = |editor: &CanvasEditor| editor.active_outline().shown_text();
6792 editor
6793 .select(
6794 [TextPosition {
6795 paragraph: 0,
6796 offset: 4,
6797 }; 2]
6798 .into(),
6799 )
6800 .unwrap();
6801 editor.insert(&mut engine, " 5").unwrap();
6802 editor.insert_symbol(&mut engine, '€').unwrap();
6803 editor.insert_symbol(&mut engine, '±').unwrap();
6804 editor.insert(&mut engine, "1").unwrap();
6805 assert_eq!(text(&editor), "Cost 5€±1");
6806 for shown in ["Cost 5€±", "Cost 5€", "Cost 5", "Cost"] {
6807 assert!(editor.undo(&mut engine).unwrap());
6808 assert_eq!(text(&editor), shown);
6809 }
6810 }
6811
6812 #[test]
6813 fn table_horizontal_arrows_follow_reading_order() {
6814 let mut engine = TextEngine::default();
6815 let mut editor = table_editor(&mut engine);
6816 let original = editor.active_outline().document.clone();
6817 let history = editor.undo.len();
6818 for paragraph in 0..4 {
6819 let text = original.paragraphs().nth(paragraph).unwrap();
6820 let end = text.utf16_offset(text.text().len()).unwrap();
6821 editor
6822 .select(
6823 [TextPosition {
6824 paragraph,
6825 offset: end,
6826 }; 2]
6827 .into(),
6828 )
6829 .unwrap();
6830 editor
6831 .move_selection(&mut engine, Movement::Right, false)
6832 .unwrap();
6833 let next = if paragraph < 3 {
6834 TextPosition {
6835 paragraph: paragraph + 1,
6836 offset: 0,
6837 }
6838 } else {
6839 TextPosition {
6840 paragraph,
6841 offset: end,
6842 }
6843 };
6844 assert_eq!(editor.selection().positions, [next; 2]);
6845 if paragraph < 3 {
6846 editor
6847 .move_selection(&mut engine, Movement::Left, false)
6848 .unwrap();
6849 assert_eq!(
6850 editor.selection().positions,
6851 [TextPosition {
6852 paragraph,
6853 offset: end
6854 }; 2]
6855 );
6856 }
6857 }
6858 assert_eq!(editor.active_outline().document, original);
6859 assert_eq!(editor.undo.len(), history);
6860 assert!(editor.caret_outline().is_none());
6861 }
6862
6863 #[test]
6864 fn column_width_edits_reflow_and_restore_structured_drafts() {
6865 use onestore::page::ParagraphContent;
6866 let mut engine = TextEngine::default();
6867 for draft in [false, true] {
6868 let mut editor = table_editor(&mut engine);
6869 let stored = editor.outlines()[0].document.clone();
6870 editor
6871 .select(
6872 [TextPosition {
6873 paragraph: 3,
6874 offset: 4,
6875 }; 2]
6876 .into(),
6877 )
6878 .unwrap();
6879 if draft {
6880 for _ in 0..2 {
6881 editor
6882 .move_selection(&mut engine, Movement::Down, false)
6883 .unwrap();
6884 }
6885 for _ in 0..2 {
6886 editor
6887 .move_selection(&mut engine, Movement::Up, false)
6888 .unwrap();
6889 }
6890 }
6891 let before = editor.active_outline().document.clone();
6892 let selection = editor.selection();
6893 let ParagraphContent::Table(table) = &before.nodes()[0].content else {
6894 panic!()
6895 };
6896 let position = selection.positions[1];
6897 let mut edit = before
6898 .replace(
6899 position..position,
6900 vec![Paragraph::new(
6901 " long cell content".into(),
6902 Format::default(),
6903 )],
6904 )
6905 .unwrap();
6906 edit.columns.insert(table.id, vec![96.375, 36.0]);
6907 editor.commit(&mut engine, edit, selection).unwrap();
6908 let after = editor.active_outline().document.clone();
6909 let fresh = editor
6910 .active_outline()
6911 .snapshot()
6912 .layout(&mut engine, &BTreeMap::new())
6913 .unwrap();
6914 assert_eq!(editor.active_outline().shaped.size, fresh.size);
6915 assert_eq!(
6916 editor.active_outline().shaped.tables[0]
6917 .cells
6918 .iter()
6919 .map(|c| c.rect)
6920 .collect::<Vec<_>>(),
6921 fresh.tables[0]
6922 .cells
6923 .iter()
6924 .map(|c| c.rect)
6925 .collect::<Vec<_>>()
6926 );
6927 for _ in 0..2 {
6928 editor.undo(&mut engine).unwrap();
6929 assert_eq!(editor.active_outline().document, before);
6930 assert_eq!(editor.outlines()[0].document, stored);
6931 assert_eq!(editor.selection(), selection);
6932 editor.redo(&mut engine).unwrap();
6933 assert_eq!(editor.active_outline().document, after);
6934 }
6935 let selection = editor.selection();
6936 let history = editor.undo.len();
6937 for widths in [vec![f32::MAX; 2], vec![36.0, f32::NAN]] {
6938 let position = selection.positions[1];
6939 let mut edit = after
6940 .replace(
6941 position..position,
6942 vec![Paragraph::new("invalid".into(), Format::default())],
6943 )
6944 .unwrap();
6945 edit.columns.insert(table.id, widths);
6946 assert!(editor.commit(&mut engine, edit, selection).is_err());
6947 assert_eq!(editor.active_outline().document, after);
6948 assert_eq!(editor.selection(), selection);
6949 assert_eq!(editor.undo.len(), history);
6950 }
6951 }
6952 }
6953
6954 #[test]
6955 fn cell_edits_reuse_neighbor_layouts_and_restore_table_geometry() {
6956 let mut engine = TextEngine::default();
6957 let mut editor = table_editor(&mut engine);
6958 let before = editor.active_outline().document.clone();
6959 let layout_ids = editor
6960 .active_outline()
6961 .shaped
6962 .paragraphs
6963 .iter()
6964 .map(|p| p.text.id())
6965 .collect::<Vec<_>>();
6966 let size = editor.active_outline().shaped.size;
6967 let cell_rects = editor.active_outline().shaped.tables[0]
6968 .cells
6969 .iter()
6970 .map(|cell| cell.rect)
6971 .collect::<Vec<_>>();
6972 let right_text = editor
6973 .active_outline()
6974 .document
6975 .paragraphs()
6976 .nth(1)
6977 .unwrap()
6978 .text()
6979 .as_ptr();
6980 let selection = [TextPosition {
6981 paragraph: 0,
6982 offset: 2,
6983 }; 2]
6984 .into();
6985 editor.select(selection).unwrap();
6986 editor
6987 .insert(&mut engine, " long text that wraps\nsecond paragraph")
6988 .unwrap();
6989 let outline = editor.active_outline();
6990 assert_eq!(outline.document.paragraphs().count(), 5);
6991 assert!(outline.shaped.size[1] > size[1]);
6992 assert_eq!(
6993 outline
6994 .document
6995 .paragraphs()
6996 .nth(2)
6997 .unwrap()
6998 .text()
6999 .as_ptr(),
7000 right_text
7001 );
7002 for (index, original) in layout_ids.iter().enumerate().skip(1) {
7003 assert_eq!(
7004 outline.paragraph_layout(index + 1).unwrap().text.id(),
7005 *original
7006 );
7007 }
7008 assert_eq!(
7009 outline
7010 .layouts()
7011 .map(|(index, _)| index)
7012 .collect::<Vec<_>>(),
7013 [0, 1, 2, 3, 4]
7014 );
7015 assert_eq!(
7016 outline.shaped.tables[0].cells[0].rect[3],
7017 outline.shaped.tables[0].cells[1].rect[3]
7018 );
7019 assert!(outline.shaped.tables[0].cells[2].rect[1] > cell_rects[2][1]);
7020 let after = outline.document.clone();
7021 let after_selection = outline.selection;
7022 let after_rects = outline.shaped.tables[0]
7023 .cells
7024 .iter()
7025 .map(|cell| cell.rect)
7026 .collect::<Vec<_>>();
7027 for _ in 0..3 {
7028 editor.undo(&mut engine).unwrap();
7029 let outline = editor.active_outline();
7030 assert_eq!(outline.document, before);
7031 assert_eq!(outline.selection, selection);
7032 assert_eq!(outline.shaped.size, size);
7033 assert_eq!(
7034 outline.shaped.tables[0]
7035 .cells
7036 .iter()
7037 .map(|cell| cell.rect)
7038 .collect::<Vec<_>>(),
7039 cell_rects
7040 );
7041 editor.redo(&mut engine).unwrap();
7042 let outline = editor.active_outline();
7043 assert_eq!(outline.document, after);
7044 assert_eq!(outline.selection, after_selection);
7045 assert_eq!(
7046 outline.shaped.tables[0]
7047 .cells
7048 .iter()
7049 .map(|cell| cell.rect)
7050 .collect::<Vec<_>>(),
7051 after_rects
7052 );
7053 }
7054 editor.undo(&mut engine).unwrap();
7055 editor.undo(&mut engine).unwrap();
7056 assert_eq!(
7057 editor
7058 .active_outline()
7059 .document
7060 .paragraphs()
7061 .next()
7062 .unwrap()
7063 .text(),
7064 "Left"
7065 );
7066 assert!(editor.active_outline().shaped.tables.is_empty());
7067 editor.redo(&mut engine).unwrap();
7068 assert_eq!(editor.active_outline().document, before);
7069 assert_eq!(editor.active_outline().shaped.size, size);
7070 }
7071
7072 #[test]
7073 fn cell_composition_and_failed_layout_leave_atomic_history() {
7074 let mut engine = TextEngine::default();
7075 let mut editor = table_editor(&mut engine);
7076 editor
7077 .select(
7078 [TextPosition {
7079 paragraph: 2,
7080 offset: 2,
7081 }; 2]
7082 .into(),
7083 )
7084 .unwrap();
7085 let before = editor.active_outline().document.clone();
7086 let selection = editor.selection();
7087 let cells = editor.active_outline().shaped.tables[0]
7088 .cells
7089 .iter()
7090 .map(|cell| cell.rect)
7091 .collect::<Vec<_>>();
7092 let ids = editor
7093 .active_outline()
7094 .shaped
7095 .paragraphs
7096 .iter()
7097 .map(|p| p.text.id())
7098 .collect::<Vec<_>>();
7099 let history = editor.undo.len();
7100 editor.compose(&mut engine, "🧊\n中".into(), 4..4).unwrap();
7101 editor.compose(&mut engine, "short".into(), 5..5).unwrap();
7102 assert!(editor.cancel_composition(&mut engine).unwrap());
7103 assert_eq!(editor.active_outline().document, before);
7104 assert_eq!(editor.selection(), selection);
7105 assert_eq!(editor.undo.len(), history);
7106 assert_eq!(
7107 editor.active_outline().shaped.tables[0]
7108 .cells
7109 .iter()
7110 .map(|cell| cell.rect)
7111 .collect::<Vec<_>>(),
7112 cells
7113 );
7114 for index in [0, 1, 3] {
7115 assert_eq!(
7116 editor
7117 .active_outline()
7118 .paragraph_layout(index)
7119 .unwrap()
7120 .text
7121 .id(),
7122 ids[index]
7123 );
7124 }
7125 editor.compose(&mut engine, "🧊\n中".into(), 4..4).unwrap();
7126 editor.commit_text(&mut engine, "done".into()).unwrap();
7127 assert_eq!(editor.undo.len(), history + 1);
7128 editor.undo(&mut engine).unwrap();
7129 assert_eq!(editor.active_outline().document, before);
7130 assert_eq!(editor.selection(), selection);
7131 let redo = editor.redo.len();
7132 assert!(
7133 editor
7134 .replace(
7135 &mut engine,
7136 vec![Paragraph::new(
7137 "bad".into(),
7138 Format {
7139 font_size: Some(f32::NAN),
7140 ..Format::default()
7141 }
7142 )]
7143 )
7144 .is_err()
7145 );
7146 assert_eq!(editor.active_outline().document, before);
7147 assert_eq!(editor.selection(), selection);
7148 assert_eq!(editor.undo.len(), history);
7149 assert_eq!(editor.redo.len(), redo);
7150 editor.redo(&mut engine).unwrap();
7151 assert_eq!(
7152 editor
7153 .active_outline()
7154 .document
7155 .paragraphs()
7156 .nth(2)
7157 .unwrap()
7158 .text(),
7159 "Bedonelow"
7160 );
7161 }
7162
7163 #[test]
7164 fn retained_cell_edits_match_fresh_layout_for_every_text_range() {
7165 let mut engine = TextEngine::default();
7166 let base = table_editor(&mut engine);
7167 for (index, paragraph) in base.active_outline().document.paragraphs().enumerate() {
7168 let offsets = paragraph
7169 .text()
7170 .char_indices()
7171 .map(|(byte, _)| paragraph.utf16_offset(byte).unwrap())
7172 .chain(std::iter::once(
7173 paragraph.utf16_offset(paragraph.text().len()).unwrap(),
7174 ))
7175 .collect::<Vec<_>>();
7176 for &start in &offsets {
7177 for &end in offsets.iter().filter(|&&end| end >= start) {
7178 for text in ["", "🧊", "x\ny"] {
7179 let mut editor = CanvasEditor::from_text_outlines(
7180 vec![base.active_outline().clone()],
7181 BTreeMap::new(),
7182 None,
7183 )
7184 .unwrap();
7185 let selection = [
7186 TextPosition {
7187 paragraph: index,
7188 offset: start,
7189 },
7190 TextPosition {
7191 paragraph: index,
7192 offset: end,
7193 },
7194 ]
7195 .into();
7196 editor.select(selection).unwrap();
7197 editor.insert(&mut engine, text).unwrap();
7198 let retained = &editor.active_outline().shaped;
7199 let fresh = editor
7200 .active_outline()
7201 .snapshot()
7202 .layout(&mut engine, &editor.definitions)
7203 .unwrap();
7204 assert_eq!(retained.size, fresh.size);
7205 assert_eq!(
7206 retained.tables[0]
7207 .cells
7208 .iter()
7209 .map(|c| c.rect)
7210 .collect::<Vec<_>>(),
7211 fresh.tables[0]
7212 .cells
7213 .iter()
7214 .map(|c| c.rect)
7215 .collect::<Vec<_>>()
7216 );
7217 for (retained, fresh) in retained.paragraphs.iter().zip(&fresh.paragraphs) {
7218 assert_eq!(retained.id, fresh.id);
7219 assert_eq!(retained.origin, fresh.origin);
7220 assert_eq!(retained.text.height(), fresh.text.height());
7221 assert_eq!(
7222 retained
7223 .text
7224 .lines()
7225 .map(|(line, bounds)| (
7226 line.metrics().advance,
7227 bounds.source.clone()
7228 ))
7229 .collect::<Vec<_>>(),
7230 fresh
7231 .text
7232 .lines()
7233 .map(|(line, bounds)| (
7234 line.metrics().advance,
7235 bounds.source.clone()
7236 ))
7237 .collect::<Vec<_>>()
7238 );
7239 }
7240 editor.undo(&mut engine).unwrap();
7241 assert_eq!(
7242 editor.active_outline().document,
7243 base.active_outline().document
7244 );
7245 assert_eq!(editor.selection(), selection);
7246 }
7247 }
7248 }
7249 }
7250 }
7251
7252 #[test]
7253 fn cross_outline_validation_includes_table_row_and_cell_ids() {
7254 use onestore::page::ParagraphContent;
7255 let mut engine = TextEngine::default();
7256 let first = table_editor(&mut engine).active_outline().clone();
7257 let second = table_editor(&mut engine).active_outline().clone();
7258 let ParagraphContent::Table(first_table) = &first.document.nodes()[0].content else {
7259 panic!()
7260 };
7261 for alias in 0..3 {
7262 let mut second = second.clone();
7263 let mut nodes = second.document.nodes().to_vec();
7264 let ParagraphContent::Table(table) = &mut nodes[0].content else {
7265 panic!()
7266 };
7267 match alias {
7268 0 => table.id = first_table.id,
7269 1 => table.rows[0].id = first_table.rows[0].id,
7270 _ => table.rows[0].cells[0].id = first_table.rows[0].cells[0].id,
7271 }
7272 second.document = TextDocument::from_nodes(nodes).unwrap();
7273 assert!(matches!(
7274 CanvasEditor::from_text_outlines(
7275 vec![first.clone(), second],
7276 BTreeMap::new(),
7277 None
7278 ),
7279 Err(EditorError::Edit(EditError::InvalidStructure))
7280 ));
7281 }
7282 }
7283
7284 #[test]
7285 fn cutting_the_only_picture_restores_its_empty_text_paragraph_on_undo() {
7286 let mut engine = TextEngine::default();
7287 let document =
7288 TextDocument::new(vec![Paragraph::new(String::new(), Format::default())]).unwrap();
7289 let mut editor = CanvasEditor::new(&mut engine, document, 240.0).unwrap();
7290 editor
7291 .create_outline(&mut engine, [36.0, 36.0], 240.0)
7292 .unwrap();
7293 let image = picture(
7294 include_bytes!("../../../corpus/object-tags/native/notebook/photo.png").to_vec(),
7295 [40.0, 30.0],
7296 )
7297 .unwrap();
7298 let id = image.id;
7299 editor.insert_picture(&mut engine, image).unwrap();
7300 editor.remove_image(&mut engine, id).unwrap();
7301 assert!(editor.active_outline().is_empty());
7302 assert!(editor.undo(&mut engine).unwrap());
7303 assert!(editor.image_placement(id).is_some());
7304 assert_eq!(editor.active_outline().document.nodes().len(), 2);
7305 assert!(editor.redo(&mut engine).unwrap());
7306 assert!(editor.active_outline().is_empty());
7307 assert!(editor.undo(&mut engine).unwrap());
7308 assert!(editor.image_placement(id).is_some());
7309 }
7310
7311 #[test]
7312 fn pictures_move_and_resize_through_history_and_backgrounds_stay_put() {
7313 use onestore::page::{Image, Page, PageObject};
7314 let mut engine = TextEngine::default();
7315 let [picture, background] = [false, true].map(|background| Image {
7316 size: None,
7317 id: onestore::page::text::new_id().unwrap(),
7318 layout: onestore::document::Layout {
7319 x: Some(468.75),
7320 y: Some(86.4),
7321 max_width: Some(333.0),
7322 max_height: Some(200.1),
7323 ..Default::default()
7324 },
7325 bytes: Some(std::sync::Arc::from(b"deferred image payload".as_slice())),
7326 display: None,
7327 alt: None,
7328 background,
7329 printout: None,
7330 tags: Vec::new(),
7331 link: None,
7332 text: None,
7333 });
7334 let (id, background_id) = (picture.id, background.id);
7335 let mut editor = CanvasEditor::from_page(
7336 Page {
7337 title: String::new(),
7338 identity: None,
7339 created: None,
7340 margin_origin: [36.0, 14.4],
7341 rtl: false,
7342 color: None,
7343 rule_lines: None,
7344 definitions: BTreeMap::new(),
7345 objects: vec![PageObject::Image(background), PageObject::Image(picture)],
7346 },
7347 &mut engine,
7348 )
7349 .unwrap();
7350 let stored = |editor: &CanvasEditor| {
7351 editor
7352 .page()
7353 .unwrap()
7354 .objects
7355 .into_iter()
7356 .find_map(|object| match object {
7357 PageObject::Image(image) if image.id == id => Some(image.layout),
7358 _ => None,
7359 })
7360 .unwrap()
7361 };
7362 let original = stored(&editor);
7363 assert_eq!(
7364 editor.image_placement(id),
7365 Some(([468.75, 86.4], [333.0, 200.1]))
7366 );
7367 assert!(editor.image_placement(background_id).is_none());
7368 assert!(matches!(
7369 editor.place_image(&mut engine, background_id, [0.0; 2], [10.0; 2]),
7370 Err(EditorError::Edit(EditError::InvalidRange))
7371 ));
7372 assert!(matches!(
7373 editor.place_image(&mut engine, id, [0.0; 2], [0.0, 10.0]),
7374 Err(EditorError::Edit(EditError::InvalidRange))
7375 ));
7376 editor
7377 .place_image(&mut engine, id, [513.75, 104.4], [333.0, 200.1])
7378 .unwrap();
7379 let moved = stored(&editor);
7380 assert_eq!([moved.x, moved.y], [Some(513.75), Some(104.4)]);
7381 assert_eq!(moved.width_set_by_user, None);
7382 editor
7383 .place_image(&mut engine, id, [513.75, 104.4], [281.8, 169.4])
7384 .unwrap();
7385 let resized = stored(&editor);
7386 assert_eq!(
7387 [resized.max_width, resized.max_height],
7388 [Some(281.8), Some(169.4)]
7389 );
7390 assert_eq!(resized.width_set_by_user, Some(true));
7391 assert!(editor.undo(&mut engine).unwrap());
7392 assert_eq!(stored(&editor), moved);
7393 assert!(editor.undo(&mut engine).unwrap());
7394 assert_eq!(stored(&editor), original);
7395 assert!(editor.redo(&mut engine).unwrap());
7396 assert!(editor.redo(&mut engine).unwrap());
7397 assert_eq!(stored(&editor), resized);
7398
7399 let ids = |editor: &CanvasEditor| {
7400 editor
7401 .page()
7402 .unwrap()
7403 .objects
7404 .iter()
7405 .map(PageObject::id)
7406 .collect::<Vec<_>>()
7407 };
7408 let before = ids(&editor);
7409 assert!(matches!(
7410 editor.remove_image(&mut engine, background_id),
7411 Err(EditorError::Edit(EditError::InvalidRange))
7412 ));
7413 editor.remove_image(&mut engine, id).unwrap();
7414 assert!(editor.image_placement(id).is_none());
7415 assert_eq!(ids(&editor), [background_id]);
7416 assert!(editor.undo(&mut engine).unwrap());
7417 assert_eq!(ids(&editor), before);
7418 assert_eq!(stored(&editor), resized);
7419 assert!(editor.redo(&mut engine).unwrap());
7420 assert_eq!(ids(&editor), [background_id]);
7421 }
7422
7423 #[test]
7424 fn art_and_paper_change_together_as_one_step_under_the_page() {
7425 use onestore::page::{Image, Page, PageObject};
7426 let mut engine = TextEngine::default();
7427 let image = |background, bytes: &[u8]| Image {
7428 size: None,
7429 id: onestore::page::text::new_id().unwrap(),
7430 layout: onestore::document::Layout {
7431 x: Some(-27.0),
7432 y: Some(-3.6),
7433 max_width: Some(174.5),
7434 max_height: Some(640.0),
7435 ..Default::default()
7436 },
7437 bytes: Some(std::sync::Arc::from(bytes)),
7438 display: None,
7439 alt: None,
7440 background,
7441 printout: None,
7442 tags: Vec::new(),
7443 link: None,
7444 text: None,
7445 };
7446 let [old, picture] = [image(true, b"old art"), image(false, b"picture")];
7447 let mut editor = CanvasEditor::from_page(
7448 Page {
7449 title: String::new(),
7450 identity: None,
7451 created: None,
7452 margin_origin: [36.0, 14.4],
7453 rtl: false,
7454 color: None,
7455 rule_lines: None,
7456 definitions: BTreeMap::new(),
7457 objects: vec![
7458 PageObject::Image(old.clone()),
7459 PageObject::Image(picture.clone()),
7460 ],
7461 },
7462 &mut engine,
7463 )
7464 .unwrap();
7465 let ids = |editor: &CanvasEditor| {
7466 let page = editor.page().unwrap();
7467 let ids: Vec<_> = page.objects.iter().map(PageObject::id).collect();
7468 (ids, page.color)
7469 };
7470 let art = || [image(true, b"art one"), image(true, b"art two")];
7471 let [one, two] = art();
7472 let blue = Some(0x00fef5ed);
7473 assert!(editor.set_paper(blue, None, Some(vec![one.clone(), two.clone()])));
7474 let applied = (vec![one.id, two.id, picture.id], blue);
7475 assert_eq!(ids(&editor), applied);
7476 assert_eq!(
7477 editor.take_ops().unwrap(),
7478 [
7479 PageOp::Color(blue),
7480 PageOp::Delete { object: old.id },
7481 PageOp::Add {
7482 object: PageObject::Image(two.clone()),
7483 before: Some(picture.id),
7484 },
7485 PageOp::Add {
7486 object: PageObject::Image(one.clone()),
7487 before: Some(two.id),
7488 },
7489 ]
7490 );
7491 // The same art again, under fresh identities, changes nothing.
7492 assert!(!editor.set_paper(blue, None, Some(art().into())));
7493 assert!(editor.undo(&mut engine).unwrap());
7494 assert_eq!(ids(&editor), (vec![old.id, picture.id], None));
7495 assert_eq!(editor.take_ops().unwrap().len(), 4);
7496 assert!(!editor.can_undo());
7497 assert!(editor.redo(&mut engine).unwrap());
7498 assert_eq!(ids(&editor), applied);
7499 assert!(editor.set_paper(blue, None, Some(Vec::new())));
7500 assert_eq!(ids(&editor), (vec![picture.id], blue));
7501 }
7502
7503 #[test]
7504 fn arrows_leave_a_picture_for_the_neighbouring_outlines_in_page_order() {
7505 use onestore::page::{Image, Page, PageObject};
7506 let mut engine = TextEngine::default();
7507 let outline = |engine: &mut TextEngine, text: &str, x| {
7508 PageObject::Outline(
7509 TextOutline::new(
7510 engine,
7511 TextDocument::new(vec![Paragraph::new(text.into(), Default::default())])
7512 .unwrap(),
7513 120.0,
7514 [x, 0.0],
7515 )
7516 .unwrap()
7517 .snapshot(),
7518 )
7519 };
7520 let picture = Image {
7521 size: None,
7522 id: onestore::page::text::new_id().unwrap(),
7523 layout: onestore::document::Layout {
7524 x: Some(200.0),
7525 y: Some(0.0),
7526 max_width: Some(40.0),
7527 max_height: Some(40.0),
7528 ..Default::default()
7529 },
7530 bytes: Some(std::sync::Arc::from(b"deferred image payload".as_slice())),
7531 display: None,
7532 alt: None,
7533 background: false,
7534 printout: None,
7535 tags: Vec::new(),
7536 link: None,
7537 text: None,
7538 };
7539 let id = picture.id;
7540 let objects = vec![
7541 outline(&mut engine, "Before", 0.0),
7542 PageObject::Image(picture),
7543 outline(&mut engine, "After", 300.0),
7544 ];
7545 let [before, after] = [0, 2].map(|index| objects[index].id());
7546 let mut editor = CanvasEditor::from_page(
7547 Page {
7548 title: String::new(),
7549 identity: None,
7550 created: None,
7551 margin_origin: [36.0, 14.4],
7552 rtl: false,
7553 color: None,
7554 rule_lines: None,
7555 definitions: BTreeMap::new(),
7556 objects,
7557 },
7558 &mut engine,
7559 )
7560 .unwrap();
7561 let caret = |paragraph, offset| [TextPosition { paragraph, offset }; 2];
7562 assert!(editor.step_from_image(&mut engine, id, false).unwrap());
7563 assert_eq!(editor.active_outline().id, before);
7564 assert_eq!(editor.selection().positions, caret(0, 6));
7565 assert!(editor.step_from_image(&mut engine, id, true).unwrap());
7566 assert_eq!(editor.active_outline().id, after);
7567 assert_eq!(editor.selection().positions, caret(0, 0));
7568
7569 editor.select_all().unwrap();
7570 assert!(editor.delete(&mut engine, true).unwrap());
7571 editor.focus_outline(before).unwrap();
7572 assert!(editor.outlines().iter().all(|outline| outline.id != after));
7573 assert!(!editor.step_from_image(&mut engine, id, true).unwrap());
7574 assert_eq!(editor.active_outline().id, before);
7575 }
7576
7577 #[test]
7578 fn text_edits_flow_around_a_picture_inside_an_outline() {
7579 use onestore::page::{Image, Page, PageObject, ParagraphContent};
7580 let mut engine = TextEngine::default();
7581 let mut source = TextOutline::new(
7582 &mut engine,
7583 TextDocument::new(vec![
7584 Paragraph::new("Before".into(), Default::default()),
7585 Paragraph::new("After".into(), Default::default()),
7586 ])
7587 .unwrap(),
7588 240.0,
7589 [36.0, 36.0],
7590 )
7591 .unwrap()
7592 .snapshot();
7593 let mut picture = source.paragraphs[0].clone();
7594 picture.id = onestore::page::text::new_id().unwrap();
7595 let image = Image {
7596 size: Some([40.0, 30.0]),
7597 id: onestore::page::text::new_id().unwrap(),
7598 layout: Default::default(),
7599 bytes: Some(std::sync::Arc::from(b"deferred image payload".as_slice())),
7600 display: None,
7601 alt: None,
7602 background: false,
7603 printout: None,
7604 tags: Vec::new(),
7605 link: None,
7606 text: None,
7607 };
7608 picture.content = ParagraphContent::Image(image.clone());
7609 source.paragraphs.insert(1, picture);
7610 let outline_id = source.id;
7611 let mut editor = CanvasEditor::from_page(
7612 Page {
7613 title: String::new(),
7614 identity: None,
7615 created: None,
7616 margin_origin: [36.0, 14.4],
7617 rtl: false,
7618 color: None,
7619 rule_lines: None,
7620 definitions: BTreeMap::new(),
7621 objects: vec![PageObject::Outline(source)],
7622 },
7623 &mut engine,
7624 )
7625 .unwrap();
7626 let outline = editor.active_outline();
7627 assert_eq!(outline.id, outline_id);
7628 let [before, after] = [0, 1].map(|i| outline.shaped().paragraphs[i].origin[1]);
7629 let object = &outline.shaped().objects[0];
7630 assert_eq!(object.id, image.id);
7631 assert_eq!(object.rect[3] - object.rect[1], 30.0);
7632 assert!(before < object.rect[1] && object.bottom <= after);
7633
7634 editor
7635 .move_selection(&mut engine, Movement::Down, false)
7636 .unwrap();
7637 assert_eq!(editor.selection().positions[0].paragraph, 1);
7638 editor
7639 .move_selection(&mut engine, Movement::Up, false)
7640 .unwrap();
7641 editor
7642 .move_selection(&mut engine, Movement::LineEnd, false)
7643 .unwrap();
7644 editor.insert(&mut engine, "\nMore").unwrap();
7645 let shaped = editor.active_outline().shaped();
7646 assert_eq!(shaped.paragraphs.len(), 3);
7647 assert!(shaped.objects[0].rect[1] > shaped.paragraphs[1].origin[1]);
7648 assert!(shaped.paragraphs[2].origin[1] >= shaped.objects[0].bottom);
7649 // Joining across the picture is refused rather than deleting it.
7650 editor
7651 .move_selection(&mut engine, Movement::Down, false)
7652 .unwrap();
7653 editor
7654 .move_selection(&mut engine, Movement::LineStart, false)
7655 .unwrap();
7656 assert!(!editor.delete(&mut engine, true).unwrap());
7657 let saved = editor.page().unwrap();
7658 let PageObject::Outline(saved) = &saved.objects[0] else {
7659 panic!()
7660 };
7661 assert!(matches!(&saved.paragraphs[2].content, ParagraphContent::Image(i) if *i == image));
7662 assert!(editor.undo(&mut engine).unwrap());
7663 assert_eq!(editor.active_outline().shaped().paragraphs.len(), 2);
7664 assert_eq!(editor.active_outline().shaped().objects.len(), 1);
7665
7666 // Selected, it resizes and deletes in place through the outline's history.
7667 assert!(editor.image_in_outline(image.id));
7668 let (origin, size) = editor.image_placement(image.id).unwrap();
7669 assert_eq!(size, [40.0, 30.0]);
7670 editor
7671 .place_image(&mut engine, image.id, [0.0; 2], [80.0, 60.0])
7672 .unwrap();
7673 assert_eq!(
7674 editor.image_placement(image.id),
7675 Some((origin, [80.0, 60.0]))
7676 );
7677 let shaped = editor.active_outline().shaped();
7678 assert!(shaped.paragraphs[1].origin[1] >= shaped.objects[0].bottom);
7679 assert!(editor.step_from_image(&mut engine, image.id, true).unwrap());
7680 assert_eq!(
7681 editor.selection().positions[0],
7682 TextPosition {
7683 paragraph: 1,
7684 offset: 0
7685 }
7686 );
7687 assert!(
7688 editor
7689 .step_from_image(&mut engine, image.id, false)
7690 .unwrap()
7691 );
7692 assert_eq!(
7693 editor.selection().positions[0],
7694 TextPosition {
7695 paragraph: 0,
7696 offset: 6
7697 }
7698 );
7699 editor.remove_image(&mut engine, image.id).unwrap();
7700 assert!(editor.image_placement(image.id).is_none());
7701 assert!(editor.active_outline().shaped().objects.is_empty());
7702 assert!(editor.undo(&mut engine).unwrap());
7703 assert_eq!(
7704 editor.image_placement(image.id),
7705 Some((origin, [80.0, 60.0]))
7706 );
7707 assert!(editor.undo(&mut engine).unwrap());
7708 assert_eq!(
7709 editor.image_placement(image.id),
7710 Some((origin, [40.0, 30.0]))
7711 );
7712 }
7713
7714 /// OneNote 2010 attaches at the caret (`corpus/attachment-insert`): the text after it
7715 /// follows the file and keeps the caret, and an empty first half gives way to the file.
7716 #[test]
7717 fn a_file_attaches_at_the_caret_as_onenote_splits_around_it() {
7718 use onestore::page::{Attachment, Page, PageObject, ParagraphContent};
7719 let mut engine = TextEngine::default();
7720 let source = TextOutline::new(
7721 &mut engine,
7722 TextDocument::new(vec![
7723 Paragraph::new("Before".into(), Default::default()),
7724 Paragraph::new(String::new(), Default::default()),
7725 ])
7726 .unwrap(),
7727 240.0,
7728 [36.0, 36.0],
7729 )
7730 .unwrap()
7731 .snapshot();
7732 let mut editor = CanvasEditor::from_page(
7733 Page {
7734 title: String::new(),
7735 identity: None,
7736 created: None,
7737 margin_origin: [36.0, 14.4],
7738 rtl: false,
7739 color: None,
7740 rule_lines: None,
7741 definitions: BTreeMap::new(),
7742 objects: vec![PageObject::Outline(source)],
7743 },
7744 &mut engine,
7745 )
7746 .unwrap();
7747 editor.take_ops().unwrap();
7748 let file = |name: &str| Attachment {
7749 id: onestore::page::text::new_id().unwrap(),
7750 filename: name.into(),
7751 source_path: None,
7752 size: Some([24.0, 24.0]),
7753 layout: Default::default(),
7754 bytes: Some(std::sync::Arc::from(b"payload".as_slice())),
7755 preview: None,
7756 recording: None,
7757 tags: Vec::new(),
7758 };
7759 let at = |paragraph, offset| Selection {
7760 positions: [TextPosition { paragraph, offset }; 2],
7761 affinities: [Affinity::Downstream; 2],
7762 };
7763 let shown = |editor: &CanvasEditor| {
7764 let page = editor.page().unwrap();
7765 let PageObject::Outline(outline) = &page.objects[0] else {
7766 panic!()
7767 };
7768 outline
7769 .paragraphs
7770 .iter()
7771 .map(|node| match &node.content {
7772 ParagraphContent::Text(text) => text.text.text().to_owned(),
7773 ParagraphContent::Attachment(file) => format!("[{}]", file.filename),
7774 _ => panic!(),
7775 })
7776 .collect::<Vec<_>>()
7777 };
7778
7779 editor.select(at(0, 3)).unwrap();
7780 let middle = file("middle.txt");
7781 editor
7782 .insert_attachment(&mut engine, middle.clone())
7783 .unwrap();
7784 assert_eq!(shown(&editor), ["Bef", "[middle.txt]", "ore", ""]);
7785 // A file is no text position: the caret starts "ore", the second text.
7786 assert_eq!(editor.selection().positions, at(1, 0).positions);
7787 assert_eq!(editor.attachment(middle.id), Some(&middle));
7788 let ops = editor.take_ops().unwrap();
7789 assert!(ops.iter().any(|op| matches!(op, PageOp::Insert { paragraphs, .. }
7790 if paragraphs.iter().any(|p| matches!(&p.content, ParagraphContent::Attachment(a) if *a == middle)))));
7791
7792 editor.select(at(2, 0)).unwrap();
7793 editor
7794 .insert_attachment(&mut engine, file("empty.bin"))
7795 .unwrap();
7796 assert_eq!(
7797 shown(&editor),
7798 ["Bef", "[middle.txt]", "ore", "[empty.bin]", ""]
7799 );
7800 assert_eq!(editor.selection().positions[0].paragraph, 2);
7801
7802 assert!(editor.undo(&mut engine).unwrap());
7803 assert_eq!(shown(&editor), ["Bef", "[middle.txt]", "ore", ""]);
7804 editor.remove_image(&mut engine, middle.id).unwrap();
7805 assert_eq!(shown(&editor), ["Bef", "ore", ""]);
7806 }
7807
7808 #[test]
7809 fn unknown_paragraphs_stay_on_the_page_and_text_free_outlines_take_a_caret() {
7810 use onestore::page::{Image, Page, PageObject, ParagraphContent, Unsupported};
7811 let mut engine = TextEngine::default();
7812 let outline = |engine: &mut TextEngine| {
7813 TextOutline::new(
7814 engine,
7815 TextDocument::new(vec![
7816 Paragraph::new("Before".into(), Default::default()),
7817 Paragraph::new("After".into(), Default::default()),
7818 ])
7819 .unwrap(),
7820 240.0,
7821 [36.0, 36.0],
7822 )
7823 .unwrap()
7824 .snapshot()
7825 };
7826 let mut mixed = outline(&mut engine);
7827 let mut unknown = mixed.paragraphs[0].clone();
7828 unknown.id = onestore::page::text::new_id().unwrap();
7829 unknown.content = ParagraphContent::Unsupported(Unsupported {
7830 id: onestore::page::text::new_id().unwrap(),
7831 jcid: 0x60012,
7832 layout: Default::default(),
7833 });
7834 mixed.paragraphs.insert(1, unknown);
7835 let mut picture_only = outline(&mut engine);
7836 picture_only.id = onestore::page::text::new_id().unwrap();
7837 picture_only.paragraphs.truncate(1);
7838 picture_only.paragraphs[0].content = ParagraphContent::Image(Image {
7839 size: Some([40.0, 30.0]),
7840 id: onestore::page::text::new_id().unwrap(),
7841 layout: Default::default(),
7842 bytes: Some(std::sync::Arc::from(b"deferred image payload".as_slice())),
7843 display: None,
7844 alt: None,
7845 background: false,
7846 printout: None,
7847 tags: Vec::new(),
7848 link: None,
7849 text: None,
7850 });
7851 let (mixed_id, picture_id) = (mixed.id, picture_only.id);
7852 let editor = CanvasEditor::from_page(
7853 Page {
7854 title: String::new(),
7855 identity: None,
7856 created: None,
7857 margin_origin: [36.0, 14.4],
7858 rtl: false,
7859 color: None,
7860 rule_lines: None,
7861 definitions: BTreeMap::new(),
7862 objects: vec![
7863 PageObject::Outline(mixed),
7864 PageObject::Outline(picture_only),
7865 ],
7866 },
7867 &mut engine,
7868 )
7869 .unwrap();
7870 let mixed = editor.outlines().iter().find(|o| o.id == mixed_id).unwrap();
7871 let [placeholder] = &mixed.shaped().objects[..] else {
7872 panic!()
7873 };
7874 assert!(matches!(
7875 placeholder.kind,
7876 crate::outline::ObjectKind::Unsupported(_)
7877 ));
7878 assert_eq!(placeholder.rect[2] - placeholder.rect[0], 160.0);
7879 assert!(mixed.shaped().paragraphs[1].origin[1] >= placeholder.bottom);
7880 // The picture's outline takes a paragraph for the caret, which the page leaves out
7881 // until it is typed in.
7882 assert!(editor.has_page_outline(picture_id));
7883 let page = editor.page().unwrap();
7884 assert_eq!(page.objects.len(), 2);
7885 assert!(page.objects.iter().any(|object| matches!(
7886 object,
7887 PageObject::Outline(outline) if outline.id == picture_id && outline.paragraphs.len() == 1
7888 )));
7889 }
7890
7891 #[test]
7892 fn title_flow_moves_page_objects_atomically_and_cancels_composition() {
7893 use onestore::page::{Image, Page, PageObject, Title};
7894 let mut engine = TextEngine::default();
7895 let mut title = TextOutline::new(
7896 &mut engine,
7897 TextDocument::new(vec![Paragraph::new("Header".into(), Default::default())]).unwrap(),
7898 468.0,
7899 [0.0; 2],
7900 )
7901 .unwrap()
7902 .snapshot();
7903 title.title = true;
7904 title.layout.max_width = None;
7905 title.layout.width_set_by_user = None;
7906 let body = TextOutline::new(
7907 &mut engine,
7908 TextDocument::new(vec![Paragraph::new("Anchor".into(), Default::default())]).unwrap(),
7909 72.0,
7910 [120.0, 80.0],
7911 )
7912 .unwrap()
7913 .snapshot();
7914 let body_id = body.id;
7915 let images = [
7916 (120.0, 90.0, false),
7917 (600.0, 150.0, false),
7918 (120.0, 0.0, false),
7919 (120.0, 80.0, true),
7920 ]
7921 .map(|(x, y, background)| Image {
7922 size: None,
7923 id: onestore::page::text::new_id().unwrap(),
7924 layout: onestore::document::Layout {
7925 x: Some(x),
7926 y: Some(y),
7927 max_width: Some(72.0),
7928 max_height: Some(40.0),
7929 ..Default::default()
7930 },
7931 bytes: Some(std::sync::Arc::from(b"deferred image payload".as_slice())),
7932 display: None,
7933 alt: None,
7934 background,
7935 printout: None,
7936 tags: Vec::new(),
7937 link: None,
7938 text: None,
7939 });
7940 let image_ids = images.each_ref().map(|image| image.id);
7941 let image_bytes = images
7942 .each_ref()
7943 .map(|image| image.bytes.as_ref().unwrap().clone());
7944 let mut objects = vec![
7945 PageObject::Title(Title {
7946 id: onestore::page::text::new_id().unwrap(),
7947 date: None,
7948 layout: Default::default(),
7949 outlines: vec![title],
7950 }),
7951 PageObject::Outline(body),
7952 ];
7953 objects.extend(images.into_iter().map(PageObject::Image));
7954 let mut editor = CanvasEditor::from_page(
7955 Page {
7956 title: "Header".into(),
7957 identity: None,
7958 created: None,
7959 margin_origin: [36.0, 14.4],
7960 rtl: false,
7961 color: None,
7962 rule_lines: None,
7963 definitions: BTreeMap::new(),
7964 objects,
7965 },
7966 &mut engine,
7967 )
7968 .unwrap();
7969 let positions = |editor: &CanvasEditor| {
7970 editor
7971 .object_layouts()
7972 .map(|(id, layout)| (id, layout.clone()))
7973 .collect::<Vec<_>>()
7974 };
7975 let original = positions(&editor);
7976 let text = "Header\u{000b}Second\u{000b}Third\u{000b}Fourth\u{000b}Fifth\u{000b}Sixth";
7977 let end = text.encode_utf16().count().try_into().unwrap();
7978 editor.select_all().unwrap();
7979 editor.compose(&mut engine, text.into(), end..end).unwrap();
7980 let moved = positions(&editor);
7981 let y = |items: &Vec<(ExGuid, onestore::document::Layout)>, id| {
7982 items
7983 .iter()
7984 .find(|(candidate, _)| *candidate == id)
7985 .unwrap()
7986 .1
7987 .y
7988 .unwrap()
7989 };
7990 let delta = y(&moved, body_id) - y(&original, body_id);
7991 assert!(delta > 0.0);
7992 for id in &image_ids[..2] {
7993 assert!((y(&moved, *id) - y(&original, *id) - delta).abs() < 0.001);
7994 }
7995 for id in &image_ids[2..] {
7996 assert_eq!(y(&moved, *id), y(&original, *id));
7997 }
7998 editor.compose(&mut engine, "Header".into(), 6..6).unwrap();
7999 assert_eq!(positions(&editor), moved);
8000 editor.cancel_composition(&mut engine).unwrap();
8001 assert_eq!(positions(&editor), original);
8002 assert!(!editor.undo(&mut engine).unwrap());
8003 editor.select_all().unwrap();
8004 editor.compose(&mut engine, text.into(), end..end).unwrap();
8005 let taller = format!("{text}\u{000b}Seventh\u{000b}Eighth");
8006 let taller_end = taller.encode_utf16().count().try_into().unwrap();
8007 editor
8008 .compose(&mut engine, taller, taller_end..taller_end)
8009 .unwrap();
8010 let farther = positions(&editor);
8011 assert!(y(&farther, body_id) > y(&moved, body_id));
8012 editor.commit_text(&mut engine, "Header".into()).unwrap();
8013 assert_eq!(positions(&editor), farther);
8014 editor.undo(&mut engine).unwrap();
8015 assert_eq!(positions(&editor), original);
8016 assert!(!editor.undo(&mut engine).unwrap());
8017 editor.redo(&mut engine).unwrap();
8018 assert_eq!(positions(&editor), farther);
8019 editor.undo(&mut engine).unwrap();
8020 editor.select_all().unwrap();
8021 editor.insert(&mut engine, text).unwrap();
8022 assert_eq!(positions(&editor), moved);
8023 editor.select_all().unwrap();
8024 editor.insert(&mut engine, "Header").unwrap();
8025 assert_eq!(positions(&editor), moved);
8026 editor.undo(&mut engine).unwrap();
8027 assert_eq!(positions(&editor), moved);
8028 editor.undo(&mut engine).unwrap();
8029 assert_eq!(positions(&editor), original);
8030 editor.redo(&mut engine).unwrap();
8031 editor.redo(&mut engine).unwrap();
8032 assert_eq!(positions(&editor), moved);
8033 for (id, bytes) in image_ids.into_iter().zip(image_bytes) {
8034 let image = editor
8035 .objects
8036 .iter()
8037 .find_map(|object| match object {
8038 page::Content::Image(image) if image.id == id => Some(image),
8039 _ => None,
8040 })
8041 .unwrap();
8042 assert!(std::sync::Arc::ptr_eq(
8043 image.bytes.as_ref().unwrap(),
8044 &bytes
8045 ));
8046 }
8047 }
8048
8049 #[test]
8050 fn floating_outline_exit_uses_document_position_and_keeps_real_blank_lines() {
8051 let mut engine = TextEngine::default();
8052 for (x, real_blanks, size, exit_y) in [
8053 (179.9, 0, 11.0, None),
8054 (180.0, 0, 11.0, Some(230.4)),
8055 (270.0, 1, 11.0, Some(230.4)),
8056 (270.0, 0, 22.0, Some(248.4)),
8057 (270.0, 0, 44.0, Some(320.4)),
8058 ] {
8059 let mut paragraphs = vec![Paragraph::new(
8060 "Text".into(),
8061 Format {
8062 font_size: Some(size),
8063 ..Default::default()
8064 },
8065 )];
8066 paragraphs
8067 .extend((0..real_blanks).map(|_| Paragraph::new(String::new(), Format::default())));
8068 let source = TextDocument::new(paragraphs).unwrap();
8069 let outline = TextOutline::new(&mut engine, source.clone(), 72.0, [x, 158.4]).unwrap();
8070 let mut editor =
8071 CanvasEditor::from_text_outlines(vec![outline], BTreeMap::new(), None).unwrap();
8072 editor
8073 .move_selection(&mut engine, Movement::DocumentEnd, false)
8074 .unwrap();
8075 for _ in 0..3 - real_blanks {
8076 editor
8077 .move_selection(&mut engine, Movement::Down, false)
8078 .unwrap();
8079 }
8080 assert_eq!(editor.outlines[0].document, source);
8081 assert_eq!(editor.caret_outline().is_some(), exit_y.is_some());
8082 if let Some(y) = exit_y {
8083 assert_eq!(editor.active_outline().origin(), [x, y]);
8084 assert_eq!(
8085 editor.active_outline().layout.max_width,
8086 Some(DEFAULT_OUTLINE_WIDTH)
8087 );
8088 editor.insert(&mut engine, "New").unwrap();
8089 assert_eq!(editor.outlines.len(), 2);
8090 editor.undo(&mut engine).unwrap();
8091 assert_eq!(editor.outlines.len(), 1);
8092 assert_eq!(editor.active_outline().origin(), [x, y]);
8093 } else {
8094 assert_eq!(editor.active_outline().document.nodes().len(), 4);
8095 }
8096 assert!(editor.undo.is_empty());
8097 }
8098 }
8099
8100 #[test]
8101 fn vertical_exit_and_reentry_preserve_the_text_column_and_source() {
8102 let mut engine = TextEngine::default();
8103 for (x, y, above) in [
8104 (36.0, 158.4, 122.4),
8105 (180.0, 158.4, 122.4),
8106 (180.0, 178.0, 140.4),
8107 (180.0, 180.0, 140.4),
8108 (180.0, 184.0, 158.4),
8109 ] {
8110 let source =
8111 TextDocument::new(vec![Paragraph::new("Text".into(), Format::default())]).unwrap();
8112 let outline = TextOutline::new(&mut engine, source.clone(), 72.0, [x, y]).unwrap();
8113 let id = outline.id;
8114 let mut editor =
8115 CanvasEditor::from_text_outlines(vec![outline], BTreeMap::new(), None).unwrap();
8116 editor
8117 .move_selection(&mut engine, Movement::DocumentEnd, false)
8118 .unwrap();
8119 let column = editor.caret(1.0).unwrap().x0 as f32;
8120 editor
8121 .move_selection(&mut engine, Movement::Up, false)
8122 .unwrap();
8123 assert_eq!(
8124 editor.caret_outline().unwrap().origin(),
8125 [x + column, above]
8126 );
8127 editor.focus_outline(id).unwrap();
8128 if x >= 180.0 {
8129 for _ in 0..3 {
8130 editor
8131 .move_selection(&mut engine, Movement::Down, false)
8132 .unwrap();
8133 }
8134 assert!(editor.caret_outline().is_some());
8135 editor
8136 .move_selection(&mut engine, Movement::Up, false)
8137 .unwrap();
8138 assert!(editor.caret_outline().is_some());
8139 editor
8140 .move_selection(&mut engine, Movement::Up, false)
8141 .unwrap();
8142 if y == 184.0 {
8143 assert!(editor.caret_outline().is_some());
8144 editor
8145 .move_selection(&mut engine, Movement::Up, false)
8146 .unwrap();
8147 }
8148 assert_eq!(editor.active_outline().id, id);
8149 assert_eq!(editor.selection().positions[1].offset, 4);
8150 }
8151 assert_eq!(editor.outlines[0].document, source);
8152 assert!(editor.undo.is_empty());
8153 }
8154 }
8155
8156 #[test]
8157 fn provisional_paragraphs_use_the_base_style_instead_of_local_run_formatting() {
8158 let mut engine = TextEngine::default();
8159 let base = Format {
8160 font: Some("Arial".into()),
8161 font_size: Some(11.0),
8162 ..Default::default()
8163 };
8164 let style = ExGuid {
8165 n: 9,
8166 ..Default::default()
8167 };
8168 let mut nodes = TextDocument::new(vec![Paragraph::new(
8169 "Text".into(),
8170 Format {
8171 font_size: Some(22.0),
8172 bold: Some(true),
8173 ..base.clone()
8174 },
8175 )])
8176 .unwrap()
8177 .nodes()
8178 .to_vec();
8179 nodes[0].style = Some(style);
8180 let outline = TextOutline::new(
8181 &mut engine,
8182 TextDocument::from_nodes(nodes).unwrap(),
8183 240.0,
8184 [0.0; 2],
8185 )
8186 .unwrap();
8187 let mut editor = CanvasEditor::from_text_outlines(
8188 vec![outline],
8189 BTreeMap::from([(
8190 style,
8191 Definition {
8192 kind: onestore::document::Kind::Style {
8193 name: Some("p".into()),
8194 next: None,
8195 },
8196 format: base.clone(),
8197 },
8198 )]),
8199 None,
8200 )
8201 .unwrap();
8202 editor
8203 .move_selection(&mut engine, Movement::Down, false)
8204 .unwrap();
8205 let paragraph = &editor.active_outline().document.nodes()[1];
8206 assert_eq!(paragraph.style, Some(style));
8207 assert_eq!(paragraph.text().unwrap().text.spans()[0].format, base);
8208 assert_eq!(
8209 editor.active_outline().shaped.paragraphs[1].text.height(),
8210 engine
8211 .layout(&Paragraph::new(String::new(), base), 240.0)
8212 .unwrap()
8213 .height()
8214 );
8215 assert_eq!(editor.outlines[0].document.nodes().len(), 1);
8216 }
8217
8218 #[test]
8219 fn clicks_below_use_base_style_preserve_source_and_share_draft_history() {
8220 let mut engine = TextEngine::default();
8221 for font_size in [11.0, 22.0] {
8222 let style = ExGuid {
8223 n: 900,
8224 ..Default::default()
8225 };
8226 let base = Format {
8227 font: Some("Arial".into()),
8228 font_size: Some(font_size),
8229 ..Default::default()
8230 };
8231 let mut nodes = TextDocument::new(vec![Paragraph::new(
8232 "Body".into(),
8233 Format {
8234 font_size: Some(44.0),
8235 bold: Some(true),
8236 ..base.clone()
8237 },
8238 )])
8239 .unwrap()
8240 .nodes()
8241 .to_vec();
8242 nodes[0].style = Some(style);
8243 let outline = TextOutline::new(
8244 &mut engine,
8245 TextDocument::from_nodes(nodes).unwrap(),
8246 240.0,
8247 [180.0, 90.0],
8248 )
8249 .unwrap();
8250 let id = outline.id;
8251 let source = outline.document.clone();
8252 let layout = outline.layout.clone();
8253 let text_id = outline.shaped.paragraphs[0].text.id();
8254 let bottom = outline.shaped.size[1];
8255 let mut editor = CanvasEditor::from_text_outlines(
8256 vec![outline],
8257 BTreeMap::from([(
8258 style,
8259 Definition {
8260 kind: onestore::document::Kind::Style {
8261 name: Some("p".into()),
8262 next: None,
8263 },
8264 format: base.clone(),
8265 },
8266 )]),
8267 None,
8268 )
8269 .unwrap();
8270 assert!(
8271 editor
8272 .select_below(&mut engine, id, [10.0, bottom + 20.0])
8273 .unwrap()
8274 );
8275 let blanks = if font_size == 11.0 { 2 } else { 1 };
8276 assert_eq!(editor.active_outline().document.nodes().len(), blanks + 1);
8277 assert_eq!(
8278 editor.selection().positions[1],
8279 TextPosition {
8280 paragraph: blanks,
8281 offset: 0
8282 }
8283 );
8284 assert_eq!(editor.outlines[0].document, source);
8285 assert_eq!(
8286 editor.active_outline().shaped.paragraphs[0].text.id(),
8287 text_id
8288 );
8289 for paragraph in &editor.active_outline().document.nodes()[1..] {
8290 assert_eq!(paragraph.style, Some(style));
8291 assert_eq!(paragraph.text().unwrap().text.spans()[0].format, base);
8292 }
8293 let pending = editor.active_outline().document.clone();
8294 let selection = editor.selection();
8295 assert!(
8296 !editor
8297 .select_below(&mut engine, id, [10.0, bottom + 28.0])
8298 .unwrap()
8299 );
8300 assert_eq!(editor.active_outline().document, pending);
8301 assert_eq!(editor.selection(), selection);
8302 assert!(editor.undo.is_empty());
8303 editor.compose(&mut engine, "日本".into(), 2..2).unwrap();
8304 assert!(editor.cancel_composition(&mut engine).unwrap());
8305 assert_eq!(editor.active_outline().document, pending);
8306 editor.insert(&mut engine, "X").unwrap();
8307 let edited = editor.active_outline().document.clone();
8308 assert_eq!(editor.outlines[0].layout, layout);
8309 assert_eq!(edited.nodes()[0], source.nodes()[0]);
8310 assert!(editor.undo(&mut engine).unwrap());
8311 assert_eq!(editor.active_outline().document, pending);
8312 assert_eq!(editor.outlines[0].document, source);
8313 assert_eq!(editor.selection(), selection);
8314 assert!(editor.redo(&mut engine).unwrap());
8315 assert_eq!(editor.active_outline().document, edited);
8316 assert!(editor.undo(&mut engine).unwrap());
8317 assert!(
8318 editor
8319 .select_below(&mut engine, id, [10.0, bottom + 1.0])
8320 .unwrap()
8321 );
8322 assert_eq!(editor.active_outline().document.nodes().len(), blanks + 1);
8323 editor
8324 .select([selection.positions[1], editor.selection().positions[1]].into())
8325 .unwrap();
8326 assert_eq!(editor.outlines[0].document, source);
8327 assert_eq!(editor.redo.len(), 1);
8328 editor
8329 .place_caret(&mut engine, [600.0, 600.0], 240.0)
8330 .unwrap();
8331 let pending = editor.active_outline().document.clone();
8332 let selection = editor.selection();
8333 editor.definitions.get_mut(&style).unwrap().format.font_size = Some(0.001);
8334 assert!(
8335 editor
8336 .select_below(&mut engine, id, [10.0, bottom + 20.0])
8337 .is_err()
8338 );
8339 assert_eq!(editor.active_outline().document, pending);
8340 assert_eq!(editor.selection(), selection);
8341 assert_eq!(editor.outlines[0].document, source);
8342 assert_eq!(editor.redo.len(), 1);
8343 }
8344 }
8345
8346 #[test]
8347 fn editing_during_provisional_composition_keeps_only_committed_layout_height() {
8348 let mut engine = TextEngine::default();
8349 let source =
8350 TextDocument::new(vec![Paragraph::new("body".into(), Format::default())]).unwrap();
8351 let mut editor = CanvasEditor::new(&mut engine, source.clone(), 240.0).unwrap();
8352 for _ in 0..3 {
8353 editor
8354 .move_selection(&mut engine, Movement::Down, false)
8355 .unwrap();
8356 }
8357 for _ in 0..2 {
8358 editor
8359 .move_selection(&mut engine, Movement::Up, false)
8360 .unwrap();
8361 }
8362 editor.compose(&mut engine, "a".into(), 1..1).unwrap();
8363 editor.insert(&mut engine, "b").unwrap();
8364 let expected = TextOutline::from_outline(
8365 &mut engine,
8366 &editor.active_outline().snapshot(),
8367 &BTreeMap::new(),
8368 )
8369 .unwrap();
8370 assert_eq!(editor.active_outline().shaped.size, expected.shaped.size);
8371 assert_eq!(editor.active_outline().document.nodes().len(), 2);
8372 editor.undo(&mut engine).unwrap();
8373 assert_eq!(editor.outlines[0].document, source);
8374 assert_eq!(editor.active_outline().document.nodes().len(), 4);
8375 }
8376
8377 #[test]
8378 fn arrow_created_paragraphs_are_provisional_and_share_existing_shaping() {
8379 let mut engine = TextEngine::default();
8380 let source =
8381 TextDocument::new(vec![Paragraph::new("body".into(), Format::default())]).unwrap();
8382 let mut editor = CanvasEditor::new(&mut engine, source.clone(), 240.0).unwrap();
8383 let shaped = editor.outlines[0].shaped.paragraphs[0]
8384 .text
8385 .shaped
8386 .styles()
8387 .as_ptr();
8388 for _ in 0..3 {
8389 editor
8390 .move_selection(&mut engine, Movement::Down, false)
8391 .unwrap();
8392 }
8393 assert_eq!(editor.outlines[0].document, source);
8394 assert_eq!(editor.active_outline().document.nodes().len(), 4);
8395 assert_eq!(
8396 editor
8397 .visible_outlines()
8398 .next()
8399 .unwrap()
8400 .document
8401 .nodes()
8402 .len(),
8403 4
8404 );
8405 assert_eq!(
8406 editor.active_outline().shaped.paragraphs[0]
8407 .text
8408 .shaped
8409 .styles()
8410 .as_ptr(),
8411 shaped
8412 );
8413 assert!(editor.undo.is_empty());
8414 editor
8415 .move_selection(&mut engine, Movement::Up, false)
8416 .unwrap();
8417 editor
8418 .move_selection(&mut engine, Movement::Up, false)
8419 .unwrap();
8420 let pending = editor.active_outline().document.clone();
8421 let selection = editor.selection();
8422 editor.insert(&mut engine, "X").unwrap();
8423 assert_eq!(
8424 editor.outlines[0]
8425 .document
8426 .paragraphs()
8427 .map(Paragraph::text)
8428 .collect::<Vec<_>>(),
8429 ["body", "X"]
8430 );
8431 assert_eq!(
8432 editor.outlines[0].shaped.paragraphs[0]
8433 .text
8434 .shaped
8435 .styles()
8436 .as_ptr(),
8437 shaped
8438 );
8439 editor.undo(&mut engine).unwrap();
8440 assert_eq!(editor.outlines[0].document, source);
8441 assert_eq!(editor.active_outline().document, pending);
8442 assert_eq!(editor.selection(), selection);
8443 editor
8444 .place_caret(&mut engine, [500.0, 500.0], 200.0)
8445 .unwrap();
8446 editor.redo(&mut engine).unwrap();
8447 assert_eq!(
8448 editor.outlines[0]
8449 .document
8450 .paragraphs()
8451 .map(Paragraph::text)
8452 .collect::<Vec<_>>(),
8453 ["body", "X"]
8454 );
8455 editor.undo(&mut engine).unwrap();
8456 assert_eq!(editor.active_outline().document, pending);
8457 assert_eq!(editor.selection(), selection);
8458 editor
8459 .place_caret(&mut engine, [500.0, 500.0], 200.0)
8460 .unwrap();
8461 assert_eq!(editor.outlines[0].document, source);
8462 }
8463
8464 /// New text takes the page's `p` quick style in the Default font, and a page whose `p`
8465 /// is in another font gains a second `p` beside it, as OneNote 2010 does
8466 /// (`corpus/default-font`).
8467 #[test]
8468 fn new_outlines_take_a_body_style_in_the_default_font() {
8469 let mut engine = TextEngine::default();
8470 let source =
8471 TextDocument::new(vec![Paragraph::new("body".into(), Format::default())]).unwrap();
8472 let mut editor = CanvasEditor::new(&mut engine, source, 240.0).unwrap();
8473 let style = |editor: &CanvasEditor, outline: usize| {
8474 let node = &editor.outlines[outline].document.nodes()[0];
8475 let definition = &editor.definitions[&node.style.unwrap()];
8476 assert_eq!(
8477 node.text().unwrap().text.spans()[0].format,
8478 definition.format
8479 );
8480 (node.style.unwrap(), definition.clone())
8481 };
8482 editor.default_font = DefaultFont {
8483 face: "Georgia".into(),
8484 size: 14.0,
8485 color: None,
8486 };
8487 for (typed, at) in [("one", 300.0), ("two", 400.0)] {
8488 editor.place_caret(&mut engine, [300.0, at], 240.0).unwrap();
8489 editor.insert(&mut engine, typed).unwrap();
8490 }
8491 let (first, georgia) = style(&editor, 1);
8492 assert_eq!(style(&editor, 2).0, first, "one p per font");
8493 assert_eq!(georgia, editor.default_font.body_style());
8494 editor.default_font = DefaultFont::default();
8495 editor
8496 .place_caret(&mut engine, [300.0, 500.0], 240.0)
8497 .unwrap();
8498 editor.insert(&mut engine, "three").unwrap();
8499 let (third, calibri) = style(&editor, 3);
8500 assert_ne!(third, first);
8501 assert_eq!(calibri.format.font.as_deref(), Some("Calibri"));
8502 assert_eq!(calibri.format.font_size, Some(11.0));
8503 }
8504
8505 #[test]
8506 fn deleting_through_provisional_paragraphs_removes_the_outline_and_restores_draft_on_undo() {
8507 let mut engine = TextEngine::default();
8508 let source =
8509 TextDocument::new(vec![Paragraph::new("body".into(), Format::default())]).unwrap();
8510 let mut editor = CanvasEditor::new(&mut engine, source.clone(), 240.0).unwrap();
8511 for _ in 0..2 {
8512 editor
8513 .move_selection(&mut engine, Movement::Down, false)
8514 .unwrap();
8515 }
8516 let pending = editor.active_outline().document.clone();
8517 editor.select_all().unwrap();
8518 let selection = editor.selection();
8519 editor.delete(&mut engine, true).unwrap();
8520 assert!(editor.outlines.is_empty());
8521 assert!(editor.caret_outline().is_some());
8522 editor
8523 .place_caret(&mut engine, [500.0, 500.0], 200.0)
8524 .unwrap();
8525 for _ in 0..2 {
8526 editor.undo(&mut engine).unwrap();
8527 assert_eq!(editor.outlines[0].document, source);
8528 assert_eq!(editor.active_outline().document, pending);
8529 assert_eq!(editor.selection(), selection);
8530 editor.redo(&mut engine).unwrap();
8531 assert!(editor.outlines.is_empty());
8532 }
8533 }
8534
8535 #[test]
8536 fn provisional_composition_cancels_and_backspace_reenters_without_history() {
8537 let mut engine = TextEngine::default();
8538 let source =
8539 TextDocument::new(vec![Paragraph::new("body".into(), Format::default())]).unwrap();
8540 let mut editor = CanvasEditor::new(&mut engine, source.clone(), 240.0).unwrap();
8541 editor
8542 .move_selection(&mut engine, Movement::Down, false)
8543 .unwrap();
8544 let pending = editor.active_outline().document.clone();
8545 editor.compose(&mut engine, "日本".into(), 2..2).unwrap();
8546 assert_eq!(editor.outlines[0].document, source);
8547 editor.cancel_composition(&mut engine).unwrap();
8548 assert_eq!(editor.active_outline().document, pending);
8549 editor.compose(&mut engine, "日本".into(), 2..2).unwrap();
8550 editor.finish_composition();
8551 assert_eq!(
8552 editor.outlines[0]
8553 .document
8554 .paragraphs()
8555 .map(Paragraph::text)
8556 .collect::<Vec<_>>(),
8557 ["body", "日本"]
8558 );
8559 editor.undo(&mut engine).unwrap();
8560 assert_eq!(editor.active_outline().document, pending);
8561 editor.delete(&mut engine, true).unwrap();
8562 assert_eq!(editor.outlines[0].document, source);
8563 assert!(matches!(editor.active, Focus::Outline(0)));
8564 assert!(editor.undo.is_empty());
8565 assert!(editor.redo(&mut engine).unwrap());
8566 }
8567
8568 #[test]
8569 fn word_navigation_terminates_at_soft_wrapped_bidi_cycles() {
8570 let mut engine = TextEngine::default();
8571 let paragraph = Paragraph::from_runs(vec![
8572 (
8573 "العربية".into(),
8574 Format {
8575 font_size: Some(12.0),
8576 bold: Some(false),
8577 italic: Some(false),
8578 ..Default::default()
8579 },
8580 ),
8581 ("ية 日本".into(), Format::default()),
8582 ]);
8583 let source = TextDocument::new(vec![paragraph]).unwrap();
8584 let mut editor = CanvasEditor::new(&mut engine, source.clone(), 1.0).unwrap();
8585 let selection = Selection {
8586 positions: [TextPosition {
8587 paragraph: 0,
8588 offset: 7,
8589 }; 2],
8590 affinities: [Affinity::Upstream; 2],
8591 };
8592 editor.select(selection).unwrap();
8593 editor.delete_to(&mut engine, Movement::WordRight).unwrap();
8594 assert_eq!(
8595 editor
8596 .active_outline()
8597 .document
8598 .paragraphs()
8599 .next()
8600 .unwrap()
8601 .text(),
8602 "ية 日本"
8603 );
8604 editor.undo(&mut engine).unwrap();
8605 assert_eq!(editor.active_outline().document, source);
8606 assert_eq!(editor.selection(), selection);
8607 for offset in 0..=12 {
8608 for affinity in [Affinity::Upstream, Affinity::Downstream] {
8609 for movement in [Movement::WordLeft, Movement::WordRight] {
8610 editor
8611 .select(Selection {
8612 positions: [TextPosition {
8613 paragraph: 0,
8614 offset,
8615 }; 2],
8616 affinities: [affinity; 2],
8617 })
8618 .unwrap();
8619 editor.move_selection(&mut engine, movement, true).unwrap();
8620 editor.select(editor.selection()).unwrap();
8621 }
8622 }
8623 }
8624 }
8625
8626 #[test]
8627 fn indentation_reflows_and_split_join_preserves_levels_and_undo() {
8628 let mut engine = TextEngine::default();
8629 let source = TextDocument::new(vec![
8630 Paragraph::new("alpha beta gamma delta".into(), Format::default()),
8631 Paragraph::new("untouched".into(), Format::default()),
8632 ])
8633 .unwrap();
8634 let mut editor = CanvasEditor::new(&mut engine, source.clone(), 110.0).unwrap();
8635 let untouched = editor.active_outline().shaped.paragraphs[1]
8636 .text
8637 .shaped
8638 .styles()
8639 .as_ptr();
8640 let selection: Selection = [TextPosition {
8641 paragraph: 0,
8642 offset: 6,
8643 }; 2]
8644 .into();
8645 editor.select(selection).unwrap();
8646 editor.indent(&mut engine, false).unwrap();
8647 assert_eq!(editor.active_outline().document.nodes()[0].level, 2);
8648 assert_eq!(editor.active_outline().shaped.paragraphs[0].origin[0], 27.0);
8649 assert_eq!(
8650 editor.active_outline().shaped.paragraphs[1]
8651 .text
8652 .shaped
8653 .styles()
8654 .as_ptr(),
8655 untouched
8656 );
8657 editor.insert(&mut engine, "\n").unwrap();
8658 assert_eq!(
8659 editor
8660 .active_outline()
8661 .document
8662 .nodes()
8663 .iter()
8664 .map(|n| n.level)
8665 .collect::<Vec<_>>(),
8666 [2, 2, 1]
8667 );
8668 // Backspace outdents an indented paragraph before joining it (`c4-child-1.xml`).
8669 editor.delete(&mut engine, true).unwrap();
8670 assert_eq!(editor.active_outline().document.nodes()[1].level, 1);
8671 editor.delete(&mut engine, true).unwrap();
8672 assert_eq!(
8673 editor.active_outline().document.nodes()[0]
8674 .text()
8675 .unwrap()
8676 .text
8677 .text(),
8678 "alpha beta gamma delta"
8679 );
8680 for _ in 0..4 {
8681 editor.undo(&mut engine).unwrap();
8682 }
8683 assert_eq!(editor.active_outline().document, source);
8684 assert_eq!(editor.selection(), selection);
8685 assert!(!editor.indent(&mut engine, true).unwrap());
8686 assert!(editor.redo(&mut engine).unwrap());
8687 }
8688
8689 #[test]
8690 fn indentation_excludes_selected_next_paragraph_start_and_rejects_overflow_atomically() {
8691 let mut engine = TextEngine::default();
8692 let source = TextDocument::new(vec![
8693 Paragraph::new("first".into(), Format::default()),
8694 Paragraph::new("second".into(), Format::default()),
8695 ])
8696 .unwrap();
8697 let mut editor = CanvasEditor::new(&mut engine, source, 50.0).unwrap();
8698 let selection: Selection = [
8699 TextPosition {
8700 paragraph: 1,
8701 offset: 0,
8702 },
8703 TextPosition {
8704 paragraph: 0,
8705 offset: 0,
8706 },
8707 ]
8708 .into();
8709 editor.select(selection).unwrap();
8710 editor.indent(&mut engine, false).unwrap();
8711 let indented = editor.active_outline().document.clone();
8712 assert_eq!(
8713 indented.nodes().iter().map(|n| n.level).collect::<Vec<_>>(),
8714 [2, 1]
8715 );
8716 assert!(editor.indent(&mut engine, false).is_err());
8717 assert_eq!(editor.active_outline().document, indented);
8718 assert_eq!(editor.selection(), selection);
8719 editor.undo(&mut engine).unwrap();
8720 assert!(!editor.undo(&mut engine).unwrap());
8721 }
8722
8723 #[test]
8724 fn paragraph_and_document_navigation_preserve_selection_anchor() {
8725 let mut engine = TextEngine::default();
8726 let source = TextDocument::new(vec![
8727 Paragraph::new("one two three four five".into(), Format::default()),
8728 Paragraph::new("🌳 last".into(), Format::default()),
8729 ])
8730 .unwrap();
8731 let mut editor = CanvasEditor::new(&mut engine, source.clone(), 45.0).unwrap();
8732 let start = TextPosition {
8733 paragraph: 0,
8734 offset: 8,
8735 };
8736 editor.select([start; 2].into()).unwrap();
8737 editor
8738 .move_selection(&mut engine, Movement::ParagraphEnd, true)
8739 .unwrap();
8740 assert_eq!(
8741 editor.selection().positions,
8742 [
8743 start,
8744 TextPosition {
8745 paragraph: 0,
8746 offset: 23
8747 }
8748 ]
8749 );
8750 editor
8751 .move_selection(&mut engine, Movement::ParagraphEnd, true)
8752 .unwrap();
8753 assert_eq!(
8754 editor.selection().positions,
8755 [
8756 start,
8757 TextPosition {
8758 paragraph: 1,
8759 offset: 7
8760 }
8761 ]
8762 );
8763 editor
8764 .move_selection(&mut engine, Movement::DocumentStart, true)
8765 .unwrap();
8766 assert_eq!(
8767 editor.selection().positions,
8768 [
8769 start,
8770 TextPosition {
8771 paragraph: 0,
8772 offset: 0
8773 }
8774 ]
8775 );
8776 editor
8777 .move_selection(&mut engine, Movement::DocumentEnd, false)
8778 .unwrap();
8779 assert_eq!(
8780 editor.selection().positions,
8781 [TextPosition {
8782 paragraph: 1,
8783 offset: 7
8784 }; 2]
8785 );
8786 editor
8787 .move_selection(&mut engine, Movement::ParagraphStart, false)
8788 .unwrap();
8789 assert_eq!(
8790 editor.selection().positions,
8791 [TextPosition {
8792 paragraph: 1,
8793 offset: 0
8794 }; 2]
8795 );
8796 editor
8797 .move_selection(&mut engine, Movement::ParagraphStart, false)
8798 .unwrap();
8799 assert_eq!(
8800 editor.selection().positions,
8801 [TextPosition {
8802 paragraph: 0,
8803 offset: 0
8804 }; 2]
8805 );
8806 assert_eq!(editor.active_outline().document, source);
8807 assert!(!editor.undo(&mut engine).unwrap());
8808 }
8809
8810 #[test]
8811 fn word_deletion_restores_original_caret_and_styled_source_on_undo() {
8812 let mut engine = TextEngine::default();
8813 let source = TextDocument::new(vec![Paragraph::new(
8814 "first café 🌳 last".into(),
8815 Format {
8816 bold: Some(true),
8817 ..Default::default()
8818 },
8819 )])
8820 .unwrap();
8821 let mut editor = CanvasEditor::new(&mut engine, source.clone(), 200.0).unwrap();
8822 let selection: Selection = [TextPosition {
8823 paragraph: 0,
8824 offset: 10,
8825 }; 2]
8826 .into();
8827 editor.select(selection).unwrap();
8828 assert!(editor.delete_to(&mut engine, Movement::WordLeft).unwrap());
8829 assert_eq!(
8830 editor
8831 .active_outline()
8832 .document
8833 .paragraphs()
8834 .next()
8835 .unwrap()
8836 .text(),
8837 "first 🌳 last"
8838 );
8839 editor.undo(&mut engine).unwrap();
8840 assert_eq!(editor.active_outline().document, source);
8841 assert_eq!(editor.selection(), selection);
8842 editor.redo(&mut engine).unwrap();
8843 editor.undo(&mut engine).unwrap();
8844 editor
8845 .move_selection(&mut engine, Movement::DocumentStart, false)
8846 .unwrap();
8847 assert!(!editor.delete_to(&mut engine, Movement::WordLeft).unwrap());
8848 assert!(editor.redo(&mut engine).unwrap());
8849 }
8850
8851 #[test]
8852 fn horizontal_navigation_and_blank_grid_keep_documents_and_history_unchanged() {
8853 let mut engine = TextEngine::default();
8854 let mut outlines = Vec::new();
8855 for (text, origin) in [
8856 ("Solo", [36.0, 158.4]),
8857 ("Right", [504.0, 158.4]),
8858 ("Below", [504.0, 320.4]),
8859 ] {
8860 let document =
8861 TextDocument::new(vec![Paragraph::new(text.into(), Format::default())]).unwrap();
8862 outlines.push(TextOutline::new(&mut engine, document, 72.0, origin).unwrap());
8863 }
8864 let ids: Vec<_> = outlines.iter().map(|outline| outline.id).collect();
8865 let originals: Vec<_> = outlines
8866 .iter()
8867 .map(|outline| outline.document.clone())
8868 .collect();
8869 let mut editor = CanvasEditor::from_text_outlines(outlines, BTreeMap::new(), None).unwrap();
8870 editor
8871 .move_selection(&mut engine, Movement::LineEnd, false)
8872 .unwrap();
8873 editor
8874 .move_selection(&mut engine, Movement::Right, true)
8875 .unwrap();
8876 assert_eq!(editor.active_outline().id, ids[0]);
8877 editor
8878 .move_selection(&mut engine, Movement::Right, false)
8879 .unwrap();
8880 assert_eq!(editor.active_outline().id, ids[1]);
8881 assert_eq!(editor.selection().positions[1].offset, 0);
8882 editor
8883 .move_selection(&mut engine, Movement::Left, false)
8884 .unwrap();
8885 assert_eq!(editor.active_outline().id, ids[0]);
8886 assert_eq!(editor.selection().positions[1].offset, 4);
8887 editor.focus_outline(ids[1]).unwrap();
8888 editor
8889 .move_selection(&mut engine, Movement::LineEnd, false)
8890 .unwrap();
8891 editor
8892 .move_selection(&mut engine, Movement::Right, false)
8893 .unwrap();
8894 assert_eq!(editor.active_outline().id, ids[1]);
8895 assert_eq!(editor.selection().positions[1].offset, 5);
8896 editor
8897 .place_caret(&mut engine, [486.0, 194.4], 468.0)
8898 .unwrap();
8899 editor
8900 .move_selection(&mut engine, Movement::Right, false)
8901 .unwrap();
8902 assert_eq!(editor.active_outline().origin(), [504.0, 194.4]);
8903 editor
8904 .move_selection(&mut engine, Movement::Up, false)
8905 .unwrap();
8906 assert_eq!(editor.active_outline().id, ids[1]);
8907 assert_eq!(editor.selection().positions[1].offset, 0);
8908 assert!(
8909 editor
8910 .outlines
8911 .iter()
8912 .zip(&originals)
8913 .all(|(outline, original)| &outline.document == original)
8914 );
8915 assert!(!editor.undo(&mut engine).unwrap());
8916 }
8917
8918 #[test]
8919 fn word_and_paragraph_selection_use_source_ranges_across_wraps() {
8920 let mut engine = TextEngine::default();
8921 let document = TextDocument::new(vec![Paragraph::new(
8922 "alpha beta gamma 🌳 delta".into(),
8923 Format::default(),
8924 )])
8925 .unwrap();
8926 let mut editor = CanvasEditor::new(&mut engine, document, 70.0).unwrap();
8927 let point = TextPosition {
8928 paragraph: 0,
8929 offset: 7,
8930 };
8931 editor.select([point; 2].into()).unwrap();
8932 let caret = editor.caret(1.0).unwrap();
8933 let x = caret.x0 as f32;
8934 let y = ((caret.y0 + caret.y1) * 0.5) as f32;
8935 let word = editor.selection_at(x, y, SelectionUnit::Word).unwrap();
8936 assert_eq!(word.positions.map(|p| p.offset), [6, 10]);
8937 let paragraph = editor.selection_at(x, y, SelectionUnit::Paragraph).unwrap();
8938 assert_eq!(paragraph.positions.map(|p| p.offset), [0, 25]);
8939 assert_eq!(editor.selection().positions, [point; 2]);
8940 }
8941
8942 #[test]
8943 fn pointer_selection_uses_canvas_line_boxes_after_many_emoji_lines() {
8944 let text = "alpha 🌳 beta\n".repeat(30);
8945 let start = text[..text.rfind("beta").unwrap()].encode_utf16().count() as u32;
8946 let mut engine = TextEngine::default();
8947 let document = TextDocument::new(vec![Paragraph::new(text, Format::default())]).unwrap();
8948 let mut editor = CanvasEditor::new(&mut engine, document, 70.0).unwrap();
8949 let position = TextPosition {
8950 paragraph: 0,
8951 offset: start + 1,
8952 };
8953 editor.select([position; 2].into()).unwrap();
8954 let caret = editor.caret(1.0).unwrap();
8955 let point = [caret.x0 as f32, ((caret.y0 + caret.y1) * 0.5) as f32];
8956 assert_eq!(
8957 editor
8958 .selection_at(point[0], point[1], SelectionUnit::Word)
8959 .unwrap()
8960 .positions
8961 .map(|p| p.offset),
8962 [start, start + 4]
8963 );
8964 assert_eq!(
8965 editor
8966 .selection_at(point[0], point[1], SelectionUnit::Grapheme)
8967 .unwrap()
8968 .positions,
8969 [position; 2]
8970 );
8971 }
8972
8973 #[test]
8974 fn retiring_text_restores_source_selection_order_and_geometry_through_history() {
8975 let mut engine = TextEngine::default();
8976 let mut outlines = Vec::new();
8977 for text in ["before", "bold 🌳\nannotation", "after"] {
8978 let document = TextDocument::new(
8979 text.split('\n')
8980 .map(|line| {
8981 Paragraph::new(
8982 line.into(),
8983 Format {
8984 bold: Some(true),
8985 ..Default::default()
8986 },
8987 )
8988 })
8989 .collect(),
8990 )
8991 .unwrap();
8992 outlines.push(TextOutline::new(&mut engine, document, 123.0, [43.5, 230.4]).unwrap());
8993 }
8994 let id = outlines[1].id;
8995 let document = outlines[1].document.clone();
8996 let geometry = layout_snapshot(&outlines[1].shaped);
8997 let layout = outlines[1].layout.clone();
8998 let ids = outlines
8999 .iter()
9000 .map(|outline| outline.id)
9001 .collect::<Vec<_>>();
9002 let mut editor = CanvasEditor::from_text_outlines(outlines, BTreeMap::new(), None).unwrap();
9003 editor.focus_outline(id).unwrap();
9004 editor.select_all().unwrap();
9005 let selection = editor.selection();
9006 editor.delete(&mut engine, true).unwrap();
9007 assert_eq!(
9008 editor.outlines.iter().map(|o| o.id).collect::<Vec<_>>(),
9009 [ids[0], ids[2]]
9010 );
9011 assert_eq!(editor.caret_outline().unwrap().id, id);
9012 assert_eq!(editor.active_outline().layout, layout);
9013 editor.insert(&mut engine, "again").unwrap();
9014 assert_eq!(
9015 editor.outlines.iter().map(|o| o.id).collect::<Vec<_>>(),
9016 ids
9017 );
9018 assert_eq!(editor.active_outline().id, id);
9019 editor.undo(&mut engine).unwrap();
9020 assert_eq!(editor.caret_outline().unwrap().id, id);
9021 editor
9022 .place_caret(&mut engine, [900.0, 500.0], 77.0)
9023 .unwrap();
9024 editor.undo(&mut engine).unwrap();
9025 assert_eq!(editor.active_outline().document, document);
9026 assert_eq!(editor.active_outline().layout, layout);
9027 assert_eq!(editor.selection(), selection);
9028 assert_eq!(layout_snapshot(&editor.active_outline().shaped), geometry);
9029 assert_eq!(
9030 editor.outlines.iter().map(|o| o.id).collect::<Vec<_>>(),
9031 ids
9032 );
9033 editor.redo(&mut engine).unwrap();
9034 assert_eq!(editor.outlines.len(), 2);
9035 assert_eq!(editor.active_outline().id, id);
9036 assert_eq!(editor.active_outline().origin(), [43.5, 230.4]);
9037 editor.redo(&mut engine).unwrap();
9038 assert_eq!(editor.active_outline().id, id);
9039 assert_eq!(
9040 editor.outlines.iter().map(|o| o.id).collect::<Vec<_>>(),
9041 ids
9042 );
9043 assert_eq!(
9044 editor.outlines[0]
9045 .document
9046 .paragraphs()
9047 .next()
9048 .unwrap()
9049 .text(),
9050 "before"
9051 );
9052 assert_eq!(
9053 editor.outlines[2]
9054 .document
9055 .paragraphs()
9056 .next()
9057 .unwrap()
9058 .text(),
9059 "after"
9060 );
9061 }
9062
9063 #[test]
9064 fn empty_composition_commits_safely_when_focus_or_position_changes() {
9065 let mut engine = TextEngine::default();
9066 for action in 0..5 {
9067 let mut outlines = Vec::new();
9068 for text in ["delete", "keep"] {
9069 let document =
9070 TextDocument::new(vec![Paragraph::new(text.into(), Format::default())])
9071 .unwrap();
9072 outlines.push(TextOutline::new(&mut engine, document, 240.0, [0.0; 2]).unwrap());
9073 }
9074 let first = outlines[0].id;
9075 let second = outlines[1].id;
9076 let original = outlines[0].document.clone();
9077 let mut editor =
9078 CanvasEditor::from_text_outlines(outlines, BTreeMap::new(), None).unwrap();
9079 editor.select_all().unwrap();
9080 editor.compose(&mut engine, String::new(), 0..0).unwrap();
9081 assert_eq!(editor.outlines.len(), 2);
9082 editor.cancel_composition(&mut engine).unwrap();
9083 assert_eq!(editor.active_outline().document, original);
9084 editor.compose(&mut engine, String::new(), 0..0).unwrap();
9085 match action {
9086 0 => editor.focus_outline(first).unwrap(),
9087 1 => editor.focus_outline(second).unwrap(),
9088 2 => editor.move_outline(first, [30.0, 40.0]).unwrap(),
9089 3 => editor.move_outline(second, [30.0, 40.0]).unwrap(),
9090 _ => editor.commit_text(&mut engine, String::new()).unwrap(),
9091 }
9092 assert!(editor.marked_range().is_none());
9093 assert_eq!(editor.outlines.len(), 1);
9094 assert_eq!(editor.outlines[0].id, second);
9095 assert_eq!(
9096 editor.outlines[0]
9097 .document
9098 .paragraphs()
9099 .next()
9100 .unwrap()
9101 .text(),
9102 "keep"
9103 );
9104 if action == 3 {
9105 editor.undo(&mut engine).unwrap();
9106 }
9107 editor.undo(&mut engine).unwrap();
9108 assert_eq!(editor.active_outline().id, first);
9109 assert_eq!(editor.active_outline().document, original);
9110 assert_eq!(editor.outlines.len(), 2);
9111 editor.redo(&mut engine).unwrap();
9112 assert_eq!(editor.outlines.len(), 1);
9113 }
9114 let document =
9115 TextDocument::new(vec![Paragraph::new(String::new(), Format::default())]).unwrap();
9116 let mut editor = CanvasEditor::new(&mut engine, document.clone(), 240.0).unwrap();
9117 editor.compose(&mut engine, "preedit".into(), 0..7).unwrap();
9118 editor.delete(&mut engine, true).unwrap();
9119 assert_eq!(editor.undo.len(), 2);
9120 assert!(editor.outlines.is_empty());
9121 editor.undo(&mut engine).unwrap();
9122 assert_eq!(
9123 editor
9124 .active_outline()
9125 .document
9126 .paragraphs()
9127 .next()
9128 .unwrap()
9129 .text(),
9130 "preedit"
9131 );
9132 editor.undo(&mut engine).unwrap();
9133 assert_eq!(editor.active_outline().document, document);
9134 assert!(editor.outlines.is_empty());
9135 }
9136
9137 #[test]
9138 fn space_only_caret_preserves_spacing_but_blank_paragraphs_and_metadata_are_content() {
9139 let mut engine = TextEngine::default();
9140 let document =
9141 TextDocument::new(vec![Paragraph::new(String::new(), Format::default())]).unwrap();
9142 let mut editor = CanvasEditor::new(&mut engine, document, 240.0).unwrap();
9143 editor.insert(&mut engine, " ").unwrap();
9144 assert!(editor.outlines.is_empty());
9145 assert!(editor.undo.is_empty());
9146 let spaces = editor.active_outline().document.clone();
9147 let selection = editor.selection();
9148 editor.insert(&mut engine, "x").unwrap();
9149 assert_eq!(editor.outlines.len(), 1);
9150 // A caret move ends the typing run, so the backspace is its own step.
9151 for movement in [Movement::Left, Movement::Right] {
9152 editor.move_selection(&mut engine, movement, false).unwrap();
9153 }
9154 editor.delete(&mut engine, true).unwrap();
9155 assert!(editor.outlines.is_empty());
9156 assert_eq!(editor.active_outline().document, spaces);
9157 let undo_count = editor.undo.len();
9158 editor.select_all().unwrap();
9159 assert!(editor.delete_to(&mut engine, Movement::LineStart).unwrap());
9160 assert!(editor.outlines.is_empty());
9161 assert_eq!(editor.undo.len(), undo_count);
9162 assert!(editor.undo(&mut engine).unwrap());
9163 assert_eq!(
9164 editor
9165 .active_outline()
9166 .document
9167 .paragraphs()
9168 .next()
9169 .unwrap()
9170 .text(),
9171 " x"
9172 );
9173 assert!(editor.redo(&mut engine).unwrap());
9174 assert_eq!(editor.active_outline().document, spaces);
9175 for _ in 0..2 {
9176 editor.undo(&mut engine).unwrap();
9177 }
9178 assert!(editor.outlines.is_empty());
9179 assert_eq!(editor.active_outline().document, spaces);
9180 assert_eq!(editor.selection(), selection);
9181 editor.insert(&mut engine, "\n").unwrap();
9182 assert_eq!(editor.outlines.len(), 1);
9183 assert_eq!(editor.active_outline().document.nodes().len(), 2);
9184 for text in ["\t", "\u{a0}", "\u{200b}"] {
9185 editor.select_all().unwrap();
9186 editor.insert(&mut engine, text).unwrap();
9187 assert_eq!(editor.outlines.len(), 1);
9188 }
9189 let mut source = editor.active_outline().snapshot();
9190 source.paragraphs[0].collapsed = true;
9191 let mut editor =
9192 CanvasEditor::from_outlines(&mut engine, vec![source], BTreeMap::new()).unwrap();
9193 editor.select_all().unwrap();
9194 editor.delete(&mut engine, true).unwrap();
9195 assert_eq!(editor.outlines.len(), 1);
9196 assert!(editor.active_outline().document.nodes()[0].collapsed);
9197 }
9198
9199 #[test]
9200 fn caret_placement_is_not_document_history_and_first_input_is_one_undo() {
9201 let mut engine = TextEngine::default();
9202 let original = TextDocument::new(vec![Paragraph::new(
9203 "Existing notes".into(),
9204 Format::default(),
9205 )])
9206 .unwrap();
9207 let mut editor = CanvasEditor::new(&mut engine, original.clone(), 240.0).unwrap();
9208 let existing = editor.active_outline().id;
9209 for step in 0..100 {
9210 editor
9211 .place_caret(&mut engine, [step as f32, 100.0], 120.0)
9212 .unwrap();
9213 assert_eq!(editor.outlines.len(), 1);
9214 assert!(editor.undo.is_empty());
9215 assert!(editor.redo.is_empty());
9216 }
9217 let id = editor.active_outline().id;
9218 let empty = editor.active_outline().document.clone();
9219 editor
9220 .insert(&mut engine, "annotation\nsecond line")
9221 .unwrap();
9222 let document = editor.active_outline().document.clone();
9223 assert_eq!(editor.active_outline().id, id);
9224 assert!(editor.caret_outline().is_none());
9225 assert_eq!(editor.outlines.len(), 2);
9226 assert_eq!(editor.undo.len(), 1);
9227 editor.undo(&mut engine).unwrap();
9228 assert_eq!(editor.outlines.len(), 1);
9229 assert_eq!(editor.active_outline().document, empty);
9230 assert_eq!(editor.active_outline().origin(), [99.0, 100.0]);
9231 editor
9232 .place_caret(&mut engine, [600.0, 400.0], 180.0)
9233 .unwrap();
9234 editor.insert(&mut engine, "").unwrap();
9235 assert_eq!(editor.redo.len(), 1);
9236 editor.redo(&mut engine).unwrap();
9237 assert_eq!(editor.active_outline().id, id);
9238 assert_eq!(editor.active_outline().document, document);
9239 assert_eq!(editor.active_outline().origin(), [99.0, 100.0]);
9240 editor.focus_outline(existing).unwrap();
9241 assert_eq!(editor.active_outline().document, original);
9242 }
9243
9244 #[test]
9245 fn provisional_input_preserves_the_empty_document_and_redo_until_commit() {
9246 let mut engine = TextEngine::default();
9247 let document = TextDocument::new(vec![Paragraph::new(
9248 String::new(),
9249 Format {
9250 bold: Some(true),
9251 ..Default::default()
9252 },
9253 )])
9254 .unwrap();
9255 let mut editor = CanvasEditor::new(&mut engine, document.clone(), 240.0).unwrap();
9256 assert!(editor.outlines.is_empty());
9257 editor.insert(&mut engine, "first").unwrap();
9258 editor.undo(&mut engine).unwrap();
9259 let id = editor.active_outline().id;
9260 editor.compose(&mut engine, "に".into(), 1..1).unwrap();
9261 assert!(editor.outlines.is_empty());
9262 assert_eq!(editor.redo.len(), 1);
9263 assert!(
9264 editor
9265 .place_caret(&mut engine, [f32::NAN, 0.0], 240.0)
9266 .is_err()
9267 );
9268 assert_eq!(editor.active_outline().id, id);
9269 assert!(editor.marked_range().is_some());
9270 editor.cancel_composition(&mut engine).unwrap();
9271 assert_eq!(editor.active_outline().document, document);
9272 assert_eq!(editor.redo.len(), 1);
9273 editor.compose(&mut engine, "に".into(), 1..1).unwrap();
9274 editor.commit_text(&mut engine, "日本".into()).unwrap();
9275 assert_eq!(editor.outlines.len(), 1);
9276 assert_eq!(editor.active_outline().id, id);
9277 assert_eq!(editor.undo.len(), 1);
9278 assert!(editor.redo.is_empty());
9279 editor.undo(&mut engine).unwrap();
9280 assert!(editor.outlines.is_empty());
9281 assert_eq!(editor.active_outline().document, document);
9282 }
9283
9284 fn ys(outline: &TextOutline) -> Vec<f32> {
9285 outline
9286 .shaped
9287 .paragraphs
9288 .iter()
9289 .map(|paragraph| paragraph.origin[1])
9290 .collect()
9291 }
9292
9293 fn layout_snapshot(layout: &OutlineLayout) -> serde_json::Value {
9294 serde_json::json!({
9295 "size": layout.size,
9296 "paragraphs": layout.paragraphs.iter().map(|paragraph| serde_json::json!({
9297 "id": paragraph.id, "origin": paragraph.origin,
9298 "text": paragraph.projection.text().text(),
9299 "lines": paragraph.text.lines().map(|(line, bounds)| serde_json::json!({
9300 "source": bounds.source, "top": bounds.top, "baseline": bounds.baseline,
9301 "height": bounds.height, "advance": line.metrics().advance,
9302 })).collect::<Vec<_>>(),
9303 "markers": paragraph.markers.iter().map(|(text, origin)| serde_json::json!({
9304 "origin": origin, "height": text.height(),
9305 "baseline": text.lines().next().unwrap().1.baseline,
9306 "advance": text.lines().next().unwrap().0.metrics().advance,
9307 })).collect::<Vec<_>>(),
9308 })).collect::<Vec<_>>(),
9309 })
9310 }
9311
9312 #[test]
9313 fn native_outline_omits_outer_paragraph_spacing_through_split_and_undo() {
9314 use onestore::page::{Page, PageObject};
9315 use onestore::{RevisionIndex, Store, document::Document};
9316 let store = Store::parse(include_bytes!(
9317 "../../../corpus/canvas/baseline-anchors.one"
9318 ))
9319 .unwrap();
9320 let index = RevisionIndex::parse(&store).unwrap();
9321 let page =
9322 Page::from_document(&Document::parse(&index).unwrap(), "Baseline anchors").unwrap();
9323 let outline = page
9324 .objects
9325 .into_iter()
9326 .find_map(|object| match object {
9327 PageObject::Outline(outline)
9328 if outline.paragraphs[0].text().unwrap().text.text() == "Spacing first" =>
9329 {
9330 Some(outline)
9331 }
9332 _ => None,
9333 })
9334 .unwrap();
9335 let mut engine = TextEngine::default();
9336 let mut editor =
9337 CanvasEditor::from_outlines(&mut engine, vec![outline], page.definitions).unwrap();
9338 let source = editor.active_outline().document.nodes().to_vec();
9339 for step in 0..3 {
9340 let outline = editor.active_outline();
9341 let paragraphs = &outline.shaped.paragraphs;
9342 assert_eq!(paragraphs[0].origin[1], 0.0);
9343 for pair in paragraphs.windows(2) {
9344 assert!(
9345 (pair[1].origin[1] - pair[0].origin[1] - pair[0].text.height() - 144.0).abs()
9346 < 0.0001
9347 );
9348 }
9349 let height = paragraphs.iter().map(|p| p.text.height()).sum::<f32>()
9350 + (paragraphs.len() - 1) as f32 * 144.0;
9351 assert!((outline.shaped.size[1] - height).abs() < 0.0001);
9352 match step {
9353 0 => editor.insert(&mut engine, "\n").unwrap(),
9354 1 => assert!(editor.undo(&mut engine).unwrap()),
9355 _ => assert_eq!(outline.document.nodes(), source),
9356 }
9357 }
9358 }
9359
9360 #[test]
9361 fn resize_preview_preserves_history_and_commit_restores_automatic_width_on_undo() {
9362 let mut engine = TextEngine::default();
9363 let mut editor = CanvasEditor::new(
9364 &mut engine,
9365 TextDocument::new(vec![Paragraph::new(String::new(), Format::default())]).unwrap(),
9366 468.0,
9367 )
9368 .unwrap();
9369 editor.insert(&mut engine, "One").unwrap();
9370 for movement in [Movement::Left, Movement::Right] {
9371 editor.move_selection(&mut engine, movement, false).unwrap();
9372 }
9373 let source = editor.active_outline().snapshot();
9374 let selection = editor.selection();
9375 editor.insert(&mut engine, "!").unwrap();
9376 editor.undo(&mut engine).unwrap();
9377 for width in [120.0, 360.0, 180.0] {
9378 let preview = editor.preview_resize(&mut engine, width).unwrap();
9379 assert_eq!(preview.bounds().width(), f64::from(width));
9380 assert_eq!(preview.document, editor.active_outline().document);
9381 assert_eq!(editor.active_outline().layout, source.layout);
9382 assert_eq!(editor.undo.len(), 1);
9383 assert_eq!(editor.redo.len(), 1);
9384 }
9385 assert!(editor.preview_resize(&mut engine, f32::NAN).is_err());
9386 editor.resize(&mut engine, 180.0).unwrap();
9387 assert!(editor.redo.is_empty());
9388 assert_eq!(editor.undo.len(), 2);
9389 editor.undo(&mut engine).unwrap();
9390 assert_eq!(editor.active_outline().layout, source.layout);
9391 assert_eq!(editor.active_outline().bounds().width(), 72.0);
9392 assert_eq!(editor.selection(), selection);
9393 editor.redo(&mut engine).unwrap();
9394 assert_eq!(editor.active_outline().bounds().width(), 180.0);
9395 editor.resize(&mut engine, 180.0).unwrap();
9396 assert_eq!(editor.undo.len(), 2);
9397 }
9398
9399 #[test]
9400 fn new_empty_outline_grows_shrinks_and_keeps_a_manually_set_width() {
9401 let mut engine = TextEngine::default();
9402 let mut editor = CanvasEditor::new(
9403 &mut engine,
9404 TextDocument::new(vec![Paragraph::new(String::new(), Format::default())]).unwrap(),
9405 468.0,
9406 )
9407 .unwrap();
9408 assert_eq!(editor.active_outline().bounds().width(), 72.0);
9409 assert_eq!(
9410 editor.active_outline().layout.width_set_by_user,
9411 Some(false)
9412 );
9413 editor
9414 .insert(&mut engine, "A short sentence expands the container")
9415 .unwrap();
9416 let expanded = editor.active_outline().bounds().width();
9417 assert!(expanded > 72.0 && expanded < 468.0);
9418 editor.select_all().unwrap();
9419 editor.insert(&mut engine, &"words ".repeat(80)).unwrap();
9420 assert_eq!(editor.active_outline().bounds().width(), 468.0);
9421 assert!(
9422 editor
9423 .active_outline()
9424 .paragraph_layout(0)
9425 .unwrap()
9426 .text
9427 .lines()
9428 .count()
9429 > 1
9430 );
9431 editor.select_all().unwrap();
9432 editor.insert(&mut engine, "One").unwrap();
9433 assert_eq!(editor.active_outline().bounds().width(), 72.0);
9434 editor.resize(&mut engine, 180.0).unwrap();
9435 editor.select_all().unwrap();
9436 editor.insert(&mut engine, "Tiny").unwrap();
9437 assert_eq!(editor.active_outline().bounds().width(), 180.0);
9438 editor
9439 .insert(&mut engine, &" longer words".repeat(10))
9440 .unwrap();
9441 assert_eq!(editor.active_outline().bounds().width(), 180.0);
9442 editor.undo(&mut engine).unwrap();
9443 assert_eq!(editor.active_outline().bounds().width(), 180.0);
9444 }
9445
9446 #[test]
9447 fn resizing_an_automatic_outline_sets_its_explicit_width_and_preserves_spacing_metadata() {
9448 let document = TextDocument::new(vec![Paragraph::new(
9449 "short".into(),
9450 Format {
9451 space_after: Some(7.0),
9452 ..Format::default()
9453 },
9454 )])
9455 .unwrap();
9456 let source = document.nodes().to_vec();
9457 let outline = Outline {
9458 title: false,
9459 min_width: None,
9460 id: onestore::page::text::new_id().unwrap(),
9461 layout: onestore::document::Layout {
9462 max_width: Some(220.0),
9463 reserved_width: Some(180.0),
9464 width_set_by_user: Some(false),
9465 ..Default::default()
9466 },
9467 indents: vec![18.0, 0.0],
9468 paragraphs: source.clone(),
9469 unsupported: Vec::new(),
9470 };
9471 let mut engine = TextEngine::default();
9472 let mut editor =
9473 CanvasEditor::from_outlines(&mut engine, vec![outline], BTreeMap::new()).unwrap();
9474 assert!(editor.active_outline().bounds().width() < 180.0);
9475 let height = editor
9476 .active_outline()
9477 .paragraph_layout(0)
9478 .unwrap()
9479 .text
9480 .height();
9481 assert_eq!(editor.active_outline().bounds().height(), f64::from(height));
9482 editor.resize(&mut engine, 180.0).unwrap();
9483 assert_eq!(editor.active_outline().bounds().width(), 180.0);
9484 assert_eq!(editor.active_outline().layout.reserved_width, None);
9485 assert_eq!(editor.active_outline().layout.width_set_by_user, Some(true));
9486 assert_eq!(editor.active_outline().document.nodes(), source);
9487 let cached = editor
9488 .active_outline()
9489 .paragraph_layout(0)
9490 .unwrap()
9491 .text
9492 .shaped
9493 .styles()
9494 .as_ptr();
9495 editor.resize(&mut engine, 180.0).unwrap();
9496 assert_eq!(
9497 editor
9498 .active_outline()
9499 .paragraph_layout(0)
9500 .unwrap()
9501 .text
9502 .shaped
9503 .styles()
9504 .as_ptr(),
9505 cached
9506 );
9507 }
9508
9509 #[test]
9510 fn platform_text_offsets_skip_collapsed_children() {
9511 let document = TextDocument::new(
9512 ["Collapsed", "Hidden", "Last"]
9513 .into_iter()
9514 .map(|s| Paragraph::new(s.into(), Format::default()))
9515 .collect(),
9516 )
9517 .unwrap();
9518 let mut nodes = document.nodes().to_vec();
9519 nodes[0].collapsed = true;
9520 nodes[1].parent = Some(nodes[0].id);
9521 nodes[1].level = 2;
9522 let outline = Outline {
9523 title: false,
9524 min_width: None,
9525 id: onestore::page::text::new_id().unwrap(),
9526 layout: onestore::document::Layout {
9527 max_width: Some(150.0),
9528 ..Default::default()
9529 },
9530 indents: Vec::new(),
9531 paragraphs: nodes,
9532 unsupported: Vec::new(),
9533 };
9534 let mut engine = TextEngine::default();
9535 let editor =
9536 CanvasEditor::from_outlines(&mut engine, vec![outline], BTreeMap::new()).unwrap();
9537 let outline = editor.active_outline();
9538 assert_eq!(outline.shown_text(), "Collapsed\nLast");
9539 let at = |paragraph, offset| TextPosition { paragraph, offset };
9540 for (position, offset) in [
9541 (at(0, 3), 3),
9542 (at(1, 2), 10),
9543 (at(2, 0), 10),
9544 (at(2, 4), 14),
9545 ] {
9546 assert_eq!(outline.utf16_offset(position).unwrap(), offset);
9547 }
9548 for (offset, position) in [(9, at(0, 9)), (10, at(2, 0)), (99, at(2, 4))] {
9549 assert_eq!(outline.utf16_position(offset).unwrap(), position);
9550 }
9551 }
9552
9553 #[test]
9554 fn imported_indentation_markers_and_collapsed_children_share_edit_geometry() {
9555 let document = TextDocument::new(
9556 [
9557 "First line",
9558 "Indented text with enough words to wrap",
9559 "Collapsed",
9560 "Hidden",
9561 "Last",
9562 ]
9563 .into_iter()
9564 .map(|s| Paragraph::new(s.into(), Format::default()))
9565 .collect(),
9566 )
9567 .unwrap();
9568 let mut nodes = document.nodes().to_vec();
9569 nodes[1].parent = Some(nodes[0].id);
9570 nodes[1].level = 2;
9571 nodes[2].parent = Some(nodes[0].id);
9572 nodes[2].level = 2;
9573 nodes[2].collapsed = true;
9574 nodes[3].parent = Some(nodes[2].id);
9575 nodes[3].level = 3;
9576 let marker = onestore::page::text::new_id().unwrap();
9577 nodes[1].lists.push(marker);
9578 let definitions = BTreeMap::from([(
9579 marker,
9580 Definition {
9581 kind: onestore::document::Kind::List {
9582 font: Some("Courier New".into()),
9583 format: Some("○".into()),
9584 restart: None,
9585 bullet: Some(4),
9586 },
9587 format: Format {
9588 font_size: Some(22.0),
9589 ..Format::default()
9590 },
9591 },
9592 )]);
9593 let outline = Outline {
9594 title: false,
9595 min_width: None,
9596 id: onestore::page::text::new_id().unwrap(),
9597 layout: onestore::document::Layout {
9598 x: Some(30.0),
9599 y: Some(45.0),
9600 max_width: Some(150.0),
9601 width_set_by_user: Some(true),
9602 ..Default::default()
9603 },
9604 indents: vec![18.0, 0.0, 27.0],
9605 paragraphs: nodes.clone(),
9606 unsupported: Vec::new(),
9607 };
9608 let mut engine = TextEngine::default();
9609 let expected = layout_snapshot(&outline.layout(&mut engine, &definitions).unwrap());
9610 let id = outline.id;
9611 let mut editor =
9612 CanvasEditor::from_outlines(&mut engine, vec![outline], definitions).unwrap();
9613 assert_eq!(editor.active_outline().id, id);
9614 assert_eq!(editor.active_outline().origin(), [30.0, 45.0]);
9615 assert_eq!(
9616 editor
9617 .active_outline()
9618 .layouts()
9619 .map(|(i, _)| i)
9620 .collect::<Vec<_>>(),
9621 [0, 1, 2, 4]
9622 );
9623 assert_eq!(layout_snapshot(&editor.active_outline().shaped), expected);
9624 let second = editor.active_outline().paragraph_layout(1).unwrap();
9625 assert_eq!(second.origin[0], 27.0);
9626 let y = second.origin[1] + second.text.lines().next().unwrap().1.height * 0.5;
9627 editor.select_at(27.0, y, false).unwrap();
9628 assert_eq!(
9629 editor.selection().positions[1],
9630 TextPosition {
9631 paragraph: 1,
9632 offset: 0
9633 }
9634 );
9635 assert_eq!(editor.caret(1.0).unwrap().x0, 27.0);
9636 let retained = editor
9637 .active_outline()
9638 .paragraph_layout(4)
9639 .unwrap()
9640 .text
9641 .shaped
9642 .styles()
9643 .as_ptr();
9644 editor.insert(&mut engine, "More words ").unwrap();
9645 assert_eq!(
9646 editor
9647 .active_outline()
9648 .paragraph_layout(4)
9649 .unwrap()
9650 .text
9651 .shaped
9652 .styles()
9653 .as_ptr(),
9654 retained
9655 );
9656 let rebuilt = Outline {
9657 id,
9658 title: false,
9659 min_width: None,
9660 layout: editor.active_outline().layout.clone(),
9661 indents: vec![18.0, 0.0, 27.0],
9662 paragraphs: editor.active_outline().document.nodes().to_vec(),
9663 unsupported: Vec::new(),
9664 }
9665 .layout(&mut engine, &editor.definitions)
9666 .unwrap();
9667 assert_eq!(
9668 layout_snapshot(&editor.active_outline().shaped),
9669 layout_snapshot(&rebuilt)
9670 );
9671 let edited = layout_snapshot(&editor.active_outline().shaped);
9672 let before = editor.active_outline().document.clone();
9673 let selection = editor.selection();
9674 editor.enter(&mut engine, false).unwrap();
9675 let split = &editor.active_outline().document.nodes()[2];
9676 assert_eq!((split.parent, split.level), (Some(nodes[0].id), 2));
9677 assert_ne!(split.lists, [marker]);
9678 assert_eq!(
9679 editor.definitions[&split.lists[0]],
9680 editor.definitions[&marker]
9681 );
9682 editor.undo(&mut engine).unwrap();
9683 assert_eq!(editor.active_outline().document, before);
9684 assert!(editor.resize(&mut engine, 20.0).is_err());
9685 assert_eq!(editor.active_outline().document, before);
9686 assert_eq!(editor.selection(), selection);
9687 assert_eq!(layout_snapshot(&editor.active_outline().shaped), edited);
9688 editor.undo(&mut engine).unwrap();
9689 assert_eq!(editor.active_outline().document.nodes(), nodes);
9690 assert_eq!(layout_snapshot(&editor.active_outline().shaped), expected);
9691 editor.redo(&mut engine).unwrap();
9692 assert_eq!(layout_snapshot(&editor.active_outline().shaped), edited);
9693 assert!(
9694 editor
9695 .select(
9696 [TextPosition {
9697 paragraph: 3,
9698 offset: 0
9699 }; 2]
9700 .into()
9701 )
9702 .is_err()
9703 );
9704 editor
9705 .select(
9706 [TextPosition {
9707 paragraph: 2,
9708 offset: 9,
9709 }; 2]
9710 .into(),
9711 )
9712 .unwrap();
9713 editor
9714 .move_selection(&mut engine, Movement::Right, false)
9715 .unwrap();
9716 assert_eq!(editor.selection().positions[1].paragraph, 4);
9717 editor
9718 .move_selection(&mut engine, Movement::Left, false)
9719 .unwrap();
9720 assert_eq!(editor.selection().positions[1].paragraph, 2);
9721 editor
9722 .move_selection(&mut engine, Movement::Down, false)
9723 .unwrap();
9724 assert_eq!(editor.selection().positions[1].paragraph, 4);
9725 editor
9726 .move_selection(&mut engine, Movement::Up, false)
9727 .unwrap();
9728 assert_eq!(editor.selection().positions[1].paragraph, 2);
9729 editor.select_all().unwrap();
9730 assert!(!editor.selection_rects().unwrap().is_empty());
9731 }
9732
9733 #[test]
9734 fn joins_pass_over_hidden_children_and_a_split_hands_them_on_still_collapsed() {
9735 let mut engine = TextEngine::default();
9736 let mut nodes = TextDocument::new(
9737 ["Collapsed", "Hidden", "After"]
9738 .into_iter()
9739 .map(|text| Paragraph::new(text.into(), Format::default()))
9740 .collect(),
9741 )
9742 .unwrap()
9743 .nodes()
9744 .to_vec();
9745 nodes[0].collapsed = true;
9746 nodes[1].parent = Some(nodes[0].id);
9747 nodes[1].level = 2;
9748 let outline = Outline {
9749 paragraphs: nodes.clone(),
9750 ..TextOutline::new(
9751 &mut engine,
9752 TextDocument::from_nodes(nodes.clone()).unwrap(),
9753 300.0,
9754 [0.0; 2],
9755 )
9756 .unwrap()
9757 .snapshot()
9758 };
9759 let mut editor =
9760 CanvasEditor::from_outlines(&mut engine, vec![outline], BTreeMap::new()).unwrap();
9761 let texts = |editor: &CanvasEditor| {
9762 editor
9763 .active_outline()
9764 .document
9765 .paragraphs()
9766 .map(|text| text.text().to_owned())
9767 .collect::<Vec<_>>()
9768 };
9769 let at = |paragraph, offset| [TextPosition { paragraph, offset }; 2].into();
9770 editor.select(at(2, 0)).unwrap();
9771 assert!(editor.delete(&mut engine, true).unwrap());
9772 assert_eq!(texts(&editor), ["CollapsedAfter", "Hidden"]);
9773 assert_eq!(
9774 editor.selection().positions,
9775 [TextPosition {
9776 paragraph: 0,
9777 offset: 9
9778 }; 2]
9779 );
9780 editor.undo(&mut engine).unwrap();
9781 editor.select(at(0, 9)).unwrap();
9782 assert!(editor.delete(&mut engine, false).unwrap());
9783 assert_eq!(texts(&editor), ["CollapsedAfter", "Hidden"]);
9784 editor.undo(&mut engine).unwrap();
9785 editor.select(at(0, 9)).unwrap();
9786 editor.enter(&mut engine, false).unwrap();
9787 let split = editor.active_outline().document.nodes();
9788 assert!(!split[0].collapsed && split[1].collapsed);
9789 assert_eq!(split[2].parent, Some(split[1].id));
9790 assert_eq!(
9791 editor
9792 .active_outline()
9793 .layouts()
9794 .map(|(index, _)| index)
9795 .collect::<Vec<_>>(),
9796 [0, 1, 3]
9797 );
9798 editor.undo(&mut engine).unwrap();
9799 assert_eq!(editor.active_outline().document.nodes(), nodes);
9800 }
9801
9802 #[test]
9803 #[ignore = "requires CANVAS_TEST_SECTION and CANVAS_TEST_PAGE private fixture inputs"]
9804 fn imported_editor_reflows_and_restores_native_outline_geometry() {
9805 use onestore::page::{Page, PageObject};
9806 use onestore::{RevisionIndex, Store, document::Document};
9807 let bytes = std::fs::read(std::env::var_os("CANVAS_TEST_SECTION").unwrap()).unwrap();
9808 let page = {
9809 let store = Store::parse(&bytes).unwrap();
9810 let index = RevisionIndex::parse(&store).unwrap();
9811 Page::from_document(
9812 &Document::parse(&index).unwrap(),
9813 &std::env::var("CANVAS_TEST_PAGE").unwrap(),
9814 )
9815 .unwrap()
9816 };
9817 drop(bytes);
9818 let mut engine = TextEngine::default();
9819 if let Some(font) = std::env::var_os("CANVAS_TEST_SUBSTITUTE") {
9820 engine
9821 .register_substitute(parley::fontique::Blob::new(std::sync::Arc::new(
9822 std::fs::read(font).unwrap(),
9823 )))
9824 .unwrap();
9825 }
9826 let outlines: Vec<_> = page
9827 .objects
9828 .into_iter()
9829 .filter_map(|object| match object {
9830 PageObject::Outline(outline) => Some(outline),
9831 _ => None,
9832 })
9833 .collect();
9834 let original: Vec<_> = outlines
9835 .iter()
9836 .map(|outline| {
9837 (
9838 outline.id,
9839 outline.layout.clone(),
9840 outline.paragraphs.clone(),
9841 layout_snapshot(&outline.layout(&mut engine, &page.definitions).unwrap()),
9842 )
9843 })
9844 .collect();
9845 let mut editor =
9846 CanvasEditor::from_outlines(&mut engine, outlines, page.definitions).unwrap();
9847 let mut count = 0;
9848 for (id, source_layout, nodes, expected) in original {
9849 editor.focus_outline(id).unwrap();
9850 assert_eq!(editor.active_outline().layout, source_layout);
9851 assert_eq!(layout_snapshot(&editor.active_outline().shaped), expected);
9852 let visible: Vec<_> = editor.active_outline().layouts().map(|(i, _)| i).collect();
9853 for paragraph in visible {
9854 let text = &nodes[paragraph].text().unwrap().text;
9855 let byte = text
9856 .text()
9857 .char_indices()
9858 .nth(text.text().chars().count() / 2)
9859 .map(|(byte, _)| byte)
9860 .unwrap_or(0);
9861 let position = TextPosition {
9862 paragraph,
9863 offset: text.utf16_offset(byte).unwrap(),
9864 };
9865 editor.select([position; 2].into()).unwrap();
9866 editor.insert(&mut engine, "probe ").unwrap();
9867 let edited = editor.active_outline().document.clone();
9868 let rebuilt = Outline {
9869 id,
9870 title: false,
9871 min_width: None,
9872 layout: source_layout.clone(),
9873 indents: editor.active_outline().indents.clone(),
9874 paragraphs: edited.nodes().to_vec(),
9875 unsupported: Vec::new(),
9876 }
9877 .layout(&mut engine, &editor.definitions)
9878 .unwrap();
9879 let after = layout_snapshot(&editor.active_outline().shaped);
9880 assert_eq!(after, layout_snapshot(&rebuilt));
9881 editor.undo(&mut engine).unwrap();
9882 assert_eq!(editor.active_outline().document.nodes(), nodes);
9883 assert_eq!(layout_snapshot(&editor.active_outline().shaped), expected);
9884 editor.redo(&mut engine).unwrap();
9885 assert_eq!(editor.active_outline().document, edited);
9886 assert_eq!(layout_snapshot(&editor.active_outline().shaped), after);
9887 editor.undo(&mut engine).unwrap();
9888 count += 1;
9889 }
9890 }
9891 eprintln!(
9892 "Reflowed and restored {count} visible paragraphs in {} native outlines",
9893 editor.outlines().len()
9894 );
9895 assert!(count > 0);
9896 }
9897
9898 #[test]
9899 fn visual_navigation_does_not_bounce_between_opposite_direction_paragraphs() {
9900 let mut engine = TextEngine::default();
9901 let document = TextDocument::new(
9902 ["a", "שלום", "end"]
9903 .into_iter()
9904 .map(|text| Paragraph::new(text.into(), Format::default()))
9905 .collect(),
9906 )
9907 .unwrap();
9908 let mut editor = CanvasEditor::new(&mut engine, document, 100.0).unwrap();
9909 let first = TextPosition {
9910 paragraph: 0,
9911 offset: 0,
9912 };
9913 let last = TextPosition {
9914 paragraph: 2,
9915 offset: 3,
9916 };
9917 for _ in 0..32 {
9918 editor
9919 .move_selection(&mut engine, Movement::Right, false)
9920 .unwrap();
9921 if editor.selection().positions[1] == last {
9922 break;
9923 }
9924 }
9925 assert_eq!(editor.selection().positions[1], last);
9926 for _ in 0..32 {
9927 editor
9928 .move_selection(&mut engine, Movement::Left, false)
9929 .unwrap();
9930 if editor.selection().positions[1] == first {
9931 break;
9932 }
9933 }
9934 assert_eq!(editor.selection().positions[1], first);
9935 }
9936
9937 #[test]
9938 fn styled_reversed_selection_survives_reflow_undo_and_navigation_reuses_layout() {
9939 let mut engine = TextEngine::default();
9940 let original = TextDocument::new(vec![Paragraph::from_runs([
9941 ("plain ".into(), Format::default()),
9942 (
9943 "bold text".into(),
9944 Format {
9945 bold: Some(true),
9946 ..Format::default()
9947 },
9948 ),
9949 ])])
9950 .unwrap();
9951 let mut editor = CanvasEditor::new(&mut engine, original.clone(), 100.0).unwrap();
9952 let anchor = TextPosition {
9953 paragraph: 0,
9954 offset: 10,
9955 };
9956 let focus = TextPosition {
9957 paragraph: 0,
9958 offset: 2,
9959 };
9960 editor.select([anchor, focus].into()).unwrap();
9961 editor.insert(&mut engine, "🌳").unwrap();
9962 let after = editor.active_outline().document.clone();
9963 editor.resize(&mut engine, 35.0).unwrap();
9964 editor.undo(&mut engine).unwrap();
9965 assert_eq!(editor.active_outline().document, after);
9966 assert_eq!(editor.active_outline().layout.max_width, Some(100.0));
9967 editor.undo(&mut engine).unwrap();
9968 assert_eq!(editor.active_outline().document, original);
9969 assert_eq!(editor.selection().positions, [anchor, focus]);
9970 editor.redo(&mut engine).unwrap();
9971 assert_eq!(editor.active_outline().document, after);
9972 assert_eq!(editor.selection().positions[1].offset, 4);
9973 editor.redo(&mut engine).unwrap();
9974 assert_eq!(editor.active_outline().layout.max_width, Some(35.0));
9975 let styles = editor.active_outline().shaped.paragraphs[0]
9976 .text
9977 .shaped
9978 .styles()
9979 .as_ptr();
9980 for movement in [
9981 Movement::Left,
9982 Movement::WordRight,
9983 Movement::Down,
9984 Movement::Up,
9985 Movement::LineStart,
9986 Movement::LineEnd,
9987 ] {
9988 editor.move_selection(&mut engine, movement, false).unwrap();
9989 assert_eq!(
9990 editor.active_outline().shaped.paragraphs[0]
9991 .text
9992 .shaped
9993 .styles()
9994 .as_ptr(),
9995 styles
9996 );
9997 }
9998 editor.resize(&mut engine, 35.0).unwrap();
9999 editor.select_at(15.0, 20.0, true).unwrap();
10000 assert_eq!(
10001 editor.active_outline().shaped.paragraphs[0]
10002 .text
10003 .shaped
10004 .styles()
10005 .as_ptr(),
10006 styles
10007 );
10008 }
10009
10010 #[test]
10011 fn multiline_composition_is_provisional_and_commits_as_one_structural_edit() {
10012 let mut engine = TextEngine::default();
10013 let original = TextDocument::new(vec![
10014 Paragraph::new("abcd".into(), Format::default()),
10015 Paragraph::new(
10016 "middle".into(),
10017 Format {
10018 bold: Some(true),
10019 ..Format::default()
10020 },
10021 ),
10022 Paragraph::new("tail".into(), Format::default()),
10023 Paragraph::new("untouched".into(), Format::default()),
10024 ])
10025 .unwrap();
10026 let mut editor = CanvasEditor::new(&mut engine, original.clone(), 70.0).unwrap();
10027 let untouched = editor.active_outline().shaped.paragraphs[3]
10028 .text
10029 .shaped
10030 .styles()
10031 .as_ptr();
10032 let start = TextPosition {
10033 paragraph: 0,
10034 offset: 1,
10035 };
10036 let end = TextPosition {
10037 paragraph: 2,
10038 offset: 2,
10039 };
10040 editor.select([end, start].into()).unwrap();
10041 editor.compose(&mut engine, "ni\nhao".into(), 2..3).unwrap();
10042 assert_eq!(
10043 editor.selection().positions,
10044 [
10045 TextPosition {
10046 paragraph: 0,
10047 offset: 3
10048 },
10049 TextPosition {
10050 paragraph: 1,
10051 offset: 0
10052 }
10053 ]
10054 );
10055 assert_eq!(
10056 editor.marked_range(),
10057 Some(
10058 start..TextPosition {
10059 paragraph: 1,
10060 offset: 3
10061 }
10062 )
10063 );
10064 editor
10065 .compose(&mut engine, "e\u{301}".into(), 1..1)
10066 .unwrap();
10067 assert_eq!(
10068 editor.selection().positions,
10069 [TextPosition {
10070 paragraph: 0,
10071 offset: 2
10072 }; 2]
10073 );
10074 editor.commit_text(&mut engine, "日\n本".into()).unwrap();
10075 assert_eq!(
10076 editor
10077 .active_outline()
10078 .document
10079 .paragraphs()
10080 .map(Paragraph::text)
10081 .collect::<Vec<_>>(),
10082 ["a日", "本il", "untouched"]
10083 );
10084 assert!(editor.marked_range().is_none());
10085 assert_eq!(
10086 editor.active_outline().shaped.paragraphs[2]
10087 .text
10088 .shaped
10089 .styles()
10090 .as_ptr(),
10091 untouched
10092 );
10093 editor.undo(&mut engine).unwrap();
10094 assert_eq!(editor.active_outline().document, original);
10095 assert_eq!(editor.selection().positions, [end, start]);
10096 assert!(!editor.undo(&mut engine).unwrap());
10097 editor.redo(&mut engine).unwrap();
10098 assert_eq!(
10099 editor
10100 .active_outline()
10101 .document
10102 .paragraphs()
10103 .nth(1)
10104 .unwrap()
10105 .text(),
10106 "本il"
10107 );
10108 }
10109
10110 #[test]
10111 fn composition_failure_and_cancel_preserve_source_selection_and_redo() {
10112 let mut engine = TextEngine::default();
10113 let original = TextDocument::new(vec![
10114 Paragraph::new("first".into(), Format::default()),
10115 Paragraph::new("last".into(), Format::default()),
10116 ])
10117 .unwrap();
10118 let mut editor = CanvasEditor::new(&mut engine, original.clone(), 70.0).unwrap();
10119 editor.insert(&mut engine, "!").unwrap();
10120 editor.undo(&mut engine).unwrap();
10121 let first = TextPosition {
10122 paragraph: 0,
10123 offset: 0,
10124 };
10125 let last = TextPosition {
10126 paragraph: 1,
10127 offset: 4,
10128 };
10129 editor.select([last, first].into()).unwrap();
10130 editor
10131 .compose(&mut engine, "pre\nedit".into(), 8..8)
10132 .unwrap();
10133 let provisional = editor.active_outline().document.clone();
10134 let selection = editor.selection().positions;
10135 let marked = editor.marked_range();
10136 assert!(editor.compose(&mut engine, "🌳".into(), 1..1).is_err());
10137 assert_eq!(editor.active_outline().document, provisional);
10138 assert_eq!(editor.selection().positions, selection);
10139 assert_eq!(editor.marked_range(), marked);
10140 let preview = editor.preview_resize(&mut engine, 25.0).unwrap();
10141 assert_eq!(preview.document, provisional);
10142 assert_eq!(preview.layout.max_width, Some(25.0));
10143 assert_eq!(editor.active_outline().layout.max_width, Some(70.0));
10144 editor.cancel_composition(&mut engine).unwrap();
10145 assert_eq!(editor.active_outline().document, original);
10146 assert_eq!(editor.selection().positions, [last, first]);
10147 editor.redo(&mut engine).unwrap();
10148 assert_eq!(
10149 editor
10150 .active_outline()
10151 .document
10152 .paragraphs()
10153 .next()
10154 .unwrap()
10155 .text(),
10156 "!first"
10157 );
10158 }
10159
10160 #[test]
10161 fn navigation_crosses_paragraphs_and_keeps_the_preferred_column() {
10162 let mut engine = TextEngine::default();
10163 let document = TextDocument::new(vec![
10164 Paragraph::new("abcdefghij".into(), Format::default()),
10165 Paragraph::new(String::new(), Format::default()),
10166 Paragraph::new("abcdefghij".into(), Format::default()),
10167 ])
10168 .unwrap();
10169 let mut editor = CanvasEditor::new(&mut engine, document, 200.0).unwrap();
10170 let start = TextPosition {
10171 paragraph: 0,
10172 offset: 8,
10173 };
10174 editor.select([start, start].into()).unwrap();
10175 let x = editor.caret(1.0).unwrap().x0;
10176 editor
10177 .move_selection(&mut engine, Movement::Down, false)
10178 .unwrap();
10179 assert_eq!(
10180 editor.selection().positions[1],
10181 TextPosition {
10182 paragraph: 1,
10183 offset: 0
10184 }
10185 );
10186 editor
10187 .move_selection(&mut engine, Movement::Down, false)
10188 .unwrap();
10189 assert_eq!(
10190 editor.selection().positions[1],
10191 TextPosition {
10192 paragraph: 2,
10193 offset: 8
10194 }
10195 );
10196 assert_eq!(editor.caret(1.0).unwrap().x0, x);
10197 editor
10198 .move_selection(&mut engine, Movement::Up, true)
10199 .unwrap();
10200 editor
10201 .move_selection(&mut engine, Movement::Up, true)
10202 .unwrap();
10203 assert_eq!(
10204 editor.selection().positions,
10205 [
10206 TextPosition {
10207 paragraph: 2,
10208 offset: 8
10209 },
10210 start
10211 ]
10212 );
10213 editor
10214 .move_selection(&mut engine, Movement::Left, false)
10215 .unwrap();
10216 assert_eq!(editor.selection().positions, [start; 2]);
10217 let boundary = TextPosition {
10218 paragraph: 1,
10219 offset: 0,
10220 };
10221 editor.select([boundary, boundary].into()).unwrap();
10222 editor
10223 .move_selection(&mut engine, Movement::Left, false)
10224 .unwrap();
10225 assert_eq!(
10226 editor.selection().positions[1],
10227 TextPosition {
10228 paragraph: 0,
10229 offset: 10
10230 }
10231 );
10232 editor
10233 .move_selection(&mut engine, Movement::Right, false)
10234 .unwrap();
10235 assert_eq!(editor.selection().positions[1], boundary);
10236 }
10237
10238 #[test]
10239 fn typing_enter_and_boundary_delete_form_one_reversible_history() {
10240 let mut engine = TextEngine::default();
10241 let original =
10242 TextDocument::new(vec![Paragraph::new(String::new(), Format::default())]).unwrap();
10243 let mut editor = CanvasEditor::new(&mut engine, original.clone(), 80.0).unwrap();
10244 assert!(!editor.delete(&mut engine, true).unwrap());
10245 assert!(!editor.delete(&mut engine, false).unwrap());
10246 editor.insert(&mut engine, "left\n\nright").unwrap();
10247 assert_eq!(
10248 editor
10249 .active_outline()
10250 .document
10251 .paragraphs()
10252 .map(Paragraph::text)
10253 .collect::<Vec<_>>(),
10254 ["left", "", "right"]
10255 );
10256 let point = TextPosition {
10257 paragraph: 2,
10258 offset: 0,
10259 };
10260 editor.select([point, point].into()).unwrap();
10261 editor.delete(&mut engine, true).unwrap();
10262 assert_eq!(
10263 editor
10264 .active_outline()
10265 .document
10266 .paragraphs()
10267 .map(Paragraph::text)
10268 .collect::<Vec<_>>(),
10269 ["left", "right"]
10270 );
10271 let point = TextPosition {
10272 paragraph: 0,
10273 offset: 4,
10274 };
10275 editor.select([point, point].into()).unwrap();
10276 editor.delete(&mut engine, false).unwrap();
10277 assert_eq!(
10278 editor
10279 .active_outline()
10280 .document
10281 .paragraphs()
10282 .next()
10283 .unwrap()
10284 .text(),
10285 "leftright"
10286 );
10287 for _ in 0..3 {
10288 assert!(editor.undo(&mut engine).unwrap());
10289 }
10290 assert_eq!(editor.active_outline().document, original);
10291 for _ in 0..3 {
10292 assert!(editor.redo(&mut engine).unwrap());
10293 }
10294 assert_eq!(
10295 editor
10296 .active_outline()
10297 .document
10298 .paragraphs()
10299 .next()
10300 .unwrap()
10301 .text(),
10302 "leftright"
10303 );
10304 }
10305
10306 #[test]
10307 fn grapheme_deletion_preserves_adjacent_hidden_fields() {
10308 let mut engine = TextEngine::default();
10309 for grapheme in ["e\u{301}", "👩🏽‍💻", "🇨🇦", "👩‍👩‍👧‍👦"] {
10310 let original = TextDocument::new(vec![Paragraph::from_runs([
10311 (
10312 "field".into(),
10313 Format {
10314 hidden: Some(true),
10315 ..Format::default()
10316 },
10317 ),
10318 (format!("a{grapheme}z"), Format::default()),
10319 ])])
10320 .unwrap();
10321 let mut editor = CanvasEditor::new(&mut engine, original.clone(), 80.0).unwrap();
10322 for backward in [false, true] {
10323 let offset = 6 + if backward {
10324 grapheme.encode_utf16().count() as u32
10325 } else {
10326 0
10327 };
10328 let point = TextPosition {
10329 paragraph: 0,
10330 offset,
10331 };
10332 editor.select([point, point].into()).unwrap();
10333 editor.delete(&mut engine, backward).unwrap();
10334 assert_eq!(
10335 editor
10336 .active_outline()
10337 .document
10338 .paragraphs()
10339 .next()
10340 .unwrap()
10341 .text(),
10342 "fieldaz"
10343 );
10344 assert_eq!(
10345 editor.active_outline().shaped.paragraphs[0]
10346 .projection
10347 .text()
10348 .text(),
10349 "az"
10350 );
10351 editor.undo(&mut engine).unwrap();
10352 assert_eq!(editor.active_outline().document, original);
10353 assert_eq!(editor.selection().positions, [point; 2]);
10354 }
10355 }
10356 }
10357
10358 #[test]
10359 fn pointer_selection_maps_hidden_source_and_paragraph_boundaries() {
10360 let mut engine = TextEngine::default();
10361 let text = TextDocument::new(vec![
10362 Paragraph::from_runs([
10363 ("first ".into(), Format::default()),
10364 (
10365 "hidden🌳".into(),
10366 Format {
10367 hidden: Some(true),
10368 ..Format::default()
10369 },
10370 ),
10371 ("visible words wrap here".into(), Format::default()),
10372 ]),
10373 Paragraph::new(String::new(), Format::default()),
10374 Paragraph::new("last".into(), Format::default()),
10375 ])
10376 .unwrap();
10377 let mut editor = CanvasEditor::new(&mut engine, text.clone(), 70.0).unwrap();
10378 let last = TextPosition {
10379 paragraph: 2,
10380 offset: 4,
10381 };
10382 editor.select([last, last].into()).unwrap();
10383 let caret = editor.caret(1.0).unwrap();
10384 editor
10385 .select_at(caret.x0 as f32, ((caret.y0 + caret.y1) / 2.0) as f32, false)
10386 .unwrap();
10387 assert_eq!(editor.selection().positions, [last; 2]);
10388 let first = TextPosition {
10389 paragraph: 0,
10390 offset: 0,
10391 };
10392 editor.select([last, first].into()).unwrap();
10393 let rectangles = editor.selection_rects().unwrap();
10394 assert!(
10395 rectangles
10396 .iter()
10397 .any(|r| r.y0 == f64::from(ys(editor.active_outline())[1]) && r.width() == 4.0)
10398 );
10399 editor
10400 .replace(
10401 &mut engine,
10402 vec![Paragraph::new("replacement".into(), Format::default())],
10403 )
10404 .unwrap();
10405 editor.undo(&mut engine).unwrap();
10406 assert_eq!(editor.active_outline().document, text);
10407 assert_eq!(editor.selection().positions, [last, first]);
10408
10409 let source = &editor.active_outline().shaped.paragraphs[0];
10410 let cursor = source.text.cursor(6, Affinity::Downstream);
10411 let caret = source.text.caret(cursor, 1.0);
10412 editor
10413 .select_at(caret.x0 as f32, ((caret.y0 + caret.y1) / 2.0) as f32, false)
10414 .unwrap();
10415 let selected = editor.selection().positions[1];
10416 assert!(selected.offset == 6 || selected.offset == 14);
10417 assert_eq!(editor.caret(1.0).unwrap().y0, caret.y0);
10418 }
10419
10420 #[test]
10421 fn mixed_structural_edit_history_matches_full_layout_recomputation() {
10422 let mut engine = TextEngine::default();
10423 let text = TextDocument::new(vec![
10424 Paragraph::new("one two three".into(), Format::default()),
10425 Paragraph::new("日本 🌳 e\u{301}".into(), Format::default()),
10426 ])
10427 .unwrap();
10428 let mut editor = CanvasEditor::new(&mut engine, text, 72.0).unwrap();
10429 let mut seed = 0x8d45_u64;
10430 let mut next = || {
10431 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
10432 seed as usize
10433 };
10434 let mut before = Vec::new();
10435 let mut after = Vec::new();
10436 for step in 0..64 {
10437 let positions: Vec<_> = editor
10438 .active_outline()
10439 .document
10440 .paragraphs()
10441 .enumerate()
10442 .flat_map(|(paragraph, text)| {
10443 (0..=text.text().len()).filter_map(move |byte| {
10444 text.utf16_offset(byte)
10445 .ok()
10446 .map(|offset| TextPosition { paragraph, offset })
10447 })
10448 })
10449 .collect();
10450 let anchor = positions[next() % positions.len()];
10451 let focus = positions[next() % positions.len()];
10452 editor.select([anchor, focus].into()).unwrap();
10453 before.push((
10454 editor.active_outline().document.clone(),
10455 editor.selection().positions,
10456 ));
10457 let format = Format {
10458 bold: Some(step % 2 == 0),
10459 font_size: Some(10.0 + (step % 4) as f32),
10460 space_before: Some((step % 3) as f32),
10461 space_after: Some((step % 5) as f32),
10462 line_spacing: Some(12.0 + (step % 2) as f32),
10463 ..Format::default()
10464 };
10465 let replacement = match step % 4 {
10466 0 => vec![Paragraph::new(String::new(), format); 2],
10467 1 => vec![Paragraph::new("x👩🏽‍💻".into(), format)],
10468 2 => vec![Paragraph::new(String::new(), format)],
10469 _ => vec![
10470 Paragraph::new("wrapped words".into(), format.clone()),
10471 Paragraph::new("tail".into(), format),
10472 ],
10473 };
10474 editor.replace(&mut engine, replacement).unwrap();
10475 after.push(editor.active_outline().document.clone());
10476 let rebuilt =
10477 CanvasEditor::new(&mut engine, editor.active_outline().document.clone(), 72.0)
10478 .unwrap();
10479 assert_eq!(
10480 ys(editor.active_outline()),
10481 ys(rebuilt.active_outline()),
10482 "step {step}"
10483 );
10484 for (actual, expected) in editor
10485 .active_outline()
10486 .shaped
10487 .paragraphs
10488 .iter()
10489 .zip(&rebuilt.active_outline().shaped.paragraphs)
10490 {
10491 let geometry = |p: &ParagraphLayout| {
10492 p.text
10493 .lines()
10494 .map(|(line, b)| {
10495 (
10496 b.source.clone(),
10497 b.top,
10498 b.baseline,
10499 b.height,
10500 line.metrics().advance,
10501 )
10502 })
10503 .collect::<Vec<_>>()
10504 };
10505 assert_eq!(geometry(actual), geometry(expected), "step {step}");
10506 }
10507 }
10508 for (text, selection) in before.into_iter().rev() {
10509 assert!(editor.undo(&mut engine).unwrap());
10510 assert_eq!(editor.active_outline().document, text);
10511 assert_eq!(editor.selection().positions, selection);
10512 }
10513 assert!(!editor.undo(&mut engine).unwrap());
10514 for text in after {
10515 assert!(editor.redo(&mut engine).unwrap());
10516 assert_eq!(editor.active_outline().document, text);
10517 }
10518 assert!(!editor.redo(&mut engine).unwrap());
10519 }
10520
10521 #[test]
10522 fn typing_in_place_matches_a_fresh_layout_for_automatic_and_fixed_widths() {
10523 use onestore::page::{Image, ParagraphContent, text::new_id};
10524 let mut engine = TextEngine::default();
10525 let mut seed = 0x51ed_u64;
10526 let mut next = |n: usize| {
10527 seed = seed
10528 .wrapping_mul(6364136223846793005)
10529 .wrapping_add(1442695040888963407);
10530 (seed >> 33) as usize % n
10531 };
10532 for (text, objects) in [
10533 (vec!["x"], false),
10534 (vec!["one two", "three", "four five six seven"], false),
10535 (vec!["x"], true),
10536 (vec!["one two", "three", "four five six seven"], true),
10537 ] {
10538 let fixed = text.len() > 1;
10539 let empty = TextDocument::new(vec![Paragraph::new(String::new(), Format::default())]);
10540 let mut editor = CanvasEditor::new(&mut engine, empty.unwrap(), 120.0).unwrap();
10541 editor.insert(&mut engine, &text.join("\n")).unwrap();
10542 if fixed {
10543 editor.resize(&mut engine, 120.0).unwrap();
10544 }
10545 let table = table_editor(&mut engine).active_outline().document.nodes()[0].clone();
10546 let mut picture = table.clone();
10547 picture.id = new_id().unwrap();
10548 picture.content = ParagraphContent::Image(Image {
10549 id: new_id().unwrap(),
10550 layout: onestore::document::Layout {
10551 max_width: Some(90.0),
10552 max_height: Some(40.0),
10553 ..Default::default()
10554 },
10555 size: None,
10556 bytes: None,
10557 display: None,
10558 alt: None,
10559 background: false,
10560 printout: None,
10561 tags: Vec::new(),
10562 link: None,
10563 text: None,
10564 });
10565 if objects {
10566 let selection = [TextPosition {
10567 paragraph: 0,
10568 offset: 0,
10569 }; 2]
10570 .into();
10571 editor
10572 .commit(
10573 &mut engine,
10574 DocumentEdit {
10575 columns: BTreeMap::new(),
10576 container: None,
10577 range: 1..1,
10578 replacement: vec![picture.clone(), table],
10579 },
10580 selection,
10581 )
10582 .unwrap();
10583 }
10584 for step in 0..300 {
10585 let outline = editor.active_outline();
10586 let paragraph = next(outline.document.paragraphs().count());
10587 let text = outline.document.paragraph(paragraph).unwrap();
10588 let offset = text.utf16_offset(text.text().len()).unwrap();
10589 let position = TextPosition {
10590 paragraph,
10591 offset: next(offset as usize + 1) as u32,
10592 };
10593 // Paragraphs under a collapsed one hold no caret.
10594 let _ = editor.select([position; 2].into());
10595 let nodes = editor.active_outline().document.nodes();
10596 let root = next(nodes.len());
10597 let mut node = nodes[root].clone();
10598 let edit = |range, replacement| DocumentEdit {
10599 columns: BTreeMap::new(),
10600 container: None,
10601 range,
10602 replacement,
10603 };
10604 let edit = match next(14) {
10605 10 => {
10606 node.collapsed = !node.collapsed;
10607 Some(edit(root..root + 1, vec![node]))
10608 }
10609 11 if root > 0 && node.text().is_some() => {
10610 let parent = &nodes[root - 1];
10611 node.parent = Some(parent.id);
10612 node.level = parent.level + 1;
10613 Some(edit(root..root + 1, vec![node]))
10614 }
10615 12 if node.text().is_none() || nodes.len() > 3 => {
10616 Some(edit(root..root + 1, Vec::new()))
10617 }
10618 13 => {
10619 let mut picture = picture.clone();
10620 picture.id = new_id().unwrap();
10621 let ParagraphContent::Image(image) = &mut picture.content else {
10622 unreachable!()
10623 };
10624 image.id = new_id().unwrap();
10625 Some(edit(root..root, vec![picture]))
10626 }
10627 _ => None,
10628 };
10629 let _ = match (edit, next(10)) {
10630 (Some(edit), _) => {
10631 let start = [TextPosition {
10632 paragraph: 0,
10633 offset: 0,
10634 }; 2];
10635 editor.commit(&mut engine, edit, start.into())
10636 }
10637 (None, 0) => editor.insert(&mut engine, "\n"),
10638 (None, 1 | 2) => editor.delete(&mut engine, next(2) == 0).map(drop),
10639 (None, 3) => editor.undo(&mut engine).map(drop),
10640 (None, _) => editor.insert(&mut engine, ["a", " ", "wide", "W"][next(4)]),
10641 };
10642 let outline = editor.active_outline();
10643 let fresh =
10644 TextOutline::from_outline(&mut engine, &outline.snapshot(), &BTreeMap::new())
10645 .unwrap();
10646 let layout = |outline: &TextOutline| {
10647 let shaped = &outline.shaped;
10648 let paragraphs = shaped.paragraphs.iter().map(|paragraph| {
10649 (
10650 paragraph.id,
10651 paragraph.origin,
10652 paragraph
10653 .markers
10654 .iter()
10655 .map(|marker| marker.1)
10656 .collect::<Vec<_>>(),
10657 paragraph
10658 .tags
10659 .iter()
10660 .map(|tag| tag.origin)
10661 .collect::<Vec<_>>(),
10662 )
10663 });
10664 let cells = shaped.tables.iter().flat_map(|table| {
10665 table
10666 .cells
10667 .iter()
10668 .map(|cell| (cell.id, cell.rect, cell.paragraphs.clone()))
10669 });
10670 let objects = shaped.objects.iter().map(|object| {
10671 let label = object.label().map(|label| label.origin);
10672 (object.id, object.rect, object.bottom, label)
10673 });
10674 format!(
10675 "{:?}",
10676 (
10677 shaped.size,
10678 shaped.widths,
10679 outline.bounds(),
10680 paragraphs.collect::<Vec<_>>(),
10681 cells.collect::<Vec<_>>(),
10682 objects.collect::<Vec<_>>(),
10683 )
10684 )
10685 };
10686 assert_eq!(layout(outline), layout(&fresh), "step {step}");
10687 }
10688 assert_eq!(
10689 editor.active_outline().layout.width_set_by_user,
10690 Some(fixed)
10691 );
10692 }
10693 }
10694
10695 #[test]
10696 fn structural_edits_reflow_only_affected_paragraphs_and_restore_selection() {
10697 let mut engine = TextEngine::default();
10698 let text = TextDocument::new(vec![
10699 Paragraph::new("before".into(), Format::default()),
10700 Paragraph::new("split this paragraph".into(), Format::default()),
10701 Paragraph::new("after".into(), Format::default()),
10702 ])
10703 .unwrap();
10704 let mut editor = CanvasEditor::new(&mut engine, text.clone(), 60.0).unwrap();
10705 let before = editor.active_outline().shaped.paragraphs[0]
10706 .text
10707 .shaped
10708 .styles()
10709 .as_ptr();
10710 let after = editor.active_outline().shaped.paragraphs[2]
10711 .text
10712 .shaped
10713 .styles()
10714 .as_ptr();
10715 editor.resize(&mut engine, 60.0).unwrap();
10716 assert_eq!(
10717 editor.active_outline().shaped.paragraphs[0]
10718 .text
10719 .shaped
10720 .styles()
10721 .as_ptr(),
10722 before
10723 );
10724 let point = TextPosition {
10725 paragraph: 1,
10726 offset: 6,
10727 };
10728 editor.select([point, point].into()).unwrap();
10729 editor
10730 .replace(
10731 &mut engine,
10732 vec![Paragraph::new(String::new(), Format::default()); 2],
10733 )
10734 .unwrap();
10735 assert_eq!(
10736 editor
10737 .active_outline()
10738 .document
10739 .paragraphs()
10740 .map(Paragraph::text)
10741 .collect::<Vec<_>>(),
10742 ["before", "split ", "this paragraph", "after"]
10743 );
10744 assert_eq!(
10745 editor.selection().positions,
10746 [TextPosition {
10747 paragraph: 2,
10748 offset: 0
10749 }; 2]
10750 );
10751 assert_eq!(
10752 editor.active_outline().shaped.paragraphs[0]
10753 .text
10754 .shaped
10755 .styles()
10756 .as_ptr(),
10757 before
10758 );
10759 assert_eq!(
10760 editor.active_outline().shaped.paragraphs[3]
10761 .text
10762 .shaped
10763 .styles()
10764 .as_ptr(),
10765 after
10766 );
10767 let rebuilt =
10768 CanvasEditor::new(&mut engine, editor.active_outline().document.clone(), 60.0).unwrap();
10769 assert_eq!(ys(editor.active_outline()), ys(rebuilt.active_outline()));
10770 for (actual, expected) in editor
10771 .active_outline()
10772 .shaped
10773 .paragraphs
10774 .iter()
10775 .zip(&rebuilt.active_outline().shaped.paragraphs)
10776 {
10777 let lines = |p: &ParagraphLayout| {
10778 p.text
10779 .lines()
10780 .map(|(_, b)| (b.source.clone(), b.top, b.baseline, b.height))
10781 .collect::<Vec<_>>()
10782 };
10783 assert_eq!(lines(actual), lines(expected));
10784 }
10785 editor.undo(&mut engine).unwrap();
10786 assert_eq!(editor.active_outline().document, text);
10787 assert_eq!(editor.selection().positions, [point; 2]);
10788 assert_eq!(
10789 editor.active_outline().shaped.paragraphs[2]
10790 .text
10791 .shaped
10792 .styles()
10793 .as_ptr(),
10794 after
10795 );
10796 editor.redo(&mut engine).unwrap();
10797 assert_eq!(
10798 editor.active_outline().document,
10799 rebuilt.active_outline().document
10800 );
10801 let selection = editor.selection().positions;
10802 let previous_width = editor.active_outline().layout.max_width.unwrap();
10803 editor.resize(&mut engine, 30.0).unwrap();
10804 let rebuilt =
10805 CanvasEditor::new(&mut engine, editor.active_outline().document.clone(), 30.0).unwrap();
10806 assert_eq!(ys(editor.active_outline()), ys(rebuilt.active_outline()));
10807 assert_eq!(editor.selection().positions, selection);
10808 assert!(editor.resize(&mut engine, f32::NAN).is_err());
10809 assert_eq!(ys(editor.active_outline()), ys(rebuilt.active_outline()));
10810 editor.undo(&mut engine).unwrap();
10811 assert_eq!(
10812 editor.active_outline().layout.max_width,
10813 Some(previous_width)
10814 );
10815 let rebuilt = CanvasEditor::new(
10816 &mut engine,
10817 editor.active_outline().document.clone(),
10818 previous_width,
10819 )
10820 .unwrap();
10821 assert_eq!(ys(editor.active_outline()), ys(rebuilt.active_outline()));
10822 }
10823
10824 #[test]
10825 fn failed_layout_preserves_text_selection_history_and_cached_layout() {
10826 let mut engine = TextEngine::default();
10827 let text =
10828 TextDocument::new(vec![Paragraph::new("original".into(), Format::default())]).unwrap();
10829 let mut editor = CanvasEditor::new(&mut engine, text.clone(), 60.0).unwrap();
10830 editor
10831 .replace(
10832 &mut engine,
10833 vec![Paragraph::new("!".into(), Format::default())],
10834 )
10835 .unwrap();
10836 editor.undo(&mut engine).unwrap();
10837 let before = editor.active_outline().shaped.paragraphs[0]
10838 .text
10839 .shaped
10840 .styles()
10841 .as_ptr();
10842 for format in [
10843 Format {
10844 font_size: Some(f32::NAN),
10845 ..Format::default()
10846 },
10847 Format {
10848 line_spacing: Some(-1.0),
10849 ..Format::default()
10850 },
10851 Format {
10852 space_after: Some(f32::INFINITY),
10853 ..Format::default()
10854 },
10855 ] {
10856 assert!(
10857 editor
10858 .replace(&mut engine, vec![Paragraph::new("bad".into(), format)])
10859 .is_err()
10860 );
10861 assert_eq!(editor.active_outline().document, text);
10862 assert_eq!(
10863 editor.selection().positions,
10864 [TextPosition {
10865 paragraph: 0,
10866 offset: 0
10867 }; 2]
10868 );
10869 assert_eq!(
10870 editor.active_outline().shaped.paragraphs[0]
10871 .text
10872 .shaped
10873 .styles()
10874 .as_ptr(),
10875 before
10876 );
10877 }
10878 assert!(editor.redo(&mut engine).unwrap());
10879 assert_eq!(
10880 editor
10881 .active_outline()
10882 .document
10883 .paragraphs()
10884 .next()
10885 .unwrap()
10886 .text(),
10887 "!original"
10888 );
10889 }
10890 #[test]
10891 fn outline_creation_movement_and_typing_share_one_history() {
10892 let mut engine = TextEngine::default();
10893 let original =
10894 TextDocument::new(vec![Paragraph::new("lyrics".into(), Format::default())]).unwrap();
10895 let mut editor = CanvasEditor::new(&mut engine, original.clone(), 240.0).unwrap();
10896 let first = editor.active_outline().id;
10897 editor.insert(&mut engine, "A ").unwrap();
10898 let second = editor
10899 .create_outline(&mut engine, [300.0, 0.0], 120.0)
10900 .unwrap();
10901 editor
10902 .insert(&mut engine, "annotation\nsecond line")
10903 .unwrap();
10904 let annotation = editor.active_outline().document.clone();
10905 let pointers: Vec<_> = editor
10906 .outlines
10907 .iter()
10908 .map(|outline| outline.shaped.paragraphs[0].text.shaped.styles().as_ptr())
10909 .collect();
10910 editor.move_outline(first, [-50.0, 60.0]).unwrap();
10911 assert_eq!(editor.active_outline().id, first);
10912 assert_eq!(
10913 editor
10914 .outlines
10915 .iter()
10916 .map(|outline| outline.shaped.paragraphs[0].text.shaped.styles().as_ptr())
10917 .collect::<Vec<_>>(),
10918 pointers
10919 );
10920 assert_eq!(editor.outlines[1].document, annotation);
10921 editor.undo(&mut engine).unwrap();
10922 assert_eq!(editor.active_outline().origin(), [0.0; 2]);
10923 editor.undo(&mut engine).unwrap();
10924 assert_eq!(editor.active_outline().id, second);
10925 assert_eq!(
10926 editor
10927 .active_outline()
10928 .document
10929 .paragraphs()
10930 .next()
10931 .unwrap()
10932 .text(),
10933 ""
10934 );
10935 let retained = editor.active_outline().shaped.paragraphs[0]
10936 .text
10937 .shaped
10938 .styles()
10939 .as_ptr();
10940 editor.undo(&mut engine).unwrap();
10941 assert_eq!(editor.outlines.len(), 1);
10942 assert_eq!(editor.active_outline().id, first);
10943 editor.undo(&mut engine).unwrap();
10944 assert_eq!(editor.active_outline().document, original);
10945 assert!(!editor.undo(&mut engine).unwrap());
10946 editor.redo(&mut engine).unwrap();
10947 editor.redo(&mut engine).unwrap();
10948 assert_eq!(editor.active_outline().id, second);
10949 assert_eq!(
10950 editor.active_outline().shaped.paragraphs[0]
10951 .text
10952 .shaped
10953 .styles()
10954 .as_ptr(),
10955 retained
10956 );
10957 editor.redo(&mut engine).unwrap();
10958 assert_eq!(editor.active_outline().document, annotation);
10959 editor.redo(&mut engine).unwrap();
10960 assert_eq!(editor.active_outline().id, first);
10961 assert_eq!(editor.active_outline().origin(), [-50.0, 60.0]);
10962 assert!(!editor.redo(&mut engine).unwrap());
10963 }
10964
10965 #[test]
10966 fn invalid_outline_operations_preserve_composition_and_history() {
10967 let mut engine = TextEngine::default();
10968 let original =
10969 TextDocument::new(vec![Paragraph::new("original".into(), Format::default())]).unwrap();
10970 let mut editor = CanvasEditor::new(&mut engine, original.clone(), 240.0).unwrap();
10971 let first = editor.active_outline().id;
10972 editor.compose(&mut engine, "é".into(), 1..1).unwrap();
10973 let provisional = editor.active_outline().document.clone();
10974 for position in [[f32::NAN, 0.0], [0.0, f32::INFINITY]] {
10975 assert!(editor.create_outline(&mut engine, position, 120.0).is_err());
10976 assert!(editor.move_outline(first, position).is_err());
10977 }
10978 assert!(editor.create_outline(&mut engine, [0.0; 2], 0.0).is_err());
10979 assert!(editor.focus_outline(onestore::ExGuid::default()).is_err());
10980 assert!(
10981 editor
10982 .move_outline(onestore::ExGuid::default(), [1.0; 2])
10983 .is_err()
10984 );
10985 assert_eq!(editor.active_outline().document, provisional);
10986 assert!(editor.marked_range().is_some());
10987 assert!(editor.undo.is_empty());
10988 assert!(editor.redo.is_empty());
10989 let second = editor
10990 .create_outline(&mut engine, [300.0, 0.0], 120.0)
10991 .unwrap();
10992 assert!(editor.marked_range().is_none());
10993 editor.undo(&mut engine).unwrap();
10994 assert_eq!(editor.active_outline().document, provisional);
10995 editor.undo(&mut engine).unwrap();
10996 assert_eq!(editor.active_outline().document, original);
10997 editor.redo(&mut engine).unwrap();
10998 let replacement = editor
10999 .create_outline(&mut engine, [500.0, 0.0], 120.0)
11000 .unwrap();
11001 assert_ne!(replacement, second);
11002 assert!(!editor.redo(&mut engine).unwrap());
11003 assert_eq!(editor.outlines[0].document, provisional);
11004 }
11005 #[test]
11006 fn mixed_outline_history_restores_every_source_and_placement() {
11007 let mut engine = TextEngine::default();
11008 let original = TextDocument::new(vec![Paragraph::from_runs([
11009 (
11010 "hidden".into(),
11011 Format {
11012 hidden: Some(true),
11013 ..Format::default()
11014 },
11015 ),
11016 ("lyrics 🌳".into(), Format::default()),
11017 ])])
11018 .unwrap();
11019 let mut editor = CanvasEditor::new(&mut engine, original, 120.0).unwrap();
11020 let snapshot = |editor: &CanvasEditor| {
11021 editor
11022 .outlines
11023 .iter()
11024 .map(|outline| (outline.id, outline.layout.clone(), outline.document.clone()))
11025 .collect::<Vec<_>>()
11026 };
11027 let mut steps = Vec::new();
11028 let mut random = 0x29a7_u32;
11029 for step in 0..80 {
11030 random = random.wrapping_mul(1664525).wrapping_add(1013904223);
11031 let index = random as usize % editor.outlines.len();
11032 let id = editor.outlines[index].id;
11033 editor.focus_outline(id).unwrap();
11034 let before = snapshot(&editor);
11035 let undo_count = editor.undo.len();
11036 match step % 5 {
11037 0 if editor.outlines.len() < 8 => {
11038 editor
11039 .create_outline(
11040 &mut engine,
11041 [step as f32 * 9.0 - 100.0, step as f32 * 3.0],
11042 72.0 + step as f32,
11043 )
11044 .unwrap();
11045 }
11046 1 => {
11047 editor
11048 .insert(
11049 &mut engine,
11050 ["x", "é", "🌳", "a\nשלום"][random as usize % 4],
11051 )
11052 .unwrap();
11053 }
11054 2 => {
11055 editor.delete(&mut engine, random & 1 == 0).unwrap();
11056 }
11057 3 => {
11058 editor
11059 .compose(&mut engine, "e\u{301}".into(), 2..2)
11060 .unwrap();
11061 editor.finish_composition();
11062 }
11063 _ => {
11064 let [x, y] = editor.active_outline().origin();
11065 editor.move_outline(id, [x + 7.0, y - 3.0]).unwrap();
11066 }
11067 }
11068 if editor.undo.len() == undo_count {
11069 assert_eq!(snapshot(&editor), before);
11070 continue;
11071 }
11072 assert_eq!(editor.undo.len(), undo_count + 1);
11073 steps.push((before, snapshot(&editor)));
11074 for outline in &editor.outlines {
11075 let rebuilt = TextOutline::new(
11076 &mut engine,
11077 outline.document.clone(),
11078 outline.layout.max_width.unwrap(),
11079 outline.origin(),
11080 )
11081 .unwrap();
11082 assert_eq!(ys(outline), ys(&rebuilt));
11083 for (actual, expected) in outline
11084 .shaped
11085 .paragraphs
11086 .iter()
11087 .zip(&rebuilt.shaped.paragraphs)
11088 {
11089 assert_eq!(actual.projection.text(), expected.projection.text());
11090 assert_eq!(actual.text.height(), expected.text.height());
11091 assert_eq!(
11092 actual
11093 .text
11094 .lines()
11095 .map(|(_, line)| (line.source.clone(), line.baseline))
11096 .collect::<Vec<_>>(),
11097 expected
11098 .text
11099 .lines()
11100 .map(|(_, line)| (line.source.clone(), line.baseline))
11101 .collect::<Vec<_>>()
11102 );
11103 }
11104 }
11105 }
11106 assert!(steps.len() > 60);
11107 for (before, _) in steps.iter().rev() {
11108 assert!(editor.undo(&mut engine).unwrap());
11109 assert_eq!(&snapshot(&editor), before);
11110 }
11111 assert!(!editor.undo(&mut engine).unwrap());
11112 for (_, after) in &steps {
11113 assert!(editor.redo(&mut engine).unwrap());
11114 assert_eq!(&snapshot(&editor), after);
11115 }
11116 assert!(!editor.redo(&mut engine).unwrap());
11117 }
11118
11119 const CORPUS: [&[u8]; 3] = [
11120 include_bytes!("../../../corpus/canvas/baseline-anchors.one"),
11121 include_bytes!("../../../corpus/outline-edit/before/notebook/synthetic.one"),
11122 include_bytes!("../../../corpus/paragraph-edit/before/notebook/synthetic.one"),
11123 ];
11124
11125 fn corpus_pages(section: &[u8]) -> Vec<(ExGuid, Page)> {
11126 use onestore::{RevisionIndex, Store, document::Document};
11127 let store = Store::parse(section).unwrap();
11128 let index = RevisionIndex::parse(&store).unwrap();
11129 let document = Document::parse(&index).unwrap();
11130 let mut spaces = document
11131 .pages()
11132 .unwrap()
11133 .into_iter()
11134 .map(|(space, _)| space)
11135 .collect::<Vec<_>>();
11136 spaces.dedup();
11137 spaces
11138 .into_iter()
11139 .map(|space| (space, Page::from_space(&document, space).unwrap()))
11140 .collect()
11141 }
11142
11143 fn corpus_page(section: &[u8], title: &str) -> (ExGuid, Page) {
11144 corpus_pages(section)
11145 .into_iter()
11146 .find(|(_, page)| page.title == title)
11147 .unwrap()
11148 }
11149
11150 fn body_text(page: &Page, id: ExGuid) -> Option<String> {
11151 page.objects.iter().find_map(|object| match object {
11152 PageObject::Outline(outline) if outline.id == id => Some(
11153 outline
11154 .paragraphs
11155 .iter()
11156 .map(|paragraph| paragraph.text().unwrap().text.text())
11157 .collect(),
11158 ),
11159 _ => None,
11160 })
11161 }
11162
11163 #[test]
11164 fn an_unedited_page_rebuilds_into_the_model_it_was_imported_from() {
11165 let mut pages = 0;
11166 for section in CORPUS {
11167 for (_, page) in corpus_pages(section) {
11168 let mut engine = TextEngine::default();
11169 let editor = CanvasEditor::from_page(page.clone(), &mut engine).unwrap();
11170 assert_eq!(editor.page().unwrap(), page);
11171 pages += 1;
11172 }
11173 }
11174 assert_eq!(pages, 30);
11175 }
11176
11177 #[test]
11178 fn typing_moving_resizing_adding_and_deleting_reach_the_rebuilt_page() {
11179 let mut engine = TextEngine::default();
11180 let (_, source) = corpus_page(CORPUS[0], "Baseline anchors");
11181 let mut editor = CanvasEditor::from_page(source.clone(), &mut engine).unwrap();
11182 let removed = editor
11183 .outlines
11184 .iter()
11185 .find(|outline| {
11186 let [node] = outline.document.nodes() else {
11187 return false;
11188 };
11189 !outline.title
11190 && node.lists.is_empty()
11191 && node.tags.is_empty()
11192 && node.text().is_some_and(|text| text.tags.is_empty())
11193 })
11194 .unwrap()
11195 .id;
11196 let bodies = editor
11197 .outlines
11198 .iter()
11199 .filter(|outline| !outline.title && outline.id != removed)
11200 .map(|outline| outline.id)
11201 .collect::<Vec<_>>();
11202 editor.focus_outline(bodies[0]).unwrap();
11203 editor.insert(&mut engine, "typed").unwrap();
11204 editor.move_outline(bodies[1], [123.0, 456.0]).unwrap();
11205 editor.focus_outline(bodies[2]).unwrap();
11206 editor.resize(&mut engine, 200.0).unwrap();
11207 editor.focus_outline(removed).unwrap();
11208 editor.select_all().unwrap();
11209 assert!(editor.delete(&mut engine, false).unwrap());
11210 let added = editor
11211 .create_outline(&mut engine, [24.0, 600.0], 300.0)
11212 .unwrap();
11213 editor.insert(&mut engine, "added").unwrap();
11214
11215 let page = editor.page().unwrap();
11216 assert_eq!(page.title, source.title);
11217 assert_eq!(page.created, source.created);
11218 assert_eq!(page.margin_origin, source.margin_origin);
11219 assert!(body_text(&page, bodies[0]).unwrap().starts_with("typed"));
11220 let moved = page
11221 .objects
11222 .iter()
11223 .find_map(|object| match object {
11224 PageObject::Outline(outline) if outline.id == bodies[1] => Some(&outline.layout),
11225 _ => None,
11226 })
11227 .unwrap();
11228 assert_eq!([moved.x, moved.y], [Some(123.0), Some(456.0)]);
11229 let resized = page
11230 .objects
11231 .iter()
11232 .find_map(|object| match object {
11233 PageObject::Outline(outline) if outline.id == bodies[2] => Some(&outline.layout),
11234 _ => None,
11235 })
11236 .unwrap();
11237 assert_eq!(resized.max_width, Some(200.0));
11238 assert_eq!(resized.width_set_by_user, Some(true));
11239 assert!(body_text(&page, removed).is_none());
11240 assert_eq!(body_text(&page, added).as_deref(), Some("added"));
11241 assert_eq!(page.objects.last().unwrap().id(), added);
11242 assert_eq!(
11243 page.objects.iter().map(PageObject::id).collect::<Vec<_>>(),
11244 source
11245 .objects
11246 .iter()
11247 .map(PageObject::id)
11248 .filter(|id| *id != removed)
11249 .chain([added])
11250 .collect::<Vec<_>>()
11251 );
11252 assert_eq!(
11253 page.objects
11254 .iter()
11255 .find(|object| matches!(object, PageObject::Title(_))),
11256 source
11257 .objects
11258 .iter()
11259 .find(|object| matches!(object, PageObject::Title(_)))
11260 );
11261
11262 let mut engine = TextEngine::default();
11263 let reimported = CanvasEditor::from_page(page.clone(), &mut engine).unwrap();
11264 assert_eq!(reimported.page().unwrap(), page);
11265 }
11266
11267 #[test]
11268 fn an_edited_page_writes_back_as_its_ops() {
11269 let mut engine = TextEngine::default();
11270 let (space, source) = corpus_page(CORPUS[2], "Split middle");
11271 let mut editor = CanvasEditor::from_page(source.clone(), &mut engine).unwrap();
11272 let body = editor
11273 .outlines
11274 .iter()
11275 .find(|outline| !outline.title)
11276 .unwrap()
11277 .id;
11278 editor.focus_outline(body).unwrap();
11279 editor.insert(&mut engine, "Edited ").unwrap();
11280 let page = editor.page().unwrap();
11281 let written = ops::saved(CORPUS[2], space, &mut editor);
11282 let reread = corpus_pages(&written)
11283 .into_iter()
11284 .find_map(|(candidate, page)| (candidate == space).then_some(page))
11285 .unwrap();
11286 let edited = body_text(&page, body).unwrap();
11287 assert!(edited.starts_with("Edited "));
11288 assert_eq!(body_text(&reread, body).as_deref(), Some(edited.as_str()));
11289 assert_ne!(body_text(&source, body).as_deref(), Some(edited.as_str()));
11290 }
11291
11292 /// A page changed elsewhere shows in place: the caret keeps its paragraph and moves
11293 /// past text inserted before it; history goes, unless nothing changed.
11294 #[test]
11295 fn a_refresh_keeps_the_caret_by_identity() {
11296 let mut engine = TextEngine::default();
11297 let lines =
11298 ["Hello world", "Second"].map(|line| Paragraph::new(line.into(), Format::default()));
11299 let document = TextDocument::new(lines.to_vec()).unwrap();
11300 let mut editor = CanvasEditor::new(&mut engine, document, 400.0).unwrap();
11301 editor
11302 .select(
11303 [TextPosition {
11304 paragraph: 1,
11305 offset: 3,
11306 }; 2]
11307 .into(),
11308 )
11309 .unwrap();
11310 editor.insert(&mut engine, "x").unwrap();
11311 editor
11312 .select(
11313 [TextPosition {
11314 paragraph: 0,
11315 offset: 8,
11316 }; 2]
11317 .into(),
11318 )
11319 .unwrap();
11320 let _ = editor.take_ops();
11321 let page = editor.page().unwrap();
11322 assert!(!editor.refresh(page.clone(), &mut engine).unwrap());
11323 assert!(!editor.undo.is_empty(), "an unchanged page keeps history");
11324
11325 let mut remote = page;
11326 let PageObject::Outline(outline) = &mut remote.objects[0] else {
11327 unreachable!()
11328 };
11329 for (paragraph, text) in outline
11330 .paragraphs
11331 .iter_mut()
11332 .zip(["Hey, Hello world", "Other"])
11333 {
11334 paragraph.text_mut().unwrap().text = Paragraph::new(text.into(), Format::default());
11335 }
11336 let (shown, affinities) = (editor.active_outline().id, editor.selection().affinities);
11337 assert!(editor.refresh(remote, &mut engine).unwrap());
11338 assert_eq!(editor.active_outline().id, shown);
11339 assert_eq!(
11340 editor.selection(),
11341 Selection {
11342 positions: [TextPosition {
11343 paragraph: 0,
11344 offset: 13
11345 }; 2],
11346 affinities,
11347 }
11348 );
11349 assert!(editor.undo.is_empty());
11350 assert_eq!(editor.take_ops().unwrap(), []);
11351 }
11352
11353 /// Alpha, Parent > Child > Grand, a table [[A1 A2 | B1], [C1 | D1]], Omega: text leaves 0..=9.
11354 fn select_more_editor(engine: &mut TextEngine, collapsed: bool) -> CanvasEditor {
11355 use onestore::page::{ParagraphContent, Table, TableCell, TableColumn, TableRow};
11356 let text = |text: &str| {
11357 crate::document::node(
11358 Paragraph::new(text.into(), Format::default()),
11359 Format::default(),
11360 )
11361 .unwrap()
11362 };
11363 let child = |label: &str, parent: &PageParagraph| PageParagraph {
11364 parent: Some(parent.id),
11365 level: parent.level + 1,
11366 ..text(label)
11367 };
11368 let parent = PageParagraph {
11369 collapsed,
11370 ..text("Parent")
11371 };
11372 let middle = child("Child", &parent);
11373 let grand = child("Grand", &middle);
11374 let mut table = text("");
11375 table.content = ParagraphContent::Table(Table {
11376 id: onestore::page::text::new_id().unwrap(),
11377 columns: vec![
11378 TableColumn {
11379 width: 72.0,
11380 locked: true,
11381 };
11382 2
11383 ],
11384 rows: [[&["A1", "A2"][..], &["B1"]], [&["C1"], &["D1"]]]
11385 .into_iter()
11386 .map(|cells| TableRow {
11387 id: onestore::page::text::new_id().unwrap(),
11388 cells: cells
11389 .into_iter()
11390 .map(|texts| TableCell {
11391 id: onestore::page::text::new_id().unwrap(),
11392 layout: Default::default(),
11393 indents: vec![18.0, 0.0, 27.0],
11394 shading: None,
11395 paragraphs: texts.iter().map(|t| text(t)).collect(),
11396 unsupported: Vec::new(),
11397 })
11398 .collect(),
11399 })
11400 .collect(),
11401 borders: Some(true),
11402 layout: Default::default(),
11403 tags: Vec::new(),
11404 });
11405 let nodes = vec![text("Alpha"), parent, middle, grand, table, text("Omega")];
11406 CanvasEditor::new(engine, TextDocument::from_nodes(nodes).unwrap(), 300.0).unwrap()
11407 }
11408
11409 /// The selections successive `widen_selection` calls give from `start` to `end`.
11410 fn widenings(
11411 editor: &mut CanvasEditor,
11412 start: (usize, u32),
11413 end: (usize, u32),
11414 ) -> Vec<[(usize, u32); 2]> {
11415 let position = |(paragraph, offset)| TextPosition { paragraph, offset };
11416 editor
11417 .select([position(start), position(end)].into())
11418 .unwrap();
11419 (0..6)
11420 .map(|_| {
11421 editor.widen_selection().unwrap();
11422 editor
11423 .selection()
11424 .positions
11425 .map(|position| (position.paragraph, position.offset))
11426 })
11427 .collect()
11428 }
11429
11430 #[test]
11431 fn select_all_widens_as_onenote_does() {
11432 let mut engine = TextEngine::default();
11433 let mut editor = select_more_editor(&mut engine, false);
11434 let outline = [(0, 0), (9, 5)];
11435 let steps = |steps: &[[(usize, u32); 2]]| {
11436 let mut steps = steps.to_vec();
11437 steps.resize(6, outline);
11438 steps
11439 };
11440 // A paragraph takes its break when a sibling follows, then the outline.
11441 assert_eq!(
11442 widenings(&mut editor, (0, 2), (0, 2)),
11443 steps(&[[(0, 0), (1, 0)]])
11444 );
11445 // A parent, then its subtree, then the outline.
11446 assert_eq!(
11447 widenings(&mut editor, (1, 2), (1, 2)),
11448 steps(&[[(1, 0), (2, 0)], [(1, 0), (3, 5)]])
11449 );
11450 // A child widens to its own subtree, never its parent's.
11451 assert_eq!(
11452 widenings(&mut editor, (2, 1), (2, 1)),
11453 steps(&[[(2, 0), (3, 0)], [(2, 0), (3, 5)]])
11454 );
11455 assert_eq!(
11456 widenings(&mut editor, (3, 1), (3, 1)),
11457 steps(&[[(3, 0), (3, 5)]])
11458 );
11459 // In a table: paragraph, cell, row, table.
11460 assert_eq!(
11461 widenings(&mut editor, (4, 1), (4, 1)),
11462 steps(&[
11463 [(4, 0), (5, 0)],
11464 [(4, 0), (5, 2)],
11465 [(4, 0), (6, 2)],
11466 [(4, 0), (8, 2)],
11467 ])
11468 );
11469 // A cell holding one paragraph adds nothing to it.
11470 assert_eq!(
11471 widenings(&mut editor, (6, 1), (6, 1)),
11472 steps(&[[(6, 0), (6, 2)], [(4, 0), (6, 2)], [(4, 0), (8, 2)]])
11473 );
11474 assert_eq!(
11475 widenings(&mut editor, (9, 1), (9, 1)),
11476 steps(&[[(9, 0), (9, 5)]])
11477 );
11478 }
11479
11480 #[test]
11481 fn select_all_starts_from_the_current_selection() {
11482 let mut engine = TextEngine::default();
11483 let mut editor = select_more_editor(&mut engine, false);
11484 let first = |editor: &mut CanvasEditor, start, end| widenings(editor, start, end)[0];
11485 // Part or all of a paragraph's text widens to the paragraph.
11486 assert_eq!(first(&mut editor, (0, 1), (0, 3)), [(0, 0), (1, 0)]);
11487 assert_eq!(first(&mut editor, (0, 0), (0, 5)), [(0, 0), (1, 0)]);
11488 assert_eq!(first(&mut editor, (0, 3), (0, 1)), [(0, 0), (1, 0)]);
11489 // Across paragraphs, the outline; across cells of a row, the row.
11490 assert_eq!(first(&mut editor, (0, 1), (1, 1)), [(0, 0), (9, 5)]);
11491 assert_eq!(first(&mut editor, (5, 1), (6, 1)), [(4, 0), (6, 2)]);
11492 assert_eq!(first(&mut editor, (6, 1), (7, 1)), [(4, 0), (8, 2)]);
11493 // A caret moved after widening starts over at its paragraph.
11494 assert_eq!(first(&mut editor, (9, 1), (9, 1)), [(9, 0), (9, 5)]);
11495 // Selection only: nothing to undo or store.
11496 assert!(editor.undo.is_empty());
11497 assert_eq!(editor.take_ops().unwrap(), []);
11498 }
11499
11500 #[test]
11501 fn select_all_skips_the_hidden_children_of_a_collapsed_parent() {
11502 let mut engine = TextEngine::default();
11503 let mut editor = select_more_editor(&mut engine, true);
11504 assert_eq!(
11505 widenings(&mut editor, (1, 2), (1, 2))[..2],
11506 [[(1, 0), (1, 6)], [(0, 0), (9, 5)]]
11507 );
11508 }
11509
11510 #[test]
11511 fn select_all_in_a_single_paragraph_stops_at_the_paragraph() {
11512 let mut engine = TextEngine::default();
11513 let mut editor = CanvasEditor::new(
11514 &mut engine,
11515 TextDocument::new(vec![Paragraph::new("Title".into(), Format::default())]).unwrap(),
11516 180.0,
11517 )
11518 .unwrap();
11519 assert_eq!(
11520 widenings(&mut editor, (0, 5), (0, 5)),
11521 [[(0, 0), (0, 5)]; 6]
11522 );
11523 }
11524
11525 #[test]
11526 fn pasting_over_the_page_selection_is_one_step_that_undo_selects_again() {
11527 let mut engine = TextEngine::default();
11528 let lines = ["One", "Two"].map(|line| Paragraph::new(line.into(), Format::default()));
11529 let mut editor = CanvasEditor::new(
11530 &mut engine,
11531 TextDocument::new(lines.to_vec()).unwrap(),
11532 400.0,
11533 )
11534 .unwrap();
11535 editor.select_page().unwrap();
11536 editor
11537 .paste_pieces(
11538 &mut engine,
11539 vec![
11540 html_pieces("Pasted", 0x409, |_, _| None).pop().unwrap(),
11541 Piece::Picture(vec![0; 8], [10.0; 2]),
11542 ],
11543 )
11544 .unwrap();
11545 assert_eq!(editor.history_depth(), [1, 0]);
11546 let _ = editor.take_ops();
11547 let mut remote = editor.page().unwrap();
11548 remote.color = Some(0x00ff_ffff);
11549 editor.refresh(remote, &mut engine).unwrap();
11550 assert!(editor.undo(&mut engine).unwrap());
11551 assert_eq!(editor.whole(), Some(Whole::Page));
11552 }
11553}