| author | |
| committer | |
| log | f2300b7f791d89840114e52f007f185d6c858e89 |
| tree | 02659f16a75e024f205a2877faab76f0a2163c75 |
| parent | ce6f0270afea1cee267fdbdc5e772e598ee773fc |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover) |
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.55 files changed, 353 insertions(+), 54 deletions(-)
crates/canvas/src/gpu/page.rs+61-28| ... | ... | @@ -454,6 +454,7 @@ impl PageScene { |
| 454 | 454 | pub(crate) fn update_pictures( |
| 455 | 455 | &mut self, |
| 456 | 456 | editor: Option<&CanvasEditor>, |
| 457 | column: Option<&crate::reading::Column>, | |
| 457 | 458 | view: [f32; 4], |
| 458 | 459 | scale: f32, |
| 459 | 460 | paper: Paper, |
| ... | ... | @@ -463,7 +464,7 @@ impl PageScene { |
| 463 | 464 | for art in self.backgrounds.values_mut() { |
| 464 | 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 | 468 | picture::update(&mut self.pictures, rects, view, scale, waker) && settled |
| 468 | 469 | } |
| 469 | 470 | |
| ... | ... | @@ -476,7 +477,7 @@ impl PageScene { |
| 476 | 477 | f32::INFINITY, |
| 477 | 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 | 481 | std::thread::sleep(Duration::from_millis(1)); |
| 481 | 482 | } |
| 482 | 483 | } |
| ... | ... | @@ -498,7 +499,11 @@ impl PageScene { |
| 498 | 499 | } |
| 499 | 500 | |
| 500 | 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 | 507 | fn outline( |
| 503 | 508 | layout: &crate::outline::OutlineLayout, |
| 504 | 509 | [x, y]: [f32; 2], |
| ... | ... | @@ -514,12 +519,15 @@ impl PageScene { |
| 514 | 519 | let mut rects = Vec::new(); |
| 515 | 520 | for content in self.objects(editor).into_iter().flatten() { |
| 516 | 521 | 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 | } | |
| 521 | 529 | } |
| 522 | } | |
| 530 | }, | |
| 523 | 531 | Content::File { source, layout } => { |
| 524 | 532 | let origin = crate::origin(&source.layout); |
| 525 | 533 | rects.push((source.id, crate::translated(layout.rect, origin))); |
| ... | ... | @@ -530,7 +538,7 @@ impl PageScene { |
| 530 | 538 | below_title, |
| 531 | 539 | } => { |
| 532 | 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 | 542 | outline(layout, origin, &mut rects); |
| 535 | 543 | } |
| 536 | 544 | } |
| ... | ... | @@ -542,7 +550,8 @@ impl PageScene { |
| 542 | 550 | } |
| 543 | 551 | if let Some(editor) = editor { |
| 544 | 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 | 557 | rects |
| ... | ... | @@ -628,6 +637,7 @@ impl PageScene { |
| 628 | 637 | fn object_tags( |
| 629 | 638 | &self, |
| 630 | 639 | editor: Option<&CanvasEditor>, |
| 640 | column: Option<&crate::reading::Column>, | |
| 631 | 641 | moving: Option<(onestore::ExGuid, [f32; 4])>, |
| 632 | 642 | ) -> Vec<(onestore::ExGuid, crate::outline::ParagraphTag)> { |
| 633 | 643 | let definitions = match (&self.reference, editor) { |
| ... | ... | @@ -641,8 +651,9 @@ impl PageScene { |
| 641 | 651 | .iter() |
| 642 | 652 | .filter_map(|object| { |
| 643 | 653 | 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, | |
| 646 | 657 | _ => bounds, |
| 647 | 658 | }; |
| 648 | 659 | Some( |
| ... | ... | @@ -721,11 +732,14 @@ impl PageScene { |
| 721 | 732 | true, |
| 722 | 733 | )) |
| 723 | 734 | } |
| 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 | }, | |
| 729 | 743 | Content::File { .. } => Some((object.file()?.1, true)), |
| 730 | 744 | Content::Ink(ink) => Some((crate::editor::page::ink_bounds(ink)?, true)), |
| 731 | 745 | Content::ReadOnly(object) => Some((object.rect(), true)), |
| ... | ... | @@ -833,11 +847,14 @@ impl PageScene { |
| 833 | 847 | column: Option<&crate::reading::Column>, |
| 834 | 848 | mut outline: impl FnMut(onestore::ExGuid) -> Option<T>, |
| 835 | 849 | ) -> 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 | }); | |
| 841 | 858 | if let Some((id, _)) = check { |
| 842 | 859 | return Some(SceneHit::Check(id)); |
| 843 | 860 | } |
| ... | ... | @@ -867,7 +884,7 @@ impl PageScene { |
| 867 | 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 | 888 | let [x, y] = crate::origin(&source.layout); |
| 872 | 889 | if let Some([width, height]) = crate::outline::image_size(source) |
| 873 | 890 | && (x..=x + width).contains(&point[0]) |
| ... | ... | @@ -1004,7 +1021,13 @@ impl PageScene { |
| 1004 | 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 | 1031 | Content::ReadOnly(object) => { |
| 1009 | 1032 | let rect = crate::translated(object.rect(), offset); |
| 1010 | 1033 | append_placeholder(rect, paper, primitives); |
| ... | ... | @@ -1069,8 +1092,11 @@ impl PageScene { |
| 1069 | 1092 | .pictures |
| 1070 | 1093 | .get(&source.id) |
| 1071 | 1094 | .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 | }; | |
| 1074 | 1100 | if picture.failed() { |
| 1075 | 1101 | append_placeholder( |
| 1076 | 1102 | [ |
| ... | ... | @@ -1147,7 +1173,7 @@ impl PageScene { |
| 1147 | 1173 | } |
| 1148 | 1174 | } |
| 1149 | 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 | 1177 | primitives.push(Primitive::Icon { |
| 1152 | 1178 | sources: art.sources(&tag), |
| 1153 | 1179 | origin: [tag.origin[0] + offset[0], tag.origin[1] + offset[1]], |
| ... | ... | @@ -2835,7 +2861,14 @@ mod tests { |
| 2835 | 2861 | }; |
| 2836 | 2862 | let settle = |scene: &mut PageScene, view: [f32; 4]| { |
| 2837 | 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 | 2872 | assert!( |
| 2840 | 2873 | Instant::now() < give_up, |
| 2841 | 2874 | "the pictures in view never landed" |
crates/canvas/src/interaction/mod.rs+18-4| ... | ... | @@ -304,6 +304,7 @@ fn update_pictures( |
| 304 | 304 | scene: &mut PageScene, |
| 305 | 305 | [x, y]: [f32; 2], |
| 306 | 306 | editor: &CanvasEditor, |
| 307 | column: Option<&crate::reading::Column>, | |
| 307 | 308 | viewport: Viewport, |
| 308 | 309 | paper: Paper, |
| 309 | 310 | waker: &std::task::Waker, |
| ... | ... | @@ -311,7 +312,7 @@ fn update_pictures( |
| 311 | 312 | let [x0, y0] = viewport.document_point([0.0; 2]); |
| 312 | 313 | let [x1, y1] = viewport.document_point(viewport.size.map(|side| side as f32)); |
| 313 | 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 | } |
| 316 | 317 | |
| 317 | 318 | impl PageView { |
| ... | ... | @@ -982,7 +983,15 @@ impl PageView { |
| 982 | 983 | /// should be drawn again because a raster landed. |
| 983 | 984 | pub fn update_pictures(&mut self, paper: Paper, waker: &std::task::Waker) { |
| 984 | 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 | } |
| 988 | 997 | |
| ... | ... | @@ -999,7 +1008,7 @@ impl PageView { |
| 999 | 1008 | origin: home(self.display_scale), |
| 1000 | 1009 | ..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 | } |
| 1004 | 1013 | |
| 1005 | 1014 | pub fn set_reading(&mut self, width: Option<f32>) -> bool { |
| ... | ... | @@ -1013,6 +1022,10 @@ impl PageView { |
| 1013 | 1022 | .ok() |
| 1014 | 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 | 1029 | self.column.is_some() |
| 1017 | 1030 | } |
| 1018 | 1031 | |
| ... | ... | @@ -2902,6 +2915,7 @@ fn page_primitives<'a>( |
| 2902 | 2915 | primitives, |
| 2903 | 2916 | ); |
| 2904 | 2917 | } |
| 2918 | let shaped = geometry.shaped; | |
| 2905 | 2919 | append_outline( |
| 2906 | 2920 | (object_focus.is_none() |
| 2907 | 2921 | && outline.id == active |
| ... | ... | @@ -2921,7 +2935,7 @@ fn page_primitives<'a>( |
| 2921 | 2935 | _ => None, |
| 2922 | 2936 | }; |
| 2923 | 2937 | scene.append_outline_objects( |
| 2924 | outline.shaped(), | |
| 2938 | shaped, | |
| 2925 | 2939 | [origin[0] + offset[0], origin[1] + offset[1]], |
| 2926 | 2940 | moving, |
| 2927 | 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 | 3915 | ); |
| 3916 | 3916 | } |
| 3917 | 3917 | |
| 3918 | /// A picture of the corpus photo `size` points across, at `origin` on the page. | |
| 3919 | fn 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] | |
| 3941 | fn 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 | 4058 | #[test] |
| 3919 | 4059 | fn reader_consumers_share_title_header_date_and_body_geometry() { |
| 3920 | 4060 | let mut view = reader_view(reader_page()); |
crates/canvas/src/reading.rs+71-22| ... | ... | @@ -28,6 +28,18 @@ pub(crate) struct Column { |
| 28 | 28 | #[cfg(feature = "interaction")] |
| 29 | 29 | pub(crate) width: f32, |
| 30 | 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")] | |
| 36 | impl 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 | } |
| 32 | 44 | |
| 33 | 45 | #[cfg(feature = "gpu")] |
| ... | ... | @@ -51,8 +63,8 @@ pub(crate) fn geometry<'a>( |
| 51 | 63 | |
| 52 | 64 | #[cfg(feature = "interaction")] |
| 53 | 65 | impl 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. | |
| 56 | 68 | pub(crate) fn build( |
| 57 | 69 | editor: &CanvasEditor, |
| 58 | 70 | scene: Option<&crate::gpu::page::PageScene>, |
| ... | ... | @@ -96,6 +108,7 @@ impl Column { |
| 96 | 108 | below_title: Some(_), |
| 97 | 109 | .. |
| 98 | 110 | } |
| 111 | | Content::Image(_) | |
| 99 | 112 | ) |
| 100 | 113 | }) { |
| 101 | 114 | return Ok(None); |
| ... | ... | @@ -128,20 +141,40 @@ impl Column { |
| 128 | 141 | .position(|block| block.members.contains(&id)) |
| 129 | 142 | .unwrap_or(ordered.len()) |
| 130 | 143 | }; |
| 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 | |
| 132 | 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 | 167 | .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]), _)| { | |
| 137 | 169 | a_rank |
| 138 | 170 | .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)) | |
| 141 | 173 | }); |
| 142 | 174 | let mut column = Self { |
| 143 | 175 | width, |
| 144 | 176 | outlines: Vec::new(), |
| 177 | pictures: Vec::new(), | |
| 145 | 178 | }; |
| 146 | 179 | let mut top = 14.4; |
| 147 | 180 | if let Some(title) = title { |
| ... | ... | @@ -159,12 +192,24 @@ impl Column { |
| 159 | 192 | top = top.max(rect[3] + GAP); |
| 160 | 193 | } |
| 161 | 194 | } |
| 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 | } | |
| 165 | 212 | }; |
| 166 | let height = layout.size[1]; | |
| 167 | column.outlines.push((source.id, [36.0, top], layout)); | |
| 168 | 213 | top += height + GAP; |
| 169 | 214 | } |
| 170 | 215 | Ok(Some(column)) |
| ... | ... | @@ -180,14 +225,6 @@ fn column_layout( |
| 180 | 225 | engine: &mut TextEngine, |
| 181 | 226 | ) -> Result<Option<crate::outline::OutlineLayout>, crate::layout::LayoutError> { |
| 182 | 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 | 228 | if source.document().paragraphs().any(|text| { |
| 192 | 229 | text.spans() |
| 193 | 230 | .iter() |
| ... | ... | @@ -195,8 +232,20 @@ fn column_layout( |
| 195 | 232 | }) { |
| 196 | 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 | 247 | let layout = OutlineLayout::flow( |
| 199 | source.document().nodes().iter(), | |
| 248 | nodes.iter(), | |
| 200 | 249 | &source.indents, |
| 201 | 250 | width, |
| 202 | 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 | 3419 | assert!(texts.contains(&"Lower box, edited in reading 😀".to_string())); |
| 3420 | 3420 | } |
| 3421 | 3421 | |
| 3422 | /// A view of the page `title` of the corpus section `path`, copied, at 100%. | |
| 3423 | fn 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] | |
| 3443 | fn 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 | 3485 | #[test] |
| 3423 | 3486 | fn reader_local_apply_fence_tracks_overlapping_replies_and_view_lifetime() { |
| 3424 | 3487 | use std::sync::mpsc; |