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    /// Device pixel ratio passed to shapers and rasterizers.
36    /// Layout is always stored in logical pixels; this only affects
37    /// precision (physical ppem) and glyph bitmap resolution.
38    pub scale_factor: f32,
39}
40
41impl Default for FlowLayout {
42    fn default() -> Self {
43        Self::new()
44    }
45}
46
47impl FlowLayout {
48    pub fn new() -> Self {
49        Self {
50            blocks: HashMap::new(),
51            tables: HashMap::new(),
52            frames: HashMap::new(),
53            flow_order: Vec::new(),
54            content_height: 0.0,
55            viewport_width: 0.0,
56            viewport_height: 0.0,
57            cached_max_content_width: 0.0,
58            scale_factor: 1.0,
59        }
60    }
61
62    /// Add a table to the flow at the current y position.
63    pub fn add_table(
64        &mut self,
65        registry: &FontRegistry,
66        params: &TableLayoutParams,
67        available_width: f32,
68    ) {
69        let mut table = layout_table(registry, params, available_width, self.scale_factor);
70
71        let mut y = self.content_height;
72        table.y = y;
73        y += table.total_height;
74
75        self.flow_order.push(FlowItem::Table {
76            table_id: table.table_id,
77            y: table.y,
78            height: table.total_height,
79        });
80        if table.total_width > self.cached_max_content_width {
81            self.cached_max_content_width = table.total_width;
82        }
83        self.tables.insert(table.table_id, table);
84        self.content_height = y;
85    }
86
87    /// Add a frame to the flow.
88    ///
89    /// - **Inline**: placed in normal flow, advances content_height.
90    /// - **FloatLeft**: placed at current y, x=0. Does not advance content_height
91    ///   (surrounding content wraps around it).
92    /// - **FloatRight**: placed at current y, x=available_width - frame_width.
93    /// - **Absolute**: placed at (margin_left, margin_top) from document origin.
94    ///   Does not affect flow at all.
95    pub fn add_frame(
96        &mut self,
97        registry: &FontRegistry,
98        params: &FrameLayoutParams,
99        available_width: f32,
100    ) {
101        use crate::layout::frame::FramePosition;
102
103        let mut frame = layout_frame(registry, params, available_width, self.scale_factor);
104
105        match params.position {
106            FramePosition::Inline => {
107                frame.y = self.content_height;
108                frame.x = 0.0;
109                self.content_height += frame.total_height;
110            }
111            FramePosition::FloatLeft => {
112                frame.y = self.content_height;
113                frame.x = 0.0;
114                // Float doesn't advance content_height -content wraps beside it.
115                // For simplicity, we still advance so subsequent blocks appear below.
116                // True float wrapping would require a "float exclusion zone" tracked
117                // during paragraph layout, which is significantly more complex.
118                self.content_height += frame.total_height;
119            }
120            FramePosition::FloatRight => {
121                frame.y = self.content_height;
122                frame.x = (available_width - frame.total_width).max(0.0);
123                self.content_height += frame.total_height;
124            }
125            FramePosition::Absolute => {
126                // Absolute frames are positioned relative to the document origin
127                // using their margin values as coordinates. They don't affect flow.
128                frame.y = params.margin_top;
129                frame.x = params.margin_left;
130                // Don't advance content_height
131            }
132        }
133
134        self.flow_order.push(FlowItem::Frame {
135            frame_id: frame.frame_id,
136            y: frame.y,
137            height: frame.total_height,
138        });
139        if frame.total_width > self.cached_max_content_width {
140            self.cached_max_content_width = frame.total_width;
141        }
142        self.frames.insert(frame.frame_id, frame);
143    }
144
145    /// Clear all layout state. Call before rebuilding from a new FlowSnapshot.
146    pub fn clear(&mut self) {
147        self.blocks.clear();
148        self.tables.clear();
149        self.frames.clear();
150        self.flow_order.clear();
151        self.content_height = 0.0;
152        self.cached_max_content_width = 0.0;
153    }
154
155    /// Add a single block to the flow at the current y position.
156    pub fn add_block(
157        &mut self,
158        registry: &FontRegistry,
159        params: &BlockLayoutParams,
160        available_width: f32,
161    ) {
162        let mut block = layout_block(registry, params, available_width, self.scale_factor);
163
164        // Margin collapsing with previous block
165        let mut y = self.content_height;
166        if let Some(FlowItem::Block {
167            block_id: prev_id, ..
168        }) = self.flow_order.last()
169        {
170            if let Some(prev_block) = self.blocks.get(prev_id) {
171                let collapsed = prev_block.bottom_margin.max(block.top_margin);
172                y -= prev_block.bottom_margin;
173                y += collapsed;
174            } else {
175                y += block.top_margin;
176            }
177        } else {
178            y += block.top_margin;
179        }
180
181        block.y = y;
182        let block_content = block.height - block.top_margin - block.bottom_margin;
183        y += block_content + block.bottom_margin;
184
185        self.flow_order.push(FlowItem::Block {
186            block_id: block.block_id,
187            y: block.y,
188            height: block.height,
189        });
190        self.update_max_width_for_block(&block);
191        self.blocks.insert(block.block_id, block);
192        self.content_height = y;
193    }
194
195    /// Lay out a sequence of blocks vertically.
196    pub fn layout_blocks(
197        &mut self,
198        registry: &FontRegistry,
199        block_params: Vec<BlockLayoutParams>,
200        available_width: f32,
201    ) {
202        self.clear();
203        // Note: viewport_width is NOT set here. It's a display property
204        // set by Typesetter::set_viewport(), not a layout property.
205        // available_width is the layout width which may differ from viewport
206        // when using ContentWidthMode::Fixed.
207        for params in &block_params {
208            self.add_block(registry, params, available_width);
209        }
210    }
211
212    /// Update a single block's layout and shift subsequent items if height changed.
213    ///
214    /// Finds the block in top-level blocks, table cells, or frames, re-layouts
215    /// it, and propagates any height delta to subsequent flow items.
216    pub fn relayout_block(
217        &mut self,
218        registry: &FontRegistry,
219        params: &BlockLayoutParams,
220        available_width: f32,
221    ) {
222        let block_id = params.block_id;
223
224        // Top-level block
225        if self.blocks.contains_key(&block_id) {
226            self.relayout_top_level_block(registry, params, available_width);
227            return;
228        }
229
230        // Table cell block: scan tables for the block_id
231        let table_match = self.tables.iter().find_map(|(&tid, table)| {
232            for cell in &table.cell_layouts {
233                if cell.blocks.iter().any(|b| b.block_id == block_id) {
234                    return Some((tid, cell.row, cell.column));
235                }
236            }
237            None
238        });
239        if let Some((table_id, row, col)) = table_match {
240            self.relayout_table_block(registry, params, table_id, row, col);
241            return;
242        }
243
244        // Frame block: scan frames (including nested frames) for the block_id
245        let frame_match = self.frames.iter().find_map(|(&fid, frame)| {
246            if frame_contains_block(frame, block_id) {
247                return Some(fid);
248            }
249            None
250        });
251        if let Some(frame_id) = frame_match {
252            self.relayout_frame_block(registry, params, frame_id);
253        }
254    }
255
256    /// Relayout a top-level block (existing logic).
257    fn relayout_top_level_block(
258        &mut self,
259        registry: &FontRegistry,
260        params: &BlockLayoutParams,
261        available_width: f32,
262    ) {
263        let block_id = params.block_id;
264        let old_y = self.blocks.get(&block_id).map(|b| b.y).unwrap_or(0.0);
265        let old_height = self.blocks.get(&block_id).map(|b| b.height).unwrap_or(0.0);
266        let old_top_margin = self
267            .blocks
268            .get(&block_id)
269            .map(|b| b.top_margin)
270            .unwrap_or(0.0);
271        let old_bottom_margin = self
272            .blocks
273            .get(&block_id)
274            .map(|b| b.bottom_margin)
275            .unwrap_or(0.0);
276        let old_content = old_height - old_top_margin - old_bottom_margin;
277        let old_end = old_y + old_content + old_bottom_margin;
278
279        let mut block = layout_block(registry, params, available_width, self.scale_factor);
280        block.y = old_y;
281
282        if (block.top_margin - old_top_margin).abs() > 0.001 {
283            let prev_bm = self.prev_block_bottom_margin(block_id).unwrap_or(0.0);
284            let old_collapsed = prev_bm.max(old_top_margin);
285            let new_collapsed = prev_bm.max(block.top_margin);
286            block.y = old_y + (new_collapsed - old_collapsed);
287        }
288
289        let new_content = block.height - block.top_margin - block.bottom_margin;
290        let new_end = block.y + new_content + block.bottom_margin;
291        let delta = new_end - old_end;
292
293        let new_y = block.y;
294        let new_height = block.height;
295        self.update_max_width_for_block(&block);
296        self.blocks.insert(block_id, block);
297
298        // Update flow_order entry
299        for item in &mut self.flow_order {
300            if let FlowItem::Block {
301                block_id: id,
302                y,
303                height,
304            } = item
305                && *id == block_id
306            {
307                *y = new_y;
308                *height = new_height;
309                break;
310            }
311        }
312
313        self.shift_items_after_block(block_id, delta);
314    }
315
316    /// Relayout a block inside a table cell. Recomputes the row height
317    /// and propagates any table height delta to subsequent flow items.
318    fn relayout_table_block(
319        &mut self,
320        registry: &FontRegistry,
321        params: &BlockLayoutParams,
322        table_id: usize,
323        row: usize,
324        col: usize,
325    ) {
326        let table = match self.tables.get_mut(&table_id) {
327            Some(t) => t,
328            None => return,
329        };
330
331        let cell_width = table
332            .column_content_widths
333            .get(col)
334            .copied()
335            .unwrap_or(200.0);
336        let old_table_height = table.total_height;
337
338        // Find the cell and replace the block
339        let cell = match table
340            .cell_layouts
341            .iter_mut()
342            .find(|c| c.row == row && c.column == col)
343        {
344            Some(c) => c,
345            None => return,
346        };
347
348        let new_block = layout_block(registry, params, cell_width, self.scale_factor);
349        if let Some(old) = cell
350            .blocks
351            .iter_mut()
352            .find(|b| b.block_id == params.block_id)
353        {
354            *old = new_block;
355        }
356
357        // Reposition blocks within the cell and recompute cell height
358        let mut block_y = 0.0f32;
359        for block in &mut cell.blocks {
360            block.y = block_y;
361            block_y += block.height;
362        }
363        let cell_height = block_y;
364
365        // Recompute row height by scanning all cells in this row
366        if row < table.row_heights.len() {
367            let mut max_h = 0.0f32;
368            for c in &table.cell_layouts {
369                if c.row == row {
370                    let h: f32 = c.blocks.iter().map(|b| b.height).sum();
371                    max_h = max_h.max(h);
372                }
373            }
374            // Also consider the cell we just updated
375            max_h = max_h.max(cell_height);
376            table.row_heights[row] = max_h;
377        }
378
379        // Recompute row y positions and total height
380        let border = table.border_width;
381        let padding = table.cell_padding;
382        let spacing = if table.row_ys.len() > 1 {
383            // Infer spacing from existing layout
384            if table.row_ys.len() >= 2 && !table.row_heights.is_empty() {
385                let expected = table.row_ys[0] + padding + table.row_heights[0] + padding;
386                (table.row_ys.get(1).copied().unwrap_or(expected) - expected).max(0.0)
387            } else {
388                0.0
389            }
390        } else {
391            0.0
392        };
393        let mut y = border;
394        for (r, &row_h) in table.row_heights.iter().enumerate() {
395            if r < table.row_ys.len() {
396                table.row_ys[r] = y + padding;
397            }
398            y += padding * 2.0 + row_h;
399            if r < table.row_heights.len() - 1 {
400                y += spacing;
401            }
402        }
403        table.total_height = y + border;
404
405        let delta = table.total_height - old_table_height;
406
407        // Update flow_order entry for this table
408        for item in &mut self.flow_order {
409            if let FlowItem::Table {
410                table_id: id,
411                height,
412                ..
413            } = item
414                && *id == table_id
415            {
416                *height = table.total_height;
417                break;
418            }
419        }
420
421        self.shift_items_after_table(table_id, delta);
422    }
423
424    /// Relayout a block inside a frame. Recomputes frame content height
425    /// and propagates any height delta to subsequent flow items.
426    fn relayout_frame_block(
427        &mut self,
428        registry: &FontRegistry,
429        params: &BlockLayoutParams,
430        frame_id: usize,
431    ) {
432        let frame = match self.frames.get_mut(&frame_id) {
433            Some(f) => f,
434            None => return,
435        };
436
437        let old_total_height = frame.total_height;
438        let new_block = layout_block(registry, params, frame.content_width, self.scale_factor);
439
440        relayout_block_in_frame(frame, params.block_id, new_block);
441
442        let delta = frame.total_height - old_total_height;
443
444        for item in &mut self.flow_order {
445            if let FlowItem::Frame {
446                frame_id: id,
447                height,
448                ..
449            } = item
450                && *id == frame_id
451            {
452                *height = frame.total_height;
453                break;
454            }
455        }
456
457        self.shift_items_after_frame(frame_id, delta);
458    }
459
460    /// Shift all flow items after the given block by `delta` pixels.
461    fn shift_items_after_block(&mut self, block_id: usize, delta: f32) {
462        if delta.abs() <= 0.001 {
463            return;
464        }
465        let mut found = false;
466        for item in &mut self.flow_order {
467            match item {
468                FlowItem::Block {
469                    block_id: id, y, ..
470                } => {
471                    if found {
472                        *y += delta;
473                        if let Some(b) = self.blocks.get_mut(id) {
474                            b.y += delta;
475                        }
476                    }
477                    if *id == block_id {
478                        found = true;
479                    }
480                }
481                FlowItem::Table {
482                    table_id: id, y, ..
483                } => {
484                    if found {
485                        *y += delta;
486                        if let Some(t) = self.tables.get_mut(id) {
487                            t.y += delta;
488                        }
489                    }
490                }
491                FlowItem::Frame {
492                    frame_id: id, y, ..
493                } => {
494                    if found {
495                        *y += delta;
496                        if let Some(f) = self.frames.get_mut(id) {
497                            f.y += delta;
498                        }
499                    }
500                }
501            }
502        }
503        self.content_height += delta;
504    }
505
506    /// Shift all flow items after the given table by `delta` pixels.
507    fn shift_items_after_table(&mut self, table_id: usize, delta: f32) {
508        if delta.abs() <= 0.001 {
509            return;
510        }
511        let mut found = false;
512        for item in &mut self.flow_order {
513            match item {
514                FlowItem::Table {
515                    table_id: id, y, ..
516                } => {
517                    if *id == table_id {
518                        found = true;
519                        continue;
520                    }
521                    if found {
522                        *y += delta;
523                        if let Some(t) = self.tables.get_mut(id) {
524                            t.y += delta;
525                        }
526                    }
527                }
528                FlowItem::Block {
529                    block_id: id, y, ..
530                } => {
531                    if found {
532                        *y += delta;
533                        if let Some(b) = self.blocks.get_mut(id) {
534                            b.y += delta;
535                        }
536                    }
537                }
538                FlowItem::Frame {
539                    frame_id: id, y, ..
540                } => {
541                    if found {
542                        *y += delta;
543                        if let Some(f) = self.frames.get_mut(id) {
544                            f.y += delta;
545                        }
546                    }
547                }
548            }
549        }
550        self.content_height += delta;
551    }
552
553    /// Shift all flow items after the given frame by `delta` pixels.
554    fn shift_items_after_frame(&mut self, frame_id: usize, delta: f32) {
555        if delta.abs() <= 0.001 {
556            return;
557        }
558        let mut found = false;
559        for item in &mut self.flow_order {
560            match item {
561                FlowItem::Frame {
562                    frame_id: id, y, ..
563                } => {
564                    if *id == frame_id {
565                        found = true;
566                        continue;
567                    }
568                    if found {
569                        *y += delta;
570                        if let Some(f) = self.frames.get_mut(id) {
571                            f.y += delta;
572                        }
573                    }
574                }
575                FlowItem::Block {
576                    block_id: id, y, ..
577                } => {
578                    if found {
579                        *y += delta;
580                        if let Some(b) = self.blocks.get_mut(id) {
581                            b.y += delta;
582                        }
583                    }
584                }
585                FlowItem::Table {
586                    table_id: id, y, ..
587                } => {
588                    if found {
589                        *y += delta;
590                        if let Some(t) = self.tables.get_mut(id) {
591                            t.y += delta;
592                        }
593                    }
594                }
595            }
596        }
597        self.content_height += delta;
598    }
599
600    /// Update the cached max content width considering a single block's lines.
601    fn update_max_width_for_block(&mut self, block: &BlockLayout) {
602        for line in &block.lines {
603            let w = line.width + block.left_margin + block.right_margin;
604            if w > self.cached_max_content_width {
605                self.cached_max_content_width = w;
606            }
607        }
608    }
609
610    /// Find the bottom margin of the block immediately before `block_id` in flow order.
611    fn prev_block_bottom_margin(&self, block_id: usize) -> Option<f32> {
612        let mut prev_bm = None;
613        for item in &self.flow_order {
614            match item {
615                FlowItem::Block { block_id: id, .. } => {
616                    if *id == block_id {
617                        return prev_bm;
618                    }
619                    if let Some(b) = self.blocks.get(id) {
620                        prev_bm = Some(b.bottom_margin);
621                    }
622                }
623                _ => {
624                    // Non-block items reset margin collapsing
625                    prev_bm = None;
626                }
627            }
628        }
629        None
630    }
631}
632
633/// Check whether a frame (or any of its nested frames) contains a block with the given id.
634pub(crate) fn frame_contains_block(frame: &FrameLayout, block_id: usize) -> bool {
635    if frame.blocks.iter().any(|b| b.block_id == block_id) {
636        return true;
637    }
638    frame
639        .frames
640        .iter()
641        .any(|nested| frame_contains_block(nested, block_id))
642}
643
644/// Replace a block inside a frame (searching nested frames recursively)
645/// and recompute content/total heights up the tree.
646fn relayout_block_in_frame(frame: &mut FrameLayout, block_id: usize, new_block: BlockLayout) {
647    let old_content_height = frame.content_height;
648
649    // Try direct blocks first
650    if let Some(old) = frame.blocks.iter_mut().find(|b| b.block_id == block_id) {
651        *old = new_block;
652    } else {
653        // Recurse into nested frames
654        for nested in &mut frame.frames {
655            if frame_contains_block(nested, block_id) {
656                relayout_block_in_frame(nested, block_id, new_block);
657                break;
658            }
659        }
660    }
661
662    // Reposition all direct content (blocks, tables, nested frames) vertically
663    let mut content_y = 0.0f32;
664    for block in &mut frame.blocks {
665        block.y = content_y + block.top_margin;
666        let block_content = block.height - block.top_margin - block.bottom_margin;
667        content_y = block.y + block_content + block.bottom_margin;
668    }
669    for table in &mut frame.tables {
670        table.y = content_y;
671        content_y += table.total_height;
672    }
673    for nested in &mut frame.frames {
674        nested.y = content_y;
675        content_y += nested.total_height;
676    }
677
678    frame.content_height = content_y;
679    frame.total_height += content_y - old_content_height;
680}