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    /// Per-document logical text-magnification factor (`1.0` = none). Set from
42    /// `DocumentFlow::font_scale` at layout time and threaded into block layout
43    /// alongside `scale_factor`; multiplies the resolved font size so all text
44    /// grows logically (advances, line heights, content height) and reflows.
45    pub font_scale: f32,
46    /// Un-overlaid (shaped) copy of every laid-out block, keyed by block_id.
47    /// The paint-overlay fast path re-derives the live blocks from these so
48    /// repeated highlight changes never compound run splits. Populated by a
49    /// full layout (`layout_blocks`) and refreshed per block on incremental
50    /// relayout.
51    base_blocks: HashMap<usize, BlockLayout>,
52    /// Current paint-only highlight overlay per block_id. Empty for a block
53    /// means "no overlay" (base colors). Kept so an incrementally relaid block
54    /// re-applies its overlay.
55    pending_paint_spans: HashMap<usize, Vec<PaintSpan>>,
56}
57
58impl Default for FlowLayout {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64impl FlowLayout {
65    pub fn new() -> Self {
66        Self {
67            blocks: HashMap::new(),
68            tables: HashMap::new(),
69            frames: HashMap::new(),
70            flow_order: Vec::new(),
71            content_height: 0.0,
72            viewport_width: 0.0,
73            viewport_height: 0.0,
74            cached_max_content_width: 0.0,
75            scale_factor: 1.0,
76            font_scale: 1.0,
77            base_blocks: HashMap::new(),
78            pending_paint_spans: HashMap::new(),
79        }
80    }
81
82    /// Add a table to the flow at the current y position.
83    pub fn add_table(
84        &mut self,
85        registry: &FontRegistry,
86        params: &TableLayoutParams,
87        available_width: f32,
88    ) {
89        let mut table = layout_table(
90            registry,
91            params,
92            available_width,
93            self.scale_factor,
94            self.font_scale,
95        );
96
97        let mut y = self.content_height;
98        table.y = y;
99        y += table.total_height;
100
101        self.flow_order.push(FlowItem::Table {
102            table_id: table.table_id,
103            y: table.y,
104            height: table.total_height,
105        });
106        if table.total_width > self.cached_max_content_width {
107            self.cached_max_content_width = table.total_width;
108        }
109        self.tables.insert(table.table_id, table);
110        self.content_height = y;
111    }
112
113    /// Add a frame to the flow.
114    ///
115    /// - **Inline**: placed in normal flow, advances content_height.
116    /// - **FloatLeft**: placed at current y, x=0. Does not advance content_height
117    ///   (surrounding content wraps around it).
118    /// - **FloatRight**: placed at current y, x=available_width - frame_width.
119    /// - **Absolute**: placed at (margin_left, margin_top) from document origin.
120    ///   Does not affect flow at all.
121    pub fn add_frame(
122        &mut self,
123        registry: &FontRegistry,
124        params: &FrameLayoutParams,
125        available_width: f32,
126    ) {
127        use crate::layout::frame::FramePosition;
128
129        let mut frame = layout_frame(
130            registry,
131            params,
132            available_width,
133            self.scale_factor,
134            self.font_scale,
135        );
136
137        match params.position {
138            FramePosition::Inline => {
139                frame.y = self.content_height;
140                frame.x = 0.0;
141                self.content_height += frame.total_height;
142            }
143            FramePosition::FloatLeft => {
144                frame.y = self.content_height;
145                frame.x = 0.0;
146                // Float doesn't advance content_height -content wraps beside it.
147                // For simplicity, we still advance so subsequent blocks appear below.
148                // True float wrapping would require a "float exclusion zone" tracked
149                // during paragraph layout, which is significantly more complex.
150                self.content_height += frame.total_height;
151            }
152            FramePosition::FloatRight => {
153                frame.y = self.content_height;
154                frame.x = (available_width - frame.total_width).max(0.0);
155                self.content_height += frame.total_height;
156            }
157            FramePosition::Absolute => {
158                // Absolute frames are positioned relative to the document origin
159                // using their margin values as coordinates. They don't affect flow.
160                frame.y = params.margin_top;
161                frame.x = params.margin_left;
162                // Don't advance content_height
163            }
164        }
165
166        self.flow_order.push(FlowItem::Frame {
167            frame_id: frame.frame_id,
168            y: frame.y,
169            height: frame.total_height,
170        });
171        if frame.total_width > self.cached_max_content_width {
172            self.cached_max_content_width = frame.total_width;
173        }
174        self.frames.insert(frame.frame_id, frame);
175    }
176
177    /// Clear all layout state. Call before rebuilding from a new FlowSnapshot.
178    pub fn clear(&mut self) {
179        self.blocks.clear();
180        self.tables.clear();
181        self.frames.clear();
182        self.flow_order.clear();
183        self.content_height = 0.0;
184        self.cached_max_content_width = 0.0;
185        self.base_blocks.clear();
186        self.pending_paint_spans.clear();
187    }
188
189    /// Re-capture every laid-out block (top-level, table cells, frames,
190    /// recursively) as the paint-overlay base. Called after a full layout.
191    pub(crate) fn refresh_base_blocks(&mut self) {
192        self.base_blocks.clear();
193        let mut collected: Vec<(usize, BlockLayout)> = Vec::new();
194        for b in self.blocks.values() {
195            collected.push((b.block_id, b.clone()));
196        }
197        for t in self.tables.values() {
198            collect_table_base(t, &mut collected);
199        }
200        for f in self.frames.values() {
201            collect_frame_base(f, &mut collected);
202        }
203        for (id, b) in collected {
204            self.base_blocks.insert(id, b);
205        }
206    }
207
208    /// Replace the paint-only color overlay for the whole flow.
209    ///
210    /// `spans_by_block` maps block_id → its disjoint paint spans (from
211    /// `text_document`'s `extract_paint_spans`). Every block is re-derived from
212    /// its captured base, so colors/decorations change but glyph positions,
213    /// advances, line breaks, and heights do NOT — no reshape, no reflow.
214    /// Blocks absent from the map reset to base colors.
215    pub fn apply_paint_spans_for(&mut self, spans_by_block: HashMap<usize, Vec<PaintSpan>>) {
216        self.pending_paint_spans = spans_by_block;
217        let base = &self.base_blocks;
218        let pending = &self.pending_paint_spans;
219        for b in self.blocks.values_mut() {
220            overlay_block_in_place(b, base, pending);
221        }
222        for t in self.tables.values_mut() {
223            for c in &mut t.cell_layouts {
224                for b in &mut c.blocks {
225                    overlay_block_in_place(b, base, pending);
226                }
227            }
228        }
229        for f in self.frames.values_mut() {
230            overlay_frame_in_place(f, base, pending);
231        }
232    }
233
234    /// Apply (or clear, when `spans` is empty) the paint overlay for a single
235    /// block, re-derived from its base. Returns `false` if `block_id` has no
236    /// captured base.
237    pub fn apply_block_paint_spans(&mut self, block_id: usize, spans: &[PaintSpan]) -> bool {
238        if !self.base_blocks.contains_key(&block_id) {
239            return false;
240        }
241        if spans.is_empty() {
242            self.pending_paint_spans.remove(&block_id);
243        } else {
244            self.pending_paint_spans.insert(block_id, spans.to_vec());
245        }
246        let base = &self.base_blocks;
247        let pending = &self.pending_paint_spans;
248        if let Some(b) = self.blocks.get_mut(&block_id) {
249            overlay_block_in_place(b, base, pending);
250            return true;
251        }
252        for t in self.tables.values_mut() {
253            for c in &mut t.cell_layouts {
254                for b in &mut c.blocks {
255                    if b.block_id == block_id {
256                        overlay_block_in_place(b, base, pending);
257                        return true;
258                    }
259                }
260            }
261        }
262        for f in self.frames.values_mut() {
263            if overlay_one_in_frame(f, block_id, base, pending) {
264                return true;
265            }
266        }
267        true
268    }
269
270    /// After an incremental relayout of `block_id`, re-capture its (base-colored)
271    /// shaped output as the new base and re-apply its pending overlay in place.
272    ///
273    /// The base re-capture happens unconditionally — even when no overlay is
274    /// currently active. The fresh shaped output IS the new base, and a later
275    /// `apply_block_paint_spans` (the engine re-applying syntax / search /
276    /// spell highlights after an edit) overlays from `base_blocks`. If we
277    /// skipped the re-capture when no spans were pending, that overlay would
278    /// re-derive the block from the STALE pre-edit base and silently clobber
279    /// the just-typed text (visible only in highlights-on views; a full
280    /// re-layout from a resize would restore it).
281    fn refresh_base_and_overlay_block(&mut self, block_id: usize) {
282        let fresh = find_block_ref(self, block_id).cloned();
283        if let Some(b) = fresh {
284            self.base_blocks.insert(block_id, b);
285        }
286        // Nothing to overlay if no block carries pending paint spans — the
287        // freshly-reshaped block already holds the correct base-colored output.
288        if self.pending_paint_spans.is_empty() {
289            return;
290        }
291        let base = &self.base_blocks;
292        let pending = &self.pending_paint_spans;
293        if let Some(b) = self.blocks.get_mut(&block_id) {
294            overlay_block_in_place(b, base, pending);
295            return;
296        }
297        for t in self.tables.values_mut() {
298            for c in &mut t.cell_layouts {
299                for b in &mut c.blocks {
300                    if b.block_id == block_id {
301                        overlay_block_in_place(b, base, pending);
302                        return;
303                    }
304                }
305            }
306        }
307        for f in self.frames.values_mut() {
308            if overlay_one_in_frame(f, block_id, base, pending) {
309                return;
310            }
311        }
312    }
313
314    /// Add a single block to the flow at the current y position.
315    ///
316    /// **Bulk-path primitive.** This deliberately does *not* capture the
317    /// block's paint-overlay base; [`layout_blocks`](Self::layout_blocks) calls
318    /// it in a loop and captures every base once at the end, which keeps the
319    /// bulk path single-pass.
320    ///
321    /// Appending to an *existing* layout has no such follow-up pass, so use
322    /// [`append_block`](Self::append_block) instead. Reaching for this one is
323    /// silent when wrong: the block lays out and renders fine, but
324    /// [`apply_block_paint_spans`](Self::apply_block_paint_spans) then
325    /// early-returns `false` for it forever, so it never receives syntax,
326    /// search, or spell highlighting and nothing reports a problem.
327    pub fn add_block(
328        &mut self,
329        registry: &FontRegistry,
330        params: &BlockLayoutParams,
331        available_width: f32,
332    ) {
333        let mut block = layout_block(
334            registry,
335            params,
336            available_width,
337            self.scale_factor,
338            self.font_scale,
339        );
340
341        // Margin collapsing with previous block
342        let mut y = self.content_height;
343        if let Some(FlowItem::Block {
344            block_id: prev_id, ..
345        }) = self.flow_order.last()
346        {
347            if let Some(prev_block) = self.blocks.get(prev_id) {
348                let collapsed = prev_block.bottom_margin.max(block.top_margin);
349                y -= prev_block.bottom_margin;
350                y += collapsed;
351            } else {
352                y += block.top_margin;
353            }
354        } else {
355            y += block.top_margin;
356        }
357
358        block.y = y;
359        let block_content = block.height - block.top_margin - block.bottom_margin;
360        y += block_content + block.bottom_margin;
361
362        self.flow_order.push(FlowItem::Block {
363            block_id: block.block_id,
364            y: block.y,
365            height: block.height,
366        });
367        self.update_max_width_for_block(&block);
368        self.blocks.insert(block.block_id, block);
369        self.content_height = y;
370    }
371
372    /// Append one block to the tail of an *existing* layout, in O(1).
373    ///
374    /// This is [`add_block`](Self::add_block) plus the paint-overlay
375    /// bookkeeping that a bulk layout would otherwise do afterwards.
376    /// [`layout_blocks`](Self::layout_blocks) calls `add_block` in a loop and
377    /// then re-captures every block's base in one pass
378    /// (`refresh_base_blocks`, O(N)); `add_block` on its own therefore leaves
379    /// the appended block absent from `base_blocks`, and a later
380    /// `apply_paint_spans_for` would re-derive it from a missing base.
381    ///
382    /// Reusing the bulk refresh for a tail append would re-clone the whole
383    /// document once per appended line — precisely the O(N)-per-line cost this
384    /// method exists to avoid — so it refreshes just the appended block
385    /// (`refresh_base_and_overlay_block`, the same O(1) call
386    /// [`relayout_block`](Self::relayout_block) already relies on).
387    ///
388    /// `add_block` itself is left untouched: `layout_blocks` depends on its
389    /// current no-base-refresh behaviour to keep the bulk path single-pass.
390    pub fn append_block(
391        &mut self,
392        registry: &FontRegistry,
393        params: &BlockLayoutParams,
394        available_width: f32,
395    ) {
396        self.add_block(registry, params, available_width);
397        self.refresh_base_and_overlay_block(params.block_id);
398    }
399
400    /// Drop the first `n` top-level blocks, returning how many were removed.
401    ///
402    /// The eviction half of a bounded streaming buffer (a log viewer's
403    /// scrollback cap). Usually O(n) map removals plus one `Vec` memmove of the
404    /// surviving `flow_order` entries — no reshaping. The return value is the
405    /// count actually evicted, which is less than `n` when the run of leading
406    /// blocks is shorter than `n` or a table/frame stops the walk.
407    ///
408    /// Surviving blocks deliberately **keep their absolute `y`**. The vacated
409    /// band at the top simply becomes empty, and `content_height` is unchanged,
410    /// so nothing below moves and no scroll position shifts under the user.
411    /// Rewriting every survivor's `y` would make eviction O(remaining) and
412    /// would yank the viewport; leaving them is both cheaper and correct.
413    /// `flow_order`'s ascending-`y` ordering — which `hit_test`'s binary search
414    /// relies on — is preserved, since removing a prefix of a sorted run leaves
415    /// it sorted.
416    ///
417    /// Only top-level blocks are considered; a leading table or frame stops the
418    /// eviction (a streaming buffer is a flat run of blocks by construction).
419    ///
420    /// If the widest block in the flow is among those evicted,
421    /// `max_content_width` is recomputed from the survivors — O(remaining) for
422    /// that call only. Evicting narrower blocks (the overwhelmingly common
423    /// case) cannot lower the maximum and skips the recompute.
424    pub fn remove_leading(&mut self, n: usize) -> usize {
425        let mut removed = 0;
426        let mut evicted_widest = false;
427
428        for item in self.flow_order.iter().take(n) {
429            let FlowItem::Block { block_id, .. } = item else {
430                break;
431            };
432            if let Some(block) = self.blocks.remove(block_id) {
433                // `cached_max_content_width` only ever grows, so it goes stale
434                // exactly when the block that set it leaves. Comparing against
435                // it here keeps the common case O(n).
436                if block_max_width(&block) >= self.cached_max_content_width {
437                    evicted_widest = true;
438                }
439            }
440            self.base_blocks.remove(block_id);
441            self.pending_paint_spans.remove(block_id);
442            removed += 1;
443        }
444
445        self.flow_order.drain(..removed);
446        if evicted_widest {
447            self.recompute_max_content_width();
448        }
449        removed
450    }
451
452    /// Re-derive `cached_max_content_width` from everything currently laid out.
453    ///
454    /// The cache is a running maximum that only grows, which is fine while a
455    /// flow only ever gains content. Anything that *removes* content has to
456    /// re-derive it, or the horizontal scroll range keeps describing content
457    /// that is no longer there.
458    pub(crate) fn recompute_max_content_width(&mut self) {
459        let mut max: f32 = 0.0;
460        for block in self.blocks.values() {
461            max = max.max(block_max_width(block));
462        }
463        for table in self.tables.values() {
464            max = max.max(table.total_width);
465        }
466        for frame in self.frames.values() {
467            max = max.max(frame.total_width);
468        }
469        self.cached_max_content_width = max;
470    }
471
472    /// Declare the total extent of a uniform-row-height document without
473    /// shaping anything.
474    ///
475    /// In windowed mode `content_height` describes the whole document, not the
476    /// shaped window, so it cannot come from the usual accumulator. Use this to
477    /// keep the scrollbar honest when the row count changes outside the shaped
478    /// window (a line appended while the user is scrolled away from the tail).
479    ///
480    /// Leaves the shaped window untouched.
481    pub fn set_uniform_extent(&mut self, total_rows: usize, row_height: f32) {
482        self.content_height = total_rows as f32 * row_height;
483    }
484
485    /// Shape only `window` — a slice of a much larger uniform-row-height
486    /// document — placing each row at an arithmetic `y = index * row_height`.
487    ///
488    /// Shaping every line is what makes a large document expensive: a resident
489    /// shaped line costs ~6.5 KB, so a 100 000-line buffer costs ~623 MB fully
490    /// laid out, against ~1 MB for a viewport-sized window
491    /// (`docs/streaming-baseline.md`). Rendering already culls to the viewport,
492    /// so shaping the off-screen remainder buys nothing.
493    ///
494    /// `y` is arithmetic rather than accumulated precisely so that a row can be
495    /// placed without having laid out — or even having in memory — any of the
496    /// rows above it, which is what lets the window start at an arbitrary
497    /// scroll position. `content_height` is likewise derived from
498    /// `total_rows`, so the scrollbar reflects the whole document rather than
499    /// the shaped window.
500    ///
501    /// # Invariants
502    ///
503    /// The arithmetic placement is only correct for a document whose rows are
504    /// genuinely uniform: **one row = one visual line of exactly `row_height`**
505    /// — no wrapping (`non_breakable_lines`), no embedded newlines, no
506    /// per-row margins, one font size throughout. That suits log/console
507    /// output and monospaced code; it does not suit prose. Callers with
508    /// variable-height content must use [`layout_blocks`](Self::layout_blocks).
509    /// `window` must be sorted ascending by index, which keeps `flow_order`
510    /// ascending by `y` as `hit_test`'s binary search requires. Both are
511    /// checked in debug builds.
512    ///
513    /// After this call, appending at the tail with
514    /// [`append_block`](Self::append_block) stays correct: `content_height` is
515    /// exactly `total_rows * row_height`, which is where row `total_rows`
516    /// belongs. Trim the window's front with
517    /// [`remove_leading`](Self::remove_leading).
518    ///
519    /// # Behaviour worth knowing
520    ///
521    /// Like [`layout_blocks`](Self::layout_blocks), this drops any paint
522    /// overlay: re-apply spans after re-windowing, or the new rows render in
523    /// base colours. Because this runs on every visible-range change, that
524    /// re-apply is part of the scroll path, not a one-off.
525    ///
526    /// `max_content_width` accumulates across windows rather than being
527    /// re-derived per window: it reports the widest row *seen so far*, not the
528    /// widest row in the document (unknowable without shaping all of it) and
529    /// not the widest row on screen (which would make the horizontal scrollbar
530    /// jump on every vertical scroll). It therefore only grows during a
531    /// windowed session.
532    ///
533    /// `y` is `index * row_height` in `f32`, which represents consecutive
534    /// integers exactly only to 2^24: past ~840 000 rows at a 20 px row height,
535    /// row positions begin quantizing and neighbouring rows drift out of
536    /// alignment. Well beyond the sizes this targets, but not infinite.
537    pub fn layout_window(
538        &mut self,
539        registry: &FontRegistry,
540        window: &[(usize, BlockLayoutParams)],
541        total_rows: usize,
542        row_height: f32,
543        available_width: f32,
544    ) {
545        debug_assert!(
546            window.windows(2).all(|w| w[0].0 < w[1].0),
547            "layout_window: window must be sorted ascending by row index, else \
548             flow_order stops being ascending by y and hit_test's binary search \
549             silently misreports rows"
550        );
551        debug_assert!(
552            window.last().is_none_or(|(last, _)| *last < total_rows),
553            "layout_window: window reaches row {:?} but the document declares \
554             only {total_rows} rows — those rows would sit below content_height \
555             and be unreachable by scrolling",
556            window.last().map(|(i, _)| *i)
557        );
558
559        // The widest row seen so far must survive the rebuild: re-deriving it
560        // from the window alone would make the horizontal scroll range track
561        // whatever happens to be on screen, so it would jump on every vertical
562        // scroll.
563        let max_width_seen = self.cached_max_content_width;
564        self.clear();
565        self.cached_max_content_width = max_width_seen;
566
567        for (index, params) in window {
568            let mut block = layout_block(
569                registry,
570                params,
571                available_width,
572                self.scale_factor,
573                self.font_scale,
574            );
575            debug_assert!(
576                (block.height - row_height).abs() < 0.5,
577                "layout_window: row {index} laid out {:.2}px tall against a \
578                 declared row_height of {row_height:.2}px — the row is not a \
579                 single unwrapped line, so arithmetic y placement would drift",
580                block.height
581            );
582
583            let y = *index as f32 * row_height;
584            block.y = y;
585            self.flow_order.push(FlowItem::Block {
586                block_id: block.block_id,
587                y,
588                height: block.height,
589            });
590            self.update_max_width_for_block(&block);
591            self.blocks.insert(block.block_id, block);
592        }
593
594        // Describes the whole document, not the shaped window — so the
595        // scrollbar spans everything even though almost none of it is shaped.
596        self.set_uniform_extent(total_rows, row_height);
597        // O(window), not O(document): the bulk path's cost is bounded by what
598        // is actually resident.
599        self.refresh_base_blocks();
600    }
601
602    /// Lay out a sequence of blocks vertically.
603    pub fn layout_blocks(
604        &mut self,
605        registry: &FontRegistry,
606        block_params: Vec<BlockLayoutParams>,
607        available_width: f32,
608    ) {
609        self.clear();
610        // Note: viewport_width is NOT set here. It's a display property
611        // set by Typesetter::set_viewport(), not a layout property.
612        // available_width is the layout width which may differ from viewport
613        // when using ContentWidthMode::Fixed.
614        for params in &block_params {
615            self.add_block(registry, params, available_width);
616        }
617        // Capture the freshly-shaped blocks as the paint-overlay base. A full
618        // layout clears any prior overlay (see `clear`), so the live blocks ARE
619        // the base at this point; the engine applies paint spans afterward via
620        // `apply_paint_spans_for`.
621        self.refresh_base_blocks();
622    }
623
624    /// Update a single block's layout and shift subsequent items if height changed.
625    ///
626    /// Finds the block in top-level blocks, table cells, or frames, re-layouts
627    /// it, and propagates any height delta to subsequent flow items.
628    pub fn relayout_block(
629        &mut self,
630        registry: &FontRegistry,
631        params: &BlockLayoutParams,
632        available_width: f32,
633    ) {
634        let block_id = params.block_id;
635
636        // Top-level block
637        if self.blocks.contains_key(&block_id) {
638            self.relayout_top_level_block(registry, params, available_width);
639            self.refresh_base_and_overlay_block(block_id);
640            return;
641        }
642
643        // Table cell block: scan tables for the block_id
644        let table_match = self.tables.iter().find_map(|(&tid, table)| {
645            for cell in &table.cell_layouts {
646                if cell.blocks.iter().any(|b| b.block_id == block_id) {
647                    return Some((tid, cell.row, cell.column));
648                }
649            }
650            None
651        });
652        if let Some((table_id, row, col)) = table_match {
653            let old_char_len = block_char_len(find_block_ref(self, block_id));
654            self.relayout_table_block(registry, params, table_id, row, col);
655            let new_char_len = block_char_len(find_block_ref(self, block_id));
656            let char_delta = new_char_len as isize - old_char_len as isize;
657            self.shift_block_positions_after_table_block(table_id, block_id, char_delta);
658            self.refresh_base_and_overlay_block(block_id);
659            return;
660        }
661
662        // Frame block: scan frames (including nested frames) for the block_id
663        let frame_match = self.frames.iter().find_map(|(&fid, frame)| {
664            if frame_contains_block(frame, block_id) {
665                return Some(fid);
666            }
667            None
668        });
669        if let Some(frame_id) = frame_match {
670            let old_char_len = block_char_len(find_block_ref(self, block_id));
671            self.relayout_frame_block(registry, params, frame_id);
672            let new_char_len = block_char_len(find_block_ref(self, block_id));
673            let char_delta = new_char_len as isize - old_char_len as isize;
674            self.shift_block_positions_after_frame_block(frame_id, block_id, char_delta);
675            self.refresh_base_and_overlay_block(block_id);
676        }
677    }
678
679    /// Relayout a top-level block (existing logic).
680    fn relayout_top_level_block(
681        &mut self,
682        registry: &FontRegistry,
683        params: &BlockLayoutParams,
684        available_width: f32,
685    ) {
686        let block_id = params.block_id;
687        let old_y = self.blocks.get(&block_id).map(|b| b.y).unwrap_or(0.0);
688        let old_height = self.blocks.get(&block_id).map(|b| b.height).unwrap_or(0.0);
689        let old_top_margin = self
690            .blocks
691            .get(&block_id)
692            .map(|b| b.top_margin)
693            .unwrap_or(0.0);
694        let old_bottom_margin = self
695            .blocks
696            .get(&block_id)
697            .map(|b| b.bottom_margin)
698            .unwrap_or(0.0);
699        let old_content = old_height - old_top_margin - old_bottom_margin;
700        let old_end = old_y + old_content + old_bottom_margin;
701        let old_char_len = block_char_len(self.blocks.get(&block_id));
702
703        let mut block = layout_block(
704            registry,
705            params,
706            available_width,
707            self.scale_factor,
708            self.font_scale,
709        );
710        block.y = old_y;
711
712        if (block.top_margin - old_top_margin).abs() > 0.001 {
713            let prev_bm = self.prev_block_bottom_margin(block_id).unwrap_or(0.0);
714            let old_collapsed = prev_bm.max(old_top_margin);
715            let new_collapsed = prev_bm.max(block.top_margin);
716            block.y = old_y + (new_collapsed - old_collapsed);
717        }
718
719        let new_content = block.height - block.top_margin - block.bottom_margin;
720        let new_end = block.y + new_content + block.bottom_margin;
721        let delta = new_end - old_end;
722        let new_char_len = block_char_len(Some(&block));
723        let char_delta = new_char_len as isize - old_char_len as isize;
724
725        let new_y = block.y;
726        let new_height = block.height;
727        self.update_max_width_for_block(&block);
728        self.blocks.insert(block_id, block);
729
730        // Update flow_order entry
731        for item in &mut self.flow_order {
732            if let FlowItem::Block {
733                block_id: id,
734                y,
735                height,
736            } = item
737                && *id == block_id
738            {
739                *y = new_y;
740                *height = new_height;
741                break;
742            }
743        }
744
745        self.shift_items_after_block(block_id, delta);
746        self.shift_block_positions_after_block(block_id, char_delta);
747    }
748
749    /// Relayout a block inside a table cell. Recomputes the row height
750    /// and propagates any table height delta to subsequent flow items.
751    fn relayout_table_block(
752        &mut self,
753        registry: &FontRegistry,
754        params: &BlockLayoutParams,
755        table_id: usize,
756        row: usize,
757        col: usize,
758    ) {
759        let table = match self.tables.get_mut(&table_id) {
760            Some(t) => t,
761            None => return,
762        };
763
764        let old_table_height = table.total_height;
765        recompute_table_cell(
766            table,
767            registry,
768            params,
769            row,
770            col,
771            self.scale_factor,
772            self.font_scale,
773        );
774        let delta = table.total_height - old_table_height;
775
776        // Update flow_order entry for this table
777        for item in &mut self.flow_order {
778            if let FlowItem::Table {
779                table_id: id,
780                height,
781                ..
782            } = item
783                && *id == table_id
784            {
785                *height = table.total_height;
786                break;
787            }
788        }
789
790        self.shift_items_after_table(table_id, delta);
791    }
792
793    /// Relayout a block inside a frame. Handles direct blocks, blocks in
794    /// cells of frame-nested tables, and blocks in nested frames (any
795    /// depth). Recomputes frame content height and propagates any height
796    /// delta to subsequent flow items.
797    fn relayout_frame_block(
798        &mut self,
799        registry: &FontRegistry,
800        params: &BlockLayoutParams,
801        frame_id: usize,
802    ) {
803        let old_total_height = match self.frames.get(&frame_id) {
804            Some(f) => f.total_height,
805            None => return,
806        };
807
808        {
809            let frame = self.frames.get_mut(&frame_id).unwrap();
810            relayout_block_deep_in_frame(
811                frame,
812                registry,
813                params,
814                self.scale_factor,
815                self.font_scale,
816            );
817        }
818
819        let new_total_height = self.frames[&frame_id].total_height;
820        let delta = new_total_height - old_total_height;
821
822        for item in &mut self.flow_order {
823            if let FlowItem::Frame {
824                frame_id: id,
825                height,
826                ..
827            } = item
828                && *id == frame_id
829            {
830                *height = new_total_height;
831                break;
832            }
833        }
834
835        self.shift_items_after_frame(frame_id, delta);
836    }
837
838    /// Shift the document-character `position` of every block that appears
839    /// after the given target block in flow order by `char_delta` characters.
840    ///
841    /// `shift_items_after_block` only propagates the vertical pixel delta.
842    /// This method propagates the character delta so hit_test and caret_rect
843    /// keep returning correct document positions after an incremental
844    /// relayout that changed the target block's char length (e.g. a cut or
845    /// paste inside a non-last paragraph).
846    fn shift_block_positions_after_block(&mut self, block_id: usize, char_delta: isize) {
847        self.shift_block_positions_after_flow_item(FlowItemRef::Block(block_id), char_delta);
848    }
849
850    /// Shift the document-character `position` of every block belonging to
851    /// a flow item that appears after `target` in flow order.
852    fn shift_block_positions_after_flow_item(&mut self, target: FlowItemRef, char_delta: isize) {
853        if char_delta == 0 {
854            return;
855        }
856        // Snapshot the order of items so we can mutate the containing
857        // HashMaps inside the loop.
858        let refs: Vec<FlowItemRef> = self
859            .flow_order
860            .iter()
861            .map(|item| match item {
862                FlowItem::Block { block_id, .. } => FlowItemRef::Block(*block_id),
863                FlowItem::Table { table_id, .. } => FlowItemRef::Table(*table_id),
864                FlowItem::Frame { frame_id, .. } => FlowItemRef::Frame(*frame_id),
865            })
866            .collect();
867        let mut found = false;
868        for r in refs {
869            if found {
870                match r {
871                    FlowItemRef::Block(id) => {
872                        if let Some(b) = self.blocks.get_mut(&id) {
873                            b.position = apply_char_delta(b.position, char_delta);
874                        }
875                    }
876                    FlowItemRef::Table(id) => {
877                        if let Some(t) = self.tables.get_mut(&id) {
878                            shift_block_positions_in_table(t, char_delta);
879                        }
880                    }
881                    FlowItemRef::Frame(id) => {
882                        if let Some(f) = self.frames.get_mut(&id) {
883                            shift_block_positions_in_frame(f, char_delta);
884                        }
885                    }
886                }
887            } else if r == target {
888                found = true;
889            }
890        }
891    }
892
893    /// Shift document positions after an edit inside a top-level table's
894    /// cell: cell blocks after the edited block within the table, then
895    /// every flow item after the table.
896    fn shift_block_positions_after_table_block(
897        &mut self,
898        table_id: usize,
899        block_id: usize,
900        char_delta: isize,
901    ) {
902        if char_delta == 0 {
903            return;
904        }
905        if let Some(table) = self.tables.get_mut(&table_id) {
906            shift_table_positions_after_block(table, block_id, char_delta);
907        }
908        self.shift_block_positions_after_flow_item(FlowItemRef::Table(table_id), char_delta);
909    }
910
911    /// Shift document positions after an edit inside a frame (any depth):
912    /// frame content after the edited block, then every flow item after
913    /// the frame.
914    fn shift_block_positions_after_frame_block(
915        &mut self,
916        frame_id: usize,
917        block_id: usize,
918        char_delta: isize,
919    ) {
920        if char_delta == 0 {
921            return;
922        }
923        if let Some(frame) = self.frames.get_mut(&frame_id) {
924            shift_frame_positions_after_block(frame, block_id, char_delta);
925        }
926        self.shift_block_positions_after_flow_item(FlowItemRef::Frame(frame_id), char_delta);
927    }
928
929    /// Shift all flow items after the given block by `delta` pixels.
930    fn shift_items_after_block(&mut self, block_id: usize, delta: f32) {
931        if delta.abs() <= 0.001 {
932            return;
933        }
934        let mut found = false;
935        for item in &mut self.flow_order {
936            match item {
937                FlowItem::Block {
938                    block_id: id, y, ..
939                } => {
940                    if found {
941                        *y += delta;
942                        if let Some(b) = self.blocks.get_mut(id) {
943                            b.y += delta;
944                        }
945                    }
946                    if *id == block_id {
947                        found = true;
948                    }
949                }
950                FlowItem::Table {
951                    table_id: id, y, ..
952                } => {
953                    if found {
954                        *y += delta;
955                        if let Some(t) = self.tables.get_mut(id) {
956                            t.y += delta;
957                        }
958                    }
959                }
960                FlowItem::Frame {
961                    frame_id: id, y, ..
962                } => {
963                    if found {
964                        *y += delta;
965                        if let Some(f) = self.frames.get_mut(id) {
966                            f.y += delta;
967                        }
968                    }
969                }
970            }
971        }
972        self.content_height += delta;
973    }
974
975    /// Shift all flow items after the given table by `delta` pixels.
976    fn shift_items_after_table(&mut self, table_id: usize, delta: f32) {
977        if delta.abs() <= 0.001 {
978            return;
979        }
980        let mut found = false;
981        for item in &mut self.flow_order {
982            match item {
983                FlowItem::Table {
984                    table_id: id, y, ..
985                } => {
986                    if *id == table_id {
987                        found = true;
988                        continue;
989                    }
990                    if found {
991                        *y += delta;
992                        if let Some(t) = self.tables.get_mut(id) {
993                            t.y += delta;
994                        }
995                    }
996                }
997                FlowItem::Block {
998                    block_id: id, y, ..
999                } => {
1000                    if found {
1001                        *y += delta;
1002                        if let Some(b) = self.blocks.get_mut(id) {
1003                            b.y += delta;
1004                        }
1005                    }
1006                }
1007                FlowItem::Frame {
1008                    frame_id: id, y, ..
1009                } => {
1010                    if found {
1011                        *y += delta;
1012                        if let Some(f) = self.frames.get_mut(id) {
1013                            f.y += delta;
1014                        }
1015                    }
1016                }
1017            }
1018        }
1019        self.content_height += delta;
1020    }
1021
1022    /// Shift all flow items after the given frame by `delta` pixels.
1023    fn shift_items_after_frame(&mut self, frame_id: usize, delta: f32) {
1024        if delta.abs() <= 0.001 {
1025            return;
1026        }
1027        let mut found = false;
1028        for item in &mut self.flow_order {
1029            match item {
1030                FlowItem::Frame {
1031                    frame_id: id, y, ..
1032                } => {
1033                    if *id == frame_id {
1034                        found = true;
1035                        continue;
1036                    }
1037                    if found {
1038                        *y += delta;
1039                        if let Some(f) = self.frames.get_mut(id) {
1040                            f.y += delta;
1041                        }
1042                    }
1043                }
1044                FlowItem::Block {
1045                    block_id: id, y, ..
1046                } => {
1047                    if found {
1048                        *y += delta;
1049                        if let Some(b) = self.blocks.get_mut(id) {
1050                            b.y += delta;
1051                        }
1052                    }
1053                }
1054                FlowItem::Table {
1055                    table_id: id, y, ..
1056                } => {
1057                    if found {
1058                        *y += delta;
1059                        if let Some(t) = self.tables.get_mut(id) {
1060                            t.y += delta;
1061                        }
1062                    }
1063                }
1064            }
1065        }
1066        self.content_height += delta;
1067    }
1068
1069    /// Update the cached max content width considering a single block's lines.
1070    fn update_max_width_for_block(&mut self, block: &BlockLayout) {
1071        let w = block_max_width(block);
1072        if w > self.cached_max_content_width {
1073            self.cached_max_content_width = w;
1074        }
1075    }
1076
1077    /// Find the bottom margin of the block immediately before `block_id` in flow order.
1078    fn prev_block_bottom_margin(&self, block_id: usize) -> Option<f32> {
1079        let mut prev_bm = None;
1080        for item in &self.flow_order {
1081            match item {
1082                FlowItem::Block { block_id: id, .. } => {
1083                    if *id == block_id {
1084                        return prev_bm;
1085                    }
1086                    if let Some(b) = self.blocks.get(id) {
1087                        prev_bm = Some(b.bottom_margin);
1088                    }
1089                }
1090                _ => {
1091                    // Non-block items reset margin collapsing
1092                    prev_bm = None;
1093                }
1094            }
1095        }
1096        None
1097    }
1098}
1099
1100#[derive(Clone, Copy, PartialEq, Eq)]
1101enum FlowItemRef {
1102    Block(usize),
1103    Table(usize),
1104    Frame(usize),
1105}
1106
1107fn block_char_len(block: Option<&BlockLayout>) -> usize {
1108    block
1109        .and_then(|b| b.lines.last().map(|l| l.char_range.end))
1110        .unwrap_or(0)
1111}
1112
1113fn apply_char_delta(position: usize, delta: isize) -> usize {
1114    if delta >= 0 {
1115        position + delta as usize
1116    } else {
1117        position.saturating_sub((-delta) as usize)
1118    }
1119}
1120
1121fn shift_block_positions_in_slice(blocks: &mut [BlockLayout], delta: isize) {
1122    for block in blocks {
1123        block.position = apply_char_delta(block.position, delta);
1124    }
1125}
1126
1127fn shift_block_positions_in_table(table: &mut TableLayout, delta: isize) {
1128    for cell in &mut table.cell_layouts {
1129        shift_block_positions_in_slice(&mut cell.blocks, delta);
1130    }
1131}
1132
1133fn shift_block_positions_in_frame(frame: &mut FrameLayout, delta: isize) {
1134    shift_block_positions_in_slice(&mut frame.blocks, delta);
1135    for table in &mut frame.tables {
1136        shift_block_positions_in_table(table, delta);
1137    }
1138    for nested in &mut frame.frames {
1139        shift_block_positions_in_frame(nested, delta);
1140    }
1141}
1142
1143/// Shift the `position` of every cell block that comes after `block_id`
1144/// within `table` (cells iterate in document/row-major order). Returns
1145/// `true` when the edited block was found in this table.
1146fn shift_table_positions_after_block(
1147    table: &mut TableLayout,
1148    block_id: usize,
1149    delta: isize,
1150) -> bool {
1151    let mut found = false;
1152    for cell in &mut table.cell_layouts {
1153        for b in &mut cell.blocks {
1154            if found {
1155                b.position = apply_char_delta(b.position, delta);
1156            } else if b.block_id == block_id {
1157                found = true;
1158            }
1159        }
1160    }
1161    found
1162}
1163
1164/// Shift the `position` of every block that comes after `block_id` in
1165/// `frame`'s document order — walking `flow_order` so interleaved blocks,
1166/// tables, and nested frames shift correctly. Returns `true` when the
1167/// edited block was found in this frame tree.
1168fn shift_frame_positions_after_block(
1169    frame: &mut FrameLayout,
1170    block_id: usize,
1171    delta: isize,
1172) -> bool {
1173    let order = frame.flow_order.clone();
1174    let mut found = false;
1175    for child in &order {
1176        match child {
1177            crate::layout::frame::FrameChildRef::Block(bid) => {
1178                if found {
1179                    if let Some(b) = frame.blocks.iter_mut().find(|b| b.block_id == *bid) {
1180                        b.position = apply_char_delta(b.position, delta);
1181                    }
1182                } else if *bid == block_id {
1183                    found = true;
1184                }
1185            }
1186            crate::layout::frame::FrameChildRef::Table(tid) => {
1187                if let Some(t) = frame.tables.iter_mut().find(|t| t.table_id == *tid) {
1188                    if found {
1189                        shift_block_positions_in_table(t, delta);
1190                    } else {
1191                        found = shift_table_positions_after_block(t, block_id, delta);
1192                    }
1193                }
1194            }
1195            crate::layout::frame::FrameChildRef::Frame(fid) => {
1196                if let Some(nested) = frame.frames.iter_mut().find(|f| f.frame_id == *fid) {
1197                    if found {
1198                        shift_block_positions_in_frame(nested, delta);
1199                    } else {
1200                        found = shift_frame_positions_after_block(nested, block_id, delta);
1201                    }
1202                }
1203            }
1204        }
1205    }
1206    found
1207}
1208
1209// ── Paint-overlay helpers (recolor without reshape/reflow) ──────────────────
1210
1211/// Re-derive `b` in place from its base + pending overlay. No-op if no base
1212/// was captured for it.
1213fn overlay_block_in_place(
1214    b: &mut BlockLayout,
1215    base: &HashMap<usize, BlockLayout>,
1216    pending: &HashMap<usize, Vec<PaintSpan>>,
1217) {
1218    if let Some(base_b) = base.get(&b.block_id) {
1219        let empty: Vec<PaintSpan> = Vec::new();
1220        let spans = pending.get(&b.block_id).unwrap_or(&empty);
1221        *b = apply_paint_spans(base_b, spans);
1222    }
1223}
1224
1225/// Re-derive every block in a frame (recursively) from base + pending.
1226fn overlay_frame_in_place(
1227    frame: &mut FrameLayout,
1228    base: &HashMap<usize, BlockLayout>,
1229    pending: &HashMap<usize, Vec<PaintSpan>>,
1230) {
1231    for b in &mut frame.blocks {
1232        overlay_block_in_place(b, base, pending);
1233    }
1234    for t in &mut frame.tables {
1235        for c in &mut t.cell_layouts {
1236            for b in &mut c.blocks {
1237                overlay_block_in_place(b, base, pending);
1238            }
1239        }
1240    }
1241    for nested in &mut frame.frames {
1242        overlay_frame_in_place(nested, base, pending);
1243    }
1244}
1245
1246/// Re-derive a single block (by id) inside a frame (recursively). Returns true
1247/// if found.
1248fn overlay_one_in_frame(
1249    frame: &mut FrameLayout,
1250    block_id: usize,
1251    base: &HashMap<usize, BlockLayout>,
1252    pending: &HashMap<usize, Vec<PaintSpan>>,
1253) -> bool {
1254    for b in &mut frame.blocks {
1255        if b.block_id == block_id {
1256            overlay_block_in_place(b, base, pending);
1257            return true;
1258        }
1259    }
1260    for t in &mut frame.tables {
1261        for c in &mut t.cell_layouts {
1262            for b in &mut c.blocks {
1263                if b.block_id == block_id {
1264                    overlay_block_in_place(b, base, pending);
1265                    return true;
1266                }
1267            }
1268        }
1269    }
1270    for nested in &mut frame.frames {
1271        if overlay_one_in_frame(nested, block_id, base, pending) {
1272            return true;
1273        }
1274    }
1275    false
1276}
1277
1278fn collect_table_base(t: &TableLayout, out: &mut Vec<(usize, BlockLayout)>) {
1279    for c in &t.cell_layouts {
1280        for b in &c.blocks {
1281            out.push((b.block_id, b.clone()));
1282        }
1283    }
1284}
1285
1286fn collect_frame_base(f: &FrameLayout, out: &mut Vec<(usize, BlockLayout)>) {
1287    for b in &f.blocks {
1288        out.push((b.block_id, b.clone()));
1289    }
1290    for t in &f.tables {
1291        collect_table_base(t, out);
1292    }
1293    for nested in &f.frames {
1294        collect_frame_base(nested, out);
1295    }
1296}
1297
1298/// Find a block by id across top-level / table cells / frames.
1299/// Widest laid-out line of `block`, margins included.
1300///
1301/// The single definition of "how wide is this block", shared by the running
1302/// maximum (`update_max_width_for_block`) and the re-derivation eviction needs
1303/// (`recompute_max_content_width`), so the two cannot drift apart and disagree
1304/// about the horizontal scroll range.
1305fn block_max_width(block: &BlockLayout) -> f32 {
1306    block
1307        .lines
1308        .iter()
1309        .map(|line| line.width + block.left_margin + block.right_margin)
1310        .fold(0.0_f32, f32::max)
1311}
1312
1313fn find_block_ref(flow: &FlowLayout, block_id: usize) -> Option<&BlockLayout> {
1314    if let Some(b) = flow.blocks.get(&block_id) {
1315        return Some(b);
1316    }
1317    for t in flow.tables.values() {
1318        for c in &t.cell_layouts {
1319            for b in &c.blocks {
1320                if b.block_id == block_id {
1321                    return Some(b);
1322                }
1323            }
1324        }
1325    }
1326    for f in flow.frames.values() {
1327        if let Some(b) = find_block_in_frame(f, block_id) {
1328            return Some(b);
1329        }
1330    }
1331    None
1332}
1333
1334fn find_block_in_frame(frame: &FrameLayout, block_id: usize) -> Option<&BlockLayout> {
1335    for b in &frame.blocks {
1336        if b.block_id == block_id {
1337            return Some(b);
1338        }
1339    }
1340    for t in &frame.tables {
1341        for c in &t.cell_layouts {
1342            for b in &c.blocks {
1343                if b.block_id == block_id {
1344                    return Some(b);
1345                }
1346            }
1347        }
1348    }
1349    for nested in &frame.frames {
1350        if let Some(b) = find_block_in_frame(nested, block_id) {
1351            return Some(b);
1352        }
1353    }
1354    None
1355}
1356
1357/// Check whether a frame (or any of its nested frames) contains a block
1358/// with the given id — including blocks inside cells of tables that are
1359/// direct children of the frame.
1360pub(crate) fn frame_contains_block(frame: &FrameLayout, block_id: usize) -> bool {
1361    if frame.blocks.iter().any(|b| b.block_id == block_id) {
1362        return true;
1363    }
1364    for table in &frame.tables {
1365        for cell in &table.cell_layouts {
1366            if cell.blocks.iter().any(|b| b.block_id == block_id) {
1367                return true;
1368            }
1369        }
1370    }
1371    frame
1372        .frames
1373        .iter()
1374        .any(|nested| frame_contains_block(nested, block_id))
1375}
1376
1377/// Where inside a frame a given block lives (one level deep — nested
1378/// frames are reported as `NestedFrame` and must be descended into).
1379pub(crate) enum FrameBlockLocation {
1380    /// A direct child block of the frame.
1381    DirectBlock,
1382    /// A block inside a cell of a table that is a direct child of the
1383    /// frame. Carries `(table_id, row, column)`.
1384    TableCell(usize, usize, usize),
1385    /// A block somewhere inside a nested frame (carries the nested
1386    /// frame's id).
1387    NestedFrame(usize),
1388}
1389
1390/// Locate `block_id` among the direct children of `frame`.
1391pub(crate) fn find_block_location_in_frame(
1392    frame: &FrameLayout,
1393    block_id: usize,
1394) -> Option<FrameBlockLocation> {
1395    if frame.blocks.iter().any(|b| b.block_id == block_id) {
1396        return Some(FrameBlockLocation::DirectBlock);
1397    }
1398    for table in &frame.tables {
1399        for cell in &table.cell_layouts {
1400            if cell.blocks.iter().any(|b| b.block_id == block_id) {
1401                return Some(FrameBlockLocation::TableCell(
1402                    table.table_id,
1403                    cell.row,
1404                    cell.column,
1405                ));
1406            }
1407        }
1408    }
1409    for nested in &frame.frames {
1410        if frame_contains_block(nested, block_id) {
1411            return Some(FrameBlockLocation::NestedFrame(nested.frame_id));
1412        }
1413    }
1414    None
1415}
1416
1417/// Replace a direct child block of `frame` and re-stack the frame's
1418/// children. The caller is responsible for routing blocks that live in
1419/// nested frames or table cells (see `relayout_block_deep_in_frame`) —
1420/// the block must have been laid out at this frame's `content_width`.
1421fn relayout_block_in_frame(frame: &mut FrameLayout, block_id: usize, new_block: BlockLayout) {
1422    if let Some(old) = frame.blocks.iter_mut().find(|b| b.block_id == block_id) {
1423        *old = new_block;
1424    }
1425    reposition_frame_children(frame);
1426}
1427
1428/// Relayout the block identified by `params.block_id` wherever it lives
1429/// inside `frame`: as a direct block, inside a cell of a frame-nested
1430/// table, or anywhere within a nested frame (recursing to any depth).
1431/// Each level re-stacks its children in flow order on the way back up,
1432/// so height changes propagate to the outermost frame's `total_height`.
1433fn relayout_block_deep_in_frame(
1434    frame: &mut FrameLayout,
1435    registry: &FontRegistry,
1436    params: &BlockLayoutParams,
1437    scale_factor: f32,
1438    font_scale: f32,
1439) {
1440    match find_block_location_in_frame(frame, params.block_id) {
1441        None => {}
1442        Some(FrameBlockLocation::DirectBlock) => {
1443            let new_block = layout_block(
1444                registry,
1445                params,
1446                frame.content_width,
1447                scale_factor,
1448                font_scale,
1449            );
1450            relayout_block_in_frame(frame, params.block_id, new_block);
1451        }
1452        Some(FrameBlockLocation::TableCell(table_id, row, col)) => {
1453            if let Some(table) = frame.tables.iter_mut().find(|t| t.table_id == table_id) {
1454                recompute_table_cell(table, registry, params, row, col, scale_factor, font_scale);
1455            }
1456            reposition_frame_children(frame);
1457        }
1458        Some(FrameBlockLocation::NestedFrame(nested_frame_id)) => {
1459            if let Some(nested) = frame
1460                .frames
1461                .iter_mut()
1462                .find(|f| f.frame_id == nested_frame_id)
1463            {
1464                relayout_block_deep_in_frame(nested, registry, params, scale_factor, font_scale);
1465            }
1466            reposition_frame_children(frame);
1467        }
1468    }
1469}
1470
1471/// Re-layout the block identified by `params.block_id` inside the cell at
1472/// `(row, col)`, then recompute the row height, row y positions, and the
1473/// table's `total_height`. Shared by the top-level table relayout path and
1474/// the frame-nested one; the caller propagates the height delta into its
1475/// own container.
1476fn recompute_table_cell(
1477    table: &mut crate::layout::table::TableLayout,
1478    registry: &FontRegistry,
1479    params: &BlockLayoutParams,
1480    row: usize,
1481    col: usize,
1482    scale_factor: f32,
1483    font_scale: f32,
1484) {
1485    let cell_width = table
1486        .column_content_widths
1487        .get(col)
1488        .copied()
1489        .unwrap_or(200.0);
1490
1491    // Find the cell and replace the block
1492    let cell = match table
1493        .cell_layouts
1494        .iter_mut()
1495        .find(|c| c.row == row && c.column == col)
1496    {
1497        Some(c) => c,
1498        None => return,
1499    };
1500
1501    let new_block = layout_block(registry, params, cell_width, scale_factor, font_scale);
1502    if let Some(old) = cell
1503        .blocks
1504        .iter_mut()
1505        .find(|b| b.block_id == params.block_id)
1506    {
1507        *old = new_block;
1508    }
1509
1510    // Reposition blocks within the cell and recompute cell height
1511    let mut block_y = 0.0f32;
1512    for block in &mut cell.blocks {
1513        block.y = block_y;
1514        block_y += block.height;
1515    }
1516    let cell_height = block_y;
1517
1518    // Recompute row height by scanning all cells in this row
1519    if row < table.row_heights.len() {
1520        let mut max_h = 0.0f32;
1521        for c in &table.cell_layouts {
1522            if c.row == row {
1523                let h: f32 = c.blocks.iter().map(|b| b.height).sum();
1524                max_h = max_h.max(h);
1525            }
1526        }
1527        // Also consider the cell we just updated
1528        max_h = max_h.max(cell_height);
1529        table.row_heights[row] = max_h;
1530    }
1531
1532    // Recompute row y positions and total height
1533    let border = table.border_width;
1534    let padding = table.cell_padding;
1535    let spacing = if table.row_ys.len() > 1 {
1536        // Infer spacing from existing layout
1537        if table.row_ys.len() >= 2 && !table.row_heights.is_empty() {
1538            let expected = table.row_ys[0] + padding + table.row_heights[0] + padding;
1539            (table.row_ys.get(1).copied().unwrap_or(expected) - expected).max(0.0)
1540        } else {
1541            0.0
1542        }
1543    } else {
1544        0.0
1545    };
1546    let mut y = border;
1547    for (r, &row_h) in table.row_heights.iter().enumerate() {
1548        if r < table.row_ys.len() {
1549            table.row_ys[r] = y + padding;
1550        }
1551        y += padding * 2.0 + row_h;
1552        if r < table.row_heights.len() - 1 {
1553            y += spacing;
1554        }
1555    }
1556    table.total_height = y + border;
1557}
1558
1559/// Recompute the y position of every direct child of `frame` in document
1560/// (flow) order, then update `content_height` / `total_height`.
1561///
1562/// Walks `frame.flow_order` so interleaved children keep their document
1563/// order — stacking the per-kind vecs one after another would visually
1564/// reorder a frame containing e.g. [block, table, block]. Placement
1565/// mirrors `layout_frame` exactly: blocks at `content_y + top_margin`
1566/// (no collapsing between frame children), tables and nested frames at
1567/// `content_y` advancing by `total_height`.
1568pub(crate) fn reposition_frame_children(frame: &mut FrameLayout) {
1569    let old_content_height = frame.content_height;
1570    let order = frame.flow_order.clone();
1571    let mut content_y = 0.0f32;
1572
1573    for child in &order {
1574        match child {
1575            crate::layout::frame::FrameChildRef::Block(bid) => {
1576                if let Some(block) = frame.blocks.iter_mut().find(|b| b.block_id == *bid) {
1577                    block.y = content_y + block.top_margin;
1578                    let block_content = block.height - block.top_margin - block.bottom_margin;
1579                    content_y = block.y + block_content + block.bottom_margin;
1580                }
1581            }
1582            crate::layout::frame::FrameChildRef::Table(tid) => {
1583                if let Some(table) = frame.tables.iter_mut().find(|t| t.table_id == *tid) {
1584                    table.y = content_y;
1585                    content_y += table.total_height;
1586                }
1587            }
1588            crate::layout::frame::FrameChildRef::Frame(fid) => {
1589                if let Some(nested) = frame.frames.iter_mut().find(|f| f.frame_id == *fid) {
1590                    nested.y = content_y;
1591                    content_y += nested.total_height;
1592                }
1593            }
1594        }
1595    }
1596
1597    frame.content_height = content_y;
1598    frame.total_height += content_y - old_content_height;
1599}