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// CellRange / SelectionKind
188// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
189
190/// A rectangular range of cells within a single table (inclusive bounds).
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct CellRange {
193 pub table_id: usize,
194 pub start_row: usize,
195 pub start_col: usize,
196 pub end_row: usize,
197 pub end_col: usize,
198}
199
200impl CellRange {
201 /// Expand the range so that every merged cell whose span overlaps the
202 /// rectangle is fully included. `cells` is a slice of
203 /// `(row, col, row_span, col_span)` for every cell in the table.
204 ///
205 /// Uses fixed-point iteration (converges in 1-2 rounds for typical tables).
206 pub fn expand_for_spans(mut self, cells: &[(usize, usize, usize, usize)]) -> Self {
207 loop {
208 let mut expanded = false;
209 for &(row, col, rs, cs) in cells {
210 let cell_bottom = row + rs - 1;
211 let cell_right = col + cs - 1;
212 // Check overlap with current range
213 if row <= self.end_row
214 && cell_bottom >= self.start_row
215 && col <= self.end_col
216 && cell_right >= self.start_col
217 {
218 if row < self.start_row {
219 self.start_row = row;
220 expanded = true;
221 }
222 if cell_bottom > self.end_row {
223 self.end_row = cell_bottom;
224 expanded = true;
225 }
226 if col < self.start_col {
227 self.start_col = col;
228 expanded = true;
229 }
230 if cell_right > self.end_col {
231 self.end_col = cell_right;
232 expanded = true;
233 }
234 }
235 }
236 if !expanded {
237 break;
238 }
239 }
240 self
241 }
242}
243
244/// Describes what kind of selection the cursor currently has.
245#[derive(Debug, Clone, PartialEq, Eq)]
246pub enum SelectionKind {
247 /// No selection (position == anchor).
248 None,
249 /// Normal text selection within a single cell or outside any table.
250 Text,
251 /// Rectangular cell selection within a table.
252 Cells(CellRange),
253 /// Selection crosses a table boundary (starts/ends outside the table).
254 /// The table portion is a rectangular cell range; `text_before` /
255 /// `text_after` indicate whether text outside the table is also selected.
256 Mixed {
257 cell_range: CellRange,
258 text_before: bool,
259 text_after: bool,
260 },
261}
262
263// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
264// Table format types
265// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
266
267/// Table-level formatting.
268#[derive(Debug, Clone, Default, PartialEq, Eq)]
269pub struct TableFormat {
270 pub border: Option<i32>,
271 pub cell_spacing: Option<i32>,
272 pub cell_padding: Option<i32>,
273 pub width: Option<i32>,
274 pub alignment: Option<Alignment>,
275}
276
277/// Cell-level formatting.
278#[derive(Debug, Clone, Default, PartialEq, Eq)]
279pub struct CellFormat {
280 pub padding: Option<i32>,
281 pub border: Option<i32>,
282 pub vertical_alignment: Option<CellVerticalAlignment>,
283 pub background_color: Option<String>,
284}
285
286/// Vertical alignment within a table cell.
287#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
288pub enum CellVerticalAlignment {
289 #[default]
290 Top,
291 Middle,
292 Bottom,
293}
294
295// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
296// Table and Cell Snapshots
297// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
298
299/// Consistent snapshot of a table's structure and all cell content.
300#[derive(Debug, Clone, PartialEq)]
301pub struct TableSnapshot {
302 pub table_id: usize,
303 pub rows: usize,
304 pub columns: usize,
305 pub column_widths: Vec<i32>,
306 pub format: TableFormat,
307 pub cells: Vec<CellSnapshot>,
308}
309
310/// Snapshot of one table cell including its block content.
311#[derive(Debug, Clone, PartialEq)]
312pub struct CellSnapshot {
313 pub row: usize,
314 pub column: usize,
315 pub row_span: usize,
316 pub column_span: usize,
317 pub format: CellFormat,
318 pub blocks: Vec<BlockSnapshot>,
319}
320
321// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
322// Flow Snapshots
323// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
324
325/// Consistent snapshot of the entire document flow, captured in a
326/// single lock acquisition.
327#[derive(Debug, Clone, PartialEq)]
328pub struct FlowSnapshot {
329 pub elements: Vec<FlowElementSnapshot>,
330}
331
332/// Snapshot of one flow element.
333// `Block` is by far the most common variant in a document flow, so boxing it
334// to shrink the enum would add a heap allocation on the hot path for no real
335// gain — the large-variant cost only bites the rare `Table`/`Frame` elements.
336#[allow(clippy::large_enum_variant)]
337#[derive(Debug, Clone, PartialEq)]
338pub enum FlowElementSnapshot {
339 Block(BlockSnapshot),
340 Table(TableSnapshot),
341 Frame(FrameSnapshot),
342}
343
344/// Snapshot of a sub-frame and its contents.
345#[derive(Debug, Clone, PartialEq)]
346pub struct FrameSnapshot {
347 pub frame_id: usize,
348 pub format: FrameFormat,
349 pub elements: Vec<FlowElementSnapshot>,
350}
351
352// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
353// FormatChangeKind
354// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
355
356/// What kind of formatting changed.
357#[derive(Debug, Clone, Copy, PartialEq, Eq)]
358pub enum FormatChangeKind {
359 /// Block-level: alignment, margins, indent, heading level.
360 /// Requires paragraph relayout.
361 Block,
362 /// Character-level: font, bold, italic, underline, color.
363 /// Requires reshaping but not necessarily reflow.
364 Character,
365 /// List-level: style, indent, prefix, suffix.
366 /// Requires marker relayout for list items.
367 List,
368}