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