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 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 pub fn layouts(&self) -> impl Iterator<Item = (usize, &ParagraphLayout)> {
432 let mut shaped = self.shaped.paragraphs.iter().peekable();
433 self.document
434 .text_nodes()
435 .enumerate()
436 .filter_map(move |(index, node)| {
437 if shaped
438 .peek()
439 .is_some_and(|paragraph| paragraph.id == node.id)
440 {
441 Some((index, shaped.next().unwrap()))
442 } else {
443 None
444 }
445 })
446 }
447
448 /// Layouts follow the text leaves minus hidden ones, so an index shared by a leaf and its
449 /// layout needs no search.
450 fn visible_index(&self, source: usize) -> Result<usize, EditError> {
451 let id = self
452 .document
453 .leaf(source)
454 .ok_or(EditError::InvalidRange)?
455 .2
456 .id;
457 if self
458 .shaped
459 .paragraphs
460 .get(source)
461 .is_some_and(|paragraph| paragraph.id == id)
462 {
463 return Ok(source);
464 }
465 self.shaped
466 .paragraphs
467 .iter()
468 .position(|paragraph| paragraph.id == id)
469 .ok_or(EditError::InvalidRange)
470 }
471
472 fn source_index(&self, visible: usize) -> usize {
473 let id = self.shaped.paragraphs[visible].id;
474 if self
475 .document
476 .leaf(visible)
477 .is_some_and(|(_, _, node)| node.id == id)
478 {
479 return visible;
480 }
481 self.document
482 .text_nodes()
483 .position(|paragraph| paragraph.id == id)
484 .unwrap()
485 }
486
487 /// The start of the paragraph after visible paragraph `index` when it follows in the same
488 /// container, where a whole-paragraph selection ends so that deleting it takes the break.
489 fn next_sibling_start(&self, index: usize) -> Option<TextPosition> {
490 let next =
491 (index + 1 < self.shaped.paragraphs.len()).then(|| self.source_index(index + 1))?;
492 let (container, local, _) = self.document.leaf(self.source_index(index))?;
493 let (next_container, next_local, _) = self.document.leaf(next)?;
494 (container == next_container && next_local == local + 1).then_some(TextPosition {
495 paragraph: next,
496 offset: 0,
497 })
498 }
499
500 pub fn paragraph_layout(&self, source: usize) -> Result<&ParagraphLayout, EditError> {
501 Ok(&self.shaped.paragraphs[self.visible_index(source)?])
502 }
503
504 /// The shown paragraphs with their source text: those a collapsed parent hides have no
505 /// layout and are left out.
506 fn shown(&self) -> impl Iterator<Item = (usize, &Paragraph)> {
507 self.layouts()
508 .filter_map(|(index, _)| Some((index, self.document.paragraph(index)?)))
509 }
510
511 /// The outline's text as platform text input sees it: the shown paragraphs joined by
512 /// `\n`, addressed in UTF-16 units by `utf16_offset` and `utf16_position`.
513 pub fn shown_text(&self) -> String {
514 self.shown()
515 .map(|(_, paragraph)| paragraph.text())
516 .collect::<Vec<_>>()
517 .join("\n")
518 }
519
520 /// All the outline's shown text.
521 pub(crate) fn whole(&self) -> Selection {
522 let paragraph = self.source_index(self.shaped.paragraphs.len() - 1);
523 let last = self.document.paragraph(paragraph).unwrap();
524 [
525 TextPosition {
526 paragraph: 0,
527 offset: 0,
528 },
529 TextPosition {
530 paragraph,
531 offset: last
532 .utf16_offset(last.text().len())
533 .expect("paragraph text fits UTF-16 offsets"),
534 },
535 ]
536 .into()
537 }
538
539 /// The caret at `focus`, in the outline's coordinates.
540 pub fn caret_at(
541 &self,
542 focus: TextPosition,
543 affinity: Affinity,
544 width: f32,
545 ) -> Result<BoundingBox, EditError> {
546 let paragraph = self.paragraph_layout(focus.paragraph)?;
547 let cursor = paragraph.cursor(focus.offset, affinity)?;
548 let mut rect = paragraph.text.caret(cursor, width);
549 rect.x0 += f64::from(paragraph.origin[0]);
550 rect.x1 += f64::from(paragraph.origin[0]);
551 rect.y0 += f64::from(paragraph.origin[1]);
552 rect.y1 += f64::from(paragraph.origin[1]);
553 Ok(rect)
554 }
555
556 /// Highlight rectangles of `selection` in the outline, in its coordinates.
557 pub fn range_rects(&self, selection: Selection) -> Result<Vec<BoundingBox>, EditError> {
558 let [anchor, focus] = selection.positions;
559 if anchor == focus {
560 return Ok(Vec::new());
561 }
562 let (start, end, affinities) = if anchor < focus {
563 (anchor, focus, selection.affinities)
564 } else {
565 (
566 focus,
567 anchor,
568 [selection.affinities[1], selection.affinities[0]],
569 )
570 };
571 let mut rectangles = Vec::new();
572 for (visible, (index, paragraph)) in self
573 .layouts()
574 .enumerate()
575 .filter(|(_, (index, _))| *index >= start.paragraph && *index <= end.paragraph)
576 {
577 let source = self.document.paragraph(index).unwrap();
578 let first = if index == start.paragraph {
579 paragraph.cursor(start.offset, affinities[0])?
580 } else {
581 paragraph.cursor(0, Affinity::Downstream)?
582 };
583 let last = if index == end.paragraph {
584 paragraph.cursor(end.offset, affinities[1])?
585 } else {
586 paragraph.cursor(
587 source.utf16_offset(source.text().len())?,
588 Affinity::Upstream,
589 )?
590 };
591 let mut local = paragraph
592 .text
593 .selection(ParagraphSelection::new(first, last));
594 if index < end.paragraph {
595 local.push(paragraph.text.caret(last, 4.0));
596 }
597 for mut rect in local {
598 rect.x0 += f64::from(paragraph.origin[0]);
599 rect.x1 += f64::from(paragraph.origin[0]);
600 rect.y0 += f64::from(paragraph.origin[1]);
601 rect.y1 += f64::from(paragraph.origin[1]);
602 if let Some(cell) = self.shaped.paragraph_cell(visible) {
603 let Some(clipped) = cell.clip(rect) else {
604 continue;
605 };
606 rect = clipped;
607 }
608 rectangles.push(rect);
609 }
610 }
611 Ok(rectangles)
612 }
613
614 /// Where a mark under UTF-16 `range` of paragraph `index` runs, outline-local: each
615 /// line's left, right and baseline.
616 pub(crate) fn underlines(
617 &self,
618 index: usize,
619 range: Range<u32>,
620 ) -> Result<Vec<[f32; 3]>, EditError> {
621 let paragraph = self.paragraph_layout(index)?;
622 let selection = ParagraphSelection::new(
623 paragraph.cursor(range.start, Affinity::Downstream)?,
624 paragraph.cursor(range.end, Affinity::Upstream)?,
625 );
626 let [x, y] = paragraph.origin;
627 Ok(paragraph
628 .text
629 .underlines(selection)
630 .into_iter()
631 .map(|[left, right, baseline]| [left + x, right + x, baseline + y])
632 .collect())
633 }
634
635 /// Where `position` falls in `shown_text`; a hidden paragraph falls at the start of the
636 /// next shown one.
637 pub fn utf16_offset(&self, position: TextPosition) -> Result<u32, EditError> {
638 let mut offset = 0;
639 for (index, paragraph) in self.shown() {
640 if index == position.paragraph {
641 return Ok(offset + position.offset);
642 }
643 if index > position.paragraph {
644 return Ok(offset);
645 }
646 offset += paragraph.utf16_offset(paragraph.text().len())? + 1;
647 }
648 Ok(offset.saturating_sub(1))
649 }
650
651 /// The source position at `offset` in `shown_text`, clamped to its end.
652 pub fn utf16_position(&self, offset: u32) -> Result<TextPosition, EditError> {
653 let mut rest = offset;
654 let mut end = None;
655 for (index, paragraph) in self.shown() {
656 let length = paragraph.utf16_offset(paragraph.text().len())?;
657 if rest <= length {
658 return Ok(TextPosition {
659 paragraph: index,
660 offset: rest,
661 });
662 }
663 rest -= length + 1;
664 end = Some(TextPosition {
665 paragraph: index,
666 offset: length,
667 });
668 }
669 end.ok_or(EditError::InvalidRange)
670 }
671
672 fn column(&self, x: f32, current: ExGuid) -> Result<Vec<usize>, EditError> {
673 let paragraphs = self
674 .shaped
675 .paragraphs
676 .iter()
677 .enumerate()
678 .map(|(index, paragraph)| (paragraph.id, index))
679 .collect::<BTreeMap<_, _>>();
680 let tables = self
681 .shaped
682 .tables
683 .iter()
684 .map(|table| (table.id, table))
685 .collect::<BTreeMap<_, _>>();
686 let mut result = Vec::new();
687 let mut pending = vec![visible_paragraphs(self.document.nodes().iter())];
688 while let Some(nodes) = pending.last_mut() {
689 let Some(node) = nodes.next() else {
690 pending.pop();
691 continue;
692 };
693 match &node.content {
694 onestore::page::ParagraphContent::Text(_) => {
695 result.push(
696 *paragraphs
697 .get(&node.id)
698 .ok_or(EditError::InvalidStructure)?,
699 );
700 }
701 onestore::page::ParagraphContent::Table(table) => {
702 let layout = tables.get(&table.id).ok_or(EditError::InvalidStructure)?;
703 let cells = layout
704 .cells
705 .get(..table.columns.len())
706 .ok_or(EditError::InvalidStructure)?;
707 let (first, last) = cells
708 .first()
709 .zip(cells.last())
710 .ok_or(EditError::InvalidStructure)?;
711 let x = x.clamp(first.rect[0], last.rect[2]);
712 let column = cells
713 .iter()
714 .enumerate()
715 .min_by(|(_, a), (_, b)| {
716 let distance = |cell: &crate::outline::CellLayout| {
717 (f64::from(x) - f64::from(x.clamp(cell.rect[0], cell.rect[2])))
718 .abs()
719 };
720 distance(a).total_cmp(&distance(b))
721 })
722 .ok_or(EditError::InvalidStructure)?
723 .0;
724 for row in table.rows.iter().rev() {
725 let cell = row
726 .cells
727 .iter()
728 .find(|cell| {
729 leaves(&cell.paragraphs, None)
730 .any(|(_, _, node)| node.id == current)
731 })
732 .unwrap_or(&row.cells[column]);
733 pending.push(visible_paragraphs(cell.paragraphs.iter()));
734 }
735 }
736 onestore::page::ParagraphContent::Image(_)
737 | onestore::page::ParagraphContent::Attachment(_)
738 | onestore::page::ParagraphContent::Ink(_)
739 | onestore::page::ParagraphContent::Unsupported(_) => {}
740 }
741 }
742 Ok(result)
743 }
744
745 fn paragraph_at(&self, x: f32, y: f32) -> Result<usize, EditError> {
746 if self.shaped.tables.is_empty() {
747 return Ok(self
748 .shaped
749 .paragraphs
750 .partition_point(|paragraph| paragraph.origin[1] <= y)
751 .saturating_sub(1));
752 }
753 let paragraphs = self
754 .shaped
755 .paragraphs
756 .iter()
757 .enumerate()
758 .map(|(index, paragraph)| (paragraph.id, (index, paragraph)))
759 .collect::<BTreeMap<_, _>>();
760 let tables = self
761 .shaped
762 .tables
763 .iter()
764 .map(|table| (table.id, table))
765 .collect::<BTreeMap<_, _>>();
766 let mut nodes = self.document.nodes();
767 loop {
768 let mut target = None;
769 for node in visible_paragraphs(nodes.iter()) {
770 let top = match &node.content {
771 onestore::page::ParagraphContent::Text(_) => {
772 paragraphs
773 .get(&node.id)
774 .ok_or(EditError::InvalidStructure)?
775 .1
776 .origin[1]
777 }
778 onestore::page::ParagraphContent::Table(table) => {
779 tables
780 .get(&table.id)
781 .and_then(|layout| layout.cells.first())
782 .ok_or(EditError::InvalidStructure)?
783 .rect[1]
784 - 1.86
785 }
786 // Objects hold no caret; text around them takes the hit.
787 onestore::page::ParagraphContent::Image(_)
788 | onestore::page::ParagraphContent::Attachment(_)
789 | onestore::page::ParagraphContent::Ink(_)
790 | onestore::page::ParagraphContent::Unsupported(_) => continue,
791 };
792 if target.is_none() || top <= y {
793 target = Some(node);
794 }
795 if top > y {
796 break;
797 }
798 }
799 let target = target.ok_or(EditError::InvalidStructure)?;
800 let onestore::page::ParagraphContent::Table(table) = &target.content else {
801 return paragraphs
802 .get(&target.id)
803 .map(|(index, _)| *index)
804 .ok_or(EditError::InvalidStructure);
805 };
806 let layout = tables.get(&table.id).ok_or(EditError::InvalidStructure)?;
807 let (first, last) = layout
808 .cells
809 .first()
810 .zip(layout.cells.last())
811 .ok_or(EditError::InvalidStructure)?;
812 let x = x.clamp(first.rect[0], last.rect[2]);
813 let y = y.clamp(first.rect[1], last.rect[3]);
814 let cell = layout
815 .cells
816 .iter()
817 .map(|cell| {
818 let dx = f64::from(x) - f64::from(x.clamp(cell.rect[0], cell.rect[2]));
819 let dy = f64::from(y) - f64::from(y.clamp(cell.rect[1], cell.rect[3]));
820 (cell, dy.abs(), dx.abs())
821 })
822 .min_by(|a, b| a.1.total_cmp(&b.1).then(a.2.total_cmp(&b.2)))
823 .ok_or(EditError::InvalidStructure)?
824 .0;
825 nodes = self.document.container(Some(cell.id))?;
826 }
827 }
828}
829
830struct Composition {
831 original: TextChange,
832 range: Range<TextPosition>,
833}
834
835impl ParagraphLayout {
836 fn cursor(&self, source: u32, affinity: Affinity) -> Result<Cursor, EditError> {
837 let visible = self.projection.visible_offset(source)?;
838 let byte = self.projection.text().byte_offset(visible)?;
839 Ok(self.text.cursor(byte, affinity))
840 }
841}
842
843#[derive(Clone)]
844struct TextChange {
845 edit: DocumentEdit,
846 selection: Selection,
847 positions: Vec<Placement>,
848}
849
850#[derive(Clone)]
851struct Placement {
852 id: ExGuid,
853 position: [Option<f32>; 2],
854}
855
856#[derive(Clone)]
857enum RestoreFocus {
858 Outline(ExGuid),
859 Caret {
860 source: Box<Outline>,
861 selection: Selection,
862 index: usize,
863 },
864}
865
866enum History {
867 Date(Box<PageDate>),
868 /// The page's colour and rule lines before a change to either.
869 Paper {
870 color: Option<u32>,
871 rule_lines: Option<onestore::page::RuleLines>,
872 },
873 Draft {
874 outlines: Box<[TextOutline; 2]>,
875 index: usize,
876 caret: Box<Outline>,
877 selection: Selection,
878 restore_caret: bool,
879 },
880 Text {
881 outline: onestore::ExGuid,
882 change: Box<TextChange>,
883 },
884 /// A page object's position: an outline, picture or ink drawing.
885 Position {
886 object: onestore::ExGuid,
887 position: [Option<f32>; 2],
888 },
889 /// Parts `outline`'s paragraphs from root node `at` into outline `part` at `position`.
890 Split {
891 outline: ExGuid,
892 at: usize,
893 part: ExGuid,
894 position: [f32; 2],
895 },
896 /// Returns `part`'s paragraphs to the end of `outline` and removes it: each root of
897 /// `part` under the parent it had, the outline when `None`.
898 Join {
899 outline: ExGuid,
900 part: ExGuid,
901 parents: Vec<(ExGuid, Option<ExGuid>)>,
902 },
903 Layout {
904 outline: ExGuid,
905 layout: onestore::document::Layout,
906 },
907 Image {
908 image: ExGuid,
909 layout: onestore::document::Layout,
910 },
911 /// Restores the picture at `index` in paint order, or removes the one there when `None`.
912 Picture {
913 index: usize,
914 image: Option<Box<onestore::page::Image>>,
915 },
916 /// Restores the file on the page at `index` in paint order, or removes the one there
917 /// when `None`.
918 File {
919 index: usize,
920 file: Option<Box<page::Content>>,
921 },
922 /// Restores the drawing on the page at `index` in paint order, or removes the one there
923 /// when `None`.
924 Ink {
925 index: usize,
926 ink: Option<Box<onestore::page::Ink>>,
927 },
928 /// Gives a picture or file on the page back the note tags it had.
929 Tags {
930 object: ExGuid,
931 tags: Vec<onestore::document::Tag>,
932 },
933 /// Gives a drawing back the strokes it had.
934 Strokes {
935 ink: ExGuid,
936 strokes: Vec<onestore::page::InkStroke>,
937 },
938 Remove {
939 outline: onestore::ExGuid,
940 focus: RestoreFocus,
941 },
942 Insert {
943 index: usize,
944 outline: Box<TextOutline>,
945 focus: RestoreFocus,
946 },
947 Restore {
948 index: usize,
949 source: Box<Outline>,
950 selection: Selection,
951 focus: RestoreFocus,
952 },
953 /// One undo step of several edits, undone last first; `page` when they edited OneNote's
954 /// page selection, which undoing selects again.
955 Group {
956 entries: Vec<History>,
957 page: bool,
958 },
959}
960
961impl CanvasEditor {
962 pub fn new(
963 engine: &mut TextEngine,
964 document: TextDocument,
965 width: f32,
966 ) -> Result<Self, EditorError> {
967 let outline = TextOutline::new(engine, document, width, [0.0; 2])?;
968 let empty = outline.is_empty();
969 let mut editor = Self::from_text_outlines(vec![outline], BTreeMap::new(), None)?;
970 if empty {
971 editor.active = Focus::Caret {
972 outline: Box::new(editor.outlines.pop().unwrap()),
973 index: 0,
974 };
975 }
976 Ok(editor)
977 }
978
979 pub fn from_page(mut page: Page, engine: &mut TextEngine) -> Result<Self, EditorError> {
980 let stored = page.clone();
981 let page::Import {
982 objects,
983 mut outlines,
984 date,
985 areas,
986 provisional,
987 } = page::build(&mut page, engine, true)?;
988 let needs_caret = outlines.is_empty();
989 if needs_caret {
990 let x = objects
991 .iter()
992 .filter_map(|object| match object {
993 page::Content::ReadOnly(object) => Some(object.rect()[2]),
994 page::Content::File { .. } => object.file().map(|(_, rect)| rect[2]),
995 page::Content::Ink(ink) => page::ink_bounds(ink).map(|b| b[2]),
996 _ => None,
997 })
998 .fold(0.0_f32, f32::max);
999 outlines.push(TextOutline::new(
1000 engine,
1001 TextDocument::new(vec![Paragraph::new(String::new(), Default::default())])?,
1002 240.0,
1003 [if x > 0.0 { x + 24.0 } else { 0.0 }, 0.0],
1004 )?);
1005 }
1006 let mut editor = Self::from_text_outlines(outlines, page.definitions, date)?;
1007 if needs_caret {
1008 editor.active = Focus::Caret {
1009 outline: Box::new(editor.outlines.pop().unwrap()),
1010 index: 0,
1011 };
1012 }
1013 editor.objects = objects;
1014 editor.provisional = provisional
1015 .into_iter()
1016 .map(|(outline, blank)| (outline, (blank, false)))
1017 .collect();
1018 editor.header = PageHeader {
1019 title: page.title,
1020 identity: page.identity,
1021 created: page.created,
1022 margin_origin: page.margin_origin,
1023 rtl: page.rtl,
1024 color: page.color,
1025 rule_lines: page.rule_lines,
1026 areas,
1027 };
1028 let mut ids = BTreeSet::new();
1029 if !editor.object_layouts().all(|(id, _)| ids.insert(id)) {
1030 return Err(EditError::InvalidStructure.into());
1031 }
1032 editor.stored = Some((stored, Vec::new()));
1033 Ok(editor)
1034 }
1035
1036 /// Shows `page`, the stored page after a change made elsewhere. What the change did not
1037 /// reach stays as it is, history included: the page as stored before it (as last read,
1038 /// with the ops handed out since) is compared with `page`; outlines the change reached
1039 /// show anew with the caret and selection kept by paragraph identity, and history entries
1040 /// editing them are dropped. False when the change reached nothing shown. Marked text
1041 /// must be committed or cancelled first.
1042 pub fn refresh(&mut self, page: Page, engine: &mut TextEngine) -> Result<bool, EditorError> {
1043 let known = self.stored.take().and_then(|(mut stored, sent)| {
1044 sent.iter()
1045 .try_for_each(|op| onestore::op::predict(&mut stored, op))
1046 .ok()
1047 .map(|()| stored)
1048 });
1049 // Without that, the editor's page stands in, which storage may normalize apart.
1050 let known = match known {
1051 Some(known) => known,
1052 None => self.page()?,
1053 };
1054 self.stored = Some((page.clone(), Vec::new()));
1055 if known == page {
1056 return Ok(false);
1057 }
1058 fn outlines(page: &Page) -> BTreeMap<ExGuid, &Outline> {
1059 page.objects
1060 .iter()
1061 .flat_map(|object| match object {
1062 PageObject::Outline(outline) => std::slice::from_ref(outline),
1063 PageObject::Title(title) => title.outlines.as_slice(),
1064 _ => &[],
1065 })
1066 .map(|outline| (outline.id, outline))
1067 .collect()
1068 }
1069 let (before, after) = (outlines(&known), outlines(&page));
1070 let changed: BTreeSet<ExGuid> = before
1071 .keys()
1072 .chain(after.keys())
1073 .filter(|id| before.get(id) != after.get(id))
1074 .copied()
1075 .collect();
1076 // Everything but outline content: the page's objects, geometry and date.
1077 let frame = |page: &Page| {
1078 let hollow = |outline: &Outline| Outline {
1079 id: outline.id,
1080 title: false,
1081 min_width: None,
1082 layout: Default::default(),
1083 indents: Vec::new(),
1084 paragraphs: Vec::new(),
1085 unsupported: Vec::new(),
1086 };
1087 let objects: Vec<PageObject> = page
1088 .objects
1089 .iter()
1090 .map(|object| match object {
1091 PageObject::Outline(outline) => PageObject::Outline(hollow(outline)),
1092 PageObject::Title(title) => PageObject::Title(Title {
1093 id: title.id,
1094 date: title.date,
1095 layout: title.layout.clone(),
1096 outlines: title.outlines.iter().map(hollow).collect(),
1097 }),
1098 object => object.clone(),
1099 })
1100 .collect();
1101 (page.created, page.margin_origin, objects)
1102 };
1103 let objects_changed = frame(&known) != frame(&page);
1104 let date_changed = known.created != page.created
1105 || self
1106 .date
1107 .as_ref()
1108 .is_some_and(|date| changed.contains(&date.source().id));
1109 let mut fresh = Self::from_page(page, engine)?;
1110 let shown = self.active_outline();
1111 let (id, selection) = (shown.id, shown.selection);
1112 let reached = changed.contains(&id);
1113 let document = reached.then(|| shown.document.clone());
1114 let mut own: BTreeMap<ExGuid, TextOutline> = std::mem::take(&mut self.outlines)
1115 .into_iter()
1116 .map(|outline| (outline.id, outline))
1117 .collect();
1118 for outline in &mut fresh.outlines {
1119 if !changed.contains(&outline.id)
1120 && let Some(kept) = own.remove(&outline.id)
1121 {
1122 *outline = kept;
1123 match self.provisional.remove(&outline.id) {
1124 Some(provisional) => fresh.provisional.insert(outline.id, provisional),
1125 None => fresh.provisional.remove(&outline.id),
1126 };
1127 }
1128 }
1129 let position = fresh.outlines.iter().position(|outline| outline.id == id);
1130 match (
1131 std::mem::replace(&mut self.active, Focus::Outline(0)),
1132 position,
1133 ) {
1134 (Focus::Caret { outline, index }, _) => {
1135 fresh.active = Focus::Caret {
1136 outline,
1137 index: index.min(fresh.outlines.len()),
1138 };
1139 }
1140 (Focus::Draft { outline, .. }, Some(index)) if !reached => {
1141 fresh.active = Focus::Draft { index, outline };
1142 }
1143 (_, Some(index)) if !reached => fresh.active = Focus::Outline(index),
1144 (_, Some(index)) => {
1145 fresh.active = Focus::Outline(index);
1146 let document = document.expect("a reached outline's document was kept");
1147 let mapped = Selection {
1148 positions: selection.positions.map(|position| {
1149 follow(&document, &fresh.outlines[index].document, position)
1150 }),
1151 affinities: selection.affinities,
1152 };
1153 let _ = fresh.select(mapped);
1154 self.pending = self.pending.take().filter(|(_, at, _)| {
1155 follow(&document, &fresh.active_outline().document, *at) == *at
1156 });
1157 }
1158 (_, None) => self.pending = None,
1159 }
1160 if !date_changed {
1161 fresh.date = self.date.take();
1162 }
1163 if !objects_changed {
1164 fresh.objects = std::mem::take(&mut self.objects);
1165 }
1166 for (id, definition) in std::mem::take(&mut self.definitions) {
1167 fresh.definitions.entry(id).or_insert(definition);
1168 }
1169 // Undoing an outline's creation focuses the outline focused before, if it is there.
1170 let gone = |focus: &RestoreFocus| matches!(focus, RestoreFocus::Outline(id) if !fresh.outlines.iter().any(|o| o.id == *id));
1171 let reaches_one = |history: &History| match history {
1172 History::Date(_) => date_changed,
1173 History::Paper { .. } => false,
1174 History::Image { .. }
1175 | History::Tags { .. }
1176 | History::Picture { .. }
1177 | History::File { .. }
1178 | History::Ink { .. }
1179 | History::Strokes { .. } => objects_changed,
1180 History::Draft { outlines, .. } => changed.contains(&outlines[0].id),
1181 History::Text { outline, change } => {
1182 changed.contains(outline)
1183 || change
1184 .positions
1185 .iter()
1186 .any(|placement| objects_changed || changed.contains(&placement.id))
1187 }
1188 History::Position { object, .. } => changed.contains(object) || objects_changed,
1189 History::Layout { outline, .. } => changed.contains(outline),
1190 History::Split { outline, part, .. } | History::Join { outline, part, .. } => {
1191 changed.contains(outline) || changed.contains(part) || objects_changed
1192 }
1193 History::Remove { outline, focus } => changed.contains(outline) || gone(focus),
1194 History::Insert { outline, focus, .. } => changed.contains(&outline.id) || gone(focus),
1195 History::Restore { source, focus, .. } => changed.contains(&source.id) || gone(focus),
1196 History::Group { .. } => unreachable!("groups do not nest"),
1197 };
1198 let reaches = |history: &History| match history {
1199 History::Group { entries, .. } => entries.iter().any(reaches_one),
1200 history => reaches_one(history),
1201 };
1202 // A typing run goes on while the change leaves its undo step alone.
1203 let run = self
1204 .typing
1205 .filter(|_| self.undo.last().is_some_and(|last| !reaches(last)));
1206 fresh.undo = std::mem::take(&mut self.undo)
1207 .into_iter()
1208 .filter(|history| !reaches(history))
1209 .collect();
1210 fresh.typing = run.map(|(outline, caret, _)| (outline, caret, fresh.undo.len()));
1211 fresh.redo = std::mem::take(&mut self.redo)
1212 .into_iter()
1213 .filter(|history| !reaches(history))
1214 .collect();
1215 fresh.pending = self.pending.take();
1216 fresh.preferred_x = self.preferred_x;
1217 fresh.ops = std::mem::replace(&mut self.ops, Ok(Vec::new()));
1218 fresh.stored = self.stored.take();
1219 fresh.default_font = std::mem::take(&mut self.default_font);
1220 fresh.styles = std::mem::take(&mut self.styles);
1221 fresh.markdown = self.markdown.take();
1222 *self = fresh;
1223 Ok(true)
1224 }
1225
1226 /// Lays every outline out again, keeping its text and selection, as after fonts arrived
1227 /// that draw characters the old layout had no face for.
1228 pub fn relayout(&mut self, engine: &mut TextEngine) -> Result<(), EditorError> {
1229 for outline in &mut self.outlines {
1230 outline.shaped =
1231 TextOutline::from_outline(engine, &outline.snapshot(), &self.definitions)?.shaped;
1232 }
1233 Ok(())
1234 }
1235
1236 /// Rebuilds the stored page, restoring the title areas and read-only objects import split up.
1237 pub fn page(&self) -> Result<Page, EditorError> {
1238 let mut objects: Vec<PageObject> = Vec::new();
1239 for content in &self.objects {
1240 let mut outline = match content {
1241 page::Content::Editable(id) => {
1242 match self.outlines.iter().find(|outline| outline.id == *id) {
1243 Some(outline) => {
1244 let mut outline = outline.snapshot();
1245 if let Some((blank, false)) = self.provisional.get(id) {
1246 outline.paragraphs.retain(|node| node.id != blank.id);
1247 }
1248 outline
1249 }
1250 None => continue,
1251 }
1252 }
1253 page::Content::Outline { source, .. } => source.clone(),
1254 page::Content::Date { .. } => self
1255 .date
1256 .as_ref()
1257 .ok_or(EditError::InvalidStructure)?
1258 .source()
1259 .clone(),
1260 page::Content::Image(image) => {
1261 objects.push(PageObject::Image(image.clone()));
1262 continue;
1263 }
1264 page::Content::File { source, .. } => {
1265 objects.push(PageObject::Attachment(source.clone()));
1266 continue;
1267 }
1268 page::Content::Ink(ink) => {
1269 objects.push(PageObject::Ink(ink.clone()));
1270 continue;
1271 }
1272 page::Content::ReadOnly(object) => {
1273 objects.push(object.source.clone());
1274 continue;
1275 }
1276 };
1277 let area = self
1278 .header
1279 .areas
1280 .iter()
1281 .find_map(|area| Some((area, *area.origins.get(&outline.id)?)));
1282 let Some((area, origin)) = area else {
1283 objects.push(PageObject::Outline(outline));
1284 continue;
1285 };
1286 [outline.layout.x, outline.layout.y] = origin;
1287 match objects.last_mut() {
1288 Some(PageObject::Title(title)) if title.id == area.id => {
1289 title.outlines.push(outline)
1290 }
1291 _ => objects.push(PageObject::Title(Title {
1292 id: area.id,
1293 date: area.date,
1294 layout: area.layout.clone(),
1295 outlines: vec![outline],
1296 })),
1297 }
1298 }
1299 objects.extend(
1300 self.outlines
1301 .iter()
1302 .filter(|outline| !self.has_page_outline(outline.id))
1303 .map(|outline| PageObject::Outline(outline.snapshot())),
1304 );
1305 // Definitions stay for undo after their last paragraph lets go; the page, as
1306 // OneNote stores it, holds only those its paragraphs reference.
1307 let mut referenced = BTreeSet::new();
1308 for object in &objects {
1309 let tags = match object {
1310 PageObject::Image(image) => image.tags.as_slice(),
1311 PageObject::Attachment(file) => file.tags.as_slice(),
1312 _ => &[],
1313 };
1314 referenced.extend(tags.iter().filter_map(|tag| tag.definition));
1315 let outlines = match object {
1316 PageObject::Outline(outline) => std::slice::from_ref(outline),
1317 PageObject::Title(title) => title.outlines.as_slice(),
1318 _ => &[],
1319 };
1320 for (_, _, node) in outlines
1321 .iter()
1322 .flat_map(|outline| descendants(&outline.paragraphs, None))
1323 {
1324 referenced.extend(ops::references(node));
1325 }
1326 }
1327 let mut definitions = self.definitions.clone();
1328 definitions.retain(|id, _| referenced.contains(id));
1329 Ok(Page {
1330 title: self.header.title.clone(),
1331 identity: self.header.identity,
1332 created: self
1333 .date
1334 .as_ref()
1335 .map(PageDate::timestamp)
1336 .or(self.header.created),
1337 margin_origin: self.header.margin_origin,
1338 rtl: self.header.rtl,
1339 color: self.header.color,
1340 rule_lines: self.header.rule_lines,
1341 objects,
1342 definitions,
1343 })
1344 }
1345
1346 /// Whether this outline occupies a slot in the imported page's paint order.
1347 pub fn has_page_outline(&self, id: ExGuid) -> bool {
1348 self.objects
1349 .iter()
1350 .any(|object| matches!(object, page::Content::Editable(candidate) if *candidate == id))
1351 }
1352
1353 pub fn object_layouts(&self) -> impl Iterator<Item = (ExGuid, &onestore::document::Layout)> {
1354 self.outlines
1355 .iter()
1356 .map(|outline| (outline.id, &outline.layout))
1357 .chain(
1358 self.date
1359 .iter()
1360 .map(|date| (date.source().id, &date.source().layout)),
1361 )
1362 .chain(self.objects.iter().filter_map(page::Content::layout))
1363 }
1364
1365 fn object_layout_mut(&mut self, id: ExGuid) -> Option<&mut onestore::document::Layout> {
1366 self.outlines
1367 .iter_mut()
1368 .map(|outline| (outline.id, &mut outline.layout))
1369 .chain(
1370 self.objects
1371 .iter_mut()
1372 .filter_map(page::Content::layout_mut),
1373 )
1374 .find_map(|(candidate, layout)| (candidate == id).then_some(layout))
1375 }
1376
1377 fn header_bottom(&self, title: ExGuid, text_bottom: f32) -> f32 {
1378 self.objects
1379 .iter()
1380 .filter_map(|object| match object {
1381 page::Content::Outline {
1382 source,
1383 layout,
1384 below_title,
1385 } if *below_title == Some(title) => {
1386 Some(text_bottom + source.layout.y.unwrap_or(0.0) + layout.size[1])
1387 }
1388 page::Content::Date { below_title } if *below_title == Some(title) => {
1389 self.date.as_ref().map(|date| {
1390 text_bottom + date.source().layout.y.unwrap_or(0.0) + date.layout().size[1]
1391 })
1392 }
1393 _ => None,
1394 })
1395 .fold(text_bottom, f32::max)
1396 }
1397
1398 /// Where the caret goes on leaving `title`: the first body line under its header, on
1399 /// OneNote's 18-point grid.
1400 fn body_start_of(&self, title: &TextOutline) -> [f32; 2] {
1401 let bottom = self.header_bottom(title.id, title.bounds().y1 as f32);
1402 [
1403 title.origin()[0],
1404 ((((f64::from(bottom) - 14.4) / 18.0).ceil() + 1.0) * 18.0 + 14.4) as f32,
1405 ]
1406 }
1407
1408 /// The page's colour (View, Page Color), COLORREF.
1409 pub fn page_color(&self) -> Option<u32> {
1410 self.header.color
1411 }
1412
1413 /// The page's notebook-management identity, which internal links name.
1414 pub fn identity(&self) -> Option<[u8; 16]> {
1415 self.header.identity
1416 }
1417
1418 /// The page's rule lines (View, Rule Lines).
1419 pub fn rule_lines(&self) -> Option<onestore::page::RuleLines> {
1420 self.header.rule_lines
1421 }
1422
1423 /// Where the page's first body line starts, in points, under its title.
1424 pub fn body_start(&self) -> Option<[f32; 2]> {
1425 let title = self
1426 .visible_outlines()
1427 .chain(self.caret_outline())
1428 .find(|outline| outline.title)?;
1429 Some(self.body_start_of(title))
1430 }
1431
1432 pub fn leave_title(&mut self, engine: &mut TextEngine) -> Result<(), EditorError> {
1433 let title = self.active_outline();
1434 if !title.title {
1435 return Ok(());
1436 }
1437 let position = self.body_start_of(title);
1438 if let Some((id, origin)) = self
1439 .outlines()
1440 .iter()
1441 .rev()
1442 .find(|outline| {
1443 let bounds = outline.bounds();
1444 let [x, y] = position.map(f64::from);
1445 !outline.title
1446 && x >= bounds.x0
1447 && x <= bounds.x1
1448 && y >= bounds.y0
1449 && y <= bounds.y1
1450 })
1451 .map(|outline| (outline.id, outline.origin()))
1452 {
1453 self.focus_outline(id)?;
1454 self.select_at(position[0] - origin[0], position[1] - origin[1], false)?;
1455 } else {
1456 self.place_caret(engine, position, DEFAULT_OUTLINE_WIDTH)?;
1457 }
1458 Ok(())
1459 }
1460
1461 fn title_flow(&self, height: f32) -> Result<Vec<Placement>, EditorError> {
1462 let title = self.active_outline();
1463 if !title.title {
1464 return Ok(Vec::new());
1465 }
1466 // Native flow uses text height even when the stored title bounds are taller.
1467 let old = self.header_bottom(title.id, title.origin()[1] + title.shaped.size[1]);
1468 let new = self.header_bottom(title.id, title.origin()[1] + height);
1469 if !old.is_finite() || !new.is_finite() {
1470 return Err(EditorError::InvalidGeometry);
1471 }
1472 if new <= old {
1473 return Ok(Vec::new());
1474 }
1475 let left = title.origin()[0] - 36.0;
1476 let right = title.origin()[0] + title.wrap_width() + 36.0;
1477 let destination = new + 11.52;
1478 let bodies = || {
1479 self.outlines
1480 .iter()
1481 .filter(|outline| !outline.title)
1482 .map(|outline| {
1483 let rect = outline.bounds();
1484 (
1485 outline.id,
1486 &outline.layout,
1487 [
1488 rect.x0 as f32,
1489 rect.y0 as f32,
1490 rect.x1 as f32,
1491 rect.y1 as f32,
1492 ],
1493 )
1494 })
1495 .chain(self.objects.iter().filter_map(|object| match object {
1496 page::Content::Image(image) if !image.background => {
1497 let x = image.layout.x.unwrap_or(0.0);
1498 let y = image.layout.y.unwrap_or(0.0);
1499 let [width, height] = crate::outline::image_size(image)?;
1500 Some((image.id, &image.layout, [x, y, x + width, y + height]))
1501 }
1502 page::Content::ReadOnly(object)
1503 if !matches!(object.source, onestore::page::PageObject::Title(_)) =>
1504 {
1505 Some((object.source.id(), object.source.layout(), object.rect()))
1506 }
1507 _ => None,
1508 }))
1509 };
1510 let Some(first) = bodies()
1511 .filter(|(_, _, rect)| {
1512 rect[1] >= old + 10.8
1513 && rect[1] < destination
1514 && rect[0] <= right
1515 && rect[2] >= left
1516 })
1517 .map(|(_, _, rect)| rect[1])
1518 .min_by(f32::total_cmp)
1519 else {
1520 return Ok(Vec::new());
1521 };
1522 let delta = destination - first;
1523 bodies()
1524 .filter(|(_, _, rect)| rect[1] >= first)
1525 .map(|(id, layout, rect)| {
1526 let y = rect[1] + delta;
1527 if !y.is_finite() || !(rect[3] + delta).is_finite() {
1528 return Err(EditorError::InvalidGeometry);
1529 }
1530 Ok(Placement {
1531 id,
1532 position: [layout.x, Some(y)],
1533 })
1534 })
1535 .collect()
1536 }
1537
1538 #[cfg(test)]
1539 pub(crate) fn from_outlines(
1540 engine: &mut TextEngine,
1541 outlines: Vec<Outline>,
1542 definitions: BTreeMap<ExGuid, Definition>,
1543 ) -> Result<Self, EditorError> {
1544 let outlines = outlines
1545 .iter()
1546 .map(|outline| TextOutline::from_outline(engine, outline, &definitions))
1547 .collect::<Result<_, _>>()?;
1548 Self::from_text_outlines(outlines, definitions, None)
1549 }
1550
1551 pub fn from_text_outlines(
1552 outlines: Vec<TextOutline>,
1553 definitions: BTreeMap<ExGuid, Definition>,
1554 date: Option<PageDate>,
1555 ) -> Result<Self, EditorError> {
1556 if outlines.is_empty() {
1557 return Err(EditError::InvalidRange.into());
1558 }
1559 let mut ids = BTreeSet::new();
1560 for (outline_id, paragraphs) in outlines
1561 .iter()
1562 .map(|outline| (outline.id, outline.document.nodes()))
1563 .chain(
1564 date.iter()
1565 .map(|date| (date.source().id, date.source().paragraphs.as_slice())),
1566 )
1567 {
1568 if !ids.insert(outline_id) {
1569 return Err(EditError::InvalidStructure.into());
1570 }
1571 crate::document::validate_nodes(paragraphs, &mut ids)?;
1572 }
1573 Ok(Self {
1574 outlines,
1575 definitions,
1576 objects: Vec::new(),
1577 date,
1578 header: PageHeader::default(),
1579 active: Focus::Outline(0),
1580 undo: Vec::new(),
1581 redo: Vec::new(),
1582 composition: None,
1583 preferred_x: None,
1584 pending: None,
1585 ops: Ok(Vec::new()),
1586 stored: None,
1587 whole: None,
1588 typing: None,
1589 recording: None,
1590 ink_drag: None,
1591 provisional: BTreeMap::new(),
1592 default_font: DefaultFont::default(),
1593 styles: BTreeMap::new(),
1594 markdown: None,
1595 formatted: None,
1596 })
1597 }
1598
1599 pub fn date(&self) -> Option<&PageDate> {
1600 self.date.as_ref()
1601 }
1602
1603 pub fn change_date(
1604 &mut self,
1605 engine: &mut TextEngine,
1606 timestamp: u64,
1607 mut text: [String; 2],
1608 ) -> Result<bool, EditorError> {
1609 let date = self.date.as_ref().ok_or(EditError::InvalidRange)?;
1610 if timestamp == date.timestamp() {
1611 return Ok(false);
1612 }
1613 let mut source = date.source().clone();
1614 for ((field, _), paragraph) in date.fields().zip(&mut source.paragraphs) {
1615 let format = paragraph.text().unwrap().text.format_at(0)?.clone();
1616 paragraph.text_mut().unwrap().text =
1617 Paragraph::new(std::mem::take(&mut text[field as usize]), format);
1618 }
1619 let updated = PageDate::new(timestamp, source, engine, &self.definitions)?;
1620 self.finish_composition();
1621 self.undo
1622 .push(History::Date(Box::new(self.date.replace(updated).unwrap())));
1623 self.redo.clear();
1624 self.record(Ok(self.date_ops()));
1625 Ok(true)
1626 }
1627
1628 /// Gives the page `color` (View, Page Color), `rule_lines` (View, Rule Lines) and, when
1629 /// given, `art` in place of its background pictures, as one undo step; false when it had
1630 /// them.
1631 pub fn set_paper(
1632 &mut self,
1633 color: Option<u32>,
1634 rule_lines: Option<onestore::page::RuleLines>,
1635 art: Option<Vec<onestore::page::Image>>,
1636 ) -> bool {
1637 let backgrounds: Vec<usize> = (0..self.objects.len())
1638 .filter(|&index| {
1639 self.objects[index]
1640 .picture()
1641 .is_some_and(|image| image.background)
1642 })
1643 .collect();
1644 let art = art.filter(|art| {
1645 art.len() != backgrounds.len()
1646 || art.iter().zip(&backgrounds).any(|(new, &index)| {
1647 let shown = self.objects[index].picture().unwrap();
1648 (&new.layout, new.size, &new.bytes) != (&shown.layout, shown.size, &shown.bytes)
1649 })
1650 });
1651 let paper = (color, rule_lines) != (self.header.color, self.header.rule_lines);
1652 if !paper && art.is_none() {
1653 return false;
1654 }
1655 self.finish_composition();
1656 let depth = self.undo.len();
1657 if paper {
1658 let entry = self.paper(color, rule_lines);
1659 self.undo.push(entry);
1660 }
1661 if let Some(art) = art {
1662 for index in backgrounds.into_iter().rev() {
1663 let image = self.remove_picture(index);
1664 self.undo.push(History::Picture {
1665 index,
1666 image: Some(Box::new(image)),
1667 });
1668 }
1669 // Art lies under everything else on the page.
1670 for image in art.into_iter().rev() {
1671 self.add_picture(0, image);
1672 self.undo.push(History::Picture {
1673 index: 0,
1674 image: None,
1675 });
1676 }
1677 }
1678 self.group(depth, false);
1679 self.redo.clear();
1680 true
1681 }
1682
1683 /// Puts page-level picture `image` at `index` in paint order, recording its op.
1684 fn add_picture(&mut self, index: usize, image: onestore::page::Image) {
1685 let object = PageObject::Image(image.clone());
1686 self.objects.insert(index, page::Content::Image(image));
1687 let before = self.successor(object.id());
1688 self.record(Ok(vec![PageOp::Add { object, before }]));
1689 }
1690
1691 /// Takes the page-level picture at `index` in paint order off the page, recording its op.
1692 fn remove_picture(&mut self, index: usize) -> onestore::page::Image {
1693 let image = self.objects[index].picture().unwrap().clone();
1694 self.objects.remove(index);
1695 self.record(Ok(vec![PageOp::Delete { object: image.id }]));
1696 image
1697 }
1698
1699 /// Puts a file on the page, `file` being its `Content::File`, at `index` in paint order,
1700 /// recording its op.
1701 fn add_file(&mut self, index: usize, file: page::Content) {
1702 let object = PageObject::Attachment(file.file().unwrap().0.clone());
1703 self.objects.insert(index, file);
1704 let before = self.successor(object.id());
1705 self.record(Ok(vec![PageOp::Add { object, before }]));
1706 }
1707
1708 /// Takes the file at `index` in paint order off the page, recording its op.
1709 fn remove_file(&mut self, index: usize) -> page::Content {
1710 let file = self.objects.remove(index);
1711 let object = file.file().unwrap().0.id;
1712 self.record(Ok(vec![PageOp::Delete { object }]));
1713 file
1714 }
1715
1716 /// Shows `color` and `rule_lines`, recording the ops that store them; the history entry
1717 /// restoring what the page had.
1718 fn paper(
1719 &mut self,
1720 color: Option<u32>,
1721 rule_lines: Option<onestore::page::RuleLines>,
1722 ) -> History {
1723 let mut ops = Vec::new();
1724 if color != self.header.color {
1725 ops.push(PageOp::Color(color));
1726 }
1727 if rule_lines != self.header.rule_lines {
1728 ops.push(PageOp::RuleLines(rule_lines));
1729 }
1730 self.record(Ok(ops));
1731 History::Paper {
1732 color: std::mem::replace(&mut self.header.color, color),
1733 rule_lines: std::mem::replace(&mut self.header.rule_lines, rule_lines),
1734 }
1735 }
1736
1737 /// Stored content; arrow-created paragraphs appear only in `visible_outlines`.
1738 pub fn outlines(&self) -> &[TextOutline] {
1739 &self.outlines
1740 }
1741 pub fn visible_outlines(&self) -> impl DoubleEndedIterator<Item = &TextOutline> {
1742 self.outlines
1743 .iter()
1744 .enumerate()
1745 .map(|(index, outline)| match &self.active {
1746 Focus::Draft {
1747 index: active,
1748 outline,
1749 } if index == *active => outline.as_ref(),
1750 _ => outline,
1751 })
1752 }
1753
1754 pub fn active_outline(&self) -> &TextOutline {
1755 match &self.active {
1756 Focus::Outline(index) => &self.outlines[*index],
1757 Focus::Caret { outline, .. } | Focus::Draft { outline, .. } => outline,
1758 }
1759 }
1760
1761 fn active_outline_mut(&mut self) -> &mut TextOutline {
1762 match &mut self.active {
1763 Focus::Outline(index) => &mut self.outlines[*index],
1764 Focus::Caret { outline, .. } | Focus::Draft { outline, .. } => outline,
1765 }
1766 }
1767
1768 /// The caret a run of typing left while typing there still joins the run: the word
1769 /// before it is still being written.
1770 pub fn typing(&self) -> Option<(ExGuid, TextPosition)> {
1771 let (outline, at, depth) = self.typing?;
1772 (outline == self.active_outline().id
1773 && self.selection().positions == [at; 2]
1774 && depth == self.undo.len())
1775 .then_some((outline, at))
1776 }
1777
1778 /// Provisional input geometry outside the document's outline collection.
1779 pub fn caret_outline(&self) -> Option<&TextOutline> {
1780 match &self.active {
1781 Focus::Caret { outline, .. } => Some(outline),
1782 Focus::Outline(_) | Focus::Draft { .. } => None,
1783 }
1784 }
1785
1786 pub fn place_caret(
1787 &mut self,
1788 engine: &mut TextEngine,
1789 position: [f32; 2],
1790 width: f32,
1791 ) -> Result<(), EditorError> {
1792 let document = TextDocument::from_nodes(vec![self.body_paragraph()?])?;
1793 let outline = TextOutline::new(engine, document, width, position)?;
1794 self.finish_composition();
1795 self.active = Focus::Caret {
1796 outline: Box::new(outline),
1797 index: self.outlines.len(),
1798 };
1799 self.preferred_x = None;
1800 Ok(())
1801 }
1802
1803 /// The format a paragraph style gives text, which clearing formatting returns to.
1804 fn style_format(&self, style: Option<ExGuid>) -> Result<onestore::document::Format, EditError> {
1805 Ok(match style {
1806 Some(id) => self
1807 .definitions
1808 .get(&id)
1809 .filter(|definition| {
1810 matches!(definition.kind, onestore::document::Kind::Style { .. })
1811 })
1812 .ok_or(EditError::InvalidStructure)?
1813 .format
1814 .clone(),
1815 None => Default::default(),
1816 })
1817 }
1818
1819 /// A new outline's first paragraph as OneNote 2010 starts one: in the page's `p` quick
1820 /// style of the Default font, which a page whose `p` is in another font gains beside it.
1821 fn body_paragraph(&mut self) -> Result<PageParagraph, EditError> {
1822 let definition = match self.styles.get("p") {
1823 Some(themed) => themed.clone(),
1824 None => self.default_font.body_style(),
1825 };
1826 let known = self.definitions.iter().find(|(_, known)| {
1827 matches!(&known.kind, Kind::Style { name: Some(name), .. } if name == "p")
1828 && known.format.font == definition.format.font
1829 && known.format.font_size == definition.format.font_size
1830 });
1831 let style = match known {
1832 Some((id, _)) => *id,
1833 None => {
1834 let id = onestore::page::text::new_id()?;
1835 self.definitions.insert(id, definition.clone());
1836 id
1837 }
1838 };
1839 let mut node = crate::document::node(
1840 Paragraph::new(String::new(), definition.format),
1841 onestore::document::Format::default(),
1842 )?;
1843 node.style = Some(style);
1844 Ok(node)
1845 }
1846
1847 /// An empty paragraph in `base`'s style, whose runs keep `base`'s language as OneNote's do.
1848 fn blank_paragraph(&self, base: &PageParagraph) -> Result<PageParagraph, EditError> {
1849 let mut format = self.style_format(base.style)?;
1850 format.language = format.language.or(base
1851 .text()
1852 .and_then(|text| text.text.spans()[0].format.language));
1853 let mut node =
1854 crate::document::node(Paragraph::new(String::new(), format), base.format.clone())?;
1855 node.level = base.level;
1856 node.style = base.style;
1857 Ok(node)
1858 }
1859
1860 pub fn select_below(
1861 &mut self,
1862 engine: &mut TextEngine,
1863 id: ExGuid,
1864 point: [f32; 2],
1865 ) -> Result<bool, EditorError> {
1866 if point.iter().any(|coordinate| !coordinate.is_finite()) {
1867 return Err(EditError::InvalidRange.into());
1868 }
1869 self.finish_composition();
1870 let Some(index) = self.outlines.iter().position(|outline| outline.id == id) else {
1871 return Ok(false);
1872 };
1873 let outline = &self.outlines[index];
1874 if !outline.contains_extension(point) {
1875 return Ok(false);
1876 }
1877 let draft = Box::new(match &self.active {
1878 Focus::Draft {
1879 outline,
1880 index: active,
1881 } if *active == index => (**outline).clone(),
1882 _ => outline.clone(),
1883 });
1884 let previous = std::mem::replace(
1885 &mut self.active,
1886 Focus::Draft {
1887 outline: draft,
1888 index,
1889 },
1890 );
1891 let preferred_x = self.preferred_x;
1892 let result = (|| {
1893 // Tiny source font sizes must not make one click generate unbounded paragraphs.
1894 for _ in 0..256 {
1895 if self.active_outline().shaped.size[1] >= point[1] {
1896 break;
1897 }
1898 let outline = self.active_outline();
1899 let count = outline.document.nodes().len();
1900 let height = outline.shaped.size[1];
1901 let last = &outline.document.nodes()[count - 1];
1902 let node = PageParagraph {
1903 parent: last.parent,
1904 ..self.blank_paragraph(last)?
1905 };
1906 self.apply(
1907 engine,
1908 DocumentEdit {
1909 columns: BTreeMap::new(),
1910 container: None,
1911 range: count..count,
1912 replacement: vec![node],
1913 },
1914 outline.selection,
1915 false,
1916 None,
1917 )?;
1918 if self.active_outline().shaped.size[1] <= height {
1919 return Err(EditError::InvalidStructure.into());
1920 }
1921 }
1922 if self.active_outline().shaped.size[1] < point[1] {
1923 return Err(EditError::TextTooLong.into());
1924 }
1925 self.select_at(point[0], point[1], false)?;
1926 Ok(true)
1927 })();
1928 if result.is_err() {
1929 self.active = previous;
1930 self.preferred_x = preferred_x;
1931 }
1932 result
1933 }
1934
1935 pub fn focus_outline(&mut self, id: onestore::ExGuid) -> Result<(), EditError> {
1936 if self.active_outline().id != id && !self.outlines.iter().any(|outline| outline.id == id) {
1937 return Err(EditError::InvalidRange);
1938 }
1939 self.finish_composition();
1940 if let Some(index) = self.outlines.iter().position(|outline| outline.id == id)
1941 && !matches!(self.active, Focus::Draft { index: active, .. } if active == index)
1942 {
1943 self.active = Focus::Outline(index);
1944 }
1945 self.preferred_x = None;
1946 Ok(())
1947 }
1948
1949 pub fn create_outline(
1950 &mut self,
1951 engine: &mut TextEngine,
1952 position: [f32; 2],
1953 width: f32,
1954 ) -> Result<onestore::ExGuid, EditorError> {
1955 let document = TextDocument::from_nodes(vec![self.body_paragraph()?])?;
1956 let outline = TextOutline::new(engine, document, width, position)?;
1957 let id = outline.id;
1958 self.finish_composition();
1959 self.undo.push(History::Remove {
1960 outline: id,
1961 focus: match &self.active {
1962 Focus::Outline(index) | Focus::Draft { index, .. } => {
1963 RestoreFocus::Outline(self.outlines[*index].id)
1964 }
1965 Focus::Caret { outline, index } => RestoreFocus::Caret {
1966 source: Box::new(outline.snapshot()),
1967 selection: outline.selection,
1968 index: *index,
1969 },
1970 },
1971 });
1972 self.redo.clear();
1973 self.active = Focus::Outline(self.outlines.len());
1974 self.outlines.push(outline);
1975 self.record(self.outline_ops(id, None));
1976 self.preferred_x = None;
1977 Ok(id)
1978 }
1979
1980 pub fn move_outline(
1981 &mut self,
1982 id: onestore::ExGuid,
1983 position: [f32; 2],
1984 ) -> Result<(), EditError> {
1985 if !position.iter().all(|value| value.is_finite()) {
1986 return Err(EditError::InvalidRange);
1987 }
1988 let outline = if self.active_outline().id == id {
1989 self.active_outline()
1990 } else {
1991 self.outlines
1992 .iter()
1993 .find(|outline| outline.id == id)
1994 .ok_or(EditError::InvalidRange)?
1995 };
1996 if outline.title {
1997 return Err(EditError::UnsupportedContent);
1998 }
1999 if position == outline.origin() {
2000 return Ok(());
2001 }
2002 self.finish_composition();
2003 if self.caret_outline().is_some_and(|outline| outline.id == id) {
2004 let outline = self.active_outline_mut();
2005 outline.layout.x = Some(position[0]);
2006 outline.layout.y = Some(position[1]);
2007 self.preferred_x = None;
2008 return Ok(());
2009 }
2010 let index = self
2011 .outlines
2012 .iter()
2013 .position(|outline| outline.id == id)
2014 .unwrap();
2015 let layout = &mut self.outlines[index].layout;
2016 let previous = [layout.x, layout.y];
2017 self.undo.push(History::Position {
2018 object: id,
2019 position: previous,
2020 });
2021 self.redo.clear();
2022 layout.x = Some(position[0]);
2023 layout.y = Some(position[1]);
2024 self.record(self.placement_ops(&Placement {
2025 id,
2026 position: previous,
2027 }));
2028 self.active = Focus::Outline(index);
2029 self.preferred_x = None;
2030 Ok(())
2031 }
2032
2033 /// Origin and size of a picture or file on the page the user can select; OneNote passes
2034 /// clicks through backgrounds.
2035 pub fn image_placement(&self, id: ExGuid) -> Option<([f32; 2], [f32; 2])> {
2036 if let Some((_, [x0, y0, x1, y1])) = self.page_file(id) {
2037 return Some(([x0, y0], [x1 - x0, y1 - y0]));
2038 }
2039 if let Some(image) = self.image(id) {
2040 return Some((
2041 crate::origin(&image.layout),
2042 crate::outline::image_size(image)?,
2043 ));
2044 }
2045 let (outline, ..) = self.outline_picture(id)?;
2046 let outline = self.outlines.iter().find(|item| item.id == outline)?;
2047 let [x0, y0, x1, y1] = outline.shaped.objects.iter().find(|o| o.id == id)?.rect;
2048 let origin = outline.origin();
2049 Some(([origin[0] + x0, origin[1] + y0], [x1 - x0, y1 - y0]))
2050 }
2051
2052 /// Whether the picture sits in an outline's flow, where only its size can change.
2053 pub(crate) fn image_in_outline(&self, id: ExGuid) -> bool {
2054 self.outline_picture(id).is_some()
2055 }
2056
2057 /// A picture or file held as a paragraph: its outline, the paragraph's container and index
2058 /// there, and the paragraph.
2059 fn outline_picture(
2060 &self,
2061 id: ExGuid,
2062 ) -> Option<(ExGuid, Option<ExGuid>, usize, &PageParagraph)> {
2063 self.outlines.iter().find_map(|outline| {
2064 descendants(outline.document.nodes(), None).find_map(|(container, index, node)| {
2065 holds(&node.content, id).then_some((outline.id, container, index, node))
2066 })
2067 })
2068 }
2069
2070 /// The address picture `id` links to, in an outline or on the page.
2071 pub(crate) fn picture_link(&self, id: ExGuid) -> Option<&str> {
2072 self.picture(id)?.link.as_deref()
2073 }
2074
2075 pub(crate) fn picture(&self, id: ExGuid) -> Option<&onestore::page::Image> {
2076 self.image(id)
2077 .or_else(|| match &self.outline_picture(id)?.3.content {
2078 ParagraphContent::Image(image) => Some(image),
2079 _ => None,
2080 })
2081 }
2082
2083 /// The file a paragraph holds or the page shows, which the host opens and saves.
2084 pub(crate) fn attachment(&self, id: ExGuid) -> Option<&onestore::page::Attachment> {
2085 if let Some((file, _)) = self.page_file(id) {
2086 return Some(file);
2087 }
2088 match &self.outline_picture(id)?.3.content {
2089 ParagraphContent::Attachment(file) => Some(file),
2090 _ => None,
2091 }
2092 }
2093
2094 /// A file on the page and where its column lies, in page coordinates.
2095 fn page_file(&self, id: ExGuid) -> Option<(&onestore::page::Attachment, [f32; 4])> {
2096 self.objects
2097 .iter()
2098 .filter_map(page::Content::file)
2099 .find(|(file, _)| file.id == id)
2100 }
2101
2102 /// Where a file draws with its name, in page coordinates.
2103 pub(crate) fn attachment_rect(&self, id: ExGuid) -> Option<[f32; 4]> {
2104 if let Some((_, rect)) = self.page_file(id) {
2105 return Some(rect);
2106 }
2107 let (outline, ..) = self.outline_picture(id)?;
2108 let outline = self.outlines.iter().find(|item| item.id == outline)?;
2109 let bounds = outline.shaped.objects.iter().find(|o| o.id == id)?.bounds();
2110 Some(crate::translated(bounds, outline.origin()))
2111 }
2112
2113 /// Replaces a picture paragraph through the outline's text history.
2114 fn edit_outline_picture(
2115 &mut self,
2116 engine: &mut TextEngine,
2117 id: ExGuid,
2118 replacement: Option<PageParagraph>,
2119 ) -> Result<(), EditorError> {
2120 let (outline, container, index, _) =
2121 self.outline_picture(id).ok_or(EditError::InvalidRange)?;
2122 self.focus_outline(outline)?;
2123 let selection = self.selection();
2124 self.commit(
2125 engine,
2126 DocumentEdit {
2127 columns: BTreeMap::new(),
2128 container,
2129 range: index..index + 1,
2130 replacement: replacement.into_iter().collect(),
2131 },
2132 selection,
2133 )
2134 }
2135
2136 fn image(&self, id: ExGuid) -> Option<&onestore::page::Image> {
2137 self.objects.iter().find_map(|object| match object {
2138 page::Content::Image(image) if image.id == id && !image.background => Some(image),
2139 _ => None,
2140 })
2141 }
2142
2143 fn object_tags_mut(&mut self, id: ExGuid) -> Option<&mut Vec<onestore::document::Tag>> {
2144 self.objects.iter_mut().find_map(|object| match object {
2145 page::Content::Image(image) if image.id == id => Some(&mut image.tags),
2146 page::Content::File { source, .. } if source.id == id => Some(&mut source.tags),
2147 _ => None,
2148 })
2149 }
2150
2151 /// Gives picture or file `id` on the page `tags`, returning those it had.
2152 fn swap_object_tags(
2153 &mut self,
2154 id: ExGuid,
2155 tags: Vec<onestore::document::Tag>,
2156 ) -> Vec<onestore::document::Tag> {
2157 let previous = std::mem::replace(self.object_tags_mut(id).unwrap(), tags);
2158 let tags = self.object_tags_mut(id).unwrap().clone();
2159 self.record(Ok(ops::tags(id, tags, &self.definitions)));
2160 previous
2161 }
2162
2163 /// Gives picture or file `id` on the page `tags`, as one undo step.
2164 pub(super) fn set_object_tags(&mut self, id: ExGuid, tags: Vec<onestore::document::Tag>) {
2165 let previous = self.swap_object_tags(id, tags);
2166 self.undo.push(History::Tags {
2167 object: id,
2168 tags: previous,
2169 });
2170 self.redo.clear();
2171 }
2172
2173 fn image_mut(&mut self, id: ExGuid) -> Option<&mut onestore::page::Image> {
2174 self.objects.iter_mut().find_map(|object| match object {
2175 page::Content::Image(image) if image.id == id && !image.background => Some(image),
2176 _ => None,
2177 })
2178 }
2179
2180 /// Moves a picture or a file on the page, and resizes a picture when `size` differs from
2181 /// the stored size. A picture in an outline's flow keeps its place.
2182 pub fn place_image(
2183 &mut self,
2184 engine: &mut TextEngine,
2185 id: ExGuid,
2186 origin: [f32; 2],
2187 size: [f32; 2],
2188 ) -> Result<(), EditorError> {
2189 if !origin.iter().chain(&size).all(|value| value.is_finite())
2190 || size.iter().any(|value| *value <= 0.0)
2191 {
2192 return Err(EditError::InvalidRange.into());
2193 }
2194 if let Some((.., node)) = self.outline_picture(id) {
2195 let mut node = node.clone();
2196 let onestore::page::ParagraphContent::Image(image) = &mut node.content else {
2197 return Ok(());
2198 };
2199 if crate::outline::image_size(image) == Some(size) {
2200 return Ok(());
2201 }
2202 [image.layout.max_width, image.layout.max_height] = size.map(Some);
2203 image.layout.width_set_by_user = Some(true);
2204 return self.edit_outline_picture(engine, id, Some(node));
2205 }
2206 if let Some((file, _)) = self.page_file(id) {
2207 let previous = [file.layout.x, file.layout.y];
2208 if previous == origin.map(Some) {
2209 return Ok(());
2210 }
2211 self.finish_composition();
2212 let layout = self.object_layout_mut(id).unwrap();
2213 [layout.x, layout.y] = origin.map(Some);
2214 self.undo.push(History::Position {
2215 object: id,
2216 position: previous,
2217 });
2218 self.redo.clear();
2219 self.record(self.placement_ops(&Placement {
2220 id,
2221 position: previous,
2222 }));
2223 return Ok(());
2224 }
2225 let image = self.image(id).ok_or(EditError::InvalidRange)?;
2226 let mut layout = image.layout.clone();
2227 [layout.x, layout.y] = origin.map(Some);
2228 if crate::outline::image_size(image) != Some(size) {
2229 [layout.max_width, layout.max_height] = size.map(Some);
2230 layout.width_set_by_user = Some(true);
2231 }
2232 if layout == image.layout {
2233 return Ok(());
2234 }
2235 self.finish_composition();
2236 let previous = std::mem::replace(&mut self.image_mut(id).unwrap().layout, layout);
2237 self.undo.push(History::Image {
2238 image: id,
2239 layout: previous,
2240 });
2241 self.redo.clear();
2242 let image = self.image(id).unwrap();
2243 self.record(Ok(ops::picture_layout(image)));
2244 Ok(())
2245 }
2246
2247 /// Leaves a picture as OneNote's Left and Right arrows do: the caret goes to the end of the
2248 /// text outline before it in page order, or to the start of the one after. False when there
2249 /// is none.
2250 pub(crate) fn step_from_image(
2251 &mut self,
2252 engine: &mut TextEngine,
2253 id: ExGuid,
2254 forward: bool,
2255 ) -> Result<bool, EditorError> {
2256 if let Some((outline, ..)) = self.outline_picture(id) {
2257 // Within an outline, the caret goes to the text beside the picture.
2258 let source = self
2259 .outlines
2260 .iter()
2261 .find(|item| item.id == outline)
2262 .unwrap();
2263 let mut leaf = 0;
2264 let mut before = None;
2265 let mut after = None;
2266 let mut passed = false;
2267 for (_, _, node) in descendants(source.document.nodes(), None) {
2268 match &node.content {
2269 content if holds(content, id) => passed = true,
2270 onestore::page::ParagraphContent::Text(text) => {
2271 let end = text.text.utf16_offset(text.text.text().len())?;
2272 if passed {
2273 after = after.or(Some((leaf, 0)));
2274 } else {
2275 before = Some((leaf, end));
2276 }
2277 leaf += 1;
2278 }
2279 _ => {}
2280 }
2281 }
2282 let Some((paragraph, offset)) = (if forward { after } else { before }) else {
2283 return Ok(false);
2284 };
2285 self.focus_outline(outline)?;
2286 self.select(Selection {
2287 positions: [TextPosition { paragraph, offset }; 2],
2288 affinities: [Affinity::Downstream; 2],
2289 })?;
2290 return Ok(true);
2291 }
2292 let index = self
2293 .objects
2294 .iter()
2295 .position(|object| {
2296 matches!(object, page::Content::Image(image) if image.id == id)
2297 || object.file().is_some_and(|(file, _)| file.id == id)
2298 })
2299 .ok_or(EditError::InvalidRange)?;
2300 // An emptied outline keeps its slot for undo after leaving `outlines`.
2301 let editable = |object: &page::Content| match object {
2302 page::Content::Editable(outline)
2303 if self.outlines.iter().any(|item| item.id == *outline) =>
2304 {
2305 Some(*outline)
2306 }
2307 _ => None,
2308 };
2309 let outline = if forward {
2310 self.objects[index + 1..].iter().find_map(editable)
2311 } else {
2312 self.objects[..index].iter().rev().find_map(editable)
2313 };
2314 let Some(outline) = outline else {
2315 return Ok(false);
2316 };
2317 self.focus_outline(outline)?;
2318 let edge = if forward {
2319 Movement::DocumentStart
2320 } else {
2321 Movement::DocumentEnd
2322 };
2323 self.move_selection(engine, edge, false)?;
2324 Ok(true)
2325 }
2326
2327 /// Moves the caret from the title to where OneNote 2010 puts what the title can't take
2328 /// (lab, 2026-09-30 and 2026-10-02): the end of an outline where the body starts, a new
2329 /// one there on a page without body content, and otherwise a caret on blank page two grid
2330 /// rows below the content, which is the one case it returns false.
2331 fn leave_title_for_body(&mut self, engine: &mut TextEngine) -> Result<bool, EditorError> {
2332 let start = self.body_start_of(self.active_outline());
2333 let first = self
2334 .outlines
2335 .iter()
2336 .find(|outline| !outline.title && outline.origin() == start)
2337 .map(|outline| outline.id);
2338 if let Some(id) = first {
2339 self.focus_outline(id)?;
2340 self.move_selection(engine, Movement::DocumentEnd, false)?;
2341 return Ok(true);
2342 }
2343 let Some(bottom) = self.body_bottom() else {
2344 self.place_caret(engine, start, DEFAULT_OUTLINE_WIDTH)?;
2345 return Ok(true);
2346 };
2347 let margin = self.margin_origin();
2348 let row = ((bottom - margin[1]) / 18.0).floor() + 2.0;
2349 self.place_caret(
2350 engine,
2351 [start[0], margin[1] + row * 18.0],
2352 DEFAULT_OUTLINE_WIDTH,
2353 )?;
2354 Ok(false)
2355 }
2356
2357 /// Puts `image` at the caret as OneNote 2010 pastes and inserts a picture: in an outline
2358 /// as [`Self::insert_attachment`] puts a file, and on blank page at the caret with the
2359 /// caret moving to the grid row below it, where typing starts a new outline. From the
2360 /// title it goes where [`Self::leave_title_for_body`] puts the caret, in the flow there
2361 /// unless on blank page.
2362 pub fn insert_picture(
2363 &mut self,
2364 engine: &mut TextEngine,
2365 mut image: onestore::page::Image,
2366 ) -> Result<(), EditorError> {
2367 if self.active_outline().title && self.leave_title_for_body(engine)? {
2368 return self.insert_in_flow(engine, ParagraphContent::Image(image));
2369 }
2370 let margin = self.margin_origin();
2371 let Focus::Caret { outline, .. } = &self.active else {
2372 return self.insert_in_flow(engine, ParagraphContent::Image(image));
2373 };
2374 let [x, y] = outline.origin();
2375 [image.layout.x, image.layout.y] = [Some(x), Some(y)];
2376 image.layout.width_set_by_user = Some(false);
2377 let height = image.layout.max_height.unwrap_or_default();
2378 self.finish_composition();
2379 let index = self.objects.len();
2380 self.add_picture(index, image);
2381 self.undo.push(History::Picture { index, image: None });
2382 self.redo.clear();
2383 let row = ((y + height - margin[1]) / 18.0).floor() + 1.0;
2384 self.place_caret(engine, [x, margin[1] + row * 18.0], DEFAULT_OUTLINE_WIDTH)
2385 }
2386
2387 /// Pastes clips and pictures in order, as one undo step, as OneNote 2010 pastes a web
2388 /// page's text with its pictures; the places of the awaited ones, in order.
2389 pub fn paste_pieces(
2390 &mut self,
2391 engine: &mut TextEngine,
2392 pieces: Vec<Piece>,
2393 ) -> Result<Vec<Awaited>, EditorError> {
2394 let depth = self.undo.len();
2395 let mut result = Ok(());
2396 let mut awaited = Vec::new();
2397 for piece in pieces {
2398 result = match piece {
2399 Piece::Clip(clip) => self.paste_clip(engine, clip),
2400 Piece::Picture(bytes, size) => picture(bytes, size)
2401 .map_err(EditorError::from)
2402 .and_then(|image| self.insert_picture(engine, image)),
2403 Piece::Awaited => self.enter(engine, false).and_then(|()| {
2404 let caret = self.selection().positions[1];
2405 let outline = self.active_outline();
2406 let (.., node) = outline
2407 .document
2408 .leaf(caret.paragraph)
2409 .ok_or(EditError::InvalidRange)?;
2410 awaited.push(Awaited {
2411 outline: outline.id,
2412 before: node.id,
2413 });
2414 Ok(())
2415 }),
2416 };
2417 if result.is_err() {
2418 break;
2419 }
2420 }
2421 self.group(depth, false);
2422 result.map(|()| awaited)
2423 }
2424
2425 /// Puts an awaited picture in its place as an undo step of its own, keeping the focused
2426 /// outline's selection; false where the place has gone.
2427 pub fn insert_awaited(
2428 &mut self,
2429 engine: &mut TextEngine,
2430 at: Awaited,
2431 image: onestore::page::Image,
2432 ) -> Result<bool, EditorError> {
2433 let Some(outline) = self
2434 .outlines
2435 .iter()
2436 .find(|outline| outline.id == at.outline)
2437 else {
2438 return Ok(false);
2439 };
2440 let Some((container, index, node)) = descendants(outline.document.nodes(), None)
2441 .find(|(.., node)| node.id == at.before)
2442 .map(|(container, index, node)| (container, index, node.clone()))
2443 else {
2444 return Ok(false);
2445 };
2446 let picture = PageParagraph {
2447 id: onestore::page::text::new_id()?,
2448 content: ParagraphContent::Image(image),
2449 style: None,
2450 lists: Vec::new(),
2451 tags: Vec::new(),
2452 collapsed: false,
2453 ..node
2454 };
2455 // A caret on blank page is let go; picture paragraphs take no text positions.
2456 let focused = match self.active {
2457 Focus::Outline(index) => Some((self.outlines[index].id, self.selection())),
2458 _ => None,
2459 };
2460 self.focus_outline(at.outline)?;
2461 let selection = self.selection();
2462 self.commit(
2463 engine,
2464 DocumentEdit {
2465 columns: BTreeMap::new(),
2466 container,
2467 range: index..index,
2468 replacement: vec![picture],
2469 },
2470 selection,
2471 )?;
2472 if let Some((id, selection)) = focused {
2473 self.focus_outline(id)?;
2474 self.select(selection)?;
2475 }
2476 Ok(true)
2477 }
2478
2479 /// The lowest edge of the page's body content, in page points; none on an empty page.
2480 fn body_bottom(&self) -> Option<f32> {
2481 let outlines = self
2482 .outlines
2483 .iter()
2484 .filter(|outline| !outline.title)
2485 .map(|outline| outline.bounds().y1 as f32);
2486 let objects = self
2487 .objects
2488 .iter()
2489 .filter(|object| !object.picture().is_some_and(|image| image.background))
2490 .filter_map(|object| {
2491 let (_, layout) = object.layout()?;
2492 Some(layout.y? + layout.max_height?)
2493 });
2494 outlines.chain(objects).reduce(f32::max)
2495 }
2496
2497 /// Attaches `file` at the caret as OneNote 2010 does (`corpus/attachment-insert`): the
2498 /// caret's paragraph splits there, the file takes the place of an empty first half, and the
2499 /// caret starts the second. A caret on blank page puts the file on the page there
2500 /// (`corpus/attachment-floating`).
2501 pub fn insert_attachment(
2502 &mut self,
2503 engine: &mut TextEngine,
2504 mut file: onestore::page::Attachment,
2505 ) -> Result<(), EditorError> {
2506 if self.active_outline().title && self.leave_title_for_body(engine)? {
2507 return self.insert_in_flow(engine, ParagraphContent::Attachment(file));
2508 }
2509 if let Focus::Caret { outline, .. } = &self.active {
2510 [file.layout.x, file.layout.y] = outline.origin().map(Some);
2511 let layout = Box::new(crate::outline::page_file(engine, &file)?);
2512 let [x0, y0, x1, y1] = layout.bounds();
2513 [file.layout.max_width, file.layout.max_height] = [Some(x1 - x0), Some(y1 - y0)];
2514 file.layout.width_set_by_user = Some(false);
2515 self.finish_composition();
2516 let index = self.objects.len();
2517 self.add_file(
2518 index,
2519 page::Content::File {
2520 source: file,
2521 layout,
2522 },
2523 );
2524 self.undo.push(History::File { index, file: None });
2525 self.redo.clear();
2526 return Ok(());
2527 }
2528 self.insert_in_flow(engine, ParagraphContent::Attachment(file))
2529 }
2530
2531 /// Splits the caret's paragraph around `content`, which takes the place of an empty first
2532 /// half; the caret starts the second.
2533 fn insert_in_flow(
2534 &mut self,
2535 engine: &mut TextEngine,
2536 content: ParagraphContent,
2537 ) -> Result<(), EditorError> {
2538 let content = match self.across(engine, |editor, engine| {
2539 editor.insert_in_flow(engine, content.clone())
2540 })? {
2541 Some(()) => return Ok(()),
2542 None => content,
2543 };
2544 let [anchor, focus] = self.selection().positions;
2545 let (start, end) = (anchor.min(focus), anchor.max(focus));
2546 let mut format = self.typing_format(start)?;
2547 link::unlinked(&mut format);
2548 let mut edit = self
2549 .active_outline()
2550 .document
2551 .replace(start..end, vec![Paragraph::new(String::new(), format); 2])?;
2552 let [head, tail, ..] = &mut edit.replacement[..] else {
2553 unreachable!("a split holds both halves")
2554 };
2555 let node = PageParagraph {
2556 id: onestore::page::text::new_id()?,
2557 content,
2558 style: None,
2559 lists: Vec::new(),
2560 tags: Vec::new(),
2561 collapsed: false,
2562 ..head.clone()
2563 };
2564 let empty = head.text().unwrap().text.text().is_empty();
2565 if empty {
2566 tail.tags = std::mem::take(&mut head.tags);
2567 tail.text_mut().unwrap().tags = std::mem::take(&mut head.text_mut().unwrap().tags);
2568 }
2569 edit.replacement.insert(1, node);
2570 if empty {
2571 edit.replacement.remove(0);
2572 }
2573 let caret = TextPosition {
2574 paragraph: start.paragraph + usize::from(!empty),
2575 offset: 0,
2576 };
2577 self.commit(
2578 engine,
2579 edit,
2580 Selection {
2581 positions: [caret; 2],
2582 affinities: [Affinity::Upstream; 2],
2583 },
2584 )
2585 }
2586
2587 pub fn remove_image(&mut self, engine: &mut TextEngine, id: ExGuid) -> Result<(), EditorError> {
2588 if self.outline_picture(id).is_some() {
2589 return self.edit_outline_picture(engine, id, None);
2590 }
2591 if let Some(index) = self
2592 .objects
2593 .iter()
2594 .position(|object| object.file().is_some_and(|(file, _)| file.id == id))
2595 {
2596 self.finish_composition();
2597 let file = self.remove_file(index);
2598 self.undo.push(History::File {
2599 index,
2600 file: Some(Box::new(file)),
2601 });
2602 self.redo.clear();
2603 return Ok(());
2604 }
2605 let index = self
2606 .objects
2607 .iter()
2608 .position(|object| {
2609 matches!(object, page::Content::Image(image) if image.id == id && !image.background)
2610 })
2611 .ok_or(EditError::InvalidRange)?;
2612 self.finish_composition();
2613 let image = self.remove_picture(index);
2614 self.undo.push(History::Picture {
2615 index,
2616 image: Some(Box::new(image)),
2617 });
2618 self.redo.clear();
2619 Ok(())
2620 }
2621
2622 pub fn selection(&self) -> Selection {
2623 self.active_outline().selection
2624 }
2625
2626 /// OneNote's 18 pt placement grid passes through this point.
2627 pub fn margin_origin(&self) -> [f32; 2] {
2628 self.header.margin_origin
2629 }
2630
2631 /// Whether the page runs right to left, which OneNote 2010 opens scrolled to its right
2632 /// end.
2633 pub fn rtl(&self) -> bool {
2634 self.header.rtl
2635 }
2636
2637 pub fn resize(&mut self, engine: &mut TextEngine, width: f32) -> Result<(), EditorError> {
2638 let outline = self.active_outline();
2639 if outline.layout.reserved_width.or(outline.layout.max_width) == Some(width)
2640 && outline.layout.width_set_by_user == Some(true)
2641 {
2642 return Ok(());
2643 }
2644 let previous = outline.layout.clone();
2645 let resized = self.preview_resize(engine, width)?;
2646 self.finish_composition();
2647 if let Focus::Draft { index, .. } = self.active {
2648 self.active = Focus::Outline(index);
2649 }
2650 let id = resized.id;
2651 *self.active_outline_mut() = resized;
2652 if self.caret_outline().is_none() {
2653 self.record(ops::layout_ops(
2654 id,
2655 &previous,
2656 &self.active_outline().layout,
2657 ));
2658 self.undo.push(History::Layout {
2659 outline: id,
2660 layout: previous,
2661 });
2662 self.redo.clear();
2663 }
2664 self.preferred_x = None;
2665 Ok(())
2666 }
2667
2668 pub fn preview_resize(
2669 &self,
2670 engine: &mut TextEngine,
2671 width: f32,
2672 ) -> Result<TextOutline, EditorError> {
2673 let outline = match &self.active {
2674 Focus::Draft { index, .. } => &self.outlines[*index],
2675 _ => self.active_outline(),
2676 };
2677 if outline.title {
2678 return Err(EditError::UnsupportedContent.into());
2679 }
2680 if !width.is_finite() || width <= 0.0 {
2681 return Err(LayoutError::InvalidWidth.into());
2682 }
2683 let width = width.max(outline.shaped.table_width());
2684 let shaped = OutlineLayout::flow(
2685 outline.document.nodes().iter(),
2686 &outline.indents,
2687 width,
2688 true,
2689 0,
2690 None,
2691 &mut |paragraph, previous, width, indents| {
2692 ParagraphLayout::shape(
2693 engine,
2694 paragraph,
2695 previous,
2696 width,
2697 indents,
2698 &self.definitions,
2699 )
2700 },
2701 )?;
2702 let mut layout = outline.layout.clone();
2703 layout.max_width = Some(width);
2704 layout.reserved_width = None;
2705 layout.width_set_by_user = Some(true);
2706 Ok(TextOutline {
2707 id: outline.id,
2708 title: outline.title,
2709 min_width: outline.min_width,
2710 layout,
2711 document: outline.document.clone(),
2712 indents: outline.indents.clone(),
2713 shaped,
2714 selection: outline.selection,
2715 })
2716 }
2717
2718 pub fn select(&mut self, selection: Selection) -> Result<(), EditError> {
2719 for position in selection.positions {
2720 self.active_outline().visible_index(position.paragraph)?;
2721 self.active_outline()
2722 .document
2723 .paragraph(position.paragraph)
2724 .ok_or(EditError::InvalidRange)?
2725 .byte_offset(position.offset)?;
2726 }
2727 self.finish_composition();
2728 self.preferred_x = None;
2729 self.whole = None;
2730 self.active_outline_mut().selection = selection;
2731 Ok(())
2732 }
2733
2734 pub fn select_all(&mut self) -> Result<(), EditError> {
2735 self.select(self.active_outline().whole())
2736 }
2737
2738 /// Selects the smallest unit strictly holding the selection, as OneNote 2010's Ctrl+A widens
2739 /// it: the paragraph, its subtree, each enclosing table cell, row and table, then a body
2740 /// outline as an object (the title's text instead), then every body outline on the page.
2741 pub fn widen_selection(&mut self) -> Result<(), EditError> {
2742 match self.whole() {
2743 Some(Whole::Page) => return Ok(()),
2744 Some(Whole::Outline) => return self.select_page(),
2745 None => {}
2746 }
2747 let outline = self.active_outline();
2748 let [anchor, focus] = outline.selection.positions;
2749 let (start, end) = (anchor.min(focus), anchor.max(focus));
2750 let last_shown = |range: Range<usize>| {
2751 let paragraph = range
2752 .rev()
2753 .find(|&paragraph| outline.visible_index(paragraph).is_ok())?;
2754 let text = outline.document.paragraph(paragraph)?;
2755 Some(TextPosition {
2756 paragraph,
2757 offset: text.utf16_offset(text.text().len()).ok()?,
2758 })
2759 };
2760 let first = |paragraph| TextPosition {
2761 paragraph,
2762 offset: 0,
2763 };
2764 let own = outline
2765 .next_sibling_start(outline.visible_index(start.paragraph)?)
2766 .or_else(|| last_shown(start.paragraph..start.paragraph + 1));
2767 let units = std::iter::once((first(start.paragraph), own))
2768 .chain(
2769 outline
2770 .document
2771 .enclosing(start.paragraph)
2772 .into_iter()
2773 .map(|range| (first(range.start), last_shown(range))),
2774 )
2775 .chain(outline.title.then(|| {
2776 (
2777 first(0),
2778 last_shown(0..outline.document.paragraphs().count()),
2779 )
2780 }))
2781 .filter_map(|(unit_start, unit_end)| Some([unit_start, unit_end?]))
2782 .find(|&unit| unit[0] <= start && end <= unit[1] && unit != [start, end]);
2783 let (id, title) = (outline.id, outline.title);
2784 match units {
2785 Some(unit) => self.select(unit.into()),
2786 None if title => self.select_page(),
2787 None => self.select_outline(id),
2788 }
2789 }
2790
2791 /// Selects outline `id` as an object, as a click on its handle does.
2792 pub(crate) fn select_outline(&mut self, id: ExGuid) -> Result<(), EditError> {
2793 self.focus_outline(id)?;
2794 let whole = self.active_outline().whole();
2795 self.select(whole)?;
2796 self.whole = Some((Whole::Outline, id, whole));
2797 Ok(())
2798 }
2799
2800 /// What Select All has selected beyond text, if the selection is still what it chose.
2801 pub fn whole(&self) -> Option<Whole> {
2802 let (whole, id, selection) = self.whole?;
2803 let outline = self.active_outline();
2804 (outline.id == id && outline.selection == selection && self.composition.is_none())
2805 .then_some(whole)
2806 }
2807
2808 fn page_selected(&self) -> bool {
2809 self.whole() == Some(Whole::Page)
2810 }
2811
2812 /// Selects every body outline, focusing the last in page order as OneNote does: arrows
2813 /// leave the selection at its ends, and typing replaces its text.
2814 fn select_page(&mut self) -> Result<(), EditError> {
2815 let Some(last) = self.outlines.iter().rev().find(|outline| !outline.title) else {
2816 return Ok(());
2817 };
2818 let (id, whole) = (last.id, last.whole());
2819 self.focus_outline(id)?;
2820 self.select(whole)?;
2821 self.whole = Some((Whole::Page, id, whole));
2822 Ok(())
2823 }
2824
2825 /// Makes the history entries past `depth` one undo step, `page` when they edited OneNote's
2826 /// page selection; false when there are none. Groups among them are flattened into it.
2827 fn group(&mut self, depth: usize, mut page: bool) -> bool {
2828 let mut entries = Vec::new();
2829 for entry in self.undo.split_off(depth) {
2830 match entry {
2831 History::Group {
2832 entries: inner,
2833 page: selected,
2834 } => {
2835 page |= selected;
2836 entries.extend(inner);
2837 }
2838 entry => entries.push(entry),
2839 }
2840 }
2841 match entries.len() {
2842 0 => return false,
2843 1 if !page => self.undo.extend(entries),
2844 _ => self.undo.push(History::Group { entries, page }),
2845 }
2846 true
2847 }
2848
2849 /// Runs `edit`, an edit of OneNote's page selection, as one undo step.
2850 fn grouped<T>(
2851 &mut self,
2852 edit: impl FnOnce(&mut Self) -> Result<T, EditorError>,
2853 ) -> Result<T, EditorError> {
2854 let depth = self.undo.len();
2855 let result = edit(self);
2856 self.group(depth, true);
2857 result
2858 }
2859
2860 /// Runs `edit` once a selection crossing a table's edge is deleted, as OneNote 2010 deletes
2861 /// one before what replaces it goes in at its start, in one undo step; `None` for any
2862 /// other selection.
2863 fn across<T>(
2864 &mut self,
2865 engine: &mut TextEngine,
2866 edit: impl FnOnce(&mut Self, &mut TextEngine) -> Result<T, EditorError>,
2867 ) -> Result<Option<T>, EditorError> {
2868 let [anchor, focus] = self.active_outline().selection.positions;
2869 if !self
2870 .active_outline()
2871 .document
2872 .crosses_table(anchor.min(focus)..anchor.max(focus))?
2873 {
2874 return Ok(None);
2875 }
2876 let depth = self.undo.len();
2877 let result = self.delete(engine, false).and_then(|_| edit(self, engine));
2878 self.group(depth, false);
2879 result.map(Some)
2880 }
2881
2882 /// Runs `edit`, typing or a backspace at a caret, as part of the typing run the last undo
2883 /// step holds if the caret is where that run left it and nothing came between; OneNote 2010
2884 /// undoes a run whole however long it pauses.
2885 fn typed<T>(
2886 &mut self,
2887 edit: impl FnOnce(&mut Self) -> Result<T, EditorError>,
2888 ) -> Result<T, EditorError> {
2889 let [anchor, focus] = self.selection().positions;
2890 let caret = match self.marked_range() {
2891 Some(marked) => Some(marked.start),
2892 None => (anchor == focus).then_some(focus),
2893 };
2894 let joins = self.typing.take().is_some_and(|(outline, at, depth)| {
2895 outline == self.active_outline().id
2896 && caret == Some(at)
2897 && depth == self.undo.len()
2898 && self.pending.is_none()
2899 });
2900 let depth = self.undo.len() - usize::from(joins);
2901 let result = edit(self);
2902 if !self.group(depth, false) {
2903 return result;
2904 }
2905 if result.is_ok() {
2906 let caret = self.selection().positions[1];
2907 self.typing = Some((self.active_outline().id, caret, self.undo.len()));
2908 }
2909 result
2910 }
2911
2912 /// Removes the page selection's outlines, last first, but its last when `keep`, whose
2913 /// text stays selected to be replaced. Deleting the page selection leaves a caret where
2914 /// its first outline began, as OneNote 2010's does.
2915 fn remove_page(&mut self, engine: &mut TextEngine, keep: bool) -> Result<(), EditorError> {
2916 self.finish_composition();
2917 self.whole = None;
2918 let mut body = (0..self.outlines.len())
2919 .filter(|&index| !self.outlines[index].title)
2920 .collect::<Vec<_>>();
2921 let Some(&first) = body.first() else {
2922 return Ok(());
2923 };
2924 let kept = if keep { body.pop() } else { None };
2925 let caret = TextOutline::new(
2926 engine,
2927 TextDocument::new(vec![Paragraph::new(String::new(), Default::default())])?,
2928 DEFAULT_OUTLINE_WIDTH,
2929 self.outlines[first].origin(),
2930 )?;
2931 let index = self.outlines.len() - body.len();
2932 let focus = RestoreFocus::Caret {
2933 source: Box::new(caret.snapshot()),
2934 selection: caret.selection,
2935 index,
2936 };
2937 for &removed in body.iter().rev() {
2938 let outline = self.outlines.remove(removed);
2939 self.record(Ok(vec![PageOp::Delete { object: outline.id }]));
2940 self.undo.push(History::Insert {
2941 index: removed,
2942 outline: Box::new(outline),
2943 focus: focus.clone(),
2944 });
2945 }
2946 self.redo.clear();
2947 self.preferred_x = None;
2948 match kept {
2949 Some(kept) => {
2950 self.active = Focus::Outline(kept - body.len());
2951 self.select_all()?;
2952 }
2953 None => {
2954 self.active = Focus::Caret {
2955 outline: Box::new(caret),
2956 index,
2957 }
2958 }
2959 }
2960 Ok(())
2961 }
2962
2963 /// Moves every outline of the page selection by `delta`, as one undo step keeping it.
2964 pub fn move_page(&mut self, delta: [f32; 2]) -> Result<(), EditorError> {
2965 let moves = self
2966 .outlines
2967 .iter()
2968 .filter(|outline| !outline.title)
2969 .map(|outline| {
2970 let [x, y] = outline.origin();
2971 (outline.id, [x + delta[0], y + delta[1]])
2972 })
2973 .collect::<Vec<_>>();
2974 self.grouped(|editor| {
2975 for (id, position) in moves {
2976 editor.move_outline(id, position)?;
2977 }
2978 Ok(editor.select_page()?)
2979 })
2980 }
2981
2982 pub(crate) fn select_at(&mut self, x: f32, y: f32, extend: bool) -> Result<(), EditError> {
2983 let mut selection = self.selection_at(x, y, SelectionUnit::Grapheme)?;
2984 if extend {
2985 selection.positions[0] = self.selection().positions[0];
2986 selection.affinities[0] = self.selection().affinities[0];
2987 }
2988 self.select(selection)
2989 }
2990
2991 pub fn selection_at(
2992 &self,
2993 x: f32,
2994 y: f32,
2995 unit: SelectionUnit,
2996 ) -> Result<Selection, EditError> {
2997 if !x.is_finite() || !y.is_finite() {
2998 return Err(EditError::InvalidRange);
2999 }
3000 let index = self.active_outline().paragraph_at(x, y)?;
3001 let paragraph = &self.active_outline().shaped.paragraphs[index];
3002 let layout = &paragraph.text.shaped;
3003 let point = [x - paragraph.origin[0], y - paragraph.origin[1]];
3004 let local = edit::selection_at(
3005 layout,
3006 paragraph.text.hit_test(point[0], point[1]),
3007 point[0],
3008 unit,
3009 );
3010 let mut selection = self.selection();
3011 for (slot, cursor) in [local.anchor(), local.focus()].into_iter().enumerate() {
3012 let visible = paragraph.projection.text().utf16_offset(cursor.index())?;
3013 selection.positions[slot] = TextPosition {
3014 paragraph: self.active_outline().source_index(index),
3015 offset: paragraph
3016 .projection
3017 .source_offset(visible, crate::affinity(cursor.affinity()))?,
3018 };
3019 selection.affinities[slot] = cursor.affinity();
3020 }
3021 if unit == SelectionUnit::Paragraph
3022 && let Some(next) = self.active_outline().next_sibling_start(index)
3023 {
3024 selection.positions[1] = next;
3025 selection.affinities[1] = Affinity::Downstream;
3026 }
3027 Ok(selection)
3028 }
3029
3030 pub fn caret(&self, width: f32) -> Result<BoundingBox, EditError> {
3031 let selection = self.active_outline().selection;
3032 self.caret_at(selection.positions[1], selection.affinities[1], width)
3033 }
3034
3035 /// The caret at `focus` in the active outline, in its coordinates.
3036 pub fn caret_at(
3037 &self,
3038 focus: TextPosition,
3039 affinity: Affinity,
3040 width: f32,
3041 ) -> Result<BoundingBox, EditError> {
3042 self.active_outline().caret_at(focus, affinity, width)
3043 }
3044
3045 pub fn move_selection(
3046 &mut self,
3047 engine: &mut TextEngine,
3048 movement: Movement,
3049 extend: bool,
3050 ) -> Result<(), EditorError> {
3051 self.finish_composition();
3052 self.whole = None;
3053 self.typing = None;
3054 if !extend && movement == Movement::Right && self.leave_equation()? {
3055 return Ok(());
3056 }
3057 let [anchor, focus] = self.active_outline().selection.positions;
3058 if !extend
3059 && self.caret_outline().is_some()
3060 && self
3061 .active_outline()
3062 .document
3063 .paragraphs()
3064 .next()
3065 .unwrap()
3066 .text()
3067 .is_empty()
3068 && let Some(step) = match movement {
3069 Movement::Left => Some([-18.0, 0.0]),
3070 Movement::Right => Some([18.0, 0.0]),
3071 Movement::Up => Some([0.0, -18.0]),
3072 Movement::Down => Some([0.0, 18.0]),
3073 _ => None,
3074 }
3075 {
3076 let origin = self.active_outline().origin();
3077 let position = [origin[0] + step[0], origin[1] + step[1]];
3078 let center_y = position[1] + self.caret(1.0)?.height() as f32 * 0.5;
3079 let target = self
3080 .outlines
3081 .iter()
3082 .rev()
3083 .find(|outline| {
3084 let rect = outline.bounds();
3085 let bottom = rect.y1
3086 + if matches!(movement, Movement::Up) {
3087 36.0
3088 } else {
3089 0.0
3090 };
3091 (rect.x0..=rect.x1).contains(&f64::from(position[0]))
3092 && (rect.y0..=bottom).contains(&f64::from(center_y))
3093 })
3094 .map(|outline| (outline.id, outline.origin()));
3095 if let Some((id, origin)) = target {
3096 let x = position[0]
3097 + if step[0] == 0.0 {
3098 self.preferred_x.unwrap_or(0.0)
3099 } else {
3100 0.0
3101 }
3102 - origin[0];
3103 self.focus_outline(id)?;
3104 self.select_at(x, center_y - origin[1], false)?;
3105 self.preferred_x = (step[0] == 0.0).then_some(x);
3106 } else {
3107 let outline = self.active_outline_mut();
3108 outline.layout.x = Some(position[0]);
3109 outline.layout.y = Some(position[1]);
3110 if step[0] != 0.0 {
3111 self.preferred_x = None;
3112 }
3113 }
3114 return Ok(());
3115 }
3116 if matches!(
3117 movement,
3118 Movement::ParagraphStart
3119 | Movement::ParagraphEnd
3120 | Movement::DocumentStart
3121 | Movement::DocumentEnd
3122 ) {
3123 let outline = self.active_outline();
3124 let current = outline.visible_index(focus.paragraph)?;
3125 let end = matches!(movement, Movement::ParagraphEnd | Movement::DocumentEnd);
3126 let current_end = outline.document.paragraph(focus.paragraph).unwrap();
3127 let current_end = current_end.utf16_offset(current_end.text().len())?;
3128 let index = match movement {
3129 Movement::DocumentStart => 0,
3130 Movement::DocumentEnd => outline.shaped.paragraphs.len() - 1,
3131 Movement::ParagraphStart if focus.offset == 0 => current.saturating_sub(1),
3132 Movement::ParagraphEnd if focus.offset == current_end => {
3133 (current + 1).min(outline.shaped.paragraphs.len() - 1)
3134 }
3135 _ => current,
3136 };
3137 let source = outline.source_index(index);
3138 let paragraph = outline.document.paragraph(source).unwrap();
3139 let position = TextPosition {
3140 paragraph: source,
3141 offset: if end {
3142 paragraph.utf16_offset(paragraph.text().len())?
3143 } else {
3144 0
3145 },
3146 };
3147 let affinity = if end {
3148 Affinity::Upstream
3149 } else {
3150 Affinity::Downstream
3151 };
3152 let selection = &mut self.active_outline_mut().selection;
3153 if !extend {
3154 selection.positions[0] = position;
3155 selection.affinities[0] = affinity;
3156 }
3157 selection.positions[1] = position;
3158 selection.affinities[1] = affinity;
3159 self.preferred_x = None;
3160 return Ok(());
3161 }
3162 let horizontal = matches!(
3163 movement,
3164 Movement::Left | Movement::Right | Movement::WordLeft | Movement::WordRight
3165 );
3166 let left = matches!(movement, Movement::Left | Movement::WordLeft);
3167 if horizontal && !extend && anchor.paragraph != focus.paragraph {
3168 let index = usize::from(if left { anchor > focus } else { anchor < focus });
3169 self.active_outline_mut().selection.positions =
3170 [self.active_outline().selection.positions[index]; 2];
3171 self.active_outline_mut().selection.affinities =
3172 [self.active_outline().selection.affinities[index]; 2];
3173 self.preferred_x = None;
3174 return Ok(());
3175 }
3176 let mut index = self.active_outline().visible_index(focus.paragraph)?;
3177 let paragraph = &self.active_outline().shaped.paragraphs[index];
3178 let cursor =
3179 paragraph.cursor(focus.offset, self.active_outline().selection.affinities[1])?;
3180 let mut next;
3181 if matches!(movement, Movement::Up | Movement::Down) {
3182 let caret = paragraph.text.caret(cursor, 1.0);
3183 let x = self
3184 .preferred_x
3185 .unwrap_or(caret.x0 as f32 + paragraph.origin[0]);
3186 let line = paragraph
3187 .text
3188 .lines()
3189 .position(|(_, line)| f64::from(line.top) == caret.y0)
3190 .unwrap();
3191 let up = matches!(movement, Movement::Up);
3192 let target = if up {
3193 line.checked_sub(1)
3194 .and_then(|line| paragraph.text.lines().nth(line))
3195 } else {
3196 paragraph.text.lines().nth(line + 1)
3197 }
3198 .map(|(_, line)| line);
3199 let target = if target.is_some() {
3200 target
3201 } else {
3202 let neighbor = if self.active_outline().shaped.tables.is_empty() {
3203 if up {
3204 index.checked_sub(1)
3205 } else {
3206 (index + 1 < self.active_outline().shaped.paragraphs.len())
3207 .then_some(index + 1)
3208 }
3209 } else {
3210 let column = self.active_outline().column(x, paragraph.id)?;
3211 let current = column
3212 .iter()
3213 .position(|candidate| *candidate == index)
3214 .ok_or(EditError::InvalidStructure)?;
3215 if up {
3216 current.checked_sub(1).map(|i| column[i])
3217 } else {
3218 column.get(current + 1).copied()
3219 }
3220 };
3221 neighbor.and_then(|neighbor| {
3222 index = neighbor;
3223 let paragraph = &self.active_outline().shaped.paragraphs[index];
3224 if up {
3225 paragraph.text.lines().last()
3226 } else {
3227 paragraph.text.lines().next()
3228 }
3229 .map(|(_, line)| line)
3230 })
3231 };
3232 if target.is_none() && !extend && anchor == focus && !self.active_outline().title {
3233 let outline = self.active_outline();
3234 let last = outline.document.nodes().len() - 1;
3235 let up = matches!(movement, Movement::Up);
3236 let base_node = &outline.document.nodes()[if up { 0 } else { last }];
3237 let node = self.blank_paragraph(base_node)?;
3238 let trailing = outline
3239 .document
3240 .nodes()
3241 .iter()
3242 .rev()
3243 .take_while(|node| {
3244 node.text()
3245 .is_some_and(|text| text.text.text().bytes().all(|byte| byte == b' '))
3246 })
3247 .count();
3248 let position = if up {
3249 Some([
3250 outline.origin()[0] + x,
3251 (((f64::from(outline.origin()[1]) / 18.0).ceil() - 3.0) * 18.0 + 14.4)
3252 as f32,
3253 ])
3254 } else if outline.origin()[0] >= 180.0 && trailing >= 2 {
3255 let node = &outline.document.nodes()[last.saturating_sub(trailing)];
3256 let bottom = match &node.content {
3257 onestore::page::ParagraphContent::Text(_) => {
3258 let paragraph = outline
3259 .shaped
3260 .paragraphs
3261 .iter()
3262 .find(|paragraph| paragraph.id == node.id)
3263 .ok_or(EditError::InvalidStructure)?;
3264 f64::from(paragraph.origin[1]) + f64::from(paragraph.text.height())
3265 }
3266 onestore::page::ParagraphContent::Table(table) => {
3267 let cell = outline
3268 .shaped
3269 .tables
3270 .iter()
3271 .find(|layout| layout.id == table.id)
3272 .and_then(|layout| layout.cells.last())
3273 .ok_or(EditError::InvalidStructure)?;
3274 f64::from(cell.rect[3]) + 1.68
3275 }
3276 onestore::page::ParagraphContent::Image(onestore::page::Image {
3277 id,
3278 ..
3279 })
3280 | onestore::page::ParagraphContent::Attachment(
3281 onestore::page::Attachment { id, .. },
3282 )
3283 | onestore::page::ParagraphContent::Ink(onestore::page::Ink {
3284 id, ..
3285 })
3286 | onestore::page::ParagraphContent::Unsupported(
3287 onestore::page::Unsupported { id, .. },
3288 ) => outline
3289 .shaped
3290 .objects
3291 .iter()
3292 .find(|object| object.id == *id)
3293 .map(|object| f64::from(object.bottom))
3294 .ok_or(EditError::InvalidStructure)?,
3295 } + f64::from(outline.origin()[1]);
3296 let cell = (bottom - 14.4) / 18.0;
3297 // Stored coordinates can straddle a grid boundary by one f32 ULP.
3298 let grid = if (cell - cell.round()).abs()
3299 <= f64::from(f32::EPSILON) * cell.abs().max(1.0)
3300 {
3301 cell.round()
3302 } else {
3303 cell.ceil()
3304 };
3305 Some([outline.origin()[0], ((grid + 3.0) * 18.0 + 14.4) as f32])
3306 } else {
3307 None
3308 };
3309 if let Some(position) = position {
3310 let outline = TextOutline::new(
3311 engine,
3312 TextDocument::from_nodes(vec![node])?,
3313 DEFAULT_OUTLINE_WIDTH,
3314 position,
3315 )?;
3316 self.active = Focus::Caret {
3317 outline: Box::new(outline),
3318 index: self.outlines.len(),
3319 };
3320 self.preferred_x = Some(if up { 0.0 } else { x });
3321 return Ok(());
3322 }
3323 let edit = DocumentEdit {
3324 columns: BTreeMap::new(),
3325 container: None,
3326 range: last + 1..last + 1,
3327 replacement: vec![PageParagraph {
3328 parent: base_node.parent,
3329 ..node
3330 }],
3331 };
3332 let paragraph = outline.document.paragraphs().count();
3333 let previous = if let Focus::Outline(index) = self.active {
3334 let draft = self.outlines[index].clone();
3335 Some(std::mem::replace(
3336 &mut self.active,
3337 Focus::Draft {
3338 index,
3339 outline: Box::new(draft),
3340 },
3341 ))
3342 } else {
3343 None
3344 };
3345 if let Err(error) = self.apply(
3346 engine,
3347 edit,
3348 [TextPosition {
3349 paragraph,
3350 offset: 0,
3351 }; 2]
3352 .into(),
3353 false,
3354 None,
3355 ) {
3356 if let Some(previous) = previous {
3357 self.active = previous;
3358 }
3359 return Err(error);
3360 }
3361 self.preferred_x = Some(x);
3362 return Ok(());
3363 }
3364 next = target
3365 .map(|line| {
3366 self.active_outline().shaped.paragraphs[index]
3367 .text
3368 .hit_test(
3369 x - self.active_outline().shaped.paragraphs[index].origin[0],
3370 line.top + line.height * 0.5,
3371 )
3372 })
3373 .unwrap_or(cursor);
3374 self.preferred_x = Some(x);
3375 } else {
3376 let local = if anchor.paragraph == focus.paragraph {
3377 ParagraphSelection::new(
3378 paragraph
3379 .cursor(anchor.offset, self.active_outline().selection.affinities[0])?,
3380 cursor,
3381 )
3382 } else {
3383 cursor.into()
3384 };
3385 next = edit::step(&paragraph.text.shaped, local, movement, extend).focus();
3386 if horizontal && next == cursor && (extend || anchor == focus) {
3387 let neighbor = if left {
3388 index.checked_sub(1)
3389 } else {
3390 (index + 1 < self.active_outline().shaped.paragraphs.len()).then_some(index + 1)
3391 };
3392 if let Some(neighbor) = neighbor {
3393 index = neighbor;
3394 let paragraph = &self.active_outline().shaped.paragraphs[index];
3395 let line = if left {
3396 paragraph.text.lines().last()
3397 } else {
3398 paragraph.text.lines().next()
3399 }
3400 .unwrap()
3401 .1;
3402 next = paragraph.text.hit_test(
3403 if left { f32::MAX } else { -1.0 },
3404 line.top + line.height * 0.5,
3405 );
3406 if matches!(movement, Movement::WordLeft | Movement::WordRight) {
3407 next = edit::word_cursor(&paragraph.text.shaped, next, left);
3408 }
3409 }
3410 }
3411 }
3412 if !extend
3413 && anchor == focus
3414 && next == cursor
3415 && self.active_outline().source_index(index) == focus.paragraph
3416 && matches!(movement, Movement::Left | Movement::Right)
3417 {
3418 let caret = self.caret(1.0)?;
3419 let origin = self.active_outline().origin();
3420 let x = caret.x0 + f64::from(origin[0]);
3421 let y = (caret.y0 + caret.y1) * 0.5 + f64::from(origin[1]);
3422 let target = self
3423 .outlines
3424 .iter()
3425 .filter(|outline| outline.id != self.active_outline().id)
3426 .filter_map(|outline| {
3427 let rect = outline.bounds();
3428 let distance = if left { x - rect.x1 } else { rect.x0 - x };
3429 ((rect.y0..rect.y1).contains(&y) && distance > 0.0).then_some((
3430 distance,
3431 outline.id,
3432 outline.origin(),
3433 ))
3434 })
3435 .min_by(|a, b| a.0.total_cmp(&b.0));
3436 if let Some((_, id, origin)) = target {
3437 self.focus_outline(id)?;
3438 self.select_at(
3439 if left { f32::MAX } else { -1.0 },
3440 y as f32 - origin[1],
3441 false,
3442 )?;
3443 return Ok(());
3444 }
3445 }
3446 if !matches!(movement, Movement::Up | Movement::Down) {
3447 self.preferred_x = None;
3448 }
3449 let paragraph = &self.active_outline().shaped.paragraphs[index];
3450 let visible = paragraph.projection.text().utf16_offset(next.index())?;
3451 let position = TextPosition {
3452 paragraph: self.active_outline().source_index(index),
3453 offset: paragraph
3454 .projection
3455 .source_offset(visible, crate::affinity(next.affinity()))?,
3456 };
3457 if !extend {
3458 self.active_outline_mut().selection.positions[0] = position;
3459 self.active_outline_mut().selection.affinities[0] = next.affinity();
3460 }
3461 self.active_outline_mut().selection.positions[1] = position;
3462 self.active_outline_mut().selection.affinities[1] = next.affinity();
3463 Ok(())
3464 }
3465
3466 pub(crate) fn selection_rects(&self) -> Result<Vec<BoundingBox>, EditError> {
3467 self.active_outline()
3468 .range_rects(self.active_outline().selection)
3469 }
3470
3471 pub(crate) fn marked_rects(&self) -> Result<Vec<BoundingBox>, EditError> {
3472 match &self.composition {
3473 Some(composition) => self.active_outline().range_rects(Selection {
3474 positions: [composition.range.start, composition.range.end],
3475 affinities: [Affinity::Downstream, Affinity::Upstream],
3476 }),
3477 None => Ok(Vec::new()),
3478 }
3479 }
3480
3481 pub fn replace(
3482 &mut self,
3483 engine: &mut TextEngine,
3484 replacement: Vec<Paragraph>,
3485 ) -> Result<(), EditorError> {
3486 self.take_objects()?;
3487 let start = self.active_outline().selection.positions[0]
3488 .min(self.active_outline().selection.positions[1]);
3489 let end = self.active_outline().selection.positions[0]
3490 .max(self.active_outline().selection.positions[1]);
3491 let last = replacement.last().ok_or(EditError::InvalidRange)?;
3492 let offset = last.utf16_offset(last.text().len())?;
3493 let offset = if replacement.len() == 1 {
3494 start
3495 .offset
3496 .checked_add(offset)
3497 .ok_or(EditError::TextTooLong)?
3498 } else {
3499 offset
3500 };
3501 let caret = TextPosition {
3502 paragraph: start
3503 .paragraph
3504 .checked_add(replacement.len() - 1)
3505 .ok_or(EditError::TextTooLong)?,
3506 offset,
3507 };
3508 let edit = self
3509 .active_outline()
3510 .document
3511 .replace(start..end, replacement)?;
3512 self.commit(
3513 engine,
3514 edit,
3515 Selection {
3516 positions: [caret; 2],
3517 affinities: [Affinity::Upstream; 2],
3518 },
3519 )
3520 }
3521
3522 /// Replaces the selection with `text` in the formatting of the text it replaces, as a
3523 /// spelling correction does.
3524 pub(crate) fn correct(
3525 &mut self,
3526 engine: &mut TextEngine,
3527 text: &str,
3528 ) -> Result<(), EditorError> {
3529 let [anchor, focus] = self.selection().positions;
3530 let end = anchor.max(focus);
3531 let format = self
3532 .active_outline()
3533 .document
3534 .paragraph(end.paragraph)
3535 .ok_or(EditError::InvalidRange)?
3536 .format_at(end.offset)?
3537 .clone();
3538 self.replace(engine, vec![Paragraph::new(text.to_owned(), format)])
3539 }
3540
3541 /// Types `text` at the selection; a space ends a typed URL, which becomes a link. A line
3542 /// of text joins the typing run before it, and may complete a Markdown shortcut.
3543 pub fn insert(&mut self, engine: &mut TextEngine, text: &str) -> Result<(), EditorError> {
3544 if text.is_empty() || text.contains('\n') {
3545 return self.insert_text(engine, text);
3546 }
3547 self.typed(|editor| editor.insert_text(engine, text))?;
3548 self.markdown(engine, text)
3549 }
3550
3551 /// Insert Symbol: `symbol` at the selection, as an undo step that neither joins the
3552 /// typing before it nor takes the typing after.
3553 pub fn insert_symbol(
3554 &mut self,
3555 engine: &mut TextEngine,
3556 symbol: char,
3557 ) -> Result<(), EditorError> {
3558 self.typing = None;
3559 self.insert(engine, symbol.encode_utf8(&mut [0; 4]))?;
3560 self.typing = None;
3561 Ok(())
3562 }
3563
3564 fn insert_text(&mut self, engine: &mut TextEngine, text: &str) -> Result<(), EditorError> {
3565 if self.page_selected() {
3566 return self.grouped(|editor| {
3567 editor.remove_page(engine, true)?;
3568 editor.insert_text(engine, text)
3569 });
3570 }
3571 self.leave_link_code()?;
3572 let start = self.active_outline().selection.positions[0]
3573 .min(self.active_outline().selection.positions[1]);
3574 let format = self.typing_format(start)?;
3575 let replacement = text
3576 .split('\n')
3577 .map(|line| Paragraph::new(line.to_owned(), format.clone()))
3578 .collect();
3579 self.replace(engine, replacement)?;
3580 if matches!(text, " " | "\t") {
3581 self.link_typed_url(engine, start)?;
3582 self.build_typed_equation(engine, start)?;
3583 }
3584 Ok(())
3585 }
3586
3587 /// Pastes plain text as OneNote does: lines become plain Calibri 11 paragraphs without style
3588 /// or list between the halves of the caret's paragraph (`evidence/structural-edits/xml/c7-*`),
3589 /// an empty half dropped; from the title they go where [`Self::leave_title_for_body`] puts
3590 /// the caret.
3591 /// Pasted runs take the clipboard's `language`, an LCID, not the caret's run's as typing
3592 /// does; Windows derives it from the keyboard language at copy time.
3593 pub fn paste(
3594 &mut self,
3595 engine: &mut TextEngine,
3596 text: &str,
3597 language: u32,
3598 ) -> Result<(), EditorError> {
3599 if self.page_selected() {
3600 return self.grouped(|editor| {
3601 editor.remove_page(engine, true)?;
3602 editor.paste(engine, text, language)
3603 });
3604 }
3605 self.take_objects()?;
3606 let lines = text
3607 .split('\n')
3608 .map(|line| line.strip_suffix('\r').unwrap_or(line))
3609 .collect::<Vec<_>>();
3610 if lines.len() > 1 && self.active_outline().title {
3611 self.leave_title_for_body(engine)?;
3612 }
3613 let last = lines[lines.len() - 1];
3614 let [anchor, focus] = self.active_outline().selection.positions;
3615 let (start, end) = (anchor.min(focus), anchor.max(focus));
3616 if lines.len() == 1 {
3617 let mut format = self.typing_format(start)?;
3618 format.language = Some(language);
3619 return self.replace(engine, vec![Paragraph::new(last.to_owned(), format)]);
3620 }
3621 if let Some(pasted) = self.across(engine, |editor, engine| {
3622 editor.paste(engine, text, language)
3623 })? {
3624 return Ok(pasted);
3625 }
3626 let edge = Paragraph::new(String::new(), self.typing_format(start)?);
3627 let pasted = Format {
3628 font: Some("Calibri".into()),
3629 font_size: Some(11.0),
3630 language: Some(language),
3631 ..Format::default()
3632 };
3633 let mut edit = self.active_outline().document.replace(
3634 start..end,
3635 std::iter::once(edge.clone())
3636 .chain(
3637 lines
3638 .iter()
3639 .map(|line| Paragraph::new((*line).to_owned(), pasted.clone())),
3640 )
3641 .chain([edge])
3642 .collect(),
3643 )?;
3644 for node in &mut edit.replacement[1..=lines.len()] {
3645 node.style = None;
3646 node.lists.clear();
3647 }
3648 let head = clip::drop_empty_halves(&mut edit, lines.len(), true);
3649 let caret = TextPosition {
3650 paragraph: start.paragraph + lines.len() - usize::from(head),
3651 offset: u32::try_from(last.encode_utf16().count())
3652 .map_err(|_| EditError::TextTooLong)?,
3653 };
3654 self.commit(
3655 engine,
3656 edit,
3657 Selection {
3658 positions: [caret; 2],
3659 affinities: [Affinity::Upstream; 2],
3660 },
3661 )
3662 }
3663
3664 /// Enter as OneNote 2010 does (`evidence/structural-edits`): the new paragraph takes the
3665 /// level, lists and character formatting at the caret but no note tags. At a paragraph's
3666 /// start its tags stay with its text below; an empty paragraph opens a plain one above and
3667 /// leaves the list it ends; a heading continues as body text. Unlike OneNote, a to-do list
3668 /// continues as lists do: the new paragraph takes unchecked copies of the check boxes, and
3669 /// an empty item loses them.
3670 fn split(&mut self, engine: &mut TextEngine) -> Result<(), EditorError> {
3671 if let Some(split) = self.across(engine, Self::split)? {
3672 return Ok(split);
3673 }
3674 let [anchor, focus] = self.active_outline().selection.positions;
3675 let (start, end) = (anchor.min(focus), anchor.max(focus));
3676 let mut format = self.typing_format(start)?;
3677 // A link or equation ends with its paragraph.
3678 link::unlinked(&mut format);
3679 if format.math == Some(true) {
3680 let text = self.active_outline().document.paragraph(start.paragraph);
3681 format = equation::text_after(text.ok_or(EditError::InvalidRange)?);
3682 }
3683 let document = &self.active_outline().document;
3684 let mut edit = document.replace(
3685 start..end,
3686 vec![Paragraph::new(String::new(), format.clone()); 2],
3687 )?;
3688 let nodes = document.container(edit.container)?;
3689 let next = nodes
3690 .get(crate::document::subtree_end(nodes, edit.range.start))
3691 .filter(|next| {
3692 let node = &nodes[edit.range.start];
3693 next.parent == node.parent && next.level == node.level
3694 });
3695 let [head, tail, ..] = &mut edit.replacement[..] else {
3696 unreachable!("a split holds both halves")
3697 };
3698 let empty = |node: &PageParagraph| node.text().unwrap().text.text().is_empty();
3699 if start.offset == 0 {
3700 tail.tags = std::mem::take(&mut head.tags);
3701 tail.text_mut().unwrap().tags = std::mem::take(&mut head.text_mut().unwrap().tags);
3702 }
3703 // Enter at a paragraph's end gives the new one its style's NextStyle, as OneNote 2010
3704 // follows a heading with Normal.
3705 let following = match head.style.and_then(|id| self.definitions.get(&id)) {
3706 Some(Definition {
3707 kind: Kind::Style { name, next },
3708 ..
3709 }) => next.clone().filter(|next| Some(next) != name.as_ref()),
3710 _ => None,
3711 };
3712 if empty(head) && empty(tail) {
3713 head.lists.clear();
3714 if next.is_none_or(|next| next.lists.is_empty()) {
3715 tail.lists.clear();
3716 }
3717 self.end_checks(tail);
3718 } else {
3719 if start.offset == 0 {
3720 self.continue_checks(tail, head);
3721 } else {
3722 self.continue_checks(head, tail);
3723 }
3724 if empty(tail)
3725 && let Some(following) = following
3726 {
3727 tail.style = self.style_named(&following)?;
3728 // The heading's run formatting carries over under the body style (`c9-h1-*`).
3729 let run = format.over(&self.style_format(head.style)?);
3730 tail.text_mut().unwrap().text =
3731 Paragraph::new(String::new(), run.inherit(&self.style_format(tail.style)?));
3732 }
3733 }
3734 let caret = TextPosition {
3735 paragraph: start.paragraph + 1,
3736 offset: 0,
3737 };
3738 self.commit(
3739 engine,
3740 edit,
3741 Selection {
3742 positions: [caret; 2],
3743 affinities: [Affinity::Upstream; 2],
3744 },
3745 )
3746 }
3747
3748 /// Tab or Shift+Tab at the start of the selected paragraphs; see [`crate::document::indent`].
3749 /// A default bullet or number steps to the style OneNote gives its new depth.
3750 pub fn indent(&mut self, engine: &mut TextEngine, outdent: bool) -> Result<bool, EditorError> {
3751 let outline = self.active_outline();
3752 if outline.title {
3753 return Ok(false);
3754 }
3755 let selection = outline.selection;
3756 let [anchor, focus] = selection.positions;
3757 let start = anchor.min(focus);
3758 let end = anchor.max(focus);
3759 let (container, local_start, _) = outline
3760 .document
3761 .leaf(start.paragraph)
3762 .ok_or(EditError::InvalidRange)?;
3763 let (end_container, local_end, _) = outline
3764 .document
3765 .leaf(end.paragraph)
3766 .ok_or(EditError::InvalidRange)?;
3767 // OneNote 2010's Tab leaves a selection crossing a table's edge alone.
3768 if container != end_container {
3769 return Ok(false);
3770 }
3771 let range = local_start
3772 ..local_end + usize::from(end.offset != 0 || start.paragraph == end.paragraph);
3773 let nodes = outline.document.container(container)?;
3774 let Some(mut edit) = crate::document::indent(nodes, container, range, outdent) else {
3775 return Ok(false);
3776 };
3777 let levels = nodes[edit.range.clone()]
3778 .iter()
3779 .map(|node| node.level)
3780 .collect::<Vec<_>>();
3781 for (node, level) in edit.replacement.iter_mut().zip(levels) {
3782 if node.level == level {
3783 continue;
3784 }
3785 for list in &mut node.lists {
3786 let Some(definition) = self
3787 .definitions
3788 .get(list)
3789 .and_then(|definition| format::nested_list(definition, node.level > level))
3790 else {
3791 continue;
3792 };
3793 *list = onestore::page::text::new_id()?;
3794 self.definitions.insert(*list, definition);
3795 }
3796 }
3797 self.commit(engine, edit, selection)?;
3798 Ok(true)
3799 }
3800
3801 /// Alt+Shift+Up or Down: swaps the selected paragraphs, with their children, and the
3802 /// sibling above or below with its children. Does nothing past the first or last sibling.
3803 pub(crate) fn move_paragraphs(
3804 &mut self,
3805 engine: &mut TextEngine,
3806 up: bool,
3807 ) -> Result<bool, EditorError> {
3808 use crate::document::{previous_sibling, subtree_end};
3809 let outline = self.active_outline();
3810 if outline.title {
3811 return Ok(false);
3812 }
3813 let selection = outline.selection;
3814 let [anchor, focus] = selection.positions;
3815 let document = &outline.document;
3816 let (container, first, _) = document
3817 .leaf(anchor.min(focus).paragraph)
3818 .ok_or(EditError::InvalidRange)?;
3819 let (end_container, last, _) = document
3820 .leaf(anchor.max(focus).paragraph)
3821 .ok_or(EditError::InvalidRange)?;
3822 if container != end_container {
3823 return Ok(false);
3824 }
3825 let nodes = document.container(container)?;
3826 let parent = nodes[first].parent;
3827 let mut end = first;
3828 while end <= last {
3829 if nodes[end].parent != parent {
3830 return Ok(false);
3831 }
3832 end = subtree_end(nodes, end);
3833 }
3834 let (range, [moved, other]) = if up {
3835 let Some(sibling) = previous_sibling(nodes, first, &BTreeSet::new()) else {
3836 return Ok(false);
3837 };
3838 (sibling..end, [first..end, sibling..first])
3839 } else {
3840 if nodes.get(end).is_none_or(|node| node.parent != parent) {
3841 return Ok(false);
3842 }
3843 let next = subtree_end(nodes, end);
3844 (first..next, [first..end, end..next])
3845 };
3846 let passed = leaves(&nodes[other.clone()], None).count();
3847 let replacement = if up {
3848 [&nodes[moved], &nodes[other]].concat()
3849 } else {
3850 [&nodes[other], &nodes[moved]].concat()
3851 };
3852 let mut moved = selection;
3853 for position in &mut moved.positions {
3854 position.paragraph = if up {
3855 position.paragraph - passed
3856 } else {
3857 position.paragraph + passed
3858 };
3859 }
3860 let edit = DocumentEdit {
3861 columns: BTreeMap::new(),
3862 container,
3863 range,
3864 replacement,
3865 };
3866 self.commit(engine, edit, moved)?;
3867 Ok(true)
3868 }
3869
3870 /// Deletes the selection or the character beside the caret. Backspace at a paragraph's start
3871 /// first removes its list, then outdents it, then joins it to the paragraph above, whose
3872 /// properties win; Delete at its end joins the paragraph below (`evidence/structural-edits/
3873 /// xml/c4-*`, `c5b-*`). Joins pass over a collapsed paragraph's hidden children.
3874 pub fn delete(&mut self, engine: &mut TextEngine, backward: bool) -> Result<bool, EditorError> {
3875 if self.page_selected() {
3876 self.grouped(|editor| editor.remove_page(engine, false))?;
3877 return Ok(true);
3878 }
3879 self.take_objects()?;
3880 let selection = self.active_outline().selection;
3881 let [anchor, focus] = selection.positions;
3882 let mut range = anchor.min(focus)..anchor.max(focus);
3883 let at_caret = range.is_empty();
3884 if backward
3885 && at_caret
3886 && self.formatted.take() == Some((self.active_outline().id, focus, self.undo.len()))
3887 {
3888 return self.undo(engine);
3889 }
3890 if at_caret {
3891 let outline = self.active_outline();
3892 let paragraph = outline.paragraph_layout(focus.paragraph)?;
3893 let cursor = paragraph.cursor(focus.offset, selection.affinities[1])?;
3894 let Some(cluster) =
3895 cursor.logical_clusters(&paragraph.text.shaped)[usize::from(!backward)]
3896 else {
3897 let (container, local, node) = outline
3898 .document
3899 .leaf(focus.paragraph)
3900 .ok_or(EditError::InvalidRange)?;
3901 if backward && !node.lists.is_empty() {
3902 let item = PageParagraph {
3903 lists: Vec::new(),
3904 ..node.clone()
3905 };
3906 let edit = DocumentEdit {
3907 columns: BTreeMap::new(),
3908 container,
3909 range: local..local + 1,
3910 replacement: vec![item],
3911 };
3912 self.commit(engine, edit, selection)?;
3913 return Ok(true);
3914 }
3915 if backward && node.level > 1 {
3916 return self.indent(engine, true);
3917 }
3918 let visible = outline.visible_index(focus.paragraph)?;
3919 let Some(neighbor) = (if backward {
3920 visible.checked_sub(1)
3921 } else {
3922 Some(visible + 1).filter(|next| *next < outline.shaped.paragraphs.len())
3923 }) else {
3924 return Ok(false);
3925 };
3926 let neighbor = outline.source_index(neighbor);
3927 let [upper, lower] = if backward {
3928 [neighbor, focus.paragraph]
3929 } else {
3930 [focus.paragraph, neighbor]
3931 };
3932 let (_, _, top) = outline
3933 .document
3934 .leaf(upper)
3935 .ok_or(EditError::InvalidRange)?;
3936 let base = self.style_format(top.style)?;
3937 let Some(edit) = outline.document.join(upper, lower, &base)? else {
3938 return Ok(false);
3939 };
3940 let text = outline.document.paragraph(upper).unwrap();
3941 let caret = TextPosition {
3942 paragraph: upper,
3943 offset: text.utf16_offset(text.text().len())?,
3944 };
3945 self.commit(
3946 engine,
3947 edit,
3948 Selection {
3949 positions: [caret; 2],
3950 affinities: [Affinity::Upstream; 2],
3951 },
3952 )?;
3953 return Ok(true);
3954 };
3955 let visible = paragraph.projection.text();
3956 let bytes = cluster.text_range();
3957 range.start.offset = paragraph.projection.source_offset(
3958 visible.utf16_offset(bytes.start)?,
3959 onestore::page::text::Affinity::Downstream,
3960 )?;
3961 range.end.offset = paragraph.projection.source_offset(
3962 visible.utf16_offset(bytes.end)?,
3963 onestore::page::text::Affinity::Upstream,
3964 )?;
3965 }
3966 if backward && at_caret {
3967 self.typed(|editor| editor.delete_range(engine, range))
3968 } else {
3969 self.delete_range(engine, range)
3970 }
3971 }
3972
3973 pub fn delete_to(
3974 &mut self,
3975 engine: &mut TextEngine,
3976 movement: Movement,
3977 ) -> Result<bool, EditorError> {
3978 if self.page_selected() {
3979 return self.delete(engine, false);
3980 }
3981 self.finish_composition();
3982 let original = self.selection();
3983 let (preferred_x, typing) = (self.preferred_x, self.typing);
3984 let [caret, focus] = original.positions;
3985 let collapsed = caret == focus;
3986 if collapsed {
3987 let result = self.move_selection(engine, movement, true);
3988 if let Err(error) = result {
3989 self.active_outline_mut().selection = original;
3990 (self.preferred_x, self.typing) = (preferred_x, typing);
3991 return Err(error);
3992 }
3993 }
3994 let [anchor, focus] = self.selection().positions;
3995 self.active_outline_mut().selection = original;
3996 (self.preferred_x, self.typing) = (preferred_x, typing);
3997 let range = anchor.min(focus)..anchor.max(focus);
3998 if collapsed && range.end == caret && range.start.paragraph == caret.paragraph {
3999 self.typed(|editor| editor.delete_range(engine, range))
4000 } else {
4001 self.delete_range(engine, range)
4002 }
4003 }
4004
4005 fn delete_range(
4006 &mut self,
4007 engine: &mut TextEngine,
4008 range: Range<TextPosition>,
4009 ) -> Result<bool, EditorError> {
4010 if range.is_empty() {
4011 return Ok(false);
4012 }
4013 let caret = range.start;
4014 let format = self
4015 .active_outline()
4016 .document
4017 .paragraph(caret.paragraph)
4018 .unwrap()
4019 .format_at(caret.offset)?
4020 .clone();
4021 let edit = self
4022 .active_outline()
4023 .document
4024 .replace(range, vec![Paragraph::new(String::new(), format)])?;
4025 self.commit(
4026 engine,
4027 edit,
4028 Selection {
4029 positions: [caret; 2],
4030 affinities: [Affinity::Upstream; 2],
4031 },
4032 )?;
4033 Ok(true)
4034 }
4035
4036 pub fn can_undo(&self) -> bool {
4037 !self.undo.is_empty()
4038 }
4039
4040 pub fn can_redo(&self) -> bool {
4041 !self.redo.is_empty()
4042 }
4043
4044 /// How many steps Undo and Redo hold.
4045 pub fn history_depth(&self) -> [usize; 2] {
4046 [self.undo.len(), self.redo.len()]
4047 }
4048
4049 /// Takes up `parked`, the editor this page had when it was last left, history and all,
4050 /// in place of this one: what storage changed since reaches it as `refresh` would.
4051 pub(crate) fn resume(
4052 &mut self,
4053 mut parked: CanvasEditor,
4054 engine: &mut TextEngine,
4055 ) -> Result<(), EditorError> {
4056 if let Some((page, _)) = &self.stored {
4057 parked.refresh(page.clone(), engine)?;
4058 parked.default_font = std::mem::take(&mut self.default_font);
4059 parked.markdown = self.markdown.take();
4060 *self = parked;
4061 }
4062 Ok(())
4063 }
4064
4065 pub fn undo(&mut self, engine: &mut TextEngine) -> Result<bool, EditorError> {
4066 if self.composition.is_some() {
4067 return self.cancel_composition(engine);
4068 }
4069 let Some(history) = self.undo.pop() else {
4070 return Ok(false);
4071 };
4072 match self.apply_history(engine, history) {
4073 Ok(inverse) => {
4074 self.unstore_provisional();
4075 let page = matches!(inverse, History::Group { page: true, .. });
4076 self.redo.push(inverse);
4077 if page {
4078 self.select_page()?;
4079 }
4080 Ok(true)
4081 }
4082 Err((history, error)) => {
4083 self.undo.push(history);
4084 Err(error)
4085 }
4086 }
4087 }
4088
4089 /// Removes from storage each stored provisional paragraph an undo left empty and the only
4090 /// text of its outline, which shows it still.
4091 fn unstore_provisional(&mut self) {
4092 let removed: Vec<ExGuid> = self
4093 .provisional
4094 .iter()
4095 .filter(|(outline, (blank, stored))| {
4096 let Some(outline) = self.outlines.iter().find(|item| item.id == **outline) else {
4097 return false;
4098 };
4099 let mut leaves = crate::document::leaves(outline.document.nodes(), None);
4100 *stored
4101 && matches!(
4102 (leaves.next(), leaves.next()),
4103 (Some((None, _, node)), None) if node.id == blank.id
4104 && node.tags.is_empty()
4105 && node.lists.is_empty()
4106 && node.text().is_some_and(|text| {
4107 text.text.text().is_empty() && text.tags.is_empty()
4108 })
4109 )
4110 })
4111 .map(|(outline, _)| *outline)
4112 .collect();
4113 for outline in removed {
4114 let (blank, _) = &self.provisional[&outline];
4115 self.record(Ok(vec![PageOp::Delete { object: blank.id }]));
4116 self.provisional.get_mut(&outline).unwrap().1 = false;
4117 }
4118 }
4119
4120 pub fn redo(&mut self, engine: &mut TextEngine) -> Result<bool, EditorError> {
4121 self.finish_composition();
4122 let Some(history) = self.redo.pop() else {
4123 return Ok(false);
4124 };
4125 match self.apply_history(engine, history) {
4126 Ok(inverse) => {
4127 self.undo.push(inverse);
4128 Ok(true)
4129 }
4130 Err((history, error)) => {
4131 self.redo.push(history);
4132 Err(error)
4133 }
4134 }
4135 }
4136
4137 fn apply_history(
4138 &mut self,
4139 engine: &mut TextEngine,
4140 history: History,
4141 ) -> Result<History, (History, EditorError)> {
4142 self.pending = None;
4143 self.typing = None;
4144 let valid = match &history {
4145 History::Date(date) => self
4146 .date
4147 .as_ref()
4148 .is_some_and(|current| current.source().id == date.source().id),
4149 History::Paper { .. } => true,
4150 History::Draft {
4151 outlines, index, ..
4152 } => {
4153 *index <= self.outlines.len()
4154 && self
4155 .outlines
4156 .iter()
4157 .position(|item| item.id == outlines[0].id)
4158 .is_none_or(|found| found == *index)
4159 }
4160 History::Text { outline, .. } | History::Layout { outline, .. } => {
4161 self.outlines.iter().any(|item| item.id == *outline)
4162 }
4163 History::Position { object, .. } => self.object_layout_mut(*object).is_some(),
4164 History::Split {
4165 outline, at, part, ..
4166 } => {
4167 self.outlines
4168 .iter()
4169 .find(|item| item.id == *outline)
4170 .is_some_and(|item| (1..item.document.nodes().len()).contains(at))
4171 && !self.outlines.iter().any(|item| item.id == *part)
4172 }
4173 History::Join { outline, part, .. } => [outline, part]
4174 .iter()
4175 .all(|id| self.outlines.iter().any(|item| item.id == **id)),
4176 History::Image { image, .. } => self.image(*image).is_some(),
4177 History::Tags { object, .. } => self.object_tags_mut(*object).is_some(),
4178 History::Picture {
4179 index,
4180 image: Some(_),
4181 } => *index <= self.objects.len(),
4182 History::Picture { index, image: None } => self
4183 .objects
4184 .get(*index)
4185 .and_then(page::Content::picture)
4186 .is_some(),
4187 History::File {
4188 index,
4189 file: Some(_),
4190 } => *index <= self.objects.len(),
4191 History::File { index, file: None } => self
4192 .objects
4193 .get(*index)
4194 .and_then(page::Content::file)
4195 .is_some(),
4196 History::Ink {
4197 index,
4198 ink: Some(_),
4199 } => *index <= self.objects.len(),
4200 History::Ink { index, ink: None } => {
4201 matches!(self.objects.get(*index), Some(page::Content::Ink(_)))
4202 }
4203 History::Strokes { ink, .. } => self.has_ink(*ink),
4204 History::Remove { outline, focus } => {
4205 self.outlines.iter().any(|item| item.id == *outline)
4206 && match focus {
4207 RestoreFocus::Outline(focus) => {
4208 outline != focus && self.outlines.iter().any(|item| item.id == *focus)
4209 }
4210 RestoreFocus::Caret { .. } => true,
4211 }
4212 }
4213 History::Insert { index, .. } | History::Restore { index, .. } => {
4214 *index <= self.outlines.len()
4215 }
4216 History::Group { .. } => true,
4217 };
4218 if !valid {
4219 return Err((history, EditError::InvalidRange.into()));
4220 }
4221 let inverse = match history {
4222 History::Date(date) => {
4223 let shown = self.date.replace(*date).unwrap();
4224 self.record(Ok(self.date_ops()));
4225 History::Date(Box::new(shown))
4226 }
4227 History::Paper { color, rule_lines } => self.paper(color, rule_lines),
4228 History::Draft {
4229 outlines,
4230 index,
4231 caret,
4232 selection,
4233 restore_caret,
4234 } => {
4235 let draft = if restore_caret {
4236 match TextOutline::from_outline(engine, &caret, &self.definitions) {
4237 Ok(mut draft) => {
4238 draft.selection = selection;
4239 Some(draft)
4240 }
4241 Err(error) => {
4242 return Err((
4243 History::Draft {
4244 outlines,
4245 index,
4246 caret,
4247 selection,
4248 restore_caret,
4249 },
4250 error,
4251 ));
4252 }
4253 }
4254 } else {
4255 None
4256 };
4257 let outline = outlines[usize::from(!restore_caret)].clone();
4258 let id = outline.id;
4259 let stored = self
4260 .outlines
4261 .get(index)
4262 .is_some_and(|item| item.id == id)
4263 .then(|| self.outlines.remove(index));
4264 self.active = if outline.is_empty() {
4265 Focus::Caret {
4266 outline: Box::new(outline),
4267 index,
4268 }
4269 } else {
4270 self.outlines.insert(index, outline);
4271 match draft {
4272 Some(outline) => Focus::Draft {
4273 index,
4274 outline: Box::new(outline),
4275 },
4276 None => Focus::Outline(index),
4277 }
4278 };
4279 self.record(self.outline_ops(id, stored.as_ref()));
4280 History::Draft {
4281 outlines,
4282 index,
4283 caret,
4284 selection,
4285 restore_caret: !restore_caret,
4286 }
4287 }
4288 History::Text { outline, change } => {
4289 let index = self
4290 .outlines
4291 .iter()
4292 .position(|item| item.id == outline)
4293 .unwrap();
4294 let previous = std::mem::replace(&mut self.active, Focus::Outline(index));
4295 let inverse = match self.apply(
4296 engine,
4297 change.edit.clone(),
4298 change.selection,
4299 false,
4300 Some(&change.positions),
4301 ) {
4302 Ok(change) => change,
4303 Err(error) => {
4304 self.active = previous;
4305 return Err((History::Text { outline, change }, error));
4306 }
4307 };
4308 self.record(self.change_ops(&self.outlines[index], &inverse));
4309 History::Text {
4310 outline,
4311 change: Box::new(inverse),
4312 }
4313 }
4314 History::Position { object, position } => {
4315 let layout = self.object_layout_mut(object).unwrap();
4316 let previous = [layout.x, layout.y];
4317 [layout.x, layout.y] = position;
4318 if let Some(index) = self.outlines.iter().position(|item| item.id == object) {
4319 self.active = Focus::Outline(index);
4320 }
4321 self.record(self.placement_ops(&Placement {
4322 id: object,
4323 position: previous,
4324 }));
4325 History::Position {
4326 object,
4327 position: previous,
4328 }
4329 }
4330 History::Split {
4331 outline,
4332 at,
4333 part,
4334 position,
4335 } => match self.split_outline(engine, outline, at, part, position) {
4336 Ok(parents) => History::Join {
4337 outline,
4338 part,
4339 parents,
4340 },
4341 Err(error) => {
4342 return Err((
4343 History::Split {
4344 outline,
4345 at,
4346 part,
4347 position,
4348 },
4349 error,
4350 ));
4351 }
4352 },
4353 History::Join {
4354 outline,
4355 part,
4356 parents,
4357 } => match self.join_outline(engine, outline, part, &parents) {
4358 Ok((at, position)) => History::Split {
4359 outline,
4360 at,
4361 part,
4362 position,
4363 },
4364 Err(error) => {
4365 return Err((
4366 History::Join {
4367 outline,
4368 part,
4369 parents,
4370 },
4371 error,
4372 ));
4373 }
4374 },
4375 History::Layout { outline, layout } => {
4376 let index = self
4377 .outlines
4378 .iter()
4379 .position(|item| item.id == outline)
4380 .unwrap();
4381 let mut source = self.outlines[index].snapshot();
4382 let previous = std::mem::replace(&mut source.layout, layout);
4383 let mut resized =
4384 match TextOutline::from_outline(engine, &source, &self.definitions) {
4385 Ok(resized) => resized,
4386 Err(error) => {
4387 return Err((
4388 History::Layout {
4389 outline,
4390 layout: source.layout,
4391 },
4392 error,
4393 ));
4394 }
4395 };
4396 resized.selection = self.outlines[index].selection;
4397 self.outlines[index] = resized;
4398 self.record(ops::layout_ops(
4399 outline,
4400 &previous,
4401 &self.outlines[index].layout,
4402 ));
4403 self.active = Focus::Outline(index);
4404 History::Layout {
4405 outline,
4406 layout: previous,
4407 }
4408 }
4409 History::Tags { object, tags } => History::Tags {
4410 object,
4411 tags: self.swap_object_tags(object, tags),
4412 },
4413 History::Image { image, layout } => {
4414 let previous =
4415 std::mem::replace(&mut self.image_mut(image).unwrap().layout, layout);
4416 self.record(Ok(ops::picture_layout(self.image(image).unwrap())));
4417 History::Image {
4418 image,
4419 layout: previous,
4420 }
4421 }
4422 History::Picture { index, image } => History::Picture {
4423 index,
4424 image: match image {
4425 Some(image) => {
4426 self.add_picture(index, *image);
4427 None
4428 }
4429 None => Some(Box::new(self.remove_picture(index))),
4430 },
4431 },
4432 History::File { index, file } => History::File {
4433 index,
4434 file: match file {
4435 Some(file) => {
4436 self.add_file(index, *file);
4437 None
4438 }
4439 None => Some(Box::new(self.remove_file(index))),
4440 },
4441 },
4442 History::Ink { index, ink } => History::Ink {
4443 index,
4444 ink: match ink {
4445 Some(ink) => {
4446 self.add_ink(index, *ink);
4447 None
4448 }
4449 None => Some(Box::new(self.remove_ink(index))),
4450 },
4451 },
4452 History::Strokes { ink, strokes } => self.set_strokes(ink, strokes),
4453 History::Remove { outline, focus } => {
4454 let index = self
4455 .outlines
4456 .iter()
4457 .position(|item| item.id == outline)
4458 .unwrap();
4459 let next_focus = match &focus {
4460 RestoreFocus::Caret {
4461 source,
4462 selection,
4463 index,
4464 } => match TextOutline::from_outline(engine, source, &self.definitions) {
4465 Ok(mut outline) => {
4466 outline.selection = *selection;
4467 Focus::Caret {
4468 outline: Box::new(outline),
4469 index: *index,
4470 }
4471 }
4472 Err(error) => return Err((History::Remove { outline, focus }, error)),
4473 },
4474 RestoreFocus::Outline(id) => {
4475 let previous = self
4476 .outlines
4477 .iter()
4478 .position(|outline| outline.id == *id)
4479 .unwrap();
4480 Focus::Outline(previous - usize::from(previous > index))
4481 }
4482 };
4483 let outline = Box::new(self.outlines.remove(index));
4484 self.record(Ok(vec![PageOp::Delete { object: outline.id }]));
4485 self.active = next_focus;
4486 History::Insert {
4487 index,
4488 outline,
4489 focus,
4490 }
4491 }
4492 History::Restore {
4493 index,
4494 source,
4495 selection,
4496 focus,
4497 } => {
4498 let mut outline =
4499 match TextOutline::from_outline(engine, &source, &self.definitions) {
4500 Ok(outline) => outline,
4501 Err(error) => {
4502 return Err((
4503 History::Restore {
4504 index,
4505 source,
4506 selection,
4507 focus,
4508 },
4509 error,
4510 ));
4511 }
4512 };
4513 outline.selection = selection;
4514 return self.apply_history(
4515 engine,
4516 History::Insert {
4517 index,
4518 outline: Box::new(outline),
4519 focus,
4520 },
4521 );
4522 }
4523 History::Insert {
4524 index,
4525 outline,
4526 focus,
4527 } => {
4528 let id = outline.id;
4529 self.outlines.insert(index, *outline);
4530 self.record(self.outline_ops(id, None));
4531 self.active = Focus::Outline(index);
4532 History::Remove { outline: id, focus }
4533 }
4534 History::Group { mut entries, page } => {
4535 let mut inverses = Vec::with_capacity(entries.len());
4536 while let Some(entry) = entries.pop() {
4537 match self.apply_history(engine, entry) {
4538 Ok(inverse) => inverses.push(inverse),
4539 Err((entry, error)) => {
4540 entries.push(entry);
4541 return Err((History::Group { entries, page }, error));
4542 }
4543 }
4544 }
4545 History::Group {
4546 entries: inverses,
4547 page,
4548 }
4549 }
4550 };
4551 self.preferred_x = None;
4552 Ok(inverse)
4553 }
4554
4555 pub fn marked_range(&self) -> Option<Range<TextPosition>> {
4556 self.composition
4557 .as_ref()
4558 .map(|composition| composition.range.clone())
4559 }
4560
4561 pub fn compose(
4562 &mut self,
4563 engine: &mut TextEngine,
4564 text: String,
4565 selected: Range<u32>,
4566 ) -> Result<(), EditorError> {
4567 if selected.start > selected.end {
4568 return Err(EditError::InvalidRange.into());
4569 }
4570 if self.page_selected() {
4571 self.grouped(|editor| editor.remove_page(engine, true))?;
4572 }
4573 let range = self.marked_range().unwrap_or_else(|| {
4574 self.active_outline().selection.positions[0]
4575 .min(self.active_outline().selection.positions[1])
4576 ..self.active_outline().selection.positions[0]
4577 .max(self.active_outline().selection.positions[1])
4578 });
4579 let positions = [
4580 inserted_position(range.start, &text, selected.start)?,
4581 inserted_position(range.start, &text, selected.end)?,
4582 ];
4583 let end = inserted_position(
4584 range.start,
4585 &text,
4586 text.encode_utf16()
4587 .count()
4588 .try_into()
4589 .map_err(|_| EditError::TextTooLong)?,
4590 )?;
4591 let format = self.typing_format(range.start)?;
4592 let replacement = text
4593 .split('\n')
4594 .map(|part| Paragraph::new(part.to_owned(), format.clone()))
4595 .collect();
4596 let mut edit = self
4597 .active_outline()
4598 .document
4599 .replace(range.clone(), replacement)?;
4600 self.own_lists(&mut edit)?;
4601 self.link_notes(&mut edit)?;
4602 self.fit_columns(engine, &mut edit)?;
4603 let inverse = self.apply(
4604 engine,
4605 edit,
4606 Selection {
4607 positions,
4608 affinities: [Affinity::Downstream; 2],
4609 },
4610 false,
4611 None,
4612 )?;
4613 let original = if let Some(composition) = self.composition.take() {
4614 let mut original = composition.original;
4615 // Nodes this edit reaches past what the composition changed are still as they were.
4616 let changed = original.edit.range.len();
4617 original
4618 .edit
4619 .replacement
4620 .extend(inverse.edit.replacement.iter().skip(changed).cloned());
4621 original.edit.range = inverse.edit.range;
4622 for (id, widths) in inverse.edit.columns {
4623 original.edit.columns.entry(id).or_insert(widths);
4624 }
4625 for position in inverse.positions {
4626 if !original
4627 .positions
4628 .iter()
4629 .any(|original| original.id == position.id)
4630 {
4631 original.positions.push(position);
4632 }
4633 }
4634 original
4635 } else {
4636 inverse
4637 };
4638 self.composition = Some(Composition {
4639 original,
4640 range: range.start..end,
4641 });
4642 Ok(())
4643 }
4644
4645 pub fn finish_composition(&mut self) {
4646 if let Some(composition) = self.composition.take() {
4647 self.record_change(composition.original);
4648 }
4649 }
4650
4651 pub fn cancel_composition(&mut self, engine: &mut TextEngine) -> Result<bool, EditorError> {
4652 let Some(composition) = &self.composition else {
4653 return Ok(false);
4654 };
4655 let original = composition.original.clone();
4656 self.apply(
4657 engine,
4658 original.edit,
4659 original.selection,
4660 false,
4661 Some(&original.positions),
4662 )?;
4663 self.composition = None;
4664 Ok(true)
4665 }
4666
4667 pub fn commit_text(
4668 &mut self,
4669 engine: &mut TextEngine,
4670 text: String,
4671 ) -> Result<(), EditorError> {
4672 if self.composition.is_some() {
4673 let end = text
4674 .encode_utf16()
4675 .count()
4676 .try_into()
4677 .map_err(|_| EditError::TextTooLong)?;
4678 let lines = text.contains('\n');
4679 let commit = |editor: &mut Self| {
4680 editor.compose(engine, text, end..end)?;
4681 editor.finish_composition();
4682 Ok(())
4683 };
4684 if lines {
4685 commit(self)
4686 } else {
4687 self.typed(commit)
4688 }
4689 } else {
4690 self.insert(engine, &text)
4691 }
4692 }
4693
4694 fn commit(
4695 &mut self,
4696 engine: &mut TextEngine,
4697 mut edit: DocumentEdit,
4698 selection: Selection,
4699 ) -> Result<(), EditorError> {
4700 self.own_lists(&mut edit)?;
4701 self.link_notes(&mut edit)?;
4702 self.fit_columns(engine, &mut edit)?;
4703 let inverse = self.apply(engine, edit, selection, true, None)?;
4704 self.record_change(inverse);
4705 Ok(())
4706 }
4707
4708 /// Gives each paragraph an edit adds its own copy of the lists it carries, as OneNote keeps
4709 /// a list node per paragraph.
4710 fn own_lists(&mut self, edit: &mut DocumentEdit) -> Result<(), EditError> {
4711 let existing = descendants(
4712 &self.active_outline().document.container(edit.container)?[edit.range.clone()],
4713 None,
4714 )
4715 .map(|(_, _, node)| node.id)
4716 .collect::<BTreeSet<_>>();
4717 let mut pending = edit.replacement.iter_mut().collect::<Vec<_>>();
4718 while let Some(node) = pending.pop() {
4719 if !existing.contains(&node.id) {
4720 for list in &mut node.lists {
4721 let definition = self
4722 .definitions
4723 .get(list)
4724 .ok_or(EditError::InvalidStructure)?
4725 .clone();
4726 *list = onestore::page::text::new_id()?;
4727 self.definitions.insert(*list, definition);
4728 }
4729 }
4730 if let ParagraphContent::Table(table) = &mut node.content {
4731 pending.extend(
4732 table
4733 .rows
4734 .iter_mut()
4735 .flat_map(|row| &mut row.cells)
4736 .flat_map(|cell| &mut cell.paragraphs),
4737 );
4738 }
4739 }
4740 Ok(())
4741 }
4742
4743 fn record_change(&mut self, change: TextChange) {
4744 self.pending = None;
4745 if let Focus::Draft { index, .. } = self.active {
4746 let Focus::Draft { mut outline, .. } =
4747 std::mem::replace(&mut self.active, Focus::Outline(index))
4748 else {
4749 unreachable!()
4750 };
4751 let mut caret = outline.snapshot();
4752 crate::document::container_mut(&mut caret.paragraphs, change.edit.container)
4753 .expect("inverse edit container exists")
4754 .splice(change.edit.range, change.edit.replacement);
4755 let mut columns = change.edit.columns;
4756 crate::document::swap_columns(&mut caret.paragraphs, &mut columns);
4757 let last_selected = outline.selection.positions[0]
4758 .paragraph
4759 .max(outline.selection.positions[1].paragraph);
4760 let mut count = outline.document.nodes().len();
4761 let mut selected_end = 0;
4762 let selected_root = outline
4763 .document
4764 .nodes()
4765 .iter()
4766 .position(|node| {
4767 selected_end += leaves(std::slice::from_ref(node), None).count();
4768 selected_end > last_selected
4769 })
4770 .expect("selection addresses a text leaf");
4771 let original_ids = self.outlines[index]
4772 .document
4773 .nodes()
4774 .iter()
4775 .map(|node| node.id)
4776 .collect::<BTreeSet<_>>();
4777 while count > selected_root + 1 {
4778 let node = &outline.document.nodes()[count - 1];
4779 if node.text().is_none_or(|text| !text.text.text().is_empty())
4780 || original_ids.contains(&node.id)
4781 {
4782 break;
4783 }
4784 count -= 1;
4785 }
4786 if count < outline.document.nodes().len() {
4787 let edit = DocumentEdit {
4788 columns: BTreeMap::new(),
4789 container: None,
4790 range: count..outline.document.nodes().len(),
4791 replacement: Vec::new(),
4792 };
4793 let relayout = outline
4794 .shaped
4795 .relayout(
4796 outline.document.nodes(),
4797 &edit,
4798 edit.range.clone(),
4799 &outline.indents,
4800 outline.wrap_width(),
4801 outline.layout.width_set_by_user == Some(true),
4802 &mut |_, _, _, _| Err(LayoutError::UnsupportedContent),
4803 )
4804 .expect("removing trailing text lays nothing out anew");
4805 outline
4806 .document
4807 .apply(edit)
4808 .expect("removing a provisional suffix preserves a valid document");
4809 outline.shaped.commit(relayout);
4810 }
4811 if outline.document == self.outlines[index].document {
4812 self.outlines[index].selection = outline.selection;
4813 return;
4814 }
4815 let versions = Box::new([self.outlines[index].clone(), (*outline).clone()]);
4816 if outline.is_empty() {
4817 self.outlines.remove(index);
4818 self.active = Focus::Caret { outline, index };
4819 } else {
4820 self.outlines[index] = *outline;
4821 }
4822 self.record(self.outline_ops(versions[0].id, Some(&versions[0])));
4823 self.undo.push(History::Draft {
4824 outlines: versions,
4825 index,
4826 caret: Box::new(caret),
4827 selection: change.selection,
4828 restore_caret: true,
4829 });
4830 } else if self.caret_outline().is_some() {
4831 if self.active_outline().is_empty() {
4832 return;
4833 }
4834 let Focus::Caret { index, .. } = self.active else {
4835 unreachable!()
4836 };
4837 let Focus::Caret { outline, .. } =
4838 std::mem::replace(&mut self.active, Focus::Outline(index))
4839 else {
4840 unreachable!();
4841 };
4842 let mut source = outline.snapshot();
4843 source.paragraphs = change.edit.replacement;
4844 let id = outline.id;
4845 self.undo.push(History::Remove {
4846 outline: id,
4847 focus: RestoreFocus::Caret {
4848 source: Box::new(source),
4849 selection: change.selection,
4850 index,
4851 },
4852 });
4853 self.outlines.insert(index, *outline);
4854 self.record(self.outline_ops(id, None));
4855 } else if self.active_outline().is_empty() && !self.active_outline().title {
4856 let Focus::Outline(index) = self.active else {
4857 unreachable!()
4858 };
4859 let outline = self.outlines.remove(index);
4860 self.record(Ok(vec![PageOp::Delete { object: outline.id }]));
4861 let mut source = outline.snapshot();
4862 source
4863 .paragraphs
4864 .splice(change.edit.range, change.edit.replacement);
4865 self.undo.push(History::Restore {
4866 index,
4867 source: Box::new(source),
4868 selection: change.selection,
4869 focus: RestoreFocus::Caret {
4870 source: Box::new(outline.snapshot()),
4871 selection: outline.selection,
4872 index,
4873 },
4874 });
4875 self.active = Focus::Caret {
4876 outline: Box::new(outline),
4877 index,
4878 };
4879 } else {
4880 self.record(self.change_ops(self.active_outline(), &change));
4881 self.undo.push(History::Text {
4882 outline: self.active_outline().id,
4883 change: Box::new(change),
4884 });
4885 }
4886 self.redo.clear();
4887 }
4888
4889 fn apply(
4890 &mut self,
4891 engine: &mut TextEngine,
4892 edit: DocumentEdit,
4893 selection: Selection,
4894 finish_composition: bool,
4895 positions: Option<&[Placement]>,
4896 ) -> Result<TextChange, EditorError> {
4897 let outline = self.active_outline();
4898 outline.document.validate_edit(&edit)?;
4899 let range = match edit.container {
4900 None => edit.range.clone(),
4901 Some(cell) => {
4902 let root = outline.document.root(cell)?;
4903 root..root + 1
4904 }
4905 };
4906 let relayout = outline.shaped.relayout(
4907 outline.document.nodes(),
4908 &edit,
4909 range,
4910 &outline.indents,
4911 outline.wrap_width(),
4912 outline.layout.width_set_by_user == Some(true),
4913 &mut |node, previous, width, indents| {
4914 ParagraphLayout::shape(engine, node, previous, width, indents, &self.definitions)
4915 },
4916 )?;
4917 let [replaced, ..] = outline.shaped.pieces(relayout.range.clone());
4918 for position in selection.positions {
4919 let (node, before) = outline
4920 .document
4921 .edited_leaf(&edit, position.paragraph)
4922 .ok_or(EditError::InvalidRange)?;
4923 node.text().unwrap().text.byte_offset(position.offset)?;
4924 // A paragraph the edit leaves keeps its layout unless that layout is replaced.
4925 if !relayout
4926 .segment
4927 .paragraphs
4928 .iter()
4929 .any(|paragraph| paragraph.id == node.id)
4930 && !before
4931 .and_then(|before| outline.visible_index(before).ok())
4932 .is_some_and(|index| !replaced.contains(&index))
4933 {
4934 return Err(EditError::InvalidRange.into());
4935 }
4936 }
4937 let placements = match positions {
4938 Some(positions) => positions.to_vec(),
4939 None => self.title_flow(relayout.size[1])?,
4940 };
4941 let previous_positions = placements
4942 .iter()
4943 .map(|placement| {
4944 let layout = self
4945 .object_layouts()
4946 .find_map(|(id, layout)| (id == placement.id).then_some(layout))
4947 .ok_or(EditError::InvalidStructure)?;
4948 if placement
4949 .position
4950 .iter()
4951 .flatten()
4952 .any(|value| !value.is_finite())
4953 {
4954 return Err(EditorError::InvalidGeometry);
4955 }
4956 Ok(Placement {
4957 id: placement.id,
4958 position: [layout.x, layout.y],
4959 })
4960 })
4961 .collect::<Result<Vec<_>, EditorError>>()?;
4962 if finish_composition && self.composition.is_some() {
4963 // Committing marked text can remove provisional paragraphs from the validated view.
4964 self.finish_composition();
4965 return self.apply(engine, edit, selection, false, positions);
4966 }
4967 let outline = self.active_outline_mut();
4968 let inverse = outline.document.splice(edit);
4969 outline.shaped.commit(relayout);
4970 self.preferred_x = None;
4971 let previous_selection =
4972 std::mem::replace(&mut self.active_outline_mut().selection, selection);
4973 for placement in placements {
4974 let layout = self
4975 .object_layout_mut(placement.id)
4976 .expect("validated object position");
4977 [layout.x, layout.y] = placement.position;
4978 }
4979 Ok(TextChange {
4980 edit: inverse,
4981 selection: previous_selection,
4982 positions: previous_positions,
4983 })
4984 }
4985}
4986
4987/// Whether `content` is picture or file `id`.
4988fn holds(content: &ParagraphContent, id: ExGuid) -> bool {
4989 match content {
4990 ParagraphContent::Image(image) => image.id == id,
4991 ParagraphContent::Attachment(file) => file.id == id,
4992 _ => false,
4993 }
4994}
4995
4996/// Where `position` in `old` lies in `new`, the same outline changed elsewhere: in the same
4997/// paragraph, past what changed in its text when it lies after it; at the start of the
4998/// paragraph now at its place when that one is gone.
4999fn follow(old: &TextDocument, new: &TextDocument, position: TextPosition) -> TextPosition {
5000 let found = old.leaf(position.paragraph).and_then(|(_, _, node)| {
5001 let paragraph = new
5002 .text_nodes()
5003 .position(|candidate| candidate.id == node.id)?;
5004 Some((paragraph, node, new.leaf(paragraph)?.2))
5005 });
5006 let Some((paragraph, before, after)) = found else {
5007 let count = new.text_nodes().count();
5008 return TextPosition {
5009 paragraph: position.paragraph.min(count.saturating_sub(1)),
5010 offset: 0,
5011 };
5012 };
5013 let units = |text: &str| text.encode_utf16().count() as u32;
5014 let (a, b) = (
5015 before.text().unwrap().text.text(),
5016 after.text().unwrap().text.text(),
5017 );
5018 let prefix = units(
5019 &a[..a
5020 .char_indices()
5021 .zip(b.chars())
5022 .find(|((_, x), y)| x != y)
5023 .map_or(a.len().min(b.len()), |((at, _), _)| at)],
5024 );
5025 let suffix = a
5026 .chars()
5027 .rev()
5028 .zip(b.chars().rev())
5029 .take_while(|(x, y)| x == y)
5030 .map(|(x, _)| x.len_utf16() as u32)
5031 .scan(0, |sum, units| {
5032 *sum += units;
5033 Some(*sum)
5034 })
5035 .take_while(|sum| prefix + sum <= units(a).min(units(b)))
5036 .last()
5037 .unwrap_or(0);
5038 let (old_length, new_length) = (units(a), units(b));
5039 let offset = if position.offset <= prefix {
5040 position.offset
5041 } else if position.offset >= old_length - suffix {
5042 new_length - (old_length - position.offset)
5043 } else {
5044 new_length - suffix
5045 };
5046 TextPosition { paragraph, offset }
5047}
5048
5049fn inserted_position(
5050 mut position: TextPosition,
5051 text: &str,
5052 offset: u32,
5053) -> Result<TextPosition, EditError> {
5054 let mut units = 0_u32;
5055 for character in text.chars() {
5056 if units == offset {
5057 return Ok(position);
5058 }
5059 units = units
5060 .checked_add(character.len_utf16() as u32)
5061 .ok_or(EditError::TextTooLong)?;
5062 if units > offset {
5063 return Err(EditError::InvalidRange);
5064 }
5065 if character == '\n' {
5066 position.paragraph = position
5067 .paragraph
5068 .checked_add(1)
5069 .ok_or(EditError::TextTooLong)?;
5070 position.offset = 0;
5071 } else {
5072 position.offset = position
5073 .offset
5074 .checked_add(character.len_utf16() as u32)
5075 .ok_or(EditError::TextTooLong)?;
5076 }
5077 }
5078 if units == offset {
5079 Ok(position)
5080 } else {
5081 Err(EditError::InvalidRange)
5082 }
5083}
5084
5085#[cfg(test)]
5086mod tests {
5087 use super::*;
5088 use onestore::document::Format;
5089
5090 fn table_editor(engine: &mut TextEngine) -> CanvasEditor {
5091 use onestore::page::text::new_id;
5092 use onestore::page::{ParagraphContent, Table, TableCell, TableColumn, TableRow};
5093 let mut editor = CanvasEditor::new(
5094 engine,
5095 TextDocument::new(vec![Paragraph::new("Left".into(), Format::default())]).unwrap(),
5096 180.0,
5097 )
5098 .unwrap();
5099 let first = editor.active_outline().document.nodes()[0].clone();
5100 let wrapper = PageParagraph {
5101 id: new_id().unwrap(),
5102 parent: None,
5103 level: 1,
5104 style: None,
5105 format: Format::default(),
5106 lists: Vec::new(),
5107 tags: Vec::new(),
5108 media: Default::default(),
5109 collapsed: false,
5110 content: ParagraphContent::Table(Table {
5111 id: new_id().unwrap(),
5112 columns: vec![
5113 TableColumn {
5114 width: 72.0,
5115 locked: true
5116 };
5117 2
5118 ],
5119 rows: [["Left", "Right"], ["Below", "Last"]]
5120 .into_iter()
5121 .enumerate()
5122 .map(|(row, texts)| TableRow {
5123 id: new_id().unwrap(),
5124 cells: texts
5125 .into_iter()
5126 .enumerate()
5127 .map(|(column, text)| TableCell {
5128 id: new_id().unwrap(),
5129 layout: Default::default(),
5130 indents: vec![18.0, 0.0, 27.0],
5131 shading: None,
5132 paragraphs: vec![if row == 0 && column == 0 {
5133 first.clone()
5134 } else {
5135 crate::document::node(
5136 Paragraph::new(text.into(), Format::default()),
5137 Format::default(),
5138 )
5139 .unwrap()
5140 }],
5141 unsupported: Vec::new(),
5142 })
5143 .collect(),
5144 })
5145 .collect(),
5146 borders: Some(true),
5147 layout: Default::default(),
5148 tags: Vec::new(),
5149 }),
5150 };
5151 editor
5152 .commit(
5153 engine,
5154 DocumentEdit {
5155 columns: BTreeMap::new(),
5156 container: None,
5157 range: 0..1,
5158 replacement: vec![wrapper],
5159 },
5160 [TextPosition {
5161 paragraph: 1,
5162 offset: 0,
5163 }; 2]
5164 .into(),
5165 )
5166 .unwrap();
5167 editor
5168 }
5169
5170 #[test]
5171 fn structured_construction_and_resize_preserve_cells_and_enclose_locked_columns() {
5172 let mut engine = TextEngine::default();
5173 let mut source = table_editor(&mut engine).active_outline().snapshot();
5174 let onestore::page::ParagraphContent::Table(table) = &mut source.paragraphs[0].content
5175 else {
5176 panic!()
5177 };
5178 table.rows[0].cells[0].paragraphs[0]
5179 .text_mut()
5180 .unwrap()
5181 .text = Paragraph::new(String::new(), Format::default());
5182 source.paragraphs.push(
5183 crate::document::node(
5184 Paragraph::new(
5185 "Text below the table must use the same width during drag and after release."
5186 .into(),
5187 Format::default(),
5188 ),
5189 Format::default(),
5190 )
5191 .unwrap(),
5192 );
5193 let document = TextDocument::from_nodes(source.paragraphs.clone()).unwrap();
5194 let mut editor = CanvasEditor::new(&mut engine, document.clone(), 180.0).unwrap();
5195 let imported = TextOutline::from_outline(&mut engine, &source, &BTreeMap::new()).unwrap();
5196 let cells = |outline: &TextOutline| {
5197 outline
5198 .shaped
5199 .tables
5200 .iter()
5201 .flat_map(|table| {
5202 table
5203 .cells
5204 .iter()
5205 .map(move |cell| (table.id, table.borders, cell.id, cell.rect))
5206 })
5207 .collect::<Vec<_>>()
5208 };
5209 assert_eq!(editor.active_outline().layout.width_set_by_user, Some(true));
5210 assert_eq!(editor.active_outline().shaped.size, imported.shaped.size);
5211 assert_eq!(cells(editor.active_outline()), cells(&imported));
5212 for (actual, expected) in editor
5213 .active_outline()
5214 .shaped
5215 .paragraphs
5216 .iter()
5217 .zip(&imported.shaped.paragraphs)
5218 {
5219 assert_eq!(actual.id, expected.id);
5220 assert_eq!(actual.origin, expected.origin);
5221 assert_eq!(actual.text.height(), expected.text.height());
5222 }
5223 let minimum = 2.0 * (72.0 + 4.98) - 1.83;
5224 let preview = editor.preview_resize(&mut engine, 36.0).unwrap();
5225 assert!((preview.bounds().width() as f32 - minimum).abs() < 0.0001);
5226 assert_eq!(preview.document, document);
5227 assert_eq!(cells(&preview), cells(&imported));
5228 assert!((preview.wrap_width() - minimum).abs() < 0.0001);
5229 let final_width_preview = editor
5230 .preview_resize(&mut engine, preview.bounds().width() as f32)
5231 .unwrap();
5232 assert_eq!(preview.shaped.size, final_width_preview.shaped.size);
5233 assert_eq!(
5234 preview
5235 .shaped
5236 .paragraphs
5237 .last()
5238 .unwrap()
5239 .text
5240 .lines()
5241 .count(),
5242 final_width_preview
5243 .shaped
5244 .paragraphs
5245 .last()
5246 .unwrap()
5247 .text
5248 .lines()
5249 .count()
5250 );
5251 assert_eq!(editor.active_outline().bounds().width(), 180.0);
5252 assert!(editor.undo.is_empty());
5253 editor.resize(&mut engine, 36.0).unwrap();
5254 assert_eq!(editor.active_outline().bounds(), preview.bounds());
5255 editor.undo(&mut engine).unwrap();
5256 assert_eq!(editor.active_outline().bounds().width(), 180.0);
5257 editor.redo(&mut engine).unwrap();
5258 assert_eq!(editor.active_outline().bounds(), preview.bounds());
5259 assert_eq!(editor.active_outline().document, document);
5260 }
5261
5262 #[test]
5263 fn nested_table_extents_remain_reachable_beyond_the_parent_columns() {
5264 use onestore::page::ParagraphContent;
5265 let mut engine = TextEngine::default();
5266 let mut source = table_editor(&mut engine).active_outline().snapshot();
5267 let mut nested = table_editor(&mut engine).active_outline().document.nodes()[0].clone();
5268 let ParagraphContent::Table(table) = &mut nested.content else {
5269 panic!()
5270 };
5271 table.columns[0].width = 240.0;
5272 table.columns[1].width = 240.0;
5273 let ParagraphContent::Table(table) = &mut source.paragraphs[0].content else {
5274 panic!()
5275 };
5276 table.rows[0].cells[1].paragraphs = vec![nested];
5277 source.layout.width_set_by_user = Some(false);
5278 let mut editor =
5279 CanvasEditor::from_outlines(&mut engine, vec![source], BTreeMap::new()).unwrap();
5280 let paragraph = editor.active_outline().paragraph_layout(2).unwrap();
5281 let point = [
5282 paragraph.origin[0] + 1.0,
5283 paragraph.origin[1] + paragraph.text.height() * 0.5,
5284 ];
5285 assert!(point[0] > 180.0);
5286 assert!(f64::from(point[0]) < editor.active_outline().bounds().x1);
5287 assert_eq!(
5288 editor
5289 .selection_at(point[0], point[1], SelectionUnit::Grapheme)
5290 .unwrap()
5291 .positions[1]
5292 .paragraph,
5293 2
5294 );
5295 let preview = editor.preview_resize(&mut engine, 36.0).unwrap();
5296 assert!(preview.bounds().x1 > f64::from(point[0]));
5297 editor
5298 .select(
5299 [TextPosition {
5300 paragraph: 2,
5301 offset: 0,
5302 }; 2]
5303 .into(),
5304 )
5305 .unwrap();
5306 editor.insert(&mut engine, "!").unwrap();
5307 assert!(editor.active_outline().bounds().x1 > f64::from(point[0]));
5308 editor.undo(&mut engine).unwrap();
5309 assert!(editor.active_outline().bounds().x1 > f64::from(point[0]));
5310 }
5311
5312 #[test]
5313 fn automatic_table_bounds_add_typing_room_only_to_root_text() {
5314 let mut engine = TextEngine::default();
5315 let mut source = table_editor(&mut engine).active_outline().snapshot();
5316 source.layout.width_set_by_user = Some(false);
5317 let outline = TextOutline::from_outline(&mut engine, &source, &BTreeMap::new()).unwrap();
5318 assert_eq!(outline.bounds().width(), f64::from(outline.shaped.size[0]));
5319 source.paragraphs.push(
5320 crate::document::node(
5321 Paragraph::new(
5322 "A root paragraph needs room to continue typing".into(),
5323 Format::default(),
5324 ),
5325 Format::default(),
5326 )
5327 .unwrap(),
5328 );
5329 let outline = TextOutline::from_outline(&mut engine, &source, &BTreeMap::new()).unwrap();
5330 let paragraph = outline.shaped.paragraphs.last().unwrap();
5331 let expected = (paragraph.origin[0] + paragraph.text.shaped.width() + 36.0)
5332 .clamp(72.0, 180.0)
5333 .max(outline.shaped.size[0]);
5334 assert_eq!(outline.bounds().width(), f64::from(expected));
5335 assert!(outline.bounds().width() > f64::from(outline.shaped.size[0]));
5336 }
5337
5338 #[test]
5339 fn cell_mouse_targets_and_paragraph_selection_follow_columns() {
5340 let mut engine = TextEngine::default();
5341 let mut editor = table_editor(&mut engine);
5342 editor
5343 .select(
5344 [TextPosition {
5345 paragraph: 0,
5346 offset: 4,
5347 }; 2]
5348 .into(),
5349 )
5350 .unwrap();
5351 editor.insert(&mut engine, "\nSecond\nThird").unwrap();
5352 for source in 0..6 {
5353 let paragraph = editor.active_outline().paragraph_layout(source).unwrap();
5354 let (_, line) = paragraph.text.lines().next().unwrap();
5355 let x = paragraph.origin[0] + 1.0;
5356 let y = paragraph.origin[1] + line.top + line.height * 0.5;
5357 for unit in [
5358 SelectionUnit::Grapheme,
5359 SelectionUnit::Word,
5360 SelectionUnit::Paragraph,
5361 ] {
5362 let selection = editor.selection_at(x, y, unit).unwrap();
5363 assert_eq!(selection.positions[0].paragraph, source);
5364 if unit == SelectionUnit::Paragraph {
5365 let end = if source < 2 {
5366 TextPosition {
5367 paragraph: source + 1,
5368 offset: 0,
5369 }
5370 } else {
5371 let text = editor
5372 .active_outline()
5373 .document
5374 .paragraphs()
5375 .nth(source)
5376 .unwrap();
5377 TextPosition {
5378 paragraph: source,
5379 offset: text.utf16_offset(text.text().len()).unwrap(),
5380 }
5381 };
5382 assert_eq!(selection.positions[1], end);
5383 }
5384 }
5385 }
5386 let right = editor.active_outline().shaped.tables[0].cells[1].rect;
5387 for x in [right[0] + 0.5, right[2] - 0.5, f32::MAX] {
5388 let selection = editor
5389 .selection_at(x, right[3] - 1.0, SelectionUnit::Grapheme)
5390 .unwrap();
5391 assert_eq!(selection.positions[1].paragraph, 3);
5392 }
5393 let left = editor.active_outline().shaped.tables[0].cells[0].rect;
5394 let selection = editor
5395 .selection_at(-f32::MAX, left[3] - 1.0, SelectionUnit::Grapheme)
5396 .unwrap();
5397 assert_eq!(selection.positions[1].paragraph, 2);
5398 let history = editor.undo.len();
5399 let before = editor.active_outline().document.clone();
5400 editor
5401 .select_at(right[0] + 5.0, right[1] + 8.0, false)
5402 .unwrap();
5403 editor
5404 .select_at(left[0] + 5.0, left[3] - 2.0, true)
5405 .unwrap();
5406 assert_eq!(editor.selection().positions[0].paragraph, 3);
5407 assert_eq!(editor.selection().positions[1].paragraph, 2);
5408 assert!(!editor.selection_rects().unwrap().is_empty());
5409 assert_eq!(editor.active_outline().document, before);
5410 assert_eq!(editor.undo.len(), history);
5411 }
5412
5413 #[test]
5414 fn nested_table_mouse_targets_resolve_the_source_cell() {
5415 use onestore::page::ParagraphContent;
5416 use onestore::page::text::new_id;
5417 let mut engine = TextEngine::default();
5418 let nested = table_editor(&mut engine).active_outline().document.nodes()[0].clone();
5419 let mut editor = table_editor(&mut engine);
5420 let mut wrapper = editor.active_outline().document.nodes()[0].clone();
5421 let ParagraphContent::Table(table) = &mut wrapper.content else {
5422 panic!()
5423 };
5424 table.columns[0].width = 180.0;
5425 let cell = &mut table.rows[0].cells[0];
5426 cell.paragraphs.push(nested);
5427 let mut tail = cell.paragraphs[0].clone();
5428 tail.id = new_id().unwrap();
5429 tail.text_mut().unwrap().id = new_id().unwrap();
5430 cell.paragraphs.push(tail);
5431 let before = crate::document::node(
5432 Paragraph::new("Before".into(), Format::default()),
5433 Format::default(),
5434 )
5435 .unwrap();
5436 let after = crate::document::node(
5437 Paragraph::new("After".into(), Format::default()),
5438 Format::default(),
5439 )
5440 .unwrap();
5441 editor
5442 .commit(
5443 &mut engine,
5444 DocumentEdit {
5445 columns: BTreeMap::new(),
5446 container: None,
5447 range: 0..1,
5448 replacement: vec![before, wrapper, after],
5449 },
5450 [TextPosition {
5451 paragraph: 0,
5452 offset: 0,
5453 }; 2]
5454 .into(),
5455 )
5456 .unwrap();
5457 assert_eq!(editor.active_outline().document.paragraphs().count(), 11);
5458 for source in 0..11 {
5459 let paragraph = editor.active_outline().paragraph_layout(source).unwrap();
5460 let (_, line) = paragraph.text.lines().next().unwrap();
5461 let selection = editor
5462 .selection_at(
5463 paragraph.origin[0] + 1.0,
5464 paragraph.origin[1] + line.height * 0.5,
5465 SelectionUnit::Grapheme,
5466 )
5467 .unwrap();
5468 assert_eq!(selection.positions[0].paragraph, source);
5469 }
5470 let paragraph = editor.active_outline().paragraph_layout(1).unwrap();
5471 let selection = editor
5472 .selection_at(
5473 paragraph.origin[0] + 1.0,
5474 paragraph.origin[1] + 5.0,
5475 SelectionUnit::Paragraph,
5476 )
5477 .unwrap();
5478 assert_eq!(
5479 selection.positions[1],
5480 TextPosition {
5481 paragraph: 1,
5482 offset: 4
5483 }
5484 );
5485 let original = editor.active_outline().document.clone();
5486 editor
5487 .select(
5488 [TextPosition {
5489 paragraph: 10,
5490 offset: 2,
5491 }; 2]
5492 .into(),
5493 )
5494 .unwrap();
5495 assert!(editor.indent(&mut engine, false).unwrap());
5496 assert_eq!(editor.active_outline().document.nodes()[2].level, 2);
5497 editor.undo(&mut engine).unwrap();
5498 assert_eq!(editor.active_outline().document, original);
5499 for (position, backward) in [
5500 (
5501 TextPosition {
5502 paragraph: 0,
5503 offset: 6,
5504 },
5505 false,
5506 ),
5507 (
5508 TextPosition {
5509 paragraph: 10,
5510 offset: 0,
5511 },
5512 true,
5513 ),
5514 ] {
5515 editor.select([position; 2].into()).unwrap();
5516 assert!(!editor.delete(&mut engine, backward).unwrap());
5517 assert_eq!(editor.active_outline().document, original);
5518 }
5519 }
5520
5521 #[test]
5522 fn cell_delete_boundaries_and_local_indentation_preserve_neighbors() {
5523 let mut engine = TextEngine::default();
5524 let mut editor = table_editor(&mut engine);
5525 let original = editor.active_outline().document.clone();
5526 let history = editor.undo.len();
5527 for paragraph in 0..4 {
5528 let text = original.paragraphs().nth(paragraph).unwrap();
5529 for (backward, offset) in [
5530 (true, 0),
5531 (false, text.utf16_offset(text.text().len()).unwrap()),
5532 ] {
5533 let selection = [TextPosition { paragraph, offset }; 2].into();
5534 editor.select(selection).unwrap();
5535 assert!(!editor.delete(&mut engine, backward).unwrap());
5536 assert_eq!(editor.active_outline().document, original);
5537 assert_eq!(editor.selection(), selection);
5538 assert_eq!(editor.undo.len(), history);
5539 }
5540 }
5541 editor
5542 .select(
5543 [TextPosition {
5544 paragraph: 2,
5545 offset: 2,
5546 }; 2]
5547 .into(),
5548 )
5549 .unwrap();
5550 editor.insert(&mut engine, "\n").unwrap();
5551 let split = editor.active_outline().document.clone();
5552 for backward in [true, false] {
5553 let position = if backward {
5554 TextPosition {
5555 paragraph: 3,
5556 offset: 0,
5557 }
5558 } else {
5559 TextPosition {
5560 paragraph: 2,
5561 offset: 2,
5562 }
5563 };
5564 let selection = [position; 2].into();
5565 editor.select(selection).unwrap();
5566 assert!(editor.delete(&mut engine, backward).unwrap());
5567 assert_eq!(editor.active_outline().document, original);
5568 editor.undo(&mut engine).unwrap();
5569 assert_eq!(editor.active_outline().document, split);
5570 assert_eq!(editor.selection(), selection);
5571 }
5572 let selection = [
5573 TextPosition {
5574 paragraph: 2,
5575 offset: 1,
5576 },
5577 TextPosition {
5578 paragraph: 3,
5579 offset: 0,
5580 },
5581 ]
5582 .into();
5583 editor.select(selection).unwrap();
5584 assert!(editor.indent(&mut engine, false).unwrap());
5585 assert_eq!(
5586 editor
5587 .active_outline()
5588 .document
5589 .text_nodes()
5590 .map(|node| node.level)
5591 .collect::<Vec<_>>(),
5592 [1, 1, 2, 1, 1]
5593 );
5594 assert_eq!(editor.selection(), selection);
5595 editor.undo(&mut engine).unwrap();
5596 assert_eq!(editor.active_outline().document, split);
5597 editor.redo(&mut engine).unwrap();
5598 assert!(editor.indent(&mut engine, true).unwrap());
5599 assert_eq!(editor.active_outline().document, split);
5600 }
5601
5602 #[test]
5603 fn overflowing_cell_caret_moves_to_the_next_rows_visual_column() {
5604 use onestore::page::ParagraphContent;
5605 let mut engine = TextEngine::default();
5606 let mut source = table_editor(&mut engine).active_outline().snapshot();
5607 let ParagraphContent::Table(table) = &mut source.paragraphs[0].content else {
5608 panic!()
5609 };
5610 table.rows[0].cells[0].paragraphs[0]
5611 .text_mut()
5612 .unwrap()
5613 .text = Paragraph::new(
5614 "M".into(),
5615 Format {
5616 font_size: Some(130.0),
5617 ..Default::default()
5618 },
5619 );
5620 let mut editor =
5621 CanvasEditor::from_outlines(&mut engine, vec![source], BTreeMap::new()).unwrap();
5622 let original = editor.active_outline().document.clone();
5623 editor
5624 .select(
5625 [
5626 TextPosition {
5627 paragraph: 0,
5628 offset: 0,
5629 },
5630 TextPosition {
5631 paragraph: 0,
5632 offset: 1,
5633 },
5634 ]
5635 .into(),
5636 )
5637 .unwrap();
5638 let [_, _, right, _] = editor
5639 .active_outline()
5640 .shaped
5641 .paragraph_cell(0)
5642 .unwrap()
5643 .text_bounds();
5644 let selection = editor.selection_rects().unwrap();
5645 assert!(!selection.is_empty());
5646 assert!(selection.iter().all(|rect| rect.x1 <= f64::from(right)));
5647 for extend in [false, true] {
5648 editor
5649 .select(
5650 [TextPosition {
5651 paragraph: 0,
5652 offset: 1,
5653 }; 2]
5654 .into(),
5655 )
5656 .unwrap();
5657 let caret = editor.caret(1.0).unwrap();
5658 assert!(
5659 caret.x0 > f64::from(editor.active_outline().shaped.tables[0].cells[0].rect[2])
5660 );
5661 editor
5662 .move_selection(&mut engine, Movement::Down, extend)
5663 .unwrap();
5664 assert_eq!(editor.selection().positions[1].paragraph, 3);
5665 editor
5666 .move_selection(&mut engine, Movement::Up, extend)
5667 .unwrap();
5668 assert_eq!(
5669 editor.selection().positions[1],
5670 TextPosition {
5671 paragraph: 1,
5672 offset: 5
5673 }
5674 );
5675 assert_eq!(editor.active_outline().document, original);
5676 assert!(editor.undo.is_empty());
5677 }
5678 }
5679
5680 #[test]
5681 fn table_vertical_arrows_preserve_columns_and_cell_paragraphs() {
5682 let mut engine = TextEngine::default();
5683 let mut editor = table_editor(&mut engine);
5684 let original = editor.active_outline().document.clone();
5685 for column in 0..2 {
5686 editor
5687 .select(
5688 [TextPosition {
5689 paragraph: column,
5690 offset: 2,
5691 }; 2]
5692 .into(),
5693 )
5694 .unwrap();
5695 editor
5696 .move_selection(&mut engine, Movement::Down, false)
5697 .unwrap();
5698 assert_eq!(editor.selection().positions[1].paragraph, column + 2);
5699 editor
5700 .move_selection(&mut engine, Movement::Up, false)
5701 .unwrap();
5702 assert_eq!(
5703 editor.selection().positions[1],
5704 TextPosition {
5705 paragraph: column,
5706 offset: 2
5707 }
5708 );
5709 }
5710 editor
5711 .select(
5712 [TextPosition {
5713 paragraph: 0,
5714 offset: 2,
5715 }; 2]
5716 .into(),
5717 )
5718 .unwrap();
5719 editor.insert(&mut engine, "\n").unwrap();
5720 editor
5721 .select(
5722 [TextPosition {
5723 paragraph: 0,
5724 offset: 1,
5725 }; 2]
5726 .into(),
5727 )
5728 .unwrap();
5729 editor
5730 .move_selection(&mut engine, Movement::Down, false)
5731 .unwrap();
5732 assert_eq!(editor.selection().positions[1].paragraph, 1);
5733 editor
5734 .move_selection(&mut engine, Movement::Down, false)
5735 .unwrap();
5736 assert_eq!(editor.selection().positions[1].paragraph, 3);
5737 editor
5738 .select(
5739 [TextPosition {
5740 paragraph: 2,
5741 offset: 2,
5742 }; 2]
5743 .into(),
5744 )
5745 .unwrap();
5746 editor
5747 .move_selection(&mut engine, Movement::Down, false)
5748 .unwrap();
5749 assert_eq!(editor.selection().positions[1].paragraph, 4);
5750 editor
5751 .move_selection(&mut engine, Movement::Up, false)
5752 .unwrap();
5753 assert_eq!(
5754 editor.selection().positions[1],
5755 TextPosition {
5756 paragraph: 2,
5757 offset: 2
5758 }
5759 );
5760 editor.undo(&mut engine).unwrap();
5761 assert_eq!(editor.active_outline().document, original);
5762 }
5763
5764 #[test]
5765 fn table_edges_create_provisional_space_and_restore_it_with_history() {
5766 let mut engine = TextEngine::default();
5767 let mut editor = table_editor(&mut engine);
5768 editor.active_outline_mut().layout.x = Some(120.0);
5769 editor.active_outline_mut().layout.y = Some(120.0);
5770 let source = editor.active_outline().document.clone();
5771 editor
5772 .select(
5773 [TextPosition {
5774 paragraph: 3,
5775 offset: 4,
5776 }; 2]
5777 .into(),
5778 )
5779 .unwrap();
5780 let history = editor.undo.len();
5781 editor
5782 .move_selection(&mut engine, Movement::Down, false)
5783 .unwrap();
5784 assert_eq!(editor.outlines()[0].document, source);
5785 assert_eq!(editor.undo.len(), history);
5786 assert_eq!(
5787 editor.selection().positions,
5788 [TextPosition {
5789 paragraph: 4,
5790 offset: 0
5791 }; 2]
5792 );
5793 let draft = editor.active_outline().document.clone();
5794 let caret = editor.selection();
5795 editor.insert(&mut engine, "X").unwrap();
5796 let committed = editor.active_outline().document.clone();
5797 assert_eq!(committed.nodes().len(), 2);
5798 assert_eq!(committed.paragraphs().last().unwrap().text(), "X");
5799 for _ in 0..2 {
5800 editor.undo(&mut engine).unwrap();
5801 assert_eq!(editor.outlines()[0].document, source);
5802 assert_eq!(editor.active_outline().document, draft);
5803 assert_eq!(editor.selection(), caret);
5804 editor.redo(&mut engine).unwrap();
5805 assert_eq!(editor.active_outline().document, committed);
5806 }
5807 editor.undo(&mut engine).unwrap();
5808 editor
5809 .select(
5810 [TextPosition {
5811 paragraph: 0,
5812 offset: 0,
5813 }; 2]
5814 .into(),
5815 )
5816 .unwrap();
5817 editor
5818 .move_selection(&mut engine, Movement::Up, false)
5819 .unwrap();
5820 assert_eq!(editor.caret_outline().unwrap().origin(), [120.0, 86.4]);
5821 assert_eq!(editor.outlines()[0].document, source);
5822 editor.insert(&mut engine, "X").unwrap();
5823 assert_eq!(editor.outlines().len(), 2);
5824 editor.undo(&mut engine).unwrap();
5825 assert!(editor.caret_outline().is_some());
5826 assert_eq!(editor.outlines().len(), 1);
5827 assert_eq!(editor.outlines()[0].document, source);
5828 }
5829
5830 #[test]
5831 fn editing_a_cell_trims_only_provisional_root_paragraphs() {
5832 let mut engine = TextEngine::default();
5833 for compose in [false, true] {
5834 let mut editor = table_editor(&mut engine);
5835 let source = editor.active_outline().document.clone();
5836 editor
5837 .select(
5838 [TextPosition {
5839 paragraph: 3,
5840 offset: 4,
5841 }; 2]
5842 .into(),
5843 )
5844 .unwrap();
5845 for _ in 0..2 {
5846 editor
5847 .move_selection(&mut engine, Movement::Down, false)
5848 .unwrap();
5849 }
5850 for _ in 0..2 {
5851 editor
5852 .move_selection(&mut engine, Movement::Up, false)
5853 .unwrap();
5854 }
5855 assert_eq!(editor.selection().positions[1].paragraph, 3);
5856 let draft = editor.active_outline().document.clone();
5857 assert_eq!(draft.nodes().len(), 3);
5858 let selection = editor.selection();
5859 if compose {
5860 editor
5861 .compose(&mut engine, "temporary".into(), 9..9)
5862 .unwrap();
5863 editor.cancel_composition(&mut engine).unwrap();
5864 assert_eq!(editor.active_outline().document, draft);
5865 editor.compose(&mut engine, "marked".into(), 6..6).unwrap();
5866 editor.commit_text(&mut engine, "X".into()).unwrap();
5867 } else {
5868 editor.insert(&mut engine, "X").unwrap();
5869 }
5870 let after = editor.active_outline().document.clone();
5871 assert_eq!(after.nodes().len(), 1);
5872 assert_eq!(after.paragraphs().nth(3).unwrap().text(), "LastX");
5873 let fresh = editor
5874 .active_outline()
5875 .snapshot()
5876 .layout(&mut engine, &BTreeMap::new())
5877 .unwrap();
5878 assert_eq!(editor.active_outline().shaped.size, fresh.size);
5879 assert_eq!(editor.active_outline().shaped.paragraphs.len(), 4);
5880 editor.undo(&mut engine).unwrap();
5881 assert_eq!(editor.outlines()[0].document, source);
5882 assert_eq!(editor.active_outline().document, draft);
5883 assert_eq!(editor.selection(), selection);
5884 editor.redo(&mut engine).unwrap();
5885 assert_eq!(editor.active_outline().document, after);
5886 }
5887 }
5888
5889 #[test]
5890 fn editing_during_cell_composition_keeps_table_flow_and_history() {
5891 let mut engine = TextEngine::default();
5892 let mut editor = table_editor(&mut engine);
5893 let source = editor.active_outline().document.clone();
5894 editor
5895 .select(
5896 [TextPosition {
5897 paragraph: 3,
5898 offset: 4,
5899 }; 2]
5900 .into(),
5901 )
5902 .unwrap();
5903 for _ in 0..2 {
5904 editor
5905 .move_selection(&mut engine, Movement::Down, false)
5906 .unwrap();
5907 }
5908 for _ in 0..2 {
5909 editor
5910 .move_selection(&mut engine, Movement::Up, false)
5911 .unwrap();
5912 }
5913 let draft = editor.active_outline().document.clone();
5914 editor.compose(&mut engine, "a".into(), 1..1).unwrap();
5915 editor.insert(&mut engine, "b").unwrap();
5916 assert_eq!(
5917 editor
5918 .active_outline()
5919 .document
5920 .paragraphs()
5921 .last()
5922 .unwrap()
5923 .text(),
5924 "Lastab"
5925 );
5926 let fresh = editor
5927 .active_outline()
5928 .snapshot()
5929 .layout(&mut engine, &BTreeMap::new())
5930 .unwrap();
5931 assert_eq!(editor.active_outline().shaped.size, fresh.size);
5932 assert_eq!(
5933 editor.active_outline().shaped.paragraphs.len(),
5934 fresh.paragraphs.len()
5935 );
5936 // The committed composition and the typing after it are one run.
5937 editor.undo(&mut engine).unwrap();
5938 assert_eq!(editor.outlines()[0].document, source);
5939 assert_eq!(editor.active_outline().document, draft);
5940 }
5941
5942 /// OneNote 2010's undo steps (Windows 7 lab, 2026-09-29): typing, backspaces and word
5943 /// backspaces within a paragraph are one step however long the pauses; a caret move, Enter,
5944 /// a backspace joining paragraphs, Delete, paste and a format chosen at the caret each end it.
5945 #[test]
5946 fn typing_runs_undo_whole_until_the_caret_moves_or_another_edit() {
5947 let mut engine = TextEngine::default();
5948 let source =
5949 TextDocument::new(vec![Paragraph::new("Base".into(), Format::default())]).unwrap();
5950 let mut editor = CanvasEditor::new(&mut engine, source, 240.0).unwrap();
5951 let at = |paragraph, offset| [TextPosition { paragraph, offset }; 2].into();
5952 let text = |editor: &CanvasEditor| editor.active_outline().shown_text();
5953 editor.select(at(0, 4)).unwrap();
5954 editor.insert(&mut engine, " quick").unwrap();
5955 editor.insert(&mut engine, " brown").unwrap();
5956 editor.delete(&mut engine, true).unwrap();
5957 editor.delete_to(&mut engine, Movement::WordLeft).unwrap();
5958 editor.commit_text(&mut engine, "fox".into()).unwrap();
5959 editor.select(editor.selection()).unwrap();
5960 editor.insert(&mut engine, " jumps").unwrap();
5961 assert_eq!(text(&editor), "Base quick fox jumps");
5962 assert!(editor.undo(&mut engine).unwrap());
5963 assert_eq!(text(&editor), "Base");
5964 assert_eq!(editor.selection().positions, [at(0, 4).positions[0]; 2]);
5965 assert!(!editor.can_undo());
5966 assert!(editor.redo(&mut engine).unwrap());
5967 assert_eq!(text(&editor), "Base quick fox jumps");
5968
5969 let mut shown = vec!["Base".to_owned(), text(&editor)];
5970 for movement in [Movement::Left, Movement::Right] {
5971 editor.move_selection(&mut engine, movement, false).unwrap();
5972 }
5973 editor.insert(&mut engine, "!").unwrap();
5974 editor.insert(&mut engine, "?").unwrap();
5975 shown.push(text(&editor));
5976 editor.enter(&mut engine, false).unwrap();
5977 shown.push(text(&editor));
5978 editor.insert(&mut engine, "ab").unwrap();
5979 shown.push(text(&editor));
5980 editor
5981 .move_selection(&mut engine, Movement::Left, false)
5982 .unwrap();
5983 editor.delete(&mut engine, false).unwrap();
5984 shown.push(text(&editor));
5985 editor.insert(&mut engine, "c").unwrap();
5986 shown.push(text(&editor));
5987 editor
5988 .format(&mut engine, Formatting::Toggle(Toggle::Bold))
5989 .unwrap();
5990 editor.insert(&mut engine, "d").unwrap();
5991 editor.insert(&mut engine, "e").unwrap();
5992 shown.push(text(&editor));
5993 editor.select(at(1, 0)).unwrap();
5994 editor.delete(&mut engine, true).unwrap();
5995 shown.push(text(&editor));
5996 editor.insert(&mut engine, "f").unwrap();
5997 assert_eq!(text(&editor), "Base quick fox jumps!?facde");
5998 while editor.undo(&mut engine).unwrap() {
5999 assert_eq!(text(&editor), shown.pop().unwrap());
6000 }
6001 assert!(shown.is_empty());
6002 }
6003
6004 #[test]
6005 fn a_symbol_is_an_undo_step_between_typing_runs() {
6006 let mut engine = TextEngine::default();
6007 let source =
6008 TextDocument::new(vec![Paragraph::new("Cost".into(), Format::default())]).unwrap();
6009 let mut editor = CanvasEditor::new(&mut engine, source, 240.0).unwrap();
6010 let text = |editor: &CanvasEditor| editor.active_outline().shown_text();
6011 editor
6012 .select(
6013 [TextPosition {
6014 paragraph: 0,
6015 offset: 4,
6016 }; 2]
6017 .into(),
6018 )
6019 .unwrap();
6020 editor.insert(&mut engine, " 5").unwrap();
6021 editor.insert_symbol(&mut engine, '€').unwrap();
6022 editor.insert_symbol(&mut engine, '±').unwrap();
6023 editor.insert(&mut engine, "1").unwrap();
6024 assert_eq!(text(&editor), "Cost 5€±1");
6025 for shown in ["Cost 5€±", "Cost 5€", "Cost 5", "Cost"] {
6026 assert!(editor.undo(&mut engine).unwrap());
6027 assert_eq!(text(&editor), shown);
6028 }
6029 }
6030
6031 #[test]
6032 fn table_horizontal_arrows_follow_reading_order() {
6033 let mut engine = TextEngine::default();
6034 let mut editor = table_editor(&mut engine);
6035 let original = editor.active_outline().document.clone();
6036 let history = editor.undo.len();
6037 for paragraph in 0..4 {
6038 let text = original.paragraphs().nth(paragraph).unwrap();
6039 let end = text.utf16_offset(text.text().len()).unwrap();
6040 editor
6041 .select(
6042 [TextPosition {
6043 paragraph,
6044 offset: end,
6045 }; 2]
6046 .into(),
6047 )
6048 .unwrap();
6049 editor
6050 .move_selection(&mut engine, Movement::Right, false)
6051 .unwrap();
6052 let next = if paragraph < 3 {
6053 TextPosition {
6054 paragraph: paragraph + 1,
6055 offset: 0,
6056 }
6057 } else {
6058 TextPosition {
6059 paragraph,
6060 offset: end,
6061 }
6062 };
6063 assert_eq!(editor.selection().positions, [next; 2]);
6064 if paragraph < 3 {
6065 editor
6066 .move_selection(&mut engine, Movement::Left, false)
6067 .unwrap();
6068 assert_eq!(
6069 editor.selection().positions,
6070 [TextPosition {
6071 paragraph,
6072 offset: end
6073 }; 2]
6074 );
6075 }
6076 }
6077 assert_eq!(editor.active_outline().document, original);
6078 assert_eq!(editor.undo.len(), history);
6079 assert!(editor.caret_outline().is_none());
6080 }
6081
6082 #[test]
6083 fn column_width_edits_reflow_and_restore_structured_drafts() {
6084 use onestore::page::ParagraphContent;
6085 let mut engine = TextEngine::default();
6086 for draft in [false, true] {
6087 let mut editor = table_editor(&mut engine);
6088 let stored = editor.outlines()[0].document.clone();
6089 editor
6090 .select(
6091 [TextPosition {
6092 paragraph: 3,
6093 offset: 4,
6094 }; 2]
6095 .into(),
6096 )
6097 .unwrap();
6098 if draft {
6099 for _ in 0..2 {
6100 editor
6101 .move_selection(&mut engine, Movement::Down, false)
6102 .unwrap();
6103 }
6104 for _ in 0..2 {
6105 editor
6106 .move_selection(&mut engine, Movement::Up, false)
6107 .unwrap();
6108 }
6109 }
6110 let before = editor.active_outline().document.clone();
6111 let selection = editor.selection();
6112 let ParagraphContent::Table(table) = &before.nodes()[0].content else {
6113 panic!()
6114 };
6115 let position = selection.positions[1];
6116 let mut edit = before
6117 .replace(
6118 position..position,
6119 vec![Paragraph::new(
6120 " long cell content".into(),
6121 Format::default(),
6122 )],
6123 )
6124 .unwrap();
6125 edit.columns.insert(table.id, vec![96.375, 36.0]);
6126 editor.commit(&mut engine, edit, selection).unwrap();
6127 let after = editor.active_outline().document.clone();
6128 let fresh = editor
6129 .active_outline()
6130 .snapshot()
6131 .layout(&mut engine, &BTreeMap::new())
6132 .unwrap();
6133 assert_eq!(editor.active_outline().shaped.size, fresh.size);
6134 assert_eq!(
6135 editor.active_outline().shaped.tables[0]
6136 .cells
6137 .iter()
6138 .map(|c| c.rect)
6139 .collect::<Vec<_>>(),
6140 fresh.tables[0]
6141 .cells
6142 .iter()
6143 .map(|c| c.rect)
6144 .collect::<Vec<_>>()
6145 );
6146 for _ in 0..2 {
6147 editor.undo(&mut engine).unwrap();
6148 assert_eq!(editor.active_outline().document, before);
6149 assert_eq!(editor.outlines()[0].document, stored);
6150 assert_eq!(editor.selection(), selection);
6151 editor.redo(&mut engine).unwrap();
6152 assert_eq!(editor.active_outline().document, after);
6153 }
6154 let selection = editor.selection();
6155 let history = editor.undo.len();
6156 for widths in [vec![f32::MAX; 2], vec![36.0, f32::NAN]] {
6157 let position = selection.positions[1];
6158 let mut edit = after
6159 .replace(
6160 position..position,
6161 vec![Paragraph::new("invalid".into(), Format::default())],
6162 )
6163 .unwrap();
6164 edit.columns.insert(table.id, widths);
6165 assert!(editor.commit(&mut engine, edit, selection).is_err());
6166 assert_eq!(editor.active_outline().document, after);
6167 assert_eq!(editor.selection(), selection);
6168 assert_eq!(editor.undo.len(), history);
6169 }
6170 }
6171 }
6172
6173 #[test]
6174 fn cell_edits_reuse_neighbor_layouts_and_restore_table_geometry() {
6175 let mut engine = TextEngine::default();
6176 let mut editor = table_editor(&mut engine);
6177 let before = editor.active_outline().document.clone();
6178 let layout_ids = editor
6179 .active_outline()
6180 .shaped
6181 .paragraphs
6182 .iter()
6183 .map(|p| p.text.id())
6184 .collect::<Vec<_>>();
6185 let size = editor.active_outline().shaped.size;
6186 let cell_rects = editor.active_outline().shaped.tables[0]
6187 .cells
6188 .iter()
6189 .map(|cell| cell.rect)
6190 .collect::<Vec<_>>();
6191 let right_text = editor
6192 .active_outline()
6193 .document
6194 .paragraphs()
6195 .nth(1)
6196 .unwrap()
6197 .text()
6198 .as_ptr();
6199 let selection = [TextPosition {
6200 paragraph: 0,
6201 offset: 2,
6202 }; 2]
6203 .into();
6204 editor.select(selection).unwrap();
6205 editor
6206 .insert(&mut engine, " long text that wraps\nsecond paragraph")
6207 .unwrap();
6208 let outline = editor.active_outline();
6209 assert_eq!(outline.document.paragraphs().count(), 5);
6210 assert!(outline.shaped.size[1] > size[1]);
6211 assert_eq!(
6212 outline
6213 .document
6214 .paragraphs()
6215 .nth(2)
6216 .unwrap()
6217 .text()
6218 .as_ptr(),
6219 right_text
6220 );
6221 for (index, original) in layout_ids.iter().enumerate().skip(1) {
6222 assert_eq!(
6223 outline.paragraph_layout(index + 1).unwrap().text.id(),
6224 *original
6225 );
6226 }
6227 assert_eq!(
6228 outline
6229 .layouts()
6230 .map(|(index, _)| index)
6231 .collect::<Vec<_>>(),
6232 [0, 1, 2, 3, 4]
6233 );
6234 assert_eq!(
6235 outline.shaped.tables[0].cells[0].rect[3],
6236 outline.shaped.tables[0].cells[1].rect[3]
6237 );
6238 assert!(outline.shaped.tables[0].cells[2].rect[1] > cell_rects[2][1]);
6239 let after = outline.document.clone();
6240 let after_selection = outline.selection;
6241 let after_rects = outline.shaped.tables[0]
6242 .cells
6243 .iter()
6244 .map(|cell| cell.rect)
6245 .collect::<Vec<_>>();
6246 for _ in 0..3 {
6247 editor.undo(&mut engine).unwrap();
6248 let outline = editor.active_outline();
6249 assert_eq!(outline.document, before);
6250 assert_eq!(outline.selection, selection);
6251 assert_eq!(outline.shaped.size, size);
6252 assert_eq!(
6253 outline.shaped.tables[0]
6254 .cells
6255 .iter()
6256 .map(|cell| cell.rect)
6257 .collect::<Vec<_>>(),
6258 cell_rects
6259 );
6260 editor.redo(&mut engine).unwrap();
6261 let outline = editor.active_outline();
6262 assert_eq!(outline.document, after);
6263 assert_eq!(outline.selection, after_selection);
6264 assert_eq!(
6265 outline.shaped.tables[0]
6266 .cells
6267 .iter()
6268 .map(|cell| cell.rect)
6269 .collect::<Vec<_>>(),
6270 after_rects
6271 );
6272 }
6273 editor.undo(&mut engine).unwrap();
6274 editor.undo(&mut engine).unwrap();
6275 assert_eq!(
6276 editor
6277 .active_outline()
6278 .document
6279 .paragraphs()
6280 .next()
6281 .unwrap()
6282 .text(),
6283 "Left"
6284 );
6285 assert!(editor.active_outline().shaped.tables.is_empty());
6286 editor.redo(&mut engine).unwrap();
6287 assert_eq!(editor.active_outline().document, before);
6288 assert_eq!(editor.active_outline().shaped.size, size);
6289 }
6290
6291 #[test]
6292 fn cell_composition_and_failed_layout_leave_atomic_history() {
6293 let mut engine = TextEngine::default();
6294 let mut editor = table_editor(&mut engine);
6295 editor
6296 .select(
6297 [TextPosition {
6298 paragraph: 2,
6299 offset: 2,
6300 }; 2]
6301 .into(),
6302 )
6303 .unwrap();
6304 let before = editor.active_outline().document.clone();
6305 let selection = editor.selection();
6306 let cells = editor.active_outline().shaped.tables[0]
6307 .cells
6308 .iter()
6309 .map(|cell| cell.rect)
6310 .collect::<Vec<_>>();
6311 let ids = editor
6312 .active_outline()
6313 .shaped
6314 .paragraphs
6315 .iter()
6316 .map(|p| p.text.id())
6317 .collect::<Vec<_>>();
6318 let history = editor.undo.len();
6319 editor.compose(&mut engine, "🧊\n中".into(), 4..4).unwrap();
6320 editor.compose(&mut engine, "short".into(), 5..5).unwrap();
6321 assert!(editor.cancel_composition(&mut engine).unwrap());
6322 assert_eq!(editor.active_outline().document, before);
6323 assert_eq!(editor.selection(), selection);
6324 assert_eq!(editor.undo.len(), history);
6325 assert_eq!(
6326 editor.active_outline().shaped.tables[0]
6327 .cells
6328 .iter()
6329 .map(|cell| cell.rect)
6330 .collect::<Vec<_>>(),
6331 cells
6332 );
6333 for index in [0, 1, 3] {
6334 assert_eq!(
6335 editor
6336 .active_outline()
6337 .paragraph_layout(index)
6338 .unwrap()
6339 .text
6340 .id(),
6341 ids[index]
6342 );
6343 }
6344 editor.compose(&mut engine, "🧊\n中".into(), 4..4).unwrap();
6345 editor.commit_text(&mut engine, "done".into()).unwrap();
6346 assert_eq!(editor.undo.len(), history + 1);
6347 editor.undo(&mut engine).unwrap();
6348 assert_eq!(editor.active_outline().document, before);
6349 assert_eq!(editor.selection(), selection);
6350 let redo = editor.redo.len();
6351 assert!(
6352 editor
6353 .replace(
6354 &mut engine,
6355 vec![Paragraph::new(
6356 "bad".into(),
6357 Format {
6358 font_size: Some(f32::NAN),
6359 ..Format::default()
6360 }
6361 )]
6362 )
6363 .is_err()
6364 );
6365 assert_eq!(editor.active_outline().document, before);
6366 assert_eq!(editor.selection(), selection);
6367 assert_eq!(editor.undo.len(), history);
6368 assert_eq!(editor.redo.len(), redo);
6369 editor.redo(&mut engine).unwrap();
6370 assert_eq!(
6371 editor
6372 .active_outline()
6373 .document
6374 .paragraphs()
6375 .nth(2)
6376 .unwrap()
6377 .text(),
6378 "Bedonelow"
6379 );
6380 }
6381
6382 #[test]
6383 fn retained_cell_edits_match_fresh_layout_for_every_text_range() {
6384 let mut engine = TextEngine::default();
6385 let base = table_editor(&mut engine);
6386 for (index, paragraph) in base.active_outline().document.paragraphs().enumerate() {
6387 let offsets = paragraph
6388 .text()
6389 .char_indices()
6390 .map(|(byte, _)| paragraph.utf16_offset(byte).unwrap())
6391 .chain(std::iter::once(
6392 paragraph.utf16_offset(paragraph.text().len()).unwrap(),
6393 ))
6394 .collect::<Vec<_>>();
6395 for &start in &offsets {
6396 for &end in offsets.iter().filter(|&&end| end >= start) {
6397 for text in ["", "🧊", "x\ny"] {
6398 let mut editor = CanvasEditor::from_text_outlines(
6399 vec![base.active_outline().clone()],
6400 BTreeMap::new(),
6401 None,
6402 )
6403 .unwrap();
6404 let selection = [
6405 TextPosition {
6406 paragraph: index,
6407 offset: start,
6408 },
6409 TextPosition {
6410 paragraph: index,
6411 offset: end,
6412 },
6413 ]
6414 .into();
6415 editor.select(selection).unwrap();
6416 editor.insert(&mut engine, text).unwrap();
6417 let retained = &editor.active_outline().shaped;
6418 let fresh = editor
6419 .active_outline()
6420 .snapshot()
6421 .layout(&mut engine, &editor.definitions)
6422 .unwrap();
6423 assert_eq!(retained.size, fresh.size);
6424 assert_eq!(
6425 retained.tables[0]
6426 .cells
6427 .iter()
6428 .map(|c| c.rect)
6429 .collect::<Vec<_>>(),
6430 fresh.tables[0]
6431 .cells
6432 .iter()
6433 .map(|c| c.rect)
6434 .collect::<Vec<_>>()
6435 );
6436 for (retained, fresh) in retained.paragraphs.iter().zip(&fresh.paragraphs) {
6437 assert_eq!(retained.id, fresh.id);
6438 assert_eq!(retained.origin, fresh.origin);
6439 assert_eq!(retained.text.height(), fresh.text.height());
6440 assert_eq!(
6441 retained
6442 .text
6443 .lines()
6444 .map(|(line, bounds)| (
6445 line.metrics().advance,
6446 bounds.source.clone()
6447 ))
6448 .collect::<Vec<_>>(),
6449 fresh
6450 .text
6451 .lines()
6452 .map(|(line, bounds)| (
6453 line.metrics().advance,
6454 bounds.source.clone()
6455 ))
6456 .collect::<Vec<_>>()
6457 );
6458 }
6459 editor.undo(&mut engine).unwrap();
6460 assert_eq!(
6461 editor.active_outline().document,
6462 base.active_outline().document
6463 );
6464 assert_eq!(editor.selection(), selection);
6465 }
6466 }
6467 }
6468 }
6469 }
6470
6471 #[test]
6472 fn cross_outline_validation_includes_table_row_and_cell_ids() {
6473 use onestore::page::ParagraphContent;
6474 let mut engine = TextEngine::default();
6475 let first = table_editor(&mut engine).active_outline().clone();
6476 let second = table_editor(&mut engine).active_outline().clone();
6477 let ParagraphContent::Table(first_table) = &first.document.nodes()[0].content else {
6478 panic!()
6479 };
6480 for alias in 0..3 {
6481 let mut second = second.clone();
6482 let mut nodes = second.document.nodes().to_vec();
6483 let ParagraphContent::Table(table) = &mut nodes[0].content else {
6484 panic!()
6485 };
6486 match alias {
6487 0 => table.id = first_table.id,
6488 1 => table.rows[0].id = first_table.rows[0].id,
6489 _ => table.rows[0].cells[0].id = first_table.rows[0].cells[0].id,
6490 }
6491 second.document = TextDocument::from_nodes(nodes).unwrap();
6492 assert!(matches!(
6493 CanvasEditor::from_text_outlines(
6494 vec![first.clone(), second],
6495 BTreeMap::new(),
6496 None
6497 ),
6498 Err(EditorError::Edit(EditError::InvalidStructure))
6499 ));
6500 }
6501 }
6502
6503 #[test]
6504 fn cutting_the_only_picture_restores_its_empty_text_paragraph_on_undo() {
6505 let mut engine = TextEngine::default();
6506 let document =
6507 TextDocument::new(vec![Paragraph::new(String::new(), Format::default())]).unwrap();
6508 let mut editor = CanvasEditor::new(&mut engine, document, 240.0).unwrap();
6509 editor
6510 .create_outline(&mut engine, [36.0, 36.0], 240.0)
6511 .unwrap();
6512 let image = picture(
6513 include_bytes!("../../../corpus/object-tags/native/notebook/photo.png").to_vec(),
6514 [40.0, 30.0],
6515 )
6516 .unwrap();
6517 let id = image.id;
6518 editor.insert_picture(&mut engine, image).unwrap();
6519 editor.remove_image(&mut engine, id).unwrap();
6520 assert!(editor.active_outline().is_empty());
6521 assert!(editor.undo(&mut engine).unwrap());
6522 assert!(editor.image_placement(id).is_some());
6523 assert_eq!(editor.active_outline().document.nodes().len(), 2);
6524 assert!(editor.redo(&mut engine).unwrap());
6525 assert!(editor.active_outline().is_empty());
6526 assert!(editor.undo(&mut engine).unwrap());
6527 assert!(editor.image_placement(id).is_some());
6528 }
6529
6530 #[test]
6531 fn pictures_move_and_resize_through_history_and_backgrounds_stay_put() {
6532 use onestore::page::{Image, Page, PageObject};
6533 let mut engine = TextEngine::default();
6534 let [picture, background] = [false, true].map(|background| Image {
6535 size: None,
6536 id: onestore::page::text::new_id().unwrap(),
6537 layout: onestore::document::Layout {
6538 x: Some(468.75),
6539 y: Some(86.4),
6540 max_width: Some(333.0),
6541 max_height: Some(200.1),
6542 ..Default::default()
6543 },
6544 bytes: Some(std::sync::Arc::from(b"deferred image payload".as_slice())),
6545 display: None,
6546 alt: None,
6547 background,
6548 printout: None,
6549 tags: Vec::new(),
6550 link: None,
6551 text: None,
6552 });
6553 let (id, background_id) = (picture.id, background.id);
6554 let mut editor = CanvasEditor::from_page(
6555 Page {
6556 title: String::new(),
6557 identity: None,
6558 created: None,
6559 margin_origin: [36.0, 14.4],
6560 rtl: false,
6561 color: None,
6562 rule_lines: None,
6563 definitions: BTreeMap::new(),
6564 objects: vec![PageObject::Image(background), PageObject::Image(picture)],
6565 },
6566 &mut engine,
6567 )
6568 .unwrap();
6569 let stored = |editor: &CanvasEditor| {
6570 editor
6571 .page()
6572 .unwrap()
6573 .objects
6574 .into_iter()
6575 .find_map(|object| match object {
6576 PageObject::Image(image) if image.id == id => Some(image.layout),
6577 _ => None,
6578 })
6579 .unwrap()
6580 };
6581 let original = stored(&editor);
6582 assert_eq!(
6583 editor.image_placement(id),
6584 Some(([468.75, 86.4], [333.0, 200.1]))
6585 );
6586 assert!(editor.image_placement(background_id).is_none());
6587 assert!(matches!(
6588 editor.place_image(&mut engine, background_id, [0.0; 2], [10.0; 2]),
6589 Err(EditorError::Edit(EditError::InvalidRange))
6590 ));
6591 assert!(matches!(
6592 editor.place_image(&mut engine, id, [0.0; 2], [0.0, 10.0]),
6593 Err(EditorError::Edit(EditError::InvalidRange))
6594 ));
6595 editor
6596 .place_image(&mut engine, id, [513.75, 104.4], [333.0, 200.1])
6597 .unwrap();
6598 let moved = stored(&editor);
6599 assert_eq!([moved.x, moved.y], [Some(513.75), Some(104.4)]);
6600 assert_eq!(moved.width_set_by_user, None);
6601 editor
6602 .place_image(&mut engine, id, [513.75, 104.4], [281.8, 169.4])
6603 .unwrap();
6604 let resized = stored(&editor);
6605 assert_eq!(
6606 [resized.max_width, resized.max_height],
6607 [Some(281.8), Some(169.4)]
6608 );
6609 assert_eq!(resized.width_set_by_user, Some(true));
6610 assert!(editor.undo(&mut engine).unwrap());
6611 assert_eq!(stored(&editor), moved);
6612 assert!(editor.undo(&mut engine).unwrap());
6613 assert_eq!(stored(&editor), original);
6614 assert!(editor.redo(&mut engine).unwrap());
6615 assert!(editor.redo(&mut engine).unwrap());
6616 assert_eq!(stored(&editor), resized);
6617
6618 let ids = |editor: &CanvasEditor| {
6619 editor
6620 .page()
6621 .unwrap()
6622 .objects
6623 .iter()
6624 .map(PageObject::id)
6625 .collect::<Vec<_>>()
6626 };
6627 let before = ids(&editor);
6628 assert!(matches!(
6629 editor.remove_image(&mut engine, background_id),
6630 Err(EditorError::Edit(EditError::InvalidRange))
6631 ));
6632 editor.remove_image(&mut engine, id).unwrap();
6633 assert!(editor.image_placement(id).is_none());
6634 assert_eq!(ids(&editor), [background_id]);
6635 assert!(editor.undo(&mut engine).unwrap());
6636 assert_eq!(ids(&editor), before);
6637 assert_eq!(stored(&editor), resized);
6638 assert!(editor.redo(&mut engine).unwrap());
6639 assert_eq!(ids(&editor), [background_id]);
6640 }
6641
6642 #[test]
6643 fn art_and_paper_change_together_as_one_step_under_the_page() {
6644 use onestore::page::{Image, Page, PageObject};
6645 let mut engine = TextEngine::default();
6646 let image = |background, bytes: &[u8]| Image {
6647 size: None,
6648 id: onestore::page::text::new_id().unwrap(),
6649 layout: onestore::document::Layout {
6650 x: Some(-27.0),
6651 y: Some(-3.6),
6652 max_width: Some(174.5),
6653 max_height: Some(640.0),
6654 ..Default::default()
6655 },
6656 bytes: Some(std::sync::Arc::from(bytes)),
6657 display: None,
6658 alt: None,
6659 background,
6660 printout: None,
6661 tags: Vec::new(),
6662 link: None,
6663 text: None,
6664 };
6665 let [old, picture] = [image(true, b"old art"), image(false, b"picture")];
6666 let mut editor = CanvasEditor::from_page(
6667 Page {
6668 title: String::new(),
6669 identity: None,
6670 created: None,
6671 margin_origin: [36.0, 14.4],
6672 rtl: false,
6673 color: None,
6674 rule_lines: None,
6675 definitions: BTreeMap::new(),
6676 objects: vec![
6677 PageObject::Image(old.clone()),
6678 PageObject::Image(picture.clone()),
6679 ],
6680 },
6681 &mut engine,
6682 )
6683 .unwrap();
6684 let ids = |editor: &CanvasEditor| {
6685 let page = editor.page().unwrap();
6686 let ids: Vec<_> = page.objects.iter().map(PageObject::id).collect();
6687 (ids, page.color)
6688 };
6689 let art = || [image(true, b"art one"), image(true, b"art two")];
6690 let [one, two] = art();
6691 let blue = Some(0x00fef5ed);
6692 assert!(editor.set_paper(blue, None, Some(vec![one.clone(), two.clone()])));
6693 let applied = (vec![one.id, two.id, picture.id], blue);
6694 assert_eq!(ids(&editor), applied);
6695 assert_eq!(
6696 editor.take_ops().unwrap(),
6697 [
6698 PageOp::Color(blue),
6699 PageOp::Delete { object: old.id },
6700 PageOp::Add {
6701 object: PageObject::Image(two.clone()),
6702 before: Some(picture.id),
6703 },
6704 PageOp::Add {
6705 object: PageObject::Image(one.clone()),
6706 before: Some(two.id),
6707 },
6708 ]
6709 );
6710 // The same art again, under fresh identities, changes nothing.
6711 assert!(!editor.set_paper(blue, None, Some(art().into())));
6712 assert!(editor.undo(&mut engine).unwrap());
6713 assert_eq!(ids(&editor), (vec![old.id, picture.id], None));
6714 assert_eq!(editor.take_ops().unwrap().len(), 4);
6715 assert!(!editor.can_undo());
6716 assert!(editor.redo(&mut engine).unwrap());
6717 assert_eq!(ids(&editor), applied);
6718 assert!(editor.set_paper(blue, None, Some(Vec::new())));
6719 assert_eq!(ids(&editor), (vec![picture.id], blue));
6720 }
6721
6722 #[test]
6723 fn arrows_leave_a_picture_for_the_neighbouring_outlines_in_page_order() {
6724 use onestore::page::{Image, Page, PageObject};
6725 let mut engine = TextEngine::default();
6726 let outline = |engine: &mut TextEngine, text: &str, x| {
6727 PageObject::Outline(
6728 TextOutline::new(
6729 engine,
6730 TextDocument::new(vec![Paragraph::new(text.into(), Default::default())])
6731 .unwrap(),
6732 120.0,
6733 [x, 0.0],
6734 )
6735 .unwrap()
6736 .snapshot(),
6737 )
6738 };
6739 let picture = Image {
6740 size: None,
6741 id: onestore::page::text::new_id().unwrap(),
6742 layout: onestore::document::Layout {
6743 x: Some(200.0),
6744 y: Some(0.0),
6745 max_width: Some(40.0),
6746 max_height: Some(40.0),
6747 ..Default::default()
6748 },
6749 bytes: Some(std::sync::Arc::from(b"deferred image payload".as_slice())),
6750 display: None,
6751 alt: None,
6752 background: false,
6753 printout: None,
6754 tags: Vec::new(),
6755 link: None,
6756 text: None,
6757 };
6758 let id = picture.id;
6759 let objects = vec![
6760 outline(&mut engine, "Before", 0.0),
6761 PageObject::Image(picture),
6762 outline(&mut engine, "After", 300.0),
6763 ];
6764 let [before, after] = [0, 2].map(|index| objects[index].id());
6765 let mut editor = CanvasEditor::from_page(
6766 Page {
6767 title: String::new(),
6768 identity: None,
6769 created: None,
6770 margin_origin: [36.0, 14.4],
6771 rtl: false,
6772 color: None,
6773 rule_lines: None,
6774 definitions: BTreeMap::new(),
6775 objects,
6776 },
6777 &mut engine,
6778 )
6779 .unwrap();
6780 let caret = |paragraph, offset| [TextPosition { paragraph, offset }; 2];
6781 assert!(editor.step_from_image(&mut engine, id, false).unwrap());
6782 assert_eq!(editor.active_outline().id, before);
6783 assert_eq!(editor.selection().positions, caret(0, 6));
6784 assert!(editor.step_from_image(&mut engine, id, true).unwrap());
6785 assert_eq!(editor.active_outline().id, after);
6786 assert_eq!(editor.selection().positions, caret(0, 0));
6787
6788 editor.select_all().unwrap();
6789 assert!(editor.delete(&mut engine, true).unwrap());
6790 editor.focus_outline(before).unwrap();
6791 assert!(editor.outlines().iter().all(|outline| outline.id != after));
6792 assert!(!editor.step_from_image(&mut engine, id, true).unwrap());
6793 assert_eq!(editor.active_outline().id, before);
6794 }
6795
6796 #[test]
6797 fn text_edits_flow_around_a_picture_inside_an_outline() {
6798 use onestore::page::{Image, Page, PageObject, ParagraphContent};
6799 let mut engine = TextEngine::default();
6800 let mut source = TextOutline::new(
6801 &mut engine,
6802 TextDocument::new(vec![
6803 Paragraph::new("Before".into(), Default::default()),
6804 Paragraph::new("After".into(), Default::default()),
6805 ])
6806 .unwrap(),
6807 240.0,
6808 [36.0, 36.0],
6809 )
6810 .unwrap()
6811 .snapshot();
6812 let mut picture = source.paragraphs[0].clone();
6813 picture.id = onestore::page::text::new_id().unwrap();
6814 let image = Image {
6815 size: Some([40.0, 30.0]),
6816 id: onestore::page::text::new_id().unwrap(),
6817 layout: Default::default(),
6818 bytes: Some(std::sync::Arc::from(b"deferred image payload".as_slice())),
6819 display: None,
6820 alt: None,
6821 background: false,
6822 printout: None,
6823 tags: Vec::new(),
6824 link: None,
6825 text: None,
6826 };
6827 picture.content = ParagraphContent::Image(image.clone());
6828 source.paragraphs.insert(1, picture);
6829 let outline_id = source.id;
6830 let mut editor = CanvasEditor::from_page(
6831 Page {
6832 title: String::new(),
6833 identity: None,
6834 created: None,
6835 margin_origin: [36.0, 14.4],
6836 rtl: false,
6837 color: None,
6838 rule_lines: None,
6839 definitions: BTreeMap::new(),
6840 objects: vec![PageObject::Outline(source)],
6841 },
6842 &mut engine,
6843 )
6844 .unwrap();
6845 let outline = editor.active_outline();
6846 assert_eq!(outline.id, outline_id);
6847 let [before, after] = [0, 1].map(|i| outline.shaped().paragraphs[i].origin[1]);
6848 let object = &outline.shaped().objects[0];
6849 assert_eq!(object.id, image.id);
6850 assert_eq!(object.rect[3] - object.rect[1], 30.0);
6851 assert!(before < object.rect[1] && object.bottom <= after);
6852
6853 editor
6854 .move_selection(&mut engine, Movement::Down, false)
6855 .unwrap();
6856 assert_eq!(editor.selection().positions[0].paragraph, 1);
6857 editor
6858 .move_selection(&mut engine, Movement::Up, false)
6859 .unwrap();
6860 editor
6861 .move_selection(&mut engine, Movement::LineEnd, false)
6862 .unwrap();
6863 editor.insert(&mut engine, "\nMore").unwrap();
6864 let shaped = editor.active_outline().shaped();
6865 assert_eq!(shaped.paragraphs.len(), 3);
6866 assert!(shaped.objects[0].rect[1] > shaped.paragraphs[1].origin[1]);
6867 assert!(shaped.paragraphs[2].origin[1] >= shaped.objects[0].bottom);
6868 // Joining across the picture is refused rather than deleting it.
6869 editor
6870 .move_selection(&mut engine, Movement::Down, false)
6871 .unwrap();
6872 editor
6873 .move_selection(&mut engine, Movement::LineStart, false)
6874 .unwrap();
6875 assert!(!editor.delete(&mut engine, true).unwrap());
6876 let saved = editor.page().unwrap();
6877 let PageObject::Outline(saved) = &saved.objects[0] else {
6878 panic!()
6879 };
6880 assert!(matches!(&saved.paragraphs[2].content, ParagraphContent::Image(i) if *i == image));
6881 assert!(editor.undo(&mut engine).unwrap());
6882 assert_eq!(editor.active_outline().shaped().paragraphs.len(), 2);
6883 assert_eq!(editor.active_outline().shaped().objects.len(), 1);
6884
6885 // Selected, it resizes and deletes in place through the outline's history.
6886 assert!(editor.image_in_outline(image.id));
6887 let (origin, size) = editor.image_placement(image.id).unwrap();
6888 assert_eq!(size, [40.0, 30.0]);
6889 editor
6890 .place_image(&mut engine, image.id, [0.0; 2], [80.0, 60.0])
6891 .unwrap();
6892 assert_eq!(
6893 editor.image_placement(image.id),
6894 Some((origin, [80.0, 60.0]))
6895 );
6896 let shaped = editor.active_outline().shaped();
6897 assert!(shaped.paragraphs[1].origin[1] >= shaped.objects[0].bottom);
6898 assert!(editor.step_from_image(&mut engine, image.id, true).unwrap());
6899 assert_eq!(
6900 editor.selection().positions[0],
6901 TextPosition {
6902 paragraph: 1,
6903 offset: 0
6904 }
6905 );
6906 assert!(
6907 editor
6908 .step_from_image(&mut engine, image.id, false)
6909 .unwrap()
6910 );
6911 assert_eq!(
6912 editor.selection().positions[0],
6913 TextPosition {
6914 paragraph: 0,
6915 offset: 6
6916 }
6917 );
6918 editor.remove_image(&mut engine, image.id).unwrap();
6919 assert!(editor.image_placement(image.id).is_none());
6920 assert!(editor.active_outline().shaped().objects.is_empty());
6921 assert!(editor.undo(&mut engine).unwrap());
6922 assert_eq!(
6923 editor.image_placement(image.id),
6924 Some((origin, [80.0, 60.0]))
6925 );
6926 assert!(editor.undo(&mut engine).unwrap());
6927 assert_eq!(
6928 editor.image_placement(image.id),
6929 Some((origin, [40.0, 30.0]))
6930 );
6931 }
6932
6933 /// OneNote 2010 attaches at the caret (`corpus/attachment-insert`): the text after it
6934 /// follows the file and keeps the caret, and an empty first half gives way to the file.
6935 #[test]
6936 fn a_file_attaches_at_the_caret_as_onenote_splits_around_it() {
6937 use onestore::page::{Attachment, Page, PageObject, ParagraphContent};
6938 let mut engine = TextEngine::default();
6939 let source = TextOutline::new(
6940 &mut engine,
6941 TextDocument::new(vec![
6942 Paragraph::new("Before".into(), Default::default()),
6943 Paragraph::new(String::new(), Default::default()),
6944 ])
6945 .unwrap(),
6946 240.0,
6947 [36.0, 36.0],
6948 )
6949 .unwrap()
6950 .snapshot();
6951 let mut editor = CanvasEditor::from_page(
6952 Page {
6953 title: String::new(),
6954 identity: None,
6955 created: None,
6956 margin_origin: [36.0, 14.4],
6957 rtl: false,
6958 color: None,
6959 rule_lines: None,
6960 definitions: BTreeMap::new(),
6961 objects: vec![PageObject::Outline(source)],
6962 },
6963 &mut engine,
6964 )
6965 .unwrap();
6966 editor.take_ops().unwrap();
6967 let file = |name: &str| Attachment {
6968 id: onestore::page::text::new_id().unwrap(),
6969 filename: name.into(),
6970 source_path: None,
6971 size: Some([24.0, 24.0]),
6972 layout: Default::default(),
6973 bytes: Some(std::sync::Arc::from(b"payload".as_slice())),
6974 preview: None,
6975 recording: None,
6976 tags: Vec::new(),
6977 };
6978 let at = |paragraph, offset| Selection {
6979 positions: [TextPosition { paragraph, offset }; 2],
6980 affinities: [Affinity::Downstream; 2],
6981 };
6982 let shown = |editor: &CanvasEditor| {
6983 let page = editor.page().unwrap();
6984 let PageObject::Outline(outline) = &page.objects[0] else {
6985 panic!()
6986 };
6987 outline
6988 .paragraphs
6989 .iter()
6990 .map(|node| match &node.content {
6991 ParagraphContent::Text(text) => text.text.text().to_owned(),
6992 ParagraphContent::Attachment(file) => format!("[{}]", file.filename),
6993 _ => panic!(),
6994 })
6995 .collect::<Vec<_>>()
6996 };
6997
6998 editor.select(at(0, 3)).unwrap();
6999 let middle = file("middle.txt");
7000 editor
7001 .insert_attachment(&mut engine, middle.clone())
7002 .unwrap();
7003 assert_eq!(shown(&editor), ["Bef", "[middle.txt]", "ore", ""]);
7004 // A file is no text position: the caret starts "ore", the second text.
7005 assert_eq!(editor.selection().positions, at(1, 0).positions);
7006 assert_eq!(editor.attachment(middle.id), Some(&middle));
7007 let ops = editor.take_ops().unwrap();
7008 assert!(ops.iter().any(|op| matches!(op, PageOp::Insert { paragraphs, .. }
7009 if paragraphs.iter().any(|p| matches!(&p.content, ParagraphContent::Attachment(a) if *a == middle)))));
7010
7011 editor.select(at(2, 0)).unwrap();
7012 editor
7013 .insert_attachment(&mut engine, file("empty.bin"))
7014 .unwrap();
7015 assert_eq!(
7016 shown(&editor),
7017 ["Bef", "[middle.txt]", "ore", "[empty.bin]", ""]
7018 );
7019 assert_eq!(editor.selection().positions[0].paragraph, 2);
7020
7021 assert!(editor.undo(&mut engine).unwrap());
7022 assert_eq!(shown(&editor), ["Bef", "[middle.txt]", "ore", ""]);
7023 editor.remove_image(&mut engine, middle.id).unwrap();
7024 assert_eq!(shown(&editor), ["Bef", "ore", ""]);
7025 }
7026
7027 #[test]
7028 fn unknown_paragraphs_stay_on_the_page_and_text_free_outlines_take_a_caret() {
7029 use onestore::page::{Image, Page, PageObject, ParagraphContent, Unsupported};
7030 let mut engine = TextEngine::default();
7031 let outline = |engine: &mut TextEngine| {
7032 TextOutline::new(
7033 engine,
7034 TextDocument::new(vec![
7035 Paragraph::new("Before".into(), Default::default()),
7036 Paragraph::new("After".into(), Default::default()),
7037 ])
7038 .unwrap(),
7039 240.0,
7040 [36.0, 36.0],
7041 )
7042 .unwrap()
7043 .snapshot()
7044 };
7045 let mut mixed = outline(&mut engine);
7046 let mut unknown = mixed.paragraphs[0].clone();
7047 unknown.id = onestore::page::text::new_id().unwrap();
7048 unknown.content = ParagraphContent::Unsupported(Unsupported {
7049 id: onestore::page::text::new_id().unwrap(),
7050 jcid: 0x60012,
7051 layout: Default::default(),
7052 });
7053 mixed.paragraphs.insert(1, unknown);
7054 let mut picture_only = outline(&mut engine);
7055 picture_only.id = onestore::page::text::new_id().unwrap();
7056 picture_only.paragraphs.truncate(1);
7057 picture_only.paragraphs[0].content = ParagraphContent::Image(Image {
7058 size: Some([40.0, 30.0]),
7059 id: onestore::page::text::new_id().unwrap(),
7060 layout: Default::default(),
7061 bytes: Some(std::sync::Arc::from(b"deferred image payload".as_slice())),
7062 display: None,
7063 alt: None,
7064 background: false,
7065 printout: None,
7066 tags: Vec::new(),
7067 link: None,
7068 text: None,
7069 });
7070 let (mixed_id, picture_id) = (mixed.id, picture_only.id);
7071 let editor = CanvasEditor::from_page(
7072 Page {
7073 title: String::new(),
7074 identity: None,
7075 created: None,
7076 margin_origin: [36.0, 14.4],
7077 rtl: false,
7078 color: None,
7079 rule_lines: None,
7080 definitions: BTreeMap::new(),
7081 objects: vec![
7082 PageObject::Outline(mixed),
7083 PageObject::Outline(picture_only),
7084 ],
7085 },
7086 &mut engine,
7087 )
7088 .unwrap();
7089 let mixed = editor.outlines().iter().find(|o| o.id == mixed_id).unwrap();
7090 let [placeholder] = &mixed.shaped().objects[..] else {
7091 panic!()
7092 };
7093 assert!(matches!(
7094 placeholder.kind,
7095 crate::outline::ObjectKind::Unsupported(_)
7096 ));
7097 assert_eq!(placeholder.rect[2] - placeholder.rect[0], 160.0);
7098 assert!(mixed.shaped().paragraphs[1].origin[1] >= placeholder.bottom);
7099 // The picture's outline takes a paragraph for the caret, which the page leaves out
7100 // until it is typed in.
7101 assert!(editor.has_page_outline(picture_id));
7102 let page = editor.page().unwrap();
7103 assert_eq!(page.objects.len(), 2);
7104 assert!(page.objects.iter().any(|object| matches!(
7105 object,
7106 PageObject::Outline(outline) if outline.id == picture_id && outline.paragraphs.len() == 1
7107 )));
7108 }
7109
7110 #[test]
7111 fn title_flow_moves_page_objects_atomically_and_cancels_composition() {
7112 use onestore::page::{Image, Page, PageObject, Title};
7113 let mut engine = TextEngine::default();
7114 let mut title = TextOutline::new(
7115 &mut engine,
7116 TextDocument::new(vec![Paragraph::new("Header".into(), Default::default())]).unwrap(),
7117 468.0,
7118 [0.0; 2],
7119 )
7120 .unwrap()
7121 .snapshot();
7122 title.title = true;
7123 title.layout.max_width = None;
7124 title.layout.width_set_by_user = None;
7125 let body = TextOutline::new(
7126 &mut engine,
7127 TextDocument::new(vec![Paragraph::new("Anchor".into(), Default::default())]).unwrap(),
7128 72.0,
7129 [120.0, 80.0],
7130 )
7131 .unwrap()
7132 .snapshot();
7133 let body_id = body.id;
7134 let images = [
7135 (120.0, 90.0, false),
7136 (600.0, 150.0, false),
7137 (120.0, 0.0, false),
7138 (120.0, 80.0, true),
7139 ]
7140 .map(|(x, y, background)| Image {
7141 size: None,
7142 id: onestore::page::text::new_id().unwrap(),
7143 layout: onestore::document::Layout {
7144 x: Some(x),
7145 y: Some(y),
7146 max_width: Some(72.0),
7147 max_height: Some(40.0),
7148 ..Default::default()
7149 },
7150 bytes: Some(std::sync::Arc::from(b"deferred image payload".as_slice())),
7151 display: None,
7152 alt: None,
7153 background,
7154 printout: None,
7155 tags: Vec::new(),
7156 link: None,
7157 text: None,
7158 });
7159 let image_ids = images.each_ref().map(|image| image.id);
7160 let image_bytes = images
7161 .each_ref()
7162 .map(|image| image.bytes.as_ref().unwrap().clone());
7163 let mut objects = vec![
7164 PageObject::Title(Title {
7165 id: onestore::page::text::new_id().unwrap(),
7166 date: None,
7167 layout: Default::default(),
7168 outlines: vec![title],
7169 }),
7170 PageObject::Outline(body),
7171 ];
7172 objects.extend(images.into_iter().map(PageObject::Image));
7173 let mut editor = CanvasEditor::from_page(
7174 Page {
7175 title: "Header".into(),
7176 identity: None,
7177 created: None,
7178 margin_origin: [36.0, 14.4],
7179 rtl: false,
7180 color: None,
7181 rule_lines: None,
7182 definitions: BTreeMap::new(),
7183 objects,
7184 },
7185 &mut engine,
7186 )
7187 .unwrap();
7188 let positions = |editor: &CanvasEditor| {
7189 editor
7190 .object_layouts()
7191 .map(|(id, layout)| (id, layout.clone()))
7192 .collect::<Vec<_>>()
7193 };
7194 let original = positions(&editor);
7195 let text = "Header\u{000b}Second\u{000b}Third\u{000b}Fourth\u{000b}Fifth\u{000b}Sixth";
7196 let end = text.encode_utf16().count().try_into().unwrap();
7197 editor.select_all().unwrap();
7198 editor.compose(&mut engine, text.into(), end..end).unwrap();
7199 let moved = positions(&editor);
7200 let y = |items: &Vec<(ExGuid, onestore::document::Layout)>, id| {
7201 items
7202 .iter()
7203 .find(|(candidate, _)| *candidate == id)
7204 .unwrap()
7205 .1
7206 .y
7207 .unwrap()
7208 };
7209 let delta = y(&moved, body_id) - y(&original, body_id);
7210 assert!(delta > 0.0);
7211 for id in &image_ids[..2] {
7212 assert!((y(&moved, *id) - y(&original, *id) - delta).abs() < 0.001);
7213 }
7214 for id in &image_ids[2..] {
7215 assert_eq!(y(&moved, *id), y(&original, *id));
7216 }
7217 editor.compose(&mut engine, "Header".into(), 6..6).unwrap();
7218 assert_eq!(positions(&editor), moved);
7219 editor.cancel_composition(&mut engine).unwrap();
7220 assert_eq!(positions(&editor), original);
7221 assert!(!editor.undo(&mut engine).unwrap());
7222 editor.select_all().unwrap();
7223 editor.compose(&mut engine, text.into(), end..end).unwrap();
7224 let taller = format!("{text}\u{000b}Seventh\u{000b}Eighth");
7225 let taller_end = taller.encode_utf16().count().try_into().unwrap();
7226 editor
7227 .compose(&mut engine, taller, taller_end..taller_end)
7228 .unwrap();
7229 let farther = positions(&editor);
7230 assert!(y(&farther, body_id) > y(&moved, body_id));
7231 editor.commit_text(&mut engine, "Header".into()).unwrap();
7232 assert_eq!(positions(&editor), farther);
7233 editor.undo(&mut engine).unwrap();
7234 assert_eq!(positions(&editor), original);
7235 assert!(!editor.undo(&mut engine).unwrap());
7236 editor.redo(&mut engine).unwrap();
7237 assert_eq!(positions(&editor), farther);
7238 editor.undo(&mut engine).unwrap();
7239 editor.select_all().unwrap();
7240 editor.insert(&mut engine, text).unwrap();
7241 assert_eq!(positions(&editor), moved);
7242 editor.select_all().unwrap();
7243 editor.insert(&mut engine, "Header").unwrap();
7244 assert_eq!(positions(&editor), moved);
7245 editor.undo(&mut engine).unwrap();
7246 assert_eq!(positions(&editor), moved);
7247 editor.undo(&mut engine).unwrap();
7248 assert_eq!(positions(&editor), original);
7249 editor.redo(&mut engine).unwrap();
7250 editor.redo(&mut engine).unwrap();
7251 assert_eq!(positions(&editor), moved);
7252 for (id, bytes) in image_ids.into_iter().zip(image_bytes) {
7253 let image = editor
7254 .objects
7255 .iter()
7256 .find_map(|object| match object {
7257 page::Content::Image(image) if image.id == id => Some(image),
7258 _ => None,
7259 })
7260 .unwrap();
7261 assert!(std::sync::Arc::ptr_eq(
7262 image.bytes.as_ref().unwrap(),
7263 &bytes
7264 ));
7265 }
7266 }
7267
7268 #[test]
7269 fn floating_outline_exit_uses_document_position_and_keeps_real_blank_lines() {
7270 let mut engine = TextEngine::default();
7271 for (x, real_blanks, size, exit_y) in [
7272 (179.9, 0, 11.0, None),
7273 (180.0, 0, 11.0, Some(230.4)),
7274 (270.0, 1, 11.0, Some(230.4)),
7275 (270.0, 0, 22.0, Some(248.4)),
7276 (270.0, 0, 44.0, Some(320.4)),
7277 ] {
7278 let mut paragraphs = vec![Paragraph::new(
7279 "Text".into(),
7280 Format {
7281 font_size: Some(size),
7282 ..Default::default()
7283 },
7284 )];
7285 paragraphs
7286 .extend((0..real_blanks).map(|_| Paragraph::new(String::new(), Format::default())));
7287 let source = TextDocument::new(paragraphs).unwrap();
7288 let outline = TextOutline::new(&mut engine, source.clone(), 72.0, [x, 158.4]).unwrap();
7289 let mut editor =
7290 CanvasEditor::from_text_outlines(vec![outline], BTreeMap::new(), None).unwrap();
7291 editor
7292 .move_selection(&mut engine, Movement::DocumentEnd, false)
7293 .unwrap();
7294 for _ in 0..3 - real_blanks {
7295 editor
7296 .move_selection(&mut engine, Movement::Down, false)
7297 .unwrap();
7298 }
7299 assert_eq!(editor.outlines[0].document, source);
7300 assert_eq!(editor.caret_outline().is_some(), exit_y.is_some());
7301 if let Some(y) = exit_y {
7302 assert_eq!(editor.active_outline().origin(), [x, y]);
7303 assert_eq!(
7304 editor.active_outline().layout.max_width,
7305 Some(DEFAULT_OUTLINE_WIDTH)
7306 );
7307 editor.insert(&mut engine, "New").unwrap();
7308 assert_eq!(editor.outlines.len(), 2);
7309 editor.undo(&mut engine).unwrap();
7310 assert_eq!(editor.outlines.len(), 1);
7311 assert_eq!(editor.active_outline().origin(), [x, y]);
7312 } else {
7313 assert_eq!(editor.active_outline().document.nodes().len(), 4);
7314 }
7315 assert!(editor.undo.is_empty());
7316 }
7317 }
7318
7319 #[test]
7320 fn vertical_exit_and_reentry_preserve_the_text_column_and_source() {
7321 let mut engine = TextEngine::default();
7322 for (x, y, above) in [
7323 (36.0, 158.4, 122.4),
7324 (180.0, 158.4, 122.4),
7325 (180.0, 178.0, 140.4),
7326 (180.0, 180.0, 140.4),
7327 (180.0, 184.0, 158.4),
7328 ] {
7329 let source =
7330 TextDocument::new(vec![Paragraph::new("Text".into(), Format::default())]).unwrap();
7331 let outline = TextOutline::new(&mut engine, source.clone(), 72.0, [x, y]).unwrap();
7332 let id = outline.id;
7333 let mut editor =
7334 CanvasEditor::from_text_outlines(vec![outline], BTreeMap::new(), None).unwrap();
7335 editor
7336 .move_selection(&mut engine, Movement::DocumentEnd, false)
7337 .unwrap();
7338 let column = editor.caret(1.0).unwrap().x0 as f32;
7339 editor
7340 .move_selection(&mut engine, Movement::Up, false)
7341 .unwrap();
7342 assert_eq!(
7343 editor.caret_outline().unwrap().origin(),
7344 [x + column, above]
7345 );
7346 editor.focus_outline(id).unwrap();
7347 if x >= 180.0 {
7348 for _ in 0..3 {
7349 editor
7350 .move_selection(&mut engine, Movement::Down, false)
7351 .unwrap();
7352 }
7353 assert!(editor.caret_outline().is_some());
7354 editor
7355 .move_selection(&mut engine, Movement::Up, false)
7356 .unwrap();
7357 assert!(editor.caret_outline().is_some());
7358 editor
7359 .move_selection(&mut engine, Movement::Up, false)
7360 .unwrap();
7361 if y == 184.0 {
7362 assert!(editor.caret_outline().is_some());
7363 editor
7364 .move_selection(&mut engine, Movement::Up, false)
7365 .unwrap();
7366 }
7367 assert_eq!(editor.active_outline().id, id);
7368 assert_eq!(editor.selection().positions[1].offset, 4);
7369 }
7370 assert_eq!(editor.outlines[0].document, source);
7371 assert!(editor.undo.is_empty());
7372 }
7373 }
7374
7375 #[test]
7376 fn provisional_paragraphs_use_the_base_style_instead_of_local_run_formatting() {
7377 let mut engine = TextEngine::default();
7378 let base = Format {
7379 font: Some("Arial".into()),
7380 font_size: Some(11.0),
7381 ..Default::default()
7382 };
7383 let style = ExGuid {
7384 n: 9,
7385 ..Default::default()
7386 };
7387 let mut nodes = TextDocument::new(vec![Paragraph::new(
7388 "Text".into(),
7389 Format {
7390 font_size: Some(22.0),
7391 bold: Some(true),
7392 ..base.clone()
7393 },
7394 )])
7395 .unwrap()
7396 .nodes()
7397 .to_vec();
7398 nodes[0].style = Some(style);
7399 let outline = TextOutline::new(
7400 &mut engine,
7401 TextDocument::from_nodes(nodes).unwrap(),
7402 240.0,
7403 [0.0; 2],
7404 )
7405 .unwrap();
7406 let mut editor = CanvasEditor::from_text_outlines(
7407 vec![outline],
7408 BTreeMap::from([(
7409 style,
7410 Definition {
7411 kind: onestore::document::Kind::Style {
7412 name: Some("p".into()),
7413 next: None,
7414 },
7415 format: base.clone(),
7416 },
7417 )]),
7418 None,
7419 )
7420 .unwrap();
7421 editor
7422 .move_selection(&mut engine, Movement::Down, false)
7423 .unwrap();
7424 let paragraph = &editor.active_outline().document.nodes()[1];
7425 assert_eq!(paragraph.style, Some(style));
7426 assert_eq!(paragraph.text().unwrap().text.spans()[0].format, base);
7427 assert_eq!(
7428 editor.active_outline().shaped.paragraphs[1].text.height(),
7429 engine
7430 .layout(&Paragraph::new(String::new(), base), 240.0)
7431 .unwrap()
7432 .height()
7433 );
7434 assert_eq!(editor.outlines[0].document.nodes().len(), 1);
7435 }
7436
7437 #[test]
7438 fn clicks_below_use_base_style_preserve_source_and_share_draft_history() {
7439 let mut engine = TextEngine::default();
7440 for font_size in [11.0, 22.0] {
7441 let style = ExGuid {
7442 n: 900,
7443 ..Default::default()
7444 };
7445 let base = Format {
7446 font: Some("Arial".into()),
7447 font_size: Some(font_size),
7448 ..Default::default()
7449 };
7450 let mut nodes = TextDocument::new(vec![Paragraph::new(
7451 "Body".into(),
7452 Format {
7453 font_size: Some(44.0),
7454 bold: Some(true),
7455 ..base.clone()
7456 },
7457 )])
7458 .unwrap()
7459 .nodes()
7460 .to_vec();
7461 nodes[0].style = Some(style);
7462 let outline = TextOutline::new(
7463 &mut engine,
7464 TextDocument::from_nodes(nodes).unwrap(),
7465 240.0,
7466 [180.0, 90.0],
7467 )
7468 .unwrap();
7469 let id = outline.id;
7470 let source = outline.document.clone();
7471 let layout = outline.layout.clone();
7472 let text_id = outline.shaped.paragraphs[0].text.id();
7473 let bottom = outline.shaped.size[1];
7474 let mut editor = CanvasEditor::from_text_outlines(
7475 vec![outline],
7476 BTreeMap::from([(
7477 style,
7478 Definition {
7479 kind: onestore::document::Kind::Style {
7480 name: Some("p".into()),
7481 next: None,
7482 },
7483 format: base.clone(),
7484 },
7485 )]),
7486 None,
7487 )
7488 .unwrap();
7489 assert!(
7490 editor
7491 .select_below(&mut engine, id, [10.0, bottom + 20.0])
7492 .unwrap()
7493 );
7494 let blanks = if font_size == 11.0 { 2 } else { 1 };
7495 assert_eq!(editor.active_outline().document.nodes().len(), blanks + 1);
7496 assert_eq!(
7497 editor.selection().positions[1],
7498 TextPosition {
7499 paragraph: blanks,
7500 offset: 0
7501 }
7502 );
7503 assert_eq!(editor.outlines[0].document, source);
7504 assert_eq!(
7505 editor.active_outline().shaped.paragraphs[0].text.id(),
7506 text_id
7507 );
7508 for paragraph in &editor.active_outline().document.nodes()[1..] {
7509 assert_eq!(paragraph.style, Some(style));
7510 assert_eq!(paragraph.text().unwrap().text.spans()[0].format, base);
7511 }
7512 let pending = editor.active_outline().document.clone();
7513 let selection = editor.selection();
7514 assert!(
7515 !editor
7516 .select_below(&mut engine, id, [10.0, bottom + 28.0])
7517 .unwrap()
7518 );
7519 assert_eq!(editor.active_outline().document, pending);
7520 assert_eq!(editor.selection(), selection);
7521 assert!(editor.undo.is_empty());
7522 editor.compose(&mut engine, "日本".into(), 2..2).unwrap();
7523 assert!(editor.cancel_composition(&mut engine).unwrap());
7524 assert_eq!(editor.active_outline().document, pending);
7525 editor.insert(&mut engine, "X").unwrap();
7526 let edited = editor.active_outline().document.clone();
7527 assert_eq!(editor.outlines[0].layout, layout);
7528 assert_eq!(edited.nodes()[0], source.nodes()[0]);
7529 assert!(editor.undo(&mut engine).unwrap());
7530 assert_eq!(editor.active_outline().document, pending);
7531 assert_eq!(editor.outlines[0].document, source);
7532 assert_eq!(editor.selection(), selection);
7533 assert!(editor.redo(&mut engine).unwrap());
7534 assert_eq!(editor.active_outline().document, edited);
7535 assert!(editor.undo(&mut engine).unwrap());
7536 assert!(
7537 editor
7538 .select_below(&mut engine, id, [10.0, bottom + 1.0])
7539 .unwrap()
7540 );
7541 assert_eq!(editor.active_outline().document.nodes().len(), blanks + 1);
7542 editor
7543 .select([selection.positions[1], editor.selection().positions[1]].into())
7544 .unwrap();
7545 assert_eq!(editor.outlines[0].document, source);
7546 assert_eq!(editor.redo.len(), 1);
7547 editor
7548 .place_caret(&mut engine, [600.0, 600.0], 240.0)
7549 .unwrap();
7550 let pending = editor.active_outline().document.clone();
7551 let selection = editor.selection();
7552 editor.definitions.get_mut(&style).unwrap().format.font_size = Some(0.001);
7553 assert!(
7554 editor
7555 .select_below(&mut engine, id, [10.0, bottom + 20.0])
7556 .is_err()
7557 );
7558 assert_eq!(editor.active_outline().document, pending);
7559 assert_eq!(editor.selection(), selection);
7560 assert_eq!(editor.outlines[0].document, source);
7561 assert_eq!(editor.redo.len(), 1);
7562 }
7563 }
7564
7565 #[test]
7566 fn editing_during_provisional_composition_keeps_only_committed_layout_height() {
7567 let mut engine = TextEngine::default();
7568 let source =
7569 TextDocument::new(vec![Paragraph::new("body".into(), Format::default())]).unwrap();
7570 let mut editor = CanvasEditor::new(&mut engine, source.clone(), 240.0).unwrap();
7571 for _ in 0..3 {
7572 editor
7573 .move_selection(&mut engine, Movement::Down, false)
7574 .unwrap();
7575 }
7576 for _ in 0..2 {
7577 editor
7578 .move_selection(&mut engine, Movement::Up, false)
7579 .unwrap();
7580 }
7581 editor.compose(&mut engine, "a".into(), 1..1).unwrap();
7582 editor.insert(&mut engine, "b").unwrap();
7583 let expected = TextOutline::from_outline(
7584 &mut engine,
7585 &editor.active_outline().snapshot(),
7586 &BTreeMap::new(),
7587 )
7588 .unwrap();
7589 assert_eq!(editor.active_outline().shaped.size, expected.shaped.size);
7590 assert_eq!(editor.active_outline().document.nodes().len(), 2);
7591 editor.undo(&mut engine).unwrap();
7592 assert_eq!(editor.outlines[0].document, source);
7593 assert_eq!(editor.active_outline().document.nodes().len(), 4);
7594 }
7595
7596 #[test]
7597 fn arrow_created_paragraphs_are_provisional_and_share_existing_shaping() {
7598 let mut engine = TextEngine::default();
7599 let source =
7600 TextDocument::new(vec![Paragraph::new("body".into(), Format::default())]).unwrap();
7601 let mut editor = CanvasEditor::new(&mut engine, source.clone(), 240.0).unwrap();
7602 let shaped = editor.outlines[0].shaped.paragraphs[0]
7603 .text
7604 .shaped
7605 .styles()
7606 .as_ptr();
7607 for _ in 0..3 {
7608 editor
7609 .move_selection(&mut engine, Movement::Down, false)
7610 .unwrap();
7611 }
7612 assert_eq!(editor.outlines[0].document, source);
7613 assert_eq!(editor.active_outline().document.nodes().len(), 4);
7614 assert_eq!(
7615 editor
7616 .visible_outlines()
7617 .next()
7618 .unwrap()
7619 .document
7620 .nodes()
7621 .len(),
7622 4
7623 );
7624 assert_eq!(
7625 editor.active_outline().shaped.paragraphs[0]
7626 .text
7627 .shaped
7628 .styles()
7629 .as_ptr(),
7630 shaped
7631 );
7632 assert!(editor.undo.is_empty());
7633 editor
7634 .move_selection(&mut engine, Movement::Up, false)
7635 .unwrap();
7636 editor
7637 .move_selection(&mut engine, Movement::Up, false)
7638 .unwrap();
7639 let pending = editor.active_outline().document.clone();
7640 let selection = editor.selection();
7641 editor.insert(&mut engine, "X").unwrap();
7642 assert_eq!(
7643 editor.outlines[0]
7644 .document
7645 .paragraphs()
7646 .map(Paragraph::text)
7647 .collect::<Vec<_>>(),
7648 ["body", "X"]
7649 );
7650 assert_eq!(
7651 editor.outlines[0].shaped.paragraphs[0]
7652 .text
7653 .shaped
7654 .styles()
7655 .as_ptr(),
7656 shaped
7657 );
7658 editor.undo(&mut engine).unwrap();
7659 assert_eq!(editor.outlines[0].document, source);
7660 assert_eq!(editor.active_outline().document, pending);
7661 assert_eq!(editor.selection(), selection);
7662 editor
7663 .place_caret(&mut engine, [500.0, 500.0], 200.0)
7664 .unwrap();
7665 editor.redo(&mut engine).unwrap();
7666 assert_eq!(
7667 editor.outlines[0]
7668 .document
7669 .paragraphs()
7670 .map(Paragraph::text)
7671 .collect::<Vec<_>>(),
7672 ["body", "X"]
7673 );
7674 editor.undo(&mut engine).unwrap();
7675 assert_eq!(editor.active_outline().document, pending);
7676 assert_eq!(editor.selection(), selection);
7677 editor
7678 .place_caret(&mut engine, [500.0, 500.0], 200.0)
7679 .unwrap();
7680 assert_eq!(editor.outlines[0].document, source);
7681 }
7682
7683 /// New text takes the page's `p` quick style in the Default font, and a page whose `p`
7684 /// is in another font gains a second `p` beside it, as OneNote 2010 does
7685 /// (`corpus/default-font`).
7686 #[test]
7687 fn new_outlines_take_a_body_style_in_the_default_font() {
7688 let mut engine = TextEngine::default();
7689 let source =
7690 TextDocument::new(vec![Paragraph::new("body".into(), Format::default())]).unwrap();
7691 let mut editor = CanvasEditor::new(&mut engine, source, 240.0).unwrap();
7692 let style = |editor: &CanvasEditor, outline: usize| {
7693 let node = &editor.outlines[outline].document.nodes()[0];
7694 let definition = &editor.definitions[&node.style.unwrap()];
7695 assert_eq!(
7696 node.text().unwrap().text.spans()[0].format,
7697 definition.format
7698 );
7699 (node.style.unwrap(), definition.clone())
7700 };
7701 editor.default_font = DefaultFont {
7702 face: "Georgia".into(),
7703 size: 14.0,
7704 color: None,
7705 };
7706 for (typed, at) in [("one", 300.0), ("two", 400.0)] {
7707 editor.place_caret(&mut engine, [300.0, at], 240.0).unwrap();
7708 editor.insert(&mut engine, typed).unwrap();
7709 }
7710 let (first, georgia) = style(&editor, 1);
7711 assert_eq!(style(&editor, 2).0, first, "one p per font");
7712 assert_eq!(georgia, editor.default_font.body_style());
7713 editor.default_font = DefaultFont::default();
7714 editor
7715 .place_caret(&mut engine, [300.0, 500.0], 240.0)
7716 .unwrap();
7717 editor.insert(&mut engine, "three").unwrap();
7718 let (third, calibri) = style(&editor, 3);
7719 assert_ne!(third, first);
7720 assert_eq!(calibri.format.font.as_deref(), Some("Calibri"));
7721 assert_eq!(calibri.format.font_size, Some(11.0));
7722 }
7723
7724 #[test]
7725 fn deleting_through_provisional_paragraphs_removes_the_outline_and_restores_draft_on_undo() {
7726 let mut engine = TextEngine::default();
7727 let source =
7728 TextDocument::new(vec![Paragraph::new("body".into(), Format::default())]).unwrap();
7729 let mut editor = CanvasEditor::new(&mut engine, source.clone(), 240.0).unwrap();
7730 for _ in 0..2 {
7731 editor
7732 .move_selection(&mut engine, Movement::Down, false)
7733 .unwrap();
7734 }
7735 let pending = editor.active_outline().document.clone();
7736 editor.select_all().unwrap();
7737 let selection = editor.selection();
7738 editor.delete(&mut engine, true).unwrap();
7739 assert!(editor.outlines.is_empty());
7740 assert!(editor.caret_outline().is_some());
7741 editor
7742 .place_caret(&mut engine, [500.0, 500.0], 200.0)
7743 .unwrap();
7744 for _ in 0..2 {
7745 editor.undo(&mut engine).unwrap();
7746 assert_eq!(editor.outlines[0].document, source);
7747 assert_eq!(editor.active_outline().document, pending);
7748 assert_eq!(editor.selection(), selection);
7749 editor.redo(&mut engine).unwrap();
7750 assert!(editor.outlines.is_empty());
7751 }
7752 }
7753
7754 #[test]
7755 fn provisional_composition_cancels_and_backspace_reenters_without_history() {
7756 let mut engine = TextEngine::default();
7757 let source =
7758 TextDocument::new(vec![Paragraph::new("body".into(), Format::default())]).unwrap();
7759 let mut editor = CanvasEditor::new(&mut engine, source.clone(), 240.0).unwrap();
7760 editor
7761 .move_selection(&mut engine, Movement::Down, false)
7762 .unwrap();
7763 let pending = editor.active_outline().document.clone();
7764 editor.compose(&mut engine, "日本".into(), 2..2).unwrap();
7765 assert_eq!(editor.outlines[0].document, source);
7766 editor.cancel_composition(&mut engine).unwrap();
7767 assert_eq!(editor.active_outline().document, pending);
7768 editor.compose(&mut engine, "日本".into(), 2..2).unwrap();
7769 editor.finish_composition();
7770 assert_eq!(
7771 editor.outlines[0]
7772 .document
7773 .paragraphs()
7774 .map(Paragraph::text)
7775 .collect::<Vec<_>>(),
7776 ["body", "日本"]
7777 );
7778 editor.undo(&mut engine).unwrap();
7779 assert_eq!(editor.active_outline().document, pending);
7780 editor.delete(&mut engine, true).unwrap();
7781 assert_eq!(editor.outlines[0].document, source);
7782 assert!(matches!(editor.active, Focus::Outline(0)));
7783 assert!(editor.undo.is_empty());
7784 assert!(editor.redo(&mut engine).unwrap());
7785 }
7786
7787 #[test]
7788 fn word_navigation_terminates_at_soft_wrapped_bidi_cycles() {
7789 let mut engine = TextEngine::default();
7790 let paragraph = Paragraph::from_runs(vec![
7791 (
7792 "العربية".into(),
7793 Format {
7794 font_size: Some(12.0),
7795 bold: Some(false),
7796 italic: Some(false),
7797 ..Default::default()
7798 },
7799 ),
7800 ("ية 日本".into(), Format::default()),
7801 ]);
7802 let source = TextDocument::new(vec![paragraph]).unwrap();
7803 let mut editor = CanvasEditor::new(&mut engine, source.clone(), 1.0).unwrap();
7804 let selection = Selection {
7805 positions: [TextPosition {
7806 paragraph: 0,
7807 offset: 7,
7808 }; 2],
7809 affinities: [Affinity::Upstream; 2],
7810 };
7811 editor.select(selection).unwrap();
7812 editor.delete_to(&mut engine, Movement::WordRight).unwrap();
7813 assert_eq!(
7814 editor
7815 .active_outline()
7816 .document
7817 .paragraphs()
7818 .next()
7819 .unwrap()
7820 .text(),
7821 "ية 日本"
7822 );
7823 editor.undo(&mut engine).unwrap();
7824 assert_eq!(editor.active_outline().document, source);
7825 assert_eq!(editor.selection(), selection);
7826 for offset in 0..=12 {
7827 for affinity in [Affinity::Upstream, Affinity::Downstream] {
7828 for movement in [Movement::WordLeft, Movement::WordRight] {
7829 editor
7830 .select(Selection {
7831 positions: [TextPosition {
7832 paragraph: 0,
7833 offset,
7834 }; 2],
7835 affinities: [affinity; 2],
7836 })
7837 .unwrap();
7838 editor.move_selection(&mut engine, movement, true).unwrap();
7839 editor.select(editor.selection()).unwrap();
7840 }
7841 }
7842 }
7843 }
7844
7845 #[test]
7846 fn indentation_reflows_and_split_join_preserves_levels_and_undo() {
7847 let mut engine = TextEngine::default();
7848 let source = TextDocument::new(vec![
7849 Paragraph::new("alpha beta gamma delta".into(), Format::default()),
7850 Paragraph::new("untouched".into(), Format::default()),
7851 ])
7852 .unwrap();
7853 let mut editor = CanvasEditor::new(&mut engine, source.clone(), 110.0).unwrap();
7854 let untouched = editor.active_outline().shaped.paragraphs[1]
7855 .text
7856 .shaped
7857 .styles()
7858 .as_ptr();
7859 let selection: Selection = [TextPosition {
7860 paragraph: 0,
7861 offset: 6,
7862 }; 2]
7863 .into();
7864 editor.select(selection).unwrap();
7865 editor.indent(&mut engine, false).unwrap();
7866 assert_eq!(editor.active_outline().document.nodes()[0].level, 2);
7867 assert_eq!(editor.active_outline().shaped.paragraphs[0].origin[0], 27.0);
7868 assert_eq!(
7869 editor.active_outline().shaped.paragraphs[1]
7870 .text
7871 .shaped
7872 .styles()
7873 .as_ptr(),
7874 untouched
7875 );
7876 editor.insert(&mut engine, "\n").unwrap();
7877 assert_eq!(
7878 editor
7879 .active_outline()
7880 .document
7881 .nodes()
7882 .iter()
7883 .map(|n| n.level)
7884 .collect::<Vec<_>>(),
7885 [2, 2, 1]
7886 );
7887 // Backspace outdents an indented paragraph before joining it (`c4-child-1.xml`).
7888 editor.delete(&mut engine, true).unwrap();
7889 assert_eq!(editor.active_outline().document.nodes()[1].level, 1);
7890 editor.delete(&mut engine, true).unwrap();
7891 assert_eq!(
7892 editor.active_outline().document.nodes()[0]
7893 .text()
7894 .unwrap()
7895 .text
7896 .text(),
7897 "alpha beta gamma delta"
7898 );
7899 for _ in 0..4 {
7900 editor.undo(&mut engine).unwrap();
7901 }
7902 assert_eq!(editor.active_outline().document, source);
7903 assert_eq!(editor.selection(), selection);
7904 assert!(!editor.indent(&mut engine, true).unwrap());
7905 assert!(editor.redo(&mut engine).unwrap());
7906 }
7907
7908 #[test]
7909 fn indentation_excludes_selected_next_paragraph_start_and_rejects_overflow_atomically() {
7910 let mut engine = TextEngine::default();
7911 let source = TextDocument::new(vec![
7912 Paragraph::new("first".into(), Format::default()),
7913 Paragraph::new("second".into(), Format::default()),
7914 ])
7915 .unwrap();
7916 let mut editor = CanvasEditor::new(&mut engine, source, 50.0).unwrap();
7917 let selection: Selection = [
7918 TextPosition {
7919 paragraph: 1,
7920 offset: 0,
7921 },
7922 TextPosition {
7923 paragraph: 0,
7924 offset: 0,
7925 },
7926 ]
7927 .into();
7928 editor.select(selection).unwrap();
7929 editor.indent(&mut engine, false).unwrap();
7930 let indented = editor.active_outline().document.clone();
7931 assert_eq!(
7932 indented.nodes().iter().map(|n| n.level).collect::<Vec<_>>(),
7933 [2, 1]
7934 );
7935 assert!(editor.indent(&mut engine, false).is_err());
7936 assert_eq!(editor.active_outline().document, indented);
7937 assert_eq!(editor.selection(), selection);
7938 editor.undo(&mut engine).unwrap();
7939 assert!(!editor.undo(&mut engine).unwrap());
7940 }
7941
7942 #[test]
7943 fn paragraph_and_document_navigation_preserve_selection_anchor() {
7944 let mut engine = TextEngine::default();
7945 let source = TextDocument::new(vec![
7946 Paragraph::new("one two three four five".into(), Format::default()),
7947 Paragraph::new("🌳 last".into(), Format::default()),
7948 ])
7949 .unwrap();
7950 let mut editor = CanvasEditor::new(&mut engine, source.clone(), 45.0).unwrap();
7951 let start = TextPosition {
7952 paragraph: 0,
7953 offset: 8,
7954 };
7955 editor.select([start; 2].into()).unwrap();
7956 editor
7957 .move_selection(&mut engine, Movement::ParagraphEnd, true)
7958 .unwrap();
7959 assert_eq!(
7960 editor.selection().positions,
7961 [
7962 start,
7963 TextPosition {
7964 paragraph: 0,
7965 offset: 23
7966 }
7967 ]
7968 );
7969 editor
7970 .move_selection(&mut engine, Movement::ParagraphEnd, true)
7971 .unwrap();
7972 assert_eq!(
7973 editor.selection().positions,
7974 [
7975 start,
7976 TextPosition {
7977 paragraph: 1,
7978 offset: 7
7979 }
7980 ]
7981 );
7982 editor
7983 .move_selection(&mut engine, Movement::DocumentStart, true)
7984 .unwrap();
7985 assert_eq!(
7986 editor.selection().positions,
7987 [
7988 start,
7989 TextPosition {
7990 paragraph: 0,
7991 offset: 0
7992 }
7993 ]
7994 );
7995 editor
7996 .move_selection(&mut engine, Movement::DocumentEnd, false)
7997 .unwrap();
7998 assert_eq!(
7999 editor.selection().positions,
8000 [TextPosition {
8001 paragraph: 1,
8002 offset: 7
8003 }; 2]
8004 );
8005 editor
8006 .move_selection(&mut engine, Movement::ParagraphStart, false)
8007 .unwrap();
8008 assert_eq!(
8009 editor.selection().positions,
8010 [TextPosition {
8011 paragraph: 1,
8012 offset: 0
8013 }; 2]
8014 );
8015 editor
8016 .move_selection(&mut engine, Movement::ParagraphStart, false)
8017 .unwrap();
8018 assert_eq!(
8019 editor.selection().positions,
8020 [TextPosition {
8021 paragraph: 0,
8022 offset: 0
8023 }; 2]
8024 );
8025 assert_eq!(editor.active_outline().document, source);
8026 assert!(!editor.undo(&mut engine).unwrap());
8027 }
8028
8029 #[test]
8030 fn word_deletion_restores_original_caret_and_styled_source_on_undo() {
8031 let mut engine = TextEngine::default();
8032 let source = TextDocument::new(vec![Paragraph::new(
8033 "first café 🌳 last".into(),
8034 Format {
8035 bold: Some(true),
8036 ..Default::default()
8037 },
8038 )])
8039 .unwrap();
8040 let mut editor = CanvasEditor::new(&mut engine, source.clone(), 200.0).unwrap();
8041 let selection: Selection = [TextPosition {
8042 paragraph: 0,
8043 offset: 10,
8044 }; 2]
8045 .into();
8046 editor.select(selection).unwrap();
8047 assert!(editor.delete_to(&mut engine, Movement::WordLeft).unwrap());
8048 assert_eq!(
8049 editor
8050 .active_outline()
8051 .document
8052 .paragraphs()
8053 .next()
8054 .unwrap()
8055 .text(),
8056 "first 🌳 last"
8057 );
8058 editor.undo(&mut engine).unwrap();
8059 assert_eq!(editor.active_outline().document, source);
8060 assert_eq!(editor.selection(), selection);
8061 editor.redo(&mut engine).unwrap();
8062 editor.undo(&mut engine).unwrap();
8063 editor
8064 .move_selection(&mut engine, Movement::DocumentStart, false)
8065 .unwrap();
8066 assert!(!editor.delete_to(&mut engine, Movement::WordLeft).unwrap());
8067 assert!(editor.redo(&mut engine).unwrap());
8068 }
8069
8070 #[test]
8071 fn horizontal_navigation_and_blank_grid_keep_documents_and_history_unchanged() {
8072 let mut engine = TextEngine::default();
8073 let mut outlines = Vec::new();
8074 for (text, origin) in [
8075 ("Solo", [36.0, 158.4]),
8076 ("Right", [504.0, 158.4]),
8077 ("Below", [504.0, 320.4]),
8078 ] {
8079 let document =
8080 TextDocument::new(vec![Paragraph::new(text.into(), Format::default())]).unwrap();
8081 outlines.push(TextOutline::new(&mut engine, document, 72.0, origin).unwrap());
8082 }
8083 let ids: Vec<_> = outlines.iter().map(|outline| outline.id).collect();
8084 let originals: Vec<_> = outlines
8085 .iter()
8086 .map(|outline| outline.document.clone())
8087 .collect();
8088 let mut editor = CanvasEditor::from_text_outlines(outlines, BTreeMap::new(), None).unwrap();
8089 editor
8090 .move_selection(&mut engine, Movement::LineEnd, false)
8091 .unwrap();
8092 editor
8093 .move_selection(&mut engine, Movement::Right, true)
8094 .unwrap();
8095 assert_eq!(editor.active_outline().id, ids[0]);
8096 editor
8097 .move_selection(&mut engine, Movement::Right, false)
8098 .unwrap();
8099 assert_eq!(editor.active_outline().id, ids[1]);
8100 assert_eq!(editor.selection().positions[1].offset, 0);
8101 editor
8102 .move_selection(&mut engine, Movement::Left, false)
8103 .unwrap();
8104 assert_eq!(editor.active_outline().id, ids[0]);
8105 assert_eq!(editor.selection().positions[1].offset, 4);
8106 editor.focus_outline(ids[1]).unwrap();
8107 editor
8108 .move_selection(&mut engine, Movement::LineEnd, false)
8109 .unwrap();
8110 editor
8111 .move_selection(&mut engine, Movement::Right, false)
8112 .unwrap();
8113 assert_eq!(editor.active_outline().id, ids[1]);
8114 assert_eq!(editor.selection().positions[1].offset, 5);
8115 editor
8116 .place_caret(&mut engine, [486.0, 194.4], 468.0)
8117 .unwrap();
8118 editor
8119 .move_selection(&mut engine, Movement::Right, false)
8120 .unwrap();
8121 assert_eq!(editor.active_outline().origin(), [504.0, 194.4]);
8122 editor
8123 .move_selection(&mut engine, Movement::Up, false)
8124 .unwrap();
8125 assert_eq!(editor.active_outline().id, ids[1]);
8126 assert_eq!(editor.selection().positions[1].offset, 0);
8127 assert!(
8128 editor
8129 .outlines
8130 .iter()
8131 .zip(&originals)
8132 .all(|(outline, original)| &outline.document == original)
8133 );
8134 assert!(!editor.undo(&mut engine).unwrap());
8135 }
8136
8137 #[test]
8138 fn word_and_paragraph_selection_use_source_ranges_across_wraps() {
8139 let mut engine = TextEngine::default();
8140 let document = TextDocument::new(vec![Paragraph::new(
8141 "alpha beta gamma 🌳 delta".into(),
8142 Format::default(),
8143 )])
8144 .unwrap();
8145 let mut editor = CanvasEditor::new(&mut engine, document, 70.0).unwrap();
8146 let point = TextPosition {
8147 paragraph: 0,
8148 offset: 7,
8149 };
8150 editor.select([point; 2].into()).unwrap();
8151 let caret = editor.caret(1.0).unwrap();
8152 let x = caret.x0 as f32;
8153 let y = ((caret.y0 + caret.y1) * 0.5) as f32;
8154 let word = editor.selection_at(x, y, SelectionUnit::Word).unwrap();
8155 assert_eq!(word.positions.map(|p| p.offset), [6, 10]);
8156 let paragraph = editor.selection_at(x, y, SelectionUnit::Paragraph).unwrap();
8157 assert_eq!(paragraph.positions.map(|p| p.offset), [0, 25]);
8158 assert_eq!(editor.selection().positions, [point; 2]);
8159 }
8160
8161 #[test]
8162 fn pointer_selection_uses_canvas_line_boxes_after_many_emoji_lines() {
8163 let text = "alpha 🌳 beta\n".repeat(30);
8164 let start = text[..text.rfind("beta").unwrap()].encode_utf16().count() as u32;
8165 let mut engine = TextEngine::default();
8166 let document = TextDocument::new(vec![Paragraph::new(text, Format::default())]).unwrap();
8167 let mut editor = CanvasEditor::new(&mut engine, document, 70.0).unwrap();
8168 let position = TextPosition {
8169 paragraph: 0,
8170 offset: start + 1,
8171 };
8172 editor.select([position; 2].into()).unwrap();
8173 let caret = editor.caret(1.0).unwrap();
8174 let point = [caret.x0 as f32, ((caret.y0 + caret.y1) * 0.5) as f32];
8175 assert_eq!(
8176 editor
8177 .selection_at(point[0], point[1], SelectionUnit::Word)
8178 .unwrap()
8179 .positions
8180 .map(|p| p.offset),
8181 [start, start + 4]
8182 );
8183 assert_eq!(
8184 editor
8185 .selection_at(point[0], point[1], SelectionUnit::Grapheme)
8186 .unwrap()
8187 .positions,
8188 [position; 2]
8189 );
8190 }
8191
8192 #[test]
8193 fn retiring_text_restores_source_selection_order_and_geometry_through_history() {
8194 let mut engine = TextEngine::default();
8195 let mut outlines = Vec::new();
8196 for text in ["before", "bold 🌳\nannotation", "after"] {
8197 let document = TextDocument::new(
8198 text.split('\n')
8199 .map(|line| {
8200 Paragraph::new(
8201 line.into(),
8202 Format {
8203 bold: Some(true),
8204 ..Default::default()
8205 },
8206 )
8207 })
8208 .collect(),
8209 )
8210 .unwrap();
8211 outlines.push(TextOutline::new(&mut engine, document, 123.0, [43.5, 230.4]).unwrap());
8212 }
8213 let id = outlines[1].id;
8214 let document = outlines[1].document.clone();
8215 let geometry = layout_snapshot(&outlines[1].shaped);
8216 let layout = outlines[1].layout.clone();
8217 let ids = outlines
8218 .iter()
8219 .map(|outline| outline.id)
8220 .collect::<Vec<_>>();
8221 let mut editor = CanvasEditor::from_text_outlines(outlines, BTreeMap::new(), None).unwrap();
8222 editor.focus_outline(id).unwrap();
8223 editor.select_all().unwrap();
8224 let selection = editor.selection();
8225 editor.delete(&mut engine, true).unwrap();
8226 assert_eq!(
8227 editor.outlines.iter().map(|o| o.id).collect::<Vec<_>>(),
8228 [ids[0], ids[2]]
8229 );
8230 assert_eq!(editor.caret_outline().unwrap().id, id);
8231 assert_eq!(editor.active_outline().layout, layout);
8232 editor.insert(&mut engine, "again").unwrap();
8233 assert_eq!(
8234 editor.outlines.iter().map(|o| o.id).collect::<Vec<_>>(),
8235 ids
8236 );
8237 assert_eq!(editor.active_outline().id, id);
8238 editor.undo(&mut engine).unwrap();
8239 assert_eq!(editor.caret_outline().unwrap().id, id);
8240 editor
8241 .place_caret(&mut engine, [900.0, 500.0], 77.0)
8242 .unwrap();
8243 editor.undo(&mut engine).unwrap();
8244 assert_eq!(editor.active_outline().document, document);
8245 assert_eq!(editor.active_outline().layout, layout);
8246 assert_eq!(editor.selection(), selection);
8247 assert_eq!(layout_snapshot(&editor.active_outline().shaped), geometry);
8248 assert_eq!(
8249 editor.outlines.iter().map(|o| o.id).collect::<Vec<_>>(),
8250 ids
8251 );
8252 editor.redo(&mut engine).unwrap();
8253 assert_eq!(editor.outlines.len(), 2);
8254 assert_eq!(editor.active_outline().id, id);
8255 assert_eq!(editor.active_outline().origin(), [43.5, 230.4]);
8256 editor.redo(&mut engine).unwrap();
8257 assert_eq!(editor.active_outline().id, id);
8258 assert_eq!(
8259 editor.outlines.iter().map(|o| o.id).collect::<Vec<_>>(),
8260 ids
8261 );
8262 assert_eq!(
8263 editor.outlines[0]
8264 .document
8265 .paragraphs()
8266 .next()
8267 .unwrap()
8268 .text(),
8269 "before"
8270 );
8271 assert_eq!(
8272 editor.outlines[2]
8273 .document
8274 .paragraphs()
8275 .next()
8276 .unwrap()
8277 .text(),
8278 "after"
8279 );
8280 }
8281
8282 #[test]
8283 fn empty_composition_commits_safely_when_focus_or_position_changes() {
8284 let mut engine = TextEngine::default();
8285 for action in 0..5 {
8286 let mut outlines = Vec::new();
8287 for text in ["delete", "keep"] {
8288 let document =
8289 TextDocument::new(vec![Paragraph::new(text.into(), Format::default())])
8290 .unwrap();
8291 outlines.push(TextOutline::new(&mut engine, document, 240.0, [0.0; 2]).unwrap());
8292 }
8293 let first = outlines[0].id;
8294 let second = outlines[1].id;
8295 let original = outlines[0].document.clone();
8296 let mut editor =
8297 CanvasEditor::from_text_outlines(outlines, BTreeMap::new(), None).unwrap();
8298 editor.select_all().unwrap();
8299 editor.compose(&mut engine, String::new(), 0..0).unwrap();
8300 assert_eq!(editor.outlines.len(), 2);
8301 editor.cancel_composition(&mut engine).unwrap();
8302 assert_eq!(editor.active_outline().document, original);
8303 editor.compose(&mut engine, String::new(), 0..0).unwrap();
8304 match action {
8305 0 => editor.focus_outline(first).unwrap(),
8306 1 => editor.focus_outline(second).unwrap(),
8307 2 => editor.move_outline(first, [30.0, 40.0]).unwrap(),
8308 3 => editor.move_outline(second, [30.0, 40.0]).unwrap(),
8309 _ => editor.commit_text(&mut engine, String::new()).unwrap(),
8310 }
8311 assert!(editor.marked_range().is_none());
8312 assert_eq!(editor.outlines.len(), 1);
8313 assert_eq!(editor.outlines[0].id, second);
8314 assert_eq!(
8315 editor.outlines[0]
8316 .document
8317 .paragraphs()
8318 .next()
8319 .unwrap()
8320 .text(),
8321 "keep"
8322 );
8323 if action == 3 {
8324 editor.undo(&mut engine).unwrap();
8325 }
8326 editor.undo(&mut engine).unwrap();
8327 assert_eq!(editor.active_outline().id, first);
8328 assert_eq!(editor.active_outline().document, original);
8329 assert_eq!(editor.outlines.len(), 2);
8330 editor.redo(&mut engine).unwrap();
8331 assert_eq!(editor.outlines.len(), 1);
8332 }
8333 let document =
8334 TextDocument::new(vec![Paragraph::new(String::new(), Format::default())]).unwrap();
8335 let mut editor = CanvasEditor::new(&mut engine, document.clone(), 240.0).unwrap();
8336 editor.compose(&mut engine, "preedit".into(), 0..7).unwrap();
8337 editor.delete(&mut engine, true).unwrap();
8338 assert_eq!(editor.undo.len(), 2);
8339 assert!(editor.outlines.is_empty());
8340 editor.undo(&mut engine).unwrap();
8341 assert_eq!(
8342 editor
8343 .active_outline()
8344 .document
8345 .paragraphs()
8346 .next()
8347 .unwrap()
8348 .text(),
8349 "preedit"
8350 );
8351 editor.undo(&mut engine).unwrap();
8352 assert_eq!(editor.active_outline().document, document);
8353 assert!(editor.outlines.is_empty());
8354 }
8355
8356 #[test]
8357 fn space_only_caret_preserves_spacing_but_blank_paragraphs_and_metadata_are_content() {
8358 let mut engine = TextEngine::default();
8359 let document =
8360 TextDocument::new(vec![Paragraph::new(String::new(), Format::default())]).unwrap();
8361 let mut editor = CanvasEditor::new(&mut engine, document, 240.0).unwrap();
8362 editor.insert(&mut engine, " ").unwrap();
8363 assert!(editor.outlines.is_empty());
8364 assert!(editor.undo.is_empty());
8365 let spaces = editor.active_outline().document.clone();
8366 let selection = editor.selection();
8367 editor.insert(&mut engine, "x").unwrap();
8368 assert_eq!(editor.outlines.len(), 1);
8369 // A caret move ends the typing run, so the backspace is its own step.
8370 for movement in [Movement::Left, Movement::Right] {
8371 editor.move_selection(&mut engine, movement, false).unwrap();
8372 }
8373 editor.delete(&mut engine, true).unwrap();
8374 assert!(editor.outlines.is_empty());
8375 assert_eq!(editor.active_outline().document, spaces);
8376 let undo_count = editor.undo.len();
8377 editor.select_all().unwrap();
8378 assert!(editor.delete_to(&mut engine, Movement::LineStart).unwrap());
8379 assert!(editor.outlines.is_empty());
8380 assert_eq!(editor.undo.len(), undo_count);
8381 assert!(editor.undo(&mut engine).unwrap());
8382 assert_eq!(
8383 editor
8384 .active_outline()
8385 .document
8386 .paragraphs()
8387 .next()
8388 .unwrap()
8389 .text(),
8390 " x"
8391 );
8392 assert!(editor.redo(&mut engine).unwrap());
8393 assert_eq!(editor.active_outline().document, spaces);
8394 for _ in 0..2 {
8395 editor.undo(&mut engine).unwrap();
8396 }
8397 assert!(editor.outlines.is_empty());
8398 assert_eq!(editor.active_outline().document, spaces);
8399 assert_eq!(editor.selection(), selection);
8400 editor.insert(&mut engine, "\n").unwrap();
8401 assert_eq!(editor.outlines.len(), 1);
8402 assert_eq!(editor.active_outline().document.nodes().len(), 2);
8403 for text in ["\t", "\u{a0}", "\u{200b}"] {
8404 editor.select_all().unwrap();
8405 editor.insert(&mut engine, text).unwrap();
8406 assert_eq!(editor.outlines.len(), 1);
8407 }
8408 let mut source = editor.active_outline().snapshot();
8409 source.paragraphs[0].collapsed = true;
8410 let mut editor =
8411 CanvasEditor::from_outlines(&mut engine, vec![source], BTreeMap::new()).unwrap();
8412 editor.select_all().unwrap();
8413 editor.delete(&mut engine, true).unwrap();
8414 assert_eq!(editor.outlines.len(), 1);
8415 assert!(editor.active_outline().document.nodes()[0].collapsed);
8416 }
8417
8418 #[test]
8419 fn caret_placement_is_not_document_history_and_first_input_is_one_undo() {
8420 let mut engine = TextEngine::default();
8421 let original = TextDocument::new(vec![Paragraph::new(
8422 "Existing notes".into(),
8423 Format::default(),
8424 )])
8425 .unwrap();
8426 let mut editor = CanvasEditor::new(&mut engine, original.clone(), 240.0).unwrap();
8427 let existing = editor.active_outline().id;
8428 for step in 0..100 {
8429 editor
8430 .place_caret(&mut engine, [step as f32, 100.0], 120.0)
8431 .unwrap();
8432 assert_eq!(editor.outlines.len(), 1);
8433 assert!(editor.undo.is_empty());
8434 assert!(editor.redo.is_empty());
8435 }
8436 let id = editor.active_outline().id;
8437 let empty = editor.active_outline().document.clone();
8438 editor
8439 .insert(&mut engine, "annotation\nsecond line")
8440 .unwrap();
8441 let document = editor.active_outline().document.clone();
8442 assert_eq!(editor.active_outline().id, id);
8443 assert!(editor.caret_outline().is_none());
8444 assert_eq!(editor.outlines.len(), 2);
8445 assert_eq!(editor.undo.len(), 1);
8446 editor.undo(&mut engine).unwrap();
8447 assert_eq!(editor.outlines.len(), 1);
8448 assert_eq!(editor.active_outline().document, empty);
8449 assert_eq!(editor.active_outline().origin(), [99.0, 100.0]);
8450 editor
8451 .place_caret(&mut engine, [600.0, 400.0], 180.0)
8452 .unwrap();
8453 editor.insert(&mut engine, "").unwrap();
8454 assert_eq!(editor.redo.len(), 1);
8455 editor.redo(&mut engine).unwrap();
8456 assert_eq!(editor.active_outline().id, id);
8457 assert_eq!(editor.active_outline().document, document);
8458 assert_eq!(editor.active_outline().origin(), [99.0, 100.0]);
8459 editor.focus_outline(existing).unwrap();
8460 assert_eq!(editor.active_outline().document, original);
8461 }
8462
8463 #[test]
8464 fn provisional_input_preserves_the_empty_document_and_redo_until_commit() {
8465 let mut engine = TextEngine::default();
8466 let document = TextDocument::new(vec![Paragraph::new(
8467 String::new(),
8468 Format {
8469 bold: Some(true),
8470 ..Default::default()
8471 },
8472 )])
8473 .unwrap();
8474 let mut editor = CanvasEditor::new(&mut engine, document.clone(), 240.0).unwrap();
8475 assert!(editor.outlines.is_empty());
8476 editor.insert(&mut engine, "first").unwrap();
8477 editor.undo(&mut engine).unwrap();
8478 let id = editor.active_outline().id;
8479 editor.compose(&mut engine, "に".into(), 1..1).unwrap();
8480 assert!(editor.outlines.is_empty());
8481 assert_eq!(editor.redo.len(), 1);
8482 assert!(
8483 editor
8484 .place_caret(&mut engine, [f32::NAN, 0.0], 240.0)
8485 .is_err()
8486 );
8487 assert_eq!(editor.active_outline().id, id);
8488 assert!(editor.marked_range().is_some());
8489 editor.cancel_composition(&mut engine).unwrap();
8490 assert_eq!(editor.active_outline().document, document);
8491 assert_eq!(editor.redo.len(), 1);
8492 editor.compose(&mut engine, "に".into(), 1..1).unwrap();
8493 editor.commit_text(&mut engine, "日本".into()).unwrap();
8494 assert_eq!(editor.outlines.len(), 1);
8495 assert_eq!(editor.active_outline().id, id);
8496 assert_eq!(editor.undo.len(), 1);
8497 assert!(editor.redo.is_empty());
8498 editor.undo(&mut engine).unwrap();
8499 assert!(editor.outlines.is_empty());
8500 assert_eq!(editor.active_outline().document, document);
8501 }
8502
8503 fn ys(outline: &TextOutline) -> Vec<f32> {
8504 outline
8505 .shaped
8506 .paragraphs
8507 .iter()
8508 .map(|paragraph| paragraph.origin[1])
8509 .collect()
8510 }
8511
8512 fn layout_snapshot(layout: &OutlineLayout) -> serde_json::Value {
8513 serde_json::json!({
8514 "size": layout.size,
8515 "paragraphs": layout.paragraphs.iter().map(|paragraph| serde_json::json!({
8516 "id": paragraph.id, "origin": paragraph.origin,
8517 "text": paragraph.projection.text().text(),
8518 "lines": paragraph.text.lines().map(|(line, bounds)| serde_json::json!({
8519 "source": bounds.source, "top": bounds.top, "baseline": bounds.baseline,
8520 "height": bounds.height, "advance": line.metrics().advance,
8521 })).collect::<Vec<_>>(),
8522 "markers": paragraph.markers.iter().map(|(text, origin)| serde_json::json!({
8523 "origin": origin, "height": text.height(),
8524 "baseline": text.lines().next().unwrap().1.baseline,
8525 "advance": text.lines().next().unwrap().0.metrics().advance,
8526 })).collect::<Vec<_>>(),
8527 })).collect::<Vec<_>>(),
8528 })
8529 }
8530
8531 #[test]
8532 fn native_outline_omits_outer_paragraph_spacing_through_split_and_undo() {
8533 use onestore::page::{Page, PageObject};
8534 use onestore::{RevisionIndex, Store, document::Document};
8535 let store = Store::parse(include_bytes!(
8536 "../../../corpus/canvas/baseline-anchors.one"
8537 ))
8538 .unwrap();
8539 let index = RevisionIndex::parse(&store).unwrap();
8540 let page =
8541 Page::from_document(&Document::parse(&index).unwrap(), "Baseline anchors").unwrap();
8542 let outline = page
8543 .objects
8544 .into_iter()
8545 .find_map(|object| match object {
8546 PageObject::Outline(outline)
8547 if outline.paragraphs[0].text().unwrap().text.text() == "Spacing first" =>
8548 {
8549 Some(outline)
8550 }
8551 _ => None,
8552 })
8553 .unwrap();
8554 let mut engine = TextEngine::default();
8555 let mut editor =
8556 CanvasEditor::from_outlines(&mut engine, vec![outline], page.definitions).unwrap();
8557 let source = editor.active_outline().document.nodes().to_vec();
8558 for step in 0..3 {
8559 let outline = editor.active_outline();
8560 let paragraphs = &outline.shaped.paragraphs;
8561 assert_eq!(paragraphs[0].origin[1], 0.0);
8562 for pair in paragraphs.windows(2) {
8563 assert!(
8564 (pair[1].origin[1] - pair[0].origin[1] - pair[0].text.height() - 144.0).abs()
8565 < 0.0001
8566 );
8567 }
8568 let height = paragraphs.iter().map(|p| p.text.height()).sum::<f32>()
8569 + (paragraphs.len() - 1) as f32 * 144.0;
8570 assert!((outline.shaped.size[1] - height).abs() < 0.0001);
8571 match step {
8572 0 => editor.insert(&mut engine, "\n").unwrap(),
8573 1 => assert!(editor.undo(&mut engine).unwrap()),
8574 _ => assert_eq!(outline.document.nodes(), source),
8575 }
8576 }
8577 }
8578
8579 #[test]
8580 fn resize_preview_preserves_history_and_commit_restores_automatic_width_on_undo() {
8581 let mut engine = TextEngine::default();
8582 let mut editor = CanvasEditor::new(
8583 &mut engine,
8584 TextDocument::new(vec![Paragraph::new(String::new(), Format::default())]).unwrap(),
8585 468.0,
8586 )
8587 .unwrap();
8588 editor.insert(&mut engine, "One").unwrap();
8589 for movement in [Movement::Left, Movement::Right] {
8590 editor.move_selection(&mut engine, movement, false).unwrap();
8591 }
8592 let source = editor.active_outline().snapshot();
8593 let selection = editor.selection();
8594 editor.insert(&mut engine, "!").unwrap();
8595 editor.undo(&mut engine).unwrap();
8596 for width in [120.0, 360.0, 180.0] {
8597 let preview = editor.preview_resize(&mut engine, width).unwrap();
8598 assert_eq!(preview.bounds().width(), f64::from(width));
8599 assert_eq!(preview.document, editor.active_outline().document);
8600 assert_eq!(editor.active_outline().layout, source.layout);
8601 assert_eq!(editor.undo.len(), 1);
8602 assert_eq!(editor.redo.len(), 1);
8603 }
8604 assert!(editor.preview_resize(&mut engine, f32::NAN).is_err());
8605 editor.resize(&mut engine, 180.0).unwrap();
8606 assert!(editor.redo.is_empty());
8607 assert_eq!(editor.undo.len(), 2);
8608 editor.undo(&mut engine).unwrap();
8609 assert_eq!(editor.active_outline().layout, source.layout);
8610 assert_eq!(editor.active_outline().bounds().width(), 72.0);
8611 assert_eq!(editor.selection(), selection);
8612 editor.redo(&mut engine).unwrap();
8613 assert_eq!(editor.active_outline().bounds().width(), 180.0);
8614 editor.resize(&mut engine, 180.0).unwrap();
8615 assert_eq!(editor.undo.len(), 2);
8616 }
8617
8618 #[test]
8619 fn new_empty_outline_grows_shrinks_and_keeps_a_manually_set_width() {
8620 let mut engine = TextEngine::default();
8621 let mut editor = CanvasEditor::new(
8622 &mut engine,
8623 TextDocument::new(vec![Paragraph::new(String::new(), Format::default())]).unwrap(),
8624 468.0,
8625 )
8626 .unwrap();
8627 assert_eq!(editor.active_outline().bounds().width(), 72.0);
8628 assert_eq!(
8629 editor.active_outline().layout.width_set_by_user,
8630 Some(false)
8631 );
8632 editor
8633 .insert(&mut engine, "A short sentence expands the container")
8634 .unwrap();
8635 let expanded = editor.active_outline().bounds().width();
8636 assert!(expanded > 72.0 && expanded < 468.0);
8637 editor.select_all().unwrap();
8638 editor.insert(&mut engine, &"words ".repeat(80)).unwrap();
8639 assert_eq!(editor.active_outline().bounds().width(), 468.0);
8640 assert!(
8641 editor
8642 .active_outline()
8643 .paragraph_layout(0)
8644 .unwrap()
8645 .text
8646 .lines()
8647 .count()
8648 > 1
8649 );
8650 editor.select_all().unwrap();
8651 editor.insert(&mut engine, "One").unwrap();
8652 assert_eq!(editor.active_outline().bounds().width(), 72.0);
8653 editor.resize(&mut engine, 180.0).unwrap();
8654 editor.select_all().unwrap();
8655 editor.insert(&mut engine, "Tiny").unwrap();
8656 assert_eq!(editor.active_outline().bounds().width(), 180.0);
8657 editor
8658 .insert(&mut engine, &" longer words".repeat(10))
8659 .unwrap();
8660 assert_eq!(editor.active_outline().bounds().width(), 180.0);
8661 editor.undo(&mut engine).unwrap();
8662 assert_eq!(editor.active_outline().bounds().width(), 180.0);
8663 }
8664
8665 #[test]
8666 fn resizing_an_automatic_outline_sets_its_explicit_width_and_preserves_spacing_metadata() {
8667 let document = TextDocument::new(vec![Paragraph::new(
8668 "short".into(),
8669 Format {
8670 space_after: Some(7.0),
8671 ..Format::default()
8672 },
8673 )])
8674 .unwrap();
8675 let source = document.nodes().to_vec();
8676 let outline = Outline {
8677 title: false,
8678 min_width: None,
8679 id: onestore::page::text::new_id().unwrap(),
8680 layout: onestore::document::Layout {
8681 max_width: Some(220.0),
8682 reserved_width: Some(180.0),
8683 width_set_by_user: Some(false),
8684 ..Default::default()
8685 },
8686 indents: vec![18.0, 0.0],
8687 paragraphs: source.clone(),
8688 unsupported: Vec::new(),
8689 };
8690 let mut engine = TextEngine::default();
8691 let mut editor =
8692 CanvasEditor::from_outlines(&mut engine, vec![outline], BTreeMap::new()).unwrap();
8693 assert!(editor.active_outline().bounds().width() < 180.0);
8694 let height = editor
8695 .active_outline()
8696 .paragraph_layout(0)
8697 .unwrap()
8698 .text
8699 .height();
8700 assert_eq!(editor.active_outline().bounds().height(), f64::from(height));
8701 editor.resize(&mut engine, 180.0).unwrap();
8702 assert_eq!(editor.active_outline().bounds().width(), 180.0);
8703 assert_eq!(editor.active_outline().layout.reserved_width, None);
8704 assert_eq!(editor.active_outline().layout.width_set_by_user, Some(true));
8705 assert_eq!(editor.active_outline().document.nodes(), source);
8706 let cached = editor
8707 .active_outline()
8708 .paragraph_layout(0)
8709 .unwrap()
8710 .text
8711 .shaped
8712 .styles()
8713 .as_ptr();
8714 editor.resize(&mut engine, 180.0).unwrap();
8715 assert_eq!(
8716 editor
8717 .active_outline()
8718 .paragraph_layout(0)
8719 .unwrap()
8720 .text
8721 .shaped
8722 .styles()
8723 .as_ptr(),
8724 cached
8725 );
8726 }
8727
8728 #[test]
8729 fn platform_text_offsets_skip_collapsed_children() {
8730 let document = TextDocument::new(
8731 ["Collapsed", "Hidden", "Last"]
8732 .into_iter()
8733 .map(|s| Paragraph::new(s.into(), Format::default()))
8734 .collect(),
8735 )
8736 .unwrap();
8737 let mut nodes = document.nodes().to_vec();
8738 nodes[0].collapsed = true;
8739 nodes[1].parent = Some(nodes[0].id);
8740 nodes[1].level = 2;
8741 let outline = Outline {
8742 title: false,
8743 min_width: None,
8744 id: onestore::page::text::new_id().unwrap(),
8745 layout: onestore::document::Layout {
8746 max_width: Some(150.0),
8747 ..Default::default()
8748 },
8749 indents: Vec::new(),
8750 paragraphs: nodes,
8751 unsupported: Vec::new(),
8752 };
8753 let mut engine = TextEngine::default();
8754 let editor =
8755 CanvasEditor::from_outlines(&mut engine, vec![outline], BTreeMap::new()).unwrap();
8756 let outline = editor.active_outline();
8757 assert_eq!(outline.shown_text(), "Collapsed\nLast");
8758 let at = |paragraph, offset| TextPosition { paragraph, offset };
8759 for (position, offset) in [
8760 (at(0, 3), 3),
8761 (at(1, 2), 10),
8762 (at(2, 0), 10),
8763 (at(2, 4), 14),
8764 ] {
8765 assert_eq!(outline.utf16_offset(position).unwrap(), offset);
8766 }
8767 for (offset, position) in [(9, at(0, 9)), (10, at(2, 0)), (99, at(2, 4))] {
8768 assert_eq!(outline.utf16_position(offset).unwrap(), position);
8769 }
8770 }
8771
8772 #[test]
8773 fn imported_indentation_markers_and_collapsed_children_share_edit_geometry() {
8774 let document = TextDocument::new(
8775 [
8776 "First line",
8777 "Indented text with enough words to wrap",
8778 "Collapsed",
8779 "Hidden",
8780 "Last",
8781 ]
8782 .into_iter()
8783 .map(|s| Paragraph::new(s.into(), Format::default()))
8784 .collect(),
8785 )
8786 .unwrap();
8787 let mut nodes = document.nodes().to_vec();
8788 nodes[1].parent = Some(nodes[0].id);
8789 nodes[1].level = 2;
8790 nodes[2].parent = Some(nodes[0].id);
8791 nodes[2].level = 2;
8792 nodes[2].collapsed = true;
8793 nodes[3].parent = Some(nodes[2].id);
8794 nodes[3].level = 3;
8795 let marker = onestore::page::text::new_id().unwrap();
8796 nodes[1].lists.push(marker);
8797 let definitions = BTreeMap::from([(
8798 marker,
8799 Definition {
8800 kind: onestore::document::Kind::List {
8801 font: Some("Courier New".into()),
8802 format: Some("○".into()),
8803 restart: None,
8804 bullet: Some(4),
8805 },
8806 format: Format {
8807 font_size: Some(22.0),
8808 ..Format::default()
8809 },
8810 },
8811 )]);
8812 let outline = Outline {
8813 title: false,
8814 min_width: None,
8815 id: onestore::page::text::new_id().unwrap(),
8816 layout: onestore::document::Layout {
8817 x: Some(30.0),
8818 y: Some(45.0),
8819 max_width: Some(150.0),
8820 width_set_by_user: Some(true),
8821 ..Default::default()
8822 },
8823 indents: vec![18.0, 0.0, 27.0],
8824 paragraphs: nodes.clone(),
8825 unsupported: Vec::new(),
8826 };
8827 let mut engine = TextEngine::default();
8828 let expected = layout_snapshot(&outline.layout(&mut engine, &definitions).unwrap());
8829 let id = outline.id;
8830 let mut editor =
8831 CanvasEditor::from_outlines(&mut engine, vec![outline], definitions).unwrap();
8832 assert_eq!(editor.active_outline().id, id);
8833 assert_eq!(editor.active_outline().origin(), [30.0, 45.0]);
8834 assert_eq!(
8835 editor
8836 .active_outline()
8837 .layouts()
8838 .map(|(i, _)| i)
8839 .collect::<Vec<_>>(),
8840 [0, 1, 2, 4]
8841 );
8842 assert_eq!(layout_snapshot(&editor.active_outline().shaped), expected);
8843 let second = editor.active_outline().paragraph_layout(1).unwrap();
8844 assert_eq!(second.origin[0], 27.0);
8845 let y = second.origin[1] + second.text.lines().next().unwrap().1.height * 0.5;
8846 editor.select_at(27.0, y, false).unwrap();
8847 assert_eq!(
8848 editor.selection().positions[1],
8849 TextPosition {
8850 paragraph: 1,
8851 offset: 0
8852 }
8853 );
8854 assert_eq!(editor.caret(1.0).unwrap().x0, 27.0);
8855 let retained = editor
8856 .active_outline()
8857 .paragraph_layout(4)
8858 .unwrap()
8859 .text
8860 .shaped
8861 .styles()
8862 .as_ptr();
8863 editor.insert(&mut engine, "More words ").unwrap();
8864 assert_eq!(
8865 editor
8866 .active_outline()
8867 .paragraph_layout(4)
8868 .unwrap()
8869 .text
8870 .shaped
8871 .styles()
8872 .as_ptr(),
8873 retained
8874 );
8875 let rebuilt = Outline {
8876 id,
8877 title: false,
8878 min_width: None,
8879 layout: editor.active_outline().layout.clone(),
8880 indents: vec![18.0, 0.0, 27.0],
8881 paragraphs: editor.active_outline().document.nodes().to_vec(),
8882 unsupported: Vec::new(),
8883 }
8884 .layout(&mut engine, &editor.definitions)
8885 .unwrap();
8886 assert_eq!(
8887 layout_snapshot(&editor.active_outline().shaped),
8888 layout_snapshot(&rebuilt)
8889 );
8890 let edited = layout_snapshot(&editor.active_outline().shaped);
8891 let before = editor.active_outline().document.clone();
8892 let selection = editor.selection();
8893 editor.enter(&mut engine, false).unwrap();
8894 let split = &editor.active_outline().document.nodes()[2];
8895 assert_eq!((split.parent, split.level), (Some(nodes[0].id), 2));
8896 assert_ne!(split.lists, [marker]);
8897 assert_eq!(
8898 editor.definitions[&split.lists[0]],
8899 editor.definitions[&marker]
8900 );
8901 editor.undo(&mut engine).unwrap();
8902 assert_eq!(editor.active_outline().document, before);
8903 assert!(editor.resize(&mut engine, 20.0).is_err());
8904 assert_eq!(editor.active_outline().document, before);
8905 assert_eq!(editor.selection(), selection);
8906 assert_eq!(layout_snapshot(&editor.active_outline().shaped), edited);
8907 editor.undo(&mut engine).unwrap();
8908 assert_eq!(editor.active_outline().document.nodes(), nodes);
8909 assert_eq!(layout_snapshot(&editor.active_outline().shaped), expected);
8910 editor.redo(&mut engine).unwrap();
8911 assert_eq!(layout_snapshot(&editor.active_outline().shaped), edited);
8912 assert!(
8913 editor
8914 .select(
8915 [TextPosition {
8916 paragraph: 3,
8917 offset: 0
8918 }; 2]
8919 .into()
8920 )
8921 .is_err()
8922 );
8923 editor
8924 .select(
8925 [TextPosition {
8926 paragraph: 2,
8927 offset: 9,
8928 }; 2]
8929 .into(),
8930 )
8931 .unwrap();
8932 editor
8933 .move_selection(&mut engine, Movement::Right, false)
8934 .unwrap();
8935 assert_eq!(editor.selection().positions[1].paragraph, 4);
8936 editor
8937 .move_selection(&mut engine, Movement::Left, false)
8938 .unwrap();
8939 assert_eq!(editor.selection().positions[1].paragraph, 2);
8940 editor
8941 .move_selection(&mut engine, Movement::Down, false)
8942 .unwrap();
8943 assert_eq!(editor.selection().positions[1].paragraph, 4);
8944 editor
8945 .move_selection(&mut engine, Movement::Up, false)
8946 .unwrap();
8947 assert_eq!(editor.selection().positions[1].paragraph, 2);
8948 editor.select_all().unwrap();
8949 assert!(!editor.selection_rects().unwrap().is_empty());
8950 }
8951
8952 #[test]
8953 fn joins_pass_over_hidden_children_and_a_split_hands_them_on_still_collapsed() {
8954 let mut engine = TextEngine::default();
8955 let mut nodes = TextDocument::new(
8956 ["Collapsed", "Hidden", "After"]
8957 .into_iter()
8958 .map(|text| Paragraph::new(text.into(), Format::default()))
8959 .collect(),
8960 )
8961 .unwrap()
8962 .nodes()
8963 .to_vec();
8964 nodes[0].collapsed = true;
8965 nodes[1].parent = Some(nodes[0].id);
8966 nodes[1].level = 2;
8967 let outline = Outline {
8968 paragraphs: nodes.clone(),
8969 ..TextOutline::new(
8970 &mut engine,
8971 TextDocument::from_nodes(nodes.clone()).unwrap(),
8972 300.0,
8973 [0.0; 2],
8974 )
8975 .unwrap()
8976 .snapshot()
8977 };
8978 let mut editor =
8979 CanvasEditor::from_outlines(&mut engine, vec![outline], BTreeMap::new()).unwrap();
8980 let texts = |editor: &CanvasEditor| {
8981 editor
8982 .active_outline()
8983 .document
8984 .paragraphs()
8985 .map(|text| text.text().to_owned())
8986 .collect::<Vec<_>>()
8987 };
8988 let at = |paragraph, offset| [TextPosition { paragraph, offset }; 2].into();
8989 editor.select(at(2, 0)).unwrap();
8990 assert!(editor.delete(&mut engine, true).unwrap());
8991 assert_eq!(texts(&editor), ["CollapsedAfter", "Hidden"]);
8992 assert_eq!(
8993 editor.selection().positions,
8994 [TextPosition {
8995 paragraph: 0,
8996 offset: 9
8997 }; 2]
8998 );
8999 editor.undo(&mut engine).unwrap();
9000 editor.select(at(0, 9)).unwrap();
9001 assert!(editor.delete(&mut engine, false).unwrap());
9002 assert_eq!(texts(&editor), ["CollapsedAfter", "Hidden"]);
9003 editor.undo(&mut engine).unwrap();
9004 editor.select(at(0, 9)).unwrap();
9005 editor.enter(&mut engine, false).unwrap();
9006 let split = editor.active_outline().document.nodes();
9007 assert!(!split[0].collapsed && split[1].collapsed);
9008 assert_eq!(split[2].parent, Some(split[1].id));
9009 assert_eq!(
9010 editor
9011 .active_outline()
9012 .layouts()
9013 .map(|(index, _)| index)
9014 .collect::<Vec<_>>(),
9015 [0, 1, 3]
9016 );
9017 editor.undo(&mut engine).unwrap();
9018 assert_eq!(editor.active_outline().document.nodes(), nodes);
9019 }
9020
9021 #[test]
9022 #[ignore = "requires CANVAS_TEST_SECTION and CANVAS_TEST_PAGE private fixture inputs"]
9023 fn imported_editor_reflows_and_restores_native_outline_geometry() {
9024 use onestore::page::{Page, PageObject};
9025 use onestore::{RevisionIndex, Store, document::Document};
9026 let bytes = std::fs::read(std::env::var_os("CANVAS_TEST_SECTION").unwrap()).unwrap();
9027 let page = {
9028 let store = Store::parse(&bytes).unwrap();
9029 let index = RevisionIndex::parse(&store).unwrap();
9030 Page::from_document(
9031 &Document::parse(&index).unwrap(),
9032 &std::env::var("CANVAS_TEST_PAGE").unwrap(),
9033 )
9034 .unwrap()
9035 };
9036 drop(bytes);
9037 let mut engine = TextEngine::default();
9038 if let Some(font) = std::env::var_os("CANVAS_TEST_SUBSTITUTE") {
9039 engine
9040 .register_substitute(parley::fontique::Blob::new(std::sync::Arc::new(
9041 std::fs::read(font).unwrap(),
9042 )))
9043 .unwrap();
9044 }
9045 let outlines: Vec<_> = page
9046 .objects
9047 .into_iter()
9048 .filter_map(|object| match object {
9049 PageObject::Outline(outline) => Some(outline),
9050 _ => None,
9051 })
9052 .collect();
9053 let original: Vec<_> = outlines
9054 .iter()
9055 .map(|outline| {
9056 (
9057 outline.id,
9058 outline.layout.clone(),
9059 outline.paragraphs.clone(),
9060 layout_snapshot(&outline.layout(&mut engine, &page.definitions).unwrap()),
9061 )
9062 })
9063 .collect();
9064 let mut editor =
9065 CanvasEditor::from_outlines(&mut engine, outlines, page.definitions).unwrap();
9066 let mut count = 0;
9067 for (id, source_layout, nodes, expected) in original {
9068 editor.focus_outline(id).unwrap();
9069 assert_eq!(editor.active_outline().layout, source_layout);
9070 assert_eq!(layout_snapshot(&editor.active_outline().shaped), expected);
9071 let visible: Vec<_> = editor.active_outline().layouts().map(|(i, _)| i).collect();
9072 for paragraph in visible {
9073 let text = &nodes[paragraph].text().unwrap().text;
9074 let byte = text
9075 .text()
9076 .char_indices()
9077 .nth(text.text().chars().count() / 2)
9078 .map(|(byte, _)| byte)
9079 .unwrap_or(0);
9080 let position = TextPosition {
9081 paragraph,
9082 offset: text.utf16_offset(byte).unwrap(),
9083 };
9084 editor.select([position; 2].into()).unwrap();
9085 editor.insert(&mut engine, "probe ").unwrap();
9086 let edited = editor.active_outline().document.clone();
9087 let rebuilt = Outline {
9088 id,
9089 title: false,
9090 min_width: None,
9091 layout: source_layout.clone(),
9092 indents: editor.active_outline().indents.clone(),
9093 paragraphs: edited.nodes().to_vec(),
9094 unsupported: Vec::new(),
9095 }
9096 .layout(&mut engine, &editor.definitions)
9097 .unwrap();
9098 let after = layout_snapshot(&editor.active_outline().shaped);
9099 assert_eq!(after, layout_snapshot(&rebuilt));
9100 editor.undo(&mut engine).unwrap();
9101 assert_eq!(editor.active_outline().document.nodes(), nodes);
9102 assert_eq!(layout_snapshot(&editor.active_outline().shaped), expected);
9103 editor.redo(&mut engine).unwrap();
9104 assert_eq!(editor.active_outline().document, edited);
9105 assert_eq!(layout_snapshot(&editor.active_outline().shaped), after);
9106 editor.undo(&mut engine).unwrap();
9107 count += 1;
9108 }
9109 }
9110 eprintln!(
9111 "Reflowed and restored {count} visible paragraphs in {} native outlines",
9112 editor.outlines().len()
9113 );
9114 assert!(count > 0);
9115 }
9116
9117 #[test]
9118 fn visual_navigation_does_not_bounce_between_opposite_direction_paragraphs() {
9119 let mut engine = TextEngine::default();
9120 let document = TextDocument::new(
9121 ["a", "שלום", "end"]
9122 .into_iter()
9123 .map(|text| Paragraph::new(text.into(), Format::default()))
9124 .collect(),
9125 )
9126 .unwrap();
9127 let mut editor = CanvasEditor::new(&mut engine, document, 100.0).unwrap();
9128 let first = TextPosition {
9129 paragraph: 0,
9130 offset: 0,
9131 };
9132 let last = TextPosition {
9133 paragraph: 2,
9134 offset: 3,
9135 };
9136 for _ in 0..32 {
9137 editor
9138 .move_selection(&mut engine, Movement::Right, false)
9139 .unwrap();
9140 if editor.selection().positions[1] == last {
9141 break;
9142 }
9143 }
9144 assert_eq!(editor.selection().positions[1], last);
9145 for _ in 0..32 {
9146 editor
9147 .move_selection(&mut engine, Movement::Left, false)
9148 .unwrap();
9149 if editor.selection().positions[1] == first {
9150 break;
9151 }
9152 }
9153 assert_eq!(editor.selection().positions[1], first);
9154 }
9155
9156 #[test]
9157 fn styled_reversed_selection_survives_reflow_undo_and_navigation_reuses_layout() {
9158 let mut engine = TextEngine::default();
9159 let original = TextDocument::new(vec![Paragraph::from_runs([
9160 ("plain ".into(), Format::default()),
9161 (
9162 "bold text".into(),
9163 Format {
9164 bold: Some(true),
9165 ..Format::default()
9166 },
9167 ),
9168 ])])
9169 .unwrap();
9170 let mut editor = CanvasEditor::new(&mut engine, original.clone(), 100.0).unwrap();
9171 let anchor = TextPosition {
9172 paragraph: 0,
9173 offset: 10,
9174 };
9175 let focus = TextPosition {
9176 paragraph: 0,
9177 offset: 2,
9178 };
9179 editor.select([anchor, focus].into()).unwrap();
9180 editor.insert(&mut engine, "🌳").unwrap();
9181 let after = editor.active_outline().document.clone();
9182 editor.resize(&mut engine, 35.0).unwrap();
9183 editor.undo(&mut engine).unwrap();
9184 assert_eq!(editor.active_outline().document, after);
9185 assert_eq!(editor.active_outline().layout.max_width, Some(100.0));
9186 editor.undo(&mut engine).unwrap();
9187 assert_eq!(editor.active_outline().document, original);
9188 assert_eq!(editor.selection().positions, [anchor, focus]);
9189 editor.redo(&mut engine).unwrap();
9190 assert_eq!(editor.active_outline().document, after);
9191 assert_eq!(editor.selection().positions[1].offset, 4);
9192 editor.redo(&mut engine).unwrap();
9193 assert_eq!(editor.active_outline().layout.max_width, Some(35.0));
9194 let styles = editor.active_outline().shaped.paragraphs[0]
9195 .text
9196 .shaped
9197 .styles()
9198 .as_ptr();
9199 for movement in [
9200 Movement::Left,
9201 Movement::WordRight,
9202 Movement::Down,
9203 Movement::Up,
9204 Movement::LineStart,
9205 Movement::LineEnd,
9206 ] {
9207 editor.move_selection(&mut engine, movement, false).unwrap();
9208 assert_eq!(
9209 editor.active_outline().shaped.paragraphs[0]
9210 .text
9211 .shaped
9212 .styles()
9213 .as_ptr(),
9214 styles
9215 );
9216 }
9217 editor.resize(&mut engine, 35.0).unwrap();
9218 editor.select_at(15.0, 20.0, true).unwrap();
9219 assert_eq!(
9220 editor.active_outline().shaped.paragraphs[0]
9221 .text
9222 .shaped
9223 .styles()
9224 .as_ptr(),
9225 styles
9226 );
9227 }
9228
9229 #[test]
9230 fn multiline_composition_is_provisional_and_commits_as_one_structural_edit() {
9231 let mut engine = TextEngine::default();
9232 let original = TextDocument::new(vec![
9233 Paragraph::new("abcd".into(), Format::default()),
9234 Paragraph::new(
9235 "middle".into(),
9236 Format {
9237 bold: Some(true),
9238 ..Format::default()
9239 },
9240 ),
9241 Paragraph::new("tail".into(), Format::default()),
9242 Paragraph::new("untouched".into(), Format::default()),
9243 ])
9244 .unwrap();
9245 let mut editor = CanvasEditor::new(&mut engine, original.clone(), 70.0).unwrap();
9246 let untouched = editor.active_outline().shaped.paragraphs[3]
9247 .text
9248 .shaped
9249 .styles()
9250 .as_ptr();
9251 let start = TextPosition {
9252 paragraph: 0,
9253 offset: 1,
9254 };
9255 let end = TextPosition {
9256 paragraph: 2,
9257 offset: 2,
9258 };
9259 editor.select([end, start].into()).unwrap();
9260 editor.compose(&mut engine, "ni\nhao".into(), 2..3).unwrap();
9261 assert_eq!(
9262 editor.selection().positions,
9263 [
9264 TextPosition {
9265 paragraph: 0,
9266 offset: 3
9267 },
9268 TextPosition {
9269 paragraph: 1,
9270 offset: 0
9271 }
9272 ]
9273 );
9274 assert_eq!(
9275 editor.marked_range(),
9276 Some(
9277 start..TextPosition {
9278 paragraph: 1,
9279 offset: 3
9280 }
9281 )
9282 );
9283 editor
9284 .compose(&mut engine, "e\u{301}".into(), 1..1)
9285 .unwrap();
9286 assert_eq!(
9287 editor.selection().positions,
9288 [TextPosition {
9289 paragraph: 0,
9290 offset: 2
9291 }; 2]
9292 );
9293 editor.commit_text(&mut engine, "日\n本".into()).unwrap();
9294 assert_eq!(
9295 editor
9296 .active_outline()
9297 .document
9298 .paragraphs()
9299 .map(Paragraph::text)
9300 .collect::<Vec<_>>(),
9301 ["a日", "本il", "untouched"]
9302 );
9303 assert!(editor.marked_range().is_none());
9304 assert_eq!(
9305 editor.active_outline().shaped.paragraphs[2]
9306 .text
9307 .shaped
9308 .styles()
9309 .as_ptr(),
9310 untouched
9311 );
9312 editor.undo(&mut engine).unwrap();
9313 assert_eq!(editor.active_outline().document, original);
9314 assert_eq!(editor.selection().positions, [end, start]);
9315 assert!(!editor.undo(&mut engine).unwrap());
9316 editor.redo(&mut engine).unwrap();
9317 assert_eq!(
9318 editor
9319 .active_outline()
9320 .document
9321 .paragraphs()
9322 .nth(1)
9323 .unwrap()
9324 .text(),
9325 "本il"
9326 );
9327 }
9328
9329 #[test]
9330 fn composition_failure_and_cancel_preserve_source_selection_and_redo() {
9331 let mut engine = TextEngine::default();
9332 let original = TextDocument::new(vec![
9333 Paragraph::new("first".into(), Format::default()),
9334 Paragraph::new("last".into(), Format::default()),
9335 ])
9336 .unwrap();
9337 let mut editor = CanvasEditor::new(&mut engine, original.clone(), 70.0).unwrap();
9338 editor.insert(&mut engine, "!").unwrap();
9339 editor.undo(&mut engine).unwrap();
9340 let first = TextPosition {
9341 paragraph: 0,
9342 offset: 0,
9343 };
9344 let last = TextPosition {
9345 paragraph: 1,
9346 offset: 4,
9347 };
9348 editor.select([last, first].into()).unwrap();
9349 editor
9350 .compose(&mut engine, "pre\nedit".into(), 8..8)
9351 .unwrap();
9352 let provisional = editor.active_outline().document.clone();
9353 let selection = editor.selection().positions;
9354 let marked = editor.marked_range();
9355 assert!(editor.compose(&mut engine, "🌳".into(), 1..1).is_err());
9356 assert_eq!(editor.active_outline().document, provisional);
9357 assert_eq!(editor.selection().positions, selection);
9358 assert_eq!(editor.marked_range(), marked);
9359 let preview = editor.preview_resize(&mut engine, 25.0).unwrap();
9360 assert_eq!(preview.document, provisional);
9361 assert_eq!(preview.layout.max_width, Some(25.0));
9362 assert_eq!(editor.active_outline().layout.max_width, Some(70.0));
9363 editor.cancel_composition(&mut engine).unwrap();
9364 assert_eq!(editor.active_outline().document, original);
9365 assert_eq!(editor.selection().positions, [last, first]);
9366 editor.redo(&mut engine).unwrap();
9367 assert_eq!(
9368 editor
9369 .active_outline()
9370 .document
9371 .paragraphs()
9372 .next()
9373 .unwrap()
9374 .text(),
9375 "!first"
9376 );
9377 }
9378
9379 #[test]
9380 fn navigation_crosses_paragraphs_and_keeps_the_preferred_column() {
9381 let mut engine = TextEngine::default();
9382 let document = TextDocument::new(vec![
9383 Paragraph::new("abcdefghij".into(), Format::default()),
9384 Paragraph::new(String::new(), Format::default()),
9385 Paragraph::new("abcdefghij".into(), Format::default()),
9386 ])
9387 .unwrap();
9388 let mut editor = CanvasEditor::new(&mut engine, document, 200.0).unwrap();
9389 let start = TextPosition {
9390 paragraph: 0,
9391 offset: 8,
9392 };
9393 editor.select([start, start].into()).unwrap();
9394 let x = editor.caret(1.0).unwrap().x0;
9395 editor
9396 .move_selection(&mut engine, Movement::Down, false)
9397 .unwrap();
9398 assert_eq!(
9399 editor.selection().positions[1],
9400 TextPosition {
9401 paragraph: 1,
9402 offset: 0
9403 }
9404 );
9405 editor
9406 .move_selection(&mut engine, Movement::Down, false)
9407 .unwrap();
9408 assert_eq!(
9409 editor.selection().positions[1],
9410 TextPosition {
9411 paragraph: 2,
9412 offset: 8
9413 }
9414 );
9415 assert_eq!(editor.caret(1.0).unwrap().x0, x);
9416 editor
9417 .move_selection(&mut engine, Movement::Up, true)
9418 .unwrap();
9419 editor
9420 .move_selection(&mut engine, Movement::Up, true)
9421 .unwrap();
9422 assert_eq!(
9423 editor.selection().positions,
9424 [
9425 TextPosition {
9426 paragraph: 2,
9427 offset: 8
9428 },
9429 start
9430 ]
9431 );
9432 editor
9433 .move_selection(&mut engine, Movement::Left, false)
9434 .unwrap();
9435 assert_eq!(editor.selection().positions, [start; 2]);
9436 let boundary = TextPosition {
9437 paragraph: 1,
9438 offset: 0,
9439 };
9440 editor.select([boundary, boundary].into()).unwrap();
9441 editor
9442 .move_selection(&mut engine, Movement::Left, false)
9443 .unwrap();
9444 assert_eq!(
9445 editor.selection().positions[1],
9446 TextPosition {
9447 paragraph: 0,
9448 offset: 10
9449 }
9450 );
9451 editor
9452 .move_selection(&mut engine, Movement::Right, false)
9453 .unwrap();
9454 assert_eq!(editor.selection().positions[1], boundary);
9455 }
9456
9457 #[test]
9458 fn typing_enter_and_boundary_delete_form_one_reversible_history() {
9459 let mut engine = TextEngine::default();
9460 let original =
9461 TextDocument::new(vec![Paragraph::new(String::new(), Format::default())]).unwrap();
9462 let mut editor = CanvasEditor::new(&mut engine, original.clone(), 80.0).unwrap();
9463 assert!(!editor.delete(&mut engine, true).unwrap());
9464 assert!(!editor.delete(&mut engine, false).unwrap());
9465 editor.insert(&mut engine, "left\n\nright").unwrap();
9466 assert_eq!(
9467 editor
9468 .active_outline()
9469 .document
9470 .paragraphs()
9471 .map(Paragraph::text)
9472 .collect::<Vec<_>>(),
9473 ["left", "", "right"]
9474 );
9475 let point = TextPosition {
9476 paragraph: 2,
9477 offset: 0,
9478 };
9479 editor.select([point, point].into()).unwrap();
9480 editor.delete(&mut engine, true).unwrap();
9481 assert_eq!(
9482 editor
9483 .active_outline()
9484 .document
9485 .paragraphs()
9486 .map(Paragraph::text)
9487 .collect::<Vec<_>>(),
9488 ["left", "right"]
9489 );
9490 let point = TextPosition {
9491 paragraph: 0,
9492 offset: 4,
9493 };
9494 editor.select([point, point].into()).unwrap();
9495 editor.delete(&mut engine, false).unwrap();
9496 assert_eq!(
9497 editor
9498 .active_outline()
9499 .document
9500 .paragraphs()
9501 .next()
9502 .unwrap()
9503 .text(),
9504 "leftright"
9505 );
9506 for _ in 0..3 {
9507 assert!(editor.undo(&mut engine).unwrap());
9508 }
9509 assert_eq!(editor.active_outline().document, original);
9510 for _ in 0..3 {
9511 assert!(editor.redo(&mut engine).unwrap());
9512 }
9513 assert_eq!(
9514 editor
9515 .active_outline()
9516 .document
9517 .paragraphs()
9518 .next()
9519 .unwrap()
9520 .text(),
9521 "leftright"
9522 );
9523 }
9524
9525 #[test]
9526 fn grapheme_deletion_preserves_adjacent_hidden_fields() {
9527 let mut engine = TextEngine::default();
9528 for grapheme in ["e\u{301}", "👩🏽‍💻", "🇨🇦", "👩‍👩‍👧‍👦"] {
9529 let original = TextDocument::new(vec![Paragraph::from_runs([
9530 (
9531 "field".into(),
9532 Format {
9533 hidden: Some(true),
9534 ..Format::default()
9535 },
9536 ),
9537 (format!("a{grapheme}z"), Format::default()),
9538 ])])
9539 .unwrap();
9540 let mut editor = CanvasEditor::new(&mut engine, original.clone(), 80.0).unwrap();
9541 for backward in [false, true] {
9542 let offset = 6 + if backward {
9543 grapheme.encode_utf16().count() as u32
9544 } else {
9545 0
9546 };
9547 let point = TextPosition {
9548 paragraph: 0,
9549 offset,
9550 };
9551 editor.select([point, point].into()).unwrap();
9552 editor.delete(&mut engine, backward).unwrap();
9553 assert_eq!(
9554 editor
9555 .active_outline()
9556 .document
9557 .paragraphs()
9558 .next()
9559 .unwrap()
9560 .text(),
9561 "fieldaz"
9562 );
9563 assert_eq!(
9564 editor.active_outline().shaped.paragraphs[0]
9565 .projection
9566 .text()
9567 .text(),
9568 "az"
9569 );
9570 editor.undo(&mut engine).unwrap();
9571 assert_eq!(editor.active_outline().document, original);
9572 assert_eq!(editor.selection().positions, [point; 2]);
9573 }
9574 }
9575 }
9576
9577 #[test]
9578 fn pointer_selection_maps_hidden_source_and_paragraph_boundaries() {
9579 let mut engine = TextEngine::default();
9580 let text = TextDocument::new(vec![
9581 Paragraph::from_runs([
9582 ("first ".into(), Format::default()),
9583 (
9584 "hidden🌳".into(),
9585 Format {
9586 hidden: Some(true),
9587 ..Format::default()
9588 },
9589 ),
9590 ("visible words wrap here".into(), Format::default()),
9591 ]),
9592 Paragraph::new(String::new(), Format::default()),
9593 Paragraph::new("last".into(), Format::default()),
9594 ])
9595 .unwrap();
9596 let mut editor = CanvasEditor::new(&mut engine, text.clone(), 70.0).unwrap();
9597 let last = TextPosition {
9598 paragraph: 2,
9599 offset: 4,
9600 };
9601 editor.select([last, last].into()).unwrap();
9602 let caret = editor.caret(1.0).unwrap();
9603 editor
9604 .select_at(caret.x0 as f32, ((caret.y0 + caret.y1) / 2.0) as f32, false)
9605 .unwrap();
9606 assert_eq!(editor.selection().positions, [last; 2]);
9607 let first = TextPosition {
9608 paragraph: 0,
9609 offset: 0,
9610 };
9611 editor.select([last, first].into()).unwrap();
9612 let rectangles = editor.selection_rects().unwrap();
9613 assert!(
9614 rectangles
9615 .iter()
9616 .any(|r| r.y0 == f64::from(ys(editor.active_outline())[1]) && r.width() == 4.0)
9617 );
9618 editor
9619 .replace(
9620 &mut engine,
9621 vec![Paragraph::new("replacement".into(), Format::default())],
9622 )
9623 .unwrap();
9624 editor.undo(&mut engine).unwrap();
9625 assert_eq!(editor.active_outline().document, text);
9626 assert_eq!(editor.selection().positions, [last, first]);
9627
9628 let source = &editor.active_outline().shaped.paragraphs[0];
9629 let cursor = source.text.cursor(6, Affinity::Downstream);
9630 let caret = source.text.caret(cursor, 1.0);
9631 editor
9632 .select_at(caret.x0 as f32, ((caret.y0 + caret.y1) / 2.0) as f32, false)
9633 .unwrap();
9634 let selected = editor.selection().positions[1];
9635 assert!(selected.offset == 6 || selected.offset == 14);
9636 assert_eq!(editor.caret(1.0).unwrap().y0, caret.y0);
9637 }
9638
9639 #[test]
9640 fn mixed_structural_edit_history_matches_full_layout_recomputation() {
9641 let mut engine = TextEngine::default();
9642 let text = TextDocument::new(vec![
9643 Paragraph::new("one two three".into(), Format::default()),
9644 Paragraph::new("日本 🌳 e\u{301}".into(), Format::default()),
9645 ])
9646 .unwrap();
9647 let mut editor = CanvasEditor::new(&mut engine, text, 72.0).unwrap();
9648 let mut seed = 0x8d45_u64;
9649 let mut next = || {
9650 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
9651 seed as usize
9652 };
9653 let mut before = Vec::new();
9654 let mut after = Vec::new();
9655 for step in 0..64 {
9656 let positions: Vec<_> = editor
9657 .active_outline()
9658 .document
9659 .paragraphs()
9660 .enumerate()
9661 .flat_map(|(paragraph, text)| {
9662 (0..=text.text().len()).filter_map(move |byte| {
9663 text.utf16_offset(byte)
9664 .ok()
9665 .map(|offset| TextPosition { paragraph, offset })
9666 })
9667 })
9668 .collect();
9669 let anchor = positions[next() % positions.len()];
9670 let focus = positions[next() % positions.len()];
9671 editor.select([anchor, focus].into()).unwrap();
9672 before.push((
9673 editor.active_outline().document.clone(),
9674 editor.selection().positions,
9675 ));
9676 let format = Format {
9677 bold: Some(step % 2 == 0),
9678 font_size: Some(10.0 + (step % 4) as f32),
9679 space_before: Some((step % 3) as f32),
9680 space_after: Some((step % 5) as f32),
9681 line_spacing: Some(12.0 + (step % 2) as f32),
9682 ..Format::default()
9683 };
9684 let replacement = match step % 4 {
9685 0 => vec![Paragraph::new(String::new(), format); 2],
9686 1 => vec![Paragraph::new("x👩🏽‍💻".into(), format)],
9687 2 => vec![Paragraph::new(String::new(), format)],
9688 _ => vec![
9689 Paragraph::new("wrapped words".into(), format.clone()),
9690 Paragraph::new("tail".into(), format),
9691 ],
9692 };
9693 editor.replace(&mut engine, replacement).unwrap();
9694 after.push(editor.active_outline().document.clone());
9695 let rebuilt =
9696 CanvasEditor::new(&mut engine, editor.active_outline().document.clone(), 72.0)
9697 .unwrap();
9698 assert_eq!(
9699 ys(editor.active_outline()),
9700 ys(rebuilt.active_outline()),
9701 "step {step}"
9702 );
9703 for (actual, expected) in editor
9704 .active_outline()
9705 .shaped
9706 .paragraphs
9707 .iter()
9708 .zip(&rebuilt.active_outline().shaped.paragraphs)
9709 {
9710 let geometry = |p: &ParagraphLayout| {
9711 p.text
9712 .lines()
9713 .map(|(line, b)| {
9714 (
9715 b.source.clone(),
9716 b.top,
9717 b.baseline,
9718 b.height,
9719 line.metrics().advance,
9720 )
9721 })
9722 .collect::<Vec<_>>()
9723 };
9724 assert_eq!(geometry(actual), geometry(expected), "step {step}");
9725 }
9726 }
9727 for (text, selection) in before.into_iter().rev() {
9728 assert!(editor.undo(&mut engine).unwrap());
9729 assert_eq!(editor.active_outline().document, text);
9730 assert_eq!(editor.selection().positions, selection);
9731 }
9732 assert!(!editor.undo(&mut engine).unwrap());
9733 for text in after {
9734 assert!(editor.redo(&mut engine).unwrap());
9735 assert_eq!(editor.active_outline().document, text);
9736 }
9737 assert!(!editor.redo(&mut engine).unwrap());
9738 }
9739
9740 #[test]
9741 fn typing_in_place_matches_a_fresh_layout_for_automatic_and_fixed_widths() {
9742 use onestore::page::{Image, ParagraphContent, text::new_id};
9743 let mut engine = TextEngine::default();
9744 let mut seed = 0x51ed_u64;
9745 let mut next = |n: usize| {
9746 seed = seed
9747 .wrapping_mul(6364136223846793005)
9748 .wrapping_add(1442695040888963407);
9749 (seed >> 33) as usize % n
9750 };
9751 for (text, objects) in [
9752 (vec!["x"], false),
9753 (vec!["one two", "three", "four five six seven"], false),
9754 (vec!["x"], true),
9755 (vec!["one two", "three", "four five six seven"], true),
9756 ] {
9757 let fixed = text.len() > 1;
9758 let empty = TextDocument::new(vec![Paragraph::new(String::new(), Format::default())]);
9759 let mut editor = CanvasEditor::new(&mut engine, empty.unwrap(), 120.0).unwrap();
9760 editor.insert(&mut engine, &text.join("\n")).unwrap();
9761 if fixed {
9762 editor.resize(&mut engine, 120.0).unwrap();
9763 }
9764 let table = table_editor(&mut engine).active_outline().document.nodes()[0].clone();
9765 let mut picture = table.clone();
9766 picture.id = new_id().unwrap();
9767 picture.content = ParagraphContent::Image(Image {
9768 id: new_id().unwrap(),
9769 layout: onestore::document::Layout {
9770 max_width: Some(90.0),
9771 max_height: Some(40.0),
9772 ..Default::default()
9773 },
9774 size: None,
9775 bytes: None,
9776 display: None,
9777 alt: None,
9778 background: false,
9779 printout: None,
9780 tags: Vec::new(),
9781 link: None,
9782 text: None,
9783 });
9784 if objects {
9785 let selection = [TextPosition {
9786 paragraph: 0,
9787 offset: 0,
9788 }; 2]
9789 .into();
9790 editor
9791 .commit(
9792 &mut engine,
9793 DocumentEdit {
9794 columns: BTreeMap::new(),
9795 container: None,
9796 range: 1..1,
9797 replacement: vec![picture.clone(), table],
9798 },
9799 selection,
9800 )
9801 .unwrap();
9802 }
9803 for step in 0..300 {
9804 let outline = editor.active_outline();
9805 let paragraph = next(outline.document.paragraphs().count());
9806 let text = outline.document.paragraph(paragraph).unwrap();
9807 let offset = text.utf16_offset(text.text().len()).unwrap();
9808 let position = TextPosition {
9809 paragraph,
9810 offset: next(offset as usize + 1) as u32,
9811 };
9812 // Paragraphs under a collapsed one hold no caret.
9813 let _ = editor.select([position; 2].into());
9814 let nodes = editor.active_outline().document.nodes();
9815 let root = next(nodes.len());
9816 let mut node = nodes[root].clone();
9817 let edit = |range, replacement| DocumentEdit {
9818 columns: BTreeMap::new(),
9819 container: None,
9820 range,
9821 replacement,
9822 };
9823 let edit = match next(14) {
9824 10 => {
9825 node.collapsed = !node.collapsed;
9826 Some(edit(root..root + 1, vec![node]))
9827 }
9828 11 if root > 0 && node.text().is_some() => {
9829 let parent = &nodes[root - 1];
9830 node.parent = Some(parent.id);
9831 node.level = parent.level + 1;
9832 Some(edit(root..root + 1, vec![node]))
9833 }
9834 12 if node.text().is_none() || nodes.len() > 3 => {
9835 Some(edit(root..root + 1, Vec::new()))
9836 }
9837 13 => {
9838 let mut picture = picture.clone();
9839 picture.id = new_id().unwrap();
9840 let ParagraphContent::Image(image) = &mut picture.content else {
9841 unreachable!()
9842 };
9843 image.id = new_id().unwrap();
9844 Some(edit(root..root, vec![picture]))
9845 }
9846 _ => None,
9847 };
9848 let _ = match (edit, next(10)) {
9849 (Some(edit), _) => {
9850 let start = [TextPosition {
9851 paragraph: 0,
9852 offset: 0,
9853 }; 2];
9854 editor.commit(&mut engine, edit, start.into())
9855 }
9856 (None, 0) => editor.insert(&mut engine, "\n"),
9857 (None, 1 | 2) => editor.delete(&mut engine, next(2) == 0).map(drop),
9858 (None, 3) => editor.undo(&mut engine).map(drop),
9859 (None, _) => editor.insert(&mut engine, ["a", " ", "wide", "W"][next(4)]),
9860 };
9861 let outline = editor.active_outline();
9862 let fresh =
9863 TextOutline::from_outline(&mut engine, &outline.snapshot(), &BTreeMap::new())
9864 .unwrap();
9865 let layout = |outline: &TextOutline| {
9866 let shaped = &outline.shaped;
9867 let paragraphs = shaped.paragraphs.iter().map(|paragraph| {
9868 (
9869 paragraph.id,
9870 paragraph.origin,
9871 paragraph
9872 .markers
9873 .iter()
9874 .map(|marker| marker.1)
9875 .collect::<Vec<_>>(),
9876 paragraph
9877 .tags
9878 .iter()
9879 .map(|tag| tag.origin)
9880 .collect::<Vec<_>>(),
9881 )
9882 });
9883 let cells = shaped.tables.iter().flat_map(|table| {
9884 table
9885 .cells
9886 .iter()
9887 .map(|cell| (cell.id, cell.rect, cell.paragraphs.clone()))
9888 });
9889 let objects = shaped.objects.iter().map(|object| {
9890 let label = object.label().map(|label| label.origin);
9891 (object.id, object.rect, object.bottom, label)
9892 });
9893 format!(
9894 "{:?}",
9895 (
9896 shaped.size,
9897 shaped.widths,
9898 outline.bounds(),
9899 paragraphs.collect::<Vec<_>>(),
9900 cells.collect::<Vec<_>>(),
9901 objects.collect::<Vec<_>>(),
9902 )
9903 )
9904 };
9905 assert_eq!(layout(outline), layout(&fresh), "step {step}");
9906 }
9907 assert_eq!(
9908 editor.active_outline().layout.width_set_by_user,
9909 Some(fixed)
9910 );
9911 }
9912 }
9913
9914 #[test]
9915 fn structural_edits_reflow_only_affected_paragraphs_and_restore_selection() {
9916 let mut engine = TextEngine::default();
9917 let text = TextDocument::new(vec![
9918 Paragraph::new("before".into(), Format::default()),
9919 Paragraph::new("split this paragraph".into(), Format::default()),
9920 Paragraph::new("after".into(), Format::default()),
9921 ])
9922 .unwrap();
9923 let mut editor = CanvasEditor::new(&mut engine, text.clone(), 60.0).unwrap();
9924 let before = editor.active_outline().shaped.paragraphs[0]
9925 .text
9926 .shaped
9927 .styles()
9928 .as_ptr();
9929 let after = editor.active_outline().shaped.paragraphs[2]
9930 .text
9931 .shaped
9932 .styles()
9933 .as_ptr();
9934 editor.resize(&mut engine, 60.0).unwrap();
9935 assert_eq!(
9936 editor.active_outline().shaped.paragraphs[0]
9937 .text
9938 .shaped
9939 .styles()
9940 .as_ptr(),
9941 before
9942 );
9943 let point = TextPosition {
9944 paragraph: 1,
9945 offset: 6,
9946 };
9947 editor.select([point, point].into()).unwrap();
9948 editor
9949 .replace(
9950 &mut engine,
9951 vec![Paragraph::new(String::new(), Format::default()); 2],
9952 )
9953 .unwrap();
9954 assert_eq!(
9955 editor
9956 .active_outline()
9957 .document
9958 .paragraphs()
9959 .map(Paragraph::text)
9960 .collect::<Vec<_>>(),
9961 ["before", "split ", "this paragraph", "after"]
9962 );
9963 assert_eq!(
9964 editor.selection().positions,
9965 [TextPosition {
9966 paragraph: 2,
9967 offset: 0
9968 }; 2]
9969 );
9970 assert_eq!(
9971 editor.active_outline().shaped.paragraphs[0]
9972 .text
9973 .shaped
9974 .styles()
9975 .as_ptr(),
9976 before
9977 );
9978 assert_eq!(
9979 editor.active_outline().shaped.paragraphs[3]
9980 .text
9981 .shaped
9982 .styles()
9983 .as_ptr(),
9984 after
9985 );
9986 let rebuilt =
9987 CanvasEditor::new(&mut engine, editor.active_outline().document.clone(), 60.0).unwrap();
9988 assert_eq!(ys(editor.active_outline()), ys(rebuilt.active_outline()));
9989 for (actual, expected) in editor
9990 .active_outline()
9991 .shaped
9992 .paragraphs
9993 .iter()
9994 .zip(&rebuilt.active_outline().shaped.paragraphs)
9995 {
9996 let lines = |p: &ParagraphLayout| {
9997 p.text
9998 .lines()
9999 .map(|(_, b)| (b.source.clone(), b.top, b.baseline, b.height))
10000 .collect::<Vec<_>>()
10001 };
10002 assert_eq!(lines(actual), lines(expected));
10003 }
10004 editor.undo(&mut engine).unwrap();
10005 assert_eq!(editor.active_outline().document, text);
10006 assert_eq!(editor.selection().positions, [point; 2]);
10007 assert_eq!(
10008 editor.active_outline().shaped.paragraphs[2]
10009 .text
10010 .shaped
10011 .styles()
10012 .as_ptr(),
10013 after
10014 );
10015 editor.redo(&mut engine).unwrap();
10016 assert_eq!(
10017 editor.active_outline().document,
10018 rebuilt.active_outline().document
10019 );
10020 let selection = editor.selection().positions;
10021 let previous_width = editor.active_outline().layout.max_width.unwrap();
10022 editor.resize(&mut engine, 30.0).unwrap();
10023 let rebuilt =
10024 CanvasEditor::new(&mut engine, editor.active_outline().document.clone(), 30.0).unwrap();
10025 assert_eq!(ys(editor.active_outline()), ys(rebuilt.active_outline()));
10026 assert_eq!(editor.selection().positions, selection);
10027 assert!(editor.resize(&mut engine, f32::NAN).is_err());
10028 assert_eq!(ys(editor.active_outline()), ys(rebuilt.active_outline()));
10029 editor.undo(&mut engine).unwrap();
10030 assert_eq!(
10031 editor.active_outline().layout.max_width,
10032 Some(previous_width)
10033 );
10034 let rebuilt = CanvasEditor::new(
10035 &mut engine,
10036 editor.active_outline().document.clone(),
10037 previous_width,
10038 )
10039 .unwrap();
10040 assert_eq!(ys(editor.active_outline()), ys(rebuilt.active_outline()));
10041 }
10042
10043 #[test]
10044 fn failed_layout_preserves_text_selection_history_and_cached_layout() {
10045 let mut engine = TextEngine::default();
10046 let text =
10047 TextDocument::new(vec![Paragraph::new("original".into(), Format::default())]).unwrap();
10048 let mut editor = CanvasEditor::new(&mut engine, text.clone(), 60.0).unwrap();
10049 editor
10050 .replace(
10051 &mut engine,
10052 vec![Paragraph::new("!".into(), Format::default())],
10053 )
10054 .unwrap();
10055 editor.undo(&mut engine).unwrap();
10056 let before = editor.active_outline().shaped.paragraphs[0]
10057 .text
10058 .shaped
10059 .styles()
10060 .as_ptr();
10061 for format in [
10062 Format {
10063 font_size: Some(f32::NAN),
10064 ..Format::default()
10065 },
10066 Format {
10067 line_spacing: Some(-1.0),
10068 ..Format::default()
10069 },
10070 Format {
10071 space_after: Some(f32::INFINITY),
10072 ..Format::default()
10073 },
10074 ] {
10075 assert!(
10076 editor
10077 .replace(&mut engine, vec![Paragraph::new("bad".into(), format)])
10078 .is_err()
10079 );
10080 assert_eq!(editor.active_outline().document, text);
10081 assert_eq!(
10082 editor.selection().positions,
10083 [TextPosition {
10084 paragraph: 0,
10085 offset: 0
10086 }; 2]
10087 );
10088 assert_eq!(
10089 editor.active_outline().shaped.paragraphs[0]
10090 .text
10091 .shaped
10092 .styles()
10093 .as_ptr(),
10094 before
10095 );
10096 }
10097 assert!(editor.redo(&mut engine).unwrap());
10098 assert_eq!(
10099 editor
10100 .active_outline()
10101 .document
10102 .paragraphs()
10103 .next()
10104 .unwrap()
10105 .text(),
10106 "!original"
10107 );
10108 }
10109 #[test]
10110 fn outline_creation_movement_and_typing_share_one_history() {
10111 let mut engine = TextEngine::default();
10112 let original =
10113 TextDocument::new(vec![Paragraph::new("lyrics".into(), Format::default())]).unwrap();
10114 let mut editor = CanvasEditor::new(&mut engine, original.clone(), 240.0).unwrap();
10115 let first = editor.active_outline().id;
10116 editor.insert(&mut engine, "A ").unwrap();
10117 let second = editor
10118 .create_outline(&mut engine, [300.0, 0.0], 120.0)
10119 .unwrap();
10120 editor
10121 .insert(&mut engine, "annotation\nsecond line")
10122 .unwrap();
10123 let annotation = editor.active_outline().document.clone();
10124 let pointers: Vec<_> = editor
10125 .outlines
10126 .iter()
10127 .map(|outline| outline.shaped.paragraphs[0].text.shaped.styles().as_ptr())
10128 .collect();
10129 editor.move_outline(first, [-50.0, 60.0]).unwrap();
10130 assert_eq!(editor.active_outline().id, first);
10131 assert_eq!(
10132 editor
10133 .outlines
10134 .iter()
10135 .map(|outline| outline.shaped.paragraphs[0].text.shaped.styles().as_ptr())
10136 .collect::<Vec<_>>(),
10137 pointers
10138 );
10139 assert_eq!(editor.outlines[1].document, annotation);
10140 editor.undo(&mut engine).unwrap();
10141 assert_eq!(editor.active_outline().origin(), [0.0; 2]);
10142 editor.undo(&mut engine).unwrap();
10143 assert_eq!(editor.active_outline().id, second);
10144 assert_eq!(
10145 editor
10146 .active_outline()
10147 .document
10148 .paragraphs()
10149 .next()
10150 .unwrap()
10151 .text(),
10152 ""
10153 );
10154 let retained = editor.active_outline().shaped.paragraphs[0]
10155 .text
10156 .shaped
10157 .styles()
10158 .as_ptr();
10159 editor.undo(&mut engine).unwrap();
10160 assert_eq!(editor.outlines.len(), 1);
10161 assert_eq!(editor.active_outline().id, first);
10162 editor.undo(&mut engine).unwrap();
10163 assert_eq!(editor.active_outline().document, original);
10164 assert!(!editor.undo(&mut engine).unwrap());
10165 editor.redo(&mut engine).unwrap();
10166 editor.redo(&mut engine).unwrap();
10167 assert_eq!(editor.active_outline().id, second);
10168 assert_eq!(
10169 editor.active_outline().shaped.paragraphs[0]
10170 .text
10171 .shaped
10172 .styles()
10173 .as_ptr(),
10174 retained
10175 );
10176 editor.redo(&mut engine).unwrap();
10177 assert_eq!(editor.active_outline().document, annotation);
10178 editor.redo(&mut engine).unwrap();
10179 assert_eq!(editor.active_outline().id, first);
10180 assert_eq!(editor.active_outline().origin(), [-50.0, 60.0]);
10181 assert!(!editor.redo(&mut engine).unwrap());
10182 }
10183
10184 #[test]
10185 fn invalid_outline_operations_preserve_composition_and_history() {
10186 let mut engine = TextEngine::default();
10187 let original =
10188 TextDocument::new(vec![Paragraph::new("original".into(), Format::default())]).unwrap();
10189 let mut editor = CanvasEditor::new(&mut engine, original.clone(), 240.0).unwrap();
10190 let first = editor.active_outline().id;
10191 editor.compose(&mut engine, "é".into(), 1..1).unwrap();
10192 let provisional = editor.active_outline().document.clone();
10193 for position in [[f32::NAN, 0.0], [0.0, f32::INFINITY]] {
10194 assert!(editor.create_outline(&mut engine, position, 120.0).is_err());
10195 assert!(editor.move_outline(first, position).is_err());
10196 }
10197 assert!(editor.create_outline(&mut engine, [0.0; 2], 0.0).is_err());
10198 assert!(editor.focus_outline(onestore::ExGuid::default()).is_err());
10199 assert!(
10200 editor
10201 .move_outline(onestore::ExGuid::default(), [1.0; 2])
10202 .is_err()
10203 );
10204 assert_eq!(editor.active_outline().document, provisional);
10205 assert!(editor.marked_range().is_some());
10206 assert!(editor.undo.is_empty());
10207 assert!(editor.redo.is_empty());
10208 let second = editor
10209 .create_outline(&mut engine, [300.0, 0.0], 120.0)
10210 .unwrap();
10211 assert!(editor.marked_range().is_none());
10212 editor.undo(&mut engine).unwrap();
10213 assert_eq!(editor.active_outline().document, provisional);
10214 editor.undo(&mut engine).unwrap();
10215 assert_eq!(editor.active_outline().document, original);
10216 editor.redo(&mut engine).unwrap();
10217 let replacement = editor
10218 .create_outline(&mut engine, [500.0, 0.0], 120.0)
10219 .unwrap();
10220 assert_ne!(replacement, second);
10221 assert!(!editor.redo(&mut engine).unwrap());
10222 assert_eq!(editor.outlines[0].document, provisional);
10223 }
10224 #[test]
10225 fn mixed_outline_history_restores_every_source_and_placement() {
10226 let mut engine = TextEngine::default();
10227 let original = TextDocument::new(vec![Paragraph::from_runs([
10228 (
10229 "hidden".into(),
10230 Format {
10231 hidden: Some(true),
10232 ..Format::default()
10233 },
10234 ),
10235 ("lyrics 🌳".into(), Format::default()),
10236 ])])
10237 .unwrap();
10238 let mut editor = CanvasEditor::new(&mut engine, original, 120.0).unwrap();
10239 let snapshot = |editor: &CanvasEditor| {
10240 editor
10241 .outlines
10242 .iter()
10243 .map(|outline| (outline.id, outline.layout.clone(), outline.document.clone()))
10244 .collect::<Vec<_>>()
10245 };
10246 let mut steps = Vec::new();
10247 let mut random = 0x29a7_u32;
10248 for step in 0..80 {
10249 random = random.wrapping_mul(1664525).wrapping_add(1013904223);
10250 let index = random as usize % editor.outlines.len();
10251 let id = editor.outlines[index].id;
10252 editor.focus_outline(id).unwrap();
10253 let before = snapshot(&editor);
10254 let undo_count = editor.undo.len();
10255 match step % 5 {
10256 0 if editor.outlines.len() < 8 => {
10257 editor
10258 .create_outline(
10259 &mut engine,
10260 [step as f32 * 9.0 - 100.0, step as f32 * 3.0],
10261 72.0 + step as f32,
10262 )
10263 .unwrap();
10264 }
10265 1 => {
10266 editor
10267 .insert(
10268 &mut engine,
10269 ["x", "é", "🌳", "a\nשלום"][random as usize % 4],
10270 )
10271 .unwrap();
10272 }
10273 2 => {
10274 editor.delete(&mut engine, random & 1 == 0).unwrap();
10275 }
10276 3 => {
10277 editor
10278 .compose(&mut engine, "e\u{301}".into(), 2..2)
10279 .unwrap();
10280 editor.finish_composition();
10281 }
10282 _ => {
10283 let [x, y] = editor.active_outline().origin();
10284 editor.move_outline(id, [x + 7.0, y - 3.0]).unwrap();
10285 }
10286 }
10287 if editor.undo.len() == undo_count {
10288 assert_eq!(snapshot(&editor), before);
10289 continue;
10290 }
10291 assert_eq!(editor.undo.len(), undo_count + 1);
10292 steps.push((before, snapshot(&editor)));
10293 for outline in &editor.outlines {
10294 let rebuilt = TextOutline::new(
10295 &mut engine,
10296 outline.document.clone(),
10297 outline.layout.max_width.unwrap(),
10298 outline.origin(),
10299 )
10300 .unwrap();
10301 assert_eq!(ys(outline), ys(&rebuilt));
10302 for (actual, expected) in outline
10303 .shaped
10304 .paragraphs
10305 .iter()
10306 .zip(&rebuilt.shaped.paragraphs)
10307 {
10308 assert_eq!(actual.projection.text(), expected.projection.text());
10309 assert_eq!(actual.text.height(), expected.text.height());
10310 assert_eq!(
10311 actual
10312 .text
10313 .lines()
10314 .map(|(_, line)| (line.source.clone(), line.baseline))
10315 .collect::<Vec<_>>(),
10316 expected
10317 .text
10318 .lines()
10319 .map(|(_, line)| (line.source.clone(), line.baseline))
10320 .collect::<Vec<_>>()
10321 );
10322 }
10323 }
10324 }
10325 assert!(steps.len() > 60);
10326 for (before, _) in steps.iter().rev() {
10327 assert!(editor.undo(&mut engine).unwrap());
10328 assert_eq!(&snapshot(&editor), before);
10329 }
10330 assert!(!editor.undo(&mut engine).unwrap());
10331 for (_, after) in &steps {
10332 assert!(editor.redo(&mut engine).unwrap());
10333 assert_eq!(&snapshot(&editor), after);
10334 }
10335 assert!(!editor.redo(&mut engine).unwrap());
10336 }
10337
10338 const CORPUS: [&[u8]; 3] = [
10339 include_bytes!("../../../corpus/canvas/baseline-anchors.one"),
10340 include_bytes!("../../../corpus/outline-edit/before/notebook/synthetic.one"),
10341 include_bytes!("../../../corpus/paragraph-edit/before/notebook/synthetic.one"),
10342 ];
10343
10344 fn corpus_pages(section: &[u8]) -> Vec<(ExGuid, Page)> {
10345 use onestore::{RevisionIndex, Store, document::Document};
10346 let store = Store::parse(section).unwrap();
10347 let index = RevisionIndex::parse(&store).unwrap();
10348 let document = Document::parse(&index).unwrap();
10349 let mut spaces = document
10350 .pages()
10351 .unwrap()
10352 .into_iter()
10353 .map(|(space, _)| space)
10354 .collect::<Vec<_>>();
10355 spaces.dedup();
10356 spaces
10357 .into_iter()
10358 .map(|space| (space, Page::from_space(&document, space).unwrap()))
10359 .collect()
10360 }
10361
10362 fn corpus_page(section: &[u8], title: &str) -> (ExGuid, Page) {
10363 corpus_pages(section)
10364 .into_iter()
10365 .find(|(_, page)| page.title == title)
10366 .unwrap()
10367 }
10368
10369 fn body_text(page: &Page, id: ExGuid) -> Option<String> {
10370 page.objects.iter().find_map(|object| match object {
10371 PageObject::Outline(outline) if outline.id == id => Some(
10372 outline
10373 .paragraphs
10374 .iter()
10375 .map(|paragraph| paragraph.text().unwrap().text.text())
10376 .collect(),
10377 ),
10378 _ => None,
10379 })
10380 }
10381
10382 #[test]
10383 fn an_unedited_page_rebuilds_into_the_model_it_was_imported_from() {
10384 let mut pages = 0;
10385 for section in CORPUS {
10386 for (_, page) in corpus_pages(section) {
10387 let mut engine = TextEngine::default();
10388 let editor = CanvasEditor::from_page(page.clone(), &mut engine).unwrap();
10389 assert_eq!(editor.page().unwrap(), page);
10390 pages += 1;
10391 }
10392 }
10393 assert_eq!(pages, 30);
10394 }
10395
10396 #[test]
10397 fn typing_moving_resizing_adding_and_deleting_reach_the_rebuilt_page() {
10398 let mut engine = TextEngine::default();
10399 let (_, source) = corpus_page(CORPUS[0], "Baseline anchors");
10400 let mut editor = CanvasEditor::from_page(source.clone(), &mut engine).unwrap();
10401 let removed = editor
10402 .outlines
10403 .iter()
10404 .find(|outline| {
10405 let [node] = outline.document.nodes() else {
10406 return false;
10407 };
10408 !outline.title
10409 && node.lists.is_empty()
10410 && node.tags.is_empty()
10411 && node.text().is_some_and(|text| text.tags.is_empty())
10412 })
10413 .unwrap()
10414 .id;
10415 let bodies = editor
10416 .outlines
10417 .iter()
10418 .filter(|outline| !outline.title && outline.id != removed)
10419 .map(|outline| outline.id)
10420 .collect::<Vec<_>>();
10421 editor.focus_outline(bodies[0]).unwrap();
10422 editor.insert(&mut engine, "typed").unwrap();
10423 editor.move_outline(bodies[1], [123.0, 456.0]).unwrap();
10424 editor.focus_outline(bodies[2]).unwrap();
10425 editor.resize(&mut engine, 200.0).unwrap();
10426 editor.focus_outline(removed).unwrap();
10427 editor.select_all().unwrap();
10428 assert!(editor.delete(&mut engine, false).unwrap());
10429 let added = editor
10430 .create_outline(&mut engine, [24.0, 600.0], 300.0)
10431 .unwrap();
10432 editor.insert(&mut engine, "added").unwrap();
10433
10434 let page = editor.page().unwrap();
10435 assert_eq!(page.title, source.title);
10436 assert_eq!(page.created, source.created);
10437 assert_eq!(page.margin_origin, source.margin_origin);
10438 assert!(body_text(&page, bodies[0]).unwrap().starts_with("typed"));
10439 let moved = page
10440 .objects
10441 .iter()
10442 .find_map(|object| match object {
10443 PageObject::Outline(outline) if outline.id == bodies[1] => Some(&outline.layout),
10444 _ => None,
10445 })
10446 .unwrap();
10447 assert_eq!([moved.x, moved.y], [Some(123.0), Some(456.0)]);
10448 let resized = page
10449 .objects
10450 .iter()
10451 .find_map(|object| match object {
10452 PageObject::Outline(outline) if outline.id == bodies[2] => Some(&outline.layout),
10453 _ => None,
10454 })
10455 .unwrap();
10456 assert_eq!(resized.max_width, Some(200.0));
10457 assert_eq!(resized.width_set_by_user, Some(true));
10458 assert!(body_text(&page, removed).is_none());
10459 assert_eq!(body_text(&page, added).as_deref(), Some("added"));
10460 assert_eq!(page.objects.last().unwrap().id(), added);
10461 assert_eq!(
10462 page.objects.iter().map(PageObject::id).collect::<Vec<_>>(),
10463 source
10464 .objects
10465 .iter()
10466 .map(PageObject::id)
10467 .filter(|id| *id != removed)
10468 .chain([added])
10469 .collect::<Vec<_>>()
10470 );
10471 assert_eq!(
10472 page.objects
10473 .iter()
10474 .find(|object| matches!(object, PageObject::Title(_))),
10475 source
10476 .objects
10477 .iter()
10478 .find(|object| matches!(object, PageObject::Title(_)))
10479 );
10480
10481 let mut engine = TextEngine::default();
10482 let reimported = CanvasEditor::from_page(page.clone(), &mut engine).unwrap();
10483 assert_eq!(reimported.page().unwrap(), page);
10484 }
10485
10486 #[test]
10487 fn an_edited_page_writes_back_as_its_ops() {
10488 let mut engine = TextEngine::default();
10489 let (space, source) = corpus_page(CORPUS[2], "Split middle");
10490 let mut editor = CanvasEditor::from_page(source.clone(), &mut engine).unwrap();
10491 let body = editor
10492 .outlines
10493 .iter()
10494 .find(|outline| !outline.title)
10495 .unwrap()
10496 .id;
10497 editor.focus_outline(body).unwrap();
10498 editor.insert(&mut engine, "Edited ").unwrap();
10499 let page = editor.page().unwrap();
10500 let written = ops::saved(CORPUS[2], space, &mut editor);
10501 let reread = corpus_pages(&written)
10502 .into_iter()
10503 .find_map(|(candidate, page)| (candidate == space).then_some(page))
10504 .unwrap();
10505 let edited = body_text(&page, body).unwrap();
10506 assert!(edited.starts_with("Edited "));
10507 assert_eq!(body_text(&reread, body).as_deref(), Some(edited.as_str()));
10508 assert_ne!(body_text(&source, body).as_deref(), Some(edited.as_str()));
10509 }
10510
10511 /// A page changed elsewhere shows in place: the caret keeps its paragraph and moves
10512 /// past text inserted before it; history goes, unless nothing changed.
10513 #[test]
10514 fn a_refresh_keeps_the_caret_by_identity() {
10515 let mut engine = TextEngine::default();
10516 let lines =
10517 ["Hello world", "Second"].map(|line| Paragraph::new(line.into(), Format::default()));
10518 let document = TextDocument::new(lines.to_vec()).unwrap();
10519 let mut editor = CanvasEditor::new(&mut engine, document, 400.0).unwrap();
10520 editor
10521 .select(
10522 [TextPosition {
10523 paragraph: 1,
10524 offset: 3,
10525 }; 2]
10526 .into(),
10527 )
10528 .unwrap();
10529 editor.insert(&mut engine, "x").unwrap();
10530 editor
10531 .select(
10532 [TextPosition {
10533 paragraph: 0,
10534 offset: 8,
10535 }; 2]
10536 .into(),
10537 )
10538 .unwrap();
10539 let _ = editor.take_ops();
10540 let page = editor.page().unwrap();
10541 assert!(!editor.refresh(page.clone(), &mut engine).unwrap());
10542 assert!(!editor.undo.is_empty(), "an unchanged page keeps history");
10543
10544 let mut remote = page;
10545 let PageObject::Outline(outline) = &mut remote.objects[0] else {
10546 unreachable!()
10547 };
10548 for (paragraph, text) in outline
10549 .paragraphs
10550 .iter_mut()
10551 .zip(["Hey, Hello world", "Other"])
10552 {
10553 paragraph.text_mut().unwrap().text = Paragraph::new(text.into(), Format::default());
10554 }
10555 let (shown, affinities) = (editor.active_outline().id, editor.selection().affinities);
10556 assert!(editor.refresh(remote, &mut engine).unwrap());
10557 assert_eq!(editor.active_outline().id, shown);
10558 assert_eq!(
10559 editor.selection(),
10560 Selection {
10561 positions: [TextPosition {
10562 paragraph: 0,
10563 offset: 13
10564 }; 2],
10565 affinities,
10566 }
10567 );
10568 assert!(editor.undo.is_empty());
10569 assert_eq!(editor.take_ops().unwrap(), []);
10570 }
10571
10572 /// Alpha, Parent > Child > Grand, a table [[A1 A2 | B1], [C1 | D1]], Omega: text leaves 0..=9.
10573 fn select_more_editor(engine: &mut TextEngine, collapsed: bool) -> CanvasEditor {
10574 use onestore::page::{ParagraphContent, Table, TableCell, TableColumn, TableRow};
10575 let text = |text: &str| {
10576 crate::document::node(
10577 Paragraph::new(text.into(), Format::default()),
10578 Format::default(),
10579 )
10580 .unwrap()
10581 };
10582 let child = |label: &str, parent: &PageParagraph| PageParagraph {
10583 parent: Some(parent.id),
10584 level: parent.level + 1,
10585 ..text(label)
10586 };
10587 let parent = PageParagraph {
10588 collapsed,
10589 ..text("Parent")
10590 };
10591 let middle = child("Child", &parent);
10592 let grand = child("Grand", &middle);
10593 let mut table = text("");
10594 table.content = ParagraphContent::Table(Table {
10595 id: onestore::page::text::new_id().unwrap(),
10596 columns: vec![
10597 TableColumn {
10598 width: 72.0,
10599 locked: true,
10600 };
10601 2
10602 ],
10603 rows: [[&["A1", "A2"][..], &["B1"]], [&["C1"], &["D1"]]]
10604 .into_iter()
10605 .map(|cells| TableRow {
10606 id: onestore::page::text::new_id().unwrap(),
10607 cells: cells
10608 .into_iter()
10609 .map(|texts| TableCell {
10610 id: onestore::page::text::new_id().unwrap(),
10611 layout: Default::default(),
10612 indents: vec![18.0, 0.0, 27.0],
10613 shading: None,
10614 paragraphs: texts.iter().map(|t| text(t)).collect(),
10615 unsupported: Vec::new(),
10616 })
10617 .collect(),
10618 })
10619 .collect(),
10620 borders: Some(true),
10621 layout: Default::default(),
10622 tags: Vec::new(),
10623 });
10624 let nodes = vec![text("Alpha"), parent, middle, grand, table, text("Omega")];
10625 CanvasEditor::new(engine, TextDocument::from_nodes(nodes).unwrap(), 300.0).unwrap()
10626 }
10627
10628 /// The selections successive `widen_selection` calls give from `start` to `end`.
10629 fn widenings(
10630 editor: &mut CanvasEditor,
10631 start: (usize, u32),
10632 end: (usize, u32),
10633 ) -> Vec<[(usize, u32); 2]> {
10634 let position = |(paragraph, offset)| TextPosition { paragraph, offset };
10635 editor
10636 .select([position(start), position(end)].into())
10637 .unwrap();
10638 (0..6)
10639 .map(|_| {
10640 editor.widen_selection().unwrap();
10641 editor
10642 .selection()
10643 .positions
10644 .map(|position| (position.paragraph, position.offset))
10645 })
10646 .collect()
10647 }
10648
10649 #[test]
10650 fn select_all_widens_as_onenote_does() {
10651 let mut engine = TextEngine::default();
10652 let mut editor = select_more_editor(&mut engine, false);
10653 let outline = [(0, 0), (9, 5)];
10654 let steps = |steps: &[[(usize, u32); 2]]| {
10655 let mut steps = steps.to_vec();
10656 steps.resize(6, outline);
10657 steps
10658 };
10659 // A paragraph takes its break when a sibling follows, then the outline.
10660 assert_eq!(
10661 widenings(&mut editor, (0, 2), (0, 2)),
10662 steps(&[[(0, 0), (1, 0)]])
10663 );
10664 // A parent, then its subtree, then the outline.
10665 assert_eq!(
10666 widenings(&mut editor, (1, 2), (1, 2)),
10667 steps(&[[(1, 0), (2, 0)], [(1, 0), (3, 5)]])
10668 );
10669 // A child widens to its own subtree, never its parent's.
10670 assert_eq!(
10671 widenings(&mut editor, (2, 1), (2, 1)),
10672 steps(&[[(2, 0), (3, 0)], [(2, 0), (3, 5)]])
10673 );
10674 assert_eq!(
10675 widenings(&mut editor, (3, 1), (3, 1)),
10676 steps(&[[(3, 0), (3, 5)]])
10677 );
10678 // In a table: paragraph, cell, row, table.
10679 assert_eq!(
10680 widenings(&mut editor, (4, 1), (4, 1)),
10681 steps(&[
10682 [(4, 0), (5, 0)],
10683 [(4, 0), (5, 2)],
10684 [(4, 0), (6, 2)],
10685 [(4, 0), (8, 2)],
10686 ])
10687 );
10688 // A cell holding one paragraph adds nothing to it.
10689 assert_eq!(
10690 widenings(&mut editor, (6, 1), (6, 1)),
10691 steps(&[[(6, 0), (6, 2)], [(4, 0), (6, 2)], [(4, 0), (8, 2)]])
10692 );
10693 assert_eq!(
10694 widenings(&mut editor, (9, 1), (9, 1)),
10695 steps(&[[(9, 0), (9, 5)]])
10696 );
10697 }
10698
10699 #[test]
10700 fn select_all_starts_from_the_current_selection() {
10701 let mut engine = TextEngine::default();
10702 let mut editor = select_more_editor(&mut engine, false);
10703 let first = |editor: &mut CanvasEditor, start, end| widenings(editor, start, end)[0];
10704 // Part or all of a paragraph's text widens to the paragraph.
10705 assert_eq!(first(&mut editor, (0, 1), (0, 3)), [(0, 0), (1, 0)]);
10706 assert_eq!(first(&mut editor, (0, 0), (0, 5)), [(0, 0), (1, 0)]);
10707 assert_eq!(first(&mut editor, (0, 3), (0, 1)), [(0, 0), (1, 0)]);
10708 // Across paragraphs, the outline; across cells of a row, the row.
10709 assert_eq!(first(&mut editor, (0, 1), (1, 1)), [(0, 0), (9, 5)]);
10710 assert_eq!(first(&mut editor, (5, 1), (6, 1)), [(4, 0), (6, 2)]);
10711 assert_eq!(first(&mut editor, (6, 1), (7, 1)), [(4, 0), (8, 2)]);
10712 // A caret moved after widening starts over at its paragraph.
10713 assert_eq!(first(&mut editor, (9, 1), (9, 1)), [(9, 0), (9, 5)]);
10714 // Selection only: nothing to undo or store.
10715 assert!(editor.undo.is_empty());
10716 assert_eq!(editor.take_ops().unwrap(), []);
10717 }
10718
10719 #[test]
10720 fn select_all_skips_the_hidden_children_of_a_collapsed_parent() {
10721 let mut engine = TextEngine::default();
10722 let mut editor = select_more_editor(&mut engine, true);
10723 assert_eq!(
10724 widenings(&mut editor, (1, 2), (1, 2))[..2],
10725 [[(1, 0), (1, 6)], [(0, 0), (9, 5)]]
10726 );
10727 }
10728
10729 #[test]
10730 fn select_all_in_a_single_paragraph_stops_at_the_paragraph() {
10731 let mut engine = TextEngine::default();
10732 let mut editor = CanvasEditor::new(
10733 &mut engine,
10734 TextDocument::new(vec![Paragraph::new("Title".into(), Format::default())]).unwrap(),
10735 180.0,
10736 )
10737 .unwrap();
10738 assert_eq!(
10739 widenings(&mut editor, (0, 5), (0, 5)),
10740 [[(0, 0), (0, 5)]; 6]
10741 );
10742 }
10743
10744 #[test]
10745 fn pasting_over_the_page_selection_is_one_step_that_undo_selects_again() {
10746 let mut engine = TextEngine::default();
10747 let lines = ["One", "Two"].map(|line| Paragraph::new(line.into(), Format::default()));
10748 let mut editor = CanvasEditor::new(
10749 &mut engine,
10750 TextDocument::new(lines.to_vec()).unwrap(),
10751 400.0,
10752 )
10753 .unwrap();
10754 editor.select_page().unwrap();
10755 editor
10756 .paste_pieces(
10757 &mut engine,
10758 vec![
10759 html_pieces("Pasted", 0x409, |_, _| None).pop().unwrap(),
10760 Piece::Picture(vec![0; 8], [10.0; 2]),
10761 ],
10762 )
10763 .unwrap();
10764 assert_eq!(editor.history_depth(), [1, 0]);
10765 let _ = editor.take_ops();
10766 let mut remote = editor.page().unwrap();
10767 remote.color = Some(0x00ff_ffff);
10768 editor.refresh(remote, &mut engine).unwrap();
10769 assert!(editor.undo(&mut engine).unwrap());
10770 assert_eq!(editor.whole(), Some(Whole::Page));
10771 }
10772}