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, 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    /// An inline image. The layout engine reserves space for it.
80    ///
81    /// To retrieve the image pixel data, use the existing
82    /// [`TextDocument::resource(name)`](crate::TextDocument::resource) method.
83    Image {
84        name: String,
85        width: u32,
86        height: u32,
87        quality: u32,
88        format: TextFormat,
89        /// Character offset within the block (block-relative).
90        offset: usize,
91        /// Stable synthesized id for the underlying image anchor
92        /// (see [`synth_element_id`](common::format_runs::synth_element_id)).
93        element_id: u64,
94    },
95}
96
97// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
98// BlockSnapshot
99// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
100
101/// All layout-relevant data for one block, captured atomically.
102#[derive(Debug, Clone, PartialEq)]
103pub struct BlockSnapshot {
104    pub block_id: usize,
105    pub position: usize,
106    pub length: usize,
107    pub text: String,
108    pub fragments: Vec<FragmentContent>,
109    pub block_format: BlockFormat,
110    pub list_info: Option<ListInfo>,
111    /// Parent frame ID. Needed to know where this block lives in the
112    /// frame tree (e.g. main frame vs. a sub-frame or table cell frame).
113    pub parent_frame_id: Option<usize>,
114    /// If this block is inside a table cell, the cell coordinates.
115    /// Needed so the typesetter can propagate height changes to the
116    /// enclosing table row.
117    pub table_cell: Option<TableCellContext>,
118    /// Paint-only highlight overlay for this block.
119    ///
120    /// Non-empty **only** when the active syntax highlighter is paint-only
121    /// (colors / underline decorations, no metric changes). In that case
122    /// `fragments` carry the *base* formatting (no highlight merge) and the
123    /// layout engine applies these spans as a post-shape recolor — no
124    /// reshaping. When a metric-affecting highlighter is active, highlights
125    /// are merged into `fragments` as usual and this is empty.
126    pub paint_highlights: Vec<PaintHighlightSpan>,
127}
128
129/// A resolved paint-only highlight span for one character range of a block.
130///
131/// Char offsets are block-relative, matching [`HighlightSpan`](crate::HighlightSpan).
132/// Each color field is `None` when the highlight does not override it. This is
133/// the post-shape overlay counterpart of the merged-into-`fragments` path —
134/// it carries only attributes that do not change glyph metrics.
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct PaintHighlightSpan {
137    pub start: usize,
138    pub length: usize,
139    pub foreground_color: Option<crate::Color>,
140    pub background_color: Option<crate::Color>,
141    pub underline_color: Option<crate::Color>,
142    pub underline_style: Option<crate::UnderlineStyle>,
143    pub font_underline: Option<bool>,
144    pub font_overline: Option<bool>,
145    pub font_strikeout: Option<bool>,
146}
147
148/// Snapshot-friendly reference to a table cell (plain IDs, no live handles).
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct TableCellContext {
151    pub table_id: usize,
152    pub row: usize,
153    pub column: usize,
154}
155
156// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
157// ListInfo
158// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
159
160/// List membership and marker information for a block.
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct ListInfo {
163    pub list_id: usize,
164    /// The list style (Disc, Decimal, LowerAlpha, etc.).
165    pub style: ListStyle,
166    /// Indentation level.
167    pub indent: u8,
168    /// Pre-formatted marker text: "•", "3.", "(c)", "IV.", etc.
169    pub marker: String,
170    /// 0-based index of this item within its list.
171    pub item_index: usize,
172}
173
174// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
175// TableCellRef
176// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
177
178/// Reference to a table cell that contains a block.
179#[derive(Clone)]
180pub struct TableCellRef {
181    pub table: TextTable,
182    pub row: usize,
183    pub column: usize,
184}
185
186// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
187// FrameRef
188// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
189
190/// Reference to the frame that immediately encloses the cursor's block.
191/// `depth` is the nesting level (1 for a direct child of the root).
192/// `is_blockquote` is true iff `fmt_is_blockquote` is `Some(true)`.
193#[derive(Clone, Debug, PartialEq, Eq)]
194pub struct FrameRef {
195    pub frame_id: usize,
196    pub parent_frame_id: Option<usize>,
197    pub is_blockquote: bool,
198    pub depth: usize,
199}
200
201// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
202// CellRange / SelectionKind
203// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
204
205/// A rectangular range of cells within a single table (inclusive bounds).
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct CellRange {
208    pub table_id: usize,
209    pub start_row: usize,
210    pub start_col: usize,
211    pub end_row: usize,
212    pub end_col: usize,
213}
214
215impl CellRange {
216    /// Expand the range so that every merged cell whose span overlaps the
217    /// rectangle is fully included. `cells` is a slice of
218    /// `(row, col, row_span, col_span)` for every cell in the table.
219    ///
220    /// Uses fixed-point iteration (converges in 1-2 rounds for typical tables).
221    pub fn expand_for_spans(mut self, cells: &[(usize, usize, usize, usize)]) -> Self {
222        loop {
223            let mut expanded = false;
224            for &(row, col, rs, cs) in cells {
225                let cell_bottom = row + rs - 1;
226                let cell_right = col + cs - 1;
227                // Check overlap with current range
228                if row <= self.end_row
229                    && cell_bottom >= self.start_row
230                    && col <= self.end_col
231                    && cell_right >= self.start_col
232                {
233                    if row < self.start_row {
234                        self.start_row = row;
235                        expanded = true;
236                    }
237                    if cell_bottom > self.end_row {
238                        self.end_row = cell_bottom;
239                        expanded = true;
240                    }
241                    if col < self.start_col {
242                        self.start_col = col;
243                        expanded = true;
244                    }
245                    if cell_right > self.end_col {
246                        self.end_col = cell_right;
247                        expanded = true;
248                    }
249                }
250            }
251            if !expanded {
252                break;
253            }
254        }
255        self
256    }
257}
258
259/// Describes what kind of selection the cursor currently has.
260#[derive(Debug, Clone, PartialEq, Eq)]
261pub enum SelectionKind {
262    /// No selection (position == anchor).
263    None,
264    /// Normal text selection within a single cell or outside any table.
265    Text,
266    /// Rectangular cell selection within a table.
267    Cells(CellRange),
268    /// Selection crosses a table boundary (starts/ends outside the table).
269    /// The table portion is a rectangular cell range; `text_before` /
270    /// `text_after` indicate whether text outside the table is also selected.
271    Mixed {
272        cell_range: CellRange,
273        text_before: bool,
274        text_after: bool,
275    },
276}
277
278// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
279// Table format types
280// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
281
282/// Table-level formatting.
283#[derive(Debug, Clone, Default, PartialEq, Eq)]
284pub struct TableFormat {
285    pub border: Option<i32>,
286    pub cell_spacing: Option<i32>,
287    pub cell_padding: Option<i32>,
288    pub width: Option<i32>,
289    pub alignment: Option<Alignment>,
290}
291
292/// Cell-level formatting.
293#[derive(Debug, Clone, Default, PartialEq, Eq)]
294pub struct CellFormat {
295    pub padding: Option<i32>,
296    pub border: Option<i32>,
297    pub vertical_alignment: Option<CellVerticalAlignment>,
298    pub background_color: Option<String>,
299}
300
301/// Vertical alignment within a table cell.
302#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
303pub enum CellVerticalAlignment {
304    #[default]
305    Top,
306    Middle,
307    Bottom,
308}
309
310// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
311// Table and Cell Snapshots
312// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
313
314/// Consistent snapshot of a table's structure and all cell content.
315#[derive(Debug, Clone, PartialEq)]
316pub struct TableSnapshot {
317    pub table_id: usize,
318    pub rows: usize,
319    pub columns: usize,
320    pub column_widths: Vec<i32>,
321    pub format: TableFormat,
322    pub cells: Vec<CellSnapshot>,
323}
324
325/// Snapshot of one table cell including its block content.
326#[derive(Debug, Clone, PartialEq)]
327pub struct CellSnapshot {
328    pub row: usize,
329    pub column: usize,
330    pub row_span: usize,
331    pub column_span: usize,
332    pub format: CellFormat,
333    pub blocks: Vec<BlockSnapshot>,
334}
335
336// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
337// Flow Snapshots
338// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
339
340/// Consistent snapshot of the entire document flow, captured in a
341/// single lock acquisition.
342#[derive(Debug, Clone, PartialEq)]
343pub struct FlowSnapshot {
344    pub elements: Vec<FlowElementSnapshot>,
345}
346
347/// Snapshot of one flow element.
348// `Block` is by far the most common variant in a document flow, so boxing it
349// to shrink the enum would add a heap allocation on the hot path for no real
350// gain — the large-variant cost only bites the rare `Table`/`Frame` elements.
351#[allow(clippy::large_enum_variant)]
352#[derive(Debug, Clone, PartialEq)]
353pub enum FlowElementSnapshot {
354    Block(BlockSnapshot),
355    Table(TableSnapshot),
356    Frame(FrameSnapshot),
357}
358
359/// Snapshot of a sub-frame and its contents.
360#[derive(Debug, Clone, PartialEq)]
361pub struct FrameSnapshot {
362    pub frame_id: usize,
363    pub format: FrameFormat,
364    pub elements: Vec<FlowElementSnapshot>,
365}
366
367// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
368// FormatChangeKind
369// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
370
371/// What kind of formatting changed.
372#[derive(Debug, Clone, Copy, PartialEq, Eq)]
373pub enum FormatChangeKind {
374    /// Block-level: alignment, margins, indent, heading level.
375    /// Requires paragraph relayout.
376    Block,
377    /// Character-level: font, bold, italic, underline, color.
378    /// Requires reshaping but not necessarily reflow.
379    Character,
380    /// List-level: style, indent, prefix, suffix.
381    /// Requires marker relayout for list items.
382    List,
383}