Skip to main content

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