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