Skip to main content

text_document/
flow.rs

1//! Flow types for document traversal and layout engine support.
2//!
3//! The layout engine processes [`FlowElement`]s in order to build its layout
4//! tree. Snapshot types capture consistent views for thread-safe reads.
5
6use crate::text_block::TextBlock;
7use crate::text_frame::TextFrame;
8use crate::text_table::TextTable;
9use crate::{Alignment, BlockFormat, FrameFormat, InlineContent, ListStyle, TextFormat};
10
11// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12// FlowElement
13// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
14
15/// An element in the document's visual flow.
16///
17/// The layout engine processes these in order to build its layout tree.
18/// Obtained from [`TextDocument::flow()`](crate::TextDocument::flow) or
19/// [`TextFrame::flow()`].
20#[derive(Clone)]
21pub enum FlowElement {
22    /// A paragraph or heading. Layout as a text block.
23    Block(TextBlock),
24
25    /// A table at this position in the flow. Layout as a grid.
26    /// The anchor frame's `table` field identifies the table entity.
27    Table(TextTable),
28
29    /// A non-table sub-frame (float, sidebar, blockquote).
30    /// Contains its own nested flow, accessible via
31    /// [`TextFrame::flow()`].
32    Frame(TextFrame),
33}
34
35impl FlowElement {
36    /// Snapshot this element into a thread-safe, plain-data representation.
37    ///
38    /// Dispatches to [`TextBlock::snapshot()`], [`TextTable::snapshot()`],
39    /// or [`TextFrame::snapshot()`] as appropriate.
40    pub fn snapshot(&self) -> FlowElementSnapshot {
41        match self {
42            FlowElement::Block(b) => FlowElementSnapshot::Block(b.snapshot()),
43            FlowElement::Table(t) => FlowElementSnapshot::Table(t.snapshot()),
44            FlowElement::Frame(f) => FlowElementSnapshot::Frame(f.snapshot()),
45        }
46    }
47}
48
49// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
50// FragmentContent
51// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
52
53/// A contiguous run of content with uniform formatting within a block.
54///
55/// Offsets are **block-relative**: `offset` is the character position
56/// within the block where this fragment starts (0 = block start).
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum FragmentContent {
59    /// A text run. The layout engine shapes these into glyphs.
60    Text {
61        text: String,
62        format: TextFormat,
63        /// Character offset within the block (block-relative).
64        offset: usize,
65        /// Character count.
66        length: usize,
67        /// Stable synthesized id for the underlying format run
68        /// (see [`synth_element_id`](common::format_runs::synth_element_id)).
69        /// Survives edits that don't delete the run (character insertions
70        /// inside the run keep the same id). Used by accessibility layers
71        /// to build stable `NodeId`s for AccessKit `TextRun` children.
72        element_id: u64,
73        /// Unicode word starts within `text`, expressed as character
74        /// indices (not byte offsets). Computed per UAX #29 via
75        /// `unicode-segmentation`. Fed directly into AccessKit's
76        /// `set_word_starts` on the corresponding `Role::TextRun`.
77        word_starts: Vec<u8>,
78    },
79    /// A footnote reference. The layout engine draws `marker` at this position
80    /// and reserves what those glyphs advance to.
81    ///
82    /// Occupies exactly **one** character of the document however many glyphs
83    /// `marker` shapes to — the same `U+FFFC` an image occupies. The two facts
84    /// are what make the reference atomic to the caret: a layout engine must
85    /// map every glyph of the marker back to this one offset.
86    FootnoteReference {
87        /// Identifies the note. Stable, stored, and never shown.
88        label: String,
89        /// What to draw — a number, usually. **Presentation only**: derived from
90        /// document order, or supplied by the host, and never part of the
91        /// document. Storing it would mean rewriting the author's prose every
92        /// time a note was inserted above this one.
93        marker: String,
94        format: TextFormat,
95        /// Character offset within the block (block-relative).
96        offset: usize,
97        /// Stable synthesized id for the underlying reference anchor.
98        element_id: u64,
99    },
100    /// An inline image. The layout engine reserves space for it.
101    ///
102    /// To retrieve the image pixel data, use the existing
103    /// [`TextDocument::resource(name)`](crate::TextDocument::resource) method.
104    Image {
105        name: String,
106        /// Alternative text describing the image. May be empty.
107        ///
108        /// Carried through to layout so an accessibility layer can name the
109        /// image without a second lookup, in the same way `word_starts` is
110        /// precomputed for text runs.
111        alt: String,
112        width: u32,
113        height: u32,
114        quality: u32,
115        format: TextFormat,
116        /// Character offset within the block (block-relative).
117        offset: usize,
118        /// Stable synthesized id for the underlying image anchor
119        /// (see [`synth_element_id`](common::format_runs::synth_element_id)).
120        element_id: u64,
121    },
122}
123
124// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
125// AddressablePiece
126// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
127
128/// One piece of a block's inline content — a text run, an image, or a footnote
129/// reference — addressed in the document's own **addressable character space**: the
130/// space [`TextDocument::to_addressable_text`](crate::TextDocument::to_addressable_text),
131/// `find_all` match positions, and [`TextBlock::position`] all share.
132///
133/// [`FragmentContent`]'s `offset` is deliberately **block-relative** (0 at the block's own
134/// start) — it feeds the layout engine, which lays out one block at a time and has no use
135/// for a document-wide number. `AddressablePiece` is the other half: `start`/`end` are
136/// **document-relative**, for a caller correlating a piece boundary against something that
137/// *is* addressed in the document's own space — a comment's stored character range, another
138/// block's [`position()`](TextBlock::position), a [`find_all`](crate::TextDocument::find_all)
139/// match. Pairing a block-relative offset with a document-relative string (or the reverse)
140/// is exactly the "offset from one space, string from another" bug
141/// [`to_addressable_text`](crate::TextDocument::to_addressable_text)'s doc comment describes
142/// for whole-document offsets — this type exists so a caller never has to do that
143/// arithmetic, and never gets it wrong, one level down inside a single block.
144///
145/// `end - start` is always `1` for [`InlineContent::Image`] and [`InlineContent::
146/// FootnoteRef`] — the `U+FFFC` sentinel counts as one character, matching how the document
147/// itself counts it — and equals `content`'s own char count for [`InlineContent::Text`].
148///
149/// Obtained from [`TextBlock::addressable_inline_pieces`].
150#[derive(Debug, Clone, PartialEq)]
151pub struct AddressablePiece {
152    /// `[start, end)` in the document's addressable character space.
153    pub start: usize,
154    pub end: usize,
155    pub content: InlineContent,
156    pub format: TextFormat,
157}
158
159// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
160// BlockSnapshot
161// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
162
163/// All layout-relevant data for one block, captured atomically.
164#[derive(Debug, Clone, PartialEq)]
165pub struct BlockSnapshot {
166    pub block_id: usize,
167    pub position: usize,
168    pub length: usize,
169    pub text: String,
170    pub fragments: Vec<FragmentContent>,
171    pub block_format: BlockFormat,
172    pub list_info: Option<ListInfo>,
173    /// Parent frame ID. Needed to know where this block lives in the
174    /// frame tree (e.g. main frame vs. a sub-frame or table cell frame).
175    pub parent_frame_id: Option<usize>,
176    /// If this block is inside a table cell, the cell coordinates.
177    /// Needed so the typesetter can propagate height changes to the
178    /// enclosing table row.
179    pub table_cell: Option<TableCellContext>,
180    /// Paint-only highlight overlay for this block.
181    ///
182    /// Non-empty **only** when the active syntax highlighter is paint-only
183    /// (colors / underline decorations, no metric changes). In that case
184    /// `fragments` carry the *base* formatting (no highlight merge) and the
185    /// layout engine applies these spans as a post-shape recolor — no
186    /// reshaping. When a metric-affecting highlighter is active, highlights
187    /// are merged into `fragments` as usual and this is empty.
188    pub paint_highlights: Vec<PaintHighlightSpan>,
189}
190
191/// A resolved paint-only highlight span for one character range of a block.
192///
193/// Char offsets are block-relative, matching [`HighlightSpan`](crate::HighlightSpan).
194/// Each color field is `None` when the highlight does not override it. This is
195/// the post-shape overlay counterpart of the merged-into-`fragments` path —
196/// it carries only attributes that do not change glyph metrics.
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct PaintHighlightSpan {
199    pub start: usize,
200    pub length: usize,
201    pub foreground_color: Option<crate::Color>,
202    pub background_color: Option<crate::Color>,
203    pub underline_color: Option<crate::Color>,
204    pub underline_style: Option<crate::UnderlineStyle>,
205    pub font_underline: Option<bool>,
206    pub font_overline: Option<bool>,
207    pub font_strikeout: Option<bool>,
208}
209
210/// Snapshot-friendly reference to a table cell (plain IDs, no live handles).
211#[derive(Debug, Clone, PartialEq, Eq)]
212pub struct TableCellContext {
213    pub table_id: usize,
214    pub row: usize,
215    pub column: usize,
216}
217
218// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
219// ListInfo
220// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
221
222/// List membership and marker information for a block.
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct ListInfo {
225    pub list_id: usize,
226    /// The list style (Disc, Decimal, LowerAlpha, etc.).
227    pub style: ListStyle,
228    /// Indentation level.
229    pub indent: u8,
230    /// Pre-formatted marker text: "•", "3.", "(c)", "IV.", etc.
231    pub marker: String,
232    /// 0-based index of this item within its list.
233    pub item_index: usize,
234}
235
236// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
237// TableCellRef
238// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
239
240/// Reference to a table cell that contains a block.
241#[derive(Clone)]
242pub struct TableCellRef {
243    pub table: TextTable,
244    pub row: usize,
245    pub column: usize,
246}
247
248// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
249// FrameRef
250// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
251
252/// Reference to the frame that immediately encloses the cursor's block.
253/// `depth` is the nesting level (1 for a direct child of the root).
254/// `is_blockquote` is true iff `fmt_is_blockquote` is `Some(true)`.
255#[derive(Clone, Debug, PartialEq, Eq)]
256pub struct FrameRef {
257    pub frame_id: usize,
258    pub parent_frame_id: Option<usize>,
259    pub is_blockquote: bool,
260    pub depth: usize,
261}
262
263// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
264// CellRange / SelectionKind
265// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
266
267/// A rectangular range of cells within a single table (inclusive bounds).
268#[derive(Debug, Clone, PartialEq, Eq)]
269pub struct CellRange {
270    pub table_id: usize,
271    pub start_row: usize,
272    pub start_col: usize,
273    pub end_row: usize,
274    pub end_col: usize,
275}
276
277impl CellRange {
278    /// Expand the range so that every merged cell whose span overlaps the
279    /// rectangle is fully included. `cells` is a slice of
280    /// `(row, col, row_span, col_span)` for every cell in the table.
281    ///
282    /// Uses fixed-point iteration (converges in 1-2 rounds for typical tables).
283    pub fn expand_for_spans(mut self, cells: &[(usize, usize, usize, usize)]) -> Self {
284        loop {
285            let mut expanded = false;
286            for &(row, col, rs, cs) in cells {
287                let cell_bottom = row + rs - 1;
288                let cell_right = col + cs - 1;
289                // Check overlap with current range
290                if row <= self.end_row
291                    && cell_bottom >= self.start_row
292                    && col <= self.end_col
293                    && cell_right >= self.start_col
294                {
295                    if row < self.start_row {
296                        self.start_row = row;
297                        expanded = true;
298                    }
299                    if cell_bottom > self.end_row {
300                        self.end_row = cell_bottom;
301                        expanded = true;
302                    }
303                    if col < self.start_col {
304                        self.start_col = col;
305                        expanded = true;
306                    }
307                    if cell_right > self.end_col {
308                        self.end_col = cell_right;
309                        expanded = true;
310                    }
311                }
312            }
313            if !expanded {
314                break;
315            }
316        }
317        self
318    }
319}
320
321/// Describes what kind of selection the cursor currently has.
322#[derive(Debug, Clone, PartialEq, Eq)]
323pub enum SelectionKind {
324    /// No selection (position == anchor).
325    None,
326    /// Normal text selection within a single cell or outside any table.
327    Text,
328    /// Rectangular cell selection within a table.
329    Cells(CellRange),
330    /// Selection crosses a table boundary (starts/ends outside the table).
331    /// The table portion is a rectangular cell range; `text_before` /
332    /// `text_after` indicate whether text outside the table is also selected.
333    Mixed {
334        cell_range: CellRange,
335        text_before: bool,
336        text_after: bool,
337    },
338}
339
340// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
341// Table format types
342// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
343
344/// Table-level formatting.
345#[derive(Debug, Clone, Default, PartialEq, Eq)]
346pub struct TableFormat {
347    pub border: Option<i32>,
348    pub cell_spacing: Option<i32>,
349    pub cell_padding: Option<i32>,
350    pub width: Option<i32>,
351    pub alignment: Option<Alignment>,
352}
353
354/// Cell-level formatting.
355#[derive(Debug, Clone, Default, PartialEq, Eq)]
356pub struct CellFormat {
357    pub padding: Option<i32>,
358    pub border: Option<i32>,
359    pub vertical_alignment: Option<CellVerticalAlignment>,
360    pub background_color: Option<String>,
361}
362
363/// Vertical alignment within a table cell.
364#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
365pub enum CellVerticalAlignment {
366    #[default]
367    Top,
368    Middle,
369    Bottom,
370}
371
372// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
373// Table and Cell Snapshots
374// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
375
376/// Consistent snapshot of a table's structure and all cell content.
377#[derive(Debug, Clone, PartialEq)]
378pub struct TableSnapshot {
379    pub table_id: usize,
380    pub rows: usize,
381    pub columns: usize,
382    pub column_widths: Vec<i32>,
383    pub format: TableFormat,
384    pub cells: Vec<CellSnapshot>,
385}
386
387/// Snapshot of one table cell including its block content.
388#[derive(Debug, Clone, PartialEq)]
389pub struct CellSnapshot {
390    pub row: usize,
391    pub column: usize,
392    pub row_span: usize,
393    pub column_span: usize,
394    pub format: CellFormat,
395    pub blocks: Vec<BlockSnapshot>,
396}
397
398// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
399// Flow Snapshots
400// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
401
402/// Consistent snapshot of the entire document flow, captured in a
403/// single lock acquisition.
404#[derive(Debug, Clone, PartialEq)]
405pub struct FlowSnapshot {
406    pub elements: Vec<FlowElementSnapshot>,
407}
408
409/// Snapshot of one flow element.
410// `Block` is by far the most common variant in a document flow, so boxing it
411// to shrink the enum would add a heap allocation on the hot path for no real
412// gain — the large-variant cost only bites the rare `Table`/`Frame` elements.
413#[allow(clippy::large_enum_variant)]
414#[derive(Debug, Clone, PartialEq)]
415pub enum FlowElementSnapshot {
416    Block(BlockSnapshot),
417    Table(TableSnapshot),
418    Frame(FrameSnapshot),
419}
420
421/// Snapshot of a sub-frame and its contents.
422#[derive(Debug, Clone, PartialEq)]
423pub struct FrameSnapshot {
424    pub frame_id: usize,
425    pub format: FrameFormat,
426    pub elements: Vec<FlowElementSnapshot>,
427}
428
429// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
430// FormatChangeKind
431// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
432
433/// What kind of formatting changed.
434#[derive(Debug, Clone, Copy, PartialEq, Eq)]
435pub enum FormatChangeKind {
436    /// Block-level: alignment, margins, indent, heading level.
437    /// Requires paragraph relayout.
438    Block,
439    /// Character-level: font, bold, italic, underline, color.
440    /// Requires reshaping but not necessarily reflow.
441    Character,
442    /// List-level: style, indent, prefix, suffix.
443    /// Requires marker relayout for list items.
444    List,
445}