Skip to main content

leaf_ffi/
lib.rs

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