Skip to main content

text_typeset/layout/
flow.rs

1use std::collections::HashMap;
2
3use crate::font::registry::FontRegistry;
4use crate::layout::block::{
5    BlockLayout, BlockLayoutParams, PaintSpan, apply_paint_spans, layout_block,
6};
7use crate::layout::frame::{FrameLayout, FrameLayoutParams, layout_frame};
8use crate::layout::table::{TableLayout, TableLayoutParams, layout_table};
9
10pub enum FlowItem {
11    Block {
12        block_id: usize,
13        y: f32,
14        height: f32,
15    },
16    Table {
17        table_id: usize,
18        y: f32,
19        height: f32,
20    },
21    Frame {
22        frame_id: usize,
23        y: f32,
24        height: f32,
25    },
26}
27
28pub struct FlowLayout {
29    pub blocks: HashMap<usize, BlockLayout>,
30    pub tables: HashMap<usize, TableLayout>,
31    pub frames: HashMap<usize, FrameLayout>,
32    pub flow_order: Vec<FlowItem>,
33    pub content_height: f32,
34    pub viewport_width: f32,
35    pub viewport_height: f32,
36    pub cached_max_content_width: f32,
37    /// Device pixel ratio passed to shapers and rasterizers.
38    /// Layout is always stored in logical pixels; this only affects
39    /// precision (physical ppem) and glyph bitmap resolution.
40    pub scale_factor: f32,
41    /// Un-overlaid (shaped) copy of every laid-out block, keyed by block_id.
42    /// The paint-overlay fast path re-derives the live blocks from these so
43    /// repeated highlight changes never compound run splits. Populated by a
44    /// full layout (`layout_blocks`) and refreshed per block on incremental
45    /// relayout.
46    base_blocks: HashMap<usize, BlockLayout>,
47    /// Current paint-only highlight overlay per block_id. Empty for a block
48    /// means "no overlay" (base colors). Kept so an incrementally relaid block
49    /// re-applies its overlay.
50    pending_paint_spans: HashMap<usize, Vec<PaintSpan>>,
51}
52
53impl Default for FlowLayout {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59impl FlowLayout {
60    pub fn new() -> Self {
61        Self {
62            blocks: HashMap::new(),
63            tables: HashMap::new(),
64            frames: HashMap::new(),
65            flow_order: Vec::new(),
66            content_height: 0.0,
67            viewport_width: 0.0,
68            viewport_height: 0.0,
69            cached_max_content_width: 0.0,
70            scale_factor: 1.0,
71            base_blocks: HashMap::new(),
72            pending_paint_spans: HashMap::new(),
73        }
74    }
75
76    /// Add a table to the flow at the current y position.
77    pub fn add_table(
78        &mut self,
79        registry: &FontRegistry,
80        params: &TableLayoutParams,
81        available_width: f32,
82    ) {
83        let mut table = layout_table(registry, params, available_width, self.scale_factor);
84
85        let mut y = self.content_height;
86        table.y = y;
87        y += table.total_height;
88
89        self.flow_order.push(FlowItem::Table {
90            table_id: table.table_id,
91            y: table.y,
92            height: table.total_height,
93        });
94        if table.total_width > self.cached_max_content_width {
95            self.cached_max_content_width = table.total_width;
96        }
97        self.tables.insert(table.table_id, table);
98        self.content_height = y;
99    }
100
101    /// Add a frame to the flow.
102    ///
103    /// - **Inline**: placed in normal flow, advances content_height.
104    /// - **FloatLeft**: placed at current y, x=0. Does not advance content_height
105    ///   (surrounding content wraps around it).
106    /// - **FloatRight**: placed at current y, x=available_width - frame_width.
107    /// - **Absolute**: placed at (margin_left, margin_top) from document origin.
108    ///   Does not affect flow at all.
109    pub fn add_frame(
110        &mut self,
111        registry: &FontRegistry,
112        params: &FrameLayoutParams,
113        available_width: f32,
114    ) {
115        use crate::layout::frame::FramePosition;
116
117        let mut frame = layout_frame(registry, params, available_width, self.scale_factor);
118
119        match params.position {
120            FramePosition::Inline => {
121                frame.y = self.content_height;
122                frame.x = 0.0;
123                self.content_height += frame.total_height;
124            }
125            FramePosition::FloatLeft => {
126                frame.y = self.content_height;
127                frame.x = 0.0;
128                // Float doesn't advance content_height -content wraps beside it.
129                // For simplicity, we still advance so subsequent blocks appear below.
130                // True float wrapping would require a "float exclusion zone" tracked
131                // during paragraph layout, which is significantly more complex.
132                self.content_height += frame.total_height;
133            }
134            FramePosition::FloatRight => {
135                frame.y = self.content_height;
136                frame.x = (available_width - frame.total_width).max(0.0);
137                self.content_height += frame.total_height;
138            }
139            FramePosition::Absolute => {
140                // Absolute frames are positioned relative to the document origin
141                // using their margin values as coordinates. They don't affect flow.
142                frame.y = params.margin_top;
143                frame.x = params.margin_left;
144                // Don't advance content_height
145            }
146        }
147
148        self.flow_order.push(FlowItem::Frame {
149            frame_id: frame.frame_id,
150            y: frame.y,
151            height: frame.total_height,
152        });
153        if frame.total_width > self.cached_max_content_width {
154            self.cached_max_content_width = frame.total_width;
155        }
156        self.frames.insert(frame.frame_id, frame);
157    }
158
159    /// Clear all layout state. Call before rebuilding from a new FlowSnapshot.
160    pub fn clear(&mut self) {
161        self.blocks.clear();
162        self.tables.clear();
163        self.frames.clear();
164        self.flow_order.clear();
165        self.content_height = 0.0;
166        self.cached_max_content_width = 0.0;
167        self.base_blocks.clear();
168        self.pending_paint_spans.clear();
169    }
170
171    /// Re-capture every laid-out block (top-level, table cells, frames,
172    /// recursively) as the paint-overlay base. Called after a full layout.
173    pub(crate) fn refresh_base_blocks(&mut self) {
174        self.base_blocks.clear();
175        let mut collected: Vec<(usize, BlockLayout)> = Vec::new();
176        for b in self.blocks.values() {
177            collected.push((b.block_id, b.clone()));
178        }
179        for t in self.tables.values() {
180            collect_table_base(t, &mut collected);
181        }
182        for f in self.frames.values() {
183            collect_frame_base(f, &mut collected);
184        }
185        for (id, b) in collected {
186            self.base_blocks.insert(id, b);
187        }
188    }
189
190    /// Replace the paint-only color overlay for the whole flow.
191    ///
192    /// `spans_by_block` maps block_id → its disjoint paint spans (from
193    /// `text_document`'s `extract_paint_spans`). Every block is re-derived from
194    /// its captured base, so colors/decorations change but glyph positions,
195    /// advances, line breaks, and heights do NOT — no reshape, no reflow.
196    /// Blocks absent from the map reset to base colors.
197    pub fn apply_paint_spans_for(&mut self, spans_by_block: HashMap<usize, Vec<PaintSpan>>) {
198        self.pending_paint_spans = spans_by_block;
199        let base = &self.base_blocks;
200        let pending = &self.pending_paint_spans;
201        for b in self.blocks.values_mut() {
202            overlay_block_in_place(b, base, pending);
203        }
204        for t in self.tables.values_mut() {
205            for c in &mut t.cell_layouts {
206                for b in &mut c.blocks {
207                    overlay_block_in_place(b, base, pending);
208                }
209            }
210        }
211        for f in self.frames.values_mut() {
212            overlay_frame_in_place(f, base, pending);
213        }
214    }
215
216    /// Apply (or clear, when `spans` is empty) the paint overlay for a single
217    /// block, re-derived from its base. Returns `false` if `block_id` has no
218    /// captured base.
219    pub fn apply_block_paint_spans(&mut self, block_id: usize, spans: &[PaintSpan]) -> bool {
220        if !self.base_blocks.contains_key(&block_id) {
221            return false;
222        }
223        if spans.is_empty() {
224            self.pending_paint_spans.remove(&block_id);
225        } else {
226            self.pending_paint_spans.insert(block_id, spans.to_vec());
227        }
228        let base = &self.base_blocks;
229        let pending = &self.pending_paint_spans;
230        if let Some(b) = self.blocks.get_mut(&block_id) {
231            overlay_block_in_place(b, base, pending);
232            return true;
233        }
234        for t in self.tables.values_mut() {
235            for c in &mut t.cell_layouts {
236                for b in &mut c.blocks {
237                    if b.block_id == block_id {
238                        overlay_block_in_place(b, base, pending);
239                        return true;
240                    }
241                }
242            }
243        }
244        for f in self.frames.values_mut() {
245            if overlay_one_in_frame(f, block_id, base, pending) {
246                return true;
247            }
248        }
249        true
250    }
251
252    /// After an incremental relayout of `block_id`, re-capture its (base-colored)
253    /// shaped output as the new base and re-apply its pending overlay in place.
254    ///
255    /// The base re-capture happens unconditionally — even when no overlay is
256    /// currently active. The fresh shaped output IS the new base, and a later
257    /// `apply_block_paint_spans` (the engine re-applying syntax / search /
258    /// spell highlights after an edit) overlays from `base_blocks`. If we
259    /// skipped the re-capture when no spans were pending, that overlay would
260    /// re-derive the block from the STALE pre-edit base and silently clobber
261    /// the just-typed text (visible only in highlights-on views; a full
262    /// re-layout from a resize would restore it).
263    fn refresh_base_and_overlay_block(&mut self, block_id: usize) {
264        let fresh = find_block_ref(self, block_id).cloned();
265        if let Some(b) = fresh {
266            self.base_blocks.insert(block_id, b);
267        }
268        // Nothing to overlay if no block carries pending paint spans — the
269        // freshly-reshaped block already holds the correct base-colored output.
270        if self.pending_paint_spans.is_empty() {
271            return;
272        }
273        let base = &self.base_blocks;
274        let pending = &self.pending_paint_spans;
275        if let Some(b) = self.blocks.get_mut(&block_id) {
276            overlay_block_in_place(b, base, pending);
277            return;
278        }
279        for t in self.tables.values_mut() {
280            for c in &mut t.cell_layouts {
281                for b in &mut c.blocks {
282                    if b.block_id == block_id {
283                        overlay_block_in_place(b, base, pending);
284                        return;
285                    }
286                }
287            }
288        }
289        for f in self.frames.values_mut() {
290            if overlay_one_in_frame(f, block_id, base, pending) {
291                return;
292            }
293        }
294    }
295
296    /// Add a single block to the flow at the current y position.
297    pub fn add_block(
298        &mut self,
299        registry: &FontRegistry,
300        params: &BlockLayoutParams,
301        available_width: f32,
302    ) {
303        let mut block = layout_block(registry, params, available_width, self.scale_factor);
304
305        // Margin collapsing with previous block
306        let mut y = self.content_height;
307        if let Some(FlowItem::Block {
308            block_id: prev_id, ..
309        }) = self.flow_order.last()
310        {
311            if let Some(prev_block) = self.blocks.get(prev_id) {
312                let collapsed = prev_block.bottom_margin.max(block.top_margin);
313                y -= prev_block.bottom_margin;
314                y += collapsed;
315            } else {
316                y += block.top_margin;
317            }
318        } else {
319            y += block.top_margin;
320        }
321
322        block.y = y;
323        let block_content = block.height - block.top_margin - block.bottom_margin;
324        y += block_content + block.bottom_margin;
325
326        self.flow_order.push(FlowItem::Block {
327            block_id: block.block_id,
328            y: block.y,
329            height: block.height,
330        });
331        self.update_max_width_for_block(&block);
332        self.blocks.insert(block.block_id, block);
333        self.content_height = y;
334    }
335
336    /// Lay out a sequence of blocks vertically.
337    pub fn layout_blocks(
338        &mut self,
339        registry: &FontRegistry,
340        block_params: Vec<BlockLayoutParams>,
341        available_width: f32,
342    ) {
343        self.clear();
344        // Note: viewport_width is NOT set here. It's a display property
345        // set by Typesetter::set_viewport(), not a layout property.
346        // available_width is the layout width which may differ from viewport
347        // when using ContentWidthMode::Fixed.
348        for params in &block_params {
349            self.add_block(registry, params, available_width);
350        }
351        // Capture the freshly-shaped blocks as the paint-overlay base. A full
352        // layout clears any prior overlay (see `clear`), so the live blocks ARE
353        // the base at this point; the engine applies paint spans afterward via
354        // `apply_paint_spans_for`.
355        self.refresh_base_blocks();
356    }
357
358    /// Update a single block's layout and shift subsequent items if height changed.
359    ///
360    /// Finds the block in top-level blocks, table cells, or frames, re-layouts
361    /// it, and propagates any height delta to subsequent flow items.
362    pub fn relayout_block(
363        &mut self,
364        registry: &FontRegistry,
365        params: &BlockLayoutParams,
366        available_width: f32,
367    ) {
368        let block_id = params.block_id;
369
370        // Top-level block
371        if self.blocks.contains_key(&block_id) {
372            self.relayout_top_level_block(registry, params, available_width);
373            self.refresh_base_and_overlay_block(block_id);
374            return;
375        }
376
377        // Table cell block: scan tables for the block_id
378        let table_match = self.tables.iter().find_map(|(&tid, table)| {
379            for cell in &table.cell_layouts {
380                if cell.blocks.iter().any(|b| b.block_id == block_id) {
381                    return Some((tid, cell.row, cell.column));
382                }
383            }
384            None
385        });
386        if let Some((table_id, row, col)) = table_match {
387            self.relayout_table_block(registry, params, table_id, row, col);
388            self.refresh_base_and_overlay_block(block_id);
389            return;
390        }
391
392        // Frame block: scan frames (including nested frames) for the block_id
393        let frame_match = self.frames.iter().find_map(|(&fid, frame)| {
394            if frame_contains_block(frame, block_id) {
395                return Some(fid);
396            }
397            None
398        });
399        if let Some(frame_id) = frame_match {
400            self.relayout_frame_block(registry, params, frame_id);
401            self.refresh_base_and_overlay_block(block_id);
402        }
403    }
404
405    /// Relayout a top-level block (existing logic).
406    fn relayout_top_level_block(
407        &mut self,
408        registry: &FontRegistry,
409        params: &BlockLayoutParams,
410        available_width: f32,
411    ) {
412        let block_id = params.block_id;
413        let old_y = self.blocks.get(&block_id).map(|b| b.y).unwrap_or(0.0);
414        let old_height = self.blocks.get(&block_id).map(|b| b.height).unwrap_or(0.0);
415        let old_top_margin = self
416            .blocks
417            .get(&block_id)
418            .map(|b| b.top_margin)
419            .unwrap_or(0.0);
420        let old_bottom_margin = self
421            .blocks
422            .get(&block_id)
423            .map(|b| b.bottom_margin)
424            .unwrap_or(0.0);
425        let old_content = old_height - old_top_margin - old_bottom_margin;
426        let old_end = old_y + old_content + old_bottom_margin;
427        let old_char_len = block_char_len(self.blocks.get(&block_id));
428
429        let mut block = layout_block(registry, params, available_width, self.scale_factor);
430        block.y = old_y;
431
432        if (block.top_margin - old_top_margin).abs() > 0.001 {
433            let prev_bm = self.prev_block_bottom_margin(block_id).unwrap_or(0.0);
434            let old_collapsed = prev_bm.max(old_top_margin);
435            let new_collapsed = prev_bm.max(block.top_margin);
436            block.y = old_y + (new_collapsed - old_collapsed);
437        }
438
439        let new_content = block.height - block.top_margin - block.bottom_margin;
440        let new_end = block.y + new_content + block.bottom_margin;
441        let delta = new_end - old_end;
442        let new_char_len = block_char_len(Some(&block));
443        let char_delta = new_char_len as isize - old_char_len as isize;
444
445        let new_y = block.y;
446        let new_height = block.height;
447        self.update_max_width_for_block(&block);
448        self.blocks.insert(block_id, block);
449
450        // Update flow_order entry
451        for item in &mut self.flow_order {
452            if let FlowItem::Block {
453                block_id: id,
454                y,
455                height,
456            } = item
457                && *id == block_id
458            {
459                *y = new_y;
460                *height = new_height;
461                break;
462            }
463        }
464
465        self.shift_items_after_block(block_id, delta);
466        self.shift_block_positions_after_block(block_id, char_delta);
467    }
468
469    /// Relayout a block inside a table cell. Recomputes the row height
470    /// and propagates any table height delta to subsequent flow items.
471    fn relayout_table_block(
472        &mut self,
473        registry: &FontRegistry,
474        params: &BlockLayoutParams,
475        table_id: usize,
476        row: usize,
477        col: usize,
478    ) {
479        let table = match self.tables.get_mut(&table_id) {
480            Some(t) => t,
481            None => return,
482        };
483
484        let cell_width = table
485            .column_content_widths
486            .get(col)
487            .copied()
488            .unwrap_or(200.0);
489        let old_table_height = table.total_height;
490
491        // Find the cell and replace the block
492        let cell = match table
493            .cell_layouts
494            .iter_mut()
495            .find(|c| c.row == row && c.column == col)
496        {
497            Some(c) => c,
498            None => return,
499        };
500
501        let new_block = layout_block(registry, params, cell_width, self.scale_factor);
502        if let Some(old) = cell
503            .blocks
504            .iter_mut()
505            .find(|b| b.block_id == params.block_id)
506        {
507            *old = new_block;
508        }
509
510        // Reposition blocks within the cell and recompute cell height
511        let mut block_y = 0.0f32;
512        for block in &mut cell.blocks {
513            block.y = block_y;
514            block_y += block.height;
515        }
516        let cell_height = block_y;
517
518        // Recompute row height by scanning all cells in this row
519        if row < table.row_heights.len() {
520            let mut max_h = 0.0f32;
521            for c in &table.cell_layouts {
522                if c.row == row {
523                    let h: f32 = c.blocks.iter().map(|b| b.height).sum();
524                    max_h = max_h.max(h);
525                }
526            }
527            // Also consider the cell we just updated
528            max_h = max_h.max(cell_height);
529            table.row_heights[row] = max_h;
530        }
531
532        // Recompute row y positions and total height
533        let border = table.border_width;
534        let padding = table.cell_padding;
535        let spacing = if table.row_ys.len() > 1 {
536            // Infer spacing from existing layout
537            if table.row_ys.len() >= 2 && !table.row_heights.is_empty() {
538                let expected = table.row_ys[0] + padding + table.row_heights[0] + padding;
539                (table.row_ys.get(1).copied().unwrap_or(expected) - expected).max(0.0)
540            } else {
541                0.0
542            }
543        } else {
544            0.0
545        };
546        let mut y = border;
547        for (r, &row_h) in table.row_heights.iter().enumerate() {
548            if r < table.row_ys.len() {
549                table.row_ys[r] = y + padding;
550            }
551            y += padding * 2.0 + row_h;
552            if r < table.row_heights.len() - 1 {
553                y += spacing;
554            }
555        }
556        table.total_height = y + border;
557
558        let delta = table.total_height - old_table_height;
559
560        // Update flow_order entry for this table
561        for item in &mut self.flow_order {
562            if let FlowItem::Table {
563                table_id: id,
564                height,
565                ..
566            } = item
567                && *id == table_id
568            {
569                *height = table.total_height;
570                break;
571            }
572        }
573
574        self.shift_items_after_table(table_id, delta);
575    }
576
577    /// Relayout a block inside a frame. Recomputes frame content height
578    /// and propagates any height delta to subsequent flow items.
579    fn relayout_frame_block(
580        &mut self,
581        registry: &FontRegistry,
582        params: &BlockLayoutParams,
583        frame_id: usize,
584    ) {
585        let frame = match self.frames.get_mut(&frame_id) {
586            Some(f) => f,
587            None => return,
588        };
589
590        let old_total_height = frame.total_height;
591        let new_block = layout_block(registry, params, frame.content_width, self.scale_factor);
592
593        relayout_block_in_frame(frame, params.block_id, new_block);
594
595        let delta = frame.total_height - old_total_height;
596
597        for item in &mut self.flow_order {
598            if let FlowItem::Frame {
599                frame_id: id,
600                height,
601                ..
602            } = item
603                && *id == frame_id
604            {
605                *height = frame.total_height;
606                break;
607            }
608        }
609
610        self.shift_items_after_frame(frame_id, delta);
611    }
612
613    /// Shift the document-character `position` of every block that appears
614    /// after the given target block in flow order by `char_delta` characters.
615    ///
616    /// `shift_items_after_block` only propagates the vertical pixel delta.
617    /// This method propagates the character delta so hit_test and caret_rect
618    /// keep returning correct document positions after an incremental
619    /// relayout that changed the target block's char length (e.g. a cut or
620    /// paste inside a non-last paragraph).
621    fn shift_block_positions_after_block(&mut self, block_id: usize, char_delta: isize) {
622        if char_delta == 0 {
623            return;
624        }
625        // Snapshot the order of items so we can mutate the containing
626        // HashMaps inside the loop.
627        let refs: Vec<FlowItemRef> = self
628            .flow_order
629            .iter()
630            .map(|item| match item {
631                FlowItem::Block { block_id, .. } => FlowItemRef::Block(*block_id),
632                FlowItem::Table { table_id, .. } => FlowItemRef::Table(*table_id),
633                FlowItem::Frame { frame_id, .. } => FlowItemRef::Frame(*frame_id),
634            })
635            .collect();
636        let mut found = false;
637        for r in refs {
638            match r {
639                FlowItemRef::Block(id) => {
640                    if found && let Some(b) = self.blocks.get_mut(&id) {
641                        b.position = apply_char_delta(b.position, char_delta);
642                    }
643                    if id == block_id {
644                        found = true;
645                    }
646                }
647                FlowItemRef::Table(id) => {
648                    if found && let Some(t) = self.tables.get_mut(&id) {
649                        shift_block_positions_in_table(t, char_delta);
650                    }
651                }
652                FlowItemRef::Frame(id) => {
653                    if found && let Some(f) = self.frames.get_mut(&id) {
654                        shift_block_positions_in_frame(f, char_delta);
655                    }
656                }
657            }
658        }
659    }
660
661    /// Shift all flow items after the given block by `delta` pixels.
662    fn shift_items_after_block(&mut self, block_id: usize, delta: f32) {
663        if delta.abs() <= 0.001 {
664            return;
665        }
666        let mut found = false;
667        for item in &mut self.flow_order {
668            match item {
669                FlowItem::Block {
670                    block_id: id, y, ..
671                } => {
672                    if found {
673                        *y += delta;
674                        if let Some(b) = self.blocks.get_mut(id) {
675                            b.y += delta;
676                        }
677                    }
678                    if *id == block_id {
679                        found = true;
680                    }
681                }
682                FlowItem::Table {
683                    table_id: id, y, ..
684                } => {
685                    if found {
686                        *y += delta;
687                        if let Some(t) = self.tables.get_mut(id) {
688                            t.y += delta;
689                        }
690                    }
691                }
692                FlowItem::Frame {
693                    frame_id: id, y, ..
694                } => {
695                    if found {
696                        *y += delta;
697                        if let Some(f) = self.frames.get_mut(id) {
698                            f.y += delta;
699                        }
700                    }
701                }
702            }
703        }
704        self.content_height += delta;
705    }
706
707    /// Shift all flow items after the given table by `delta` pixels.
708    fn shift_items_after_table(&mut self, table_id: usize, delta: f32) {
709        if delta.abs() <= 0.001 {
710            return;
711        }
712        let mut found = false;
713        for item in &mut self.flow_order {
714            match item {
715                FlowItem::Table {
716                    table_id: id, y, ..
717                } => {
718                    if *id == table_id {
719                        found = true;
720                        continue;
721                    }
722                    if found {
723                        *y += delta;
724                        if let Some(t) = self.tables.get_mut(id) {
725                            t.y += delta;
726                        }
727                    }
728                }
729                FlowItem::Block {
730                    block_id: id, y, ..
731                } => {
732                    if found {
733                        *y += delta;
734                        if let Some(b) = self.blocks.get_mut(id) {
735                            b.y += delta;
736                        }
737                    }
738                }
739                FlowItem::Frame {
740                    frame_id: id, y, ..
741                } => {
742                    if found {
743                        *y += delta;
744                        if let Some(f) = self.frames.get_mut(id) {
745                            f.y += delta;
746                        }
747                    }
748                }
749            }
750        }
751        self.content_height += delta;
752    }
753
754    /// Shift all flow items after the given frame by `delta` pixels.
755    fn shift_items_after_frame(&mut self, frame_id: usize, delta: f32) {
756        if delta.abs() <= 0.001 {
757            return;
758        }
759        let mut found = false;
760        for item in &mut self.flow_order {
761            match item {
762                FlowItem::Frame {
763                    frame_id: id, y, ..
764                } => {
765                    if *id == frame_id {
766                        found = true;
767                        continue;
768                    }
769                    if found {
770                        *y += delta;
771                        if let Some(f) = self.frames.get_mut(id) {
772                            f.y += delta;
773                        }
774                    }
775                }
776                FlowItem::Block {
777                    block_id: id, y, ..
778                } => {
779                    if found {
780                        *y += delta;
781                        if let Some(b) = self.blocks.get_mut(id) {
782                            b.y += delta;
783                        }
784                    }
785                }
786                FlowItem::Table {
787                    table_id: id, y, ..
788                } => {
789                    if found {
790                        *y += delta;
791                        if let Some(t) = self.tables.get_mut(id) {
792                            t.y += delta;
793                        }
794                    }
795                }
796            }
797        }
798        self.content_height += delta;
799    }
800
801    /// Update the cached max content width considering a single block's lines.
802    fn update_max_width_for_block(&mut self, block: &BlockLayout) {
803        for line in &block.lines {
804            let w = line.width + block.left_margin + block.right_margin;
805            if w > self.cached_max_content_width {
806                self.cached_max_content_width = w;
807            }
808        }
809    }
810
811    /// Find the bottom margin of the block immediately before `block_id` in flow order.
812    fn prev_block_bottom_margin(&self, block_id: usize) -> Option<f32> {
813        let mut prev_bm = None;
814        for item in &self.flow_order {
815            match item {
816                FlowItem::Block { block_id: id, .. } => {
817                    if *id == block_id {
818                        return prev_bm;
819                    }
820                    if let Some(b) = self.blocks.get(id) {
821                        prev_bm = Some(b.bottom_margin);
822                    }
823                }
824                _ => {
825                    // Non-block items reset margin collapsing
826                    prev_bm = None;
827                }
828            }
829        }
830        None
831    }
832}
833
834enum FlowItemRef {
835    Block(usize),
836    Table(usize),
837    Frame(usize),
838}
839
840fn block_char_len(block: Option<&BlockLayout>) -> usize {
841    block
842        .and_then(|b| b.lines.last().map(|l| l.char_range.end))
843        .unwrap_or(0)
844}
845
846fn apply_char_delta(position: usize, delta: isize) -> usize {
847    if delta >= 0 {
848        position + delta as usize
849    } else {
850        position.saturating_sub((-delta) as usize)
851    }
852}
853
854fn shift_block_positions_in_slice(blocks: &mut [BlockLayout], delta: isize) {
855    for block in blocks {
856        block.position = apply_char_delta(block.position, delta);
857    }
858}
859
860fn shift_block_positions_in_table(table: &mut TableLayout, delta: isize) {
861    for cell in &mut table.cell_layouts {
862        shift_block_positions_in_slice(&mut cell.blocks, delta);
863    }
864}
865
866fn shift_block_positions_in_frame(frame: &mut FrameLayout, delta: isize) {
867    shift_block_positions_in_slice(&mut frame.blocks, delta);
868    for table in &mut frame.tables {
869        shift_block_positions_in_table(table, delta);
870    }
871    for nested in &mut frame.frames {
872        shift_block_positions_in_frame(nested, delta);
873    }
874}
875
876// ── Paint-overlay helpers (recolor without reshape/reflow) ──────────────────
877
878/// Re-derive `b` in place from its base + pending overlay. No-op if no base
879/// was captured for it.
880fn overlay_block_in_place(
881    b: &mut BlockLayout,
882    base: &HashMap<usize, BlockLayout>,
883    pending: &HashMap<usize, Vec<PaintSpan>>,
884) {
885    if let Some(base_b) = base.get(&b.block_id) {
886        let empty: Vec<PaintSpan> = Vec::new();
887        let spans = pending.get(&b.block_id).unwrap_or(&empty);
888        *b = apply_paint_spans(base_b, spans);
889    }
890}
891
892/// Re-derive every block in a frame (recursively) from base + pending.
893fn overlay_frame_in_place(
894    frame: &mut FrameLayout,
895    base: &HashMap<usize, BlockLayout>,
896    pending: &HashMap<usize, Vec<PaintSpan>>,
897) {
898    for b in &mut frame.blocks {
899        overlay_block_in_place(b, base, pending);
900    }
901    for t in &mut frame.tables {
902        for c in &mut t.cell_layouts {
903            for b in &mut c.blocks {
904                overlay_block_in_place(b, base, pending);
905            }
906        }
907    }
908    for nested in &mut frame.frames {
909        overlay_frame_in_place(nested, base, pending);
910    }
911}
912
913/// Re-derive a single block (by id) inside a frame (recursively). Returns true
914/// if found.
915fn overlay_one_in_frame(
916    frame: &mut FrameLayout,
917    block_id: usize,
918    base: &HashMap<usize, BlockLayout>,
919    pending: &HashMap<usize, Vec<PaintSpan>>,
920) -> bool {
921    for b in &mut frame.blocks {
922        if b.block_id == block_id {
923            overlay_block_in_place(b, base, pending);
924            return true;
925        }
926    }
927    for t in &mut frame.tables {
928        for c in &mut t.cell_layouts {
929            for b in &mut c.blocks {
930                if b.block_id == block_id {
931                    overlay_block_in_place(b, base, pending);
932                    return true;
933                }
934            }
935        }
936    }
937    for nested in &mut frame.frames {
938        if overlay_one_in_frame(nested, block_id, base, pending) {
939            return true;
940        }
941    }
942    false
943}
944
945fn collect_table_base(t: &TableLayout, out: &mut Vec<(usize, BlockLayout)>) {
946    for c in &t.cell_layouts {
947        for b in &c.blocks {
948            out.push((b.block_id, b.clone()));
949        }
950    }
951}
952
953fn collect_frame_base(f: &FrameLayout, out: &mut Vec<(usize, BlockLayout)>) {
954    for b in &f.blocks {
955        out.push((b.block_id, b.clone()));
956    }
957    for t in &f.tables {
958        collect_table_base(t, out);
959    }
960    for nested in &f.frames {
961        collect_frame_base(nested, out);
962    }
963}
964
965/// Find a block by id across top-level / table cells / frames.
966fn find_block_ref(flow: &FlowLayout, block_id: usize) -> Option<&BlockLayout> {
967    if let Some(b) = flow.blocks.get(&block_id) {
968        return Some(b);
969    }
970    for t in flow.tables.values() {
971        for c in &t.cell_layouts {
972            for b in &c.blocks {
973                if b.block_id == block_id {
974                    return Some(b);
975                }
976            }
977        }
978    }
979    for f in flow.frames.values() {
980        if let Some(b) = find_block_in_frame(f, block_id) {
981            return Some(b);
982        }
983    }
984    None
985}
986
987fn find_block_in_frame(frame: &FrameLayout, block_id: usize) -> Option<&BlockLayout> {
988    for b in &frame.blocks {
989        if b.block_id == block_id {
990            return Some(b);
991        }
992    }
993    for t in &frame.tables {
994        for c in &t.cell_layouts {
995            for b in &c.blocks {
996                if b.block_id == block_id {
997                    return Some(b);
998                }
999            }
1000        }
1001    }
1002    for nested in &frame.frames {
1003        if let Some(b) = find_block_in_frame(nested, block_id) {
1004            return Some(b);
1005        }
1006    }
1007    None
1008}
1009
1010/// Check whether a frame (or any of its nested frames) contains a block with the given id.
1011pub(crate) fn frame_contains_block(frame: &FrameLayout, block_id: usize) -> bool {
1012    if frame.blocks.iter().any(|b| b.block_id == block_id) {
1013        return true;
1014    }
1015    frame
1016        .frames
1017        .iter()
1018        .any(|nested| frame_contains_block(nested, block_id))
1019}
1020
1021/// Replace a block inside a frame (searching nested frames recursively)
1022/// and recompute content/total heights up the tree.
1023fn relayout_block_in_frame(frame: &mut FrameLayout, block_id: usize, new_block: BlockLayout) {
1024    let old_content_height = frame.content_height;
1025
1026    // Try direct blocks first
1027    if let Some(old) = frame.blocks.iter_mut().find(|b| b.block_id == block_id) {
1028        *old = new_block;
1029    } else {
1030        // Recurse into nested frames
1031        for nested in &mut frame.frames {
1032            if frame_contains_block(nested, block_id) {
1033                relayout_block_in_frame(nested, block_id, new_block);
1034                break;
1035            }
1036        }
1037    }
1038
1039    // Reposition all direct content (blocks, tables, nested frames) vertically
1040    let mut content_y = 0.0f32;
1041    for block in &mut frame.blocks {
1042        block.y = content_y + block.top_margin;
1043        let block_content = block.height - block.top_margin - block.bottom_margin;
1044        content_y = block.y + block_content + block.bottom_margin;
1045    }
1046    for table in &mut frame.tables {
1047        table.y = content_y;
1048        content_y += table.total_height;
1049    }
1050    for nested in &mut frame.frames {
1051        nested.y = content_y;
1052        content_y += nested.total_height;
1053    }
1054
1055    frame.content_height = content_y;
1056    frame.total_height += content_y - old_content_height;
1057}