Skip to main content

azul_layout/solver3/
display_list.rs

1//! Generates a renderer-agnostic display list from a laid-out tree
2
3use std::{collections::BTreeMap, sync::Arc};
4
5use allsorts::glyph_position;
6use azul_core::{
7    dom::{DomId, FormattingContext, NodeId, NodeType, ScrollbarOrientation},
8    geom::{LogicalPosition, LogicalRect, LogicalSize},
9    gpu::GpuValueCache,
10    hit_test::ScrollPosition,
11    hit_test_tag::{CursorType, TAG_TYPE_CURSOR},
12    resources::{
13        IdNamespace, ImageRef, OpacityKey, RendererResources, TransformKey,
14    },
15    transform::ComputedTransform3D,
16    selection::{Selection, SelectionRange, SelectionState, TextSelection},
17    styled_dom::StyledDom,
18    ui_solver::GlyphInstance,
19};
20use azul_css::{
21    css::CssPropertyValue,
22    format_rust_code::GetHash,
23    props::{
24        basic::{ColorU, FontRef, PixelValue},
25        layout::{LayoutDisplay, LayoutOverflow, LayoutPosition},
26        property::{CssProperty, CssPropertyType},
27        style::{
28            background::{ConicGradient, ExtendMode, LinearGradient, RadialGradient},
29            border_radius::StyleBorderRadius,
30            box_shadow::{BoxShadowClipMode, StyleBoxShadow},
31            filter::{StyleFilter, StyleFilterVec},
32            BorderStyle, LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth,
33            LayoutBorderTopWidth, StyleBorderBottomColor, StyleBorderBottomStyle,
34            StyleBorderLeftColor, StyleBorderLeftStyle, StyleBorderRightColor,
35            StyleBorderRightStyle, StyleBorderTopColor, StyleBorderTopStyle,
36        },
37    },
38    LayoutDebugMessage,
39};
40
41#[cfg(feature = "text_layout")]
42use crate::text3;
43#[cfg(feature = "text_layout")]
44use crate::text3::cache::{InlineShape, PositionedItem};
45use crate::{
46    debug_info,
47    font_traits::{
48        FontHash, FontLoaderTrait, ImageSource, InlineContent, ParsedFontTrait, ShapedItem,
49        UnifiedLayout,
50    },
51    solver3::{
52        getters::{
53            get_background_color, get_background_contents, get_border_info, get_border_radius,
54            get_break_after, get_break_before, get_caret_style, get_overflow_x, get_overflow_y,
55            get_scrollbar_info_from_layout, get_scrollbar_style, get_selection_style,
56            get_style_border_radius, get_z_index, is_forced_page_break, BorderInfo, CaretStyle,
57            ComputedScrollbarStyle, SelectionStyle,
58        },
59        layout_tree::{LayoutNode, LayoutTree},
60        positioning::get_position_type,
61        scrollbar::ScrollbarRequirements,
62        LayoutContext, LayoutError, Result,
63    },
64};
65
66/// Border widths for all four sides.
67///
68/// Each field is optional to allow partial border specifications.
69/// Used in [`DisplayListItem::Border`] to specify per-side border widths.
70#[derive(Debug, Clone, Copy)]
71pub struct StyleBorderWidths {
72    /// Top border width (CSS `border-top-width`)
73    pub top: Option<CssPropertyValue<LayoutBorderTopWidth>>,
74    /// Right border width (CSS `border-right-width`)
75    pub right: Option<CssPropertyValue<LayoutBorderRightWidth>>,
76    /// Bottom border width (CSS `border-bottom-width`)
77    pub bottom: Option<CssPropertyValue<LayoutBorderBottomWidth>>,
78    /// Left border width (CSS `border-left-width`)
79    pub left: Option<CssPropertyValue<LayoutBorderLeftWidth>>,
80}
81
82/// Border colors for all four sides.
83///
84/// Each field is optional to allow partial border specifications.
85/// Used in [`DisplayListItem::Border`] to specify per-side border colors.
86#[derive(Debug, Clone, Copy)]
87pub struct StyleBorderColors {
88    /// Top border color (CSS `border-top-color`)
89    pub top: Option<CssPropertyValue<StyleBorderTopColor>>,
90    /// Right border color (CSS `border-right-color`)
91    pub right: Option<CssPropertyValue<StyleBorderRightColor>>,
92    /// Bottom border color (CSS `border-bottom-color`)
93    pub bottom: Option<CssPropertyValue<StyleBorderBottomColor>>,
94    /// Left border color (CSS `border-left-color`)
95    pub left: Option<CssPropertyValue<StyleBorderLeftColor>>,
96}
97
98/// Border styles for all four sides.
99///
100/// Each field is optional to allow partial border specifications.
101/// Used in [`DisplayListItem::Border`] to specify per-side border styles
102/// (solid, dashed, dotted, none, etc.).
103#[derive(Debug, Clone, Copy)]
104pub struct StyleBorderStyles {
105    /// Top border style (CSS `border-top-style`)
106    pub top: Option<CssPropertyValue<StyleBorderTopStyle>>,
107    /// Right border style (CSS `border-right-style`)
108    pub right: Option<CssPropertyValue<StyleBorderRightStyle>>,
109    /// Bottom border style (CSS `border-bottom-style`)
110    pub bottom: Option<CssPropertyValue<StyleBorderBottomStyle>>,
111    /// Left border style (CSS `border-left-style`)
112    pub left: Option<CssPropertyValue<StyleBorderLeftStyle>>,
113}
114
115/// A rectangle in border-box coordinates (includes padding and border).
116/// This is what layout calculates and stores in `used_size` and absolute positions.
117#[derive(Debug, Clone, Copy, PartialEq)]
118pub struct BorderBoxRect(pub LogicalRect);
119
120/// Simple struct for passing element dimensions to border-radius calculation
121#[derive(Debug, Clone, Copy)]
122pub struct PhysicalSizeImport {
123    pub width: f32,
124    pub height: f32,
125}
126
127/// Complete drawing information for a scrollbar with all visual components.
128///
129/// This contains the resolved geometry and colors for all scrollbar parts:
130/// - Track: The background area where the thumb slides
131/// - Thumb: The draggable indicator showing current scroll position
132/// - Buttons: Optional up/down or left/right arrow buttons
133/// - Corner: The area where horizontal and vertical scrollbars meet
134#[derive(Debug, Clone)]
135pub struct ScrollbarDrawInfo {
136    /// Overall bounds of the entire scrollbar (including track and buttons)
137    pub bounds: LogicalRect,
138    /// Scrollbar orientation (horizontal or vertical)
139    pub orientation: ScrollbarOrientation,
140
141    // Track area (the background rail)
142    /// Bounds of the track area
143    pub track_bounds: LogicalRect,
144    /// Color of the track background
145    pub track_color: ColorU,
146
147    // Thumb (the draggable part)
148    /// Bounds of the thumb
149    pub thumb_bounds: LogicalRect,
150    /// Color of the thumb
151    pub thumb_color: ColorU,
152    /// Border radius for rounded thumb corners
153    pub thumb_border_radius: BorderRadius,
154
155    // Optional buttons (arrows at ends)
156    /// Optional decrement button bounds (up/left arrow)
157    pub button_decrement_bounds: Option<LogicalRect>,
158    /// Optional increment button bounds (down/right arrow)
159    pub button_increment_bounds: Option<LogicalRect>,
160    /// Color for buttons
161    pub button_color: ColorU,
162
163    /// Optional opacity key for GPU-side fading animation.
164    pub opacity_key: Option<OpacityKey>,
165    /// Optional hit-test ID for WebRender hit-testing.
166    pub hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
167    /// Whether to clip scrollbar to container's border-radius
168    pub clip_to_container_border: bool,
169    /// Container's border-radius (for clipping)
170    pub container_border_radius: BorderRadius,
171}
172
173impl BorderBoxRect {
174    /// Convert border-box to content-box by subtracting padding and border.
175    /// Content-box is where inline layout and text actually render.
176    pub fn to_content_box(
177        self,
178        padding: &crate::solver3::geometry::EdgeSizes,
179        border: &crate::solver3::geometry::EdgeSizes,
180    ) -> ContentBoxRect {
181        ContentBoxRect(LogicalRect {
182            origin: LogicalPosition {
183                x: self.0.origin.x + padding.left + border.left,
184                y: self.0.origin.y + padding.top + border.top,
185            },
186            size: LogicalSize {
187                width: self.0.size.width
188                    - padding.left
189                    - padding.right
190                    - border.left
191                    - border.right,
192                height: self.0.size.height
193                    - padding.top
194                    - padding.bottom
195                    - border.top
196                    - border.bottom,
197            },
198        })
199    }
200
201    /// Get the inner LogicalRect
202    pub fn rect(&self) -> LogicalRect {
203        self.0
204    }
205}
206
207/// A rectangle in content-box coordinates (excludes padding and border).
208/// This is where text and inline content is positioned by the inline formatter.
209#[derive(Debug, Clone, Copy, PartialEq)]
210pub struct ContentBoxRect(pub LogicalRect);
211
212impl ContentBoxRect {
213    /// Get the inner LogicalRect
214    pub fn rect(&self) -> LogicalRect {
215        self.0
216    }
217}
218
219/// The final, renderer-agnostic output of the layout engine.
220///
221/// This is a flat list of drawing and state-management commands, already sorted
222/// according to the CSS paint order. A renderer can consume this list directly.
223#[derive(Debug, Default)]
224pub struct DisplayList {
225    pub items: Vec<DisplayListItem>,
226    /// Optional mapping from item index to the DOM NodeId that generated it.
227    /// Used for pagination to look up CSS break properties.
228    /// Not all items have a source node (e.g., synthesized decorations).
229    pub node_mapping: Vec<Option<NodeId>>,
230    /// Y-positions where forced page breaks should occur (from break-before/break-after: always).
231    /// These are absolute Y coordinates in the infinite canvas coordinate system.
232    /// The slicer will ensure page boundaries align with these positions.
233    pub forced_page_breaks: Vec<f32>,
234}
235
236impl DisplayList {
237    /// Generates a JSON representation of the display list for debugging.
238    /// This includes clip chain analysis showing how clips are stacked.
239    pub fn to_debug_json(&self) -> String {
240        use std::fmt::Write;
241        let mut json = String::new();
242        writeln!(json, "{{").unwrap();
243        writeln!(json, "  \"total_items\": {},", self.items.len()).unwrap();
244        writeln!(json, "  \"items\": [").unwrap();
245
246        let mut clip_depth = 0i32;
247        let mut scroll_depth = 0i32;
248        let mut stacking_depth = 0i32;
249
250        for (i, item) in self.items.iter().enumerate() {
251            let comma = if i < self.items.len() - 1 { "," } else { "" };
252            let node_id = self.node_mapping.get(i).and_then(|n| *n);
253
254            match item {
255                DisplayListItem::PushClip {
256                    bounds,
257                    border_radius,
258                } => {
259                    clip_depth += 1;
260                    writeln!(json, "    {{").unwrap();
261                    writeln!(json, "      \"index\": {},", i).unwrap();
262                    writeln!(json, "      \"type\": \"PushClip\",").unwrap();
263                    writeln!(json, "      \"clip_depth\": {},", clip_depth).unwrap();
264                    writeln!(json, "      \"scroll_depth\": {},", scroll_depth).unwrap();
265                    writeln!(json, "      \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},", 
266                        bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height).unwrap();
267                    writeln!(json, "      \"border_radius\": {{ \"tl\": {:.1}, \"tr\": {:.1}, \"bl\": {:.1}, \"br\": {:.1} }},",
268                        border_radius.top_left, border_radius.top_right,
269                        border_radius.bottom_left, border_radius.bottom_right).unwrap();
270                    writeln!(json, "      \"node_id\": {:?}", node_id).unwrap();
271                    writeln!(json, "    }}{}", comma).unwrap();
272                }
273                DisplayListItem::PopClip => {
274                    writeln!(json, "    {{").unwrap();
275                    writeln!(json, "      \"index\": {},", i).unwrap();
276                    writeln!(json, "      \"type\": \"PopClip\",").unwrap();
277                    writeln!(json, "      \"clip_depth_before\": {},", clip_depth).unwrap();
278                    writeln!(json, "      \"clip_depth_after\": {}", clip_depth - 1).unwrap();
279                    writeln!(json, "    }}{}", comma).unwrap();
280                    clip_depth -= 1;
281                }
282                DisplayListItem::PushScrollFrame {
283                    clip_bounds,
284                    content_size,
285                    scroll_id,
286                } => {
287                    scroll_depth += 1;
288                    writeln!(json, "    {{").unwrap();
289                    writeln!(json, "      \"index\": {},", i).unwrap();
290                    writeln!(json, "      \"type\": \"PushScrollFrame\",").unwrap();
291                    writeln!(json, "      \"clip_depth\": {},", clip_depth).unwrap();
292                    writeln!(json, "      \"scroll_depth\": {},", scroll_depth).unwrap();
293                    writeln!(json, "      \"clip_bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},",
294                        clip_bounds.origin.x, clip_bounds.origin.y,
295                        clip_bounds.size.width, clip_bounds.size.height).unwrap();
296                    writeln!(
297                        json,
298                        "      \"content_size\": {{ \"w\": {:.1}, \"h\": {:.1} }},",
299                        content_size.width, content_size.height
300                    )
301                    .unwrap();
302                    writeln!(json, "      \"scroll_id\": {},", scroll_id).unwrap();
303                    writeln!(json, "      \"node_id\": {:?}", node_id).unwrap();
304                    writeln!(json, "    }}{}", comma).unwrap();
305                }
306                DisplayListItem::PopScrollFrame => {
307                    writeln!(json, "    {{").unwrap();
308                    writeln!(json, "      \"index\": {},", i).unwrap();
309                    writeln!(json, "      \"type\": \"PopScrollFrame\",").unwrap();
310                    writeln!(json, "      \"scroll_depth_before\": {},", scroll_depth).unwrap();
311                    writeln!(json, "      \"scroll_depth_after\": {}", scroll_depth - 1).unwrap();
312                    writeln!(json, "    }}{}", comma).unwrap();
313                    scroll_depth -= 1;
314                }
315                DisplayListItem::PushStackingContext { z_index, bounds } => {
316                    stacking_depth += 1;
317                    writeln!(json, "    {{").unwrap();
318                    writeln!(json, "      \"index\": {},", i).unwrap();
319                    writeln!(json, "      \"type\": \"PushStackingContext\",").unwrap();
320                    writeln!(json, "      \"stacking_depth\": {},", stacking_depth).unwrap();
321                    writeln!(json, "      \"z_index\": {},", z_index).unwrap();
322                    writeln!(json, "      \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }}",
323                        bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height).unwrap();
324                    writeln!(json, "    }}{}", comma).unwrap();
325                }
326                DisplayListItem::PopStackingContext => {
327                    writeln!(json, "    {{").unwrap();
328                    writeln!(json, "      \"index\": {},", i).unwrap();
329                    writeln!(json, "      \"type\": \"PopStackingContext\",").unwrap();
330                    writeln!(json, "      \"stacking_depth_before\": {},", stacking_depth).unwrap();
331                    writeln!(
332                        json,
333                        "      \"stacking_depth_after\": {}",
334                        stacking_depth - 1
335                    )
336                    .unwrap();
337                    writeln!(json, "    }}{}", comma).unwrap();
338                    stacking_depth -= 1;
339                }
340                DisplayListItem::Rect {
341                    bounds,
342                    color,
343                    border_radius,
344                } => {
345                    writeln!(json, "    {{").unwrap();
346                    writeln!(json, "      \"index\": {},", i).unwrap();
347                    writeln!(json, "      \"type\": \"Rect\",").unwrap();
348                    writeln!(json, "      \"clip_depth\": {},", clip_depth).unwrap();
349                    writeln!(json, "      \"scroll_depth\": {},", scroll_depth).unwrap();
350                    writeln!(json, "      \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},",
351                        bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height).unwrap();
352                    writeln!(
353                        json,
354                        "      \"color\": \"rgba({},{},{},{})\",",
355                        color.r, color.g, color.b, color.a
356                    )
357                    .unwrap();
358                    writeln!(json, "      \"node_id\": {:?}", node_id).unwrap();
359                    writeln!(json, "    }}{}", comma).unwrap();
360                }
361                DisplayListItem::Border { bounds, .. } => {
362                    writeln!(json, "    {{").unwrap();
363                    writeln!(json, "      \"index\": {},", i).unwrap();
364                    writeln!(json, "      \"type\": \"Border\",").unwrap();
365                    writeln!(json, "      \"clip_depth\": {},", clip_depth).unwrap();
366                    writeln!(json, "      \"scroll_depth\": {},", scroll_depth).unwrap();
367                    writeln!(json, "      \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},",
368                        bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height).unwrap();
369                    writeln!(json, "      \"node_id\": {:?}", node_id).unwrap();
370                    writeln!(json, "    }}{}", comma).unwrap();
371                }
372                DisplayListItem::ScrollBarStyled { info } => {
373                    writeln!(json, "    {{").unwrap();
374                    writeln!(json, "      \"index\": {},", i).unwrap();
375                    writeln!(json, "      \"type\": \"ScrollBarStyled\",").unwrap();
376                    writeln!(json, "      \"clip_depth\": {},", clip_depth).unwrap();
377                    writeln!(json, "      \"scroll_depth\": {},", scroll_depth).unwrap();
378                    writeln!(json, "      \"orientation\": \"{:?}\",", info.orientation).unwrap();
379                    writeln!(json, "      \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }}",
380                        info.bounds.origin.x, info.bounds.origin.y,
381                        info.bounds.size.width, info.bounds.size.height).unwrap();
382                    writeln!(json, "    }}{}", comma).unwrap();
383                }
384                _ => {
385                    writeln!(json, "    {{").unwrap();
386                    writeln!(json, "      \"index\": {},", i).unwrap();
387                    writeln!(
388                        json,
389                        "      \"type\": \"{:?}\",",
390                        std::mem::discriminant(item)
391                    )
392                    .unwrap();
393                    writeln!(json, "      \"clip_depth\": {},", clip_depth).unwrap();
394                    writeln!(json, "      \"scroll_depth\": {},", scroll_depth).unwrap();
395                    writeln!(json, "      \"node_id\": {:?}", node_id).unwrap();
396                    writeln!(json, "    }}{}", comma).unwrap();
397                }
398            }
399        }
400
401        writeln!(json, "  ],").unwrap();
402        writeln!(json, "  \"final_clip_depth\": {},", clip_depth).unwrap();
403        writeln!(json, "  \"final_scroll_depth\": {},", scroll_depth).unwrap();
404        writeln!(json, "  \"final_stacking_depth\": {},", stacking_depth).unwrap();
405        writeln!(
406            json,
407            "  \"balanced\": {}",
408            clip_depth == 0 && scroll_depth == 0 && stacking_depth == 0
409        )
410        .unwrap();
411        writeln!(json, "}}").unwrap();
412
413        json
414    }
415}
416
417/// A command in the display list. Can be either a drawing primitive or a
418/// state-management instruction for the renderer's graphics context.
419#[derive(Debug, Clone)]
420pub enum DisplayListItem {
421    // Drawing Primitives
422    /// A filled rectangle with optional rounded corners.
423    /// Used for backgrounds, colored boxes, and other solid fills.
424    Rect {
425        /// The rectangle bounds in logical coordinates
426        bounds: LogicalRect,
427        /// The fill color (RGBA)
428        color: ColorU,
429        /// Corner radii for rounded rectangles
430        border_radius: BorderRadius,
431    },
432    /// A selection highlight rectangle (e.g., for text selection).
433    /// Rendered behind text to show selected regions.
434    SelectionRect {
435        /// The rectangle bounds in logical coordinates
436        bounds: LogicalRect,
437        /// Corner radii for rounded selection
438        border_radius: BorderRadius,
439        /// The selection highlight color (typically semi-transparent)
440        color: ColorU,
441    },
442    /// A text cursor (caret) rectangle.
443    /// Typically a thin vertical line indicating text insertion point.
444    CursorRect {
445        /// The cursor bounds (usually narrow width)
446        bounds: LogicalRect,
447        /// The cursor color
448        color: ColorU,
449    },
450    /// A CSS border with per-side widths, colors, and styles.
451    /// Supports different styles per side (solid, dashed, dotted, etc.).
452    Border {
453        /// The border-box bounds
454        bounds: LogicalRect,
455        /// Border widths for each side
456        widths: StyleBorderWidths,
457        /// Border colors for each side
458        colors: StyleBorderColors,
459        /// Border styles for each side (solid, dashed, etc.)
460        styles: StyleBorderStyles,
461        /// Corner radii for rounded borders
462        border_radius: StyleBorderRadius,
463    },
464    /// Text layout with full metadata (for PDF, accessibility, etc.)
465    /// This is pushed BEFORE the individual Text items and contains
466    /// the original text, glyph-to-unicode mapping, and positioning info
467    TextLayout {
468        layout: Arc<dyn std::any::Any + Send + Sync>, // Type-erased UnifiedLayout
469        bounds: LogicalRect,
470        font_hash: FontHash,
471        font_size_px: f32,
472        color: ColorU,
473    },
474    /// Text rendered with individual glyph positioning (for simple renderers)
475    Text {
476        glyphs: Vec<GlyphInstance>,
477        font_hash: FontHash, // Changed from FontRef - just store the hash
478        font_size_px: f32,
479        color: ColorU,
480        clip_rect: LogicalRect,
481    },
482    /// Underline decoration for text (CSS text-decoration: underline)
483    Underline {
484        bounds: LogicalRect,
485        color: ColorU,
486        thickness: f32,
487    },
488    /// Strikethrough decoration for text (CSS text-decoration: line-through)
489    Strikethrough {
490        bounds: LogicalRect,
491        color: ColorU,
492        thickness: f32,
493    },
494    /// Overline decoration for text (CSS text-decoration: overline)
495    Overline {
496        bounds: LogicalRect,
497        color: ColorU,
498        thickness: f32,
499    },
500    Image {
501        bounds: LogicalRect,
502        image: ImageRef,
503    },
504    /// A dedicated primitive for a scrollbar with optional GPU-animated opacity.
505    /// This is a simple single-color scrollbar used for basic rendering.
506    ScrollBar {
507        bounds: LogicalRect,
508        color: ColorU,
509        orientation: ScrollbarOrientation,
510        /// Optional opacity key for GPU-side fading animation.
511        /// If present, the renderer will use this key to look up dynamic opacity.
512        /// If None, the alpha channel of `color` is used directly.
513        opacity_key: Option<OpacityKey>,
514        /// Optional hit-test ID for WebRender hit-testing.
515        /// If present, allows event handlers to identify which scrollbar component was clicked.
516        hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
517    },
518    /// A fully styled scrollbar with separate track, thumb, and optional buttons.
519    /// Used when CSS scrollbar properties are specified.
520    ScrollBarStyled {
521        /// Complete drawing information for all scrollbar components
522        info: Box<ScrollbarDrawInfo>,
523    },
524
525    /// An embedded IFrame that references a child DOM with its own display list.
526    /// This mirrors webrender's IframeDisplayItem. The renderer will look up
527    /// the child display list by child_dom_id and render it within the bounds.
528    IFrame {
529        /// The DomId of the child DOM (similar to webrender's pipeline_id)
530        child_dom_id: DomId,
531        /// The bounds where the IFrame should be rendered
532        bounds: LogicalRect,
533        /// The clip rect for the IFrame content
534        clip_rect: LogicalRect,
535    },
536
537    // --- State-Management Commands ---
538    /// Pushes a new clipping rectangle onto the renderer's clip stack.
539    /// All subsequent primitives will be clipped by this rect until a PopClip.
540    PushClip {
541        bounds: LogicalRect,
542        border_radius: BorderRadius,
543    },
544    /// Pops the current clip from the renderer's clip stack.
545    PopClip,
546
547    /// Defines a scrollable area. This is a specialized clip that also
548    /// establishes a new coordinate system for its children, which can be offset.
549    PushScrollFrame {
550        /// The clip rect in the parent's coordinate space.
551        clip_bounds: LogicalRect,
552        /// The total size of the scrollable content.
553        content_size: LogicalSize,
554        /// An ID for the renderer to track this scrollable area between frames.
555        scroll_id: LocalScrollId, // This would be a renderer-agnostic ID type
556    },
557    /// Pops the current scroll frame.
558    PopScrollFrame,
559
560    /// Pushes a new stacking context for proper z-index layering.
561    /// All subsequent primitives until PopStackingContext will be in this stacking context.
562    PushStackingContext {
563        /// The z-index for this stacking context (for debugging/validation)
564        z_index: i32,
565        /// The bounds of the stacking context root element
566        bounds: LogicalRect,
567    },
568    /// Pops the current stacking context.
569    PopStackingContext,
570
571    /// Pushes a reference frame with a GPU-accelerated transform.
572    /// Used for CSS transforms and drag visual offsets.
573    /// Creates a new spatial coordinate system for all children.
574    PushReferenceFrame {
575        /// The transform key for GPU-animated property binding
576        transform_key: TransformKey,
577        /// The initial transform value (identity for drag, computed for CSS transform)
578        initial_transform: ComputedTransform3D,
579        /// The bounds of the reference frame (origin = transform origin)
580        bounds: LogicalRect,
581    },
582    /// Pops the current reference frame.
583    PopReferenceFrame,
584
585    /// Defines a region for hit-testing.
586    HitTestArea {
587        bounds: LogicalRect,
588        tag: DisplayListTagId, // This would be a renderer-agnostic ID type
589    },
590
591    // --- Gradient Primitives ---
592    /// A linear gradient fill.
593    LinearGradient {
594        bounds: LogicalRect,
595        gradient: LinearGradient,
596        border_radius: BorderRadius,
597    },
598    /// A radial gradient fill.
599    RadialGradient {
600        bounds: LogicalRect,
601        gradient: RadialGradient,
602        border_radius: BorderRadius,
603    },
604    /// A conic (angular) gradient fill.
605    ConicGradient {
606        bounds: LogicalRect,
607        gradient: ConicGradient,
608        border_radius: BorderRadius,
609    },
610
611    // --- Shadow Effects ---
612    /// A box shadow (either outset or inset).
613    BoxShadow {
614        bounds: LogicalRect,
615        shadow: StyleBoxShadow,
616        border_radius: BorderRadius,
617    },
618
619    // --- Filter Effects ---
620    /// Push a filter effect that applies to subsequent content.
621    PushFilter {
622        bounds: LogicalRect,
623        filters: Vec<StyleFilter>,
624    },
625    /// Pop a previously pushed filter.
626    PopFilter,
627
628    /// Push a backdrop filter (applies to content behind the element).
629    PushBackdropFilter {
630        bounds: LogicalRect,
631        filters: Vec<StyleFilter>,
632    },
633    /// Pop a previously pushed backdrop filter.
634    PopBackdropFilter,
635
636    /// Push an opacity layer.
637    PushOpacity {
638        bounds: LogicalRect,
639        opacity: f32,
640    },
641    /// Pop an opacity layer.
642    PopOpacity,
643
644    /// Push a text shadow that applies to subsequent text content.
645    PushTextShadow {
646        shadow: azul_css::props::style::box_shadow::StyleBoxShadow,
647    },
648    /// Pop all text shadows.
649    PopTextShadow,
650}
651
652// Helper structs for the DisplayList
653#[derive(Debug, Copy, Clone, Default)]
654pub struct BorderRadius {
655    pub top_left: f32,
656    pub top_right: f32,
657    pub bottom_left: f32,
658    pub bottom_right: f32,
659}
660
661impl BorderRadius {
662    pub fn is_zero(&self) -> bool {
663        self.top_left == 0.0
664            && self.top_right == 0.0
665            && self.bottom_left == 0.0
666            && self.bottom_right == 0.0
667    }
668}
669
670// Dummy types for compilation
671pub type LocalScrollId = u64;
672/// Display list tag ID as (payload, type_marker) tuple.
673/// The u16 field is used as a namespace marker:
674/// - 0x0100 = DOM Node (regular interactive elements)
675/// - 0x0200 = Scrollbar component
676pub type DisplayListTagId = (u64, u16);
677
678/// Internal builder to accumulate display list items during generation.
679#[derive(Debug, Default)]
680struct DisplayListBuilder {
681    items: Vec<DisplayListItem>,
682    node_mapping: Vec<Option<NodeId>>,
683    /// Current node being processed (set by generator)
684    current_node: Option<NodeId>,
685    /// Collected debug messages (transferred to ctx on finalize)
686    debug_messages: Vec<LayoutDebugMessage>,
687    /// Whether debug logging is enabled
688    debug_enabled: bool,
689    /// Y-positions where forced page breaks should occur
690    forced_page_breaks: Vec<f32>,
691}
692
693impl DisplayListBuilder {
694    pub fn new() -> Self {
695        Self::default()
696    }
697
698    pub fn with_debug(debug_enabled: bool) -> Self {
699        Self {
700            items: Vec::new(),
701            node_mapping: Vec::new(),
702            current_node: None,
703            debug_messages: Vec::new(),
704            debug_enabled,
705            forced_page_breaks: Vec::new(),
706        }
707    }
708
709    /// Log a debug message if debug is enabled
710    fn debug_log(&mut self, message: String) {
711        if self.debug_enabled {
712            self.debug_messages.push(LayoutDebugMessage::info(message));
713        }
714    }
715
716    /// Build the display list and transfer debug messages to the provided option
717    pub fn build_with_debug(
718        mut self,
719        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
720    ) -> DisplayList {
721        // Transfer collected debug messages to the context
722        if let Some(msgs) = debug_messages.as_mut() {
723            msgs.append(&mut self.debug_messages);
724        }
725        DisplayList {
726            items: self.items,
727            node_mapping: self.node_mapping,
728            forced_page_breaks: self.forced_page_breaks,
729        }
730    }
731
732    /// Set the current node context for subsequent push operations
733    pub fn set_current_node(&mut self, node_id: Option<NodeId>) {
734        self.current_node = node_id;
735    }
736
737    /// Register a forced page break at the given Y position.
738    /// This is used for CSS break-before: always and break-after: always.
739    pub fn add_forced_page_break(&mut self, y_position: f32) {
740        // Avoid duplicates and keep sorted
741        if !self.forced_page_breaks.contains(&y_position) {
742            self.forced_page_breaks.push(y_position);
743            self.forced_page_breaks.sort_by(|a, b| a.partial_cmp(b).unwrap());
744        }
745    }
746
747    /// Push an item and record its node mapping
748    fn push_item(&mut self, item: DisplayListItem) {
749        self.items.push(item);
750        self.node_mapping.push(self.current_node);
751    }
752
753    pub fn build(self) -> DisplayList {
754        DisplayList {
755            items: self.items,
756            node_mapping: self.node_mapping,
757            forced_page_breaks: self.forced_page_breaks,
758        }
759    }
760
761    pub fn push_hit_test_area(&mut self, bounds: LogicalRect, tag: DisplayListTagId) {
762        self.push_item(DisplayListItem::HitTestArea { bounds, tag });
763    }
764
765    /// Push a simple single-color scrollbar (legacy method).
766    pub fn push_scrollbar(
767        &mut self,
768        bounds: LogicalRect,
769        color: ColorU,
770        orientation: ScrollbarOrientation,
771        opacity_key: Option<OpacityKey>,
772        hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
773    ) {
774        if color.a > 0 || opacity_key.is_some() {
775            // Optimization: Don't draw fully transparent items without opacity keys.
776            self.push_item(DisplayListItem::ScrollBar {
777                bounds,
778                color,
779                orientation,
780                opacity_key,
781                hit_id,
782            });
783        }
784    }
785
786    /// Push a fully styled scrollbar with track, thumb, and optional buttons.
787    pub fn push_scrollbar_styled(&mut self, info: ScrollbarDrawInfo) {
788        // Only push if at least the thumb or track is visible
789        if info.thumb_color.a > 0 || info.track_color.a > 0 || info.opacity_key.is_some() {
790            self.push_item(DisplayListItem::ScrollBarStyled {
791                info: Box::new(info),
792            });
793        }
794    }
795
796    pub fn push_rect(&mut self, bounds: LogicalRect, color: ColorU, border_radius: BorderRadius) {
797        if color.a > 0 {
798            // Optimization: Don't draw fully transparent items.
799            self.push_item(DisplayListItem::Rect {
800                bounds,
801                color,
802                border_radius,
803            });
804        }
805    }
806
807    /// Unified method to paint all background layers and border for an element.
808    ///
809    /// This consolidates the background/border painting logic that was previously
810    /// duplicated across:
811    /// - paint_node_background_and_border() for block elements
812    /// - paint_inline_shape() for inline-block elements
813    ///
814    /// The backgrounds are painted in order (back to front per CSS spec), followed
815    /// by the border.
816    pub fn push_backgrounds_and_border(
817        &mut self,
818        bounds: LogicalRect,
819        background_contents: &[azul_css::props::style::StyleBackgroundContent],
820        border_info: &BorderInfo,
821        simple_border_radius: BorderRadius,
822        style_border_radius: StyleBorderRadius,
823    ) {
824        use azul_css::props::style::StyleBackgroundContent;
825
826        // Paint all background layers in order (CSS paints backgrounds back to front)
827        for bg in background_contents {
828            match bg {
829                StyleBackgroundContent::Color(color) => {
830                    self.push_rect(bounds, *color, simple_border_radius);
831                }
832                StyleBackgroundContent::LinearGradient(gradient) => {
833                    self.push_linear_gradient(bounds, gradient.clone(), simple_border_radius);
834                }
835                StyleBackgroundContent::RadialGradient(gradient) => {
836                    self.push_radial_gradient(bounds, gradient.clone(), simple_border_radius);
837                }
838                StyleBackgroundContent::ConicGradient(gradient) => {
839                    self.push_conic_gradient(bounds, gradient.clone(), simple_border_radius);
840                }
841                StyleBackgroundContent::Image(_image_id) => {
842                    // TODO: Implement image backgrounds
843                }
844            }
845        }
846
847        // Paint border
848        self.push_border(
849            bounds,
850            border_info.widths,
851            border_info.colors,
852            border_info.styles,
853            style_border_radius,
854        );
855    }
856
857    /// Paint backgrounds and border for inline text elements.
858    ///
859    /// Similar to push_backgrounds_and_border but uses InlineBorderInfo which stores
860    /// pre-resolved pixel values instead of CSS property values. This is used for
861    /// inline (display: inline) elements where the border info is computed during
862    /// text layout and stored in the glyph runs.
863    pub fn push_inline_backgrounds_and_border(
864        &mut self,
865        bounds: LogicalRect,
866        background_color: Option<ColorU>,
867        background_contents: &[azul_css::props::style::StyleBackgroundContent],
868        border: Option<&crate::text3::cache::InlineBorderInfo>,
869    ) {
870        use azul_css::props::style::StyleBackgroundContent;
871
872        // Paint solid background color if present
873        if let Some(bg_color) = background_color {
874            self.push_rect(bounds, bg_color, BorderRadius::default());
875        }
876
877        // Paint all background layers in order (CSS paints backgrounds back to front)
878        for bg in background_contents {
879            match bg {
880                StyleBackgroundContent::Color(color) => {
881                    self.push_rect(bounds, *color, BorderRadius::default());
882                }
883                StyleBackgroundContent::LinearGradient(gradient) => {
884                    self.push_linear_gradient(bounds, gradient.clone(), BorderRadius::default());
885                }
886                StyleBackgroundContent::RadialGradient(gradient) => {
887                    self.push_radial_gradient(bounds, gradient.clone(), BorderRadius::default());
888                }
889                StyleBackgroundContent::ConicGradient(gradient) => {
890                    self.push_conic_gradient(bounds, gradient.clone(), BorderRadius::default());
891                }
892                StyleBackgroundContent::Image(_image_id) => {
893                    // TODO: Implement image backgrounds for inline text
894                }
895            }
896        }
897
898        // Paint border if present
899        if let Some(border) = border {
900            if border.top > 0.0 || border.right > 0.0 || border.bottom > 0.0 || border.left > 0.0 {
901                let border_widths = StyleBorderWidths {
902                    top: Some(CssPropertyValue::Exact(LayoutBorderTopWidth {
903                        inner: PixelValue::px(border.top),
904                    })),
905                    right: Some(CssPropertyValue::Exact(LayoutBorderRightWidth {
906                        inner: PixelValue::px(border.right),
907                    })),
908                    bottom: Some(CssPropertyValue::Exact(LayoutBorderBottomWidth {
909                        inner: PixelValue::px(border.bottom),
910                    })),
911                    left: Some(CssPropertyValue::Exact(LayoutBorderLeftWidth {
912                        inner: PixelValue::px(border.left),
913                    })),
914                };
915                let border_colors = StyleBorderColors {
916                    top: Some(CssPropertyValue::Exact(StyleBorderTopColor {
917                        inner: border.top_color,
918                    })),
919                    right: Some(CssPropertyValue::Exact(StyleBorderRightColor {
920                        inner: border.right_color,
921                    })),
922                    bottom: Some(CssPropertyValue::Exact(StyleBorderBottomColor {
923                        inner: border.bottom_color,
924                    })),
925                    left: Some(CssPropertyValue::Exact(StyleBorderLeftColor {
926                        inner: border.left_color,
927                    })),
928                };
929                let border_styles = StyleBorderStyles {
930                    top: Some(CssPropertyValue::Exact(StyleBorderTopStyle {
931                        inner: BorderStyle::Solid,
932                    })),
933                    right: Some(CssPropertyValue::Exact(StyleBorderRightStyle {
934                        inner: BorderStyle::Solid,
935                    })),
936                    bottom: Some(CssPropertyValue::Exact(StyleBorderBottomStyle {
937                        inner: BorderStyle::Solid,
938                    })),
939                    left: Some(CssPropertyValue::Exact(StyleBorderLeftStyle {
940                        inner: BorderStyle::Solid,
941                    })),
942                };
943                let radius_px = PixelValue::px(border.radius.unwrap_or(0.0));
944                let border_radius = StyleBorderRadius {
945                    top_left: radius_px,
946                    top_right: radius_px,
947                    bottom_left: radius_px,
948                    bottom_right: radius_px,
949                };
950
951                self.push_border(
952                    bounds,
953                    border_widths,
954                    border_colors,
955                    border_styles,
956                    border_radius,
957                );
958            }
959        }
960    }
961
962    /// Push a linear gradient background
963    pub fn push_linear_gradient(
964        &mut self,
965        bounds: LogicalRect,
966        gradient: LinearGradient,
967        border_radius: BorderRadius,
968    ) {
969        self.push_item(DisplayListItem::LinearGradient {
970            bounds,
971            gradient,
972            border_radius,
973        });
974    }
975
976    /// Push a radial gradient background
977    pub fn push_radial_gradient(
978        &mut self,
979        bounds: LogicalRect,
980        gradient: RadialGradient,
981        border_radius: BorderRadius,
982    ) {
983        self.push_item(DisplayListItem::RadialGradient {
984            bounds,
985            gradient,
986            border_radius,
987        });
988    }
989
990    /// Push a conic gradient background
991    pub fn push_conic_gradient(
992        &mut self,
993        bounds: LogicalRect,
994        gradient: ConicGradient,
995        border_radius: BorderRadius,
996    ) {
997        self.push_item(DisplayListItem::ConicGradient {
998            bounds,
999            gradient,
1000            border_radius,
1001        });
1002    }
1003
1004    pub fn push_selection_rect(
1005        &mut self,
1006        bounds: LogicalRect,
1007        color: ColorU,
1008        border_radius: BorderRadius,
1009    ) {
1010        if color.a > 0 {
1011            self.push_item(DisplayListItem::SelectionRect {
1012                bounds,
1013                color,
1014                border_radius,
1015            });
1016        }
1017    }
1018
1019    pub fn push_cursor_rect(&mut self, bounds: LogicalRect, color: ColorU) {
1020        if color.a > 0 {
1021            self.push_item(DisplayListItem::CursorRect { bounds, color });
1022        }
1023    }
1024    pub fn push_clip(&mut self, bounds: LogicalRect, border_radius: BorderRadius) {
1025        self.push_item(DisplayListItem::PushClip {
1026            bounds,
1027            border_radius,
1028        });
1029    }
1030    pub fn pop_clip(&mut self) {
1031        self.push_item(DisplayListItem::PopClip);
1032    }
1033    pub fn push_scroll_frame(
1034        &mut self,
1035        clip_bounds: LogicalRect,
1036        content_size: LogicalSize,
1037        scroll_id: LocalScrollId,
1038    ) {
1039        self.push_item(DisplayListItem::PushScrollFrame {
1040            clip_bounds,
1041            content_size,
1042            scroll_id,
1043        });
1044    }
1045    pub fn pop_scroll_frame(&mut self) {
1046        self.push_item(DisplayListItem::PopScrollFrame);
1047    }
1048    pub fn push_border(
1049        &mut self,
1050        bounds: LogicalRect,
1051        widths: StyleBorderWidths,
1052        colors: StyleBorderColors,
1053        styles: StyleBorderStyles,
1054        border_radius: StyleBorderRadius,
1055    ) {
1056        // Check if any border side is visible
1057        let has_visible_border = {
1058            let has_width = widths.top.is_some()
1059                || widths.right.is_some()
1060                || widths.bottom.is_some()
1061                || widths.left.is_some();
1062            let has_style = styles.top.is_some()
1063                || styles.right.is_some()
1064                || styles.bottom.is_some()
1065                || styles.left.is_some();
1066            has_width && has_style
1067        };
1068
1069        if has_visible_border {
1070            self.push_item(DisplayListItem::Border {
1071                bounds,
1072                widths,
1073                colors,
1074                styles,
1075                border_radius,
1076            });
1077        }
1078    }
1079
1080    pub fn push_stacking_context(&mut self, z_index: i32, bounds: LogicalRect) {
1081        self.push_item(DisplayListItem::PushStackingContext { z_index, bounds });
1082    }
1083
1084    pub fn pop_stacking_context(&mut self) {
1085        self.push_item(DisplayListItem::PopStackingContext);
1086    }
1087
1088    pub fn push_reference_frame(
1089        &mut self,
1090        transform_key: TransformKey,
1091        initial_transform: ComputedTransform3D,
1092        bounds: LogicalRect,
1093    ) {
1094        self.push_item(DisplayListItem::PushReferenceFrame {
1095            transform_key,
1096            initial_transform,
1097            bounds,
1098        });
1099    }
1100
1101    pub fn pop_reference_frame(&mut self) {
1102        self.push_item(DisplayListItem::PopReferenceFrame);
1103    }
1104
1105    pub fn push_text_run(
1106        &mut self,
1107        glyphs: Vec<GlyphInstance>,
1108        font_hash: FontHash, // Just the hash, not the full FontRef
1109        font_size_px: f32,
1110        color: ColorU,
1111        clip_rect: LogicalRect,
1112    ) {
1113        self.debug_log(format!(
1114            "[push_text_run] {} glyphs, font_size={}px, color=({},{},{},{}), clip={:?}",
1115            glyphs.len(),
1116            font_size_px,
1117            color.r,
1118            color.g,
1119            color.b,
1120            color.a,
1121            clip_rect
1122        ));
1123
1124        if !glyphs.is_empty() && color.a > 0 {
1125            self.push_item(DisplayListItem::Text {
1126                glyphs,
1127                font_hash,
1128                font_size_px,
1129                color,
1130                clip_rect,
1131            });
1132        } else {
1133            self.debug_log(format!(
1134                "[push_text_run] SKIPPED: glyphs.is_empty()={}, color.a={}",
1135                glyphs.is_empty(),
1136                color.a
1137            ));
1138        }
1139    }
1140
1141    pub fn push_text_layout(
1142        &mut self,
1143        layout: Arc<dyn std::any::Any + Send + Sync>,
1144        bounds: LogicalRect,
1145        font_hash: FontHash,
1146        font_size_px: f32,
1147        color: ColorU,
1148    ) {
1149        if color.a > 0 {
1150            self.push_item(DisplayListItem::TextLayout {
1151                layout,
1152                bounds,
1153                font_hash,
1154                font_size_px,
1155                color,
1156            });
1157        }
1158    }
1159
1160    pub fn push_underline(&mut self, bounds: LogicalRect, color: ColorU, thickness: f32) {
1161        if color.a > 0 && thickness > 0.0 {
1162            self.push_item(DisplayListItem::Underline {
1163                bounds,
1164                color,
1165                thickness,
1166            });
1167        }
1168    }
1169
1170    pub fn push_strikethrough(&mut self, bounds: LogicalRect, color: ColorU, thickness: f32) {
1171        if color.a > 0 && thickness > 0.0 {
1172            self.push_item(DisplayListItem::Strikethrough {
1173                bounds,
1174                color,
1175                thickness,
1176            });
1177        }
1178    }
1179
1180    pub fn push_overline(&mut self, bounds: LogicalRect, color: ColorU, thickness: f32) {
1181        if color.a > 0 && thickness > 0.0 {
1182            self.push_item(DisplayListItem::Overline {
1183                bounds,
1184                color,
1185                thickness,
1186            });
1187        }
1188    }
1189
1190    pub fn push_image(&mut self, bounds: LogicalRect, image: ImageRef) {
1191        self.push_item(DisplayListItem::Image { bounds, image });
1192    }
1193}
1194
1195/// Main entry point for generating the display list.
1196pub fn generate_display_list<T: ParsedFontTrait + Sync + 'static>(
1197    ctx: &mut LayoutContext<T>,
1198    tree: &LayoutTree,
1199    calculated_positions: &super::PositionVec,
1200    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
1201    scroll_ids: &BTreeMap<usize, u64>,
1202    gpu_value_cache: Option<&GpuValueCache>,
1203    renderer_resources: &RendererResources,
1204    id_namespace: IdNamespace,
1205    dom_id: DomId,
1206) -> Result<DisplayList> {
1207    debug_info!(
1208        ctx,
1209        "[DisplayList] generate_display_list: tree has {} nodes, {} positions calculated",
1210        tree.nodes.len(),
1211        calculated_positions.len()
1212    );
1213
1214    debug_info!(ctx, "Starting display list generation");
1215    debug_info!(
1216        ctx,
1217        "Collecting stacking contexts from root node {}",
1218        tree.root
1219    );
1220
1221    let positioned_tree = PositionedTree {
1222        tree,
1223        calculated_positions,
1224    };
1225    let mut generator = DisplayListGenerator::new(
1226        ctx,
1227        scroll_offsets,
1228        &positioned_tree,
1229        scroll_ids,
1230        gpu_value_cache,
1231        renderer_resources,
1232        id_namespace,
1233        dom_id,
1234    );
1235
1236    // Create builder with debug enabled if ctx has debug messages
1237    let debug_enabled = generator.ctx.debug_messages.is_some();
1238    let mut builder = DisplayListBuilder::with_debug(debug_enabled);
1239
1240    // 0. Canvas background propagation (CSS 2.1 § 14.2):
1241    //    "The background of the root element becomes the background of the canvas."
1242    //    If the root (html) has a transparent background, propagate from <body>.
1243    //    The canvas background fills the ENTIRE viewport, not just the root's content box.
1244    //    This is critical when <html> doesn't have height:100% — without this,
1245    //    the body's background only covers the body's content area, not the viewport.
1246    {
1247        let root_node = tree.get(tree.root);
1248        if let Some(root) = root_node {
1249            if let Some(root_dom_id) = root.dom_node_id {
1250                let root_state = generator.get_styled_node_state(root_dom_id);
1251                let canvas_bg = get_background_color(
1252                    generator.ctx.styled_dom,
1253                    root_dom_id,
1254                    &root_state,
1255                );
1256                if canvas_bg.a > 0 {
1257                    let viewport_rect = LogicalRect {
1258                        origin: LogicalPosition::zero(),
1259                        size: generator.ctx.viewport_size,
1260                    };
1261                    builder.push_rect(viewport_rect, canvas_bg, BorderRadius::default());
1262                    debug_info!(
1263                        generator.ctx,
1264                        "[DisplayList] Canvas background: color=({},{},{},{}), size={:?}",
1265                        canvas_bg.r, canvas_bg.g, canvas_bg.b, canvas_bg.a,
1266                        generator.ctx.viewport_size
1267                    );
1268                }
1269            }
1270        }
1271    }
1272
1273    // 1. Build a tree of stacking contexts, which defines the global paint order.
1274    let stacking_context_tree = generator.collect_stacking_contexts(tree.root)?;
1275
1276    // 2. Traverse the stacking context tree to generate display items in the correct order.
1277    debug_info!(
1278        generator.ctx,
1279        "Generating display items from stacking context tree"
1280    );
1281    generator.generate_for_stacking_context(&mut builder, &stacking_context_tree)?;
1282
1283    // Build display list and transfer debug messages to context
1284    let display_list = builder.build_with_debug(generator.ctx.debug_messages);
1285    debug_info!(
1286        generator.ctx,
1287        "[DisplayList] Generated {} display items",
1288        display_list.items.len()
1289    );
1290    Ok(display_list)
1291}
1292
1293/// A helper struct that holds all necessary state and context for the generation process.
1294struct DisplayListGenerator<'a, 'b, T: ParsedFontTrait> {
1295    ctx: &'a mut LayoutContext<'b, T>,
1296    scroll_offsets: &'a BTreeMap<NodeId, ScrollPosition>,
1297    positioned_tree: &'a PositionedTree<'a>,
1298    scroll_ids: &'a BTreeMap<usize, u64>,
1299    gpu_value_cache: Option<&'a GpuValueCache>,
1300    renderer_resources: &'a RendererResources,
1301    id_namespace: IdNamespace,
1302    dom_id: DomId,
1303}
1304
1305/// Represents a node in the CSS stacking context tree, not the DOM tree.
1306#[derive(Debug)]
1307struct StackingContext {
1308    node_index: usize,
1309    z_index: i32,
1310    child_contexts: Vec<StackingContext>,
1311    /// Children that do not create their own stacking contexts and are painted in DOM order.
1312    in_flow_children: Vec<usize>,
1313}
1314
1315impl<'a, 'b, T> DisplayListGenerator<'a, 'b, T>
1316where
1317    T: ParsedFontTrait + Sync + 'static,
1318{
1319    pub fn new(
1320        ctx: &'a mut LayoutContext<'b, T>,
1321        scroll_offsets: &'a BTreeMap<NodeId, ScrollPosition>,
1322        positioned_tree: &'a PositionedTree<'a>,
1323        scroll_ids: &'a BTreeMap<usize, u64>,
1324        gpu_value_cache: Option<&'a GpuValueCache>,
1325        renderer_resources: &'a RendererResources,
1326        id_namespace: IdNamespace,
1327        dom_id: DomId,
1328    ) -> Self {
1329        Self {
1330            ctx,
1331            scroll_offsets,
1332            positioned_tree,
1333            scroll_ids,
1334            gpu_value_cache,
1335            renderer_resources,
1336            id_namespace,
1337            dom_id,
1338        }
1339    }
1340
1341    /// Helper to get styled node state for a node
1342    fn get_styled_node_state(&self, dom_id: NodeId) -> azul_core::styled_dom::StyledNodeState {
1343        self.ctx
1344            .styled_dom
1345            .styled_nodes
1346            .as_container()
1347            .get(dom_id)
1348            .map(|n| n.styled_node_state.clone())
1349            .unwrap_or_default()
1350    }
1351
1352    /// Gets the cursor type for a text node from its CSS properties.
1353    /// Defaults to Text (I-beam) cursor if no explicit cursor is set.
1354    fn get_cursor_type_for_text_node(&self, node_id: NodeId) -> CursorType {
1355        use azul_css::props::style::effects::StyleCursor;
1356        
1357        let styled_node_state = self.get_styled_node_state(node_id);
1358        let node_data_container = self.ctx.styled_dom.node_data.as_container();
1359        let node_data = node_data_container.get(node_id);
1360        
1361        // Query the cursor CSS property for this text node
1362        if let Some(node_data) = node_data {
1363            if let Some(cursor_value) = self.ctx.styled_dom.get_css_property_cache().get_cursor(
1364                node_data,
1365                &node_id,
1366                &styled_node_state,
1367            ) {
1368                if let CssPropertyValue::Exact(cursor) = cursor_value {
1369                    return match cursor {
1370                        StyleCursor::Default => CursorType::Default,
1371                        StyleCursor::Pointer => CursorType::Pointer,
1372                        StyleCursor::Text => CursorType::Text,
1373                        StyleCursor::Crosshair => CursorType::Crosshair,
1374                        StyleCursor::Move => CursorType::Move,
1375                        StyleCursor::Help => CursorType::Help,
1376                        StyleCursor::Wait => CursorType::Wait,
1377                        StyleCursor::Progress => CursorType::Progress,
1378                        StyleCursor::NsResize => CursorType::NsResize,
1379                        StyleCursor::EwResize => CursorType::EwResize,
1380                        StyleCursor::NeswResize => CursorType::NeswResize,
1381                        StyleCursor::NwseResize => CursorType::NwseResize,
1382                        StyleCursor::NResize => CursorType::NResize,
1383                        StyleCursor::SResize => CursorType::SResize,
1384                        StyleCursor::EResize => CursorType::EResize,
1385                        StyleCursor::WResize => CursorType::WResize,
1386                        StyleCursor::Grab => CursorType::Grab,
1387                        StyleCursor::Grabbing => CursorType::Grabbing,
1388                        StyleCursor::RowResize => CursorType::RowResize,
1389                        StyleCursor::ColResize => CursorType::ColResize,
1390                        // Map less common cursors to closest available
1391                        StyleCursor::SeResize | StyleCursor::NeswResize => CursorType::NeswResize,
1392                        StyleCursor::ZoomIn | StyleCursor::ZoomOut => CursorType::Default,
1393                        StyleCursor::Copy | StyleCursor::Alias => CursorType::Default,
1394                        StyleCursor::Cell => CursorType::Crosshair,
1395                        StyleCursor::AllScroll => CursorType::Move,
1396                        StyleCursor::ContextMenu => CursorType::Default,
1397                        StyleCursor::VerticalText => CursorType::Text,
1398                        StyleCursor::Unset => CursorType::Text, // Default to text for text nodes
1399                    };
1400                }
1401            }
1402        }
1403        
1404        // Default: Text cursor (I-beam) for text nodes
1405        CursorType::Text
1406    }
1407
1408    /// Emits drawing commands for text selections only (not cursor).
1409    /// The cursor is drawn separately via `paint_cursor()`.
1410    fn paint_selections(
1411        &self,
1412        builder: &mut DisplayListBuilder,
1413        node_index: usize,
1414    ) -> Result<()> {
1415        let node = self
1416            .positioned_tree
1417            .tree
1418            .get(node_index)
1419            .ok_or(LayoutError::InvalidTree)?;
1420        let Some(dom_id) = node.dom_node_id else {
1421            return Ok(());
1422        };
1423        
1424        // Get inline layout using the unified helper that handles IFC membership
1425        // This is critical: text nodes don't have their own inline_layout_result,
1426        // but they have ifc_membership pointing to their IFC root
1427        let Some(layout) = self.positioned_tree.tree.get_inline_layout_for_node(node_index) else {
1428            return Ok(());
1429        };
1430
1431        // Get the absolute position of this node (border-box position)
1432        let node_pos = self
1433            .positioned_tree
1434            .calculated_positions
1435            .get(node_index)
1436            .copied()
1437            .unwrap_or_default();
1438
1439        // Selection rects are relative to content-box origin
1440        let padding = &node.box_props.padding;
1441        let border = &node.box_props.border;
1442        let content_box_offset_x = node_pos.x + padding.left + border.left;
1443        let content_box_offset_y = node_pos.y + padding.top + border.top;
1444
1445        // Check if text is selectable (respects CSS user-select property)
1446        let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
1447        let is_selectable = super::getters::is_text_selectable(self.ctx.styled_dom, dom_id, node_state);
1448        
1449        if !is_selectable {
1450            return Ok(());
1451        }
1452
1453        // === NEW: Check text_selections first (multi-node selection support) ===
1454        if let Some(text_selection) = self.ctx.text_selections.get(&self.ctx.styled_dom.dom_id) {
1455            if let Some(range) = text_selection.affected_nodes.get(&dom_id) {
1456                let is_collapsed = text_selection.is_collapsed();
1457                
1458                // Only draw selection highlight if NOT collapsed
1459                if !is_collapsed {
1460                    let rects = layout.get_selection_rects(range);
1461                    let style = get_selection_style(self.ctx.styled_dom, Some(dom_id), self.ctx.system_style.as_ref());
1462
1463                    let border_radius = BorderRadius {
1464                        top_left: style.radius,
1465                        top_right: style.radius,
1466                        bottom_left: style.radius,
1467                        bottom_right: style.radius,
1468                    };
1469
1470                    for mut rect in rects {
1471                        rect.origin.x += content_box_offset_x;
1472                        rect.origin.y += content_box_offset_y;
1473                        builder.push_selection_rect(rect, style.bg_color, border_radius);
1474                    }
1475                }
1476                
1477                return Ok(());
1478            }
1479        }
1480
1481        // === LEGACY: Fall back to old selections for backward compatibility ===
1482        let Some(selection_state) = self.ctx.selections.get(&self.ctx.styled_dom.dom_id) else {
1483            return Ok(());
1484        };
1485
1486        if selection_state.node_id.node.into_crate_internal() != Some(dom_id) {
1487            return Ok(());
1488        }
1489
1490        for selection in selection_state.selections.as_slice() {
1491            if let Selection::Range(range) = &selection {
1492                let rects = layout.get_selection_rects(range);
1493                let style = get_selection_style(self.ctx.styled_dom, Some(dom_id), self.ctx.system_style.as_ref());
1494
1495                let border_radius = BorderRadius {
1496                    top_left: style.radius,
1497                    top_right: style.radius,
1498                    bottom_left: style.radius,
1499                    bottom_right: style.radius,
1500                };
1501
1502                for mut rect in rects {
1503                    rect.origin.x += content_box_offset_x;
1504                    rect.origin.y += content_box_offset_y;
1505                    builder.push_selection_rect(rect, style.bg_color, border_radius);
1506                }
1507            }
1508        }
1509
1510        Ok(())
1511    }
1512
1513    /// Emits drawing commands for the text cursor (caret) only.
1514    /// This is separate from selections and reads from `ctx.cursor_location`.
1515    fn paint_cursor(
1516        &self,
1517        builder: &mut DisplayListBuilder,
1518        node_index: usize,
1519    ) -> Result<()> {
1520        // Early exit if cursor is not visible (blinking off phase)
1521        if !self.ctx.cursor_is_visible {
1522            return Ok(());
1523        }
1524        
1525        // Early exit if no cursor location is set
1526        let Some((cursor_dom_id, cursor_node_id, cursor)) = &self.ctx.cursor_location else {
1527            return Ok(());
1528        };
1529
1530        let node = self
1531            .positioned_tree
1532            .tree
1533            .get(node_index)
1534            .ok_or(LayoutError::InvalidTree)?;
1535        let Some(dom_id) = node.dom_node_id else {
1536            return Ok(());
1537        };
1538        
1539        // Only paint cursor on the node that has the cursor
1540        if dom_id != *cursor_node_id {
1541            return Ok(());
1542        }
1543        
1544        // Check DOM ID matches
1545        if self.ctx.styled_dom.dom_id != *cursor_dom_id {
1546            return Ok(());
1547        }
1548
1549        // Get inline layout using the unified helper that handles IFC membership
1550        // This is critical: text nodes don't have their own inline_layout_result,
1551        // but they have ifc_membership pointing to their IFC root
1552        let Some(layout) = self.positioned_tree.tree.get_inline_layout_for_node(node_index) else {
1553            return Ok(());
1554        };
1555
1556        // Check if this node is contenteditable (or inherits contenteditable from ancestor)
1557        // Text nodes don't have contenteditable directly, but inherit it from their container
1558        let is_contenteditable = super::getters::is_node_contenteditable_inherited(self.ctx.styled_dom, dom_id);
1559        if !is_contenteditable {
1560            return Ok(());
1561        }
1562        
1563        // Check if text is selectable
1564        let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
1565        let is_selectable = super::getters::is_text_selectable(self.ctx.styled_dom, dom_id, node_state);
1566        if !is_selectable {
1567            return Ok(());
1568        }
1569
1570        // Get cursor rect from text layout
1571        let Some(mut rect) = layout.get_cursor_rect(cursor) else {
1572            return Ok(());
1573        };
1574
1575        // Get the absolute position of this node (border-box position)
1576        let node_pos = self
1577            .positioned_tree
1578            .calculated_positions
1579            .get(node_index)
1580            .copied()
1581            .unwrap_or_default();
1582
1583        // Adjust to content-box coordinates
1584        let padding = &node.box_props.padding;
1585        let border = &node.box_props.border;
1586        let content_box_offset_x = node_pos.x + padding.left + border.left;
1587        let content_box_offset_y = node_pos.y + padding.top + border.top;
1588
1589        rect.origin.x += content_box_offset_x;
1590        rect.origin.y += content_box_offset_y;
1591
1592        let style = get_caret_style(self.ctx.styled_dom, Some(dom_id));
1593        
1594        // Apply caret width from CSS (default is 2px, get_cursor_rect returns 1px)
1595        rect.size.width = style.width;
1596        
1597        builder.push_cursor_rect(rect, style.color);
1598
1599        Ok(())
1600    }
1601
1602    /// Emits drawing commands for selection and cursor.
1603    /// Delegates to `paint_selections()` and `paint_cursor()`.
1604    fn paint_selection_and_cursor(
1605        &self,
1606        builder: &mut DisplayListBuilder,
1607        node_index: usize,
1608    ) -> Result<()> {
1609        self.paint_selections(builder, node_index)?;
1610        self.paint_cursor(builder, node_index)?;
1611        Ok(())
1612    }
1613
1614    /// Recursively builds the tree of stacking contexts starting from a given layout node.
1615    fn collect_stacking_contexts(&mut self, node_index: usize) -> Result<StackingContext> {
1616        let node = self
1617            .positioned_tree
1618            .tree
1619            .get(node_index)
1620            .ok_or(LayoutError::InvalidTree)?;
1621        let z_index = get_z_index(self.ctx.styled_dom, node.dom_node_id);
1622
1623        if let Some(dom_id) = node.dom_node_id {
1624            let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
1625            debug_info!(
1626                self.ctx,
1627                "Collecting stacking context for node {} ({:?}), z-index={}",
1628                node_index,
1629                node_type.get_node_type(),
1630                z_index
1631            );
1632        }
1633
1634        let mut child_contexts = Vec::new();
1635        let mut in_flow_children = Vec::new();
1636
1637        for &child_index in &node.children {
1638            if self.establishes_stacking_context(child_index) {
1639                child_contexts.push(self.collect_stacking_contexts(child_index)?);
1640            } else {
1641                in_flow_children.push(child_index);
1642            }
1643        }
1644
1645        Ok(StackingContext {
1646            node_index,
1647            z_index,
1648            child_contexts,
1649            in_flow_children,
1650        })
1651    }
1652
1653    /// Recursively traverses the stacking context tree, emitting drawing commands to the builder
1654    /// according to the CSS Painting Algorithm specification.
1655    fn generate_for_stacking_context(
1656        &mut self,
1657        builder: &mut DisplayListBuilder,
1658        context: &StackingContext,
1659    ) -> Result<()> {
1660        // Before painting the node, check if it establishes a new clip or scroll frame.
1661        let node = self
1662            .positioned_tree
1663            .tree
1664            .get(context.node_index)
1665            .ok_or(LayoutError::InvalidTree)?;
1666
1667        if let Some(dom_id) = node.dom_node_id {
1668            let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
1669            debug_info!(
1670                self.ctx,
1671                "Painting stacking context for node {} ({:?}), z-index={}, {} child contexts, {} \
1672                 in-flow children",
1673                context.node_index,
1674                node_type.get_node_type(),
1675                context.z_index,
1676                context.child_contexts.len(),
1677                context.in_flow_children.len()
1678            );
1679        }
1680
1681        // Set current node BEFORE pushing stacking context so that
1682        // the PushStackingContext item gets the correct node_mapping entry.
1683        // This is critical for drag visual offset matching.
1684        builder.set_current_node(node.dom_node_id);
1685
1686        // Check if this node has a GPU-accelerated transform (CSS transform or drag).
1687        // If so, wrap in a reference frame so WebRender can animate it on the GPU.
1688        let has_reference_frame = node.dom_node_id.and_then(|dom_id| {
1689            self.gpu_value_cache.and_then(|cache| {
1690                let key = cache.transform_keys.get(&dom_id)?;
1691                let transform = cache.current_transform_values.get(&dom_id)?;
1692                Some((*key, *transform))
1693            })
1694        });
1695
1696        // Push a stacking context for WebRender
1697        // Get the node's bounds for the stacking context
1698        let node_pos = self
1699            .positioned_tree
1700            .calculated_positions
1701            .get(context.node_index)
1702            .copied()
1703            .unwrap_or_default();
1704        let node_size = node.used_size.unwrap_or(LogicalSize {
1705            width: 0.0,
1706            height: 0.0,
1707        });
1708        let node_bounds = LogicalRect {
1709            origin: node_pos,
1710            size: node_size,
1711        };
1712
1713        // Push reference frame BEFORE stacking context if node has a transform
1714        if let Some((transform_key, initial_transform)) = has_reference_frame {
1715            builder.push_reference_frame(transform_key, initial_transform, node_bounds);
1716        }
1717
1718        builder.push_stacking_context(context.z_index, node_bounds);
1719
1720        // Push opacity/filter effects if the node has them
1721        let mut pushed_opacity = false;
1722        let mut pushed_filter = false;
1723        let mut pushed_backdrop_filter = false;
1724
1725        if let Some(dom_id) = node.dom_node_id {
1726            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
1727            let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
1728
1729            // Opacity
1730            let opacity = self.ctx.styled_dom.css_property_cache.ptr
1731                .get_opacity(node_data, &dom_id, node_state)
1732                .and_then(|v| v.get_property())
1733                .map(|v| v.inner.normalized())
1734                .unwrap_or(1.0);
1735
1736            if opacity < 1.0 {
1737                builder.push_item(DisplayListItem::PushOpacity {
1738                    bounds: node_bounds,
1739                    opacity,
1740                });
1741                pushed_opacity = true;
1742            }
1743
1744            // Filter
1745            if let Some(filter_vec_value) = self.ctx.styled_dom.css_property_cache.ptr
1746                .get_filter(node_data, &dom_id, node_state)
1747            {
1748                if let Some(filter_vec) = filter_vec_value.get_property() {
1749                    let filters: Vec<_> = filter_vec.as_ref().to_vec();
1750                    if !filters.is_empty() {
1751                        builder.push_item(DisplayListItem::PushFilter {
1752                            bounds: node_bounds,
1753                            filters,
1754                        });
1755                        pushed_filter = true;
1756                    }
1757                }
1758            }
1759
1760            // Backdrop filter
1761            if let Some(backdrop_filter_value) = self.ctx.styled_dom.css_property_cache.ptr
1762                .get_backdrop_filter(node_data, &dom_id, node_state)
1763            {
1764                if let Some(filter_vec) = backdrop_filter_value.get_property() {
1765                    let filters: Vec<_> = filter_vec.as_ref().to_vec();
1766                    if !filters.is_empty() {
1767                        builder.push_item(DisplayListItem::PushBackdropFilter {
1768                            bounds: node_bounds,
1769                            filters,
1770                        });
1771                        pushed_backdrop_filter = true;
1772                    }
1773                }
1774            }
1775        }
1776
1777        // 1. Paint background and borders for the context's root element.
1778        // This must be BEFORE push_node_clips so the container background
1779        // is rendered in parent space (stationary), not scroll space.
1780        self.paint_node_background_and_border(builder, context.node_index)?;
1781
1782        // 1b. For scrollable containers, push the hit-test area BEFORE the scroll frame
1783        // so the hit-test covers the entire container box (including visible area),
1784        // not just the scrolled content. This ensures scroll wheel events hit the
1785        // container regardless of scroll position.
1786        if let Some(dom_id) = node.dom_node_id {
1787            let styled_node_state = self.get_styled_node_state(dom_id);
1788            let overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
1789            let overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
1790            if overflow_x.is_scroll() || overflow_y.is_scroll() {
1791                if let Some(tag_id) = get_tag_id(self.ctx.styled_dom, node.dom_node_id) {
1792                    builder.push_hit_test_area(node_bounds, tag_id);
1793                }
1794            }
1795        }
1796
1797        // 2. Push clips and scroll frames AFTER painting background
1798        let did_push_clip_or_scroll = self.push_node_clips(builder, context.node_index, node)?;
1799
1800        // 3. Paint child stacking contexts with negative z-indices.
1801        let mut negative_z_children: Vec<_> = context
1802            .child_contexts
1803            .iter()
1804            .filter(|c| c.z_index < 0)
1805            .collect();
1806        negative_z_children.sort_by_key(|c| c.z_index);
1807        for child in negative_z_children {
1808            self.generate_for_stacking_context(builder, child)?;
1809        }
1810
1811        // 4. Paint the in-flow descendants of the context root.
1812        self.paint_in_flow_descendants(builder, context.node_index, &context.in_flow_children)?;
1813
1814        // 5. Paint child stacking contexts with z-index: 0 / auto.
1815        for child in context.child_contexts.iter().filter(|c| c.z_index == 0) {
1816            self.generate_for_stacking_context(builder, child)?;
1817        }
1818
1819        // 6. Paint child stacking contexts with positive z-indices.
1820        let mut positive_z_children: Vec<_> = context
1821            .child_contexts
1822            .iter()
1823            .filter(|c| c.z_index > 0)
1824            .collect();
1825
1826        positive_z_children.sort_by_key(|c| c.z_index);
1827
1828        for child in positive_z_children {
1829            self.generate_for_stacking_context(builder, child)?;
1830        }
1831
1832        // Pop filter/opacity effects (in reverse order of push)
1833        if pushed_backdrop_filter {
1834            builder.push_item(DisplayListItem::PopBackdropFilter);
1835        }
1836        if pushed_filter {
1837            builder.push_item(DisplayListItem::PopFilter);
1838        }
1839        if pushed_opacity {
1840            builder.push_item(DisplayListItem::PopOpacity);
1841        }
1842
1843        // Pop the stacking context for WebRender
1844        builder.pop_stacking_context();
1845
1846        // Pop reference frame if we pushed one
1847        if has_reference_frame.is_some() {
1848            builder.pop_reference_frame();
1849        }
1850
1851        // After painting the node and all its descendants, pop any contexts it pushed.
1852        if did_push_clip_or_scroll {
1853            self.pop_node_clips(builder, node)?;
1854        }
1855
1856        // Paint scrollbars AFTER popping the clip, so they appear on top of content
1857        // and are not clipped by the scroll frame
1858        self.paint_scrollbars(builder, context.node_index)?;
1859
1860        Ok(())
1861    }
1862
1863    /// Paints the content and non-stacking-context children.
1864    fn paint_in_flow_descendants(
1865        &mut self,
1866        builder: &mut DisplayListBuilder,
1867        node_index: usize,
1868        children_indices: &[usize],
1869    ) -> Result<()> {
1870        // NOTE: We do NOT paint the node's background here - that was already done by
1871        // generate_for_stacking_context! Only paint selection, cursor, and content for the
1872        // current node
1873
1874        // 2. Paint selection highlights and the text cursor if applicable.
1875        self.paint_selection_and_cursor(builder, node_index)?;
1876
1877        // 3. Paint the node's own content (text, images, hit-test areas).
1878        self.paint_node_content(builder, node_index)?;
1879
1880        // 4. Recursively paint the in-flow children in correct CSS painting order:
1881        //    - First: Non-float, non-dragging block-level children
1882        //    - Then: Float, non-dragging children (so they appear on top)
1883        //    - Finally: Dragging children (so they appear on top of everything per W3C spec)
1884
1885        // Separate children into floats, non-floats, and dragging
1886        let mut non_float_children = Vec::new();
1887        let mut float_children = Vec::new();
1888        let mut dragging_children = Vec::new();
1889
1890        for &child_index in children_indices {
1891            let child_node = self
1892                .positioned_tree
1893                .tree
1894                .get(child_index)
1895                .ok_or(LayoutError::InvalidTree)?;
1896
1897            // Check if this child is being dragged (paint last for z-order)
1898            let is_dragging = if let Some(dom_id) = child_node.dom_node_id {
1899                let styled_node_state = self.get_styled_node_state(dom_id);
1900                styled_node_state.dragging
1901            } else {
1902                false
1903            };
1904
1905            if is_dragging {
1906                dragging_children.push(child_index);
1907                continue;
1908            }
1909
1910            // Check if this child is a float
1911            let is_float = if let Some(dom_id) = child_node.dom_node_id {
1912                use crate::solver3::getters::get_float;
1913                let styled_node_state = self.get_styled_node_state(dom_id);
1914                let float_value = get_float(self.ctx.styled_dom, dom_id, &styled_node_state);
1915                !matches!(
1916                    float_value.unwrap_or_default(),
1917                    azul_css::props::layout::LayoutFloat::None
1918                )
1919            } else {
1920                false
1921            };
1922
1923            if is_float {
1924                float_children.push(child_index);
1925            } else {
1926                non_float_children.push(child_index);
1927            }
1928        }
1929
1930        // Paint non-float children first
1931        for child_index in non_float_children {
1932            let child_node = self
1933                .positioned_tree
1934                .tree
1935                .get(child_index)
1936                .ok_or(LayoutError::InvalidTree)?;
1937
1938            // Check if this child has a GPU transform (CSS transform or drag)
1939            let child_ref_frame = child_node.dom_node_id.and_then(|dom_id| {
1940                self.gpu_value_cache.and_then(|cache| {
1941                    let key = cache.transform_keys.get(&dom_id)?;
1942                    let transform = cache.current_transform_values.get(&dom_id)?;
1943                    Some((*key, *transform))
1944                })
1945            });
1946
1947            // Push reference frame if child has a transform
1948            if let Some((transform_key, initial_transform)) = child_ref_frame {
1949                let child_pos = self
1950                    .positioned_tree
1951                    .calculated_positions
1952            .get(child_index)
1953                    .copied()
1954                    .unwrap_or_default();
1955                let child_size = child_node.used_size.unwrap_or(LogicalSize {
1956                    width: 0.0,
1957                    height: 0.0,
1958                });
1959                let child_bounds = LogicalRect {
1960                    origin: child_pos,
1961                    size: child_size,
1962                };
1963                builder.set_current_node(child_node.dom_node_id);
1964                builder.push_reference_frame(transform_key, initial_transform, child_bounds);
1965            }
1966
1967            // IMPORTANT: Paint background and border BEFORE pushing clips!
1968            // This ensures the container's background is in parent space (stationary),
1969            // not in scroll space. Same logic as generate_for_stacking_context.
1970            self.paint_node_background_and_border(builder, child_index)?;
1971
1972            // Push clips and scroll frames AFTER painting background
1973            let did_push_clip = self.push_node_clips(builder, child_index, child_node)?;
1974
1975            // Paint descendants inside the clip/scroll frame
1976            self.paint_in_flow_descendants(builder, child_index, &child_node.children)?;
1977
1978            // Pop the child's clips.
1979            if did_push_clip {
1980                self.pop_node_clips(builder, child_node)?;
1981            }
1982
1983            // Paint scrollbars AFTER popping clips so they appear on top of content
1984            self.paint_scrollbars(builder, child_index)?;
1985
1986            // Pop reference frame if we pushed one
1987            if child_ref_frame.is_some() {
1988                builder.pop_reference_frame();
1989            }
1990        }
1991
1992        // Paint float children AFTER non-floats (so they appear on top)
1993        for child_index in float_children {
1994            let child_node = self
1995                .positioned_tree
1996                .tree
1997                .get(child_index)
1998                .ok_or(LayoutError::InvalidTree)?;
1999
2000            // Check if this child has a GPU transform (CSS transform or drag)
2001            let child_ref_frame = child_node.dom_node_id.and_then(|dom_id| {
2002                self.gpu_value_cache.and_then(|cache| {
2003                    let key = cache.transform_keys.get(&dom_id)?;
2004                    let transform = cache.current_transform_values.get(&dom_id)?;
2005                    Some((*key, *transform))
2006                })
2007            });
2008
2009            // Push reference frame if child has a transform
2010            if let Some((transform_key, initial_transform)) = child_ref_frame {
2011                let child_pos = self
2012                    .positioned_tree
2013                    .calculated_positions
2014            .get(child_index)
2015                    .copied()
2016                    .unwrap_or_default();
2017                let child_size = child_node.used_size.unwrap_or(LogicalSize {
2018                    width: 0.0,
2019                    height: 0.0,
2020                });
2021                let child_bounds = LogicalRect {
2022                    origin: child_pos,
2023                    size: child_size,
2024                };
2025                builder.set_current_node(child_node.dom_node_id);
2026                builder.push_reference_frame(transform_key, initial_transform, child_bounds);
2027            }
2028
2029            // Same as above: paint background BEFORE clips
2030            self.paint_node_background_and_border(builder, child_index)?;
2031            let did_push_clip = self.push_node_clips(builder, child_index, child_node)?;
2032            self.paint_in_flow_descendants(builder, child_index, &child_node.children)?;
2033
2034            if did_push_clip {
2035                self.pop_node_clips(builder, child_node)?;
2036            }
2037
2038            // Paint scrollbars AFTER popping clips so they appear on top of content
2039            self.paint_scrollbars(builder, child_index)?;
2040
2041            // Pop reference frame if we pushed one
2042            if child_ref_frame.is_some() {
2043                builder.pop_reference_frame();
2044            }
2045        }
2046
2047        // Paint dragging children LAST so they appear on top of everything (W3C spec)
2048        for child_index in dragging_children {
2049            let child_node = self
2050                .positioned_tree
2051                .tree
2052                .get(child_index)
2053                .ok_or(LayoutError::InvalidTree)?;
2054
2055            // Check if this child has a GPU transform (CSS transform or drag)
2056            let child_ref_frame = child_node.dom_node_id.and_then(|dom_id| {
2057                self.gpu_value_cache.and_then(|cache| {
2058                    let key = cache.transform_keys.get(&dom_id)?;
2059                    let transform = cache.current_transform_values.get(&dom_id)?;
2060                    Some((*key, *transform))
2061                })
2062            });
2063
2064            // Push reference frame if child has a transform
2065            if let Some((transform_key, initial_transform)) = child_ref_frame {
2066                let child_pos = self
2067                    .positioned_tree
2068                    .calculated_positions
2069            .get(child_index)
2070                    .copied()
2071                    .unwrap_or_default();
2072                let child_size = child_node.used_size.unwrap_or(LogicalSize {
2073                    width: 0.0,
2074                    height: 0.0,
2075                });
2076                let child_bounds = LogicalRect {
2077                    origin: child_pos,
2078                    size: child_size,
2079                };
2080                builder.set_current_node(child_node.dom_node_id);
2081                builder.push_reference_frame(transform_key, initial_transform, child_bounds);
2082            }
2083
2084            // Same as above: paint background BEFORE clips
2085            self.paint_node_background_and_border(builder, child_index)?;
2086            let did_push_clip = self.push_node_clips(builder, child_index, child_node)?;
2087            self.paint_in_flow_descendants(builder, child_index, &child_node.children)?;
2088
2089            if did_push_clip {
2090                self.pop_node_clips(builder, child_node)?;
2091            }
2092
2093            // Paint scrollbars AFTER popping clips so they appear on top of content
2094            self.paint_scrollbars(builder, child_index)?;
2095
2096            // Pop reference frame if we pushed one
2097            if child_ref_frame.is_some() {
2098                builder.pop_reference_frame();
2099            }
2100        }
2101
2102        Ok(())
2103    }
2104
2105    /// Checks if a node requires clipping or scrolling and pushes the appropriate commands.
2106    /// Returns true if any command was pushed.
2107    fn push_node_clips(
2108        &self,
2109        builder: &mut DisplayListBuilder,
2110        node_index: usize,
2111        node: &LayoutNode,
2112    ) -> Result<bool> {
2113        let Some(dom_id) = node.dom_node_id else {
2114            return Ok(false);
2115        };
2116
2117        let styled_node_state = self.get_styled_node_state(dom_id);
2118
2119        let overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
2120        let overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
2121
2122        let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
2123        let element_size = PhysicalSizeImport {
2124            width: paint_rect.size.width,
2125            height: paint_rect.size.height,
2126        };
2127        let border_radius = get_border_radius(
2128            self.ctx.styled_dom,
2129            dom_id,
2130            &styled_node_state,
2131            element_size,
2132            self.ctx.viewport_size,
2133        );
2134
2135        let needs_clip = overflow_x.is_clipped() || overflow_y.is_clipped();
2136
2137        if !needs_clip {
2138            return Ok(false);
2139        }
2140
2141        let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
2142
2143        let border = &node.box_props.border;
2144
2145        // Get scrollbar info to adjust clip rect for content area
2146        let scrollbar_info = get_scrollbar_info_from_layout(node);
2147
2148        // The clip rect for content should exclude the scrollbar area
2149        // Scrollbars are drawn inside the border-box, on the right/bottom edges
2150        let clip_rect = LogicalRect {
2151            origin: LogicalPosition {
2152                x: paint_rect.origin.x + border.left,
2153                y: paint_rect.origin.y + border.top,
2154            },
2155            size: LogicalSize {
2156                // Reduce width/height by scrollbar dimensions so content doesn't overlap scrollbar
2157                width: (paint_rect.size.width
2158                    - border.left
2159                    - border.right
2160                    - scrollbar_info.scrollbar_width)
2161                    .max(0.0),
2162                height: (paint_rect.size.height
2163                    - border.top
2164                    - border.bottom
2165                    - scrollbar_info.scrollbar_height)
2166                    .max(0.0),
2167            },
2168        };
2169
2170        if overflow_x.is_scroll() || overflow_y.is_scroll() {
2171            // For scroll/auto: push BOTH a clip AND a scroll frame
2172            // The clip ensures content is clipped (in parent space)
2173            // The scroll frame enables scrolling (creates new spatial node)
2174            builder.push_clip(clip_rect, border_radius);
2175            let scroll_id = self.scroll_ids.get(&node_index).copied().unwrap_or(0);
2176            let content_size = get_scroll_content_size(node);
2177            builder.push_scroll_frame(clip_rect, content_size, scroll_id);
2178        } else {
2179            // Simple clip for hidden/clip
2180            builder.push_clip(clip_rect, border_radius);
2181        }
2182
2183        Ok(true)
2184    }
2185
2186    /// Pops any clip/scroll commands associated with a node.
2187    fn pop_node_clips(&self, builder: &mut DisplayListBuilder, node: &LayoutNode) -> Result<()> {
2188        let Some(dom_id) = node.dom_node_id else {
2189            return Ok(());
2190        };
2191
2192        let styled_node_state = self.get_styled_node_state(dom_id);
2193        let overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
2194        let overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
2195
2196        let paint_rect = self
2197            .get_paint_rect(
2198                self.positioned_tree
2199                    .tree
2200                    .nodes
2201                    .iter()
2202                    .position(|n| n.dom_node_id == Some(dom_id))
2203                    .unwrap_or(0),
2204            )
2205            .unwrap_or_default();
2206
2207        let element_size = PhysicalSizeImport {
2208            width: paint_rect.size.width,
2209            height: paint_rect.size.height,
2210        };
2211        let border_radius = get_border_radius(
2212            self.ctx.styled_dom,
2213            dom_id,
2214            &styled_node_state,
2215            element_size,
2216            self.ctx.viewport_size,
2217        );
2218
2219        let needs_clip =
2220            overflow_x.is_clipped() || overflow_y.is_clipped() || !border_radius.is_zero();
2221
2222        if needs_clip {
2223            if overflow_x.is_scroll() || overflow_y.is_scroll() {
2224                // For scroll or auto overflow, pop both scroll frame AND clip
2225                builder.pop_scroll_frame();
2226                builder.pop_clip();
2227            } else {
2228                // For hidden/clip, pop the simple clip
2229                builder.pop_clip();
2230            }
2231        }
2232        Ok(())
2233    }
2234
2235    /// Calculates the final paint-time rectangle for a node.
2236    /// 
2237    /// ## Coordinate Space
2238    /// 
2239    /// Returns the node's position in **absolute window coordinates** (logical pixels).
2240    /// This is the coordinate space used throughout the display list:
2241    /// 
2242    /// - Origin: Top-left corner of the window
2243    /// - Units: Logical pixels (HiDPI scaling happens in compositor2.rs)
2244    /// - Scroll: NOT applied here - WebRender scroll frames handle scroll offset
2245    ///   transformation internally via `define_scroll_frame()`
2246    /// 
2247    /// ## Important
2248    /// 
2249    /// Do NOT manually subtract scroll offset here! WebRender's scroll spatial
2250    /// transforms handle this. Subtracting here would cause double-offset and
2251    /// parallax effects (backgrounds and text moving at different speeds).
2252    fn get_paint_rect(&self, node_index: usize) -> Option<LogicalRect> {
2253        let node = self.positioned_tree.tree.get(node_index)?;
2254        let pos = self
2255            .positioned_tree
2256            .calculated_positions
2257            .get(node_index)
2258            .copied()
2259            .unwrap_or_default();
2260        let size = node.used_size.unwrap_or_default();
2261
2262        // NOTE: Scroll offset is NOT applied here!
2263        // WebRender scroll frames handle scroll transformation.
2264        // See compositor2.rs PushScrollFrame for details.
2265
2266        Some(LogicalRect::new(pos, size))
2267    }
2268
2269    /// Emits drawing commands for the background and border of a single node.
2270    fn paint_node_background_and_border(
2271        &mut self,
2272        builder: &mut DisplayListBuilder,
2273        node_index: usize,
2274    ) -> Result<()> {
2275        let Some(paint_rect) = self.get_paint_rect(node_index) else {
2276            return Ok(());
2277        };
2278        let node = self
2279            .positioned_tree
2280            .tree
2281            .get(node_index)
2282            .ok_or(LayoutError::InvalidTree)?;
2283
2284        // Set current node for node mapping (for pagination break properties)
2285        builder.set_current_node(node.dom_node_id);
2286
2287        // Check for CSS break-before/break-after properties and register forced page breaks
2288        // This is used by the pagination slicer to insert page breaks at correct positions
2289        if let Some(dom_id) = node.dom_node_id {
2290            let break_before = get_break_before(self.ctx.styled_dom, Some(dom_id));
2291            let break_after = get_break_after(self.ctx.styled_dom, Some(dom_id));
2292
2293            // For break-before: always, insert a page break at the top of this element
2294            if is_forced_page_break(break_before) {
2295                let y_position = paint_rect.origin.y;
2296                builder.add_forced_page_break(y_position);
2297                debug_info!(
2298                    self.ctx,
2299                    "Registered forced page break BEFORE node {} at y={}",
2300                    node_index,
2301                    y_position
2302                );
2303            }
2304
2305            // For break-after: always, insert a page break at the bottom of this element
2306            if is_forced_page_break(break_after) {
2307                let y_position = paint_rect.origin.y + paint_rect.size.height;
2308                builder.add_forced_page_break(y_position);
2309                debug_info!(
2310                    self.ctx,
2311                    "Registered forced page break AFTER node {} at y={}",
2312                    node_index,
2313                    y_position
2314                );
2315            }
2316        }
2317
2318        // Skip inline and inline-block elements ONLY if they participate in an IFC (Inline Formatting Context).
2319        // In Flex or Grid containers, inline-block elements are treated as flex/grid items and must be painted here.
2320        // Inline elements participate in inline formatting context and their backgrounds
2321        // must be positioned by the text layout engine, not the block layout engine
2322        //
2323        // IMPORTANT: The parent check must look at the PARENT NODE's formatting_context,
2324        // not the current node's. If parent is Flex/Grid, we paint this element as a flex/grid item.
2325        // Also check parent_formatting_context field which stores parent's FC during tree construction.
2326        let parent_is_flex_or_grid = node.parent_formatting_context
2327            .as_ref()
2328            .map(|fc| matches!(fc, FormattingContext::Flex | FormattingContext::Grid))
2329            .unwrap_or(false);
2330        
2331        if let Some(dom_id) = node.dom_node_id {
2332            let display = {
2333                use crate::solver3::getters::get_display_property;
2334                get_display_property(self.ctx.styled_dom, Some(dom_id))
2335                    .unwrap_or(LayoutDisplay::Inline)
2336            };
2337
2338            if display == LayoutDisplay::InlineBlock || display == LayoutDisplay::Inline {
2339                debug_info!(
2340                    self.ctx,
2341                    "[paint_node] node {} has display={:?}, parent_formatting_context={:?}, parent_is_flex_or_grid={}",
2342                    node_index,
2343                    display,
2344                    node.parent_formatting_context,
2345                    parent_is_flex_or_grid
2346                );
2347
2348                if !parent_is_flex_or_grid {
2349                    // text3 will handle this via InlineShape (for inline-block)
2350                    // or glyph runs with background_color (for inline)
2351                    return Ok(());
2352                }
2353                // Fall through to paint this element - it's a flex/grid item
2354            }
2355        }
2356
2357        // CSS 2.2 Section 17.5.1: Tables in the visual formatting model
2358        // Tables have a special 6-layer background painting order
2359        if matches!(node.formatting_context, FormattingContext::Table) {
2360            debug_info!(
2361                self.ctx,
2362                "Painting table backgrounds/borders for node {} at {:?}",
2363                node_index,
2364                paint_rect
2365            );
2366            // Delegate to specialized table painting function
2367            return self.paint_table_items(builder, node_index);
2368        }
2369
2370        let border_radius = if let Some(dom_id) = node.dom_node_id {
2371            let styled_node_state = self.get_styled_node_state(dom_id);
2372            let background_contents =
2373                get_background_contents(self.ctx.styled_dom, dom_id, &styled_node_state);
2374            let border_info = get_border_info(self.ctx.styled_dom, dom_id, &styled_node_state);
2375
2376            let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
2377            debug_info!(
2378                self.ctx,
2379                "Painting background/border for node {} ({:?}) at {:?}, backgrounds={:?}",
2380                node_index,
2381                node_type.get_node_type(),
2382                paint_rect,
2383                background_contents.len()
2384            );
2385
2386            // Get both versions: simple BorderRadius for rect clipping and StyleBorderRadius for
2387            // border rendering
2388            let element_size = PhysicalSizeImport {
2389                width: paint_rect.size.width,
2390                height: paint_rect.size.height,
2391            };
2392            let simple_border_radius = get_border_radius(
2393                self.ctx.styled_dom,
2394                dom_id,
2395                &styled_node_state,
2396                element_size,
2397                self.ctx.viewport_size,
2398            );
2399            let style_border_radius =
2400                get_style_border_radius(self.ctx.styled_dom, dom_id, &styled_node_state);
2401
2402            // Paint box shadows before backgrounds (CSS spec: shadows render behind the element)
2403            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
2404            let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
2405
2406            // Check all four sides for box-shadow (azul stores them per-side)
2407            for get_shadow_fn in [
2408                azul_core::prop_cache::CssPropertyCache::get_box_shadow_left,
2409                azul_core::prop_cache::CssPropertyCache::get_box_shadow_right,
2410                azul_core::prop_cache::CssPropertyCache::get_box_shadow_top,
2411                azul_core::prop_cache::CssPropertyCache::get_box_shadow_bottom,
2412            ] {
2413                if let Some(shadow_value) = get_shadow_fn(
2414                    &self.ctx.styled_dom.css_property_cache.ptr,
2415                    node_data,
2416                    &dom_id,
2417                    &node_state,
2418                ) {
2419                    if let Some(shadow) = shadow_value.get_property() {
2420                        builder.push_item(DisplayListItem::BoxShadow {
2421                            bounds: paint_rect,
2422                            shadow: shadow.clone(),
2423                            border_radius: simple_border_radius,
2424                        });
2425                    }
2426                }
2427            }
2428
2429            // Use unified background/border painting
2430            builder.push_backgrounds_and_border(
2431                paint_rect,
2432                &background_contents,
2433                &border_info,
2434                simple_border_radius,
2435                style_border_radius,
2436            );
2437
2438            simple_border_radius
2439        } else {
2440            BorderRadius::default()
2441        };
2442
2443        Ok(())
2444    }
2445
2446    /// CSS 2.2 Section 17.5.1: Table background painting in 6 layers
2447    ///
2448    /// Implements the CSS 2.2 specification for table background painting order.
2449    /// Unlike regular block elements, tables paint backgrounds in layers from back to front:
2450    ///
2451    /// 1. Table background (lowest layer)
2452    /// 2. Column group backgrounds
2453    /// 3. Column backgrounds
2454    /// 4. Row group backgrounds
2455    /// 5. Row backgrounds
2456    /// 6. Cell backgrounds (topmost layer)
2457    ///
2458    /// Then borders are painted (respecting border-collapse mode).
2459    /// Finally, cell content is painted on top of everything.
2460    ///
2461    /// This function generates simple display list items (Rect, Border) in the correct
2462    /// CSS paint order, making WebRender integration trivial.
2463    fn paint_table_items(
2464        &self,
2465        builder: &mut DisplayListBuilder,
2466        table_index: usize,
2467    ) -> Result<()> {
2468        let table_node = self
2469            .positioned_tree
2470            .tree
2471            .get(table_index)
2472            .ok_or(LayoutError::InvalidTree)?;
2473
2474        let Some(table_paint_rect) = self.get_paint_rect(table_index) else {
2475            return Ok(());
2476        };
2477
2478        // Layer 1: Table background
2479        if let Some(dom_id) = table_node.dom_node_id {
2480            let styled_node_state = self.get_styled_node_state(dom_id);
2481            let bg_color = get_background_color(self.ctx.styled_dom, dom_id, &styled_node_state);
2482            let element_size = PhysicalSizeImport {
2483                width: table_paint_rect.size.width,
2484                height: table_paint_rect.size.height,
2485            };
2486            let border_radius = get_border_radius(
2487                self.ctx.styled_dom,
2488                dom_id,
2489                &styled_node_state,
2490                element_size,
2491                self.ctx.viewport_size,
2492            );
2493
2494            builder.push_rect(table_paint_rect, bg_color, border_radius);
2495        }
2496
2497        // Traverse table children to paint layers 2-6
2498
2499        // Layer 2: Column group backgrounds
2500        // Layer 3: Column backgrounds (columns are children of column groups)
2501        for &child_idx in &table_node.children {
2502            let child_node = self.positioned_tree.tree.get(child_idx);
2503            if let Some(node) = child_node {
2504                if matches!(node.formatting_context, FormattingContext::TableColumnGroup) {
2505                    // Paint column group background
2506                    self.paint_element_background(builder, child_idx)?;
2507
2508                    // Paint backgrounds of individual columns within this group
2509                    for &col_idx in &node.children {
2510                        self.paint_element_background(builder, col_idx)?;
2511                    }
2512                }
2513            }
2514        }
2515
2516        // Layer 4: Row group backgrounds (tbody, thead, tfoot)
2517        // Layer 5: Row backgrounds
2518        // Layer 6: Cell backgrounds
2519        for &child_idx in &table_node.children {
2520            let child_node = self.positioned_tree.tree.get(child_idx);
2521            if let Some(node) = child_node {
2522                match node.formatting_context {
2523                    FormattingContext::TableRowGroup => {
2524                        // Paint row group background
2525                        self.paint_element_background(builder, child_idx)?;
2526
2527                        // Paint rows within this group
2528                        for &row_idx in &node.children {
2529                            self.paint_table_row_and_cells(builder, row_idx)?;
2530                        }
2531                    }
2532                    FormattingContext::TableRow => {
2533                        // Direct row child (no row group wrapper)
2534                        self.paint_table_row_and_cells(builder, child_idx)?;
2535                    }
2536                    _ => {}
2537                }
2538            }
2539        }
2540
2541        // Borders are painted separately after all backgrounds
2542        // This is handled by the normal rendering flow for each element
2543        // TODO: Implement border-collapse conflict resolution using BorderInfo::resolve_conflict()
2544
2545        Ok(())
2546    }
2547
2548    /// Helper function to paint a table row's background and then its cells' backgrounds
2549    /// Layer 5: Row background
2550    /// Layer 6: Cell backgrounds (painted after row, so they appear on top)
2551    fn paint_table_row_and_cells(
2552        &self,
2553        builder: &mut DisplayListBuilder,
2554        row_idx: usize,
2555    ) -> Result<()> {
2556        // Layer 5: Paint row background
2557        self.paint_element_background(builder, row_idx)?;
2558
2559        // Layer 6: Paint cell backgrounds (topmost layer)
2560        let row_node = self.positioned_tree.tree.get(row_idx);
2561        if let Some(node) = row_node {
2562            for &cell_idx in &node.children {
2563                self.paint_element_background(builder, cell_idx)?;
2564            }
2565        }
2566
2567        Ok(())
2568    }
2569
2570    /// Helper function to paint an element's background (used for all table elements)
2571    /// Reads background-color and border-radius from CSS properties and emits push_rect()
2572    fn paint_element_background(
2573        &self,
2574        builder: &mut DisplayListBuilder,
2575        node_index: usize,
2576    ) -> Result<()> {
2577        let Some(paint_rect) = self.get_paint_rect(node_index) else {
2578            return Ok(());
2579        };
2580
2581        let Some(node) = self.positioned_tree.tree.get(node_index) else {
2582            return Ok(());
2583        };
2584        let Some(dom_id) = node.dom_node_id else {
2585            return Ok(());
2586        };
2587
2588        let styled_node_state = self.get_styled_node_state(dom_id);
2589        let bg_color = get_background_color(self.ctx.styled_dom, dom_id, &styled_node_state);
2590
2591        // Only paint if background color has alpha > 0 (optimization)
2592        if bg_color.a == 0 {
2593            return Ok(());
2594        }
2595
2596        let element_size = PhysicalSizeImport {
2597            width: paint_rect.size.width,
2598            height: paint_rect.size.height,
2599        };
2600        let border_radius = get_border_radius(
2601            self.ctx.styled_dom,
2602            dom_id,
2603            &styled_node_state,
2604            element_size,
2605            self.ctx.viewport_size,
2606        );
2607
2608        builder.push_rect(paint_rect, bg_color, border_radius);
2609
2610        Ok(())
2611    }
2612
2613    /// Emits drawing commands for the foreground content, including hit-test areas and scrollbars.
2614    fn paint_node_content(
2615        &mut self,
2616        builder: &mut DisplayListBuilder,
2617        node_index: usize,
2618    ) -> Result<()> {
2619        let node = self
2620            .positioned_tree
2621            .tree
2622            .get(node_index)
2623            .ok_or(LayoutError::InvalidTree)?;
2624
2625        // Set current node for node mapping (for pagination break properties)
2626        builder.set_current_node(node.dom_node_id);
2627
2628        let Some(mut paint_rect) = self.get_paint_rect(node_index) else {
2629            return Ok(());
2630        };
2631
2632        // For text nodes (with inline layout), the used_size might be 0x0.
2633        // In this case, compute the bounds from the inline layout result.
2634        if paint_rect.size.width == 0.0 || paint_rect.size.height == 0.0 {
2635            if let Some(cached_layout) = &node.inline_layout_result {
2636                let content_bounds = cached_layout.layout.bounds();
2637                paint_rect.size.width = content_bounds.width;
2638                paint_rect.size.height = content_bounds.height;
2639            }
2640        }
2641
2642        // Add a hit-test area for this node if it's interactive.
2643        // NOTE: For scrollable containers (overflow: scroll/auto), the hit-test area
2644        // was already pushed in generate_for_stacking_context BEFORE the scroll frame,
2645        // so we skip it here to avoid duplicate hit-test areas that would scroll with content.
2646        if let Some(tag_id) = get_tag_id(self.ctx.styled_dom, node.dom_node_id) {
2647            let is_scrollable = if let Some(dom_id) = node.dom_node_id {
2648                let styled_node_state = self.get_styled_node_state(dom_id);
2649                let overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
2650                let overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
2651                overflow_x.is_scroll() || overflow_y.is_scroll()
2652            } else {
2653                false
2654            };
2655
2656            // Push hit-test area for this node ONLY if it's not a scrollable container.
2657            // Scrollable containers already have their hit-test area pushed BEFORE the scroll frame
2658            // in generate_for_stacking_context, ensuring the hit-test stays stationary in parent space
2659            // while content scrolls. Pushing it again here would create a duplicate that scrolls
2660            // with content, causing hit-test failures when scrolled to the bottom.
2661            if !is_scrollable {
2662                builder.push_hit_test_area(paint_rect, tag_id);
2663            }
2664        }
2665
2666        // Paint the node's visible content.
2667        if let Some(cached_layout) = &node.inline_layout_result {
2668            let inline_layout = &cached_layout.layout;
2669            debug_info!(
2670                self.ctx,
2671                "[paint_node] node {} has inline_layout with {} items",
2672                node_index,
2673                inline_layout.items.len()
2674            );
2675
2676            if let Some(dom_id) = node.dom_node_id {
2677                let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
2678                debug_info!(
2679                    self.ctx,
2680                    "Painting inline content for node {} ({:?}) at {:?}, {} layout items",
2681                    node_index,
2682                    node_type.get_node_type(),
2683                    paint_rect,
2684                    inline_layout.items.len()
2685                );
2686            }
2687
2688            // paint_rect is the border-box, but inline layout positions are relative to
2689            // content-box. Use type-safe conversion to make this clear and avoid manual
2690            // calculations.
2691            let border_box = BorderBoxRect(paint_rect);
2692            let mut content_box_rect =
2693                border_box.to_content_box(&node.box_props.padding, &node.box_props.border).rect();
2694            
2695            // For scrollable containers, extend the content rect to the full content size.
2696            // The scroll frame handles clipping - we need to paint ALL content, not just
2697            // what fits in the viewport. Otherwise glyphs beyond the viewport are not rendered.
2698            let content_size = get_scroll_content_size(node);
2699            if content_size.height > content_box_rect.size.height {
2700                content_box_rect.size.height = content_size.height;
2701            }
2702            if content_size.width > content_box_rect.size.width {
2703                content_box_rect.size.width = content_size.width;
2704            }
2705
2706            // Check for text-shadow and wrap inline content with push/pop shadow
2707            let mut pushed_text_shadow = false;
2708            if let Some(dom_id) = node.dom_node_id {
2709                let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
2710                let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
2711                if let Some(shadow_val) = self.ctx.styled_dom.css_property_cache.ptr
2712                    .get_text_shadow(node_data, &dom_id, node_state)
2713                {
2714                    if let Some(shadow) = shadow_val.get_property() {
2715                        builder.push_item(DisplayListItem::PushTextShadow {
2716                            shadow: shadow.clone(),
2717                        });
2718                        pushed_text_shadow = true;
2719                    }
2720                }
2721            }
2722
2723            self.paint_inline_content(builder, content_box_rect, inline_layout)?;
2724
2725            if pushed_text_shadow {
2726                builder.push_item(DisplayListItem::PopTextShadow);
2727            }
2728        } else if let Some(dom_id) = node.dom_node_id {
2729            // This node might be a simple replaced element, like an <img> tag.
2730            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
2731            if let NodeType::Image(image_ref) = node_data.get_node_type() {
2732                debug_info!(
2733                    self.ctx,
2734                    "Painting image for node {} at {:?}",
2735                    node_index,
2736                    paint_rect
2737                );
2738                // Store the ImageRef directly in the display list
2739                builder.push_image(paint_rect, image_ref.clone());
2740            }
2741        }
2742
2743        Ok(())
2744    }
2745
2746    /// Emits drawing commands for scrollbars. This is called AFTER popping the scroll frame
2747    /// clip so scrollbars appear on top of content and are not clipped.
2748    fn paint_scrollbars(&self, builder: &mut DisplayListBuilder, node_index: usize) -> Result<()> {
2749        let node = self
2750            .positioned_tree
2751            .tree
2752            .get(node_index)
2753            .ok_or(LayoutError::InvalidTree)?;
2754
2755        let Some(paint_rect) = self.get_paint_rect(node_index) else {
2756            return Ok(());
2757        };
2758
2759        // Check if we need to draw scrollbars for this node.
2760        let scrollbar_info = get_scrollbar_info_from_layout(node);
2761
2762        // Get node_id for GPU cache lookup and CSS style lookup
2763        let node_id = node.dom_node_id;
2764
2765        // Get CSS scrollbar style for this node
2766        let scrollbar_style = node_id
2767            .map(|nid| {
2768                let node_state =
2769                    &self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
2770                get_scrollbar_style(self.ctx.styled_dom, nid, node_state)
2771            })
2772            .unwrap_or_default();
2773
2774        // Skip if scrollbar-width: none
2775        if matches!(
2776            scrollbar_style.width_mode,
2777            azul_css::props::style::scrollbar::LayoutScrollbarWidth::None
2778        ) {
2779            return Ok(());
2780        }
2781
2782        // Get border dimensions to position scrollbar inside the border-box
2783        let border = &node.box_props.border;
2784
2785        // Get border-radius for potential clipping
2786        let container_border_radius = node_id
2787            .map(|nid| {
2788                let node_state =
2789                    &self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
2790                let element_size = PhysicalSizeImport {
2791                    width: paint_rect.size.width,
2792                    height: paint_rect.size.height,
2793                };
2794                let viewport_size =
2795                    LogicalSize::new(self.ctx.viewport_size.width, self.ctx.viewport_size.height);
2796                get_border_radius(
2797                    self.ctx.styled_dom,
2798                    nid,
2799                    node_state,
2800                    element_size,
2801                    viewport_size,
2802                )
2803            })
2804            .unwrap_or_default();
2805
2806        // Calculate the inner rect (content-box) where scrollbars should be placed
2807        // Scrollbars are positioned inside the border, at the right/bottom edges
2808        let inner_rect = LogicalRect {
2809            origin: LogicalPosition::new(
2810                paint_rect.origin.x + border.left,
2811                paint_rect.origin.y + border.top,
2812            ),
2813            size: LogicalSize::new(
2814                (paint_rect.size.width - border.left - border.right).max(0.0),
2815                (paint_rect.size.height - border.top - border.bottom).max(0.0),
2816            ),
2817        };
2818
2819        // Get scroll position for thumb calculation
2820        // ScrollPosition contains parent_rect and children_rect
2821        // The scroll offset is the difference between children_rect.origin and parent_rect.origin
2822        let (scroll_offset_x, scroll_offset_y) = node_id
2823            .and_then(|nid| {
2824                self.scroll_offsets.get(&nid).map(|pos| {
2825                    (
2826                        pos.children_rect.origin.x - pos.parent_rect.origin.x,
2827                        pos.children_rect.origin.y - pos.parent_rect.origin.y,
2828                    )
2829                })
2830            })
2831            .unwrap_or((0.0, 0.0));
2832
2833        // Get content size for thumb proportional sizing
2834        // Use the node's get_content_size() method which returns the actual content size
2835        // from overflow_content_size (set during layout) or computes it from text/children.
2836        // This is critical for correct thumb sizing - we must NOT use arbitrary multipliers.
2837        let content_size = node.get_content_size();
2838
2839        // Calculate thumb border-radius (half the scrollbar width for pill-shaped thumb)
2840        let thumb_radius = scrollbar_style.width_px / 2.0;
2841        let thumb_border_radius = BorderRadius {
2842            top_left: thumb_radius,
2843            top_right: thumb_radius,
2844            bottom_left: thumb_radius,
2845            bottom_right: thumb_radius,
2846        };
2847
2848        if scrollbar_info.needs_vertical {
2849            // Look up opacity key from GPU cache
2850            let opacity_key = node_id.and_then(|nid| {
2851                self.gpu_value_cache.and_then(|cache| {
2852                    cache
2853                        .scrollbar_v_opacity_keys
2854                        .get(&(self.dom_id, nid))
2855                        .copied()
2856                })
2857            });
2858
2859            // Vertical scrollbar: positioned at the right edge of the inner rect
2860            let track_height = if scrollbar_info.needs_horizontal {
2861                inner_rect.size.height - scrollbar_style.width_px
2862            } else {
2863                inner_rect.size.height
2864            };
2865
2866            let track_bounds = LogicalRect {
2867                origin: LogicalPosition::new(
2868                    inner_rect.origin.x + inner_rect.size.width - scrollbar_style.width_px,
2869                    inner_rect.origin.y,
2870                ),
2871                size: LogicalSize::new(scrollbar_style.width_px, track_height),
2872            };
2873
2874            // Calculate thumb size and position
2875            let viewport_height = inner_rect.size.height;
2876            let thumb_ratio = (viewport_height / content_size.height).min(1.0);
2877            let thumb_height = (track_height * thumb_ratio).max(scrollbar_style.width_px * 2.0);
2878
2879            let max_scroll = (content_size.height - viewport_height).max(0.0);
2880            let scroll_ratio = if max_scroll > 0.0 {
2881                scroll_offset_y.abs() / max_scroll
2882            } else {
2883                0.0
2884            };
2885            let thumb_y = track_bounds.origin.y
2886                + (track_height - thumb_height) * scroll_ratio.clamp(0.0, 1.0);
2887
2888            let thumb_bounds = LogicalRect {
2889                origin: LogicalPosition::new(track_bounds.origin.x, thumb_y),
2890                size: LogicalSize::new(scrollbar_style.width_px, thumb_height),
2891            };
2892
2893            // Generate hit-test ID for vertical scrollbar thumb
2894            let hit_id = node_id
2895                .map(|nid| azul_core::hit_test::ScrollbarHitId::VerticalThumb(self.dom_id, nid));
2896
2897            // Add page-scroll buttons at top/bottom of scrollbar track (green for debug)
2898            let button_size = scrollbar_style.width_px;
2899            let button_decrement_bounds = Some(LogicalRect {
2900                origin: LogicalPosition::new(track_bounds.origin.x, track_bounds.origin.y),
2901                size: LogicalSize::new(button_size, button_size),
2902            });
2903            let button_increment_bounds = Some(LogicalRect {
2904                origin: LogicalPosition::new(
2905                    track_bounds.origin.x,
2906                    track_bounds.origin.y + track_height - button_size,
2907                ),
2908                size: LogicalSize::new(button_size, button_size),
2909            });
2910            // Light green color for debug visibility
2911            let debug_button_color = ColorU { r: 144, g: 238, b: 144, a: 255 };
2912
2913            builder.push_scrollbar_styled(ScrollbarDrawInfo {
2914                bounds: track_bounds,
2915                orientation: ScrollbarOrientation::Vertical,
2916                track_bounds,
2917                track_color: scrollbar_style.track_color,
2918                thumb_bounds,
2919                thumb_color: scrollbar_style.thumb_color,
2920                thumb_border_radius,
2921                button_decrement_bounds,
2922                button_increment_bounds,
2923                button_color: debug_button_color,
2924                opacity_key,
2925                hit_id,
2926                clip_to_container_border: scrollbar_style.clip_to_container_border,
2927                container_border_radius,
2928            });
2929        }
2930
2931        if scrollbar_info.needs_horizontal {
2932            // Look up opacity key from GPU cache
2933            let opacity_key = node_id.and_then(|nid| {
2934                self.gpu_value_cache.and_then(|cache| {
2935                    cache
2936                        .scrollbar_h_opacity_keys
2937                        .get(&(self.dom_id, nid))
2938                        .copied()
2939                })
2940            });
2941
2942            // Horizontal scrollbar: positioned at the bottom edge of the inner rect
2943            let track_width = if scrollbar_info.needs_vertical {
2944                inner_rect.size.width - scrollbar_style.width_px
2945            } else {
2946                inner_rect.size.width
2947            };
2948
2949            let track_bounds = LogicalRect {
2950                origin: LogicalPosition::new(
2951                    inner_rect.origin.x,
2952                    inner_rect.origin.y + inner_rect.size.height - scrollbar_style.width_px,
2953                ),
2954                size: LogicalSize::new(track_width, scrollbar_style.width_px),
2955            };
2956
2957            // Calculate thumb size and position
2958            let viewport_width = inner_rect.size.width;
2959            let thumb_ratio = (viewport_width / content_size.width).min(1.0);
2960            let thumb_width = (track_width * thumb_ratio).max(scrollbar_style.width_px * 2.0);
2961
2962            let max_scroll = (content_size.width - viewport_width).max(0.0);
2963            let scroll_ratio = if max_scroll > 0.0 {
2964                scroll_offset_x.abs() / max_scroll
2965            } else {
2966                0.0
2967            };
2968            let thumb_x = track_bounds.origin.x
2969                + (track_width - thumb_width) * scroll_ratio.clamp(0.0, 1.0);
2970
2971            let thumb_bounds = LogicalRect {
2972                origin: LogicalPosition::new(thumb_x, track_bounds.origin.y),
2973                size: LogicalSize::new(thumb_width, scrollbar_style.width_px),
2974            };
2975
2976            // Generate hit-test ID for horizontal scrollbar thumb
2977            let hit_id = node_id
2978                .map(|nid| azul_core::hit_test::ScrollbarHitId::HorizontalThumb(self.dom_id, nid));
2979
2980            // Add page-scroll buttons at left/right of scrollbar track (green for debug)
2981            let button_size = scrollbar_style.width_px;
2982            let button_decrement_bounds = Some(LogicalRect {
2983                origin: LogicalPosition::new(track_bounds.origin.x, track_bounds.origin.y),
2984                size: LogicalSize::new(button_size, button_size),
2985            });
2986            let button_increment_bounds = Some(LogicalRect {
2987                origin: LogicalPosition::new(
2988                    track_bounds.origin.x + track_width - button_size,
2989                    track_bounds.origin.y,
2990                ),
2991                size: LogicalSize::new(button_size, button_size),
2992            });
2993            // Light green color for debug visibility
2994            let debug_button_color = ColorU { r: 144, g: 238, b: 144, a: 255 };
2995
2996            builder.push_scrollbar_styled(ScrollbarDrawInfo {
2997                bounds: track_bounds,
2998                orientation: ScrollbarOrientation::Horizontal,
2999                track_bounds,
3000                track_color: scrollbar_style.track_color,
3001                thumb_bounds,
3002                thumb_color: scrollbar_style.thumb_color,
3003                thumb_border_radius,
3004                button_decrement_bounds,
3005                button_increment_bounds,
3006                button_color: debug_button_color,
3007                opacity_key,
3008                hit_id,
3009                clip_to_container_border: scrollbar_style.clip_to_container_border,
3010                container_border_radius,
3011            });
3012        }
3013
3014        Ok(())
3015    }
3016
3017    /// Converts the rich layout information from `text3` into drawing commands.
3018    fn paint_inline_content(
3019        &self,
3020        builder: &mut DisplayListBuilder,
3021        container_rect: LogicalRect,
3022        layout: &UnifiedLayout,
3023    ) -> Result<()> {
3024        // TODO: This will always paint images over the glyphs
3025        // TODO: Handle z-index within inline content (e.g. background images)
3026        // NOTE: Text decorations (underline, strikethrough, overline) are handled in push_text_layout_to_display_list
3027        // TODO: Text shadows not yet implemented
3028        // TODO: Handle text overflowing (based on container_rect and overflow behavior)
3029
3030        // Calculate actual content bounds from the layout
3031        // Use these bounds instead of container_rect to avoid inflated bounds
3032        // that extend beyond actual text content
3033        let layout_bounds = layout.bounds();
3034        let actual_bounds = if layout_bounds.width > 0.0 && layout_bounds.height > 0.0 {
3035            LogicalRect {
3036                origin: container_rect.origin,
3037                size: LogicalSize {
3038                    width: layout_bounds.width,
3039                    height: layout_bounds.height,
3040                },
3041            }
3042        } else {
3043            // If layout has no content, don't push TextLayout item at all
3044            // This prevents 0x0 TextLayout items that pollute height calculation
3045            LogicalRect {
3046                origin: container_rect.origin,
3047                size: LogicalSize::default(),
3048            }
3049        };
3050
3051        // Only push TextLayout if layout has actual content
3052        // This prevents empty TextLayout items with 0x0 bounds at various Y positions
3053        // from affecting pagination height calculations
3054        if layout_bounds.width > 0.0 || layout_bounds.height > 0.0 {
3055            builder.push_text_layout(
3056                Arc::new(layout.clone()) as Arc<dyn std::any::Any + Send + Sync>,
3057                actual_bounds,
3058                FontHash::from_hash(0), // Will be updated per glyph run
3059                12.0,                   // Default font size, will be updated per glyph run
3060                ColorU {
3061                    r: 0,
3062                    g: 0,
3063                    b: 0,
3064                    a: 255,
3065                }, // Default color
3066            );
3067        }
3068
3069        let glyph_runs = crate::text3::glyphs::get_glyph_runs_simple(layout);
3070
3071        // FIRST PASS: Render backgrounds (solid colors, gradients) and borders for each glyph run
3072        // This must happen BEFORE rendering text so that backgrounds appear behind text.
3073        for glyph_run in glyph_runs.iter() {
3074            // Calculate the bounding box for this glyph run
3075            if let (Some(first_glyph), Some(last_glyph)) =
3076                (glyph_run.glyphs.first(), glyph_run.glyphs.last())
3077            {
3078                // Calculate run bounds from glyph positions
3079                let run_start_x = container_rect.origin.x + first_glyph.point.x;
3080                let run_end_x = container_rect.origin.x + last_glyph.point.x;
3081                let run_width = (run_end_x - run_start_x).max(0.0);
3082
3083                // Skip if run has no width
3084                if run_width <= 0.0 {
3085                    continue;
3086                }
3087
3088                // Approximate height based on font size (baseline is at glyph.point.y)
3089                let baseline_y = container_rect.origin.y + first_glyph.point.y;
3090                let font_size = glyph_run.font_size_px;
3091                let ascent = font_size * 0.8; // Approximate ascent
3092
3093                let mut run_bounds = LogicalRect::new(
3094                    LogicalPosition::new(run_start_x, baseline_y - ascent),
3095                    LogicalSize::new(run_width, font_size),
3096                );
3097
3098                // Expand run_bounds by padding + border so the background/border
3099                // rect covers the full inline box, not just the glyph area.
3100                if let Some(border) = &glyph_run.border {
3101                    let left_inset = border.left_inset();
3102                    let right_inset = border.right_inset();
3103                    let top_inset = border.top_inset();
3104                    let bottom_inset = border.bottom_inset();
3105
3106                    run_bounds.origin.x -= left_inset;
3107                    run_bounds.origin.y -= top_inset;
3108                    run_bounds.size.width += left_inset + right_inset;
3109                    run_bounds.size.height += top_inset + bottom_inset;
3110                }
3111
3112                // Use unified inline background/border painting
3113                builder.push_inline_backgrounds_and_border(
3114                    run_bounds,
3115                    glyph_run.background_color,
3116                    &glyph_run.background_content,
3117                    glyph_run.border.as_ref(),
3118                );
3119            }
3120        }
3121
3122        // SECOND PASS: Render text runs
3123        for (idx, glyph_run) in glyph_runs.iter().enumerate() {
3124            let clip_rect = container_rect; // Clip to the container rect
3125
3126            // Fix: Offset glyph positions by the container origin.
3127            // Text layout is relative to (0,0) of the IFC, but we need absolute coordinates.
3128            let offset_glyphs: Vec<GlyphInstance> = glyph_run
3129                .glyphs
3130                .iter()
3131                .map(|g| {
3132                    let mut g = g.clone();
3133                    g.point.x += container_rect.origin.x;
3134                    g.point.y += container_rect.origin.y;
3135                    g
3136                })
3137                .collect();
3138
3139            // Store only the font hash in the display list to keep it lean
3140            builder.push_text_run(
3141                offset_glyphs,
3142                FontHash::from_hash(glyph_run.font_hash),
3143                glyph_run.font_size_px,
3144                glyph_run.color,
3145                clip_rect,
3146            );
3147
3148            // Render text decorations if present OR if this is IME composition preview
3149            let needs_underline = glyph_run.text_decoration.underline || glyph_run.is_ime_preview;
3150            let needs_strikethrough = glyph_run.text_decoration.strikethrough;
3151            let needs_overline = glyph_run.text_decoration.overline;
3152
3153            if needs_underline || needs_strikethrough || needs_overline {
3154                // Calculate the bounding box for this glyph run
3155                if let (Some(first_glyph), Some(last_glyph)) =
3156                    (glyph_run.glyphs.first(), glyph_run.glyphs.last())
3157                {
3158                    let decoration_start_x = container_rect.origin.x + first_glyph.point.x;
3159                    let decoration_end_x = container_rect.origin.x + last_glyph.point.x;
3160                    let decoration_width = decoration_end_x - decoration_start_x;
3161
3162                    // Use font metrics to determine decoration positions
3163                    // Standard ratios based on CSS specification
3164                    let font_size = glyph_run.font_size_px;
3165                    let thickness = (font_size * 0.08).max(1.0); // ~8% of font size, min 1px
3166
3167                    // Baseline is at glyph.point.y
3168                    let baseline_y = container_rect.origin.y + first_glyph.point.y;
3169
3170                    if needs_underline {
3171                        // Underline is typically 10-15% below baseline
3172                        // IME composition always gets underlined
3173                        let underline_y = baseline_y + (font_size * 0.12);
3174                        let underline_bounds = LogicalRect::new(
3175                            LogicalPosition::new(decoration_start_x, underline_y),
3176                            LogicalSize::new(decoration_width, thickness),
3177                        );
3178                        builder.push_underline(underline_bounds, glyph_run.color, thickness);
3179                    }
3180
3181                    if needs_strikethrough {
3182                        // Strikethrough is typically 40% above baseline (middle of x-height)
3183                        let strikethrough_y = baseline_y - (font_size * 0.3);
3184                        let strikethrough_bounds = LogicalRect::new(
3185                            LogicalPosition::new(decoration_start_x, strikethrough_y),
3186                            LogicalSize::new(decoration_width, thickness),
3187                        );
3188                        builder.push_strikethrough(
3189                            strikethrough_bounds,
3190                            glyph_run.color,
3191                            thickness,
3192                        );
3193                    }
3194
3195                    if needs_overline {
3196                        // Overline is typically at cap-height (75% above baseline)
3197                        let overline_y = baseline_y - (font_size * 0.85);
3198                        let overline_bounds = LogicalRect::new(
3199                            LogicalPosition::new(decoration_start_x, overline_y),
3200                            LogicalSize::new(decoration_width, thickness),
3201                        );
3202                        builder.push_overline(overline_bounds, glyph_run.color, thickness);
3203                    }
3204                }
3205            }
3206        }
3207
3208        // THIRD PASS: Generate hit-test areas for text runs
3209        // This enables cursor resolution directly on text nodes instead of their containers
3210        for glyph_run in glyph_runs.iter() {
3211            // Only generate hit-test areas for runs with a source node id
3212            let Some(source_node_id) = glyph_run.source_node_id else {
3213                continue;
3214            };
3215
3216            // Calculate the bounding box for this glyph run
3217            if let (Some(first_glyph), Some(last_glyph)) =
3218                (glyph_run.glyphs.first(), glyph_run.glyphs.last())
3219            {
3220                let run_start_x = container_rect.origin.x + first_glyph.point.x;
3221                let run_end_x = container_rect.origin.x + last_glyph.point.x;
3222                let run_width = (run_end_x - run_start_x).max(0.0);
3223
3224                // Skip if run has no width
3225                if run_width <= 0.0 {
3226                    continue;
3227                }
3228
3229                // Calculate run bounds using font metrics
3230                let baseline_y = container_rect.origin.y + first_glyph.point.y;
3231                let font_size = glyph_run.font_size_px;
3232                let ascent = font_size * 0.8; // Approximate ascent
3233
3234                let run_bounds = LogicalRect::new(
3235                    LogicalPosition::new(run_start_x, baseline_y - ascent),
3236                    LogicalSize::new(run_width, font_size),
3237                );
3238
3239                // Query the cursor type for this text node from the CSS property cache
3240                // Default to Text cursor (I-beam) for text nodes
3241                let cursor_type = self.get_cursor_type_for_text_node(source_node_id);
3242
3243                // Construct the hit-test tag for cursor resolution
3244                // tag.0 = DomId (upper 32 bits) | NodeId (lower 32 bits)
3245                // tag.1 = TAG_TYPE_CURSOR | cursor_type
3246                let tag_value = ((self.dom_id.inner as u64) << 32) | (source_node_id.index() as u64);
3247                let tag_type = TAG_TYPE_CURSOR | (cursor_type as u16);
3248                let tag_id = (tag_value, tag_type);
3249
3250                builder.push_hit_test_area(run_bounds, tag_id);
3251            }
3252        }
3253
3254        // Render inline objects (images, shapes/inline-blocks, etc.)
3255        // These are positioned by the text3 engine and need to be rendered at their calculated
3256        // positions
3257        for positioned_item in &layout.items {
3258            self.paint_inline_object(builder, container_rect.origin, positioned_item)?;
3259        }
3260        Ok(())
3261    }
3262
3263    /// Paints a single inline object (image, shape, or inline-block)
3264    fn paint_inline_object(
3265        &self,
3266        builder: &mut DisplayListBuilder,
3267        base_pos: LogicalPosition,
3268        positioned_item: &PositionedItem,
3269    ) -> Result<()> {
3270        let ShapedItem::Object {
3271            content, bounds, ..
3272        } = &positioned_item.item
3273        else {
3274            // Other item types (e.g., breaks) don't produce painted output.
3275            return Ok(());
3276        };
3277
3278        // Calculate the absolute position of this object
3279        // positioned_item.position is relative to the container
3280        let object_bounds = LogicalRect::new(
3281            LogicalPosition::new(
3282                base_pos.x + positioned_item.position.x,
3283                base_pos.y + positioned_item.position.y,
3284            ),
3285            LogicalSize::new(bounds.width, bounds.height),
3286        );
3287
3288        match content {
3289            InlineContent::Image(image) => {
3290                if let Some(image_ref) = get_image_ref_for_image_source(&image.source) {
3291                    builder.push_image(object_bounds, image_ref);
3292                }
3293            }
3294            InlineContent::Shape(shape) => {
3295                self.paint_inline_shape(builder, object_bounds, shape, bounds)?;
3296            }
3297            _ => {}
3298        }
3299        Ok(())
3300    }
3301
3302    /// Paints an inline shape (inline-block background and border)
3303    fn paint_inline_shape(
3304        &self,
3305        builder: &mut DisplayListBuilder,
3306        object_bounds: LogicalRect,
3307        shape: &InlineShape,
3308        bounds: &crate::text3::cache::Rect,
3309    ) -> Result<()> {
3310        // Render inline-block backgrounds and borders using their CSS styling
3311        // The text3 engine positions these correctly in the inline flow
3312        let Some(node_id) = shape.source_node_id else {
3313            return Ok(());
3314        };
3315
3316        let styled_node_state =
3317            &self.ctx.styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
3318
3319        // Get all background layers (colors, gradients, images)
3320        let background_contents =
3321            get_background_contents(self.ctx.styled_dom, node_id, styled_node_state);
3322
3323        // Get border information
3324        let border_info = get_border_info(self.ctx.styled_dom, node_id, styled_node_state);
3325
3326        // FIX: object_bounds is the margin-box position from text3.
3327        // We need to convert to border-box for painting backgrounds/borders.
3328        let margins = if let Some(indices) = self.positioned_tree.tree.dom_to_layout.get(&node_id) {
3329            if let Some(&idx) = indices.first() {
3330                self.positioned_tree.tree.nodes[idx].box_props.margin
3331            } else {
3332                Default::default()
3333            }
3334        } else {
3335            Default::default()
3336        };
3337
3338        // Convert margin-box bounds to border-box bounds
3339        let border_box_bounds = LogicalRect {
3340            origin: LogicalPosition {
3341                x: object_bounds.origin.x + margins.left,
3342                y: object_bounds.origin.y + margins.top,
3343            },
3344            size: LogicalSize {
3345                width: (object_bounds.size.width - margins.left - margins.right).max(0.0),
3346                height: (object_bounds.size.height - margins.top - margins.bottom).max(0.0),
3347            },
3348        };
3349
3350        let element_size = PhysicalSizeImport {
3351            width: border_box_bounds.size.width,
3352            height: border_box_bounds.size.height,
3353        };
3354
3355        // Get border radius for background clipping
3356        let simple_border_radius = get_border_radius(
3357            self.ctx.styled_dom,
3358            node_id,
3359            styled_node_state,
3360            element_size,
3361            self.ctx.viewport_size,
3362        );
3363
3364        // Get style border radius for border rendering
3365        let style_border_radius =
3366            get_style_border_radius(self.ctx.styled_dom, node_id, styled_node_state);
3367
3368        // Use unified background/border painting with border-box bounds
3369        builder.push_backgrounds_and_border(
3370            border_box_bounds,
3371            &background_contents,
3372            &border_info,
3373            simple_border_radius,
3374            style_border_radius,
3375        );
3376
3377        // Push hit-test area for this inline-block element
3378        // This is critical for buttons and other inline-block elements to receive
3379        // mouse events and display the correct cursor (e.g., cursor: pointer)
3380        if let Some(tag_id) = get_tag_id(self.ctx.styled_dom, Some(node_id)) {
3381            builder.push_hit_test_area(border_box_bounds, tag_id);
3382        }
3383
3384        Ok(())
3385    }
3386
3387    /// Determines if a node establishes a new stacking context based on CSS rules.
3388    fn establishes_stacking_context(&self, node_index: usize) -> bool {
3389        let Some(node) = self.positioned_tree.tree.get(node_index) else {
3390            return false;
3391        };
3392        let Some(dom_id) = node.dom_node_id else {
3393            return false;
3394        };
3395
3396        let position = get_position_type(self.ctx.styled_dom, Some(dom_id));
3397        if position == LayoutPosition::Absolute || position == LayoutPosition::Fixed {
3398            return true;
3399        }
3400
3401        let z_index = get_z_index(self.ctx.styled_dom, Some(dom_id));
3402        if position == LayoutPosition::Relative && z_index != 0 {
3403            return true;
3404        }
3405
3406        if let Some(styled_node) = self.ctx.styled_dom.styled_nodes.as_container().get(dom_id) {
3407            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
3408            let node_state =
3409                &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
3410
3411            // Opacity < 1
3412            let opacity = self
3413                .ctx
3414                .styled_dom
3415                .css_property_cache
3416                .ptr
3417                .get_opacity(node_data, &dom_id, node_state)
3418                .and_then(|v| v.get_property())
3419                .map(|v| v.inner.normalized())
3420                .unwrap_or(1.0);
3421
3422            if opacity < 1.0 {
3423                return true;
3424            }
3425
3426            // Transform != none
3427            let has_transform = self
3428                .ctx
3429                .styled_dom
3430                .css_property_cache
3431                .ptr
3432                .get_transform(node_data, &dom_id, node_state)
3433                .and_then(|v| v.get_property())
3434                .map(|v| !v.is_empty())
3435                .unwrap_or(false);
3436
3437            if has_transform {
3438                return true;
3439            }
3440        }
3441
3442        false
3443    }
3444}
3445
3446/// Helper struct to pass layout results to the display list generator.
3447///
3448/// Combines the layout tree with pre-calculated absolute positions for each node.
3449/// The positions are stored separately because they are computed in a final
3450/// positioning pass after layout is complete.
3451pub struct PositionedTree<'a> {
3452    /// The layout tree containing all nodes with their computed sizes
3453    pub tree: &'a LayoutTree,
3454    /// Map from node index to its absolute position in the document
3455    pub calculated_positions: &'a super::PositionVec,
3456}
3457
3458/// Describes how overflow content should be handled for an element.
3459///
3460/// This maps to the CSS `overflow-x` and `overflow-y` properties and determines
3461/// whether content that exceeds the element's bounds should be visible, clipped,
3462/// or scrollable.
3463#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3464pub enum OverflowBehavior {
3465    /// Content is not clipped and may render outside the element's box (default)
3466    Visible,
3467    /// Content is clipped to the padding box, no scrollbars provided
3468    Hidden,
3469    /// Content is clipped to the padding box (CSS `overflow: clip`)
3470    Clip,
3471    /// Content is clipped and scrollbars are always shown
3472    Scroll,
3473    /// Content is clipped and scrollbars appear only when needed
3474    Auto,
3475}
3476
3477impl OverflowBehavior {
3478    /// Returns `true` if this overflow behavior clips content.
3479    ///
3480    /// All behaviors except `Visible` result in content being clipped
3481    /// to the element's padding box.
3482    pub fn is_clipped(&self) -> bool {
3483        matches!(self, Self::Hidden | Self::Clip | Self::Scroll | Self::Auto)
3484    }
3485
3486    /// Returns `true` if this overflow behavior enables scrolling.
3487    ///
3488    /// Only `Scroll` and `Auto` allow the user to scroll to see
3489    /// overflowing content.
3490    pub fn is_scroll(&self) -> bool {
3491        matches!(self, Self::Scroll | Self::Auto)
3492    }
3493}
3494
3495fn get_scroll_id(id: Option<NodeId>) -> LocalScrollId {
3496    id.map(|i| i.index() as u64).unwrap_or(0)
3497}
3498
3499/// Calculates the actual content size of a node, including all children and text.
3500/// This is used to determine if scrollbars should appear for overflow: auto.
3501fn get_scroll_content_size(node: &LayoutNode) -> LogicalSize {
3502    // First check if we have a pre-calculated overflow_content_size (for block children)
3503    if let Some(overflow_size) = node.overflow_content_size {
3504        return overflow_size;
3505    }
3506
3507    // Start with the node's own size
3508    let mut content_size = node.used_size.unwrap_or_default();
3509
3510    // If this node has text layout, calculate the bounds of all text items
3511    if let Some(ref cached_layout) = node.inline_layout_result {
3512        let text_layout = &cached_layout.layout;
3513        // Find the maximum extent of all positioned items
3514        let mut max_x: f32 = 0.0;
3515        let mut max_y: f32 = 0.0;
3516
3517        for positioned_item in &text_layout.items {
3518            let item_bounds = positioned_item.item.bounds();
3519            let item_right = positioned_item.position.x + item_bounds.width;
3520            let item_bottom = positioned_item.position.y + item_bounds.height;
3521
3522            max_x = max_x.max(item_right);
3523            max_y = max_y.max(item_bottom);
3524        }
3525
3526        // Use the maximum extent as content size if it's larger
3527        content_size.width = content_size.width.max(max_x);
3528        content_size.height = content_size.height.max(max_y);
3529    }
3530
3531    content_size
3532}
3533
3534fn get_tag_id(dom: &StyledDom, id: Option<NodeId>) -> Option<DisplayListTagId> {
3535    let node_id = id?;
3536    let tag_mapping = dom.tag_ids_to_node_ids.as_ref().iter().find(|m| {
3537        m.node_id.into_crate_internal() == Some(node_id)
3538    })?;
3539    // Use TAG_TYPE_DOM_NODE (0x0100) as namespace marker in u16 field
3540    // This distinguishes DOM nodes from scrollbars (0x0200) and other tag types
3541    Some((tag_mapping.tag_id.inner, 0x0100))
3542}
3543
3544fn get_image_ref_for_image_source(
3545    source: &ImageSource,
3546) -> Option<ImageRef> {
3547    match source {
3548        ImageSource::Ref(image_ref) => Some(image_ref.clone()),
3549        ImageSource::Url(_url) => {
3550            // TODO: Look up in ImageCache
3551            // For now, CSS url() images are not yet supported
3552            None
3553        }
3554        ImageSource::Data(_) | ImageSource::Svg(_) | ImageSource::Placeholder(_) => {
3555            // TODO: Decode raw data / SVG to ImageRef
3556            None
3557        }
3558    }
3559}
3560
3561/// Get the bounds of a display list item, if it has spatial extent.
3562fn get_display_item_bounds(item: &DisplayListItem) -> Option<LogicalRect> {
3563    match item {
3564        DisplayListItem::Rect { bounds, .. } => Some(*bounds),
3565        DisplayListItem::SelectionRect { bounds, .. } => Some(*bounds),
3566        DisplayListItem::CursorRect { bounds, .. } => Some(*bounds),
3567        DisplayListItem::Border { bounds, .. } => Some(*bounds),
3568        DisplayListItem::TextLayout { bounds, .. } => Some(*bounds),
3569        DisplayListItem::Text { clip_rect, .. } => Some(*clip_rect),
3570        DisplayListItem::Underline { bounds, .. } => Some(*bounds),
3571        DisplayListItem::Strikethrough { bounds, .. } => Some(*bounds),
3572        DisplayListItem::Overline { bounds, .. } => Some(*bounds),
3573        DisplayListItem::Image { bounds, .. } => Some(*bounds),
3574        DisplayListItem::ScrollBar { bounds, .. } => Some(*bounds),
3575        DisplayListItem::ScrollBarStyled { info } => Some(info.bounds),
3576        DisplayListItem::PushClip { bounds, .. } => Some(*bounds),
3577        DisplayListItem::PushScrollFrame { clip_bounds, .. } => Some(*clip_bounds),
3578        DisplayListItem::HitTestArea { bounds, .. } => Some(*bounds),
3579        DisplayListItem::PushStackingContext { bounds, .. } => Some(*bounds),
3580        DisplayListItem::IFrame { bounds, .. } => Some(*bounds),
3581        _ => None,
3582    }
3583}
3584
3585/// Clip a display list item to page bounds and offset to page-relative coordinates.
3586/// Returns None if the item is completely outside the page bounds.
3587fn clip_and_offset_display_item(
3588    item: &DisplayListItem,
3589    page_top: f32,
3590    page_bottom: f32,
3591) -> Option<DisplayListItem> {
3592    match item {
3593        DisplayListItem::Rect {
3594            bounds,
3595            color,
3596            border_radius,
3597        } => clip_rect_item(*bounds, *color, *border_radius, page_top, page_bottom),
3598
3599        DisplayListItem::Border {
3600            bounds,
3601            widths,
3602            colors,
3603            styles,
3604            border_radius,
3605        } => clip_border_item(
3606            *bounds,
3607            *widths,
3608            *colors,
3609            *styles,
3610            border_radius.clone(),
3611            page_top,
3612            page_bottom,
3613        ),
3614
3615        DisplayListItem::SelectionRect {
3616            bounds,
3617            border_radius,
3618            color,
3619        } => clip_selection_rect_item(*bounds, *border_radius, *color, page_top, page_bottom),
3620
3621        DisplayListItem::CursorRect { bounds, color } => {
3622            clip_cursor_rect_item(*bounds, *color, page_top, page_bottom)
3623        }
3624
3625        DisplayListItem::Image { bounds, image } => {
3626            clip_image_item(*bounds, image.clone(), page_top, page_bottom)
3627        }
3628
3629        DisplayListItem::TextLayout {
3630            layout,
3631            bounds,
3632            font_hash,
3633            font_size_px,
3634            color,
3635        } => clip_text_layout_item(
3636            layout,
3637            *bounds,
3638            *font_hash,
3639            *font_size_px,
3640            *color,
3641            page_top,
3642            page_bottom,
3643        ),
3644
3645        DisplayListItem::Text {
3646            glyphs,
3647            font_hash,
3648            font_size_px,
3649            color,
3650            clip_rect,
3651        } => clip_text_item(
3652            glyphs,
3653            *font_hash,
3654            *font_size_px,
3655            *color,
3656            *clip_rect,
3657            page_top,
3658            page_bottom,
3659        ),
3660
3661        DisplayListItem::Underline {
3662            bounds,
3663            color,
3664            thickness,
3665        } => clip_text_decoration_item(
3666            *bounds,
3667            *color,
3668            *thickness,
3669            TextDecorationType::Underline,
3670            page_top,
3671            page_bottom,
3672        ),
3673
3674        DisplayListItem::Strikethrough {
3675            bounds,
3676            color,
3677            thickness,
3678        } => clip_text_decoration_item(
3679            *bounds,
3680            *color,
3681            *thickness,
3682            TextDecorationType::Strikethrough,
3683            page_top,
3684            page_bottom,
3685        ),
3686
3687        DisplayListItem::Overline {
3688            bounds,
3689            color,
3690            thickness,
3691        } => clip_text_decoration_item(
3692            *bounds,
3693            *color,
3694            *thickness,
3695            TextDecorationType::Overline,
3696            page_top,
3697            page_bottom,
3698        ),
3699
3700        DisplayListItem::ScrollBar {
3701            bounds,
3702            color,
3703            orientation,
3704            opacity_key,
3705            hit_id,
3706        } => clip_scrollbar_item(
3707            *bounds,
3708            *color,
3709            *orientation,
3710            *opacity_key,
3711            *hit_id,
3712            page_top,
3713            page_bottom,
3714        ),
3715
3716        DisplayListItem::HitTestArea { bounds, tag } => {
3717            clip_hit_test_area_item(*bounds, *tag, page_top, page_bottom)
3718        }
3719
3720        DisplayListItem::IFrame {
3721            child_dom_id,
3722            bounds,
3723            clip_rect,
3724        } => clip_iframe_item(*child_dom_id, *bounds, *clip_rect, page_top, page_bottom),
3725
3726        // ScrollBarStyled - clip based on overall bounds
3727        DisplayListItem::ScrollBarStyled { info } => {
3728            let bounds = info.bounds;
3729            if bounds.origin.y + bounds.size.height < page_top || bounds.origin.y > page_bottom {
3730                None
3731            } else {
3732                // Clone and offset all the internal bounds
3733                let mut clipped_info = (**info).clone();
3734                let y_offset = -page_top;
3735                clipped_info.bounds = offset_rect_y(clipped_info.bounds, y_offset);
3736                clipped_info.track_bounds = offset_rect_y(clipped_info.track_bounds, y_offset);
3737                clipped_info.thumb_bounds = offset_rect_y(clipped_info.thumb_bounds, y_offset);
3738                if let Some(b) = clipped_info.button_decrement_bounds {
3739                    clipped_info.button_decrement_bounds = Some(offset_rect_y(b, y_offset));
3740                }
3741                if let Some(b) = clipped_info.button_increment_bounds {
3742                    clipped_info.button_increment_bounds = Some(offset_rect_y(b, y_offset));
3743                }
3744                Some(DisplayListItem::ScrollBarStyled {
3745                    info: Box::new(clipped_info),
3746                })
3747            }
3748        }
3749
3750        // State management items - skip for now (would need proper per-page tracking)
3751        DisplayListItem::PushClip { .. }
3752        | DisplayListItem::PopClip
3753        | DisplayListItem::PushScrollFrame { .. }
3754        | DisplayListItem::PopScrollFrame
3755        | DisplayListItem::PushStackingContext { .. }
3756        | DisplayListItem::PopStackingContext => None,
3757
3758        // Gradient items - simple bounds check
3759        DisplayListItem::LinearGradient {
3760            bounds,
3761            gradient,
3762            border_radius,
3763        } => {
3764            if bounds.origin.y + bounds.size.height < page_top || bounds.origin.y > page_bottom {
3765                None
3766            } else {
3767                Some(DisplayListItem::LinearGradient {
3768                    bounds: offset_rect_y(*bounds, -page_top),
3769                    gradient: gradient.clone(),
3770                    border_radius: *border_radius,
3771                })
3772            }
3773        }
3774        DisplayListItem::RadialGradient {
3775            bounds,
3776            gradient,
3777            border_radius,
3778        } => {
3779            if bounds.origin.y + bounds.size.height < page_top || bounds.origin.y > page_bottom {
3780                None
3781            } else {
3782                Some(DisplayListItem::RadialGradient {
3783                    bounds: offset_rect_y(*bounds, -page_top),
3784                    gradient: gradient.clone(),
3785                    border_radius: *border_radius,
3786                })
3787            }
3788        }
3789        DisplayListItem::ConicGradient {
3790            bounds,
3791            gradient,
3792            border_radius,
3793        } => {
3794            if bounds.origin.y + bounds.size.height < page_top || bounds.origin.y > page_bottom {
3795                None
3796            } else {
3797                Some(DisplayListItem::ConicGradient {
3798                    bounds: offset_rect_y(*bounds, -page_top),
3799                    gradient: gradient.clone(),
3800                    border_radius: *border_radius,
3801                })
3802            }
3803        }
3804
3805        // BoxShadow - simple bounds check
3806        DisplayListItem::BoxShadow {
3807            bounds,
3808            shadow,
3809            border_radius,
3810        } => {
3811            if bounds.origin.y + bounds.size.height < page_top || bounds.origin.y > page_bottom {
3812                None
3813            } else {
3814                Some(DisplayListItem::BoxShadow {
3815                    bounds: offset_rect_y(*bounds, -page_top),
3816                    shadow: *shadow,
3817                    border_radius: *border_radius,
3818                })
3819            }
3820        }
3821
3822        // Filter effects - skip for now (would need proper per-page tracking)
3823        DisplayListItem::PushFilter { .. }
3824        | DisplayListItem::PopFilter
3825        | DisplayListItem::PushBackdropFilter { .. }
3826        | DisplayListItem::PopBackdropFilter
3827        | DisplayListItem::PushOpacity { .. }
3828        | DisplayListItem::PopOpacity
3829        | DisplayListItem::PushReferenceFrame { .. }
3830        | DisplayListItem::PopReferenceFrame
3831        | DisplayListItem::PushTextShadow { .. }
3832        | DisplayListItem::PopTextShadow => None,
3833    }
3834}
3835
3836// Helper functions for clip_and_offset_display_item
3837
3838/// Internal enum for text decoration type dispatch
3839#[derive(Debug, Clone, Copy)]
3840enum TextDecorationType {
3841    Underline,
3842    Strikethrough,
3843    Overline,
3844}
3845
3846/// Clips a filled rectangle to page bounds.
3847fn clip_rect_item(
3848    bounds: LogicalRect,
3849    color: ColorU,
3850    border_radius: BorderRadius,
3851    page_top: f32,
3852    page_bottom: f32,
3853) -> Option<DisplayListItem> {
3854    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::Rect {
3855        bounds: clipped,
3856        color,
3857        border_radius,
3858    })
3859}
3860
3861/// Clips a border to page bounds, hiding top/bottom borders when clipped.
3862fn clip_border_item(
3863    bounds: LogicalRect,
3864    widths: StyleBorderWidths,
3865    colors: StyleBorderColors,
3866    styles: StyleBorderStyles,
3867    border_radius: StyleBorderRadius,
3868    page_top: f32,
3869    page_bottom: f32,
3870) -> Option<DisplayListItem> {
3871    let original_bounds = bounds;
3872    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| {
3873        let new_widths = adjust_border_widths_for_clipping(
3874            widths,
3875            original_bounds,
3876            clipped,
3877            page_top,
3878            page_bottom,
3879        );
3880        DisplayListItem::Border {
3881            bounds: clipped,
3882            widths: new_widths,
3883            colors,
3884            styles,
3885            border_radius,
3886        }
3887    })
3888}
3889
3890/// Adjusts border widths when a border is clipped at page boundaries.
3891/// Hides top border if clipped at top, bottom border if clipped at bottom.
3892fn adjust_border_widths_for_clipping(
3893    mut widths: StyleBorderWidths,
3894    original_bounds: LogicalRect,
3895    clipped: LogicalRect,
3896    page_top: f32,
3897    page_bottom: f32,
3898) -> StyleBorderWidths {
3899    // Hide top border if we clipped the top
3900    if clipped.origin.y > 0.0 && original_bounds.origin.y < page_top {
3901        widths.top = None;
3902    }
3903
3904    // Hide bottom border if we clipped the bottom
3905    let original_bottom = original_bounds.origin.y + original_bounds.size.height;
3906    let clipped_bottom = clipped.origin.y + clipped.size.height;
3907    if original_bottom > page_bottom && clipped_bottom >= page_bottom - page_top - 1.0 {
3908        widths.bottom = None;
3909    }
3910
3911    widths
3912}
3913
3914/// Clips a selection rectangle to page bounds.
3915fn clip_selection_rect_item(
3916    bounds: LogicalRect,
3917    border_radius: BorderRadius,
3918    color: ColorU,
3919    page_top: f32,
3920    page_bottom: f32,
3921) -> Option<DisplayListItem> {
3922    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::SelectionRect {
3923        bounds: clipped,
3924        border_radius,
3925        color,
3926    })
3927}
3928
3929/// Clips a cursor rectangle to page bounds.
3930fn clip_cursor_rect_item(
3931    bounds: LogicalRect,
3932    color: ColorU,
3933    page_top: f32,
3934    page_bottom: f32,
3935) -> Option<DisplayListItem> {
3936    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::CursorRect {
3937        bounds: clipped,
3938        color,
3939    })
3940}
3941
3942/// Clips an image to page bounds if it overlaps the page.
3943fn clip_image_item(
3944    bounds: LogicalRect,
3945    image: ImageRef,
3946    page_top: f32,
3947    page_bottom: f32,
3948) -> Option<DisplayListItem> {
3949    if !rect_intersects(&bounds, page_top, page_bottom) {
3950        return None;
3951    }
3952    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::Image {
3953        bounds: clipped,
3954        image,
3955    })
3956}
3957
3958/// Clips a text layout block to page bounds, filtering individual text items.
3959fn clip_text_layout_item(
3960    layout: &Arc<dyn std::any::Any + Send + Sync>,
3961    bounds: LogicalRect,
3962    font_hash: FontHash,
3963    font_size_px: f32,
3964    color: ColorU,
3965    page_top: f32,
3966    page_bottom: f32,
3967) -> Option<DisplayListItem> {
3968    if !rect_intersects(&bounds, page_top, page_bottom) {
3969        return None;
3970    }
3971
3972    // Try to downcast and filter UnifiedLayout items
3973    #[cfg(feature = "text_layout")]
3974    if let Some(unified_layout) = layout.downcast_ref::<crate::text3::cache::UnifiedLayout>() {
3975        return clip_unified_layout(
3976            unified_layout,
3977            bounds,
3978            font_hash,
3979            font_size_px,
3980            color,
3981            page_top,
3982            page_bottom,
3983        );
3984    }
3985
3986    // Fallback: simple bounds offset (legacy behavior)
3987    Some(DisplayListItem::TextLayout {
3988        layout: layout.clone(),
3989        bounds: offset_rect_y(bounds, -page_top),
3990        font_hash,
3991        font_size_px,
3992        color,
3993    })
3994}
3995
3996/// Clips a UnifiedLayout by filtering items to those on the current page.
3997#[cfg(feature = "text_layout")]
3998fn clip_unified_layout(
3999    unified_layout: &crate::text3::cache::UnifiedLayout,
4000    bounds: LogicalRect,
4001    font_hash: FontHash,
4002    font_size_px: f32,
4003    color: ColorU,
4004    page_top: f32,
4005    page_bottom: f32,
4006) -> Option<DisplayListItem> {
4007    let layout_origin_y = bounds.origin.y;
4008    let layout_origin_x = bounds.origin.x;
4009
4010    // Filter items whose center falls within this page
4011    let filtered_items: Vec<_> = unified_layout
4012        .items
4013        .iter()
4014        .filter(|item| item_center_on_page(item, layout_origin_y, page_top, page_bottom))
4015        .cloned()
4016        .collect();
4017
4018    if filtered_items.is_empty() {
4019        return None;
4020    }
4021
4022    // Calculate new origin for page-relative positioning
4023    let new_origin_y = (layout_origin_y - page_top).max(0.0);
4024
4025    // Transform items to page-relative coordinates and calculate bounds
4026    let (offset_items, min_y, max_y, max_width) =
4027        transform_items_to_page_coords(filtered_items, layout_origin_y, page_top, new_origin_y);
4028
4029    let new_layout = crate::text3::cache::UnifiedLayout {
4030        items: offset_items,
4031        overflow: unified_layout.overflow.clone(),
4032    };
4033
4034    let new_bounds = LogicalRect {
4035        origin: LogicalPosition {
4036            x: layout_origin_x,
4037            y: new_origin_y,
4038        },
4039        size: LogicalSize {
4040            width: max_width.max(bounds.size.width),
4041            height: (max_y - min_y.min(0.0)).max(0.0),
4042        },
4043    };
4044
4045    Some(DisplayListItem::TextLayout {
4046        layout: Arc::new(new_layout) as Arc<dyn std::any::Any + Send + Sync>,
4047        bounds: new_bounds,
4048        font_hash,
4049        font_size_px,
4050        color,
4051    })
4052}
4053
4054/// Checks if an item's center point falls within the page bounds.
4055#[cfg(feature = "text_layout")]
4056fn item_center_on_page(
4057    item: &crate::text3::cache::PositionedItem,
4058    layout_origin_y: f32,
4059    page_top: f32,
4060    page_bottom: f32,
4061) -> bool {
4062    let item_y_absolute = layout_origin_y + item.position.y;
4063    let item_height = item.item.bounds().height;
4064    let item_center_y = item_y_absolute + (item_height / 2.0);
4065    item_center_y >= page_top && item_center_y < page_bottom
4066}
4067
4068/// Transforms filtered items to page-relative coordinates.
4069/// Returns (items, min_y, max_y, max_width).
4070#[cfg(feature = "text_layout")]
4071fn transform_items_to_page_coords(
4072    items: Vec<crate::text3::cache::PositionedItem>,
4073    layout_origin_y: f32,
4074    page_top: f32,
4075    new_origin_y: f32,
4076) -> (Vec<crate::text3::cache::PositionedItem>, f32, f32, f32) {
4077    let mut min_y = f32::MAX;
4078    let mut max_y = f32::MIN;
4079    let mut max_width = 0.0f32;
4080
4081    let offset_items: Vec<_> = items
4082        .into_iter()
4083        .map(|mut item| {
4084            let abs_y = layout_origin_y + item.position.y;
4085            let page_y = abs_y - page_top;
4086            let new_item_y = page_y - new_origin_y;
4087
4088            let item_bounds = item.item.bounds();
4089            min_y = min_y.min(new_item_y);
4090            max_y = max_y.max(new_item_y + item_bounds.height);
4091            max_width = max_width.max(item.position.x + item_bounds.width);
4092
4093            item.position.y = new_item_y;
4094            item
4095        })
4096        .collect();
4097
4098    (offset_items, min_y, max_y, max_width)
4099}
4100
4101/// Clips a text glyph run to page bounds, filtering individual glyphs.
4102fn clip_text_item(
4103    glyphs: &[GlyphInstance],
4104    font_hash: FontHash,
4105    font_size_px: f32,
4106    color: ColorU,
4107    clip_rect: LogicalRect,
4108    page_top: f32,
4109    page_bottom: f32,
4110) -> Option<DisplayListItem> {
4111    if !rect_intersects(&clip_rect, page_top, page_bottom) {
4112        return None;
4113    }
4114
4115    // Filter glyphs using center-point decision (baseline position)
4116    let page_glyphs: Vec<_> = glyphs
4117        .iter()
4118        .filter(|g| g.point.y >= page_top && g.point.y < page_bottom)
4119        .map(|g| GlyphInstance {
4120            index: g.index,
4121            point: LogicalPosition {
4122                x: g.point.x,
4123                y: g.point.y - page_top,
4124            },
4125            size: g.size,
4126        })
4127        .collect();
4128
4129    if page_glyphs.is_empty() {
4130        return None;
4131    }
4132
4133    Some(DisplayListItem::Text {
4134        glyphs: page_glyphs,
4135        font_hash,
4136        font_size_px,
4137        color,
4138        clip_rect: offset_rect_y(clip_rect, -page_top),
4139    })
4140}
4141
4142/// Clips a text decoration (underline, strikethrough, or overline) to page bounds.
4143fn clip_text_decoration_item(
4144    bounds: LogicalRect,
4145    color: ColorU,
4146    thickness: f32,
4147    decoration_type: TextDecorationType,
4148    page_top: f32,
4149    page_bottom: f32,
4150) -> Option<DisplayListItem> {
4151    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| match decoration_type {
4152        TextDecorationType::Underline => DisplayListItem::Underline {
4153            bounds: clipped,
4154            color,
4155            thickness,
4156        },
4157        TextDecorationType::Strikethrough => DisplayListItem::Strikethrough {
4158            bounds: clipped,
4159            color,
4160            thickness,
4161        },
4162        TextDecorationType::Overline => DisplayListItem::Overline {
4163            bounds: clipped,
4164            color,
4165            thickness,
4166        },
4167    })
4168}
4169
4170/// Clips a scrollbar to page bounds.
4171fn clip_scrollbar_item(
4172    bounds: LogicalRect,
4173    color: ColorU,
4174    orientation: ScrollbarOrientation,
4175    opacity_key: Option<OpacityKey>,
4176    hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
4177    page_top: f32,
4178    page_bottom: f32,
4179) -> Option<DisplayListItem> {
4180    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::ScrollBar {
4181        bounds: clipped,
4182        color,
4183        orientation,
4184        opacity_key,
4185        hit_id,
4186    })
4187}
4188
4189/// Clips a hit test area to page bounds.
4190fn clip_hit_test_area_item(
4191    bounds: LogicalRect,
4192    tag: DisplayListTagId,
4193    page_top: f32,
4194    page_bottom: f32,
4195) -> Option<DisplayListItem> {
4196    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::HitTestArea {
4197        bounds: clipped,
4198        tag,
4199    })
4200}
4201
4202/// Clips an iframe to page bounds.
4203fn clip_iframe_item(
4204    child_dom_id: DomId,
4205    bounds: LogicalRect,
4206    clip_rect: LogicalRect,
4207    page_top: f32,
4208    page_bottom: f32,
4209) -> Option<DisplayListItem> {
4210    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::IFrame {
4211        child_dom_id,
4212        bounds: clipped,
4213        clip_rect: offset_rect_y(clip_rect, -page_top),
4214    })
4215}
4216
4217/// Clip a rectangle to page bounds and offset to page-relative coordinates.
4218/// Returns None if the rectangle is completely outside the page.
4219fn clip_rect_bounds(bounds: LogicalRect, page_top: f32, page_bottom: f32) -> Option<LogicalRect> {
4220    let item_top = bounds.origin.y;
4221    let item_bottom = bounds.origin.y + bounds.size.height;
4222
4223    // Check if completely outside page
4224    if item_bottom <= page_top || item_top >= page_bottom {
4225        return None;
4226    }
4227
4228    // Calculate clipped bounds
4229    let clipped_top = item_top.max(page_top);
4230    let clipped_bottom = item_bottom.min(page_bottom);
4231    let clipped_height = clipped_bottom - clipped_top;
4232
4233    // Offset to page-relative coordinates
4234    let page_relative_y = clipped_top - page_top;
4235
4236    Some(LogicalRect {
4237        origin: LogicalPosition {
4238            x: bounds.origin.x,
4239            y: page_relative_y,
4240        },
4241        size: LogicalSize {
4242            width: bounds.size.width,
4243            height: clipped_height,
4244        },
4245    })
4246}
4247
4248/// Check if a rectangle intersects the page bounds.
4249fn rect_intersects(bounds: &LogicalRect, page_top: f32, page_bottom: f32) -> bool {
4250    let item_top = bounds.origin.y;
4251    let item_bottom = bounds.origin.y + bounds.size.height;
4252    item_bottom > page_top && item_top < page_bottom
4253}
4254
4255/// Offset a rectangle's Y coordinate.
4256fn offset_rect_y(bounds: LogicalRect, offset_y: f32) -> LogicalRect {
4257    LogicalRect {
4258        origin: LogicalPosition {
4259            x: bounds.origin.x,
4260            y: bounds.origin.y + offset_y,
4261        },
4262        size: bounds.size,
4263    }
4264}
4265
4266// Slicer based pagination: "Infinite Canvas with Clipping"
4267//
4268// This approach treats pages as "viewports" into a single infinite canvas:
4269//
4270// 1. Layout generates ONE display list on an infinite vertical strip
4271// 2. Each page is a clip rectangle that "views" a portion of that strip
4272// 3. Items that span page boundaries are clipped and appear on BOTH pages
4273
4274use azul_css::props::layout::fragmentation::{BreakInside, PageBreak};
4275
4276use crate::solver3::pagination::{
4277    HeaderFooterConfig, MarginBoxContent, PageInfo, TableHeaderInfo, TableHeaderTracker,
4278};
4279
4280/// Configuration for the slicer-based pagination.
4281#[derive(Debug, Clone, Default)]
4282pub struct SlicerConfig {
4283    /// Height of each page's content area (excludes margins, headers, footers)
4284    pub page_content_height: f32,
4285    /// Height of "dead zone" between pages (for margins, headers, footers)
4286    /// This represents space that content should NOT overlap with
4287    pub page_gap: f32,
4288    /// Whether to clip items that span page boundaries (true) or push them to next page (false)
4289    pub allow_clipping: bool,
4290    /// Header and footer configuration
4291    pub header_footer: HeaderFooterConfig,
4292    /// Width of the page content area (for centering headers/footers)
4293    pub page_width: f32,
4294    /// Table headers that need repetition across pages
4295    pub table_headers: TableHeaderTracker,
4296}
4297
4298impl SlicerConfig {
4299    /// Create a simple slicer config with no gaps between pages.
4300    pub fn simple(page_height: f32) -> Self {
4301        Self {
4302            page_content_height: page_height,
4303            page_gap: 0.0,
4304            allow_clipping: true,
4305            header_footer: HeaderFooterConfig::default(),
4306            page_width: 595.0, // Default A4 width in points
4307            table_headers: TableHeaderTracker::default(),
4308        }
4309    }
4310
4311    /// Create a slicer config with margins/gaps between pages.
4312    pub fn with_gap(page_height: f32, gap: f32) -> Self {
4313        Self {
4314            page_content_height: page_height,
4315            page_gap: gap,
4316            allow_clipping: true,
4317            header_footer: HeaderFooterConfig::default(),
4318            page_width: 595.0,
4319            table_headers: TableHeaderTracker::default(),
4320        }
4321    }
4322
4323    /// Add header/footer configuration.
4324    pub fn with_header_footer(mut self, config: HeaderFooterConfig) -> Self {
4325        self.header_footer = config;
4326        self
4327    }
4328
4329    /// Set the page width (for header/footer positioning).
4330    pub fn with_page_width(mut self, width: f32) -> Self {
4331        self.page_width = width;
4332        self
4333    }
4334
4335    /// Add table headers for repetition.
4336    pub fn with_table_headers(mut self, tracker: TableHeaderTracker) -> Self {
4337        self.table_headers = tracker;
4338        self
4339    }
4340
4341    /// Register a single table header.
4342    pub fn register_table_header(&mut self, info: TableHeaderInfo) {
4343        self.table_headers.register_table_header(info);
4344    }
4345
4346    /// The total height of a page "slot" including the gap.
4347    pub fn page_slot_height(&self) -> f32 {
4348        self.page_content_height + self.page_gap
4349    }
4350
4351    /// Calculate which page a Y coordinate falls on.
4352    pub fn page_for_y(&self, y: f32) -> usize {
4353        if self.page_slot_height() <= 0.0 {
4354            return 0;
4355        }
4356        (y / self.page_slot_height()).floor() as usize
4357    }
4358
4359    /// Get the Y range for a specific page (in infinite canvas coordinates).
4360    pub fn page_bounds(&self, page_index: usize) -> (f32, f32) {
4361        let start = page_index as f32 * self.page_slot_height();
4362        let end = start + self.page_content_height;
4363        (start, end)
4364    }
4365}
4366
4367/// Paginate with CSS break property support.
4368///
4369/// This function calculates page boundaries based on CSS break-before, break-after,
4370/// and break-inside properties, then clips content to those boundaries.
4371///
4372/// **Key insight**: Items are NEVER shifted. Instead, page boundaries are adjusted
4373/// to honor break properties.
4374pub fn paginate_display_list_with_slicer_and_breaks(
4375    full_display_list: DisplayList,
4376    config: &SlicerConfig,
4377) -> Result<Vec<DisplayList>> {
4378    if config.page_content_height <= 0.0 || config.page_content_height >= f32::MAX {
4379        return Ok(vec![full_display_list]);
4380    }
4381
4382    // Calculate base header/footer space (used for pages that show headers/footers)
4383    let base_header_space = if config.header_footer.show_header {
4384        config.header_footer.header_height
4385    } else {
4386        0.0
4387    };
4388    let base_footer_space = if config.header_footer.show_footer {
4389        config.header_footer.footer_height
4390    } else {
4391        0.0
4392    };
4393
4394    // Calculate effective heights for different page types
4395    let normal_page_content_height =
4396        config.page_content_height - base_header_space - base_footer_space;
4397    let first_page_content_height = if config.header_footer.skip_first_page {
4398        // First page has full height when skipping headers/footers
4399        config.page_content_height
4400    } else {
4401        normal_page_content_height
4402    };
4403
4404    // Step 1: Calculate page break positions based on CSS properties
4405    //
4406    // Instead of using regular intervals, we calculate where page breaks
4407    // should occur based on:
4408    //
4409    // - break-before: always → force break before this item
4410    // - break-after: always → force break after this item
4411    // - break-inside: avoid → don't break inside this item (push to next page if needed)
4412
4413    let page_breaks = calculate_page_break_positions(
4414        &full_display_list,
4415        first_page_content_height,
4416        normal_page_content_height,
4417    );
4418
4419    let num_pages = page_breaks.len();
4420
4421    // Create per-page display lists by slicing the master list
4422    let mut pages: Vec<DisplayList> = Vec::with_capacity(num_pages);
4423
4424    for (page_idx, &(content_start_y, content_end_y)) in page_breaks.iter().enumerate() {
4425        // Generate page info for header/footer content
4426        let page_info = PageInfo::new(page_idx + 1, num_pages);
4427
4428        // Calculate per-page header/footer space
4429        let skip_this_page = config.header_footer.skip_first_page && page_info.is_first;
4430        let header_space = if config.header_footer.show_header && !skip_this_page {
4431            config.header_footer.header_height
4432        } else {
4433            0.0
4434        };
4435        let footer_space = if config.header_footer.show_footer && !skip_this_page {
4436            config.header_footer.footer_height
4437        } else {
4438            0.0
4439        };
4440
4441        let _ = footer_space; // Currently unused but reserved for future
4442
4443        let mut page_items = Vec::new();
4444        let mut page_node_mapping = Vec::new();
4445
4446        // 1. Add header if enabled
4447        if config.header_footer.show_header && !skip_this_page {
4448            let header_text = config.header_footer.header_text(page_info);
4449            if !header_text.is_empty() {
4450                let header_items = generate_text_display_items(
4451                    &header_text,
4452                    LogicalRect {
4453                        origin: LogicalPosition { x: 0.0, y: 0.0 },
4454                        size: LogicalSize {
4455                            width: config.page_width,
4456                            height: config.header_footer.header_height,
4457                        },
4458                    },
4459                    config.header_footer.font_size,
4460                    config.header_footer.text_color,
4461                    TextAlignment::Center,
4462                );
4463                for item in header_items {
4464                    page_items.push(item);
4465                    page_node_mapping.push(None);
4466                }
4467            }
4468        }
4469
4470        // 2. Inject repeated table headers (if any)
4471        let repeated_headers = config.table_headers.get_repeated_headers_for_page(
4472            page_idx,
4473            content_start_y,
4474            content_end_y,
4475        );
4476
4477        let mut thead_total_height = 0.0f32;
4478        for (y_offset_from_page_top, thead_items, thead_height) in repeated_headers {
4479            let thead_y = header_space + y_offset_from_page_top;
4480            for item in thead_items {
4481                let translated_item = offset_display_item_y(item, thead_y);
4482                page_items.push(translated_item);
4483                page_node_mapping.push(None);
4484            }
4485            thead_total_height = thead_total_height.max(thead_height);
4486        }
4487
4488        // 3. Calculate content offset (after header and repeated table headers)
4489        let content_y_offset = header_space + thead_total_height;
4490
4491        // 4. Slice and offset content items
4492        for (item_idx, item) in full_display_list.items.iter().enumerate() {
4493            if let Some(clipped_item) =
4494                clip_and_offset_display_item(item, content_start_y, content_end_y)
4495            {
4496                let final_item = if content_y_offset > 0.0 {
4497                    offset_display_item_y(&clipped_item, content_y_offset)
4498                } else {
4499                    clipped_item
4500                };
4501                page_items.push(final_item);
4502                let node_mapping = full_display_list
4503                    .node_mapping
4504                    .get(item_idx)
4505                    .copied()
4506                    .flatten();
4507                page_node_mapping.push(node_mapping);
4508            }
4509        }
4510
4511        // 5. Add footer if enabled
4512        if config.header_footer.show_footer && !skip_this_page {
4513            let footer_text = config.header_footer.footer_text(page_info);
4514            if !footer_text.is_empty() {
4515                let footer_y = config.page_content_height - config.header_footer.footer_height;
4516                let footer_items = generate_text_display_items(
4517                    &footer_text,
4518                    LogicalRect {
4519                        origin: LogicalPosition {
4520                            x: 0.0,
4521                            y: footer_y,
4522                        },
4523                        size: LogicalSize {
4524                            width: config.page_width,
4525                            height: config.header_footer.footer_height,
4526                        },
4527                    },
4528                    config.header_footer.font_size,
4529                    config.header_footer.text_color,
4530                    TextAlignment::Center,
4531                );
4532                for item in footer_items {
4533                    page_items.push(item);
4534                    page_node_mapping.push(None);
4535                }
4536            }
4537        }
4538
4539        pages.push(DisplayList {
4540            items: page_items,
4541            node_mapping: page_node_mapping,
4542            forced_page_breaks: Vec::new(), // Per-page lists don't need this
4543        });
4544    }
4545
4546    // Ensure at least one page
4547    if pages.is_empty() {
4548        pages.push(DisplayList::default());
4549    }
4550
4551    Ok(pages)
4552}
4553
4554/// Calculate page break positions respecting CSS forced page breaks.
4555///
4556/// Returns a vector of (start_y, end_y) tuples representing each page's content bounds.
4557///
4558/// This function uses the `forced_page_breaks` from the DisplayList to insert
4559/// page breaks at positions specified by CSS `break-before: always` and `break-after: always`.
4560/// Regular page breaks still occur at normal intervals when no forced break is present.
4561fn calculate_page_break_positions(
4562    display_list: &DisplayList,
4563    first_page_height: f32,
4564    normal_page_height: f32,
4565) -> Vec<(f32, f32)> {
4566    let total_height = calculate_display_list_height(display_list);
4567
4568    if total_height <= 0.0 || first_page_height <= 0.0 {
4569        return vec![(0.0, total_height.max(first_page_height))];
4570    }
4571
4572    // Collect all potential break points: forced breaks + regular interval breaks
4573    let mut break_points: Vec<f32> = Vec::new();
4574
4575    // Add forced page breaks from the display list (from CSS break-before/break-after)
4576    for &forced_break_y in &display_list.forced_page_breaks {
4577        if forced_break_y > 0.0 && forced_break_y < total_height {
4578            break_points.push(forced_break_y);
4579        }
4580    }
4581
4582    // Generate regular interval break points
4583    let mut y = first_page_height;
4584    while y < total_height {
4585        break_points.push(y);
4586        y += normal_page_height;
4587    }
4588
4589    // Sort and deduplicate break points
4590    break_points.sort_by(|a, b| a.partial_cmp(b).unwrap());
4591    break_points.dedup_by(|a, b| (*a - *b).abs() < 1.0); // Merge breaks within 1px
4592
4593    // Convert break points to page ranges
4594    let mut page_breaks: Vec<(f32, f32)> = Vec::new();
4595    let mut page_start = 0.0f32;
4596
4597    for break_y in break_points {
4598        if break_y > page_start {
4599            page_breaks.push((page_start, break_y));
4600            page_start = break_y;
4601        }
4602    }
4603
4604    // Add final page if there's remaining content
4605    if page_start < total_height {
4606        page_breaks.push((page_start, total_height));
4607    }
4608
4609    // Ensure at least one page
4610    if page_breaks.is_empty() {
4611        page_breaks.push((0.0, total_height.max(first_page_height)));
4612    }
4613
4614    page_breaks
4615}
4616
4617/// Text alignment for generated header/footer text.
4618#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4619pub enum TextAlignment {
4620    Left,
4621    Center,
4622    Right,
4623}
4624
4625/// Helper to offset all Y coordinates of a display item.
4626fn offset_display_item_y(item: &DisplayListItem, y_offset: f32) -> DisplayListItem {
4627    if y_offset == 0.0 {
4628        return item.clone();
4629    }
4630
4631    match item {
4632        DisplayListItem::Rect {
4633            bounds,
4634            color,
4635            border_radius,
4636        } => DisplayListItem::Rect {
4637            bounds: offset_rect_y(*bounds, y_offset),
4638            color: *color,
4639            border_radius: *border_radius,
4640        },
4641        DisplayListItem::Border {
4642            bounds,
4643            widths,
4644            colors,
4645            styles,
4646            border_radius,
4647        } => DisplayListItem::Border {
4648            bounds: offset_rect_y(*bounds, y_offset),
4649            widths: widths.clone(),
4650            colors: *colors,
4651            styles: *styles,
4652            border_radius: border_radius.clone(),
4653        },
4654        DisplayListItem::Text {
4655            glyphs,
4656            font_hash,
4657            font_size_px,
4658            color,
4659            clip_rect,
4660        } => {
4661            let offset_glyphs: Vec<GlyphInstance> = glyphs
4662                .iter()
4663                .map(|g| GlyphInstance {
4664                    index: g.index,
4665                    point: LogicalPosition {
4666                        x: g.point.x,
4667                        y: g.point.y + y_offset,
4668                    },
4669                    size: g.size,
4670                })
4671                .collect();
4672            DisplayListItem::Text {
4673                glyphs: offset_glyphs,
4674                font_hash: *font_hash,
4675                font_size_px: *font_size_px,
4676                color: *color,
4677                clip_rect: offset_rect_y(*clip_rect, y_offset),
4678            }
4679        }
4680        DisplayListItem::TextLayout {
4681            layout,
4682            bounds,
4683            font_hash,
4684            font_size_px,
4685            color,
4686        } => DisplayListItem::TextLayout {
4687            layout: layout.clone(),
4688            bounds: offset_rect_y(*bounds, y_offset),
4689            font_hash: *font_hash,
4690            font_size_px: *font_size_px,
4691            color: *color,
4692        },
4693        DisplayListItem::Image { bounds, image } => DisplayListItem::Image {
4694            bounds: offset_rect_y(*bounds, y_offset),
4695            image: image.clone(),
4696        },
4697        // Pass through other items with their bounds offset
4698        DisplayListItem::SelectionRect {
4699            bounds,
4700            border_radius,
4701            color,
4702        } => DisplayListItem::SelectionRect {
4703            bounds: offset_rect_y(*bounds, y_offset),
4704            border_radius: *border_radius,
4705            color: *color,
4706        },
4707        DisplayListItem::CursorRect { bounds, color } => DisplayListItem::CursorRect {
4708            bounds: offset_rect_y(*bounds, y_offset),
4709            color: *color,
4710        },
4711        DisplayListItem::Underline {
4712            bounds,
4713            color,
4714            thickness,
4715        } => DisplayListItem::Underline {
4716            bounds: offset_rect_y(*bounds, y_offset),
4717            color: *color,
4718            thickness: *thickness,
4719        },
4720        DisplayListItem::Strikethrough {
4721            bounds,
4722            color,
4723            thickness,
4724        } => DisplayListItem::Strikethrough {
4725            bounds: offset_rect_y(*bounds, y_offset),
4726            color: *color,
4727            thickness: *thickness,
4728        },
4729        DisplayListItem::Overline {
4730            bounds,
4731            color,
4732            thickness,
4733        } => DisplayListItem::Overline {
4734            bounds: offset_rect_y(*bounds, y_offset),
4735            color: *color,
4736            thickness: *thickness,
4737        },
4738        DisplayListItem::ScrollBar {
4739            bounds,
4740            color,
4741            orientation,
4742            opacity_key,
4743            hit_id,
4744        } => DisplayListItem::ScrollBar {
4745            bounds: offset_rect_y(*bounds, y_offset),
4746            color: *color,
4747            orientation: *orientation,
4748            opacity_key: *opacity_key,
4749            hit_id: *hit_id,
4750        },
4751        DisplayListItem::HitTestArea { bounds, tag } => DisplayListItem::HitTestArea {
4752            bounds: offset_rect_y(*bounds, y_offset),
4753            tag: *tag,
4754        },
4755        DisplayListItem::PushClip {
4756            bounds,
4757            border_radius,
4758        } => DisplayListItem::PushClip {
4759            bounds: offset_rect_y(*bounds, y_offset),
4760            border_radius: *border_radius,
4761        },
4762        DisplayListItem::PushScrollFrame {
4763            clip_bounds,
4764            content_size,
4765            scroll_id,
4766        } => DisplayListItem::PushScrollFrame {
4767            clip_bounds: offset_rect_y(*clip_bounds, y_offset),
4768            content_size: *content_size,
4769            scroll_id: *scroll_id,
4770        },
4771        DisplayListItem::PushStackingContext { bounds, z_index } => {
4772            DisplayListItem::PushStackingContext {
4773                bounds: offset_rect_y(*bounds, y_offset),
4774                z_index: *z_index,
4775            }
4776        }
4777        DisplayListItem::IFrame {
4778            child_dom_id,
4779            bounds,
4780            clip_rect,
4781        } => DisplayListItem::IFrame {
4782            child_dom_id: *child_dom_id,
4783            bounds: offset_rect_y(*bounds, y_offset),
4784            clip_rect: offset_rect_y(*clip_rect, y_offset),
4785        },
4786        // Pass through stateless items
4787        DisplayListItem::PopClip => DisplayListItem::PopClip,
4788        DisplayListItem::PopScrollFrame => DisplayListItem::PopScrollFrame,
4789        DisplayListItem::PopStackingContext => DisplayListItem::PopStackingContext,
4790
4791        // Gradient items
4792        DisplayListItem::LinearGradient {
4793            bounds,
4794            gradient,
4795            border_radius,
4796        } => DisplayListItem::LinearGradient {
4797            bounds: offset_rect_y(*bounds, y_offset),
4798            gradient: gradient.clone(),
4799            border_radius: *border_radius,
4800        },
4801        DisplayListItem::RadialGradient {
4802            bounds,
4803            gradient,
4804            border_radius,
4805        } => DisplayListItem::RadialGradient {
4806            bounds: offset_rect_y(*bounds, y_offset),
4807            gradient: gradient.clone(),
4808            border_radius: *border_radius,
4809        },
4810        DisplayListItem::ConicGradient {
4811            bounds,
4812            gradient,
4813            border_radius,
4814        } => DisplayListItem::ConicGradient {
4815            bounds: offset_rect_y(*bounds, y_offset),
4816            gradient: gradient.clone(),
4817            border_radius: *border_radius,
4818        },
4819
4820        // BoxShadow
4821        DisplayListItem::BoxShadow {
4822            bounds,
4823            shadow,
4824            border_radius,
4825        } => DisplayListItem::BoxShadow {
4826            bounds: offset_rect_y(*bounds, y_offset),
4827            shadow: *shadow,
4828            border_radius: *border_radius,
4829        },
4830
4831        // Filter effects
4832        DisplayListItem::PushFilter { bounds, filters } => DisplayListItem::PushFilter {
4833            bounds: offset_rect_y(*bounds, y_offset),
4834            filters: filters.clone(),
4835        },
4836        DisplayListItem::PopFilter => DisplayListItem::PopFilter,
4837        DisplayListItem::PushBackdropFilter { bounds, filters } => {
4838            DisplayListItem::PushBackdropFilter {
4839                bounds: offset_rect_y(*bounds, y_offset),
4840                filters: filters.clone(),
4841            }
4842        }
4843        DisplayListItem::PopBackdropFilter => DisplayListItem::PopBackdropFilter,
4844        DisplayListItem::PushOpacity { bounds, opacity } => DisplayListItem::PushOpacity {
4845            bounds: offset_rect_y(*bounds, y_offset),
4846            opacity: *opacity,
4847        },
4848        DisplayListItem::PopOpacity => DisplayListItem::PopOpacity,
4849        DisplayListItem::ScrollBarStyled { info } => {
4850            let mut offset_info = (**info).clone();
4851            offset_info.bounds = offset_rect_y(offset_info.bounds, y_offset);
4852            offset_info.track_bounds = offset_rect_y(offset_info.track_bounds, y_offset);
4853            offset_info.thumb_bounds = offset_rect_y(offset_info.thumb_bounds, y_offset);
4854            if let Some(b) = offset_info.button_decrement_bounds {
4855                offset_info.button_decrement_bounds = Some(offset_rect_y(b, y_offset));
4856            }
4857            if let Some(b) = offset_info.button_increment_bounds {
4858                offset_info.button_increment_bounds = Some(offset_rect_y(b, y_offset));
4859            }
4860            DisplayListItem::ScrollBarStyled {
4861                info: Box::new(offset_info),
4862            }
4863        }
4864
4865        // Reference frames - offset the bounds
4866        DisplayListItem::PushReferenceFrame {
4867            transform_key,
4868            initial_transform,
4869            bounds,
4870        } => DisplayListItem::PushReferenceFrame {
4871            transform_key: *transform_key,
4872            initial_transform: *initial_transform,
4873            bounds: offset_rect_y(*bounds, y_offset),
4874        },
4875        DisplayListItem::PopReferenceFrame => DisplayListItem::PopReferenceFrame,
4876        DisplayListItem::PushTextShadow { shadow } => DisplayListItem::PushTextShadow {
4877            shadow: shadow.clone(),
4878        },
4879        DisplayListItem::PopTextShadow => DisplayListItem::PopTextShadow,
4880    }
4881}
4882
4883/// Generate display list items for simple text (headers/footers).
4884///
4885/// This creates a simplified text rendering without full text layout.
4886/// For now, this creates a placeholder that renderers should handle specially.
4887fn generate_text_display_items(
4888    text: &str,
4889    bounds: LogicalRect,
4890    font_size: f32,
4891    color: ColorU,
4892    alignment: TextAlignment,
4893) -> Vec<DisplayListItem> {
4894    use crate::font_traits::FontHash;
4895
4896    if text.is_empty() {
4897        return Vec::new();
4898    }
4899
4900    // Calculate approximate text position based on alignment
4901    // For now, we estimate character width as 0.5 * font_size (monospace approximation)
4902    let char_width = font_size * 0.5;
4903    let text_width = text.len() as f32 * char_width;
4904
4905    let x_offset = match alignment {
4906        TextAlignment::Left => bounds.origin.x,
4907        TextAlignment::Center => bounds.origin.x + (bounds.size.width - text_width) / 2.0,
4908        TextAlignment::Right => bounds.origin.x + bounds.size.width - text_width,
4909    };
4910
4911    // Position text vertically centered in the bounds
4912    let y_pos = bounds.origin.y + (bounds.size.height + font_size) / 2.0 - font_size * 0.2;
4913
4914    // Create simple glyph instances for each character
4915    // Note: This is a simplified approach - proper text rendering should use text3
4916    let glyphs: Vec<GlyphInstance> = text
4917        .chars()
4918        .enumerate()
4919        .filter(|(_, c)| !c.is_control())
4920        .map(|(i, c)| GlyphInstance {
4921            index: c as u32, // Use Unicode codepoint as glyph index (placeholder)
4922            point: LogicalPosition {
4923                x: x_offset + i as f32 * char_width,
4924                y: y_pos,
4925            },
4926            size: LogicalSize::new(char_width, font_size),
4927        })
4928        .collect();
4929
4930    if glyphs.is_empty() {
4931        return Vec::new();
4932    }
4933
4934    vec![DisplayListItem::Text {
4935        glyphs,
4936        font_hash: FontHash::from_hash(0), // Default font hash - renderer should use default font
4937        font_size_px: font_size,
4938        color,
4939        clip_rect: bounds,
4940    }]
4941}
4942
4943/// Calculate the total height of a display list (max Y + height of all items).
4944fn calculate_display_list_height(display_list: &DisplayList) -> f32 {
4945    let mut max_bottom = 0.0f32;
4946
4947    for item in &display_list.items {
4948        if let Some(bounds) = get_display_item_bounds(item) {
4949            // Skip items with zero height - they don't contribute to visible content
4950            if bounds.size.height < 0.1 {
4951                continue;
4952            }
4953            
4954            let item_bottom = bounds.origin.y + bounds.size.height;
4955            if item_bottom > max_bottom {
4956                max_bottom = item_bottom;
4957            }
4958        }
4959    }
4960
4961    max_bottom
4962}
4963
4964/// Break property information for pagination decisions.
4965#[derive(Debug, Clone, Copy, Default)]
4966pub struct BreakProperties {
4967    pub break_before: PageBreak,
4968    pub break_after: PageBreak,
4969    pub break_inside: BreakInside,
4970}