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