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.
221pub(crate) fn build_flow_snapshot(
222    inner: &TextDocumentInner,
223    frame_id: EntityId,
224) -> Vec<FlowElementSnapshot> {
225    let frame_dto = match frame_commands::get_frame(&inner.ctx, &frame_id)
226        .ok()
227        .flatten()
228    {
229        Some(f) => f,
230        None => return Vec::new(),
231    };
232
233    if !frame_dto.child_order.is_empty() {
234        snapshot_from_child_order(inner, &frame_dto.child_order)
235    } else {
236        snapshot_fallback(inner, &frame_dto)
237    }
238}
239
240fn snapshot_from_child_order(
241    inner: &TextDocumentInner,
242    child_order: &[i64],
243) -> Vec<FlowElementSnapshot> {
244    let mut elements = Vec::with_capacity(child_order.len());
245
246    for &entry in child_order {
247        if entry > 0 {
248            let block_id = entry as u64;
249            if let Some(snap) = crate::text_block::build_block_snapshot(inner, block_id) {
250                elements.push(FlowElementSnapshot::Block(snap));
251            }
252        } else if entry < 0 {
253            let sub_frame_id = (-entry) as EntityId;
254            if let Some(sub_frame) = frame_commands::get_frame(&inner.ctx, &sub_frame_id)
255                .ok()
256                .flatten()
257            {
258                if let Some(table_id) = sub_frame.table {
259                    if let Some(snap) = build_table_snapshot(inner, table_id) {
260                        elements.push(FlowElementSnapshot::Table(snap));
261                    }
262                } else {
263                    let nested = build_flow_snapshot(inner, sub_frame_id);
264                    elements.push(FlowElementSnapshot::Frame(FrameSnapshot {
265                        frame_id: sub_frame_id as usize,
266                        format: frame_dto_to_format(&sub_frame),
267                        elements: nested,
268                    }));
269                }
270            }
271        }
272    }
273
274    elements
275}
276
277fn snapshot_fallback(
278    inner: &TextDocumentInner,
279    frame_dto: &frontend::frame::dtos::FrameDto,
280) -> Vec<FlowElementSnapshot> {
281    let cell_frame_ids = build_cell_frame_ids(inner);
282
283    let block_ids = &frame_dto.blocks;
284    let mut block_dtos: Vec<_> = block_ids
285        .iter()
286        .filter_map(|&id| {
287            block_commands::get_block(&inner.ctx, &{ id })
288                .ok()
289                .flatten()
290        })
291        .collect();
292    block_dtos.sort_by_key(|b| b.document_position);
293
294    let mut elements: Vec<FlowElementSnapshot> = block_dtos
295        .iter()
296        .filter_map(|b| crate::text_block::build_block_snapshot(inner, b.id))
297        .map(FlowElementSnapshot::Block)
298        .collect();
299
300    let all_frames = frame_commands::get_all_frame(&inner.ctx).unwrap_or_default();
301    for f in &all_frames {
302        if f.id == frame_dto.id {
303            continue;
304        }
305        if cell_frame_ids.contains(&(f.id as EntityId)) {
306            continue;
307        }
308        if f.parent_frame == Some(frame_dto.id) {
309            if let Some(table_id) = f.table {
310                if let Some(snap) = build_table_snapshot(inner, table_id) {
311                    elements.push(FlowElementSnapshot::Table(snap));
312                }
313            } else {
314                let nested = build_flow_snapshot(inner, f.id as EntityId);
315                elements.push(FlowElementSnapshot::Frame(FrameSnapshot {
316                    frame_id: f.id as usize,
317                    format: frame_dto_to_format(f),
318                    elements: nested,
319                }));
320            }
321        }
322    }
323
324    elements
325}
326
327/// Build a TableSnapshot for the given table ID. Called while lock is held.
328pub(crate) fn build_table_snapshot(
329    inner: &TextDocumentInner,
330    table_id: u64,
331) -> Option<TableSnapshot> {
332    let table_dto = table_commands::get_table(&inner.ctx, &table_id)
333        .ok()
334        .flatten()?;
335
336    let mut cells = Vec::new();
337    for &cell_id in &table_dto.cells {
338        if let Some(cell_dto) = table_cell_commands::get_table_cell(&inner.ctx, &{ cell_id })
339            .ok()
340            .flatten()
341        {
342            let blocks = if let Some(cell_frame_id) = cell_dto.cell_frame {
343                crate::text_block::build_blocks_snapshot_for_frame(inner, cell_frame_id)
344            } else {
345                Vec::new()
346            };
347            cells.push(CellSnapshot {
348                row: to_usize(cell_dto.row),
349                column: to_usize(cell_dto.column),
350                row_span: to_usize(cell_dto.row_span),
351                column_span: to_usize(cell_dto.column_span),
352                format: cell_dto_to_format(&cell_dto),
353                blocks,
354            });
355        }
356    }
357
358    Some(TableSnapshot {
359        table_id: table_id as usize,
360        rows: to_usize(table_dto.rows),
361        columns: to_usize(table_dto.columns),
362        column_widths: table_dto.column_widths.iter().map(|&v| v as i32).collect(),
363        format: table_dto_to_format(&table_dto),
364        cells,
365    })
366}
367
368// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
369// DTO → public format conversions
370// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
371
372pub(crate) fn frame_dto_to_format(f: &frontend::frame::dtos::FrameDto) -> FrameFormat {
373    FrameFormat {
374        height: f.fmt_height.map(|v| v as i32),
375        width: f.fmt_width.map(|v| v as i32),
376        top_margin: f.fmt_top_margin.map(|v| v as i32),
377        bottom_margin: f.fmt_bottom_margin.map(|v| v as i32),
378        left_margin: f.fmt_left_margin.map(|v| v as i32),
379        right_margin: f.fmt_right_margin.map(|v| v as i32),
380        padding: f.fmt_padding.map(|v| v as i32),
381        border: f.fmt_border.map(|v| v as i32),
382        position: f.fmt_position.clone(),
383    }
384}
385
386pub(crate) fn table_dto_to_format(t: &frontend::table::dtos::TableDto) -> crate::flow::TableFormat {
387    crate::flow::TableFormat {
388        border: t.fmt_border.map(|v| v as i32),
389        cell_spacing: t.fmt_cell_spacing.map(|v| v as i32),
390        cell_padding: t.fmt_cell_padding.map(|v| v as i32),
391        width: t.fmt_width.map(|v| v as i32),
392        alignment: t.fmt_alignment.clone(),
393    }
394}
395
396pub(crate) fn cell_dto_to_format(
397    c: &frontend::table_cell::dtos::TableCellDto,
398) -> crate::flow::CellFormat {
399    use frontend::common::entities::CellVerticalAlignment as BackendCVA;
400    crate::flow::CellFormat {
401        padding: c.fmt_padding.map(|v| v as i32),
402        border: c.fmt_border.map(|v| v as i32),
403        vertical_alignment: c.fmt_vertical_alignment.as_ref().map(|v| match v {
404            BackendCVA::Top => crate::flow::CellVerticalAlignment::Top,
405            BackendCVA::Middle => crate::flow::CellVerticalAlignment::Middle,
406            BackendCVA::Bottom => crate::flow::CellVerticalAlignment::Bottom,
407        }),
408        background_color: c.fmt_background_color.clone(),
409    }
410}