Skip to main content

text_typeset/layout/
flow.rs

1use std::collections::HashMap;
2
3use crate::font::registry::FontRegistry;
4use crate::layout::block::{BlockLayout, BlockLayoutParams, layout_block};
5use crate::layout::frame::{FrameLayout, FrameLayoutParams, layout_frame};
6use crate::layout::table::{TableLayout, TableLayoutParams, layout_table};
7
8pub enum FlowItem {
9    Block {
10        block_id: usize,
11        y: f32,
12        height: f32,
13    },
14    Table {
15        table_id: usize,
16        y: f32,
17        height: f32,
18    },
19    Frame {
20        frame_id: usize,
21        y: f32,
22        height: f32,
23    },
24}
25
26pub struct FlowLayout {
27    pub blocks: HashMap<usize, BlockLayout>,
28    pub tables: HashMap<usize, TableLayout>,
29    pub frames: HashMap<usize, FrameLayout>,
30    pub flow_order: Vec<FlowItem>,
31    pub content_height: f32,
32    pub viewport_width: f32,
33    pub viewport_height: f32,
34}
35
36impl Default for FlowLayout {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl FlowLayout {
43    pub fn new() -> Self {
44        Self {
45            blocks: HashMap::new(),
46            tables: HashMap::new(),
47            frames: HashMap::new(),
48            flow_order: Vec::new(),
49            content_height: 0.0,
50            viewport_width: 0.0,
51            viewport_height: 0.0,
52        }
53    }
54
55    /// Add a table to the flow at the current y position.
56    pub fn add_table(
57        &mut self,
58        registry: &FontRegistry,
59        params: &TableLayoutParams,
60        available_width: f32,
61    ) {
62        let mut table = layout_table(registry, params, available_width);
63
64        let mut y = self.content_height;
65        table.y = y;
66        y += table.total_height;
67
68        self.flow_order.push(FlowItem::Table {
69            table_id: table.table_id,
70            y: table.y,
71            height: table.total_height,
72        });
73        self.tables.insert(table.table_id, table);
74        self.content_height = y;
75    }
76
77    /// Add a frame to the flow.
78    ///
79    /// - **Inline**: placed in normal flow, advances content_height.
80    /// - **FloatLeft**: placed at current y, x=0. Does not advance content_height
81    ///   (surrounding content wraps around it).
82    /// - **FloatRight**: placed at current y, x=available_width - frame_width.
83    /// - **Absolute**: placed at (margin_left, margin_top) from document origin.
84    ///   Does not affect flow at all.
85    pub fn add_frame(
86        &mut self,
87        registry: &FontRegistry,
88        params: &FrameLayoutParams,
89        available_width: f32,
90    ) {
91        use crate::layout::frame::FramePosition;
92
93        let mut frame = layout_frame(registry, params, available_width);
94
95        match params.position {
96            FramePosition::Inline => {
97                frame.y = self.content_height;
98                frame.x = 0.0;
99                self.content_height += frame.total_height;
100            }
101            FramePosition::FloatLeft => {
102                frame.y = self.content_height;
103                frame.x = 0.0;
104                // Float doesn't advance content_height -content wraps beside it.
105                // For simplicity, we still advance so subsequent blocks appear below.
106                // True float wrapping would require a "float exclusion zone" tracked
107                // during paragraph layout, which is significantly more complex.
108                self.content_height += frame.total_height;
109            }
110            FramePosition::FloatRight => {
111                frame.y = self.content_height;
112                frame.x = (available_width - frame.total_width).max(0.0);
113                self.content_height += frame.total_height;
114            }
115            FramePosition::Absolute => {
116                // Absolute frames are positioned relative to the document origin
117                // using their margin values as coordinates. They don't affect flow.
118                frame.y = params.margin_top;
119                frame.x = params.margin_left;
120                // Don't advance content_height
121            }
122        }
123
124        self.flow_order.push(FlowItem::Frame {
125            frame_id: frame.frame_id,
126            y: frame.y,
127            height: frame.total_height,
128        });
129        self.frames.insert(frame.frame_id, frame);
130    }
131
132    /// Clear all layout state. Call before rebuilding from a new FlowSnapshot.
133    pub fn clear(&mut self) {
134        self.blocks.clear();
135        self.tables.clear();
136        self.frames.clear();
137        self.flow_order.clear();
138        self.content_height = 0.0;
139    }
140
141    /// Add a single block to the flow at the current y position.
142    pub fn add_block(
143        &mut self,
144        registry: &FontRegistry,
145        params: &BlockLayoutParams,
146        available_width: f32,
147    ) {
148        let mut block = layout_block(registry, params, available_width);
149
150        // Margin collapsing with previous block
151        let mut y = self.content_height;
152        if let Some(FlowItem::Block {
153            block_id: prev_id, ..
154        }) = self.flow_order.last()
155        {
156            if let Some(prev_block) = self.blocks.get(prev_id) {
157                let collapsed = prev_block.bottom_margin.max(block.top_margin);
158                y -= prev_block.bottom_margin;
159                y += collapsed;
160            } else {
161                y += block.top_margin;
162            }
163        } else {
164            y += block.top_margin;
165        }
166
167        block.y = y;
168        let block_content = block.height - block.top_margin - block.bottom_margin;
169        y += block_content + block.bottom_margin;
170
171        self.flow_order.push(FlowItem::Block {
172            block_id: block.block_id,
173            y: block.y,
174            height: block.height,
175        });
176        self.blocks.insert(block.block_id, block);
177        self.content_height = y;
178    }
179
180    /// Lay out a sequence of blocks vertically.
181    pub fn layout_blocks(
182        &mut self,
183        registry: &FontRegistry,
184        block_params: Vec<BlockLayoutParams>,
185        available_width: f32,
186    ) {
187        self.blocks.clear();
188        self.tables.clear();
189        self.frames.clear();
190        self.flow_order.clear();
191
192        let mut y = 0.0f32;
193
194        for params in &block_params {
195            let mut block = layout_block(registry, params, available_width);
196
197            // Margin collapsing: the space between two blocks is the max of
198            // the first block's bottom margin and the second block's top margin,
199            // not the sum.
200            if let Some(FlowItem::Block {
201                block_id: prev_id, ..
202            }) = self.flow_order.last()
203            {
204                if let Some(prev_block) = self.blocks.get(prev_id) {
205                    let collapsed = prev_block.bottom_margin.max(block.top_margin);
206                    // Undo the previous block's bottom margin and this block's top margin,
207                    // apply the collapsed margin instead.
208                    y -= prev_block.bottom_margin;
209                    y += collapsed;
210                } else {
211                    y += block.top_margin;
212                }
213            } else {
214                y += block.top_margin;
215            }
216
217            block.y = y;
218            let block_height = block.height - block.top_margin - block.bottom_margin;
219            y += block_height + block.bottom_margin;
220
221            self.flow_order.push(FlowItem::Block {
222                block_id: block.block_id,
223                y: block.y,
224                height: block.height,
225            });
226            self.blocks.insert(block.block_id, block);
227        }
228
229        self.content_height = y;
230        // Note: viewport_width is NOT set here. It's a display property
231        // set by Typesetter::set_viewport(), not a layout property.
232        // available_width is the layout width which may differ from viewport
233        // when using ContentWidthMode::Fixed.
234    }
235
236    /// Update a single block's layout and shift subsequent blocks if height changed.
237    pub fn relayout_block(
238        &mut self,
239        registry: &FontRegistry,
240        params: &BlockLayoutParams,
241        available_width: f32,
242    ) {
243        let block_id = params.block_id;
244        let old_height = self.blocks.get(&block_id).map(|b| b.height).unwrap_or(0.0);
245
246        let mut block = layout_block(registry, params, available_width);
247
248        // Preserve the old y position
249        if let Some(old_block) = self.blocks.get(&block_id) {
250            block.y = old_block.y;
251        }
252
253        let new_height = block.height;
254        let delta = new_height - old_height;
255
256        self.blocks.insert(block_id, block);
257
258        // Update flow_order entry
259        for item in &mut self.flow_order {
260            if let FlowItem::Block {
261                block_id: id,
262                height,
263                ..
264            } = item
265                && *id == block_id
266            {
267                *height = new_height;
268                break;
269            }
270        }
271
272        // Shift subsequent blocks if height changed
273        if delta.abs() > 0.001 {
274            let mut found = false;
275            for item in &mut self.flow_order {
276                match item {
277                    FlowItem::Block {
278                        block_id: id,
279                        y,
280                        height: _,
281                    } => {
282                        if found {
283                            *y += delta;
284                            if let Some(b) = self.blocks.get_mut(id) {
285                                b.y += delta;
286                            }
287                        }
288                        if *id == block_id {
289                            found = true;
290                        }
291                    }
292                    FlowItem::Table {
293                        table_id: id, y, ..
294                    } => {
295                        if found {
296                            *y += delta;
297                            if let Some(t) = self.tables.get_mut(id) {
298                                t.y += delta;
299                            }
300                        }
301                    }
302                    FlowItem::Frame {
303                        frame_id: id, y, ..
304                    } => {
305                        if found {
306                            *y += delta;
307                            if let Some(f) = self.frames.get_mut(id) {
308                                f.y += delta;
309                            }
310                        }
311                    }
312                }
313            }
314            self.content_height += delta;
315        }
316    }
317}