Skip to main content

text_document/
text_frame.rs

1//! Read-only frame handle and shared flow traversal logic.
2
3use std::collections::HashSet;
4use std::sync::Arc;
5
6use parking_lot::Mutex;
7
8use frontend::commands::{block_commands, frame_commands, table_cell_commands, table_commands};
9use frontend::common::types::EntityId;
10
11use crate::FrameFormat;
12use crate::convert::to_usize;
13use crate::flow::{CellSnapshot, FlowElement, FlowElementSnapshot, FrameSnapshot, TableSnapshot};
14use crate::inner::TextDocumentInner;
15use crate::text_block::TextBlock;
16use crate::text_table::TextTable;
17
18/// A read-only handle to a frame in the document.
19///
20/// Obtained from [`FlowElement::Frame`] or [`TextBlock::frame()`].
21#[derive(Clone)]
22pub struct TextFrame {
23    pub(crate) doc: Arc<Mutex<TextDocumentInner>>,
24    pub(crate) frame_id: usize,
25}
26
27impl TextFrame {
28    /// Stable entity ID.
29    pub fn id(&self) -> usize {
30        self.frame_id
31    }
32
33    /// Frame formatting (height, width, margins, padding, border, position).
34    pub fn format(&self) -> FrameFormat {
35        let inner = self.doc.lock();
36        let frame_dto = frame_commands::get_frame(&inner.ctx, &(self.frame_id as EntityId))
37            .ok()
38            .flatten();
39        match frame_dto {
40            Some(f) => frame_dto_to_format(&f),
41            None => FrameFormat::default(),
42        }
43    }
44
45    /// Nested flow within this frame. Same `child_order` traversal as
46    /// [`TextDocument::flow()`](crate::TextDocument::flow).
47    pub fn flow(&self) -> Vec<FlowElement> {
48        let inner = self.doc.lock();
49        build_flow_elements(&inner, &self.doc, self.frame_id as EntityId)
50    }
51
52    /// Snapshot of this frame and all its contents, captured in a single
53    /// lock acquisition. Thread-safe — the returned [`FrameSnapshot`]
54    /// contains only plain data.
55    pub fn snapshot(&self) -> FrameSnapshot {
56        let inner = self.doc.lock();
57        let format = frame_commands::get_frame(&inner.ctx, &(self.frame_id as EntityId))
58            .ok()
59            .flatten()
60            .map(|f| frame_dto_to_format(&f))
61            .unwrap_or_default();
62        let elements = build_flow_snapshot(
63            &inner,
64            self.frame_id as EntityId,
65            crate::highlight::SnapshotHighlights {
66                kind: inner.highlight_kind,
67                mask: &crate::highlight::HighlightMask::ALL,
68                suppress_paint: false,
69            },
70        );
71        FrameSnapshot {
72            frame_id: self.frame_id,
73            format,
74            elements,
75        }
76    }
77}
78
79// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
80// Shared flow traversal (used by TextDocument and TextFrame)
81// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
82
83/// Build flow elements for a frame, returning FlowElement variants.
84///
85/// This is the main entry point. `doc_arc` is the shared document handle
86/// that will be cloned into each returned handle.
87pub(crate) fn build_flow_elements(
88    inner: &TextDocumentInner,
89    doc_arc: &Arc<Mutex<TextDocumentInner>>,
90    frame_id: EntityId,
91) -> Vec<FlowElement> {
92    let frame_dto = match frame_commands::get_frame(&inner.ctx, &frame_id)
93        .ok()
94        .flatten()
95    {
96        Some(f) => f,
97        None => return Vec::new(),
98    };
99
100    if !frame_dto.child_order.is_empty() {
101        flow_from_child_order(inner, doc_arc, &frame_dto.child_order)
102    } else {
103        flow_fallback(inner, doc_arc, &frame_dto)
104    }
105}
106
107/// Build flow from populated `child_order`.
108fn flow_from_child_order(
109    inner: &TextDocumentInner,
110    doc_arc: &Arc<Mutex<TextDocumentInner>>,
111    child_order: &[i64],
112) -> Vec<FlowElement> {
113    let mut elements = Vec::with_capacity(child_order.len());
114
115    for &entry in child_order {
116        if entry > 0 {
117            // Positive: block ID
118            elements.push(FlowElement::Block(TextBlock {
119                doc: Arc::clone(doc_arc),
120                block_id: entry as usize,
121            }));
122        } else if entry < 0 {
123            // Negative: frame ID (negated)
124            let sub_frame_id = (-entry) as EntityId;
125            if let Some(sub_frame) = frame_commands::get_frame(&inner.ctx, &sub_frame_id)
126                .ok()
127                .flatten()
128            {
129                if let Some(table_id) = sub_frame.table {
130                    // Anchor frame for a table
131                    elements.push(FlowElement::Table(TextTable {
132                        doc: Arc::clone(doc_arc),
133                        table_id: table_id as usize,
134                    }));
135                } else {
136                    // Non-table sub-frame
137                    elements.push(FlowElement::Frame(TextFrame {
138                        doc: Arc::clone(doc_arc),
139                        frame_id: sub_frame_id as usize,
140                    }));
141                }
142            }
143        }
144        // entry == 0 is ignored (shouldn't happen)
145    }
146
147    elements
148}
149
150/// Fallback flow: iterate blocks sorted by document_position, skip cell frames.
151fn flow_fallback(
152    inner: &TextDocumentInner,
153    doc_arc: &Arc<Mutex<TextDocumentInner>>,
154    frame_dto: &frontend::frame::dtos::FrameDto,
155) -> Vec<FlowElement> {
156    // Build set of cell frame IDs to skip
157    let cell_frame_ids = build_cell_frame_ids(inner);
158
159    // Get blocks in this frame, sorted by document_position
160    let block_ids = &frame_dto.blocks;
161    let mut block_dtos: Vec<_> = block_ids
162        .iter()
163        .filter_map(|&id| {
164            block_commands::get_block(&inner.ctx, &{ id })
165                .ok()
166                .flatten()
167        })
168        .collect();
169    let store = inner.ctx.db_context.get_store();
170    crate::inner::refresh_block_positions(&mut block_dtos, store);
171    block_dtos.sort_by_key(|b| b.document_position);
172
173    let mut elements: Vec<FlowElement> = block_dtos
174        .iter()
175        .map(|b| {
176            FlowElement::Block(TextBlock {
177                doc: Arc::clone(doc_arc),
178                block_id: b.id as usize,
179            })
180        })
181        .collect();
182
183    // Also check for sub-frames that are children of this frame's document
184    // but not cell frames. In fallback mode, we can't interleave perfectly,
185    // so we append sub-frames after blocks.
186    // For the main frame, get all document frames and check parentage.
187    let all_frames = frame_commands::get_all_frame(&inner.ctx).unwrap_or_default();
188    for f in &all_frames {
189        if f.id == frame_dto.id {
190            continue; // skip self
191        }
192        if cell_frame_ids.contains(&(f.id as EntityId)) {
193            continue; // skip cell frames
194        }
195        // Check if this frame's parent is the current frame
196        if f.parent_frame == Some(frame_dto.id) {
197            if let Some(table_id) = f.table {
198                elements.push(FlowElement::Table(TextTable {
199                    doc: Arc::clone(doc_arc),
200                    table_id: table_id as usize,
201                }));
202            } else {
203                elements.push(FlowElement::Frame(TextFrame {
204                    doc: Arc::clone(doc_arc),
205                    frame_id: f.id as usize,
206                }));
207            }
208        }
209    }
210
211    elements
212}
213
214/// Build a set of all frame IDs that are table cell frames.
215fn build_cell_frame_ids(inner: &TextDocumentInner) -> HashSet<EntityId> {
216    let mut ids = HashSet::new();
217    let all_cells = table_cell_commands::get_all_table_cell(&inner.ctx).unwrap_or_default();
218    for cell in &all_cells {
219        if let Some(frame_id) = cell.cell_frame {
220            ids.insert(frame_id);
221        }
222    }
223    ids
224}
225
226// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
227// Snapshot helpers (called while lock is held)
228// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
229
230/// Build a FlowSnapshot for the given frame. Called while lock is held.
231///
232/// Block positions are computed on-the-fly from child_order + text_length
233/// rather than using stored `document_position` values (which may be stale
234/// after insert_text defers position updates for performance).
235pub(crate) fn build_flow_snapshot(
236    inner: &TextDocumentInner,
237    frame_id: EntityId,
238    hl: crate::highlight::SnapshotHighlights,
239) -> Vec<FlowElementSnapshot> {
240    let frame_dto = match frame_commands::get_frame(&inner.ctx, &frame_id)
241        .ok()
242        .flatten()
243    {
244        Some(f) => f,
245        None => return Vec::new(),
246    };
247
248    if !frame_dto.child_order.is_empty() {
249        let (elements, _) =
250            snapshot_from_child_order(inner, &frame_dto.child_order, 0, frame_id, hl);
251        elements
252    } else {
253        snapshot_fallback(inner, &frame_dto, hl)
254    }
255}
256
257/// Walk child_order, building snapshots with on-the-fly position computation.
258/// `parent_frame_id` is passed down so per-block snapshots skip the
259/// expensive `find_parent_frame` walk over every frame in the store —
260/// a major contributor to per-keystroke editor lag.
261/// Returns (elements, running_position_after_last_block).
262fn snapshot_from_child_order(
263    inner: &TextDocumentInner,
264    child_order: &[i64],
265    start_pos: usize,
266    parent_frame_id: EntityId,
267    hl: crate::highlight::SnapshotHighlights,
268) -> (Vec<FlowElementSnapshot>, usize) {
269    let mut elements = Vec::with_capacity(child_order.len());
270    let mut running_pos = start_pos;
271
272    for &entry in child_order {
273        if entry > 0 {
274            let block_id = entry as u64;
275            if let Some(snap) = crate::text_block::build_block_snapshot_with_position_and_parent(
276                inner,
277                block_id,
278                Some(running_pos),
279                Some(parent_frame_id),
280                hl,
281            ) {
282                running_pos += snap.length + 1; // +1 for block separator
283                elements.push(FlowElementSnapshot::Block(snap));
284            }
285        } else if entry < 0 {
286            let sub_frame_id = (-entry) as EntityId;
287            if let Some(sub_frame) = frame_commands::get_frame(&inner.ctx, &sub_frame_id)
288                .ok()
289                .flatten()
290            {
291                if let Some(table_id) = sub_frame.table {
292                    if let Some((snap, new_pos)) =
293                        build_table_snapshot_with_positions(inner, table_id, running_pos, hl)
294                    {
295                        running_pos = new_pos;
296                        elements.push(FlowElementSnapshot::Table(snap));
297                    }
298                } else {
299                    let (nested, new_pos) = snapshot_from_child_order(
300                        inner,
301                        &sub_frame.child_order,
302                        running_pos,
303                        sub_frame_id,
304                        hl,
305                    );
306                    running_pos = new_pos;
307                    elements.push(FlowElementSnapshot::Frame(FrameSnapshot {
308                        frame_id: sub_frame_id as usize,
309                        format: frame_dto_to_format(&sub_frame),
310                        elements: nested,
311                    }));
312                }
313            }
314        }
315    }
316
317    (elements, running_pos)
318}
319
320fn snapshot_fallback(
321    inner: &TextDocumentInner,
322    frame_dto: &frontend::frame::dtos::FrameDto,
323    hl: crate::highlight::SnapshotHighlights,
324) -> Vec<FlowElementSnapshot> {
325    let cell_frame_ids = build_cell_frame_ids(inner);
326
327    let block_ids = &frame_dto.blocks;
328    let mut block_dtos: Vec<_> = block_ids
329        .iter()
330        .filter_map(|&id| {
331            block_commands::get_block(&inner.ctx, &{ id })
332                .ok()
333                .flatten()
334        })
335        .collect();
336    let store = inner.ctx.db_context.get_store();
337    crate::inner::refresh_block_positions(&mut block_dtos, store);
338    block_dtos.sort_by_key(|b| b.document_position);
339
340    let mut elements: Vec<FlowElementSnapshot> = block_dtos
341        .iter()
342        .filter_map(|b| crate::text_block::build_block_snapshot(inner, b.id, hl))
343        .map(FlowElementSnapshot::Block)
344        .collect();
345
346    let all_frames = frame_commands::get_all_frame(&inner.ctx).unwrap_or_default();
347    for f in &all_frames {
348        if f.id == frame_dto.id {
349            continue;
350        }
351        if cell_frame_ids.contains(&(f.id as EntityId)) {
352            continue;
353        }
354        if f.parent_frame == Some(frame_dto.id) {
355            if let Some(table_id) = f.table {
356                if let Some(snap) = build_table_snapshot(inner, table_id, hl) {
357                    elements.push(FlowElementSnapshot::Table(snap));
358                }
359            } else {
360                let nested = build_flow_snapshot(inner, f.id as EntityId, hl);
361                elements.push(FlowElementSnapshot::Frame(FrameSnapshot {
362                    frame_id: f.id as usize,
363                    format: frame_dto_to_format(f),
364                    elements: nested,
365                }));
366            }
367        }
368    }
369
370    elements
371}
372
373/// Build a TableSnapshot for the given table ID. Called while lock is held.
374pub(crate) fn build_table_snapshot(
375    inner: &TextDocumentInner,
376    table_id: u64,
377    hl: crate::highlight::SnapshotHighlights,
378) -> Option<TableSnapshot> {
379    let table_dto = table_commands::get_table(&inner.ctx, &table_id)
380        .ok()
381        .flatten()?;
382
383    let mut cells = Vec::new();
384    for &cell_id in &table_dto.cells {
385        if let Some(cell_dto) = table_cell_commands::get_table_cell(&inner.ctx, &{ cell_id })
386            .ok()
387            .flatten()
388        {
389            let blocks = if let Some(cell_frame_id) = cell_dto.cell_frame {
390                crate::text_block::build_blocks_snapshot_for_frame(inner, cell_frame_id, hl)
391            } else {
392                Vec::new()
393            };
394            cells.push(CellSnapshot {
395                row: to_usize(cell_dto.row),
396                column: to_usize(cell_dto.column),
397                row_span: to_usize(cell_dto.row_span),
398                column_span: to_usize(cell_dto.column_span),
399                format: cell_dto_to_format(&cell_dto),
400                blocks,
401            });
402        }
403    }
404
405    Some(TableSnapshot {
406        table_id: table_id as usize,
407        rows: to_usize(table_dto.rows),
408        columns: to_usize(table_dto.columns),
409        column_widths: table_dto.column_widths.iter().map(|&v| v as i32).collect(),
410        format: table_dto_to_format(&table_dto),
411        cells,
412    })
413}
414
415/// Build a TableSnapshot with computed positions for cell blocks, starting from
416/// `start_pos`. Returns `(snapshot, running_pos_after_last_cell_block)`.
417///
418/// Cells are processed in row-major order, and block positions within each cell
419/// are computed sequentially — matching `find_block_at_position_sequential` in
420/// the editing use cases.
421fn build_table_snapshot_with_positions(
422    inner: &TextDocumentInner,
423    table_id: u64,
424    start_pos: usize,
425    hl: crate::highlight::SnapshotHighlights,
426) -> Option<(TableSnapshot, usize)> {
427    let table_dto = table_commands::get_table(&inner.ctx, &table_id)
428        .ok()
429        .flatten()?;
430
431    // Collect and sort cell DTOs in row-major order
432    let mut cell_dtos: Vec<_> = table_dto
433        .cells
434        .iter()
435        .filter_map(|&cell_id| {
436            table_cell_commands::get_table_cell(&inner.ctx, &{ cell_id })
437                .ok()
438                .flatten()
439        })
440        .collect();
441    cell_dtos.sort_by(|a, b| a.row.cmp(&b.row).then(a.column.cmp(&b.column)));
442
443    let mut running_pos = start_pos;
444    let mut cells = Vec::with_capacity(cell_dtos.len());
445    for cell_dto in &cell_dtos {
446        let blocks = if let Some(cell_frame_id) = cell_dto.cell_frame {
447            let (snaps, new_pos) =
448                crate::text_block::build_blocks_snapshot_for_frame_with_positions(
449                    inner,
450                    cell_frame_id,
451                    running_pos,
452                    hl,
453                );
454            running_pos = new_pos;
455            snaps
456        } else {
457            Vec::new()
458        };
459        cells.push(CellSnapshot {
460            row: to_usize(cell_dto.row),
461            column: to_usize(cell_dto.column),
462            row_span: to_usize(cell_dto.row_span),
463            column_span: to_usize(cell_dto.column_span),
464            format: cell_dto_to_format(cell_dto),
465            blocks,
466        });
467    }
468
469    Some((
470        TableSnapshot {
471            table_id: table_id as usize,
472            rows: to_usize(table_dto.rows),
473            columns: to_usize(table_dto.columns),
474            column_widths: table_dto.column_widths.iter().map(|&v| v as i32).collect(),
475            format: table_dto_to_format(&table_dto),
476            cells,
477        },
478        running_pos,
479    ))
480}
481
482// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
483// DTO → public format conversions
484// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
485
486pub(crate) fn frame_dto_to_format(f: &frontend::frame::dtos::FrameDto) -> FrameFormat {
487    FrameFormat {
488        height: f.fmt_height.map(|v| v as i32),
489        width: f.fmt_width.map(|v| v as i32),
490        top_margin: f.fmt_top_margin.map(|v| v as i32),
491        bottom_margin: f.fmt_bottom_margin.map(|v| v as i32),
492        left_margin: f.fmt_left_margin.map(|v| v as i32),
493        right_margin: f.fmt_right_margin.map(|v| v as i32),
494        padding: f.fmt_padding.map(|v| v as i32),
495        border: f.fmt_border.map(|v| v as i32),
496        position: f.fmt_position.clone(),
497        is_blockquote: f.fmt_is_blockquote,
498    }
499}
500
501pub(crate) fn table_dto_to_format(t: &frontend::table::dtos::TableDto) -> crate::flow::TableFormat {
502    crate::flow::TableFormat {
503        border: t.fmt_border.map(|v| v as i32),
504        cell_spacing: t.fmt_cell_spacing.map(|v| v as i32),
505        cell_padding: t.fmt_cell_padding.map(|v| v as i32),
506        width: t.fmt_width.map(|v| v as i32),
507        alignment: t.fmt_alignment.clone(),
508    }
509}
510
511pub(crate) fn cell_dto_to_format(
512    c: &frontend::table_cell::dtos::TableCellDto,
513) -> crate::flow::CellFormat {
514    use frontend::common::entities::CellVerticalAlignment as BackendCVA;
515    crate::flow::CellFormat {
516        padding: c.fmt_padding.map(|v| v as i32),
517        border: c.fmt_border.map(|v| v as i32),
518        vertical_alignment: c.fmt_vertical_alignment.as_ref().map(|v| match v {
519            BackendCVA::Top => crate::flow::CellVerticalAlignment::Top,
520            BackendCVA::Middle => crate::flow::CellVerticalAlignment::Middle,
521            BackendCVA::Bottom => crate::flow::CellVerticalAlignment::Bottom,
522        }),
523        background_color: c.fmt_background_color.clone(),
524    }
525}