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