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 items if height changed.
208    ///
209    /// Finds the block in top-level blocks, table cells, or frames, re-layouts
210    /// it, and propagates any height delta to subsequent flow items.
211    pub fn relayout_block(
212        &mut self,
213        registry: &FontRegistry,
214        params: &BlockLayoutParams,
215        available_width: f32,
216    ) {
217        let block_id = params.block_id;
218
219        // Top-level block
220        if self.blocks.contains_key(&block_id) {
221            self.relayout_top_level_block(registry, params, available_width);
222            return;
223        }
224
225        // Table cell block: scan tables for the block_id
226        let table_match = self.tables.iter().find_map(|(&tid, table)| {
227            for cell in &table.cell_layouts {
228                if cell.blocks.iter().any(|b| b.block_id == block_id) {
229                    return Some((tid, cell.row, cell.column));
230                }
231            }
232            None
233        });
234        if let Some((table_id, row, col)) = table_match {
235            self.relayout_table_block(registry, params, table_id, row, col);
236            return;
237        }
238
239        // Frame block: scan frames (including nested frames) for the block_id
240        let frame_match = self.frames.iter().find_map(|(&fid, frame)| {
241            if frame_contains_block(frame, block_id) {
242                return Some(fid);
243            }
244            None
245        });
246        if let Some(frame_id) = frame_match {
247            self.relayout_frame_block(registry, params, frame_id);
248        }
249    }
250
251    /// Relayout a top-level block (existing logic).
252    fn relayout_top_level_block(
253        &mut self,
254        registry: &FontRegistry,
255        params: &BlockLayoutParams,
256        available_width: f32,
257    ) {
258        let block_id = params.block_id;
259        let old_y = self.blocks.get(&block_id).map(|b| b.y).unwrap_or(0.0);
260        let old_height = self.blocks.get(&block_id).map(|b| b.height).unwrap_or(0.0);
261        let old_top_margin = self
262            .blocks
263            .get(&block_id)
264            .map(|b| b.top_margin)
265            .unwrap_or(0.0);
266        let old_bottom_margin = self
267            .blocks
268            .get(&block_id)
269            .map(|b| b.bottom_margin)
270            .unwrap_or(0.0);
271        let old_content = old_height - old_top_margin - old_bottom_margin;
272        let old_end = old_y + old_content + old_bottom_margin;
273
274        let mut block = layout_block(registry, params, available_width);
275        block.y = old_y;
276
277        if (block.top_margin - old_top_margin).abs() > 0.001 {
278            let prev_bm = self.prev_block_bottom_margin(block_id).unwrap_or(0.0);
279            let old_collapsed = prev_bm.max(old_top_margin);
280            let new_collapsed = prev_bm.max(block.top_margin);
281            block.y = old_y + (new_collapsed - old_collapsed);
282        }
283
284        let new_content = block.height - block.top_margin - block.bottom_margin;
285        let new_end = block.y + new_content + block.bottom_margin;
286        let delta = new_end - old_end;
287
288        let new_y = block.y;
289        let new_height = block.height;
290        self.update_max_width_for_block(&block);
291        self.blocks.insert(block_id, block);
292
293        // Update flow_order entry
294        for item in &mut self.flow_order {
295            if let FlowItem::Block {
296                block_id: id,
297                y,
298                height,
299            } = item
300                && *id == block_id
301            {
302                *y = new_y;
303                *height = new_height;
304                break;
305            }
306        }
307
308        self.shift_items_after_block(block_id, delta);
309    }
310
311    /// Relayout a block inside a table cell. Recomputes the row height
312    /// and propagates any table height delta to subsequent flow items.
313    fn relayout_table_block(
314        &mut self,
315        registry: &FontRegistry,
316        params: &BlockLayoutParams,
317        table_id: usize,
318        row: usize,
319        col: usize,
320    ) {
321        let table = match self.tables.get_mut(&table_id) {
322            Some(t) => t,
323            None => return,
324        };
325
326        let cell_width = table
327            .column_content_widths
328            .get(col)
329            .copied()
330            .unwrap_or(200.0);
331        let old_table_height = table.total_height;
332
333        // Find the cell and replace the block
334        let cell = match table
335            .cell_layouts
336            .iter_mut()
337            .find(|c| c.row == row && c.column == col)
338        {
339            Some(c) => c,
340            None => return,
341        };
342
343        let new_block = layout_block(registry, params, cell_width);
344        if let Some(old) = cell
345            .blocks
346            .iter_mut()
347            .find(|b| b.block_id == params.block_id)
348        {
349            *old = new_block;
350        }
351
352        // Reposition blocks within the cell and recompute cell height
353        let mut block_y = 0.0f32;
354        for block in &mut cell.blocks {
355            block.y = block_y;
356            block_y += block.height;
357        }
358        let cell_height = block_y;
359
360        // Recompute row height by scanning all cells in this row
361        if row < table.row_heights.len() {
362            let mut max_h = 0.0f32;
363            for c in &table.cell_layouts {
364                if c.row == row {
365                    let h: f32 = c.blocks.iter().map(|b| b.height).sum();
366                    max_h = max_h.max(h);
367                }
368            }
369            // Also consider the cell we just updated
370            max_h = max_h.max(cell_height);
371            table.row_heights[row] = max_h;
372        }
373
374        // Recompute row y positions and total height
375        let border = table.border_width;
376        let padding = table.cell_padding;
377        let spacing = if table.row_ys.len() > 1 {
378            // Infer spacing from existing layout
379            if table.row_ys.len() >= 2 && !table.row_heights.is_empty() {
380                let expected = table.row_ys[0] + padding + table.row_heights[0] + padding;
381                (table.row_ys.get(1).copied().unwrap_or(expected) - expected).max(0.0)
382            } else {
383                0.0
384            }
385        } else {
386            0.0
387        };
388        let mut y = border;
389        for (r, &row_h) in table.row_heights.iter().enumerate() {
390            if r < table.row_ys.len() {
391                table.row_ys[r] = y + padding;
392            }
393            y += padding * 2.0 + row_h;
394            if r < table.row_heights.len() - 1 {
395                y += spacing;
396            }
397        }
398        table.total_height = y + border;
399
400        let delta = table.total_height - old_table_height;
401
402        // Update flow_order entry for this table
403        for item in &mut self.flow_order {
404            if let FlowItem::Table {
405                table_id: id,
406                height,
407                ..
408            } = item
409                && *id == table_id
410            {
411                *height = table.total_height;
412                break;
413            }
414        }
415
416        self.shift_items_after_table(table_id, delta);
417    }
418
419    /// Relayout a block inside a frame. Recomputes frame content height
420    /// and propagates any height delta to subsequent flow items.
421    fn relayout_frame_block(
422        &mut self,
423        registry: &FontRegistry,
424        params: &BlockLayoutParams,
425        frame_id: usize,
426    ) {
427        let frame = match self.frames.get_mut(&frame_id) {
428            Some(f) => f,
429            None => return,
430        };
431
432        let old_total_height = frame.total_height;
433        let new_block = layout_block(registry, params, frame.content_width);
434
435        relayout_block_in_frame(frame, params.block_id, new_block);
436
437        let delta = frame.total_height - old_total_height;
438
439        for item in &mut self.flow_order {
440            if let FlowItem::Frame {
441                frame_id: id,
442                height,
443                ..
444            } = item
445                && *id == frame_id
446            {
447                *height = frame.total_height;
448                break;
449            }
450        }
451
452        self.shift_items_after_frame(frame_id, delta);
453    }
454
455    /// Shift all flow items after the given block by `delta` pixels.
456    fn shift_items_after_block(&mut self, block_id: usize, delta: f32) {
457        if delta.abs() <= 0.001 {
458            return;
459        }
460        let mut found = false;
461        for item in &mut self.flow_order {
462            match item {
463                FlowItem::Block {
464                    block_id: id, y, ..
465                } => {
466                    if found {
467                        *y += delta;
468                        if let Some(b) = self.blocks.get_mut(id) {
469                            b.y += delta;
470                        }
471                    }
472                    if *id == block_id {
473                        found = true;
474                    }
475                }
476                FlowItem::Table {
477                    table_id: id, y, ..
478                } => {
479                    if found {
480                        *y += delta;
481                        if let Some(t) = self.tables.get_mut(id) {
482                            t.y += delta;
483                        }
484                    }
485                }
486                FlowItem::Frame {
487                    frame_id: id, y, ..
488                } => {
489                    if found {
490                        *y += delta;
491                        if let Some(f) = self.frames.get_mut(id) {
492                            f.y += delta;
493                        }
494                    }
495                }
496            }
497        }
498        self.content_height += delta;
499    }
500
501    /// Shift all flow items after the given table by `delta` pixels.
502    fn shift_items_after_table(&mut self, table_id: usize, delta: f32) {
503        if delta.abs() <= 0.001 {
504            return;
505        }
506        let mut found = false;
507        for item in &mut self.flow_order {
508            match item {
509                FlowItem::Table {
510                    table_id: id, y, ..
511                } => {
512                    if *id == table_id {
513                        found = true;
514                        continue;
515                    }
516                    if found {
517                        *y += delta;
518                        if let Some(t) = self.tables.get_mut(id) {
519                            t.y += delta;
520                        }
521                    }
522                }
523                FlowItem::Block {
524                    block_id: id, y, ..
525                } => {
526                    if found {
527                        *y += delta;
528                        if let Some(b) = self.blocks.get_mut(id) {
529                            b.y += delta;
530                        }
531                    }
532                }
533                FlowItem::Frame {
534                    frame_id: id, y, ..
535                } => {
536                    if found {
537                        *y += delta;
538                        if let Some(f) = self.frames.get_mut(id) {
539                            f.y += delta;
540                        }
541                    }
542                }
543            }
544        }
545        self.content_height += delta;
546    }
547
548    /// Shift all flow items after the given frame by `delta` pixels.
549    fn shift_items_after_frame(&mut self, frame_id: usize, delta: f32) {
550        if delta.abs() <= 0.001 {
551            return;
552        }
553        let mut found = false;
554        for item in &mut self.flow_order {
555            match item {
556                FlowItem::Frame {
557                    frame_id: id, y, ..
558                } => {
559                    if *id == frame_id {
560                        found = true;
561                        continue;
562                    }
563                    if found {
564                        *y += delta;
565                        if let Some(f) = self.frames.get_mut(id) {
566                            f.y += delta;
567                        }
568                    }
569                }
570                FlowItem::Block {
571                    block_id: id, y, ..
572                } => {
573                    if found {
574                        *y += delta;
575                        if let Some(b) = self.blocks.get_mut(id) {
576                            b.y += delta;
577                        }
578                    }
579                }
580                FlowItem::Table {
581                    table_id: id, y, ..
582                } => {
583                    if found {
584                        *y += delta;
585                        if let Some(t) = self.tables.get_mut(id) {
586                            t.y += delta;
587                        }
588                    }
589                }
590            }
591        }
592        self.content_height += delta;
593    }
594
595    /// Update the cached max content width considering a single block's lines.
596    fn update_max_width_for_block(&mut self, block: &BlockLayout) {
597        for line in &block.lines {
598            let w = line.width + block.left_margin + block.right_margin;
599            if w > self.cached_max_content_width {
600                self.cached_max_content_width = w;
601            }
602        }
603    }
604
605    /// Find the bottom margin of the block immediately before `block_id` in flow order.
606    fn prev_block_bottom_margin(&self, block_id: usize) -> Option<f32> {
607        let mut prev_bm = None;
608        for item in &self.flow_order {
609            match item {
610                FlowItem::Block { block_id: id, .. } => {
611                    if *id == block_id {
612                        return prev_bm;
613                    }
614                    if let Some(b) = self.blocks.get(id) {
615                        prev_bm = Some(b.bottom_margin);
616                    }
617                }
618                _ => {
619                    // Non-block items reset margin collapsing
620                    prev_bm = None;
621                }
622            }
623        }
624        None
625    }
626}
627
628/// Check whether a frame (or any of its nested frames) contains a block with the given id.
629pub(crate) fn frame_contains_block(frame: &FrameLayout, block_id: usize) -> bool {
630    if frame.blocks.iter().any(|b| b.block_id == block_id) {
631        return true;
632    }
633    frame
634        .frames
635        .iter()
636        .any(|nested| frame_contains_block(nested, block_id))
637}
638
639/// Replace a block inside a frame (searching nested frames recursively)
640/// and recompute content/total heights up the tree.
641fn relayout_block_in_frame(frame: &mut FrameLayout, block_id: usize, new_block: BlockLayout) {
642    let old_content_height = frame.content_height;
643
644    // Try direct blocks first
645    if let Some(old) = frame.blocks.iter_mut().find(|b| b.block_id == block_id) {
646        *old = new_block;
647    } else {
648        // Recurse into nested frames
649        for nested in &mut frame.frames {
650            if frame_contains_block(nested, block_id) {
651                relayout_block_in_frame(nested, block_id, new_block);
652                break;
653            }
654        }
655    }
656
657    // Reposition all direct content (blocks, tables, nested frames) vertically
658    let mut content_y = 0.0f32;
659    for block in &mut frame.blocks {
660        block.y = content_y + block.top_margin;
661        let block_content = block.height - block.top_margin - block.bottom_margin;
662        content_y = block.y + block_content + block.bottom_margin;
663    }
664    for table in &mut frame.tables {
665        table.y = content_y;
666        content_y += table.total_height;
667    }
668    for nested in &mut frame.frames {
669        nested.y = content_y;
670        content_y += nested.total_height;
671    }
672
673    frame.content_height = content_y;
674    frame.total_height += content_y - old_content_height;
675}