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.clear();
188        // Note: viewport_width is NOT set here. It's a display property
189        // set by Typesetter::set_viewport(), not a layout property.
190        // available_width is the layout width which may differ from viewport
191        // when using ContentWidthMode::Fixed.
192        for params in &block_params {
193            self.add_block(registry, params, available_width);
194        }
195    }
196
197    /// Update a single block's layout and shift subsequent blocks if the
198    /// position or height changed.
199    ///
200    /// If the block's top margin changed, its y position is recomputed using
201    /// margin collapsing with the previous block. Subsequent items are shifted
202    /// by the resulting delta.
203    pub fn relayout_block(
204        &mut self,
205        registry: &FontRegistry,
206        params: &BlockLayoutParams,
207        available_width: f32,
208    ) {
209        let block_id = params.block_id;
210        let old_y = self.blocks.get(&block_id).map(|b| b.y).unwrap_or(0.0);
211        let old_height = self.blocks.get(&block_id).map(|b| b.height).unwrap_or(0.0);
212        let old_top_margin = self
213            .blocks
214            .get(&block_id)
215            .map(|b| b.top_margin)
216            .unwrap_or(0.0);
217        let old_bottom_margin = self
218            .blocks
219            .get(&block_id)
220            .map(|b| b.bottom_margin)
221            .unwrap_or(0.0);
222        let old_content = old_height - old_top_margin - old_bottom_margin;
223        let old_end = old_y + old_content + old_bottom_margin;
224
225        let mut block = layout_block(registry, params, available_width);
226        block.y = old_y;
227
228        // If top margin changed, recompute this block's y position
229        if (block.top_margin - old_top_margin).abs() > 0.001 {
230            let prev_bm = self.prev_block_bottom_margin(block_id).unwrap_or(0.0);
231            let old_collapsed = prev_bm.max(old_top_margin);
232            let new_collapsed = prev_bm.max(block.top_margin);
233            block.y = old_y + (new_collapsed - old_collapsed);
234        }
235
236        let new_content = block.height - block.top_margin - block.bottom_margin;
237        let new_end = block.y + new_content + block.bottom_margin;
238        let delta = new_end - old_end;
239
240        let new_y = block.y;
241        let new_height = block.height;
242        self.blocks.insert(block_id, block);
243
244        // Update flow_order entry
245        for item in &mut self.flow_order {
246            if let FlowItem::Block {
247                block_id: id,
248                y,
249                height,
250            } = item
251                && *id == block_id
252            {
253                *y = new_y;
254                *height = new_height;
255                break;
256            }
257        }
258
259        // Shift subsequent items if position or height changed
260        if delta.abs() > 0.001 {
261            let mut found = false;
262            for item in &mut self.flow_order {
263                match item {
264                    FlowItem::Block {
265                        block_id: id,
266                        y,
267                        height: _,
268                    } => {
269                        if found {
270                            *y += delta;
271                            if let Some(b) = self.blocks.get_mut(id) {
272                                b.y += delta;
273                            }
274                        }
275                        if *id == block_id {
276                            found = true;
277                        }
278                    }
279                    FlowItem::Table {
280                        table_id: id, y, ..
281                    } => {
282                        if found {
283                            *y += delta;
284                            if let Some(t) = self.tables.get_mut(id) {
285                                t.y += delta;
286                            }
287                        }
288                    }
289                    FlowItem::Frame {
290                        frame_id: id, y, ..
291                    } => {
292                        if found {
293                            *y += delta;
294                            if let Some(f) = self.frames.get_mut(id) {
295                                f.y += delta;
296                            }
297                        }
298                    }
299                }
300            }
301            self.content_height += delta;
302        }
303    }
304
305    /// Find the bottom margin of the block immediately before `block_id` in flow order.
306    fn prev_block_bottom_margin(&self, block_id: usize) -> Option<f32> {
307        let mut prev_bm = None;
308        for item in &self.flow_order {
309            match item {
310                FlowItem::Block { block_id: id, .. } => {
311                    if *id == block_id {
312                        return prev_bm;
313                    }
314                    if let Some(b) = self.blocks.get(id) {
315                        prev_bm = Some(b.bottom_margin);
316                    }
317                }
318                _ => {
319                    // Non-block items reset margin collapsing
320                    prev_bm = None;
321                }
322            }
323        }
324        None
325    }
326}