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(&inner, self.frame_id as EntityId);
63        FrameSnapshot {
64            frame_id: self.frame_id,
65            format,
66            elements,
67        }
68    }
69}
70
71// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
72// Shared flow traversal (used by TextDocument and TextFrame)
73// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
74
75/// Build flow elements for a frame, returning FlowElement variants.
76///
77/// This is the main entry point. `doc_arc` is the shared document handle
78/// that will be cloned into each returned handle.
79pub(crate) fn build_flow_elements(
80    inner: &TextDocumentInner,
81    doc_arc: &Arc<Mutex<TextDocumentInner>>,
82    frame_id: EntityId,
83) -> Vec<FlowElement> {
84    let frame_dto = match frame_commands::get_frame(&inner.ctx, &frame_id)
85        .ok()
86        .flatten()
87    {
88        Some(f) => f,
89        None => return Vec::new(),
90    };
91
92    if !frame_dto.child_order.is_empty() {
93        flow_from_child_order(inner, doc_arc, &frame_dto.child_order)
94    } else {
95        flow_fallback(inner, doc_arc, &frame_dto)
96    }
97}
98
99/// Build flow from populated `child_order`.
100fn flow_from_child_order(
101    inner: &TextDocumentInner,
102    doc_arc: &Arc<Mutex<TextDocumentInner>>,
103    child_order: &[i64],
104) -> Vec<FlowElement> {
105    let mut elements = Vec::with_capacity(child_order.len());
106
107    for &entry in child_order {
108        if entry > 0 {
109            // Positive: block ID
110            elements.push(FlowElement::Block(TextBlock {
111                doc: Arc::clone(doc_arc),
112                block_id: entry as usize,
113            }));
114        } else if entry < 0 {
115            // Negative: frame ID (negated)
116            let sub_frame_id = (-entry) as EntityId;
117            if let Some(sub_frame) = frame_commands::get_frame(&inner.ctx, &sub_frame_id)
118                .ok()
119                .flatten()
120            {
121                if let Some(table_id) = sub_frame.table {
122                    // Anchor frame for a table
123                    elements.push(FlowElement::Table(TextTable {
124                        doc: Arc::clone(doc_arc),
125                        table_id: table_id as usize,
126                    }));
127                } else {
128                    // Non-table sub-frame
129                    elements.push(FlowElement::Frame(TextFrame {
130                        doc: Arc::clone(doc_arc),
131                        frame_id: sub_frame_id as usize,
132                    }));
133                }
134            }
135        }
136        // entry == 0 is ignored (shouldn't happen)
137    }
138
139    elements
140}
141
142/// Fallback flow: iterate blocks sorted by document_position, skip cell frames.
143fn flow_fallback(
144    inner: &TextDocumentInner,
145    doc_arc: &Arc<Mutex<TextDocumentInner>>,
146    frame_dto: &frontend::frame::dtos::FrameDto,
147) -> Vec<FlowElement> {
148    // Build set of cell frame IDs to skip
149    let cell_frame_ids = build_cell_frame_ids(inner);
150
151    // Get blocks in this frame, sorted by document_position
152    let block_ids = &frame_dto.blocks;
153    let mut block_dtos: Vec<_> = block_ids
154        .iter()
155        .filter_map(|&id| {
156            block_commands::get_block(&inner.ctx, &{ id })
157                .ok()
158                .flatten()
159        })
160        .collect();
161    block_dtos.sort_by_key(|b| b.document_position);
162
163    let mut elements: Vec<FlowElement> = block_dtos
164        .iter()
165        .map(|b| {
166            FlowElement::Block(TextBlock {
167                doc: Arc::clone(doc_arc),
168                block_id: b.id as usize,
169            })
170        })
171        .collect();
172
173    // Also check for sub-frames that are children of this frame's document
174    // but not cell frames. In fallback mode, we can't interleave perfectly,
175    // so we append sub-frames after blocks.
176    // For the main frame, get all document frames and check parentage.
177    let all_frames = frame_commands::get_all_frame(&inner.ctx).unwrap_or_default();
178    for f in &all_frames {
179        if f.id == frame_dto.id {
180            continue; // skip self
181        }
182        if cell_frame_ids.contains(&(f.id as EntityId)) {
183            continue; // skip cell frames
184        }
185        // Check if this frame's parent is the current frame
186        if f.parent_frame == Some(frame_dto.id) {
187            if let Some(table_id) = f.table {
188                elements.push(FlowElement::Table(TextTable {
189                    doc: Arc::clone(doc_arc),
190                    table_id: table_id as usize,
191                }));
192            } else {
193                elements.push(FlowElement::Frame(TextFrame {
194                    doc: Arc::clone(doc_arc),
195                    frame_id: f.id as usize,
196                }));
197            }
198        }
199    }
200
201    elements
202}
203
204/// Build a set of all frame IDs that are table cell frames.
205fn build_cell_frame_ids(inner: &TextDocumentInner) -> HashSet<EntityId> {
206    let mut ids = HashSet::new();
207    let all_cells = table_cell_commands::get_all_table_cell(&inner.ctx).unwrap_or_default();
208    for cell in &all_cells {
209        if let Some(frame_id) = cell.cell_frame {
210            ids.insert(frame_id);
211        }
212    }
213    ids
214}
215
216// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
217// Snapshot helpers (called while lock is held)
218// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
219
220/// Build a FlowSnapshot for the given frame. Called while lock is held.
221///
222/// Block positions are computed on-the-fly from child_order + text_length
223/// rather than using stored `document_position` values (which may be stale
224/// after insert_text defers position updates for performance).
225pub(crate) fn build_flow_snapshot(
226    inner: &TextDocumentInner,
227    frame_id: EntityId,
228) -> Vec<FlowElementSnapshot> {
229    let frame_dto = match frame_commands::get_frame(&inner.ctx, &frame_id)
230        .ok()
231        .flatten()
232    {
233        Some(f) => f,
234        None => return Vec::new(),
235    };
236
237    if !frame_dto.child_order.is_empty() {
238        let (elements, _) = snapshot_from_child_order(inner, &frame_dto.child_order, 0);
239        elements
240    } else {
241        snapshot_fallback(inner, &frame_dto)
242    }
243}
244
245/// Walk child_order, building snapshots with on-the-fly position computation.
246/// Returns (elements, running_position_after_last_block).
247fn snapshot_from_child_order(
248    inner: &TextDocumentInner,
249    child_order: &[i64],
250    start_pos: usize,
251) -> (Vec<FlowElementSnapshot>, usize) {
252    let mut elements = Vec::with_capacity(child_order.len());
253    let mut running_pos = start_pos;
254
255    for &entry in child_order {
256        if entry > 0 {
257            let block_id = entry as u64;
258            if let Some(snap) = crate::text_block::build_block_snapshot_with_position(
259                inner,
260                block_id,
261                Some(running_pos),
262            ) {
263                running_pos += snap.length + 1; // +1 for block separator
264                elements.push(FlowElementSnapshot::Block(snap));
265            }
266        } else if entry < 0 {
267            let sub_frame_id = (-entry) as EntityId;
268            if let Some(sub_frame) = frame_commands::get_frame(&inner.ctx, &sub_frame_id)
269                .ok()
270                .flatten()
271            {
272                if let Some(table_id) = sub_frame.table {
273                    if let Some(snap) = build_table_snapshot(inner, table_id) {
274                        // Table cells have their own position spaces — don't advance running_pos
275                        // for block separators here; the table occupies positions based on its
276                        // content. For now, treat the table as opaque.
277                        elements.push(FlowElementSnapshot::Table(snap));
278                    }
279                } else {
280                    let (nested, new_pos) =
281                        snapshot_from_child_order(inner, &sub_frame.child_order, running_pos);
282                    running_pos = new_pos;
283                    elements.push(FlowElementSnapshot::Frame(FrameSnapshot {
284                        frame_id: sub_frame_id as usize,
285                        format: frame_dto_to_format(&sub_frame),
286                        elements: nested,
287                    }));
288                }
289            }
290        }
291    }
292
293    (elements, running_pos)
294}
295
296fn snapshot_fallback(
297    inner: &TextDocumentInner,
298    frame_dto: &frontend::frame::dtos::FrameDto,
299) -> Vec<FlowElementSnapshot> {
300    let cell_frame_ids = build_cell_frame_ids(inner);
301
302    let block_ids = &frame_dto.blocks;
303    let mut block_dtos: Vec<_> = block_ids
304        .iter()
305        .filter_map(|&id| {
306            block_commands::get_block(&inner.ctx, &{ id })
307                .ok()
308                .flatten()
309        })
310        .collect();
311    block_dtos.sort_by_key(|b| b.document_position);
312
313    let mut elements: Vec<FlowElementSnapshot> = block_dtos
314        .iter()
315        .filter_map(|b| crate::text_block::build_block_snapshot(inner, b.id))
316        .map(FlowElementSnapshot::Block)
317        .collect();
318
319    let all_frames = frame_commands::get_all_frame(&inner.ctx).unwrap_or_default();
320    for f in &all_frames {
321        if f.id == frame_dto.id {
322            continue;
323        }
324        if cell_frame_ids.contains(&(f.id as EntityId)) {
325            continue;
326        }
327        if f.parent_frame == Some(frame_dto.id) {
328            if let Some(table_id) = f.table {
329                if let Some(snap) = build_table_snapshot(inner, table_id) {
330                    elements.push(FlowElementSnapshot::Table(snap));
331                }
332            } else {
333                let nested = build_flow_snapshot(inner, f.id as EntityId);
334                elements.push(FlowElementSnapshot::Frame(FrameSnapshot {
335                    frame_id: f.id as usize,
336                    format: frame_dto_to_format(f),
337                    elements: nested,
338                }));
339            }
340        }
341    }
342
343    elements
344}
345
346/// Build a TableSnapshot for the given table ID. Called while lock is held.
347pub(crate) fn build_table_snapshot(
348    inner: &TextDocumentInner,
349    table_id: u64,
350) -> Option<TableSnapshot> {
351    let table_dto = table_commands::get_table(&inner.ctx, &table_id)
352        .ok()
353        .flatten()?;
354
355    let mut cells = Vec::new();
356    for &cell_id in &table_dto.cells {
357        if let Some(cell_dto) = table_cell_commands::get_table_cell(&inner.ctx, &{ cell_id })
358            .ok()
359            .flatten()
360        {
361            let blocks = if let Some(cell_frame_id) = cell_dto.cell_frame {
362                crate::text_block::build_blocks_snapshot_for_frame(inner, cell_frame_id)
363            } else {
364                Vec::new()
365            };
366            cells.push(CellSnapshot {
367                row: to_usize(cell_dto.row),
368                column: to_usize(cell_dto.column),
369                row_span: to_usize(cell_dto.row_span),
370                column_span: to_usize(cell_dto.column_span),
371                format: cell_dto_to_format(&cell_dto),
372                blocks,
373            });
374        }
375    }
376
377    Some(TableSnapshot {
378        table_id: table_id as usize,
379        rows: to_usize(table_dto.rows),
380        columns: to_usize(table_dto.columns),
381        column_widths: table_dto.column_widths.iter().map(|&v| v as i32).collect(),
382        format: table_dto_to_format(&table_dto),
383        cells,
384    })
385}
386
387// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
388// DTO → public format conversions
389// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
390
391pub(crate) fn frame_dto_to_format(f: &frontend::frame::dtos::FrameDto) -> FrameFormat {
392    FrameFormat {
393        height: f.fmt_height.map(|v| v as i32),
394        width: f.fmt_width.map(|v| v as i32),
395        top_margin: f.fmt_top_margin.map(|v| v as i32),
396        bottom_margin: f.fmt_bottom_margin.map(|v| v as i32),
397        left_margin: f.fmt_left_margin.map(|v| v as i32),
398        right_margin: f.fmt_right_margin.map(|v| v as i32),
399        padding: f.fmt_padding.map(|v| v as i32),
400        border: f.fmt_border.map(|v| v as i32),
401        position: f.fmt_position.clone(),
402        is_blockquote: f.fmt_is_blockquote,
403    }
404}
405
406pub(crate) fn table_dto_to_format(t: &frontend::table::dtos::TableDto) -> crate::flow::TableFormat {
407    crate::flow::TableFormat {
408        border: t.fmt_border.map(|v| v as i32),
409        cell_spacing: t.fmt_cell_spacing.map(|v| v as i32),
410        cell_padding: t.fmt_cell_padding.map(|v| v as i32),
411        width: t.fmt_width.map(|v| v as i32),
412        alignment: t.fmt_alignment.clone(),
413    }
414}
415
416pub(crate) fn cell_dto_to_format(
417    c: &frontend::table_cell::dtos::TableCellDto,
418) -> crate::flow::CellFormat {
419    use frontend::common::entities::CellVerticalAlignment as BackendCVA;
420    crate::flow::CellFormat {
421        padding: c.fmt_padding.map(|v| v as i32),
422        border: c.fmt_border.map(|v| v as i32),
423        vertical_alignment: c.fmt_vertical_alignment.as_ref().map(|v| match v {
424            BackendCVA::Top => crate::flow::CellVerticalAlignment::Top,
425            BackendCVA::Middle => crate::flow::CellVerticalAlignment::Middle,
426            BackendCVA::Bottom => crate::flow::CellVerticalAlignment::Bottom,
427        }),
428        background_color: c.fmt_background_color.clone(),
429    }
430}