| author | |
| committer | |
| log | 8bafe305be064627aa2527be1b4c4b45efb185b0 |
| tree | 1c96dbb11610539a987a1e50a577659b09835659 |
| parent | f2300b7f791d89840114e52f007f185d6c858e89 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover) |
A page with drawings on it is now offered the Reading View too, unless drawings cover most of it. The drawings stay out of the column, and a small grey line at its end says the page has them and how to see them. They cannot be picked, moved or deleted from the column, and any picked in the page view let go when it opens. Handwriting inside a text box still flows with its text where it fits.
Refs #128
Assisted-by: claude-opus-5.57 files changed, 295 insertions(+), 23 deletions(-)
crates/canvas/src/gpu/page.rs+7| ... | @@ -741,10 +741,16 @@ impl PageScene { | ... | @@ -741,10 +741,16 @@ impl PageScene { |
| 741 | } | 741 | } |
| 742 | }, | 742 | }, |
| 743 | Content::File { .. } => Some((object.file()?.1, true)), | 743 | Content::File { .. } => Some((object.file()?.1, true)), |
| 744 | Content::Ink(_) if column.is_some() => None, | ||
| 744 | Content::Ink(ink) => Some((crate::editor::page::ink_bounds(ink)?, true)), | 745 | Content::Ink(ink) => Some((crate::editor::page::ink_bounds(ink)?, true)), |
| 745 | Content::ReadOnly(object) => Some((object.rect(), true)), | 746 | Content::ReadOnly(object) => Some((object.rect(), true)), |
| 746 | Content::Editable(_) => None, | 747 | Content::Editable(_) => None, |
| 747 | }) | 748 | }) |
| 749 | .chain( | ||
| 750 | column | ||
| 751 | .and_then(crate::reading::Column::drawings_rect) | ||
| 752 | .map(|rect| (rect, true)), | ||
| 753 | ) | ||
| 748 | } | 754 | } |
| 749 | 755 | ||
| 750 | pub(crate) fn header_text<'a>( | 756 | pub(crate) fn header_text<'a>( |
| ... | @@ -1043,6 +1049,7 @@ impl PageScene { | ... | @@ -1043,6 +1049,7 @@ impl PageScene { |
| 1043 | outline(*id, offset, primitives)?; | 1049 | outline(*id, offset, primitives)?; |
| 1044 | continue; | 1050 | continue; |
| 1045 | } | 1051 | } |
| 1052 | Content::Ink(_) if column.is_some() => continue, | ||
| 1046 | Content::Ink(ink) => { | 1053 | Content::Ink(ink) => { |
| 1047 | let [x, y] = crate::origin(&ink.layout); | 1054 | let [x, y] = crate::origin(&ink.layout); |
| 1048 | let [dx, dy] = editor.map_or([0.0; 2], |editor| editor.ink_drag_offset(ink.id)); | 1055 | let [dx, dy] = editor.map_or([0.0; 2], |editor| editor.ink_drag_offset(ink.id)); |
crates/canvas/src/interaction/accessibility.rs+4-1| ... | @@ -358,7 +358,10 @@ impl Accessibility { | ... | @@ -358,7 +358,10 @@ impl Accessibility { |
| 358 | self.read_only.push((id, node)); | 358 | self.read_only.push((id, node)); |
| 359 | } | 359 | } |
| 360 | if let Some(column) = column { | 360 | if let Some(column) = column { |
| 361 | for (text, bounds) in scene.header_text(editor, column) { | 361 | let note = column |
| 362 | .drawings_rect() | ||
| 363 | .map(|bounds| (crate::reading::DRAWINGS_NOTE.to_owned(), bounds)); | ||
| 364 | for (text, bounds) in scene.header_text(editor, column).chain(note) { | ||
| 362 | let index = self.read_only.len(); | 365 | let index = self.read_only.len(); |
| 363 | let mut node = Node::new(Role::Label); | 366 | let mut node = Node::new(Role::Label); |
| 364 | node.set_value(text); | 367 | node.set_value(text); |
crates/canvas/src/interaction/ink.rs+6| ... | @@ -149,6 +149,9 @@ impl PageView { | ... | @@ -149,6 +149,9 @@ impl PageView { |
| 149 | 149 | ||
| 150 | /// A press at page point `point`, when a drawing tool takes it or it picks ink. | 150 | /// A press at page point `point`, when a drawing tool takes it or it picks ink. |
| 151 | pub(super) fn ink_pressed(&mut self, point: [f32; 2]) -> Result<Option<Response>> { | 151 | pub(super) fn ink_pressed(&mut self, point: [f32; 2]) -> Result<Option<Response>> { |
| 152 | if self.column.is_some() { | ||
| 153 | return Ok(None); | ||
| 154 | } | ||
| 152 | let selected = !self.ink.selection.is_empty() | 155 | let selected = !self.ink.selection.is_empty() |
| 153 | && self | 156 | && self |
| 154 | .editor | 157 | .editor |
| ... | @@ -282,6 +285,9 @@ impl PageView { | ... | @@ -282,6 +285,9 @@ impl PageView { |
| 282 | /// Delete and Backspace delete picked drawings; Escape lets them go, then leaves a | 285 | /// Delete and Backspace delete picked drawings; Escape lets them go, then leaves a |
| 283 | /// drawing tool for Select & Type, as OneNote's Escape does. | 286 | /// drawing tool for Select & Type, as OneNote's Escape does. |
| 284 | pub(super) fn ink_key(&mut self, key: &Key) -> Result<Option<Response>> { | 287 | pub(super) fn ink_key(&mut self, key: &Key) -> Result<Option<Response>> { |
| 288 | if self.column.is_some() { | ||
| 289 | return Ok(None); | ||
| 290 | } | ||
| 285 | match key { | 291 | match key { |
| 286 | Key::Named(NamedKey::Backspace | NamedKey::Delete) | 292 | Key::Named(NamedKey::Backspace | NamedKey::Delete) |
| 287 | if !self.ink.selection.is_empty() => | 293 | if !self.ink.selection.is_empty() => |
crates/canvas/src/interaction/mod.rs+12-1| ... | @@ -1022,9 +1022,11 @@ impl PageView { | ... | @@ -1022,9 +1022,11 @@ impl PageView { |
| 1022 | .ok() | 1022 | .ok() |
| 1023 | .flatten() | 1023 | .flatten() |
| 1024 | }); | 1024 | }); |
| 1025 | // The column shows pictures only; one picked in the page view lets go. | 1025 | // The column only shows pictures and leaves drawings out; what the page view |
| 1026 | // picked lets go. | ||
| 1026 | if self.column.is_some() { | 1027 | if self.column.is_some() { |
| 1027 | self.set_object_focus(None); | 1028 | self.set_object_focus(None); |
| 1029 | self.leave_ink(); | ||
| 1028 | } | 1030 | } |
| 1029 | self.column.is_some() | 1031 | self.column.is_some() |
| 1030 | } | 1032 | } |
| ... | @@ -2985,6 +2987,15 @@ fn page_primitives<'a>( | ... | @@ -2985,6 +2987,15 @@ fn page_primitives<'a>( |
| 2985 | &mut primitives, | 2987 | &mut primitives, |
| 2986 | )?; | 2988 | )?; |
| 2987 | } | 2989 | } |
| 2990 | if let Some(([x, y], note)) = column.and_then(|column| column.drawings.as_ref()) { | ||
| 2991 | let [dx, dy] = scene.map_or([0.0; 2], |(_, offset)| *offset); | ||
| 2992 | primitives.push(Primitive::Text { | ||
| 2993 | clip: None, | ||
| 2994 | text: note, | ||
| 2995 | origin: [x + dx, y + dy], | ||
| 2996 | ink: paint.colors.paper.shade(crate::gpu::colorref(0x00767676)), | ||
| 2997 | }); | ||
| 2998 | } | ||
| 2988 | if let Some(ObjectFocus::Image(id)) = object_focus { | 2999 | if let Some(ObjectFocus::Image(id)) = object_focus { |
| 2989 | let (origin, size) = match preview { | 3000 | let (origin, size) = match preview { |
| 2990 | Some(PointerFeedback::Image(moving, origin, size)) if moving == id => (origin, size), | 3001 | Some(PointerFeedback::Image(moving, origin, size)) if moving == id => (origin, size), |
crates/canvas/src/interaction/tests.rs+124| ... | @@ -4055,6 +4055,130 @@ fn reader_shrinks_pictures_to_the_column_in_reading_order_and_leaves_them_alone( | ... | @@ -4055,6 +4055,130 @@ fn reader_shrinks_pictures_to_the_column_in_reading_order_and_leaves_them_alone( |
| 4055 | assert!(!view.set_reading(None) && view.column.is_none()); | 4055 | assert!(!view.set_reading(None) && view.column.is_none()); |
| 4056 | } | 4056 | } |
| 4057 | 4057 | ||
| 4058 | #[test] | ||
| 4059 | fn reader_leaves_drawings_out_and_ends_with_a_note_that_the_page_has_them() { | ||
| 4060 | use onestore::page::{Ink, InkStroke, PageObject, ink::ShapeKind}; | ||
| 4061 | // A mostly drawn page is a canvas, not offered. | ||
| 4062 | let drawn = scene_view( | ||
| 4063 | include_bytes!("../../../../corpus/ink-edit/drawing/candidate/ink.one"), | ||
| 4064 | "Beside the drawing", | ||
| 4065 | ); | ||
| 4066 | assert!(!drawn.reading_status(200.0).0); | ||
| 4067 | let (mut page, _) = reader_columns_page(); | ||
| 4068 | let pen = InkStroke { | ||
| 4069 | id: onestore::page::text::new_id().unwrap(), | ||
| 4070 | points: Vec::new(), | ||
| 4071 | width: 2.0, | ||
| 4072 | height: 2.0, | ||
| 4073 | color: None, | ||
| 4074 | transparency: None, | ||
| 4075 | pen_tip: None, | ||
| 4076 | raster_operation: None, | ||
| 4077 | pressure: Vec::new(), | ||
| 4078 | }; | ||
| 4079 | let ink = Ink::drawn(ShapeKind::Ellipse, [320.0, 380.0], [400.0, 420.0], &pen).unwrap(); | ||
| 4080 | let ink_id = ink.id; | ||
| 4081 | page.objects.push(PageObject::Ink(ink)); | ||
| 4082 | let mut view = reader_view(page); | ||
| 4083 | view.viewport.size = [800, 900]; | ||
| 4084 | let original = view.editor.page().unwrap(); | ||
| 4085 | let strokes = |view: &PageView| { | ||
| 4086 | view.primitives(COLORS) | ||
| 4087 | .unwrap() | ||
| 4088 | .iter() | ||
| 4089 | .filter(|primitive| { | ||
| 4090 | matches!( | ||
| 4091 | primitive, | ||
| 4092 | Primitive::Segment { .. } | Primitive::Taper { .. } | ||
| 4093 | ) | ||
| 4094 | }) | ||
| 4095 | .count() | ||
| 4096 | }; | ||
| 4097 | assert!(strokes(&view) > 0); | ||
| 4098 | // A drawing picked in the page view lets go in the column. | ||
| 4099 | let offset = view.scene.as_ref().unwrap().1; | ||
| 4100 | click(&mut view, [400.0, 400.0], Instant::now()); | ||
| 4101 | assert_eq!(view.ink_selection(), [ink_id]); | ||
| 4102 | assert!(view.set_reading(Some(200.0))); | ||
| 4103 | assert!(view.ink_selection().is_empty()); | ||
| 4104 | assert_eq!(strokes(&view), 0); | ||
| 4105 | // Where the drawing lies on the page, the column can neither pick, move nor delete it. | ||
| 4106 | for point in [ | ||
| 4107 | [400.0, 400.0], | ||
| 4108 | [360.0, 380.0], | ||
| 4109 | [400.0 + offset[0], 400.0 + offset[1]], | ||
| 4110 | ] { | ||
| 4111 | let _ = view.pointer_moved(point).unwrap(); | ||
| 4112 | let _ = view.pointer_pressed(Instant::now()).unwrap(); | ||
| 4113 | assert!(view.ink_selection().is_empty()); | ||
| 4114 | let _ = view | ||
| 4115 | .pointer_moved([point[0] + 30.0, point[1] + 30.0]) | ||
| 4116 | .unwrap(); | ||
| 4117 | let _ = view.pointer_released().unwrap(); | ||
| 4118 | assert!(view.ink_selection().is_empty()); | ||
| 4119 | } | ||
| 4120 | let _ = view.key(&Key::Named(NamedKey::Delete), None).unwrap(); | ||
| 4121 | let drawing = |page: &Page| { | ||
| 4122 | page.objects | ||
| 4123 | .iter() | ||
| 4124 | .find(|object| object.id() == ink_id) | ||
| 4125 | .cloned() | ||
| 4126 | }; | ||
| 4127 | assert!(drawing(&view.editor.page().unwrap()).is_some()); | ||
| 4128 | assert_eq!(drawing(&view.editor.page().unwrap()), drawing(&original)); | ||
| 4129 | let column = view.column.as_ref().unwrap(); | ||
| 4130 | let note = column.drawings_rect().unwrap(); | ||
| 4131 | let last = column | ||
| 4132 | .outlines | ||
| 4133 | .iter() | ||
| 4134 | .map(|(_, origin, shaped)| origin[1] + shaped.size[1]) | ||
| 4135 | .fold(f32::NEG_INFINITY, f32::max); | ||
| 4136 | assert!(note[1] >= last + 18.0); | ||
| 4137 | assert!(note[2] <= 36.0 + 200.0); | ||
| 4138 | let (_, text) = column.drawings.as_ref().unwrap(); | ||
| 4139 | assert!( | ||
| 4140 | view.primitives(COLORS) | ||
| 4141 | .unwrap() | ||
| 4142 | .iter() | ||
| 4143 | .any(|primitive| matches!( | ||
| 4144 | primitive, | ||
| 4145 | Primitive::Text { text: drawn, origin, .. } | ||
| 4146 | if painted_layout(*drawn).id() == text.id() | ||
| 4147 | && *origin == [note[0] + offset[0], note[1] + offset[1]] | ||
| 4148 | )) | ||
| 4149 | ); | ||
| 4150 | let bounds: Vec<_> = view | ||
| 4151 | .scene | ||
| 4152 | .as_ref() | ||
| 4153 | .unwrap() | ||
| 4154 | .0 | ||
| 4155 | .content_bounds(&view.editor, view.column.as_ref()) | ||
| 4156 | .map(|(rect, _)| rect) | ||
| 4157 | .collect(); | ||
| 4158 | assert_eq!( | ||
| 4159 | bounds | ||
| 4160 | .iter() | ||
| 4161 | .map(|rect| rect[3]) | ||
| 4162 | .fold(f32::NEG_INFINITY, f32::max), | ||
| 4163 | note[3] | ||
| 4164 | ); | ||
| 4165 | let mut access = accessibility::Accessibility::default(); | ||
| 4166 | let mut tree = None; | ||
| 4167 | let nodes = accessibility::tests::nodes(accessibility::tests::apply( | ||
| 4168 | &mut tree, | ||
| 4169 | access.update(&view, view.viewport, "Reader").unwrap(), | ||
| 4170 | )); | ||
| 4171 | assert!( | ||
| 4172 | nodes | ||
| 4173 | .iter() | ||
| 4174 | .any(|node| node.role() == accesskit::Role::Label | ||
| 4175 | && node.data().value() == Some(crate::reading::DRAWINGS_NOTE)) | ||
| 4176 | ); | ||
| 4177 | assert!(!view.set_reading(None)); | ||
| 4178 | assert!(strokes(&view) > 0); | ||
| 4179 | assert_eq!(drawing(&view.editor.page().unwrap()), drawing(&original)); | ||
| 4180 | } | ||
| 4181 | |||
| 4058 | #[test] | 4182 | #[test] |
| 4059 | fn reader_consumers_share_title_header_date_and_body_geometry() { | 4183 | fn reader_consumers_share_title_header_date_and_body_geometry() { |
| 4060 | let mut view = reader_view(reader_page()); | 4184 | let mut view = reader_view(reader_page()); |
crates/canvas/src/reading.rs+59-21| ... | @@ -30,10 +30,18 @@ pub(crate) struct Column { | ... | @@ -30,10 +30,18 @@ pub(crate) struct Column { |
| 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. | 31 | /// Page pictures as the column shows them, `[x0, y0, x1, y1]` in page points. |
| 32 | pub(crate) pictures: Vec<(ExGuid, [f32; 4])>, | 32 | pub(crate) pictures: Vec<(ExGuid, [f32; 4])>, |
| 33 | /// Where the note that the page has drawings, which the column leaves out, starts. | ||
| 34 | pub(crate) drawings: Option<([f32; 2], crate::layout::TextLayout)>, | ||
| 33 | } | 35 | } |
| 34 | 36 | ||
| 35 | #[cfg(feature = "gpu")] | 37 | #[cfg(feature = "gpu")] |
| 36 | impl Column { | 38 | impl Column { |
| 39 | /// The note's `[x0, y0, x1, y1]` in page points. | ||
| 40 | pub(crate) fn drawings_rect(&self) -> Option<[f32; 4]> { | ||
| 41 | let ([x, y], note) = self.drawings.as_ref()?; | ||
| 42 | Some([*x, *y, x + note.shaped.width(), y + note.height()]) | ||
| 43 | } | ||
| 44 | |||
| 37 | /// Where the column shows page picture `id`; None for one it leaves out. | 45 | /// Where the column shows page picture `id`; None for one it leaves out. |
| 38 | pub(crate) fn picture(&self, id: ExGuid) -> Option<[f32; 4]> { | 46 | pub(crate) fn picture(&self, id: ExGuid) -> Option<[f32; 4]> { |
| 39 | self.pictures | 47 | self.pictures |
| ... | @@ -109,6 +117,7 @@ impl Column { | ... | @@ -109,6 +117,7 @@ impl Column { |
| 109 | .. | 117 | .. |
| 110 | } | 118 | } |
| 111 | | Content::Image(_) | 119 | | Content::Image(_) |
| 120 | | Content::Ink(_) | ||
| 112 | ) | 121 | ) |
| 113 | }) { | 122 | }) { |
| 114 | return Ok(None); | 123 | return Ok(None); |
| ... | @@ -127,14 +136,14 @@ impl Column { | ... | @@ -127,14 +136,14 @@ impl Column { |
| 127 | .map(|outline| (outline.id, root_tops(outline))) | 136 | .map(|outline| (outline.id, root_tops(outline))) |
| 128 | .collect(); | 137 | .collect(); |
| 129 | // Emptied outlines, and one being started, keep their place while typing. | 138 | // Emptied outlines, and one being started, keep their place while typing. |
| 130 | let ordered = order( | 139 | let blocks = blocks( |
| 131 | blocks( | 140 | editor, |
| 132 | editor, | 141 | editor.visible_outlines().chain(editor.caret_outline()), |
| 133 | editor.visible_outlines().chain(editor.caret_outline()), | ||
| 134 | ), | ||
| 135 | &tops, | ||
| 136 | &mut Vec::new(), | ||
| 137 | ); | 142 | ); |
| 143 | if drawn_or_arranged(&blocks).is_some() { | ||
| 144 | return Ok(None); | ||
| 145 | } | ||
| 146 | let ordered = order(blocks, &tops, &mut Vec::new()); | ||
| 138 | let rank = |id: ExGuid| { | 147 | let rank = |id: ExGuid| { |
| 139 | ordered | 148 | ordered |
| 140 | .iter() | 149 | .iter() |
| ... | @@ -175,6 +184,7 @@ impl Column { | ... | @@ -175,6 +184,7 @@ impl Column { |
| 175 | width, | 184 | width, |
| 176 | outlines: Vec::new(), | 185 | outlines: Vec::new(), |
| 177 | pictures: Vec::new(), | 186 | pictures: Vec::new(), |
| 187 | drawings: None, | ||
| 178 | }; | 188 | }; |
| 179 | let mut top = 14.4; | 189 | let mut top = 14.4; |
| 180 | if let Some(title) = title { | 190 | if let Some(title) = title { |
| ... | @@ -212,6 +222,23 @@ impl Column { | ... | @@ -212,6 +222,23 @@ impl Column { |
| 212 | }; | 222 | }; |
| 213 | top += height + GAP; | 223 | top += height + GAP; |
| 214 | } | 224 | } |
| 225 | if editor | ||
| 226 | .objects | ||
| 227 | .iter() | ||
| 228 | .any(|object| matches!(object, Content::Ink(_))) | ||
| 229 | { | ||
| 230 | let note = engine.layout( | ||
| 231 | &onestore::page::text::Paragraph::new( | ||
| 232 | DRAWINGS_NOTE.into(), | ||
| 233 | onestore::document::Format { | ||
| 234 | font_size: Some(10.0), | ||
| 235 | ..Default::default() | ||
| 236 | }, | ||
| 237 | ), | ||
| 238 | width, | ||
| 239 | )?; | ||
| 240 | column.drawings = Some(([36.0, top], note)); | ||
| 241 | } | ||
| 215 | Ok(Some(column)) | 242 | Ok(Some(column)) |
| 216 | } | 243 | } |
| 217 | } | 244 | } |
| ... | @@ -236,7 +263,7 @@ fn column_layout( | ... | @@ -236,7 +263,7 @@ fn column_layout( |
| 236 | let mut nodes = source.document().nodes().to_vec(); | 263 | let mut nodes = source.document().nodes().to_vec(); |
| 237 | for node in &mut nodes { | 264 | for node in &mut nodes { |
| 238 | match &mut node.content { | 265 | match &mut node.content { |
| 239 | ParagraphContent::Text(_) => {} | 266 | ParagraphContent::Text(_) | ParagraphContent::Ink(_) => {} |
| 240 | ParagraphContent::Image(image) => { | 267 | ParagraphContent::Image(image) => { |
| 241 | let indent = crate::outline::indentation(node.level, &source.indents, width)?; | 268 | let indent = crate::outline::indentation(node.level, &source.indents, width)?; |
| 242 | shrink(image, width - indent); | 269 | shrink(image, width - indent); |
| ... | @@ -271,6 +298,9 @@ fn column_layout( | ... | @@ -271,6 +298,9 @@ fn column_layout( |
| 271 | Ok(fits.then_some(layout)) | 298 | Ok(fits.then_some(layout)) |
| 272 | } | 299 | } |
| 273 | 300 | ||
| 301 | /// Ends a column that leaves out the page's drawings. | ||
| 302 | pub(crate) const DRAWINGS_NOTE: &str = "This page has drawings. Turn off Reading View to see them."; | ||
| 303 | |||
| 274 | /// Points between blocks in the column: OneNote's grid row. | 304 | /// Points between blocks in the column: OneNote's grid row. |
| 275 | const GAP: f32 = 18.0; | 305 | const GAP: f32 = 18.0; |
| 276 | /// Strokes this near one another are one drawing, as handwriting is many strokes. | 306 | /// Strokes this near one another are one drawing, as handwriting is many strokes. |
| ... | @@ -390,19 +420,8 @@ pub fn read(page: &Page, column: f32, engine: &mut TextEngine) -> Result<Reading | ... | @@ -390,19 +420,8 @@ pub fn read(page: &Page, column: f32, engine: &mut TextEngine) -> Result<Reading |
| 390 | page: None, | 420 | page: None, |
| 391 | }); | 421 | }); |
| 392 | } | 422 | } |
| 393 | let area = |kind: Option<Kind>| -> f64 { | 423 | if let Some(refusal) = drawn_or_arranged(&blocks) { |
| 394 | blocks | 424 | return Ok(refused(refusal, blocks)); |
| 395 | .iter() | ||
| 396 | .filter(|b| kind.is_none_or(|kind| b.kind == kind)) | ||
| 397 | .map(|b| f64::from(b.bounds[2] - b.bounds[0]) * f64::from(b.bounds[3] - b.bounds[1])) | ||
| 398 | .sum() | ||
| 399 | }; | ||
| 400 | let total = area(None).max(1.0); | ||
| 401 | if area(Some(Kind::Drawing)) / total >= CANVAS_SHARE { | ||
| 402 | return Ok(refused(Refusal::Drawn, blocks)); | ||
| 403 | } | ||
| 404 | if area(Some(Kind::Group)) / total >= CANVAS_SHARE { | ||
| 405 | return Ok(refused(Refusal::Arranged, blocks)); | ||
| 406 | } | 425 | } |
| 407 | let mut kept = Vec::new(); | 426 | let mut kept = Vec::new(); |
| 408 | if blocks.iter().any(|b| b.kind == Kind::Group) { | 427 | if blocks.iter().any(|b| b.kind == Kind::Group) { |
| ... | @@ -438,6 +457,25 @@ pub fn read(page: &Page, column: f32, engine: &mut TextEngine) -> Result<Reading | ... | @@ -438,6 +457,25 @@ pub fn read(page: &Page, column: f32, engine: &mut TextEngine) -> Result<Reading |
| 438 | }) | 457 | }) |
| 439 | } | 458 | } |
| 440 | 459 | ||
| 460 | /// Why `blocks` make a canvas rather than a document: mostly ink, or mostly overlapping. | ||
| 461 | fn drawn_or_arranged(blocks: &[Block]) -> Option<Refusal> { | ||
| 462 | let area = |kind: Option<Kind>| -> f64 { | ||
| 463 | blocks | ||
| 464 | .iter() | ||
| 465 | .filter(|b| kind.is_none_or(|kind| b.kind == kind)) | ||
| 466 | .map(|b| f64::from(b.bounds[2] - b.bounds[0]) * f64::from(b.bounds[3] - b.bounds[1])) | ||
| 467 | .sum() | ||
| 468 | }; | ||
| 469 | let total = area(None).max(1.0); | ||
| 470 | if area(Some(Kind::Drawing)) / total >= CANVAS_SHARE { | ||
| 471 | Some(Refusal::Drawn) | ||
| 472 | } else if area(Some(Kind::Group)) / total >= CANVAS_SHARE { | ||
| 473 | Some(Refusal::Arranged) | ||
| 474 | } else { | ||
| 475 | None | ||
| 476 | } | ||
| 477 | } | ||
| 478 | |||
| 441 | /// Where `outline`'s lines, table cells and pictures lie, in page points. | 479 | /// Where `outline`'s lines, table cells and pictures lie, in page points. |
| 442 | fn marks(outline: &TextOutline) -> Vec<[f32; 4]> { | 480 | fn marks(outline: &TextOutline) -> Vec<[f32; 4]> { |
| 443 | let [x, y] = outline.origin(); | 481 | let [x, y] = outline.origin(); |
crates/mobile/src/tests.rs+83| ... | @@ -3482,6 +3482,89 @@ fn reader_shows_a_corpus_pages_picture_no_wider_than_the_column() { | ... | @@ -3482,6 +3482,89 @@ fn reader_shows_a_corpus_pages_picture_no_wider_than_the_column() { |
| 3482 | assert!(pictures[0][2] - pictures[0][0] <= 240.0 / POINT + 0.01); | 3482 | assert!(pictures[0][2] - pictures[0][0] <= 240.0 / POINT + 0.01); |
| 3483 | } | 3483 | } |
| 3484 | 3484 | ||
| 3485 | #[test] | ||
| 3486 | fn reader_leaves_a_small_drawing_out_and_refuses_a_mostly_drawn_page() { | ||
| 3487 | let reading = |view: &View| { | ||
| 3488 | let query = sb_view_reading(view, 240.0); | ||
| 3489 | let status = serde_json::from_str::<serde_json::Value>(&string(query)).unwrap(); | ||
| 3490 | unsafe { sb_string_free(query) }; | ||
| 3491 | status | ||
| 3492 | }; | ||
| 3493 | let (_directory, _section, drawn) = | ||
| 3494 | reader_corpus_view("ink-edit/drawing/candidate/ink.one", "Beside the drawing"); | ||
| 3495 | assert_eq!(reading(&drawn)["offered"], false); | ||
| 3496 | let (directory, section) = features(); | ||
| 3497 | let space = section.new_page(None, "Mon", "6:14 AM").unwrap(); | ||
| 3498 | let (page, read_only) = section.shared.page(space).unwrap(); | ||
| 3499 | let mut view = View { | ||
| 3500 | canvas: ManuallyDrop::new(Canvas::new(space, page, [720, 1280], 2.0).unwrap()), | ||
| 3501 | presentation: None, | ||
| 3502 | frame: Arc::default(), | ||
| 3503 | section: Arc::clone(&section.shared), | ||
| 3504 | read_only, | ||
| 3505 | reload_pending: false, | ||
| 3506 | local_edits: Arc::new(Mutex::new(LocalEdits::default())), | ||
| 3507 | }; | ||
| 3508 | view.canvas.set_transform(1.0, [0.0; 2]); | ||
| 3509 | assert!(sb_view_focus_title(&mut view)); | ||
| 3510 | assert!(unsafe { sb_insert(&mut view, c"Notes with a doodle".as_ptr()) }); | ||
| 3511 | assert!(unsafe { sb_insert(&mut view, c"\n".as_ptr()) }); | ||
| 3512 | assert!(unsafe { | ||
| 3513 | sb_insert( | ||
| 3514 | &mut view, | ||
| 3515 | c"A paragraph long enough to fill several lines of the column on a phone, with a small drawing beside it." | ||
| 3516 | .as_ptr(), | ||
| 3517 | ) | ||
| 3518 | }); | ||
| 3519 | sb_view_set_tool(&mut view, 1, 0, 0); | ||
| 3520 | let stroke = | ||
| 3521 | [[300.0, 400.0], [320.0, 410.0], [340.0, 400.0]].map(|[x, y]| [x * POINT, y * POINT]); | ||
| 3522 | assert!(sb_view_press(&mut view, stroke[0][0], stroke[0][1])); | ||
| 3523 | for [x, y] in &stroke[1..] { | ||
| 3524 | sb_view_drag(&mut view, *x, *y); | ||
| 3525 | } | ||
| 3526 | sb_view_release(&mut view); | ||
| 3527 | sb_view_set_tool(&mut view, 0, 0, 0); | ||
| 3528 | let strokes = |view: &View| { | ||
| 3529 | view.canvas | ||
| 3530 | .page | ||
| 3531 | .primitives(view.canvas.colors()) | ||
| 3532 | .unwrap() | ||
| 3533 | .iter() | ||
| 3534 | .filter(|primitive| { | ||
| 3535 | matches!( | ||
| 3536 | primitive, | ||
| 3537 | draw::Primitive::Segment { .. } | draw::Primitive::Taper { .. } | ||
| 3538 | ) | ||
| 3539 | }) | ||
| 3540 | .count() | ||
| 3541 | }; | ||
| 3542 | assert!(strokes(&view) > 0); | ||
| 3543 | assert_eq!(reading(&view)["offered"], true); | ||
| 3544 | let mut anchor = [0.0; 2]; | ||
| 3545 | assert!(sb_view_set_reading(&mut view, 240.0, &mut anchor)); | ||
| 3546 | assert_eq!(strokes(&view), 0); | ||
| 3547 | // A tap where the drawing lies on the page picks nothing to delete. | ||
| 3548 | assert!(sb_view_press(&mut view, stroke[1][0], stroke[1][1])); | ||
| 3549 | sb_view_release(&mut view); | ||
| 3550 | let mut rect = [0.0; 4]; | ||
| 3551 | assert!(!sb_view_ink_selection(&view, &mut rect)); | ||
| 3552 | assert!(!sb_view_set_reading(&mut view, 0.0, &mut anchor)); | ||
| 3553 | assert!(strokes(&view) > 0); | ||
| 3554 | assert!(section.flush(Duration::from_secs(20))); | ||
| 3555 | let arena = onestore::Arena::default(); | ||
| 3556 | let file = directory.path().join("Features.one"); | ||
| 3557 | let stored = onestore::Section::open(&arena, onestore::read_file(file).unwrap()).unwrap(); | ||
| 3558 | assert!( | ||
| 3559 | stored | ||
| 3560 | .page(space) | ||
| 3561 | .unwrap() | ||
| 3562 | .objects | ||
| 3563 | .iter() | ||
| 3564 | .any(|object| matches!(object, onestore::page::PageObject::Ink(_))) | ||
| 3565 | ); | ||
| 3566 | } | ||
| 3567 | |||
| 3485 | #[test] | 3568 | #[test] |
| 3486 | fn reader_local_apply_fence_tracks_overlapping_replies_and_view_lifetime() { | 3569 | fn reader_local_apply_fence_tracks_overlapping_replies_and_view_lifetime() { |
| 3487 | use std::sync::mpsc; | 3570 | use std::sync::mpsc; |