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 {...@@ -454,6 +454,7 @@ impl PageScene {
454 pub(crate) fn update_pictures(454 pub(crate) fn update_pictures(
455 &mut self,455 &mut self,
456 editor: Option<&CanvasEditor>,456 editor: Option<&CanvasEditor>,
457 column: Option<&crate::reading::Column>,
457 view: [f32; 4],458 view: [f32; 4],
458 scale: f32,459 scale: f32,
459 paper: Paper,460 paper: Paper,
...@@ -463,7 +464,7 @@ impl PageScene {...@@ -463,7 +464,7 @@ impl PageScene {
463 for art in self.backgrounds.values_mut() {464 for art in self.backgrounds.values_mut() {
464 settled &= art.update(paper, scale, waker);465 settled &= art.update(paper, scale, waker);
465 }466 }
466 let rects = self.picture_rects(editor);467 let rects = self.picture_rects(editor, column);
467 picture::update(&mut self.pictures, rects, view, scale, waker) && settled468 picture::update(&mut self.pictures, rects, view, scale, waker) && settled
468 }469 }
469470
...@@ -476,7 +477,7 @@ impl PageScene {...@@ -476,7 +477,7 @@ impl PageScene {
476 f32::INFINITY,477 f32::INFINITY,
477 f32::INFINITY,478 f32::INFINITY,
478 ];479 ];
479 while !self.update_pictures(editor, everything, scale, paper, Waker::noop()) {480 while !self.update_pictures(editor, None, everything, scale, paper, Waker::noop()) {
480 std::thread::sleep(Duration::from_millis(1));481 std::thread::sleep(Duration::from_millis(1));
481 }482 }
482 }483 }
...@@ -498,7 +499,11 @@ impl PageScene {...@@ -498,7 +499,11 @@ impl PageScene {
498 }499 }
499500
500 /// Where pictures and file icons sit, in scene coordinates.501 /// 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])> {
502 fn outline(507 fn outline(
503 layout: &crate::outline::OutlineLayout,508 layout: &crate::outline::OutlineLayout,
504 [x, y]: [f32; 2],509 [x, y]: [f32; 2],
...@@ -514,12 +519,15 @@ impl PageScene {...@@ -514,12 +519,15 @@ impl PageScene {
514 let mut rects = Vec::new();519 let mut rects = Vec::new();
515 for content in self.objects(editor).into_iter().flatten() {520 for content in self.objects(editor).into_iter().flatten() {
516 match content {521 match content {
517 Content::Image(source) => {522 Content::Image(source) => match column {
518 let [x, y] = crate::origin(&source.layout);523 Some(column) => rects.extend(column.picture(source.id).map(|r| (source.id, r))),
519 if let Some([width, height]) = crate::outline::image_size(source) {524 None => {
520 rects.push((source.id, [x, y, x + width, y + height]));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 }
521 }529 }
522 }530 },
523 Content::File { source, layout } => {531 Content::File { source, layout } => {
524 let origin = crate::origin(&source.layout);532 let origin = crate::origin(&source.layout);
525 rects.push((source.id, crate::translated(layout.rect, origin)));533 rects.push((source.id, crate::translated(layout.rect, origin)));
...@@ -530,7 +538,7 @@ impl PageScene {...@@ -530,7 +538,7 @@ impl PageScene {
530 below_title,538 below_title,
531 } => {539 } => {
532 let origin = crate::origin(&source.layout);540 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) {
534 outline(layout, origin, &mut rects);542 outline(layout, origin, &mut rects);
535 }543 }
536 }544 }
...@@ -542,7 +550,8 @@ impl PageScene {...@@ -542,7 +550,8 @@ impl PageScene {
542 }550 }
543 if let Some(editor) = editor {551 if let Some(editor) = editor {
544 for editable in editor.visible_outlines() {552 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);
546 }555 }
547 }556 }
548 rects557 rects
...@@ -628,6 +637,7 @@ impl PageScene {...@@ -628,6 +637,7 @@ impl PageScene {
628 fn object_tags(637 fn object_tags(
629 &self,638 &self,
630 editor: Option<&CanvasEditor>,639 editor: Option<&CanvasEditor>,
640 column: Option<&crate::reading::Column>,
631 moving: Option<(onestore::ExGuid, [f32; 4])>,641 moving: Option<(onestore::ExGuid, [f32; 4])>,
632 ) -> Vec<(onestore::ExGuid, crate::outline::ParagraphTag)> {642 ) -> Vec<(onestore::ExGuid, crate::outline::ParagraphTag)> {
633 let definitions = match (&self.reference, editor) {643 let definitions = match (&self.reference, editor) {
...@@ -641,8 +651,9 @@ impl PageScene {...@@ -641,8 +651,9 @@ impl PageScene {
641 .iter()651 .iter()
642 .filter_map(|object| {652 .filter_map(|object| {
643 let (id, tags, bounds) = object.tagged()?;653 let (id, tags, bounds) = object.tagged()?;
644 let bounds = match moving {654 let bounds = match (column, moving) {
645 Some((moved, rect)) if moved == id => rect,655 (Some(column), _) => column.picture(id)?,
656 (None, Some((moved, rect))) if moved == id => rect,
646 _ => bounds,657 _ => bounds,
647 };658 };
648 Some(659 Some(
...@@ -721,11 +732,14 @@ impl PageScene {...@@ -721,11 +732,14 @@ impl PageScene {
721 true,732 true,
722 ))733 ))
723 }734 }
724 Content::Image(source) => {735 Content::Image(source) => match column {
725 let [x, y] = crate::origin(&source.layout);736 Some(column) => Some((column.picture(source.id)?, false)),
726 let [width, height] = crate::outline::image_size(source)?;737 None => {
727 Some(([x, y, x + width, y + height], false))738 let [x, y] = crate::origin(&source.layout);
728 }739 let [width, height] = crate::outline::image_size(source)?;
740 Some(([x, y, x + width, y + height], false))
741 }
742 },
729 Content::File { .. } => Some((object.file()?.1, true)),743 Content::File { .. } => Some((object.file()?.1, true)),
730 Content::Ink(ink) => Some((crate::editor::page::ink_bounds(ink)?, true)),744 Content::Ink(ink) => Some((crate::editor::page::ink_bounds(ink)?, true)),
731 Content::ReadOnly(object) => Some((object.rect(), true)),745 Content::ReadOnly(object) => Some((object.rect(), true)),
...@@ -833,11 +847,14 @@ impl PageScene {...@@ -833,11 +847,14 @@ impl PageScene {
833 column: Option<&crate::reading::Column>,847 column: Option<&crate::reading::Column>,
834 mut outline: impl FnMut(onestore::ExGuid) -> Option<T>,848 mut outline: impl FnMut(onestore::ExGuid) -> Option<T>,
835 ) -> Option<SceneHit<T>> {849 ) -> Option<SceneHit<T>> {
836 let check = self.object_tags(editor, None).into_iter().find(|(_, tag)| {850 let check = self
837 tag.icon.checkable()851 .object_tags(editor, column, None)
838 && (tag.origin[0]..=tag.origin[0] + tag.size).contains(&point[0])852 .into_iter()
839 && (tag.origin[1]..=tag.origin[1] + tag.size).contains(&point[1])853 .find(|(_, tag)| {
840 });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 });
841 if let Some((id, _)) = check {858 if let Some((id, _)) = check {
842 return Some(SceneHit::Check(id));859 return Some(SceneHit::Check(id));
843 }860 }
...@@ -867,7 +884,7 @@ impl PageScene {...@@ -867,7 +884,7 @@ impl PageScene {
867 return Some(SceneHit::ReadOnly(readonly));884 return Some(SceneHit::ReadOnly(readonly));
868 }885 }
869 }886 }
870 Content::Image(source) if !source.background => {887 Content::Image(source) if !source.background && column.is_none() => {
871 let [x, y] = crate::origin(&source.layout);888 let [x, y] = crate::origin(&source.layout);
872 if let Some([width, height]) = crate::outline::image_size(source)889 if let Some([width, height]) = crate::outline::image_size(source)
873 && (x..=x + width).contains(&point[0])890 && (x..=x + width).contains(&point[0])
...@@ -1004,7 +1021,13 @@ impl PageScene {...@@ -1004,7 +1021,13 @@ impl PageScene {
1004 column,1021 column,
1005 )?1022 )?
1006 }1023 }
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 },
1008 Content::ReadOnly(object) => {1031 Content::ReadOnly(object) => {
1009 let rect = crate::translated(object.rect(), offset);1032 let rect = crate::translated(object.rect(), offset);
1010 append_placeholder(rect, paper, primitives);1033 append_placeholder(rect, paper, primitives);
...@@ -1069,8 +1092,11 @@ impl PageScene {...@@ -1069,8 +1092,11 @@ impl PageScene {
1069 .pictures1092 .pictures
1070 .get(&source.id)1093 .get(&source.id)
1071 .ok_or(SceneError::MissingImage)?;1094 .ok_or(SceneError::MissingImage)?;
1072 let size = crate::outline::image_size(source)1095 let size = match column.and_then(|column| column.picture(source.id)) {
1073 .ok_or(SceneError::MissingImage)?;1096 Some([x0, y0, x1, y1]) => [x1 - x0, y1 - y0],
1097 None => crate::outline::image_size(source)
1098 .ok_or(SceneError::MissingImage)?,
1099 };
1074 if picture.failed() {1100 if picture.failed() {
1075 append_placeholder(1101 append_placeholder(
1076 [1102 [
...@@ -1147,7 +1173,7 @@ impl PageScene {...@@ -1147,7 +1173,7 @@ impl PageScene {
1147 }1173 }
1148 }1174 }
1149 let art = super::TagArt::default();1175 let art = super::TagArt::default();
1150 for (_, tag) in self.object_tags(editor, moving) {1176 for (_, tag) in self.object_tags(editor, column, moving) {
1151 primitives.push(Primitive::Icon {1177 primitives.push(Primitive::Icon {
1152 sources: art.sources(&tag),1178 sources: art.sources(&tag),
1153 origin: [tag.origin[0] + offset[0], tag.origin[1] + offset[1]],1179 origin: [tag.origin[0] + offset[0], tag.origin[1] + offset[1]],
...@@ -2835,7 +2861,14 @@ mod tests {...@@ -2835,7 +2861,14 @@ mod tests {
2835 };2861 };
2836 let settle = |scene: &mut PageScene, view: [f32; 4]| {2862 let settle = |scene: &mut PageScene, view: [f32; 4]| {
2837 let give_up = Instant::now() + Duration::from_secs(20);2863 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 ) {
2839 assert!(2872 assert!(
2840 Instant::now() < give_up,2873 Instant::now() < give_up,
2841 "the pictures in view never landed"2874 "the pictures in view never landed"
crates/canvas/src/interaction/mod.rs+18-4
...@@ -304,6 +304,7 @@ fn update_pictures(...@@ -304,6 +304,7 @@ fn update_pictures(
304 scene: &mut PageScene,304 scene: &mut PageScene,
305 [x, y]: [f32; 2],305 [x, y]: [f32; 2],
306 editor: &CanvasEditor,306 editor: &CanvasEditor,
307 column: Option<&crate::reading::Column>,
307 viewport: Viewport,308 viewport: Viewport,
308 paper: Paper,309 paper: Paper,
309 waker: &std::task::Waker,310 waker: &std::task::Waker,
...@@ -311,7 +312,7 @@ fn update_pictures(...@@ -311,7 +312,7 @@ fn update_pictures(
311 let [x0, y0] = viewport.document_point([0.0; 2]);312 let [x0, y0] = viewport.document_point([0.0; 2]);
312 let [x1, y1] = viewport.document_point(viewport.size.map(|side| side as f32));313 let [x1, y1] = viewport.document_point(viewport.size.map(|side| side as f32));
313 let view = [x0 - x, y0 - y, x1 - x, y1 - y];314 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)
315}316}
316317
317impl PageView {318impl PageView {
...@@ -982,7 +983,15 @@ impl PageView {...@@ -982,7 +983,15 @@ impl PageView {
982 /// should be drawn again because a raster landed.983 /// should be drawn again because a raster landed.
983 pub fn update_pictures(&mut self, paper: Paper, waker: &std::task::Waker) {984 pub fn update_pictures(&mut self, paper: Paper, waker: &std::task::Waker) {
984 if let Some((scene, offset)) = &mut self.scene {985 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 );
986 }995 }
987 }996 }
988997
...@@ -999,7 +1008,7 @@ impl PageView {...@@ -999,7 +1008,7 @@ impl PageView {
999 origin: home(self.display_scale),1008 origin: home(self.display_scale),
1000 ..self.viewport1009 ..self.viewport
1001 };1010 };
1002 update_pictures(scene, *offset, editor, viewport, paper, waker)1011 update_pictures(scene, *offset, editor, None, viewport, paper, waker)
1003 }1012 }
10041013
1005 pub fn set_reading(&mut self, width: Option<f32>) -> bool {1014 pub fn set_reading(&mut self, width: Option<f32>) -> bool {
...@@ -1013,6 +1022,10 @@ impl PageView {...@@ -1013,6 +1022,10 @@ impl PageView {
1013 .ok()1022 .ok()
1014 .flatten()1023 .flatten()
1015 });1024 });
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 }
1016 self.column.is_some()1029 self.column.is_some()
1017 }1030 }
10181031
...@@ -2902,6 +2915,7 @@ fn page_primitives<'a>(...@@ -2902,6 +2915,7 @@ fn page_primitives<'a>(
2902 primitives,2915 primitives,
2903 );2916 );
2904 }2917 }
2918 let shaped = geometry.shaped;
2905 append_outline(2919 append_outline(
2906 (object_focus.is_none()2920 (object_focus.is_none()
2907 && outline.id == active2921 && outline.id == active
...@@ -2921,7 +2935,7 @@ fn page_primitives<'a>(...@@ -2921,7 +2935,7 @@ fn page_primitives<'a>(
2921 _ => None,2935 _ => None,
2922 };2936 };
2923 scene.append_outline_objects(2937 scene.append_outline_objects(
2924 outline.shaped(),2938 shaped,
2925 [origin[0] + offset[0], origin[1] + offset[1]],2939 [origin[0] + offset[0], origin[1] + offset[1]],
2926 moving,2940 moving,
2927 paint.colors.paper,2941 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() {...@@ -3915,6 +3915,146 @@ fn reader_stacks_several_outlines_in_reading_order_each_editable_where_it_is() {
3915 );3915 );
3916}3916}
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
3918#[test]4058#[test]
3919fn reader_consumers_share_title_header_date_and_body_geometry() {4059fn reader_consumers_share_title_header_date_and_body_geometry() {
3920 let mut view = reader_view(reader_page());4060 let mut view = reader_view(reader_page());
crates/canvas/src/reading.rs+71-22
...@@ -28,6 +28,18 @@ pub(crate) struct Column {...@@ -28,6 +28,18 @@ pub(crate) struct Column {
28 #[cfg(feature = "interaction")]28 #[cfg(feature = "interaction")]
29 pub(crate) width: f32,29 pub(crate) width: f32,
30 pub(crate) outlines: Vec<(ExGuid, [f32; 2], crate::outline::OutlineLayout)>,30 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 }
31}43}
3244
33#[cfg(feature = "gpu")]45#[cfg(feature = "gpu")]
...@@ -51,8 +63,8 @@ pub(crate) fn geometry<'a>(...@@ -51,8 +63,8 @@ pub(crate) fn geometry<'a>(
5163
52#[cfg(feature = "interaction")]64#[cfg(feature = "interaction")]
53impl Column {65impl Column {
54 /// The page's editable outlines stacked in reading order, title and its header first,66 /// The page's editable outlines and pictures stacked in reading order, title and its
55 /// or None where something on the page would not reflow.67 /// header first, or None where something on the page would not reflow.
56 pub(crate) fn build(68 pub(crate) fn build(
57 editor: &CanvasEditor,69 editor: &CanvasEditor,
58 scene: Option<&crate::gpu::page::PageScene>,70 scene: Option<&crate::gpu::page::PageScene>,
...@@ -96,6 +108,7 @@ impl Column {...@@ -96,6 +108,7 @@ impl Column {
96 below_title: Some(_),108 below_title: Some(_),
97 ..109 ..
98 }110 }
111 | Content::Image(_)
99 )112 )
100 }) {113 }) {
101 return Ok(None);114 return Ok(None);
...@@ -128,20 +141,40 @@ impl Column {...@@ -128,20 +141,40 @@ impl Column {
128 .position(|block| block.members.contains(&id))141 .position(|block| block.members.contains(&id))
129 .unwrap_or(ordered.len())142 .unwrap_or(ordered.len())
130 };143 };
131 let mut body: Vec<_> = body144 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
132 .into_iter()157 .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 })
134 .collect();167 .collect();
135 body.sort_by(|(a_rank, a), (b_rank, b)| {168 pieces.sort_by(|((a_rank, [ax, ay]), _), ((b_rank, [bx, by]), _)| {
136 let ([ax, ay], [bx, by]) = (a.origin(), b.origin());
137 a_rank169 a_rank
138 .cmp(b_rank)170 .cmp(b_rank)
139 .then(ay.total_cmp(&by))171 .then(ay.total_cmp(by))
140 .then(ax.total_cmp(&bx))172 .then(ax.total_cmp(bx))
141 });173 });
142 let mut column = Self {174 let mut column = Self {
143 width,175 width,
144 outlines: Vec::new(),176 outlines: Vec::new(),
177 pictures: Vec::new(),
145 };178 };
146 let mut top = 14.4;179 let mut top = 14.4;
147 if let Some(title) = title {180 if let Some(title) = title {
...@@ -159,12 +192,24 @@ impl Column {...@@ -159,12 +192,24 @@ impl Column {
159 top = top.max(rect[3] + GAP);192 top = top.max(rect[3] + GAP);
160 }193 }
161 }194 }
162 for (_, source) in body {195 for (_, piece) in pieces {
163 let Some(layout) = column_layout(source, editor, width, engine)? else {196 let height = match piece {
164 return Ok(None);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 }
165 };212 };
166 let height = layout.size[1];
167 column.outlines.push((source.id, [36.0, top], layout));
168 top += height + GAP;213 top += height + GAP;
169 }214 }
170 Ok(Some(column))215 Ok(Some(column))
...@@ -180,14 +225,6 @@ fn column_layout(...@@ -180,14 +225,6 @@ fn column_layout(
180 engine: &mut TextEngine,225 engine: &mut TextEngine,
181) -> Result<Option<crate::outline::OutlineLayout>, crate::layout::LayoutError> {226) -> Result<Option<crate::outline::OutlineLayout>, crate::layout::LayoutError> {
182 use crate::outline::{OutlineLayout, ParagraphLayout};227 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 }
191 if source.document().paragraphs().any(|text| {228 if source.document().paragraphs().any(|text| {
192 text.spans()229 text.spans()
193 .iter()230 .iter()
...@@ -195,8 +232,20 @@ fn column_layout(...@@ -195,8 +232,20 @@ fn column_layout(
195 }) {232 }) {
196 return Ok(None);233 return Ok(None);
197 }234 }
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 }
198 let layout = OutlineLayout::flow(247 let layout = OutlineLayout::flow(
199 source.document().nodes().iter(),248 nodes.iter(),
200 &source.indents,249 &source.indents,
201 width,250 width,
202 false,251 false,
crates/mobile/src/tests.rs+63
...@@ -3419,6 +3419,69 @@ fn reader_stacks_text_boxes_and_stores_an_edit_in_the_second_one() {...@@ -3419,6 +3419,69 @@ fn reader_stacks_text_boxes_and_stores_an_edit_in_the_second_one() {
3419 assert!(texts.contains(&"Lower box, edited in reading 😀".to_string()));3419 assert!(texts.contains(&"Lower box, edited in reading 😀".to_string()));
3420}3420}
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
3422#[test]3485#[test]
3423fn reader_local_apply_fence_tracks_overlapping_replies_and_view_lifetime() {3486fn reader_local_apply_fence_tracks_overlapping_replies_and_view_lifetime() {
3424 use std::sync::mpsc;3487 use std::sync::mpsc;