1//! How a page responds to pointer, keyboard and text input as OneNote does: hit layers, drags,
2//! the placement grid, picture handles, outline and picture chrome, and key routing. Hosts
3//! translate their platform's events into these calls and carry out the returned requests.
4
5pub mod accessibility;
6pub mod ink;
7#[cfg(test)]
8mod profile;
9mod scroll;
10mod space;
11
12pub use scroll::Scroll;
13#[cfg(test)]
14mod tests;
15
16use crate::gpu::{Paper, Viewport, page::PageScene};
17use crate::{
18 date::DateField,
19 editor::{
20 Awaited, CanvasEditor, Clip, DEFAULT_OUTLINE_WIDTH, Formatting, Piece, Selection,
21 TextOutline, Whole,
22 },
23 layout::TextEngine,
24};
25use draw::{
26 Primitive, Stroke,
27 edit::{self, Clicks, Command, Key, Modifiers, Movement, NamedKey, SelectionUnit},
28};
29use std::{error::Error, time::Duration};
30use web_time::Instant;
31
32const HANDLE_HEIGHT: f32 = 6.75;
33/// Accessible names of the page date's fields.
34pub const DATE_LABELS: [&str; 2] = ["Page date", "Page time"];
35
36pub(crate) type Result<T> = std::result::Result<T, Box<dyn Error>>;
37
38#[derive(Clone, Copy, Debug, PartialEq)]
39pub enum Cursor {
40 Default,
41 Text,
42 Pointer,
43 Move,
44 EwResize,
45 NsResize,
46 NwseResize,
47 NeswResize,
48 /// Insert Space's line across the page.
49 RowResize,
50 /// Insert Space's line down the page.
51 ColResize,
52 /// A drawing tool's.
53 Crosshair,
54}
55
56/// Work only the host can do, asked for by an event.
57#[derive(Debug, PartialEq)]
58pub enum Request {
59 /// Show the date or time picker, then report the choice through `PageView::change_date`.
60 EditDate(DateField),
61 /// Put this on the clipboard.
62 Copy(Clip),
63 /// Read the clipboard and hand its text to `PageView::commit_text`.
64 Paste,
65 /// Open a link's address, as a click on a link does.
66 OpenLink(String),
67 /// Open a copy of a file with the system's application for it, as a double click does.
68 OpenAttachment(onestore::page::Attachment),
69 /// Play recording `file` from `at_ms`, as a note's play button does.
70 Play {
71 file: onestore::page::Attachment,
72 at_ms: u32,
73 },
74}
75
76/// What a secondary press lands on, which its context menu offers commands for.
77#[derive(Debug, Default, PartialEq)]
78pub struct Context {
79 /// The address of the link at the caret.
80 pub link: Option<String>,
81 pub equation: bool,
82 /// Whether text or a picture is selected, which Cut and Copy take.
83 pub selected: bool,
84 /// The paragraph at the caret, which Copy Link to Paragraph names.
85 pub paragraph: Option<onestore::ExGuid>,
86 /// The file the press selected, which Open and Save As take.
87 pub attachment: Option<onestore::page::Attachment>,
88 pub image: Option<onestore::page::Image>,
89 /// The marked word the press landed on, which the menu offers corrections for.
90 pub spelling: Option<Correction>,
91}
92
93/// A marked word under a context menu, and what it could become.
94#[derive(Debug, PartialEq)]
95pub struct Correction {
96 pub word: String,
97 /// Replacements, best first; none for a repeated word, which Delete Repeated Word
98 /// removes with the space before it.
99 pub suggestions: Vec<String>,
100 pub repeated: bool,
101 outline: onestore::ExGuid,
102 /// What a correction replaces: the word, or a repeated word and the space before it.
103 range: Selection,
104}
105
106/// What an event did: `changed` means the page or selection changed (the host saves,
107/// updates accessibility and redraws); `moved` that only the view scrolled or zoomed (the
108/// host updates accessibility and redraws); `redraw` alone repaints hover feedback.
109#[must_use]
110#[derive(Debug, Default, PartialEq)]
111pub struct Response {
112 pub changed: bool,
113 pub moved: bool,
114 pub(crate) redraw: bool,
115 pub request: Option<Request>,
116}
117
118impl Response {
119 fn changed() -> Self {
120 Self {
121 changed: true,
122 redraw: true,
123 ..Self::default()
124 }
125 }
126
127 fn redraw() -> Self {
128 Self {
129 redraw: true,
130 ..Self::default()
131 }
132 }
133
134 fn request(request: Request) -> Self {
135 Self {
136 request: Some(request),
137 ..Self::changed()
138 }
139 }
140}
141
142/// The platform's caret and text selection colours, linear RGBA, and the page's paper.
143#[derive(Clone, Copy, Debug)]
144pub struct TextColors {
145 pub caret: [f32; 4],
146 pub selection: [f32; 4],
147 pub paper: Paper,
148}
149
150/// How to paint an outline: the caret's opacity, the view scale, document points per
151/// logical pixel, and the text colours.
152#[derive(Clone, Copy)]
153struct Paint<'a> {
154 caret: f32,
155 scale: f32,
156 /// The device pixel the document origin lands on, for marks drawn on the pixel grid.
157 device_origin: [f32; 2],
158 pixel: f32,
159 colors: TextColors,
160 /// The document's top and bottom the view shows; paragraphs wholly outside it are
161 /// not painted.
162 visible: [f32; 2],
163 /// Whether the active outline shows its frame and grips.
164 chrome: bool,
165 /// Search matches, marked as OneNote marks them.
166 found: &'a [crate::search::PageMatch],
167 /// The note See Playback highlights.
168 played: Option<&'a crate::search::PageMatch>,
169 spelling: Option<&'a crate::spelling::Spelling>,
170 tag_art: &'a crate::gpu::TagArt,
171}
172
173enum Drag {
174 Text {
175 anchor: Selection,
176 unit: SelectionUnit,
177 },
178 Resize {
179 outline: Option<Box<TextOutline>>,
180 grab: f32,
181 },
182 /// A table column's right border from document x `press`, where the column was `width`
183 /// wide; the preview holds the width it has reached.
184 Column {
185 table: onestore::ExGuid,
186 column: usize,
187 width: f32,
188 press: f32,
189 preview: Option<(f32, Box<TextOutline>)>,
190 },
191 Outline {
192 id: onestore::ExGuid,
193 grab: [f32; 2],
194 pending_press: Option<[f32; 2]>,
195 },
196 Image {
197 id: onestore::ExGuid,
198 handle: [i8; 2],
199 /// Document point of the press.
200 press: [f32; 2],
201 pending_press: Option<[f32; 2]>,
202 },
203 /// Insert Space from a press on `line` along `axis`, 0 for x.
204 Space { axis: usize, line: f32 },
205}
206
207#[derive(Clone, Copy)]
208enum PointerFeedback<'a> {
209 Hover(onestore::ExGuid),
210 /// An outline dragged this far, with the rest of the page selection it belongs to.
211 Move(onestore::ExGuid, [f32; 2]),
212 Resize(&'a TextOutline),
213 /// A picture being moved or resized, drawn at this origin and size.
214 Image(onestore::ExGuid, [f32; 2], [f32; 2]),
215}
216
217/// A non-text object holding focus, which hides the text caret and suspends typing.
218#[derive(Clone, Copy, Debug, PartialEq)]
219pub enum ObjectFocus {
220 ReadOnly(usize),
221 Image(onestore::ExGuid),
222 /// A file in an outline's flow or on the page.
223 File(onestore::ExGuid),
224}
225
226impl ObjectFocus {
227 pub fn read_only(self) -> Option<usize> {
228 match self {
229 Self::ReadOnly(index) => Some(index),
230 Self::Image(_) | Self::File(_) => None,
231 }
232 }
233}
234
235fn read_only_shortcut(key: &Key, modifiers: Modifiers) -> bool {
236 key == &Key::Named(NamedKey::Escape) || (modifiers.control && key == &Key::Named(NamedKey::Tab))
237}
238
239/// Where a page was left: its scroll and the focused outline's selection.
240#[derive(Clone, Copy, Debug)]
241pub struct Place {
242 origin: [f32; 2],
243 outline: onestore::ExGuid,
244 selection: Selection,
245}
246
247/// A page on screen: the editor, the drawn scene, the view onto them and the pointer and
248/// keyboard state between events.
249pub struct PageView {
250 pub editor: CanvasEditor,
251 pub(crate) column: Option<crate::reading::Column>,
252 pub engine: TextEngine,
253 /// The page's noneditable content and where it sits in the view.
254 pub scene: Option<(PageScene, [f32; 2])>,
255 pub viewport: Viewport,
256 /// Device pixels per logical pixel.
257 display_scale: f32,
258 clicks: Clicks,
259 pointer: [f32; 2],
260 pointer_inside: bool,
261 drag: Option<Drag>,
262 /// Insert Space waits for its press: its line follows the pointer.
263 space: bool,
264 object_focus: Option<ObjectFocus>,
265 modifiers: Modifiers,
266 focused: bool,
267 /// Input is by finger: the focused outline's grips reach further, and outline chrome
268 /// shows only while the view is focused, as there is no hover to reveal it.
269 pub touch: bool,
270 /// A platform scroll view owns the viewport: changes neither clamp it nor reveal the
271 /// caret, which the host does knowing its bars and keyboard.
272 pub host_viewport: bool,
273 /// OneNote's Snap to Grid: clicks, drags and shapes land on the placement grid unless
274 /// Option (Alt) is held. Off, they land where the pointer is.
275 pub snap_to_grid: bool,
276 /// Matches of the search shown on the page, marked under their text.
277 pub found: Vec<crate::search::PageMatch>,
278 /// The note playing, highlighted as See Playback highlights it.
279 played: Option<crate::search::PageMatch>,
280 /// Marks misspelled and repeated words; none leaves words unmarked.
281 pub spelling: Option<crate::spelling::Spelling>,
282 /// The art the page's notebook draws its tags with.
283 pub tag_art: std::sync::Arc<crate::gpu::TagArt>,
284 /// The caret's opacity in its blink.
285 caret: f32,
286 /// When the caret last moved, which restarts its blink.
287 blink_from: Instant,
288 ink: ink::State,
289 /// What the view showed before each zoom since the page opened, in document points:
290 /// scrolling still reaches it, so a zoom keeps the point under the pointer in place.
291 reach: Option<[f32; 4]>,
292}
293
294/// Room, in OneNote pixels, the view leaves beyond content it scrolls to.
295const PAD: f32 = 11.0;
296
297/// Where a page's origin sits in a view that has not scrolled, in device pixels.
298fn home(display_scale: f32) -> [f32; 2] {
299 [48.0 * display_scale; 2]
300}
301
302/// Brings `scene`, drawn at `offset` in the view, to what `viewport` shows.
303fn update_pictures(
304 scene: &mut PageScene,
305 [x, y]: [f32; 2],
306 editor: &CanvasEditor,
307 column: Option<&crate::reading::Column>,
308 viewport: Viewport,
309 paper: Paper,
310 waker: &std::task::Waker,
311) -> bool {
312 let [x0, y0] = viewport.document_point([0.0; 2]);
313 let [x1, y1] = viewport.document_point(viewport.size.map(|side| side as f32));
314 let view = [x0 - x, y0 - y, x1 - x, y1 - y];
315 scene.update_pictures(Some(editor), column, view, viewport.scale, paper, waker)
316}
317
318impl PageView {
319 /// `size` is in device pixels; `double_click` is the platform's double-click interval.
320 pub fn new(
321 editor: CanvasEditor,
322 engine: TextEngine,
323 scene: Option<(PageScene, [f32; 2])>,
324 size: [u32; 2],
325 display_scale: f32,
326 double_click: Duration,
327 ) -> Self {
328 let mut view = Self {
329 editor,
330 column: None,
331 engine,
332 scene,
333 viewport: Viewport {
334 size,
335 scale: display_scale * 96.0 / 72.0,
336 origin: home(display_scale),
337 },
338 display_scale,
339 clicks: Clicks::new(double_click),
340 pointer: [0.0; 2],
341 pointer_inside: false,
342 drag: None,
343 space: false,
344 object_focus: None,
345 modifiers: Modifiers::default(),
346 focused: true,
347 touch: false,
348 host_viewport: false,
349 snap_to_grid: true,
350 found: Vec::new(),
351 played: None,
352 spelling: None,
353 tag_art: Default::default(),
354 caret: 1.0,
355 blink_from: Instant::now(),
356 ink: Default::default(),
357 reach: None,
358 };
359 view.place_opened();
360 view
361 }
362
363 /// Shows a page reloaded from storage in place of the edited one, whose editor it returns.
364 pub(crate) fn replace(
365 &mut self,
366 mut editor: CanvasEditor,
367 scene: Option<(PageScene, [f32; 2])>,
368 ) -> CanvasEditor {
369 editor.default_font = std::mem::take(&mut self.editor.default_font);
370 editor.markdown = self.editor.markdown.take();
371 let left = std::mem::replace(&mut self.editor, editor);
372 self.scene = scene;
373 self.column = None;
374 self.drag = None;
375 self.space = false;
376 self.object_focus = None;
377 self.found.clear();
378 self.played = None;
379 self.leave_ink();
380 left
381 }
382
383 /// Shows the stored page after a change made elsewhere in place of the one shown,
384 /// keeping the scroll, the caret and selection, and the pictures already drawn. Marked
385 /// text must be committed or cancelled first.
386 pub fn refresh(&mut self, page: onestore::page::Page) -> Result<Response> {
387 let width = self.column.take().map(|column| column.width);
388 if !self.editor.refresh(page, &mut self.engine)? {
389 self.set_reading(width);
390 return Ok(Response::default());
391 }
392 if let Some((scene, _)) = &mut self.scene {
393 scene.refresh(&mut self.editor, &mut self.engine)?;
394 }
395 self.drag = None;
396 self.object_focus = None;
397 self.refresh_ink();
398 self.set_reading(width);
399 self.moved()
400 }
401
402 /// Lays the page out again where it stands, as after fonts arrived.
403 pub fn relayout(&mut self) -> Result<Response> {
404 let width = self.column.take().map(|column| column.width);
405 self.editor.relayout(&mut self.engine)?;
406 self.set_reading(width);
407 self.moved()
408 }
409
410 /// Shows another page as OneNote opens one, keeping the zoom: at `place` if the page was
411 /// left there earlier. OneNote keeps the scroll in device pixels across zoom changes and
412 /// does not reveal the restored selection. Returns the editor of the page left.
413 pub fn open(
414 &mut self,
415 editor: CanvasEditor,
416 scene: Option<(PageScene, [f32; 2])>,
417 place: Option<Place>,
418 ) -> CanvasEditor {
419 let left = self.replace(editor, scene);
420 self.reach = None;
421 match place {
422 Some(place) => {
423 self.viewport.origin = place.origin;
424 self.scroll().clamp(&mut self.viewport);
425 if self.editor.focus_outline(place.outline).is_ok() {
426 let _ = self.editor.select(place.selection);
427 }
428 }
429 None => self.place_opened(),
430 }
431 left
432 }
433
434 /// Takes up `parked`, the editor the page `open` just showed was left with, history and
435 /// all, in place of the one it opened with.
436 pub fn resume(&mut self, parked: CanvasEditor) -> Result<()> {
437 let width = self.column.take().map(|column| column.width);
438 self.editor.resume(parked, &mut self.engine)?;
439 if let Some((scene, _)) = &mut self.scene {
440 scene.refresh(&mut self.editor, &mut self.engine)?;
441 }
442 self.set_reading(width);
443 Ok(())
444 }
445
446 /// Where the page is left, for `open` to return to.
447 pub fn place(&self) -> Place {
448 Place {
449 origin: self.viewport.origin,
450 outline: self.editor.active_outline().id,
451 selection: self.editor.selection(),
452 }
453 }
454
455 /// OneNote 2010 opens a page scrolled fully up, however far down its content lies, and
456 /// fully left, or right on a right-to-left page.
457 fn place_opened(&mut self) {
458 let scroll = self.scroll();
459 let x = if self.editor.rtl() {
460 scroll.max[0]
461 } else {
462 scroll.min[0]
463 };
464 self.viewport.origin = [-x, -scroll.min[1]];
465 }
466
467 pub fn modifiers(&self) -> Modifiers {
468 self.modifiers
469 }
470
471 pub fn object_focus(&self) -> Option<ObjectFocus> {
472 self.object_focus
473 }
474
475 /// Starts OneNote's Insert Space: a line follows the pointer, across the page or, within
476 /// half an inch of the view's left or right edge, down it; dragging from it moves what
477 /// lies past it, and Escape cancels.
478 pub fn insert_space(&mut self) -> Response {
479 self.space = true;
480 self.drag = None;
481 Response::redraw()
482 }
483
484 /// Whether Insert Space waits for or follows a drag.
485 pub fn inserting_space(&self) -> bool {
486 self.space || matches!(self.drag, Some(Drag::Space { .. }))
487 }
488
489 /// The axis Insert Space's line moves the page along, 0 for x, while it shows.
490 fn space_axis(&self) -> Option<usize> {
491 if let Some(Drag::Space { axis, .. }) = self.drag {
492 return Some(axis);
493 }
494 let reach = 48.0 * self.display_scale;
495 let x = self.pointer[0];
496 self.space.then_some(usize::from(
497 x >= reach && x <= self.viewport.size[0] as f32 - reach,
498 ))
499 }
500
501 /// Text input goes to the editor unless an object holds focus.
502 pub fn accepts_text(&self) -> bool {
503 self.object_focus.is_none()
504 }
505
506 /// Edits from the toolbar and menus wait while an input method composes or an object holds
507 /// focus.
508 fn edits_wait(&self) -> bool {
509 !self.accepts_text() || self.editor.marked_range().is_some()
510 }
511
512 /// The outline being dragged and where it would land.
513 pub fn outline_preview(&self) -> Option<(onestore::ExGuid, [f32; 2])> {
514 let Drag::Outline {
515 id,
516 grab,
517 pending_press,
518 } = self.drag.as_ref()?
519 else {
520 return None;
521 };
522 let origin = self
523 .editor
524 .outlines()
525 .iter()
526 .find(|outline| outline.id == *id)?
527 .origin();
528 if pending_press.is_some() {
529 return Some((*id, origin));
530 }
531 let point = self.viewport.document_point(self.pointer);
532 let position = [point[0] - grab[0], point[1] - grab[1]];
533 Some((*id, self.grid(position)))
534 }
535
536 /// The dragged outline and how far it would move.
537 fn drag_delta(&self) -> Option<(onestore::ExGuid, [f32; 2])> {
538 let (id, [x, y]) = self.outline_preview()?;
539 let outline = self
540 .editor
541 .outlines()
542 .iter()
543 .find(|outline| outline.id == id)?;
544 let [from_x, from_y] = outline.origin();
545 Some((id, [x - from_x, y - from_y]))
546 }
547
548 fn image_preview(&self) -> Option<(onestore::ExGuid, [f32; 2], [f32; 2])> {
549 let Drag::Image {
550 id,
551 handle,
552 press,
553 pending_press: None,
554 } = self.drag.as_ref()?
555 else {
556 return None;
557 };
558 let (origin, size) = self.editor.image_placement(*id)?;
559 let point = self.viewport.document_point(self.pointer);
560 let delta = [point[0] - press[0], point[1] - press[1]];
561 if *handle != [0, 0] {
562 let (origin, size) = resize_image(origin, size, *handle, delta);
563 return Some((*id, origin, size));
564 }
565 // A picture in an outline's flow keeps its place.
566 if self.editor.image_in_outline(*id) {
567 return Some((*id, origin, size));
568 }
569 let origin = [origin[0] + delta[0], origin[1] + delta[1]];
570 Some((*id, self.grid(origin), size))
571 }
572
573 /// `point` on the placement grid, where Snap to Grid is on and Option isn't held.
574 fn grid(&self, point: [f32; 2]) -> [f32; 2] {
575 if self.snap_to_grid && !self.modifiers.option {
576 snap_to_grid(point, self.editor.margin_origin())
577 } else {
578 point
579 }
580 }
581
582 fn pixel(&self) -> f32 {
583 self.display_scale / self.viewport.scale
584 }
585
586 /// The selected picture's handles lie above the page; its body keeps its paint order.
587 fn hit_test(&self, point: [f32; 2]) -> Option<Hit> {
588 if let Some(ObjectFocus::Image(id)) = self.object_focus
589 && let Some((origin, size)) = self.editor.image_placement(id)
590 && let Some(handle) = image_handle_at(
591 image_rect(origin, size),
592 self.pixel(),
593 point,
594 if self.touch { TOUCH_REACH } else { 0.0 },
595 )
596 {
597 return Some(Hit::Image { id, handle });
598 }
599 let touched = (self.touch && self.focused && self.object_focus.is_none())
600 .then(|| self.editor.active_outline().id);
601 page_hit(
602 &self.editor,
603 self.scene.as_ref(),
604 point,
605 self.pixel(),
606 touched,
607 self.column.as_ref(),
608 )
609 }
610
611 /// What the view point `position`, in device pixels, lands on, for the host to route a
612 /// gesture before it starts.
613 pub fn hit(&self, position: [f32; 2]) -> Option<Hit> {
614 self.hit_test(self.viewport.document_point(position))
615 }
616
617 fn link_under(&self, id: onestore::ExGuid, point: [f32; 2]) -> Option<String> {
618 if self.column.is_none() {
619 return self.editor.link_under(id, point);
620 }
621 self.editor
622 .visible_outlines()
623 .find(|source| source.id == id)
624 .and_then(|source| {
625 crate::reading::geometry(source, self.column.as_ref())
626 .0
627 .link_under(point)
628 })
629 }
630
631 /// The Outlook task icon under the pointer, in view device pixels, for the host to name.
632 pub fn task_under_pointer(&self) -> Option<[f32; 4]> {
633 if !self.pointer_inside || self.drag.is_some() {
634 return None;
635 }
636 let [x, y] = self.viewport.document_point(self.pointer);
637 let view = |v: f32, axis: usize| v * self.viewport.scale + self.viewport.origin[axis];
638 self.editor.visible_outlines().find_map(|outline| {
639 let offset = match &self.scene {
640 Some((_, offset))
641 if self.column.is_some() || self.editor.has_page_outline(outline.id) =>
642 {
643 *offset
644 }
645 _ => [0.0; 2],
646 };
647 let (geometry, origin) = crate::reading::geometry(outline, self.column.as_ref());
648 let shaped = geometry.shaped;
649 let left = offset[0] + origin[0] + shaped.tag_column_offset();
650 let top = offset[1] + origin[1];
651 shaped.tags().find_map(|(_, origin, tag)| {
652 let x0 = left + origin[0];
653 let y0 = top + origin[1];
654 (matches!(tag.icon, crate::outline::TagIcon::Task { .. })
655 && (x0..=x0 + tag.size).contains(&x)
656 && (y0..=y0 + tag.size).contains(&y))
657 .then(|| {
658 let [x1, y1] = [x0 + tag.size, y0 + tag.size];
659 [view(x0, 0), view(y0, 1), view(x1, 0), view(y1, 1)]
660 })
661 })
662 })
663 }
664
665 fn set_object_focus(&mut self, focus: Option<ObjectFocus>) {
666 if self.object_focus != focus {
667 self.editor.finish_composition();
668 self.drag = None;
669 self.object_focus = focus;
670 }
671 }
672
673 /// Page coordinates of a focused object.
674 fn object_rect(&self, focus: ObjectFocus) -> [f32; 4] {
675 let rect = match focus {
676 ObjectFocus::ReadOnly(index) => self
677 .scene
678 .as_ref()
679 .unwrap()
680 .0
681 .read_only(Some(&self.editor))
682 .nth(index)
683 .unwrap()
684 .rect(),
685 ObjectFocus::Image(id) => {
686 let (origin, size) = self.editor.image_placement(id).unwrap();
687 image_rect(origin, size)
688 }
689 ObjectFocus::File(id) => self.editor.attachment_rect(id).unwrap(),
690 };
691 crate::translated(
692 rect,
693 self.scene.as_ref().map_or([0.0; 2], |(_, offset)| *offset),
694 )
695 }
696
697 /// Keeps the view in bounds and restarts the caret blink after a change.
698 fn changed(&mut self) -> Result<Response> {
699 self.reconcile_reading();
700 self.finish_change()
701 }
702
703 fn finish_change(&mut self) -> Result<Response> {
704 if !self.host_viewport {
705 self.scroll().clamp(&mut self.viewport);
706 }
707 self.caret = 1.0;
708 self.blink_from = Instant::now();
709 Ok(Response::changed())
710 }
711
712 fn moved(&mut self) -> Result<Response> {
713 if !self.host_viewport {
714 self.scroll().clamp(&mut self.viewport);
715 }
716 Ok(Response {
717 moved: true,
718 redraw: true,
719 ..Response::default()
720 })
721 }
722
723 /// The caret's rectangle in device pixels, for placing the platform's input method.
724 pub fn caret_area(&self) -> Result<[f32; 4]> {
725 let outline = self.editor.active_outline();
726 let (geometry, displayed) = self.text_geometry(outline.id)?;
727 let selection = self.editor.selection();
728 let mut rect = geometry.caret_at(selection.positions[1], selection.affinities[1], 1.0)?;
729 let origin = self
730 .outline_preview()
731 .map(|(_, origin)| origin)
732 .unwrap_or(displayed);
733 rect.x0 += f64::from(origin[0]);
734 rect.y0 += f64::from(origin[1]);
735 rect.x1 += f64::from(origin[0]);
736 rect.y1 += f64::from(origin[1]);
737 let v = self.viewport;
738 let x = rect.x0 as f32 * v.scale + v.origin[0];
739 let y = rect.y0 as f32 * v.scale + v.origin[1];
740 Ok([
741 x,
742 y,
743 x + (rect.width() as f32 * v.scale).max(1.0),
744 y + (rect.height() as f32 * v.scale).max(1.0),
745 ])
746 }
747
748 fn reveal_focus(&mut self) -> Result<()> {
749 if self.viewport.size.contains(&0) {
750 return Ok(());
751 }
752 let rect = if let Some(focus) = self.object_focus {
753 let [x0, y0, x1, y1] = self.object_rect(focus).map(f64::from);
754 parley::BoundingBox { x0, y0, x1, y1 }
755 } else {
756 let outline = self.editor.active_outline();
757 let (geometry, origin) = self.text_geometry(outline.id)?;
758 let selection = self.editor.selection();
759 let mut rect =
760 geometry.caret_at(selection.positions[1], selection.affinities[1], 1.0)?;
761 rect.x0 += f64::from(origin[0]);
762 rect.x1 += f64::from(origin[0]);
763 rect.y0 += f64::from(origin[1]);
764 rect.y1 += f64::from(origin[1]);
765 rect
766 };
767 let outline = (self.column.is_none() && self.object_focus.is_none())
768 .then(|| self.editor.active_outline().bounds());
769 self.reveal(rect, outline);
770 Ok(())
771 }
772
773 /// Scrolls document `rect` into view, and with it all of `outline` where that fits.
774 fn reveal(&mut self, rect: parley::BoundingBox, outline: Option<parley::BoundingBox>) {
775 for (axis, (mut start, mut end)) in [(rect.x0, rect.x1), (rect.y0, rect.y1)]
776 .into_iter()
777 .enumerate()
778 {
779 let size = f64::from(self.viewport.size[axis]);
780 let margin = f64::from(16.0 * self.display_scale).min(size * 0.25);
781 if let Some(outline) = outline {
782 let (outline_start, outline_end) =
783 [(outline.x0, outline.x1), (outline.y0, outline.y1)][axis];
784 let outline_start = outline_start.min(start);
785 let outline_end = outline_end.max(end);
786 if (outline_end - outline_start) * f64::from(self.viewport.scale)
787 <= size - margin * 2.0
788 {
789 start = outline_start;
790 end = outline_end;
791 }
792 }
793 let start =
794 start * f64::from(self.viewport.scale) + f64::from(self.viewport.origin[axis]);
795 let end = end * f64::from(self.viewport.scale) + f64::from(self.viewport.origin[axis]);
796 let shift = if start < margin {
797 margin - start
798 } else if end > size - margin {
799 size - margin - end
800 } else {
801 0.0
802 };
803 self.viewport.origin[axis] += shift as f32;
804 }
805 }
806
807 /// Highlights `played`, the note playing, as See Playback does, scrolling it into view
808 /// as OneNote does when the note changes.
809 pub fn set_played(&mut self, played: Option<crate::search::PageMatch>) -> Result<Response> {
810 if played == self.played {
811 return Ok(Response::default());
812 }
813 self.played = played;
814 let outline = played.and_then(|(id, _)| {
815 self.editor
816 .outlines()
817 .iter()
818 .find(|outline| outline.id == id)
819 });
820 if let (Some(outline), Some((_, selection))) = (outline, played)
821 && !self.host_viewport
822 && !self.viewport.size.contains(&0)
823 {
824 let rect = self
825 .range_rects(outline.id, selection)?
826 .into_iter()
827 .reduce(|a, b| a.union(b));
828 if let Some(rect) = rect {
829 self.reveal(rect, None);
830 return self.moved();
831 }
832 }
833 Ok(Response::redraw())
834 }
835
836 /// Moves the caret by lines until it has gone a view's height, then scrolls as far so
837 /// it keeps its place on screen.
838 fn move_page(&mut self, up: bool, extend: bool) -> Result<()> {
839 let movement = if up { Movement::Up } else { Movement::Down };
840 if self.editor.prepare_navigation(movement, extend)? {
841 self.reconcile_reading();
842 return Ok(());
843 }
844 self.reconcile_reading();
845 let start = self.caret_area()?[1];
846 let mut caret = self.caret_area()?;
847 while (caret[1] - start).abs() < self.viewport.size[1] as f32 {
848 self.move_selection_prepared(movement, extend)?;
849 let moved = self.caret_area()?;
850 if moved == caret {
851 break;
852 }
853 caret = moved;
854 }
855 self.viewport.origin[1] -= caret[1] - start;
856 Ok(())
857 }
858
859 /// Reveals the caret or focused object after an edit, then reports the change.
860 fn edited(&mut self) -> Result<Response> {
861 self.reconcile_reading();
862 if !self.host_viewport {
863 self.reveal_focus()?;
864 }
865 self.finish_change()
866 }
867
868 /// The page's zoom, where 1 is OneNote's 100%: 96 pixels per inch.
869 pub fn zoom(&self) -> f32 {
870 self.viewport.scale / (self.display_scale * 96.0 / 72.0)
871 }
872
873 /// Zooms to `zoom` about the middle of the view, within 25% to 400%.
874 pub fn set_zoom(&mut self, zoom: f32) -> Result<Response> {
875 let middle = self.viewport.size.map(|size| size as f32 / 2.0);
876 self.zoom_about(zoom / self.zoom(), middle);
877 self.moved()
878 }
879
880 /// Scales the view by `factor` about `anchor`, in device pixels from the view's
881 /// top-left, as a trackpad pinch does about the pointer.
882 pub fn pinch(&mut self, factor: f32, anchor: [f32; 2]) -> Result<Response> {
883 self.zoom_about(factor, anchor);
884 self.moved()
885 }
886
887 /// Scales the view by `factor`, keeping the document point under `anchor` in place.
888 fn zoom_about(&mut self, factor: f32, anchor: [f32; 2]) {
889 let [x0, y0] = self.viewport.document_point([0.0; 2]);
890 let [x1, y1] = self
891 .viewport
892 .document_point(self.viewport.size.map(|size| size as f32));
893 self.reach = Some(self.reach.map_or([x0, y0, x1, y1], |reach| {
894 [
895 reach[0].min(x0),
896 reach[1].min(y0),
897 reach[2].max(x1),
898 reach[3].max(y1),
899 ]
900 }));
901 let point = self.viewport.document_point(anchor);
902 let dpr = self.display_scale;
903 self.viewport.scale = (self.viewport.scale * factor).clamp(dpr / 3.0, dpr * 16.0 / 3.0);
904 self.viewport.origin = [
905 anchor[0] - point[0] * self.viewport.scale,
906 anchor[1] - point[1] * self.viewport.scale,
907 ];
908 }
909
910 /// How far the view may scroll; the current offset is `-viewport.origin`.
911 pub fn scroll(&self) -> Scroll {
912 let editable = self
913 .editor
914 .visible_outlines()
915 .chain(self.editor.caret_outline())
916 .map(|outline| {
917 let (geometry, origin) = crate::reading::geometry(outline, self.column.as_ref());
918 let rect = if self.column.is_some() {
919 parley::BoundingBox {
920 x0: f64::from(origin[0]),
921 y0: f64::from(origin[1]),
922 x1: f64::from(origin[0] + geometry.shaped.size[0]),
923 y1: f64::from(origin[1] + geometry.shaped.size[1]),
924 }
925 } else {
926 outline.bounds()
927 };
928 let offset = self
929 .scene
930 .as_ref()
931 .filter(|_| self.column.is_some() || self.editor.has_page_outline(outline.id))
932 .map(|(_, offset)| *offset)
933 .unwrap_or([0.0; 2]);
934 let [left, top] = if self.column.is_some() {
935 [origin[0] - 7.5, origin[1] - 6.0]
936 } else {
937 reach(outline)
938 };
939 [
940 left + offset[0],
941 top + offset[1],
942 rect.x1 as f32 + offset[0],
943 rect.y1 as f32 + offset[1],
944 ]
945 });
946 let pad = PAD * self.display_scale;
947 let fixed = self.scene.iter().flat_map(|(scene, offset)| {
948 scene
949 .content_bounds(&self.editor, self.column.as_ref())
950 .map(move |(rect, padded)| {
951 (
952 crate::translated(rect, *offset),
953 if padded { pad } else { 0.0 },
954 )
955 })
956 });
957 let mut scroll = scroll::Scroll::new(
958 self.viewport,
959 editable.map(|rect| (rect, pad)).chain(fixed),
960 self.editor.rtl(),
961 );
962 if self.column.is_none()
963 && let Some(reach) = self.reach
964 {
965 for axis in 0..2 {
966 scroll.min[axis] = scroll.min[axis].min(reach[axis] * self.viewport.scale);
967 scroll.max[axis] = scroll.max[axis]
968 .max(reach[axis + 2] * self.viewport.scale - self.viewport.size[axis] as f32);
969 }
970 }
971 scroll
972 }
973
974 /// Scrolls the view's corner `offset` device pixels from the page origin along `axis`,
975 /// within the page's bounds.
976 pub fn scroll_to(&mut self, axis: usize, offset: f32) -> Result<Response> {
977 self.viewport.origin[axis] = -offset;
978 self.moved()
979 }
980
981 /// Brings the page's template backgrounds to `paper` and its pictures to the view and
982 /// zoom before a frame's primitives; `waker` is woken off the main thread when the view
983 /// should be drawn again because a raster landed.
984 pub fn update_pictures(&mut self, paper: Paper, waker: &std::task::Waker) {
985 if let Some((scene, offset)) = &mut self.scene {
986 update_pictures(
987 scene,
988 *offset,
989 &self.editor,
990 self.column.as_ref(),
991 self.viewport,
992 paper,
993 waker,
994 );
995 }
996 }
997
998 /// `update_pictures` for a page `open` is about to show at `offset`; true once the
999 /// pictures it would show at first are drawn.
1000 pub fn prepare(
1001 &self,
1002 (scene, offset): &mut (PageScene, [f32; 2]),
1003 editor: &CanvasEditor,
1004 paper: Paper,
1005 waker: &std::task::Waker,
1006 ) -> bool {
1007 let viewport = Viewport {
1008 origin: home(self.display_scale),
1009 ..self.viewport
1010 };
1011 update_pictures(scene, *offset, editor, None, viewport, paper, waker)
1012 }
1013
1014 pub fn set_reading(&mut self, width: Option<f32>) -> bool {
1015 self.column = width.and_then(|width| {
1016 crate::reading::Column::build(
1017 &self.editor,
1018 self.scene.as_ref().map(|(scene, _)| scene),
1019 width,
1020 &mut self.engine,
1021 )
1022 .ok()
1023 .flatten()
1024 });
1025 // The column only shows pictures and leaves drawings out; what the page view
1026 // picked lets go.
1027 if self.column.is_some() {
1028 self.set_object_focus(None);
1029 self.leave_ink();
1030 }
1031 self.column.is_some()
1032 }
1033
1034 pub fn reading_status(&self, width: f32) -> (bool, bool) {
1035 let offered = crate::reading::Column::build(
1036 &self.editor,
1037 self.scene.as_ref().map(|(scene, _)| scene),
1038 width,
1039 &mut self.engine.clone(),
1040 )
1041 .ok()
1042 .flatten()
1043 .is_some();
1044 (offered, self.column.is_some())
1045 }
1046
1047 pub fn reconcile_reading(&mut self) {
1048 if let Some(column) = &self.column {
1049 self.set_reading(Some(column.width));
1050 }
1051 }
1052
1053 fn text_geometry(
1054 &self,
1055 id: onestore::ExGuid,
1056 ) -> Result<(crate::editor::OutlineGeometry<'_>, [f32; 2])> {
1057 let source = self
1058 .editor
1059 .visible_outlines()
1060 .chain(self.editor.caret_outline())
1061 .find(|outline| outline.id == id)
1062 .ok_or(onestore::page::text::EditError::InvalidRange)?;
1063 let (geometry, mut origin) = crate::reading::geometry(source, self.column.as_ref());
1064 if (self.column.is_some() || self.editor.has_page_outline(id))
1065 && let Some((_, offset)) = &self.scene
1066 {
1067 origin = [origin[0] + offset[0], origin[1] + offset[1]];
1068 }
1069 Ok((geometry, origin))
1070 }
1071
1072 pub fn caret_at(
1073 &self,
1074 id: onestore::ExGuid,
1075 position: crate::document::TextPosition,
1076 affinity: parley::Affinity,
1077 width: f32,
1078 ) -> Result<parley::BoundingBox> {
1079 let (geometry, origin) = self.text_geometry(id)?;
1080 let rect = geometry.caret_at(position, affinity, width)?;
1081 Ok(parley::BoundingBox {
1082 x0: rect.x0 + f64::from(origin[0]),
1083 x1: rect.x1 + f64::from(origin[0]),
1084 y0: rect.y0 + f64::from(origin[1]),
1085 y1: rect.y1 + f64::from(origin[1]),
1086 })
1087 }
1088
1089 pub fn range_rects(
1090 &self,
1091 id: onestore::ExGuid,
1092 selection: Selection,
1093 ) -> Result<Vec<parley::BoundingBox>> {
1094 let (geometry, origin) = self.text_geometry(id)?;
1095 Ok(geometry
1096 .range_rects(selection)?
1097 .into_iter()
1098 .map(|rect| parley::BoundingBox {
1099 x0: rect.x0 + f64::from(origin[0]),
1100 x1: rect.x1 + f64::from(origin[0]),
1101 y0: rect.y0 + f64::from(origin[1]),
1102 y1: rect.y1 + f64::from(origin[1]),
1103 })
1104 .collect())
1105 }
1106
1107 pub fn selection_at(
1108 &self,
1109 id: onestore::ExGuid,
1110 point: [f32; 2],
1111 unit: SelectionUnit,
1112 ) -> Result<Selection> {
1113 let (geometry, origin) = self.text_geometry(id)?;
1114 Ok(geometry.selection_at(point[0] - origin[0], point[1] - origin[1], unit)?)
1115 }
1116
1117 pub fn text_block(&self, point: [f32; 2]) -> Option<[f32; 4]> {
1118 self.nearest_text(point, |_| true).map(|(_, rect)| rect)
1119 }
1120
1121 /// The outline `keep` accepts nearest `point`, with its frame as shown.
1122 fn nearest_text(
1123 &self,
1124 point: [f32; 2],
1125 keep: impl Fn(&TextOutline) -> bool,
1126 ) -> Option<(onestore::ExGuid, [f32; 4])> {
1127 self.editor
1128 .visible_outlines()
1129 .chain(
1130 self.editor
1131 .caret_outline()
1132 .filter(|_| self.column.is_some()),
1133 )
1134 .filter(|source| keep(source))
1135 .filter_map(|source| {
1136 let (geometry, origin) = self.text_geometry(source.id).ok()?;
1137 let rect = if self.column.is_some() {
1138 [
1139 origin[0],
1140 origin[1],
1141 origin[0] + geometry.shaped.size[0],
1142 origin[1] + geometry.shaped.size[1],
1143 ]
1144 } else {
1145 let bounds = source.bounds();
1146 [bounds.x0, bounds.y0, bounds.x1, bounds.y1].map(|value| value as f32)
1147 };
1148 let dx = (rect[0] - point[0]).max(point[0] - rect[2]).max(0.0);
1149 let dy = (rect[1] - point[1]).max(point[1] - rect[3]).max(0.0);
1150 Some((dx.hypot(dy), source.id, rect))
1151 })
1152 .min_by(|a, b| a.0.total_cmp(&b.0))
1153 .map(|(_, id, rect)| (id, rect))
1154 }
1155
1156 fn move_selection(&mut self, movement: Movement, extend: bool) -> Result<()> {
1157 if self.editor.prepare_navigation(movement, extend)? {
1158 self.reconcile_reading();
1159 return Ok(());
1160 }
1161 self.reconcile_reading();
1162 self.move_selection_prepared(movement, extend)
1163 }
1164
1165 fn move_selection_prepared(&mut self, movement: Movement, extend: bool) -> Result<()> {
1166 if let Some(column) = &self.column {
1167 let id = self.editor.active_outline().id;
1168 let (_, _, shaped) = column
1169 .outlines
1170 .iter()
1171 .find(|(source, _, _)| *source == id)
1172 .expect("active source is projected");
1173 self.editor.move_selection_in(shaped, movement, extend)?;
1174 } else {
1175 self.editor
1176 .move_selection_prepared(&mut self.engine, movement, extend)?;
1177 self.reconcile_reading();
1178 }
1179 Ok(())
1180 }
1181
1182 /// Everything to draw this frame.
1183 pub fn primitives(&self, colors: TextColors) -> Result<Vec<Primitive<'_>>> {
1184 let preview = match &self.drag {
1185 Some(
1186 Drag::Resize {
1187 outline: Some(outline),
1188 ..
1189 }
1190 | Drag::Column {
1191 preview: Some((_, outline)),
1192 ..
1193 },
1194 ) => Some(PointerFeedback::Resize(outline)),
1195 _ => self
1196 .drag_delta()
1197 .map(|(id, delta)| PointerFeedback::Move(id, delta))
1198 .or_else(|| {
1199 self.image_preview()
1200 .map(|(id, origin, size)| PointerFeedback::Image(id, origin, size))
1201 })
1202 .or_else(|| {
1203 if !self.pointer_inside || self.space {
1204 return None;
1205 }
1206 match page_hit(
1207 &self.editor,
1208 self.scene.as_ref(),
1209 self.viewport.document_point(self.pointer),
1210 self.pixel(),
1211 None,
1212 self.column.as_ref(),
1213 ) {
1214 Some(
1215 Hit::Text { id, .. } | Hit::Handle { id, .. } | Hit::Resize { id, .. },
1216 ) => Some(PointerFeedback::Hover(id)),
1217 _ => None,
1218 }
1219 }),
1220 };
1221 let [left, top] = self.viewport.document_point([0.0; 2]);
1222 let [right, bottom] = self
1223 .viewport
1224 .document_point(self.viewport.size.map(|side| side as f32));
1225 let mut primitives = rule_primitives(
1226 self.editor.rule_lines(),
1227 self.editor.margin_origin(),
1228 [left, top, right, bottom],
1229 self.pixel(),
1230 colors.paper,
1231 );
1232 primitives.extend(page_primitives(
1233 &self.editor,
1234 self.scene.as_ref(),
1235 self.column.as_ref(),
1236 preview,
1237 self.object_focus,
1238 Paint {
1239 caret: if self.focused && matches!(self.drag, None | Some(Drag::Text { .. })) {
1240 self.caret
1241 } else {
1242 0.0
1243 },
1244 scale: self.viewport.scale,
1245 device_origin: self.viewport.origin,
1246 pixel: self.pixel(),
1247 colors,
1248 visible: [0.0, self.viewport.size[1] as f32]
1249 .map(|y| (y - self.viewport.origin[1]) / self.viewport.scale),
1250 chrome: !self.touch || self.focused,
1251 found: &self.found,
1252 played: self.played.as_ref(),
1253 spelling: self.spelling.as_ref(),
1254 tag_art: &self.tag_art,
1255 },
1256 )?);
1257 if self.drag.is_none()
1258 && self.pointer_inside
1259 && !self.space
1260 && let Some((button, ..)) = self
1261 .editor
1262 .play_button(self.viewport.document_point(self.pointer))
1263 {
1264 append_play_button(button, colors.paper, &mut primitives);
1265 }
1266 if let Some(axis) = self
1267 .space_axis()
1268 .filter(|_| self.pointer_inside || self.drag.is_some())
1269 {
1270 let point = self.viewport.document_point(self.pointer)[axis];
1271 let (line, to, moved) = match self.drag {
1272 Some(Drag::Space { line, .. }) => {
1273 let delta = (point - line).max(self.editor.space_limit(axis, line));
1274 (line, line + delta, self.editor.space_preview(axis, line))
1275 }
1276 _ => (point, point, Vec::new()),
1277 };
1278 space::append(
1279 axis,
1280 [line, to],
1281 &moved,
1282 [left, top, right, bottom],
1283 self.pixel(),
1284 &mut primitives,
1285 );
1286 }
1287 self.append_ink_feedback(colors.paper.ink, &mut primitives);
1288 Ok(primitives)
1289 }
1290
1291 /// The pointer shape at the pointer's position.
1292 pub fn cursor(&self) -> Cursor {
1293 if let Some(axis) = self.space_axis() {
1294 return [Cursor::ColResize, Cursor::RowResize][axis];
1295 }
1296 let point = self.viewport.document_point(self.pointer);
1297 if self
1298 .editor
1299 .ink_extent(self.ink_selection())
1300 .is_some_and(|[x0, y0, x1, y1]| {
1301 (x0..=x1).contains(&point[0]) && (y0..=y1).contains(&point[1])
1302 })
1303 {
1304 return Cursor::Move;
1305 }
1306 if self.drawing() {
1307 return Cursor::Crosshair;
1308 }
1309 if self.drag.is_none() && self.play_button(point).is_some() {
1310 return Cursor::Pointer;
1311 }
1312 let hit = self.hit_test(point);
1313 match (&self.drag, hit) {
1314 (Some(Drag::Image { handle, .. }), _) => handle_cursor(*handle),
1315 (None, Some(Hit::Image { id, handle: [0, 0] }))
1316 if self.modifiers.command && self.editor.picture_link(id).is_some() =>
1317 {
1318 Cursor::Pointer
1319 }
1320 (None, Some(Hit::Image { handle, .. })) => handle_cursor(handle),
1321 (Some(Drag::Resize { .. }), _) | (None, Some(Hit::Resize { .. })) => Cursor::EwResize,
1322 (Some(Drag::Column { .. }), _) | (None, Some(Hit::Column { .. })) => Cursor::ColResize,
1323 (Some(Drag::Outline { .. }), _) | (None, Some(Hit::Handle { .. })) => Cursor::Move,
1324 (None, Some(Hit::Date(_))) => Cursor::Pointer,
1325 (None, Some(Hit::Text { id, point })) if self.link_under(id, point).is_some() => {
1326 Cursor::Pointer
1327 }
1328 (
1329 None,
1330 Some(Hit::ReadOnly(_) | Hit::Check { .. } | Hit::ObjectCheck(_) | Hit::File(_)),
1331 ) => Cursor::Default,
1332 _ => Cursor::Text,
1333 }
1334 }
1335
1336 /// Advances the caret blink; returns whether to repaint and when to call again.
1337 pub fn blink(&mut self, now: Instant) -> (bool, Option<Instant>) {
1338 let [anchor, focus] = self.editor.selection().positions;
1339 if !(self.focused && self.object_focus.is_none() && anchor == focus) {
1340 return (false, None);
1341 }
1342 let (caret, hold) = edit::caret_blink(now.saturating_duration_since(self.blink_from));
1343 let repaint = caret != self.caret;
1344 self.caret = caret;
1345 (repaint, hold.map(|hold| now + hold))
1346 }
1347
1348 /// `size` in device pixels. A right-to-left page keeps its right edge in place, as
1349 /// OneNote 2010's does.
1350 pub fn resized(&mut self, size: [u32; 2]) -> Result<Response> {
1351 if self.editor.rtl() {
1352 self.viewport.origin[0] += size[0] as f32 - self.viewport.size[0] as f32;
1353 }
1354 self.viewport.size = size;
1355 if size.contains(&0) {
1356 return Ok(Response::default());
1357 }
1358 self.moved()
1359 }
1360
1361 pub fn scale_factor_changed(&mut self, scale: f32) -> Result<Response> {
1362 let ratio = scale / self.display_scale;
1363 self.viewport.scale *= ratio;
1364 self.viewport.origin[0] *= ratio;
1365 self.viewport.origin[1] *= ratio;
1366 self.display_scale = scale;
1367 self.moved()
1368 }
1369
1370 pub fn focus_changed(&mut self, focused: bool) -> Result<Response> {
1371 self.focused = focused;
1372 if !focused {
1373 self.drag = None;
1374 self.space = false;
1375 self.end_ink();
1376 }
1377 self.changed()
1378 }
1379
1380 pub fn modifiers_changed(&mut self, modifiers: Modifiers) -> Result<Response> {
1381 self.modifiers = modifiers;
1382 if matches!(self.drag, Some(Drag::Outline { .. })) {
1383 return self.changed();
1384 }
1385 Ok(Response::default())
1386 }
1387
1388 pub fn pointer_left(&mut self) -> Response {
1389 self.pointer_inside = false;
1390 Response::redraw()
1391 }
1392
1393 /// `position` in device pixels from the view's top-left.
1394 pub fn pointer_moved(&mut self, position: [f32; 2]) -> Result<Response> {
1395 self.pointer_inside = true;
1396 self.pointer = position;
1397 if self.inking() {
1398 return self.ink_moved(self.viewport.document_point(position));
1399 }
1400 match &mut self.drag {
1401 Some(Drag::Text { anchor, unit }) => {
1402 let (anchor, unit) = (*anchor, *unit);
1403 let point = self.viewport.document_point(self.pointer);
1404 let target = self.selection_at(self.editor.active_outline().id, point, unit)?;
1405 let pair = |selection: Selection| {
1406 [0, 1].map(|end| (selection.positions[end], selection.affinities[end]))
1407 };
1408 let ends = edit::drag(pair(anchor), pair(target), unit, |(position, _)| position);
1409 self.editor.select(Selection {
1410 positions: ends.map(|(position, _)| position),
1411 affinities: ends.map(|(_, affinity)| affinity),
1412 })?;
1413 self.changed()
1414 }
1415 Some(Drag::Outline { pending_press, .. } | Drag::Image { pending_press, .. }) => {
1416 if pending_press.is_some_and(|press| {
1417 (0..2)
1418 .any(|axis| (position[axis] - press[axis]).abs() > 2.0 * self.display_scale)
1419 }) {
1420 *pending_press = None;
1421 }
1422 self.changed()
1423 }
1424 Some(Drag::Space { .. }) => Ok(Response::redraw()),
1425 Some(Drag::Resize { outline, grab }) => {
1426 let point = self.viewport.document_point(self.pointer);
1427 let width = (point[0] - self.editor.active_outline().origin()[0] - *grab).max(36.0);
1428 if outline.is_some()
1429 || (width - self.editor.active_outline().bounds().width() as f32).abs()
1430 * self.viewport.scale
1431 > 2.0 * self.display_scale
1432 {
1433 *outline = Some(Box::new(
1434 self.editor.preview_resize(&mut self.engine, width)?,
1435 ));
1436 return self.changed();
1437 }
1438 Ok(Response::default())
1439 }
1440 Some(Drag::Column {
1441 table,
1442 column,
1443 width,
1444 press,
1445 preview,
1446 }) => {
1447 let width = *width + self.viewport.document_point(self.pointer)[0] - *press;
1448 if preview.as_ref().is_none_or(|(shown, _)| *shown != width) {
1449 let outline =
1450 self.editor
1451 .preview_column(&mut self.engine, *table, *column, width)?;
1452 *preview = Some((width, Box::new(outline)));
1453 return self.changed();
1454 }
1455 Ok(Response::default())
1456 }
1457 None => Ok(Response::redraw()),
1458 }
1459 }
1460
1461 /// A primary-button press at the pointer's position, at `now` for click counting.
1462 pub fn pointer_pressed(&mut self, now: Instant) -> Result<Response> {
1463 let point = self.viewport.document_point(self.pointer);
1464 if let Some(axis) = self.space_axis().filter(|_| self.space) {
1465 self.space = false;
1466 self.set_object_focus(None);
1467 self.drag = Some(Drag::Space {
1468 axis,
1469 line: point[axis],
1470 });
1471 return Ok(Response::redraw());
1472 }
1473 if let Some((file, at_ms)) = self.play_button(point) {
1474 self.drag = None;
1475 return Ok(Response::request(Request::Play { file, at_ms }));
1476 }
1477 if let Some(response) = self.ink_pressed(point)? {
1478 return Ok(response);
1479 }
1480 let unit = self
1481 .clicks
1482 .press(now, self.pointer, 4.0 * self.display_scale);
1483 match self.hit_test(point) {
1484 Some(Hit::Date(field)) => {
1485 self.editor.finish_composition();
1486 self.drag = None;
1487 return Ok(Response::request(Request::EditDate(field)));
1488 }
1489 Some(Hit::ReadOnly(index)) => self.set_object_focus(Some(ObjectFocus::ReadOnly(index))),
1490 Some(Hit::Check { outline, paragraph }) => {
1491 self.set_object_focus(None);
1492 self.drag = None;
1493 self.editor
1494 .click_check(&mut self.engine, outline, paragraph)?;
1495 return self.changed();
1496 }
1497 Some(Hit::ObjectCheck(id)) => {
1498 self.set_object_focus(None);
1499 self.drag = None;
1500 self.editor.click_object_check(id)?;
1501 return self.changed();
1502 }
1503 Some(Hit::File(id)) => {
1504 self.set_object_focus(Some(ObjectFocus::File(id)));
1505 self.drag = None;
1506 // A finger plays a recording with a tap, having no play button to hover.
1507 if self.touch
1508 && let Some(file) = self.editor.attachment(id)
1509 && file.recording.is_some()
1510 {
1511 let file = file.clone();
1512 return Ok(Response::request(Request::Play { file, at_ms: 0 }));
1513 }
1514 if unit != SelectionUnit::Grapheme
1515 && let Some(file) = self.editor.attachment(id)
1516 {
1517 return Ok(Response::request(Request::OpenAttachment(file.clone())));
1518 }
1519 // A file on the page drags as a picture does.
1520 if !self.editor.image_in_outline(id) {
1521 self.drag = Some(Drag::Image {
1522 id,
1523 handle: [0, 0],
1524 press: point,
1525 pending_press: Some(self.pointer),
1526 });
1527 }
1528 }
1529 // A click selects a linked picture; Ctrl+click follows its link, as in OneNote.
1530 Some(Hit::Image { id, handle: [0, 0] })
1531 if self.modifiers.command
1532 && let Some(address) = self.editor.picture_link(id) =>
1533 {
1534 self.drag = None;
1535 return Ok(Response {
1536 request: Some(Request::OpenLink(address.to_owned())),
1537 ..Response::default()
1538 });
1539 }
1540 Some(Hit::Image { id, handle }) => {
1541 self.set_object_focus(Some(ObjectFocus::Image(id)));
1542 self.drag = Some(Drag::Image {
1543 id,
1544 handle,
1545 press: point,
1546 pending_press: Some(self.pointer),
1547 });
1548 }
1549 Some(Hit::Handle { id, grab }) => {
1550 self.set_object_focus(None);
1551 // Any outline's handle drags the whole page selection.
1552 if self.editor.whole() != Some(Whole::Page) {
1553 self.editor.focus_outline(id)?;
1554 }
1555 self.drag = Some(Drag::Outline {
1556 id,
1557 grab,
1558 pending_press: Some(self.pointer),
1559 });
1560 }
1561 Some(Hit::Resize { id, grab }) => {
1562 self.set_object_focus(None);
1563 self.editor.focus_outline(id)?;
1564 self.drag = Some(Drag::Resize {
1565 outline: None,
1566 grab,
1567 });
1568 }
1569 Some(Hit::Column {
1570 id,
1571 table,
1572 column,
1573 width,
1574 }) => {
1575 self.set_object_focus(None);
1576 self.editor.focus_outline(id)?;
1577 self.drag = Some(Drag::Column {
1578 table,
1579 column,
1580 width,
1581 press: point[0],
1582 preview: None,
1583 });
1584 }
1585 Some(Hit::Text { id, point })
1586 if unit == SelectionUnit::Grapheme
1587 && !self.modifiers.shift
1588 && let Some(address) = self.link_under(id, point) =>
1589 {
1590 self.drag = None;
1591 return Ok(Response {
1592 request: Some(Request::OpenLink(address)),
1593 ..Response::default()
1594 });
1595 }
1596 Some(Hit::Text { id, point: local }) => {
1597 let reading = self.column.is_some();
1598 let extend = self.modifiers.shift
1599 && self.object_focus.is_none()
1600 && id == self.editor.active_outline().id;
1601 self.set_object_focus(None);
1602 self.editor.focus_outline(id)?;
1603 let previous = self.editor.selection();
1604 self.reconcile_reading();
1605 let selection = if reading {
1606 self.selection_at(id, point, unit)?
1607 } else {
1608 self.editor.select_below(&mut self.engine, id, local)?;
1609 self.editor.selection_at(local[0], local[1], unit)?
1610 };
1611 let selection = if extend {
1612 Selection {
1613 positions: [previous.positions[0], selection.positions[1]],
1614 affinities: [previous.affinities[0], selection.affinities[1]],
1615 }
1616 } else {
1617 selection
1618 };
1619 self.editor.select(selection)?;
1620 self.drag = Some(Drag::Text {
1621 anchor: selection,
1622 unit,
1623 });
1624 }
1625 None => {
1626 self.place_caret(point)?;
1627 self.set_object_focus(None);
1628 self.drag = Some(Drag::Text {
1629 anchor: self.editor.selection(),
1630 unit: SelectionUnit::Grapheme,
1631 });
1632 }
1633 }
1634 self.changed()
1635 }
1636
1637 /// A fresh click places text 7 px above the pointer, on the grid.
1638 fn place_caret(&mut self, point: [f32; 2]) -> Result<()> {
1639 if self.column.is_some() {
1640 let id = self
1641 .nearest_text(point, |outline| !outline.title)
1642 .map(|(id, _)| id);
1643 if let Some(id) = id {
1644 self.editor.focus_outline(id)?;
1645 self.reconcile_reading();
1646 let selection = self.selection_at(id, point, SelectionUnit::Grapheme)?;
1647 self.editor.select(selection)?;
1648 return Ok(());
1649 }
1650 self.editor.leave_title(&mut self.engine)?;
1651 self.reconcile_reading();
1652 return Ok(());
1653 }
1654 let position = [point[0], point[1] - 7.0 * self.pixel()];
1655 let position = self.grid(position);
1656 self.editor
1657 .place_caret(&mut self.engine, position, DEFAULT_OUTLINE_WIDTH)?;
1658 Ok(())
1659 }
1660
1661 pub fn pointer_released(&mut self) -> Result<Response> {
1662 if self.inking() {
1663 return self.ink_released(self.viewport.document_point(self.pointer));
1664 }
1665 if let Some(Drag::Space { axis, line }) = self.drag {
1666 self.drag = None;
1667 let delta = self.viewport.document_point(self.pointer)[axis] - line;
1668 self.editor
1669 .insert_space(&mut self.engine, axis, line, delta)?;
1670 return self.changed();
1671 }
1672 // As in OneNote, an outline or picture dropped outside the view stays where it was.
1673 let inside =
1674 (0..2).all(|axis| (0.0..self.viewport.size[axis] as f32).contains(&self.pointer[axis]));
1675 let moving_image = matches!(self.drag, Some(Drag::Image { handle: [0, 0], .. }));
1676 let clicked = match self.drag {
1677 Some(Drag::Outline {
1678 id,
1679 pending_press: Some(_),
1680 ..
1681 }) => Some(id),
1682 _ => None,
1683 };
1684 let preview = self.outline_preview().filter(|_| inside);
1685 let delta = self.drag_delta().filter(|_| inside);
1686 if let Some((id, origin, size)) = self.image_preview().filter(|_| inside || !moving_image) {
1687 self.editor
1688 .place_image(&mut self.engine, id, origin, size)?;
1689 }
1690 match self.drag.take() {
1691 Some(Drag::Resize {
1692 outline: Some(outline),
1693 ..
1694 }) => {
1695 self.editor
1696 .resize(&mut self.engine, outline.bounds().width() as f32)?;
1697 }
1698 Some(Drag::Column {
1699 table,
1700 column,
1701 preview: Some((width, _)),
1702 ..
1703 }) => {
1704 self.editor
1705 .resize_column(&mut self.engine, table, column, width)?;
1706 }
1707 _ => {}
1708 }
1709 if let Some(id) = clicked {
1710 // A click on an outline's handle selects it as OneNote does.
1711 self.editor.select_outline(id)?;
1712 } else if let Some((_, delta)) = delta
1713 && self.editor.whole() == Some(Whole::Page)
1714 {
1715 self.editor.move_page(delta)?;
1716 } else if let Some((id, origin)) = preview {
1717 self.editor.move_outline(id, origin)?;
1718 }
1719 self.changed()
1720 }
1721
1722 /// `delta` in device pixels; Command zooms about the pointer instead of scrolling.
1723 pub fn wheel(&mut self, delta: [f32; 2]) -> Result<Response> {
1724 if self.modifiers.command {
1725 self.zoom_about((delta[1] * 0.005).exp(), self.pointer);
1726 } else {
1727 self.viewport.origin[0] += delta[0];
1728 self.viewport.origin[1] += delta[1];
1729 }
1730 self.moved()
1731 }
1732
1733 /// Text the platform inserts outside key events, such as the character picker's;
1734 /// control characters are not text here.
1735 pub fn insert_text(&mut self, text: String) -> Result<Response> {
1736 if text.is_empty() || text.chars().any(char::is_control) {
1737 return Ok(Response::default());
1738 }
1739 self.commit_text(text)
1740 }
1741
1742 /// Insert Symbol: see [`CanvasEditor::insert_symbol`].
1743 pub fn insert_symbol(&mut self, symbol: char) -> Result<Response> {
1744 if self.edits_wait() {
1745 return Ok(Response::default());
1746 }
1747 self.editor.insert_symbol(&mut self.engine, symbol)?;
1748 self.edited()
1749 }
1750
1751 /// Text committed by an input method, line breaks included.
1752 pub fn commit_text(&mut self, text: String) -> Result<Response> {
1753 if !self.accepts_text() {
1754 return Ok(Response::default());
1755 }
1756 self.editor.commit_text(&mut self.engine, text)?;
1757 self.edited()
1758 }
1759
1760 /// Content Snowbound copied; see [`CanvasEditor::paste_clip`].
1761 pub fn paste_clip(&mut self, clip: Clip) -> Result<Response> {
1762 if !self.accepts_text() {
1763 return Ok(Response::default());
1764 }
1765 self.editor.paste_clip(&mut self.engine, clip)?;
1766 self.follow_pictures()?;
1767 self.edited()
1768 }
1769
1770 /// Clipboard text in `language`, an LCID; see [`CanvasEditor::paste`].
1771 pub fn paste(&mut self, text: &str, language: u32) -> Result<Response> {
1772 if !self.accepts_text() {
1773 return Ok(Response::default());
1774 }
1775 self.editor.paste(&mut self.engine, text, language)?;
1776 self.edited()
1777 }
1778
1779 /// An input method's marked text; `cursor` is its UTF-8 selection within `text`.
1780 pub fn compose(&mut self, text: String, cursor: Option<(usize, usize)>) -> Result<Response> {
1781 // Wayland input methods clear an absent composition after each cursor update.
1782 if !self.accepts_text() || text.is_empty() && self.editor.marked_range().is_none() {
1783 return Ok(Response::default());
1784 }
1785 if text.is_empty() {
1786 self.editor.cancel_composition(&mut self.engine)?;
1787 } else {
1788 let (start, end) = cursor.unwrap_or((text.len(), text.len()));
1789 let utf16 = |end: usize| -> Result<u32> {
1790 Ok(text
1791 .get(..end)
1792 .ok_or("IME range splits UTF-8")?
1793 .encode_utf16()
1794 .count()
1795 .try_into()?)
1796 };
1797 let range = utf16(start)?..utf16(end)?;
1798 self.editor.compose(&mut self.engine, text, range)?;
1799 }
1800 self.edited()
1801 }
1802
1803 /// The input method was switched off mid-composition.
1804 pub fn cancel_composition(&mut self) -> Result<Response> {
1805 if !self.accepts_text() {
1806 return Ok(Response::default());
1807 }
1808 self.editor.cancel_composition(&mut self.engine)?;
1809 self.edited()
1810 }
1811
1812 /// A toolbar command, ignored like other edits while an input method composes or an object
1813 /// holds focus.
1814 pub fn format(&mut self, command: Formatting) -> Result<Response> {
1815 if let Some(ObjectFocus::Image(id) | ObjectFocus::File(id)) = self.object_focus
1816 && self.editor.format_object(&mut self.engine, id, &command)?
1817 {
1818 return self.edited();
1819 }
1820 if self.edits_wait() {
1821 return Ok(Response::default());
1822 }
1823 self.editor.format(&mut self.engine, command)?;
1824 self.edited()
1825 }
1826
1827 /// Copy, or Cut with `cut`: [`CanvasEditor::clip`] goes to the clipboard.
1828 pub fn copy(&mut self, cut: bool) -> Result<Response> {
1829 if let Some(ObjectFocus::Image(id)) = self.object_focus {
1830 let Some(clip) = self.editor.clip_picture(id)? else {
1831 return Ok(Response::default());
1832 };
1833 if cut {
1834 self.editor.remove_image(&mut self.engine, id)?;
1835 self.set_object_focus(None);
1836 }
1837 return Ok(Response {
1838 request: Some(Request::Copy(clip)),
1839 ..self.changed()?
1840 });
1841 }
1842 if self.object_focus.is_some() {
1843 return Ok(Response::default());
1844 }
1845 let page = self.editor.whole() == Some(Whole::Page);
1846 let Some(clip) = self.editor.clip()? else {
1847 return Ok(Response::default());
1848 };
1849 if cut && page {
1850 self.editor.delete(&mut self.engine, false)?;
1851 } else if cut {
1852 self.editor.insert(&mut self.engine, "")?;
1853 }
1854 Ok(Response {
1855 request: Some(Request::Copy(clip)),
1856 ..self.edited()?
1857 })
1858 }
1859
1860 pub fn restore_picture_size(&mut self, id: onestore::ExGuid) -> Result<Response> {
1861 let Some(size) = self.editor.picture(id).and_then(|image| image.size) else {
1862 return Ok(Response::default());
1863 };
1864 let Some((origin, _)) = self.editor.image_placement(id) else {
1865 return Ok(Response::default());
1866 };
1867 self.editor
1868 .place_image(&mut self.engine, id, origin, size)?;
1869 self.edited()
1870 }
1871
1872 /// A secondary press at the pointer: text there takes the caret unless it lies in the
1873 /// selection, and a picture or file is selected.
1874 /// `None` off text, pictures and files.
1875 pub fn context(&mut self) -> Result<Option<(Response, Context)>> {
1876 let point = self.viewport.document_point(self.pointer);
1877 let id = match self.hit_test(point) {
1878 Some(Hit::Text { id, .. }) => id,
1879 Some(Hit::File(id)) | Some(Hit::Image { id, .. }) => {
1880 let focus = if self.editor.attachment(id).is_some() {
1881 ObjectFocus::File(id)
1882 } else {
1883 ObjectFocus::Image(id)
1884 };
1885 self.set_object_focus(Some(focus));
1886 let context = self.caret_context().unwrap_or_default();
1887 return Ok(Some((self.changed()?, context)));
1888 }
1889 _ => return Ok(None),
1890 };
1891 if self.object_focus.is_some() || !self.accepts_text() {
1892 return Ok(None);
1893 }
1894 let outline = self.editor.active_outline().id;
1895 let [anchor, focus] = self.editor.selection().positions;
1896 self.editor.focus_outline(id)?;
1897 self.reconcile_reading();
1898 let at = self.selection_at(id, point, SelectionUnit::Grapheme)?;
1899 let inside = outline == id
1900 && anchor.min(focus) <= at.positions[0]
1901 && at.positions[0] <= anchor.max(focus);
1902 if !inside {
1903 self.editor.select(at)?;
1904 }
1905 let context = self.caret_context().unwrap_or_default();
1906 Ok(Some((self.changed()?, context)))
1907 }
1908
1909 /// What a context menu at the caret or the selected object acts on; `None` where neither
1910 /// takes one.
1911 pub fn caret_context(&self) -> Option<Context> {
1912 if let Some(ObjectFocus::Image(id)) = self.object_focus {
1913 return Some(Context {
1914 image: self.editor.picture(id).cloned(),
1915 selected: true,
1916 ..Context::default()
1917 });
1918 }
1919 if let Some(ObjectFocus::File(id)) = self.object_focus {
1920 return Some(Context {
1921 attachment: self.editor.attachment(id).cloned(),
1922 ..Context::default()
1923 });
1924 }
1925 if self.object_focus.is_some() || !self.accepts_text() {
1926 return None;
1927 }
1928 let [anchor, focus] = self.editor.selection().positions;
1929 Some(Context {
1930 link: self
1931 .editor
1932 .link_at(anchor.min(focus))
1933 .map(|link| link.target),
1934 equation: self.editor.in_equation(),
1935 selected: anchor != focus,
1936 paragraph: self
1937 .editor
1938 .active_outline()
1939 .document()
1940 .leaf(anchor.min(focus).paragraph)
1941 .map(|(_, _, node)| node.id),
1942 attachment: None,
1943 image: None,
1944 spelling: self.selected_correction(),
1945 })
1946 }
1947
1948 /// The marked word at the caret, or wholly selected, with its corrections.
1949 pub fn selected_correction(&self) -> Option<Correction> {
1950 let [anchor, focus] = self.editor.selection().positions;
1951 let correction = self.correction(anchor.min(focus))?;
1952 (anchor == focus || self.correction(anchor.max(focus)).as_ref() == Some(&correction))
1953 .then_some(correction)
1954 }
1955
1956 /// The marked word at `at` in the focused outline, with its corrections.
1957 fn correction(&self, at: crate::document::TextPosition) -> Option<Correction> {
1958 let spelling = self.spelling.as_ref()?;
1959 let outline = self.editor.active_outline();
1960 let paragraph = outline.document().paragraph(at.paragraph)?;
1961 let byte = paragraph.byte_offset(at.offset).ok()?;
1962 let mark = spelling
1963 .marks(paragraph)
1964 .into_iter()
1965 .find(|mark| mark.range.start <= byte && byte <= mark.range.end)?;
1966 correction(spelling, outline, at.paragraph, &mark)
1967 }
1968
1969 /// The Spelling pane's next word: the first marked word after the selection in page
1970 /// order, wrapping round to the top, selected. None once the page marks no word.
1971 pub fn next_correction(&mut self) -> Result<Option<(Response, Correction)>> {
1972 let Some(spelling) = &self.spelling else {
1973 return Ok(None);
1974 };
1975 let (id, word, correction) = {
1976 let mut outlines: Vec<&TextOutline> = self.editor.outlines().iter().collect();
1977 outlines.sort_by(|a, b| {
1978 let [ax, ay] = crate::reading::geometry(a, self.column.as_ref()).1;
1979 let [bx, by] = crate::reading::geometry(b, self.column.as_ref()).1;
1980 ay.total_cmp(&by).then(ax.total_cmp(&bx))
1981 });
1982 let active = self.editor.active_outline().id;
1983 let active = outlines.iter().position(|outline| outline.id == active);
1984 let [anchor, focus] = self.editor.selection().positions;
1985 let after = anchor.max(focus);
1986 let mut marked = outlines.iter().enumerate().flat_map(|(order, outline)| {
1987 outline.layouts().flat_map(move |(index, _)| {
1988 let paragraph = outline.document().paragraph(index);
1989 paragraph
1990 .map(|paragraph| spelling.marks_now(paragraph))
1991 .unwrap_or_default()
1992 .into_iter()
1993 .map(move |mark| (order, *outline, index, mark))
1994 })
1995 });
1996 let mut first = None;
1997 // The first word not wholly before the selection; the page's first after them all.
1998 let found = marked.find(|(order, outline, index, mark)| {
1999 first.get_or_insert_with(|| (*order, *outline, *index, mark.clone()));
2000 let Some(active) = active else {
2001 return true;
2002 };
2003 let paragraph = outline.document().paragraph(*index);
2004 let offset = |byte| {
2005 paragraph.map_or(0, |paragraph| paragraph.utf16_offset(byte).unwrap_or(0))
2006 };
2007 let caret = (after.paragraph, after.offset);
2008 *order > active
2009 || *order == active
2010 && ((*index, offset(mark.range.end)) > caret
2011 || (*index, offset(mark.range.start)) >= caret)
2012 });
2013 let Some((_, outline, index, mark)) = found.or(first) else {
2014 return Ok(None);
2015 };
2016 let correction = correction(spelling, outline, index, &mark).ok_or("Unmarked word")?;
2017 let paragraph = outline
2018 .document()
2019 .paragraph(index)
2020 .ok_or("Missing paragraph")?;
2021 let at = |byte| -> Result<crate::document::TextPosition> {
2022 Ok(crate::document::TextPosition {
2023 paragraph: index,
2024 offset: paragraph.utf16_offset(byte)?,
2025 })
2026 };
2027 let word: Selection = [at(mark.range.start)?, at(mark.range.end)?].into();
2028 (outline.id, word, correction)
2029 };
2030 self.editor.focus_outline(id)?;
2031 self.editor.select(word)?;
2032 Ok(Some((self.edited()?, correction)))
2033 }
2034
2035 /// Replaces the word `correction` names with `text`, or with nothing, as one edit; not
2036 /// once an edit since has changed the word.
2037 pub fn correct(&mut self, correction: &Correction, text: &str) -> Result<Response> {
2038 let [start, end] = correction.range.positions;
2039 let unchanged = self
2040 .editor
2041 .outlines()
2042 .iter()
2043 .find(|outline| outline.id == correction.outline)
2044 .and_then(|outline| outline.document().paragraph(start.paragraph))
2045 .and_then(|paragraph| {
2046 let range = paragraph.byte_offset(start.offset).ok()?
2047 ..paragraph.byte_offset(end.offset).ok()?;
2048 Some(paragraph.text().get(range)?.trim_start() == correction.word)
2049 });
2050 if unchanged != Some(true) {
2051 return Ok(Response::default());
2052 }
2053 self.editor.focus_outline(correction.outline)?;
2054 self.editor.select(correction.range)?;
2055 self.editor.correct(&mut self.engine, text)?;
2056 self.edited()
2057 }
2058
2059 /// The text and address the Link dialog opens with for the selection; none where it does
2060 /// not open.
2061 pub fn link_prefill(&self) -> Option<(String, String)> {
2062 self.editor.link_prefill()
2063 }
2064
2065 /// OK in the Link dialog: links what [`Self::link_prefill`] picked, shown as `text`.
2066 pub fn set_link(&mut self, text: &str, address: &str) -> Result<Response> {
2067 if !self.accepts_text() {
2068 return Ok(Response::default());
2069 }
2070 self.editor.set_link(&mut self.engine, text, address)?;
2071 self.edited()
2072 }
2073
2074 /// Remove Link, or Select Link with `select`, on the link at the caret.
2075 pub fn unlink(&mut self, select: bool) -> Result<Response> {
2076 if select {
2077 self.editor.select_link()?;
2078 } else {
2079 self.editor.remove_link(&mut self.engine)?;
2080 }
2081 self.edited()
2082 }
2083
2084 /// Alt+= and the toolbar's Equation: see [`CanvasEditor::insert_equation`].
2085 pub fn insert_equation(&mut self) -> Result<Response> {
2086 if self.edits_wait() {
2087 return Ok(Response::default());
2088 }
2089 self.editor.insert_equation(&mut self.engine)?;
2090 self.edited()
2091 }
2092
2093 /// Insert, Table: see [`CanvasEditor::insert_table`].
2094 pub fn insert_table(&mut self, rows: usize, columns: usize) -> Result<Response> {
2095 if self.edits_wait() {
2096 return Ok(Response::default());
2097 }
2098 self.editor.insert_table(&mut self.engine, rows, columns)?;
2099 self.edited()
2100 }
2101
2102 /// Insert, Picture, or a pasted one: a PNG, JPEG or GIF `size` points large; see
2103 /// [`CanvasEditor::insert_picture`].
2104 pub fn insert_picture(&mut self, bytes: Vec<u8>, size: [f32; 2]) -> Result<Response> {
2105 if self.edits_wait() {
2106 return Ok(Response::default());
2107 }
2108 let image = crate::editor::picture(bytes, size)?;
2109 self.editor.insert_picture(&mut self.engine, image)?;
2110 self.follow_pictures()?;
2111 self.edited()
2112 }
2113
2114 /// Pasted clips and pictures, in order and as one undo step; see
2115 /// [`CanvasEditor::paste_pieces`].
2116 pub fn paste_pieces(&mut self, pieces: Vec<Piece>) -> Result<(Vec<Awaited>, Response)> {
2117 if self.edits_wait() {
2118 return Ok((Vec::new(), Response::default()));
2119 }
2120 let awaited = self.editor.paste_pieces(&mut self.engine, pieces)?;
2121 self.follow_pictures()?;
2122 Ok((awaited, self.edited()?))
2123 }
2124
2125 /// An awaited picture arrived: see [`CanvasEditor::insert_awaited`]. The view stays.
2126 pub fn insert_awaited(
2127 &mut self,
2128 at: Awaited,
2129 bytes: Vec<u8>,
2130 size: [f32; 2],
2131 ) -> Result<Response> {
2132 let image = crate::editor::picture(bytes, size)?;
2133 if !self.editor.insert_awaited(&mut self.engine, at, image)? {
2134 return Ok(Response::default());
2135 }
2136 self.follow_pictures()?;
2137 self.changed()
2138 }
2139
2140 /// Record Audio: see [`CanvasEditor::start_recording`]; none where text cannot go.
2141 pub fn start_recording(&mut self, label: &str) -> Result<(Option<[u8; 16]>, Response)> {
2142 if self.edits_wait() {
2143 return Ok((None, Response::default()));
2144 }
2145 let id = self.editor.start_recording(&mut self.engine, label)?;
2146 Ok((Some(id), self.edited()?))
2147 }
2148
2149 /// Stop: see [`CanvasEditor::finish_recording`]. A file without an icon takes the
2150 /// page's blank one.
2151 pub fn finish_recording(&mut self, mut file: onestore::page::Attachment) -> Result<Response> {
2152 file.preview
2153 .get_or_insert_with(|| crate::gpu::page::file_icon().into());
2154 self.editor.finish_composition();
2155 self.editor.finish_recording(&mut self.engine, file)?;
2156 self.follow_pictures()?;
2157 self.edited()
2158 }
2159
2160 /// The recording a press at document point `point` plays and the moment it plays from:
2161 /// that of a play button OneNote shows beside a linked note or a recording.
2162 fn play_button(&self, point: [f32; 2]) -> Option<(onestore::page::Attachment, u32)> {
2163 if !self.pointer_inside || self.space {
2164 return None;
2165 }
2166 let pointer = self.viewport.document_point(self.pointer);
2167 let ([x0, y0, x1, y1], recording, at) = self.editor.play_button(pointer)?;
2168 ((x0..=x1).contains(&point[0]) && (y0..=y1).contains(&point[1]))
2169 .then(|| Some((self.editor.recording_file(recording)?.clone(), at)))
2170 .flatten()
2171 }
2172
2173 /// Insert, Attach File: see [`CanvasEditor::insert_attachment`]. A file without an icon
2174 /// takes the page's blank one.
2175 pub fn insert_attachment(&mut self, mut file: onestore::page::Attachment) -> Result<Response> {
2176 if self.edits_wait() {
2177 return Ok(Response::default());
2178 }
2179 file.preview
2180 .get_or_insert_with(|| crate::gpu::page::file_icon().into());
2181 self.editor.insert_attachment(&mut self.engine, file)?;
2182 self.follow_pictures()?;
2183 self.edited()
2184 }
2185
2186 /// A file dropped at view point `position`, in device pixels: text or blank page there
2187 /// takes the caret as a click would, then the file is attached at the caret, on the page
2188 /// where the caret is on blank page.
2189 pub fn drop_attachment(
2190 &mut self,
2191 position: [f32; 2],
2192 file: onestore::page::Attachment,
2193 ) -> Result<Response> {
2194 self.drop_caret(position)?;
2195 self.insert_attachment(file)
2196 }
2197
2198 /// A picture file dropped at view point `position`, in device pixels, placed as
2199 /// [`Self::drop_attachment`] places a file and sized as [`Self::insert_picture`].
2200 pub fn drop_picture(
2201 &mut self,
2202 position: [f32; 2],
2203 bytes: Vec<u8>,
2204 size: [f32; 2],
2205 ) -> Result<Response> {
2206 self.drop_caret(position)?;
2207 self.insert_picture(bytes, size)
2208 }
2209
2210 /// Puts the caret where a drop at `position` lands, as a click there would.
2211 fn drop_caret(&mut self, position: [f32; 2]) -> Result<()> {
2212 let point = self.viewport.document_point(position);
2213 let reading = self.column.is_some();
2214 self.set_object_focus(None);
2215 match self.hit_test(point) {
2216 Some(Hit::Text { id, point: local }) => {
2217 self.editor.focus_outline(id)?;
2218 self.reconcile_reading();
2219 if !reading {
2220 self.editor.select_below(&mut self.engine, id, local)?;
2221 }
2222 let at = self.selection_at(id, point, SelectionUnit::Grapheme)?;
2223 self.editor.select(at)?;
2224 }
2225 None => self.place_caret(point)?,
2226 _ => {}
2227 }
2228 Ok(())
2229 }
2230
2231 /// Professional, or Linear with `linear`, on the equation at the caret.
2232 pub fn switch_equation(&mut self, linear: bool) -> Result<Response> {
2233 if linear {
2234 self.editor.linear_equation(&mut self.engine)?;
2235 } else {
2236 self.editor.build_equation(&mut self.engine)?;
2237 }
2238 self.edited()
2239 }
2240
2241 /// Undoes the last edit, or redoes the last undone one.
2242 pub fn undo(&mut self, redo: bool) -> Result<Response> {
2243 if redo {
2244 self.editor.redo(&mut self.engine)?;
2245 } else {
2246 self.editor.undo(&mut self.engine)?;
2247 }
2248 self.follow_pictures()?;
2249 if let Some(ObjectFocus::Image(id) | ObjectFocus::File(id)) = self.object_focus
2250 && self.editor.image_placement(id).is_none()
2251 {
2252 self.set_object_focus(None);
2253 }
2254 self.edited()
2255 }
2256
2257 /// Select All, widening the selection a unit at a time as OneNote 2010's Ctrl+A does.
2258 pub fn widen_selection(&mut self) -> Result<Response> {
2259 self.editor.widen_selection()?;
2260 // Revealing the last outline would scroll the page selection's start away.
2261 if self.editor.whole() == Some(Whole::Page) {
2262 return self.changed();
2263 }
2264 self.edited()
2265 }
2266
2267 /// The date or time chosen after `Request::EditDate`.
2268 pub fn change_date(&mut self, timestamp: u64, text: [String; 2]) -> Result<Response> {
2269 self.editor.change_date(&mut self.engine, timestamp, text)?;
2270 self.changed()
2271 }
2272
2273 /// Gives the page a colour (COLORREF), rule lines and, when given, `art` in place of its
2274 /// background pictures, as one undo step: see [`CanvasEditor::set_paper`].
2275 pub fn set_paper(
2276 &mut self,
2277 color: Option<u32>,
2278 rule_lines: Option<onestore::page::RuleLines>,
2279 art: Option<Vec<onestore::page::Image>>,
2280 ) -> Result<Response> {
2281 if !self.editor.set_paper(color, rule_lines, art) {
2282 return Ok(Response::default());
2283 }
2284 self.follow_pictures()?;
2285 self.changed()
2286 }
2287
2288 /// Draws pictures and file previews brought by an edit or undo.
2289 fn follow_pictures(&mut self) -> Result<()> {
2290 if let Some((scene, _)) = &mut self.scene {
2291 scene.follow(&mut self.editor, &mut self.engine)?;
2292 }
2293 Ok(())
2294 }
2295
2296 /// Gives keyboard focus to a read-only object, as an assistive technology asks.
2297 pub fn focus_read_only(&mut self, index: usize) -> Result<Response> {
2298 self.set_object_focus(Some(ObjectFocus::ReadOnly(index)));
2299 self.edited()
2300 }
2301
2302 /// Returns focus to text after the host acted on the editor directly, as an assistive
2303 /// technology's selection or value change does.
2304 pub fn focus_text(&mut self) -> Result<Response> {
2305 self.set_object_focus(None);
2306 self.edited()
2307 }
2308
2309 pub fn key(&mut self, key: &Key, text: Option<&str>) -> Result<Response> {
2310 let Modifiers {
2311 shift,
2312 control,
2313 option,
2314 command,
2315 } = self.modifiers;
2316 if key == &Key::Named(NamedKey::Modifier) {
2317 return Ok(Response::default());
2318 }
2319 if let Some(response) = self.ink_key(key)? {
2320 return Ok(response);
2321 }
2322 if let Some(ObjectFocus::Image(id) | ObjectFocus::File(id)) = self.object_focus
2323 && matches!(key, Key::Named(NamedKey::Backspace | NamedKey::Delete))
2324 {
2325 self.editor.remove_image(&mut self.engine, id)?;
2326 self.set_object_focus(None);
2327 return self.changed();
2328 }
2329 if let Some(ObjectFocus::Image(id) | ObjectFocus::File(id)) = self.object_focus
2330 && !(shift || command || option || control)
2331 && let Key::Named(NamedKey::ArrowLeft | NamedKey::ArrowRight) = key
2332 && self.editor.step_from_image(
2333 &mut self.engine,
2334 id,
2335 key == &Key::Named(NamedKey::ArrowRight),
2336 )?
2337 {
2338 self.set_object_focus(None);
2339 return self.edited();
2340 }
2341 if self.object_focus.is_some() {
2342 if !read_only_shortcut(key, self.modifiers) {
2343 return Ok(Response::default());
2344 }
2345 if key == &Key::Named(NamedKey::Escape) {
2346 self.set_object_focus(None);
2347 return self.edited();
2348 }
2349 }
2350 if !matches!(self.drag, None | Some(Drag::Text { .. })) || self.space {
2351 self.drag = None;
2352 self.space = false;
2353 if key == &Key::Named(NamedKey::Escape) {
2354 return self.changed();
2355 }
2356 }
2357 if self.editor.whole() == Some(Whole::Page) {
2358 match (Command::from_key(key, self.modifiers), key) {
2359 // OneNote 2010 keeps its page selection on these.
2360 (
2361 Some(
2362 Command::Move(Movement::Up | Movement::Down)
2363 | Command::MovePage { .. }
2364 | Command::MoveParagraphs { .. },
2365 ),
2366 _,
2367 )
2368 | (_, Key::Named(NamedKey::Tab)) => return Ok(Response::default()),
2369 // Enter leaves the caret at the selection's end, as Right does.
2370 (_, Key::Named(NamedKey::Enter)) => {
2371 self.move_selection(Movement::Right, false)?;
2372 return self.edited();
2373 }
2374 _ => {}
2375 }
2376 }
2377 if let Some(Command::MoveParagraphs { up }) = Command::from_key(key, self.modifiers)
2378 && self.editor.marked_range().is_none()
2379 {
2380 self.editor.move_paragraphs(&mut self.engine, up)?;
2381 return self.edited();
2382 }
2383 if command && option {
2384 let delta = match key {
2385 Key::Named(NamedKey::ArrowLeft) => Some([-1.0, 0.0]),
2386 Key::Named(NamedKey::ArrowRight) => Some([1.0, 0.0]),
2387 Key::Named(NamedKey::ArrowUp) => Some([0.0, -1.0]),
2388 Key::Named(NamedKey::ArrowDown) => Some([0.0, 1.0]),
2389 _ => None,
2390 };
2391 if let Some(delta) = delta
2392 && self.editor.whole() == Some(Whole::Page)
2393 {
2394 let step = if shift { 10.0 } else { 1.0 };
2395 self.editor.move_page(delta.map(|axis| axis * step))?;
2396 return self.edited();
2397 }
2398 if let Some(delta) = delta {
2399 let outline = self.editor.active_outline();
2400 let origin = outline.origin();
2401 let step = if shift { 10.0 } else { 1.0 };
2402 self.editor.move_outline(
2403 outline.id,
2404 [origin[0] + delta[0] * step, origin[1] + delta[1] * step],
2405 )?;
2406 return self.edited();
2407 }
2408 }
2409 if control && key == &Key::Named(NamedKey::Tab) {
2410 let outlines = self.editor.outlines();
2411 let count = outlines.len()
2412 + self
2413 .scene
2414 .as_ref()
2415 .map_or(0, |(scene, _)| scene.read_only(Some(&self.editor)).count());
2416 if count == 0 {
2417 return self.changed();
2418 }
2419 let index = self
2420 .object_focus
2421 .and_then(ObjectFocus::read_only)
2422 .map_or_else(
2423 || {
2424 outlines
2425 .iter()
2426 .position(|outline| outline.id == self.editor.active_outline().id)
2427 },
2428 |index| Some(outlines.len() + index),
2429 );
2430 let next = index.map_or(if shift { count - 1 } else { 0 }, |index| {
2431 if shift {
2432 (index + count - 1) % count
2433 } else {
2434 (index + 1) % count
2435 }
2436 });
2437 if next < outlines.len() {
2438 self.editor.focus_outline(outlines[next].id)?;
2439 self.set_object_focus(None);
2440 } else {
2441 self.set_object_focus(Some(ObjectFocus::ReadOnly(next - outlines.len())));
2442 }
2443 return self.edited();
2444 }
2445 // The host runs the shortcut modifier's chords from its command table.
2446 if command && let Key::Character(_) = key {
2447 return Ok(Response::default());
2448 }
2449 let chord = Command::from_key(key, self.modifiers);
2450 if let Some(Command::Move(movement)) = chord {
2451 self.move_selection(movement, shift)?;
2452 } else if let Some(Command::ScrollPage { up }) = chord {
2453 // AppKit keeps ten points of the last page in view.
2454 let page = (self.viewport.size[1] as f32 - 10.0 * self.display_scale).max(0.0);
2455 self.viewport.origin[1] += if up { page } else { -page };
2456 return self.moved();
2457 } else if let Some(Command::MovePage { up }) = chord {
2458 self.move_page(up, shift)?;
2459 } else if self.editor.marked_range().is_none() {
2460 match (chord, key) {
2461 (Some(Command::DeleteTo(movement)), _) => {
2462 self.editor.delete_to(&mut self.engine, movement)?;
2463 }
2464 (Some(Command::Delete { backward }), _) => {
2465 self.editor.delete(&mut self.engine, backward)?;
2466 }
2467 (Some(Command::Kill), _) => {
2468 if !self.editor.delete_to(&mut self.engine, Movement::LineEnd)? {
2469 self.editor.delete(&mut self.engine, false)?;
2470 }
2471 }
2472 (_, Key::Named(end @ (NamedKey::Home | NamedKey::End))) => {
2473 let limits = self.scroll();
2474 self.viewport.origin[1] = -if *end == NamedKey::Home {
2475 limits.min[1]
2476 } else {
2477 limits.max[1]
2478 };
2479 return self.moved();
2480 }
2481 (_, Key::Named(NamedKey::Enter)) => {
2482 // Enter inside a link follows it, as OneNote's does.
2483 let [anchor, focus] = self.editor.selection().positions;
2484 if let Some(link) = self.editor.link_at(focus).filter(|link| {
2485 anchor == focus
2486 && !shift
2487 && link.label.start < focus.offset
2488 && focus.offset < link.label.end
2489 }) {
2490 return Ok(Response {
2491 request: Some(Request::OpenLink(link.target)),
2492 ..Response::default()
2493 });
2494 }
2495 self.editor.enter(&mut self.engine, shift)?;
2496 }
2497 (_, Key::Named(NamedKey::Tab)) => {
2498 self.editor.tab(&mut self.engine, shift)?;
2499 }
2500 _ if !command && !control => {
2501 if let Some(text) =
2502 text.filter(|text| !text.is_empty() && !text.chars().any(char::is_control))
2503 {
2504 self.editor.insert(&mut self.engine, text)?;
2505 }
2506 }
2507 _ => {}
2508 }
2509 } else if key == &Key::Named(NamedKey::Escape) {
2510 self.editor.cancel_composition(&mut self.engine)?;
2511 }
2512 self.edited()
2513 }
2514}
2515
2516/// What a document point lands on.
2517#[derive(Debug, PartialEq)]
2518pub enum Hit {
2519 Date(DateField),
2520 Resize {
2521 id: onestore::ExGuid,
2522 grab: f32,
2523 },
2524 /// The right border of `column` of `table`, which is `width` wide.
2525 Column {
2526 id: onestore::ExGuid,
2527 table: onestore::ExGuid,
2528 column: usize,
2529 width: f32,
2530 },
2531 Text {
2532 id: onestore::ExGuid,
2533 point: [f32; 2],
2534 },
2535 Handle {
2536 id: onestore::ExGuid,
2537 grab: [f32; 2],
2538 },
2539 ReadOnly(usize),
2540 /// A check box tag on paragraph `paragraph`.
2541 Check {
2542 outline: onestore::ExGuid,
2543 paragraph: onestore::ExGuid,
2544 },
2545 /// `handle` is [0, 0] on the picture and a direction on the selected picture's handles.
2546 Image {
2547 id: onestore::ExGuid,
2548 handle: [i8; 2],
2549 },
2550 /// A file in an outline's flow or on the page, its icon or name.
2551 File(onestore::ExGuid),
2552 /// A check box tag on a picture or file on the page.
2553 ObjectCheck(onestore::ExGuid),
2554}
2555
2556/// What a document point lands on; `pixel` is document points per device pixel.
2557#[cfg(test)]
2558fn page_hit_test(
2559 editor: &CanvasEditor,
2560 scene: Option<&(PageScene, [f32; 2])>,
2561 point: [f32; 2],
2562 pixel: f32,
2563) -> Option<Hit> {
2564 page_hit(editor, scene, point, pixel, None, None)
2565}
2566
2567/// OneNote 2010's play button: a blue disc with a white triangle, in `rect`.
2568fn append_play_button(rect: [f32; 4], paper: Paper, primitives: &mut Vec<Primitive<'_>>) {
2569 let side = rect[2] - rect[0];
2570 primitives.push(Primitive::Gradient {
2571 rect,
2572 radius: [side / 2.0; 2],
2573 colors: [0x00e6a56e, 0x00c8783a].map(|color| paper.tint(crate::gpu::colorref(color))),
2574 });
2575 primitives.push(Primitive::Path {
2576 data: "M4 3L8 5.25L4 7.5Z",
2577 origin: [rect[0], rect[1]],
2578 style: draw::PathStyle::Fill,
2579 colors: [[1.0; 4]; 2],
2580 });
2581}
2582
2583/// Logical pixels a finger's grip targets extend beyond the drawn grips.
2584const TOUCH_REACH: f32 = 16.0;
2585
2586/// `page_hit_test` where the `touched` outline's grips reach `TOUCH_REACH` further: outward,
2587/// and inward along its header.
2588fn page_hit(
2589 editor: &CanvasEditor,
2590 scene: Option<&(PageScene, [f32; 2])>,
2591 point: [f32; 2],
2592 pixel: f32,
2593 touched: Option<onestore::ExGuid>,
2594 column: Option<&crate::reading::Column>,
2595) -> Option<Hit> {
2596 if let Some(column) = column {
2597 let hit = |id| {
2598 let source = editor
2599 .visible_outlines()
2600 .chain(editor.caret_outline())
2601 .find(|outline| outline.id == id)?;
2602 let (geometry, mut origin) = crate::reading::geometry(source, Some(column));
2603 if let Some((_, offset)) = scene {
2604 origin = [origin[0] + offset[0], origin[1] + offset[1]];
2605 }
2606 let local = [point[0] - origin[0], point[1] - origin[1]];
2607 if let Some((paragraph, ..)) = geometry.shaped.tags().find(|(_, origin, tag)| {
2608 let x = geometry.shaped.tag_column_offset() + origin[0];
2609 let y = origin[1];
2610 tag.icon.checkable()
2611 && (x..=x + tag.size).contains(&local[0])
2612 && (y..=y + tag.size).contains(&local[1])
2613 }) {
2614 return Some(Hit::Check {
2615 outline: id,
2616 paragraph,
2617 });
2618 }
2619 if local[0] >= 0.0
2620 && local[0] <= column.width
2621 && local[1] >= 0.0
2622 && local[1] <= geometry.shaped.size[1]
2623 {
2624 Some(Hit::Text { id, point: local })
2625 } else {
2626 None
2627 }
2628 };
2629 if let Some((scene, offset)) = scene
2630 && let Some(hit) = scene.hit_test_with(
2631 [point[0] - offset[0], point[1] - offset[1]],
2632 Some(editor),
2633 Some(column),
2634 hit,
2635 )
2636 {
2637 return Some(match hit {
2638 crate::gpu::page::SceneHit::Outline(hit) => hit,
2639 crate::gpu::page::SceneHit::Date(field) => Hit::Date(field),
2640 _ => return None,
2641 });
2642 }
2643 return column.outlines.iter().rev().find_map(|(id, _, _)| hit(*id));
2644 }
2645 /// Grips and text resolve front to back before any outline's padding, as a native
2646 /// width handle stays reachable under the next outline's left padding; the typing room
2647 /// below an outline comes last.
2648 #[derive(Clone, Copy, PartialEq)]
2649 enum Layer {
2650 Grips,
2651 Body,
2652 Below,
2653 }
2654 let hit = |outline: &TextOutline, offset: [f32; 2], layer: Layer| {
2655 if layer == Layer::Below {
2656 let outline = editor
2657 .outlines()
2658 .iter()
2659 .find(|source| source.id == outline.id)?;
2660 let local = [
2661 point[0] - offset[0] - outline.origin()[0],
2662 point[1] - offset[1] - outline.origin()[1],
2663 ];
2664 return outline.contains_extension(local).then_some(Hit::Text {
2665 id: outline.id,
2666 point: local,
2667 });
2668 }
2669 let (bounds, body_top) = outline_chrome(outline, pixel);
2670 let local = [point[0] - offset[0], point[1] - offset[1]];
2671 let [x, y] = local;
2672 if layer == Layer::Grips {
2673 let extra = if touched == Some(outline.id) && !outline.title {
2674 TOUCH_REACH * pixel
2675 } else {
2676 0.0
2677 };
2678 // Width handles: 15 px inside to 6 px outside the header's right end, and 6 px
2679 // either side of the right border below it.
2680 let inside = if y < body_top {
2681 15.0 * pixel + extra
2682 } else {
2683 6.0 * pixel
2684 };
2685 if !outline.title
2686 && (bounds[2] - inside..=bounds[2] + 6.0 * pixel + extra).contains(&x)
2687 && (bounds[1] - extra..=bounds[3] + extra).contains(&y)
2688 {
2689 return Some(Hit::Resize {
2690 id: outline.id,
2691 grab: point[0] - outline.bounds().x1 as f32,
2692 });
2693 }
2694 if (bounds[0] - extra..=bounds[2] + extra).contains(&x)
2695 && y >= bounds[1] - extra
2696 && y < body_top
2697 {
2698 return Some(Hit::Handle {
2699 id: outline.id,
2700 grab: [
2701 point[0] - outline.origin()[0],
2702 point[1] - outline.origin()[1],
2703 ],
2704 });
2705 }
2706 let shaped = outline.shaped();
2707 let [left, top] = outline.origin();
2708 let check = shaped.tags().find(|(_, origin, tag)| {
2709 let tag_x = left + shaped.tag_column_offset() + origin[0];
2710 let tag_y = top + origin[1];
2711 tag.icon.checkable()
2712 && (tag_x..=tag_x + tag.size).contains(&x)
2713 && (tag_y..=tag_y + tag.size).contains(&y)
2714 });
2715 if let Some((paragraph, ..)) = check {
2716 return Some(Hit::Check {
2717 outline: outline.id,
2718 paragraph,
2719 });
2720 }
2721 let text = outline.bounds();
2722 if !(text.x0..=text.x1).contains(&f64::from(x))
2723 || !(text.y0..=text.y1).contains(&f64::from(y))
2724 {
2725 return None;
2726 }
2727 }
2728 let inner = [x - outline.origin()[0], y - outline.origin()[1]];
2729 if let Some((table, column, width)) = outline.column_border(inner, 3.0 * pixel) {
2730 return Some(Hit::Column {
2731 id: outline.id,
2732 table,
2733 column,
2734 width,
2735 });
2736 }
2737 if let Some(picture) = outline.shaped().objects.iter().find(|object| {
2738 matches!(object.kind, crate::outline::ObjectKind::Picture)
2739 && (object.rect[0]..=object.rect[2]).contains(&inner[0])
2740 && (object.rect[1]..=object.rect[3]).contains(&inner[1])
2741 }) {
2742 return Some(Hit::Image {
2743 id: picture.id,
2744 handle: [0, 0],
2745 });
2746 }
2747 if let Some(file) = outline.shaped().objects.iter().find(|object| {
2748 let [x0, y0, x1, y1] = object.bounds();
2749 matches!(object.kind, crate::outline::ObjectKind::File(_))
2750 && (x0..=x1).contains(&inner[0])
2751 && (y0..=y1).contains(&inner[1])
2752 }) {
2753 return Some(Hit::File(file.id));
2754 }
2755 if x >= bounds[0] && x <= bounds[2] && y >= body_top && y <= bounds[3] {
2756 return Some(Hit::Text {
2757 id: outline.id,
2758 point: [
2759 local[0] - outline.origin()[0],
2760 local[1] - outline.origin()[1],
2761 ],
2762 });
2763 }
2764 None
2765 };
2766 let pass = |layer| {
2767 if let Some(hit) = editor
2768 .visible_outlines()
2769 .rev()
2770 .filter(|outline| scene.is_none() || !editor.has_page_outline(outline.id))
2771 .find_map(|outline| hit(outline, [0.0; 2], layer))
2772 {
2773 return Some(hit);
2774 }
2775 let (scene, offset) = scene?;
2776 match scene.hit_test(
2777 [point[0] - offset[0], point[1] - offset[1]],
2778 Some(editor),
2779 |id| {
2780 editor
2781 .visible_outlines()
2782 .find(|outline| outline.id == id)
2783 .and_then(|outline| hit(outline, *offset, layer))
2784 },
2785 )? {
2786 crate::gpu::page::SceneHit::Outline(hit) => Some(hit),
2787 crate::gpu::page::SceneHit::Date(field) => Some(Hit::Date(field)),
2788 crate::gpu::page::SceneHit::ReadOnly(index) => Some(Hit::ReadOnly(index)),
2789 crate::gpu::page::SceneHit::Image(id) => Some(Hit::Image { id, handle: [0, 0] }),
2790 crate::gpu::page::SceneHit::File(id) => Some(Hit::File(id)),
2791 crate::gpu::page::SceneHit::Check(id) => Some(Hit::ObjectCheck(id)),
2792 }
2793 };
2794 [Layer::Grips, Layer::Body, Layer::Below]
2795 .into_iter()
2796 .find_map(pass)
2797}
2798
2799/// The page's rule lines over `visible`, a document rectangle, as OneNote 2010 draws them:
2800/// 1/96 inch wide, or a device pixel where that is wider, horizontal lines from the margin
2801/// origin down, and a grid's vertical lines through it or a margin line 1/96 inch left of it.
2802pub(crate) fn rule_primitives(
2803 lines: Option<onestore::page::RuleLines>,
2804 [x, y]: [f32; 2],
2805 [left, top, right, bottom]: [f32; 4],
2806 pixel: f32,
2807 paper: Paper,
2808) -> Vec<Primitive<'static>> {
2809 use onestore::page::VerticalRule;
2810 let Some(lines) = lines else {
2811 return Vec::new();
2812 };
2813 let width = pixel.max(0.75);
2814 // Stored spacings are in half inches; nothing draws closer than a device pixel apart.
2815 let step = |spacing: f32| (spacing * 36.0).max(pixel);
2816 let color = |color| paper.tint(crate::gpu::colorref(color));
2817 let horizontal = step(lines.spacing);
2818 let first = ((top - y) / horizontal).ceil().max(0.0);
2819 let mut primitives: Vec<_> = (first as u32..)
2820 .map(|index| y + index as f32 * horizontal)
2821 .take_while(|at| *at <= bottom + width)
2822 .map(|at| Primitive::Rect {
2823 rect: [left, at - width / 2.0, right, at + width / 2.0],
2824 color: color(lines.color),
2825 })
2826 .collect();
2827 let vertical = |at: f32, rule_color| Primitive::Rect {
2828 rect: [at - width / 2.0, top, at + width / 2.0, bottom],
2829 color: color(rule_color),
2830 };
2831 match lines.vertical {
2832 VerticalRule::Margin(rule_color) => primitives.push(vertical(x - 0.75, rule_color)),
2833 VerticalRule::Grid { spacing, color } => {
2834 let spacing = step(spacing);
2835 let first = ((left - x) / spacing).floor();
2836 primitives.extend(
2837 (0..)
2838 .map(|index| x + (first + index as f32) * spacing)
2839 .take_while(|at| *at <= right + width)
2840 .map(|at| vertical(at, color)),
2841 );
2842 }
2843 }
2844 primitives
2845}
2846
2847fn page_primitives<'a>(
2848 editor: &'a CanvasEditor,
2849 scene: Option<&'a (PageScene, [f32; 2])>,
2850 column: Option<&'a crate::reading::Column>,
2851 preview: Option<PointerFeedback<'a>>,
2852 object_focus: Option<ObjectFocus>,
2853 paint: Paint,
2854) -> Result<Vec<Primitive<'a>>> {
2855 let mut primitives = Vec::new();
2856 let active = editor.active_outline().id;
2857 let whole = editor
2858 .whole()
2859 .filter(|_| paint.chrome && object_focus.is_none());
2860 let selected = |outline: &TextOutline| match whole {
2861 Some(Whole::Page) => !outline.title,
2862 Some(Whole::Outline) => outline.id == active,
2863 None => false,
2864 };
2865 let draw_outline = |id, offset: [f32; 2], primitives: &mut Vec<_>| {
2866 let offset = if column.is_some() {
2867 scene.map_or([0.0; 2], |(_, offset)| *offset)
2868 } else {
2869 offset
2870 };
2871 // An emptied page outline leaves the editor but keeps its paint slot for undo.
2872 let Some(outline) = editor.visible_outlines().find(|outline| outline.id == id) else {
2873 return Ok(());
2874 };
2875 let outline = match preview {
2876 Some(PointerFeedback::Resize(resized)) if resized.id == outline.id => resized,
2877 _ => outline,
2878 };
2879 let (geometry, displayed) = crate::reading::geometry(outline, column);
2880 let [x, y] = displayed;
2881 let origin = match preview {
2882 Some(PointerFeedback::Move(id, [dx, dy]))
2883 if id == outline.id || whole == Some(Whole::Page) && !outline.title =>
2884 {
2885 [x + dx, y + dy]
2886 }
2887 _ => [x, y],
2888 };
2889 // OneNote frames the title whether or not it is focused or hovered.
2890 if column.is_none()
2891 && (outline.title
2892 || selected(outline)
2893 || (paint.chrome && object_focus.is_none() && outline.id == active)
2894 || matches!(preview, Some(PointerFeedback::Hover(id) | PointerFeedback::Move(id, _)) if id == outline.id)
2895 || matches!(preview, Some(PointerFeedback::Resize(resized)) if resized.id == outline.id))
2896 {
2897 append_outline_chrome(
2898 outline,
2899 [origin[0] + offset[0], origin[1] + offset[1]],
2900 paint.pixel,
2901 paint.colors.paper,
2902 selected(outline),
2903 primitives,
2904 );
2905 }
2906 if column.is_some() && outline.title {
2907 let [x, y] = [origin[0] + offset[0], origin[1] + offset[1]];
2908 append_title_frame(
2909 [
2910 x - 8.25,
2911 y - 3.75,
2912 x + geometry.shaped.size[0] + 2.25,
2913 y + geometry.shaped.size[1] + 3.75,
2914 ],
2915 paint.pixel,
2916 paint.colors.paper,
2917 primitives,
2918 );
2919 }
2920 let shaped = geometry.shaped;
2921 append_outline(
2922 (object_focus.is_none()
2923 && outline.id == active
2924 && !matches!(preview, Some(PointerFeedback::Resize(_))))
2925 .then_some(editor),
2926 selected(outline),
2927 geometry,
2928 [origin[0] + offset[0], origin[1] + offset[1]],
2929 paint,
2930 primitives,
2931 )?;
2932 if let Some((scene, _)) = scene {
2933 let moving = match preview {
2934 Some(PointerFeedback::Image(id, origin, size)) => {
2935 Some((id, crate::translated(image_rect(origin, size), offset)))
2936 }
2937 _ => None,
2938 };
2939 scene.append_outline_objects(
2940 shaped,
2941 [origin[0] + offset[0], origin[1] + offset[1]],
2942 moving,
2943 paint.colors.paper,
2944 primitives,
2945 );
2946 }
2947 Ok::<_, Box<dyn Error>>(())
2948 };
2949 if let Some((scene, origin)) = scene {
2950 let moving = match preview {
2951 Some(PointerFeedback::Image(id, origin, size)) => Some((id, image_rect(origin, size))),
2952 _ => None,
2953 };
2954 scene.append_primitives_with(
2955 &mut primitives,
2956 *origin,
2957 Some(editor),
2958 column,
2959 moving,
2960 paint.colors.paper,
2961 &draw_outline,
2962 )?;
2963 }
2964 for outline in editor
2965 .visible_outlines()
2966 .filter(|outline| scene.is_none() || !editor.has_page_outline(outline.id))
2967 {
2968 draw_outline(outline.id, [0.0; 2], &mut primitives)?;
2969 }
2970 if object_focus.is_none()
2971 && let Some(outline) = editor.caret_outline()
2972 {
2973 append_outline(
2974 Some(editor),
2975 false,
2976 crate::reading::geometry(outline, column).0,
2977 {
2978 let origin = crate::reading::geometry(outline, column).1;
2979 let offset = if column.is_some() {
2980 scene.map_or([0.0; 2], |(_, offset)| *offset)
2981 } else {
2982 [0.0; 2]
2983 };
2984 [origin[0] + offset[0], origin[1] + offset[1]]
2985 },
2986 paint,
2987 &mut primitives,
2988 )?;
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 }
2999 if let Some(ObjectFocus::Image(id)) = object_focus {
3000 let (origin, size) = match preview {
3001 Some(PointerFeedback::Image(moving, origin, size)) if moving == id => (origin, size),
3002 _ => editor
3003 .image_placement(id)
3004 .ok_or("The selected picture is missing.")?,
3005 };
3006 append_image_chrome(image_rect(origin, size), paint.pixel, &mut primitives);
3007 }
3008 // OneNote shades a selected file's column and frames it with dashes.
3009 if let Some(ObjectFocus::File(id)) = object_focus {
3010 let rect = match preview {
3011 Some(PointerFeedback::Image(moving, origin, size)) if moving == id => {
3012 image_rect(origin, size)
3013 }
3014 _ => editor
3015 .attachment_rect(id)
3016 .ok_or("The selected file is missing.")?,
3017 };
3018 primitives.extend([
3019 Primitive::Rect {
3020 rect,
3021 color: [0.0, 0.0, 0.0, 0.08],
3022 },
3023 Primitive::RoundedRect {
3024 rect,
3025 radius: [0.0; 2],
3026 stroke: Some(Stroke::Dashed(paint.pixel)),
3027 color: [0.25, 0.45, 0.7, 1.0],
3028 },
3029 ]);
3030 }
3031 if let Some(ObjectFocus::ReadOnly(index)) = object_focus {
3032 let (scene, offset) = scene.unwrap();
3033 let [x0, y0, x1, y1] = crate::translated(
3034 scene.read_only(Some(editor)).nth(index).unwrap().rect(),
3035 *offset,
3036 );
3037 let border = 2.0 / paint.scale;
3038 for rect in [
3039 [x0, y0, x1, y0 + border],
3040 [x0, y1 - border, x1, y1],
3041 [x0, y0, x0 + border, y1],
3042 [x1 - border, y0, x1, y1],
3043 ] {
3044 primitives.push(Primitive::Rect {
3045 rect,
3046 color: [0.25, 0.45, 0.7, 1.0],
3047 });
3048 }
3049 }
3050 Ok(primitives)
3051}
3052
3053/// Native picture handles sit on a selection border drawn 5 px outside the picture, named
3054/// by their direction from its center.
3055fn image_handles(rect: [f32; 4], pixel: f32) -> impl Iterator<Item = ([i8; 2], [f32; 2])> {
3056 let border = [
3057 rect[0] - 5.0 * pixel,
3058 rect[1] - 5.0 * pixel,
3059 rect[2] + 5.0 * pixel,
3060 rect[3] + 5.0 * pixel,
3061 ];
3062 [-1, 0, 1]
3063 .into_iter()
3064 .flat_map(|y| [-1, 0, 1].map(|x| [x, y]))
3065 .filter(|handle| *handle != [0, 0])
3066 .map(move |handle| {
3067 (
3068 handle,
3069 std::array::from_fn(|axis| {
3070 let [start, end] = [border[axis], border[axis + 2]];
3071 start + (end - start) * f32::from(handle[axis] + 1) * 0.5
3072 }),
3073 )
3074 })
3075}
3076
3077/// The handle of a picture at `rect` under `point`, each reaching `reach` pixels beyond its
3078/// drawn square.
3079fn image_handle_at(rect: [f32; 4], pixel: f32, point: [f32; 2], reach: f32) -> Option<[i8; 2]> {
3080 image_handles(rect, pixel)
3081 .find(|(_, center)| {
3082 (0..2).all(|axis| (point[axis] - center[axis]).abs() <= (5.0 + reach) * pixel)
3083 })
3084 .map(|(handle, _)| handle)
3085}
3086
3087fn handle_cursor(handle: [i8; 2]) -> Cursor {
3088 match handle {
3089 [0, 0] => Cursor::Move,
3090 [_, 0] => Cursor::EwResize,
3091 [0, _] => Cursor::NsResize,
3092 [x, y] if x == y => Cursor::NwseResize,
3093 _ => Cursor::NeswResize,
3094 }
3095}
3096
3097fn image_rect(origin: [f32; 2], size: [f32; 2]) -> [f32; 4] {
3098 [
3099 origin[0],
3100 origin[1],
3101 origin[0] + size[0],
3102 origin[1] + size[1],
3103 ]
3104}
3105
3106/// Drags a picture's `handle` by `delta`: edges stretch one axis, corners keep the aspect
3107/// ratio at the larger of the two scales, and the opposite side stays fixed.
3108fn resize_image(
3109 origin: [f32; 2],
3110 size: [f32; 2],
3111 handle: [i8; 2],
3112 delta: [f32; 2],
3113) -> ([f32; 2], [f32; 2]) {
3114 let mut scale: [f32; 2] = std::array::from_fn(|axis| {
3115 (size[axis] + f32::from(handle[axis]) * delta[axis]) / size[axis]
3116 });
3117 if !handle.contains(&0) {
3118 scale = [scale[0].max(scale[1]); 2];
3119 }
3120 let resized: [f32; 2] = std::array::from_fn(|axis| (size[axis] * scale[axis]).max(1.0));
3121 let origin = std::array::from_fn(|axis| {
3122 if handle[axis] < 0 {
3123 origin[axis] + size[axis] - resized[axis]
3124 } else {
3125 origin[axis]
3126 }
3127 });
3128 (origin, resized)
3129}
3130
3131fn append_image_chrome(rect: [f32; 4], pixel: f32, primitives: &mut Vec<Primitive<'_>>) {
3132 let mut tint = crate::gpu::colorref(0x00e0d2e6);
3133 // Native pictures take this tint at 25% in sRGB; 10% in linear light matches it.
3134 tint[3] = 0.1;
3135 primitives.push(Primitive::Rect { rect, color: tint });
3136 primitives.push(Primitive::RoundedRect {
3137 rect: [
3138 rect[0] - 5.0 * pixel,
3139 rect[1] - 5.0 * pixel,
3140 rect[2] + 5.0 * pixel,
3141 rect[3] + 5.0 * pixel,
3142 ],
3143 radius: [0.0; 2],
3144 stroke: Some(Stroke::Dashed(pixel)),
3145 color: crate::gpu::colorref(0x00ff9a31),
3146 });
3147 for (handle, [x, y]) in image_handles(rect, pixel) {
3148 let (half, radius) = if handle.contains(&0) {
3149 (3.5, 0.0)
3150 } else {
3151 (4.0, 4.0)
3152 };
3153 let rect = [
3154 x - half * pixel,
3155 y - half * pixel,
3156 x + half * pixel,
3157 y + half * pixel,
3158 ];
3159 primitives.push(Primitive::RoundedRect {
3160 rect,
3161 radius: [radius * pixel; 2],
3162 stroke: None,
3163 color: crate::gpu::colorref(0x00ffefe7),
3164 });
3165 primitives.push(Primitive::RoundedRect {
3166 rect,
3167 radius: [radius * pixel; 2],
3168 stroke: Some(Stroke::Solid(pixel)),
3169 color: crate::gpu::colorref(0x00dea67b),
3170 });
3171 }
3172}
3173
3174/// How far left and up OneNote 2010 scrolls to show an outline: 7.5 pt past its text, to
3175/// its list markers, 0.75 pt into its tag column's first slot, and 6 pt above it.
3176fn reach(outline: &TextOutline) -> [f32; 2] {
3177 let shaped = outline.shaped();
3178 let column = shaped.tag_column_offset();
3179 let left = shaped
3180 .paragraphs
3181 .iter()
3182 .flat_map(|paragraph| {
3183 let markers = paragraph.markers.iter().map(|(_, [x, _])| *x);
3184 std::iter::once(paragraph.origin[0] - 7.5).chain(markers)
3185 })
3186 .chain(
3187 shaped
3188 .tags()
3189 .map(|(_, origin, _)| origin[0] + column + 0.75),
3190 )
3191 .fold(-7.5, f32::min);
3192 let [x, y] = outline.origin();
3193 [x + left, y - 6.0]
3194}
3195
3196fn snap_to_grid(point: [f32; 2], margin: [f32; 2]) -> [f32; 2] {
3197 std::array::from_fn(|axis| {
3198 let offset = margin[axis];
3199 let cell = (point[axis] - offset) / 18.0;
3200 let nearest = cell.round();
3201 // Native midpoints remain free; account for the source coordinate's float precision.
3202 if ((cell - nearest).abs() - 0.5).abs() <= f32::EPSILON * cell.abs().max(1.0) {
3203 point[axis]
3204 } else {
3205 nearest * 18.0 + offset
3206 }
3207 })
3208}
3209
3210fn outline_chrome(outline: &TextOutline, pixel: f32) -> ([f32; 4], f32) {
3211 let bounds = outline.bounds();
3212 if outline.title {
3213 // The title's box scales with the page, 5 px from its text at 100% (6 px more to the
3214 // left, 2 px less to the right).
3215 let inset = 3.75;
3216 let (_, paragraph) = outline.layouts().last().unwrap();
3217 let bottom = outline.origin()[1] + paragraph.origin[1] + paragraph.text.height();
3218 let top = bounds.y0 as f32 - inset;
3219 return (
3220 [
3221 bounds.x0 as f32 - inset - 4.5,
3222 top,
3223 bounds.x1 as f32 + inset - 1.5,
3224 bottom + inset,
3225 ],
3226 top,
3227 );
3228 }
3229 // Native chrome combines page-scaled gutters with a fixed screen inset.
3230 let inset = 5.0 * pixel;
3231 let body_top = bounds.y0 as f32 - inset;
3232 // OneNote widens the box leftward to hold the tag column and list markers; text and right
3233 // edge stay.
3234 let shaped = outline.shaped();
3235 let column = shaped.tag_column_offset() + crate::outline::ParagraphTag::INSET
3236 - crate::outline::ParagraphTag::SIZE;
3237 let left = shaped
3238 .paragraphs
3239 .iter()
3240 .flat_map(|paragraph| paragraph.markers.iter().map(|(_, [x, _])| x + 7.5))
3241 .chain(shaped.tags().map(|(_, origin, _)| origin[0] + column))
3242 .fold(0.0, f32::min);
3243 (
3244 [
3245 bounds.x0 as f32 + left - 7.5 - inset,
3246 body_top - HANDLE_HEIGHT,
3247 bounds.x1 as f32 + inset,
3248 bounds.y1 as f32 + HANDLE_HEIGHT + inset,
3249 ],
3250 body_top,
3251 )
3252}
3253
3254/// `selected` greys the body, as OneNote marks an outline Select All has selected whole.
3255fn append_outline_chrome(
3256 outline: &TextOutline,
3257 origin: [f32; 2],
3258 pixel: f32,
3259 paper: crate::gpu::Paper,
3260 selected: bool,
3261 primitives: &mut Vec<Primitive<'_>>,
3262) {
3263 let [x, y] = origin;
3264 let (bounds, body_top) = outline_chrome(outline, pixel);
3265 let [dx, dy] = [x - outline.origin()[0], y - outline.origin()[1]];
3266 let [left, top, right, bottom] = crate::translated(bounds, [dx, dy]);
3267 let body_top = body_top + dy;
3268 if outline.title {
3269 append_title_frame([left, top, right, bottom], pixel, paper, primitives);
3270 return;
3271 }
3272 primitives.push(Primitive::RoundedRect {
3273 rect: [left, top, right, body_top],
3274 radius: [3.0 * pixel; 2],
3275 stroke: None,
3276 color: paper.shade(crate::gpu::colorref(0x00e8ebed)),
3277 });
3278 primitives.push(Primitive::RoundedRect {
3279 rect: [right - 9.0 * pixel, top, right, body_top],
3280 radius: [3.0 * pixel; 2],
3281 stroke: None,
3282 color: paper.shade(crate::gpu::colorref(0x00e5dee7)),
3283 });
3284 if selected {
3285 primitives.push(Primitive::RoundedRect {
3286 rect: [left, body_top, right, bottom],
3287 radius: [3.0 * pixel; 2],
3288 stroke: None,
3289 color: paper.shade(crate::gpu::colorref(0x00f0f0f0)),
3290 });
3291 }
3292 primitives.push(Primitive::RoundedRect {
3293 rect: [left, top, right, bottom],
3294 radius: [3.0 * pixel; 2],
3295 stroke: Some(Stroke::Solid(pixel)),
3296 color: paper.shade(crate::gpu::colorref(0x00d9cfd8)),
3297 });
3298 let middle = (top + body_top) * 0.5;
3299 for offset in [-3.0, 0.0, 3.0] {
3300 let center = (left + right) * 0.5 + offset * pixel;
3301 primitives.push(Primitive::RoundedRect {
3302 rect: [
3303 center - pixel * 0.5,
3304 middle - pixel * 0.5,
3305 center + pixel * 0.5,
3306 middle + pixel * 0.5,
3307 ],
3308 radius: [pixel * 0.5; 2],
3309 stroke: None,
3310 color: paper.shade(crate::gpu::colorref(0x00b4a5b4)),
3311 });
3312 }
3313 for column in [0.0, 1.0, 2.0] {
3314 let half = (column + 0.5) * pixel;
3315 for x in [
3316 right - (8.0 - column) * pixel,
3317 right - (2.0 + column) * pixel,
3318 ] {
3319 primitives.push(Primitive::Rect {
3320 rect: [x, middle - half, x + pixel, middle + half],
3321 color: paper.shade(crate::gpu::colorref(0x00b4a5b4)),
3322 });
3323 }
3324 }
3325}
3326
3327fn append_title_frame(
3328 [left, top, right, bottom]: [f32; 4],
3329 pixel: f32,
3330 paper: Paper,
3331 primitives: &mut Vec<Primitive<'_>>,
3332) {
3333 primitives.push(Primitive::RoundedRect {
3334 rect: [left, top, right, bottom],
3335 radius: [4.5, (bottom - top) * 0.5],
3336 // A page pixel at 100% zoom, so it scales when zoomed in but stays a screen pixel
3337 // when zoomed out.
3338 stroke: Some(Stroke::Dashed(pixel.max(0.75))),
3339 color: paper.shade(crate::gpu::colorref(0x007f7f7f)),
3340 });
3341}
3342
3343/// `editor` paints its selection and caret in the focused outline; `selected` highlights all
3344/// the text of another.
3345fn append_outline<'a>(
3346 editor: Option<&'a CanvasEditor>,
3347 selected: bool,
3348 geometry: crate::editor::OutlineGeometry<'a>,
3349 origin: [f32; 2],
3350 paint: Paint,
3351 primitives: &mut Vec<Primitive<'a>>,
3352) -> Result<()> {
3353 let outline = geometry.source;
3354 let [x, y] = origin;
3355 let Paint {
3356 caret,
3357 scale,
3358 device_origin,
3359 pixel,
3360 colors,
3361 visible,
3362 found,
3363 played,
3364 spelling,
3365 ..
3366 } = paint;
3367 let rows = [visible[0] - y, visible[1] - y];
3368 geometry
3369 .shaped
3370 .append_table_primitives(primitives, origin, colors.paper);
3371 geometry
3372 .shaped
3373 .append_block_tag_primitives(primitives, origin, paint.tag_art);
3374 geometry
3375 .shaped
3376 .append_background_primitives(primitives, origin, rows, colors.paper);
3377 // OneNote's search highlight: yellow, deepened to gold under light text on dark paper.
3378 let [red, green, blue, _] = colors.paper.color;
3379 let marker = if red + green + blue < 1.5 {
3380 draw::srgb(0x7a, 0x62, 0x00)
3381 } else {
3382 draw::srgb(0xff, 0xe6, 0x00)
3383 };
3384 for (_, selection) in found.iter().filter(|(id, _)| *id == outline.id) {
3385 for rect in geometry.range_rects(*selection)? {
3386 primitives.push(Primitive::Rect {
3387 rect: [
3388 rect.x0 as f32 + x,
3389 rect.y0 as f32 + y,
3390 rect.x1 as f32 + x,
3391 rect.y1 as f32 + y,
3392 ],
3393 color: marker,
3394 });
3395 }
3396 }
3397 let mut highlight = match editor {
3398 Some(editor) => geometry.range_rects(editor.selection())?,
3399 None if selected => geometry.range_rects(outline.whole())?,
3400 None => Vec::new(),
3401 };
3402 // OneNote highlights the note playing as it highlights a selection.
3403 if let Some((_, selection)) = played.filter(|(id, _)| *id == outline.id) {
3404 highlight.extend(geometry.range_rects(*selection)?);
3405 }
3406 for rect in highlight {
3407 primitives.push(Primitive::Rect {
3408 rect: [
3409 rect.x0 as f32 + x,
3410 rect.y0 as f32 + y,
3411 rect.x1 as f32 + x,
3412 rect.y1 as f32 + y,
3413 ],
3414 color: colors.selection,
3415 });
3416 }
3417 for (index, paragraph) in geometry.shaped.visible(rows) {
3418 geometry.shaped.append_paragraph_primitives(
3419 index,
3420 paragraph,
3421 origin,
3422 colors.paper.ink,
3423 paint.tag_art,
3424 primitives,
3425 );
3426 }
3427 if let Some(spelling) = spelling {
3428 let typing = editor
3429 .and_then(CanvasEditor::typing)
3430 .filter(|(id, _)| *id == outline.id)
3431 .map(|(_, at)| at);
3432 append_marks(
3433 spelling,
3434 geometry,
3435 typing,
3436 origin,
3437 rows,
3438 [scale, device_origin[0], device_origin[1]],
3439 pixel,
3440 primitives,
3441 )?;
3442 }
3443 if let Some(editor) = editor {
3444 for rect in editor
3445 .marked_range()
3446 .map(|range| {
3447 geometry.range_rects(Selection {
3448 positions: [range.start, range.end],
3449 affinities: [parley::Affinity::Downstream, parley::Affinity::Upstream],
3450 })
3451 })
3452 .transpose()?
3453 .unwrap_or_default()
3454 {
3455 primitives.push(Primitive::Rect {
3456 rect: [
3457 rect.x0 as f32 + x,
3458 rect.y1 as f32 + y - 1.0 / scale,
3459 rect.x1 as f32 + x,
3460 rect.y1 as f32 + y,
3461 ],
3462 color: colors.paper.ink,
3463 });
3464 }
3465 let [anchor, focus] = editor.selection().positions;
3466 if caret > 0.0 && anchor == focus {
3467 // AppKit centres the caret on the insertion point.
3468 let selection = editor.selection();
3469 let rect = geometry.caret_at(selection.positions[1], selection.affinities[1], 0.0)?;
3470 let half = edit::CARET_WIDTH / 2.0 * pixel;
3471 let middle = rect.x0 as f32 + x;
3472 primitives.push(Primitive::RoundedRect {
3473 rect: [
3474 middle - half,
3475 rect.y0 as f32 + y,
3476 middle + half,
3477 rect.y1 as f32 + y,
3478 ],
3479 radius: [half; 2],
3480 stroke: None,
3481 color: edit::caret_color(colors.caret, colors.paper.color, caret),
3482 });
3483 }
3484 }
3485 Ok(())
3486}
3487
3488/// OneNote's red zigzag under each marked word of the paragraphs between outline-local
3489/// `rows`: a pixel thick, two high and four long, 1.5 to 3.5 pixels below the baseline, laid
3490/// on the device grid `[scale, x, y]` so it stays hard-edged. The word ending at the caret of
3491/// a run of typing, `typing`, is still being written and goes unmarked.
3492#[allow(clippy::too_many_arguments)]
3493fn append_marks(
3494 spelling: &crate::spelling::Spelling,
3495 geometry: crate::editor::OutlineGeometry<'_>,
3496 typing: Option<crate::document::TextPosition>,
3497 origin: [f32; 2],
3498 rows: [f32; 2],
3499 [scale, device_x, device_y]: [f32; 3],
3500 pixel: f32,
3501 primitives: &mut Vec<Primitive<'_>>,
3502) -> Result<()> {
3503 let outline = geometry.source;
3504 let color = draw::srgb(0xff, 0x00, 0x00);
3505 let dot = (pixel * scale).round().max(1.0);
3506 let shown = geometry.layouts().filter(|(_, paragraph)| {
3507 paragraph.origin[1] + paragraph.text.height() >= rows[0] && paragraph.origin[1] <= rows[1]
3508 });
3509 for (index, _) in shown {
3510 let Some(paragraph) = outline.document().paragraph(index) else {
3511 continue;
3512 };
3513 for mark in spelling.marks(paragraph) {
3514 let start = paragraph.utf16_offset(mark.range.start)?;
3515 let end = paragraph.utf16_offset(mark.range.end)?;
3516 if typing.is_some_and(|at| at.paragraph == index && at.offset == end) {
3517 continue;
3518 }
3519 for [left, right, baseline] in geometry.underlines(index, start..end)? {
3520 let right = ((origin[0] + right) * scale + device_x).round();
3521 let mut x = ((origin[0] + left) * scale + device_x).round();
3522 let top = ((origin[1] + baseline) * scale + device_y).round() + dot;
3523 for step in [1.0, 2.0, 1.0, 0.0].into_iter().cycle() {
3524 if x >= right {
3525 break;
3526 }
3527 let y = top + step * dot;
3528 primitives.push(Primitive::Rect {
3529 rect: [
3530 (x - device_x) / scale,
3531 (y - device_y) / scale,
3532 (x + dot - device_x) / scale,
3533 (y + dot - device_y) / scale,
3534 ],
3535 color,
3536 });
3537 x += dot;
3538 }
3539 }
3540 }
3541 }
3542 Ok(())
3543}
3544
3545/// Marked word `mark` of paragraph `index` of `outline`, with its corrections.
3546fn correction(
3547 spelling: &crate::spelling::Spelling,
3548 outline: &TextOutline,
3549 index: usize,
3550 mark: &crate::spelling::Mark,
3551) -> Option<Correction> {
3552 let paragraph = outline.document().paragraph(index)?;
3553 let text = paragraph.text();
3554 let word = text[mark.range.clone()].to_owned();
3555 let start = if mark.repeated {
3556 text[..mark.range.start].trim_end().len()
3557 } else {
3558 mark.range.start
3559 };
3560 let position = |byte| {
3561 Some(crate::document::TextPosition {
3562 paragraph: index,
3563 offset: paragraph.utf16_offset(byte).ok()?,
3564 })
3565 };
3566 let suggestions = if mark.repeated {
3567 Vec::new()
3568 } else {
3569 let language = paragraph
3570 .spans()
3571 .iter()
3572 .find(|span| span.end > mark.range.start)
3573 .and_then(|span| span.format.language);
3574 let mut suggestions = spelling.suggest(&word, language);
3575 suggestions.truncate(5);
3576 suggestions
3577 };
3578 Some(Correction {
3579 suggestions,
3580 repeated: mark.repeated,
3581 outline: outline.id,
3582 range: [position(start)?, position(mark.range.end)?].into(),
3583 word,
3584 })
3585}