authorclover caruso <git@paperclover.net> 2026-10-15 23:28:56-07:00
committerclover caruso <git@paperclover.net> 2026-10-15 23:54:24-07:00
log8bafe305be064627aa2527be1b4c4b45efb185b0
tree1c96dbb11610539a987a1e50a577659b09835659
parentf2300b7f791d89840114e52f007f185d6c858e89
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover)

feat(mobile): leave drawings out of the Reading View

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.5

7 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 }
749755
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 {
149149
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 && self156 && self
154 .editor157 .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 a285 /// 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}
40574057
4058#[test]
4059fn 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]
4059fn reader_consumers_share_title_header_date_and_body_geometry() {4183fn 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}
3436
35#[cfg(feature = "gpu")]37#[cfg(feature = "gpu")]
36impl Column {38impl 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.pictures47 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 ordered148 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}
273300
301/// Ends a column that leaves out the page's drawings.
302pub(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.
275const GAP: f32 = 18.0;305const 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 blocks424 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}
440459
460/// Why `blocks` make a canvas rather than a document: mostly ink, or mostly overlapping.
461fn 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.
442fn marks(outline: &TextOutline) -> Vec<[f32; 4]> {480fn 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}
34843484
3485#[test]
3486fn 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]
3486fn reader_local_apply_fence_tracks_overlapping_replies_and_view_lifetime() {3569fn reader_local_apply_fence_tracks_overlapping_replies_and_view_lifetime() {
3487 use std::sync::mpsc;3570 use std::sync::mpsc;