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