Skip to main content

leaf_ffi/
lib.rs

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