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