1mod background;
2mod picture;
3
4use super::{Paper, colorref};
5use crate::editor::ReadOnlyObject;
6use crate::editor::page::Content;
7use crate::{
8 date::DateField,
9 editor::{CanvasEditor, EditorError},
10 layout::{LayoutError, TextEngine},
11};
12use background::Background;
13pub use background::{template_art, template_picture};
14use draw::Primitive;
15use onestore::page::Page;
16use picture::Picture;
17use std::{
18 fmt,
19 sync::{Arc, Mutex},
20 task::Waker,
21 time::Duration,
22};
23use web_time::Instant;
24
25/// Retained drawing data in the source page's coordinate system.
26pub struct PageScene {
27 reference: Option<Vec<Content>>,
28 /// The definitions a page read without an editor names, as its objects' tags do.
29 definitions: std::collections::BTreeMap<onestore::ExGuid, onestore::page::Definition>,
30 /// Pictures and file icons, by the identity of the object that shows them.
31 pictures: std::collections::BTreeMap<onestore::ExGuid, Picture>,
32 /// Background pictures from OneNote's page templates, painted from their recreations.
33 backgrounds: std::collections::BTreeMap<onestore::ExGuid, Background>,
34}
35
36/// How long the zoom must hold before a raster is made again for it.
37const SETTLE: Duration = Duration::from_millis(150);
38
39/// Where work done off the frame thread lands while its requester keeps it.
40type Slot<T> = Arc<Mutex<Option<T>>>;
41
42/// Runs `work` on the scenes' one raster thread once `start` has passed, unless the slot
43/// returned has been dropped by then; the result lands in the slot and wakes `waker`.
44#[cfg(not(target_arch = "wasm32"))]
45fn queue<T: Send + 'static>(
46 start: Instant,
47 waker: &Waker,
48 work: impl FnOnce() -> T + Send + 'static,
49) -> Slot<T> {
50 use std::sync::{OnceLock, mpsc};
51 static WORKER: OnceLock<mpsc::Sender<Box<dyn FnOnce() + Send>>> = OnceLock::new();
52 let slot = Slot::default();
53 let requester = Arc::downgrade(&slot);
54 let waker = waker.clone();
55 WORKER
56 .get_or_init(|| {
57 let (sender, jobs) = mpsc::channel::<Box<dyn FnOnce() + Send>>();
58 std::thread::Builder::new()
59 .name("page pictures".into())
60 .spawn(move || jobs.into_iter().for_each(|job| job()))
61 .expect("The picture thread starts");
62 sender
63 })
64 .send(Box::new(move || {
65 std::thread::sleep(start.saturating_duration_since(Instant::now()));
66 if let Some(slot) = requester.upgrade() {
67 *slot.lock().unwrap() = Some(work());
68 waker.wake();
69 }
70 }))
71 .expect("The picture thread outlives its jobs");
72 slot
73}
74
75/// `queue` without threads, as in the browser: `work` runs now.
76#[cfg(target_arch = "wasm32")]
77fn queue<T: Send + 'static>(
78 _: Instant,
79 waker: &Waker,
80 work: impl FnOnce() -> T + Send + 'static,
81) -> Slot<T> {
82 waker.wake_by_ref();
83 Arc::new(Mutex::new(Some(work())))
84}
85
86/// Device pixels per point a raster is made at for `scale`: the next half power of two,
87/// so zooming makes rasters again in steps.
88fn density(scale: f32) -> f32 {
89 2_f32.powf((scale.max(0.25).log2() * 2.0).ceil() / 2.0)
90}
91
92#[derive(Debug, PartialEq)]
93pub enum SceneHit<T> {
94 Outline(T),
95 Date(DateField),
96 ReadOnly(usize),
97 Image(onestore::ExGuid),
98 File(onestore::ExGuid),
99 /// A check box tag on a picture or file on the page.
100 Check(onestore::ExGuid),
101}
102
103fn outline_origin(
104 mut origin: [f32; 2],
105 title: Option<onestore::ExGuid>,
106 editor: Option<&CanvasEditor>,
107) -> Result<[f32; 2], SceneError> {
108 if let Some(id) = title {
109 let title = editor
110 .and_then(|editor| editor.visible_outlines().find(|outline| outline.id == id))
111 .ok_or(SceneError::MissingOutline)?;
112 origin[1] += title.bounds().y1 as f32;
113 }
114 Ok(origin)
115}
116
117/// Draws `ink` moved by `offset`, strokes without a colour in the paper's `automatic` ink. A
118/// highlighter's rectangular tip sweeps a band as wide as the tip reaches across each
119/// segment, its colour multiplying what lies beneath as OneNote's MaskPen does. A pressure
120/// pen's width follows its pressure from point to point, as OneNote's does.
121pub(crate) fn append_ink(
122 ink: &onestore::page::Ink,
123 offset: [f32; 2],
124 automatic: [f32; 4],
125 primitives: &mut Vec<Primitive<'_>>,
126) {
127 for stroke in &ink.strokes {
128 let highlighter = stroke.raster_operation == Some(9);
129 let mut color = stroke.color.map_or(automatic, colorref);
130 if !highlighter {
131 color[3] = 1.0 - f32::from(stroke.transparency.unwrap_or(0)) / 255.0;
132 }
133 let round = stroke.pen_tip != Some(1);
134 let size = stroke.width.max(stroke.height);
135 let segment = |from: [f32; 2], to: [f32; 2], [t0, t1]: [f32; 2]| {
136 if !highlighter {
137 if t0 != t1 {
138 return Primitive::Taper {
139 from,
140 to,
141 widths: [size * t0, size * t1],
142 round,
143 color,
144 };
145 }
146 return Primitive::Segment {
147 from,
148 to,
149 width: size * (t0 + t1) / 2.0,
150 round,
151 color,
152 };
153 }
154 let [dx, dy] = [to[0] - from[0], to[1] - from[1]];
155 let length = dx.hypot(dy);
156 let [along, across] = if length > 0.0 {
157 [dx.abs() / length, dy.abs() / length]
158 } else {
159 [1.0, 0.0]
160 };
161 Primitive::Highlight {
162 from,
163 to,
164 width: (stroke.width * across + stroke.height * along) * (t0 + t1) / 2.0,
165 color,
166 }
167 };
168 let mut points = stroke
169 .points
170 .iter()
171 .enumerate()
172 .map(|(index, [x, y])| ([x + offset[0], y + offset[1]], stroke.thickness(index)));
173 let Some((mut from, mut thick)) = points.next() else {
174 continue;
175 };
176 // Samples along a straight run of one width make one segment, since each joint paints
177 // its overlap twice.
178 let (mut end, mut end_thick) = (from, thick);
179 for (to, to_thick) in points {
180 let same = end == from || (to_thick == end_thick && end_thick == thick);
181 if !(same && between(from, end, to)) {
182 primitives.push(segment(from, end, [thick, end_thick]));
183 (from, thick) = (end, end_thick);
184 }
185 (end, end_thick) = (to, to_thick);
186 }
187 primitives.push(segment(from, end, [thick, end_thick]));
188 }
189 for group in &ink.groups {
190 append_ink(group, offset, automatic, primitives);
191 }
192}
193
194/// Whether `middle` lies within a tenth of a point of the segment from `from` to `to`.
195fn between(from: [f32; 2], middle: [f32; 2], to: [f32; 2]) -> bool {
196 let [dx, dy] = [to[0] - from[0], to[1] - from[1]];
197 let [mx, my] = [middle[0] - from[0], middle[1] - from[1]];
198 let length = dx * dx + dy * dy;
199 if length == 0.0 {
200 return mx.hypot(my) <= 0.1;
201 }
202 let along = (mx * dx + my * dy) / length;
203 (0.0..=1.0).contains(&along) && (mx * dy - my * dx).abs() / length.sqrt() <= 0.1
204}
205
206/// The size OneNote 2010 shows a file's icon at, in points.
207pub const ICON_SIZE: [f32; 2] = [24.0, 24.0];
208
209const FILE_ICON: &str = include_str!("../../assets/file.svg");
210
211/// The blank page for a file without an icon of its own, as the 32 pixel PNG OneNote 2010
212/// stores with every attachment; it draws a broken picture for one stored without.
213pub fn file_icon() -> Vec<u8> {
214 background::png(FILE_ICON, 32)
215}
216
217fn append_placeholder(rect: [f32; 4], paper: Paper, primitives: &mut Vec<Primitive<'_>>) {
218 primitives.push(Primitive::Rect {
219 rect,
220 color: paper.shade(colorref(0x00e4ddd6)),
221 });
222 let inner = [rect[0] + 1.0, rect[1] + 1.0, rect[2] - 1.0, rect[3] - 1.0];
223 if inner[0] < inner[2] && inner[1] < inner[3] {
224 primitives.push(Primitive::Rect {
225 rect: inner,
226 color: paper.shade(colorref(0x00faf7f3)),
227 });
228 }
229}
230
231#[derive(Debug)]
232pub enum SceneError {
233 Layout(LayoutError),
234 Editor(EditorError),
235 MissingOutline,
236 MissingImage,
237 InvalidGeometry,
238}
239
240impl fmt::Display for SceneError {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 match self {
243 Self::Layout(error) => error.fmt(f),
244 Self::Editor(error) => error.fmt(f),
245 Self::MissingOutline => f.write_str("An editable outline is missing from this page."),
246 Self::MissingImage => f.write_str("An image is missing its data or dimensions."),
247 Self::InvalidGeometry => {
248 f.write_str("The page contains invalid object dimensions or positions.")
249 }
250 }
251 }
252}
253impl std::error::Error for SceneError {}
254impl From<LayoutError> for SceneError {
255 fn from(error: LayoutError) -> Self {
256 Self::Layout(error)
257 }
258}
259
260impl From<EditorError> for SceneError {
261 fn from(error: EditorError) -> Self {
262 match error {
263 EditorError::InvalidGeometry => Self::InvalidGeometry,
264 error => Self::Editor(error),
265 }
266 }
267}
268
269impl PageScene {
270 pub fn new(mut page: Page, engine: &mut TextEngine) -> Result<Self, SceneError> {
271 let mut objects = crate::editor::page::build(&mut page, engine, false)
272 .map_err(SceneError::from)?
273 .objects;
274 let pictures = Self::pictures(&objects, None)?;
275 pictures.mark_unavailable(&mut objects, engine)?;
276 Ok(Self {
277 reference: Some(objects),
278 definitions: std::mem::take(&mut page.definitions),
279 ..pictures
280 })
281 }
282
283 pub fn from_page(
284 page: Page,
285 engine: &mut TextEngine,
286 ) -> Result<(Self, CanvasEditor), SceneError> {
287 let mut editor = CanvasEditor::from_page(page, engine).map_err(SceneError::from)?;
288 let scene = Self::pictures(&editor.objects, Some(&editor))?;
289 scene.mark_unavailable(&mut editor.objects, engine)?;
290 Ok((scene, editor))
291 }
292
293 /// Follows `editor` after it refreshed from storage, keeping the rasters of pictures
294 /// whose bytes did not change so they do not blink.
295 pub fn refresh(
296 &mut self,
297 editor: &mut CanvasEditor,
298 engine: &mut TextEngine,
299 ) -> Result<(), SceneError> {
300 let mut scene = Self::pictures(&editor.objects, Some(editor))?;
301 for (id, picture) in &mut scene.pictures {
302 if let Some(shown) = self.pictures.remove(id)
303 && shown.same(picture)
304 {
305 *picture = shown;
306 }
307 }
308 for (id, art) in &mut scene.backgrounds {
309 match self.backgrounds.remove(id) {
310 Some(shown) if shown.same(art) => *art = shown,
311 _ => art.inline = true,
312 }
313 }
314 scene.mark_unavailable(&mut editor.objects, engine)?;
315 *self = scene;
316 Ok(())
317 }
318
319 /// Follows pictures and file previews brought by an edit or undo.
320 pub(crate) fn follow(
321 &mut self,
322 editor: &mut CanvasEditor,
323 engine: &mut TextEngine,
324 ) -> Result<(), SceneError> {
325 let known = |object: &Content| match object {
326 Content::Image(source) => {
327 self.pictures.contains_key(&source.id) || self.backgrounds.contains_key(&source.id)
328 }
329 Content::File { source, .. } => {
330 source.preview.is_none() || self.pictures.contains_key(&source.id)
331 }
332 _ => true,
333 };
334 let outlines_known = editor.outlines().iter().all(|outline| {
335 crate::document::descendants(outline.document().nodes(), None).all(|(_, _, node)| {
336 match &node.content {
337 onestore::page::ParagraphContent::Image(image) => {
338 self.pictures.contains_key(&image.id)
339 }
340 onestore::page::ParagraphContent::Attachment(file) => {
341 file.preview.is_none() || self.pictures.contains_key(&file.id)
342 }
343 _ => true,
344 }
345 })
346 });
347 if editor.objects.iter().all(known) && outlines_known {
348 return Ok(());
349 }
350 self.refresh(editor, engine)
351 }
352
353 /// Turns page pictures the renderer cannot decode into placeholders; their stored data
354 /// is kept.
355 fn mark_unavailable(
356 &self,
357 objects: &mut Vec<Content>,
358 engine: &mut TextEngine,
359 ) -> Result<(), SceneError> {
360 *objects = std::mem::take(objects)
361 .into_iter()
362 .map(|object| match object {
363 Content::Image(source)
364 if !self.pictures.contains_key(&source.id)
365 && !self.backgrounds.contains_key(&source.id) =>
366 {
367 Content::unavailable(onestore::page::PageObject::Image(source), engine)
368 }
369 object => Ok(object),
370 })
371 .collect::<Result<_, _>>()?;
372 Ok(())
373 }
374
375 /// Every picture the page draws, page-level ones and those inside outlines, except the
376 /// template backgrounds it recognises; one the renderer cannot decode is left out. A
377 /// picture shows its stored bytes, else the raster OneNote made of them.
378 fn pictures(objects: &[Content], editor: Option<&CanvasEditor>) -> Result<Self, SceneError> {
379 type Payloads<'a> = Vec<(onestore::ExGuid, [Option<&'a Arc<[u8]>>; 2])>;
380 fn nested<'a>(nodes: &'a [onestore::page::PageParagraph], payloads: &mut Payloads<'a>) {
381 for node in nodes {
382 match &node.content {
383 onestore::page::ParagraphContent::Image(image) => {
384 payloads.push((image.id, [image.bytes.as_ref(), image.display.as_ref()]))
385 }
386 onestore::page::ParagraphContent::Attachment(file) => {
387 payloads.push((file.id, [file.preview.as_ref(), None]))
388 }
389 onestore::page::ParagraphContent::Table(table) => {
390 for cell in table.rows.iter().flat_map(|row| &row.cells) {
391 nested(&cell.paragraphs, payloads);
392 }
393 }
394 _ => {}
395 }
396 }
397 }
398 let mut payloads = Vec::new();
399 let mut backgrounds = std::collections::BTreeMap::new();
400 for object in objects {
401 match object {
402 Content::Image(source) => {
403 if source.background
404 && let Some(art) = Background::recognise(source)
405 {
406 backgrounds.insert(source.id, art);
407 } else {
408 payloads.push((source.id, [source.bytes.as_ref(), source.display.as_ref()]))
409 }
410 }
411 Content::File { source, .. } => {
412 payloads.push((source.id, [source.preview.as_ref(), None]))
413 }
414 Content::Outline { source, .. } => nested(&source.paragraphs, &mut payloads),
415 Content::Editable(_)
416 | Content::Date { .. }
417 | Content::Ink(_)
418 | Content::ReadOnly(_) => {}
419 }
420 }
421 if let Some(editor) = editor {
422 for outline in editor.outlines() {
423 nested(outline.document().nodes(), &mut payloads);
424 }
425 }
426 let mut pictures = std::collections::BTreeMap::new();
427 for (id, encoded) in payloads {
428 let Some(picture) = encoded.into_iter().flatten().find_map(Picture::new) else {
429 continue;
430 };
431 if pictures.insert(id, picture).is_some() {
432 return Err(SceneError::InvalidGeometry);
433 }
434 }
435 Ok(Self {
436 reference: None,
437 definitions: Default::default(),
438 pictures,
439 backgrounds,
440 })
441 }
442
443 /// Brings template backgrounds to `paper` and the pictures near `view`, a rectangle in
444 /// scene coordinates, to `scale` device pixels per point, rasterizing and decoding off
445 /// this thread; `waker` is woken when one lands and the page should be drawn again.
446 /// Call before collecting each frame's primitives. True once everything in view shows
447 /// its raster for this paper and scale.
448 pub(crate) fn update_pictures(
449 &mut self,
450 editor: Option<&CanvasEditor>,
451 view: [f32; 4],
452 scale: f32,
453 paper: Paper,
454 waker: &Waker,
455 ) -> bool {
456 let mut settled = true;
457 for art in self.backgrounds.values_mut() {
458 settled &= art.update(paper, scale, waker);
459 }
460 let rects = self.picture_rects(editor);
461 picture::update(&mut self.pictures, rects, view, scale, waker) && settled
462 }
463
464 /// Waits until the whole page shows its rasters for `paper` at `scale`, for drawing it
465 /// offscreen.
466 pub fn settle(&mut self, editor: Option<&CanvasEditor>, scale: f32, paper: Paper) {
467 let everything = [
468 f32::NEG_INFINITY,
469 f32::NEG_INFINITY,
470 f32::INFINITY,
471 f32::INFINITY,
472 ];
473 while !self.update_pictures(editor, everything, scale, paper, Waker::noop()) {
474 std::thread::sleep(Duration::from_millis(1));
475 }
476 }
477
478 /// Bytes of the rasters the scene keeps decoded, for bounding what a cache of scenes holds.
479 pub fn raster_bytes(&self) -> u64 {
480 let size = |image: &draw::RasterImage| {
481 let [width, height] = image.size();
482 u64::from(width) * u64::from(height) * 4
483 };
484 let pictures = self.pictures.values().filter_map(Picture::image);
485 let art = self.backgrounds.values().flat_map(Background::images);
486 pictures.chain(art).map(size).sum()
487 }
488
489 /// The raster a picture or file icon shows, once decoded.
490 pub fn image(&self, id: onestore::ExGuid) -> Option<&draw::RasterImage> {
491 self.pictures.get(&id)?.image()
492 }
493
494 /// Where pictures and file icons sit, in scene coordinates.
495 fn picture_rects(&self, editor: Option<&CanvasEditor>) -> Vec<(onestore::ExGuid, [f32; 4])> {
496 fn outline(
497 layout: &crate::outline::OutlineLayout,
498 [x, y]: [f32; 2],
499 rects: &mut Vec<(onestore::ExGuid, [f32; 4])>,
500 ) {
501 rects.extend(
502 layout
503 .objects
504 .iter()
505 .map(|object| (object.id, crate::translated(object.rect, [x, y]))),
506 );
507 }
508 let mut rects = Vec::new();
509 for content in self.objects(editor).into_iter().flatten() {
510 match content {
511 Content::Image(source) => {
512 let [x, y] = crate::origin(&source.layout);
513 if let Some([width, height]) = crate::outline::image_size(source) {
514 rects.push((source.id, [x, y, x + width, y + height]));
515 }
516 }
517 Content::File { source, layout } => {
518 let origin = crate::origin(&source.layout);
519 rects.push((source.id, crate::translated(layout.rect, origin)));
520 }
521 Content::Outline {
522 source,
523 layout,
524 below_title,
525 } => {
526 let origin = crate::origin(&source.layout);
527 if let Ok(origin) = outline_origin(origin, *below_title, editor) {
528 outline(layout, origin, &mut rects);
529 }
530 }
531 Content::Editable(_)
532 | Content::Date { .. }
533 | Content::Ink(_)
534 | Content::ReadOnly(_) => {}
535 }
536 }
537 if let Some(editor) = editor {
538 for editable in editor.visible_outlines() {
539 outline(editable.shaped(), editable.origin(), &mut rects);
540 }
541 }
542 rects
543 }
544
545 /// Pictures, files and handwriting inside an outline whose origin is `origin`.
546 /// `moving` draws one picture at a previewed rectangle, in the same coordinates as `origin`.
547 pub(crate) fn append_outline_objects<'a>(
548 &'a self,
549 outline: &'a crate::outline::OutlineLayout,
550 origin: [f32; 2],
551 moving: Option<(onestore::ExGuid, [f32; 4])>,
552 paper: Paper,
553 primitives: &mut Vec<Primitive<'a>>,
554 ) {
555 for object in &outline.objects {
556 self.append_object(object, origin, moving, paper, primitives);
557 }
558 }
559
560 /// One picture, file, drawing or placeholder laid out from `origin`; `moving` as for
561 /// `append_outline_objects`.
562 fn append_object<'a>(
563 &'a self,
564 object: &'a crate::outline::ObjectLayout,
565 origin: [f32; 2],
566 moving: Option<(onestore::ExGuid, [f32; 4])>,
567 paper: Paper,
568 primitives: &mut Vec<Primitive<'a>>,
569 ) {
570 let rect = match moving {
571 Some((id, rect)) if id == object.id => rect,
572 _ => crate::translated(object.rect, origin),
573 };
574 let picture = self.pictures.get(&object.id);
575 if let Some(image) = picture.and_then(Picture::image) {
576 primitives.push(Primitive::Image { image, rect });
577 }
578 match &object.kind {
579 crate::outline::ObjectKind::Ink(ink) => {
580 append_ink(ink, [rect[0], rect[1]], paper.ink, primitives)
581 }
582 crate::outline::ObjectKind::Unsupported(_) => {
583 append_placeholder(rect, paper, primitives)
584 }
585 crate::outline::ObjectKind::Picture if picture.is_none_or(Picture::failed) => {
586 append_placeholder(rect, paper, primitives)
587 }
588 // A file stored without the icon OneNote rendered for it shows a blank page.
589 crate::outline::ObjectKind::File(_) if picture.is_none() => {
590 primitives.push(Primitive::Icon {
591 sources: &[FILE_ICON],
592 origin: [rect[0], rect[1]],
593 size: rect[2] - rect[0],
594 tint: [1.0; 4],
595 palette: draw::Palette::default(),
596 })
597 }
598 crate::outline::ObjectKind::Picture | crate::outline::ObjectKind::File(_) => {}
599 }
600 if let Some(label) = object.label() {
601 primitives.push(Primitive::Text {
602 text: &label.text,
603 origin: [origin[0] + label.origin[0], origin[1] + label.origin[1]],
604 clip: None,
605 ink: paper.ink,
606 });
607 }
608 }
609
610 fn objects<'a>(
611 &'a self,
612 editor: Option<&'a CanvasEditor>,
613 ) -> Result<&'a [Content], SceneError> {
614 self.reference
615 .as_deref()
616 .or_else(|| editor.map(|editor| editor.objects.as_slice()))
617 .ok_or(SceneError::MissingOutline)
618 }
619
620 /// The note tags of the pictures and files on the page, each with the object it marks,
621 /// in page points; `moving` shows one at a previewed rectangle.
622 fn object_tags(
623 &self,
624 editor: Option<&CanvasEditor>,
625 moving: Option<(onestore::ExGuid, [f32; 4])>,
626 ) -> Vec<(onestore::ExGuid, crate::outline::ParagraphTag)> {
627 let definitions = match (&self.reference, editor) {
628 (None, Some(editor)) => &editor.definitions,
629 _ => &self.definitions,
630 };
631 let Ok(objects) = self.objects(editor) else {
632 return Vec::new();
633 };
634 objects
635 .iter()
636 .filter_map(|object| {
637 let (id, tags, bounds) = object.tagged()?;
638 let bounds = match moving {
639 Some((moved, rect)) if moved == id => rect,
640 _ => bounds,
641 };
642 Some(
643 crate::outline::object_tags(tags, definitions, bounds)
644 .into_iter()
645 .map(move |tag| (id, tag)),
646 )
647 })
648 .flatten()
649 .collect()
650 }
651
652 pub fn read_only<'a>(
653 &'a self,
654 editor: Option<&'a CanvasEditor>,
655 ) -> impl Iterator<Item = &'a ReadOnlyObject> {
656 self.objects(editor)
657 .into_iter()
658 .flatten()
659 .filter_map(|object| match object {
660 Content::ReadOnly(object) => Some(object.as_ref()),
661 _ => None,
662 })
663 }
664
665 /// Bounds of noneditable content in page coordinates, each with whether OneNote's view
666 /// leaves room beyond it: a picture's corner is as far as the view reaches.
667 pub(crate) fn content_bounds<'a>(
668 &'a self,
669 editor: &'a CanvasEditor,
670 ) -> impl Iterator<Item = ([f32; 4], bool)> + 'a {
671 self.objects(Some(editor))
672 .into_iter()
673 .flatten()
674 .filter_map(move |object| match object {
675 Content::Outline {
676 source,
677 layout,
678 below_title,
679 } => {
680 let origin =
681 outline_origin(crate::origin(&source.layout), *below_title, Some(editor))
682 .ok()?;
683 Some((
684 [
685 origin[0],
686 origin[1],
687 origin[0] + layout.size[0],
688 origin[1] + layout.size[1],
689 ],
690 true,
691 ))
692 }
693 Content::Date { below_title } => {
694 let date = editor.date()?;
695 let origin = outline_origin(
696 crate::origin(&date.source().layout),
697 *below_title,
698 Some(editor),
699 )
700 .ok()?;
701 let size = date.layout().size;
702 Some((
703 [
704 origin[0],
705 origin[1],
706 origin[0] + size[0],
707 origin[1] + size[1],
708 ],
709 true,
710 ))
711 }
712 Content::Image(source) => {
713 let [x, y] = crate::origin(&source.layout);
714 let [width, height] = crate::outline::image_size(source)?;
715 Some(([x, y, x + width, y + height], false))
716 }
717 Content::File { .. } => Some((object.file()?.1, true)),
718 Content::Ink(ink) => Some((crate::editor::page::ink_bounds(ink)?, true)),
719 Content::ReadOnly(object) => Some((object.rect(), true)),
720 Content::Editable(_) => None,
721 })
722 }
723
724 pub(crate) fn date_fields(
725 &self,
726 editor: &CanvasEditor,
727 ) -> impl Iterator<Item = (DateField, [f32; 4])> {
728 let mut fields = [None; 2];
729 if let Some(date) = editor.date()
730 && let Some((origin, title)) = self
731 .objects(Some(editor))
732 .into_iter()
733 .flatten()
734 .find_map(|object| match object {
735 Content::Date { below_title } => {
736 Some((crate::origin(&date.source().layout), *below_title))
737 }
738 _ => None,
739 })
740 && let Ok(origin) = outline_origin(origin, title, Some(editor))
741 {
742 for (slot, (field, paragraph)) in fields.iter_mut().zip(
743 date.fields()
744 .map(|(field, _)| field)
745 .zip(&date.layout().paragraphs),
746 ) {
747 let x = origin[0] + paragraph.origin[0];
748 let y = origin[1] + paragraph.origin[1];
749 let width = paragraph
750 .text
751 .lines()
752 .map(|(line, _)| line.metrics().advance)
753 .fold(0.0_f32, f32::max);
754 *slot = Some((
755 field,
756 [x, y, x + width.max(12.0), y + paragraph.text.height()],
757 ));
758 }
759 }
760 fields.into_iter().flatten()
761 }
762
763 /// The point and outline hit callback use scene coordinates.
764 pub fn hit_test<T>(
765 &self,
766 point: [f32; 2],
767 editor: Option<&CanvasEditor>,
768 mut outline: impl FnMut(onestore::ExGuid) -> Option<T>,
769 ) -> Option<SceneHit<T>> {
770 let check = self.object_tags(editor, None).into_iter().find(|(_, tag)| {
771 tag.icon.checkable()
772 && (tag.origin[0]..=tag.origin[0] + tag.size).contains(&point[0])
773 && (tag.origin[1]..=tag.origin[1] + tag.size).contains(&point[1])
774 });
775 if let Some((id, _)) = check {
776 return Some(SceneHit::Check(id));
777 }
778 let mut readonly = self.read_only(editor).count();
779 for object in self.objects(editor).ok()?.iter().rev() {
780 match object {
781 Content::Date { .. } => {
782 if let Some(editor) = editor
783 && let Some((field, _)) = self.date_fields(editor).find(|(_, rect)| {
784 (rect[0]..=rect[2]).contains(&point[0])
785 && (rect[1]..=rect[3]).contains(&point[1])
786 })
787 {
788 return Some(SceneHit::Date(field));
789 }
790 }
791 Content::Editable(id) => {
792 if let Some(hit) = outline(*id) {
793 return Some(SceneHit::Outline(hit));
794 }
795 }
796 Content::ReadOnly(object) => {
797 readonly -= 1;
798 let [x0, y0, x1, y1] = object.rect();
799 if (x0..=x1).contains(&point[0]) && (y0..=y1).contains(&point[1]) {
800 return Some(SceneHit::ReadOnly(readonly));
801 }
802 }
803 Content::Image(source) if !source.background => {
804 let [x, y] = crate::origin(&source.layout);
805 if let Some([width, height]) = crate::outline::image_size(source)
806 && (x..=x + width).contains(&point[0])
807 && (y..=y + height).contains(&point[1])
808 {
809 return Some(SceneHit::Image(source.id));
810 }
811 }
812 Content::File { .. } => {
813 let (file, [x0, y0, x1, y1]) = object.file()?;
814 if (x0..=x1).contains(&point[0]) && (y0..=y1).contains(&point[1]) {
815 return Some(SceneHit::File(file.id));
816 }
817 }
818 Content::Outline { .. } | Content::Image(_) | Content::Ink(_) => {}
819 }
820 }
821 None
822 }
823
824 /// Where the page read without an editor lies, in page coordinates: the rectangle all
825 /// its content covers, and the top and bottom of each line of text and each picture, which
826 /// a sheet of paper does not cut through where it can help it.
827 #[cfg(feature = "pdf")]
828 pub(crate) fn printed_extent(&self) -> Result<([f32; 4], Vec<[f32; 2]>), SceneError> {
829 let mut bounds = [
830 f32::INFINITY,
831 f32::INFINITY,
832 f32::NEG_INFINITY,
833 f32::NEG_INFINITY,
834 ];
835 let mut rows = Vec::new();
836 let mut cover = |[x0, y0, x1, y1]: [f32; 4], row: bool| {
837 bounds = [
838 bounds[0].min(x0),
839 bounds[1].min(y0),
840 bounds[2].max(x1),
841 bounds[3].max(y1),
842 ];
843 if row {
844 rows.push([y0, y1]);
845 }
846 };
847 for content in self.objects(None)? {
848 match content {
849 Content::Outline {
850 source,
851 layout,
852 below_title,
853 } => {
854 let [x, y] = outline_origin(crate::origin(&source.layout), *below_title, None)?;
855 cover([x, y, x + layout.size[0], y + layout.size[1]], false);
856 for paragraph in &layout.paragraphs {
857 let [px, py] = [x + paragraph.origin[0], y + paragraph.origin[1]];
858 for (line, bounds) in paragraph.text.lines() {
859 let top = py + bounds.top;
860 let right = px + line.metrics().advance;
861 cover([px, top, right, top + bounds.height], true);
862 }
863 }
864 for object in &layout.objects {
865 let [x0, y0, x1, y1] = object.rect;
866 cover([x + x0, y + y0, x + x1, y + object.bottom.max(y1)], true);
867 }
868 }
869 Content::Image(source) => {
870 let [x, y] = crate::origin(&source.layout);
871 let size = match self.backgrounds.get(&source.id) {
872 Some(art) => Some(art.size),
873 None => crate::outline::image_size(source),
874 };
875 if let Some([width, height]) = size {
876 cover([x, y, x + width, y + height], !source.background);
877 }
878 }
879 Content::File { .. } => {
880 if let Some((_, rect)) = content.file() {
881 cover(rect, true);
882 }
883 }
884 Content::Ink(ink) => {
885 if let Some(rect) = crate::editor::page::ink_bounds(ink) {
886 cover(rect, false);
887 }
888 }
889 Content::ReadOnly(object) => cover(object.rect(), true),
890 Content::Date { .. } | Content::Editable(_) => {}
891 }
892 }
893 Ok((bounds, rows))
894 }
895
896 /// The offset is in document points.
897 pub fn append_primitives<'a>(
898 &'a self,
899 primitives: &mut Vec<Primitive<'a>>,
900 offset: [f32; 2],
901 paper: Paper,
902 ) -> Result<(), SceneError> {
903 self.append_primitives_with(primitives, offset, None, None, paper, |_, _, _| {
904 Err(SceneError::MissingOutline)
905 })
906 }
907
908 /// `moving` draws one picture at a previewed rectangle instead of its stored layout.
909 /// Outlines the editor does not hold draw their tags' symbols.
910 pub(crate) fn append_primitives_with<'a, E: From<SceneError>>(
911 &'a self,
912 primitives: &mut Vec<Primitive<'a>>,
913 offset: [f32; 2],
914 editor: Option<&'a CanvasEditor>,
915 moving: Option<(onestore::ExGuid, [f32; 4])>,
916 paper: Paper,
917 mut outline: impl FnMut(onestore::ExGuid, [f32; 2], &mut Vec<Primitive<'a>>) -> Result<(), E>,
918 ) -> Result<(), E> {
919 for content in self.objects(editor)? {
920 let origin = match content {
921 Content::Outline {
922 source,
923 below_title,
924 ..
925 } => outline_origin(crate::origin(&source.layout), *below_title, editor)?,
926 Content::Date { below_title } => {
927 let date = editor
928 .and_then(CanvasEditor::date)
929 .ok_or(SceneError::MissingOutline)?;
930 outline_origin(crate::origin(&date.source().layout), *below_title, editor)?
931 }
932 Content::Image(source) => crate::origin(&source.layout),
933 Content::ReadOnly(object) => {
934 let rect = crate::translated(object.rect(), offset);
935 append_placeholder(rect, paper, primitives);
936 primitives.push(Primitive::Text {
937 clip: None,
938 text: &object.label,
939 origin: [rect[0] + 8.0, rect[1] + 8.0],
940 ink: paper.shade(colorref(0x005d554e)),
941 });
942 continue;
943 }
944 Content::Editable(id) => {
945 outline(*id, offset, primitives)?;
946 continue;
947 }
948 Content::Ink(ink) => {
949 let [x, y] = crate::origin(&ink.layout);
950 let [dx, dy] = editor.map_or([0.0; 2], |editor| editor.ink_drag_offset(ink.id));
951 let offset = [offset[0] + x + dx, offset[1] + y + dy];
952 append_ink(ink, offset, paper.ink, primitives);
953 continue;
954 }
955 Content::File { source, layout } => {
956 // A dragged file draws where its column would land.
957 let [x, y] = match moving {
958 Some((id, [x0, y0, ..])) if id == source.id => [x0, y0],
959 _ => crate::origin(&source.layout),
960 };
961 self.append_object(
962 layout,
963 [x + offset[0], y + offset[1]],
964 None,
965 paper,
966 primitives,
967 );
968 continue;
969 }
970 };
971 let object_origin = [origin[0] + offset[0], origin[1] + offset[1]];
972 match content {
973 Content::Image(source) => {
974 let (image, [width, height]) = match self.backgrounds.get(&source.id) {
975 Some(art) => {
976 // OneNote's art is opaque, over white, and hides the rule lines.
977 primitives.push(Primitive::Rect {
978 rect: [
979 object_origin[0],
980 object_origin[1],
981 object_origin[0] + art.size[0],
982 object_origin[1] + art.size[1],
983 ],
984 color: if background::dark(paper) {
985 paper.color
986 } else {
987 Paper::WHITE.color
988 },
989 });
990 (art.image(paper), art.size)
991 }
992 None => {
993 let picture = self
994 .pictures
995 .get(&source.id)
996 .ok_or(SceneError::MissingImage)?;
997 let size = crate::outline::image_size(source)
998 .ok_or(SceneError::MissingImage)?;
999 if picture.failed() {
1000 append_placeholder(
1001 [
1002 object_origin[0],
1003 object_origin[1],
1004 object_origin[0] + size[0],
1005 object_origin[1] + size[1],
1006 ],
1007 paper,
1008 primitives,
1009 );
1010 }
1011 (picture.image(), size)
1012 }
1013 };
1014 let Some(image) = image else {
1015 continue;
1016 };
1017 let rect = match moving {
1018 Some((id, rect)) if id == source.id => crate::translated(rect, offset),
1019 _ => [
1020 object_origin[0],
1021 object_origin[1],
1022 object_origin[0] + width,
1023 object_origin[1] + height,
1024 ],
1025 };
1026 primitives.push(Primitive::Image { image, rect });
1027 // OneNote frames a printout's page with a one-pixel grey line.
1028 if source.printout.is_some() {
1029 let [x0, y0, x1, y1] = rect;
1030 primitives.push(Primitive::RoundedRect {
1031 rect: [x0 + 0.375, y0 + 0.375, x1 - 0.375, y1 - 0.375],
1032 radius: [0.0; 2],
1033 stroke: Some(draw::Stroke::Solid(0.75)),
1034 color: paper.shade(colorref(0x00cc_cccc)),
1035 });
1036 }
1037 }
1038 Content::Outline { .. } | Content::Date { .. } => {
1039 let outline = match content {
1040 Content::Outline { layout, .. } => layout,
1041 _ => editor
1042 .and_then(CanvasEditor::date)
1043 .ok_or(SceneError::MissingOutline)?
1044 .layout(),
1045 };
1046 outline.append_table_primitives(primitives, object_origin, paper);
1047 let art = super::TagArt::default();
1048 outline.append_block_tag_primitives(primitives, object_origin, &art);
1049 let everything = [f32::NEG_INFINITY, f32::INFINITY];
1050 outline.append_background_primitives(
1051 primitives,
1052 object_origin,
1053 everything,
1054 paper,
1055 );
1056 self.append_outline_objects(outline, object_origin, None, paper, primitives);
1057 for (index, paragraph) in outline.visible(everything) {
1058 outline.append_paragraph_primitives(
1059 index,
1060 paragraph,
1061 object_origin,
1062 paper.ink,
1063 &art,
1064 primitives,
1065 );
1066 }
1067 }
1068 Content::Editable(_)
1069 | Content::ReadOnly(_)
1070 | Content::Ink(_)
1071 | Content::File { .. } => unreachable!(),
1072 }
1073 }
1074 let art = super::TagArt::default();
1075 for (_, tag) in self.object_tags(editor, moving) {
1076 primitives.push(Primitive::Icon {
1077 sources: art.sources(&tag),
1078 origin: [tag.origin[0] + offset[0], tag.origin[1] + offset[1]],
1079 size: tag.size,
1080 tint: [1.0, 1.0, 1.0, if tag.disabled { 0.45 } else { 1.0 }],
1081 palette: draw::Palette::default(),
1082 });
1083 }
1084 Ok(())
1085 }
1086}
1087
1088impl crate::outline::OutlineLayout {
1089 /// Paragraphs that may paint between outline-local `rows`, with their indices.
1090 pub(crate) fn visible(
1091 &self,
1092 rows: [f32; 2],
1093 ) -> impl Iterator<Item = (usize, &crate::outline::ParagraphLayout)> {
1094 // Markers, tags and overhanging ink stay within this of a paragraph's lines.
1095 const OVERHANG: f32 = 72.0;
1096 self.paragraphs
1097 .iter()
1098 .enumerate()
1099 .filter(move |(_, paragraph)| {
1100 let [start, end] = [
1101 paragraph.origin[1] - OVERHANG,
1102 paragraph.origin[1] + paragraph.text.height() + OVERHANG,
1103 ];
1104 end >= rows[0] && start <= rows[1]
1105 })
1106 }
1107
1108 /// Highlights behind the paragraphs `visible` finds between outline-local `rows`. A black
1109 /// highlight paints in the paper's ink, censoring the automatic text on it in any theme.
1110 /// A paragraph's band spans its outline, or its cell, tinted for the paper.
1111 pub(crate) fn append_background_primitives(
1112 &self,
1113 primitives: &mut Vec<Primitive<'_>>,
1114 origin: [f32; 2],
1115 rows: [f32; 2],
1116 paper: Paper,
1117 ) {
1118 for (index, paragraph) in self.visible(rows) {
1119 if let Some(band) = paragraph.band {
1120 let mut rect = parley::BoundingBox::new(
1121 0.0,
1122 f64::from(paragraph.origin[1]),
1123 f64::from(self.size[0]),
1124 f64::from(paragraph.origin[1] + paragraph.text.height()),
1125 );
1126 let cell = self.paragraph_cell(index);
1127 if let Some(clipped) = cell.map_or(Some(rect), |cell| cell.clip(rect)) {
1128 rect = clipped;
1129 primitives.push(Primitive::Rect {
1130 rect: [
1131 rect.x0 as f32 + origin[0],
1132 rect.y0 as f32 + origin[1],
1133 rect.x1 as f32 + origin[0],
1134 rect.y1 as f32 + origin[1],
1135 ],
1136 color: paper.tint(colorref(band)),
1137 });
1138 }
1139 }
1140 for (mut rect, color) in paragraph.text.backgrounds() {
1141 rect.x0 += f64::from(paragraph.origin[0]);
1142 rect.x1 += f64::from(paragraph.origin[0]);
1143 rect.y0 += f64::from(paragraph.origin[1]);
1144 rect.y1 += f64::from(paragraph.origin[1]);
1145 if let Some(cell) = self.paragraph_cell(index) {
1146 let Some(clipped) = cell.clip(rect) else {
1147 continue;
1148 };
1149 rect = clipped;
1150 }
1151 primitives.push(Primitive::Rect {
1152 rect: [
1153 rect.x0 as f32 + origin[0],
1154 rect.y0 as f32 + origin[1],
1155 rect.x1 as f32 + origin[0],
1156 rect.y1 as f32 + origin[1],
1157 ],
1158 color: if color == 0 {
1159 paper.ink
1160 } else {
1161 colorref(color)
1162 },
1163 });
1164 }
1165 }
1166 }
1167
1168 /// One paragraph of this outline, whose origin is `origin`: its text or equation, list
1169 /// markers and tags, those `art` maps drawn with their art.
1170 pub(crate) fn append_paragraph_primitives<'a>(
1171 &'a self,
1172 index: usize,
1173 paragraph: &'a crate::outline::ParagraphLayout,
1174 origin: [f32; 2],
1175 ink: [f32; 4],
1176 art: &super::TagArt,
1177 primitives: &mut Vec<Primitive<'a>>,
1178 ) {
1179 let [x, y] = [
1180 origin[0] + paragraph.origin[0],
1181 origin[1] + paragraph.origin[1],
1182 ];
1183 let clip = self.paragraph_cell(index).map(|cell| {
1184 let [left, top, right, bottom] = cell.text_bounds();
1185 [
1186 left + origin[0],
1187 top + origin[1],
1188 right + origin[0],
1189 bottom + origin[1],
1190 ]
1191 });
1192 primitives.push(Primitive::Text {
1193 clip,
1194 text: &paragraph.text,
1195 origin: [x, y],
1196 ink,
1197 });
1198 for (index, [left, baseline]) in paragraph.text.spaces() {
1199 let math = &paragraph.math[index];
1200 let [x, y] = [x + left, y + baseline - math.baseline];
1201 for item in &math.items {
1202 primitives.push(match item {
1203 crate::math::MathItem::Text { layout, origin } => Primitive::Text {
1204 clip,
1205 text: layout,
1206 origin: [x + origin[0], y + origin[1]],
1207 ink,
1208 },
1209 // As pen strokes, so hairline rules keep a device pixel.
1210 crate::math::MathItem::Rule([x0, y0, x1, y1]) => {
1211 let width = (x1 - x0).min(y1 - y0);
1212 let [from, to] = if x1 - x0 >= y1 - y0 {
1213 let middle = (y0 + y1) / 2.0;
1214 [[x0 + width / 2.0, middle], [x1 - width / 2.0, middle]]
1215 } else {
1216 let middle = (x0 + x1) / 2.0;
1217 [[middle, y0 + width / 2.0], [middle, y1 - width / 2.0]]
1218 };
1219 Primitive::Segment {
1220 from: [x + from[0], y + from[1]],
1221 to: [x + to[0], y + to[1]],
1222 width,
1223 round: false,
1224 color: math.color.map_or(ink, colorref),
1225 }
1226 }
1227 crate::math::MathItem::Stroke { from, to, width } => Primitive::Segment {
1228 from: [x + from[0], y + from[1]],
1229 to: [x + to[0], y + to[1]],
1230 width: *width,
1231 round: true,
1232 color: math.color.map_or(ink, colorref),
1233 },
1234 });
1235 }
1236 }
1237 for (layout, marker) in &paragraph.markers {
1238 primitives.push(Primitive::Text {
1239 clip,
1240 text: layout,
1241 origin: [origin[0] + marker[0], y + marker[1]],
1242 ink,
1243 });
1244 }
1245 for tag in &paragraph.tags {
1246 primitives.push(Primitive::Icon {
1247 sources: art.sources(tag),
1248 origin: [
1249 origin[0] + self.tag_column_offset() + tag.origin[0],
1250 y + tag.origin[1],
1251 ],
1252 size: tag.size,
1253 tint: [1.0, 1.0, 1.0, if tag.disabled { 0.45 } else { 1.0 }],
1254 palette: draw::Palette::default(),
1255 });
1256 }
1257 }
1258
1259 /// The note tags of this outline's tables, pictures and files, whose origin is `origin`,
1260 /// those `art` maps drawn with their art.
1261 pub(crate) fn append_block_tag_primitives(
1262 &self,
1263 primitives: &mut Vec<Primitive<'_>>,
1264 origin: [f32; 2],
1265 art: &super::TagArt,
1266 ) {
1267 let tables = self.tables.iter().filter_map(|table| table.tags.as_ref());
1268 let objects = self
1269 .objects
1270 .iter()
1271 .filter_map(|object| object.tags.as_ref());
1272 for tag in tables.chain(objects).flat_map(|block| &block.tags) {
1273 primitives.push(Primitive::Icon {
1274 sources: art.sources(tag),
1275 origin: [
1276 origin[0] + self.tag_column_offset() + tag.origin[0],
1277 origin[1] + tag.origin[1],
1278 ],
1279 size: tag.size,
1280 tint: [1.0, 1.0, 1.0, if tag.disabled { 0.45 } else { 1.0 }],
1281 palette: draw::Palette::default(),
1282 });
1283 }
1284 }
1285
1286 pub(crate) fn append_table_primitives(
1287 &self,
1288 primitives: &mut Vec<Primitive<'_>>,
1289 origin: [f32; 2],
1290 paper: Paper,
1291 ) {
1292 for table in &self.tables {
1293 let (Some(first), Some(last)) = (table.cells.first(), table.cells.last()) else {
1294 continue;
1295 };
1296 if !table.borders {
1297 continue;
1298 }
1299 let bounds = [first.rect[0], first.rect[1], last.rect[2], last.rect[3]];
1300 let color = paper.shade(colorref(0x00a3a3a3));
1301 primitives.push(Primitive::RoundedRect {
1302 rect: [
1303 bounds[0] + origin[0] - 0.375,
1304 bounds[1] + origin[1] - 0.375,
1305 bounds[2] + origin[0] + 0.375,
1306 bounds[3] + origin[1] + 0.375,
1307 ],
1308 radius: [3.6; 2],
1309 stroke: Some(draw::Stroke::Solid(0.75)),
1310 color,
1311 });
1312 for cell in &table.cells {
1313 let [left, top, right, bottom] = cell.rect;
1314 if right < bounds[2] {
1315 primitives.push(Primitive::Rect {
1316 rect: [
1317 right + origin[0] - 0.375,
1318 top + origin[1],
1319 right + origin[0] + 0.375,
1320 bottom + origin[1],
1321 ],
1322 color,
1323 });
1324 }
1325 if bottom < bounds[3] {
1326 primitives.push(Primitive::Rect {
1327 rect: [
1328 left + origin[0],
1329 bottom + origin[1] - 0.375,
1330 right + origin[0],
1331 bottom + origin[1] + 0.375,
1332 ],
1333 color,
1334 });
1335 }
1336 }
1337 }
1338 }
1339}
1340
1341#[cfg(test)]
1342mod tests {
1343 use super::*;
1344 use crate::editor::TextOutline;
1345 use crate::gpu::painted_layout;
1346 use onestore::page::text::Paragraph;
1347 use onestore::page::{Image, PageObject};
1348 use onestore::{ExGuid, document::Layout};
1349 use std::{collections::BTreeMap, sync::Arc};
1350
1351 #[test]
1352 fn title_editing_keeps_identity_and_date_geometry_through_composition_and_undo() {
1353 use crate::document::TextDocument;
1354 use onestore::page::Title;
1355 let mut engine = TextEngine::default();
1356 let mut fields = ["Header", "September 8, 2026"].map(|text| {
1357 let document =
1358 TextDocument::new(vec![Paragraph::new(text.into(), Default::default())]).unwrap();
1359 TextOutline::new(&mut engine, document, 468.0, [0.0; 2])
1360 .unwrap()
1361 .snapshot()
1362 });
1363 for field in &mut fields {
1364 field.layout.max_width = None;
1365 field.layout.width_set_by_user = None;
1366 field.layout.max_height = Some(21.6);
1367 }
1368 fields[0].title = true;
1369 fields[0].min_width = Some(162.0);
1370 fields[1].layout.x = Some(2.0);
1371 fields[1].layout.y = Some(3.0);
1372 let id = fields[0].id;
1373 let source = fields[0].paragraphs.clone();
1374 let page = Page {
1375 identity: None,
1376 created: Some(1),
1377 title: "Header".into(),
1378 margin_origin: [36.0, 14.4],
1379 rtl: false,
1380 color: None,
1381 rule_lines: None,
1382 definitions: BTreeMap::new(),
1383 objects: vec![PageObject::Title(Title {
1384 date: Some(fields[1].id),
1385 id: ExGuid::default(),
1386 layout: Layout {
1387 x: Some(10.0),
1388 y: Some(20.0),
1389 ..Default::default()
1390 },
1391 outlines: fields.into(),
1392 })],
1393 };
1394 let (scene, mut editor) = PageScene::from_page(page, &mut engine).unwrap();
1395 assert!(editor.active_outline().title);
1396 assert_eq!(editor.active_outline().origin(), [46.0, 34.4]);
1397 assert_eq!(editor.active_outline().layout().max_width, None);
1398 let bounds = |editor: &CanvasEditor| {
1399 scene
1400 .content_bounds(editor)
1401 .map(|(rect, _)| rect)
1402 .collect::<Vec<_>>()
1403 };
1404 let before = bounds(&editor);
1405 assert_eq!(before[0][0], 48.0);
1406 assert!((before[0][1] - 62.6).abs() < 0.00001);
1407 let text = "A long title with multiple wrapped lines ".repeat(10);
1408 editor.select_all().unwrap();
1409 let end = text.encode_utf16().count() as u32;
1410 editor.compose(&mut engine, text.clone(), end..end).unwrap();
1411 let wrapped = bounds(&editor);
1412 assert!(wrapped[0][1] > before[0][1] + 20.0);
1413 assert_eq!(
1414 wrapped[0][1],
1415 editor.active_outline().bounds().y1 as f32 + 3.0 + 3.6
1416 );
1417 editor.cancel_composition(&mut engine).unwrap();
1418 assert_eq!(editor.active_outline().document().nodes(), source);
1419 assert_eq!(bounds(&editor), before);
1420 editor.commit_text(&mut engine, text).unwrap();
1421 assert_eq!(bounds(&editor), wrapped);
1422 assert_eq!(editor.active_outline().id, id);
1423 assert_eq!(
1424 editor.active_outline().document().nodes()[0].id,
1425 source[0].id
1426 );
1427 editor.undo(&mut engine).unwrap();
1428 assert_eq!(editor.active_outline().document().nodes(), source);
1429 assert_eq!(bounds(&editor), before);
1430 editor.redo(&mut engine).unwrap();
1431 editor.select_all().unwrap();
1432 editor.insert(&mut engine, "").unwrap();
1433 assert_eq!(editor.outlines().len(), 1);
1434 assert!(editor.caret_outline().is_none());
1435 assert_eq!(editor.active_outline().id, id);
1436 assert_eq!(bounds(&editor), before);
1437 assert!(editor.move_outline(id, [0.0; 2]).is_err());
1438 assert!(editor.resize(&mut engine, 90.0).is_err());
1439 editor.undo(&mut engine).unwrap();
1440 let primitives = paint_editor(&scene, &editor);
1441 let Primitive::Text { origin, .. } = primitives.last().unwrap() else {
1442 panic!()
1443 };
1444 assert_eq!(origin[1], wrapped[0][1]);
1445 let title = editor.active_outline().document().clone();
1446 editor
1447 .change_date(
1448 &mut engine,
1449 2,
1450 ["A date label that wraps ".repeat(30), String::new()],
1451 )
1452 .unwrap();
1453 let changed = bounds(&editor);
1454 assert_eq!(changed[0][1], wrapped[0][1]);
1455 assert!(changed[0][3] > wrapped[0][3]);
1456 let (field, rect) = scene.date_fields(&editor).next().unwrap();
1457 assert_eq!(field, DateField::Date);
1458 assert_eq!(rect[1], changed[0][1]);
1459 assert_eq!(rect[3], changed[0][3]);
1460 editor.undo(&mut engine).unwrap();
1461 assert_eq!(editor.date().unwrap().timestamp(), 1);
1462 assert_eq!(bounds(&editor), wrapped);
1463 assert_eq!(editor.active_outline().document(), &title);
1464 }
1465
1466 #[test]
1467 #[ignore = "requires the native baseline section via CANVAS_TEST_SECTION"]
1468 fn native_title_metrics_and_exit() {
1469 let bytes = std::fs::read(std::env::var_os("CANVAS_TEST_SECTION").unwrap()).unwrap();
1470 let store = onestore::Store::parse(&bytes).unwrap();
1471 let index = onestore::RevisionIndex::parse(&store).unwrap();
1472 let document = onestore::document::Document::parse(&index).unwrap();
1473 let page = Page::from_document(&document, "Baseline anchors").unwrap();
1474 let mut engine = TextEngine::default();
1475 let mut editor = CanvasEditor::from_page(page, &mut engine).unwrap();
1476 let title_id = editor
1477 .outlines()
1478 .iter()
1479 .find(|outline| outline.title)
1480 .unwrap()
1481 .id;
1482 for (text, lines, y) in [
1483 ("Header", 1, 86.4),
1484 (
1485 "A page title that is intentionally long enough to wrap across several lines at this window size while retaining its own date and time below the text",
1486 3,
1487 140.4,
1488 ),
1489 ] {
1490 editor.focus_outline(title_id).unwrap();
1491 editor.select_all().unwrap();
1492 editor.insert(&mut engine, text).unwrap();
1493 let paragraph = editor.active_outline().layouts().next().unwrap().1;
1494 assert_eq!(paragraph.text.lines().count(), lines);
1495 assert!((paragraph.text.lines().next().unwrap().1.height - 20.751953).abs() < 0.001);
1496 editor.leave_title(&mut engine).unwrap();
1497 assert_eq!(editor.caret_outline().unwrap().origin(), [36.0, y]);
1498 editor.undo(&mut engine).unwrap();
1499 }
1500 let date = editor.date().unwrap();
1501 let timestamp = date.timestamp();
1502 let source = date.source().paragraphs.clone();
1503 assert_eq!(
1504 date.fields().map(|(field, _)| field).collect::<Vec<_>>(),
1505 [DateField::Date, DateField::Time]
1506 );
1507 let positions = editor
1508 .outlines()
1509 .iter()
1510 .map(TextOutline::origin)
1511 .collect::<Vec<_>>();
1512 editor
1513 .change_date(
1514 &mut engine,
1515 timestamp + 2 * 86_400 * 10_000_000,
1516 ["Wednesday, September 09, 2026".into(), "6:14 AM".into()],
1517 )
1518 .unwrap();
1519 assert_eq!(
1520 editor
1521 .outlines()
1522 .iter()
1523 .map(TextOutline::origin)
1524 .collect::<Vec<_>>(),
1525 positions
1526 );
1527 assert_eq!(editor.date().unwrap().source().paragraphs[1], source[1]);
1528 editor.undo(&mut engine).unwrap();
1529 assert_eq!(editor.date().unwrap().timestamp(), timestamp);
1530 assert_eq!(editor.date().unwrap().source().paragraphs, source);
1531 for (run_size, base_size, screen_y, paragraphs) in [
1532 (11.0, 11.0, 235.0, Some(2)),
1533 (11.0, 11.0, 252.0, Some(3)),
1534 (11.0, 11.0, 256.0, Some(3)),
1535 (11.0, 11.0, 260.0, None),
1536 (22.0, 11.0, 306.0, Some(3)),
1537 (22.0, 22.0, 306.0, Some(2)),
1538 (22.0, 22.0, 324.0, None),
1539 ] {
1540 let style = ExGuid {
1541 n: 900,
1542 ..Default::default()
1543 };
1544 let format = onestore::document::Format {
1545 font: Some("Calibri".into()),
1546 font_size: Some(run_size),
1547 ..Default::default()
1548 };
1549 let mut nodes = crate::document::TextDocument::new(vec![Paragraph::new(
1550 "Anchor0".into(),
1551 format.clone(),
1552 )])
1553 .unwrap()
1554 .nodes()
1555 .to_vec();
1556 nodes[0].style = Some(style);
1557 let outline = TextOutline::new(
1558 &mut engine,
1559 crate::document::TextDocument::from_nodes(nodes).unwrap(),
1560 72.0,
1561 [120.0, 90.0],
1562 )
1563 .unwrap();
1564 let id = outline.id;
1565 let source = outline.document().clone();
1566 let mut editor = CanvasEditor::from_text_outlines(
1567 vec![outline],
1568 BTreeMap::from([(
1569 style,
1570 onestore::page::Definition {
1571 kind: onestore::document::Kind::Style {
1572 name: Some("p".into()),
1573 next: None,
1574 },
1575 format: onestore::document::Format {
1576 font_size: Some(base_size),
1577 ..format
1578 },
1579 },
1580 )]),
1581 None,
1582 )
1583 .unwrap();
1584 let point = [
1585 (220.0 - 48.0) * 0.75 - 120.0,
1586 (screen_y - 83.0) * 0.75 - 90.0,
1587 ];
1588 assert_eq!(
1589 editor.select_below(&mut engine, id, point).unwrap(),
1590 paragraphs.is_some(),
1591 "run={run_size} base={base_size} y={screen_y}"
1592 );
1593 assert_eq!(editor.outlines()[0].document(), &source);
1594 if let Some(count) = paragraphs {
1595 editor.insert(&mut engine, "X").unwrap();
1596 assert_eq!(editor.active_outline().document().nodes().len(), count);
1597 assert_eq!(editor.active_outline().origin(), [120.0, 90.0]);
1598 assert_eq!(
1599 editor
1600 .active_outline()
1601 .document()
1602 .paragraphs()
1603 .last()
1604 .unwrap()
1605 .text(),
1606 "X"
1607 );
1608 assert!(
1609 (editor
1610 .active_outline()
1611 .layouts()
1612 .last()
1613 .unwrap()
1614 .1
1615 .text
1616 .height()
1617 - base_size * (12.207_031 / 10.0))
1618 .abs()
1619 < 0.001
1620 );
1621 }
1622 }
1623 }
1624
1625 #[test]
1626 #[ignore = "requires the native baseline section via CANVAS_TEST_SECTION"]
1627 fn native_title_flow() {
1628 use crate::document::TextDocument;
1629 use onestore::page::Image;
1630 let bytes = std::fs::read(std::env::var_os("CANVAS_TEST_SECTION").unwrap()).unwrap();
1631 let store = onestore::Store::parse(&bytes).unwrap();
1632 let index = onestore::RevisionIndex::parse(&store).unwrap();
1633 let document = onestore::document::Document::parse(&index).unwrap();
1634 let mut engine = TextEngine::default();
1635 let long = "A page title that is intentionally long enough to wrap across several lines at this window size while retaining its own date and time below the text";
1636 for (font_size, text, x, y, expected) in [
1637 (17.0, long, 120.0, 90.0, 116.18992),
1638 (17.0, long, 120.0, 110.0, 116.18992),
1639 (17.0, long, 120.0, 150.0, 150.0),
1640 (17.0, long, 120.0, 73.8, 73.8),
1641 (17.0, long, 120.0, 74.0, 116.18992),
1642 (17.0, long, 120.0, 40.0, 40.0),
1643 (17.0, long, 540.0, 90.0, 116.18992),
1644 (17.0, long, 550.0, 90.0, 90.0),
1645 (17.0, long, 600.0, 90.0, 90.0),
1646 (17.0, long, 0.0, 90.0, 116.18992),
1647 (17.0, long, -50.0, 90.0, 116.18992),
1648 (17.0, long, -100.0, 90.0, 90.0),
1649 (17.0, "Header\u{000b}Second", 120.0, 90.0, 95.43796),
1650 (17.0, "Header\u{000b}Second", 250.0, 90.0, 95.43796),
1651 (17.0, "Header\u{000b}Second", 540.0, 90.0, 95.43796),
1652 (17.0, "Header\u{000b}Second", 550.0, 90.0, 90.0),
1653 (11.0, long, 120.0, 60.0, 60.0),
1654 (11.0, long, 120.0, 70.0, 80.78945),
1655 (22.0, long, 120.0, 80.0, 80.0),
1656 (22.0, long, 120.0, 90.0, 134.5003),
1657 ] {
1658 let mut page = Page::from_document(&document, "Baseline anchors").unwrap();
1659 page.objects
1660 .retain(|object| matches!(object, PageObject::Title(_)));
1661 let PageObject::Title(title) = &mut page.objects[0] else {
1662 unreachable!()
1663 };
1664 let title = title
1665 .outlines
1666 .iter_mut()
1667 .find(|outline| outline.title)
1668 .unwrap();
1669 title.paragraphs[0].text_mut().unwrap().text = Paragraph::new(
1670 "Header".into(),
1671 onestore::document::Format {
1672 font: Some("Calibri".into()),
1673 font_size: Some(font_size),
1674 ..Default::default()
1675 },
1676 );
1677 let body = TextOutline::new(
1678 &mut engine,
1679 TextDocument::new(vec![Paragraph::new("Anchor".into(), Default::default())])
1680 .unwrap(),
1681 72.0,
1682 [x, y],
1683 )
1684 .unwrap()
1685 .snapshot();
1686 let id = body.id;
1687 page.objects.push(PageObject::Outline(body));
1688 let mut editor = CanvasEditor::from_page(page, &mut engine).unwrap();
1689 editor.select_all().unwrap();
1690 editor.insert(&mut engine, text).unwrap();
1691 let position = |editor: &CanvasEditor| {
1692 editor
1693 .outlines()
1694 .iter()
1695 .find(|outline| outline.id == id)
1696 .unwrap()
1697 .origin()
1698 };
1699 assert_eq!(position(&editor)[0], x);
1700 assert!(
1701 (position(&editor)[1] - expected).abs() < 0.002,
1702 "({x}, {y}) after {text}: {:?}, expected {expected}",
1703 position(&editor)
1704 );
1705 editor.select_all().unwrap();
1706 editor.insert(&mut engine, "Header").unwrap();
1707 assert!((position(&editor)[1] - expected).abs() < 0.002);
1708 editor.undo(&mut engine).unwrap();
1709 editor.undo(&mut engine).unwrap();
1710 assert_eq!(position(&editor), [x, y]);
1711 editor.redo(&mut engine).unwrap();
1712 assert!((position(&editor)[1] - expected).abs() < 0.002);
1713 }
1714 for trigger in [true, false] {
1715 let mut page = Page::from_document(&document, "Baseline anchors").unwrap();
1716 let image_bytes = page
1717 .objects
1718 .iter()
1719 .find_map(|object| match object {
1720 PageObject::Image(image) => image.bytes.clone(),
1721 _ => None,
1722 })
1723 .unwrap();
1724 page.objects
1725 .retain(|object| matches!(object, PageObject::Title(_)));
1726 for (x, y, background) in [
1727 (120.0, 90.0, !trigger),
1728 (600.0, 150.0, false),
1729 (600.0, 90.0, true),
1730 (120.0, 60.0, false),
1731 ] {
1732 page.objects.push(PageObject::Image(Image {
1733 size: None,
1734 id: onestore::page::text::new_id().unwrap(),
1735 bytes: Some(image_bytes.clone()),
1736 display: None,
1737 layout: Layout {
1738 x: Some(x),
1739 y: Some(y),
1740 max_width: Some(72.0),
1741 max_height: Some(80.0),
1742 ..Default::default()
1743 },
1744 alt: None,
1745 background,
1746 printout: None,
1747 tags: Vec::new(),
1748 link: None,
1749 text: None,
1750 }));
1751 }
1752 let (mut scene, mut editor) = PageScene::from_page(page, &mut engine).unwrap();
1753 scene.settle(Some(&editor), 1.0, Paper::WHITE);
1754 let images = |editor: &CanvasEditor| {
1755 let mut primitives = Vec::new();
1756 scene
1757 .append_primitives_with::<SceneError>(
1758 &mut primitives,
1759 [0.0; 2],
1760 Some(editor),
1761 None,
1762 Paper::WHITE,
1763 |_, _, _| Ok(()),
1764 )
1765 .unwrap();
1766 primitives
1767 .into_iter()
1768 .filter_map(|primitive| match primitive {
1769 Primitive::Image { image, rect } => Some((image.id(), rect)),
1770 _ => None,
1771 })
1772 .collect::<Vec<_>>()
1773 };
1774 let before = images(&editor);
1775 editor.select_all().unwrap();
1776 editor.insert(&mut engine, "Header").unwrap();
1777 editor.select_all().unwrap();
1778 editor.insert(&mut engine, long).unwrap();
1779 let after = images(&editor);
1780 for (index, ((old_id, old), (new_id, new))) in before.iter().zip(&after).enumerate() {
1781 assert_eq!(old_id, new_id);
1782 let delta = if trigger && index < 2 { 26.18992 } else { 0.0 };
1783 assert_eq!([old[0], old[2]], [new[0], new[2]]);
1784 assert!((old[1] + delta - new[1]).abs() < 0.002);
1785 assert!((old[3] + delta - new[3]).abs() < 0.002);
1786 assert!(
1787 scene
1788 .content_bounds(&editor)
1789 .any(|(bounds, _)| bounds == *new)
1790 );
1791 }
1792 editor.undo(&mut engine).unwrap();
1793 assert_eq!(images(&editor), before);
1794 editor.redo(&mut engine).unwrap();
1795 assert_eq!(images(&editor), after);
1796 }
1797 }
1798
1799 #[test]
1800 fn title_exit_places_an_uncommitted_body_caret_below_the_date() {
1801 use crate::document::TextDocument;
1802 use onestore::page::Title;
1803 let mut engine = TextEngine::default();
1804 let fields =
1805 [("Header", 20.751953), ("Date\u{000b}Time", 12.207031)].map(|(text, line_spacing)| {
1806 let document = TextDocument::new(vec![Paragraph::new(
1807 text.into(),
1808 onestore::document::Format {
1809 font: Some("Arial".into()),
1810 font_size: Some(8.0),
1811 line_spacing: Some(line_spacing),
1812 ..Default::default()
1813 },
1814 )])
1815 .unwrap();
1816 TextOutline::new(&mut engine, document, 468.0, [0.0; 2])
1817 .unwrap()
1818 .snapshot()
1819 });
1820 let mut fields = Vec::from(fields);
1821 fields[0].title = true;
1822 fields[0].min_width = Some(162.0);
1823 let title_id = fields[0].id;
1824 let page = Page {
1825 identity: None,
1826 created: None,
1827 title: "Header".into(),
1828 margin_origin: [36.0, 14.4],
1829 rtl: false,
1830 color: None,
1831 rule_lines: None,
1832 definitions: BTreeMap::new(),
1833 objects: vec![PageObject::Title(Title {
1834 date: None,
1835 id: ExGuid::default(),
1836 layout: Layout::default(),
1837 outlines: fields,
1838 })],
1839 };
1840 let mut editor = CanvasEditor::from_page(page, &mut engine).unwrap();
1841 let original = editor.active_outline().document().clone();
1842 editor.leave_title(&mut engine).unwrap();
1843 assert_eq!(editor.caret_outline().unwrap().origin(), [36.0, 86.4]);
1844 assert_eq!(editor.outlines().len(), 1);
1845 assert!(!editor.undo(&mut engine).unwrap());
1846 editor.insert(&mut engine, "body").unwrap();
1847 let body_id = editor.active_outline().id;
1848 editor.focus_outline(title_id).unwrap();
1849 editor.leave_title(&mut engine).unwrap();
1850 assert_eq!(editor.active_outline().id, body_id);
1851 assert!(editor.caret_outline().is_none());
1852 editor.undo(&mut engine).unwrap();
1853 assert_eq!(editor.outlines().len(), 1);
1854 editor.focus_outline(title_id).unwrap();
1855 editor.select_all().unwrap();
1856 editor
1857 .insert(&mut engine, "Header\u{000b}Second\u{000b}Third")
1858 .unwrap();
1859 assert_eq!(editor.active_outline().document().nodes().len(), 1);
1860 assert_eq!(
1861 editor
1862 .active_outline()
1863 .layouts()
1864 .next()
1865 .unwrap()
1866 .1
1867 .text
1868 .lines()
1869 .count(),
1870 3
1871 );
1872 editor.leave_title(&mut engine).unwrap();
1873 assert_eq!(editor.caret_outline().unwrap().origin(), [36.0, 140.4]);
1874 assert_eq!(editor.outlines().len(), 1);
1875 editor.undo(&mut engine).unwrap();
1876 assert_eq!(editor.active_outline().id, title_id);
1877 assert_eq!(editor.active_outline().document(), &original);
1878 }
1879
1880 #[test]
1881 fn reverse_hit_order_keeps_read_only_focus_indices() {
1882 let objects = [0.0, 20.0]
1883 .into_iter()
1884 .map(|position| {
1885 PageObject::Unsupported(onestore::page::Unsupported {
1886 id: ExGuid {
1887 guid: [position as u8; 16],
1888 n: 1,
1889 },
1890 jcid: 0xdead,
1891 layout: Layout {
1892 x: Some(position),
1893 y: Some(position),
1894 max_width: Some(160.0),
1895 max_height: Some(80.0),
1896 ..Default::default()
1897 },
1898 })
1899 })
1900 .collect();
1901 let page = Page {
1902 identity: None,
1903 created: None,
1904 title: String::new(),
1905 margin_origin: [0.0; 2],
1906 rtl: false,
1907 color: None,
1908 rule_lines: None,
1909 definitions: BTreeMap::new(),
1910 objects,
1911 };
1912 let scene = PageScene::new(page, &mut TextEngine::default()).unwrap();
1913 assert_eq!(
1914 scene.hit_test::<()>([30.0, 30.0], None, |_| unreachable!()),
1915 Some(SceneHit::ReadOnly(1))
1916 );
1917 assert_eq!(
1918 scene.hit_test::<()>([5.0, 5.0], None, |_| unreachable!()),
1919 Some(SceneHit::ReadOnly(0))
1920 );
1921 assert_eq!(
1922 scene.hit_test::<()>([300.0, 300.0], None, |_| unreachable!()),
1923 None
1924 );
1925 }
1926
1927 #[test]
1928 fn unsupported_outlines_and_titles_leave_supported_outlines_editable() {
1929 use crate::document::TextDocument;
1930 use onestore::document::{Kind, Tag};
1931 use onestore::page::{Definition, Outline, Title};
1932 for title in [false, true] {
1933 let [tag, list] = [9, 10].map(|byte| ExGuid {
1934 guid: [byte; 16],
1935 n: 1,
1936 });
1937 let make_outline = |text: &str| {
1938 let document =
1939 TextDocument::new(vec![Paragraph::new(text.into(), Default::default())])
1940 .unwrap();
1941 Outline {
1942 title: false,
1943 min_width: None,
1944 id: ExGuid {
1945 guid: [u8::from(text == "Editable") + 1; 16],
1946 n: 1,
1947 },
1948 layout: Layout {
1949 max_width: Some(200.0),
1950 ..Default::default()
1951 },
1952 indents: vec![18.0, 0.0],
1953 paragraphs: document.nodes().to_vec(),
1954 unsupported: Vec::new(),
1955 }
1956 };
1957 let mut numbered = make_outline("Numbered");
1958 numbered.paragraphs[0].lists.push(list);
1959 let unsupported = if title {
1960 PageObject::Title(Title {
1961 date: None,
1962 id: ExGuid::default(),
1963 layout: Layout {
1964 x: Some(10.0),
1965 y: Some(20.0),
1966 ..Default::default()
1967 },
1968 outlines: vec![numbered],
1969 })
1970 } else {
1971 PageObject::Outline(numbered)
1972 };
1973 // A tag symbol without artwork draws as a plain tag and stays editable.
1974 let mut editable = make_outline("Editable");
1975 editable.paragraphs[0].tags.push(Tag {
1976 definition: Some(tag),
1977 status: 1,
1978 action_type: None,
1979 shape: None,
1980 property_status: None,
1981 created: None,
1982 completed: None,
1983 start: None,
1984 due: None,
1985 task_id: None,
1986 extra_set: 0,
1987 });
1988 let page = Page {
1989 identity: None,
1990 created: None,
1991 title: String::new(),
1992 margin_origin: [36.0, 14.0],
1993 rtl: false,
1994 color: None,
1995 rule_lines: None,
1996 objects: vec![unsupported, PageObject::Outline(editable)],
1997 definitions: BTreeMap::from([
1998 (
1999 tag,
2000 Definition {
2001 kind: Kind::TagDefinition {
2002 shape: Some(999),
2003 label: Some("Unknown icon".into()),
2004 color: None,
2005 highlight: None,
2006 action_type: None,
2007 },
2008 format: Default::default(),
2009 },
2010 ),
2011 (
2012 // A numbering sequence OneNote 2010 does not offer.
2013 list,
2014 Definition {
2015 kind: Kind::List {
2016 font: None,
2017 format: Some("\u{fffd}\u{63}.".into()),
2018 restart: None,
2019 bullet: None,
2020 },
2021 format: Default::default(),
2022 },
2023 ),
2024 ]),
2025 };
2026 let mut engine = TextEngine::default();
2027 let (scene, mut editor) = PageScene::from_page(page, &mut engine).unwrap();
2028 assert_eq!(editor.outlines().len(), 1);
2029 let readonly = scene.read_only(Some(&editor)).next().unwrap();
2030 assert_eq!(scene.read_only(Some(&editor)).count(), 1);
2031 if title {
2032 assert_eq!(&readonly.rect()[..2], &[46.0, 34.0]);
2033 }
2034 editor.insert(&mut engine, "Still ").unwrap();
2035 assert_eq!(
2036 editor
2037 .active_outline()
2038 .document()
2039 .paragraphs()
2040 .next()
2041 .unwrap()
2042 .text(),
2043 "Still Editable"
2044 );
2045 editor.undo(&mut engine).unwrap();
2046 assert_eq!(
2047 editor
2048 .active_outline()
2049 .document()
2050 .paragraphs()
2051 .next()
2052 .unwrap()
2053 .text(),
2054 "Editable"
2055 );
2056 }
2057 }
2058
2059 #[test]
2060 fn unsupported_sources_stay_owned_and_read_only_across_edit_and_undo() {
2061 use crate::document::TextDocument;
2062 use onestore::page::{Outline, Unsupported};
2063 let document = TextDocument::new(vec![Paragraph::new(
2064 "preserved text".into(),
2065 Default::default(),
2066 )])
2067 .unwrap();
2068 let original = document.nodes().to_vec();
2069 let unknown = Unsupported {
2070 id: ExGuid {
2071 guid: [8; 16],
2072 n: 1,
2073 },
2074 jcid: 0xdead,
2075 layout: Layout::default(),
2076 };
2077 let page = Page {
2078 identity: None,
2079 created: None,
2080 title: String::new(),
2081 margin_origin: [0.0; 2],
2082 rtl: false,
2083 color: None,
2084 rule_lines: None,
2085 definitions: BTreeMap::new(),
2086 objects: vec![
2087 PageObject::Outline(Outline {
2088 title: false,
2089 min_width: None,
2090 id: ExGuid {
2091 guid: [7; 16],
2092 n: 1,
2093 },
2094 layout: Layout {
2095 x: Some(50.0),
2096 y: Some(70.0),
2097 max_width: Some(200.0),
2098 ..Default::default()
2099 },
2100 indents: vec![18.0, 0.0],
2101 paragraphs: original.clone(),
2102 unsupported: vec![unknown.clone()],
2103 }),
2104 PageObject::Image(Image {
2105 size: None,
2106 id: ExGuid::default(),
2107 layout: Layout {
2108 y: Some(150.0),
2109 ..Default::default()
2110 },
2111 bytes: None,
2112 display: None,
2113 alt: Some("missing diagram".into()),
2114 background: false,
2115 printout: None,
2116 tags: Vec::new(),
2117 link: None,
2118 text: None,
2119 }),
2120 PageObject::Unsupported(unknown.clone()),
2121 ],
2122 };
2123 let mut engine = TextEngine::default();
2124 let (scene, mut editor) = PageScene::from_page(page, &mut engine).unwrap();
2125 assert_eq!(scene.read_only(Some(&editor)).count(), 3);
2126 assert!(editor.outlines().is_empty());
2127 assert!(editor.caret_outline().is_some());
2128 assert!(
2129 editor.active_outline().origin()[0]
2130 > scene
2131 .read_only(Some(&editor))
2132 .map(|object| object.rect()[2])
2133 .fold(0.0_f32, f32::max)
2134 );
2135 let labels: Vec<_> = scene
2136 .read_only(Some(&editor))
2137 .map(|object| object.label.id())
2138 .collect();
2139 for edit in [true, false] {
2140 if edit {
2141 editor.insert(&mut engine, "new annotation").unwrap();
2142 } else {
2143 editor.undo(&mut engine).unwrap();
2144 }
2145 assert_eq!(editor.caret_outline().is_none(), edit);
2146 assert_eq!(editor.outlines().len(), usize::from(edit));
2147 let readonly: Vec<_> = scene.read_only(Some(&editor)).collect();
2148 let PageObject::Outline(outline) = &readonly[0].source else {
2149 panic!()
2150 };
2151 assert_eq!(outline.paragraphs, original);
2152 assert_eq!(
2153 outline.unsupported.as_slice(),
2154 std::slice::from_ref(&unknown)
2155 );
2156 let PageObject::Image(image) = &readonly[1].source else {
2157 panic!()
2158 };
2159 assert_eq!(image.alt.as_deref(), Some("missing diagram"));
2160 assert!(image.bytes.is_none());
2161 let PageObject::Unsupported(source) = &readonly[2].source else {
2162 panic!()
2163 };
2164 assert_eq!(source, &unknown);
2165 assert_eq!(
2166 readonly
2167 .iter()
2168 .map(|object| object.label.id())
2169 .collect::<Vec<_>>(),
2170 labels
2171 );
2172 let primitives = paint_editor(&scene, &editor);
2173 assert_eq!(primitives.len(), 9);
2174 assert_eq!(
2175 primitives
2176 .iter()
2177 .filter(|p| matches!(p, Primitive::Text { .. }))
2178 .count(),
2179 3
2180 );
2181 }
2182 assert_eq!(
2183 editor
2184 .active_outline()
2185 .document()
2186 .paragraphs()
2187 .next()
2188 .unwrap()
2189 .text(),
2190 ""
2191 );
2192 }
2193
2194 fn paint_editor<'a>(scene: &'a PageScene, editor: &'a CanvasEditor) -> Vec<Primitive<'a>> {
2195 let mut primitives = Vec::new();
2196 scene
2197 .append_primitives_with(
2198 &mut primitives,
2199 [0.0; 2],
2200 Some(editor),
2201 None,
2202 Paper::WHITE,
2203 |id, offset, primitives| {
2204 let outline = editor
2205 .outlines()
2206 .iter()
2207 .find(|outline| outline.id == id)
2208 .ok_or(SceneError::MissingOutline)?;
2209 for (_, paragraph) in outline.layouts() {
2210 primitives.push(Primitive::Text {
2211 clip: None,
2212 text: &paragraph.text,
2213 ink: Paper::WHITE.ink,
2214 origin: [
2215 outline.origin()[0] + paragraph.origin[0] + offset[0],
2216 outline.origin()[1] + paragraph.origin[1] + offset[1],
2217 ],
2218 });
2219 }
2220 Ok::<_, SceneError>(())
2221 },
2222 )
2223 .unwrap();
2224 primitives
2225 }
2226
2227 #[test]
2228 fn editable_outlines_keep_paint_order_and_images_across_edit_and_undo() {
2229 use crate::document::TextDocument;
2230 use onestore::page::Outline;
2231 use onestore::page::text::Paragraph;
2232 let outline = |x, text: &str| {
2233 let document =
2234 TextDocument::new(vec![Paragraph::new(text.into(), Default::default())]).unwrap();
2235 PageObject::Outline(Outline {
2236 title: false,
2237 min_width: None,
2238 id: ExGuid {
2239 guid: [x as u8; 16],
2240 n: 1,
2241 },
2242 layout: Layout {
2243 x: Some(x),
2244 max_width: Some(180.0),
2245 width_set_by_user: Some(true),
2246 ..Default::default()
2247 },
2248 indents: vec![18.0, 0.0],
2249 paragraphs: document.nodes().to_vec(),
2250 unsupported: Vec::new(),
2251 })
2252 };
2253 let mut bytes = Vec::new();
2254 {
2255 let mut encoder = png::Encoder::new(&mut bytes, 1, 1);
2256 encoder.set_color(png::ColorType::Rgba);
2257 encoder.set_depth(png::BitDepth::Eight);
2258 encoder
2259 .write_header()
2260 .unwrap()
2261 .write_image_data(&[17, 34, 51, 255])
2262 .unwrap();
2263 }
2264 let page = || Page {
2265 identity: None,
2266 created: None,
2267 title: String::new(),
2268 margin_origin: [0.0; 2],
2269 rtl: false,
2270 color: None,
2271 rule_lines: None,
2272 definitions: BTreeMap::new(),
2273 objects: vec![
2274 outline(2.0, "first"),
2275 PageObject::Image(Image {
2276 size: None,
2277 id: ExGuid::default(),
2278 layout: Layout {
2279 x: Some(5.0),
2280 y: Some(7.0),
2281 max_width: Some(40.0),
2282 max_height: Some(30.0),
2283 ..Default::default()
2284 },
2285 bytes: Some(Arc::from(bytes.clone())),
2286 display: None,
2287 alt: None,
2288 background: false,
2289 printout: None,
2290 tags: Vec::new(),
2291 link: None,
2292 text: None,
2293 }),
2294 outline(50.0, "last"),
2295 ],
2296 };
2297 let mut engine = TextEngine::default();
2298 let mut reference = PageScene::new(page(), &mut engine).unwrap();
2299 let (mut scene, mut editor) = PageScene::from_page(page(), &mut engine).unwrap();
2300 reference.settle(None, 1.0, Paper::WHITE);
2301 scene.settle(Some(&editor), 1.0, Paper::WHITE);
2302 let mut reference_primitives = Vec::new();
2303 reference
2304 .append_primitives(&mut reference_primitives, [0.0; 2], Paper::WHITE)
2305 .unwrap();
2306 let image_id;
2307 {
2308 let actual = paint_editor(&scene, &editor);
2309 assert_eq!(actual.len(), 3);
2310 for (actual, expected) in actual.iter().zip(&reference_primitives) {
2311 match (actual, expected) {
2312 (
2313 Primitive::Text {
2314 clip: None,
2315 text: a,
2316 origin: x,
2317 ..
2318 },
2319 Primitive::Text {
2320 clip: None,
2321 text: b,
2322 origin: y,
2323 ..
2324 },
2325 ) => {
2326 assert_eq!(x, y);
2327 assert_eq!(
2328 painted_layout(*a)
2329 .lines()
2330 .next()
2331 .unwrap()
2332 .0
2333 .metrics()
2334 .advance,
2335 painted_layout(*b)
2336 .lines()
2337 .next()
2338 .unwrap()
2339 .0
2340 .metrics()
2341 .advance
2342 );
2343 }
2344 (
2345 Primitive::Image { image: a, rect: x },
2346 Primitive::Image { image: b, rect: y },
2347 ) => {
2348 assert_eq!(x, y);
2349 assert_eq!(a.pixels(), b.pixels());
2350 }
2351 _ => panic!("paint order changed"),
2352 }
2353 }
2354 let Primitive::Image { image, .. } = &actual[1] else {
2355 panic!()
2356 };
2357 image_id = image.id();
2358 }
2359 let original = editor.active_outline().document().clone();
2360 editor.insert(&mut engine, "new words ").unwrap();
2361 {
2362 let actual = paint_editor(&scene, &editor);
2363 let Primitive::Text { text, .. } = &actual[0] else {
2364 panic!()
2365 };
2366 let Primitive::Text { text: before, .. } = &reference_primitives[0] else {
2367 panic!()
2368 };
2369 assert!(
2370 painted_layout(*text)
2371 .lines()
2372 .next()
2373 .unwrap()
2374 .0
2375 .metrics()
2376 .advance
2377 > painted_layout(*before)
2378 .lines()
2379 .next()
2380 .unwrap()
2381 .0
2382 .metrics()
2383 .advance
2384 );
2385 let Primitive::Image { image, .. } = &actual[1] else {
2386 panic!()
2387 };
2388 assert_eq!(image.id(), image_id);
2389 }
2390 editor.undo(&mut engine).unwrap();
2391 assert_eq!(editor.active_outline().document(), &original);
2392 let actual = paint_editor(&scene, &editor);
2393 let Primitive::Text { text, .. } = &actual[0] else {
2394 panic!()
2395 };
2396 let Primitive::Text { text: before, .. } = &reference_primitives[0] else {
2397 panic!()
2398 };
2399 assert_eq!(
2400 painted_layout(*text)
2401 .lines()
2402 .next()
2403 .unwrap()
2404 .0
2405 .metrics()
2406 .advance,
2407 painted_layout(*before)
2408 .lines()
2409 .next()
2410 .unwrap()
2411 .0
2412 .metrics()
2413 .advance
2414 );
2415 assert!(matches!(
2416 scene.append_primitives(&mut Vec::new(), [0.0; 2], Paper::WHITE),
2417 Err(SceneError::MissingOutline)
2418 ));
2419 }
2420
2421 #[test]
2422 fn ink_draws_every_stroke_and_bounds_include_the_pen() {
2423 use onestore::page::{Ink, InkStroke};
2424 let stroke = |points: Vec<[f32; 2]>, color| InkStroke {
2425 id: ExGuid::default(),
2426 points,
2427 width: 2.0,
2428 height: 2.0,
2429 color,
2430 transparency: Some(51),
2431 pen_tip: None,
2432 raster_operation: None,
2433 pressure: Vec::new(),
2434 };
2435 let ink = Ink {
2436 id: ExGuid::default(),
2437 layout: Layout::default(),
2438 // A straight vertical line has no width of its own.
2439 strokes: vec![stroke(vec![[10.0, 20.0], [10.0, 20.1], [10.0, 60.0]], None)],
2440 groups: vec![Ink {
2441 id: ExGuid::default(),
2442 layout: Layout::default(),
2443 strokes: vec![stroke(vec![[40.0, 30.0]], Some(0x0000ff))],
2444 groups: Vec::new(),
2445 shape: None,
2446 }],
2447 shape: None,
2448 };
2449 let page = Page {
2450 identity: None,
2451 created: None,
2452 title: String::new(),
2453 margin_origin: [0.0; 2],
2454 rtl: false,
2455 color: None,
2456 rule_lines: None,
2457 definitions: BTreeMap::new(),
2458 objects: vec![PageObject::Ink(ink.clone())],
2459 };
2460 let mut engine = TextEngine::default();
2461 let (scene, editor) = PageScene::from_page(page, &mut engine).unwrap();
2462 assert!(
2463 scene
2464 .content_bounds(&editor)
2465 .any(|(bounds, _)| bounds == [9.0, 19.0, 41.0, 61.0])
2466 );
2467 let mut primitives = Vec::new();
2468 scene
2469 .append_primitives_with::<SceneError>(
2470 &mut primitives,
2471 [5.0, 0.0],
2472 Some(&editor),
2473 None,
2474 Paper::WHITE,
2475 |_, _, _| Ok(()),
2476 )
2477 .unwrap();
2478 let segments: Vec<_> = primitives
2479 .iter()
2480 .filter_map(|primitive| match primitive {
2481 Primitive::Segment {
2482 from, to, color, ..
2483 } => Some((*from, *to, *color)),
2484 _ => None,
2485 })
2486 .collect();
2487 let red = colorref(0x0000ff);
2488 assert_eq!(
2489 segments,
2490 [
2491 ([15.0, 20.0], [15.0, 60.0], [0.0, 0.0, 0.0, 0.8]),
2492 ([45.0, 30.0], [45.0, 30.0], [red[0], red[1], red[2], 0.8]),
2493 ]
2494 );
2495 assert_eq!(editor.page().unwrap().objects, [PageObject::Ink(ink)]);
2496 }
2497
2498 #[test]
2499 fn pressure_widens_a_stroke_from_point_to_point_as_onenote_draws_it() {
2500 use onestore::page::{Ink, InkStroke};
2501 let stroke = |points: Vec<[f32; 2]>, pressure: Vec<f32>| InkStroke {
2502 id: ExGuid::default(),
2503 points,
2504 width: 4.0,
2505 height: 4.0,
2506 color: None,
2507 transparency: None,
2508 pen_tip: None,
2509 raster_operation: None,
2510 pressure,
2511 };
2512 let ink = Ink {
2513 id: ExGuid::default(),
2514 layout: Layout::default(),
2515 strokes: vec![
2516 stroke(
2517 vec![[0.0, 0.0], [10.0, 0.0], [20.0, 0.0]],
2518 vec![0.0, 0.5, 1.0],
2519 ),
2520 // A straight run at one pressure is one segment.
2521 stroke(vec![[0.0, 10.0], [10.0, 10.0], [20.0, 10.0]], vec![0.0; 3]),
2522 ],
2523 groups: Vec::new(),
2524 shape: None,
2525 };
2526 let mut primitives = Vec::new();
2527 append_ink(&ink, [0.0; 2], [0.0, 0.0, 0.0, 1.0], &mut primitives);
2528 let found: Vec<([f32; 2], [f32; 2], [f32; 2])> = primitives
2529 .iter()
2530 .map(|primitive| match primitive {
2531 Primitive::Taper {
2532 from, to, widths, ..
2533 } => (*from, *to, *widths),
2534 Primitive::Segment {
2535 from, to, width, ..
2536 } => (*from, *to, [*width; 2]),
2537 _ => panic!(),
2538 })
2539 .collect();
2540 // A quarter of the pen at no pressure, all of it at half and 1.75 times at full.
2541 assert_eq!(
2542 found,
2543 [
2544 ([0.0, 0.0], [10.0, 0.0], [1.0, 4.0]),
2545 ([10.0, 0.0], [20.0, 0.0], [4.0, 7.0]),
2546 ([0.0, 10.0], [20.0, 10.0], [1.0, 1.0]),
2547 ]
2548 );
2549 }
2550
2551 #[test]
2552 fn scene_owns_decoded_images_and_reuses_identity_across_translated_frames() {
2553 let mut encoded = Vec::new();
2554 {
2555 let mut encoder = png::Encoder::new(&mut encoded, 1, 1);
2556 encoder.set_color(png::ColorType::Rgba);
2557 encoder.set_depth(png::BitDepth::Eight);
2558 encoder
2559 .write_header()
2560 .unwrap()
2561 .write_image_data(&[255, 0, 0, 255])
2562 .unwrap();
2563 }
2564 let page = |width| Page {
2565 identity: None,
2566 created: None,
2567 title: String::new(),
2568 margin_origin: [0.0; 2],
2569 rtl: false,
2570 color: None,
2571 rule_lines: None,
2572 definitions: BTreeMap::new(),
2573 objects: vec![PageObject::Image(Image {
2574 size: None,
2575 id: ExGuid::default(),
2576 layout: Layout {
2577 x: Some(2.0),
2578 y: Some(5.0),
2579 max_width: Some(width),
2580 max_height: Some(4.0),
2581 ..Default::default()
2582 },
2583 bytes: Some(Arc::from(encoded.clone())),
2584 display: None,
2585 alt: None,
2586 background: false,
2587 printout: None,
2588 tags: Vec::new(),
2589 link: None,
2590 text: None,
2591 })],
2592 };
2593 let mut engine = TextEngine::default();
2594 let mut scene = PageScene::new(page(3.0), &mut engine).unwrap();
2595 scene.settle(None, 1.0, Paper::WHITE);
2596 for invalid in [f32::NAN, f32::INFINITY, -1.0, 0.0] {
2597 assert!(matches!(
2598 PageScene::new(page(invalid), &mut engine),
2599 Err(SceneError::InvalidGeometry)
2600 ));
2601 }
2602 let mut primitives = Vec::new();
2603 scene
2604 .append_primitives(&mut primitives, [0.0; 2], Paper::WHITE)
2605 .unwrap();
2606 scene
2607 .append_primitives(&mut primitives, [10.0, -20.0], Paper::WHITE)
2608 .unwrap();
2609 let [
2610 Primitive::Image {
2611 image: first,
2612 rect: a,
2613 },
2614 Primitive::Image {
2615 image: second,
2616 rect: b,
2617 },
2618 ] = primitives.as_slice()
2619 else {
2620 panic!()
2621 };
2622 assert_eq!(*a, [2.0, 5.0, 5.0, 9.0]);
2623 assert_eq!(*b, [12.0, -15.0, 15.0, -11.0]);
2624 assert_eq!(first.id(), second.id());
2625 assert_eq!(first.pixels(), [255, 0, 0, 255]);
2626 }
2627
2628 #[test]
2629 fn foreground_pictures_take_hits_and_draw_at_their_moving_preview() {
2630 let mut bytes = Vec::new();
2631 {
2632 let mut encoder = png::Encoder::new(&mut bytes, 1, 1);
2633 encoder.set_color(png::ColorType::Rgba);
2634 encoder.set_depth(png::BitDepth::Eight);
2635 encoder
2636 .write_header()
2637 .unwrap()
2638 .write_image_data(&[17, 34, 51, 255])
2639 .unwrap();
2640 }
2641 let image = |x: f32, background| Image {
2642 size: None,
2643 id: onestore::page::text::new_id().unwrap(),
2644 layout: Layout {
2645 x: Some(x),
2646 y: Some(0.0),
2647 max_width: Some(100.0),
2648 max_height: Some(100.0),
2649 ..Default::default()
2650 },
2651 bytes: Some(Arc::from(bytes.clone())),
2652 display: None,
2653 alt: None,
2654 background,
2655 printout: None,
2656 tags: Vec::new(),
2657 link: None,
2658 text: None,
2659 };
2660 let [background, picture] = [image(0.0, true), image(50.0, false)];
2661 let id = picture.id;
2662 let page = Page {
2663 identity: None,
2664 created: None,
2665 title: String::new(),
2666 margin_origin: [0.0; 2],
2667 rtl: false,
2668 color: None,
2669 rule_lines: None,
2670 definitions: BTreeMap::new(),
2671 objects: vec![PageObject::Image(background), PageObject::Image(picture)],
2672 };
2673 let mut engine = TextEngine::default();
2674 let (mut scene, editor) = PageScene::from_page(page, &mut engine).unwrap();
2675 scene.settle(Some(&editor), 1.0, Paper::WHITE);
2676 let hit = |x| scene.hit_test::<()>([x, 10.0], Some(&editor), |_| None);
2677 assert_eq!(hit(60.0), Some(SceneHit::Image(id)));
2678 assert_eq!(hit(10.0), None);
2679 let mut primitives = Vec::new();
2680 scene
2681 .append_primitives_with::<SceneError>(
2682 &mut primitives,
2683 [0.0; 2],
2684 Some(&editor),
2685 Some((id, [70.0, 5.0, 120.0, 55.0])),
2686 Paper::WHITE,
2687 |_, _, _| Ok(()),
2688 )
2689 .unwrap();
2690 let rects: Vec<_> = primitives
2691 .iter()
2692 .filter_map(|primitive| match primitive {
2693 Primitive::Image { rect, .. } => Some(*rect),
2694 _ => None,
2695 })
2696 .collect();
2697 assert_eq!(rects, [[0.0, 0.0, 100.0, 100.0], [70.0, 5.0, 120.0, 55.0]]);
2698 }
2699
2700 #[test]
2701 fn pictures_past_the_budget_open_decode_in_view_and_keep_their_bytes() {
2702 let side = 1024;
2703 let mut bytes = Vec::new();
2704 {
2705 let mut encoder = png::Encoder::new(&mut bytes, side, side);
2706 encoder.set_color(png::ColorType::Rgba);
2707 encoder.set_depth(png::BitDepth::Eight);
2708 encoder
2709 .write_header()
2710 .unwrap()
2711 .write_image_data(&[40, 90, 160, 255].repeat((side * side) as usize))
2712 .unwrap();
2713 }
2714 let bytes: Arc<[u8]> = Arc::from(bytes);
2715 let pictures: Vec<_> = (0..20)
2716 .map(|index| Image {
2717 size: None,
2718 id: onestore::page::text::new_id().unwrap(),
2719 layout: Layout {
2720 x: Some(0.0),
2721 y: Some(index as f32 * 600.0),
2722 max_width: Some(512.0),
2723 max_height: Some(512.0),
2724 ..Default::default()
2725 },
2726 bytes: Some(Arc::clone(&bytes)),
2727 display: None,
2728 alt: None,
2729 background: false,
2730 printout: None,
2731 tags: Vec::new(),
2732 link: None,
2733 text: None,
2734 })
2735 .collect();
2736 assert!(20 * u64::from(side * side) * 4 > draw::MAX_IMAGE_BYTES);
2737 let page = Page {
2738 identity: None,
2739 created: None,
2740 title: String::new(),
2741 margin_origin: [0.0; 2],
2742 rtl: false,
2743 color: None,
2744 rule_lines: None,
2745 definitions: BTreeMap::new(),
2746 objects: pictures.iter().cloned().map(PageObject::Image).collect(),
2747 };
2748 let mut engine = TextEngine::default();
2749 let (mut scene, editor) = PageScene::from_page(page, &mut engine).unwrap();
2750 let kept = |scene: &PageScene| -> u64 {
2751 let shown = scene.pictures.values().filter_map(Picture::image);
2752 shown.map(|image| image.pixels().len() as u64).sum()
2753 };
2754 let shown = |scene: &PageScene, index: usize| {
2755 scene.image(pictures[index].id).map(|image| image.size())
2756 };
2757 let settle = |scene: &mut PageScene, view: [f32; 4]| {
2758 let give_up = Instant::now() + Duration::from_secs(20);
2759 while !scene.update_pictures(Some(&editor), view, 2.0, Paper::WHITE, Waker::noop()) {
2760 assert!(
2761 Instant::now() < give_up,
2762 "the pictures in view never landed"
2763 );
2764 std::thread::sleep(Duration::from_millis(2));
2765 }
2766 assert!(kept(scene) <= picture::BUDGET, "{}", kept(scene));
2767 };
2768 for top in (0..20).map(|index| index as f32 * 600.0) {
2769 let view = [0.0, top, 600.0, top + 1200.0];
2770 settle(&mut scene, view);
2771 let index = (top / 600.0) as usize;
2772 assert_eq!(shown(&scene, index), Some([side; 2]));
2773 let mut primitives = Vec::new();
2774 scene
2775 .append_primitives_with::<SceneError>(
2776 &mut primitives,
2777 [0.0; 2],
2778 Some(&editor),
2779 None,
2780 Paper::WHITE,
2781 |_, _, _| Ok(()),
2782 )
2783 .unwrap();
2784 assert!(primitives.iter().any(|primitive| matches!(
2785 primitive,
2786 Primitive::Image { rect, .. } if rect[1] == top
2787 )));
2788 }
2789 assert_eq!(shown(&scene, 0), None, "the first picture was let go");
2790 // Every picture in view at once shrinks to fit rather than failing the frame.
2791 settle(&mut scene, [0.0, 0.0, 600.0, 12_000.0]);
2792 for index in 0..20 {
2793 let [width, height] = shown(&scene, index).unwrap();
2794 assert!(width < side && width == height, "{width}×{height}");
2795 }
2796 for object in editor.page().unwrap().objects {
2797 let PageObject::Image(picture) = object else {
2798 panic!()
2799 };
2800 assert!(Arc::ptr_eq(picture.bytes.as_ref().unwrap(), &bytes));
2801 }
2802 }
2803}