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        let old_char_len = block_char_len(self.blocks.get(&block_id));
279
280        let mut block = layout_block(registry, params, available_width, self.scale_factor);
281        block.y = old_y;
282
283        if (block.top_margin - old_top_margin).abs() > 0.001 {
284            let prev_bm = self.prev_block_bottom_margin(block_id).unwrap_or(0.0);
285            let old_collapsed = prev_bm.max(old_top_margin);
286            let new_collapsed = prev_bm.max(block.top_margin);
287            block.y = old_y + (new_collapsed - old_collapsed);
288        }
289
290        let new_content = block.height - block.top_margin - block.bottom_margin;
291        let new_end = block.y + new_content + block.bottom_margin;
292        let delta = new_end - old_end;
293        let new_char_len = block_char_len(Some(&block));
294        let char_delta = new_char_len as isize - old_char_len as isize;
295
296        let new_y = block.y;
297        let new_height = block.height;
298        self.update_max_width_for_block(&block);
299        self.blocks.insert(block_id, block);
300
301        // Update flow_order entry
302        for item in &mut self.flow_order {
303            if let FlowItem::Block {
304                block_id: id,
305                y,
306                height,
307            } = item
308                && *id == block_id
309            {
310                *y = new_y;
311                *height = new_height;
312                break;
313            }
314        }
315
316        self.shift_items_after_block(block_id, delta);
317        self.shift_block_positions_after_block(block_id, char_delta);
318    }
319
320    /// Relayout a block inside a table cell. Recomputes the row height
321    /// and propagates any table height delta to subsequent flow items.
322    fn relayout_table_block(
323        &mut self,
324        registry: &FontRegistry,
325        params: &BlockLayoutParams,
326        table_id: usize,
327        row: usize,
328        col: usize,
329    ) {
330        let table = match self.tables.get_mut(&table_id) {
331            Some(t) => t,
332            None => return,
333        };
334
335        let cell_width = table
336            .column_content_widths
337            .get(col)
338            .copied()
339            .unwrap_or(200.0);
340        let old_table_height = table.total_height;
341
342        // Find the cell and replace the block
343        let cell = match table
344            .cell_layouts
345            .iter_mut()
346            .find(|c| c.row == row && c.column == col)
347        {
348            Some(c) => c,
349            None => return,
350        };
351
352        let new_block = layout_block(registry, params, cell_width, self.scale_factor);
353        if let Some(old) = cell
354            .blocks
355            .iter_mut()
356            .find(|b| b.block_id == params.block_id)
357        {
358            *old = new_block;
359        }
360
361        // Reposition blocks within the cell and recompute cell height
362        let mut block_y = 0.0f32;
363        for block in &mut cell.blocks {
364            block.y = block_y;
365            block_y += block.height;
366        }
367        let cell_height = block_y;
368
369        // Recompute row height by scanning all cells in this row
370        if row < table.row_heights.len() {
371            let mut max_h = 0.0f32;
372            for c in &table.cell_layouts {
373                if c.row == row {
374                    let h: f32 = c.blocks.iter().map(|b| b.height).sum();
375                    max_h = max_h.max(h);
376                }
377            }
378            // Also consider the cell we just updated
379            max_h = max_h.max(cell_height);
380            table.row_heights[row] = max_h;
381        }
382
383        // Recompute row y positions and total height
384        let border = table.border_width;
385        let padding = table.cell_padding;
386        let spacing = if table.row_ys.len() > 1 {
387            // Infer spacing from existing layout
388            if table.row_ys.len() >= 2 && !table.row_heights.is_empty() {
389                let expected = table.row_ys[0] + padding + table.row_heights[0] + padding;
390                (table.row_ys.get(1).copied().unwrap_or(expected) - expected).max(0.0)
391            } else {
392                0.0
393            }
394        } else {
395            0.0
396        };
397        let mut y = border;
398        for (r, &row_h) in table.row_heights.iter().enumerate() {
399            if r < table.row_ys.len() {
400                table.row_ys[r] = y + padding;
401            }
402            y += padding * 2.0 + row_h;
403            if r < table.row_heights.len() - 1 {
404                y += spacing;
405            }
406        }
407        table.total_height = y + border;
408
409        let delta = table.total_height - old_table_height;
410
411        // Update flow_order entry for this table
412        for item in &mut self.flow_order {
413            if let FlowItem::Table {
414                table_id: id,
415                height,
416                ..
417            } = item
418                && *id == table_id
419            {
420                *height = table.total_height;
421                break;
422            }
423        }
424
425        self.shift_items_after_table(table_id, delta);
426    }
427
428    /// Relayout a block inside a frame. Recomputes frame content height
429    /// and propagates any height delta to subsequent flow items.
430    fn relayout_frame_block(
431        &mut self,
432        registry: &FontRegistry,
433        params: &BlockLayoutParams,
434        frame_id: usize,
435    ) {
436        let frame = match self.frames.get_mut(&frame_id) {
437            Some(f) => f,
438            None => return,
439        };
440
441        let old_total_height = frame.total_height;
442        let new_block = layout_block(registry, params, frame.content_width, self.scale_factor);
443
444        relayout_block_in_frame(frame, params.block_id, new_block);
445
446        let delta = frame.total_height - old_total_height;
447
448        for item in &mut self.flow_order {
449            if let FlowItem::Frame {
450                frame_id: id,
451                height,
452                ..
453            } = item
454                && *id == frame_id
455            {
456                *height = frame.total_height;
457                break;
458            }
459        }
460
461        self.shift_items_after_frame(frame_id, delta);
462    }
463
464    /// Shift the document-character `position` of every block that appears
465    /// after the given target block in flow order by `char_delta` characters.
466    ///
467    /// `shift_items_after_block` only propagates the vertical pixel delta.
468    /// This method propagates the character delta so hit_test and caret_rect
469    /// keep returning correct document positions after an incremental
470    /// relayout that changed the target block's char length (e.g. a cut or
471    /// paste inside a non-last paragraph).
472    fn shift_block_positions_after_block(&mut self, block_id: usize, char_delta: isize) {
473        if char_delta == 0 {
474            return;
475        }
476        // Snapshot the order of items so we can mutate the containing
477        // HashMaps inside the loop.
478        let refs: Vec<FlowItemRef> = self
479            .flow_order
480            .iter()
481            .map(|item| match item {
482                FlowItem::Block { block_id, .. } => FlowItemRef::Block(*block_id),
483                FlowItem::Table { table_id, .. } => FlowItemRef::Table(*table_id),
484                FlowItem::Frame { frame_id, .. } => FlowItemRef::Frame(*frame_id),
485            })
486            .collect();
487        let mut found = false;
488        for r in refs {
489            match r {
490                FlowItemRef::Block(id) => {
491                    if found && let Some(b) = self.blocks.get_mut(&id) {
492                        b.position = apply_char_delta(b.position, char_delta);
493                    }
494                    if id == block_id {
495                        found = true;
496                    }
497                }
498                FlowItemRef::Table(id) => {
499                    if found && let Some(t) = self.tables.get_mut(&id) {
500                        shift_block_positions_in_table(t, char_delta);
501                    }
502                }
503                FlowItemRef::Frame(id) => {
504                    if found && let Some(f) = self.frames.get_mut(&id) {
505                        shift_block_positions_in_frame(f, char_delta);
506                    }
507                }
508            }
509        }
510    }
511
512    /// Shift all flow items after the given block by `delta` pixels.
513    fn shift_items_after_block(&mut self, block_id: usize, delta: f32) {
514        if delta.abs() <= 0.001 {
515            return;
516        }
517        let mut found = false;
518        for item in &mut self.flow_order {
519            match item {
520                FlowItem::Block {
521                    block_id: id, y, ..
522                } => {
523                    if found {
524                        *y += delta;
525                        if let Some(b) = self.blocks.get_mut(id) {
526                            b.y += delta;
527                        }
528                    }
529                    if *id == block_id {
530                        found = true;
531                    }
532                }
533                FlowItem::Table {
534                    table_id: id, y, ..
535                } => {
536                    if found {
537                        *y += delta;
538                        if let Some(t) = self.tables.get_mut(id) {
539                            t.y += delta;
540                        }
541                    }
542                }
543                FlowItem::Frame {
544                    frame_id: id, y, ..
545                } => {
546                    if found {
547                        *y += delta;
548                        if let Some(f) = self.frames.get_mut(id) {
549                            f.y += delta;
550                        }
551                    }
552                }
553            }
554        }
555        self.content_height += delta;
556    }
557
558    /// Shift all flow items after the given table by `delta` pixels.
559    fn shift_items_after_table(&mut self, table_id: usize, delta: f32) {
560        if delta.abs() <= 0.001 {
561            return;
562        }
563        let mut found = false;
564        for item in &mut self.flow_order {
565            match item {
566                FlowItem::Table {
567                    table_id: id, y, ..
568                } => {
569                    if *id == table_id {
570                        found = true;
571                        continue;
572                    }
573                    if found {
574                        *y += delta;
575                        if let Some(t) = self.tables.get_mut(id) {
576                            t.y += delta;
577                        }
578                    }
579                }
580                FlowItem::Block {
581                    block_id: id, y, ..
582                } => {
583                    if found {
584                        *y += delta;
585                        if let Some(b) = self.blocks.get_mut(id) {
586                            b.y += delta;
587                        }
588                    }
589                }
590                FlowItem::Frame {
591                    frame_id: id, y, ..
592                } => {
593                    if found {
594                        *y += delta;
595                        if let Some(f) = self.frames.get_mut(id) {
596                            f.y += delta;
597                        }
598                    }
599                }
600            }
601        }
602        self.content_height += delta;
603    }
604
605    /// Shift all flow items after the given frame by `delta` pixels.
606    fn shift_items_after_frame(&mut self, frame_id: usize, delta: f32) {
607        if delta.abs() <= 0.001 {
608            return;
609        }
610        let mut found = false;
611        for item in &mut self.flow_order {
612            match item {
613                FlowItem::Frame {
614                    frame_id: id, y, ..
615                } => {
616                    if *id == frame_id {
617                        found = true;
618                        continue;
619                    }
620                    if found {
621                        *y += delta;
622                        if let Some(f) = self.frames.get_mut(id) {
623                            f.y += delta;
624                        }
625                    }
626                }
627                FlowItem::Block {
628                    block_id: id, y, ..
629                } => {
630                    if found {
631                        *y += delta;
632                        if let Some(b) = self.blocks.get_mut(id) {
633                            b.y += delta;
634                        }
635                    }
636                }
637                FlowItem::Table {
638                    table_id: id, y, ..
639                } => {
640                    if found {
641                        *y += delta;
642                        if let Some(t) = self.tables.get_mut(id) {
643                            t.y += delta;
644                        }
645                    }
646                }
647            }
648        }
649        self.content_height += delta;
650    }
651
652    /// Update the cached max content width considering a single block's lines.
653    fn update_max_width_for_block(&mut self, block: &BlockLayout) {
654        for line in &block.lines {
655            let w = line.width + block.left_margin + block.right_margin;
656            if w > self.cached_max_content_width {
657                self.cached_max_content_width = w;
658            }
659        }
660    }
661
662    /// Find the bottom margin of the block immediately before `block_id` in flow order.
663    fn prev_block_bottom_margin(&self, block_id: usize) -> Option<f32> {
664        let mut prev_bm = None;
665        for item in &self.flow_order {
666            match item {
667                FlowItem::Block { block_id: id, .. } => {
668                    if *id == block_id {
669                        return prev_bm;
670                    }
671                    if let Some(b) = self.blocks.get(id) {
672                        prev_bm = Some(b.bottom_margin);
673                    }
674                }
675                _ => {
676                    // Non-block items reset margin collapsing
677                    prev_bm = None;
678                }
679            }
680        }
681        None
682    }
683}
684
685enum FlowItemRef {
686    Block(usize),
687    Table(usize),
688    Frame(usize),
689}
690
691fn block_char_len(block: Option<&BlockLayout>) -> usize {
692    block
693        .and_then(|b| b.lines.last().map(|l| l.char_range.end))
694        .unwrap_or(0)
695}
696
697fn apply_char_delta(position: usize, delta: isize) -> usize {
698    if delta >= 0 {
699        position + delta as usize
700    } else {
701        position.saturating_sub((-delta) as usize)
702    }
703}
704
705fn shift_block_positions_in_slice(blocks: &mut [BlockLayout], delta: isize) {
706    for block in blocks {
707        block.position = apply_char_delta(block.position, delta);
708    }
709}
710
711fn shift_block_positions_in_table(table: &mut TableLayout, delta: isize) {
712    for cell in &mut table.cell_layouts {
713        shift_block_positions_in_slice(&mut cell.blocks, delta);
714    }
715}
716
717fn shift_block_positions_in_frame(frame: &mut FrameLayout, delta: isize) {
718    shift_block_positions_in_slice(&mut frame.blocks, delta);
719    for table in &mut frame.tables {
720        shift_block_positions_in_table(table, delta);
721    }
722    for nested in &mut frame.frames {
723        shift_block_positions_in_frame(nested, delta);
724    }
725}
726
727/// Check whether a frame (or any of its nested frames) contains a block with the given id.
728pub(crate) fn frame_contains_block(frame: &FrameLayout, block_id: usize) -> bool {
729    if frame.blocks.iter().any(|b| b.block_id == block_id) {
730        return true;
731    }
732    frame
733        .frames
734        .iter()
735        .any(|nested| frame_contains_block(nested, block_id))
736}
737
738/// Replace a block inside a frame (searching nested frames recursively)
739/// and recompute content/total heights up the tree.
740fn relayout_block_in_frame(frame: &mut FrameLayout, block_id: usize, new_block: BlockLayout) {
741    let old_content_height = frame.content_height;
742
743    // Try direct blocks first
744    if let Some(old) = frame.blocks.iter_mut().find(|b| b.block_id == block_id) {
745        *old = new_block;
746    } else {
747        // Recurse into nested frames
748        for nested in &mut frame.frames {
749            if frame_contains_block(nested, block_id) {
750                relayout_block_in_frame(nested, block_id, new_block);
751                break;
752            }
753        }
754    }
755
756    // Reposition all direct content (blocks, tables, nested frames) vertically
757    let mut content_y = 0.0f32;
758    for block in &mut frame.blocks {
759        block.y = content_y + block.top_margin;
760        let block_content = block.height - block.top_margin - block.bottom_margin;
761        content_y = block.y + block_content + block.bottom_margin;
762    }
763    for table in &mut frame.tables {
764        table.y = content_y;
765        content_y += table.total_height;
766    }
767    for nested in &mut frame.frames {
768        nested.y = content_y;
769        content_y += nested.total_height;
770    }
771
772    frame.content_height = content_y;
773    frame.total_height += content_y - old_content_height;
774}