leaf_ffi/lib.rs
1//! leaf-ffi — the Swift / C-ABI frontend binding for leaf.
2//!
3//! This is the native-Apple analogue of `leaf-wasm`: it takes `leaf-core`'s
4//! frontend-neutral [`Doc`] — the byte-offset caret model and the AST→glyph
5//! [`VisualMap`] — and exposes it across a C ABI (via UniFFI) in the shape an
6//! AppKit/SwiftUI renderer wants. Core stays the single source of truth for the
7//! text, the caret math, and the offset⇄position mapping; the Swift side only
8//! paints glyphs and forwards key/mouse events back in, exactly as the TUI, gpui,
9//! and wasm frontends do.
10//!
11//! ## The boundary is style *runs*, not glyphs
12//!
13//! [`Doc::build_visual`] resolves the document to rows of per-character glyphs,
14//! each tagged with a semantic [`Role`] and the author's emphasis. Sending one
15//! object per character would make every keystroke O(document) in boundary
16//! crossings. Instead [`LeafDoc::view`] coalesces each row's glyphs into maximal
17//! **runs** of identical style and ships those — a handful of records per line.
18//! The Swift renderer maps each run's `role` to a font/size/weight and its
19//! emphasis flags to traits, the native counterpart of the TUI's `to_ratatui`
20//! and the web's CSS class.
21//!
22//! ## Core owns the grid; Swift owns the pixels
23//!
24//! Core lays a row out in whole character *columns* (a terminal-cell measure),
25//! and every offset⇄position method speaks that grid. It deliberately does *not*
26//! dictate presentation. So a native renderer is *proportional* — body text in a
27//! real family, headings by **size** and weight, code in a monospace panel — and
28//! never multiplies `col × cell_width`. It lets `NSLayoutManager` / Core Text
29//! shape each row, places the caret at [`DocView::caret_ch`] (a UTF-16 offset,
30//! which is exactly what `NSAttributedString` and `NSTextView` count in), and
31//! hit-tests a click through `characterIndex(for:)`, feeding the resulting
32//! row + UTF-16 offset back through [`LeafDoc::click_ch`]. Core measures nothing
33//! in pixels; Swift positions nothing in the model.
34//!
35//! ## Threading
36//!
37//! A UniFFI object is handed to Swift as a reference-counted handle whose methods
38//! take `&self`, so the [`Doc`] lives behind a [`Mutex`]. Every call locks, edits
39//! or reads, and returns a fresh [`DocView`] — one boundary crossing both mutates
40//! and repaints, same as the wasm frontend. Drive it from the main thread.
41
42use std::borrow::Cow;
43use std::sync::{Arc, Mutex};
44
45use leaf_core::style::{Baseline, Role, Style as LStyle};
46use leaf_core::wysiwyg::text_width;
47use leaf_core::{
48 Align as CoreAlign, Alignment, BlockKind, Capabilities as CoreCapabilities, ColorScheme, Doc,
49 FaceTable as CoreFaceTable, FontFace as CoreFontFace, FontFamily as CoreFontFamily,
50 FontSize as CoreFontSize, Format, Hundredths as CoreHundredths, InlineKind,
51 LineFlow as CoreLineFlow, LineHeight as CoreLineHeight, LineSpacing as CoreLineSpacing,
52 MarkColor as CoreMarkColor, MarkupMode as CoreMarkupMode, MediaKind as CoreMediaKind,
53 SizeStep as CoreSizeStep, SourceMap, TextColor as CoreTextColor, TextCounts as CoreTextCounts,
54 View, VisualMap,
55};
56use unicode_segmentation::UnicodeSegmentation;
57
58// Linked, not used: see the dependency's note in Cargo.toml. The `as _` is
59// what makes rustc treat the crate as referenced and carry its objects into
60// the staticlib.
61use resvg_uniffi as _;
62
63uniffi::setup_scaffolding!();
64
65/// A parse failure constructing a document — the only fallible entry point. Every
66/// other method is infallible (it operates on an already-parsed model), so they
67/// return a [`DocView`] directly.
68#[derive(Debug, thiserror::Error, uniffi::Error)]
69pub enum LeafError {
70 /// The `format` string handed to [`LeafDoc::new`] wasn't one leaf understands.
71 #[error("unknown format: {name}")]
72 UnknownFormat { name: String },
73 /// `leaf-core` failed to parse `source` as the requested format.
74 #[error("parse error: {message}")]
75 Parse { message: String },
76 /// [`typeset_math`] could not read its TeX. `position` is a byte offset
77 /// into the formula's text where the parser gave up, when it can say.
78 #[error("math error: {message}")]
79 Math {
80 message: String,
81 position: Option<u32>,
82 },
83}
84
85/// A selection cited out of the source — the text, a little of what
86/// surrounded it, and the byte range it came from. The FFI shape of
87/// `leaf_core::Quote`; see [`LeafDoc::selection_quote`].
88#[derive(uniffi::Record)]
89pub struct SelectionQuote {
90 /// The selected source, verbatim.
91 pub exact: String,
92 /// What immediately preceded it — empty at the document's start.
93 pub prefix: String,
94 /// What immediately followed it — empty at the document's end.
95 pub suffix: String,
96 /// Byte offset in the source where the selection begins.
97 pub start: u64,
98 /// Byte offset where it ends (exclusive).
99 pub end: u64,
100}
101
102/// How much writing there is — over the whole document, or over the
103/// selection. The FFI shape of `leaf_core::TextCounts`; see
104/// [`LeafDoc::counts`] for what is counted and what isn't.
105#[derive(uniffi::Record)]
106pub struct TextCounts {
107 /// Words, by UAX#29 word segmentation: a segment holding at least one
108 /// letter or digit, so `don't` is one and a lone dash is none.
109 /// A hyphenated compound is two, which is what the algorithm says.
110 pub words: u64,
111 /// Characters as a reader counts them — grapheme clusters, spaces
112 /// included. An emoji family and an accented letter are each one.
113 pub characters: u64,
114 /// The same, less every whitespace grapheme.
115 pub characters_without_spaces: u64,
116 /// Block-level containers holding at least one non-whitespace character:
117 /// a paragraph, a heading, each list item, each paragraph inside a
118 /// blockquote, a whole code block, a whole table.
119 pub paragraphs: u64,
120}
121
122impl From<CoreTextCounts> for TextCounts {
123 fn from(c: CoreTextCounts) -> Self {
124 TextCounts {
125 words: c.words as u64,
126 characters: c.characters as u64,
127 characters_without_spaces: c.characters_without_spaces as u64,
128 paragraphs: c.paragraphs as u64,
129 }
130 }
131}
132
133/// A host-painted range of the source — an annotation's footprint, a search
134/// hit. The FFI shape of `leaf_core::Highlight`; see
135/// [`LeafDoc::set_highlights`].
136#[derive(uniffi::Record)]
137pub struct Highlight {
138 /// Byte offset in the source where the wash begins.
139 pub start: u64,
140 /// Byte offset where it ends (exclusive).
141 pub end: u64,
142 /// The host's name for it, handed back on activation. Opaque to leaf.
143 pub id: String,
144 /// A rendering hint (`#RRGGBB`), or `None` for the theme's default wash.
145 pub color: Option<String>,
146 /// A margin glyph's name (an SF Symbol, for this binding's frontends), or
147 /// `None` for wash-only ink. The marker — not the wash — is what
148 /// activates a highlight; see `leaf_core::Highlight::marker`.
149 pub marker: Option<String>,
150}
151
152/// One maximal span of same-styled glyphs on a visual row — the unit the Swift
153/// renderer turns into a single styled attributed-string run.
154#[derive(Clone, Debug, PartialEq, uniffi::Record)]
155pub struct Run {
156 /// The run's text, glyphs concatenated in column order.
157 pub text: String,
158 /// The glyph's semantic role as a renderer class id: `body`, `h1`…`h6`,
159 /// `code`, `link`, `mark`, `list`, `quote`, `rule`.
160 pub role: String,
161 pub bold: bool,
162 pub italic: bool,
163 pub underline: bool,
164 pub strike: bool,
165 /// Raised off the baseline and drawn smaller — a footnote reference's `[1]`,
166 /// or an author's `^x^`. Mutually exclusive with [`sub`](Self::sub); core's
167 /// `Baseline` is one value, and these are its two non-default cases flattened
168 /// to the flag shape the rest of this record is spelled in.
169 pub sup: bool,
170 /// Lowered off the baseline and drawn smaller — an author's `~x~`.
171 pub sub: bool,
172 /// The byte offset in the source this run's first glyph came from.
173 ///
174 /// What a run *means*, as opposed to how it looks: a `link` role says a span
175 /// is drawn as a link but not where it points, and the only way back to that
176 /// is the source. A frontend drawing part of the document somewhere the caret
177 /// isn't — a footnote's text in a popover — pairs this with
178 /// [`LeafDoc::link_destination_at`] or [`LeafDoc::footnote_at`] to make those
179 /// runs followable.
180 ///
181 /// The alternative was for a frontend to count its way along the row's text
182 /// and ask [`LeafDoc::offset_for_pos`], which means converting between three
183 /// units that only agree on ASCII: this is a byte offset, the run's text is
184 /// characters, and a row's column is a *display* cell (a wide CJK glyph is
185 /// two). Handing the offset over is exact, O(1), and needs none of that.
186 ///
187 /// `0` for the runs of the source view, whose rows are split from raw text
188 /// rather than laid out from glyphs.
189 pub src: u32,
190 /// Whether this run lies inside the active selection — so the renderer can
191 /// paint a selection background without re-deriving it from offsets.
192 pub sel: bool,
193 /// The id of the host highlight covering this run, if one does — see
194 /// [`LeafDoc::set_highlights`]. A highlight splits a run the way the
195 /// selection does, so a wash begins and ends exactly on its bytes.
196 pub hl: Option<String>,
197 /// That highlight's rendering hint (`#RRGGBB`, or `None` for the theme's
198 /// default wash), carried beside the id so a renderer needs no lookup.
199 pub hl_color: Option<String>,
200 /// The colour the author named on a `mark` run — `"red"`, `"orange"`,
201 /// `"yellow"`, `"green"`, `"blue"`, `"purple"`, `"brown"` — or absent for a
202 /// plain `==highlight==` and for every other role.
203 ///
204 /// A *name*, unlike [`hl_color`](Self::hl_color)'s `#RRGGBB`, and that is
205 /// the difference between the two: a host highlight's colour is the host's
206 /// own choice and arrives as a value to paint, while this one is the
207 /// document's word for it and the renderer picks the wash. It rides beside
208 /// `role` rather than folding into it (`"mark-red"`) so a renderer that
209 /// knows nothing about colours still draws the run as the highlight it is.
210 pub mark_color: Option<String>,
211 /// What a `code` run is to the language its fenced block is written in —
212 /// `"punctuation"`, `"keyword"`, `"entity"`, `"support"`, `"constant"`,
213 /// `"string"`, `"comment"`, `"invalid"` — or absent for a run the grammar
214 /// left plain, for every run of a block in a language no grammar covers,
215 /// for inline code, and for every other role.
216 ///
217 /// A class id like `role`, and beside it for the reason `mark_color` is: a
218 /// renderer that knows nothing about tokens still draws the run as the code
219 /// it is, and one that does keys a palette on the name.
220 pub token: Option<String>,
221 /// How large this run is set — one of CSS's seven keywords (`"xx-small"`,
222 /// `"x-small"`, `"small"`, `"large"`, `"x-large"`, `"xx-large"`,
223 /// `"xxx-large"`), or the exact size the author asked for (`"14pt"`,
224 /// `"13.5pt"`) — and absent for the theme's own size, which is every run
225 /// there was before the presentation vocabulary.
226 ///
227 /// A *token* rather than an enum, for [`mark_color`](Self::mark_color)'s
228 /// reason and one more: a keyword says how much bigger and leaves how big
229 /// to the theme (`leaf_core::SizeStep::scale` has CSS's own ratios for a
230 /// renderer that wants a default ramp), while a `pt` size is exactly that
231 /// many points of the sheet. A renderer reads its table first and parses
232 /// the suffix when the table has no entry.
233 pub size: Option<String>,
234 /// The face this run is set in — one of CSS's four generics (`"serif"`,
235 /// `"sans-serif"`, `"monospace"`, `"cursive"`), or a family the author
236 /// named (`"Garamond"`) — and absent for the theme's body face.
237 ///
238 /// A generic is the theme's to name, so `serif` is whichever serif this
239 /// platform's theme has and opens everywhere. A family name is resolved
240 /// through the platform's font registry and falls back to the body face
241 /// where it is not installed, which is the portability the author traded
242 /// away knowingly.
243 pub font: Option<String>,
244 /// The run's *foreground* colour — one of the seven names
245 /// [`mark_color`](Self::mark_color) carries, or six lowercase hex digits
246 /// behind a `#` (`"#c03030"`) — and absent for the theme's text colour.
247 ///
248 /// A name is two inks, one per appearance, and the theme owns both; a
249 /// triple is painted as written in the light appearance and in the dark
250 /// one alike, which is what "exact" means.
251 ///
252 /// Not [`mark_color`](Self::mark_color), though they share a vocabulary on
253 /// purpose: that is a highlight's *background* and reaches a run through its
254 /// `mark` role, this is what the letters themselves are painted. A renderer
255 /// with a red for a highlight has a red for text, and both should be that
256 /// red.
257 pub text_color: Option<String>,
258}
259
260/// Where a locator lands — what [`LeafDoc::locate`] answers with, and the FFI
261/// mirror of [`leaf_core::Landing`].
262///
263/// A span rather than an offset because the two things a host does with a
264/// locator want different halves of it: following one puts a caret at `start`,
265/// while peeking at one draws the rows between `start` and `end`. Only the first
266/// can be recovered from an offset alone.
267#[derive(uniffi::Record)]
268pub struct LandingView {
269 /// The first byte of the block the locator names — where a caret goes.
270 pub start: u32,
271 /// One past its last byte, so the pair maps through `pos_for_offset` to the
272 /// rendered rows the block occupies, the way [`FootnoteView`]'s pair does.
273 pub end: u32,
274}
275
276impl From<leaf_core::Landing> for LandingView {
277 fn from(l: leaf_core::Landing) -> Self {
278 LandingView {
279 start: l.start as u32,
280 end: l.end as u32,
281 }
282 }
283}
284
285/// The heading a place sits under — what [`LeafDoc::heading_at`] answers
286/// with, and the FFI mirror of [`leaf_core::Heading`].
287#[derive(uniffi::Record)]
288pub struct HeadingView {
289 /// The heading's words with their markup stripped — what a `#slug` is made
290 /// from.
291 pub text: String,
292 /// 1 for `#`, 2 for `##`, and so on.
293 pub level: u32,
294 /// The heading block's first byte.
295 pub start: u32,
296 /// One past its last byte, marker and all — the heading, not its section.
297 pub end: u32,
298}
299
300impl From<leaf_core::Heading> for HeadingView {
301 fn from(h: leaf_core::Heading) -> Self {
302 HeadingView {
303 text: h.text,
304 level: h.level,
305 start: h.span.start as u32,
306 end: h.span.end as u32,
307 }
308 }
309}
310
311/// Where a dragged block would land — what [`LeafDoc::drop_target_at`]
312/// answers with, and the FFI mirror of [`leaf_core::DropTarget`]. A drop is
313/// aimed at a row and lands at a boundary, and a host needs both halves: the
314/// `offset` to hand [`LeafDoc::move_block`], and the `row` to draw the
315/// indicator above — `rows.len()` for a drop below everything.
316#[derive(uniffi::Record)]
317pub struct DropTargetView {
318 pub offset: u32,
319 pub row: u32,
320}
321
322impl From<leaf_core::DropTarget> for DropTargetView {
323 fn from(t: leaf_core::DropTarget) -> Self {
324 DropTargetView {
325 offset: t.offset as u32,
326 row: t.row as u32,
327 }
328 }
329}
330
331/// A footnote reference and the note it names — what [`LeafDoc::footnote_at`]
332/// answers with. The FFI mirror of [`leaf_core::FootnoteRef`].
333///
334/// A reference whose definition the document is missing still comes back, with
335/// its `label` and no `text`: that a `[^99]` names nothing is a thing to tell
336/// the reader, and it is not the same as the caret standing on no reference at
337/// all (which is `None`).
338#[derive(uniffi::Record)]
339pub struct FootnoteView {
340 /// The reference's label — the `1` of `[^1]`, without the `^` or brackets.
341 pub label: String,
342 /// The note's body as source text, or `None` when nothing defines it.
343 pub text: Option<String>,
344 /// The byte offset the note's body starts at, for a "go to note" that moves
345 /// the caret there. `None` alongside a `None` `text`.
346 pub offset: Option<u32>,
347 /// Where the body ends, exclusive. With `offset` this bounds the note, so a
348 /// frontend can map the pair through `pos_for_offset` to the *rendered rows*
349 /// it occupies and draw those — the note with its markup resolved, rather
350 /// than the asterisks and backticks `text` carries. `None` alongside a
351 /// `None` `offset`.
352 pub end: Option<u32>,
353}
354
355impl From<leaf_core::FootnoteRef> for FootnoteView {
356 fn from(f: leaf_core::FootnoteRef) -> Self {
357 FootnoteView {
358 label: f.label,
359 text: f.text,
360 offset: f.offset.map(|o| o as u32),
361 end: f.end.map(|o| o as u32),
362 }
363 }
364}
365
366/// A footnote definition and the reference that sends a reader to it — what
367/// [`LeafDoc::footnote_definition_at_caret`] answers with, and the FFI mirror of
368/// [`leaf_core::FootnoteDef`].
369///
370/// The other half of [`FootnoteView`]'s round trip: that one carries a reader
371/// down to the note, this one carries them back up. A definition nothing cites
372/// still comes back, with its `label` and no `offset`, for the reason an
373/// undefined reference does — "nothing refers to this note" is worth saying.
374#[derive(uniffi::Record)]
375pub struct FootnoteDefView {
376 /// The definition's label — the `1` of `[^1]: …`, spelled exactly as
377 /// [`FootnoteView::label`] spells the same footnote's.
378 pub label: String,
379 /// The byte offset the first reference starts at, for a "back to reference"
380 /// that moves the caret there. `None` for a note nothing refers to.
381 pub offset: Option<u32>,
382}
383
384impl From<leaf_core::FootnoteDef> for FootnoteDefView {
385 fn from(f: leaf_core::FootnoteDef) -> Self {
386 FootnoteDefView {
387 label: f.label,
388 offset: f.offset.map(|o| o as u32),
389 }
390 }
391}
392
393/// One visual line: its styled runs plus the row-level flags a frontend draws
394/// chrome from.
395#[derive(Clone, Debug, PartialEq, uniffi::Record)]
396pub struct Row {
397 pub runs: Vec<Run>,
398 /// Drawn but holds no caret (a table rule, a block-gap blank line): the
399 /// renderer skips it for click/caret math. See [`leaf_core::VRow`].
400 pub decoration: bool,
401 /// A fenced/indented code-block line — the renderer draws a tinted, bordered
402 /// panel around each maximal run of these.
403 pub code: bool,
404 /// A fenced block's language, carried on the block's first code row only.
405 pub code_lang: Option<String>,
406 /// A `:::name{.class}` directive-container line — the renderer draws a
407 /// tinted panel around each maximal run of these, the `code` recipe. See
408 /// [`leaf_core::VRow::directive`].
409 pub directive: bool,
410 /// A directive container's space-joined `.class` attrs, carried on the
411 /// block's first row only. See [`leaf_core::VRow::directive_label`].
412 pub directive_label: Option<String>,
413 /// The heading level (1–6) if this row belongs to a heading block, else
414 /// `None`. A proportional renderer sizes the *whole* row from this so an
415 /// inline `` `code` `` run inside a heading still reads at the heading's size.
416 pub heading: Option<u8>,
417 /// How this row's block is aligned across the measure — `"center"`,
418 /// `"right"`, `"justify"` — and `None` for the theme's default, which is
419 /// left. On every row the block emits.
420 ///
421 /// A *row* fact and not a run one for [`heading`](Self::heading)'s reason,
422 /// and more sharply: alignment is a property of the line, not of the letters
423 /// on it, so an empty paragraph the author has just centred carries it with
424 /// no run to hang it on. A renderer sets its paragraph style's alignment
425 /// from it. See [`leaf_core::VRow::align`].
426 pub align: Option<String>,
427 /// How far apart this row's block sets its lines, as a multiple of the
428 /// theme's own line height — the menu's three (`"1.15"`, `"1.5"`, `"2"`)
429 /// or any other positive decimal the author asked for (`"1.3"`) — and
430 /// `None` for the theme's spacing, which is what `"1"` would mean and is
431 /// why it is never written. On every row the block emits.
432 ///
433 /// The token *is* the ratio, so a renderer laying rows out in points
434 /// multiplies its line height by it whether or not the menu has a row for
435 /// it; one drawing a row per terminal line ignores it, the way it ignores a
436 /// heading's size. See [`leaf_core::VRow::line_height`].
437 pub line_height: Option<String>,
438 /// What this row divides, on the blank rows a block boundary is drawn with
439 /// and `None` everywhere else — so `boundary != nil` is exactly "this row is
440 /// a drawn block boundary". A frontend spaces a boundary by the pair it
441 /// falls between (the margin above a heading is wider than the one between
442 /// two paragraphs); the *height* is the frontend's, the *kind* is core's.
443 /// See [`leaf_core::Boundary`].
444 pub boundary: Option<Boundary>,
445}
446
447/// What a drawn block boundary separates. The FFI mirror of
448/// [`leaf_core::Boundary`].
449#[derive(Clone, Debug, PartialEq, uniffi::Record)]
450pub struct Boundary {
451 pub above: BlockClass,
452 pub below: BlockClass,
453}
454
455/// The block kinds core tells apart — the vocabulary a [`Boundary`] is spelled
456/// in. The FFI mirror of [`leaf_core::BlockClass`]; `Other` covers every kind
457/// core doesn't separate out, so a frontend's `match` stays exhaustive as the
458/// list grows.
459#[derive(Clone, Copy, Debug, PartialEq, Eq, uniffi::Enum)]
460pub enum BlockClass {
461 Paragraph,
462 Heading,
463 /// A whole list. Core draws no boundary row *between* two items of one list,
464 /// tight or loose, so an `ListItem`↔`ListItem` pair never reaches a frontend.
465 List,
466 ListItem,
467 Quote,
468 Code,
469 Table,
470 Media,
471 /// A display formula on lines of its own — a `$$…$$` block.
472 Math,
473 Directive,
474 Rule,
475 Footnote,
476 Other,
477}
478
479impl From<leaf_core::BlockClass> for BlockClass {
480 fn from(k: leaf_core::BlockClass) -> Self {
481 use leaf_core::BlockClass as K;
482 match k {
483 K::Paragraph => BlockClass::Paragraph,
484 K::Heading => BlockClass::Heading,
485 K::List => BlockClass::List,
486 K::ListItem => BlockClass::ListItem,
487 K::Quote => BlockClass::Quote,
488 K::Code => BlockClass::Code,
489 K::Table => BlockClass::Table,
490 K::Media => BlockClass::Media,
491 K::Math => BlockClass::Math,
492 K::Directive => BlockClass::Directive,
493 K::Rule => BlockClass::Rule,
494 K::Footnote => BlockClass::Footnote,
495 K::Other => BlockClass::Other,
496 }
497 }
498}
499
500/// One *visual line* of a table cell: its styled runs and the source offsets
501/// bounding it. A cell is usually one line, but an in-cell hard break (an inline
502/// `<br>`) splits it into several — each its own line here, so the frontend
503/// shapes and caret-maps them independently (the byte↔UTF-16 offset math a cell
504/// needs holds within a line, which carries no break). The runs are *unwrapped*:
505/// column width — and any soft wrap within it — is the frontend's to decide.
506#[derive(uniffi::Record)]
507pub struct TableCellLineView {
508 pub runs: Vec<Run>,
509 /// The source offsets bounding this line's content — the caret home at its
510 /// start and the stop just past its end.
511 pub start: u32,
512 pub end: u32,
513}
514
515/// One cell of a table's structural grid: its content as one or more visual
516/// lines, the column alignment its text honours, and the source range the whole
517/// cell occupies (where a click or the caret lands).
518#[derive(uniffi::Record)]
519pub struct TableCellView {
520 /// The cell's lines, in order — one unless an in-cell `<br>` splits it.
521 pub lines: Vec<TableCellLineView>,
522 /// `"left"`, `"right"`, `"center"`, or `"default"`.
523 pub align: String,
524 /// The source offsets bounding the cell's content — the caret anchors a
525 /// click in the cell resolves to.
526 pub start: u32,
527 pub end: u32,
528}
529
530/// One row of a table's structural grid; a header row draws bold and is ruled
531/// off from the body below it.
532#[derive(uniffi::Record)]
533pub struct TableRowView {
534 pub head: bool,
535 pub cells: Vec<TableCellView>,
536}
537
538/// A table described *structurally* rather than as the monospace box-glyph
539/// picture that spells it in [`DocView::rows`]. A proportional renderer draws its
540/// own grid from this — columns sized to content, real borders — and SKIPS the
541/// picture rows in `[start_row, end_row)`. The two describe the same cells at the
542/// same source offsets, so the caret lands identically either way. See
543/// [`leaf_core::TableInfo`].
544#[derive(uniffi::Record)]
545pub struct TableView {
546 /// The [`DocView::rows`] indices the box-drawn picture occupies — the rows a
547 /// grid-drawing frontend skips.
548 pub start_row: u32,
549 pub end_row: u32,
550 pub grid: Vec<TableRowView>,
551}
552
553/// A leaf directive (`::name{…}`) — a standalone block with no body, drawn in
554/// [`DocView::rows`] as a one-row `⧉ name` placeholder. A frontend that knows
555/// the host app's vocabulary reads this and paints the real thing over the rows
556/// in `[start_row, end_row)` — a web view for diaryx's `::embed{src=…}`, say —
557/// exactly as a grid-drawing one replaces a [`TableView`]'s picture rows. One
558/// that doesn't just paints the placeholder, which is already framed by the
559/// directive panel chrome.
560///
561/// Core resolves nothing here and neither does this layer: the vocabulary
562/// belongs to the app. See [`leaf_core::DirectiveInfo`].
563#[derive(uniffi::Record)]
564pub struct DirectiveView {
565 /// The [`DocView::rows`] indices the placeholder occupies.
566 pub start_row: u32,
567 pub end_row: u32,
568 /// The directive's type (`embed`, `toc`, `vis`), no leading colons.
569 pub name: String,
570 /// Its `[label]` text, or empty — what the placeholder row shows.
571 pub label: String,
572 /// Its `{…}` attributes in source order. A bare attribute (`{public}`) has an
573 /// empty value, which a consumer reads as a flag.
574 pub attrs: Vec<DirectiveAttr>,
575}
576
577/// One `{key=value}` attribute of a [`DirectiveView`]. A record rather than a
578/// tuple because UniFFI has no tuple type; an absent value flattens to `""`,
579/// since a bare attribute is a flag and the distinction from `key=""` has no
580/// consumer on this side.
581#[derive(uniffi::Record)]
582pub struct DirectiveAttr {
583 pub key: String,
584 pub value: String,
585}
586
587/// What a block-level media placeholder is, so Swift knows which view to build
588/// over the rows core reserved: an `NSImageView`/`UIImageView`, or an
589/// `AVPlayerView` with or without a picture to show. The peer of
590/// [`leaf_core::MediaKind`].
591#[derive(uniffi::Enum)]
592pub enum MediaKind {
593 Image,
594 Video,
595 Audio,
596}
597
598/// One `<source>` alternative of a block media element — a candidate URL plus
599/// whichever of the two things HTML picks a `<source>` by: a media query
600/// (`<picture>`) or a MIME type (`<video>`/`<audio>`).
601///
602/// Unlike the web frontend, which hands the whole list to the browser and lets
603/// it choose, a native renderer usually wants [`MediaView::src`] — already
604/// resolved for the current appearance — and reaches in here only to pick a
605/// codec `AVFoundation` can actually play.
606#[derive(uniffi::Record)]
607pub struct MediaSourceView {
608 /// The `media="…"` query, or empty for an unconditional source.
609 pub media: String,
610 /// The candidate URL (a `<picture>` `srcset` or a `<video>`/`<audio>` `src`).
611 pub src: String,
612 /// The `type="…"` MIME (`"video/webm"`), or empty when none is declared.
613 pub mime: String,
614}
615
616/// One block-level image, video, or audio: which rows core reserved for it and
617/// what to build there. The peer of [`leaf_core::MediaInfo`], and the media
618/// analogue of [`DirectiveView`] — a frontend **skips the rows in
619/// `start_row..end_row`** and lays its own view over them, rather than painting
620/// the `🖼`/`🎬`/`🔊` placeholder glyphs core put there for a surface that can't.
621#[derive(uniffi::Record)]
622pub struct MediaView {
623 /// The [`DocView::rows`] indices the placeholder occupies.
624 pub start_row: u32,
625 pub end_row: u32,
626 /// Which of the three this is — the view to build.
627 pub kind: MediaKind,
628 /// The URL to load, already resolved against the current appearance (see
629 /// [`LeafDoc::set_dark_appearance`]). A relative path resolves against the
630 /// document's own directory, which core does not know — the host does.
631 /// Empty only when a `<video>`/`<audio>` named neither a `src` nor a
632 /// `<source>`, which is a broken document.
633 pub src: String,
634 /// A `<video>`'s poster frame URL, or empty. An image destination, so it
635 /// loads exactly as an image `src` does — worth showing before the movie is
636 /// ready, or in place of one that won't play.
637 pub poster: String,
638 /// The alt / fallback text, for the view's accessibility label.
639 pub alt: String,
640 /// The `<source>` alternatives in document order; empty for a plain image.
641 pub sources: Vec<MediaSourceView>,
642}
643
644/// A per-destination measured height, the way Swift reports one back — the input
645/// half of the loop [`LeafDoc::set_media_rows`] closes.
646#[derive(uniffi::Record)]
647pub struct MediaHeight {
648 /// The media's `src` as it appeared in the document, keying it to a
649 /// [`MediaView`].
650 pub destination: String,
651 /// How many visual rows the laid-out view needs.
652 pub rows: u32,
653}
654
655/// One formula standing as a picture: what to typeset and where its picture
656/// goes. The peer of [`leaf_core::MathInfo`]. Two shapes:
657///
658/// - **Inline** (`inline == true`): one row, and on it exactly one run with
659/// role `math` whose `src` equals this `src` — a single `∑` standing for the
660/// whole formula. The renderer typesets the TeX at the run's font size
661/// ([`typeset_math`]) and draws the picture in the run's place with the
662/// text baseline through it at the picture's height: a run delegate on
663/// Apple. The run is a caret stop at the formula's start; the one after it
664/// is the next run's first character.
665/// - **Block** (`inline == false`): the rows in `start_row..end_row` are the
666/// placeholder, exactly a [`MediaView`]'s shape — the renderer **skips
667/// them** and lays the typeset picture over them, centred on the measure.
668///
669/// A formula on the caret's line is not here: there it is its TeX, drawn as
670/// `code` runs between `delimiter` runs, in every markup mode.
671#[derive(uniffi::Record)]
672pub struct MathView {
673 /// The [`DocView::rows`] indices the formula occupies — its own row for an
674 /// inline one, the placeholder and its fillers for a block.
675 pub start_row: u32,
676 pub end_row: u32,
677 /// Whether this is an atom in a line of text, or a block of its own.
678 pub inline: bool,
679 /// The TeX between the delimiters, verbatim. What [`typeset_math`] takes.
680 pub tex: String,
681 /// Display style (limits above and below, full-height fractions) rather
682 /// than text style — a `$$…$$`, inline or not.
683 pub display: bool,
684 /// The formula's source start: what the `math` run's `src` carries, and
685 /// where a click on the picture lands the caret.
686 pub src: u32,
687}
688
689/// A per-formula measured height, the way a renderer that reserves rows
690/// (rather than laying pictures out in its own units) reports one back — the
691/// input half of the loop [`LeafDoc::set_math_rows`] closes. The Swift views
692/// lay a formula out in points and never need this.
693#[derive(uniffi::Record)]
694pub struct MathHeight {
695 /// The formula's `tex` as [`MathView`] handed it over.
696 pub tex: String,
697 /// How many visual rows the picture needs.
698 pub rows: u32,
699}
700
701/// A typeset formula: a standalone SVG document and where its baseline is.
702/// The peer of `leaf_math::MathPicture`; see [`typeset_math`].
703#[derive(uniffi::Record)]
704pub struct MathPicture {
705 /// A self-contained SVG — every glyph an outline, no font to find. Its
706 /// `viewBox`, `width` and `height` are in pixels at the size it was
707 /// typeset at, so drawn at its intrinsic size the glyphs land at that
708 /// font size.
709 pub svg: String,
710 /// The picture's advance width, in em of the size it was typeset at.
711 pub width: f64,
712 /// How far it rises above its baseline, in em — the ascent a run
713 /// delegate reports, so the text baseline passes through the picture
714 /// here.
715 pub height: f64,
716 /// How far it reaches below its baseline, in em — the descent.
717 pub depth: f64,
718}
719
720/// A rendered frame: the rows to paint, where the caret sits, and the
721/// toolbar state — everything the Swift side needs for one repaint, in one value.
722/// Returned by every view-producing method.
723///
724/// ## Whole or a change
725///
726/// By default every frame is whole: `rows` is every row of the document, and
727/// the frame before it is forgotten. After [`LeafDoc::set_incremental_frames`]
728/// the same methods answer with a frame whose `rows` are only the rows that
729/// changed since the frame before — a caret move lifts none, a keystroke lifts
730/// the row it landed on — and the five fields after `rows` say where they go.
731/// Which kind a frame is, it says itself: `basis` is `0` on a whole frame and
732/// the frame before's number on a change. A caller applies a change to the
733/// frame it holds (see `rows`), and one that holds a frame other than `basis`
734/// has lost step and asks [`LeafDoc::view`] for a whole one, which every
735/// change after that is against. The rest of the frame — the caret, the
736/// selection, the toolbar state, `tables`, `directives`, `media` and `math` —
737/// is complete on every frame of either kind.
738#[derive(uniffi::Record)]
739pub struct DocView {
740 /// The rows to paint: every row of the document on a whole frame, and on
741 /// a change (`basis != 0`) the rows that replace `replaced` of the frame
742 /// before's from `row_start`, after which the rows that follow are the
743 /// frame before's with `src_shift` added to every `Run::src` they carry.
744 /// So a frame is applied as: splice `rows` over `row_start..row_start +
745 /// replaced`, then move the offsets of the rows after the splice. An
746 /// empty `rows` with `replaced == 0` is a frame that changed no row.
747 pub rows: Vec<Row>,
748 /// This frame's number: one more than the frame before's, from `1` at
749 /// the first. What a change names as its `basis`.
750 pub frame: u32,
751 /// The number of the frame these `rows` are a change against, or `0` when
752 /// they are the whole document. A change is applied only to a copy of
753 /// exactly that frame; see the type's docs.
754 pub basis: u32,
755 /// Where `rows` begin, as an index into the document's rows — the
756 /// frame before's and, since a change never moves the rows above it, this
757 /// one's. `0` on a whole frame.
758 pub row_start: u32,
759 /// How many of the frame before's rows, from `row_start`, `rows` replace.
760 /// `0` on a whole frame.
761 pub replaced: u32,
762 /// How many rows the document has once this frame is applied — what
763 /// `rows.len()` is on a whole frame, so a caller sizing a scroll view
764 /// reads this on either kind.
765 pub row_count: u32,
766 /// The byte offset every `Run::src` in a row *after* the replaced span
767 /// moved by — an edit shifts the source of everything below it, and
768 /// those rows are otherwise the frame before's, so they are kept and
769 /// moved rather than lifted. `0` on a whole frame, and on a change that
770 /// moved nothing.
771 pub src_shift: i32,
772 /// Tables described structurally, for a frontend that draws its own grid
773 /// instead of painting the box-glyph rows. Empty in the source view. Each
774 /// names the span of the *document's* rows its picture occupies, to be
775 /// skipped — an index into a whole frame's `rows`, and into the rows a
776 /// change has been applied to. These lists are small and ride every
777 /// frame complete, so no frame has to say whether they changed.
778 pub tables: Vec<TableView>,
779 /// Leaf directives (`::name{…}`) described structurally, for a frontend that
780 /// paints what the host app's vocabulary makes of them instead of the `⧉`
781 /// placeholder row. Empty in the source view, where the directive is the
782 /// literal text the caret is editing.
783 pub directives: Vec<DirectiveView>,
784 /// Block-level images, videos, and audio described structurally, for a
785 /// frontend that lays real views over the rows core reserved instead of
786 /// painting the placeholder glyphs. Empty in the source view, where the
787 /// `` or `<video>` markup is the literal text being edited.
788 pub media: Vec<MediaView>,
789 /// Formulas standing as pictures — each inline atom and each display
790 /// block — for a frontend that typesets and draws them in place of the
791 /// `math` run or the placeholder rows. Empty in the source view, and
792 /// empty of any formula on the caret's line, which is its TeX there.
793 pub math: Vec<MathView>,
794 /// The caret's row: an index into [`Self::rows`].
795 pub caret_row: u32,
796 /// The caret's display *column* within its row — core's grid position. Kept
797 /// for callers reasoning in columns; a proportional renderer wants
798 /// [`Self::caret_ch`] instead.
799 pub caret_col: u32,
800 /// The caret's offset within its row's text in **UTF-16 code units** — what
801 /// `NSAttributedString`/`NSTextView` count to. This is `caret_col` mapped
802 /// through the row's grapheme widths, so it lands the caret correctly past
803 /// wide glyphs (CJK, emoji) where a column and a character index diverge.
804 pub caret_ch: u32,
805 /// The caret's **source byte offset** — the coordinate a table cell is keyed
806 /// by (`TableCellView::start`/`end`), so a frontend drawing its own grid can
807 /// find which cell the caret sits in without the picture-row indices.
808 pub caret_src: u32,
809 /// Whether a (non-empty) selection is active.
810 pub has_selection: bool,
811 /// The selection's *fixed* end (the caret is the moving end), as a row and a
812 /// UTF-16 offset — so the renderer can restore a native selection with the
813 /// same direction the model has. Equal to the caret when `has_selection` is
814 /// false.
815 pub anchor_row: u32,
816 pub anchor_ch: u32,
817 /// Whether the buffer differs from the last saved bytes — for a "● modified"
818 /// affordance.
819 pub dirty: bool,
820 /// Whether there is a step to undo, and one to redo — what a native Edit
821 /// menu or an undo manager enables its items by. Both false on a read-only
822 /// document. Exact, not a bound: `can_undo` is true precisely when
823 /// [`LeafDoc::undo`] would move the document, so a host composing several
824 /// histories into one can ask before it dispatches.
825 pub can_undo: bool,
826 pub can_redo: bool,
827 /// `"wysiwyg"` or `"source"`, for a view-toggle affordance.
828 pub view: String,
829 /// The heading level at the caret, if any — a toolbar lights H1…H6 from it.
830 pub heading: Option<u32>,
831 /// Whether the caret stands in a code block — the toolbar lights its Code
832 /// Block button from it. Rides the frame for `heading`'s reason: walking
833 /// the caret into a fence changes no mark, and a button asking for itself
834 /// would never be told.
835 pub code_block: bool,
836 /// Whether the caret stands inside a block quote, at any depth — the
837 /// toolbar lights and ticks its Block Quote control from it. A quote wraps
838 /// blocks rather than being one, so this is true alongside `heading` or
839 /// `code_block`, not instead of them. Rides the frame for `heading`'s
840 /// reason.
841 pub blockquote: bool,
842 /// Whether the list item at the caret carries a checkbox, and which way it
843 /// faces — `Some(true)` ticked, `Some(false)` empty, `None` for a plain
844 /// item or no item at all. A toolbar lights its Checklist button from the
845 /// first question and ticks its Checked item from the second. Rides the
846 /// frame for `heading`'s reason: stepping the caret from a bullet into a
847 /// task item changes no mark, and a button asking for itself would never
848 /// be told.
849 pub task: Option<bool>,
850 /// The inline marks active at the caret (`bold`, `italic`, `code`, …) — the
851 /// toolbar lights the matching buttons.
852 pub active: Vec<String>,
853 /// The destination of the link the caret stands in, or `None` — the toolbar
854 /// lights its Link button from it and seeds an edit of that link with it.
855 ///
856 /// It rides the frame rather than being a query a toolbar makes for itself
857 /// because a toolbar only redraws when the *state* changes: walking the caret
858 /// out of a link changes no mark, no heading, and no dirty flag, so a Link
859 /// button reading this by a call of its own would keep a stale light on. Same
860 /// reason `heading` is here and not asked for.
861 ///
862 /// Only a *parsed* link answers ([`LeafDoc::link_destination_at_caret`]);
863 /// a wikilink is literal text with no node behind it, and has nothing to
864 /// repoint — see `LinkTarget.swift`.
865 pub link: Option<String>,
866 /// The colour of the highlight the caret stands in — which swatch a colour
867 /// palette draws as the current one, and `None` both outside a highlight and
868 /// inside an uncoloured one.
869 ///
870 /// It rides the frame for `link`'s reason, and more sharply: walking from a
871 /// red highlight into a blue one changes no mark, no heading, no dirty flag
872 /// and no link, so a palette asking for itself would never be told to move
873 /// its checkmark.
874 ///
875 /// An enum rather than the name string [`Run::mark_color`] carries, because
876 /// the two are different questions. A run's colour is a *rendering* hint that
877 /// has to survive a name this build has never heard of (a newer twig's
878 /// eighth colour draws as a plain highlight rather than not at all); this is
879 /// the closed palette a control offers, and a name outside it is not a
880 /// swatch anyone can press.
881 pub mark_color: Option<MarkColor>,
882}
883
884/// The colour of a highlight — the closed palette [`LeafDoc::set_mark_color`]
885/// writes and [`DocView::mark_color`] reports.
886///
887/// Obsidian's spelling, which is twig's: a circle emoji straight after the
888/// opening `==`, so `==🔴 text==` is a highlight of `text` in [`MarkColor::Red`].
889/// The emoji is *spelling* rather than content — it never appears in the text a
890/// reader sees, and never in a [`Run`].
891#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
892pub enum MarkColor {
893 Red,
894 Orange,
895 Yellow,
896 Green,
897 Blue,
898 Purple,
899 Brown,
900}
901
902impl From<CoreMarkColor> for MarkColor {
903 fn from(c: CoreMarkColor) -> Self {
904 match c {
905 CoreMarkColor::Red => MarkColor::Red,
906 CoreMarkColor::Orange => MarkColor::Orange,
907 CoreMarkColor::Yellow => MarkColor::Yellow,
908 CoreMarkColor::Green => MarkColor::Green,
909 CoreMarkColor::Blue => MarkColor::Blue,
910 CoreMarkColor::Purple => MarkColor::Purple,
911 CoreMarkColor::Brown => MarkColor::Brown,
912 }
913 }
914}
915
916impl From<MarkColor> for CoreMarkColor {
917 fn from(c: MarkColor) -> Self {
918 match c {
919 MarkColor::Red => CoreMarkColor::Red,
920 MarkColor::Orange => CoreMarkColor::Orange,
921 MarkColor::Yellow => CoreMarkColor::Yellow,
922 MarkColor::Green => CoreMarkColor::Green,
923 MarkColor::Blue => CoreMarkColor::Blue,
924 MarkColor::Purple => CoreMarkColor::Purple,
925 MarkColor::Brown => CoreMarkColor::Brown,
926 }
927 }
928}
929
930/// How a block's lines are set across the measure — the closed vocabulary
931/// [`LeafDoc::set_alignment`] writes and [`LeafDoc::alignment_at_caret`]
932/// reports.
933///
934/// There is no `left`, because absence is left: the default alignment is the
935/// theme's, and a document that agrees with it has no reason to say so. A
936/// segmented control draws a fourth segment for it and calls `setAlignment(nil)`.
937///
938/// An enum rather than the name string [`Row::align`] carries, for
939/// [`MarkColor`]'s reason: a row's alignment is a *rendering* fact that has to
940/// survive a token this build has never heard of, while this is the closed set a
941/// control offers.
942#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
943pub enum Align {
944 Center,
945 Right,
946 Justify,
947}
948
949impl From<CoreAlign> for Align {
950 fn from(a: CoreAlign) -> Self {
951 match a {
952 CoreAlign::Center => Align::Center,
953 CoreAlign::Right => Align::Right,
954 CoreAlign::Justify => Align::Justify,
955 }
956 }
957}
958
959impl From<Align> for CoreAlign {
960 fn from(a: Align) -> Self {
961 match a {
962 Align::Center => CoreAlign::Center,
963 Align::Right => CoreAlign::Right,
964 Align::Justify => CoreAlign::Justify,
965 }
966 }
967}
968
969/// How far apart a block's lines are set, as a multiple of the theme's own line
970/// height — the vocabulary [`LeafDoc::set_line_spacing`] writes.
971///
972/// Single spacing is absent for the reason `left` is absent from [`Align`]: it
973/// is the theme's, and the menu entry for it is `setLineSpacing(nil)`.
974#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
975pub enum LineSpacing {
976 /// `1.15` — the word processor's default "a little more air".
977 OneFifteen,
978 /// `1.5`.
979 OneHalf,
980 /// `2` — double spacing.
981 Double,
982}
983
984impl From<CoreLineSpacing> for LineSpacing {
985 fn from(l: CoreLineSpacing) -> Self {
986 match l {
987 CoreLineSpacing::OneFifteen => LineSpacing::OneFifteen,
988 CoreLineSpacing::OneHalf => LineSpacing::OneHalf,
989 CoreLineSpacing::Double => LineSpacing::Double,
990 }
991 }
992}
993
994impl From<LineSpacing> for CoreLineSpacing {
995 fn from(l: LineSpacing) -> Self {
996 match l {
997 LineSpacing::OneFifteen => CoreLineSpacing::OneFifteen,
998 LineSpacing::OneHalf => CoreLineSpacing::OneHalf,
999 LineSpacing::Double => CoreLineSpacing::Double,
1000 }
1001 }
1002}
1003
1004/// How large a run is set relative to the text around it — CSS's
1005/// `<absolute-size>` keywords with `medium` removed, because `medium` is
1006/// absence. What [`LeafDoc::set_font_size`] writes.
1007///
1008/// A *step*, never a measurement: a run set to `14pt` in a 12pt theme is a step
1009/// up and the same run under a 16pt theme is a step *down*, the author's intent
1010/// inverted by a change they never made. `Large` is a step up under every theme,
1011/// and `leaf_core::SizeStep::scale` has CSS's own ratio for each if the theme
1012/// wants a default ramp.
1013#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
1014pub enum SizeStep {
1015 XxSmall,
1016 XSmall,
1017 Small,
1018 Large,
1019 XLarge,
1020 XxLarge,
1021 XxxLarge,
1022}
1023
1024impl From<CoreSizeStep> for SizeStep {
1025 fn from(s: CoreSizeStep) -> Self {
1026 match s {
1027 CoreSizeStep::XxSmall => SizeStep::XxSmall,
1028 CoreSizeStep::XSmall => SizeStep::XSmall,
1029 CoreSizeStep::Small => SizeStep::Small,
1030 CoreSizeStep::Large => SizeStep::Large,
1031 CoreSizeStep::XLarge => SizeStep::XLarge,
1032 CoreSizeStep::XxLarge => SizeStep::XxLarge,
1033 CoreSizeStep::XxxLarge => SizeStep::XxxLarge,
1034 }
1035 }
1036}
1037
1038impl From<SizeStep> for CoreSizeStep {
1039 fn from(s: SizeStep) -> Self {
1040 match s {
1041 SizeStep::XxSmall => CoreSizeStep::XxSmall,
1042 SizeStep::XSmall => CoreSizeStep::XSmall,
1043 SizeStep::Small => CoreSizeStep::Small,
1044 SizeStep::Large => CoreSizeStep::Large,
1045 SizeStep::XLarge => CoreSizeStep::XLarge,
1046 SizeStep::XxLarge => CoreSizeStep::XxLarge,
1047 SizeStep::XxxLarge => CoreSizeStep::XxxLarge,
1048 }
1049 }
1050}
1051
1052/// The face a run is set in — CSS's generic families, less `fantasy` and
1053/// `system-ui`, neither of which an author asks for. What
1054/// [`LeafDoc::set_font_family`] writes.
1055///
1056/// A generic, never a font name, for [`SizeStep`]'s reason: a document naming
1057/// `Georgia` renders in the fallback everywhere Georgia is not installed. The
1058/// theme names the concrete face for each — `Serif` is whichever serif this
1059/// platform's theme has, and `Monospace` is the face inline code already uses.
1060/// The theme's own body face is absent because it is absence; the menu entry for
1061/// it is `setFontFamily(nil)`.
1062#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
1063pub enum FontFamily {
1064 Serif,
1065 SansSerif,
1066 Monospace,
1067 Cursive,
1068}
1069
1070impl From<CoreFontFamily> for FontFamily {
1071 fn from(f: CoreFontFamily) -> Self {
1072 match f {
1073 CoreFontFamily::Serif => FontFamily::Serif,
1074 CoreFontFamily::SansSerif => FontFamily::SansSerif,
1075 CoreFontFamily::Monospace => FontFamily::Monospace,
1076 CoreFontFamily::Cursive => FontFamily::Cursive,
1077 }
1078 }
1079}
1080
1081impl From<FontFamily> for CoreFontFamily {
1082 fn from(f: FontFamily) -> Self {
1083 match f {
1084 FontFamily::Serif => CoreFontFamily::Serif,
1085 FontFamily::SansSerif => CoreFontFamily::SansSerif,
1086 FontFamily::Monospace => CoreFontFamily::Monospace,
1087 FontFamily::Cursive => CoreFontFamily::Cursive,
1088 }
1089 }
1090}
1091
1092// ── the exact forms: a value where a name will not do ───────────────────────
1093//
1094// Each of the four closed enums above gets an *open* one beside it, which is a
1095// Swift enum with associated values: `.step(.large)` or `.points(14)`,
1096// `.generic(.serif)` or `.named("Garamond")`, `.named(.red)` or `.rgb(…)`,
1097// `.step(.oneHalf)` or `.ratio(1.3)`. The four gestures and four queries take
1098// and answer these; the closed enums stay because they are what a menu offers
1099// *first* — a name is portable under every theme, and the value under the
1100// divider is the author's to take knowingly. See
1101// `docs/proposals/exact-presentation-values.md`.
1102//
1103// **A value this vocabulary cannot carry is refused, and the gesture writes
1104// nothing at all.** A size of `0pt` or `700pt`, a ratio of `0`, a face named
1105// `" "` — an author who typed one of those asked for something the document
1106// cannot hold, and the answer to that is to leave the document as it is. It is
1107// emphatically *not* to clear the key: the size that was already on the run is
1108// not the author's mistake, and a field that refuses by throwing away what was
1109// there is a field that punishes a typo. Validate before calling anyway — the
1110// range is 0.01 to 655.35 — because from here a refusal is silent.
1111//
1112// **One value does mean absence**, and it is the one core already states: a
1113// line height of `1` is single spacing, which is the theme's own and has no
1114// token, so `.ratio(1)` *clears* the key. That is the author asking for the
1115// default, not failing to ask for anything. [`Meant`] is the three answers
1116// written down once.
1117
1118/// What a presentation value a caller handed in comes to — the three answers
1119/// the note above states, written down once.
1120///
1121/// A gesture asks for this and writes `Some(value)`, writes `None`, or writes
1122/// nothing at all. The middle one is a *clearing* the author asked for and the
1123/// last is a refusal; from the other side of the binding they look alike, which
1124/// is why a field that offers a number validates before it calls.
1125enum Meant<T> {
1126 /// A value core carries. The gesture writes it.
1127 Value(T),
1128 /// A value that *means* the theme's own. The gesture clears the key — only
1129 /// a line height of 1 is one of these.
1130 Absence,
1131 /// A value outside what the vocabulary carries. The gesture writes nothing,
1132 /// and what the run already said stands.
1133 Refused,
1134}
1135
1136impl<T> Meant<T> {
1137 /// A core constructor's `Option` read as a refusal — the shape three of the
1138 /// four conversions below have, since only a spacing has an absence.
1139 fn of(value: Option<T>) -> Self {
1140 match value {
1141 Some(value) => Self::Value(value),
1142 None => Self::Refused,
1143 }
1144 }
1145}
1146
1147/// What a gesture handed `value` writes: `Some(Some(v))` for a value,
1148/// `Some(None)` for the clearing both a `nil` argument and an absence mean, and
1149/// `None` for a value the vocabulary refuses — which the gesture spells by not
1150/// calling core at all.
1151fn written<T, C>(value: Option<T>, into_core: impl FnOnce(T) -> Meant<C>) -> Option<Option<C>> {
1152 match value.map(into_core) {
1153 None | Some(Meant::Absence) => Some(None),
1154 Some(Meant::Value(value)) => Some(Some(value)),
1155 Some(Meant::Refused) => None,
1156 }
1157}
1158
1159/// How large a run is set: a [`SizeStep`] relative to the text around it, or
1160/// the point size the author asked for. What [`LeafDoc::set_font_size`] writes
1161/// and [`LeafDoc::font_size_at_caret`] answers.
1162///
1163/// The step is what a menu offers first and what a document should say where a
1164/// name will do — it reads as a step up under every theme. The point size is
1165/// what the author typed and is all it is: 14 points of the sheet on paper, and
1166/// 14 points before the zoom on screen. A heading set to an exact size is that
1167/// size and not its ramp scaled.
1168#[derive(Clone, Copy, Debug, PartialEq, uniffi::Enum)]
1169pub enum FontSize {
1170 Step(SizeStep),
1171 /// Points. 0.01 to 655.35; anything else is refused — see the note above.
1172 Points(f64),
1173}
1174
1175impl FontSize {
1176 /// The core size this names — [`Meant::Refused`] for a number of points
1177 /// core cannot carry, which leaves the run's size alone.
1178 fn into_core(self) -> Meant<CoreFontSize> {
1179 match self {
1180 FontSize::Step(step) => Meant::Value(CoreFontSize::Step(step.into())),
1181 FontSize::Points(points) => Meant::of(CoreFontSize::points(points as f32)),
1182 }
1183 }
1184}
1185
1186impl From<CoreFontSize> for FontSize {
1187 fn from(s: CoreFontSize) -> Self {
1188 match s {
1189 CoreFontSize::Step(step) => FontSize::Step(step.into()),
1190 // Divided in `f64` rather than widened from the `f32` `as_f32`
1191 // answers: 1.3 as an `f32` widens to 1.2999999523, and a caller
1192 // that passed 1.3 in has to get 1.3 back or no menu row ticks.
1193 CoreFontSize::Points(pt) => FontSize::Points(f64::from(pt.hundredths()) / 100.0),
1194 }
1195 }
1196}
1197
1198/// How far apart a block's lines are set: a [`LineSpacing`] from the menu's
1199/// three, or the ratio the author asked for. [`FontSize`]'s peer one property
1200/// along, and with no unit at all — a line height is a multiple.
1201///
1202/// A ratio that spells one of the three names *is* that name, so
1203/// `.ratio(1.5)` comes back as `.step(.oneHalf)` and a menu has a row to tick.
1204/// A ratio of 1 is single spacing, which is absence: it clears the key.
1205#[derive(Clone, Copy, Debug, PartialEq, uniffi::Enum)]
1206pub enum LineHeight {
1207 Step(LineSpacing),
1208 /// A multiple of the theme's line height. 1 is absence, and clears; a
1209 /// number outside 0.01 to 655.35 is refused — see the note above.
1210 Ratio(f64),
1211}
1212
1213impl LineHeight {
1214 /// The core spacing this names — the one [`into_core`](FontSize::into_core)
1215 /// here whose answer can be [`Meant::Absence`], because a ratio of 1 is
1216 /// single spacing and single spacing has no token.
1217 fn into_core(self) -> Meant<CoreLineHeight> {
1218 match self {
1219 LineHeight::Step(step) => Meant::Value(CoreLineHeight::Step(step.into())),
1220 // A number the vocabulary carries at all is asked first, because
1221 // `ratio` answers `None` both for a 1 — which *means* the theme's
1222 // own — and for a 0 or a NaN, which mean nothing at all, and the
1223 // two have opposite answers.
1224 LineHeight::Ratio(ratio) => match CoreHundredths::from_f32(ratio as f32) {
1225 None => Meant::Refused,
1226 Some(_) => match CoreLineHeight::ratio(ratio as f32) {
1227 Some(height) => Meant::Value(height),
1228 None => Meant::Absence,
1229 },
1230 },
1231 }
1232 }
1233}
1234
1235impl From<CoreLineHeight> for LineHeight {
1236 fn from(l: CoreLineHeight) -> Self {
1237 match l {
1238 CoreLineHeight::Step(step) => LineHeight::Step(step.into()),
1239 // In `f64` throughout, for [`FontSize`]'s reason.
1240 CoreLineHeight::Ratio(r) => LineHeight::Ratio(f64::from(r.hundredths()) / 100.0),
1241 }
1242 }
1243}
1244
1245/// A run's *foreground* colour: one of the seven [`MarkColor`] names, or the
1246/// RGB triple the author asked for.
1247///
1248/// A name is two inks, one per appearance, and the theme owns both. A triple is
1249/// painted as written in the light appearance and in the dark one alike — that
1250/// is what "exact" means, and the theme does not soften it.
1251#[derive(Clone, Copy, Debug, PartialEq, uniffi::Enum)]
1252pub enum TextColor {
1253 Named(MarkColor),
1254 Rgb { r: u8, g: u8, b: u8 },
1255}
1256
1257impl TextColor {
1258 /// The core colour this names. A plain conversion and not a [`Meant`],
1259 /// because every triple of bytes is a colour: this is the one of the four
1260 /// with nothing to refuse.
1261 fn into_core(self) -> CoreTextColor {
1262 match self {
1263 TextColor::Named(color) => CoreTextColor::Named(color.into()),
1264 TextColor::Rgb { r, g, b } => CoreTextColor::Rgb { r, g, b },
1265 }
1266 }
1267}
1268
1269impl From<CoreTextColor> for TextColor {
1270 fn from(c: CoreTextColor) -> Self {
1271 match c {
1272 CoreTextColor::Named(color) => TextColor::Named(color.into()),
1273 CoreTextColor::Rgb { r, g, b } => TextColor::Rgb { r, g, b },
1274 }
1275 }
1276}
1277
1278/// The face a run is set in: one of CSS's four generics, or the family the
1279/// author named.
1280///
1281/// A generic opens on every machine and a family name does not, which is the
1282/// trade [`FontFamily`]'s note states once. A named family is resolved through
1283/// the platform's font registry and falls back to the theme's body face where
1284/// it is not installed.
1285#[derive(Clone, Debug, PartialEq, uniffi::Enum)]
1286pub enum FontFace {
1287 Generic(FontFamily),
1288 /// A family name, as the font panel spells it. Trimmed on the way in, and
1289 /// one that spells a generic (`"Serif"`) is read as that generic.
1290 Named(String),
1291}
1292
1293impl FontFace {
1294 /// The core face this names — [`Meant::Refused`] for a name that names
1295 /// nothing, which leaves the run's face alone.
1296 fn into_core(self) -> Meant<CoreFontFace> {
1297 match self {
1298 FontFace::Generic(family) => Meant::Value(CoreFontFace::Generic(family.into())),
1299 // Through the parser rather than straight into `Named`, so a name
1300 // gets the trim and the generic-keyword reading a document's own
1301 // `data-font` gets, and an empty one names nothing.
1302 FontFace::Named(name) => Meant::of(CoreFontFace::from_attr(&name)),
1303 }
1304 }
1305}
1306
1307impl From<CoreFontFace> for FontFace {
1308 fn from(f: CoreFontFace) -> Self {
1309 match f {
1310 CoreFontFace::Generic(family) => FontFace::Generic(family.into()),
1311 CoreFontFace::Named(name) => FontFace::Named(name),
1312 }
1313 }
1314}
1315
1316/// A visual position: a row index plus a UTF-16 offset within that row's text —
1317/// the coordinate the geometry side (Core Text) draws from. Returned by
1318/// [`LeafDoc::pos_for_offset`], the bridge from a source offset (what a
1319/// `UITextPosition` wraps) to where it sits on screen.
1320#[derive(uniffi::Record)]
1321pub struct RowCol {
1322 pub row: u32,
1323 pub ch: u32,
1324}
1325
1326/// The rows a source range covers, both ends **inclusive** — what a frontend
1327/// slices out of a frame to draw a block somewhere other than where it sits: a
1328/// footnote peek, a link peek, a landing flash. Returned by
1329/// [`LeafDoc::row_range_for`].
1330///
1331/// Inclusive rather than half-open because the answer is "these rows", not "up
1332/// to here": every caller wants `rows[first...last]`, and a `last` one past the
1333/// end would be a second thing to get wrong at each of them. `last >= first`
1334/// always, so the pair is never empty — a range with no visible byte still
1335/// covers the row it opened on.
1336#[derive(uniffi::Record)]
1337pub struct RowRange {
1338 pub first: u32,
1339 pub last: u32,
1340}
1341
1342/// Which formatting controls this document's format can spell — the toolbar's
1343/// enabled state, one flag per button, from [`LeafDoc::capabilities`]. Mirrors
1344/// [`leaf_core::Capabilities`], where the reasoning lives.
1345///
1346/// Its shape is a flat record of `Bool`s rather than a query taking a gesture
1347/// because the Swift side wants exactly one crossing and a value it can hold in
1348/// an `@Observable`: `let caps = doc.capabilities()`, then
1349/// `.disabled(!caps.bold)` on each control.
1350#[derive(uniffi::Record)]
1351pub struct Capabilities {
1352 pub bold: bool,
1353 pub italic: bool,
1354 pub code: bool,
1355 pub mark: bool,
1356 pub underline: bool,
1357 pub strike: bool,
1358 /// [`LeafDoc::set_mark_color`] — the highlight *palette*, which Markdown
1359 /// spells and djot does not, though both spell the highlight itself. Gate
1360 /// the swatches on this *and* on [`LeafDoc::caret_in_mark`], the way the grid
1361 /// controls take `table` and `caret_in_table`: one is a fact about the
1362 /// format, the other about where the caret is standing.
1363 pub mark_color: bool,
1364 pub superscript: bool,
1365 pub subscript: bool,
1366 /// Both [`LeafDoc::set_heading`] and [`LeafDoc::set_paragraph`] — they are
1367 /// the same gesture in core and stand or fall together.
1368 pub heading: bool,
1369 pub blockquote: bool,
1370 pub bullet_list: bool,
1371 pub ordered_list: bool,
1372 /// [`LeafDoc::toggle_task_item`], [`LeafDoc::toggle_task_checked`] and
1373 /// [`LeafDoc::toggle_task_at`] — including a *tap* on a rendered checkbox,
1374 /// which should not be live where the box cannot be spelled.
1375 pub task: bool,
1376 pub link: bool,
1377 /// [`LeafDoc::insert_image`] and [`LeafDoc::insert_media`] both.
1378 pub image: bool,
1379 pub thematic_break: bool,
1380 /// [`LeafDoc::insert_footnote`]. Markdown and djot spell the pair; HTML does
1381 /// not, so the button goes rather than dims into a refusal.
1382 pub footnote: bool,
1383 /// [`LeafDoc::toggle_code_block`]. Markdown and djot spell the fence; HTML
1384 /// rebuilds the block as `<pre><code>`. Pair with [`DocView::code_block`]
1385 /// for the button's lit state.
1386 pub code_block: bool,
1387 pub code_language: bool,
1388 /// The grid controls. Gate them on this *and* [`LeafDoc::caret_in_table`]:
1389 /// this asks whether the format's tables are editable, that whether the
1390 /// caret is in one.
1391 pub table: bool,
1392 /// Shift+Return inside a cell — [`LeafDoc::cell_line_break`].
1393 pub cell_line_break: bool,
1394 /// The alignment control — [`LeafDoc::set_alignment`]. Every format leaf
1395 /// opens but XML spells a block's attributes.
1396 pub alignment: bool,
1397 /// The line-spacing menu — [`LeafDoc::set_line_spacing`]. The same gesture
1398 /// as [`alignment`](Self::alignment) and so the same answer, and its own
1399 /// flag because a toolbar dims controls one at a time.
1400 pub line_spacing: bool,
1401 /// The size menu — [`LeafDoc::set_font_size`]. **Narrower than the block
1402 /// pair**: it wraps a selection in an attributed span, which AsciiDoc has no
1403 /// slot for, so this is `false` there while [`alignment`](Self::alignment) is
1404 /// `true`. The block-level form of the same property — the caret in a
1405 /// paragraph, nothing selected — still works, which is why the flag
1406 /// describes the control rather than the caret.
1407 pub font_size: bool,
1408 /// The face menu — [`LeafDoc::set_font_family`]. A span, as
1409 /// [`font_size`](Self::font_size) is.
1410 pub font_family: bool,
1411 /// The text-colour swatches — [`LeafDoc::set_text_color`]. A span again, and
1412 /// not to be confused with [`mark_color`](Self::mark_color): that is a
1413 /// highlight's background and rides the `mark` node, this is a run's
1414 /// foreground and rides an attributed span.
1415 pub text_color: bool,
1416 /// The page-break button — [`LeafDoc::insert_page_break`]. Markdown, djot,
1417 /// HTML and AsciiDoc, each spelling it its own way and each drawn as the
1418 /// same placeholder row.
1419 pub page_break: bool,
1420 /// Moving a block — [`LeafDoc::move_block`] and the
1421 /// [`move_block_up`](LeafDoc::move_block_up)/`down` pair. Every format
1422 /// with blocks a caret can name; XML has none.
1423 pub move_block: bool,
1424}
1425
1426impl From<CoreCapabilities> for Capabilities {
1427 fn from(c: CoreCapabilities) -> Self {
1428 Self {
1429 bold: c.bold,
1430 italic: c.italic,
1431 code: c.code,
1432 mark: c.mark,
1433 underline: c.underline,
1434 strike: c.strike,
1435 mark_color: c.mark_color,
1436 superscript: c.superscript,
1437 // `subscript` is a Swift keyword; uniffi escapes it in the generated
1438 // binding (`caps.`subscript``), so the field keeps its real name
1439 // here rather than wearing a suffix on both sides of the boundary.
1440 subscript: c.subscript,
1441 heading: c.heading,
1442 blockquote: c.blockquote,
1443 bullet_list: c.bullet_list,
1444 ordered_list: c.ordered_list,
1445 task: c.task,
1446 link: c.link,
1447 image: c.image,
1448 thematic_break: c.thematic_break,
1449 footnote: c.footnote,
1450 code_block: c.code_block,
1451 code_language: c.code_language,
1452 table: c.table,
1453 cell_line_break: c.cell_line_break,
1454 alignment: c.alignment,
1455 line_spacing: c.line_spacing,
1456 font_size: c.font_size,
1457 font_family: c.font_family,
1458 text_color: c.text_color,
1459 page_break: c.page_break,
1460 move_block: c.move_block,
1461 }
1462 }
1463}
1464
1465/// A table column's text alignment — the argument to
1466/// [`LeafDoc::table_set_alignment`]. Mirrors twig's `Alignment`.
1467#[derive(uniffi::Enum)]
1468pub enum TableAlignment {
1469 Default,
1470 Left,
1471 Right,
1472 Center,
1473}
1474
1475impl TableAlignment {
1476 fn into_core(self) -> Alignment {
1477 match self {
1478 TableAlignment::Default => Alignment::Default,
1479 TableAlignment::Left => Alignment::Left,
1480 TableAlignment::Right => Alignment::Right,
1481 TableAlignment::Center => Alignment::Center,
1482 }
1483 }
1484}
1485
1486/// How much of the source markup the rich view exposes — the argument to
1487/// [`LeafDoc::set_markup_mode`]. Mirrors [`leaf_core::MarkupMode`]; `None`
1488/// is the default (the clean surface Diaryx ships, with typed syntax kept
1489/// literal).
1490///
1491/// A single three-way ladder rather than a pair of toggles, because only three
1492/// of the four combinations of its two axes — reveal the caret's delimiters,
1493/// author markup from typing — are coherent. See [`leaf_core::MarkupMode`]
1494/// for which one is left out and why.
1495#[derive(uniffi::Enum)]
1496pub enum MarkupMode {
1497 None,
1498 Shortcuts,
1499 Full,
1500}
1501
1502impl MarkupMode {
1503 fn into_core(self) -> CoreMarkupMode {
1504 match self {
1505 MarkupMode::None => CoreMarkupMode::None,
1506 MarkupMode::Shortcuts => CoreMarkupMode::Shortcuts,
1507 MarkupMode::Full => CoreMarkupMode::Full,
1508 }
1509 }
1510
1511 fn from_core(mode: CoreMarkupMode) -> Self {
1512 match mode {
1513 CoreMarkupMode::None => MarkupMode::None,
1514 CoreMarkupMode::Shortcuts => MarkupMode::Shortcuts,
1515 CoreMarkupMode::Full => MarkupMode::Full,
1516 }
1517 }
1518}
1519
1520/// How the rich view treats a soft break (a bare newline inside a paragraph) —
1521/// the argument to [`LeafDoc::set_line_flow`]. Mirrors [`leaf_core::LineFlow`];
1522/// `Fold` is the default (soft breaks reflow into the paragraph, as before).
1523#[derive(uniffi::Enum)]
1524pub enum LineFlow {
1525 Fold,
1526 Preserve,
1527}
1528
1529impl LineFlow {
1530 fn into_core(self) -> CoreLineFlow {
1531 match self {
1532 LineFlow::Fold => CoreLineFlow::Fold,
1533 LineFlow::Preserve => CoreLineFlow::Preserve,
1534 }
1535 }
1536
1537 fn from_core(mode: CoreLineFlow) -> Self {
1538 match mode {
1539 CoreLineFlow::Fold => LineFlow::Fold,
1540 CoreLineFlow::Preserve => LineFlow::Preserve,
1541 }
1542 }
1543}
1544
1545/// A live leaf document bound for a native Apple frontend: `leaf_core::Doc` plus
1546/// the wrap width the current viewport implies, behind a mutex. Constructed from
1547/// an in-memory string and driven entirely through method calls — there is no
1548/// filesystem behind it.
1549#[derive(uniffi::Object)]
1550pub struct LeafDoc {
1551 inner: Mutex<Inner>,
1552}
1553
1554/// The guarded state. Its methods assume the lock is held (they take `&mut
1555/// self`); the [`LeafDoc`] exported wrappers acquire it, delegate, and return the
1556/// resulting frame.
1557struct Inner {
1558 doc: Doc,
1559 /// The wrap mode. `Some(cols)` wraps the map at that column budget (a terminal,
1560 /// or a fixed-cell frontend); `None` builds it **unwrapped** — one row per block —
1561 /// for a proportional GUI that wraps at its own pixel width. `build_visual`
1562 /// caches on `(revision, width)`, so re-syncing when neither moved is free.
1563 width: Option<usize>,
1564 /// The host's current appearance, which a `<picture>`'s `prefers-color-scheme`
1565 /// `<source>`s are matched against when resolving a block image's URL. Core
1566 /// has no theme of its own, so this is AppKit/UIKit answering on its behalf;
1567 /// defaults to light until the host calls
1568 /// [`LeafDoc::set_dark_appearance`].
1569 scheme: ColorScheme,
1570 /// Whether a frame is the change since the frame before rather than the
1571 /// whole document — see [`LeafDoc::set_incremental_frames`].
1572 incremental: bool,
1573 /// The rows of the last frame handed out, kept only while `incremental`
1574 /// so the next frame can be the difference from them; `None` makes the
1575 /// next frame whole. Whenever it is `Some`, it is frame `frame`'s rows.
1576 last_rows: Option<Vec<Row>>,
1577 /// The number of the last frame handed out — `0` before the first.
1578 frame: u32,
1579}
1580
1581// SAFETY: `Doc` embeds a `twig::Editor`, which holds a `NonNull<TwigEditor>` and
1582// is therefore `!Send`. UniFFI hands `LeafDoc` to Swift as a reference-counted
1583// handle that must be `Send + Sync`, so `Inner` must be `Send`. This is sound
1584// because:
1585// 1. Every access goes through `LeafDoc::lock()` — the `Mutex` serializes all
1586// reads and mutations, so there is never concurrent access to the handle.
1587// 2. twig's editor handle owns a plain heap allocation with no thread-affinity
1588// (no thread-locals, no per-thread state) — moving the pointer between
1589// threads is fine as long as use is serialized, which (1) guarantees.
1590// The intended usage is still main-thread-driven; this impl only permits the
1591// handle to cross threads safely, it does not invite concurrent use.
1592unsafe impl Send for Inner {}
1593
1594impl Inner {
1595 /// Rebuild the visual map at the current width. Cheap (cached) when nothing
1596 /// changed; the guard that lets every movement/click method assume a fresh
1597 /// grid regardless of call order.
1598 fn sync(&mut self) {
1599 match self.width {
1600 Some(w) => self.doc.build_visual(w),
1601 None => self.doc.build_visual_unwrapped(),
1602 }
1603 // The source view's styling, keyed on the revision alone — a no-op on
1604 // every call that isn't the first after an edit, like the map above.
1605 if self.doc.view == View::Source {
1606 self.doc.build_source();
1607 }
1608 }
1609
1610 /// The plain text of visual row `row` in the active view — the string the
1611 /// renderer concatenates its runs into. Backs the column⇄UTF-16 mapping.
1612 fn row_text(&self, row: usize) -> String {
1613 match self.doc.view {
1614 View::Wysiwyg => self
1615 .doc
1616 .vmap
1617 .rows
1618 .get(row)
1619 .map(|r| r.glyphs.iter().map(|g| g.ch).collect())
1620 .unwrap_or_default(),
1621 View::Source => self
1622 .doc
1623 .source
1624 .split('\n')
1625 .nth(row)
1626 .unwrap_or("")
1627 .to_string(),
1628 }
1629 }
1630
1631 /// The `(row, display-column)` a source offset sits at in the active view.
1632 fn pos_of_offset(&self, off: usize) -> (usize, usize) {
1633 match self.doc.view {
1634 View::Wysiwyg => self.doc.vmap.pos_of_offset(off),
1635 View::Source => {
1636 let s = &self.doc.source;
1637 // Walk back to a character boundary, not just into range. Every
1638 // offset here arrives from a UI toolkit counting in its own
1639 // units, so one landing mid-character is ordinary input — and
1640 // slicing on it aborts the process across an FFI boundary that
1641 // has no unwinding. `snap_stop` and `text_in_range` already do
1642 // this; this was the one door left open.
1643 let mut off = off.min(s.len());
1644 while off > 0 && !s.is_char_boundary(off) {
1645 off -= 1;
1646 }
1647 let row = s[..off].bytes().filter(|&b| b == b'\n').count();
1648 let line_start = s[..off].rfind('\n').map_or(0, |i| i + 1);
1649 (row, text_width(&s[line_start..off]))
1650 }
1651 }
1652 }
1653
1654 /// The inclusive row span a source range occupies in the active view.
1655 ///
1656 /// The rich view defers to [`leaf_core::wysiwyg::VisualMap::row_range_for`],
1657 /// where the reasoning lives. The source view has no hidden bytes at all —
1658 /// every byte is drawn on the line it is written on — so counting newlines
1659 /// is the whole answer, and the last row is the one holding the range's last
1660 /// byte rather than the one past it.
1661 fn row_range_for(&self, start: usize, end: usize) -> (usize, usize) {
1662 match self.doc.view {
1663 View::Wysiwyg => self.doc.vmap.row_range_for(start..end),
1664 View::Source => {
1665 let first = self.pos_of_offset(start).0;
1666 let last = self.pos_of_offset(end.max(start.saturating_add(1)) - 1).0;
1667 (first, last.max(first))
1668 }
1669 }
1670 }
1671
1672 /// The source offset under a click at row `row`, `ch` UTF-16 units in.
1673 fn offset_at(&mut self, row: usize, ch: usize) -> usize {
1674 self.sync();
1675 let col = utf16_to_col(&self.row_text(row), ch);
1676 self.doc.click(row, col, false);
1677 self.doc.caret
1678 }
1679
1680 // ── position mapping for UITextInput (non-mutating; caret untouched) ───────
1681 // These branch by view exactly as `pos_of_offset` does, so the WYSIWYG map and
1682 // the raw-source grid each answer in their own coordinates.
1683
1684 /// The source offset of display column `col` on visual `row` — the inverse of
1685 /// [`Self::pos_of_offset`] in column space.
1686 fn offset_of_col(&self, row: usize, col: usize) -> usize {
1687 match self.doc.view {
1688 View::Wysiwyg => self.doc.vmap.offset_of_pos(row, col),
1689 View::Source => {
1690 let line = self.row_text(row);
1691 let (mut c, mut b) = (0usize, 0usize);
1692 for g in line.graphemes(true) {
1693 if c >= col {
1694 break;
1695 }
1696 c += text_width(g);
1697 b += g.len();
1698 }
1699 self.source_line_start(row) + b
1700 }
1701 }
1702 }
1703
1704 /// The byte offset where visual `row` begins in the source view.
1705 fn source_line_start(&self, row: usize) -> usize {
1706 self.doc
1707 .source
1708 .split('\n')
1709 .take(row)
1710 .map(|l| l.len() + 1)
1711 .sum()
1712 }
1713
1714 /// The next caret stop after `off`, or `None` at the end.
1715 fn stop_after(&self, off: usize) -> Option<usize> {
1716 match self.doc.view {
1717 View::Wysiwyg => self.doc.vmap.stop_after(off),
1718 View::Source => {
1719 let s = &self.doc.source;
1720 if off >= s.len() {
1721 None
1722 } else {
1723 Some(
1724 s[off..]
1725 .grapheme_indices(true)
1726 .nth(1)
1727 .map_or(s.len(), |(i, _)| off + i),
1728 )
1729 }
1730 }
1731 }
1732 }
1733
1734 /// The previous caret stop before `off`, or `None` at the start.
1735 fn stop_before(&self, off: usize) -> Option<usize> {
1736 match self.doc.view {
1737 View::Wysiwyg => self.doc.vmap.stop_before(off),
1738 View::Source => {
1739 let s = &self.doc.source;
1740 let off = off.min(s.len());
1741 if off == 0 {
1742 None
1743 } else {
1744 s[..off].grapheme_indices(true).next_back().map(|(i, _)| i)
1745 }
1746 }
1747 }
1748 }
1749
1750 /// Snap `off` to a valid caret stop (WYSIWYG) / char boundary (source).
1751 fn snap_stop(&self, off: usize) -> usize {
1752 let s = &self.doc.source;
1753 let mut off = off.min(s.len());
1754 match self.doc.view {
1755 View::Wysiwyg => self.doc.vmap.snap_to_stop(off),
1756 View::Source => {
1757 while off > 0 && !s.is_char_boundary(off) {
1758 off -= 1;
1759 }
1760 off
1761 }
1762 }
1763 }
1764
1765 /// The UTF-16 index of source offset `off` in the visible text — the
1766 /// body of [`LeafDoc::utf16_index_for_offset`], for the one call and for
1767 /// the many.
1768 fn utf16_index_of(&self, off: u32) -> u32 {
1769 let off = (off as usize).min(self.doc.source.len());
1770 match self.doc.view {
1771 View::Wysiwyg => self.doc.vmap.visible_utf16_len(0, off) as u32,
1772 View::Source => {
1773 let off = self.snap_stop(off);
1774 self.doc.source[..off].encode_utf16().count() as u32
1775 }
1776 }
1777 }
1778
1779 /// [`snap_stop`](Self::snap_stop) for the walks that pair stops with
1780 /// characters — `step_offset` and `distance_offset`, which a system text
1781 /// input counts against the text it was shown. The caret's home at the
1782 /// end of a hidden mark (`VisualMap::mark_ends`) has no character of its
1783 /// own, so those walks start from the glyph stop drawn at the same spot.
1784 fn snap_glyph_stop(&self, off: usize) -> usize {
1785 match self.doc.view {
1786 View::Wysiwyg => self
1787 .doc
1788 .vmap
1789 .snap_to_glyph_stop(off.min(self.doc.source.len())),
1790 View::Source => self.snap_stop(off),
1791 }
1792 }
1793
1794 /// The navigable visual row above `row`, if any.
1795 fn nav_above(&self, row: usize) -> Option<usize> {
1796 match self.doc.view {
1797 View::Wysiwyg => self.doc.vmap.navigable_above(row),
1798 View::Source => (row > 0).then(|| row - 1),
1799 }
1800 }
1801
1802 /// The navigable visual row below `row`, if any.
1803 fn nav_below(&self, row: usize) -> Option<usize> {
1804 match self.doc.view {
1805 View::Wysiwyg => self.doc.vmap.navigable_below(row),
1806 View::Source => {
1807 let n = self.doc.source.split('\n').count();
1808 (row + 1 < n).then_some(row + 1)
1809 }
1810 }
1811 }
1812
1813 /// Resolve the current document to a whole frame of style runs, numbered
1814 /// as the next one. What both [`view`](Self::view) and
1815 /// [`frame`](Self::frame) start from; neither the frame count nor the
1816 /// rows kept for the next change are touched here.
1817 fn whole(&mut self) -> DocView {
1818 self.sync();
1819
1820 let (ss, se) = self.doc.selection().unwrap_or((usize::MAX, usize::MAX));
1821
1822 // The two views speak different grids — the WYSIWYG map's resolved glyphs
1823 // vs the raw source split on newlines — and `caret_pos` branches to match,
1824 // so the rows must too or the caret lands on the wrong text.
1825 let rows = match self.doc.view {
1826 View::Wysiwyg => wysiwyg_rows(&self.doc.vmap, ss, se, self.doc.highlights()),
1827 View::Source => source_rows(
1828 &self.doc.source,
1829 &self.doc.smap,
1830 ss,
1831 se,
1832 self.doc.highlights(),
1833 ),
1834 };
1835 // Structural tables, for a proportional renderer that draws its own grid;
1836 // none in the source view (the caret rides raw pipe text there).
1837 let tables = match self.doc.view {
1838 View::Wysiwyg => wysiwyg_tables(&self.doc.vmap, ss, se, self.doc.highlights()),
1839 View::Source => Vec::new(),
1840 };
1841
1842 // Leaf directives, on the same terms as the tables above: structural in
1843 // the rich view, absent in the source view.
1844 let directives = match self.doc.view {
1845 View::Wysiwyg => wysiwyg_directives(&self.doc.vmap),
1846 View::Source => Vec::new(),
1847 };
1848
1849 // Block media, on the same terms again: only the rich view has
1850 // placeholder rows to lay a view over.
1851 let media = match self.doc.view {
1852 View::Wysiwyg => wysiwyg_media(&self.doc.vmap, self.scheme),
1853 View::Source => Vec::new(),
1854 };
1855
1856 // Formulas, likewise: pictures stand in only in the rich view.
1857 let math = match self.doc.view {
1858 View::Wysiwyg => wysiwyg_math(&self.doc.vmap),
1859 View::Source => Vec::new(),
1860 };
1861
1862 let (caret_row, caret_col) = self.doc.caret_pos();
1863 // Map the caret's display column to a UTF-16 text offset so a native
1864 // renderer can place it past wide glyphs (see [`DocView::caret_ch`]).
1865 let caret_ch = col_to_utf16(&self.row_text(caret_row), caret_col);
1866 // The selection's fixed (anchor) end, in the same row/UTF-16 terms.
1867 let (has_selection, anchor_row, anchor_ch) = match self.doc.selection() {
1868 Some(_) => {
1869 let a = self.doc.anchor.unwrap_or(self.doc.caret);
1870 let (ar, ac) = self.pos_of_offset(a);
1871 (true, ar, col_to_utf16(&self.row_text(ar), ac))
1872 }
1873 None => (false, caret_row, caret_ch),
1874 };
1875 let heading = self.doc.current_heading_level();
1876 let code_block = self.doc.caret_in_code_block();
1877 let blockquote = self.doc.caret_in_blockquote();
1878 let task = self.doc.task_checked_at_caret();
1879 let active = self
1880 .doc
1881 .active_inline_marks()
1882 .iter()
1883 .map(|k| mark_id(k).to_string())
1884 .collect();
1885 let link = self.doc.link_destination_at_caret();
1886 let mark_color = self.doc.mark_color_at_caret().map(MarkColor::from);
1887
1888 let row_count = rows.len() as u32;
1889 DocView {
1890 rows,
1891 frame: self.frame + 1,
1892 basis: 0,
1893 row_start: 0,
1894 replaced: 0,
1895 row_count,
1896 src_shift: 0,
1897 tables,
1898 directives,
1899 media,
1900 math,
1901 caret_row: caret_row as u32,
1902 caret_col: caret_col as u32,
1903 caret_ch: caret_ch as u32,
1904 caret_src: self.doc.caret.min(self.doc.source.len()) as u32,
1905 has_selection,
1906 anchor_row: anchor_row as u32,
1907 anchor_ch: anchor_ch as u32,
1908 dirty: self.doc.dirty,
1909 can_undo: self.doc.can_undo(),
1910 can_redo: self.doc.can_redo(),
1911 view: self.doc.view_name().to_string(),
1912 heading,
1913 code_block,
1914 blockquote,
1915 task,
1916 active,
1917 link,
1918 mark_color,
1919 }
1920 }
1921
1922 /// The whole frame, and the one every later change is against. Called
1923 /// for the first paint, on resize, and by a frontend that has lost step
1924 /// with the changes — see [`DocView`].
1925 fn view(&mut self) -> DocView {
1926 let v = self.whole();
1927 self.frame = v.frame;
1928 self.last_rows = self.incremental.then(|| v.rows.clone());
1929 v
1930 }
1931
1932 /// The frame every mutating wrapper answers with, so one boundary
1933 /// crossing both mutates and repaints: whole unless the frontend asked
1934 /// for changes, and then the rows that differ from the frame before's —
1935 /// found by comparing them in memory, which costs microseconds and lifts
1936 /// nothing — with the frame before's rows kept for the next one.
1937 fn frame(&mut self) -> DocView {
1938 let mut v = self.whole();
1939 self.frame = v.frame;
1940 if !self.incremental {
1941 return v;
1942 }
1943 let Some(old) = self.last_rows.take() else {
1944 self.last_rows = Some(v.rows.clone());
1945 return v;
1946 };
1947 let rows = std::mem::take(&mut v.rows);
1948 let d = leaf_core::row_delta(&old, &rows, same_row_shifted, shift_between_rows);
1949 v.basis = v.frame - 1;
1950 v.row_start = d.start as u32;
1951 v.replaced = d.replaced as u32;
1952 v.src_shift = d.src_shift as i32;
1953 v.rows = rows[d.start..d.start + d.len].to_vec();
1954 self.last_rows = Some(rows);
1955 v
1956 }
1957}
1958
1959/// Whether `b` is `a` with every `Run::src` moved by `shift` — the row
1960/// equality [`leaf_core::row_delta`] matches the frame before's suffix on. Every
1961/// field is named so that a field added to [`Row`] or [`Run`] is a compile
1962/// error here rather than a row the delta silently ignores.
1963fn same_row_shifted(a: &Row, b: &Row, shift: i64) -> bool {
1964 let Row {
1965 runs,
1966 decoration,
1967 code,
1968 code_lang,
1969 directive,
1970 directive_label,
1971 heading,
1972 align,
1973 line_height,
1974 boundary,
1975 } = a;
1976 *decoration == b.decoration
1977 && *code == b.code
1978 && *code_lang == b.code_lang
1979 && *directive == b.directive
1980 && *directive_label == b.directive_label
1981 && *heading == b.heading
1982 && *align == b.align
1983 && *line_height == b.line_height
1984 && *boundary == b.boundary
1985 && runs.len() == b.runs.len()
1986 && runs
1987 .iter()
1988 .zip(&b.runs)
1989 .all(|(x, y)| same_run_shifted(x, y, shift))
1990}
1991
1992fn same_run_shifted(a: &Run, b: &Run, shift: i64) -> bool {
1993 let Run {
1994 text,
1995 role,
1996 bold,
1997 italic,
1998 underline,
1999 strike,
2000 sup,
2001 sub,
2002 src,
2003 sel,
2004 hl,
2005 hl_color,
2006 mark_color,
2007 token,
2008 size,
2009 font,
2010 text_color,
2011 } = a;
2012 *src as i64 + shift == b.src as i64
2013 && *text == b.text
2014 && *role == b.role
2015 && *bold == b.bold
2016 && *italic == b.italic
2017 && *underline == b.underline
2018 && *strike == b.strike
2019 && *sup == b.sup
2020 && *sub == b.sub
2021 && *sel == b.sel
2022 && *hl == b.hl
2023 && *hl_color == b.hl_color
2024 && *mark_color == b.mark_color
2025 && *token == b.token
2026 && *size == b.size
2027 && *font == b.font
2028 && *text_color == b.text_color
2029}
2030
2031/// The shift `b`'s offsets stand at from `a`'s, read off the first run — or
2032/// `None` for a row with no run to read it off, which matches at any shift.
2033fn shift_between_rows(a: &Row, b: &Row) -> Option<i64> {
2034 Some(b.runs.first()?.src as i64 - a.runs.first()?.src as i64)
2035}
2036
2037/// Move every offset `row` carries by `by` — what a caller does to the rows
2038/// after a change's span, and what the tests do to check one.
2039#[cfg(test)]
2040fn shift_row(row: &mut Row, by: i64) {
2041 for run in &mut row.runs {
2042 run.src = (run.src as i64 + by) as u32;
2043 }
2044}
2045
2046/// Typeset `tex` — the text between a formula's delimiters, as a [`MathView`]
2047/// hands it over — to a picture. `display` is the view's `display`; `size` is
2048/// the font size in points the formula is set at (an inline formula takes the
2049/// run's, a block the body's); `r`, `g`, `b`, `a` are the ink, as bytes. A
2050/// theme change is a re-render with a new colour.
2051///
2052/// Pure layout over fonts embedded in the binary — no I/O, fast enough to
2053/// call from a layout pass — so a renderer caches by `(tex, display, size,
2054/// colour)` and nothing more. TeX the typesetter cannot read is a
2055/// [`LeafError::Math`]; the renderer shows the revealed source in its place.
2056#[uniffi::export]
2057pub fn typeset_math(
2058 tex: String,
2059 display: bool,
2060 size: f64,
2061 r: u8,
2062 g: u8,
2063 b: u8,
2064 a: u8,
2065) -> Result<MathPicture, LeafError> {
2066 let p = leaf_math::typeset(&tex, display, size, [r, g, b, a]).map_err(|e| LeafError::Math {
2067 message: e.message,
2068 position: e.position.map(|p| p as u32),
2069 })?;
2070 Ok(MathPicture {
2071 svg: p.svg,
2072 width: p.width,
2073 height: p.height,
2074 depth: p.depth,
2075 })
2076}
2077
2078#[uniffi::export]
2079impl LeafDoc {
2080 /// Parse `source` as `format` (`"markdown"`/`"md"`, `"djot"`/`"dj"`,
2081 /// `"html"`, `"xml"`) into a live, untitled document.
2082 #[uniffi::constructor]
2083 pub fn new(source: String, format: String) -> Result<Arc<Self>, LeafError> {
2084 let format = match format.to_ascii_lowercase().as_str() {
2085 "markdown" | "md" => Format::Markdown,
2086 "djot" | "dj" => Format::Djot,
2087 "html" | "htm" => Format::Html,
2088 "xml" => Format::Xml,
2089 other => {
2090 return Err(LeafError::UnknownFormat {
2091 name: other.to_string(),
2092 });
2093 }
2094 };
2095 let doc = Doc::from_source(source, format).map_err(|e| LeafError::Parse {
2096 message: e.to_string(),
2097 })?;
2098 Ok(Arc::new(LeafDoc {
2099 inner: Mutex::new(Inner {
2100 doc,
2101 width: Some(80),
2102 scheme: ColorScheme::Light,
2103 incremental: false,
2104 last_rows: None,
2105 frame: 0,
2106 }),
2107 }))
2108 }
2109
2110 /// Resolve the current document to a whole frame — the first paint, and
2111 /// the frame a frontend taking changes ([`set_incremental_frames`]) is
2112 /// brought back into step by: every change after this is against it.
2113 ///
2114 /// [`set_incremental_frames`]: Self::set_incremental_frames
2115 pub fn view(&self) -> DocView {
2116 self.lock().view()
2117 }
2118
2119 /// A whole frame of the document as a page shows it: no line revealed,
2120 /// where [`view`](Self::view) reveals the caret's — its delimiters under
2121 /// `MarkupMode::Full`, and in every mode a formula on it as its TeX. What
2122 /// the sheet a PDF or a printout is laid out from asks for in place of
2123 /// `view`, since paper has no caret. See [`leaf_core::Doc::set_unrevealed`].
2124 ///
2125 /// Kept apart from the screen: the screen's map is set aside and put back,
2126 /// and neither the frame count nor the rows kept for the next change move,
2127 /// so the frame after this one is still a change from the screen's last.
2128 pub fn paper_view(&self) -> DocView {
2129 let mut g = self.lock();
2130 // Bring the screen's map up to date first, so an edit it has not yet
2131 // been built for is spent on it rather than on the paper's build.
2132 g.sync();
2133 g.doc.set_unrevealed(true);
2134 let v = g.whole();
2135 g.doc.set_unrevealed(false);
2136 v
2137 }
2138
2139 /// Whether the frame every method answers with is the change since the
2140 /// frame before rather than the whole document — see [`DocView`] for the
2141 /// shape, and for what a frontend does with one. Off by default, so a
2142 /// frontend that reads `rows` as the document goes on getting it; a
2143 /// frontend that keeps its own copy of the rows turns it on once and
2144 /// splices each frame into that copy, which makes a caret move lift no
2145 /// row across the binding and a keystroke lift the row it changed.
2146 ///
2147 /// The first frame after turning it on is whole (there is no frame before
2148 /// to be a change from), and [`view`](Self::view) is whole at any time.
2149 pub fn set_incremental_frames(&self, on: bool) {
2150 let mut g = self.lock();
2151 g.incremental = on;
2152 g.last_rows = None;
2153 }
2154
2155 /// The document's rows `from..to`, as the last frame had them — for a
2156 /// frontend that wants a window of rows back without a whole frame,
2157 /// having applied every change so far or not. Clamped to the document;
2158 /// empty when `from >= to`. Costs the rows of the document to build and
2159 /// the window to lift, so it is the occasional resynchronisation, not the
2160 /// per-gesture path.
2161 pub fn rows(&self, from: u32, to: u32) -> Vec<Row> {
2162 let mut g = self.lock();
2163 let mut rows = g.whole().rows;
2164 let to = (to as usize).min(rows.len());
2165 let from = (from as usize).min(to);
2166 rows.truncate(to);
2167 rows.drain(..from);
2168 rows
2169 }
2170
2171 /// Set the wrap width (in columns) the viewport implies and repaint. For a
2172 /// fixed-cell frontend (a terminal); a proportional GUI uses [`set_unwrapped`].
2173 pub fn set_width(&self, cols: u32) -> DocView {
2174 let mut g = self.lock();
2175 g.width = Some((cols as usize).max(1));
2176 g.frame()
2177 }
2178
2179 /// Switch to **unwrapped** layout — one visual row per block, no column wrapping —
2180 /// and repaint. A proportional GUI calls this once at start-up, then wraps each
2181 /// row at its own pixel width (the caret/hit/selection geometry it derives from
2182 /// the pixel wrap; core still owns the caret model, in byte offsets). Idempotent
2183 /// and cheap to leave in place across edits.
2184 pub fn set_unwrapped(&self) -> DocView {
2185 let mut g = self.lock();
2186 g.width = None;
2187 g.frame()
2188 }
2189
2190 /// Tell core whether the host is in a dark appearance, so a `<picture>`'s
2191 /// `prefers-color-scheme` `<source>`s resolve to the right banner. Call it
2192 /// from `viewDidChangeEffectiveAppearance` (AppKit) or
2193 /// `traitCollectionDidChange` (UIKit).
2194 ///
2195 /// Cheap to call repeatedly: resolving at the same appearance yields the same
2196 /// URLs, and a renderer keying its views by `src` tears nothing down.
2197 pub fn set_dark_appearance(&self, dark: bool) -> DocView {
2198 let mut g = self.lock();
2199 g.scheme = if dark {
2200 ColorScheme::Dark
2201 } else {
2202 ColorScheme::Light
2203 };
2204 g.frame()
2205 }
2206
2207 /// Report how many visual rows each block media actually needs, measured from
2208 /// the views the renderer laid out, keyed by the media's `src`.
2209 ///
2210 /// Core does no I/O and can't know how tall a picture or a player is, so this
2211 /// is the only way a placeholder grows past its default single row. The loop
2212 /// is: lay out at the current reservation → measure → call this → repaint if
2213 /// it changed. Handing over the same measurements again is a no-op, so a
2214 /// renderer can report its current state each frame without diffing first.
2215 ///
2216 /// A frontend that lays media out in its own units and simply reserves the
2217 /// vertical space itself (the way the gpui GUI does with images) never needs
2218 /// to call this at all.
2219 pub fn set_media_rows(&self, heights: Vec<MediaHeight>) -> DocView {
2220 let mut g = self.lock();
2221 g.doc.set_media_rows(
2222 heights
2223 .into_iter()
2224 .map(|h| (h.destination, h.rows.max(1) as usize))
2225 .collect(),
2226 );
2227 g.frame()
2228 }
2229
2230 /// Report how many visual rows each display formula needs, keyed by its
2231 /// TeX as [`MathView`] handed it over — [`set_media_rows`]'s peer for a
2232 /// renderer that reserves rows. One that lays a formula out in its own
2233 /// units, as the Swift views do, never calls this.
2234 ///
2235 /// [`set_media_rows`]: Self::set_media_rows
2236 pub fn set_math_rows(&self, heights: Vec<MathHeight>) -> DocView {
2237 let mut g = self.lock();
2238 g.doc.set_math_rows(
2239 heights
2240 .into_iter()
2241 .map(|h| (h.tex, h.rows.max(1) as usize))
2242 .collect(),
2243 );
2244 g.frame()
2245 }
2246
2247 /// Say whether the renderer can paint a picture *inside* a line of text.
2248 /// When it can, an inline formula arrives as one `math` run and a
2249 /// [`MathView`] with `inline` set, for the renderer to draw its typeset
2250 /// picture over; when it cannot, as the code-styled TeX it always was.
2251 /// Off until called, so a host that has not caught up sees what it saw.
2252 pub fn set_inline_pictures(&self, on: bool) -> DocView {
2253 let mut g = self.lock();
2254 g.doc.set_inline_pictures(on);
2255 g.frame()
2256 }
2257
2258 /// Insert a block-level image, video, or audio at the caret. Any selection
2259 /// becomes the alt / fallback text. See [`leaf_core::Doc::insert_media`] for
2260 /// the markup each kind spells.
2261 pub fn insert_media(&self, kind: MediaKind, destination: String, alt: String) -> DocView {
2262 let mut g = self.lock();
2263 let kind = match kind {
2264 MediaKind::Image => CoreMediaKind::Image,
2265 MediaKind::Video => CoreMediaKind::Video,
2266 MediaKind::Audio => CoreMediaKind::Audio,
2267 };
2268 g.doc.insert_media(kind, &destination, &alt);
2269 g.frame()
2270 }
2271
2272 /// Append an image, video, or audio at the end of the document, as a
2273 /// block of its own — for media that *arrives* rather than media the
2274 /// writer places at the caret. See [`leaf_core::Doc::append_media`].
2275 pub fn append_media(&self, kind: MediaKind, destination: String, alt: String) -> DocView {
2276 let mut g = self.lock();
2277 let kind = match kind {
2278 MediaKind::Image => CoreMediaKind::Image,
2279 MediaKind::Video => CoreMediaKind::Video,
2280 MediaKind::Audio => CoreMediaKind::Audio,
2281 };
2282 g.doc.append_media(kind, &destination, &alt);
2283 g.frame()
2284 }
2285
2286 /// Move the caret's block one place up — Alt+↑ and the Format menu's Move
2287 /// Block Up: above the block before it, and out of its container to just
2288 /// above it when it is the first block there. A list item goes with its
2289 /// children. The caret rides the block. Gate on
2290 /// [`Capabilities::move_block`]; see [`leaf_core::Doc::move_block_up`].
2291 pub fn move_block_up(&self) -> DocView {
2292 let mut g = self.lock();
2293 g.doc.move_block_up();
2294 g.frame()
2295 }
2296
2297 /// Move the caret's block one place down — the mirror of
2298 /// [`move_block_up`](Self::move_block_up).
2299 pub fn move_block_down(&self) -> DocView {
2300 let mut g = self.lock();
2301 g.doc.move_block_down();
2302 g.frame()
2303 }
2304
2305 /// Move the block at source offset `from` to the boundary `to` — the drop
2306 /// half of a drag, with `to` from [`drop_target_at`](Self::drop_target_at)
2307 /// and `from` any offset inside the block being carried. One undo step;
2308 /// the caret rides the block; a drop back onto the block's own boundary
2309 /// is a quiet no-op. See [`leaf_core::Doc::move_block`].
2310 pub fn move_block(&self, from: u32, to: u32) -> DocView {
2311 let mut g = self.lock();
2312 g.doc.move_block(from as usize, to as usize);
2313 g.frame()
2314 }
2315
2316 /// The source range of the block a drag starting at visual `(row, ch)`
2317 /// would pick up — the whole paragraph, picture, table or fence, or the
2318 /// whole list item with its children — for the outline drawn under the
2319 /// pointer. `None` on a blank line. Does not move the caret; map the pair
2320 /// through [`row_range_for`](Self::row_range_for) for the rows.
2321 pub fn block_range_at(&self, row: u32, ch: u32) -> Option<LandingView> {
2322 let mut g = self.lock();
2323 g.sync();
2324 let col = utf16_to_col(&g.row_text(row as usize), ch as usize);
2325 let off = g.offset_of_col(row as usize, col);
2326 g.doc.block_range_at(off).map(|r| LandingView {
2327 start: r.start as u32,
2328 end: r.end as u32,
2329 })
2330 }
2331
2332 /// Where a block dragged over visual `row` would land: the boundary
2333 /// before the row's block when the row is in its upper half, after it
2334 /// otherwise, the document's end for a row below everything. `None` for
2335 /// a row with no block under it. See [`leaf_core::Doc::drop_target_at`].
2336 pub fn drop_target_at(&self, row: u32) -> Option<DropTargetView> {
2337 let mut g = self.lock();
2338 g.sync();
2339 g.doc.drop_target_at(row as usize).map(DropTargetView::from)
2340 }
2341
2342 /// Insert a thematic break (`---`) at the caret — the toolbar's Horizontal
2343 /// Rule button. See [`leaf_core::Doc::insert_thematic_break`] for how it
2344 /// handles a selection, a blank line, and the caret sitting mid-paragraph,
2345 /// mid-list, or inside a quote.
2346 pub fn insert_thematic_break(&self) -> DocView {
2347 let mut g = self.lock();
2348 g.doc.insert_thematic_break();
2349 g.frame()
2350 }
2351
2352 /// The current source text — for a save (write to disk / iCloud / a document
2353 /// wrapper) or a source-view display.
2354 pub fn source(&self) -> String {
2355 self.lock().doc.source.clone()
2356 }
2357
2358 /// The selected text, if any — for a clipboard copy/cut.
2359 pub fn selected_text(&self) -> Option<String> {
2360 self.lock().doc.selected_text().map(str::to_string)
2361 }
2362
2363 /// The selection as a quote with up to `context` characters of what
2364 /// surrounded it, cut from the **source** — the shape a host that cites or
2365 /// annotates a passage wants, findable in the document again by plain
2366 /// string search. `None` when nothing is selected. See
2367 /// `leaf_core::Doc::selection_quote`.
2368 pub fn selection_quote(&self, context: u32) -> Option<SelectionQuote> {
2369 let g = self.lock();
2370 g.doc
2371 .selection_quote(context as usize)
2372 .map(|q| SelectionQuote {
2373 exact: q.exact,
2374 prefix: q.prefix,
2375 suffix: q.suffix,
2376 start: q.start as u64,
2377 end: q.end as u64,
2378 })
2379 }
2380
2381 /// Words, characters, and paragraphs over the whole document — the numbers
2382 /// a status bar or an inspector puts next to a piece of writing.
2383 ///
2384 /// Counted over the text a reader sees rather than the markup that spells
2385 /// it: `**bold**` is one word and four characters, a link is its label and
2386 /// not its destination, a picture counts nothing, and frontmatter is not
2387 /// writing. The same in both views — the count reads neither the view nor
2388 /// the map the host last built. See `leaf_core::Doc::counts`.
2389 ///
2390 /// It is O(document) and not free (about 4 ms on a 45 KB file), so ask
2391 /// when the typing settles rather than on every keystroke; there is no
2392 /// `DocView` in it, because nothing about the document changes by being
2393 /// counted.
2394 pub fn counts(&self) -> TextCounts {
2395 self.lock().doc.counts().into()
2396 }
2397
2398 /// The same statistics over the selection alone — `None` when nothing is
2399 /// selected. See `leaf_core::Doc::selection_counts`.
2400 pub fn selection_counts(&self) -> Option<TextCounts> {
2401 self.lock().doc.selection_counts().map(Into::into)
2402 }
2403
2404 /// Whether the document refuses to change — see `set_read_only`.
2405 pub fn read_only(&self) -> bool {
2406 self.lock().doc.read_only()
2407 }
2408
2409 /// Turn the read-only gate on or off — a *reading* surface over the same
2410 /// rendering, selection and navigation the editor has. Enforced in core at
2411 /// the three doors every mutation goes through, so a host that also quiets
2412 /// its input chrome is polishing, not protecting.
2413 pub fn set_read_only(&self, on: bool) -> DocView {
2414 let mut g = self.lock();
2415 g.doc.set_read_only(on);
2416 g.frame()
2417 }
2418
2419 /// Replace the host-painted source ranges wholesale and repaint — see
2420 /// `leaf_core::Doc::set_highlights` for why it is a replace, and
2421 /// [`Highlight`] for what one is.
2422 pub fn set_highlights(&self, highlights: Vec<Highlight>) -> DocView {
2423 let mut g = self.lock();
2424 let hls = highlights
2425 .into_iter()
2426 .map(|h| leaf_core::Highlight {
2427 start: h.start as usize,
2428 end: h.end as usize,
2429 id: h.id,
2430 color: h.color,
2431 marker: h.marker,
2432 })
2433 .collect();
2434 g.doc.set_highlights(hls);
2435 g.frame()
2436 }
2437
2438 /// The id of the highlight covering source `offset`, if one does — what a
2439 /// frontend asks when the reader activates a spot on the page.
2440 pub fn highlight_at(&self, offset: u32) -> Option<String> {
2441 self.lock()
2442 .doc
2443 .highlight_at(offset as usize)
2444 .map(|h| h.id.clone())
2445 }
2446
2447 /// The host-painted ranges as last set, sorted by start — what a frontend
2448 /// walks to lay out margin markers.
2449 pub fn highlights(&self) -> Vec<Highlight> {
2450 self.lock()
2451 .doc
2452 .highlights()
2453 .iter()
2454 .map(|h| Highlight {
2455 start: h.start as u64,
2456 end: h.end as u64,
2457 id: h.id.clone(),
2458 color: h.color.clone(),
2459 marker: h.marker.clone(),
2460 })
2461 .collect()
2462 }
2463
2464 /// Mark the buffer saved after the host persisted [`LeafDoc::source`] its own
2465 /// way — clears the dirty flag without touching a filesystem.
2466 pub fn mark_saved(&self) -> DocView {
2467 let mut g = self.lock();
2468 g.doc.mark_saved();
2469 g.frame()
2470 }
2471
2472 /// [`LeafDoc::mark_saved`] for a write that took a while: `saved` is the
2473 /// [`LeafDoc::source`] the host read before writing, and whatever was
2474 /// typed since stays dirty.
2475 pub fn mark_saved_as(&self, saved: String) -> DocView {
2476 let mut g = self.lock();
2477 g.doc.mark_saved_as(&saved);
2478 g.frame()
2479 }
2480
2481 // ── text input ───────────────────────────────────────────────────────────
2482
2483 pub fn insert(&self, text: String) -> DocView {
2484 let mut g = self.lock();
2485 g.doc.insert(&text);
2486 g.frame()
2487 }
2488
2489 pub fn paste(&self, text: String) -> DocView {
2490 let mut g = self.lock();
2491 g.doc.paste(&text);
2492 g.frame()
2493 }
2494
2495 pub fn newline(&self) -> DocView {
2496 let mut g = self.lock();
2497 g.doc.newline();
2498 g.frame()
2499 }
2500
2501 /// Tab away from a table: indent the caret's line (or the selected lines) one
2502 /// level, nesting a list item under its sibling. The frontend calls this when
2503 /// [`LeafDoc::cell_tab`] declined because the caret isn't in a table.
2504 pub fn indent(&self) -> DocView {
2505 let mut g = self.lock();
2506 g.doc.indent();
2507 g.frame()
2508 }
2509
2510 /// Shift+Tab away from a table: take one indent level back off the caret's
2511 /// line (or the selected lines), unnesting a list item. The mirror of
2512 /// [`LeafDoc::indent`].
2513 pub fn outdent(&self) -> DocView {
2514 let mut g = self.lock();
2515 g.doc.outdent();
2516 g.frame()
2517 }
2518
2519 // ── table keys ────────────────────────────────────────────────────────────
2520 // Tab, Return, and Shift+Return take on table meanings when the caret is in
2521 // one. Each returns `Some(view)` when it acted as a table key and `None` when
2522 // the caret isn't in a table — the frontend then does the key's ordinary job
2523 // (indent, newline), so these keep their meaning everywhere else.
2524
2525 /// Tab (`forward`) / Shift+Tab hops to the next/previous cell; Tab past the
2526 /// last cell appends a fresh row and enters it.
2527 pub fn cell_tab(&self, forward: bool) -> Option<DocView> {
2528 let mut g = self.lock();
2529 g.sync();
2530 g.doc.cell_tab(forward).then(|| g.frame())
2531 }
2532
2533 /// Return drops to the cell below in the same column, appending a row at the
2534 /// table's bottom.
2535 pub fn cell_return(&self) -> Option<DocView> {
2536 let mut g = self.lock();
2537 g.sync();
2538 g.doc.cell_return().then(|| g.frame())
2539 }
2540
2541 /// Shift+Return inserts a hard line break *within* the current cell.
2542 pub fn cell_line_break(&self) -> Option<DocView> {
2543 let mut g = self.lock();
2544 g.sync();
2545 g.doc.cell_line_break().then(|| g.frame())
2546 }
2547
2548 pub fn backspace(&self) -> DocView {
2549 let mut g = self.lock();
2550 g.doc.backspace();
2551 g.frame()
2552 }
2553
2554 pub fn delete_forward(&self) -> DocView {
2555 let mut g = self.lock();
2556 g.doc.delete_forward();
2557 g.frame()
2558 }
2559
2560 pub fn delete_word_back(&self) -> DocView {
2561 let mut g = self.lock();
2562 g.doc.delete_word_back();
2563 g.frame()
2564 }
2565
2566 pub fn delete_word_forward(&self) -> DocView {
2567 let mut g = self.lock();
2568 g.doc.delete_word_forward();
2569 g.frame()
2570 }
2571
2572 // ── caret movement ───────────────────────────────────────────────────────
2573 // Each syncs the grid first (movement reads the stop table / column layout),
2574 // moves, then repaints — `Inner::view` re-syncs but that's the cached no-op.
2575
2576 pub fn move_left(&self, extend: bool) -> DocView {
2577 let mut g = self.lock();
2578 g.sync();
2579 g.doc.move_left(extend);
2580 g.frame()
2581 }
2582
2583 pub fn move_right(&self, extend: bool) -> DocView {
2584 let mut g = self.lock();
2585 g.sync();
2586 g.doc.move_right(extend);
2587 g.frame()
2588 }
2589
2590 pub fn move_up(&self, extend: bool) -> DocView {
2591 let mut g = self.lock();
2592 g.sync();
2593 g.doc.move_up(extend);
2594 g.frame()
2595 }
2596
2597 pub fn move_down(&self, extend: bool) -> DocView {
2598 let mut g = self.lock();
2599 g.sync();
2600 g.doc.move_down(extend);
2601 g.frame()
2602 }
2603
2604 pub fn move_word_left(&self, extend: bool) -> DocView {
2605 let mut g = self.lock();
2606 g.sync();
2607 g.doc.move_word_left(extend);
2608 g.frame()
2609 }
2610
2611 pub fn move_word_right(&self, extend: bool) -> DocView {
2612 let mut g = self.lock();
2613 g.sync();
2614 g.doc.move_word_right(extend);
2615 g.frame()
2616 }
2617
2618 pub fn move_home(&self, extend: bool) -> DocView {
2619 let mut g = self.lock();
2620 g.sync();
2621 g.doc.move_home(extend);
2622 g.frame()
2623 }
2624
2625 pub fn move_end(&self, extend: bool) -> DocView {
2626 let mut g = self.lock();
2627 g.sync();
2628 g.doc.move_end(extend);
2629 g.frame()
2630 }
2631
2632 pub fn move_doc_start(&self, extend: bool) -> DocView {
2633 let mut g = self.lock();
2634 g.sync();
2635 g.doc.move_doc_start(extend);
2636 g.frame()
2637 }
2638
2639 pub fn move_doc_end(&self, extend: bool) -> DocView {
2640 let mut g = self.lock();
2641 g.sync();
2642 g.doc.move_doc_end(extend);
2643 g.frame()
2644 }
2645
2646 pub fn select_all(&self) -> DocView {
2647 let mut g = self.lock();
2648 g.doc.select_all();
2649 g.frame()
2650 }
2651
2652 /// Place the caret from a click, in core's column grid: `row` indexes the
2653 /// visual [`Row`]s and `col` is the glyph column within it. Core clamps both
2654 /// to real caret stops. Prefer [`LeafDoc::click_ch`] from a proportional
2655 /// renderer.
2656 pub fn click(&self, row: u32, col: u32, extend: bool) -> DocView {
2657 let mut g = self.lock();
2658 g.sync();
2659 g.doc.click(row as usize, col as usize, extend);
2660 g.frame()
2661 }
2662
2663 /// Place the caret from a click whose horizontal position is a **UTF-16
2664 /// offset** into the visual row's text — what `characterIndex(for:)` hands
2665 /// back. Converted to core's display column before clicking, so a proportional
2666 /// renderer never reasons about column widths itself.
2667 pub fn click_ch(&self, row: u32, ch: u32, extend: bool) -> DocView {
2668 let mut g = self.lock();
2669 g.sync();
2670 let col = utf16_to_col(&g.row_text(row as usize), ch as usize);
2671 g.doc.click(row as usize, col, extend);
2672 g.frame()
2673 }
2674
2675 /// Select the word under a click (row, `ch`) — the double-click gesture.
2676 pub fn select_word_ch(&self, row: u32, ch: u32) -> DocView {
2677 let mut g = self.lock();
2678 let off = g.offset_at(row as usize, ch as usize);
2679 g.doc.select_word_at(off);
2680 g.frame()
2681 }
2682
2683 /// Select the whole logical text block under a click (row, `ch`) — the
2684 /// triple-click gesture. Grabs the entire block even where it soft-wraps.
2685 pub fn select_block_ch(&self, row: u32, ch: u32) -> DocView {
2686 let mut g = self.lock();
2687 let off = g.offset_at(row as usize, ch as usize);
2688 g.doc.select_block_at(off);
2689 g.frame()
2690 }
2691
2692 /// A click in the blank space under the last row — the caret goes onto an
2693 /// empty paragraph under the last block, opening one if the document does
2694 /// not end with one, wherever the pointer was horizontally. See
2695 /// [`leaf_core::Doc::click_past_end`]. The frontend decides "under": the
2696 /// point is below every line box it laid out.
2697 pub fn click_past_end(&self) -> DocView {
2698 let mut g = self.lock();
2699 g.sync();
2700 g.doc.click_past_end();
2701 g.frame()
2702 }
2703
2704 /// Mirror a native selection into the model: `[anchor, focus]` given as
2705 /// row + UTF-16 offset pairs. Each is resolved to a source offset the way a
2706 /// click is, then set as the selection's fixed and moving ends. A collapsed
2707 /// range (`anchor == focus`) just places the caret.
2708 pub fn set_selection(
2709 &self,
2710 anchor_row: u32,
2711 anchor_ch: u32,
2712 focus_row: u32,
2713 focus_ch: u32,
2714 ) -> DocView {
2715 let mut g = self.lock();
2716 let anchor = g.offset_at(anchor_row as usize, anchor_ch as usize);
2717 let focus = g.offset_at(focus_row as usize, focus_ch as usize);
2718 g.doc.place_caret(anchor, false);
2719 if anchor != focus {
2720 g.doc.place_caret(focus, true);
2721 }
2722 g.frame()
2723 }
2724
2725 // ── rich clipboard (mirrors leaf-tui / leaf-gpui / leaf-wasm) ─────────────
2726
2727 /// The current selection rendered to HTML by twig — the rich flavor a copy
2728 /// writes alongside the plain [`LeafDoc::selected_text`]. `None` when nothing
2729 /// is selected.
2730 pub fn selection_html(&self) -> Option<String> {
2731 self.lock().doc.selection_html()
2732 }
2733
2734 /// Paste, preferring the clipboard's rich (`text/html`) flavor: twig parses
2735 /// `html` into the document's own markup and inserts it. Falls back to the
2736 /// plain `text` when there's no HTML or it doesn't parse.
2737 pub fn paste_rich(&self, html: Option<String>, text: String) -> DocView {
2738 let mut g = self.lock();
2739 let took = html.as_deref().is_some_and(|h| g.doc.paste_html(h));
2740 if !took {
2741 g.doc.paste(&text);
2742 }
2743 g.frame()
2744 }
2745
2746 // ── formatting commands (mirror leaf-gpui's EditorCommand) ────────────────
2747
2748 pub fn toggle_bold(&self) -> DocView {
2749 let mut g = self.lock();
2750 g.doc.toggle(InlineKind::Strong);
2751 g.frame()
2752 }
2753
2754 pub fn toggle_italic(&self) -> DocView {
2755 let mut g = self.lock();
2756 g.doc.toggle(InlineKind::Emph);
2757 g.frame()
2758 }
2759
2760 pub fn toggle_code(&self) -> DocView {
2761 let mut g = self.lock();
2762 g.doc.toggle(InlineKind::Verbatim);
2763 g.frame()
2764 }
2765
2766 pub fn toggle_mark(&self) -> DocView {
2767 let mut g = self.lock();
2768 g.doc.toggle(InlineKind::Mark);
2769 g.frame()
2770 }
2771
2772 /// Whether the caret stands in a highlight — what a colour palette enables
2773 /// itself by, since a colour is a property of a highlight that already
2774 /// exists. The caret-side half of [`Capabilities::mark_color`].
2775 pub fn caret_in_mark(&self) -> bool {
2776 self.lock().doc.caret_in_mark()
2777 }
2778
2779 /// Colour the highlight at the caret, or clear its colour with `None`.
2780 ///
2781 /// Markdown only — `==🔴 text==` is its spelling and djot has none — and
2782 /// only where there is a highlight to colour: this does not make one, so a
2783 /// coloured highlight from bare text is [`LeafDoc::toggle_mark`] and then
2784 /// this, which is the order the button and its palette already sit in. Both
2785 /// refusals leave the document alone and say so in the status line.
2786 pub fn set_mark_color(&self, color: Option<MarkColor>) -> DocView {
2787 let mut g = self.lock();
2788 g.doc.set_mark_color(color.map(CoreMarkColor::from));
2789 g.frame()
2790 }
2791
2792 /// One press of a colour swatch: colour the highlight at the caret, or —
2793 /// over a selection that isn't highlighted yet — highlight it and colour it,
2794 /// as **one** undo step.
2795 ///
2796 /// [`LeafDoc::set_mark_color`] is the exact gesture; this is the compound a
2797 /// toolbar presses, and it lives in core so that every frontend answers
2798 /// "what does a swatch mean over plain text" the same way. `None` clears the
2799 /// colour, and over an unhighlighted selection means simply "highlight
2800 /// this". A bare caret in no highlight is left alone.
2801 pub fn highlight(&self, color: Option<MarkColor>) -> DocView {
2802 let mut g = self.lock();
2803 g.doc.highlight(color.map(CoreMarkColor::from));
2804 g.frame()
2805 }
2806
2807 pub fn toggle_underline(&self) -> DocView {
2808 let mut g = self.lock();
2809 g.doc.toggle(InlineKind::Insert);
2810 g.frame()
2811 }
2812
2813 pub fn toggle_strike(&self) -> DocView {
2814 let mut g = self.lock();
2815 g.doc.toggle(InlineKind::Delete);
2816 g.frame()
2817 }
2818
2819 pub fn set_paragraph(&self) -> DocView {
2820 let mut g = self.lock();
2821 g.doc.set_block(BlockKind::Paragraph);
2822 g.frame()
2823 }
2824
2825 /// Toggle the current block to a heading of `level` (1–6); toggling the
2826 /// active level off returns it to a paragraph, per core.
2827 pub fn set_heading(&self, level: u32) -> DocView {
2828 let mut g = self.lock();
2829 g.doc.toggle_heading(level);
2830 g.frame()
2831 }
2832
2833 pub fn toggle_blockquote(&self) -> DocView {
2834 let mut g = self.lock();
2835 g.doc.toggle_blockquote();
2836 g.frame()
2837 }
2838
2839 pub fn toggle_list(&self, ordered: bool) -> DocView {
2840 let mut g = self.lock();
2841 g.doc.toggle_list(ordered);
2842 g.frame()
2843 }
2844
2845 /// Toggle a fenced code block over the selection or the block at the caret;
2846 /// on a blank line, open an empty one with the caret inside. See
2847 /// [`leaf_core::Doc::toggle_code_block`]. Gate on
2848 /// [`Capabilities::code_block`]; light from [`DocView::code_block`].
2849 pub fn toggle_code_block(&self) -> DocView {
2850 let mut g = self.lock();
2851 g.doc.toggle_code_block();
2852 g.frame()
2853 }
2854
2855 /// Tick or untick the task item at the caret. See
2856 /// [`leaf_core::Doc::toggle_task_checked`].
2857 pub fn toggle_task_checked(&self) -> DocView {
2858 let mut g = self.lock();
2859 g.doc.toggle_task_checked();
2860 g.frame()
2861 }
2862
2863 /// Tick or untick the task item covering `offset` — a tap on a rendered
2864 /// checkbox, which must not drag the caret across the document to get there.
2865 pub fn toggle_task_at(&self, offset: u64) -> DocView {
2866 let mut g = self.lock();
2867 g.doc.toggle_task_at(offset as usize);
2868 g.frame()
2869 }
2870
2871 /// Give the list item at the caret a checkbox, or take its checkbox away.
2872 pub fn toggle_task_item(&self) -> DocView {
2873 let mut g = self.lock();
2874 g.doc.toggle_task_item();
2875 g.frame()
2876 }
2877
2878 /// Whether the item at the caret has a box and which way it faces — `None`
2879 /// for a plain list item or no item at all. Drives a toolbar's checked state.
2880 pub fn task_checked_at_caret(&self) -> Option<bool> {
2881 let mut g = self.lock();
2882 g.doc.task_checked_at_caret()
2883 }
2884
2885 // ── the presentation vocabulary ───────────────────────────────────────────
2886 //
2887 // Six gestures and five queries over a document's *presentation*: alignment
2888 // and line spacing, which are the block's, and size, face and colour, which
2889 // are the run's. Each value is a **name** the renderer's theme resolves —
2890 // `center`, `1.5`, `large`, `serif`, `red` — or, for the four properties
2891 // whose type is open, the exact value the author asked for: `14pt`,
2892 // `Garamond`, `#c03030`, `1.3`. A name outlives the theme it was written
2893 // under and a value does not, which is the trade the *Other…* row makes
2894 // visible; see [`FontSize`] and its three peers.
2895 //
2896 // Each gesture edits one attribute key and keeps the rest, so a document
2897 // from elsewhere passes through the editor unharmed, and `nil` clears the
2898 // key. Each takes its enabled state from the matching [`Capabilities`] flag,
2899 // and its *lit* state from the query beside it — the queries read the
2900 // nearest node that names the property, so a control follows the caret into
2901 // a centred `<div>` the way the H1 light follows it into a heading.
2902
2903 /// Align the caret's block, or return it to the theme's default with `nil`.
2904 ///
2905 /// A block property, so it is the caret's *block* whatever is selected: a
2906 /// line belongs to a block, and "centre this" with three words selected
2907 /// means the paragraph, not the words. Other `class` tokens on the block are
2908 /// kept. Gate on [`Capabilities::alignment`].
2909 pub fn set_alignment(&self, align: Option<Align>) -> DocView {
2910 let mut g = self.lock();
2911 g.doc.set_alignment(align.map(CoreAlign::from));
2912 g.frame()
2913 }
2914
2915 /// Set the line spacing of the caret's block, or return it to the theme's
2916 /// with `nil`. [`set_alignment`](Self::set_alignment)'s peer in every
2917 /// respect but the key. Gate on [`Capabilities::line_spacing`].
2918 ///
2919 /// `.step(.oneHalf)` from the menu's three, or `.ratio(1.3)` from an
2920 /// *Other…* field. A ratio that spells one of the three *is* that name, and
2921 /// a ratio of 1 is single spacing, which is absence and clears the key. A
2922 /// ratio the vocabulary cannot carry at all — `0`, `700`, a NaN — writes
2923 /// nothing, and the block's own spacing stands.
2924 pub fn set_line_spacing(&self, spacing: Option<LineHeight>) -> DocView {
2925 let mut g = self.lock();
2926 if let Some(spacing) = written(spacing, LineHeight::into_core) {
2927 g.doc.set_line_spacing(spacing);
2928 }
2929 g.frame()
2930 }
2931
2932 /// Set the size of the selected run, or of the caret's whole block when
2933 /// nothing is selected; `nil` returns it to the theme's own size.
2934 ///
2935 /// Size, face and colour are the *run's*, and the block's when no run is
2936 /// chosen — so "make this paragraph larger" is a press with the caret in it
2937 /// rather than a select-all first. With a selection the range is wrapped in
2938 /// an attributed span, or the span it already lies in is re-styled, never
2939 /// nested. Gate on [`Capabilities::font_size`].
2940 ///
2941 /// `.step(.large)` from the menu's seven, or `.points(14)` from an
2942 /// *Other…* field — a size in points, which is what the paper will show. A
2943 /// number of points the vocabulary cannot carry (0.01 to 655.35 is the
2944 /// range) writes nothing at all, and the run's own size stands: validate
2945 /// the field before calling, because from here the refusal is silent.
2946 pub fn set_font_size(&self, size: Option<FontSize>) -> DocView {
2947 let mut g = self.lock();
2948 if let Some(size) = written(size, FontSize::into_core) {
2949 g.doc.set_font_size(size);
2950 }
2951 g.frame()
2952 }
2953
2954 /// Set the face of the selected run, or of the caret's whole block.
2955 /// [`set_font_size`](Self::set_font_size)'s peer. Gate on
2956 /// [`Capabilities::font_family`].
2957 ///
2958 /// `.generic(.serif)` from the menu's four, or `.named("Garamond")` from
2959 /// the platform's font picker — which draws in that family where it is
2960 /// installed and in the theme's body face where it is not. A name that
2961 /// names nothing (`" "`) writes nothing, and the run's own face stands.
2962 pub fn set_font_family(&self, font: Option<FontFace>) -> DocView {
2963 let mut g = self.lock();
2964 if let Some(font) = written(font, FontFace::into_core) {
2965 g.doc.set_font_family(font);
2966 }
2967 g.frame()
2968 }
2969
2970 /// Set the *text* colour of the selected run, or of the caret's whole block.
2971 /// [`set_font_size`](Self::set_font_size)'s peer, and **not**
2972 /// [`set_mark_color`](Self::set_mark_color): that one colours a highlight's
2973 /// background and needs a highlight to colour, this one paints the letters
2974 /// and needs nothing. They share the seven names on purpose. Gate on
2975 /// [`Capabilities::text_color`].
2976 ///
2977 /// `.named(.red)` from the seven swatches, or `.rgb(r:g:b:)` from the
2978 /// system colour picker — painted as written in both appearances, which is
2979 /// what "exact" costs.
2980 pub fn set_text_color(&self, color: Option<TextColor>) -> DocView {
2981 let mut g = self.lock();
2982 g.doc.set_text_color(color.map(TextColor::into_core));
2983 g.frame()
2984 }
2985
2986 /// Insert a page break at the caret — a leaf directive with no label, placed
2987 /// exactly as [`insert_thematic_break`](Self::insert_thematic_break) places a
2988 /// rule, a selection replaced by it and a bare paragraph parted at the caret
2989 /// first.
2990 ///
2991 /// A frontend that paginates opens a page at the row's directive mark and
2992 /// gives the row no height; one that does not draws the `⧉ page-break`
2993 /// placeholder every leaf directive gets. Gate on
2994 /// [`Capabilities::page_break`].
2995 pub fn insert_page_break(&self) -> DocView {
2996 let mut g = self.lock();
2997 g.doc.insert_page_break();
2998 g.frame()
2999 }
3000
3001 /// The alignment in force at the caret, or `nil` for the theme's default —
3002 /// which segment of an alignment control is lit.
3003 ///
3004 /// Read off the nearest node that names one: the caret's block, and the
3005 /// `div`s around it after that, so the control follows the caret into a
3006 /// centred `<div>`.
3007 pub fn alignment_at_caret(&self) -> Option<Align> {
3008 let mut g = self.lock();
3009 g.doc.alignment_at_caret().map(Align::from)
3010 }
3011
3012 /// The line spacing in force at the caret, or `nil` for the theme's own —
3013 /// which entry a spacing menu shows ticked.
3014 /// [`alignment_at_caret`](Self::alignment_at_caret)'s peer.
3015 ///
3016 /// A `.ratio` is a spacing no row of the menu's three can tick, and is the
3017 /// author's own: the menu shows it as a row of its own above *Other…*.
3018 pub fn line_spacing_at_caret(&self) -> Option<LineHeight> {
3019 let mut g = self.lock();
3020 g.doc.line_spacing_at_caret().map(LineHeight::from)
3021 }
3022
3023 /// The size in force at the caret, or `nil` for the theme's own — which
3024 /// entry a size menu shows ticked. Run-level, so the chain starts one node
3025 /// deeper: the attributed span the caret stands in, then its block, then the
3026 /// `div`s around it, the nearest winning.
3027 ///
3028 /// A `.points` is a size no row of the menu's seven can tick, and the menu
3029 /// shows it as a row of its own — "14 pt" — above *Other…*.
3030 pub fn font_size_at_caret(&self) -> Option<FontSize> {
3031 let mut g = self.lock();
3032 g.doc.font_size_at_caret().map(FontSize::from)
3033 }
3034
3035 /// The face in force at the caret, or `nil` for the theme's body face.
3036 /// [`font_size_at_caret`](Self::font_size_at_caret)'s peer, and a `.named`
3037 /// is the family the author picked, shown as its own ticked row.
3038 pub fn font_family_at_caret(&self) -> Option<FontFace> {
3039 let mut g = self.lock();
3040 g.doc.font_family_at_caret().map(FontFace::from)
3041 }
3042
3043 /// The *text* colour in force at the caret, or `nil` for the theme's — which
3044 /// swatch a text-colour control marks as the current one.
3045 /// [`font_size_at_caret`](Self::font_size_at_caret)'s peer, and not
3046 /// [`DocView::mark_color`], which reads a highlight's background off a
3047 /// `mark` node the caret is standing in. A `.rgb` is the author's own
3048 /// triple, which the palette shows as a swatch of its own.
3049 pub fn text_color_at_caret(&self) -> Option<TextColor> {
3050 let mut g = self.lock();
3051 g.doc.text_color_at_caret().map(TextColor::from)
3052 }
3053
3054 /// Which of the formatting commands above this document's format can
3055 /// actually spell — one flag per control, for building the toolbar.
3056 ///
3057 /// Read once when a document opens: the answer depends only on the format,
3058 /// so it cannot change under an edit. Every command refuses on its own
3059 /// regardless — the model is the authority, not the toolbar — so a frontend
3060 /// that ignores this stays correct, it just offers buttons whose only effect
3061 /// is a line in the status bar.
3062 ///
3063 /// Don't collapse it to one flag. An HTML document takes ⌘B, ⌘I and inline
3064 /// code (its marks are a tag pair) while refusing every heading, list, quote
3065 /// and link, and Markdown refuses the superscript djot spells — so a toolbar
3066 /// driven by [`Self::authorable`] alone would be wrong in both directions.
3067 pub fn capabilities(&self) -> Capabilities {
3068 self.lock().doc.capabilities().into()
3069 }
3070
3071 /// Whether this document's format offers *any* door in — `false` only for a
3072 /// wholly parse-only one (XML), where an app may as well open the file
3073 /// read-only and hide the formatting section outright. For anything finer,
3074 /// including whether to dim an individual button, use [`Self::capabilities`].
3075 pub fn authorable(&self) -> bool {
3076 self.lock().doc.authorable()
3077 }
3078
3079 // ── table editing ─────────────────────────────────────────────────────────
3080
3081 /// Whether the caret is inside a table — for enabling the table controls.
3082 /// Pair it with [`Capabilities::table`]: the caret is genuinely inside an
3083 /// HTML `<table>`, and the grid controls still cannot edit one.
3084 pub fn caret_in_table(&self) -> bool {
3085 self.lock().doc.caret_in_table()
3086 }
3087
3088 /// Insert an empty row below (`below`) or above the caret's row.
3089 pub fn table_insert_row(&self, below: bool) -> DocView {
3090 let mut g = self.lock();
3091 g.doc.table_insert_row(below);
3092 g.frame()
3093 }
3094
3095 /// Delete the caret's row (not the header or the last body row).
3096 pub fn table_delete_row(&self) -> DocView {
3097 let mut g = self.lock();
3098 g.doc.table_delete_row();
3099 g.frame()
3100 }
3101
3102 /// Insert an empty column right (`right`) or left of the caret's column.
3103 pub fn table_insert_column(&self, right: bool) -> DocView {
3104 let mut g = self.lock();
3105 g.doc.table_insert_column(right);
3106 g.frame()
3107 }
3108
3109 /// Delete the caret's column (unless it is the only one).
3110 pub fn table_delete_column(&self) -> DocView {
3111 let mut g = self.lock();
3112 g.doc.table_delete_column();
3113 g.frame()
3114 }
3115
3116 /// Set the caret's column to `alignment`.
3117 pub fn table_set_alignment(&self, alignment: TableAlignment) -> DocView {
3118 let mut g = self.lock();
3119 g.doc.table_set_alignment(alignment.into_core());
3120 g.frame()
3121 }
3122
3123 /// Move the caret's row one place down (`down`) or up.
3124 pub fn table_move_row(&self, down: bool) -> DocView {
3125 let mut g = self.lock();
3126 g.doc.table_move_row(down);
3127 g.frame()
3128 }
3129
3130 /// Move the caret's column one place right (`right`) or left.
3131 pub fn table_move_column(&self, right: bool) -> DocView {
3132 let mut g = self.lock();
3133 g.doc.table_move_column(right);
3134 g.frame()
3135 }
3136
3137 /// Insert a fresh table at the caret — one header row, `rows` empty body
3138 /// rows, `cols` columns — and leave the caret in its first header cell.
3139 /// The one table verb that needs no table under the caret; gate it on
3140 /// [`Capabilities::table`] alone. See [`leaf_core::Doc::insert_table`] for
3141 /// the placement (a paragraph is parted around the caret, as for the rule)
3142 /// and for what a zero shape does.
3143 pub fn insert_table(&self, rows: u32, cols: u32) -> DocView {
3144 let mut g = self.lock();
3145 g.doc.insert_table(rows as usize, cols as usize);
3146 g.frame()
3147 }
3148
3149 pub fn insert_link(&self, destination: String) -> DocView {
3150 let mut g = self.lock();
3151 g.doc.insert_link(&destination);
3152 g.frame()
3153 }
3154
3155 /// The destination of the link under the caret, if the caret is inside one —
3156 /// so a frontend can open it (⌘-click / "Open Link") or show it. `None` when the
3157 /// caret isn't on a link.
3158 pub fn link_destination_at_caret(&self) -> Option<String> {
3159 self.lock().doc.link_destination_at_caret()
3160 }
3161
3162 /// The source of the image the caret stands in — the `src` of an
3163 /// `` or a `<img>`, exactly as the document spells it. `None`
3164 /// when the caret is in no image.
3165 ///
3166 /// Two hosts ask. An image prompt seeds from it, so editing an existing
3167 /// picture starts from its current URL rather than blank; and a host that
3168 /// gives an attachment a place of its own — a node, a page, a file
3169 /// inspector — asks it to answer "show me *this* one" from a menu raised
3170 /// over the body. See `LeafEditorModel.onShowMedia` in the Swift package.
3171 ///
3172 /// A caret resting just after a block image (its trailing stop) is already
3173 /// past it and gets `None`, which is the same half-open rule
3174 /// [`link_destination_at_caret`](Self::link_destination_at_caret) follows.
3175 pub fn image_destination_at_caret(&self) -> Option<String> {
3176 self.lock().doc.image_destination_at_caret()
3177 }
3178
3179 /// The destination of the link at byte offset `off` —
3180 /// [`link_destination_at_caret`](Self::link_destination_at_caret) for a place
3181 /// the caret isn't.
3182 ///
3183 /// What a frontend drawing part of the document *outside* the document asks:
3184 /// a footnote's text in a popover has link runs in it, and this is how those
3185 /// runs learn where they point, since a `Run` carries how a span looks and
3186 /// not what it means.
3187 pub fn link_destination_at(&self, off: u32) -> Option<String> {
3188 self.lock().doc.link_destination_at(off as usize)
3189 }
3190
3191 /// The heading byte offset `off` is under — the nearest heading at or
3192 /// above it — or `None` above the first. What a host writing a link *to*
3193 /// a place names it by: the `#slug` comes from its `text`. See
3194 /// [`leaf_core::Doc::heading_at`].
3195 pub fn heading_at(&self, off: u32) -> Option<HeadingView> {
3196 self.lock()
3197 .doc
3198 .heading_at(off as usize)
3199 .map(HeadingView::from)
3200 }
3201
3202 /// The heading the caret is under — [`heading_at`](Self::heading_at) at
3203 /// the caret.
3204 pub fn heading_at_caret(&self) -> Option<HeadingView> {
3205 self.lock().doc.heading_at_caret().map(HeadingView::from)
3206 }
3207
3208 /// Where the locator `id` lands in this document — the `#v2` half of a
3209 /// `chapter.dj#v2`, resolved to the block it names. `None` when nothing here
3210 /// answers to it, which is a host's cue to open the document at its top
3211 /// rather than refuse to go.
3212 ///
3213 /// The query that gives a link finer granularity than the file. It reads an
3214 /// explicit `{#v1}`, a djot heading's minted id, or (for Markdown, which
3215 /// mints none) a heading's own words slugged — see [`leaf_core::Doc::locate`].
3216 ///
3217 /// Asked of *any* document, not only the open one: a host peeking at a
3218 /// citation builds a [`LeafDoc`] over the other file's bytes and asks this,
3219 /// which is what lets a hover show the verse instead of the filename.
3220 pub fn locate(&self, id: String) -> Option<LandingView> {
3221 self.lock().doc.locate(&id).map(LandingView::from)
3222 }
3223
3224 /// Write a footnote at the caret — the toolbar's Footnote button. Both the
3225 /// `[^1]` and the definition it needs go in as one edit (one undo takes both
3226 /// back), the label is the lowest number the document has free, and the caret
3227 /// is left **in the empty note** ready to type it. Gate the button on
3228 /// [`Capabilities::footnote`]; see [`leaf_core::Doc::insert_footnote`].
3229 pub fn insert_footnote(&self) -> DocView {
3230 let mut g = self.lock();
3231 g.doc.insert_footnote();
3232 g.frame()
3233 }
3234
3235 /// The footnote reference under the caret, resolved to the note it names —
3236 /// so a frontend can show the note when a reader activates a `[1]`, instead
3237 /// of the nothing a reference click used to do. `None` when the caret isn't
3238 /// on a reference; see [`FootnoteView`] for the reference that resolved to
3239 /// no definition.
3240 pub fn footnote_at_caret(&self) -> Option<FootnoteView> {
3241 self.lock().doc.footnote_at_caret().map(FootnoteView::from)
3242 }
3243
3244 /// The footnote reference at byte offset `off`, resolved to the note it
3245 /// names — [`footnote_at_caret`](Self::footnote_at_caret) for a place the
3246 /// caret isn't.
3247 ///
3248 /// This is what a hover asks: a pointer resting on a `[1]` wants the note's
3249 /// text in a popover, and moving the caret to find out would yank the reader
3250 /// out of wherever they were typing.
3251 pub fn footnote_at(&self, off: u32) -> Option<FootnoteView> {
3252 self.lock()
3253 .doc
3254 .footnote_at(off as usize)
3255 .map(FootnoteView::from)
3256 }
3257
3258 /// The footnote definition the caret stands in, and where the reference that
3259 /// names it is — the return leg of [`footnote_at_caret`](Self::footnote_at_caret),
3260 /// so following a footnote is a round trip rather than a fall.
3261 ///
3262 /// `None` when the caret isn't in a definition, which is also how a frontend
3263 /// tells the two directions apart: the reference query answers up top, this
3264 /// one answers down in the notes, and never both at once.
3265 pub fn footnote_definition_at_caret(&self) -> Option<FootnoteDefView> {
3266 self.lock()
3267 .doc
3268 .footnote_definition_at_caret()
3269 .map(FootnoteDefView::from)
3270 }
3271
3272 pub fn undo(&self) -> DocView {
3273 let mut g = self.lock();
3274 g.doc.undo();
3275 g.frame()
3276 }
3277
3278 pub fn redo(&self) -> DocView {
3279 let mut g = self.lock();
3280 g.doc.redo();
3281 g.frame()
3282 }
3283
3284 /// Open an undo group: every edit until the matching
3285 /// [`end_undo_group`](Self::end_undo_group) undoes and redoes as one step —
3286 /// a Replace All, or a Writing Tools session. Groups nest; an undo or redo
3287 /// closes any that is open. See `Doc::begin_undo_group`.
3288 pub fn begin_undo_group(&self) {
3289 self.lock().doc.begin_undo_group();
3290 }
3291
3292 /// Close the group [`begin_undo_group`](Self::begin_undo_group) opened; a
3293 /// no-op when none is open.
3294 pub fn end_undo_group(&self) {
3295 self.lock().doc.end_undo_group();
3296 }
3297
3298 /// Switch between the rendered WYSIWYG surface and the raw source.
3299 pub fn toggle_view(&self) -> DocView {
3300 let mut g = self.lock();
3301 g.doc.toggle_view();
3302 g.frame()
3303 }
3304
3305 /// The current markup-exposure preference (see [`MarkupMode`]).
3306 pub fn markup_mode(&self) -> MarkupMode {
3307 MarkupMode::from_core(self.lock().doc.markup_mode())
3308 }
3309
3310 /// Set the markup-exposure preference. Returns a fresh view so a frontend
3311 /// can repaint — and under `Full` it must, because the returned view is the
3312 /// first one showing the caret's line raw. Diaryx leaves it at the `None`
3313 /// default.
3314 pub fn set_markup_mode(&self, mode: MarkupMode) -> DocView {
3315 let mut g = self.lock();
3316 g.doc.set_markup_mode(mode.into_core());
3317 g.frame()
3318 }
3319
3320 /// The current soft-break flow preference (see [`LineFlow`]).
3321 pub fn line_flow(&self) -> LineFlow {
3322 LineFlow::from_core(self.lock().doc.line_flow())
3323 }
3324
3325 /// Set the soft-break flow preference. Returns a fresh view so a frontend
3326 /// can repaint: like the markup-exposure preference this one changes rendering
3327 /// immediately, laying preserved soft breaks out as their own rows.
3328 pub fn set_line_flow(&self, mode: LineFlow) -> DocView {
3329 let mut g = self.lock();
3330 g.doc.set_line_flow(mode.into_core());
3331 g.frame()
3332 }
3333}
3334
3335// ── UITextInput support ──────────────────────────────────────────────────────
3336// A `UITextPosition` on the Swift side wraps a source byte offset; these are the
3337// offset↔geometry, stepping, and range-editing primitives the protocol needs.
3338// Queries never move the caret — they only read the (synced) visual map — so the
3339// system can probe positions freely while the model's selection stays put.
3340#[uniffi::export]
3341impl LeafDoc {
3342 /// The caret's source offset (the selection's moving end).
3343 pub fn caret_offset(&self) -> u32 {
3344 self.lock().doc.caret as u32
3345 }
3346
3347 /// The selection's fixed end (equals the caret when there's no selection).
3348 pub fn anchor_offset(&self) -> u32 {
3349 let g = self.lock();
3350 g.doc.anchor.unwrap_or(g.doc.caret) as u32
3351 }
3352
3353 /// The last caret stop in the document — `UITextInput.endOfDocument`.
3354 pub fn doc_end_offset(&self) -> u32 {
3355 let mut g = self.lock();
3356 g.sync();
3357 let end = g.doc.source.len();
3358 g.snap_stop(end) as u32
3359 }
3360
3361 /// Snap an arbitrary offset to the nearest valid caret stop.
3362 pub fn snap_offset(&self, off: u32) -> u32 {
3363 let mut g = self.lock();
3364 g.sync();
3365 g.snap_stop(off as usize) as u32
3366 }
3367
3368 /// Where a source offset sits on screen: its visual `(row, ch)`.
3369 pub fn pos_for_offset(&self, off: u32) -> RowCol {
3370 let mut g = self.lock();
3371 g.sync();
3372 let (row, col) = g.pos_of_offset(off as usize);
3373 let ch = col_to_utf16(&g.row_text(row), col);
3374 RowCol {
3375 row: row as u32,
3376 ch: ch as u32,
3377 }
3378 }
3379
3380 /// The rows a source range covers, inclusive — for drawing a block away
3381 /// from where it sits (a footnote peek, a link peek, a landing flash).
3382 ///
3383 /// Ask this rather than mapping `start` and `end - 1` through
3384 /// [`Self::pos_for_offset`]. That pair reads correctly and is wrong: a
3385 /// block's last byte is often *hidden* — a note or a paragraph ending in a
3386 /// link ends inside the link's destination — and `pos_for_offset` snaps a
3387 /// hidden offset forward to the next visible glyph, which for a trailing
3388 /// one is on the next block's row. A peek slicing that span drew the block
3389 /// after it too. `pos_for_offset`'s snap is right for a caret and wrong for
3390 /// a span; this is the question spans should be asking.
3391 pub fn row_range_for(&self, start: u32, end: u32) -> RowRange {
3392 let mut g = self.lock();
3393 g.sync();
3394 let (first, last) = g.row_range_for(start as usize, end as usize);
3395 RowRange {
3396 first: first as u32,
3397 last: last as u32,
3398 }
3399 }
3400
3401 /// The source offset at visual `(row, ch)` — the inverse of
3402 /// [`Self::pos_for_offset`], for hit-testing a point to a position.
3403 pub fn offset_for_pos(&self, row: u32, ch: u32) -> u32 {
3404 let mut g = self.lock();
3405 g.sync();
3406 let col = utf16_to_col(&g.row_text(row as usize), ch as usize);
3407 g.offset_of_col(row as usize, col) as u32
3408 }
3409
3410 /// Move `off` by `delta` caret stops (negative = left) — `position(from:offset:)`.
3411 pub fn step_offset(&self, off: u32, delta: i32) -> u32 {
3412 let mut g = self.lock();
3413 g.sync();
3414 let mut o = g.snap_glyph_stop(off as usize);
3415 if delta >= 0 {
3416 for _ in 0..delta {
3417 match g.stop_after(o) {
3418 Some(n) => o = n,
3419 None => break,
3420 }
3421 }
3422 } else {
3423 for _ in 0..(-delta) {
3424 match g.stop_before(o) {
3425 Some(p) => o = p,
3426 None => break,
3427 }
3428 }
3429 }
3430 o as u32
3431 }
3432
3433 /// The count of caret stops between two offsets (signed) — `offset(from:to:)`.
3434 pub fn distance_offset(&self, from: u32, to: u32) -> i32 {
3435 let mut g = self.lock();
3436 g.sync();
3437 let (from, to) = (from as usize, to as usize);
3438 let (mut a, b, sign) = if from <= to {
3439 (from, to, 1i32)
3440 } else {
3441 (to, from, -1i32)
3442 };
3443 a = g.snap_glyph_stop(a);
3444 let mut n = 0i32;
3445 while a < b {
3446 match g.stop_after(a) {
3447 Some(x) => {
3448 a = x;
3449 n += 1;
3450 }
3451 None => break,
3452 }
3453 }
3454 n * sign
3455 }
3456
3457 /// The UTF-16 index at which source offset `off` sits in the visible text —
3458 /// the string `text_in_range(0, doc_end_offset())` returns — which is the
3459 /// character space AppKit's `NSTextInputClient` and `NSAccessibility` speak.
3460 ///
3461 /// leaf's own handle is the source byte offset, and the two are not one
3462 /// scale apart: WYSIWYG hides delimiters, a block gap is spelled as one
3463 /// `\n`, and a character outside the BMP is two UTF-16 units. A frontend
3464 /// hands the system an `NSRange` converted with this and turns the ranges
3465 /// it gets back through `offset_for_utf16_index`, so Look Up, dictation, and
3466 /// VoiceOver all index the same text the frontend drew.
3467 pub fn utf16_index_for_offset(&self, off: u32) -> u32 {
3468 let mut g = self.lock();
3469 g.sync();
3470 g.utf16_index_of(off)
3471 }
3472
3473 /// `utf16_index_for_offset` over many offsets in one crossing — the index
3474 /// of each, in the order given. For a caller that converts every run of
3475 /// the frame at once, as a spell checker masking the visible text does:
3476 /// thousands of runs, and a call across the binding for each was the cost.
3477 pub fn utf16_indices_for_offsets(&self, offs: Vec<u32>) -> Vec<u32> {
3478 let mut g = self.lock();
3479 g.sync();
3480 offs.into_iter().map(|off| g.utf16_index_of(off)).collect()
3481 }
3482
3483 /// The inverse of `utf16_index_for_offset`: the source offset of the
3484 /// visible character at UTF-16 `index`, or the document's end stop at or
3485 /// past the end of the text. An index inside a surrogate pair resolves to
3486 /// the character that owns it, and one on the `\n` a block gap is spelled
3487 /// with to the stop at the end of the block before it. Always a caret stop.
3488 pub fn offset_for_utf16_index(&self, index: u32) -> u32 {
3489 let mut g = self.lock();
3490 g.sync();
3491 let len = g.doc.source.len();
3492 let end = g.snap_stop(len);
3493 let index = index as usize;
3494 match g.doc.view {
3495 // A block separator resolves to the gap offset, which is no stop;
3496 // snapping lands it on the row end before it, the stop a caret
3497 // standing "after the last character" already means.
3498 View::Wysiwyg => g
3499 .doc
3500 .vmap
3501 .offset_at_visible_utf16(end, index)
3502 .map_or(end, |o| g.snap_stop(o)) as u32,
3503 View::Source => {
3504 let mut seen = 0usize;
3505 for (i, ch) in g.doc.source.char_indices() {
3506 let n = ch.len_utf16();
3507 if index < seen + n {
3508 return i as u32;
3509 }
3510 seen += n;
3511 }
3512 end as u32
3513 }
3514 }
3515 }
3516
3517 /// The offset one navigable row up/down from `off`, keeping its column —
3518 /// `position(from:in: .up/.down)`. `None` at the top/bottom edge.
3519 pub fn vertical_offset(&self, off: u32, down: bool) -> Option<u32> {
3520 let mut g = self.lock();
3521 g.sync();
3522 let (row, col) = g.pos_of_offset(off as usize);
3523 let target = if down {
3524 g.nav_below(row)
3525 } else {
3526 g.nav_above(row)
3527 };
3528 target.map(|r| g.offset_of_col(r, col) as u32)
3529 }
3530
3531 /// The visible text between two offsets — `text(in:)`. In the WYSIWYG
3532 /// view this is *not* the raw source slice: a hidden inline-mark
3533 /// delimiter (`**`, `` ` ``, `_`) contributes nothing, and a stop that
3534 /// draws no glyph — a row's end, a table cell's end — is spelled `'\n'`.
3535 /// Exactly one character per caret stop, so that for any two stops
3536 /// `text_in_range(a, b).chars().count() == distance_offset(a, b)`. That
3537 /// equality is what `UITextInput`'s word tokenizer relies on: it reads a
3538 /// window of this text, indexes into it by `offset(from:to:)`, and hands
3539 /// a character delta back through `position(from:offset:)` — see
3540 /// [`leaf_core::wysiwyg::VisualMap::visible_text`] for the rule and what
3541 /// a one-character drift did to a double-tapped word. The source view has
3542 /// nothing hidden to begin with, so there this is exactly the raw slice.
3543 pub fn text_in_range(&self, from: u32, to: u32) -> String {
3544 let mut g = self.lock();
3545 g.sync();
3546 let len = g.doc.source.len();
3547 let (mut a, mut b) = ((from as usize).min(len), (to as usize).min(len));
3548 if a > b {
3549 std::mem::swap(&mut a, &mut b);
3550 }
3551 match g.doc.view {
3552 View::Wysiwyg => g.doc.vmap.visible_text(a, b),
3553 View::Source => {
3554 let s = &g.doc.source;
3555 while a > 0 && !s.is_char_boundary(a) {
3556 a -= 1;
3557 }
3558 while b < s.len() && !s.is_char_boundary(b) {
3559 b += 1;
3560 }
3561 s[a..b].to_string()
3562 }
3563 }
3564 }
3565
3566 /// Set the selection to `[anchor, focus]` by source offsets — the setter behind
3567 /// `UITextInput.selectedTextRange` and handle dragging.
3568 pub fn set_selection_offsets(&self, anchor: u32, focus: u32) -> DocView {
3569 let mut g = self.lock();
3570 g.doc.place_caret(anchor as usize, false);
3571 if focus != anchor {
3572 g.doc.place_caret(focus as usize, true);
3573 }
3574 g.frame()
3575 }
3576
3577 /// Select the exact source range `[start, end)`, snapping neither end to a
3578 /// visible caret stop — for a host painting a range it already knows the
3579 /// bytes of (a search hit, an annotation) rather than hit-testing a touch.
3580 ///
3581 /// `set_selection_offsets` above is the *other* verb: it goes through
3582 /// `place_caret`, which snaps, and is what a drag handle wants. This one
3583 /// takes the range as given, so a selection over `**needle**`'s inner word
3584 /// is the word and not one byte short of it.
3585 pub fn select_range(&self, start: u32, end: u32) -> DocView {
3586 let mut g = self.lock();
3587 g.doc.select_range(start as usize, end as usize);
3588 g.frame()
3589 }
3590
3591 /// Replace the source range `[from, to)` with `text` behind the caret — an
3592 /// automatic substitution as the user types (a correction, a text
3593 /// replacement, a smart quote or dash). The caret and selection stay where
3594 /// they were, and the substitution is an undo step of its own. See
3595 /// `Doc::substitute`.
3596 pub fn substitute(&self, from: u32, to: u32, text: String) -> DocView {
3597 let mut g = self.lock();
3598 g.doc.substitute(from as usize, to as usize, &text);
3599 g.frame()
3600 }
3601
3602 /// Bring the source to `text`, replacing only the span that differs and
3603 /// leaving the caret and selection on the characters they were on — how a
3604 /// host lays a change that arrived from elsewhere under an open editor.
3605 /// `text` is source, written exactly; one undo step. See
3606 /// `Doc::replace_source`.
3607 pub fn replace_source(&self, text: String) -> DocView {
3608 let mut g = self.lock();
3609 g.doc.replace_source(&text);
3610 g.frame()
3611 }
3612
3613 /// Replace the source range `[from, to]` with `text` — `replace(_:withText:)`.
3614 pub fn replace_range(&self, from: u32, to: u32, text: String) -> DocView {
3615 let mut g = self.lock();
3616 g.doc.place_caret(from as usize, false);
3617 if to != from {
3618 g.doc.place_caret(to as usize, true);
3619 }
3620 g.doc.insert(&text);
3621 g.frame()
3622 }
3623}
3624
3625impl LeafDoc {
3626 /// Acquire the guard, recovering from a poisoned lock: a panic in `leaf-core`
3627 /// under one call shouldn't wedge the whole document handle for the app.
3628 fn lock(&self) -> std::sync::MutexGuard<'_, Inner> {
3629 self.inner.lock().unwrap_or_else(|p| p.into_inner())
3630 }
3631}
3632
3633/// The UTF-16 offset into `text` of display column `col`. Walks grapheme clusters
3634/// exactly as core measures columns ([`text_width`] per cluster), so a wide
3635/// cluster advances the column by its cells while the offset advances by its
3636/// UTF-16 length; the two coincide only on plain ASCII.
3637fn col_to_utf16(text: &str, col: usize) -> usize {
3638 let mut c = 0usize;
3639 let mut u = 0usize;
3640 for g in text.graphemes(true) {
3641 if c >= col {
3642 break;
3643 }
3644 c += text_width(g);
3645 u += g.chars().map(char::len_utf16).sum::<usize>();
3646 }
3647 u
3648}
3649
3650/// The display column of the grapheme boundary at or before UTF-16 offset `off`
3651/// — the inverse of [`col_to_utf16`], turning a native click position back into
3652/// core's column. Core then clamps the column to a real caret stop.
3653fn utf16_to_col(text: &str, off: usize) -> usize {
3654 let mut c = 0usize;
3655 let mut u = 0usize;
3656 for g in text.graphemes(true) {
3657 if u >= off {
3658 break;
3659 }
3660 u += g.chars().map(char::len_utf16).sum::<usize>();
3661 c += text_width(g);
3662 }
3663 c
3664}
3665
3666/// The renderer class id for a semantic role. Heading level is folded into the
3667/// id (`h1`…`h6`) so a single style rule per level applies.
3668fn role_name(r: Role) -> String {
3669 match r {
3670 Role::Body => "body".into(),
3671 Role::Heading(level) => format!("h{}", level.clamp(1, 6)),
3672 Role::Code => "code".into(),
3673 Role::Link => "link".into(),
3674 // The colour rides `Run::mark_color`, not the class id: a renderer that
3675 // styles `mark` and nothing else still draws a coloured highlight.
3676 Role::Mark(_) => "mark".into(),
3677 Role::ListMarker => "list".into(),
3678 Role::ListIndent => "list-indent".into(),
3679 Role::QuoteGutter => "quote".into(),
3680 Role::Rule => "rule".into(),
3681 Role::Image => "image".into(),
3682 // A formula's stand-in: the one-character atom run an inline formula
3683 // renders to, or a display block's placeholder label. A renderer pairs
3684 // a `math` run with its [`MathView`] by `src` and draws the picture in
3685 // its place — see [`DocView::math`].
3686 Role::Math => "math".into(),
3687 Role::Delimiter => "delimiter".into(),
3688 }
3689}
3690
3691/// The toolbar id for an inline mark — kept in sync with the Swift button ids.
3692fn mark_id(kind: InlineKind) -> &'static str {
3693 match kind {
3694 InlineKind::Strong => "bold",
3695 InlineKind::Emph => "italic",
3696 InlineKind::Verbatim => "code",
3697 InlineKind::Mark => "mark",
3698 InlineKind::Insert => "underline",
3699 InlineKind::Delete => "strike",
3700 InlineKind::Superscript => "superscript",
3701 InlineKind::Subscript => "subscript",
3702 }
3703}
3704
3705/// The WYSIWYG rows: each visual row's glyphs coalesced into maximal runs of
3706/// identical `(style, selected)`. A glyph is selected when its source byte lies
3707/// in `[ss, se)`.
3708fn wysiwyg_rows(vmap: &VisualMap, ss: usize, se: usize, hls: &[leaf_core::Highlight]) -> Vec<Row> {
3709 // The map's own face table, for the family name a glyph carries only an id
3710 // for — threaded down beside `hls`, which travels the same road.
3711 let faces = vmap.faces();
3712 vmap.rows
3713 .iter()
3714 .map(|vrow| {
3715 Row {
3716 runs: runs_of(&vrow.glyphs, ss, se, hls, faces),
3717 decoration: vrow.decoration,
3718 code: vrow.code,
3719 code_lang: vrow.code_lang.clone(),
3720 directive: vrow.directive,
3721 directive_label: vrow.directive_label.clone(),
3722 // Straight off the row, not scanned out of its glyphs: an empty
3723 // heading has none to scan, and a renderer sizing the line by a
3724 // glyph's role drew `# ` at body height until it had text.
3725 heading: vrow.heading,
3726 // Off the row for the same reason, and more sharply: alignment
3727 // and spacing are properties of the *line*, so an empty
3728 // paragraph just centred has no run to carry them.
3729 align: vrow.align.map(|a| a.name().to_string()),
3730 line_height: vrow.line_height.map(|l| l.name().to_string()),
3731 boundary: vrow.boundary.map(|b| Boundary {
3732 above: b.above.into(),
3733 below: b.below.into(),
3734 }),
3735 }
3736 })
3737 .collect()
3738}
3739
3740/// Coalesce `glyphs` into maximal runs of identical `(style, selected)` — the
3741/// shared body of a row's runs and a table cell's runs. A glyph is selected when
3742/// its source byte lies in `[ss, se)`.
3743/// Split a cell's flat glyphs into its visual lines at the in-cell break glyphs
3744/// (`\n`, from a `<br>`), each with the source range it spans. A line runs from
3745/// its first glyph's offset to the break that ends it (`cell_end` for the last);
3746/// an empty line — a leading/trailing break, or an empty cell — collapses to a
3747/// single caret home. The break glyphs themselves are dropped (they hold no
3748/// caret), exactly as the monospace picture drops them.
3749fn cell_lines(
3750 glyphs: &[leaf_core::Glyph],
3751 cell_start: usize,
3752 cell_end: usize,
3753 ss: usize,
3754 se: usize,
3755 hls: &[leaf_core::Highlight],
3756 faces: &CoreFaceTable,
3757) -> Vec<TableCellLineView> {
3758 let mut lines = Vec::new();
3759 let mut seg: Vec<leaf_core::Glyph> = Vec::new();
3760 // The current line's start offset: the cell's for the first line, then the
3761 // first real glyph after each break (`None` until that glyph is seen).
3762 let mut line_start: Option<usize> = Some(cell_start);
3763 for g in glyphs {
3764 if g.ch == '\n' {
3765 let start = line_start.unwrap_or(g.src);
3766 lines.push(TableCellLineView {
3767 runs: runs_of(&seg, ss, se, hls, faces),
3768 start: start as u32,
3769 end: g.src as u32,
3770 });
3771 seg.clear();
3772 line_start = None;
3773 } else {
3774 if line_start.is_none() {
3775 line_start = Some(g.src);
3776 }
3777 seg.push(g.clone());
3778 }
3779 }
3780 lines.push(TableCellLineView {
3781 runs: runs_of(&seg, ss, se, hls, faces),
3782 start: line_start.unwrap_or(cell_end) as u32,
3783 end: cell_end as u32,
3784 });
3785 lines
3786}
3787
3788fn runs_of(
3789 glyphs: &[leaf_core::Glyph],
3790 ss: usize,
3791 se: usize,
3792 hls: &[leaf_core::Highlight],
3793 faces: &CoreFaceTable,
3794) -> Vec<Run> {
3795 // Which highlight (by index) covers a glyph — first by start when several
3796 // overlap, matching `Doc::highlight_at`. Part of the run key: a highlight
3797 // splits a run exactly the way the selection does, so its wash begins and
3798 // ends on its own bytes.
3799 let hl_of = |src: usize| hls.iter().position(|h| h.start <= src && src < h.end);
3800 let mut runs: Vec<Run> = Vec::new();
3801 let mut buf = String::new();
3802 // The style/selection/highlight key the run is accumulating, and the source
3803 // offset its first glyph came from — carried alongside rather than
3804 // re-derived, since a run's glyphs are contiguous but its *text* has no
3805 // offsets in it.
3806 let mut cur: Option<(LStyle, bool, Option<usize>, usize)> = None;
3807 for g in glyphs {
3808 let key = (g.style, g.src >= ss && g.src < se, hl_of(g.src));
3809 match cur {
3810 Some((style, sel, hl, _)) if (style, sel, hl) == key => buf.push(g.ch),
3811 _ => {
3812 if let Some((style, was_sel, hl, src)) = cur.take() {
3813 runs.push(make_run(
3814 std::mem::take(&mut buf),
3815 style,
3816 was_sel,
3817 hl.map(|i| &hls[i]),
3818 src,
3819 faces,
3820 ));
3821 }
3822 cur = Some((key.0, key.1, key.2, g.src));
3823 buf.push(g.ch);
3824 }
3825 }
3826 }
3827 if let Some((style, was_sel, hl, src)) = cur {
3828 runs.push(make_run(
3829 buf,
3830 style,
3831 was_sel,
3832 hl.map(|i| &hls[i]),
3833 src,
3834 faces,
3835 ));
3836 }
3837 runs
3838}
3839
3840/// The leaf directives of a WYSIWYG frame — each with the `rows` span its
3841/// placeholder occupies (to be painted over) and the name/attributes a frontend
3842/// resolves it by. The peer of [`wysiwyg_tables`] for a block that renders as a
3843/// thing rather than as text.
3844fn wysiwyg_directives(vmap: &VisualMap) -> Vec<DirectiveView> {
3845 vmap.directives
3846 .iter()
3847 .map(|d| DirectiveView {
3848 start_row: d.rows_span.start as u32,
3849 end_row: d.rows_span.end as u32,
3850 name: d.name.clone(),
3851 label: d.label.clone(),
3852 attrs: d
3853 .attrs
3854 .iter()
3855 .map(|(k, v)| DirectiveAttr {
3856 key: k.clone(),
3857 value: v.clone().unwrap_or_default(),
3858 })
3859 .collect(),
3860 })
3861 .collect()
3862}
3863
3864/// The block media of a WYSIWYG frame — each with the `rows` span its
3865/// placeholder occupies (to be laid over) and what to build there. The peer of
3866/// [`wysiwyg_directives`], with each URL already resolved under `scheme`.
3867///
3868/// Resolving here rather than in Swift keeps the one piece of `<picture>` logic
3869/// core owns (`prefers-color-scheme` matching) in core. The `<source>` list
3870/// still crosses untouched, so a renderer can additionally pick by MIME — which
3871/// codecs AVFoundation has is not something core can know.
3872fn wysiwyg_media(vmap: &VisualMap, scheme: ColorScheme) -> Vec<MediaView> {
3873 vmap.media
3874 .iter()
3875 .map(|m| MediaView {
3876 start_row: m.rows_span.start as u32,
3877 end_row: m.rows_span.end as u32,
3878 kind: match m.kind {
3879 CoreMediaKind::Image => MediaKind::Image,
3880 CoreMediaKind::Video => MediaKind::Video,
3881 CoreMediaKind::Audio => MediaKind::Audio,
3882 },
3883 src: m.resolve(scheme).to_string(),
3884 poster: m.poster.clone(),
3885 alt: m.alt.clone(),
3886 sources: m
3887 .sources
3888 .iter()
3889 .map(|s| MediaSourceView {
3890 media: s.media.clone(),
3891 src: s.srcset.clone(),
3892 mime: s.mime.clone(),
3893 })
3894 .collect(),
3895 })
3896 .collect()
3897}
3898
3899/// The formulas of a WYSIWYG frame — each atom and each display block, with
3900/// the `rows` span it occupies and the TeX to typeset. The peer of
3901/// [`wysiwyg_media`].
3902fn wysiwyg_math(vmap: &VisualMap) -> Vec<MathView> {
3903 vmap.math
3904 .iter()
3905 .map(|m| MathView {
3906 start_row: m.rows_span.start as u32,
3907 end_row: m.rows_span.end as u32,
3908 inline: m.glyph.is_some(),
3909 tex: m.tex.clone(),
3910 display: m.display,
3911 src: m.src as u32,
3912 })
3913 .collect()
3914}
3915
3916/// The structural tables of a WYSIWYG frame — each with the `rows` span its
3917/// box-glyph picture occupies (to be skipped) and its grid of styled cells.
3918fn wysiwyg_tables(
3919 vmap: &VisualMap,
3920 ss: usize,
3921 se: usize,
3922 hls: &[leaf_core::Highlight],
3923) -> Vec<TableView> {
3924 let faces = vmap.faces();
3925 vmap.tables
3926 .iter()
3927 .map(|t| TableView {
3928 start_row: t.rows_span.start as u32,
3929 end_row: t.rows_span.end as u32,
3930 grid: t
3931 .grid
3932 .iter()
3933 .map(|row| TableRowView {
3934 head: row.head,
3935 cells: row
3936 .cells
3937 .iter()
3938 .map(|cell| TableCellView {
3939 lines: cell_lines(
3940 &cell.glyphs,
3941 cell.start,
3942 cell.end,
3943 ss,
3944 se,
3945 hls,
3946 faces,
3947 ),
3948 align: align_name(cell.align),
3949 start: cell.start as u32,
3950 end: cell.end as u32,
3951 })
3952 .collect(),
3953 })
3954 .collect(),
3955 })
3956 .collect()
3957}
3958
3959/// The wire name for a cell's column alignment.
3960fn align_name(a: Alignment) -> String {
3961 match a {
3962 Alignment::Left => "left",
3963 Alignment::Right => "right",
3964 Alignment::Center => "center",
3965 Alignment::Default => "default",
3966 }
3967 .to_string()
3968}
3969
3970/// The source rows: the raw document split on `'\n'`, each line cut into runs
3971/// wherever its styling changes — the markup `smap` colours, the `[ss, se)`
3972/// selection, and the host's highlights — the native counterpart of the TUI's
3973/// `build_lines`. Backs the source view, whose caret rides raw byte offsets.
3974///
3975/// An empty `smap` — a frontend that never built one, a document with no
3976/// markup — paints every line as plain text, which is what this did before
3977/// the map reached it.
3978fn source_rows(
3979 source: &str,
3980 smap: &SourceMap,
3981 ss: usize,
3982 se: usize,
3983 hls: &[leaf_core::Highlight],
3984) -> Vec<Row> {
3985 // Raw text carries no attributed span, so no run of it names a family and
3986 // the table it would be read out of is empty.
3987 let faces = CoreFaceTable::default();
3988 let mut rows = Vec::new();
3989 let mut byte = 0usize;
3990 // Reused across lines rather than allocated per line: a document is a few
3991 // thousand of them and this is rebuilt on every whole frame.
3992 let mut cuts: Vec<usize> = Vec::new();
3993 // Which highlight covers a byte — first by start when several overlap,
3994 // matching `Doc::highlight_at` and `runs_of` above.
3995 let hl_of = |src: usize| hls.iter().position(|h| h.start <= src && src < h.end);
3996
3997 for raw in source.split('\n') {
3998 let start = byte;
3999 let end = start + raw.len();
4000
4001 // Where the styling can change within this line, in document offsets:
4002 // its two ends, every selection and highlight edge inside it, and every
4003 // edge of the syntax map's runs. No style edge falls strictly inside a
4004 // run by construction, so one probe at each run's first byte answers
4005 // for all of it.
4006 cuts.clear();
4007 cuts.push(start);
4008 cuts.push(end);
4009 for at in [ss, se]
4010 .into_iter()
4011 .chain(hls.iter().flat_map(|h| [h.start, h.end]))
4012 {
4013 if at > start && at < end {
4014 cuts.push(at);
4015 }
4016 }
4017 smap.edges_in(start..end, &mut cuts);
4018 cuts.sort_unstable();
4019 cuts.dedup();
4020
4021 let mut runs = Vec::new();
4022 for pair in cuts.windows(2) {
4023 let (a, b) = (pair[0], pair[1]);
4024 runs.push(make_run(
4025 raw[a - start..b - start].to_string(),
4026 smap.style_at(a),
4027 a >= ss && a < se,
4028 hl_of(a).map(|i| &hls[i]),
4029 a,
4030 &faces,
4031 ));
4032 }
4033
4034 rows.push(Row {
4035 runs,
4036 decoration: false,
4037 code: false,
4038 code_lang: None,
4039 directive: false,
4040 directive_label: None,
4041 heading: None, // source view is raw text — no resolved heading rows
4042 align: None, // …no attributes resolved onto a block…
4043 line_height: None,
4044 boundary: None, // …and no resolved block structure to divide
4045 });
4046 byte = end + 1; // skip the '\n' that `split` consumed
4047 }
4048 rows
4049}
4050
4051/// Build a [`Run`] from an accumulated string and the core style it was drawn
4052/// with — the one place role and emphasis flags cross into the view shape.
4053fn make_run(
4054 text: String,
4055 style: LStyle,
4056 sel: bool,
4057 hl: Option<&leaf_core::Highlight>,
4058 src: usize,
4059 faces: &CoreFaceTable,
4060) -> Run {
4061 Run {
4062 text,
4063 role: role_name(style.role),
4064 bold: style.bold,
4065 italic: style.italic,
4066 underline: style.underline,
4067 strike: style.strikethrough,
4068 sup: style.baseline == Baseline::Super,
4069 sub: style.baseline == Baseline::Sub,
4070 src: src as u32,
4071 sel,
4072 hl: hl.map(|h| h.id.clone()),
4073 hl_color: hl.and_then(|h| h.color.clone()),
4074 mark_color: mark_color_name(style.role),
4075 token: style.token.map(|t| t.name().to_string()),
4076 size: style.size.map(|s| s.name().to_string()),
4077 font: style.font.and_then(|f| faces.spell(f).map(Cow::into_owned)),
4078 text_color: style.color.map(|c| c.name().to_string()),
4079 }
4080}
4081
4082/// The name of a `mark` role's colour, for [`Run::mark_color`]. `None` for a
4083/// plain highlight and for every other role — the same answer, because neither
4084/// has a colour to name.
4085fn mark_color_name(role: Role) -> Option<String> {
4086 match role {
4087 Role::Mark(c) => c.map(|c| c.name().to_string()),
4088 _ => None,
4089 }
4090}
4091
4092#[cfg(test)]
4093mod tests {
4094 use super::*;
4095
4096 fn doc(src: &str) -> Arc<LeafDoc> {
4097 LeafDoc::new(src.to_string(), "markdown".to_string()).unwrap()
4098 }
4099
4100 #[test]
4101 fn heading_at_names_the_heading_above() {
4102 let d = doc("intro\n\n## The *Second* Part\n\nbody\n");
4103 assert!(d.heading_at(0).is_none());
4104 let h = d.heading_at(29).expect("`body` is under the heading");
4105 assert_eq!(
4106 (h.text.as_str(), h.level, h.start),
4107 ("The Second Part", 2, 7)
4108 );
4109 }
4110
4111 /// The source view's rows carry the markup's styling: a heading's `# ` is
4112 /// a delimiter run, its text a heading run, and the rows split exactly at
4113 /// the map's edges — the same runs the TUI paints from the same map.
4114 #[test]
4115 fn source_rows_carry_the_source_maps_styling() {
4116 let d = doc("# Title\n\nsee [here](https://x.dev) now\n");
4117 let v = d.toggle_view();
4118 assert_eq!(v.view, "source");
4119 fn roles(row: &Row) -> Vec<(&str, &str)> {
4120 row.runs
4121 .iter()
4122 .map(|r| (r.text.as_str(), r.role.as_str()))
4123 .collect()
4124 }
4125 assert_eq!(
4126 roles(&v.rows[0]),
4127 vec![("# ", "delimiter"), ("Title", "h1")]
4128 );
4129 assert_eq!(
4130 roles(&v.rows[2]),
4131 vec", "delimiter"),
4136 (" now", "body"),
4137 ]
4138 );
4139 // Every run knows where it came from, as the rendered rows' do.
4140 let src = d.source();
4141 assert_eq!(v.rows[2].runs[2].src as usize, src.find("here").unwrap());
4142 // The text is whole: the runs concatenate back to the line.
4143 let joined: String = v.rows[2].runs.iter().map(|r| r.text.as_str()).collect();
4144 assert_eq!(joined, "see [here](https://x.dev) now");
4145 }
4146
4147 /// The selection and a host highlight split a source row on their own
4148 /// edges, on top of the map's, and neither loses the styling under it.
4149 #[test]
4150 fn source_rows_split_on_selection_and_highlight_edges_too() {
4151 let d = doc("a **bold** b\n");
4152 d.toggle_view();
4153 let src = d.source();
4154 let b = src.find("bold").unwrap() as u32;
4155 // Select "ol" — inside the bold run.
4156 d.set_selection_offsets(b + 1, b + 3);
4157 let v = d.set_highlights(vec![Highlight {
4158 start: 0,
4159 end: 3, // "a *"
4160 id: "h".into(),
4161 color: None,
4162 marker: None,
4163 }]);
4164 let runs: Vec<(String, String, bool, bool, Option<String>)> = v.rows[0]
4165 .runs
4166 .iter()
4167 .map(|r| (r.text.clone(), r.role.clone(), r.bold, r.sel, r.hl.clone()))
4168 .collect();
4169 let expect = |t: &str, role: &str, bold: bool, sel: bool, hl: Option<&str>| {
4170 (
4171 t.to_string(),
4172 role.to_string(),
4173 bold,
4174 sel,
4175 hl.map(str::to_string),
4176 )
4177 };
4178 assert_eq!(
4179 runs,
4180 vec![
4181 expect("a ", "body", false, false, Some("h")),
4182 expect("*", "delimiter", true, false, Some("h")),
4183 expect("*", "delimiter", true, false, None),
4184 expect("b", "body", true, false, None),
4185 expect("ol", "body", true, true, None),
4186 expect("d", "body", true, false, None),
4187 expect("**", "delimiter", true, false, None),
4188 expect(" b", "body", false, false, None),
4189 ]
4190 );
4191 }
4192
4193 /// A fenced block's body in the source view carries the grammar's tokens,
4194 /// as the rendered view's does — the same `let` is a keyword in both.
4195 #[cfg(feature = "syntax")]
4196 #[test]
4197 fn source_rows_carry_fence_tokens() {
4198 let d = doc("```rust\nlet x = 1;\n```\n");
4199 let v = d.toggle_view();
4200 let kw = v.rows[1]
4201 .runs
4202 .iter()
4203 .find(|r| r.text == "let")
4204 .expect("a run for the keyword");
4205 assert_eq!(kw.role, "code");
4206 assert_eq!(kw.token.as_deref(), Some("keyword"));
4207 assert_eq!(v.rows[0].runs[0].role, "delimiter", "the fence is markup");
4208 }
4209
4210 /// A token splits a run the way a style does — it *is* part of the style
4211 /// — and rides across as its class id. A block in a language no grammar
4212 /// covers is one plain `code` run with no token, as it always was.
4213 #[cfg(feature = "syntax")]
4214 #[test]
4215 fn a_highlighted_block_splits_its_runs_by_token() {
4216 let v = doc("```rust\nlet x = 1;\n```\n").set_unwrapped();
4217 let row = v.rows.iter().find(|r| r.code).expect("a code row");
4218 let classed: Vec<(&str, Option<&str>)> = row
4219 .runs
4220 .iter()
4221 .map(|r| (r.text.as_str(), r.token.as_deref()))
4222 .collect();
4223 assert_eq!(classed[0], ("let", Some("keyword")));
4224 assert!(row.runs.iter().all(|r| r.role == "code"), "{classed:?}");
4225 assert!(
4226 classed
4227 .iter()
4228 .any(|(t, k)| *t == "1" && *k == Some("constant")),
4229 "{classed:?}"
4230 );
4231
4232 let v = doc("```text\nlet x = 1;\n```\n").set_unwrapped();
4233 let row = v.rows.iter().find(|r| r.code).unwrap();
4234 assert_eq!(row.runs.len(), 1);
4235 assert_eq!(row.runs[0].token, None);
4236 }
4237
4238 #[test]
4239 fn a_footnote_definition_ending_the_file_is_itself_not_a_copy() {
4240 // No trailing newline: twig closes the last block on the virtual newline
4241 // it supplies at EOF, so the block's `span.end` is one past the source.
4242 // The definition and the `section` whose bytes contain it then both
4243 // overran, both keyed the block cache as *empty*, and the definition was
4244 // served the section's rows — this rendered the heading a second time.
4245 let src = "A claim[^1] worth checking.\n\n# A heading with a reference[^1] in it\n\n[^1]: The first note.\n[^note]: A note with a word for a label.";
4246 let d = LeafDoc::new(src.to_string(), "djot".to_string()).unwrap();
4247 let text: Vec<String> = d
4248 .view()
4249 .rows
4250 .iter()
4251 .map(|r| r.runs.iter().map(|x| x.text.as_str()).collect())
4252 .collect();
4253 assert_eq!(
4254 text.last().map(String::as_str),
4255 Some("[note] A note with a word for a label."),
4256 "the last definition should render itself: {text:?}"
4257 );
4258 assert_eq!(
4259 text.iter()
4260 .filter(|t| t.contains("A heading with a reference"))
4261 .count(),
4262 1,
4263 "the heading should render exactly once: {text:?}"
4264 );
4265 }
4266
4267 #[test]
4268 fn an_empty_heading_crosses_the_boundary_carrying_its_level() {
4269 // What the toolbar's H1 leaves on a blank line: a heading with no text
4270 // yet. The renderer sizes a row by this field, so a `nil` here is a line
4271 // (and a caret) drawn at body height that jumps to heading height on the
4272 // first keystroke — the level can't be scanned out of the runs, because
4273 // an empty heading has none.
4274 let d = doc("body\n\n# \n");
4275 let v = d.view();
4276 let head = v.rows.last().expect("the heading's row");
4277 assert!(
4278 head.runs.iter().all(|r| r.text.is_empty()),
4279 "the `# ` marker is hidden"
4280 );
4281 assert_eq!(head.heading, Some(1));
4282 assert_eq!(
4283 v.rows[0].heading, None,
4284 "the paragraph above is not a heading"
4285 );
4286 }
4287
4288 #[test]
4289 fn typing_into_a_heading_made_on_a_blank_line_keeps_the_caret_on_its_row() {
4290 // The reported bug at the boundary the Swift renderer reads: with a blank
4291 // line under it, the caret came back on a row two below the heading it
4292 // was actually in, and the view drew it there.
4293 let d = doc("one\n\ntwo\n\n\n\n");
4294 let _ = d.click(4, 0, false); // the first of the two blank lines
4295 let _ = d.set_heading(1);
4296 let mut v = d.view();
4297 for c in "title".chars() {
4298 v = d.insert(c.to_string());
4299 }
4300 assert_eq!(d.source(), "one\n\ntwo\n\n# title\n\n");
4301 assert_eq!(
4302 (v.caret_row, v.caret_ch),
4303 (4, 5),
4304 "the caret is on the heading's row"
4305 );
4306 assert_eq!(v.rows[4].heading, Some(1));
4307 }
4308
4309 #[test]
4310 fn a_video_crosses_the_boundary_as_media_with_the_rows_to_lay_it_over() {
4311 // What the Swift renderer actually consumes: a row span to cover, a kind
4312 // to build a view from, and a URL to load. A frontend that skipped the
4313 // span would paint core's `🎬` placeholder underneath its own player.
4314 let d = doc("<video src=\"clip.mp4\" poster=\"still.png\" controls></video>\n");
4315 let v = d.view();
4316 assert_eq!(v.media.len(), 1);
4317 let m = &v.media[0];
4318 assert!(matches!(m.kind, MediaKind::Video));
4319 assert_eq!(m.src, "clip.mp4");
4320 assert_eq!(m.poster, "still.png");
4321 assert!(
4322 m.end_row > m.start_row,
4323 "the span must cover at least its label row"
4324 );
4325 }
4326
4327 #[test]
4328 fn a_pictures_dark_source_resolves_by_appearance() {
4329 // The one piece of `<picture>` logic core owns, exercised across the
4330 // boundary: the same document resolves to a different URL depending on
4331 // what the host said its appearance was.
4332 let d = doc(
4333 "<picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"d.svg\">\
4334 <img src=\"l.svg\" alt=\"banner\"></picture>\n",
4335 );
4336 assert_eq!(d.view().media[0].src, "l.svg", "light by default");
4337 assert_eq!(d.set_dark_appearance(true).media[0].src, "d.svg");
4338 assert_eq!(d.set_dark_appearance(false).media[0].src, "l.svg");
4339 }
4340
4341 #[test]
4342 fn tapping_below_a_trailing_picture_and_typing_keeps_it_a_picture() {
4343 // The whole gesture, across the boundary, in the order the Apple frontend
4344 // performs it: the layout clamps a point below the last row onto the
4345 // picture's row and asks for the position past its label glyphs; that
4346 // offset becomes the selection; then a character arrives. Before the two
4347 // halves of this fix, the offset was the stop *in front of* the picture
4348 // and the character dissolved it into a paragraph with an inline image —
4349 // the photo stopped being drawn, and nothing said so.
4350 let d = doc("hi\n\n\n");
4351 let v = d.set_unwrapped();
4352 let row = v.media[0].start_row;
4353 let label: u32 = v.rows[row as usize]
4354 .runs
4355 .iter()
4356 .map(|r| r.text.encode_utf16().count() as u32)
4357 .sum();
4358
4359 let off = d.offset_for_pos(row, label);
4360 assert_eq!(
4361 off,
4362 "hi\n\n".len() as u32,
4363 "the stop past the picture"
4364 );
4365
4366 d.set_selection_offsets(off, off);
4367 let after = d.insert("x".to_string());
4368 assert_eq!(d.source(), "hi\n\n\n\nx\n");
4369 assert_eq!(after.media.len(), 1, "still a picture, one paragraph up");
4370 }
4371
4372 #[test]
4373 fn backspace_from_that_same_tap_takes_the_picture_whole() {
4374 // The other half of the same gesture, and the one that cost this project's
4375 // own test vault a photo: tap under the picture, press Backspace. That
4376 // offset is the stop past the markup, so a byte-step deleted the closing
4377 // paren and left the literal text `\n");
4379 d.set_unwrapped();
4380 let off = "hi\n\n".len() as u32;
4381 d.set_selection_offsets(off, off);
4382 let after = d.backspace();
4383 assert_eq!(d.source(), "hi\n");
4384 assert_eq!(after.media.len(), 0, "gone as a picture, not as bytes");
4385 let undone = d.undo();
4386 assert_eq!(d.source(), "hi\n\n\n");
4387 assert_eq!(
4388 undone.media.len(),
4389 1,
4390 "and one undo brings the picture back"
4391 );
4392 }
4393
4394 #[test]
4395 fn measured_heights_grow_the_reserved_span() {
4396 // The height loop: core reserves one row until the renderer measures the
4397 // real view and reports back, because core does no I/O and cannot know.
4398 let d = doc("\n");
4399 let before = &d.view().media[0];
4400 assert_eq!(
4401 before.end_row - before.start_row,
4402 1,
4403 "one row until measured"
4404 );
4405
4406 let after = d.set_media_rows(vec![MediaHeight {
4407 destination: "cat.png".to_string(),
4408 rows: 6,
4409 }]);
4410 let m = &after.media[0];
4411 assert_eq!(
4412 m.end_row - m.start_row,
4413 6,
4414 "the span grew to what was measured"
4415 );
4416 }
4417
4418 #[test]
4419 fn inserted_media_comes_straight_back_out_as_media() {
4420 // Round trip across the boundary, the pair that matters: what Swift asks
4421 // to insert, Swift sees on the very next frame.
4422 let d = doc("\n");
4423 let v = d.insert_media(
4424 MediaKind::Audio,
4425 "take.mp3".to_string(),
4426 "a take".to_string(),
4427 );
4428 assert_eq!(v.media.len(), 1);
4429 assert!(matches!(v.media[0].kind, MediaKind::Audio));
4430 assert_eq!(v.media[0].src, "take.mp3");
4431 assert_eq!(v.media[0].alt, "a take");
4432 }
4433
4434 /// The block half of the presentation vocabulary, the whole way round:
4435 /// press, and the fact comes back on **every** row of the block as the name
4436 /// the document carries, with the query lighting the control that wrote it.
4437 ///
4438 /// The row rather than a run because an empty paragraph has no run — and a
4439 /// name rather than an index because the renderer's theme owns the ramp,
4440 /// which is the same division `mark_color` makes.
4441 #[test]
4442 fn the_block_vocabulary_crosses_on_the_row_and_comes_back_at_the_caret() {
4443 let d = doc("a centred paragraph\n");
4444 let v = d.set_alignment(Some(Align::Center));
4445 let aligned: Vec<&str> = v
4446 .rows
4447 .iter()
4448 .filter_map(|r| r.align.as_deref())
4449 .collect::<Vec<_>>();
4450 assert_eq!(aligned, ["center"], "the token, on the paragraph's row");
4451 assert_eq!(d.alignment_at_caret(), Some(Align::Center));
4452
4453 // A second property on the same block keeps the first: each gesture
4454 // edits one key and passes the rest back whole. (The caret is put back
4455 // in the paragraph first — in Markdown the attributes went onto a `div`
4456 // the press wrote *around* it, so the offset the caret kept is now that
4457 // opening line, which is no block of the document's.)
4458 let at = d.source().find("centred").unwrap() as u32;
4459 d.set_selection_offsets(at, at);
4460 let v = d.set_line_spacing(Some(LineHeight::Step(LineSpacing::OneHalf)));
4461 let row = v
4462 .rows
4463 .iter()
4464 .find(|r| r.align.is_some())
4465 .expect("the block");
4466 assert_eq!(row.align.as_deref(), Some("center"));
4467 assert_eq!(row.line_height.as_deref(), Some("1.5"));
4468 assert_eq!(
4469 d.line_spacing_at_caret(),
4470 Some(LineHeight::Step(LineSpacing::OneHalf))
4471 );
4472
4473 // `nil` clears, and absence is the theme's default rather than a token
4474 // meaning "left".
4475 let at = d.source().find("centred").unwrap() as u32;
4476 d.set_selection_offsets(at, at);
4477 let v = d.set_alignment(None);
4478 assert!(v.rows.iter().all(|r| r.align.is_none()));
4479 assert_eq!(d.alignment_at_caret(), None);
4480 assert_eq!(
4481 d.line_spacing_at_caret(),
4482 Some(LineHeight::Step(LineSpacing::OneHalf)),
4483 "clearing one key leaves the other standing"
4484 );
4485 }
4486
4487 /// The run half: size, face and colour ride the run beside `role`, so a
4488 /// renderer picks a font and a foreground without re-reading the document.
4489 /// With nothing selected the caret's whole block takes them, which is what
4490 /// makes "make this paragraph larger" one press.
4491 #[test]
4492 fn the_run_vocabulary_crosses_on_the_run_and_comes_back_at_the_caret() {
4493 let d = doc("big serif blue\n");
4494 // Each press with the caret back in the paragraph, as a frontend's is —
4495 // the first wrapped the block in a `div`, and the offset the caret kept
4496 // is that opening line rather than the text.
4497 let in_text = |d: &Arc<LeafDoc>| {
4498 let at = d.source().find("serif").unwrap() as u32;
4499 d.set_selection_offsets(at, at);
4500 };
4501 d.set_font_size(Some(FontSize::Step(SizeStep::Large)));
4502 in_text(&d);
4503 d.set_font_family(Some(FontFace::Generic(FontFamily::Serif)));
4504 in_text(&d);
4505 let v = d.set_text_color(Some(TextColor::Named(MarkColor::Blue)));
4506
4507 let styled: Vec<(Option<&str>, Option<&str>, Option<&str>)> = v
4508 .rows
4509 .iter()
4510 .flat_map(|r| r.runs.iter())
4511 .filter(|r| !r.text.trim().is_empty())
4512 .map(|r| {
4513 (
4514 r.size.as_deref(),
4515 r.font.as_deref(),
4516 r.text_color.as_deref(),
4517 )
4518 })
4519 .collect();
4520 assert_eq!(styled, [(Some("large"), Some("serif"), Some("blue"))]);
4521
4522 assert_eq!(
4523 d.font_size_at_caret(),
4524 Some(FontSize::Step(SizeStep::Large))
4525 );
4526 assert_eq!(
4527 d.font_family_at_caret(),
4528 Some(FontFace::Generic(FontFamily::Serif))
4529 );
4530 assert_eq!(
4531 d.text_color_at_caret(),
4532 Some(TextColor::Named(MarkColor::Blue))
4533 );
4534
4535 // A run's text colour is not a highlight's wash: nothing here is a
4536 // `mark`, so the palette that colours one reports nothing.
4537 assert!(!d.caret_in_mark());
4538 assert!(
4539 v.rows
4540 .iter()
4541 .flat_map(|r| r.runs.iter())
4542 .all(|r| r.mark_color.is_none())
4543 );
4544 }
4545
4546 /// The other half of each open type: the value an *Other…* row writes, out
4547 /// through the gesture and back through both the query and the run view.
4548 /// The view's token is the canonical spelling, because a renderer's theme
4549 /// table is keyed by string and parses what it does not find.
4550 #[test]
4551 fn an_exact_value_crosses_as_its_own_token_and_comes_back_whole() {
4552 let d = doc("exact\n");
4553 let in_text = |d: &Arc<LeafDoc>| {
4554 let at = d.source().find("exact").unwrap() as u32;
4555 d.set_selection_offsets(at, at);
4556 };
4557 d.set_font_size(Some(FontSize::Points(14.0)));
4558 in_text(&d);
4559 d.set_font_family(Some(FontFace::Named("Garamond".to_string())));
4560 in_text(&d);
4561 d.set_text_color(Some(TextColor::Rgb {
4562 r: 0xc0,
4563 g: 0x30,
4564 b: 0x30,
4565 }));
4566 in_text(&d);
4567 let v = d.set_line_spacing(Some(LineHeight::Ratio(1.3)));
4568
4569 let styled: Vec<(Option<&str>, Option<&str>, Option<&str>)> = v
4570 .rows
4571 .iter()
4572 .flat_map(|r| r.runs.iter())
4573 .filter(|r| !r.text.trim().is_empty())
4574 .map(|r| {
4575 (
4576 r.size.as_deref(),
4577 r.font.as_deref(),
4578 r.text_color.as_deref(),
4579 )
4580 })
4581 .collect();
4582 assert_eq!(
4583 styled,
4584 [(Some("14pt"), Some("Garamond"), Some("#c03030"))],
4585 "the value's own spelling, where a name stood before"
4586 );
4587 let spaced: Vec<&str> = v
4588 .rows
4589 .iter()
4590 .filter_map(|r| r.line_height.as_deref())
4591 .collect();
4592 assert_eq!(spaced, ["1.3"]);
4593
4594 assert_eq!(d.font_size_at_caret(), Some(FontSize::Points(14.0)));
4595 assert_eq!(
4596 d.font_family_at_caret(),
4597 Some(FontFace::Named("Garamond".to_string()))
4598 );
4599 assert_eq!(
4600 d.text_color_at_caret(),
4601 Some(TextColor::Rgb {
4602 r: 0xc0,
4603 g: 0x30,
4604 b: 0x30
4605 })
4606 );
4607 assert_eq!(d.line_spacing_at_caret(), Some(LineHeight::Ratio(1.3)));
4608 }
4609
4610 /// A named face reaching a run **inside a table cell** — the other road a
4611 /// run takes to a frontend, and the one an id resolved against the wrong
4612 /// table would quietly ruin: a cell's runs come through [`cell_lines`] and
4613 /// not through [`wysiwyg_rows`], so the map's [`CoreFaceTable`] has to be
4614 /// threaded down both. A grid whose faces all named nothing would draw in
4615 /// the body face and look like a theme that simply had no Garamond.
4616 #[test]
4617 fn a_named_face_reaches_a_run_inside_a_table_cell() {
4618 let d = doc("| a | b |\n|---|---|\n| one | two |\n");
4619 let at = d.source().find("one").unwrap() as u32;
4620 d.set_selection_offsets(at, at + 3);
4621 let v = d.set_font_family(Some(FontFace::Named("Garamond".to_string())));
4622
4623 let faced: Vec<(&str, &str)> = v
4624 .tables
4625 .iter()
4626 .flat_map(|t| t.grid.iter())
4627 .flat_map(|r| r.cells.iter())
4628 .flat_map(|c| c.lines.iter())
4629 .flat_map(|l| l.runs.iter())
4630 .filter_map(|r| Some((r.text.trim(), r.font.as_deref()?)))
4631 .collect();
4632 assert_eq!(faced, [("one", "Garamond")]);
4633 }
4634
4635 /// A value the vocabulary cannot carry writes **nothing at all**, and what
4636 /// the run already said stands — a typo in an *Other…* field is not a
4637 /// reason to throw away the size the author set a minute ago. The one
4638 /// value that clears is the one that *means* the theme's own: a ratio of
4639 /// 1, which is single spacing.
4640 #[test]
4641 fn a_value_outside_the_vocabulary_leaves_the_key_alone() {
4642 let d = doc("plain\n");
4643 let in_text = |d: &Arc<LeafDoc>| {
4644 let at = d.source().find("plain").unwrap() as u32;
4645 d.set_selection_offsets(at, at);
4646 };
4647 d.set_font_size(Some(FontSize::Points(14.0)));
4648 in_text(&d);
4649 assert_eq!(d.font_size_at_caret(), Some(FontSize::Points(14.0)));
4650 for refused in [700.0, 0.0, -3.0, f64::NAN] {
4651 d.set_font_size(Some(FontSize::Points(refused)));
4652 in_text(&d);
4653 assert_eq!(
4654 d.font_size_at_caret(),
4655 Some(FontSize::Points(14.0)),
4656 "{refused} is not a size, and the author's 14pt is not its casualty"
4657 );
4658 }
4659
4660 // A ratio of 1 is single spacing, which is the theme's and has no
4661 // token: that one *is* absence, and clears. A ratio of 0 is not.
4662 d.set_line_spacing(Some(LineHeight::Ratio(1.5)));
4663 in_text(&d);
4664 assert_eq!(
4665 d.line_spacing_at_caret(),
4666 Some(LineHeight::Step(LineSpacing::OneHalf)),
4667 "a ratio that spells a name is that name"
4668 );
4669 d.set_line_spacing(Some(LineHeight::Ratio(0.0)));
4670 in_text(&d);
4671 assert_eq!(
4672 d.line_spacing_at_caret(),
4673 Some(LineHeight::Step(LineSpacing::OneHalf)),
4674 "nought is not a spacing, and refusing it keeps the block's own"
4675 );
4676 d.set_line_spacing(Some(LineHeight::Ratio(1.0)));
4677 in_text(&d);
4678 assert_eq!(d.line_spacing_at_caret(), None, "single is the theme's own");
4679
4680 // And a name that spells a generic is that generic, whatever its case
4681 // — the reading a document's own `data-font` gets. A name that names
4682 // nothing is refused, and the face the run had stands.
4683 d.set_font_family(Some(FontFace::Named(" Serif ".to_string())));
4684 in_text(&d);
4685 assert_eq!(
4686 d.font_family_at_caret(),
4687 Some(FontFace::Generic(FontFamily::Serif))
4688 );
4689 d.set_font_family(Some(FontFace::Named(" ".to_string())));
4690 in_text(&d);
4691 assert_eq!(
4692 d.font_family_at_caret(),
4693 Some(FontFace::Generic(FontFamily::Serif))
4694 );
4695
4696 // `nil` is the argument that clears, and it is the only one.
4697 d.set_font_size(None);
4698 d.set_font_family(None);
4699 in_text(&d);
4700 assert_eq!(d.font_size_at_caret(), None);
4701 assert_eq!(d.font_family_at_caret(), None);
4702 }
4703
4704 /// A page break crosses as the leaf directive it is — the row a paginating
4705 /// frontend opens a page at.
4706 #[test]
4707 fn a_page_break_crosses_as_a_directive_of_its_own() {
4708 let d = doc("before\n\nafter\n");
4709 let v = d.insert_page_break();
4710 let names: Vec<&str> = v.directives.iter().map(|x| x.name.as_str()).collect();
4711 assert_eq!(names, ["page-break"]);
4712 assert!(d.source().contains("page-break"));
4713 }
4714
4715 /// One flag per new control, answered by the format — the toolbar builds
4716 /// itself from these rather than discovering each refusal on a press.
4717 /// Markdown spells all six; XML spells none of them, being parse-only.
4718 #[test]
4719 fn capabilities_answer_for_the_presentation_controls_too() {
4720 let md = doc("x\n").capabilities();
4721 assert!(md.alignment && md.line_spacing);
4722 assert!(md.font_size && md.font_family && md.text_color);
4723 assert!(md.page_break);
4724
4725 let xml = LeafDoc::new("<a>x</a>".to_string(), "xml".to_string())
4726 .unwrap()
4727 .capabilities();
4728 assert!(!xml.alignment && !xml.line_spacing);
4729 assert!(!xml.font_size && !xml.font_family && !xml.text_color);
4730 assert!(!xml.page_break);
4731 }
4732
4733 #[test]
4734 fn the_source_view_publishes_no_media() {
4735 // In the source view the `<video>` markup is the literal text the caret
4736 // is editing — laying a player over it would cover what's being typed.
4737 let d = doc("<video src=\"clip.mp4\" controls></video>\n");
4738 assert_eq!(d.view().media.len(), 1);
4739 assert!(
4740 d.toggle_view().media.is_empty(),
4741 "no placeholders in the source view"
4742 );
4743 }
4744
4745 /// **A foreign caller's offset must never panic.** Every offset entering
4746 /// leaf comes from a UI toolkit that counts in its own units — UIKit hands
4747 /// back UTF-16 positions — so an offset landing mid-character is a normal
4748 /// thing to be handed, not a bug in the caller. Slicing on it aborts the
4749 /// process across the FFI boundary, where there is no unwinding to catch.
4750 ///
4751 /// Reproduces a real crash: `byte index 1236 is not a char boundary; it is
4752 /// inside '…'`.
4753 #[test]
4754 fn an_offset_inside_a_multibyte_char_does_not_panic() {
4755 let d = doc(
4756 "# April 02, 2026\n\nAn interesting thing AI said to me:\n\n> a person… who journals\n",
4757 );
4758 d.toggle_view(); // to the raw source view, where offsets index bytes directly
4759 let src = d.source();
4760 // The interior byte of the `…` — exactly the shape of the crash.
4761 let mid = src.find('…').expect("the ellipsis is in the fixture") + 1;
4762 assert!(
4763 !src.is_char_boundary(mid),
4764 "the fixture must be mid-character"
4765 );
4766
4767 // Every entry point that takes a raw source offset.
4768 let _ = d.pos_for_offset(mid as u32);
4769 let _ = d.vertical_offset(mid as u32, true);
4770 let _ = d.vertical_offset(mid as u32, false);
4771 let _ = d.snap_offset(mid as u32);
4772 let _ = d.step_offset(mid as u32, 1);
4773 let _ = d.step_offset(mid as u32, -1);
4774 let _ = d.distance_offset(0, mid as u32);
4775 let _ = d.text_in_range(0, mid as u32);
4776 let _ = d.set_selection_offsets(mid as u32, mid as u32);
4777 // And the caret must not come to rest inside the character either — a
4778 // mid-character caret is a later panic waiting for the next edit.
4779 let _ = d.replace_range(mid as u32, mid as u32, "x".to_string());
4780 assert!(
4781 d.source().is_char_boundary(d.caret_offset() as usize),
4782 "the caret must sit on a character boundary"
4783 );
4784 }
4785
4786 #[test]
4787 fn cell_lines_split_on_the_break_glyph_carrying_each_lines_source_range() {
4788 use leaf_core::Glyph;
4789 let g = |ch, src| Glyph {
4790 ch,
4791 style: LStyle::default(),
4792 src,
4793 stop: true,
4794 };
4795 // "a" at 10, a `<br>` at 11..15 (the break glyph), "b" at 15; cell 10..16.
4796 let glyphs = [g('a', 10), g('\n', 11), g('b', 15)];
4797 let lines = cell_lines(&glyphs, 10, 16, 0, 0, &[], &CoreFaceTable::default());
4798 assert_eq!(lines.len(), 2, "one break makes two lines");
4799 assert_eq!(
4800 (lines[0].start, lines[0].end),
4801 (10, 11),
4802 "line 1 ends at the break"
4803 );
4804 assert_eq!(
4805 (lines[1].start, lines[1].end),
4806 (15, 16),
4807 "line 2 begins past it"
4808 );
4809 let text =
4810 |l: &TableCellLineView| l.runs.iter().map(|r| r.text.clone()).collect::<String>();
4811 assert_eq!(text(&lines[0]), "a");
4812 assert_eq!(text(&lines[1]), "b");
4813
4814 // A trailing break leaves an empty last line homed at the cell's end.
4815 let trailing = [g('a', 10), g('\n', 11)];
4816 let lines = cell_lines(&trailing, 10, 15, 0, 0, &[], &CoreFaceTable::default());
4817 assert_eq!(lines.len(), 2);
4818 assert!(lines[1].runs.is_empty());
4819 assert_eq!((lines[1].start, lines[1].end), (15, 15));
4820
4821 // No break: one line spanning the whole cell.
4822 let plain = [g('P', 10), g('e', 11)];
4823 let lines = cell_lines(&plain, 10, 12, 0, 0, &[], &CoreFaceTable::default());
4824 assert_eq!(lines.len(), 1);
4825 assert_eq!((lines[0].start, lines[0].end), (10, 12));
4826 }
4827
4828 fn row_text(v: &DocView, row: usize) -> String {
4829 v.rows[row].runs.iter().map(|r| r.text.clone()).collect()
4830 }
4831
4832 #[test]
4833 fn unwrapped_collapses_a_paragraph_to_one_row() {
4834 let d = doc("one two three four five six seven eight\n");
4835 let wrapped = d.set_width(10);
4836 let unwrapped = d.set_unwrapped();
4837 assert!(
4838 unwrapped.rows.len() < wrapped.rows.len(),
4839 "a narrow column wrap splits the paragraph; unwrapped keeps it whole"
4840 );
4841 assert!(
4842 (0..unwrapped.rows.len()).any(|i| row_text(&unwrapped, i).contains("eight")),
4843 "the whole paragraph, including its last word, sits on a single unwrapped row"
4844 );
4845 }
4846
4847 #[test]
4848 fn offsets_round_trip_when_unwrapped() {
4849 let d = doc("hello world\n");
4850 d.set_unwrapped();
4851 // offset -> (row, ch) -> offset is stable, so the pixel-wrapping frontend can
4852 // map between its visual lines and core's byte-offset caret model.
4853 let rc = d.pos_for_offset(6); // the 'w' of "world"
4854 assert_eq!(d.offset_for_pos(rc.row, rc.ch), 6);
4855 }
4856
4857 #[test]
4858 fn set_unwrapped_is_idempotent() {
4859 let d = doc("a paragraph of some length here\n");
4860 let first = d.set_unwrapped();
4861 let second = d.set_unwrapped();
4862 assert_eq!(first.rows.len(), second.rows.len());
4863 }
4864
4865 #[test]
4866 fn newline_on_last_list_item_before_a_blockquote_starts_a_new_item() {
4867 let src = "- one\n- two\n- three\n\n> quote\n";
4868 let d = doc(src);
4869 let off = (src.find("three").unwrap() + "three".len()) as u32; // end of "three" = 19
4870 d.set_selection_offsets(off, off);
4871 d.newline();
4872 let after = d.source();
4873 assert!(
4874 after.contains("- three\n- ") && after.contains("> quote"),
4875 "expected a new empty list item with the blockquote intact, got: {after:?}"
4876 );
4877 }
4878
4879 #[test]
4880 fn enter_on_an_empty_line_adds_one_newline_and_one_backspace_undoes_it() {
4881 let d = doc("hello\n");
4882 d.set_selection_offsets(5, 5);
4883 d.newline(); // paragraph "hello" → a paragraph break, caret on the empty line
4884 let after_para = d.source();
4885 let caret_para = d.caret_offset();
4886 d.newline(); // Enter on the empty line
4887 assert_eq!(
4888 d.source().len(),
4889 after_para.len() + 1,
4890 "an empty-line Enter adds a single newline, not another paragraph break"
4891 );
4892 d.backspace(); // a single Backspace restores the previous state
4893 assert_eq!(d.source(), after_para);
4894 assert_eq!(d.caret_offset(), caret_para);
4895 }
4896
4897 #[test]
4898 fn enter_in_a_nonempty_paragraph_still_opens_a_new_paragraph() {
4899 let d = doc("hello\n");
4900 d.set_selection_offsets(5, 5);
4901 let before = d.source().len();
4902 d.newline();
4903 assert_eq!(
4904 d.source().len(),
4905 before + 2,
4906 "a paragraph break is still \\n\\n"
4907 );
4908 }
4909
4910 #[test]
4911 fn link_destination_at_caret_reads_the_caret_link() {
4912 let d = doc("see [t](https://x.dev) ok\n");
4913 d.set_selection_offsets(5, 5); // caret on the link text "t"
4914 assert_eq!(
4915 d.link_destination_at_caret().as_deref(),
4916 Some("https://x.dev")
4917 );
4918 d.set_selection_offsets(0, 0); // caret on plain text
4919 assert_eq!(d.link_destination_at_caret(), None);
4920 }
4921
4922 #[test]
4923 fn image_destination_at_caret_reads_the_image_under_the_caret() {
4924 let d = doc(" after\n");
4925 d.set_selection_offsets(3, 3); // caret on the alt text
4926 assert_eq!(d.image_destination_at_caret().as_deref(), Some("cat.png"));
4927 d.set_selection_offsets(15, 15); // caret past the image, in the prose
4928 assert_eq!(d.image_destination_at_caret(), None);
4929 }
4930
4931 #[test]
4932 fn the_frame_carries_the_caret_link_so_a_toolbar_can_light_and_seed_from_it() {
4933 // The reason it rides `DocView` rather than being asked for: stepping the
4934 // caret out of the link changes no other chrome fact on the frame, so a
4935 // toolbar that only redraws on a *changed* state would keep a stale light.
4936 let d = doc("see [t](https://x.dev) ok\n");
4937 d.set_selection_offsets(5, 5);
4938 let inside = d.view();
4939 assert_eq!(inside.link.as_deref(), Some("https://x.dev"));
4940 assert_eq!(inside.heading, None);
4941 assert!(inside.active.is_empty());
4942
4943 d.set_selection_offsets(0, 0);
4944 let outside = d.view();
4945 assert_eq!(outside.link, None);
4946 // Nothing else the frame reports moved with it.
4947 assert_eq!(outside.heading, inside.heading);
4948 assert_eq!(outside.active, inside.active);
4949 }
4950
4951 #[test]
4952 fn insert_footnote_crosses_and_leaves_the_caret_in_the_new_note() {
4953 // The button's round trip through the boundary: both halves written, and
4954 // a caret offset a host can type into without asking anything else.
4955 let d = doc("A claim and more.\n");
4956 d.set_selection_offsets(7, 7); // just past "A claim"
4957 d.insert_footnote();
4958 assert!(
4959 d.source().starts_with("A claim[^1] and more."),
4960 "{:?}",
4961 d.source()
4962 );
4963 assert!(d.source().contains("[^1]:"), "{:?}", d.source());
4964
4965 let note = d.footnote_at(9).expect("the reference just written");
4966 assert_eq!(note.label, "1");
4967 assert_eq!(
4968 d.caret_offset(),
4969 note.offset.expect("an empty note is still a place")
4970 );
4971 // …and the way back out is the same one a reader uses.
4972 assert_eq!(
4973 d.footnote_definition_at_caret().expect("in the note").label,
4974 "1"
4975 );
4976 }
4977
4978 #[test]
4979 fn a_highlight_is_coloured_at_the_caret_and_the_frame_says_which() {
4980 // The whole crossing a colour palette makes: press the swatch, and the
4981 // frame that comes back names the colour so the swatch can light.
4982 let d = doc("a word b\n");
4983 d.set_selection_offsets(2, 6);
4984 d.toggle_mark();
4985 assert_eq!(d.source(), "a ==word== b\n");
4986
4987 d.set_selection_offsets(5, 5); // inside the highlight
4988 assert!(d.caret_in_mark());
4989 let v = d.set_mark_color(Some(MarkColor::Red));
4990 assert_eq!(d.source(), "a ==🔴 word== b\n");
4991 assert_eq!(v.mark_color, Some(MarkColor::Red));
4992
4993 // And the way back: no colour, still a highlight.
4994 let v = d.set_mark_color(None);
4995 assert_eq!(d.source(), "a ==word== b\n");
4996 assert_eq!(v.mark_color, None);
4997 assert!(d.caret_in_mark());
4998 }
4999
5000 #[test]
5001 fn one_press_highlights_and_colours_and_one_undo_takes_it_back() {
5002 // What a toolbar swatch calls. The fold is core's, and it is what makes
5003 // the press reversible in one step rather than leaving an uncoloured
5004 // highlight behind.
5005 let d = doc("a word b\n");
5006 d.set_selection_offsets(2, 6);
5007 let v = d.highlight(Some(MarkColor::Purple));
5008 assert_eq!(d.source(), "a ==\u{1F7E3} word== b\n");
5009 assert_eq!(v.mark_color, Some(MarkColor::Purple));
5010
5011 d.undo();
5012 assert_eq!(d.source(), "a word b\n");
5013 }
5014
5015 #[test]
5016 fn the_palette_has_two_gates_and_they_ask_different_questions() {
5017 // `mark_color` is the format's answer and `caret_in_mark` the caret's.
5018 // A djot document spells the highlight and no colour for it, so the two
5019 // disagree there — which is the case a toolbar gating on either one
5020 // alone gets wrong.
5021 assert!(doc("x\n").capabilities().mark_color);
5022 let dj = LeafDoc::new("a {=word=} b\n".to_string(), "djot".to_string()).unwrap();
5023 assert!(dj.capabilities().mark, "djot writes the highlight");
5024 assert!(!dj.capabilities().mark_color, "and no colour on it");
5025 dj.set_selection_offsets(5, 5);
5026 assert!(dj.caret_in_mark(), "the caret is in one all the same");
5027
5028 let d = doc("a word b\n");
5029 d.set_selection_offsets(3, 3);
5030 assert!(!d.caret_in_mark(), "no highlight to colour here");
5031 assert_eq!(d.set_mark_color(Some(MarkColor::Blue)).mark_color, None);
5032 assert_eq!(d.source(), "a word b\n", "and nothing written");
5033 }
5034
5035 #[test]
5036 fn capabilities_answer_for_footnotes_the_way_the_format_does() {
5037 assert!(
5038 doc("x\n").capabilities().footnote,
5039 "markdown spells the pair"
5040 );
5041 let html = LeafDoc::new("<p>x</p>\n".to_string(), "html".to_string()).unwrap();
5042 assert!(
5043 !html.capabilities().footnote,
5044 "html has no footnote of its own"
5045 );
5046 }
5047
5048 #[test]
5049 fn footnote_at_caret_crosses_with_its_note_and_its_offset() {
5050 let d = doc("A claim[^1] and more.\n\n[^1]: the note\n");
5051 d.set_selection_offsets(9, 9); // caret on the reference's label
5052 let f = d
5053 .footnote_at_caret()
5054 .expect("the caret stands in a reference");
5055 assert_eq!(f.label, "1");
5056 assert_eq!(f.text.as_deref(), Some("the note"));
5057 // The note's first word — a byte the caret can actually rest on. The
5058 // definition's `[^1]:` marker is decoration with no stop of its own.
5059 assert_eq!(f.offset, Some(29));
5060 assert_eq!(f.end, Some(37));
5061
5062 d.set_selection_offsets(0, 0); // caret on plain text
5063 assert!(d.footnote_at_caret().is_none());
5064 }
5065
5066 #[test]
5067 fn footnote_at_crosses_for_an_offset_without_moving_the_caret() {
5068 // What a hover needs: the note under the pointer, and the caret left
5069 // exactly where the reader put it.
5070 let d = doc("A claim[^1] and more.\n\n[^1]: the note\n");
5071 d.set_selection_offsets(0, 0);
5072 let f = d.footnote_at(9).expect("offset 9 stands in the reference");
5073 assert_eq!(f.label, "1");
5074 assert_eq!(f.text.as_deref(), Some("the note"));
5075 assert_eq!(d.caret_offset(), 0, "asking must not move the caret");
5076 assert!(d.footnote_at(2).is_none(), "offset 2 is prose");
5077 }
5078
5079 #[test]
5080 fn footnote_definition_at_caret_crosses_with_the_way_back() {
5081 let d = doc("A claim[^1] and more.\n\n[^1]: the note\n");
5082 d.set_selection_offsets(30, 30); // caret inside the note's body
5083 let f = d
5084 .footnote_definition_at_caret()
5085 .expect("the caret stands in a definition");
5086 assert_eq!(f.label, "1");
5087 assert_eq!(f.offset, Some(9), "the reference's label");
5088
5089 // Disjoint from the reference query, which is what lets one gesture mean
5090 // "down" up top and "back up" down here.
5091 d.set_selection_offsets(9, 9);
5092 assert!(d.footnote_definition_at_caret().is_none());
5093 assert!(d.footnote_at_caret().is_some());
5094 }
5095
5096 /// The contract a peek is built on: a note's offsets map to rows whose runs
5097 /// are the note *rendered* — emphasis as an italic run, `` `code` `` as a
5098 /// code run, a link as a link run — so a frontend draws it the way the
5099 /// document draws it instead of showing the reader raw asterisks.
5100 #[test]
5101 fn a_notes_offsets_map_to_its_rendered_rows() {
5102 let src = "Claim[^a].\n\n[^a]: see *emphasis* and `code` and [a link](https://x.dev).\n";
5103 let d = doc(src);
5104 let view = d.set_unwrapped();
5105 d.set_selection_offsets(6, 6); // the reference's label
5106
5107 let f = d.footnote_at_caret().expect("a reference");
5108 let start = d.pos_for_offset(f.offset.expect("a note"));
5109 let end = d.pos_for_offset(f.end.expect("a note") - 1);
5110 assert_eq!(
5111 start.row, end.row,
5112 "a one-paragraph note is one unwrapped row"
5113 );
5114
5115 let row = &view.rows[start.row as usize];
5116 let runs: Vec<(&str, &str, bool)> = row
5117 .runs
5118 .iter()
5119 .map(|r| (r.role.as_str(), r.text.as_str(), r.italic))
5120 .collect();
5121 assert!(runs.contains(&("body", "emphasis", true)), "got {runs:?}");
5122 assert!(
5123 runs.iter()
5124 .any(|(role, text, _)| *role == "code" && *text == "code"),
5125 "got {runs:?}"
5126 );
5127 assert!(
5128 runs.iter()
5129 .any(|(role, text, _)| *role == "link" && *text == "a link"),
5130 "got {runs:?}"
5131 );
5132
5133 // The rendered row carries no markup characters at all — which is the
5134 // whole point, and what `text` (source bytes) deliberately still does.
5135 let rendered: String = row.runs.iter().map(|r| r.text.as_str()).collect();
5136 assert!(
5137 !rendered.contains('*') && !rendered.contains('`'),
5138 "got {rendered:?}"
5139 );
5140 assert!(
5141 f.text.as_deref().unwrap().contains('*'),
5142 "the source answer keeps them"
5143 );
5144
5145 // `ch` is where the body starts within the row — past the `[a] ` marker,
5146 // so a frontend that wants the note without its label can slice there.
5147 assert_eq!(row.runs[0].role, "list");
5148 assert_eq!(start.ch as usize, row.runs[0].text.chars().count());
5149
5150 // And each run says where it came from, which is how a link run drawn in
5151 // a popover learns where it points. `Run` otherwise says how a span
5152 // looks, never what it means.
5153 let link = row
5154 .runs
5155 .iter()
5156 .find(|r| r.role == "link")
5157 .expect("a link run");
5158 assert_eq!(
5159 d.link_destination_at(link.src).as_deref(),
5160 Some("https://x.dev"),
5161 "the run at {} is the link",
5162 link.src
5163 );
5164 }
5165
5166 /// The peek bug, in the shape it was actually found in: three notes, each
5167 /// ending in a link, which is what a real citation block looks like.
5168 ///
5169 /// `a_notes_offsets_map_to_its_rendered_rows` above uses a note ending in a
5170 /// visible `.`, so its last byte has a row of its own and `end - 1` reads
5171 /// right. Take the full stop away — end the note *with* the link, as a
5172 /// citation does — and the last byte falls inside the hidden destination,
5173 /// where `pos_for_offset` snaps forward onto the next note's row. Hovering
5174 /// `[^2]` peeked notes 2 *and* 3.
5175 #[test]
5176 fn a_note_ending_in_a_link_covers_its_own_row_and_no_other() {
5177 let src = "A[^1] B[^2] C[^3].\n\n\
5178 [^1]: https://en.wikipedia.org/wiki/Moravec%27s_paradox\n\n\
5179 [^2]: [\"How to Get Startup Ideas,\" Nov 2012](https://www.paulgraham.com/startupideas.html)\n\n\
5180 [^3]: [Alma 37:46](https://www.churchofjesuschrist.org/study/scriptures/bofm/alma/37?lang=eng&id=p46#p46)\n";
5181 let d = doc(src);
5182 let view = d.set_unwrapped();
5183
5184 // The caret in the [^2] reference, exactly as a hover resolves it.
5185 let off2 = src.find("[^2] C").unwrap() as u32 + 2;
5186 d.set_selection_offsets(off2, off2);
5187 let f = d.footnote_at_caret().expect("a reference");
5188 let (start, end) = (f.offset.expect("a note"), f.end.expect("a note"));
5189
5190 let span = d.row_range_for(start, end);
5191 assert_eq!(span.first, span.last, "one note is one unwrapped row");
5192
5193 // And what it draws is note 2 alone — the assertion the popover failed.
5194 let drawn: String = view.rows[span.first as usize]
5195 .runs
5196 .iter()
5197 .map(|r| r.text.as_str())
5198 .collect();
5199 assert!(drawn.contains("How to Get Startup Ideas"), "got {drawn:?}");
5200 assert!(
5201 !drawn.contains("Alma"),
5202 "note 3 leaked into the peek: {drawn:?}"
5203 );
5204
5205 // The old arithmetic, pinned as still wrong so nobody quietly restores
5206 // it: this is the failure `row_range_for` exists instead of.
5207 assert_ne!(
5208 d.pos_for_offset(end - 1).row,
5209 span.last,
5210 "the forward snap still leaves the note's row — that is the point",
5211 );
5212
5213 // Note 1 is a bare autolink, whose visible text *is* its URL, so it was
5214 // never affected and must not change.
5215 let off1 = src.find("[^1] B").unwrap() as u32 + 2;
5216 d.set_selection_offsets(off1, off1);
5217 let f1 = d.footnote_at_caret().expect("a reference");
5218 let one = d.row_range_for(f1.offset.unwrap(), f1.end.unwrap());
5219 assert_eq!(one.first, one.last);
5220 assert_ne!(one.first, span.first, "and it is a different note");
5221 }
5222
5223 /// A run's `src` is a byte offset core handed over, not something a frontend
5224 /// counted its way to — so multi-byte prose ahead of a link inside a note
5225 /// can't slide it.
5226 ///
5227 /// The offset is a *byte* offset while the run's text is characters and the
5228 /// row's columns are display cells; `src` is the only one of the three a
5229 /// frontend can use without converting between the other two.
5230 #[test]
5231 fn a_runs_source_offset_survives_multibyte_prose_ahead_of_it() {
5232 let src = "Claim[^a].\n\n[^a]: 日記 café [a link](https://x.dev).\n";
5233 let d = doc(src);
5234 let view = d.set_unwrapped();
5235 d.set_selection_offsets(6, 6);
5236
5237 let f = d.footnote_at_caret().expect("a reference");
5238 let start = d.pos_for_offset(f.offset.expect("a note"));
5239 let row = &view.rows[start.row as usize];
5240 let link = row
5241 .runs
5242 .iter()
5243 .find(|r| r.role == "link")
5244 .expect("a link run");
5245
5246 assert_eq!(
5247 d.link_destination_at(link.src).as_deref(),
5248 Some("https://x.dev")
5249 );
5250 assert_eq!(
5251 &src[link.src as usize..][.."a link".len()],
5252 "a link",
5253 "and it is a byte offset, not a character or column index"
5254 );
5255 // Which the character count is not: `日記 café ` is 9 characters and 13
5256 // bytes, so anything derived from the run text lands in the wrong place.
5257 let counted: usize = row
5258 .runs
5259 .iter()
5260 .take_while(|r| r.role != "link")
5261 .map(|r| r.text.chars().count())
5262 .sum();
5263 assert_ne!(counted, link.src as usize);
5264 }
5265
5266 /// The round trip through the API a frontend actually calls — which places
5267 /// carets, and so snaps them to real stops. Offsets that named the `[^`
5268 /// markers passed every test that assigned the caret directly and still
5269 /// dumped the reader in the paragraph above the note.
5270 #[test]
5271 fn following_a_footnote_and_coming_back_lands_on_real_caret_stops() {
5272 let d = doc("A claim[^1] and more.\n\n[^1]: the note\n");
5273 d.set_selection_offsets(9, 9);
5274
5275 let down = d
5276 .footnote_at_caret()
5277 .expect("a reference")
5278 .offset
5279 .expect("a note");
5280 d.set_selection_offsets(down, down);
5281 assert_eq!(
5282 d.caret_offset(),
5283 down,
5284 "the note is somewhere the caret fits"
5285 );
5286
5287 let up = d
5288 .footnote_definition_at_caret()
5289 .expect("arrived inside the definition")
5290 .offset
5291 .expect("a reference to return to");
5292 d.set_selection_offsets(up, up);
5293 assert_eq!(d.caret_offset(), up, "and so is the reference");
5294 assert_eq!(
5295 d.footnote_at_caret().expect("back on the reference").label,
5296 "1"
5297 );
5298 }
5299
5300 #[test]
5301 fn a_footnote_reference_crosses_the_ffi_raised() {
5302 // The whole point of the `sup` flag: without it a reference reaches
5303 // Swift as a run indistinguishable from a hyperlink's, which is why it
5304 // used to draw at body size.
5305 let d = doc("A claim[^1] and more.\n");
5306 let view = d.view();
5307 let runs: Vec<&Run> = view.rows.iter().flat_map(|r| &r.runs).collect();
5308 let chip = runs
5309 .iter()
5310 .find(|r| r.text.contains('1'))
5311 .expect("the reference's chip");
5312 assert!(chip.sup, "the reference should cross raised");
5313 assert!(!chip.sub);
5314 assert_eq!(
5315 chip.role, "link",
5316 "and still carrying the role every frontend paints"
5317 );
5318 // The prose it interrupts is a run of its own, on the normal baseline —
5319 // which is what proves the flag splits runs rather than bleeding.
5320 let prose = runs
5321 .iter()
5322 .find(|r| r.text.contains("claim"))
5323 .expect("the prose");
5324 assert!(!prose.sup && !prose.sub);
5325 }
5326
5327 #[test]
5328 fn utf16_indices_round_trip_through_the_visible_text_in_both_views() {
5329 // Hidden delimiters, an emoji outside the BMP, and a block gap: the
5330 // three ways an `NSRange` into the visible text and a source byte
5331 // offset part company.
5332 let d = doc("a **b\u{1F600}** c\n\nd\n");
5333 let end = d.doc_end_offset();
5334 let text = d.text_in_range(0, end);
5335 assert_eq!(text, "a b\u{1F600} c\nd");
5336 let total = text.encode_utf16().count() as u32;
5337 assert_eq!(d.utf16_index_for_offset(end), total);
5338 assert_eq!(d.offset_for_utf16_index(total), end);
5339 // Walk every stop: index it, and come back to the same stop.
5340 let mut off = 0u32;
5341 loop {
5342 let idx = d.utf16_index_for_offset(off);
5343 assert_eq!(
5344 d.offset_for_utf16_index(idx),
5345 off,
5346 "stop {off} via index {idx}"
5347 );
5348 let next = d.step_offset(off, 1);
5349 if next == off {
5350 break;
5351 }
5352 off = next;
5353 }
5354 // The 'c' comes after the hidden `**` and the two-unit emoji.
5355 let c = "a **b\u{1F600}** c".find(" c").unwrap() as u32 + 1;
5356 assert_eq!(
5357 d.utf16_index_for_offset(c),
5358 "a b\u{1F600} ".encode_utf16().count() as u32
5359 );
5360
5361 // Source view: the text is the raw source, so the index is the plain
5362 // UTF-16 count of the bytes before the offset — delimiters included.
5363 d.toggle_view();
5364 assert_eq!(
5365 d.text_in_range(0, d.doc_end_offset()),
5366 "a **b\u{1F600}** c\n\nd\n"
5367 );
5368 assert_eq!(
5369 d.utf16_index_for_offset(c),
5370 "a **b\u{1F600}** ".encode_utf16().count() as u32
5371 );
5372 assert_eq!(d.offset_for_utf16_index(d.utf16_index_for_offset(c)), c);
5373 // The batch form is the one call, many times, in the order asked.
5374 assert_eq!(
5375 d.utf16_indices_for_offsets(vec![c, 0, end]),
5376 vec![
5377 d.utf16_index_for_offset(c),
5378 d.utf16_index_for_offset(0),
5379 d.utf16_index_for_offset(end)
5380 ]
5381 );
5382 // Inside the emoji's surrogate pair resolves to the emoji itself.
5383 let emoji = "a **b".len() as u32;
5384 assert_eq!(
5385 d.offset_for_utf16_index(d.utf16_index_for_offset(emoji) + 1),
5386 emoji
5387 );
5388 }
5389
5390 #[test]
5391 fn text_in_range_hides_delimiters_like_the_screen_does() {
5392 // "a **bold** c\n": 0:'a' 1:' ' 2:'*' 3:'*' 4:'b' 5:'o' 6:'l' 7:'d'
5393 // 8:'*' 9:'*' 10:' ' 11:'c' 12:'\n'. Bytes 8..10 are the closing `**`
5394 // — hidden, no glyph — and bytes 2..4 the opening `**`, likewise
5395 // hidden. `caret_steps_over_hidden_delimiters` in leaf-core already
5396 // pins that one Right from 7 (just past the 'd') lands on 10 (the
5397 // space before 'c'), skipping 8/9 entirely — so the *visible* text
5398 // transiting [7, 10) is exactly "d": the closing `**` contributes
5399 // nothing, matching what's drawn on screen.
5400 let d = doc("a **bold** c\n");
5401 assert_eq!(d.text_in_range(7, 10), "d");
5402 assert_eq!(
5403 d.text_in_range(7, 10).chars().count() as i32,
5404 d.distance_offset(7, 10),
5405 "text(in:).count() must equal offset(from:to:) — the UITextInput invariant this bug broke"
5406 );
5407
5408 // Plain text with no hidden delimiter in range: unchanged, still the
5409 // raw slice, proving the fix doesn't regress the common case.
5410 assert_eq!(d.text_in_range(0, 1), "a");
5411 assert_eq!(d.text_in_range(11, 12), "c");
5412 assert_eq!(
5413 d.text_in_range(0, 1).chars().count() as i32,
5414 d.distance_offset(0, 1)
5415 );
5416 }
5417
5418 #[test]
5419 fn text_in_range_matches_distance_offset_across_marked_up_and_plain_spans() {
5420 // The general invariant, straddling bold/italic/code spans and not:
5421 // for any pair of offsets, the visible text `text_in_range` returns
5422 // must have exactly as many `chars()` as `distance_offset` reports
5423 // stops between them — otherwise iOS's word tokenizer (which fetches
5424 // a text window, finds a boundary by indexing into *that string*, and
5425 // converts the index back to a position via `position(from:offset:)`)
5426 // resolves the boundary at the wrong offset.
5427 let d = doc("a **bold** _em_ and `code` here\n");
5428 let len = d.source().len() as u32;
5429 let mut pairs = Vec::new();
5430 let mut a = 0u32;
5431 while a < len {
5432 let mut b = a + 1;
5433 while b <= len {
5434 pairs.push((a, b));
5435 b += 3; // sample rather than an O(n^2) sweep
5436 }
5437 a += 1;
5438 }
5439 for (a, b) in pairs {
5440 let text = d.text_in_range(a, b);
5441 let dist = d.distance_offset(a, b).abs();
5442 assert_eq!(
5443 text.chars().count() as i32,
5444 dist,
5445 "text_in_range({a}, {b}) = {text:?} has {} chars, but distance_offset says {dist}",
5446 text.chars().count()
5447 );
5448 }
5449 }
5450
5451 #[test]
5452 fn text_in_range_separates_paragraphs_so_words_dont_merge_across_the_gap() {
5453 // Regression: double-tapping the last word on a line immediately
5454 // followed by a paragraph break selected past the break into the
5455 // next paragraph — and kept compounding across further trivial
5456 // paragraphs in a row — because `text_in_range` returned the two
5457 // paragraphs' text with nothing between them: "hello" then "hello"
5458 // read back as one merged "hellohello" run of letters, no different
5459 // from the raw source concatenation, and iOS's word tokenizer duly
5460 // selected the whole run as a single word.
5461 let d = doc("hello\n\nhello\n\nhello\n");
5462 let src = d.source();
5463 assert_eq!(
5464 src.find("hello").unwrap(),
5465 0,
5466 "paragraph 1 at the very start"
5467 );
5468 let p2 = src[5..].find("hello").unwrap() + 5; // 7: paragraph 2's "hello"
5469
5470 // A window straddling the tail of paragraph 1 ("lo") and the head of
5471 // paragraph 2 ("he").
5472 let text = d.text_in_range(3, p2 as u32 + 2);
5473 assert_ne!(
5474 text, "lohe",
5475 "the two paragraphs' words must not read as merged"
5476 );
5477 assert!(
5478 text.chars().any(|c| !c.is_alphanumeric()),
5479 "a non-letter must separate the two paragraphs' words: got {text:?}"
5480 );
5481 assert_eq!(
5482 text, "lo\nhe",
5483 "exactly one separator opens the second paragraph's head"
5484 );
5485
5486 // A window that is nothing but paragraph 1's end stop (no glyph in
5487 // it: it starts exactly at the end of paragraph 1's own last row)
5488 // is that stop's one character, the break itself.
5489 let gap_only = d.text_in_range(5, p2 as u32);
5490 assert_eq!(gap_only, "\n");
5491
5492 // The break is the end stop's own character, not one inserted beside
5493 // it, so the equality the test above asserts holds across a
5494 // paragraph boundary too — the tokenizer's `position(from:offset:)`
5495 // walk lands exactly where the text it was handed put a boundary.
5496 for (a, b) in [(0u32, src.len() as u32), (3, p2 as u32 + 2), (5, p2 as u32)] {
5497 let text = d.text_in_range(a, b);
5498 let dist = d.distance_offset(a, b);
5499 assert_eq!(
5500 text.chars().count() as i32,
5501 dist,
5502 "text_in_range({a}, {b}) = {text:?} ({} chars) vs distance_offset {dist}",
5503 text.chars().count()
5504 );
5505 }
5506
5507 // Caret motion itself is untouched by any of this: from the very end
5508 // of paragraph 1's row, a paragraph gap still costs exactly one
5509 // Right press to reach the start of paragraph 2 — matching
5510 // leaf-core's `the_caret_skips_the_gap_between_two_paragraphs`.
5511 assert_eq!(
5512 d.distance_offset(5, p2 as u32),
5513 1,
5514 "one Right crosses the whole gap"
5515 );
5516 }
5517
5518 #[test]
5519 fn text_in_range_ends_a_line_at_each_table_cell_and_splits_none_inside() {
5520 // A cell's end reads as a line end — the tokenizer keeps `Status` and
5521 // `Tables` apart, and a tap past `Feature`'s last letter has no space
5522 // to step over into `Status` — and a table's rule rows, decoration
5523 // *inside* the one block, put nothing inside a cell (`Feature` once
5524 // came back as `F\neature`). The table's trailing stop — the caret
5525 // home past the last cell — draws no glyph either, so the document's
5526 // end is one more line end, the blank line under the table where the
5527 // caret past it stands.
5528 let d = doc("| Feature | Status |\n| --- | --- |\n| Tables | editable |\n");
5529 assert_eq!(
5530 d.text_in_range(0, d.doc_end_offset()),
5531 "Feature\nStatus\nTables\neditable\n"
5532 );
5533 }
5534
5535 #[test]
5536 fn a_coloured_highlight_rides_out_as_a_name_beside_the_mark_role() {
5537 // The host draws the wash, so the colour has to reach it. It rides
5538 // `mark_color` rather than folding into `role` (`"mark-red"`) on
5539 // purpose: a renderer that only knows `"mark"` — every version of the
5540 // Swift one before this field existed — still draws the highlight.
5541 let d = doc("a ==\u{1F534} red== and ==plain== b\n");
5542 let runs = &d.view().rows[0].runs;
5543 let marks: Vec<(&str, Option<&str>)> = runs
5544 .iter()
5545 .filter(|r| r.role == "mark")
5546 .map(|r| (r.text.as_str(), r.mark_color.as_deref()))
5547 .collect();
5548 assert_eq!(marks, [("red", Some("red")), ("plain", None)]);
5549 assert!(
5550 runs.iter()
5551 .all(|r| r.role == "mark" || r.mark_color.is_none()),
5552 "nothing but a mark names a colour"
5553 );
5554 }
5555
5556 #[test]
5557 fn a_highlight_splits_runs_on_its_own_bytes_and_carries_its_id() {
5558 let d = doc("one two three\n");
5559 let view = d.set_highlights(vec![Highlight {
5560 start: 4,
5561 end: 7,
5562 id: "remark-1".into(),
5563 color: Some("#ffe066".into()),
5564 marker: Some("text.bubble".into()),
5565 }]);
5566 let row = &view.rows[0];
5567 let texts: Vec<(&str, Option<&str>)> = row
5568 .runs
5569 .iter()
5570 .map(|r| (r.text.as_str(), r.hl.as_deref()))
5571 .collect();
5572 assert_eq!(
5573 texts,
5574 [("one ", None), ("two", Some("remark-1")), (" three", None)],
5575 "the wash begins and ends exactly on the highlight's bytes"
5576 );
5577 assert_eq!(row.runs[1].hl_color.as_deref(), Some("#ffe066"));
5578 assert_eq!(d.highlight_at(5).as_deref(), Some("remark-1"));
5579 assert_eq!(
5580 d.highlights()
5581 .first()
5582 .and_then(|h| h.marker.clone())
5583 .as_deref(),
5584 Some("text.bubble"),
5585 "the marker rides back out for the frontend's margin pass"
5586 );
5587 assert_eq!(d.highlight_at(7), None, "end is exclusive");
5588 // A replace with nothing clears the wash.
5589 let view = d.set_highlights(Vec::new());
5590 assert!(view.rows[0].runs.iter().all(|r| r.hl.is_none()));
5591 }
5592
5593 /// The whole point of the record is that the numbers cross the boundary,
5594 /// so this checks the ones a host would show — and that a selection
5595 /// narrows them and no selection answers nothing at all.
5596 #[test]
5597 fn counts_cross_the_boundary_whole_and_selected() {
5598 let d = doc("a **bold** word\n\n- item\n");
5599 let c = d.counts();
5600 assert_eq!(
5601 (
5602 c.words,
5603 c.characters,
5604 c.characters_without_spaces,
5605 c.paragraphs
5606 ),
5607 (4, 15, 13, 2)
5608 );
5609
5610 assert!(d.selection_counts().is_none(), "no selection, no counts");
5611 d.select_range(0, 10);
5612 let s = d.selection_counts().expect("a selection");
5613 assert_eq!((s.words, s.characters, s.paragraphs), (2, 6, 1));
5614 }
5615
5616 #[test]
5617 fn an_inline_formula_is_a_math_run_paired_with_its_view_by_src() {
5618 // Off until the renderer says it paints in a line: the TeX as code.
5619 let d = doc("say $x+y$ here\n\nnext\n");
5620 d.set_selection_offsets(16, 16);
5621 let v = d.view();
5622 assert!(v.math.is_empty());
5623 assert!(
5624 v.rows[0]
5625 .runs
5626 .iter()
5627 .any(|r| r.role == "code" && r.text == "x+y")
5628 );
5629 // On: one `math` run, one character, and a view whose `src` is its.
5630 let v = d.set_inline_pictures(true);
5631 assert_eq!(v.math.len(), 1);
5632 let m = &v.math[0];
5633 assert!(m.inline);
5634 assert_eq!((m.start_row, m.end_row), (0, 1));
5635 assert_eq!(m.tex, "x+y");
5636 assert!(!m.display);
5637 assert_eq!(m.src, 4);
5638 let run = v.rows[0]
5639 .runs
5640 .iter()
5641 .find(|r| r.role == "math")
5642 .expect("a math run");
5643 assert_eq!(run.text, "∑");
5644 assert_eq!(run.src, m.src);
5645 }
5646
5647 #[test]
5648 fn a_formula_on_the_caret_line_is_its_tex_and_no_view() {
5649 let d = doc("say $x+y$ here\n\nnext\n");
5650 d.set_inline_pictures(true);
5651 let v = d.set_selection_offsets(0, 0);
5652 assert!(v.math.is_empty());
5653 let text: String = v.rows[0].runs.iter().map(|r| r.text.as_str()).collect();
5654 assert_eq!(text, "say $x+y$ here");
5655 assert!(
5656 v.rows[0]
5657 .runs
5658 .iter()
5659 .any(|r| r.role == "delimiter" && r.text == "$")
5660 );
5661 }
5662
5663 #[test]
5664 fn a_paper_view_reveals_nothing_and_leaves_the_screens_frames_alone() {
5665 let d = doc("say $x+y$ here\n\nnext\n");
5666 d.set_inline_pictures(true);
5667 d.set_incremental_frames(true);
5668 let screen = d.set_selection_offsets(0, 0);
5669 assert!(
5670 screen.math.is_empty(),
5671 "the screen reveals the caret's line"
5672 );
5673
5674 let paper = d.paper_view();
5675 assert_eq!(paper.math.len(), 1, "the page shows the picture");
5676 assert!(paper.rows[0].runs.iter().any(|r| r.role == "math"));
5677 assert_eq!(d.caret_offset(), 0, "and the caret is where it was");
5678
5679 // The screen's chain is unbroken: the next frame is a change from the
5680 // screen's last, and still reveals.
5681 let next = d.set_selection_offsets(1, 1);
5682 assert_eq!(next.basis, screen.frame);
5683 assert!(next.math.is_empty());
5684 }
5685
5686 #[test]
5687 fn a_display_block_is_rows_to_lay_over_and_measured_heights_grow_them() {
5688 let d = doc("$$\n\\int_0^1 x\n$$\n\nend\n");
5689 let v = d.set_selection_offsets(17, 17);
5690 assert_eq!(v.math.len(), 1);
5691 let m = &v.math[0];
5692 assert!(!m.inline);
5693 assert!(m.display);
5694 assert_eq!((m.start_row, m.end_row), (0, 1));
5695 assert_eq!(m.tex, "\n\\int_0^1 x\n");
5696 assert_eq!(m.src, 0);
5697 let after = d.set_math_rows(vec![MathHeight {
5698 tex: m.tex.clone(),
5699 rows: 4,
5700 }]);
5701 assert_eq!((after.math[0].start_row, after.math[0].end_row), (0, 4));
5702 }
5703
5704 #[test]
5705 fn typeset_math_hands_back_a_picture_and_names_a_fault() {
5706 let p = typeset_math("E = mc^2".into(), false, 16.0, 0, 0, 0, 255).unwrap();
5707 assert!(p.svg.starts_with("<svg"));
5708 assert!(p.width > 3.0);
5709 assert!(p.height > 0.5);
5710 assert_eq!(p.depth, 0.0);
5711 match typeset_math("\\frac{".into(), true, 16.0, 0, 0, 0, 255) {
5712 Err(LeafError::Math { message, position }) => {
5713 assert!(!message.is_empty());
5714 assert!(position.is_some());
5715 }
5716 Err(other) => panic!("expected a math error, got {other}"),
5717 Ok(_) => panic!("expected a math error, got a picture"),
5718 }
5719 }
5720
5721 // ── frames as changes ────────────────────────────────────────────────
5722
5723 /// Apply `frame`, a change, to `rows`, the frame it names as its basis —
5724 /// what a frontend does with one; see [`DocView::rows`].
5725 fn apply(rows: &mut Vec<Row>, frame: &DocView) {
5726 let d = leaf_core::RowDelta {
5727 start: frame.row_start as usize,
5728 replaced: frame.replaced as usize,
5729 len: frame.rows.len(),
5730 src_shift: frame.src_shift as i64,
5731 };
5732 leaf_core::apply_row_delta(rows, d, &frame.rows, shift_row);
5733 assert_eq!(rows.len(), frame.row_count as usize);
5734 }
5735
5736 /// A long document with the blocks a design document has, deterministic
5737 /// so a failure reproduces: prose with inline markup, a list, a table,
5738 /// a fence, a formula.
5739 fn long_document() -> String {
5740 use std::fmt::Write as _;
5741 let words = [
5742 "the",
5743 "document",
5744 "carries",
5745 "its",
5746 "own",
5747 "identity",
5748 "and",
5749 "a",
5750 "reference",
5751 "resolves",
5752 "against",
5753 "whatever",
5754 "archive",
5755 "holds",
5756 "it",
5757 ];
5758 let mut out = String::from("# A long document\n\n");
5759 for para in 0..60usize {
5760 if para % 9 == 0 {
5761 let _ = writeln!(out, "## Section {}\n", para / 9 + 1);
5762 }
5763 match para % 11 {
5764 7 => {
5765 for i in 0..4 {
5766 let _ = writeln!(out, "- item {i} with `code_{i}` and *emphasis*");
5767 }
5768 out.push('\n');
5769 }
5770 9 => {
5771 out.push_str("| key | value |\n|---|---|\n| `k` | v |\n| k2 | v2 |\n\n");
5772 }
5773 10 => {
5774 out.push_str("```rust\nfn example() -> u32 {\n 42\n}\n```\n\nA formula $x^2$ inline.\n\n");
5775 }
5776 _ => {
5777 let n = 12 + (para * 7) % 30;
5778 for i in 0..n {
5779 let w = words[(para * 3 + i * 5) % words.len()];
5780 match (para + i) % 13 {
5781 0 => {
5782 let _ = write!(out, "`{w}` ");
5783 }
5784 4 => {
5785 let _ = write!(out, "**{w}** ");
5786 }
5787 8 => {
5788 let _ = write!(out, "[{w}](https://example.org/{para}) ");
5789 }
5790 _ => {
5791 out.push_str(w);
5792 out.push(' ');
5793 }
5794 }
5795 }
5796 out.push_str("\n\n");
5797 }
5798 }
5799 }
5800 out
5801 }
5802
5803 #[test]
5804 fn a_hundred_frames_applied_in_order_are_the_whole_view_after_each() {
5805 // Two documents driven identically: one answering with changes, one
5806 // with whole frames. After every gesture the changes applied so far
5807 // must be the whole frame the other document is at — and, at the
5808 // end, the whole frame the first one gives when asked outright.
5809 let src = long_document();
5810 let a = doc(&src);
5811 let b = doc(&src);
5812 a.set_incremental_frames(true);
5813 let first = a.set_unwrapped();
5814 let _ = b.set_unwrapped();
5815 assert_eq!(first.basis, 0, "the first frame after opting in is whole");
5816 let mut rows = first.rows.clone();
5817 assert!(rows.len() > 100, "{} rows", rows.len());
5818 let mut last_frame = first.frame;
5819
5820 // A small linear congruential generator: enough to spread the
5821 // gestures over the document, and reproducible.
5822 let mut seed: u64 = 0x2545_F491_4F6C_DD1D;
5823 let mut next = move |n: u64| {
5824 seed = seed
5825 .wrapping_mul(6364136223846793005)
5826 .wrapping_add(1442695040888963407);
5827 (seed >> 33) % n
5828 };
5829 for step in 0..100 {
5830 let extend = next(4) == 0;
5831 let (fa, fb) = match next(14) {
5832 0 => (a.insert("x".into()), b.insert("x".into())),
5833 1 => (a.insert("word ".into()), b.insert("word ".into())),
5834 2 => (a.backspace(), b.backspace()),
5835 3 => (a.newline(), b.newline()),
5836 4 => (a.move_left(extend), b.move_left(extend)),
5837 5 => (a.move_right(extend), b.move_right(extend)),
5838 6 => (a.move_up(extend), b.move_up(extend)),
5839 7 => (a.move_down(extend), b.move_down(extend)),
5840 8 => {
5841 let row = next(rows.len() as u64) as u32;
5842 (a.click_ch(row, 2, extend), b.click_ch(row, 2, extend))
5843 }
5844 9 => (a.toggle_bold(), b.toggle_bold()),
5845 10 => (a.undo(), b.undo()),
5846 11 => (a.move_word_right(extend), b.move_word_right(extend)),
5847 12 => (a.delete_word_back(), b.delete_word_back()),
5848 _ => (a.set_heading(2), b.set_heading(2)),
5849 };
5850 assert_eq!(
5851 fa.basis, last_frame,
5852 "step {step}: a change names the frame before"
5853 );
5854 assert_eq!(fa.frame, last_frame + 1);
5855 last_frame = fa.frame;
5856 apply(&mut rows, &fa);
5857 assert!(
5858 rows == fb.rows,
5859 "step {step}: the applied change is not the whole frame"
5860 );
5861 assert_eq!(fa.row_count as usize, fb.rows.len());
5862 assert_eq!((fa.caret_row, fa.caret_ch), (fb.caret_row, fb.caret_ch));
5863 assert_eq!(
5864 (fa.tables.len(), fa.math.len()),
5865 (fb.tables.len(), fb.math.len())
5866 );
5867 }
5868 let whole = a.view();
5869 assert_eq!(whole.basis, 0);
5870 assert!(whole.rows == rows, "view() is the frame the changes built");
5871 // And the next change is against it.
5872 let after = a.move_right(false);
5873 assert_eq!(after.basis, whole.frame);
5874 assert_eq!(a.source(), b.source());
5875 }
5876
5877 #[test]
5878 fn a_caret_move_lifts_no_row_and_a_keystroke_lifts_its_own() {
5879 let d = doc(&long_document());
5880 d.set_incremental_frames(true);
5881 let first = d.set_unwrapped();
5882 let n = first.rows.len();
5883 let mid = (n / 2) as u32;
5884 let moved = d.click_ch(mid, 3, false);
5885 assert_eq!(moved.rows.len(), 0, "a click changed no row");
5886 assert_eq!(moved.replaced, 0);
5887 assert_eq!(moved.row_count as usize, n);
5888 let typed = d.insert("x".into());
5889 assert_eq!(typed.rows.len(), 1, "a keystroke changed its row");
5890 assert_eq!(typed.replaced, 1);
5891 assert_eq!(typed.row_start, moved.caret_row);
5892 assert_eq!(typed.src_shift, 1, "every row below moved one byte");
5893 assert_eq!(typed.row_count as usize, n);
5894 let split = d.newline();
5895 assert_eq!(
5896 split.row_count as usize,
5897 n + 2,
5898 "a paragraph split adds a row and a gap"
5899 );
5900 assert!(split.rows.len() <= 4, "{} rows lifted", split.rows.len());
5901 // A selection over two rows changes those two, and the gap between.
5902 let dragged = d.click_ch(mid - 4, 0, true);
5903 assert!(
5904 dragged.rows.len() <= 6,
5905 "{} rows lifted",
5906 dragged.rows.len()
5907 );
5908 assert_eq!(dragged.src_shift, 0);
5909 }
5910
5911 #[test]
5912 fn frames_are_whole_unless_asked_for_and_view_is_whole_regardless() {
5913 let d = doc("one\n\ntwo\n\nthree\n");
5914 let v = d.set_unwrapped();
5915 assert_eq!((v.frame, v.basis, v.row_start, v.replaced), (1, 0, 0, 0));
5916 assert_eq!(v.row_count as usize, v.rows.len());
5917 let v = d.move_right(false);
5918 assert_eq!(
5919 (v.frame, v.basis),
5920 (2, 0),
5921 "not opted in: whole, and numbered"
5922 );
5923 assert_eq!(v.rows.len(), 5);
5924 d.set_incremental_frames(true);
5925 let v = d.move_right(false);
5926 assert_eq!(v.basis, 0, "the first frame after opting in is whole");
5927 assert_eq!(v.rows.len(), 5);
5928 let v = d.move_right(false);
5929 assert_eq!(v.basis, 3);
5930 assert!(v.rows.is_empty());
5931 let v = d.view();
5932 assert_eq!(v.basis, 0, "view() is whole");
5933 assert_eq!(v.rows.len(), 5);
5934 d.set_incremental_frames(false);
5935 let v = d.move_right(false);
5936 assert_eq!(v.basis, 0);
5937 assert_eq!(v.rows.len(), 5);
5938 }
5939
5940 #[test]
5941 fn a_window_of_rows_is_the_frame_s_rows_clamped() {
5942 let d = doc("one\n\ntwo\n\nthree\n");
5943 let v = d.set_unwrapped();
5944 let window = d.rows(1, 3);
5945 assert_eq!(window, v.rows[1..3].to_vec());
5946 assert_eq!(
5947 d.rows(4, 40),
5948 v.rows[4..].to_vec(),
5949 "clamped to the document"
5950 );
5951 assert!(d.rows(3, 3).is_empty());
5952 assert!(d.rows(3, 1).is_empty());
5953 assert_eq!(d.view().frame, v.frame + 1, "a window is not a frame");
5954 }
5955}