1//! A canvas page behind a C surface for native mobile shells: notebooks and their sections
2//! (`library`), one page drawn into a `CAMetalLayer`, and the active outline's text as the
3//! flat UTF-16 model UIKit's `UITextInput` speaks (see `TextOutline::shown_text`). Every
4//! edit a view takes is stored at once through its section.
5//!
6//! Lengths and positions are in the host's points; the view converts through its display
7//! scale. Calls returning `bool` report whether the page or selection changed, after which
8//! the host redraws and rereads `sb_view_content`.
9
10mod crash;
11mod library;
12mod recording;
13#[cfg(target_os = "ios")]
14mod spell;
15
16use library::Shared;
17pub use library::{Library, Section, Share};
18
19use canvas::{
20 date::DateField,
21 document::TextPosition,
22 editor::{Formatting, NoteTag, Selection, TextOutline, Toggle},
23 gpu::{Paper, Viewport, page::PageScene},
24 interaction::{
25 Hit, ObjectFocus, PageView, Request, Response, TextColors,
26 ink::{Tool, pens},
27 },
28 layout::TextEngine,
29};
30use draw::edit::{Key, NamedKey, SelectionUnit};
31use onestore::{
32 ExGuid,
33 op::{Edit, Op, PageOp},
34 page::{Page, ink::ShapeKind},
35};
36use parley::{Affinity, BoundingBox};
37use std::{
38 error::Error,
39 ffi::{CStr, CString, c_char, c_void},
40 mem::ManuallyDrop,
41 sync::{
42 Arc, LazyLock, Mutex, MutexGuard,
43 atomic::{AtomicBool, Ordering},
44 },
45 task::{Wake, Waker},
46 time::{Duration, Instant},
47};
48
49type Result<T> = std::result::Result<T, Box<dyn Error>>;
50
51/// Host points per document point at the zoom the host calls 100%: OneNote's 100% (96
52/// pixels per inch) enlarged by 15%.
53const POINT: f32 = 96.0 / 72.0 * 1.15;
54
55/// Set off the main thread when a picture raster lands and the page should be drawn again.
56#[derive(Default)]
57struct Frame(AtomicBool);
58
59impl Wake for Frame {
60 fn wake(self: Arc<Self>) {
61 self.0.store(true, Ordering::Release);
62 }
63}
64
65fn report<T>(result: Result<T>) -> Option<T> {
66 result.map_err(|error| eprintln!("snowbound: {error}")).ok()
67}
68
69fn moved(response: Response) -> bool {
70 response.changed || response.moved
71}
72
73/// What a touch lands on, so the host routes it before its gesture starts.
74#[repr(u8)]
75#[derive(Debug, PartialEq)]
76enum Target {
77 /// Empty page, a picture, a check box or the date: the canvas takes a tap.
78 Page = 0,
79 /// The text taking input, where the platform's text interaction works.
80 ActiveText = 1,
81 /// Another outline's text, which a touch focuses before the platform's text interaction
82 /// takes over.
83 Text = 2,
84 /// The focused outline's move or width grip, the selected picture and its handles, or
85 /// picked drawings.
86 Grip = 3,
87}
88
89/// Formatting the host shows and applies, by bit in `sb_view_format` and by number in
90/// `sb_view_apply`; OneNote's default tags follow from 16, in their list's order.
91const FORMATS: [Formatting; 11] = [
92 Formatting::Toggle(Toggle::Bold),
93 Formatting::Toggle(Toggle::Italic),
94 Formatting::Toggle(Toggle::Underline),
95 Formatting::Toggle(Toggle::Strikethrough),
96 Formatting::Bullets,
97 Formatting::Numbering,
98 Formatting::Indent,
99 Formatting::Outdent,
100 Formatting::RemoveTags,
101 Formatting::Clear,
102 // OneNote's first highlighter colour, yellow.
103 Formatting::Highlight(Some(0x0000_ffff)),
104];
105
106/// The shape tools, by number in `sb_view_set_tool`.
107const SHAPES: [ShapeKind; 4] = [
108 ShapeKind::Line,
109 ShapeKind::Arrow,
110 ShapeKind::Rectangle,
111 ShapeKind::Ellipse,
112];
113
114/// The page and its text model, apart from the surface it is drawn on.
115struct Canvas {
116 page: PageView,
117 /// The object space of the page shown, which its edits name.
118 space: ExGuid,
119 paper: Paper,
120 /// Device pixels per host point.
121 scale: f32,
122 /// The page date field a tap asked to change, for the host's picker.
123 asked: Option<DateField>,
124 /// The recording a tap asked to play, and from when.
125 play: Option<(onestore::page::Attachment, u32)>,
126 /// The recording playing, whose linked notes See Playback follows.
127 playing: Option<[u8; 16]>,
128}
129
130impl Canvas {
131 /// The page on `paper` as OneNote 2010 prints it, footers naming `section`.
132 fn pdf(&self, paper: [f32; 2], section: &str) -> Option<Vec<u8>> {
133 let page = self.page.editor.page().ok()?;
134 let mut engine = pooled_engine();
135 let setup = canvas::print::Setup {
136 paper,
137 fit_width: true,
138 footer: canvas::print::Footer::SectionAndPage,
139 };
140 let pdf = canvas::print::pdf(vec![(section.to_owned(), vec![page])], &mut engine, &setup);
141 pool_engine(engine);
142 pdf.ok()
143 }
144
145 fn new(space: ExGuid, page: Page, pixels: [u32; 2], scale: f32) -> Result<Self> {
146 let mut engine = pooled_engine();
147 let (scene, editor) = PageScene::from_page(page, &mut engine)?;
148 let mut page = PageView::new(
149 editor,
150 engine,
151 Some((scene, [0.0; 2])),
152 pixels,
153 scale,
154 Duration::from_millis(350),
155 );
156 page.touch = true;
157 page.host_viewport = true;
158 let markdown;
159 (page.snap_to_grid, page.editor.default_font, markdown) = options().clone();
160 page.editor.markdown = markdown.then(|| {
161 notebook::sidecar::themes::built_in()
162 .swap_remove(0)
163 .sheet(None)
164 });
165 Ok(Self {
166 page,
167 space,
168 paper: Paper::WHITE,
169 scale,
170 asked: None,
171 play: None,
172 playing: None,
173 })
174 }
175
176 /// The platform's text interaction draws the caret and selection.
177 fn colors(&self) -> TextColors {
178 TextColors {
179 caret: [0.0; 4],
180 selection: [0.0; 4],
181 paper: self.page_paper(),
182 }
183 }
184
185 /// The paper the page lies on: the appearance's, in the page's colour.
186 fn page_paper(&self) -> Paper {
187 self.paper.colored(self.page.editor.page_color())
188 }
189
190 /// The desktop's dark page, or white paper.
191 fn set_dark(&mut self, dark: bool) {
192 self.paper = if dark {
193 Paper {
194 color: draw::srgb(0x1f, 0x20, 0x22),
195 ink: draw::srgb(0xe6, 0xe6, 0xe6),
196 }
197 } else {
198 Paper::WHITE
199 };
200 }
201
202 /// What the page took since the last call as one edit, or none when it took nothing.
203 fn edit(&mut self) -> Result<Option<Edit>> {
204 let ops = self.page.editor.take_ops()?;
205 Ok((!ops.is_empty()).then(|| page_edit(self.space, ops)))
206 }
207
208 fn device(&self, point: [f32; 2]) -> [f32; 2] {
209 point.map(|value| value * self.scale)
210 }
211
212 /// The page content's extent in document points: the scroll limits of a view with no
213 /// size at scale 1.
214 fn content(&mut self) -> [f32; 4] {
215 let viewport = self.page.viewport;
216 self.page.viewport = Viewport {
217 size: [0; 2],
218 scale: 1.0,
219 origin: [0.0; 2],
220 };
221 let scroll = self.page.scroll();
222 self.page.viewport = viewport;
223 [scroll.min[0], scroll.min[1], scroll.max[0], scroll.max[1]]
224 }
225
226 fn set_transform(&mut self, zoom: f32, corner: [f32; 2]) {
227 self.page.viewport.scale = self.scale * POINT * zoom;
228 self.page.viewport.origin = corner.map(|value| -value * self.scale * zoom);
229 }
230
231 fn outline(&self, id: ExGuid) -> Option<&TextOutline> {
232 let editor = &self.page.editor;
233 editor
234 .visible_outlines()
235 .chain(editor.caret_outline())
236 .find(|outline| outline.id == id)
237 }
238
239 fn target(&self, point: [f32; 2]) -> Target {
240 let device = self.device(point);
241 let [x, y] = self.page.viewport.document_point(device);
242 let on_ink = self
243 .page
244 .editor
245 .ink_extent(self.page.ink_selection())
246 .is_some_and(|[x0, y0, x1, y1]| (x0..=x1).contains(&x) && (y0..=y1).contains(&y));
247 if self.page.inserting_space() || on_ink {
248 return Target::Grip;
249 }
250 match self.page.hit(device) {
251 Some(Hit::Handle { .. } | Hit::Resize { .. } | Hit::Column { .. }) => Target::Grip,
252 Some(Hit::Image { id, .. })
253 if self.page.object_focus() == Some(ObjectFocus::Image(id)) =>
254 {
255 Target::Grip
256 }
257 Some(Hit::Text { id, point })
258 if !self
259 .outline(id)
260 .is_some_and(|outline| outline.contains_extension(point)) =>
261 {
262 if id == self.page.editor.active_outline().id {
263 Target::ActiveText
264 } else {
265 Target::Text
266 }
267 }
268 _ => Target::Page,
269 }
270 }
271
272 /// A finger down at `point`: the start of a tap or of a grip drag.
273 fn press(&mut self, point: [f32; 2]) -> Result<bool> {
274 let point = self.device(point);
275 let changed = moved(self.page.pointer_moved(point)?);
276 let pressed = self.page.pointer_pressed(Instant::now())?;
277 Ok(self.respond(pressed) || changed)
278 }
279
280 /// Whether `response` moved the page, keeping the date field it asks to change or the
281 /// recording it asks to play.
282 fn respond(&mut self, response: Response) -> bool {
283 let changed = response.changed || response.moved;
284 match response.request {
285 Some(Request::EditDate(field)) => self.asked = Some(field),
286 Some(Request::Play { file, at_ms }) => self.play = Some((file, at_ms)),
287 _ => {}
288 }
289 changed
290 }
291
292 fn drag(&mut self, point: [f32; 2]) -> Result<bool> {
293 Ok(moved(self.page.pointer_moved(self.device(point))?))
294 }
295
296 fn release(&mut self) -> Result<bool> {
297 let released = self.page.pointer_released()?;
298 let changed = self.respond(released);
299 // A finger leaves no hover behind.
300 let _ = self.page.pointer_left();
301 Ok(changed)
302 }
303
304 /// The outline nearest `point`, framed as its chrome is, in points at 100% zoom.
305 fn block(&self, point: [f32; 2]) -> Option<[f32; 4]> {
306 let [x, y] = self
307 .page
308 .viewport
309 .document_point(self.device(point))
310 .map(f64::from);
311 let distance = |rect: &BoundingBox| {
312 let dx = (rect.x0 - x).max(x - rect.x1).max(0.0);
313 let dy = (rect.y0 - y).max(y - rect.y1).max(0.0);
314 dx.hypot(dy)
315 };
316 let rect = self
317 .page
318 .editor
319 .visible_outlines()
320 .map(TextOutline::bounds)
321 .min_by(|a, b| distance(a).total_cmp(&distance(b)))?;
322 Some([rect.x0, rect.y0, rect.x1, rect.y1].map(|value| value as f32 * POINT))
323 }
324
325 fn active(&self) -> &TextOutline {
326 self.page.editor.active_outline()
327 }
328
329 fn text(&self, range: [u32; 2]) -> String {
330 let units: Vec<u16> = self.active().shown_text().encode_utf16().collect();
331 let [start, end] = range.map(|offset| (offset as usize).min(units.len()));
332 String::from_utf16_lossy(&units[start..end.max(start)])
333 }
334
335 fn length(&self) -> u32 {
336 self.active().shown_text().encode_utf16().count() as u32
337 }
338
339 fn selection(&self) -> Result<[u32; 2]> {
340 let [anchor, focus] = self.page.editor.selection().positions;
341 let outline = self.active();
342 Ok([
343 outline.utf16_offset(anchor.min(focus))?,
344 outline.utf16_offset(anchor.max(focus))?,
345 ])
346 }
347
348 fn positions(&self, range: [u32; 2]) -> Result<[TextPosition; 2]> {
349 let outline = self.active();
350 Ok([
351 outline.utf16_position(range[0])?,
352 outline.utf16_position(range[1])?,
353 ])
354 }
355
356 fn select(&mut self, range: [u32; 2]) -> Result<()> {
357 let positions = self.positions(range)?;
358 Ok(self.page.editor.select(Selection::from(positions))?)
359 }
360
361 fn marked(&self) -> Result<Option<[u32; 2]>> {
362 let outline = self.active();
363 self.page
364 .editor
365 .marked_range()
366 .map(|range| {
367 Ok([
368 outline.utf16_offset(range.start)?,
369 outline.utf16_offset(range.end)?,
370 ])
371 })
372 .transpose()
373 }
374
375 /// Marked text with its selection in UTF-16 units of `text`.
376 fn set_marked(&mut self, text: String, selected: [u32; 2]) -> Result<bool> {
377 let byte = |utf16: u32| {
378 let mut units = 0;
379 text.char_indices()
380 .find(|(_, char)| {
381 let found = units >= utf16;
382 units += char.len_utf16() as u32;
383 found
384 })
385 .map_or(text.len(), |(byte, _)| byte)
386 };
387 let cursor = (byte(selected[0]), byte(selected[1]));
388 Ok(moved(self.page.compose(text, Some(cursor))?))
389 }
390
391 /// Types `text`; a lone newline or tab is the Return or Tab key.
392 fn insert(&mut self, text: String) -> Result<bool> {
393 Ok(moved(match text.as_str() {
394 "\n" => self.page.key(&Key::Named(NamedKey::Enter), None)?,
395 "\t" => self.page.key(&Key::Named(NamedKey::Tab), None)?,
396 _ => self.page.commit_text(text)?,
397 }))
398 }
399
400 /// The formatting the selection has, as `FORMATS` and default tag bits.
401 fn format_bits(&self) -> Result<u64> {
402 let state = self.page.editor.format_state()?;
403 let mut bits = 0;
404 for (bit, format) in FORMATS.iter().enumerate() {
405 let on = match format {
406 Formatting::Toggle(toggle) => state.toggles.contains(toggle),
407 Formatting::Bullets => state.bullets,
408 Formatting::Numbering => state.numbering,
409 _ => false,
410 };
411 bits |= u64::from(on) << bit;
412 }
413 for (place, tag) in NoteTag::defaults().into_iter().enumerate() {
414 let tagged = state.tags.contains(&(tag, place as u16));
415 bits |= u64::from(tagged) << (16 + place);
416 }
417 Ok(bits)
418 }
419
420 fn format(&mut self, command: u8) -> Result<bool> {
421 let command = match command {
422 16.. => {
423 let place = command - 16;
424 let tag = NoteTag::defaults()
425 .into_iter()
426 .nth(usize::from(place))
427 .ok_or("No such tag")?;
428 Formatting::Tag(tag, place.into())
429 }
430 _ => FORMATS
431 .get(usize::from(command))
432 .ok_or("No such formatting")?
433 .clone(),
434 };
435 Ok(moved(self.page.format(command)?))
436 }
437
438 /// The title outline, where the page has an editable one.
439 fn title(&self) -> Option<&TextOutline> {
440 self.page
441 .editor
442 .outlines()
443 .iter()
444 .find(|outline| outline.title)
445 }
446
447 /// Puts the caret at the end of the page title.
448 fn focus_title(&mut self) -> Result<bool> {
449 let Some(id) = self.title().map(|title| title.id) else {
450 return Ok(false);
451 };
452 self.page.editor.focus_outline(id)?;
453 let end = self.length();
454 self.select([end, end])?;
455 Ok(true)
456 }
457
458 /// Selects the first match of `query` on the page, as the search that found it matches.
459 fn find(&mut self, query: &str) -> Result<bool> {
460 let query = canvas::search::Query::new(query);
461 let Some(&(id, selection)) =
462 canvas::search::page_matches(&self.page.editor, &query).first()
463 else {
464 return Ok(false);
465 };
466 self.page.editor.focus_outline(id)?;
467 self.page.editor.select(selection)?;
468 Ok(true)
469 }
470
471 /// Selects paragraph `id`, as a Tags Summary entry shows it.
472 fn select_paragraph(&mut self, id: ExGuid) -> Result<bool> {
473 let Some((outline, selection)) = canvas::search::paragraph_match(&self.page.editor, id)
474 else {
475 return Ok(false);
476 };
477 self.page.editor.focus_outline(outline)?;
478 self.page.editor.select(selection)?;
479 Ok(true)
480 }
481
482 /// The page's text, outline by outline, as the desktop puts a refused edit's on the
483 /// clipboard.
484 fn page_text(&self) -> String {
485 let editor = &self.page.editor;
486 editor
487 .visible_outlines()
488 .chain(editor.caret_outline())
489 .map(|outline| {
490 outline
491 .document()
492 .paragraphs()
493 .map(canvas::search::shown)
494 .collect::<Vec<_>>()
495 .join("\n")
496 })
497 .filter(|text| !text.trim().is_empty())
498 .collect::<Vec<_>>()
499 .join("\n\n")
500 }
501
502 /// Gives the page `color`, a COLORREF, and rule lines `ruled` of `RULE_LINES`, or none.
503 fn set_paper(&mut self, color: Option<u32>, ruled: Option<usize>) -> Result<bool> {
504 let rules = ruled
505 .and_then(|index| canvas::template::RULE_LINES.get(index))
506 .map(|(_, lines)| *lines);
507 Ok(moved(self.page.set_paper(color, rules, None)?))
508 }
509
510 /// Puts template `name`'s art behind the page in place of any it had, or none.
511 fn set_art(&mut self, name: Option<&str>) -> Result<bool> {
512 let art = match name {
513 Some(name) => canvas::template::find(name)
514 .ok_or("No such template")?
515 .pictures()?,
516 None => Vec::new(),
517 };
518 let editor = &self.page.editor;
519 let (color, rules) = (editor.page_color(), editor.rule_lines());
520 Ok(moved(self.page.set_paper(color, rules, Some(art))?))
521 }
522
523 /// Puts a picture of `size` points at the caret.
524 fn insert_picture(&mut self, bytes: &[u8], size: [f32; 2]) -> Result<bool> {
525 Ok(moved(self.page.insert_picture(bytes.to_vec(), size)?))
526 }
527
528 /// Gives the page date `seconds` since 1970 (keeping the stored fraction of a second),
529 /// titled `text`: the long date and the short time as the host formats them.
530 fn change_date(&mut self, seconds: i64, text: [String; 2]) -> Result<bool> {
531 let date = self.page.editor.date().ok_or("The page has no date")?;
532 let timestamp = u64::try_from(seconds + 11_644_473_600)?
533 .checked_mul(10_000_000)
534 .and_then(|ticks| ticks.checked_add(date.timestamp() % 10_000_000))
535 .ok_or("The date is out of range")?;
536 Ok(moved(self.page.change_date(timestamp, text)?))
537 }
538
539 /// Picks what touches on the page do; see `sb_view_set_tool`.
540 fn set_tool(&mut self, tool: u8, detail: u8, section: u32) -> Result<()> {
541 let pens = pens(canvas::gpu::colorref(section));
542 let tool = match tool {
543 0 => Tool::Select,
544 1 => Tool::Pen(*pens.get(usize::from(detail)).ok_or("No such pen")?),
545 2 => Tool::Eraser,
546 3 => Tool::Lasso,
547 4 => Tool::Shape(
548 *SHAPES.get(usize::from(detail)).ok_or("No such shape")?,
549 pens[0].shape(),
550 ),
551 _ => return Err("No such tool".into()),
552 };
553 let _ = self.page.set_tool(tool);
554 Ok(())
555 }
556
557 /// The frame of the drawings the lasso or a tap picked.
558 fn ink_selection(&self) -> Option<[f32; 4]> {
559 self.page
560 .editor
561 .ink_extent(self.page.ink_selection())
562 .map(|rect| self.document_rect(rect))
563 }
564
565 /// A rectangle in the active outline's coordinates, in the view's points as `[x, y,
566 /// width, height]`.
567 fn view_rect(&self, rect: BoundingBox) -> [f32; 4] {
568 let [x, y] = self.active().origin();
569 let [x0, y0, x1, y1] = [rect.x0, rect.y0, rect.x1, rect.y1].map(|value| value as f32);
570 self.document_rect([x0 + x, y0 + y, x1 + x, y1 + y])
571 }
572
573 /// Page rectangle `[x0, y0, x1, y1]` in the view's points as `[x, y, width, height]`.
574 fn document_rect(&self, [x0, y0, x1, y1]: [f32; 4]) -> [f32; 4] {
575 let viewport = self.page.viewport;
576 [
577 (x0 * viewport.scale + viewport.origin[0]) / self.scale,
578 (y0 * viewport.scale + viewport.origin[1]) / self.scale,
579 (x1 - x0) * viewport.scale / self.scale,
580 (y1 - y0) * viewport.scale / self.scale,
581 ]
582 }
583
584 fn caret_rect(&self, offset: u32) -> Result<[f32; 4]> {
585 let position = self.active().utf16_position(offset)?;
586 let caret = self
587 .page
588 .editor
589 .caret_at(position, Affinity::Downstream, 1.0)?;
590 Ok(self.view_rect(caret))
591 }
592
593 /// The selection rectangles of a range, one or more per line.
594 fn range_rects(&self, range: [u32; 2]) -> Result<Vec<[f32; 4]>> {
595 let rects = self.active().range_rects(Selection {
596 positions: self.positions(range)?,
597 affinities: [Affinity::Downstream, Affinity::Upstream],
598 })?;
599 Ok(rects.into_iter().map(|rect| self.view_rect(rect)).collect())
600 }
601
602 fn closest(&self, point: [f32; 2]) -> Result<u32> {
603 let point = self.page.viewport.document_point(self.device(point));
604 let origin = self.active().origin();
605 let selection = self.page.editor.selection_at(
606 point[0] - origin[0],
607 point[1] - origin[1],
608 SelectionUnit::Grapheme,
609 )?;
610 Ok(self.active().utf16_offset(selection.positions[1])?)
611 }
612}
613
614/// The GPU every page view draws with, and the renderer whose glyph atlas they share.
615struct Gpu {
616 instance: wgpu::Instance,
617 adapter: wgpu::Adapter,
618 renderer: draw::Renderer,
619}
620
621/// Made with the first view's surface, whose adapter every later layer can use.
622static GPU: Mutex<Option<Gpu>> = Mutex::new(None);
623
624fn gpu() -> std::sync::MutexGuard<'static, Option<Gpu>> {
625 GPU.lock().unwrap_or_else(|error| error.into_inner())
626}
627
628/// Text engines of closed views, whose fonts and shaping caches the next view takes over
629/// rather than enumerating the system's fonts again.
630static ENGINES: Mutex<Vec<TextEngine>> = Mutex::new(Vec::new());
631
632fn pooled_engine() -> TextEngine {
633 ENGINES
634 .lock()
635 .ok()
636 .and_then(|mut engines| engines.pop())
637 .unwrap_or_default()
638}
639
640fn pool_engine(engine: TextEngine) {
641 if let Ok(mut engines) = ENGINES.lock() {
642 engines.push(engine);
643 }
644}
645
646/// The reader's Snap To Grid, Default font and Markdown Shortcuts, which every page opened
647/// from then on takes, and new pages' titles the font's face.
648static OPTIONS: LazyLock<Mutex<(bool, canvas::editor::DefaultFont, bool)>> =
649 LazyLock::new(|| Mutex::new((true, canvas::editor::DefaultFont::default(), true)));
650
651fn options() -> MutexGuard<'static, (bool, canvas::editor::DefaultFont, bool)> {
652 OPTIONS.lock().unwrap_or_else(|error| error.into_inner())
653}
654
655/// OneNote's Snap To Grid: taps, drags and shapes land on the placement grid.
656#[unsafe(no_mangle)]
657pub extern "C" fn sb_set_snap_to_grid(on: bool) {
658 options().0 = on;
659}
660
661/// OneNote's Default font: new text in `face` at `size` points, new titles in `face`.
662///
663/// # Safety
664/// `face` is NUL-terminated UTF-8.
665#[unsafe(no_mangle)]
666pub unsafe extern "C" fn sb_set_default_font(face: *const c_char, size: f32) {
667 options().1 = canvas::editor::DefaultFont {
668 face: string(face),
669 size,
670 color: None,
671 };
672}
673
674/// Markdown Shortcuts: typed Markdown becomes formatting.
675#[unsafe(no_mangle)]
676pub extern "C" fn sb_set_markdown_shortcuts(on: bool) {
677 options().2 = on;
678}
679
680pub struct View {
681 /// Taken apart on drop, which returns its text engine to `ENGINES`.
682 canvas: ManuallyDrop<Canvas>,
683 surface: wgpu::Surface<'static>,
684 config: wgpu::SurfaceConfiguration,
685 frame: Arc<Frame>,
686 section: Arc<Shared>,
687 /// A conflict page, which shows what is stored and takes no edits.
688 read_only: bool,
689 /// The column, in page points, a reading view reflows the page into; it takes no edits.
690 reading: Option<f32>,
691}
692
693impl View {
694 fn new(
695 layer: *mut c_void,
696 section: &Section,
697 space: ExGuid,
698 size: [f32; 2],
699 scale: f32,
700 ) -> Result<Self> {
701 let pixels = size.map(|side| (side * scale).round().max(1.0) as u32);
702 let mut gpu = gpu();
703 let instance = match &*gpu {
704 Some(gpu) => gpu.instance.clone(),
705 None => wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()),
706 };
707 // SAFETY: the host passes a live CAMetalLayer that outlives the view.
708 let surface = unsafe {
709 instance.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::CoreAnimationLayer(layer))
710 }?;
711 if gpu.is_none() {
712 let adapter =
713 pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
714 compatible_surface: Some(&surface),
715 ..Default::default()
716 }))?;
717 // The simulator's adapter falls short of wgpu's default limits.
718 let (device, queue) =
719 pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
720 required_limits: adapter.limits(),
721 ..Default::default()
722 }))?;
723 let format = surface
724 .get_default_config(&adapter, pixels[0], pixels[1])
725 .ok_or("No supported surface configuration")?
726 .format;
727 if let Ok(mut renderer) = crash_report::RENDERER.lock() {
728 *renderer = format!("Metal ({})", adapter.get_info().name);
729 }
730 *gpu = Some(Gpu {
731 instance,
732 renderer: draw::Renderer::new(device, queue, format),
733 adapter,
734 });
735 }
736 let gpu = gpu.as_ref().ok_or("No GPU")?;
737 let config = surface
738 .get_default_config(&gpu.adapter, pixels[0], pixels[1])
739 .ok_or("No supported surface configuration")?;
740 surface.configure(gpu.renderer.device(), &config);
741 let (mut page, read_only) = section.shared.page(space)?;
742 // The page wears its theme: style objects it gives otherwise are restyled first.
743 let sheet = section.sheet(page.identity);
744 let restyle = onestore::op::restyle(&page, &sheet)?;
745 if !read_only && !restyle.is_empty() {
746 section.shared.apply(page_edit(space, restyle))?;
747 page = section.shared.page(space)?.0;
748 }
749 let frame = Arc::<Frame>::default();
750 let mut canvas = Canvas::new(space, page, pixels, scale)?;
751 #[cfg(target_os = "ios")]
752 {
753 canvas.page.spelling = Some(canvas::spelling::Spelling::new(
754 spell::dictionary(),
755 Waker::from(frame.clone()),
756 ));
757 }
758 canvas.page.editor.styles = sheet;
759 Ok(Self {
760 canvas: ManuallyDrop::new(canvas),
761 surface,
762 config,
763 frame,
764 section: Arc::clone(&section.shared),
765 read_only,
766 reading: None,
767 })
768 }
769
770 fn render(&mut self) -> Result<()> {
771 let mut gpu = gpu();
772 let renderer = &mut gpu.as_mut().ok_or("No GPU")?.renderer;
773 let frame = match self.surface.get_current_texture() {
774 wgpu::CurrentSurfaceTexture::Success(frame)
775 | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
776 wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
777 self.surface.configure(renderer.device(), &self.config);
778 return Ok(());
779 }
780 _ => return Ok(()),
781 };
782 let canvas: &mut Canvas = &mut self.canvas;
783 let paper = canvas.page_paper();
784 canvas
785 .page
786 .update_pictures(paper, &Waker::from(self.frame.clone()));
787 let primitives = canvas.page.primitives(canvas.colors())?;
788 renderer
789 .draw(
790 &frame.texture.create_view(&Default::default()).into(),
791 [self.config.width, self.config.height],
792 paper.color,
793 &[canvas.page.viewport.layer(&primitives)],
794 )
795 .map_err(|error| format!("Page drawing failed: {error:?}"))?;
796 renderer.queue().present(frame);
797 Ok(())
798 }
799
800 /// Hands the section what the page took, after `result`, the change that took it. A
801 /// conflict page returns to what is stored instead.
802 fn stored(&mut self, result: Result<bool>) -> bool {
803 let changed = report(result).unwrap_or(false);
804 match report(self.canvas.edit()) {
805 Some(Some(_)) if self.read_only || self.reading.is_some() => {
806 let _ = report(self.reload());
807 }
808 Some(Some(edit)) => {
809 // Pictures that came or went, by insertion or undo, are drawn or dropped.
810 if edit.ops.iter().any(|op| {
811 matches!(
812 op,
813 Op::Page {
814 op: PageOp::Insert { .. } | PageOp::Add { .. } | PageOp::Delete { .. },
815 ..
816 }
817 )
818 }) {
819 let page = &mut self.canvas.page;
820 if let Some((scene, _)) = &mut page.scene {
821 let refreshed = scene.refresh(&mut page.editor, &mut page.engine);
822 let _ = report(refreshed.map_err(Into::into));
823 }
824 }
825 let _ = report(self.section.apply(edit));
826 }
827 _ => {}
828 }
829 changed
830 }
831
832 /// Shows the page as stored, in place: the caret, selection and drawn pictures stay.
833 /// Waits for marked text to end.
834 fn reload(&mut self) -> Result<bool> {
835 if self.canvas.page.editor.marked_range().is_some() {
836 return Ok(false);
837 }
838 let (page, read_only) = self.section.page(self.canvas.space)?;
839 self.read_only = read_only;
840 if let Some(column) = self.reading {
841 return self.show(page, Some(column));
842 }
843 Ok(moved(self.canvas.page.refresh(page)?))
844 }
845
846 /// Shows `page` anew, reflowed into `column` where given and the page reflows; whether
847 /// it shows reflowed.
848 fn show(&mut self, page: Page, column: Option<f32>) -> Result<bool> {
849 let mut engine = pooled_engine();
850 let reflowed = match column {
851 Some(column) => canvas::reading::read(&page, column, &mut engine)?.page,
852 None => None,
853 };
854 pool_engine(engine);
855 self.reading = column.filter(|_| reflowed.is_some());
856 let pixels = [self.config.width, self.config.height];
857 let mut canvas = Canvas::new(
858 self.canvas.space,
859 reflowed.unwrap_or(page),
860 pixels,
861 self.canvas.scale,
862 )?;
863 canvas.paper = self.canvas.paper;
864 canvas.page.spelling = self.canvas.page.spelling.take();
865 let _ = canvas.page.focus_changed(false)?;
866 canvas.page.editor.styles = std::mem::take(&mut self.canvas.page.editor.styles);
867 // SAFETY: `self.canvas` is replaced at once and not used in between.
868 let old = unsafe { ManuallyDrop::take(&mut self.canvas) };
869 self.canvas = ManuallyDrop::new(canvas);
870 pool_engine(old.page.engine);
871 Ok(self.reading.is_some())
872 }
873}
874
875impl Drop for View {
876 fn drop(&mut self) {
877 // SAFETY: `canvas` is not used again.
878 let canvas = unsafe { ManuallyDrop::take(&mut self.canvas) };
879 pool_engine(canvas.page.engine);
880 }
881}
882
883/// What a page took as one edit of the page in `space`, made now.
884fn page_edit(space: ExGuid, ops: Vec<PageOp>) -> Edit {
885 Edit {
886 at: library::filetime(),
887 ops: ops.into_iter().map(|op| Op::Page { space, op }).collect(),
888 }
889}
890
891fn string(text: *const c_char) -> String {
892 // SAFETY: every caller's contract makes `text` NUL-terminated.
893 unsafe { CStr::from_ptr(text) }
894 .to_string_lossy()
895 .into_owned()
896}
897
898fn owned(text: String) -> *mut c_char {
899 CString::new(text.replace('\0', "")).map_or(std::ptr::null_mut(), CString::into_raw)
900}
901
902/// A view drawing page `id` of `section` into `layer`, a `CAMetalLayer`, `size` points at
903/// `scale` pixels per point; null if it cannot.
904///
905/// # Safety
906/// `layer` is a live `CAMetalLayer` that outlives the view; `id` is NUL-terminated UTF-8.
907#[unsafe(no_mangle)]
908pub unsafe extern "C" fn sb_view_new(
909 layer: *mut c_void,
910 section: &Section,
911 id: *const c_char,
912 width: f32,
913 height: f32,
914 scale: f32,
915) -> *mut View {
916 report(
917 string(id)
918 .parse()
919 .map_err(Into::into)
920 .and_then(|space| View::new(layer, section, space, [width, height], scale)),
921 )
922 .map_or(std::ptr::null_mut(), |view| Box::into_raw(Box::new(view)))
923}
924
925/// # Safety
926/// `view` came from `sb_view_new` and is not used again.
927#[unsafe(no_mangle)]
928pub unsafe extern "C" fn sb_view_free(view: *mut View) {
929 drop(unsafe { Box::from_raw(view) });
930}
931
932#[unsafe(no_mangle)]
933pub extern "C" fn sb_view_resize(view: &mut View, width: f32, height: f32, scale: f32) {
934 let pixels = [width, height].map(|side| (side * scale).round().max(1.0) as u32);
935 let canvas = &mut view.canvas;
936 if scale != canvas.scale {
937 canvas.scale = scale;
938 let _ = report(canvas.page.scale_factor_changed(scale));
939 }
940 let _ = report(canvas.page.resized(pixels));
941 view.config.width = pixels[0];
942 view.config.height = pixels[1];
943 if let Some(gpu) = &*gpu() {
944 view.surface.configure(gpu.renderer.device(), &view.config);
945 }
946}
947
948#[unsafe(no_mangle)]
949pub extern "C" fn sb_view_render(view: &mut View) -> bool {
950 report(view.render()).is_some()
951}
952
953/// Whether a picture landed since the last call, so the page should be drawn again.
954#[unsafe(no_mangle)]
955pub extern "C" fn sb_view_frame_pending(view: &View) -> bool {
956 view.frame.0.swap(false, Ordering::Acquire)
957}
958
959/// Draws the page on the desktop's dark paper, or on white.
960#[unsafe(no_mangle)]
961pub extern "C" fn sb_view_set_dark(view: &mut View, dark: bool) {
962 view.canvas.set_dark(dark);
963}
964
965/// A conflict page or a reading view: it shows what is stored, and any edit returns it there.
966#[unsafe(no_mangle)]
967pub extern "C" fn sb_view_read_only(view: &View) -> bool {
968 view.read_only || view.reading.is_some()
969}
970
971/// Whether the page has a reading view `width` points wide, as JSON: `offered`, and the
972/// `verdict` saying how it reflows or why not.
973#[unsafe(no_mangle)]
974pub extern "C" fn sb_view_reading(view: &View, width: f32) -> *mut c_char {
975 let result = view.section.page(view.canvas.space).and_then(|(page, _)| {
976 let mut engine = pooled_engine();
977 let read = canvas::reading::read(&page, width / POINT, &mut engine);
978 pool_engine(engine);
979 Ok(read?.verdict)
980 });
981 report(result).map_or(std::ptr::null_mut(), |verdict| {
982 let json =
983 serde_json::json!({ "offered": verdict.offered(), "verdict": format!("{verdict:?}") });
984 owned(json.to_string())
985 })
986}
987
988/// Shows the page reflowed into a column `width` points wide, read-only, or with 0 as laid
989/// out; whether it now shows reflowed.
990#[unsafe(no_mangle)]
991pub extern "C" fn sb_view_set_reading(view: &mut View, width: f32) -> bool {
992 let column = (width > 0.0).then_some(width / POINT);
993 let shown = view
994 .section
995 .page(view.canvas.space)
996 .and_then(|(page, _)| view.show(page, column));
997 report(shown).unwrap_or(false)
998}
999
1000/// Shows the page as stored after a change made elsewhere, keeping the caret; with
1001/// `discard`, after the section refused an edit, drops what the page took since.
1002#[unsafe(no_mangle)]
1003pub extern "C" fn sb_view_reload(view: &mut View, discard: bool) -> bool {
1004 if discard {
1005 let _ = view.canvas.page.editor.take_ops();
1006 }
1007 report(view.reload()).unwrap_or(false)
1008}
1009
1010/// Whether the view has input focus: the focused outline shows its frame and grips only then.
1011#[unsafe(no_mangle)]
1012pub extern "C" fn sb_view_focus(view: &mut View, focused: bool) {
1013 let result = view.canvas.page.focus_changed(focused).map(moved);
1014 view.stored(result);
1015}
1016
1017/// The page content's `[left, top, right, bottom]` in points at 100% zoom, bottom and right
1018/// including the margin OneNote scrolls past the last object.
1019#[unsafe(no_mangle)]
1020pub extern "C" fn sb_view_content(view: &mut View, bounds: &mut [f32; 4]) {
1021 *bounds = view.canvas.content().map(|value| value * POINT);
1022}
1023
1024/// The frame of the outline nearest the view point `x`, `y`, as `[left, top, right,
1025/// bottom]` in points at 100% zoom; false on a page with none.
1026#[unsafe(no_mangle)]
1027pub extern "C" fn sb_view_block(view: &View, x: f32, y: f32, rect: &mut [f32; 4]) -> bool {
1028 view.canvas
1029 .block([x, y])
1030 .map(|block| *rect = block)
1031 .is_some()
1032}
1033
1034/// Zooms and scrolls so the page point `x`, `y` sits at the view's corner; the host's
1035/// scroll view owns the limits and rubber-banding.
1036#[unsafe(no_mangle)]
1037pub extern "C" fn sb_view_set_transform(view: &mut View, zoom: f32, x: f32, y: f32) {
1038 view.canvas.set_transform(zoom, [x, y]);
1039}
1040
1041/// What the view point `x`, `y` lands on: 0 for the page, where the canvas takes a tap; 1
1042/// for the text taking input; 2 for another outline's text; 3 for the focused outline's
1043/// move or width grip, the selected picture and its handles, or picked drawings.
1044#[unsafe(no_mangle)]
1045pub extern "C" fn sb_view_target(view: &View, x: f32, y: f32) -> u8 {
1046 view.canvas.target([x, y]) as u8
1047}
1048
1049/// A finger down: with `sb_view_release` a tap, which places the caret, focuses an outline
1050/// or starts a new one; with `sb_view_drag` between, a grip moves or widens an outline.
1051#[unsafe(no_mangle)]
1052pub extern "C" fn sb_view_press(view: &mut View, x: f32, y: f32) -> bool {
1053 let result = view.canvas.press([x, y]);
1054 // A drawing gesture is one edit, stored when it ends, or nothing when cancelled.
1055 if view.canvas.page.inking() {
1056 return report(result).unwrap_or(false);
1057 }
1058 view.stored(result)
1059}
1060
1061/// The Pencil's pressure at the touch's next point, from 0 to 1; below 0 for a finger, whose
1062/// strokes keep their pen's width.
1063#[unsafe(no_mangle)]
1064pub extern "C" fn sb_view_pressure(view: &mut View, pressure: f32) {
1065 view.canvas
1066 .page
1067 .set_pressure((pressure >= 0.0).then_some(pressure));
1068}
1069
1070#[unsafe(no_mangle)]
1071pub extern "C" fn sb_view_drag(view: &mut View, x: f32, y: f32) -> bool {
1072 report(view.canvas.drag([x, y])).unwrap_or(false)
1073}
1074
1075#[unsafe(no_mangle)]
1076pub extern "C" fn sb_view_release(view: &mut View) -> bool {
1077 let result = view.canvas.release();
1078 view.stored(result)
1079}
1080
1081/// Undoes the last edit, or with `redo` redoes the last undone one.
1082#[unsafe(no_mangle)]
1083pub extern "C" fn sb_view_undo(view: &mut View, redo: bool) -> bool {
1084 let result = view.canvas.page.undo(redo).map(moved);
1085 view.stored(result)
1086}
1087
1088#[unsafe(no_mangle)]
1089pub extern "C" fn sb_view_can_undo(view: &View, redo: bool) -> bool {
1090 let editor = &view.canvas.page.editor;
1091 if redo {
1092 editor.can_redo()
1093 } else {
1094 editor.can_undo()
1095 }
1096}
1097
1098/// The selection's formatting as bits: 0 bold, 1 italic, 2 underline, 3 strikethrough, 4
1099/// bulleted, 5 numbered, and from 16 OneNote's tags in `sb_tags` order, To Do first.
1100#[unsafe(no_mangle)]
1101pub extern "C" fn sb_view_format(view: &View) -> u64 {
1102 report(view.canvas.format_bits()).unwrap_or(0)
1103}
1104
1105/// Applies formatting to the selection: 0 bold, 1 italic, 2 underline, 3 strikethrough, 4
1106/// bullets, 5 numbering, 6 indent, 7 outdent, 8 remove tags, 9 clear formatting, 10 yellow
1107/// highlight, each toggling where it can; from 16 toggles tag `command - 16` of `sb_tags`.
1108#[unsafe(no_mangle)]
1109pub extern "C" fn sb_view_apply(view: &mut View, command: u8) -> bool {
1110 let result = view.canvas.format(command);
1111 view.stored(result)
1112}
1113
1114/// Gives the selected paragraphs style `place` of OneNote's Styles gallery (Heading 1 to 6,
1115/// Page Title, Citation, Quote, Code, Normal) in the page's theme, or OneNote 2010's.
1116#[unsafe(no_mangle)]
1117pub extern "C" fn sb_view_style(view: &mut View, place: u8) -> bool {
1118 use notebook::sidecar::themes::{STYLES, built_in, definition};
1119 let Some((name, _)) = STYLES.get(usize::from(place)) else {
1120 return false;
1121 };
1122 let editor = &view.canvas.page.editor;
1123 let style = editor.styles.get(*name).cloned().unwrap_or_else(|| {
1124 let theme = built_in().swap_remove(0);
1125 definition(name, &theme.styles[*name], None)
1126 });
1127 let result = view
1128 .canvas
1129 .page
1130 .format(canvas::editor::Formatting::Style(style))
1131 .map(moved);
1132 view.stored(result)
1133}
1134
1135/// The place in the Styles gallery of the style every selected paragraph shares, or -1.
1136#[unsafe(no_mangle)]
1137pub extern "C" fn sb_view_style_place(view: &View) -> i32 {
1138 let state = view.canvas.page.editor.format_state().unwrap_or_default();
1139 notebook::sidecar::themes::STYLES
1140 .iter()
1141 .position(|(name, _)| state.style.as_deref() == Some(*name))
1142 .map_or(-1, |place| place as i32)
1143}
1144
1145/// The marked word at the caret, or selected, as JSON `{"word", "suggestions",
1146/// "repeated"}`; null where there is none.
1147#[unsafe(no_mangle)]
1148pub extern "C" fn sb_view_correction(view: &View) -> *mut c_char {
1149 #[derive(serde::Serialize)]
1150 struct Shown {
1151 word: String,
1152 suggestions: Vec<String>,
1153 repeated: bool,
1154 }
1155 view.canvas
1156 .page
1157 .selected_correction()
1158 .map_or(std::ptr::null_mut(), |correction| {
1159 library::json(Ok(Shown {
1160 word: correction.word,
1161 suggestions: correction.suggestions,
1162 repeated: correction.repeated,
1163 }))
1164 })
1165}
1166
1167/// Acts on the word `sb_view_correction` names: 0 replaces it with `text`, empty to delete a
1168/// repeated word; 1 ignores it; 2 adds it to the dictionary. True where the text changed.
1169///
1170/// # Safety
1171/// `text` is NUL-terminated UTF-8, or null for 1 and 2.
1172#[unsafe(no_mangle)]
1173pub unsafe extern "C" fn sb_view_correct(
1174 view: &mut View,
1175 command: u8,
1176 text: *const c_char,
1177) -> bool {
1178 let page = &mut view.canvas.page;
1179 let (Some(correction), Some(spelling)) = (page.selected_correction(), page.spelling.clone())
1180 else {
1181 return false;
1182 };
1183 match command {
1184 0 => {
1185 let result = page.correct(&correction, &string(text));
1186 view.stored(result.map(moved))
1187 }
1188 1 => {
1189 spelling.ignore(&correction.word);
1190 false
1191 }
1192 _ => {
1193 spelling.learn(&correction.word);
1194 false
1195 }
1196 }
1197}
1198
1199/// The page title's text; null on a page without an editable title.
1200#[unsafe(no_mangle)]
1201pub extern "C" fn sb_view_title(view: &View) -> *mut c_char {
1202 view.canvas
1203 .title()
1204 .map_or(std::ptr::null_mut(), |title| owned(title.shown_text()))
1205}
1206
1207/// Puts the caret at the end of the page title; false on a page without one.
1208#[unsafe(no_mangle)]
1209pub extern "C" fn sb_view_focus_title(view: &mut View) -> bool {
1210 report(view.canvas.focus_title()).unwrap_or(false)
1211}
1212
1213/// Selects the first place `query` occurs on the page, ignoring case.
1214///
1215/// # Safety
1216/// `query` is NUL-terminated UTF-8.
1217#[unsafe(no_mangle)]
1218pub unsafe extern "C" fn sb_view_find(view: &mut View, query: *const c_char) -> bool {
1219 report(view.canvas.find(&string(query))).unwrap_or(false)
1220}
1221
1222/// Selects paragraph `id` of the page; false where the page has none.
1223///
1224/// # Safety
1225/// `id` is NUL-terminated UTF-8.
1226#[unsafe(no_mangle)]
1227pub unsafe extern "C" fn sb_view_select_paragraph(view: &mut View, id: *const c_char) -> bool {
1228 let result = string(id)
1229 .parse()
1230 .map_err(Into::into)
1231 .and_then(|id| view.canvas.select_paragraph(id));
1232 report(result).unwrap_or(false)
1233}
1234
1235/// Copy, or Cut with `cut`: the selected text for the clipboard, a page selection's
1236/// outlines with a blank line between; null with nothing selected.
1237#[unsafe(no_mangle)]
1238pub extern "C" fn sb_view_copy(view: &mut View, cut: bool) -> *mut c_char {
1239 let mut copied = None;
1240 let result = view.canvas.page.copy(cut).map(|response| {
1241 if let Some(Request::Copy(clip)) = &response.request {
1242 copied = Some(clip.text());
1243 }
1244 moved(response)
1245 });
1246 view.stored(result);
1247 copied.map_or(std::ptr::null_mut(), owned)
1248}
1249
1250/// Picks what touches do, as OneNote's Draw tab does, in a section of tab colour `section`
1251/// (a COLORREF): 0 Select & Type, where a tap on a drawing picks it; 1 pen `detail` of
1252/// `sb_pens`, a stroke per press, drag and release; 2 the stroke eraser; 3 the lasso; 4
1253/// shape `detail` (line, arrow, rectangle, oval) dragged out on the grid with the section's
1254/// shape pen. Picked drawings move with a drag and go with `sb_delete_backward`. Setting a
1255/// tool, even the same one, drops a stroke under way and lets picked drawings go.
1256#[unsafe(no_mangle)]
1257pub extern "C" fn sb_view_set_tool(view: &mut View, tool: u8, detail: u8, section: u32) {
1258 let _ = report(view.canvas.set_tool(tool, detail, section));
1259}
1260
1261/// Ends a drawing gesture as though it never began, as a second finger landing does; an
1262/// eraser's sweep gives back what it erased, storing nothing.
1263#[unsafe(no_mangle)]
1264pub extern "C" fn sb_view_cancel(view: &mut View) -> bool {
1265 let result = view.canvas.page.cancel_ink().map(moved);
1266 view.stored(result)
1267}
1268
1269/// The frame of the drawings the lasso or a tap picked, as `[x, y, width, height]` in the
1270/// view's points; false with none picked.
1271#[unsafe(no_mangle)]
1272pub extern "C" fn sb_view_ink_selection(view: &View, rect: &mut [f32; 4]) -> bool {
1273 view.canvas
1274 .ink_selection()
1275 .map(|frame| *rect = frame)
1276 .is_some()
1277}
1278
1279/// The pen gallery under a section of tab colour `section` (a COLORREF) as JSON, as the
1280/// desktop's: a pen in the section's accent, then OneNote 2010's favourites, each `[color,
1281/// width, highlighter]`, the colour sRGB or null for the paper's ink and the width in
1282/// HIMETRIC.
1283#[unsafe(no_mangle)]
1284pub extern "C" fn sb_pens(section: u32) -> *mut c_char {
1285 library::json(Ok(pens(canvas::gpu::colorref(section))
1286 .iter()
1287 .map(|pen| (pen.color.map(srgb), pen.width, pen.highlighter))
1288 .collect::<Vec<_>>()))
1289}
1290
1291/// Insert Space: the next drag on the page moves what lies below or right of where it
1292/// starts, as far as it goes.
1293#[unsafe(no_mangle)]
1294pub extern "C" fn sb_view_insert_space(view: &mut View) {
1295 let _ = view.canvas.page.insert_space();
1296}
1297
1298#[derive(serde::Serialize)]
1299struct Choices {
1300 /// The page colour as sRGB, or null.
1301 color: Option<[u8; 3]>,
1302 /// Index into `rules` of the page's rule lines, or null for none or others.
1303 ruled: Option<usize>,
1304 /// OneNote's page colours, rule lines and templates, by name; colours with their sRGB.
1305 colors: Vec<(&'static str, [u8; 3])>,
1306 rules: Vec<&'static str>,
1307 templates: Vec<&'static str>,
1308}
1309
1310pub(crate) fn srgb(colorref: u32) -> [u8; 3] {
1311 let [red, green, blue, _] = colorref.to_le_bytes();
1312 [red, green, blue]
1313}
1314
1315/// The page's paper and what it can take, as JSON: `color`, `ruled`, and the choices
1316/// `colors` (each `[name, [r, g, b]]`), `rules` and `templates`, by name.
1317#[unsafe(no_mangle)]
1318pub extern "C" fn sb_view_paper(view: &View) -> *mut c_char {
1319 use canvas::template::{PAGE_COLORS, RULE_LINES, TEMPLATES};
1320 let editor = &view.canvas.page.editor;
1321 let rules = editor.rule_lines();
1322 library::json(Ok(Choices {
1323 color: editor.page_color().map(srgb),
1324 ruled: RULE_LINES
1325 .iter()
1326 .position(|(_, lines)| Some(*lines) == rules),
1327 colors: PAGE_COLORS
1328 .iter()
1329 .map(|(name, color)| (*name, srgb(*color)))
1330 .collect(),
1331 rules: RULE_LINES.iter().map(|(name, _)| *name).collect(),
1332 templates: TEMPLATES.iter().map(|template| template.name).collect(),
1333 }))
1334}
1335
1336/// Gives the page the colour `red`, `green`, `blue`, or none when `red` is negative, and
1337/// rule lines `ruled` of `sb_view_paper`'s `rules`, or none when negative, as one undo step.
1338#[unsafe(no_mangle)]
1339pub extern "C" fn sb_view_set_paper(
1340 view: &mut View,
1341 red: i16,
1342 green: u8,
1343 blue: u8,
1344 ruled: i8,
1345) -> bool {
1346 let color = u8::try_from(red)
1347 .ok()
1348 .map(|red| u32::from_le_bytes([red, green, blue, 0]));
1349 let result = view.canvas.set_paper(color, usize::try_from(ruled).ok());
1350 view.stored(result)
1351}
1352
1353/// Puts template `name`'s art behind the page in place of any it had, or none when `name`
1354/// is null, as one undo step; its colour and rule lines stay.
1355///
1356/// # Safety
1357/// A non-null `name` is NUL-terminated UTF-8.
1358#[unsafe(no_mangle)]
1359pub unsafe extern "C" fn sb_view_set_art(view: &mut View, name: *const c_char) -> bool {
1360 let result = view.canvas.set_art(library::optional(name).as_deref());
1361 view.stored(result)
1362}
1363
1364/// OneNote's default tags as JSON, in `sb_view_apply`'s order from 16: each `[name, shape,
1365/// highlight]`, the shape being the symbol `sb_tag_icon` draws and the highlight sRGB or
1366/// null.
1367#[unsafe(no_mangle)]
1368pub extern "C" fn sb_tags() -> *mut c_char {
1369 library::json(Ok(NoteTag::defaults()
1370 .into_iter()
1371 .map(|tag| (tag.label, tag.shape, tag.highlight.map(srgb)))
1372 .collect::<Vec<_>>()))
1373}
1374
1375/// Draws tag symbol `shape`, a check box `checked`, into `rgba`, `pixels` square of
1376/// premultiplied RGBA; false for shape 0, a highlighting tag, which has none.
1377///
1378/// # Safety
1379/// `rgba` has room for `pixels * pixels * 4` bytes.
1380#[unsafe(no_mangle)]
1381pub unsafe extern "C" fn sb_tag_icon(
1382 shape: u16,
1383 checked: bool,
1384 pixels: u32,
1385 rgba: *mut u8,
1386) -> bool {
1387 use resvg::{tiny_skia, usvg};
1388 let Some(icon) = canvas::outline::TagIcon::of(shape, checked) else {
1389 return false;
1390 };
1391 let Some(mut pixmap) = tiny_skia::Pixmap::new(pixels, pixels) else {
1392 return false;
1393 };
1394 for source in canvas::gpu::tag_sources(icon) {
1395 let Some(tree) =
1396 report(usvg::Tree::from_str(source, &usvg::Options::default()).map_err(Into::into))
1397 else {
1398 return false;
1399 };
1400 let size = tree.size();
1401 let scale = pixels as f32 / size.width().max(size.height());
1402 resvg::render(
1403 &tree,
1404 tiny_skia::Transform::from_scale(scale, scale),
1405 &mut pixmap.as_mut(),
1406 );
1407 }
1408 // SAFETY: the caller's contract.
1409 unsafe { std::ptr::copy_nonoverlapping(pixmap.data().as_ptr(), rgba, pixmap.data().len()) };
1410 true
1411}
1412
1413/// The page's text, for the clipboard when an edit is refused; freed with `sb_string_free`.
1414#[unsafe(no_mangle)]
1415pub extern "C" fn sb_view_page_text(view: &View) -> *mut c_char {
1416 owned(view.canvas.page_text())
1417}
1418
1419/// The page on paper `width` × `height` points as OneNote 2010 prints it, each sheet's
1420/// footer naming `section`: `length` bytes of PDF, freed with `sb_bytes_free`, or null.
1421///
1422/// # Safety
1423/// `section` is NUL-terminated UTF-8.
1424#[unsafe(no_mangle)]
1425pub unsafe extern "C" fn sb_view_pdf(
1426 view: &View,
1427 width: f32,
1428 height: f32,
1429 section: *const c_char,
1430 length: &mut usize,
1431) -> *mut u8 {
1432 let Some(pdf) = view.canvas.pdf([width, height], &string(section)) else {
1433 return std::ptr::null_mut();
1434 };
1435 *length = pdf.len();
1436 Box::into_raw(pdf.into_boxed_slice()).cast()
1437}
1438
1439/// # Safety
1440/// `bytes` and `length` came from this library and are not used again.
1441#[unsafe(no_mangle)]
1442pub unsafe extern "C" fn sb_bytes_free(bytes: *mut u8, length: usize) {
1443 drop(unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(bytes, length)) });
1444}
1445
1446/// Puts the picture file `bytes` (JPEG or PNG), `width` × `height` points, at the caret:
1447/// splitting its paragraph, or on the page at a caret on blank page.
1448///
1449/// # Safety
1450/// `bytes` holds `length` bytes.
1451#[unsafe(no_mangle)]
1452pub unsafe extern "C" fn sb_view_insert_picture(
1453 view: &mut View,
1454 bytes: *const u8,
1455 length: usize,
1456 width: f32,
1457 height: f32,
1458) -> bool {
1459 // SAFETY: the caller's contract.
1460 let bytes = unsafe { std::slice::from_raw_parts(bytes, length) };
1461 let result = view.canvas.insert_picture(bytes, [width, height]);
1462 view.stored(result)
1463}
1464
1465/// The page date field the last tap asked to change, taken: 0 the date, 1 the time, or -1
1466/// for none; `seconds` gets the page's date in seconds since 1970.
1467#[unsafe(no_mangle)]
1468pub extern "C" fn sb_view_date_request(view: &mut View, seconds: &mut i64) -> i8 {
1469 let Some(field) = view.canvas.asked.take() else {
1470 return -1;
1471 };
1472 let Some(date) = view.canvas.page.editor.date() else {
1473 return -1;
1474 };
1475 *seconds = library::unix(date.timestamp());
1476 field as i8
1477}
1478
1479/// Gives the page date `seconds` since 1970, shown as `date` and `time`: the long date and
1480/// the short time, as OneNote titles a page.
1481///
1482/// # Safety
1483/// `date` and `time` are NUL-terminated UTF-8.
1484#[unsafe(no_mangle)]
1485pub unsafe extern "C" fn sb_view_change_date(
1486 view: &mut View,
1487 seconds: i64,
1488 date: *const c_char,
1489 time: *const c_char,
1490) -> bool {
1491 let result = view
1492 .canvas
1493 .change_date(seconds, [string(date), string(time)]);
1494 view.stored(result)
1495}
1496
1497/// # Safety
1498/// `text` came from this library and is not used again.
1499#[unsafe(no_mangle)]
1500pub unsafe extern "C" fn sb_string_free(text: *mut c_char) {
1501 drop(unsafe { CString::from_raw(text) });
1502}
1503
1504#[unsafe(no_mangle)]
1505pub extern "C" fn sb_text_length(view: &View) -> u32 {
1506 view.canvas.length()
1507}
1508
1509/// The text in `start..end`, freed with `sb_string_free`.
1510#[unsafe(no_mangle)]
1511pub extern "C" fn sb_text(view: &View, start: u32, end: u32) -> *mut c_char {
1512 owned(view.canvas.text([start, end]))
1513}
1514
1515#[unsafe(no_mangle)]
1516pub extern "C" fn sb_selection(view: &View, range: &mut [u32; 2]) {
1517 if let Some(selection) = report(view.canvas.selection()) {
1518 *range = selection;
1519 }
1520}
1521
1522#[unsafe(no_mangle)]
1523pub extern "C" fn sb_select(view: &mut View, start: u32, end: u32) -> bool {
1524 report(view.canvas.select([start, end])).is_some()
1525}
1526
1527/// Widens the selection as OneNote's Ctrl+A does, one enclosing unit per call.
1528#[unsafe(no_mangle)]
1529pub extern "C" fn sb_select_more(view: &mut View) -> bool {
1530 report(
1531 view.canvas
1532 .page
1533 .editor
1534 .widen_selection()
1535 .map_err(Into::into),
1536 )
1537 .is_some()
1538}
1539
1540#[unsafe(no_mangle)]
1541pub extern "C" fn sb_marked(view: &View, range: &mut [u32; 2]) -> bool {
1542 report(view.canvas.marked())
1543 .flatten()
1544 .map(|marked| *range = marked)
1545 .is_some()
1546}
1547
1548/// # Safety
1549/// `text` is NUL-terminated UTF-8.
1550#[unsafe(no_mangle)]
1551pub unsafe extern "C" fn sb_set_marked(
1552 view: &mut View,
1553 text: *const c_char,
1554 selected_start: u32,
1555 selected_end: u32,
1556) -> bool {
1557 let result = view
1558 .canvas
1559 .set_marked(string(text), [selected_start, selected_end]);
1560 view.stored(result)
1561}
1562
1563#[unsafe(no_mangle)]
1564pub extern "C" fn sb_unmark(view: &mut View) {
1565 view.canvas.page.editor.finish_composition();
1566 view.stored(Ok(false));
1567}
1568
1569/// Types `text`, a lone `\n` as the Return key and a lone `\t` as the Tab key.
1570///
1571/// # Safety
1572/// `text` is NUL-terminated UTF-8.
1573#[unsafe(no_mangle)]
1574pub unsafe extern "C" fn sb_insert(view: &mut View, text: *const c_char) -> bool {
1575 let result = view.canvas.insert(string(text));
1576 view.stored(result)
1577}
1578
1579/// Replaces `start..end` with `text`, as autocorrection, dictation and cutting do.
1580///
1581/// # Safety
1582/// `text` is NUL-terminated UTF-8.
1583#[unsafe(no_mangle)]
1584pub unsafe extern "C" fn sb_replace(
1585 view: &mut View,
1586 start: u32,
1587 end: u32,
1588 text: *const c_char,
1589) -> bool {
1590 let canvas = &mut view.canvas;
1591 let result = canvas
1592 .select([start, end])
1593 .and_then(|()| Ok(moved(canvas.page.commit_text(string(text))?)));
1594 view.stored(result)
1595}
1596
1597/// Pastes clipboard `text` in `language`, the keyboard's BCP-47 tag.
1598///
1599/// # Safety
1600/// `text` and `language` are NUL-terminated UTF-8.
1601#[unsafe(no_mangle)]
1602pub unsafe extern "C" fn sb_paste(
1603 view: &mut View,
1604 text: *const c_char,
1605 language: *const c_char,
1606) -> bool {
1607 let (text, language) = (string(text), canvas::language::lcid(&string(language)));
1608 let result = view.canvas.page.paste(&text, language).map(moved);
1609 view.stored(result)
1610}
1611
1612#[unsafe(no_mangle)]
1613pub extern "C" fn sb_delete_backward(view: &mut View) -> bool {
1614 let result = view
1615 .canvas
1616 .page
1617 .key(&Key::Named(NamedKey::Backspace), None)
1618 .map(moved);
1619 view.stored(result)
1620}
1621
1622/// The caret at `offset` as `[x, y, width, height]` in the view's points.
1623#[unsafe(no_mangle)]
1624pub extern "C" fn sb_caret_rect(view: &View, offset: u32, rect: &mut [f32; 4]) -> bool {
1625 report(view.canvas.caret_rect(offset))
1626 .map(|caret| *rect = caret)
1627 .is_some()
1628}
1629
1630/// Writes up to `capacity` rectangles covering `start..end`; returns how many there are.
1631///
1632/// # Safety
1633/// `rects` has room for `capacity` rectangles.
1634#[unsafe(no_mangle)]
1635pub unsafe extern "C" fn sb_range_rects(
1636 view: &View,
1637 start: u32,
1638 end: u32,
1639 rects: *mut [f32; 4],
1640 capacity: usize,
1641) -> usize {
1642 let found = report(view.canvas.range_rects([start, end])).unwrap_or_default();
1643 for (index, rect) in found.iter().take(capacity).enumerate() {
1644 unsafe { rects.add(index).write(*rect) };
1645 }
1646 found.len()
1647}
1648
1649#[unsafe(no_mangle)]
1650pub extern "C" fn sb_closest(view: &View, x: f32, y: f32) -> u32 {
1651 report(view.canvas.closest([x, y])).unwrap_or(0)
1652}
1653
1654#[cfg(test)]
1655mod tests;