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    pub fn add_block(
316        &mut self,
317        registry: &FontRegistry,
318        params: &BlockLayoutParams,
319        available_width: f32,
320    ) {
321        let mut block = layout_block(
322            registry,
323            params,
324            available_width,
325            self.scale_factor,
326            self.font_scale,
327        );
328
329        // Margin collapsing with previous block
330        let mut y = self.content_height;
331        if let Some(FlowItem::Block {
332            block_id: prev_id, ..
333        }) = self.flow_order.last()
334        {
335            if let Some(prev_block) = self.blocks.get(prev_id) {
336                let collapsed = prev_block.bottom_margin.max(block.top_margin);
337                y -= prev_block.bottom_margin;
338                y += collapsed;
339            } else {
340                y += block.top_margin;
341            }
342        } else {
343            y += block.top_margin;
344        }
345
346        block.y = y;
347        let block_content = block.height - block.top_margin - block.bottom_margin;
348        y += block_content + block.bottom_margin;
349
350        self.flow_order.push(FlowItem::Block {
351            block_id: block.block_id,
352            y: block.y,
353            height: block.height,
354        });
355        self.update_max_width_for_block(&block);
356        self.blocks.insert(block.block_id, block);
357        self.content_height = y;
358    }
359
360    /// Lay out a sequence of blocks vertically.
361    pub fn layout_blocks(
362        &mut self,
363        registry: &FontRegistry,
364        block_params: Vec<BlockLayoutParams>,
365        available_width: f32,
366    ) {
367        self.clear();
368        // Note: viewport_width is NOT set here. It's a display property
369        // set by Typesetter::set_viewport(), not a layout property.
370        // available_width is the layout width which may differ from viewport
371        // when using ContentWidthMode::Fixed.
372        for params in &block_params {
373            self.add_block(registry, params, available_width);
374        }
375        // Capture the freshly-shaped blocks as the paint-overlay base. A full
376        // layout clears any prior overlay (see `clear`), so the live blocks ARE
377        // the base at this point; the engine applies paint spans afterward via
378        // `apply_paint_spans_for`.
379        self.refresh_base_blocks();
380    }
381
382    /// Update a single block's layout and shift subsequent items if height changed.
383    ///
384    /// Finds the block in top-level blocks, table cells, or frames, re-layouts
385    /// it, and propagates any height delta to subsequent flow items.
386    pub fn relayout_block(
387        &mut self,
388        registry: &FontRegistry,
389        params: &BlockLayoutParams,
390        available_width: f32,
391    ) {
392        let block_id = params.block_id;
393
394        // Top-level block
395        if self.blocks.contains_key(&block_id) {
396            self.relayout_top_level_block(registry, params, available_width);
397            self.refresh_base_and_overlay_block(block_id);
398            return;
399        }
400
401        // Table cell block: scan tables for the block_id
402        let table_match = self.tables.iter().find_map(|(&tid, table)| {
403            for cell in &table.cell_layouts {
404                if cell.blocks.iter().any(|b| b.block_id == block_id) {
405                    return Some((tid, cell.row, cell.column));
406                }
407            }
408            None
409        });
410        if let Some((table_id, row, col)) = table_match {
411            let old_char_len = block_char_len(find_block_ref(self, block_id));
412            self.relayout_table_block(registry, params, table_id, row, col);
413            let new_char_len = block_char_len(find_block_ref(self, block_id));
414            let char_delta = new_char_len as isize - old_char_len as isize;
415            self.shift_block_positions_after_table_block(table_id, block_id, char_delta);
416            self.refresh_base_and_overlay_block(block_id);
417            return;
418        }
419
420        // Frame block: scan frames (including nested frames) for the block_id
421        let frame_match = self.frames.iter().find_map(|(&fid, frame)| {
422            if frame_contains_block(frame, block_id) {
423                return Some(fid);
424            }
425            None
426        });
427        if let Some(frame_id) = frame_match {
428            let old_char_len = block_char_len(find_block_ref(self, block_id));
429            self.relayout_frame_block(registry, params, frame_id);
430            let new_char_len = block_char_len(find_block_ref(self, block_id));
431            let char_delta = new_char_len as isize - old_char_len as isize;
432            self.shift_block_positions_after_frame_block(frame_id, block_id, char_delta);
433            self.refresh_base_and_overlay_block(block_id);
434        }
435    }
436
437    /// Relayout a top-level block (existing logic).
438    fn relayout_top_level_block(
439        &mut self,
440        registry: &FontRegistry,
441        params: &BlockLayoutParams,
442        available_width: f32,
443    ) {
444        let block_id = params.block_id;
445        let old_y = self.blocks.get(&block_id).map(|b| b.y).unwrap_or(0.0);
446        let old_height = self.blocks.get(&block_id).map(|b| b.height).unwrap_or(0.0);
447        let old_top_margin = self
448            .blocks
449            .get(&block_id)
450            .map(|b| b.top_margin)
451            .unwrap_or(0.0);
452        let old_bottom_margin = self
453            .blocks
454            .get(&block_id)
455            .map(|b| b.bottom_margin)
456            .unwrap_or(0.0);
457        let old_content = old_height - old_top_margin - old_bottom_margin;
458        let old_end = old_y + old_content + old_bottom_margin;
459        let old_char_len = block_char_len(self.blocks.get(&block_id));
460
461        let mut block = layout_block(
462            registry,
463            params,
464            available_width,
465            self.scale_factor,
466            self.font_scale,
467        );
468        block.y = old_y;
469
470        if (block.top_margin - old_top_margin).abs() > 0.001 {
471            let prev_bm = self.prev_block_bottom_margin(block_id).unwrap_or(0.0);
472            let old_collapsed = prev_bm.max(old_top_margin);
473            let new_collapsed = prev_bm.max(block.top_margin);
474            block.y = old_y + (new_collapsed - old_collapsed);
475        }
476
477        let new_content = block.height - block.top_margin - block.bottom_margin;
478        let new_end = block.y + new_content + block.bottom_margin;
479        let delta = new_end - old_end;
480        let new_char_len = block_char_len(Some(&block));
481        let char_delta = new_char_len as isize - old_char_len as isize;
482
483        let new_y = block.y;
484        let new_height = block.height;
485        self.update_max_width_for_block(&block);
486        self.blocks.insert(block_id, block);
487
488        // Update flow_order entry
489        for item in &mut self.flow_order {
490            if let FlowItem::Block {
491                block_id: id,
492                y,
493                height,
494            } = item
495                && *id == block_id
496            {
497                *y = new_y;
498                *height = new_height;
499                break;
500            }
501        }
502
503        self.shift_items_after_block(block_id, delta);
504        self.shift_block_positions_after_block(block_id, char_delta);
505    }
506
507    /// Relayout a block inside a table cell. Recomputes the row height
508    /// and propagates any table height delta to subsequent flow items.
509    fn relayout_table_block(
510        &mut self,
511        registry: &FontRegistry,
512        params: &BlockLayoutParams,
513        table_id: usize,
514        row: usize,
515        col: usize,
516    ) {
517        let table = match self.tables.get_mut(&table_id) {
518            Some(t) => t,
519            None => return,
520        };
521
522        let old_table_height = table.total_height;
523        recompute_table_cell(
524            table,
525            registry,
526            params,
527            row,
528            col,
529            self.scale_factor,
530            self.font_scale,
531        );
532        let delta = table.total_height - old_table_height;
533
534        // Update flow_order entry for this table
535        for item in &mut self.flow_order {
536            if let FlowItem::Table {
537                table_id: id,
538                height,
539                ..
540            } = item
541                && *id == table_id
542            {
543                *height = table.total_height;
544                break;
545            }
546        }
547
548        self.shift_items_after_table(table_id, delta);
549    }
550
551    /// Relayout a block inside a frame. Handles direct blocks, blocks in
552    /// cells of frame-nested tables, and blocks in nested frames (any
553    /// depth). Recomputes frame content height and propagates any height
554    /// delta to subsequent flow items.
555    fn relayout_frame_block(
556        &mut self,
557        registry: &FontRegistry,
558        params: &BlockLayoutParams,
559        frame_id: usize,
560    ) {
561        let old_total_height = match self.frames.get(&frame_id) {
562            Some(f) => f.total_height,
563            None => return,
564        };
565
566        {
567            let frame = self.frames.get_mut(&frame_id).unwrap();
568            relayout_block_deep_in_frame(
569                frame,
570                registry,
571                params,
572                self.scale_factor,
573                self.font_scale,
574            );
575        }
576
577        let new_total_height = self.frames[&frame_id].total_height;
578        let delta = new_total_height - old_total_height;
579
580        for item in &mut self.flow_order {
581            if let FlowItem::Frame {
582                frame_id: id,
583                height,
584                ..
585            } = item
586                && *id == frame_id
587            {
588                *height = new_total_height;
589                break;
590            }
591        }
592
593        self.shift_items_after_frame(frame_id, delta);
594    }
595
596    /// Shift the document-character `position` of every block that appears
597    /// after the given target block in flow order by `char_delta` characters.
598    ///
599    /// `shift_items_after_block` only propagates the vertical pixel delta.
600    /// This method propagates the character delta so hit_test and caret_rect
601    /// keep returning correct document positions after an incremental
602    /// relayout that changed the target block's char length (e.g. a cut or
603    /// paste inside a non-last paragraph).
604    fn shift_block_positions_after_block(&mut self, block_id: usize, char_delta: isize) {
605        self.shift_block_positions_after_flow_item(FlowItemRef::Block(block_id), char_delta);
606    }
607
608    /// Shift the document-character `position` of every block belonging to
609    /// a flow item that appears after `target` in flow order.
610    fn shift_block_positions_after_flow_item(&mut self, target: FlowItemRef, char_delta: isize) {
611        if char_delta == 0 {
612            return;
613        }
614        // Snapshot the order of items so we can mutate the containing
615        // HashMaps inside the loop.
616        let refs: Vec<FlowItemRef> = self
617            .flow_order
618            .iter()
619            .map(|item| match item {
620                FlowItem::Block { block_id, .. } => FlowItemRef::Block(*block_id),
621                FlowItem::Table { table_id, .. } => FlowItemRef::Table(*table_id),
622                FlowItem::Frame { frame_id, .. } => FlowItemRef::Frame(*frame_id),
623            })
624            .collect();
625        let mut found = false;
626        for r in refs {
627            if found {
628                match r {
629                    FlowItemRef::Block(id) => {
630                        if let Some(b) = self.blocks.get_mut(&id) {
631                            b.position = apply_char_delta(b.position, char_delta);
632                        }
633                    }
634                    FlowItemRef::Table(id) => {
635                        if let Some(t) = self.tables.get_mut(&id) {
636                            shift_block_positions_in_table(t, char_delta);
637                        }
638                    }
639                    FlowItemRef::Frame(id) => {
640                        if let Some(f) = self.frames.get_mut(&id) {
641                            shift_block_positions_in_frame(f, char_delta);
642                        }
643                    }
644                }
645            } else if r == target {
646                found = true;
647            }
648        }
649    }
650
651    /// Shift document positions after an edit inside a top-level table's
652    /// cell: cell blocks after the edited block within the table, then
653    /// every flow item after the table.
654    fn shift_block_positions_after_table_block(
655        &mut self,
656        table_id: usize,
657        block_id: usize,
658        char_delta: isize,
659    ) {
660        if char_delta == 0 {
661            return;
662        }
663        if let Some(table) = self.tables.get_mut(&table_id) {
664            shift_table_positions_after_block(table, block_id, char_delta);
665        }
666        self.shift_block_positions_after_flow_item(FlowItemRef::Table(table_id), char_delta);
667    }
668
669    /// Shift document positions after an edit inside a frame (any depth):
670    /// frame content after the edited block, then every flow item after
671    /// the frame.
672    fn shift_block_positions_after_frame_block(
673        &mut self,
674        frame_id: usize,
675        block_id: usize,
676        char_delta: isize,
677    ) {
678        if char_delta == 0 {
679            return;
680        }
681        if let Some(frame) = self.frames.get_mut(&frame_id) {
682            shift_frame_positions_after_block(frame, block_id, char_delta);
683        }
684        self.shift_block_positions_after_flow_item(FlowItemRef::Frame(frame_id), char_delta);
685    }
686
687    /// Shift all flow items after the given block by `delta` pixels.
688    fn shift_items_after_block(&mut self, block_id: usize, delta: f32) {
689        if delta.abs() <= 0.001 {
690            return;
691        }
692        let mut found = false;
693        for item in &mut self.flow_order {
694            match item {
695                FlowItem::Block {
696                    block_id: id, y, ..
697                } => {
698                    if found {
699                        *y += delta;
700                        if let Some(b) = self.blocks.get_mut(id) {
701                            b.y += delta;
702                        }
703                    }
704                    if *id == block_id {
705                        found = true;
706                    }
707                }
708                FlowItem::Table {
709                    table_id: id, y, ..
710                } => {
711                    if found {
712                        *y += delta;
713                        if let Some(t) = self.tables.get_mut(id) {
714                            t.y += delta;
715                        }
716                    }
717                }
718                FlowItem::Frame {
719                    frame_id: id, y, ..
720                } => {
721                    if found {
722                        *y += delta;
723                        if let Some(f) = self.frames.get_mut(id) {
724                            f.y += delta;
725                        }
726                    }
727                }
728            }
729        }
730        self.content_height += delta;
731    }
732
733    /// Shift all flow items after the given table by `delta` pixels.
734    fn shift_items_after_table(&mut self, table_id: usize, delta: f32) {
735        if delta.abs() <= 0.001 {
736            return;
737        }
738        let mut found = false;
739        for item in &mut self.flow_order {
740            match item {
741                FlowItem::Table {
742                    table_id: id, y, ..
743                } => {
744                    if *id == table_id {
745                        found = true;
746                        continue;
747                    }
748                    if found {
749                        *y += delta;
750                        if let Some(t) = self.tables.get_mut(id) {
751                            t.y += delta;
752                        }
753                    }
754                }
755                FlowItem::Block {
756                    block_id: id, y, ..
757                } => {
758                    if found {
759                        *y += delta;
760                        if let Some(b) = self.blocks.get_mut(id) {
761                            b.y += delta;
762                        }
763                    }
764                }
765                FlowItem::Frame {
766                    frame_id: id, y, ..
767                } => {
768                    if found {
769                        *y += delta;
770                        if let Some(f) = self.frames.get_mut(id) {
771                            f.y += delta;
772                        }
773                    }
774                }
775            }
776        }
777        self.content_height += delta;
778    }
779
780    /// Shift all flow items after the given frame by `delta` pixels.
781    fn shift_items_after_frame(&mut self, frame_id: usize, delta: f32) {
782        if delta.abs() <= 0.001 {
783            return;
784        }
785        let mut found = false;
786        for item in &mut self.flow_order {
787            match item {
788                FlowItem::Frame {
789                    frame_id: id, y, ..
790                } => {
791                    if *id == frame_id {
792                        found = true;
793                        continue;
794                    }
795                    if found {
796                        *y += delta;
797                        if let Some(f) = self.frames.get_mut(id) {
798                            f.y += delta;
799                        }
800                    }
801                }
802                FlowItem::Block {
803                    block_id: id, y, ..
804                } => {
805                    if found {
806                        *y += delta;
807                        if let Some(b) = self.blocks.get_mut(id) {
808                            b.y += delta;
809                        }
810                    }
811                }
812                FlowItem::Table {
813                    table_id: id, y, ..
814                } => {
815                    if found {
816                        *y += delta;
817                        if let Some(t) = self.tables.get_mut(id) {
818                            t.y += delta;
819                        }
820                    }
821                }
822            }
823        }
824        self.content_height += delta;
825    }
826
827    /// Update the cached max content width considering a single block's lines.
828    fn update_max_width_for_block(&mut self, block: &BlockLayout) {
829        for line in &block.lines {
830            let w = line.width + block.left_margin + block.right_margin;
831            if w > self.cached_max_content_width {
832                self.cached_max_content_width = w;
833            }
834        }
835    }
836
837    /// Find the bottom margin of the block immediately before `block_id` in flow order.
838    fn prev_block_bottom_margin(&self, block_id: usize) -> Option<f32> {
839        let mut prev_bm = None;
840        for item in &self.flow_order {
841            match item {
842                FlowItem::Block { block_id: id, .. } => {
843                    if *id == block_id {
844                        return prev_bm;
845                    }
846                    if let Some(b) = self.blocks.get(id) {
847                        prev_bm = Some(b.bottom_margin);
848                    }
849                }
850                _ => {
851                    // Non-block items reset margin collapsing
852                    prev_bm = None;
853                }
854            }
855        }
856        None
857    }
858}
859
860#[derive(Clone, Copy, PartialEq, Eq)]
861enum FlowItemRef {
862    Block(usize),
863    Table(usize),
864    Frame(usize),
865}
866
867fn block_char_len(block: Option<&BlockLayout>) -> usize {
868    block
869        .and_then(|b| b.lines.last().map(|l| l.char_range.end))
870        .unwrap_or(0)
871}
872
873fn apply_char_delta(position: usize, delta: isize) -> usize {
874    if delta >= 0 {
875        position + delta as usize
876    } else {
877        position.saturating_sub((-delta) as usize)
878    }
879}
880
881fn shift_block_positions_in_slice(blocks: &mut [BlockLayout], delta: isize) {
882    for block in blocks {
883        block.position = apply_char_delta(block.position, delta);
884    }
885}
886
887fn shift_block_positions_in_table(table: &mut TableLayout, delta: isize) {
888    for cell in &mut table.cell_layouts {
889        shift_block_positions_in_slice(&mut cell.blocks, delta);
890    }
891}
892
893fn shift_block_positions_in_frame(frame: &mut FrameLayout, delta: isize) {
894    shift_block_positions_in_slice(&mut frame.blocks, delta);
895    for table in &mut frame.tables {
896        shift_block_positions_in_table(table, delta);
897    }
898    for nested in &mut frame.frames {
899        shift_block_positions_in_frame(nested, delta);
900    }
901}
902
903/// Shift the `position` of every cell block that comes after `block_id`
904/// within `table` (cells iterate in document/row-major order). Returns
905/// `true` when the edited block was found in this table.
906fn shift_table_positions_after_block(
907    table: &mut TableLayout,
908    block_id: usize,
909    delta: isize,
910) -> bool {
911    let mut found = false;
912    for cell in &mut table.cell_layouts {
913        for b in &mut cell.blocks {
914            if found {
915                b.position = apply_char_delta(b.position, delta);
916            } else if b.block_id == block_id {
917                found = true;
918            }
919        }
920    }
921    found
922}
923
924/// Shift the `position` of every block that comes after `block_id` in
925/// `frame`'s document order — walking `flow_order` so interleaved blocks,
926/// tables, and nested frames shift correctly. Returns `true` when the
927/// edited block was found in this frame tree.
928fn shift_frame_positions_after_block(
929    frame: &mut FrameLayout,
930    block_id: usize,
931    delta: isize,
932) -> bool {
933    let order = frame.flow_order.clone();
934    let mut found = false;
935    for child in &order {
936        match child {
937            crate::layout::frame::FrameChildRef::Block(bid) => {
938                if found {
939                    if let Some(b) = frame.blocks.iter_mut().find(|b| b.block_id == *bid) {
940                        b.position = apply_char_delta(b.position, delta);
941                    }
942                } else if *bid == block_id {
943                    found = true;
944                }
945            }
946            crate::layout::frame::FrameChildRef::Table(tid) => {
947                if let Some(t) = frame.tables.iter_mut().find(|t| t.table_id == *tid) {
948                    if found {
949                        shift_block_positions_in_table(t, delta);
950                    } else {
951                        found = shift_table_positions_after_block(t, block_id, delta);
952                    }
953                }
954            }
955            crate::layout::frame::FrameChildRef::Frame(fid) => {
956                if let Some(nested) = frame.frames.iter_mut().find(|f| f.frame_id == *fid) {
957                    if found {
958                        shift_block_positions_in_frame(nested, delta);
959                    } else {
960                        found = shift_frame_positions_after_block(nested, block_id, delta);
961                    }
962                }
963            }
964        }
965    }
966    found
967}
968
969// ── Paint-overlay helpers (recolor without reshape/reflow) ──────────────────
970
971/// Re-derive `b` in place from its base + pending overlay. No-op if no base
972/// was captured for it.
973fn overlay_block_in_place(
974    b: &mut BlockLayout,
975    base: &HashMap<usize, BlockLayout>,
976    pending: &HashMap<usize, Vec<PaintSpan>>,
977) {
978    if let Some(base_b) = base.get(&b.block_id) {
979        let empty: Vec<PaintSpan> = Vec::new();
980        let spans = pending.get(&b.block_id).unwrap_or(&empty);
981        *b = apply_paint_spans(base_b, spans);
982    }
983}
984
985/// Re-derive every block in a frame (recursively) from base + pending.
986fn overlay_frame_in_place(
987    frame: &mut FrameLayout,
988    base: &HashMap<usize, BlockLayout>,
989    pending: &HashMap<usize, Vec<PaintSpan>>,
990) {
991    for b in &mut frame.blocks {
992        overlay_block_in_place(b, base, pending);
993    }
994    for t in &mut frame.tables {
995        for c in &mut t.cell_layouts {
996            for b in &mut c.blocks {
997                overlay_block_in_place(b, base, pending);
998            }
999        }
1000    }
1001    for nested in &mut frame.frames {
1002        overlay_frame_in_place(nested, base, pending);
1003    }
1004}
1005
1006/// Re-derive a single block (by id) inside a frame (recursively). Returns true
1007/// if found.
1008fn overlay_one_in_frame(
1009    frame: &mut FrameLayout,
1010    block_id: usize,
1011    base: &HashMap<usize, BlockLayout>,
1012    pending: &HashMap<usize, Vec<PaintSpan>>,
1013) -> bool {
1014    for b in &mut frame.blocks {
1015        if b.block_id == block_id {
1016            overlay_block_in_place(b, base, pending);
1017            return true;
1018        }
1019    }
1020    for t in &mut frame.tables {
1021        for c in &mut t.cell_layouts {
1022            for b in &mut c.blocks {
1023                if b.block_id == block_id {
1024                    overlay_block_in_place(b, base, pending);
1025                    return true;
1026                }
1027            }
1028        }
1029    }
1030    for nested in &mut frame.frames {
1031        if overlay_one_in_frame(nested, block_id, base, pending) {
1032            return true;
1033        }
1034    }
1035    false
1036}
1037
1038fn collect_table_base(t: &TableLayout, out: &mut Vec<(usize, BlockLayout)>) {
1039    for c in &t.cell_layouts {
1040        for b in &c.blocks {
1041            out.push((b.block_id, b.clone()));
1042        }
1043    }
1044}
1045
1046fn collect_frame_base(f: &FrameLayout, out: &mut Vec<(usize, BlockLayout)>) {
1047    for b in &f.blocks {
1048        out.push((b.block_id, b.clone()));
1049    }
1050    for t in &f.tables {
1051        collect_table_base(t, out);
1052    }
1053    for nested in &f.frames {
1054        collect_frame_base(nested, out);
1055    }
1056}
1057
1058/// Find a block by id across top-level / table cells / frames.
1059fn find_block_ref(flow: &FlowLayout, block_id: usize) -> Option<&BlockLayout> {
1060    if let Some(b) = flow.blocks.get(&block_id) {
1061        return Some(b);
1062    }
1063    for t in flow.tables.values() {
1064        for c in &t.cell_layouts {
1065            for b in &c.blocks {
1066                if b.block_id == block_id {
1067                    return Some(b);
1068                }
1069            }
1070        }
1071    }
1072    for f in flow.frames.values() {
1073        if let Some(b) = find_block_in_frame(f, block_id) {
1074            return Some(b);
1075        }
1076    }
1077    None
1078}
1079
1080fn find_block_in_frame(frame: &FrameLayout, block_id: usize) -> Option<&BlockLayout> {
1081    for b in &frame.blocks {
1082        if b.block_id == block_id {
1083            return Some(b);
1084        }
1085    }
1086    for t in &frame.tables {
1087        for c in &t.cell_layouts {
1088            for b in &c.blocks {
1089                if b.block_id == block_id {
1090                    return Some(b);
1091                }
1092            }
1093        }
1094    }
1095    for nested in &frame.frames {
1096        if let Some(b) = find_block_in_frame(nested, block_id) {
1097            return Some(b);
1098        }
1099    }
1100    None
1101}
1102
1103/// Check whether a frame (or any of its nested frames) contains a block
1104/// with the given id — including blocks inside cells of tables that are
1105/// direct children of the frame.
1106pub(crate) fn frame_contains_block(frame: &FrameLayout, block_id: usize) -> bool {
1107    if frame.blocks.iter().any(|b| b.block_id == block_id) {
1108        return true;
1109    }
1110    for table in &frame.tables {
1111        for cell in &table.cell_layouts {
1112            if cell.blocks.iter().any(|b| b.block_id == block_id) {
1113                return true;
1114            }
1115        }
1116    }
1117    frame
1118        .frames
1119        .iter()
1120        .any(|nested| frame_contains_block(nested, block_id))
1121}
1122
1123/// Where inside a frame a given block lives (one level deep — nested
1124/// frames are reported as `NestedFrame` and must be descended into).
1125pub(crate) enum FrameBlockLocation {
1126    /// A direct child block of the frame.
1127    DirectBlock,
1128    /// A block inside a cell of a table that is a direct child of the
1129    /// frame. Carries `(table_id, row, column)`.
1130    TableCell(usize, usize, usize),
1131    /// A block somewhere inside a nested frame (carries the nested
1132    /// frame's id).
1133    NestedFrame(usize),
1134}
1135
1136/// Locate `block_id` among the direct children of `frame`.
1137pub(crate) fn find_block_location_in_frame(
1138    frame: &FrameLayout,
1139    block_id: usize,
1140) -> Option<FrameBlockLocation> {
1141    if frame.blocks.iter().any(|b| b.block_id == block_id) {
1142        return Some(FrameBlockLocation::DirectBlock);
1143    }
1144    for table in &frame.tables {
1145        for cell in &table.cell_layouts {
1146            if cell.blocks.iter().any(|b| b.block_id == block_id) {
1147                return Some(FrameBlockLocation::TableCell(
1148                    table.table_id,
1149                    cell.row,
1150                    cell.column,
1151                ));
1152            }
1153        }
1154    }
1155    for nested in &frame.frames {
1156        if frame_contains_block(nested, block_id) {
1157            return Some(FrameBlockLocation::NestedFrame(nested.frame_id));
1158        }
1159    }
1160    None
1161}
1162
1163/// Replace a direct child block of `frame` and re-stack the frame's
1164/// children. The caller is responsible for routing blocks that live in
1165/// nested frames or table cells (see `relayout_block_deep_in_frame`) —
1166/// the block must have been laid out at this frame's `content_width`.
1167fn relayout_block_in_frame(frame: &mut FrameLayout, block_id: usize, new_block: BlockLayout) {
1168    if let Some(old) = frame.blocks.iter_mut().find(|b| b.block_id == block_id) {
1169        *old = new_block;
1170    }
1171    reposition_frame_children(frame);
1172}
1173
1174/// Relayout the block identified by `params.block_id` wherever it lives
1175/// inside `frame`: as a direct block, inside a cell of a frame-nested
1176/// table, or anywhere within a nested frame (recursing to any depth).
1177/// Each level re-stacks its children in flow order on the way back up,
1178/// so height changes propagate to the outermost frame's `total_height`.
1179fn relayout_block_deep_in_frame(
1180    frame: &mut FrameLayout,
1181    registry: &FontRegistry,
1182    params: &BlockLayoutParams,
1183    scale_factor: f32,
1184    font_scale: f32,
1185) {
1186    match find_block_location_in_frame(frame, params.block_id) {
1187        None => {}
1188        Some(FrameBlockLocation::DirectBlock) => {
1189            let new_block = layout_block(
1190                registry,
1191                params,
1192                frame.content_width,
1193                scale_factor,
1194                font_scale,
1195            );
1196            relayout_block_in_frame(frame, params.block_id, new_block);
1197        }
1198        Some(FrameBlockLocation::TableCell(table_id, row, col)) => {
1199            if let Some(table) = frame.tables.iter_mut().find(|t| t.table_id == table_id) {
1200                recompute_table_cell(table, registry, params, row, col, scale_factor, font_scale);
1201            }
1202            reposition_frame_children(frame);
1203        }
1204        Some(FrameBlockLocation::NestedFrame(nested_frame_id)) => {
1205            if let Some(nested) = frame
1206                .frames
1207                .iter_mut()
1208                .find(|f| f.frame_id == nested_frame_id)
1209            {
1210                relayout_block_deep_in_frame(nested, registry, params, scale_factor, font_scale);
1211            }
1212            reposition_frame_children(frame);
1213        }
1214    }
1215}
1216
1217/// Re-layout the block identified by `params.block_id` inside the cell at
1218/// `(row, col)`, then recompute the row height, row y positions, and the
1219/// table's `total_height`. Shared by the top-level table relayout path and
1220/// the frame-nested one; the caller propagates the height delta into its
1221/// own container.
1222fn recompute_table_cell(
1223    table: &mut crate::layout::table::TableLayout,
1224    registry: &FontRegistry,
1225    params: &BlockLayoutParams,
1226    row: usize,
1227    col: usize,
1228    scale_factor: f32,
1229    font_scale: f32,
1230) {
1231    let cell_width = table
1232        .column_content_widths
1233        .get(col)
1234        .copied()
1235        .unwrap_or(200.0);
1236
1237    // Find the cell and replace the block
1238    let cell = match table
1239        .cell_layouts
1240        .iter_mut()
1241        .find(|c| c.row == row && c.column == col)
1242    {
1243        Some(c) => c,
1244        None => return,
1245    };
1246
1247    let new_block = layout_block(registry, params, cell_width, scale_factor, font_scale);
1248    if let Some(old) = cell
1249        .blocks
1250        .iter_mut()
1251        .find(|b| b.block_id == params.block_id)
1252    {
1253        *old = new_block;
1254    }
1255
1256    // Reposition blocks within the cell and recompute cell height
1257    let mut block_y = 0.0f32;
1258    for block in &mut cell.blocks {
1259        block.y = block_y;
1260        block_y += block.height;
1261    }
1262    let cell_height = block_y;
1263
1264    // Recompute row height by scanning all cells in this row
1265    if row < table.row_heights.len() {
1266        let mut max_h = 0.0f32;
1267        for c in &table.cell_layouts {
1268            if c.row == row {
1269                let h: f32 = c.blocks.iter().map(|b| b.height).sum();
1270                max_h = max_h.max(h);
1271            }
1272        }
1273        // Also consider the cell we just updated
1274        max_h = max_h.max(cell_height);
1275        table.row_heights[row] = max_h;
1276    }
1277
1278    // Recompute row y positions and total height
1279    let border = table.border_width;
1280    let padding = table.cell_padding;
1281    let spacing = if table.row_ys.len() > 1 {
1282        // Infer spacing from existing layout
1283        if table.row_ys.len() >= 2 && !table.row_heights.is_empty() {
1284            let expected = table.row_ys[0] + padding + table.row_heights[0] + padding;
1285            (table.row_ys.get(1).copied().unwrap_or(expected) - expected).max(0.0)
1286        } else {
1287            0.0
1288        }
1289    } else {
1290        0.0
1291    };
1292    let mut y = border;
1293    for (r, &row_h) in table.row_heights.iter().enumerate() {
1294        if r < table.row_ys.len() {
1295            table.row_ys[r] = y + padding;
1296        }
1297        y += padding * 2.0 + row_h;
1298        if r < table.row_heights.len() - 1 {
1299            y += spacing;
1300        }
1301    }
1302    table.total_height = y + border;
1303}
1304
1305/// Recompute the y position of every direct child of `frame` in document
1306/// (flow) order, then update `content_height` / `total_height`.
1307///
1308/// Walks `frame.flow_order` so interleaved children keep their document
1309/// order — stacking the per-kind vecs one after another would visually
1310/// reorder a frame containing e.g. [block, table, block]. Placement
1311/// mirrors `layout_frame` exactly: blocks at `content_y + top_margin`
1312/// (no collapsing between frame children), tables and nested frames at
1313/// `content_y` advancing by `total_height`.
1314pub(crate) fn reposition_frame_children(frame: &mut FrameLayout) {
1315    let old_content_height = frame.content_height;
1316    let order = frame.flow_order.clone();
1317    let mut content_y = 0.0f32;
1318
1319    for child in &order {
1320        match child {
1321            crate::layout::frame::FrameChildRef::Block(bid) => {
1322                if let Some(block) = frame.blocks.iter_mut().find(|b| b.block_id == *bid) {
1323                    block.y = content_y + block.top_margin;
1324                    let block_content = block.height - block.top_margin - block.bottom_margin;
1325                    content_y = block.y + block_content + block.bottom_margin;
1326                }
1327            }
1328            crate::layout::frame::FrameChildRef::Table(tid) => {
1329                if let Some(table) = frame.tables.iter_mut().find(|t| t.table_id == *tid) {
1330                    table.y = content_y;
1331                    content_y += table.total_height;
1332                }
1333            }
1334            crate::layout::frame::FrameChildRef::Frame(fid) => {
1335                if let Some(nested) = frame.frames.iter_mut().find(|f| f.frame_id == *fid) {
1336                    nested.y = content_y;
1337                    content_y += nested.total_height;
1338                }
1339            }
1340        }
1341    }
1342
1343    frame.content_height = content_y;
1344    frame.total_height += content_y - old_content_height;
1345}