Skip to main content

text_typeset/layout/
frame.rs

1use crate::font::registry::FontRegistry;
2use crate::layout::block::layout_block;
3use crate::layout::block::{BlockLayout, BlockLayoutParams};
4use crate::layout::table::{TableLayout, TableLayoutParams, layout_table};
5
6/// Frame position type (from text-document's FramePosition).
7///
8/// **Note on floats**: `FloatLeft` and `FloatRight` currently place the frame
9/// at the correct x position but do not implement content wrapping (text does
10/// not flow beside the float). They advance `content_height` like `Inline`,
11/// so subsequent content appears below rather than beside the float.
12/// True float exclusion zones would require tracking available width per
13/// y-range during paragraph layout.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum FramePosition {
16    /// Inline: rendered in normal flow order, advances content_height.
17    #[default]
18    Inline,
19    /// Float left: placed at x=0, advances content_height.
20    /// Content wrapping is not yet implemented.
21    FloatLeft,
22    /// Float right: placed at x=available_width - frame_width, advances content_height.
23    /// Content wrapping is not yet implemented.
24    FloatRight,
25    /// Absolute: placed at (margin_left, margin_top) from document origin.
26    /// Does not affect flow or content_height.
27    Absolute,
28}
29
30/// How the frame border is drawn.
31#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
32pub enum FrameBorderStyle {
33    /// Draw border on all four sides (default).
34    #[default]
35    Full,
36    /// Draw only the left border (blockquote style).
37    LeftOnly,
38    /// No border.
39    None,
40}
41
42/// Parameters for a frame, extracted from text-document's FrameSnapshot.
43pub struct FrameLayoutParams {
44    pub frame_id: usize,
45    pub position: FramePosition,
46    /// Frame width constraint (None = use available width).
47    pub width: Option<f32>,
48    /// Frame height constraint (None = auto from content).
49    pub height: Option<f32>,
50    pub margin_top: f32,
51    pub margin_bottom: f32,
52    pub margin_left: f32,
53    pub margin_right: f32,
54    pub padding: f32,
55    pub border_width: f32,
56    pub border_style: FrameBorderStyle,
57    /// Nested flow elements: blocks, tables, and frames within the frame.
58    /// Each element carries its `flow_index` (its position in the source
59    /// `FrameSnapshot.elements` vec) so `layout_frame` can interleave
60    /// blocks / tables / frames in document order. Without the index,
61    /// nested-frame content (e.g. a deeper blockquote between two
62    /// sibling blocks) would render in the wrong visual order.
63    pub blocks: Vec<(usize, BlockLayoutParams)>,
64    pub tables: Vec<(usize, TableLayoutParams)>,
65    pub frames: Vec<(usize, FrameLayoutParams)>,
66}
67
68/// One direct child of a frame, in document (flow) order.
69///
70/// `FrameLayout` stores its children in per-kind vecs (`blocks`, `tables`,
71/// `frames`); this enum records how those vecs interleave in the source
72/// flow. Incremental relayout uses it to reposition children in document
73/// order — without it, re-stacking would group children by kind and
74/// visually reorder e.g. a blockquote containing [block, table, block].
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum FrameChildRef {
77    /// A direct block, identified by `block_id`.
78    Block(usize),
79    /// A direct table, identified by `table_id`.
80    Table(usize),
81    /// A nested frame, identified by `frame_id`.
82    Frame(usize),
83}
84
85/// Computed layout for a frame.
86pub struct FrameLayout {
87    pub frame_id: usize,
88    pub y: f32,
89    pub x: f32,
90    pub total_width: f32,
91    pub total_height: f32,
92    pub content_x: f32,
93    pub content_y: f32,
94    pub content_width: f32,
95    pub content_height: f32,
96    pub blocks: Vec<BlockLayout>,
97    pub tables: Vec<TableLayout>,
98    pub frames: Vec<FrameLayout>,
99    /// Direct children in document (flow) order — see [`FrameChildRef`].
100    pub flow_order: Vec<FrameChildRef>,
101    pub border_width: f32,
102    pub border_style: FrameBorderStyle,
103}
104
105/// Lay out a frame: compute dimensions, lay out nested content.
106pub fn layout_frame(
107    registry: &FontRegistry,
108    params: &FrameLayoutParams,
109    available_width: f32,
110    scale_factor: f32,
111    font_scale: f32,
112) -> FrameLayout {
113    let border = params.border_width;
114    let pad = params.padding;
115    let frame_width = params.width.unwrap_or(available_width);
116    let content_width =
117        (frame_width - border * 2.0 - pad * 2.0 - params.margin_left - params.margin_right)
118            .max(0.0);
119
120    // Lay out nested elements (blocks, tables, frames) in document
121    // (flow) order — interleaved by `flow_index`. Y-coordinates and
122    // visual placement follow flow order; the output Vecs (blocks /
123    // tables / frames) are pushed in that same order, which is also
124    // the natural per-kind iteration order since `flow_index` is
125    // monotonic across the input streams.
126    enum FlowItem<'a> {
127        Block(&'a BlockLayoutParams),
128        Table(&'a TableLayoutParams),
129        Frame(&'a FrameLayoutParams),
130    }
131    let mut ordered: Vec<(usize, FlowItem)> =
132        Vec::with_capacity(params.blocks.len() + params.tables.len() + params.frames.len());
133    for (idx, b) in &params.blocks {
134        ordered.push((*idx, FlowItem::Block(b)));
135    }
136    for (idx, t) in &params.tables {
137        ordered.push((*idx, FlowItem::Table(t)));
138    }
139    for (idx, f) in &params.frames {
140        ordered.push((*idx, FlowItem::Frame(f)));
141    }
142    ordered.sort_by_key(|(idx, _)| *idx);
143
144    let mut blocks: Vec<BlockLayout> = Vec::with_capacity(params.blocks.len());
145    let mut tables: Vec<TableLayout> = Vec::with_capacity(params.tables.len());
146    let mut nested_frames: Vec<FrameLayout> = Vec::with_capacity(params.frames.len());
147    let mut flow_order: Vec<FrameChildRef> = Vec::with_capacity(ordered.len());
148    let mut content_y = 0.0f32;
149
150    for (_flow_idx, item) in &ordered {
151        match item {
152            FlowItem::Block(block_params) => {
153                let mut block = layout_block(
154                    registry,
155                    block_params,
156                    content_width,
157                    scale_factor,
158                    font_scale,
159                );
160                block.y = content_y + block.top_margin;
161                let block_content = block.height - block.top_margin - block.bottom_margin;
162                content_y = block.y + block_content + block.bottom_margin;
163                flow_order.push(FrameChildRef::Block(block.block_id));
164                blocks.push(block);
165            }
166            FlowItem::Table(table_params) => {
167                let mut table = layout_table(
168                    registry,
169                    table_params,
170                    content_width,
171                    scale_factor,
172                    font_scale,
173                );
174                table.y = content_y;
175                content_y += table.total_height;
176                flow_order.push(FrameChildRef::Table(table.table_id));
177                tables.push(table);
178            }
179            FlowItem::Frame(frame_params) => {
180                let mut nested = layout_frame(
181                    registry,
182                    frame_params,
183                    content_width,
184                    scale_factor,
185                    font_scale,
186                );
187                nested.y = content_y;
188                nested.x = 0.0;
189                content_y += nested.total_height;
190                flow_order.push(FrameChildRef::Frame(nested.frame_id));
191                nested_frames.push(nested);
192            }
193        }
194    }
195
196    let auto_content_height = content_y;
197    let content_height = params
198        .height
199        .map(|h| (h - border * 2.0 - pad * 2.0).max(0.0))
200        .unwrap_or(auto_content_height);
201
202    let total_height =
203        params.margin_top + border + pad + content_height + pad + border + params.margin_bottom;
204    let total_width =
205        params.margin_left + border + pad + content_width + pad + border + params.margin_right;
206
207    let content_x = params.margin_left + border + pad;
208    let content_y_offset = params.margin_top + border + pad;
209
210    FrameLayout {
211        frame_id: params.frame_id,
212        y: 0.0, // set by flow
213        x: 0.0,
214        total_width,
215        total_height,
216        content_x,
217        content_y: content_y_offset,
218        content_width,
219        content_height,
220        blocks,
221        tables,
222        frames: nested_frames,
223        flow_order,
224        border_width: border,
225        border_style: params.border_style,
226    }
227}