1//! The Draw tab's tools through the page view, as a host drives them: every stroke and shape
2//! is one edit adding one drawing, the eraser and the lasso's Delete remove drawings, a lasso
3//! drag moves them, and each is one undo step. The ops seal into a section reading back as the
4//! editor's page. `CANVAS_INK_EXPORT` names a directory receiving that section as a notebook,
5//! for a cold reopen in OneNote 2010 (`corpus/ink-tools`).
6#![cfg(feature = "interaction")]
7
8use canvas::{
9 editor::{FAVORITES, Pen},
10 gpu::page::PageScene,
11 interaction::{PageView, TextColors, ink::Tool},
12 layout::TextEngine,
13};
14use draw::edit::{Key, NamedKey};
15use onestore::{
16 Arena, ExGuid, Section, Store,
17 op::{Edit, Op, PageOp},
18 page::{Page, PageObject, ink::ShapeKind},
19};
20use std::time::{Duration, Instant};
21
22const COLORS: TextColors = TextColors {
23 caret: [0.0, 0.0, 0.0, 1.0],
24 selection: [0.7, 0.8, 1.0, 1.0],
25 paper: canvas::gpu::Paper::WHITE,
26};
27
28struct Lab<'a> {
29 section: Section<'a>,
30 space: ExGuid,
31 view: PageView,
32 at: u64,
33}
34
35impl Lab<'_> {
36 /// Stores what the last gesture recorded, returning its ops.
37 fn store(&mut self) -> Vec<PageOp> {
38 let ops = self.view.editor.take_ops().unwrap();
39 self.at += 10_000_000;
40 let edit = Edit {
41 at: self.at,
42 ops: ops
43 .iter()
44 .cloned()
45 .map(|op| Op::Page {
46 space: self.space,
47 op,
48 })
49 .collect(),
50 };
51 self.section.apply("Author", &edit).unwrap();
52 assert_eq!(
53 self.section.page(self.space).unwrap(),
54 self.view.editor.page().unwrap()
55 );
56 ops
57 }
58
59 /// A press at page point `from` dragged through `through`, in a view at 1 device pixel
60 /// per point.
61 fn drag(&mut self, path: &[[f32; 2]]) {
62 let view = &mut self.view;
63 let _ = view.pointer_moved(path[0]).unwrap();
64 let _ = view.pointer_pressed(Instant::now()).unwrap();
65 for point in &path[1..] {
66 let _ = view.pointer_moved(*point).unwrap();
67 }
68 let _ = view.pointer_released().unwrap();
69 }
70
71 fn drawings(&self) -> Vec<onestore::page::Ink> {
72 self.view
73 .editor
74 .page()
75 .unwrap()
76 .objects
77 .into_iter()
78 .filter_map(|object| match object {
79 PageObject::Ink(ink) => Some(ink),
80 _ => None,
81 })
82 .collect()
83 }
84}
85
86fn open(arena: &Arena, source: Vec<u8>) -> Lab<'_> {
87 let mut section = Section::open(arena, source).unwrap();
88 let (space, ..) = section.pages().unwrap()[0].clone();
89 let page: Page = section.page(space).unwrap();
90 let mut engine = TextEngine::default();
91 let (scene, editor) = PageScene::from_page(page, &mut engine).unwrap();
92 let mut view = PageView::new(
93 editor,
94 engine,
95 Some((scene, [0.0; 2])),
96 [800, 600],
97 1.0,
98 Duration::from_millis(500),
99 );
100 view.viewport.scale = 1.0;
101 view.viewport.origin = [0.0; 2];
102 Lab {
103 section,
104 space,
105 view,
106 at: 133_000_000_000_000_000,
107 }
108}
109
110fn line(from: [f32; 2], to: [f32; 2], steps: usize) -> Vec<[f32; 2]> {
111 (0..=steps)
112 .map(|step| {
113 let t = step as f32 / steps as f32;
114 [
115 from[0] + (to[0] - from[0]) * t,
116 from[1] + (to[1] - from[1]) * t,
117 ]
118 })
119 .collect()
120}
121
122#[test]
123fn drawing_tools_store_each_stroke_and_undo_it_in_one_step() {
124 let source = onestore::create_section("ink.one", "Drawn in Snowbound", "Author").unwrap();
125 let arena = Arena::default();
126 let mut lab = open(&arena, source.clone());
127 let accent = Pen::new(35.0, Some(0x7a9a1f));
128
129 let _ = lab.view.set_tool(Tool::Pen(accent));
130 let wave: Vec<[f32; 2]> = (0..=40)
131 .map(|step| {
132 let x = 60.0 + step as f32 * 4.0;
133 [x, 150.0 + 12.0 * (x / 18.0).sin()]
134 })
135 .collect();
136 lab.drag(&wave);
137 let ops = lab.store();
138 let [
139 PageOp::Add {
140 object: PageObject::Ink(ink),
141 ..
142 },
143 ] = ops.as_slice()
144 else {
145 panic!("{ops:?}")
146 };
147 assert_eq!((ink.strokes.len(), ink.strokes[0].points.len()), (1, 41));
148 assert_eq!(ink.strokes[0].color, Some(0x7a9a1f));
149
150 let _ = lab.view.set_tool(Tool::Pen(FAVORITES[5]));
151 lab.drag(&line([60.0, 190.0], [220.0, 190.0], 20));
152 lab.store();
153 for (kind, from, to) in [
154 (ShapeKind::Rectangle, [250.0, 140.0], [326.0, 196.0]),
155 (ShapeKind::Ellipse, [340.0, 140.0], [430.0, 196.0]),
156 (ShapeKind::Arrow, [250.0, 230.0], [358.0, 266.0]),
157 (ShapeKind::Line, [378.0, 230.0], [450.0, 232.0]),
158 ] {
159 let _ = lab
160 .view
161 .set_tool(Tool::Shape(kind, Pen::new(50.0, Some(0x7a9a1f))));
162 lab.drag(&line(from, to, 8));
163 lab.store();
164 }
165 let drawn = lab.drawings();
166 assert_eq!(drawn.len(), 6);
167 // Shapes take their corners from the grid, anchored at the margin origin.
168 let rectangle = &drawn[2];
169 assert_eq!(
170 (
171 rectangle.strokes[0].points[0],
172 rectangle.strokes[0].points[2]
173 ),
174 (
175 [252.0, 140.4].map(onestore::page::ink::snap),
176 [324.0, 194.4].map(onestore::page::ink::snap)
177 )
178 );
179 assert_eq!(
180 drawn[4].strokes.len(),
181 2,
182 "an arrow's head is a stroke of its own"
183 );
184 let highlighter = drawn[1].strokes[0].clone();
185 assert_eq!(
186 (
187 highlighter.raster_operation,
188 highlighter.pen_tip,
189 highlighter.transparency
190 ),
191 (Some(9), Some(1), Some(127))
192 );
193 // A highlighter multiplies what lies beneath.
194 let primitives = lab.view.primitives(COLORS).unwrap();
195 assert!(
196 primitives
197 .iter()
198 .any(|primitive| matches!(primitive, draw::Primitive::Highlight { .. }))
199 );
200 if let Some(directory) = std::env::var_os("CANVAS_INK_EXPORT") {
201 let mut image = source.clone();
202 let mut sealed = Section::open(&arena, source.clone()).unwrap();
203 std::mem::swap(&mut sealed, &mut lab.section);
204 sealed.seal().unwrap().unwrap().apply(&mut image).unwrap();
205 let directory = std::path::PathBuf::from(directory);
206 std::fs::create_dir_all(&directory).unwrap();
207 std::fs::write(directory.join("ink.one"), &image).unwrap();
208 let file_id = Store::parse(&image).unwrap().header.file_id;
209 std::fs::write(
210 directory.join("Open Notebook.onetoc2"),
211 onestore::create_table_of_contents("Open Notebook.onetoc2", &[("ink.one", file_id)])
212 .unwrap(),
213 )
214 .unwrap();
215 return;
216 }
217
218 // Undo takes the last drawing away, and only that.
219 let arrow = drawn[4].id;
220 let line_id = drawn[5].id;
221 let _ = lab.view.undo(false).unwrap();
222 assert_eq!(lab.store(), [PageOp::Delete { object: line_id }]);
223 let _ = lab.view.undo(true).unwrap();
224 lab.store();
225 assert_eq!(lab.drawings().len(), 6);
226
227 // The eraser takes the wave it crosses, and the arrow's two strokes with it at a touch.
228 let _ = lab.view.set_tool(Tool::Eraser);
229 lab.drag(&[
230 [100.0, 120.0],
231 [100.0, 160.0],
232 [240.0, 160.0],
233 [240.0, 215.0],
234 [320.0, 250.0],
235 ]);
236 let ops = lab.store();
237 assert_eq!(
238 ops,
239 [
240 PageOp::Delete {
241 object: drawn[0].id
242 },
243 PageOp::Delete { object: arrow },
244 ]
245 );
246 let _ = lab.view.undo(false).unwrap();
247 lab.store();
248 assert_eq!(lab.drawings().len(), 6);
249
250 // The lasso picks the highlighter, a drag moves it, Delete removes it.
251 let _ = lab.view.set_tool(Tool::Lasso);
252 lab.drag(&[
253 [50.0, 175.0],
254 [230.0, 175.0],
255 [230.0, 205.0],
256 [50.0, 205.0],
257 [50.0, 176.0],
258 ]);
259 assert_eq!(lab.view.ink_selection(), [drawn[1].id]);
260 lab.drag(&line([140.0, 190.0], [170.0, 220.0], 4));
261 let ops = lab.store();
262 assert!(matches!(
263 ops.as_slice(),
264 [PageOp::Outline { object, .. }] if *object == drawn[1].id
265 ));
266 let moved = &lab.drawings()[1];
267 assert_eq!((moved.layout.x, moved.layout.y), (Some(30.0), Some(30.0)));
268 let _ = lab.view.key(&Key::Named(NamedKey::Delete), None).unwrap();
269 assert_eq!(
270 lab.store(),
271 [PageOp::Delete {
272 object: drawn[1].id
273 }]
274 );
275 let _ = lab.view.undo(false).unwrap();
276 lab.store();
277 let _ = lab.view.undo(false).unwrap();
278 lab.store();
279 assert_eq!(lab.drawings()[1].layout.x, Some(0.0));
280
281 // Escape leaves the drawing tools for Select & Type, where a click picks a drawing.
282 let _ = lab.view.key(&Key::Named(NamedKey::Escape), None).unwrap();
283 assert_eq!(lab.view.tool(), Tool::Select);
284 lab.drag(&[[400.0, 231.0]]);
285 assert!(lab.view.editor.take_ops().unwrap().is_empty());
286 assert_eq!(lab.view.ink_selection(), [drawn[5].id]);
287}
288
289/// A pen reporting pressure stores it at each point, following the pen between reports; a
290/// mouse's stroke after it keeps the pen's width, as OneNote stores one (`corpus/ink-pressure`).
291#[test]
292fn a_pressure_pen_stores_its_pressure_and_a_mouse_its_width() {
293 let source = onestore::create_section("ink.one", "Pressure", "Author").unwrap();
294 let arena = Arena::default();
295 let mut lab = open(&arena, source);
296 let _ = lab.view.set_tool(Tool::Pen(FAVORITES[0]));
297 let path = line([60.0, 150.0], [220.0, 150.0], 4);
298 let view = &mut lab.view;
299 view.set_pressure(Some(0.2));
300 let _ = view.pointer_moved(path[0]).unwrap();
301 let _ = view.pointer_pressed(Instant::now()).unwrap();
302 for (point, pressure) in path[1..]
303 .iter()
304 .zip([Some(0.4), None, Some(1.0), Some(0.9)])
305 {
306 if pressure.is_some() {
307 view.set_pressure(pressure);
308 }
309 let _ = view.pointer_moved(*point).unwrap();
310 }
311 let _ = view.pointer_released().unwrap();
312 lab.store();
313 lab.view.set_pressure(None);
314 lab.drag(&line([60.0, 200.0], [220.0, 200.0], 4));
315 lab.store();
316 let drawn = lab.drawings();
317 let level = onestore::page::ink::level;
318 assert_eq!(
319 drawn[0].strokes[0].pressure,
320 [0.2, 0.4, 0.4, 1.0, 0.9].map(level)
321 );
322 assert!(drawn[1].strokes[0].pressure.is_empty());
323}