| 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,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) && settled | 468 | picture::update(&mut self.pictures, rects, view, scale, waker) && settled |
| 468 | } | 469 | } |
| 469 | 470 | ||
| ... | @@ -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 | } |
| 499 | 500 | ||
| 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 | rects | 557 | 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 | .pictures | 1092 | .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 | } |
| 316 | 317 | ||
| 317 | impl PageView { | 318 | impl 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 | } |
| 988 | 997 | ||
| ... | @@ -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.viewport | 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 | 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 | } |
| 1018 | 1031 | ||
| ... | @@ -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 == active | 2921 | && 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 | } |
| 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 | #[test] | 4058 | #[test] |
| 3919 | fn reader_consumers_share_title_header_date_and_body_geometry() { | 4059 | fn 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")] | ||
| 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 | #[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>( |
| 51 | 63 | ||
| 52 | #[cfg(feature = "interaction")] | 64 | #[cfg(feature = "interaction")] |
| 53 | impl Column { | 65 | impl 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<_> = 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 | .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_rank | 169 | 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 | } |
| 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 | #[test] | 3485 | #[test] |
| 3423 | fn reader_local_apply_fence_tracks_overlapping_replies_and_view_lifetime() { | 3486 | fn reader_local_apply_fence_tracks_overlapping_replies_and_view_lifetime() { |
| 3424 | use std::sync::mpsc; | 3487 | use std::sync::mpsc; |