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/// Computed layout for a frame.
69pub struct FrameLayout {
70    pub frame_id: usize,
71    pub y: f32,
72    pub x: f32,
73    pub total_width: f32,
74    pub total_height: f32,
75    pub content_x: f32,
76    pub content_y: f32,
77    pub content_width: f32,
78    pub content_height: f32,
79    pub blocks: Vec<BlockLayout>,
80    pub tables: Vec<TableLayout>,
81    pub frames: Vec<FrameLayout>,
82    pub border_width: f32,
83    pub border_style: FrameBorderStyle,
84}
85
86/// Lay out a frame: compute dimensions, lay out nested content.
87pub fn layout_frame(
88    registry: &FontRegistry,
89    params: &FrameLayoutParams,
90    available_width: f32,
91    scale_factor: f32,
92) -> FrameLayout {
93    let border = params.border_width;
94    let pad = params.padding;
95    let frame_width = params.width.unwrap_or(available_width);
96    let content_width =
97        (frame_width - border * 2.0 - pad * 2.0 - params.margin_left - params.margin_right)
98            .max(0.0);
99
100    // Lay out nested elements (blocks, tables, frames) in document
101    // (flow) order — interleaved by `flow_index`. Y-coordinates and
102    // visual placement follow flow order; the output Vecs (blocks /
103    // tables / frames) are pushed in that same order, which is also
104    // the natural per-kind iteration order since `flow_index` is
105    // monotonic across the input streams.
106    enum FlowItem<'a> {
107        Block(&'a BlockLayoutParams),
108        Table(&'a TableLayoutParams),
109        Frame(&'a FrameLayoutParams),
110    }
111    let mut ordered: Vec<(usize, FlowItem)> =
112        Vec::with_capacity(params.blocks.len() + params.tables.len() + params.frames.len());
113    for (idx, b) in &params.blocks {
114        ordered.push((*idx, FlowItem::Block(b)));
115    }
116    for (idx, t) in &params.tables {
117        ordered.push((*idx, FlowItem::Table(t)));
118    }
119    for (idx, f) in &params.frames {
120        ordered.push((*idx, FlowItem::Frame(f)));
121    }
122    ordered.sort_by_key(|(idx, _)| *idx);
123
124    let mut blocks: Vec<BlockLayout> = Vec::with_capacity(params.blocks.len());
125    let mut tables: Vec<TableLayout> = Vec::with_capacity(params.tables.len());
126    let mut nested_frames: Vec<FrameLayout> = Vec::with_capacity(params.frames.len());
127    let mut content_y = 0.0f32;
128
129    for (_flow_idx, item) in &ordered {
130        match item {
131            FlowItem::Block(block_params) => {
132                let mut block = layout_block(registry, block_params, content_width, scale_factor);
133                block.y = content_y + block.top_margin;
134                let block_content = block.height - block.top_margin - block.bottom_margin;
135                content_y = block.y + block_content + block.bottom_margin;
136                blocks.push(block);
137            }
138            FlowItem::Table(table_params) => {
139                let mut table = layout_table(registry, table_params, content_width, scale_factor);
140                table.y = content_y;
141                content_y += table.total_height;
142                tables.push(table);
143            }
144            FlowItem::Frame(frame_params) => {
145                let mut nested = layout_frame(registry, frame_params, content_width, scale_factor);
146                nested.y = content_y;
147                nested.x = 0.0;
148                content_y += nested.total_height;
149                nested_frames.push(nested);
150            }
151        }
152    }
153
154    let auto_content_height = content_y;
155    let content_height = params
156        .height
157        .map(|h| (h - border * 2.0 - pad * 2.0).max(0.0))
158        .unwrap_or(auto_content_height);
159
160    let total_height =
161        params.margin_top + border + pad + content_height + pad + border + params.margin_bottom;
162    let total_width =
163        params.margin_left + border + pad + content_width + pad + border + params.margin_right;
164
165    let content_x = params.margin_left + border + pad;
166    let content_y_offset = params.margin_top + border + pad;
167
168    FrameLayout {
169        frame_id: params.frame_id,
170        y: 0.0, // set by flow
171        x: 0.0,
172        total_width,
173        total_height,
174        content_x,
175        content_y: content_y_offset,
176        content_width,
177        content_height,
178        blocks,
179        tables,
180        frames: nested_frames,
181        border_width: border,
182        border_style: params.border_style,
183    }
184}