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