authorclover caruso <git@paperclover.net> 2026-10-15 23:26:47-07:00
committerclover caruso <git@paperclover.net> 2026-10-15 23:54:24-07:00
logf2300b7f791d89840114e52f007f185d6c858e89
tree02659f16a75e024f205a2877faab76f0a2163c75
parentce6f0270afea1cee267fdbdc5e772e598ee773fc
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover)

feat(mobile): show pictures in the Reading View

Pictures on the page now take their place in the Reading View's reading order instead of keeping the page out of it. Each shows at the column's width, or at its own size where that is smaller, and pictures inside a text box shrink the same way within it. They are only shown there: a tap or drag on one does not select, move or resize it, a picture picked in the page view lets go when the Reading View opens, and the page keeps every picture where it was. Refs #128 Assisted-by: claude-opus-5.5

5 files changed, 353 insertions(+), 54 deletions(-)

crates/canvas/src/gpu/page.rs+61-28
......@@ -454,6 +454,7 @@ impl PageScene {
454454 pub(crate) fn update_pictures(
455455 &mut self,
456456 editor: Option<&CanvasEditor>,
457 column: Option<&crate::reading::Column>,
457458 view: [f32; 4],
458459 scale: f32,
459460 paper: Paper,
......@@ -463,7 +464,7 @@ impl PageScene {
463464 for art in self.backgrounds.values_mut() {
464465 settled &= art.update(paper, scale, waker);
465466 }
466 let rects = self.picture_rects(editor);
467 let rects = self.picture_rects(editor, column);
467468 picture::update(&mut self.pictures, rects, view, scale, waker) && settled
468469 }
469470
......@@ -476,7 +477,7 @@ impl PageScene {
476477 f32::INFINITY,
477478 f32::INFINITY,
478479 ];
479 while !self.update_pictures(editor, everything, scale, paper, Waker::noop()) {
480 while !self.update_pictures(editor, None, everything, scale, paper, Waker::noop()) {
480481 std::thread::sleep(Duration::from_millis(1));
481482 }
482483 }
......@@ -498,7 +499,11 @@ impl PageScene {
498499 }
499500
500501 /// Where pictures and file icons sit, in scene coordinates.
501 fn picture_rects(&self, editor: Option<&CanvasEditor>) -> Vec<(onestore::ExGuid, [f32; 4])> {
502 fn picture_rects(
503 &self,
504 editor: Option<&CanvasEditor>,
505 column: Option<&crate::reading::Column>,
506 ) -> Vec<(onestore::ExGuid, [f32; 4])> {
502507 fn outline(
503508 layout: &crate::outline::OutlineLayout,
504509 [x, y]: [f32; 2],
......@@ -514,12 +519,15 @@ impl PageScene {
514519 let mut rects = Vec::new();
515520 for content in self.objects(editor).into_iter().flatten() {
516521 match content {
517 Content::Image(source) => {
518 let [x, y] = crate::origin(&source.layout);
519 if let Some([width, height]) = crate::outline::image_size(source) {
520 rects.push((source.id, [x, y, x + width, y + height]));
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 }
521529 }
522 }
530 },
523531 Content::File { source, layout } => {
524532 let origin = crate::origin(&source.layout);
525533 rects.push((source.id, crate::translated(layout.rect, origin)));
......@@ -530,7 +538,7 @@ impl PageScene {
530538 below_title,
531539 } => {
532540 let origin = crate::origin(&source.layout);
533 if let Ok(origin) = outline_origin(origin, *below_title, editor, None) {
541 if let Ok(origin) = outline_origin(origin, *below_title, editor, column) {
534542 outline(layout, origin, &mut rects);
535543 }
536544 }
......@@ -542,7 +550,8 @@ impl PageScene {
542550 }
543551 if let Some(editor) = editor {
544552 for editable in editor.visible_outlines() {
545 outline(editable.shaped(), editable.origin(), &mut rects);
553 let (geometry, origin) = crate::reading::geometry(editable, column);
554 outline(geometry.shaped, origin, &mut rects);
546555 }
547556 }
548557 rects
......@@ -628,6 +637,7 @@ impl PageScene {
628637 fn object_tags(
629638 &self,
630639 editor: Option<&CanvasEditor>,
640 column: Option<&crate::reading::Column>,
631641 moving: Option<(onestore::ExGuid, [f32; 4])>,
632642 ) -> Vec<(onestore::ExGuid, crate::outline::ParagraphTag)> {
633643 let definitions = match (&self.reference, editor) {
......@@ -641,8 +651,9 @@ impl PageScene {
641651 .iter()
642652 .filter_map(|object| {
643653 let (id, tags, bounds) = object.tagged()?;
644 let bounds = match moving {
645 Some((moved, rect)) if moved == id => rect,
654 let bounds = match (column, moving) {
655 (Some(column), _) => column.picture(id)?,
656 (None, Some((moved, rect))) if moved == id => rect,
646657 _ => bounds,
647658 };
648659 Some(
......@@ -721,11 +732,14 @@ impl PageScene {
721732 true,
722733 ))
723734 }
724 Content::Image(source) => {
725 let [x, y] = crate::origin(&source.layout);
726 let [width, height] = crate::outline::image_size(source)?;
727 Some(([x, y, x + width, y + height], false))
728 }
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 },
729743 Content::File { .. } => Some((object.file()?.1, true)),
730744 Content::Ink(ink) => Some((crate::editor::page::ink_bounds(ink)?, true)),
731745 Content::ReadOnly(object) => Some((object.rect(), true)),
......@@ -833,11 +847,14 @@ impl PageScene {
833847 column: Option<&crate::reading::Column>,
834848 mut outline: impl FnMut(onestore::ExGuid) -> Option<T>,
835849 ) -> Option<SceneHit<T>> {
836 let check = self.object_tags(editor, None).into_iter().find(|(_, tag)| {
837 tag.icon.checkable()
838 && (tag.origin[0]..=tag.origin[0] + tag.size).contains(&point[0])
839 && (tag.origin[1]..=tag.origin[1] + tag.size).contains(&point[1])
840 });
850 let check = self
851 .object_tags(editor, column, None)
852 .into_iter()
853 .find(|(_, tag)| {
854 tag.icon.checkable()
855 && (tag.origin[0]..=tag.origin[0] + tag.size).contains(&point[0])
856 && (tag.origin[1]..=tag.origin[1] + tag.size).contains(&point[1])
857 });
841858 if let Some((id, _)) = check {
842859 return Some(SceneHit::Check(id));
843860 }
......@@ -867,7 +884,7 @@ impl PageScene {
867884 return Some(SceneHit::ReadOnly(readonly));
868885 }
869886 }
870 Content::Image(source) if !source.background => {
887 Content::Image(source) if !source.background && column.is_none() => {
871888 let [x, y] = crate::origin(&source.layout);
872889 if let Some([width, height]) = crate::outline::image_size(source)
873890 && (x..=x + width).contains(&point[0])
......@@ -1004,7 +1021,13 @@ impl PageScene {
10041021 column,
10051022 )?
10061023 }
1007 Content::Image(source) => crate::origin(&source.layout),
1024 Content::Image(source) => match column {
1025 Some(column) => match column.picture(source.id) {
1026 Some([x, y, ..]) => [x, y],
1027 None => continue,
1028 },
1029 None => crate::origin(&source.layout),
1030 },
10081031 Content::ReadOnly(object) => {
10091032 let rect = crate::translated(object.rect(), offset);
10101033 append_placeholder(rect, paper, primitives);
......@@ -1069,8 +1092,11 @@ impl PageScene {
10691092 .pictures
10701093 .get(&source.id)
10711094 .ok_or(SceneError::MissingImage)?;
1072 let size = crate::outline::image_size(source)
1073 .ok_or(SceneError::MissingImage)?;
1095 let size = match column.and_then(|column| column.picture(source.id)) {
1096 Some([x0, y0, x1, y1]) => [x1 - x0, y1 - y0],
1097 None => crate::outline::image_size(source)
1098 .ok_or(SceneError::MissingImage)?,
1099 };
10741100 if picture.failed() {
10751101 append_placeholder(
10761102 [
......@@ -1147,7 +1173,7 @@ impl PageScene {
11471173 }
11481174 }
11491175 let art = super::TagArt::default();
1150 for (_, tag) in self.object_tags(editor, moving) {
1176 for (_, tag) in self.object_tags(editor, column, moving) {
11511177 primitives.push(Primitive::Icon {
11521178 sources: art.sources(&tag),
11531179 origin: [tag.origin[0] + offset[0], tag.origin[1] + offset[1]],
......@@ -2835,7 +2861,14 @@ mod tests {
28352861 };
28362862 let settle = |scene: &mut PageScene, view: [f32; 4]| {
28372863 let give_up = Instant::now() + Duration::from_secs(20);
2838 while !scene.update_pictures(Some(&editor), view, 2.0, Paper::WHITE, Waker::noop()) {
2864 while !scene.update_pictures(
2865 Some(&editor),
2866 None,
2867 view,
2868 2.0,
2869 Paper::WHITE,
2870 Waker::noop(),
2871 ) {
28392872 assert!(
28402873 Instant::now() < give_up,
28412874 "the pictures in view never landed"
crates/canvas/src/interaction/mod.rs+18-4
......@@ -304,6 +304,7 @@ fn update_pictures(
304304 scene: &mut PageScene,
305305 [x, y]: [f32; 2],
306306 editor: &CanvasEditor,
307 column: Option<&crate::reading::Column>,
307308 viewport: Viewport,
308309 paper: Paper,
309310 waker: &std::task::Waker,
......@@ -311,7 +312,7 @@ fn update_pictures(
311312 let [x0, y0] = viewport.document_point([0.0; 2]);
312313 let [x1, y1] = viewport.document_point(viewport.size.map(|side| side as f32));
313314 let view = [x0 - x, y0 - y, x1 - x, y1 - y];
314 scene.update_pictures(Some(editor), view, viewport.scale, paper, waker)
315 scene.update_pictures(Some(editor), column, view, viewport.scale, paper, waker)
315316}
316317
317318impl PageView {
......@@ -982,7 +983,15 @@ impl PageView {
982983 /// should be drawn again because a raster landed.
983984 pub fn update_pictures(&mut self, paper: Paper, waker: &std::task::Waker) {
984985 if let Some((scene, offset)) = &mut self.scene {
985 update_pictures(scene, *offset, &self.editor, self.viewport, paper, waker);
986 update_pictures(
987 scene,
988 *offset,
989 &self.editor,
990 self.column.as_ref(),
991 self.viewport,
992 paper,
993 waker,
994 );
986995 }
987996 }
988997
......@@ -999,7 +1008,7 @@ impl PageView {
9991008 origin: home(self.display_scale),
10001009 ..self.viewport
10011010 };
1002 update_pictures(scene, *offset, editor, viewport, paper, waker)
1011 update_pictures(scene, *offset, editor, None, viewport, paper, waker)
10031012 }
10041013
10051014 pub fn set_reading(&mut self, width: Option<f32>) -> bool {
......@@ -1013,6 +1022,10 @@ impl PageView {
10131022 .ok()
10141023 .flatten()
10151024 });
1025 // The column shows pictures only; one picked in the page view lets go.
1026 if self.column.is_some() {
1027 self.set_object_focus(None);
1028 }
10161029 self.column.is_some()
10171030 }
10181031
......@@ -2902,6 +2915,7 @@ fn page_primitives<'a>(
29022915 primitives,
29032916 );
29042917 }
2918 let shaped = geometry.shaped;
29052919 append_outline(
29062920 (object_focus.is_none()
29072921 && outline.id == active
......@@ -2921,7 +2935,7 @@ fn page_primitives<'a>(
29212935 _ => None,
29222936 };
29232937 scene.append_outline_objects(
2924 outline.shaped(),
2938 shaped,
29252939 [origin[0] + offset[0], origin[1] + offset[1]],
29262940 moving,
29272941 paint.colors.paper,
crates/canvas/src/interaction/tests.rs+140
......@@ -3915,6 +3915,146 @@ fn reader_stacks_several_outlines_in_reading_order_each_editable_where_it_is() {
39153915 );
39163916}
39173917
3918/// A picture of the corpus photo `size` points across, at `origin` on the page.
3919fn reader_picture(origin: [f32; 2], size: [f32; 2]) -> onestore::page::Image {
3920 let bytes = include_bytes!("../../../../corpus/object-tags/native/notebook/photo.png");
3921 onestore::page::Image {
3922 size: Some(size),
3923 id: onestore::page::text::new_id().unwrap(),
3924 layout: onestore::document::Layout {
3925 x: Some(origin[0]),
3926 y: Some(origin[1]),
3927 ..Default::default()
3928 },
3929 bytes: Some(Arc::from(&bytes[..])),
3930 display: None,
3931 alt: None,
3932 background: false,
3933 printout: None,
3934 tags: Vec::new(),
3935 link: None,
3936 text: None,
3937 }
3938}
3939
3940#[test]
3941fn reader_shrinks_pictures_to_the_column_in_reading_order_and_leaves_them_alone() {
3942 use onestore::page::{PageObject, ParagraphContent};
3943 let (mut page, [left, right, below]) = reader_columns_page();
3944 let wide = reader_picture([60.0, 330.0], [400.0, 160.0]);
3945 let small = reader_picture([520.0, 524.0], [20.0, 15.0]);
3946 let [wide_id, small_id] = [wide.id, small.id];
3947 page.objects
3948 .extend([PageObject::Image(small), PageObject::Image(wide)]);
3949 let PageObject::Outline(outline) = page
3950 .objects
3951 .iter_mut()
3952 .find(|object| object.id() == left)
3953 .unwrap()
3954 else {
3955 unreachable!()
3956 };
3957 let mut inline = outline.paragraphs[0].clone();
3958 inline.id = onestore::page::text::new_id().unwrap();
3959 let inline_picture = reader_picture([0.0; 2], [240.0, 48.0]);
3960 let inline_id = inline_picture.id;
3961 inline.content = ParagraphContent::Image(onestore::page::Image {
3962 layout: Default::default(),
3963 ..inline_picture
3964 });
3965 outline.paragraphs.insert(1, inline);
3966 let mut view = reader_view(page);
3967 let (scene, _) = view.scene.as_mut().unwrap();
3968 scene.settle(Some(&view.editor), 1.0, COLORS.paper);
3969 let original = view.editor.page().unwrap();
3970 let offset = view.scene.as_ref().unwrap().1;
3971 // A picture picked in the page view lets go in the column.
3972 click(
3973 &mut view,
3974 [60.0 + offset[0] + 10.0, 330.0 + offset[1] + 10.0],
3975 Instant::now(),
3976 );
3977 assert_eq!(view.object_focus, Some(ObjectFocus::Image(wide_id)));
3978 assert!(view.set_reading(Some(200.0)));
3979 assert!(view.object_focus.is_none());
3980 let column = view.column.as_ref().unwrap();
3981 let place = |id| {
3982 column
3983 .outlines
3984 .iter()
3985 .find(|(outline, ..)| *outline == id)
3986 .map(|(_, origin, shaped)| [origin[1], origin[1] + shaped.size[1]])
3987 .or_else(|| column.picture(id).map(|rect| [rect[1], rect[3]]))
3988 .unwrap()
3989 };
3990 let stacked = [left, right, wide_id, below, small_id].map(place);
3991 for pair in stacked.windows(2) {
3992 assert!(pair[1][0] >= pair[0][1] + 18.0);
3993 }
3994 let wide_rect = column.picture(wide_id).unwrap();
3995 assert_eq!(wide_rect, [36.0, wide_rect[1], 236.0, wide_rect[1] + 80.0]);
3996 let small_rect = column.picture(small_id).unwrap();
3997 assert_eq!(
3998 small_rect,
3999 [36.0, small_rect[1], 56.0, small_rect[1] + 15.0]
4000 );
4001 let (_, _, shaped) = column.outlines.iter().find(|(id, ..)| *id == left).unwrap();
4002 let object = shaped.objects.iter().find(|o| o.id == inline_id).unwrap();
4003 let [x0, y0, x1, y1] = object.rect;
4004 assert!(x1 <= 200.0 + 0.01);
4005 assert!(((x1 - x0) / (y1 - y0) - 5.0).abs() < 0.01);
4006 let primitives = view.primitives(COLORS).unwrap();
4007 for rect in [
4008 wide_rect,
4009 small_rect,
4010 crate::translated(object.rect, [36.0, place(left)[0]]),
4011 ] {
4012 let rect = crate::translated(rect, offset);
4013 assert!(
4014 primitives
4015 .iter()
4016 .any(|primitive| matches!(primitive, Primitive::Image { rect: drawn, .. } if *drawn == rect)),
4017 "{rect:?}"
4018 );
4019 }
4020 drop(primitives);
4021 let centre = [
4022 (wide_rect[0] + wide_rect[2]) / 2.0 + offset[0],
4023 (wide_rect[1] + wide_rect[3]) / 2.0 + offset[1],
4024 ];
4025 assert!(!matches!(view.hit(centre), Some(Hit::Image { .. })));
4026 click(&mut view, centre, Instant::now());
4027 assert!(view.object_focus.is_none());
4028 // Nor does a drag across one, or across its corner, move or resize it.
4029 for from in [
4030 centre,
4031 [
4032 wide_rect[2] + offset[0] - 1.0,
4033 wide_rect[3] + offset[1] - 1.0,
4034 ],
4035 ] {
4036 let _ = view.pointer_moved(from).unwrap();
4037 let _ = view.pointer_pressed(Instant::now()).unwrap();
4038 let _ = view
4039 .pointer_moved([from[0] + 40.0, from[1] + 30.0])
4040 .unwrap();
4041 let _ = view.pointer_released().unwrap();
4042 assert!(view.object_focus.is_none());
4043 }
4044 let bounds: Vec<_> = view
4045 .scene
4046 .as_ref()
4047 .unwrap()
4048 .0
4049 .content_bounds(&view.editor, view.column.as_ref())
4050 .map(|(rect, _)| rect)
4051 .collect();
4052 assert!(bounds.contains(&wide_rect) && bounds.contains(&small_rect));
4053 assert_eq!(view.editor.page().unwrap(), original);
4054 assert!(view.editor.take_ops().unwrap().is_empty());
4055 assert!(!view.set_reading(None) && view.column.is_none());
4056}
4057
39184058#[test]
39194059fn reader_consumers_share_title_header_date_and_body_geometry() {
39204060 let mut view = reader_view(reader_page());
crates/canvas/src/reading.rs+71-22
......@@ -28,6 +28,18 @@ pub(crate) struct Column {
2828 #[cfg(feature = "interaction")]
2929 pub(crate) width: f32,
3030 pub(crate) outlines: Vec<(ExGuid, [f32; 2], crate::outline::OutlineLayout)>,
31 /// Page pictures as the column shows them, `[x0, y0, x1, y1]` in page points.
32 pub(crate) pictures: Vec<(ExGuid, [f32; 4])>,
33}
34
35#[cfg(feature = "gpu")]
36impl Column {
37 /// Where the column shows page picture `id`; None for one it leaves out.
38 pub(crate) fn picture(&self, id: ExGuid) -> Option<[f32; 4]> {
39 self.pictures
40 .iter()
41 .find_map(|(picture, rect)| (*picture == id).then_some(*rect))
42 }
3143}
3244
3345#[cfg(feature = "gpu")]
......@@ -51,8 +63,8 @@ pub(crate) fn geometry<'a>(
5163
5264#[cfg(feature = "interaction")]
5365impl Column {
54 /// The page's editable outlines stacked in reading order, title and its header first,
55 /// or None where something on the page would not reflow.
66 /// The page's editable outlines and pictures stacked in reading order, title and its
67 /// header first, or None where something on the page would not reflow.
5668 pub(crate) fn build(
5769 editor: &CanvasEditor,
5870 scene: Option<&crate::gpu::page::PageScene>,
......@@ -96,6 +108,7 @@ impl Column {
96108 below_title: Some(_),
97109 ..
98110 }
111 | Content::Image(_)
99112 )
100113 }) {
101114 return Ok(None);
......@@ -128,20 +141,40 @@ impl Column {
128141 .position(|block| block.members.contains(&id))
129142 .unwrap_or(ordered.len())
130143 };
131 let mut body: Vec<_> = body
144 enum Piece<'a> {
145 Text(&'a TextOutline),
146 Picture(ExGuid, [f32; 2], [f32; 2]),
147 }
148 let pictures = editor.objects.iter().filter_map(|object| match object {
149 Content::Image(image) if !image.background => Some(Piece::Picture(
150 image.id,
151 crate::origin(&image.layout),
152 crate::outline::image_size(image)?,
153 )),
154 _ => None,
155 });
156 let mut pieces: Vec<_> = body
132157 .into_iter()
133 .map(|outline| (rank(outline.id), outline))
158 .map(Piece::Text)
159 .chain(pictures)
160 .map(|piece| {
161 let (id, origin) = match piece {
162 Piece::Text(outline) => (outline.id, outline.origin()),
163 Piece::Picture(id, origin, _) => (id, origin),
164 };
165 ((rank(id), origin), piece)
166 })
134167 .collect();
135 body.sort_by(|(a_rank, a), (b_rank, b)| {
136 let ([ax, ay], [bx, by]) = (a.origin(), b.origin());
168 pieces.sort_by(|((a_rank, [ax, ay]), _), ((b_rank, [bx, by]), _)| {
137169 a_rank
138170 .cmp(b_rank)
139 .then(ay.total_cmp(&by))
140 .then(ax.total_cmp(&bx))
171 .then(ay.total_cmp(by))
172 .then(ax.total_cmp(bx))
141173 });
142174 let mut column = Self {
143175 width,
144176 outlines: Vec::new(),
177 pictures: Vec::new(),
145178 };
146179 let mut top = 14.4;
147180 if let Some(title) = title {
......@@ -159,12 +192,24 @@ impl Column {
159192 top = top.max(rect[3] + GAP);
160193 }
161194 }
162 for (_, source) in body {
163 let Some(layout) = column_layout(source, editor, width, engine)? else {
164 return Ok(None);
195 for (_, piece) in pieces {
196 let height = match piece {
197 Piece::Text(source) => {
198 let Some(layout) = column_layout(source, editor, width, engine)? else {
199 return Ok(None);
200 };
201 let height = layout.size[1];
202 column.outlines.push((source.id, [36.0, top], layout));
203 height
204 }
205 Piece::Picture(id, _, [w, h]) => {
206 let scale = (width / w).min(1.0);
207 column
208 .pictures
209 .push((id, [36.0, top, 36.0 + w * scale, top + h * scale]));
210 h * scale
211 }
165212 };
166 let height = layout.size[1];
167 column.outlines.push((source.id, [36.0, top], layout));
168213 top += height + GAP;
169214 }
170215 Ok(Some(column))
......@@ -180,14 +225,6 @@ fn column_layout(
180225 engine: &mut TextEngine,
181226) -> Result<Option<crate::outline::OutlineLayout>, crate::layout::LayoutError> {
182227 use crate::outline::{OutlineLayout, ParagraphLayout};
183 if source
184 .document()
185 .nodes()
186 .iter()
187 .any(|node| !matches!(node.content, ParagraphContent::Text(_)))
188 {
189 return Ok(None);
190 }
191228 if source.document().paragraphs().any(|text| {
192229 text.spans()
193230 .iter()
......@@ -195,8 +232,20 @@ fn column_layout(
195232 }) {
196233 return Ok(None);
197234 }
235 // Pictures in the flow shrink to the room their level leaves.
236 let mut nodes = source.document().nodes().to_vec();
237 for node in &mut nodes {
238 match &mut node.content {
239 ParagraphContent::Text(_) => {}
240 ParagraphContent::Image(image) => {
241 let indent = crate::outline::indentation(node.level, &source.indents, width)?;
242 shrink(image, width - indent);
243 }
244 _ => return Ok(None),
245 }
246 }
198247 let layout = OutlineLayout::flow(
199 source.document().nodes().iter(),
248 nodes.iter(),
200249 &source.indents,
201250 width,
202251 false,
crates/mobile/src/tests.rs+63
......@@ -3419,6 +3419,69 @@ fn reader_stacks_text_boxes_and_stores_an_edit_in_the_second_one() {
34193419 assert!(texts.contains(&"Lower box, edited in reading 😀".to_string()));
34203420}
34213421
3422/// A view of the page `title` of the corpus section `path`, copied, at 100%.
3423fn reader_corpus_view(path: &str, title: &str) -> (tempfile::TempDir, Section, View) {
3424 let (directory, file) = copy(path);
3425 let library = Arc::new(Library::open(&file, &directory.path().join("cache"), true).unwrap());
3426 let section = open(&library, &file.to_string_lossy());
3427 let space = space(&section, title);
3428 let (page, read_only) = section.shared.page(space).unwrap();
3429 let mut view = View {
3430 canvas: ManuallyDrop::new(Canvas::new(space, page, [720, 1280], 2.0).unwrap()),
3431 presentation: None,
3432 frame: Arc::default(),
3433 section: Arc::clone(&section.shared),
3434 read_only,
3435 reload_pending: false,
3436 local_edits: Arc::new(Mutex::new(LocalEdits::default())),
3437 };
3438 view.canvas.set_transform(1.0, [0.0; 2]);
3439 (directory, section, view)
3440}
3441
3442#[test]
3443fn reader_shows_a_corpus_pages_picture_no_wider_than_the_column() {
3444 let (_directory, _section, mut view) =
3445 reader_corpus_view("print/native/Print.one", "Printing test");
3446 let query = sb_view_reading(&view, 240.0);
3447 let status = serde_json::from_str::<serde_json::Value>(&string(query)).unwrap();
3448 unsafe { sb_string_free(query) };
3449 assert_eq!(status["offered"], true);
3450 let mut page = [0.0; 4];
3451 sb_view_content(&mut view, &mut page);
3452 let mut anchor = [0.0; 2];
3453 assert!(sb_view_set_reading(&mut view, 240.0, &mut anchor));
3454 let mut column = [0.0; 4];
3455 sb_view_content(&mut view, &mut column);
3456 assert!(column[2] < page[2]);
3457 // The picture follows the long outline: the whole column in view.
3458 view.canvas.set_transform(0.05, [0.0; 2]);
3459 let paper = view.canvas.page_paper();
3460 let deadline = std::time::Instant::now() + Duration::from_secs(20);
3461 let pictures = loop {
3462 view.canvas
3463 .page
3464 .update_pictures(paper, std::task::Waker::noop());
3465 let pictures: Vec<[f32; 4]> = view
3466 .canvas
3467 .page
3468 .primitives(view.canvas.colors())
3469 .unwrap()
3470 .iter()
3471 .filter_map(|primitive| match primitive {
3472 draw::Primitive::Image { rect, .. } => Some(*rect),
3473 _ => None,
3474 })
3475 .collect();
3476 if !pictures.is_empty() || std::time::Instant::now() > deadline {
3477 break pictures;
3478 }
3479 std::thread::sleep(Duration::from_millis(10));
3480 };
3481 assert_eq!(pictures.len(), 1);
3482 assert!(pictures[0][2] - pictures[0][0] <= 240.0 / POINT + 0.01);
3483}
3484
34223485#[test]
34233486fn reader_local_apply_fence_tracks_overlapping_replies_and_view_lifetime() {
34243487 use std::sync::mpsc;