1use crate::{
2 document::{DocumentEdit, descendants, edited_nodes, leaves},
3 layout::{InlineSpace, LayoutError, TextEngine, TextLayout},
4};
5use onestore::page::text::{Paragraph, TextProjection};
6use onestore::page::{Definition, Outline, PageParagraph, ParagraphContent, Table, Title};
7use onestore::{
8 ExGuid,
9 document::{Format, Kind},
10};
11use std::{
12 collections::{BTreeMap, BTreeSet},
13 ops::Range,
14 sync::Arc,
15};
16
17pub(crate) const TITLE_WIDTH: f32 = 468.0;
18
19#[derive(Clone, Default)]
20pub struct OutlineLayout {
21 pub paragraphs: Vec<ParagraphLayout>,
22 pub tables: Vec<TableLayout>,
23 /// Pictures, files and handwriting that occupy a paragraph of their own.
24 pub(crate) objects: Vec<ObjectLayout>,
25 pub size: [f32; 2],
26 /// One per root node, so an edit places anew only what it changes and moves what follows.
27 blocks: Vec<Block>,
28 /// The widest root node, at least 36 points, and the widest root text's line boxes; kept so
29 /// an edit sizes an automatic width without measuring every node.
30 pub(crate) widths: [f32; 2],
31}
32
33/// A root node's place in its outline's flow.
34#[derive(Clone)]
35struct Block {
36 /// Where the node's paragraphs, tables and objects start.
37 first: [usize; 3],
38 /// Spacing before and after the node and its own height, or `None` while hidden.
39 metrics: Option<[f32; 3]>,
40 /// The node's right edge and, for text, its line boxes' right edge.
41 extent: [f32; 2],
42 /// The flow's bottom and pending spacing after the node.
43 state: (f64, Option<f32>),
44 /// A table's vertical coordinates before its offset.
45 rel: Vec<f32>,
46 /// The number the next numbered sibling at the node's level continues from.
47 count: Option<Count>,
48}
49
50/// A layout of root nodes `range` once an edit applies, and how the nodes after them move.
51pub(crate) struct Relayout {
52 pub(crate) range: Range<usize>,
53 pub(crate) segment: OutlineLayout,
54 /// Each following node's new top and flow state, up to the first that stays.
55 moved: Vec<(f32, (f64, Option<f32>))>,
56 pub(crate) size: [f32; 2],
57 widths: [f32; 2],
58}
59
60#[derive(Clone)]
61pub(crate) struct ObjectLayout {
62 /// The object's identity, which keys a picture's or file icon's decoded image.
63 pub(crate) id: ExGuid,
64 /// Where the picture, file icon, drawing or placeholder draws, outline-local.
65 pub(crate) rect: [f32; 4],
66 pub(crate) kind: ObjectKind,
67 /// Outline-local bottom of the whole object, label included.
68 pub(crate) bottom: f32,
69 pub(crate) tags: Option<BlockTags>,
70}
71
72/// The note tags of a table, picture or file, which OneNote centres on it in the tag column.
73#[derive(Clone)]
74pub(crate) struct BlockTags {
75 /// The paragraph holding the block.
76 pub(crate) paragraph: ExGuid,
77 /// Its siblings' group, whose list markers the tags clear.
78 parent: Option<ExGuid>,
79 /// The block's left edge, outline-local.
80 x: f32,
81 /// Origins are outline-local.
82 pub(crate) tags: Vec<ParagraphTag>,
83}
84
85impl BlockTags {
86 /// `node`'s tags and those of its content, `content`, shaped as a tagged paragraph of
87 /// `node`'s format would show them; `None` without any.
88 fn new(
89 node: &PageParagraph,
90 content: &[onestore::document::Tag],
91 x: f32,
92 shape: &mut impl FnMut(
93 &PageParagraph,
94 Option<&Count>,
95 f32,
96 &[f32],
97 ) -> Result<ParagraphLayout, LayoutError>,
98 ) -> Result<Option<Self>, LayoutError> {
99 if node.tags.is_empty() && content.is_empty() {
100 return Ok(None);
101 }
102 let mut tagged = caption(node.id, node.id, "", node.format.clone());
103 tagged.tags.clone_from(&node.tags);
104 if let ParagraphContent::Text(text) = &mut tagged.content {
105 text.tags = content.to_vec();
106 }
107 let tags = shape(&tagged, None, ATTACHMENT_WIDTH, &[0.0, 0.0])?.tags;
108 Ok(Some(Self {
109 paragraph: node.id,
110 parent: node.parent,
111 x,
112 tags,
113 }))
114 }
115
116 /// Centres the tags on a block spanning `top..bottom`.
117 fn centre(&mut self, top: f32, bottom: f32) {
118 for tag in &mut self.tags {
119 tag.origin[1] = (top + bottom - tag.size) / 2.0;
120 }
121 }
122
123 fn offset(&mut self, [x, y]: [f32; 2]) {
124 self.x += x;
125 for tag in &mut self.tags {
126 tag.origin[0] += x;
127 tag.origin[1] += y;
128 }
129 }
130}
131
132#[derive(Clone)]
133pub(crate) enum ObjectKind {
134 Picture,
135 /// A file's icon with its name centered below.
136 File(ParagraphLayout),
137 /// Handwriting, whose strokes are relative to the rect's top-left.
138 Ink(onestore::page::Ink),
139 /// Content the canvas cannot draw, marked by a labelled box.
140 Unsupported(ParagraphLayout),
141}
142
143impl ObjectLayout {
144 pub(crate) fn label(&self) -> Option<&ParagraphLayout> {
145 match &self.kind {
146 ObjectKind::File(label) | ObjectKind::Unsupported(label) => Some(label),
147 ObjectKind::Picture | ObjectKind::Ink(_) => None,
148 }
149 }
150
151 /// The whole object, a file's column with its name included.
152 pub fn bounds(&self) -> [f32; 4] {
153 match &self.kind {
154 ObjectKind::File(label) => [
155 label.origin[0],
156 self.rect[1] - 6.0,
157 label.origin[0] + ATTACHMENT_WIDTH,
158 self.bottom,
159 ],
160 _ => [self.rect[0], self.rect[1], self.rect[2], self.bottom],
161 }
162 }
163
164 /// The rect's top and bottom, the object's bottom and its label's top once the object
165 /// starts at `y`, given its own height, and the height it takes in the flow.
166 fn at(&self, y: f32, height: f32) -> ([f32; 4], f32) {
167 match &self.kind {
168 ObjectKind::File(label) => {
169 let top = y + 6.0;
170 let label_top = top + height + 10.5;
171 let bottom = label_top + label.text.height() + 9.0;
172 ([top, top + height, bottom, label_top], bottom - y)
173 }
174 ObjectKind::Unsupported(_) => ([y, y + height, y + height, y + 8.0], height),
175 ObjectKind::Picture | ObjectKind::Ink(_) => ([y, y + height, y + height, 0.0], height),
176 }
177 }
178
179 fn place(&mut self, y: f32, height: f32) -> f32 {
180 let ([top, end, bottom, label_top], flow) = self.at(y, height);
181 [self.rect[1], self.rect[3], self.bottom] = [top, end, bottom];
182 if let ObjectKind::File(label) | ObjectKind::Unsupported(label) = &mut self.kind {
183 label.origin[1] = label_top;
184 }
185 let [_, top, _, bottom] = self.bounds();
186 if let Some(tags) = &mut self.tags {
187 tags.centre(top, bottom);
188 }
189 flow
190 }
191}
192
193/// The vertical coordinates of these pieces, in a fixed order.
194fn verticals<'a>(
195 paragraphs: &'a mut [ParagraphLayout],
196 tables: &'a mut [TableLayout],
197 objects: &'a mut [ObjectLayout],
198) -> impl Iterator<Item = &'a mut f32> {
199 let tags = |tags: &'a mut Option<BlockTags>| {
200 tags.iter_mut()
201 .flat_map(|tags| &mut tags.tags)
202 .map(|tag| &mut tag.origin[1])
203 };
204 paragraphs
205 .iter_mut()
206 .map(|paragraph| &mut paragraph.origin[1])
207 .chain(tables.iter_mut().flat_map(move |table| {
208 let cells = table.cells.iter_mut().flat_map(|cell| {
209 let [_, top, _, bottom] = &mut cell.rect;
210 [top, bottom]
211 });
212 cells.chain(tags(&mut table.tags))
213 }))
214 .chain(objects.iter_mut().flat_map(move |object| {
215 let ObjectLayout {
216 rect: [_, top, _, end],
217 bottom,
218 kind,
219 tags: block,
220 ..
221 } = object;
222 let label = match kind {
223 ObjectKind::File(label) | ObjectKind::Unsupported(label) => {
224 Some(&mut label.origin[1])
225 }
226 ObjectKind::Picture | ObjectKind::Ink(_) => None,
227 };
228 [top, end, bottom]
229 .into_iter()
230 .chain(label)
231 .chain(tags(block))
232 }))
233}
234
235/// OneNote centers a file's icon and name in a column this wide.
236const ATTACHMENT_WIDTH: f32 = 54.0;
237/// The grey OneNote gives secondary text such as the page date (its `PageDateTime` style).
238const PLACEHOLDER: u32 = 0x0080_8080;
239
240#[derive(Clone)]
241pub struct TableLayout {
242 pub id: ExGuid,
243 /// Cells are in paragraph order with disjoint visible ranges.
244 pub cells: Vec<CellLayout>,
245 pub borders: bool,
246 pub(crate) tags: Option<BlockTags>,
247}
248
249#[derive(Clone)]
250pub struct CellLayout {
251 pub id: ExGuid,
252 pub rect: [f32; 4],
253 /// Visible paragraph indices, including paragraphs in nested tables.
254 pub(crate) paragraphs: std::ops::Range<usize>,
255}
256
257impl CellLayout {
258 /// Native ink gutters extend beyond the cell borders.
259 pub(crate) fn text_bounds(&self) -> [f32; 4] {
260 [
261 self.rect[0] - 2.7,
262 self.rect[1],
263 self.rect[2] + 4.62,
264 self.rect[3],
265 ]
266 }
267
268 pub(crate) fn clip(&self, rect: parley::BoundingBox) -> Option<parley::BoundingBox> {
269 let [left, top, right, bottom] = self.text_bounds().map(f64::from);
270 let rect = parley::BoundingBox::new(
271 rect.x0.max(left),
272 rect.y0.max(top),
273 rect.x1.min(right),
274 rect.y1.min(bottom),
275 );
276 (rect.width() > 0.0 && rect.height() > 0.0).then_some(rect)
277 }
278}
279
280#[derive(Clone)]
281pub struct ParagraphLayout {
282 pub id: ExGuid,
283 pub origin: [f32; 2],
284 pub projection: TextProjection,
285 pub text: TextLayout,
286 /// Marker x is outline-local; y is paragraph-local so reflow cannot accumulate rounding drift.
287 pub markers: Vec<(TextLayout, [f32; 2])>,
288 /// A numbered paragraph's number, and whether its list restarts the count instead of
289 /// continuing from the previous sibling's.
290 pub(crate) number: Option<(Count, bool)>,
291 pub tags: Vec<ParagraphTag>,
292 /// Its siblings' group, whose list markers its tags clear: the parent paragraph, or the
293 /// table cell holding a cell's top-level paragraph.
294 pub(crate) parent: Option<ExGuid>,
295 /// The equations drawn in two dimensions in spaces of the text, in order.
296 pub math: Vec<crate::math::MathLayout>,
297 /// A highlight of the whole paragraph (its own `highlight`), COLORREF: a band across its
298 /// outline behind its lines, as OneNote marks a conflict page's conflicting changes.
299 pub(crate) band: Option<u32>,
300}
301
302/// How the page draws a note tag.
303#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
304pub enum TagIcon {
305 /// Symbol `shape` of MS-ONE's NoteTagShape, `checked` only where it is a check box.
306 Symbol { shape: u16, checked: bool },
307 /// An Outlook task, drawn with its stored follow-up flag and never checked from the page.
308 Task { shape: u16 },
309}
310
311impl TagIcon {
312 /// How the page draws tag symbol `shape`, `checked` where it is a check box; none for
313 /// shape 0, which marks the text instead.
314 pub fn of(shape: u16, checked: bool) -> Option<Self> {
315 (shape != 0).then_some(Self::Symbol {
316 shape,
317 checked: checked && checkable(shape),
318 })
319 }
320
321 pub(crate) fn checkable(self) -> bool {
322 matches!(self, Self::Symbol { shape, .. } if checkable(shape))
323 }
324}
325
326/// Whether tag symbol `shape` is a check box, as MS-ONE's NoteTagShape marks them.
327pub fn checkable(shape: u16) -> bool {
328 matches!(
329 shape,
330 1..=12 | 28 | 30 | 32 | 48 | 50 | 52 | 69 | 71 | 73 | 89..=99
331 )
332}
333
334/// OneNote 2010's name for tag symbol `shape`, as its symbol gallery's tooltips give it; the
335/// follow-up flags it keeps for Outlook tasks take MS-ONE's descriptions.
336pub fn symbol_name(shape: u16) -> Option<&'static str> {
337 SYMBOL_NAMES
338 .get(usize::from(shape).checked_sub(1)?)
339 .copied()
340}
341
342const SYMBOL_NAMES: [&str; 143] = [
343 "Green Check Box",
344 "Yellow Check Box",
345 "Blue Check Box",
346 "Green Star Check Box",
347 "Yellow Star Check Box",
348 "Blue Star Check Box",
349 "Green Exclamation Check Box",
350 "Yellow Exclamation Check Box",
351 "Blue Exclamation Check Box",
352 "Green Arrow Check Box",
353 "Yellow Arrow Check Box",
354 "Blue Arrow Check Box",
355 "Yellow Star",
356 "Follow-up Flag",
357 "Question",
358 "Blue Right Arrow",
359 "High Priority",
360 "Telephone",
361 "Calendar",
362 "Clock",
363 "Light Bulb",
364 "Pushpin",
365 "Home",
366 "Comment",
367 "Smiley",
368 "Award Ribbon",
369 "Key",
370 "Blue Check Box 1",
371 "Blue Circle 1",
372 "Blue Check Box 2",
373 "Blue Circle 2",
374 "Blue Check Box 3",
375 "Blue Circle 3",
376 "Blue 8-Point Star",
377 "Blue Check Mark",
378 "Blue Circle",
379 "Blue Down Arrow",
380 "Blue Left Arrow",
381 "Blue Solid Target",
382 "Blue Star",
383 "Blue Sun",
384 "Blue Target",
385 "Blue Triangle",
386 "Blue Umbrella",
387 "Blue Up Arrow",
388 "Blue X with Dots",
389 "Blue X",
390 "Green Check Box 1",
391 "Green Circle 1",
392 "Green Check Box 2",
393 "Green Circle 2",
394 "Green Check Box 3",
395 "Green Circle 3",
396 "Green 8-Point Star",
397 "Green Check Mark",
398 "Green Circle",
399 "Green Down Arrow",
400 "Green Left Arrow",
401 "Green Right Arrow",
402 "Green Solid Target",
403 "Green Star",
404 "Green Sun",
405 "Green Target",
406 "Green Triangle",
407 "Green Umbrella",
408 "Green Up Arrow",
409 "Green X with Dots",
410 "Green X",
411 "Yellow Check Box 1",
412 "Yellow Circle 1",
413 "Yellow Check Box 2",
414 "Yellow Circle 2",
415 "Yellow Check Box 3",
416 "Yellow Circle 3",
417 "Yellow 8-Point Star",
418 "Yellow Check Mark",
419 "Yellow Circle",
420 "Yellow Down Arrow",
421 "Yellow Left Arrow",
422 "Yellow Right Arrow",
423 "Yellow Solid Target",
424 "Yellow Sun",
425 "Yellow Target",
426 "Yellow Triangle",
427 "Yellow Umbrella",
428 "Yellow Up Arrow",
429 "Yellow X with Dots",
430 "Yellow X",
431 "Follow Up Today Flag",
432 "Follow Up Tomorrow Flag",
433 "Follow Up This Week Flag",
434 "Follow Up Next Week Flag",
435 "No Follow Up Date Flag",
436 "To Do Blue Person",
437 "To Do Yellow Person",
438 "To Do Green Person",
439 "To Do Blue Flag",
440 "To Do Yellow Flag",
441 "To Do Green Flag",
442 "Red Square (Project A)",
443 "Yellow Square (Project B)",
444 "Blue Square (Project C)",
445 "Green Square",
446 "Orange Square",
447 "Pink Square",
448 "E-mail",
449 "Envelope (Closed)",
450 "Envelope (Opened)",
451 "Mobile phone",
452 "Telephone with Clock",
453 "Question Balloon",
454 "Paperclip",
455 "Frowning",
456 "IM contact",
457 "Person",
458 "Two People",
459 "Reminder Bell",
460 "Contact (like Outlook's)",
461 "Flowers Bouquet",
462 "Date",
463 "Music Note",
464 "Movie Clip",
465 "Quote Mark",
466 "Globe",
467 "Link Globe",
468 "Laptop",
469 "Plane",
470 "Car",
471 "Binoculars",
472 "Presentation",
473 "Padlock",
474 "Book (Open)",
475 "Notebook Icon",
476 "Paper (blank with lines)",
477 "Research Icon",
478 "Marker",
479 "Dollar sign $",
480 "Coins with window behind it",
481 "Schedule Task",
482 "Lighting Bolt",
483 "Cloud",
484 "Heart",
485 "Flower",
486];
487
488#[derive(Clone, Debug, PartialEq)]
489pub struct ParagraphTag {
490 pub icon: TagIcon,
491 /// Coordinates are outline-local in x and paragraph-local in y.
492 pub origin: [f32; 2],
493 /// The icon's side, which follows its paragraph's first run.
494 pub size: f32,
495 pub label: String,
496 pub disabled: bool,
497}
498
499impl ParagraphTag {
500 /// The icon's side beside 10 to 17.5 pt text.
501 pub(crate) const SIZE: f32 = 12.0;
502 /// How far a 12 pt icon starts left of its text when no sibling has a list marker.
503 pub(crate) const INSET: f32 = 20.25;
504 /// Space between a tag and the leftmost list marker among its paragraph's siblings.
505 const MARKER_GAP: f32 = 0.9;
506
507 /// Where an icon of `side` starts below its paragraph's top, for a first line whose
508 /// baseline is `baseline` below it and a first run of `size` points: OneNote 2010 centres
509 /// it 0.357 of the run's size, less 0.2 pt, above the baseline (within 0.75 pt, 8 to 60 pt).
510 fn top(baseline: f32, size: f32, side: f32) -> f32 {
511 baseline - 0.357 * size + 0.2 - side / 2.0
512 }
513
514 /// OneNote 2010's icon side for text of `size` points.
515 fn side(size: f32) -> f32 {
516 match size {
517 24.0.. => 24.0,
518 18.0.. => 18.0,
519 10.0.. => Self::SIZE,
520 _ => 9.0,
521 }
522 }
523
524 /// Where a one-tag column of `side` starts for text at `x` among siblings whose leftmost
525 /// list marker starts at `marker`; its right edge stays put as the icon grows.
526 fn column(x: f32, marker: Option<f32>, side: f32) -> f32 {
527 let right = x - Self::INSET + Self::SIZE;
528 marker.map_or(right, |marker| right.min(marker - Self::MARKER_GAP)) - side
529 }
530}
531
532/// How far a 12 pt icon starts left of a picture or file on the page, as OneNote 2010 draws
533/// one (`corpus/object-tags`).
534const PAGE_INSET: f32 = 24.75;
535
536/// The note tags of a picture or file on the page, whose bounds are `bounds`: oldest first in
537/// a column ending left of it, centred on it; a tag whose definition is missing draws nothing.
538pub(crate) fn object_tags(
539 tags: &[onestore::document::Tag],
540 definitions: &BTreeMap<ExGuid, Definition>,
541 [x, top, _, bottom]: [f32; 4],
542) -> Vec<ParagraphTag> {
543 let side = ParagraphTag::side(crate::layout::DEFAULT_FONT_SIZE);
544 let mut drawn: Vec<ParagraphTag> = tags
545 .iter()
546 .rev()
547 .filter_map(|tag| {
548 let (icon, label) = if tag.status & 4 != 0 {
549 let shape = tag.shape.unwrap_or(0);
550 (TagIcon::Task { shape }, None)
551 } else {
552 let Kind::TagDefinition { shape, label, .. } =
553 &definitions.get(tag.definition.as_ref()?)?.kind
554 else {
555 return None;
556 };
557 let checked = tag.status & 1 != 0;
558 (TagIcon::of(shape.unwrap_or(0), checked)?, label.as_deref())
559 };
560 Some(ParagraphTag {
561 icon,
562 origin: [0.0, (top + bottom - side) / 2.0],
563 size: side,
564 label: label.unwrap_or_default().to_owned(),
565 disabled: tag.status & 2 != 0,
566 })
567 })
568 .collect();
569 let right = x - PAGE_INSET + ParagraphTag::SIZE;
570 let count = drawn.len() as f32;
571 for (index, tag) in drawn.iter_mut().enumerate() {
572 tag.origin[0] = right - side * (count - index as f32);
573 }
574 drawn
575}
576
577/// How much further than its list spacing OneNote 2010 sets a marker's advance from its text.
578const MARKER_OFFSET: f32 = 3.9;
579/// Points an equation's linear text lays out at, holding the caret without showing.
580const HELD: f32 = 0.01;
581
582/// A one-run paragraph standing in for an object's caption, so it lays out through the same
583/// shaping (and caching) as the outline's text.
584fn caption(id: ExGuid, text_id: ExGuid, text: &str, format: Format) -> PageParagraph {
585 PageParagraph {
586 id,
587 parent: None,
588 level: 1,
589 style: None,
590 format: Format::default(),
591 content: ParagraphContent::Text(onestore::page::TextObject {
592 id: text_id,
593 date_field: None,
594 text: Paragraph::new(text.into(), format),
595 tags: Vec::new(),
596 }),
597 lists: Vec::new(),
598 tags: Vec::new(),
599 media: Default::default(),
600 collapsed: false,
601 }
602}
603
604/// A file's column at `x`, before it is placed: its icon centred over its name, which OneNote
605/// shows without the extension; and the icon's height.
606fn file_column(
607 file: &onestore::page::Attachment,
608 paragraph: ExGuid,
609 format: &Format,
610 x: f32,
611 shape: &mut impl FnMut(
612 &PageParagraph,
613 Option<&Count>,
614 f32,
615 &[f32],
616 ) -> Result<ParagraphLayout, LayoutError>,
617) -> Result<(ObjectLayout, f32), LayoutError> {
618 let [w, h] = file.size.unwrap_or([24.0, 24.0]);
619 let left = x + (ATTACHMENT_WIDTH - w) / 2.0;
620 let name = std::path::Path::new(&file.filename)
621 .file_stem()
622 .and_then(|stem| stem.to_str())
623 .unwrap_or(&file.filename);
624 let format = Format {
625 alignment: Some(1),
626 ..format.clone()
627 };
628 let mut label = shape(
629 &caption(paragraph, file.id, name, format),
630 None,
631 ATTACHMENT_WIDTH,
632 &[0.0, 0.0],
633 )?;
634 label.reset_origin(x);
635 let object = ObjectLayout {
636 id: file.id,
637 rect: [left, 0.0, left + w, 0.0],
638 kind: ObjectKind::File(label),
639 bottom: 0.0,
640 tags: None,
641 };
642 Ok((object, h))
643}
644
645/// A file on the page as OneNote draws one, relative to its position: the column a
646/// paragraph's file takes, with its text in the page's default format.
647pub(crate) fn page_file(
648 engine: &mut TextEngine,
649 file: &onestore::page::Attachment,
650) -> Result<ObjectLayout, LayoutError> {
651 let (mut object, height) = file_column(
652 file,
653 file.id,
654 &Format::default(),
655 0.0,
656 &mut |paragraph, previous, width, indents| {
657 ParagraphLayout::shape(
658 engine,
659 paragraph,
660 previous,
661 width,
662 indents,
663 &BTreeMap::new(),
664 )
665 },
666 )?;
667 object.place(0.0, height);
668 Ok(object)
669}
670
671/// A picture's displayed size: the user-set layout size, else its intrinsic size.
672pub(crate) fn image_size(image: &onestore::page::Image) -> Option<[f32; 2]> {
673 let size = [
674 image.layout.max_width.or(image.size.map(|s| s[0]))?,
675 image.layout.max_height.or(image.size.map(|s| s[1]))?,
676 ];
677 size.iter()
678 .all(|v| v.is_finite() && *v > 0.0)
679 .then_some(size)
680}
681
682/// The step OneNote gives each level past the end of an indentation table.
683const MISSING_INDENT: f64 = 27.0;
684
685/// Text offset of `level` from its outline: entry `n` of `indents` steps level `n` in from
686/// level `n - 1`; entry 0 moves nothing OneNote 2010 draws, text nor markers.
687pub(crate) fn indentation(level: u32, indents: &[f32], width: f32) -> Result<f32, LayoutError> {
688 if indents.iter().any(|v| !v.is_finite() || *v < 0.0) || level == 0 {
689 return Err(LayoutError::InvalidIndentation);
690 }
691 let known = (level as usize).min(indents.len().saturating_sub(1));
692 let indent = (indents
693 .get(1..=known)
694 .unwrap_or_default()
695 .iter()
696 .map(|v| f64::from(*v))
697 .sum::<f64>()
698 + f64::from(level - known as u32) * MISSING_INDENT) as f32;
699 if !indent.is_finite() || indent >= width {
700 return Err(LayoutError::InvalidIndentation);
701 }
702 Ok(indent)
703}
704
705/// A numbered paragraph's number and its NumberListFormat.
706#[derive(Clone, Debug, PartialEq)]
707pub(crate) struct Count {
708 pub(crate) number: u32,
709 format: Arc<str>,
710}
711
712/// The number after a previous sibling's in the same `format`, or the first. A number in
713/// another format starts again at 1 even where one in this format came before it; siblings
714/// without a number leave the count alone (`evidence/toolbar-17/restart.txt`).
715fn next(previous: Option<&Count>, format: &str) -> u32 {
716 previous
717 .filter(|count| &*count.format == format)
718 .map_or(1, |count| count.number.saturating_add(1))
719}
720
721/// The number a paragraph laid out as `number` takes once it follows `previous`.
722fn follows(number: Option<&(Count, bool)>, previous: Option<&Count>) -> Option<Count> {
723 number.map(|(count, restart)| Count {
724 number: if *restart {
725 count.number
726 } else {
727 next(previous, &count.format)
728 },
729 format: count.format.clone(),
730 })
731}
732
733/// What the numbered sibling after `node`, numbered `number`, continues from: siblings without
734/// a number leave the count alone, as do empty numbered ones after another number
735/// (`evidence/structural-edits/xml/c8-bs-1.xml`, `c8-enter-empty-1.xml`, `c2s-num-1.xml`).
736fn tally(node: &PageParagraph, number: Option<Count>, previous: Option<Count>) -> Option<Count> {
737 number
738 .filter(|_| {
739 previous.is_none() || node.text().is_some_and(|text| !text.text.text().is_empty())
740 })
741 .or(previous)
742}
743
744/// Removes deeper paragraphs' entries from a flow's `(level, count)` stack of the latest
745/// paragraph at each level, and the previous sibling's at `level`, returning that one's count.
746fn sibling(siblings: &mut Vec<(u32, Option<Count>)>, level: u32) -> Option<Count> {
747 while siblings.last().is_some_and(|(deeper, _)| *deeper > level) {
748 siblings.pop();
749 }
750 match siblings.last() {
751 Some((same, _)) if *same == level => siblings.pop().unwrap().1,
752 _ => None,
753 }
754}
755
756/// `text` in `font` as a Unicode font shows it. Windows' Symbol font is symbol-encoded, so
757/// its bytes map through the Adobe Symbol encoding (a bullet is `U+00B7` there); Wingdings'
758/// map into the private use area their symbol cmaps cover, which also keeps a shaper from
759/// dropping `U+00AD` as a soft hyphen.
760pub fn symbol_text(font: &str, text: &str) -> String {
761 /// The Adobe Symbol encoding from 0x20 and from 0xA0, after Unicode's `SYMBOL.TXT`.
762 const SYMBOL: [&str; 2] = [
763 " !∀#∃%&∋()∗+,−./0123456789:;<=>?≅ΑΒΧΔΕΦΓΗΙϑΚΛΜΝΟΠΘΡΣΤΥςΩΞΨΖ[∴]⊥_‾αβχδεφγηιϕκλμνοπθρστυϖωξψζ{|}∼",
764 "€ϒ′≤⁄∞ƒ♣♦♥♠↔←↑→↓°±″≥×∝∂•÷≠≡≈…⏐⎯↵ℵℑℜ℘⊗⊕∅∩∪⊃⊇⊄⊂⊆∈∉∠∇®©™∏√⋅¬∧∨⇔⇐⇑⇒⇓◊〈®©™∑⎛⎜⎝⎡⎢⎣⎧⎨⎩⎪\u{f8ff}〉∫⌠⎮⌡⎞⎟⎠⎤⎥⎦⎫⎬⎭",
765 ];
766 if font.eq_ignore_ascii_case("Symbol") {
767 text.chars()
768 .map(|c| {
769 let (range, from) = match u32::from(c) {
770 code @ 0x20..0x7f => (SYMBOL[0], code - 0x20),
771 code @ 0xa0..0xff => (SYMBOL[1], code - 0xa0),
772 _ => return c,
773 };
774 range.chars().nth(from as usize).unwrap_or(c)
775 })
776 .collect()
777 } else if font.to_ascii_lowercase().starts_with("wingdings") {
778 text.chars()
779 .map(|c| match u32::from(c) {
780 code @ 0x20..0x100 => char::from_u32(0xf000 + code).unwrap_or(c),
781 _ => c,
782 })
783 .collect()
784 } else {
785 text.to_owned()
786 }
787}
788
789/// The marker NumberListFormat `format` shows for `number`.
790pub fn numbered(format: &str, number: u32) -> Result<String, LayoutError> {
791 let (prefix, rest) = format
792 .split_once('\u{fffd}')
793 .ok_or(LayoutError::InvalidList)?;
794 let mut rest = rest.chars();
795 Ok(format!(
796 "{prefix}{}{}",
797 numeral(rest.next(), number)?,
798 rest.as_str()
799 ))
800}
801
802/// `number` in a list numbering sequence: 0 arabic, 1 and 2 upper and lower roman, 3 and 4
803/// upper and lower letters, 5 ordinal, 6 and 7 cardinal and ordinal words, 22 arabic of two
804/// digits at least, as OneNote 2010's `numberSequence` spells them in English
805/// (`evidence/toolbar-17/sequences.txt`).
806pub(crate) fn numeral(sequence: Option<char>, number: u32) -> Result<String, LayoutError> {
807 let roman = |number: u32| {
808 const DIGITS: [(u32, &str); 13] = [
809 (1000, "M"),
810 (900, "CM"),
811 (500, "D"),
812 (400, "CD"),
813 (100, "C"),
814 (90, "XC"),
815 (50, "L"),
816 (40, "XL"),
817 (10, "X"),
818 (9, "IX"),
819 (5, "V"),
820 (4, "IV"),
821 (1, "I"),
822 ];
823 // Past 3999 the thousands run on as Ms; a million, far past what was observed,
824 // would lay out a thousand of them.
825 if !(1..1_000_000).contains(&number) {
826 return Err(LayoutError::UnsupportedContent);
827 }
828 let mut rest = number;
829 let mut text = String::new();
830 for (value, digits) in DIGITS {
831 while rest >= value {
832 text.push_str(digits);
833 rest -= value;
834 }
835 }
836 Ok(text)
837 };
838 // Past Z a letter repeats, AA to ZZ, up to thirty times, then starts over at A.
839 let letter = |number: u32| {
840 let index = number
841 .checked_sub(1)
842 .ok_or(LayoutError::UnsupportedContent)?;
843 let letter = char::from(b'A' + (index % 26) as u8);
844 Ok(std::iter::repeat_n(letter, (index / 26 % 30) as usize + 1).collect::<String>())
845 };
846 match sequence.map(u32::from) {
847 Some(0) => Ok(number.to_string()),
848 Some(22) => Ok(format!("{number:02}")),
849 Some(1) => roman(number),
850 Some(2) => roman(number).map(|text| text.to_lowercase()),
851 Some(3) => letter(number),
852 Some(4) => letter(number).map(|text| text.to_lowercase()),
853 Some(5) => {
854 let suffix = match (number % 10, number % 100) {
855 (_, 11..=13) => "th",
856 (1, _) => "st",
857 (2, _) => "nd",
858 (3, _) => "rd",
859 _ => "th",
860 };
861 Ok(format!("{number}{suffix}"))
862 }
863 Some(sequence @ (6 | 7)) => {
864 let mut words = cardinal(number)?;
865 if sequence == 7 {
866 // The last word turns ordinal: one to first, twenty to twentieth.
867 let at = words.rfind([' ', '-']).map_or(0, |at| at + 1);
868 let last = &words[at..];
869 let ordinal = match last {
870 "one" => "first".to_owned(),
871 "two" => "second".to_owned(),
872 "three" => "third".to_owned(),
873 "five" => "fifth".to_owned(),
874 "eight" => "eighth".to_owned(),
875 "nine" => "ninth".to_owned(),
876 "twelve" => "twelfth".to_owned(),
877 _ if last.ends_with('y') => format!("{}ieth", &last[..last.len() - 1]),
878 _ => format!("{last}th"),
879 };
880 words.replace_range(at.., &ordinal);
881 }
882 let mut chars = words.chars();
883 let first = chars.next().unwrap_or_default();
884 Ok(first.to_uppercase().chain(chars).collect())
885 }
886 _ => Err(LayoutError::UnsupportedContent),
887 }
888}
889
890/// `number` in lowercase English words, "one hundred twenty-one", below a million.
891fn cardinal(number: u32) -> Result<String, LayoutError> {
892 const SMALL: [&str; 20] = [
893 "zero",
894 "one",
895 "two",
896 "three",
897 "four",
898 "five",
899 "six",
900 "seven",
901 "eight",
902 "nine",
903 "ten",
904 "eleven",
905 "twelve",
906 "thirteen",
907 "fourteen",
908 "fifteen",
909 "sixteen",
910 "seventeen",
911 "eighteen",
912 "nineteen",
913 ];
914 const TENS: [&str; 10] = [
915 "", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety",
916 ];
917 fn below_thousand(number: u32) -> String {
918 let (hundreds, rest) = (number / 100, number % 100);
919 let rest = match rest {
920 0 => None,
921 1..20 => Some(SMALL[rest as usize].to_owned()),
922 _ if rest % 10 == 0 => Some(TENS[(rest / 10) as usize].to_owned()),
923 _ => Some(format!(
924 "{}-{}",
925 TENS[(rest / 10) as usize],
926 SMALL[(rest % 10) as usize]
927 )),
928 };
929 match (hundreds, rest) {
930 (0, rest) => rest.unwrap_or_default(),
931 (hundreds, None) => format!("{} hundred", SMALL[hundreds as usize]),
932 (hundreds, Some(rest)) => format!("{} hundred {rest}", SMALL[hundreds as usize]),
933 }
934 }
935 match number {
936 1..1000 => Ok(below_thousand(number)),
937 1000..1_000_000 => {
938 let (thousands, rest) = (number / 1000, number % 1000);
939 let head = format!("{} thousand", below_thousand(thousands));
940 Ok(if rest == 0 {
941 head
942 } else {
943 format!("{head} {}", below_thousand(rest))
944 })
945 }
946 _ => Err(LayoutError::UnsupportedContent),
947 }
948}
949
950fn spacing(format: &Format) -> Result<[f32; 2], LayoutError> {
951 let spacing = [format.space_before, format.space_after].map(|space| space.unwrap_or(0.0));
952 if spacing.iter().any(|v| !v.is_finite() || *v < 0.0) {
953 return Err(LayoutError::InvalidSpacing);
954 }
955 Ok(spacing)
956}
957
958/// Where a node with `spacing` starts after the flow `state` of a bottom and pending spacing.
959fn top(state: &mut (f64, Option<f32>), [before, after]: [f32; 2]) -> f32 {
960 if let Some(previous) = state.1 {
961 state.0 += f64::from(previous.max(before));
962 }
963 state.1 = Some(after);
964 state.0 as f32
965}
966
967impl ParagraphLayout {
968 pub(crate) fn reset_origin(&mut self, x: f32) {
969 let offset = x - self.origin[0];
970 self.origin = [x, 0.0];
971 for (_, origin) in &mut self.markers {
972 origin[0] += offset;
973 }
974 for tag in &mut self.tags {
975 tag.origin[0] += offset;
976 }
977 }
978
979 fn size(&self) -> [f32; 2] {
980 [
981 self.origin[0] + self.text.shaped.width(),
982 self.markers
983 .iter()
984 .map(|(layout, _)| layout.height())
985 .fold(self.text.height(), f32::max),
986 ]
987 }
988
989 /// Lays out a text paragraph after a sibling numbered `previous`.
990 pub(crate) fn shape(
991 engine: &mut TextEngine,
992 paragraph: &PageParagraph,
993 previous: Option<&Count>,
994 width: f32,
995 indents: &[f32],
996 definitions: &BTreeMap<ExGuid, Definition>,
997 ) -> Result<Self, LayoutError> {
998 if !width.is_finite() || width <= 0.0 {
999 return Err(LayoutError::InvalidWidth);
1000 }
1001 let indent = indentation(paragraph.level, indents, width)?;
1002 let source = paragraph.text().ok_or(LayoutError::UnsupportedContent)?;
1003 let projection = source
1004 .text
1005 .project()
1006 .map_err(|_| LayoutError::InvalidSourceRange)?;
1007 let format = &projection.text().spans()[0].format;
1008 let spacing = format.list_spacing.unwrap_or(7.2);
1009 if !spacing.is_finite() || spacing < 0.0 {
1010 return Err(LayoutError::InvalidSpacing);
1011 }
1012 // Stored newest first; OneNote lists and paints tags oldest first.
1013 // An Outlook task tag carries no definition but its own icon; a tag whose definition
1014 // is missing shows nothing, as OneNote draws no icon for it.
1015 let mut tag_definitions: Vec<_> = paragraph
1016 .tags
1017 .iter()
1018 .chain(&source.tags)
1019 .filter_map(|tag| {
1020 if tag.status & 4 != 0 {
1021 return Some((tag, None, None, None, None));
1022 }
1023 match &definitions.get(tag.definition.as_ref()?)?.kind {
1024 Kind::TagDefinition {
1025 shape,
1026 label,
1027 color,
1028 highlight,
1029 ..
1030 } => Some((tag, Some(*shape), label.as_deref(), *color, *highlight)),
1031 _ => None,
1032 }
1033 })
1034 .collect();
1035 tag_definitions.reverse();
1036 // The newest tag that sets a colour wins.
1037 let color = tag_definitions.iter().rev().find_map(|tag| tag.3);
1038 let highlight = tag_definitions.iter().rev().find_map(|tag| tag.4);
1039 // Equations with objects draw in two dimensions in a space kept in their line; their
1040 // linear text stays in the text, too small to see, to hold the caret.
1041 let visible = projection.text();
1042 let mut equations = Vec::new();
1043 for zone in crate::math::built(visible) {
1044 let units = |byte| {
1045 visible
1046 .utf16_offset(byte)
1047 .map_err(|_| LayoutError::InvalidSourceRange)
1048 };
1049 let math = visible
1050 .slice(units(zone.start)?..units(zone.end)?)
1051 .map_err(|_| LayoutError::InvalidSourceRange)?;
1052 // An equation that does not parse shows its linear text.
1053 if let Ok(math) = crate::math::layout(engine, &math) {
1054 equations.push((zone, math));
1055 }
1056 }
1057 let spaces: Vec<_> = equations
1058 .iter()
1059 .map(|(zone, math)| InlineSpace {
1060 index: zone.start,
1061 width: math.size[0],
1062 ascent: math.baseline,
1063 descent: math.size[1] - math.baseline,
1064 })
1065 .collect();
1066 // URL text shows as a link, as OneNote links it on opening a page.
1067 let mut marks: Vec<(Range<usize>, bool)> = crate::editor::shown_urls(visible)
1068 .into_iter()
1069 .map(|url| (url, false))
1070 .chain(equations.iter().map(|(zone, _)| (zone.clone(), true)))
1071 .collect();
1072 marks.sort_by_key(|(range, _)| range.start);
1073 let mut text = if color.is_none() && highlight.is_none() && marks.is_empty() {
1074 engine.layout(visible, width - indent)?
1075 } else {
1076 let mut start = 0;
1077 let runs = visible.spans().iter().flat_map(|span| {
1078 let mut format = span.format.clone();
1079 format.color = color.or(format.color);
1080 format.highlight = highlight.or(format.highlight);
1081 let mut runs = Vec::new();
1082 let mut from = start;
1083 for (range, equation) in &marks {
1084 let (a, b) = (
1085 range.start.clamp(from, span.end),
1086 range.end.clamp(from, span.end),
1087 );
1088 if a < b {
1089 runs.push((visible.text()[from..a].to_owned(), format.clone()));
1090 let marked = if *equation {
1091 Format {
1092 font_size: Some(HELD),
1093 superscript: None,
1094 subscript: None,
1095 ..format.clone()
1096 }
1097 } else {
1098 Format {
1099 hyperlink: Some(true),
1100 ..format.clone()
1101 }
1102 };
1103 runs.push((visible.text()[a..b].to_owned(), marked));
1104 from = b;
1105 }
1106 }
1107 runs.push((visible.text()[from..span.end].to_owned(), format));
1108 start = span.end;
1109 runs
1110 });
1111 engine.layout_with(
1112 &Paragraph::from_runs(
1113 runs.filter(|(text, _)| !text.is_empty())
1114 .collect::<Vec<_>>(),
1115 ),
1116 width - indent,
1117 &spaces,
1118 )?
1119 };
1120 let mut markers = Vec::new();
1121 let mut marker_x = indent - MARKER_OFFSET;
1122 let mut number = None;
1123 for id in paragraph.lists.iter().rev() {
1124 let definition = definitions.get(id).ok_or(LayoutError::InvalidList)?;
1125 let Kind::List {
1126 font,
1127 format: Some(value),
1128 restart,
1129 ..
1130 } = &definition.kind
1131 else {
1132 return Err(LayoutError::InvalidList);
1133 };
1134 let mut color = definition.format.color;
1135 let value = if value.contains('\u{fffd}') {
1136 // An empty numbered paragraph shows its number as a gray placeholder.
1137 if source.text.text().is_empty() {
1138 color = Some(PLACEHOLDER);
1139 }
1140 let current = restart.unwrap_or(next(previous, value));
1141 let format = value.as_str().into();
1142 number = Some((
1143 Count {
1144 number: current,
1145 format,
1146 },
1147 restart.is_some(),
1148 ));
1149 numbered(value, current)?
1150 } else {
1151 value.clone()
1152 };
1153 if value.contains('\u{fffd}') {
1154 return Err(LayoutError::UnsupportedContent);
1155 }
1156 // Without a font of its own a marker takes its text's, as a number does.
1157 let font = font
1158 .clone()
1159 .or_else(|| definition.format.font.clone())
1160 .or_else(|| format.font.clone());
1161 let value = font
1162 .as_deref()
1163 .map_or(value.clone(), |font| symbol_text(font, &value));
1164 let marker = Paragraph::new(
1165 value,
1166 Format {
1167 font,
1168 font_size: definition.format.font_size.or(format.font_size),
1169 color,
1170 ..Format::default()
1171 },
1172 );
1173 let layout = engine.layout(&marker, width)?;
1174 if layout.lines().count() != 1 {
1175 return Err(LayoutError::InvalidList);
1176 }
1177 let (line, metrics) = layout.lines().next().unwrap();
1178 marker_x -= spacing + line.metrics().advance;
1179 if !marker_x.is_finite() {
1180 return Err(LayoutError::InvalidSpacing);
1181 }
1182 let y = text.lines().next().unwrap().1.baseline - metrics.baseline;
1183 markers.push((layout, [marker_x, y]));
1184 }
1185 text.minimum_line_height(format.line_spacing.unwrap_or(0.0))?;
1186 let baseline = text.lines().next().unwrap().1.baseline;
1187 let mut tags = Vec::new();
1188 for tag in &tag_definitions {
1189 let (tag, shape, label) = (tag.0, tag.1, tag.2);
1190 let icon = match shape {
1191 None => TagIcon::Task {
1192 shape: tag.shape.unwrap_or(0),
1193 },
1194 Some(shape) => match TagIcon::of(shape.unwrap_or(0), tag.status & 1 != 0) {
1195 Some(icon) => icon,
1196 None => continue,
1197 },
1198 };
1199 let size = format.font_size.unwrap_or(crate::layout::DEFAULT_FONT_SIZE);
1200 let side = ParagraphTag::side(size);
1201 tags.push(ParagraphTag {
1202 icon,
1203 origin: [0.0, ParagraphTag::top(baseline, size, side)],
1204 size: side,
1205 label: label.unwrap_or_default().to_owned(),
1206 disabled: tag.status & 2 != 0,
1207 });
1208 }
1209 let math = equations.into_iter().map(|(_, math)| math).collect();
1210 let mut result = Self {
1211 id: paragraph.id,
1212 origin: [indent, 0.0],
1213 projection,
1214 text,
1215 markers,
1216 number,
1217 tags,
1218 math,
1219 parent: paragraph.parent,
1220 band: paragraph.format.highlight,
1221 };
1222 result.place_tags(result.marker_left());
1223 Ok(result)
1224 }
1225
1226 fn marker_left(&self) -> Option<f32> {
1227 self.markers.iter().map(|(_, [x, _])| *x).reduce(f32::min)
1228 }
1229
1230 fn place_tags(&mut self, marker: Option<f32>) {
1231 place_tags(&mut self.tags, self.origin[0], marker);
1232 }
1233}
1234
1235/// Places the tags of content at `x` among siblings whose leftmost marker starts at
1236/// `marker`; later tags follow the first to its right, toward the content.
1237fn place_tags(tags: &mut [ParagraphTag], x: f32, marker: Option<f32>) {
1238 for (index, tag) in tags.iter_mut().enumerate() {
1239 tag.origin[0] = ParagraphTag::column(x, marker, tag.size) + tag.size * index as f32;
1240 }
1241}
1242
1243pub(crate) fn visible_paragraphs<'a>(
1244 nodes: impl Iterator<Item = &'a PageParagraph>,
1245) -> impl Iterator<Item = &'a PageParagraph> {
1246 let mut hidden = BTreeSet::new();
1247 nodes.filter(move |paragraph| {
1248 let invisible = paragraph
1249 .parent
1250 .is_some_and(|parent| hidden.contains(&parent));
1251 if invisible || paragraph.collapsed {
1252 hidden.insert(paragraph.id);
1253 }
1254 !invisible
1255 })
1256}
1257
1258impl OutlineLayout {
1259 /// OneNote gives an outline one tag column, as wide as its most-tagged paragraph needs; each
1260 /// paragraph's tags start at the column's left edge. Tag origins assume a one-tag column.
1261 pub(crate) fn tag_column_offset(&self) -> f32 {
1262 -self
1263 .paragraphs
1264 .iter()
1265 .map(|p| p.tags.as_slice())
1266 .chain(self.block_tags().map(|block| block.tags.as_slice()))
1267 .map(|tags| tags.iter().skip(1).map(|tag| tag.size).sum::<f32>())
1268 .fold(0.0, f32::max)
1269 }
1270
1271 fn block_tags(&self) -> impl Iterator<Item = &BlockTags> {
1272 let tables = self.tables.iter().filter_map(|table| table.tags.as_ref());
1273 tables.chain(
1274 self.objects
1275 .iter()
1276 .filter_map(|object| object.tags.as_ref()),
1277 )
1278 }
1279
1280 /// Every note tag the outline draws, with the paragraph it marks and its outline-local
1281 /// origin before `tag_column_offset`.
1282 pub fn tags(&self) -> impl Iterator<Item = (ExGuid, [f32; 2], &ParagraphTag)> {
1283 let text = self.paragraphs.iter().flat_map(|paragraph| {
1284 paragraph.tags.iter().map(move |tag| {
1285 let y = paragraph.origin[1] + tag.origin[1];
1286 (paragraph.id, [tag.origin[0], y], tag)
1287 })
1288 });
1289 let blocks = self.block_tags().flat_map(|block| {
1290 block
1291 .tags
1292 .iter()
1293 .map(|tag| (block.paragraph, tag.origin, tag))
1294 });
1295 text.chain(blocks)
1296 }
1297
1298 /// Innermost table cell containing a visible paragraph index.
1299 pub(crate) fn paragraph_cell(&self, index: usize) -> Option<&CellLayout> {
1300 self.tables.iter().rev().find_map(|table| {
1301 let cell = table
1302 .cells
1303 .partition_point(|cell| cell.paragraphs.end <= index);
1304 table
1305 .cells
1306 .get(cell)
1307 .filter(|cell| cell.paragraphs.contains(&index))
1308 })
1309 }
1310
1311 fn append(&mut self, mut child: Self, origin: [f32; 2]) {
1312 for paragraph in &mut child.paragraphs {
1313 paragraph.origin[0] += origin[0];
1314 paragraph.origin[1] += origin[1];
1315 for (_, marker) in &mut paragraph.markers {
1316 marker[0] += origin[0];
1317 }
1318 for tag in &mut paragraph.tags {
1319 tag.origin[0] += origin[0];
1320 }
1321 }
1322 for table in &mut child.tables {
1323 for cell in &mut table.cells {
1324 cell.paragraphs.start += self.paragraphs.len();
1325 cell.paragraphs.end += self.paragraphs.len();
1326 for (value, offset) in cell.rect.iter_mut().zip(origin.into_iter().cycle()) {
1327 *value += offset;
1328 }
1329 }
1330 if let Some(tags) = &mut table.tags {
1331 tags.offset(origin);
1332 }
1333 }
1334 for object in &mut child.objects {
1335 for (value, offset) in object.rect.iter_mut().zip(origin.into_iter().cycle()) {
1336 *value += offset;
1337 }
1338 object.bottom += origin[1];
1339 if let Some(tags) = &mut object.tags {
1340 tags.offset(origin);
1341 }
1342 if let ObjectKind::File(label) | ObjectKind::Unsupported(label) = &mut object.kind {
1343 let y = label.origin[1];
1344 label.reset_origin(label.origin[0] + origin[0]);
1345 label.origin[1] = y + origin[1];
1346 }
1347 }
1348 self.paragraphs.extend(child.paragraphs);
1349 self.tables.extend(child.tables);
1350 self.objects.extend(child.objects);
1351 }
1352
1353 pub(crate) fn flow<'a>(
1354 nodes: impl Iterator<Item = &'a PageParagraph>,
1355 indents: &[f32],
1356 width: f32,
1357 fixed_width: bool,
1358 depth: usize,
1359 edit: Option<&DocumentEdit>,
1360 shape: &mut impl FnMut(
1361 &PageParagraph,
1362 Option<&Count>,
1363 f32,
1364 &[f32],
1365 ) -> Result<ParagraphLayout, LayoutError>,
1366 ) -> Result<Self, LayoutError> {
1367 let mut result = Self::stack(
1368 nodes,
1369 indents,
1370 width,
1371 depth,
1372 edit,
1373 BTreeSet::new(),
1374 (0.0, None),
1375 &mut Vec::new(),
1376 shape,
1377 )?;
1378 result.size[0] = if fixed_width { width } else { result.size[0] }.max(result.table_width());
1379 result.place_tags();
1380 Ok(result)
1381 }
1382
1383 /// Moves each paragraph's tags clear of the widest list marker among its siblings.
1384 fn place_tags(&mut self) {
1385 let mut markers = BTreeMap::<Option<ExGuid>, f32>::new();
1386 for paragraph in &self.paragraphs {
1387 if let Some(x) = paragraph.marker_left() {
1388 let left = markers.entry(paragraph.parent).or_insert(x);
1389 *left = left.min(x);
1390 }
1391 }
1392 for paragraph in &mut self.paragraphs {
1393 let marker = markers.get(&paragraph.parent).copied();
1394 paragraph.place_tags(marker);
1395 }
1396 let tables = self
1397 .tables
1398 .iter_mut()
1399 .filter_map(|table| table.tags.as_mut());
1400 let objects = self
1401 .objects
1402 .iter_mut()
1403 .filter_map(|object| object.tags.as_mut());
1404 for block in tables.chain(objects) {
1405 place_tags(
1406 &mut block.tags,
1407 block.x,
1408 markers.get(&block.parent).copied(),
1409 );
1410 }
1411 }
1412
1413 /// Lays out `nodes` after the flow `state` and `siblings` stack, hiding the children of
1414 /// `hiding`; at the root, keeps a block for each node.
1415 #[allow(clippy::too_many_arguments)]
1416 fn stack<'a>(
1417 nodes: impl Iterator<Item = &'a PageParagraph>,
1418 indents: &[f32],
1419 width: f32,
1420 depth: usize,
1421 edit: Option<&DocumentEdit>,
1422 mut hiding: BTreeSet<ExGuid>,
1423 mut state: (f64, Option<f32>),
1424 siblings: &mut Vec<(u32, Option<Count>)>,
1425 shape: &mut impl FnMut(
1426 &PageParagraph,
1427 Option<&Count>,
1428 f32,
1429 &[f32],
1430 ) -> Result<ParagraphLayout, LayoutError>,
1431 ) -> Result<Self, LayoutError> {
1432 if !width.is_finite() || width <= 0.0 {
1433 return Err(LayoutError::InvalidWidth);
1434 }
1435 if depth > 64 {
1436 return Err(LayoutError::UnsupportedContent);
1437 }
1438 let mut result = Self {
1439 size: [36.0, 0.0],
1440 widths: [36.0, f32::NEG_INFINITY],
1441 ..Self::default()
1442 };
1443 for node in nodes {
1444 let hidden = node.parent.is_some_and(|parent| hiding.contains(&parent));
1445 if hidden || node.collapsed {
1446 hiding.insert(node.id);
1447 }
1448 let previous = sibling(siblings, node.level);
1449 let mut number = None;
1450 let mut block = Block {
1451 first: [
1452 result.paragraphs.len(),
1453 result.tables.len(),
1454 result.objects.len(),
1455 ],
1456 metrics: None,
1457 extent: [f32::NEG_INFINITY; 2],
1458 state,
1459 rel: Vec::new(),
1460 count: None,
1461 };
1462 if !hidden {
1463 let (space, height, flow, extent) = match &node.content {
1464 ParagraphContent::Text(_) => {
1465 let mut paragraph = shape(node, previous.as_ref(), width, indents)?;
1466 number = paragraph.number.clone().map(|(count, _)| count);
1467 let space = spacing(&paragraph.projection.text().spans()[0].format)?;
1468 paragraph.origin[1] = top(&mut state, space);
1469 let size = paragraph.size();
1470 block.extent[1] = paragraph.origin[0] + paragraph.text.shaped.width();
1471 result.paragraphs.push(paragraph);
1472 (space, size[1], size[1], size[0])
1473 }
1474 ParagraphContent::Table(table) => {
1475 let x = indentation(node.level, indents, width)?;
1476 let space = spacing(&node.format)?;
1477 let y = top(&mut state, space);
1478 let mut child = Self::table(table, depth + 1, edit, shape)?;
1479 child.tables[0].tags =
1480 BlockTags::new(node, &table.tags, 0.0, shape)?.map(|mut tags| {
1481 tags.centre(0.0, child.size[1]);
1482 tags
1483 });
1484 if depth == 0 {
1485 block.rel = verticals(
1486 &mut child.paragraphs,
1487 &mut child.tables,
1488 &mut child.objects,
1489 )
1490 .map(|value| *value)
1491 .collect();
1492 }
1493 let size = child.size;
1494 result.append(child, [x, y]);
1495 (space, size[1], size[1], x + size[0])
1496 }
1497 content => {
1498 let x = indentation(node.level, indents, width)?;
1499 let space = spacing(&node.format)?;
1500 let y = top(&mut state, space);
1501 let (mut object, height, extent) = match content {
1502 ParagraphContent::Image(image) => {
1503 let [w, h] =
1504 image_size(image).ok_or(LayoutError::UnsupportedContent)?;
1505 let object = ObjectLayout {
1506 id: image.id,
1507 rect: [x, 0.0, x + w, 0.0],
1508 kind: ObjectKind::Picture,
1509 bottom: 0.0,
1510 tags: None,
1511 };
1512 (object, h, x + w)
1513 }
1514 ParagraphContent::Attachment(file) => {
1515 let (object, h) =
1516 file_column(file, node.id, &node.format, x, shape)?;
1517 (object, h, x + ATTACHMENT_WIDTH)
1518 }
1519 ParagraphContent::Ink(ink) => {
1520 // The paragraph reaches from its origin to the farthest stroke point.
1521 let [w, h] = ink
1522 .bounds()
1523 .map(|[x, y, w, h]| [x + w, y + h])
1524 .filter(|size| size.iter().all(|v| v.is_finite() && *v >= 0.0))
1525 .ok_or(LayoutError::UnsupportedContent)?;
1526 let object = ObjectLayout {
1527 id: ink.id,
1528 rect: [x, 0.0, x + w, 0.0],
1529 kind: ObjectKind::Ink(ink.clone()),
1530 bottom: 0.0,
1531 tags: None,
1532 };
1533 (object, h, x + w)
1534 }
1535 ParagraphContent::Unsupported(unsupported) => {
1536 let w = unsupported.layout.max_width.unwrap_or(160.0).max(160.0);
1537 let mut label = shape(
1538 &caption(
1539 node.id,
1540 unsupported.id,
1541 "Unsupported content",
1542 Format::default(),
1543 ),
1544 None,
1545 w - 16.0,
1546 &[0.0, 0.0],
1547 )?;
1548 label.reset_origin(x + 8.0);
1549 let h = unsupported
1550 .layout
1551 .max_height
1552 .unwrap_or(0.0)
1553 .max(label.text.height() + 16.0);
1554 let object = ObjectLayout {
1555 id: unsupported.id,
1556 rect: [x, 0.0, x + w, 0.0],
1557 kind: ObjectKind::Unsupported(label),
1558 bottom: 0.0,
1559 tags: None,
1560 };
1561 (object, h, x + w)
1562 }
1563 ParagraphContent::Text(_) | ParagraphContent::Table(_) => {
1564 unreachable!("text and tables are not objects")
1565 }
1566 };
1567 let tags = match content {
1568 ParagraphContent::Image(image) => image.tags.as_slice(),
1569 ParagraphContent::Attachment(file) => file.tags.as_slice(),
1570 _ => &[],
1571 };
1572 object.tags = BlockTags::new(node, tags, x, shape)?;
1573 let flow = object.place(y, height);
1574 result.objects.push(object);
1575 (space, height, flow, extent)
1576 }
1577 };
1578 state.0 += f64::from(flow);
1579 result.size[0] = result.size[0].max(extent);
1580 result.widths[1] = result.widths[1].max(block.extent[1]);
1581 block.extent[0] = extent;
1582 block.metrics = Some([space[0], space[1], height]);
1583 if !(state.0 as f32).is_finite() || !result.size[0].is_finite() {
1584 return Err(LayoutError::InvalidSpacing);
1585 }
1586 }
1587 block.count = tally(node, number, previous);
1588 siblings.push((node.level, block.count.clone()));
1589 block.state = state;
1590 if depth == 0 {
1591 result.blocks.push(block);
1592 }
1593 }
1594 result.size[1] = state.0 as f32;
1595 result.widths[0] = result.size[0];
1596 Ok(result)
1597 }
1598
1599 /// Root node `root`'s number, when it is numbered text on show.
1600 fn number(&self, nodes: &[PageParagraph], root: usize) -> Option<&(Count, bool)> {
1601 let block = &self.blocks[root];
1602 (block.metrics.is_some() && nodes[root].text().is_some())
1603 .then(|| self.paragraphs[block.first[0]].number.as_ref())
1604 .flatten()
1605 }
1606
1607 /// Where each root node's flow starts and ends in the outline, or `None` while a collapsed
1608 /// parent hides it.
1609 pub(crate) fn spans(&self) -> impl Iterator<Item = Option<[f32; 2]>> + '_ {
1610 let mut state = (0.0, None);
1611 self.blocks.iter().map(move |block| {
1612 let span = block
1613 .metrics
1614 .map(|[before, after, _]| [top(&mut state, [before, after]), block.state.0 as f32]);
1615 state = block.state;
1616 span
1617 })
1618 }
1619
1620 /// The paragraphs, tables and objects of root nodes `range`.
1621 pub(crate) fn pieces(&self, range: Range<usize>) -> [Range<usize>; 3] {
1622 let first = |root: usize| {
1623 self.blocks.get(root).map_or(
1624 [self.paragraphs.len(), self.tables.len(), self.objects.len()],
1625 |block| block.first,
1626 )
1627 };
1628 let [start, end] = [first(range.start), first(range.end)];
1629 [0, 1, 2].map(|kind| start[kind]..end[kind])
1630 }
1631
1632 /// Lays out root nodes `range` of `nodes` once `edit` applies, the edit's own or the table
1633 /// holding its cell, and works out where the nodes after them move. Resizing columns of
1634 /// other nodes or showing or hiding them lays out every node; renumbering lays out the
1635 /// renumbered.
1636 #[allow(clippy::too_many_arguments)]
1637 pub(crate) fn relayout(
1638 &self,
1639 nodes: &[PageParagraph],
1640 edit: &DocumentEdit,
1641 range: Range<usize>,
1642 indents: &[f32],
1643 width: f32,
1644 fixed_width: bool,
1645 shape: &mut impl FnMut(
1646 &PageParagraph,
1647 Option<&Count>,
1648 f32,
1649 &[f32],
1650 ) -> Result<ParagraphLayout, LayoutError>,
1651 ) -> Result<Relayout, LayoutError> {
1652 let resized = |node: &PageParagraph| match &node.content {
1653 ParagraphContent::Table(table) => edit.columns.contains_key(&table.id),
1654 _ => false,
1655 };
1656 let mut range = if edit.columns.len()
1657 == descendants(&nodes[range.clone()], None)
1658 .filter(|(_, _, node)| resized(node))
1659 .count()
1660 {
1661 range
1662 } else {
1663 0..nodes.len()
1664 };
1665 loop {
1666 let old = self.pieces(range.clone());
1667 let sources = leaves(&nodes[range.clone()], None)
1668 .map(|(_, _, node)| (node.id, node))
1669 .collect::<BTreeMap<_, _>>();
1670 let cached = self.paragraphs[old[0].clone()]
1671 .iter()
1672 .filter_map(|layout| Some((layout.id, (*sources.get(&layout.id)?, layout))))
1673 .collect::<BTreeMap<_, _>>();
1674 let count = match edit.container {
1675 None => range.len() - edit.range.len() + edit.replacement.len(),
1676 Some(_) => range.len(),
1677 };
1678 let replacement = edited_nodes(nodes, None, Some(edit))
1679 .skip(range.start)
1680 .take(count);
1681 let supplied = replacement
1682 .clone()
1683 .map(|node| node.id)
1684 .collect::<BTreeSet<_>>();
1685 // Children of a hidden or collapsed earlier node stay hidden.
1686 let hiding = replacement
1687 .clone()
1688 .filter_map(|node| {
1689 let parent = node.parent.filter(|parent| !supplied.contains(parent))?;
1690 let root = nodes[..range.start]
1691 .iter()
1692 .rposition(|node| node.id == parent)?;
1693 (self.blocks[root].metrics.is_none() || nodes[root].collapsed).then_some(parent)
1694 })
1695 .collect();
1696 let start = range
1697 .start
1698 .checked_sub(1)
1699 .map_or((0.0, None), |root| self.blocks[root].state);
1700 // Each earlier level's latest paragraph, back to the nearest at the first level.
1701 let mut siblings = Vec::new();
1702 let mut level = u32::MAX;
1703 for root in (0..range.start).rev() {
1704 if nodes[root].level < level {
1705 level = nodes[root].level;
1706 siblings.push((level, self.blocks[root].count.clone()));
1707 if level <= 1 {
1708 break;
1709 }
1710 }
1711 }
1712 siblings.reverse();
1713 let segment = Self::stack(
1714 replacement.clone(),
1715 indents,
1716 width,
1717 0,
1718 Some(edit),
1719 hiding,
1720 start,
1721 &mut siblings,
1722 &mut |node, previous, width, indents| {
1723 let indent = indentation(node.level, indents, width)?;
1724 if let Some((source, cached)) = cached.get(&node.id)
1725 && *source == node
1726 && follows(cached.number.as_ref(), previous)
1727 == cached.number.clone().map(|(count, _)| count)
1728 && cached.text.shaped.layout_max_advance() == width - indent
1729 && cached
1730 .markers
1731 .iter()
1732 .all(|(marker, _)| marker.shaped.layout_max_advance() == width)
1733 {
1734 let mut result = (*cached).clone();
1735 result.reset_origin(indent);
1736 return Ok(result);
1737 }
1738 shape(node, previous, width, indents)
1739 },
1740 )?;
1741 let hides =
1742 |block: &Block, node: &PageParagraph| block.metrics.is_none() || node.collapsed;
1743 if range.end < nodes.len() {
1744 let now = replacement
1745 .zip(&segment.blocks)
1746 .map(|(node, block)| (node.id, hides(block, node)))
1747 .collect::<BTreeMap<_, _>>();
1748 if nodes[range.clone()]
1749 .iter()
1750 .zip(&self.blocks[range.clone()])
1751 .any(|(node, block)| {
1752 now.get(&node.id)
1753 .is_some_and(|now| *now != hides(block, node))
1754 })
1755 {
1756 range = 0..nodes.len();
1757 continue;
1758 }
1759 }
1760 let mut renumbered = range.end;
1761 for root in range.end..nodes.len() {
1762 let previous = sibling(&mut siblings, nodes[root].level);
1763 let number = self.number(nodes, root);
1764 let now = follows(number, previous.as_ref());
1765 let counted = tally(&nodes[root], now.clone(), previous);
1766 if now.as_ref() != number.map(|(count, _)| count)
1767 || counted != self.blocks[root].count
1768 {
1769 renumbered = root + 1;
1770 }
1771 siblings.push((nodes[root].level, counted));
1772 if nodes[root].level <= 1 && renumbered <= root {
1773 break;
1774 }
1775 }
1776 if renumbered > range.end {
1777 range.end = renumbered;
1778 continue;
1779 }
1780 let mut state = segment.blocks.last().map_or(start, |block| block.state);
1781 let mut incoming = range
1782 .end
1783 .checked_sub(1)
1784 .map_or((0.0, None), |root| self.blocks[root].state);
1785 let mut moved = Vec::new();
1786 for root in range.end..self.blocks.len() {
1787 if state == incoming {
1788 break;
1789 }
1790 let block = &self.blocks[root];
1791 incoming = block.state;
1792 let mut y = 0.0;
1793 if let Some([before, after, height]) = block.metrics {
1794 y = top(&mut state, [before, after]);
1795 let [_, tables, objects] = self.pieces(root..root + 1);
1796 let flow = match self.objects[objects].first() {
1797 Some(object) if tables.is_empty() => object.at(y, height).1,
1798 _ => height,
1799 };
1800 state.0 += f64::from(flow);
1801 if !(state.0 as f32).is_finite() {
1802 return Err(LayoutError::InvalidSpacing);
1803 }
1804 }
1805 moved.push((y, state));
1806 }
1807 let bottom = if moved.len() == self.blocks.len() - range.end {
1808 state.0 as f32
1809 } else {
1810 self.size[1]
1811 };
1812 let widest = |blocks: &[Block], side: usize| {
1813 blocks
1814 .iter()
1815 .map(|block| block.extent[side])
1816 .fold(f32::NEG_INFINITY, f32::max)
1817 };
1818 let widths = [0, 1].map(|side| {
1819 let [before, after] = [
1820 widest(&self.blocks[range.clone()], side),
1821 widest(&segment.blocks, side),
1822 ];
1823 if after >= before || before < self.widths[side] {
1824 self.widths[side].max(after)
1825 } else {
1826 [
1827 &self.blocks[..range.start],
1828 &segment.blocks,
1829 &self.blocks[range.end..],
1830 ]
1831 .into_iter()
1832 .map(|blocks| widest(blocks, side))
1833 .fold([36.0, f32::NEG_INFINITY][side], f32::max)
1834 }
1835 });
1836 let tables = &old[1];
1837 let tables = self.tables[..tables.start]
1838 .iter()
1839 .chain(&segment.tables)
1840 .chain(&self.tables[tables.end..]);
1841 let size = [
1842 if fixed_width { width } else { widths[0] }.max(table_width(tables)),
1843 bottom,
1844 ];
1845 return Ok(Relayout {
1846 range,
1847 segment,
1848 moved,
1849 size,
1850 widths,
1851 });
1852 }
1853 }
1854
1855 /// Puts a [`Relayout`] of this layout in place.
1856 pub(crate) fn commit(&mut self, relayout: Relayout) {
1857 let Relayout {
1858 range,
1859 mut segment,
1860 moved,
1861 size,
1862 widths,
1863 } = relayout;
1864 let [paragraphs, tables, objects] = self.pieces(range.clone());
1865 let first = [paragraphs.start, tables.start, objects.start];
1866 let delta = [
1867 segment.paragraphs.len() as isize - paragraphs.len() as isize,
1868 segment.tables.len() as isize - tables.len() as isize,
1869 segment.objects.len() as isize - objects.len() as isize,
1870 ];
1871 for cell in segment.tables.iter_mut().flat_map(|table| &mut table.cells) {
1872 cell.paragraphs = cell.paragraphs.start + first[0]..cell.paragraphs.end + first[0];
1873 }
1874 for block in &mut segment.blocks {
1875 for (value, first) in block.first.iter_mut().zip(first) {
1876 *value += first;
1877 }
1878 }
1879 let after = range.start + segment.blocks.len();
1880 let later_tables = tables.start + segment.tables.len();
1881 self.paragraphs.splice(paragraphs, segment.paragraphs);
1882 self.tables.splice(tables, segment.tables);
1883 self.objects.splice(objects, segment.objects);
1884 self.blocks.splice(range, segment.blocks);
1885 if delta != [0; 3] {
1886 for block in &mut self.blocks[after..] {
1887 for (value, delta) in block.first.iter_mut().zip(delta) {
1888 *value = value.wrapping_add_signed(delta);
1889 }
1890 }
1891 for cell in self.tables[later_tables..]
1892 .iter_mut()
1893 .flat_map(|table| &mut table.cells)
1894 {
1895 cell.paragraphs = cell.paragraphs.start.wrapping_add_signed(delta[0])
1896 ..cell.paragraphs.end.wrapping_add_signed(delta[0]);
1897 }
1898 }
1899 for (root, (y, state)) in (after..).zip(moved) {
1900 let [paragraphs, tables, objects] = self.pieces(root..root + 1);
1901 let block = &mut self.blocks[root];
1902 block.state = state;
1903 let Some([.., height]) = block.metrics else {
1904 continue;
1905 };
1906 if !tables.is_empty() {
1907 for (value, rel) in verticals(
1908 &mut self.paragraphs[paragraphs],
1909 &mut self.tables[tables],
1910 &mut self.objects[objects],
1911 )
1912 .zip(&block.rel)
1913 {
1914 *value = rel + y;
1915 }
1916 } else if let Some(object) = self.objects[objects].first_mut() {
1917 object.place(y, height);
1918 } else {
1919 self.paragraphs[paragraphs.start].origin[1] = y;
1920 }
1921 }
1922 self.size = size;
1923 self.widths = widths;
1924 self.place_tags();
1925 }
1926
1927 pub(crate) fn table_width(&self) -> f32 {
1928 table_width(&self.tables)
1929 }
1930
1931 /// How far the content reaches, without the 36-point floor of `size`.
1932 pub(crate) fn content_width(&self) -> f32 {
1933 let text = self.paragraphs.iter().map(|paragraph| paragraph.size()[0]);
1934 let objects = self.objects.iter().map(|object| object.rect[2]);
1935 text.chain(objects).fold(self.table_width(), f32::max)
1936 }
1937
1938 fn table(
1939 table: &Table,
1940 depth: usize,
1941 edit: Option<&DocumentEdit>,
1942 shape: &mut impl FnMut(
1943 &PageParagraph,
1944 Option<&Count>,
1945 f32,
1946 &[f32],
1947 ) -> Result<ParagraphLayout, LayoutError>,
1948 ) -> Result<Self, LayoutError> {
1949 let widths = edit.and_then(|edit| edit.columns.get(&table.id));
1950 if widths.is_some_and(|widths| widths.len() != table.columns.len()) {
1951 return Err(LayoutError::InvalidWidth);
1952 }
1953 let width =
1954 |index: usize| widths.map_or(table.columns[index].width, |widths| widths[index]);
1955 if table.columns.is_empty()
1956 || table.rows.is_empty()
1957 || table
1958 .columns
1959 .iter()
1960 .enumerate()
1961 .any(|(index, _)| !width(index).is_finite() || width(index) < 36.0)
1962 {
1963 return Err(LayoutError::InvalidWidth);
1964 }
1965 let mut result = Self {
1966 tables: vec![TableLayout {
1967 id: table.id,
1968 cells: Vec::new(),
1969 borders: table.borders.unwrap_or(true),
1970 tags: None,
1971 }],
1972 size: [
1973 (0..table.columns.len())
1974 .map(|index| width(index) + 4.98)
1975 .sum::<f32>()
1976 - 1.83,
1977 3.54,
1978 ],
1979 ..Self::default()
1980 };
1981 let mut y = 0.0;
1982 for row in &table.rows {
1983 if row.cells.len() != table.columns.len() {
1984 return Err(LayoutError::InvalidWidth);
1985 }
1986 let start = result.tables[0].cells.len();
1987 let mut x = 0.0;
1988 let mut height = 0.0_f32;
1989 for (index, cell) in row.cells.iter().enumerate() {
1990 let width = width(index);
1991 if cell.paragraphs.is_empty() || !cell.unsupported.is_empty() {
1992 return Err(LayoutError::UnsupportedContent);
1993 }
1994 let mut child = Self::flow(
1995 edited_nodes(&cell.paragraphs, Some(cell.id), edit),
1996 &cell.indents,
1997 width,
1998 false,
1999 depth,
2000 edit,
2001 shape,
2002 )?;
2003 height = height.max(child.size[1]);
2004 for paragraph in &mut child.paragraphs {
2005 paragraph.parent.get_or_insert(cell.id);
2006 }
2007 let tables = child
2008 .tables
2009 .iter_mut()
2010 .filter_map(|table| table.tags.as_mut());
2011 let objects = child
2012 .objects
2013 .iter_mut()
2014 .filter_map(|object| object.tags.as_mut());
2015 for block in tables.chain(objects) {
2016 block.parent.get_or_insert(cell.id);
2017 }
2018 let paragraph_start = result.paragraphs.len();
2019 result.append(child, [x, y + 3.54]);
2020 result.tables[0].cells.push(CellLayout {
2021 id: cell.id,
2022 rect: [x - 3.6, y + 1.86, x + width + 1.38, 0.0],
2023 paragraphs: paragraph_start..result.paragraphs.len(),
2024 });
2025 x += width + 4.98;
2026 }
2027 y += height + 4.98;
2028 for cell in &mut result.tables[0].cells[start..] {
2029 cell.rect[3] = y + 1.86;
2030 }
2031 }
2032 result.size[1] += y;
2033 if result.size.iter().any(|v| !v.is_finite()) {
2034 return Err(LayoutError::InvalidSpacing);
2035 }
2036 Ok(result)
2037 }
2038}
2039
2040/// Stored column widths extend 1.77pt past the final cell border.
2041fn table_width<'a>(tables: impl IntoIterator<Item = &'a TableLayout>) -> f32 {
2042 tables
2043 .into_iter()
2044 .filter_map(|table| table.cells.last())
2045 .map(|cell| cell.rect[2] + 1.77)
2046 .fold(0.0, f32::max)
2047}
2048
2049/// Shapes a page object into positioned paragraph layouts.
2050pub trait Arrange {
2051 type Output;
2052 fn layout(
2053 &self,
2054 engine: &mut TextEngine,
2055 definitions: &BTreeMap<ExGuid, Definition>,
2056 ) -> Result<Self::Output, LayoutError>;
2057}
2058
2059impl Arrange for Outline {
2060 type Output = OutlineLayout;
2061
2062 fn layout(
2063 &self,
2064 engine: &mut TextEngine,
2065 definitions: &BTreeMap<ExGuid, Definition>,
2066 ) -> Result<OutlineLayout, LayoutError> {
2067 let width = self
2068 .layout
2069 .reserved_width
2070 .or(self.layout.max_width)
2071 .ok_or(LayoutError::InvalidWidth)?;
2072 outline_layout(self, engine, definitions, width)
2073 }
2074}
2075
2076pub(crate) fn outline_layout(
2077 outline: &Outline,
2078 engine: &mut TextEngine,
2079 definitions: &BTreeMap<ExGuid, Definition>,
2080 width: f32,
2081) -> Result<OutlineLayout, LayoutError> {
2082 if !outline.unsupported.is_empty() {
2083 return Err(LayoutError::UnsupportedContent);
2084 }
2085 OutlineLayout::flow(
2086 outline.paragraphs.iter(),
2087 &outline.indents,
2088 width,
2089 outline.layout.width_set_by_user == Some(true),
2090 0,
2091 None,
2092 &mut |node, previous, width, indents| {
2093 ParagraphLayout::shape(engine, node, previous, width, indents, definitions)
2094 },
2095 )
2096}
2097
2098impl Arrange for Title {
2099 type Output = Vec<([f32; 2], OutlineLayout)>;
2100
2101 fn layout(
2102 &self,
2103 engine: &mut TextEngine,
2104 definitions: &BTreeMap<ExGuid, Definition>,
2105 ) -> Result<Vec<([f32; 2], OutlineLayout)>, LayoutError> {
2106 if [self.layout.x, self.layout.y]
2107 .into_iter()
2108 .flatten()
2109 .any(|v| !v.is_finite())
2110 {
2111 return Err(LayoutError::InvalidSpacing);
2112 }
2113 let mut layouts = Vec::new();
2114 let mut bottom = 0.0_f32;
2115 for outline in &self.outlines {
2116 let width = outline
2117 .layout
2118 .reserved_width
2119 .or(outline.layout.max_width)
2120 .unwrap_or(TITLE_WIDTH);
2121 let layout = outline_layout(outline, engine, definitions, width)?;
2122 let origin = [
2123 outline.layout.x.unwrap_or(0.0),
2124 bottom + outline.layout.y.unwrap_or(0.0),
2125 ];
2126 let height = outline.layout.max_height.unwrap_or(0.0);
2127 if origin.iter().any(|v| !v.is_finite()) || !height.is_finite() || height < 0.0 {
2128 return Err(LayoutError::InvalidSpacing);
2129 }
2130 bottom = origin[1] + layout.size[1].max(height) + if outline.title { 3.6 } else { 0.0 };
2131 if !bottom.is_finite() {
2132 return Err(LayoutError::InvalidSpacing);
2133 }
2134 layouts.push((origin, layout));
2135 }
2136 Ok(layouts)
2137 }
2138}
2139
2140#[cfg(test)]
2141mod tests {
2142 use super::*;
2143 use onestore::document::Layout;
2144 use onestore::page::{PageParagraph, TextObject};
2145
2146 fn paragraph(n: u32, text: &str, level: u32, parent: Option<u32>) -> PageParagraph {
2147 PageParagraph {
2148 id: ExGuid {
2149 n,
2150 ..ExGuid::default()
2151 },
2152 parent: parent.map(|n| ExGuid {
2153 n,
2154 ..ExGuid::default()
2155 }),
2156 level,
2157 format: Format::default(),
2158 content: onestore::page::ParagraphContent::Text(TextObject {
2159 date_field: None,
2160 id: ExGuid {
2161 n: n + 100,
2162 ..ExGuid::default()
2163 },
2164 text: Paragraph::new(text.into(), Format::default()),
2165 tags: Vec::new(),
2166 }),
2167 lists: Vec::new(),
2168 tags: Vec::new(),
2169 media: Default::default(),
2170 collapsed: false,
2171 style: None,
2172 }
2173 }
2174
2175 fn table(rows: &[&[&str]], widths: &[f32], mut n: u32) -> PageParagraph {
2176 use onestore::page::{TableCell, TableColumn, TableRow};
2177 let mut id = || {
2178 n += 1;
2179 ExGuid {
2180 n,
2181 ..ExGuid::default()
2182 }
2183 };
2184 let mut node = paragraph(id().n, "", 1, None);
2185 node.content = ParagraphContent::Table(Table {
2186 id: id(),
2187 columns: widths
2188 .iter()
2189 .map(|width| TableColumn {
2190 width: *width,
2191 locked: false,
2192 })
2193 .collect(),
2194 rows: rows
2195 .iter()
2196 .map(|row| TableRow {
2197 id: id(),
2198 cells: row
2199 .iter()
2200 .map(|text| TableCell {
2201 id: id(),
2202 layout: Layout::default(),
2203 indents: vec![18.0, 0.0, 27.0],
2204 shading: None,
2205 paragraphs: vec![paragraph(id().n, text, 1, None)],
2206 unsupported: Vec::new(),
2207 })
2208 .collect(),
2209 })
2210 .collect(),
2211 borders: Some(true),
2212 layout: Layout::default(),
2213 tags: Vec::new(),
2214 });
2215 node
2216 }
2217
2218 #[test]
2219 fn table_rows_align_cells_and_expand_for_wrapped_text() {
2220 let mut engine = TextEngine::default();
2221 let mut outline = Outline {
2222 id: ExGuid::default(),
2223 title: false,
2224 min_width: None,
2225 layout: Layout {
2226 max_width: Some(300.0),
2227 ..Layout::default()
2228 },
2229 indents: vec![18.0, 0.0, 27.0],
2230 paragraphs: vec![
2231 paragraph(1, "Before", 1, None),
2232 table(
2233 &[
2234 &[
2235 "A long paragraph that wraps inside a single table cell",
2236 "B",
2237 ],
2238 &["C", "D"],
2239 ],
2240 &[72.0, 48.0],
2241 1000,
2242 ),
2243 paragraph(2, "After", 1, None),
2244 ],
2245 unsupported: Vec::new(),
2246 };
2247 let definitions = BTreeMap::new();
2248 let layout = outline.layout(&mut engine, &definitions).unwrap();
2249 assert_eq!(layout.paragraphs.len(), 6);
2250 assert_eq!(layout.tables.len(), 1);
2251 let cells = &layout.tables[0].cells;
2252 assert_eq!(cells.len(), 4);
2253 assert_eq!(
2254 cells
2255 .iter()
2256 .map(|cell| cell.paragraphs.clone())
2257 .collect::<Vec<_>>(),
2258 [1..2, 2..3, 3..4, 4..5]
2259 );
2260 assert!(layout.paragraph_cell(0).is_none());
2261 assert_eq!(layout.paragraph_cell(4).unwrap().id, cells[3].id);
2262 assert!(layout.paragraph_cell(5).is_none());
2263 let paragraphs = &layout.paragraphs;
2264 assert!(paragraphs[1].text.lines().count() > 1);
2265 assert_eq!(paragraphs[2].text.lines().count(), 1);
2266 assert_eq!(paragraphs[1].origin[1], paragraphs[2].origin[1]);
2267 assert_eq!(paragraphs[3].origin[1], paragraphs[4].origin[1]);
2268 assert_eq!(cells[0].rect[3], cells[1].rect[3]);
2269 assert_eq!(cells[0].rect[3], cells[2].rect[1]);
2270 assert!((cells[0].rect[2] - cells[1].rect[0]).abs() < 0.00001);
2271 assert_eq!(paragraphs[1].origin[0], 0.0);
2272 assert_eq!(paragraphs[2].origin[0], 76.98);
2273 assert!(paragraphs[3].origin[1] > paragraphs[1].origin[1] + paragraphs[1].text.height());
2274 assert!(paragraphs[5].origin[1] > cells[3].rect[3]);
2275 assert_eq!(
2276 layout.size[1],
2277 paragraphs[5].origin[1] + paragraphs[5].text.height()
2278 );
2279 let ParagraphContent::Table(source) = &outline.paragraphs[1].content else {
2280 panic!()
2281 };
2282 assert_eq!(layout.tables[0].id, source.id);
2283 assert_eq!(
2284 cells.iter().map(|c| c.id).collect::<Vec<_>>(),
2285 source
2286 .rows
2287 .iter()
2288 .flat_map(|row| row.cells.iter().map(|c| c.id))
2289 .collect::<Vec<_>>()
2290 );
2291
2292 outline.layout.width_set_by_user = Some(true);
2293 let fixed = outline.layout(&mut engine, &definitions).unwrap();
2294 assert_eq!(fixed.size, [300.0, layout.size[1]]);
2295 assert_eq!(fixed.paragraphs[2].origin, paragraphs[2].origin);
2296 let ParagraphContent::Table(source) = &mut outline.paragraphs[1].content else {
2297 panic!()
2298 };
2299 source.columns[0].width = 160.0;
2300 let widened = outline.layout(&mut engine, &definitions).unwrap();
2301 assert!(widened.size[1] < fixed.size[1]);
2302 assert_eq!(widened.paragraphs[2].origin[0], 164.98);
2303 }
2304
2305 #[test]
2306 fn nested_table_layout_translates_cells_and_respects_collapsed_children() {
2307 let mut outer = table(&[&["Left", "Right"]], &[160.0, 72.0], 1000);
2308 let mut inner = table(&[&["Nested", "Cell"]], &[48.0, 48.0], 2000);
2309 inner.level = 2;
2310 let ParagraphContent::Table(source) = &mut outer.content else {
2311 panic!()
2312 };
2313 let hidden = paragraph(
2314 42,
2315 "Hidden descendant",
2316 2,
2317 Some(source.rows[0].cells[0].paragraphs[0].id.n),
2318 );
2319 source.rows[0].cells[0].paragraphs[0].collapsed = true;
2320 source.rows[0].cells[0].paragraphs.push(hidden);
2321 source.rows[0].cells[0].paragraphs.push(inner);
2322 let mut engine = TextEngine::default();
2323 let layout = OutlineLayout::flow(
2324 [&outer].into_iter(),
2325 &[18.0, 0.0, 27.0],
2326 300.0,
2327 false,
2328 0,
2329 None,
2330 &mut |node, previous, width, indents| {
2331 ParagraphLayout::shape(
2332 &mut engine,
2333 node,
2334 previous,
2335 width,
2336 indents,
2337 &BTreeMap::new(),
2338 )
2339 },
2340 )
2341 .unwrap();
2342 assert_eq!(layout.tables.len(), 2);
2343 assert_eq!(layout.tables[0].cells[0].paragraphs, 0..3);
2344 assert_eq!(layout.tables[0].cells[1].paragraphs, 3..4);
2345 assert_eq!(layout.tables[1].cells[0].paragraphs, 1..2);
2346 assert_eq!(layout.tables[1].cells[1].paragraphs, 2..3);
2347 assert_eq!(
2348 layout.paragraph_cell(1).unwrap().id,
2349 layout.tables[1].cells[0].id
2350 );
2351 assert_eq!(
2352 layout
2353 .paragraphs
2354 .iter()
2355 .map(|p| p.projection.text().text())
2356 .collect::<Vec<_>>(),
2357 ["Left", "Nested", "Cell", "Right"]
2358 );
2359 assert_eq!(layout.paragraphs[1].origin[0], 27.0);
2360 assert_eq!(layout.tables[1].cells[0].rect[0], 27.0 - 3.6);
2361 assert!(layout.tables[1].cells[0].rect[1] > layout.tables[0].cells[0].rect[1]);
2362 assert!(layout.tables[1].cells[0].rect[3] < layout.tables[0].cells[0].rect[3]);
2363 assert_eq!(
2364 layout.paragraphs[0].origin[1],
2365 layout.paragraphs[3].origin[1]
2366 );
2367 }
2368
2369 #[test]
2370 #[ignore = "requires CANVAS_TEST_SECTION native Tab capture"]
2371 fn native_table_layout() {
2372 use onestore::page::{Page, PageObject};
2373 use onestore::{RevisionIndex, Store, document::Document};
2374 let bytes = std::fs::read(std::env::var_os("CANVAS_TEST_SECTION").unwrap()).unwrap();
2375 let store = Store::parse(&bytes).unwrap();
2376 let index = RevisionIndex::parse(&store).unwrap();
2377 let document = Document::parse(&index).unwrap();
2378 let mut engine = TextEngine::default();
2379 let page = Page::from_document(&document, "rows").unwrap();
2380 let outline = page
2381 .objects
2382 .iter()
2383 .find_map(|object| match object {
2384 PageObject::Outline(outline) => Some(outline),
2385 _ => None,
2386 })
2387 .unwrap();
2388 let layout = outline.layout(&mut engine, &page.definitions).unwrap();
2389 assert!((layout.size[0] - 84.6).abs() < 0.001);
2390 assert!((layout.size[1] - 58.76315).abs() < 0.001);
2391 for (paragraph, expected) in layout.paragraphs.iter().zip([
2392 [0.0, 3.54],
2393 [44.34, 3.54],
2394 [0.0, 21.947714],
2395 [44.34, 21.947714],
2396 [0.0, 40.355427],
2397 [44.34, 40.355427],
2398 ]) {
2399 assert_eq!(paragraph.text.lines().count(), 1);
2400 for (actual, expected) in paragraph.origin.into_iter().zip(expected) {
2401 assert!((actual - expected).abs() < 0.001);
2402 }
2403 }
2404 assert_eq!(layout.tables.len(), 1);
2405 assert_eq!(layout.tables[0].cells.len(), 6);
2406 for (title, size) in [
2407 ("soft-break", [82.350006, 35.375435]),
2408 ("fixed", [180.0, 21.947721]),
2409 ] {
2410 let page = Page::from_document(&document, title).unwrap();
2411 let outline = page
2412 .objects
2413 .iter()
2414 .find_map(|object| match object {
2415 PageObject::Outline(outline) => Some(outline),
2416 _ => None,
2417 })
2418 .unwrap();
2419 let layout = outline.layout(&mut engine, &page.definitions).unwrap();
2420 for (actual, expected) in layout.size.into_iter().zip(size) {
2421 assert!(
2422 (actual - expected).abs() < 0.001,
2423 "{title}: {actual} != {expected}"
2424 );
2425 }
2426 assert_eq!(layout.tables.len(), 1);
2427 assert_eq!(layout.tables[0].cells.len(), 2);
2428 assert_eq!(
2429 layout
2430 .paragraphs
2431 .iter()
2432 .map(|p| p.text.lines().count())
2433 .max(),
2434 Some(if title == "soft-break" { 2 } else { 1 })
2435 );
2436 }
2437 }
2438
2439 #[test]
2440 fn tags_preserve_line_geometry_and_follow_list_indentation() {
2441 use onestore::document::Tag;
2442 let id = ExGuid {
2443 n: 500,
2444 ..ExGuid::default()
2445 };
2446 let mut node = paragraph(1, "Text that wraps across several lines", 2, None);
2447 let mut engine = TextEngine::default();
2448 let mut definitions = BTreeMap::new();
2449 let plain = ParagraphLayout::shape(
2450 &mut engine,
2451 &node,
2452 None,
2453 120.0,
2454 &[18.0, 0.0, 27.0],
2455 &definitions,
2456 )
2457 .unwrap();
2458 node.text_mut().unwrap().tags.push(Tag {
2459 definition: Some(id),
2460 status: 3,
2461 action_type: None,
2462 shape: None,
2463 property_status: None,
2464 created: None,
2465 completed: None,
2466 start: None,
2467 due: None,
2468 task_id: None,
2469 extra_set: 0,
2470 });
2471 for shape in [3, 13, 15, 17, 18, 23, 118, 136, 100, 101, 102, 121] {
2472 definitions.insert(
2473 id,
2474 Definition {
2475 kind: Kind::TagDefinition {
2476 shape: Some(shape),
2477 label: Some("Label".into()),
2478 action_type: None,
2479 color: None,
2480 highlight: None,
2481 },
2482 format: Format::default(),
2483 },
2484 );
2485 let tagged = ParagraphLayout::shape(
2486 &mut engine,
2487 &node,
2488 None,
2489 120.0,
2490 &[18.0, 0.0, 27.0],
2491 &definitions,
2492 )
2493 .unwrap();
2494 assert_eq!(
2495 tagged.tags[0].icon,
2496 TagIcon::Symbol {
2497 shape,
2498 checked: shape == 3
2499 }
2500 );
2501 let baseline = tagged.text.lines().next().unwrap().1.baseline;
2502 assert_eq!(
2503 tagged.tags[0].origin,
2504 [6.75, baseline - 0.357 * 11.0 + 0.2 - 6.0]
2505 );
2506 assert_eq!(tagged.tags[0].label, "Label");
2507 assert!(tagged.tags[0].disabled);
2508 assert_eq!(tagged.text.height(), plain.text.height());
2509 assert_eq!(
2510 tagged
2511 .text
2512 .lines()
2513 .map(|(_, b)| (&b.source, b.baseline))
2514 .collect::<Vec<_>>(),
2515 plain
2516 .text
2517 .lines()
2518 .map(|(_, b)| (&b.source, b.baseline))
2519 .collect::<Vec<_>>()
2520 );
2521 }
2522 let list = ExGuid {
2523 n: 501,
2524 ..ExGuid::default()
2525 };
2526 definitions.insert(
2527 list,
2528 Definition {
2529 kind: Kind::List {
2530 font: Some("Arial".into()),
2531 format: Some("•".into()),
2532 bullet: None,
2533 restart: None,
2534 },
2535 format: Format::default(),
2536 },
2537 );
2538 node.lists.push(list);
2539 let tagged = ParagraphLayout::shape(
2540 &mut engine,
2541 &node,
2542 None,
2543 120.0,
2544 &[18.0, 0.0, 27.0],
2545 &definitions,
2546 )
2547 .unwrap();
2548 assert_eq!(tagged.tags[0].origin[0], tagged.markers[0].1[0] - 12.9);
2549 let baseline = tagged.text.lines().next().unwrap().1.baseline;
2550 assert_eq!(
2551 tagged.tags[0].origin[1],
2552 baseline - 0.357 * 11.0 + 0.2 - 6.0
2553 );
2554 let Kind::TagDefinition { shape, .. } = &mut definitions.get_mut(&id).unwrap().kind else {
2555 unreachable!()
2556 };
2557 *shape = Some(999);
2558 let mut shaped = |node: &PageParagraph, definitions| {
2559 ParagraphLayout::shape(
2560 &mut engine,
2561 node,
2562 None,
2563 120.0,
2564 &[18.0, 0.0, 27.0],
2565 definitions,
2566 )
2567 .unwrap()
2568 };
2569 // A symbol MS-ONE does not list keeps its number, an Outlook task shows its flag, and
2570 // a tag whose definition is missing shows nothing.
2571 assert_eq!(
2572 shaped(&node, &definitions).tags[0].icon,
2573 TagIcon::Symbol {
2574 shape: 999,
2575 checked: false
2576 }
2577 );
2578 let mut task = node.clone();
2579 let stored = &mut task.text_mut().unwrap().tags[0];
2580 stored.status |= 4;
2581 stored.shape = Some(89);
2582 assert_eq!(
2583 shaped(&task, &definitions).tags[0].icon,
2584 TagIcon::Task { shape: 89 }
2585 );
2586 let mut untagged = definitions.clone();
2587 untagged.remove(&id);
2588 assert!(shaped(&node, &untagged).tags.is_empty());
2589 }
2590
2591 /// OneNote 2010 at 400% (Calibri bullets and numbers from 8 to 24 pt, default list
2592 /// spacing): a marker's advance ends 11.1 pt before its text, and a tag's 12 pt slot ends
2593 /// 0.9 pt before the leftmost marker among its paragraph's siblings, or 8.25 pt before its
2594 /// text when none has one.
2595 #[test]
2596 fn markers_and_tags_sit_where_onenote_draws_them() {
2597 use onestore::document::Tag;
2598 let id = |n| ExGuid {
2599 n,
2600 ..ExGuid::default()
2601 };
2602 let list = |format: &str| Definition {
2603 kind: Kind::List {
2604 font: None,
2605 format: Some(format.into()),
2606 bullet: None,
2607 restart: None,
2608 },
2609 format: Format::default(),
2610 };
2611 let definitions = BTreeMap::from([
2612 (id(900), list("\u{2022}")),
2613 (id(901), list("\u{fffd}\u{0}.")),
2614 (
2615 id(902),
2616 Definition {
2617 kind: Kind::TagDefinition {
2618 shape: Some(3),
2619 label: None,
2620 action_type: None,
2621 color: None,
2622 highlight: None,
2623 },
2624 format: Format::default(),
2625 },
2626 ),
2627 ]);
2628 let tag = Tag {
2629 definition: Some(id(902)),
2630 action_type: None,
2631 shape: None,
2632 property_status: None,
2633 status: 0,
2634 created: None,
2635 completed: None,
2636 start: None,
2637 due: None,
2638 task_id: None,
2639 extra_set: 0,
2640 };
2641 let tagged = |n, level, parent| {
2642 let mut node = paragraph(n, "Tag", level, parent);
2643 node.text_mut().unwrap().tags.push(tag.clone());
2644 node
2645 };
2646 let mut number = paragraph(1, "Number", 1, None);
2647 number.lists.push(id(901));
2648 let mut bullet = paragraph(5, "Bullet", 2, Some(3));
2649 bullet.lists.push(id(900));
2650 let nodes = [
2651 number,
2652 tagged(2, 1, None),
2653 paragraph(3, "Plain", 1, None),
2654 tagged(4, 2, Some(3)),
2655 bullet,
2656 paragraph(6, "Plain", 2, Some(3)),
2657 ];
2658 let mut engine = TextEngine::default();
2659 let layout = OutlineLayout::flow(
2660 nodes.iter(),
2661 &[18.0, 0.0, 27.0, 27.0],
2662 400.0,
2663 false,
2664 0,
2665 None,
2666 &mut |node, previous, width, indents| {
2667 ParagraphLayout::shape(&mut engine, node, previous, width, indents, &definitions)
2668 },
2669 )
2670 .unwrap();
2671 let [number, first, _, second, bullet, _] = &layout.paragraphs[..] else {
2672 panic!()
2673 };
2674 for paragraph in [number, bullet] {
2675 let (marker, [x, _]) = &paragraph.markers[0];
2676 let advance = marker.lines().next().unwrap().0.metrics().advance;
2677 assert!((x + advance - (paragraph.origin[0] - 11.1)).abs() < 1e-4);
2678 }
2679 // A numbered sibling pushes the level-1 tag left; the level-2 tag clears the bullet
2680 // that follows it rather than its own text.
2681 assert_eq!(first.tags[0].origin[0], number.markers[0].1[0] - 0.9 - 12.0);
2682 assert_eq!(
2683 second.tags[0].origin[0],
2684 bullet.markers[0].1[0] - 0.9 - 12.0
2685 );
2686 assert!(first.tags[0].origin[0] < first.origin[0] - 20.25);
2687 let alone = OutlineLayout::flow(
2688 [tagged(7, 1, None)].iter(),
2689 &[18.0, 0.0, 27.0, 27.0],
2690 400.0,
2691 false,
2692 0,
2693 None,
2694 &mut |node, previous, width, indents| {
2695 ParagraphLayout::shape(&mut engine, node, previous, width, indents, &definitions)
2696 },
2697 )
2698 .unwrap();
2699 assert_eq!(alone.paragraphs[0].tags[0].origin[0], -20.25);
2700 }
2701
2702 /// OneNote 2010 at 400%: the first run's size picks the tag icon (9 pt below 10 pt text,
2703 /// 12 pt to 17.5, 18 pt to 23.5, then 24 pt up to at least 60), whose right edge stays
2704 /// 8.25 pt before the text and whose centre sits 0.357 of the run's size less 0.2 pt above
2705 /// the first baseline.
2706 #[test]
2707 fn tag_icons_follow_the_first_run() {
2708 use onestore::document::Tag;
2709 let id = |n| ExGuid {
2710 n,
2711 ..ExGuid::default()
2712 };
2713 let definitions = BTreeMap::from([(
2714 id(902),
2715 Definition {
2716 kind: Kind::TagDefinition {
2717 shape: Some(3),
2718 label: None,
2719 action_type: None,
2720 color: None,
2721 highlight: None,
2722 },
2723 format: Format::default(),
2724 },
2725 )]);
2726 let tag = Tag {
2727 definition: Some(id(902)),
2728 action_type: None,
2729 shape: None,
2730 property_status: None,
2731 status: 0,
2732 created: None,
2733 completed: None,
2734 start: None,
2735 due: None,
2736 task_id: None,
2737 extra_set: 0,
2738 };
2739 let tagged = |sizes: &[f32], tags: usize| {
2740 let mut node = paragraph(1, "", 1, None);
2741 let text = node.text_mut().unwrap();
2742 text.text = Paragraph::from_runs(sizes.iter().map(|size| {
2743 (
2744 "Tag ".to_string(),
2745 Format {
2746 font_size: Some(*size),
2747 ..Format::default()
2748 },
2749 )
2750 }));
2751 text.tags = vec![tag.clone(); tags];
2752 node
2753 };
2754 let mut engine = TextEngine::default();
2755 let mut lay = |nodes: &[PageParagraph]| {
2756 OutlineLayout::flow(
2757 nodes.iter(),
2758 &[18.0, 0.0, 27.0, 27.0],
2759 400.0,
2760 false,
2761 0,
2762 None,
2763 &mut |node, previous, width, indents| {
2764 ParagraphLayout::shape(
2765 &mut engine,
2766 node,
2767 previous,
2768 width,
2769 indents,
2770 &definitions,
2771 )
2772 },
2773 )
2774 .unwrap()
2775 };
2776 for (sizes, side) in [
2777 (&[9.5][..], 9.0),
2778 (&[10.0], 12.0),
2779 (&[17.5], 12.0),
2780 (&[18.0], 18.0),
2781 (&[23.5], 18.0),
2782 (&[24.0], 24.0),
2783 (&[60.0], 24.0),
2784 (&[11.0, 24.0], 12.0),
2785 (&[24.0, 11.0], 24.0),
2786 ] {
2787 let layout = lay(&[tagged(sizes, 1)]);
2788 let icon = &layout.paragraphs[0].tags[0];
2789 assert_eq!((icon.size, icon.origin[0] + icon.size), (side, -8.25));
2790 let baseline = layout.paragraphs[0].text.lines().next().unwrap().1.baseline;
2791 let centre = icon.origin[1] + side / 2.0;
2792 assert!((centre - (baseline - 0.357 * sizes[0] + 0.2)).abs() < 1e-4);
2793 }
2794 // A second icon extends the column left by its own side.
2795 let layout = lay(&[tagged(&[20.0], 2)]);
2796 assert_eq!(layout.tag_column_offset(), -18.0);
2797 }
2798
2799 #[test]
2800 fn files_and_ink_take_their_own_paragraph_in_the_flow() {
2801 use onestore::page::{Attachment, Ink, InkStroke};
2802 let before = paragraph(1, "Before", 1, None);
2803 let mut file = paragraph(2, "", 1, None);
2804 file.content = ParagraphContent::Attachment(Attachment {
2805 id: ExGuid {
2806 n: 20,
2807 ..ExGuid::default()
2808 },
2809 filename: "notes 🦀.txt".into(),
2810 source_path: None,
2811 size: Some([24.0, 24.0]),
2812 layout: Default::default(),
2813 bytes: None,
2814 preview: None,
2815 recording: None,
2816 tags: Vec::new(),
2817 });
2818 let mut ink = paragraph(3, "", 1, None);
2819 ink.content = ParagraphContent::Ink(Ink {
2820 id: ExGuid {
2821 n: 30,
2822 ..ExGuid::default()
2823 },
2824 layout: Default::default(),
2825 strokes: vec![InkStroke {
2826 id: ExGuid::default(),
2827 points: vec![[300.0, 120.0], [360.0, 180.0]],
2828 width: 1.0,
2829 height: 1.0,
2830 color: None,
2831 transparency: None,
2832 pen_tip: None,
2833 raster_operation: None,
2834 pressure: Vec::new(),
2835 }],
2836 groups: Vec::new(),
2837 shape: None,
2838 });
2839 let after = paragraph(4, "After", 1, None);
2840 let mut engine = TextEngine::default();
2841 let layout = OutlineLayout::flow(
2842 [&before, &file, &ink, &after].into_iter(),
2843 &[0.0, 0.0],
2844 468.0,
2845 false,
2846 0,
2847 None,
2848 &mut |node, previous, width, indents| {
2849 ParagraphLayout::shape(
2850 &mut engine,
2851 node,
2852 previous,
2853 width,
2854 indents,
2855 &BTreeMap::new(),
2856 )
2857 },
2858 )
2859 .unwrap();
2860 let top = layout.paragraphs[0].text.height();
2861 let [icon, handwriting] = [&layout.objects[0], &layout.objects[1]];
2862 assert_eq!(icon.rect, [15.0, top + 6.0, 39.0, top + 30.0]);
2863 let label = icon.label().unwrap();
2864 assert_eq!(label.projection.text().text(), "notes 🦀");
2865 assert_eq!(label.origin, [0.0, top + 40.5]);
2866 assert_eq!(icon.bottom, label.origin[1] + label.text.height() + 9.0);
2867 assert_eq!(
2868 handwriting.rect,
2869 [0.0, icon.bottom, 360.0, icon.bottom + 180.0]
2870 );
2871 assert_eq!(layout.paragraphs[1].origin[1], handwriting.bottom);
2872 assert_eq!(layout.size[0], 360.0);
2873 }
2874
2875 #[test]
2876 fn tags_paint_oldest_first_and_the_newest_colour_wins() {
2877 use onestore::document::Tag;
2878 let mut definitions = BTreeMap::new();
2879 let mut tag = |n, shape, color| {
2880 let id = ExGuid {
2881 n,
2882 ..ExGuid::default()
2883 };
2884 definitions.insert(
2885 id,
2886 Definition {
2887 kind: Kind::TagDefinition {
2888 shape: Some(shape),
2889 label: None,
2890 action_type: None,
2891 color,
2892 highlight: None,
2893 },
2894 format: Format::default(),
2895 },
2896 );
2897 Tag {
2898 definition: Some(id),
2899 status: 1,
2900 action_type: None,
2901 shape: None,
2902 property_status: None,
2903 created: None,
2904 completed: None,
2905 start: None,
2906 due: None,
2907 task_id: None,
2908 extra_set: 0,
2909 }
2910 };
2911 let [action, mood, project, question] = [
2912 tag(1, 0, Some(0x0080_0080)),
2913 tag(2, 0, Some(0x0080_8000)),
2914 tag(3, 100, None),
2915 tag(4, 15, None),
2916 ];
2917 let mut node = paragraph(1, "Lyric", 1, None);
2918 // As stored: newest first.
2919 node.text_mut().unwrap().tags = vec![question, project, mood, action];
2920 let mut engine = TextEngine::default();
2921 let shaped =
2922 ParagraphLayout::shape(&mut engine, &node, None, 200.0, &[0.0, 0.0], &definitions)
2923 .unwrap();
2924 assert_eq!(
2925 shaped
2926 .tags
2927 .iter()
2928 .map(|tag| (tag.icon, tag.origin[0]))
2929 .collect::<Vec<_>>(),
2930 [
2931 (TagIcon::of(100, false).unwrap(), -20.25),
2932 (TagIcon::of(15, false).unwrap(), -8.25)
2933 ]
2934 );
2935 let colors: Vec<_> = shaped
2936 .text
2937 .lines()
2938 .flat_map(|(line, _)| line.items().collect::<Vec<_>>())
2939 .filter_map(|item| match item {
2940 parley::PositionedLayoutItem::GlyphRun(run) => Some(run.style().brush.color),
2941 _ => None,
2942 })
2943 .collect();
2944 assert_eq!(colors, [Some(0x0080_8000)]);
2945 assert_eq!(shaped.projection.text().spans()[0].format.color, None);
2946 let outline = OutlineLayout {
2947 paragraphs: vec![shaped],
2948 ..OutlineLayout::default()
2949 };
2950 assert_eq!(outline.tag_column_offset(), -12.0);
2951 }
2952
2953 #[test]
2954 fn title_uses_a_default_wrap_limit_and_keeps_date_after_wrapped_title() {
2955 let mut title = Title {
2956 date: None,
2957 id: ExGuid::default(),
2958 layout: Layout::default(),
2959 outlines: ["A title with enough words to wrap", "A date"]
2960 .into_iter()
2961 .enumerate()
2962 .map(|(index, text)| Outline {
2963 title: index == 0,
2964 min_width: None,
2965 id: ExGuid {
2966 n: index as u32,
2967 ..ExGuid::default()
2968 },
2969 layout: Layout {
2970 max_height: Some(21.6),
2971 ..Layout::default()
2972 },
2973 indents: vec![18.0, 0.0],
2974 paragraphs: vec![paragraph(index as u32, text, 1, None)],
2975 unsupported: Vec::new(),
2976 })
2977 .collect(),
2978 };
2979 let mut engine = TextEngine::default();
2980 let definitions = BTreeMap::new();
2981 assert!(matches!(
2982 title.outlines[0].layout(&mut engine, &definitions),
2983 Err(LayoutError::InvalidWidth)
2984 ));
2985 let layouts = title.layout(&mut engine, &definitions).unwrap();
2986 assert_eq!(layouts[0].1.paragraphs[0].text.lines().count(), 1);
2987 assert_eq!(layouts[1].0, [0.0, 21.6 + 3.6]);
2988 title.outlines[0].paragraphs[0].text_mut().unwrap().text = Paragraph::new(
2989 "A title with enough words to wrap ".repeat(12),
2990 Format::default(),
2991 );
2992 let layouts = title.layout(&mut engine, &definitions).unwrap();
2993 assert!(layouts[0].1.paragraphs[0].text.lines().count() > 1);
2994 assert!(layouts[0].1.size[0] <= 468.0);
2995 assert_eq!(layouts[1].0[1], layouts[0].1.size[1] + 3.6);
2996 title.outlines[0].layout.max_width = Some(70.0);
2997 let layouts = title.layout(&mut engine, &definitions).unwrap();
2998 assert!(layouts[0].1.paragraphs[0].text.lines().count() > 1);
2999 assert_eq!(layouts[1].0[1], layouts[0].1.size[1] + 3.6);
3000 for value in [f32::NAN, f32::INFINITY, -1.0] {
3001 title.outlines[0].layout.max_height = Some(value);
3002 assert!(matches!(
3003 title.layout(&mut engine, &definitions),
3004 Err(LayoutError::InvalidSpacing)
3005 ));
3006 }
3007 title.outlines[0].layout.max_height = Some(21.6);
3008 title.layout.x = Some(f32::NAN);
3009 assert!(matches!(
3010 title.layout(&mut engine, &definitions),
3011 Err(LayoutError::InvalidSpacing)
3012 ));
3013 }
3014
3015 #[test]
3016 fn collapses_descendants_and_uses_the_larger_adjacent_spacing() {
3017 let mut first = paragraph(1, "First", 1, None);
3018 first.text_mut().unwrap().text = Paragraph::new(
3019 "First".into(),
3020 Format {
3021 space_after: Some(4.0),
3022 ..Format::default()
3023 },
3024 );
3025 let mut second = paragraph(2, "Second", 2, Some(1));
3026 second.text_mut().unwrap().text = Paragraph::new(
3027 "Second".into(),
3028 Format {
3029 space_before: Some(7.0),
3030 space_after: Some(3.0),
3031 ..Format::default()
3032 },
3033 );
3034 second.collapsed = true;
3035 let mut outline = Outline {
3036 title: false,
3037 min_width: None,
3038 id: ExGuid::default(),
3039 layout: Layout {
3040 max_width: Some(300.0),
3041 ..Layout::default()
3042 },
3043 indents: vec![18.0, 0.0, 27.0],
3044 paragraphs: vec![
3045 first,
3046 second,
3047 paragraph(3, "Hidden", 6, Some(2)),
3048 paragraph(4, "Last", 1, None),
3049 ],
3050 unsupported: Vec::new(),
3051 };
3052 let mut engine = TextEngine::default();
3053 let layout = outline.layout(&mut engine, &BTreeMap::new()).unwrap();
3054 assert_eq!(
3055 layout.paragraphs.iter().map(|p| p.id.n).collect::<Vec<_>>(),
3056 [1, 2, 4]
3057 );
3058 assert_eq!(
3059 layout.paragraphs[1].origin,
3060 [27.0, layout.paragraphs[0].text.height() + 7.0]
3061 );
3062 assert_eq!(
3063 layout.paragraphs[2].origin[1],
3064 layout.paragraphs[1].origin[1] + layout.paragraphs[1].text.height() + 3.0
3065 );
3066 outline.paragraphs[1].collapsed = false;
3067 let expanded = outline.layout(&mut engine, &BTreeMap::new()).unwrap();
3068 assert_eq!(expanded.paragraphs[2].origin[0], 135.0);
3069 assert!(expanded.size[1] > layout.size[1]);
3070 outline.indents[1] = f32::NAN;
3071 assert!(matches!(
3072 outline.layout(&mut engine, &BTreeMap::new()),
3073 Err(LayoutError::InvalidIndentation)
3074 ));
3075 outline.paragraphs.truncate(1);
3076 for (indents, x) in [(vec![18.0, 0.0, 27.0, 27.0], 0.0), (vec![0.0], 27.0)] {
3077 outline.indents = indents;
3078 let single = outline.layout(&mut engine, &BTreeMap::new()).unwrap();
3079 assert_eq!(single.paragraphs[0].origin[0], x);
3080 }
3081 }
3082
3083 /// Text offsets of levels 1 through 5 as OneNote 2010 draws each table (cold reads of
3084 /// Rust-written outlines, measured to the pixel at 96 dpi).
3085 #[test]
3086 fn indentation_follows_onenote_for_short_and_unusual_tables() {
3087 for (indents, expected) in [
3088 (&[18.0, 0.0, 27.0, 27.0][..], [0.0, 27.0, 54.0, 81.0, 108.0]),
3089 (&[18.0, 0.0], [0.0, 27.0, 54.0, 81.0, 108.0]),
3090 (&[0.0, 0.0], [0.0, 27.0, 54.0, 81.0, 108.0]),
3091 (&[0.0], [27.0, 54.0, 81.0, 108.0, 135.0]),
3092 (&[18.0], [27.0, 54.0, 81.0, 108.0, 135.0]),
3093 (&[], [27.0, 54.0, 81.0, 108.0, 135.0]),
3094 (&[0.0, 5.0], [5.0, 32.0, 59.0, 86.0, 113.0]),
3095 (&[5.0, 10.0, 20.0, 40.0], [10.0, 30.0, 70.0, 97.0, 124.0]),
3096 (
3097 &[40.0, 0.0, 10.0, 20.0, 30.0, 50.0],
3098 [0.0, 10.0, 30.0, 60.0, 110.0],
3099 ),
3100 ] {
3101 for (level, x) in (1..).zip(expected) {
3102 assert_eq!(
3103 indentation(level, indents, 500.0).unwrap(),
3104 x,
3105 "{indents:?}"
3106 );
3107 }
3108 }
3109 }
3110
3111 #[test]
3112 fn marker_height_expands_the_paragraph_box_without_changing_text_line_advances() {
3113 let marker_id = ExGuid {
3114 n: 99,
3115 ..ExGuid::default()
3116 };
3117 let mut short = paragraph(1, "Short", 2, None);
3118 short.lists.push(marker_id);
3119 let mut long = paragraph(
3120 2,
3121 "A long paragraph wraps onto several separate lines of text.",
3122 2,
3123 None,
3124 );
3125 long.lists.push(marker_id);
3126 let outline = Outline {
3127 title: false,
3128 min_width: None,
3129 id: ExGuid::default(),
3130 layout: Layout {
3131 max_width: Some(110.0),
3132 width_set_by_user: Some(true),
3133 ..Layout::default()
3134 },
3135 indents: vec![18.0, 0.0, 27.0],
3136 paragraphs: vec![short, long, paragraph(3, "Last", 1, None)],
3137 unsupported: Vec::new(),
3138 };
3139 let definitions = BTreeMap::from([(
3140 marker_id,
3141 Definition {
3142 kind: Kind::List {
3143 font: Some("Courier New".into()),
3144 format: Some("○".into()),
3145 restart: None,
3146 bullet: Some(4),
3147 },
3148 format: Format {
3149 font_size: Some(22.0),
3150 ..Format::default()
3151 },
3152 },
3153 )]);
3154 let mut engine = TextEngine::default();
3155 let layout = outline.layout(&mut engine, &definitions).unwrap();
3156 let short = &layout.paragraphs[0];
3157 let long = &layout.paragraphs[1];
3158 assert!(short.markers[0].0.height() > short.text.height());
3159 assert!(long.text.height() > long.markers[0].0.height());
3160 assert_eq!(long.origin[1], short.markers[0].0.height());
3161 assert_eq!(
3162 layout.paragraphs[2].origin[1],
3163 long.origin[1] + long.text.height()
3164 );
3165 let plain = engine.layout(long.projection.text(), 83.0).unwrap();
3166 assert_eq!(
3167 long.text
3168 .lines()
3169 .map(|(_, l)| l.baseline)
3170 .collect::<Vec<_>>(),
3171 plain.lines().map(|(_, l)| l.baseline).collect::<Vec<_>>()
3172 );
3173 assert_eq!(
3174 short.origin[1]
3175 + short.markers[0].1[1]
3176 + short.markers[0].0.lines().next().unwrap().1.baseline,
3177 short.origin[1] + short.text.lines().next().unwrap().1.baseline
3178 );
3179 assert_eq!(layout.size[0], 110.0);
3180 }
3181
3182 #[test]
3183 fn symbol_fonts_show_what_windows_draws() {
3184 // OneNote's bullet library stores these (`BULLET_LIBRARY`).
3185 assert_eq!(symbol_text("Symbol", "\u{b7}"), "\u{2022}");
3186 assert_eq!(symbol_text("Symbol", "\u{de}"), "\u{21d2}");
3187 assert_eq!(symbol_text("Symbol", "ap\u{f0}"), "\u{3b1}\u{3c0}\u{f8ff}");
3188 assert_eq!(symbol_text("Wingdings", "l\u{ad}"), "\u{f06c}\u{f0ad}");
3189 assert_eq!(symbol_text("Wingdings 2", "\u{9d}"), "\u{f09d}");
3190 assert_eq!(symbol_text("Calibri", "\u{b7}"), "\u{b7}");
3191 }
3192
3193 #[test]
3194 fn numerals_follow_onenote_number_sequences() {
3195 assert_eq!(numeral(Some('\u{0}'), 12).unwrap(), "12");
3196 assert_eq!(numeral(Some('\u{1}'), 1994).unwrap(), "MCMXCIV");
3197 assert_eq!(numeral(Some('\u{2}'), 4).unwrap(), "iv");
3198 assert_eq!(numeral(Some('\u{3}'), 3).unwrap(), "C");
3199 assert_eq!(numeral(Some('\u{4}'), 26).unwrap(), "z");
3200 assert!(numeral(Some('\u{1}'), 0).is_err());
3201 // As OneNote 2010 renders them through COM: letters to 800, Roman numerals to 4010.
3202 assert_eq!(numeral(Some('\u{4}'), 27).unwrap(), "aa");
3203 assert_eq!(numeral(Some('\u{3}'), 105).unwrap(), "AAAAA");
3204 assert_eq!(numeral(Some('\u{3}'), 780).unwrap(), "Z".repeat(30));
3205 assert_eq!(numeral(Some('\u{3}'), 781).unwrap(), "A");
3206 assert_eq!(numeral(Some('\u{3}'), 800).unwrap(), "T");
3207 assert_eq!(numeral(Some('\u{1}'), 3999).unwrap(), "MMMCMXCIX");
3208 assert_eq!(numeral(Some('\u{1}'), 4009).unwrap(), "MMMMIX");
3209 assert_eq!(numeral(Some('\u{2}'), 4000).unwrap(), "mmmm");
3210 assert_eq!(numeral(Some('\u{16}'), 7).unwrap(), "07");
3211 assert_eq!(numeral(Some('\u{16}'), 106).unwrap(), "106");
3212 assert!(numeral(Some('\u{9}'), 1).is_err());
3213 // As OneNote 2010 renders them through COM, 1 to 130.
3214 let words = |sequence, numbers: &[u32]| {
3215 numbers
3216 .iter()
3217 .map(|number| numeral(Some(sequence), *number).unwrap())
3218 .collect::<Vec<_>>()
3219 };
3220 assert_eq!(
3221 words(
3222 '\u{5}',
3223 &[1, 2, 3, 4, 11, 12, 13, 21, 22, 23, 101, 111, 112, 121]
3224 ),
3225 [
3226 "1st", "2nd", "3rd", "4th", "11th", "12th", "13th", "21st", "22nd", "23rd",
3227 "101st", "111th", "112th", "121st"
3228 ]
3229 );
3230 assert_eq!(
3231 words('\u{6}', &[1, 12, 20, 21, 99, 100, 101, 130]),
3232 [
3233 "One",
3234 "Twelve",
3235 "Twenty",
3236 "Twenty-one",
3237 "Ninety-nine",
3238 "One hundred",
3239 "One hundred one",
3240 "One hundred thirty"
3241 ]
3242 );
3243 assert_eq!(
3244 words(
3245 '\u{7}',
3246 &[1, 2, 3, 5, 8, 9, 12, 20, 21, 40, 100, 101, 112, 130]
3247 ),
3248 [
3249 "First",
3250 "Second",
3251 "Third",
3252 "Fifth",
3253 "Eighth",
3254 "Ninth",
3255 "Twelfth",
3256 "Twentieth",
3257 "Twenty-first",
3258 "Fortieth",
3259 "One hundredth",
3260 "One hundred first",
3261 "One hundred twelfth",
3262 "One hundred thirtieth"
3263 ]
3264 );
3265 }
3266}