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