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