Skip to main content

azul_layout/solver3/
display_list.rs

1//! Generates a renderer-agnostic display list from a laid-out tree.
2//!
3//! This module is the bridge between the layout solver and the compositor/renderer.
4//! Key types:
5//! - [`DisplayList`] — flat, paint-order-sorted list of drawing commands
6//! - [`DisplayListItem`] — a single drawing primitive or state-management command
7//! - [`DisplayListBuilder`] — internal builder that accumulates items during generation
8//!
9//! Entry points:
10//! - [`generate_display_list`] — converts a laid-out [`LayoutTree`] into a [`DisplayList`]
11//! - [`paginate_display_list_with_slicer_and_breaks`] — slices a display list into pages
12//!
13//! Coordinates are in **absolute window-logical pixels** ([`WindowLogicalRect`]).
14//! HiDPI scaling and scroll-offset conversion happen in the compositor.
15
16use std::{collections::{BTreeMap, HashMap}, sync::Arc};
17
18use azul_core::{
19    dom::{DomId, FormattingContext, NodeId, NodeType, ScrollbarOrientation},
20    geom::{LogicalPosition, LogicalRect, LogicalSize},
21    gpu::GpuValueCache,
22    hit_test::ScrollPosition,
23    hit_test_tag::{CursorType, TAG_TYPE_CURSOR, TAG_TYPE_DOM_NODE},
24    resources::{
25        IdNamespace, ImageRef, OpacityKey, RendererResources, TransformKey,
26    },
27    transform::ComputedTransform3D,
28    selection::{Selection, SelectionRange, TextSelection},
29    styled_dom::StyledDom,
30    ui_solver::GlyphInstance,
31};
32use azul_css::{
33    css::CssPropertyValue,
34    format_rust_code::GetHash,
35    props::{
36        basic::{ColorU, FontRef, PixelValue},
37        layout::{LayoutDisplay, LayoutOverflow, LayoutPosition},
38        property::{CssProperty, CssPropertyType},
39        style::{
40            background::{ConicGradient, ExtendMode, LinearGradient, RadialGradient},
41            border_radius::StyleBorderRadius,
42            box_shadow::{BoxShadowClipMode, StyleBoxShadow},
43            filter::{StyleFilter, StyleFilterVec},
44            BorderStyle, LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth,
45            LayoutBorderTopWidth, StyleBorderBottomColor, StyleBorderBottomStyle,
46            StyleBorderLeftColor, StyleBorderLeftStyle, StyleBorderRightColor,
47            StyleBorderRightStyle, StyleBorderTopColor, StyleBorderTopStyle,
48        },
49    },
50    LayoutDebugMessage,
51};
52
53#[cfg(feature = "text_layout")]
54use crate::text3;
55#[cfg(feature = "text_layout")]
56use crate::text3::cache::{InlineShape, PositionedItem};
57use crate::{
58    debug_info,
59    font_traits::{
60        FontHash, FontLoaderTrait, ImageSource, InlineContent, ParsedFontTrait, ShapedItem,
61        UnifiedLayout,
62    },
63    solver3::{
64        getters::{
65            get_background_color, get_background_contents, get_border_info, get_border_radius,
66            get_break_after, get_break_before, get_caret_style,
67            get_overflow_clip_margin_property, get_overflow_x, get_overflow_y,
68            get_scrollbar_gutter_property, get_scrollbar_info_from_layout, get_scrollbar_style, get_selection_style,
69            get_style_border_radius, get_visibility, get_z_index, is_forced_page_break, BorderInfo, CaretStyle,
70            ComputedScrollbarStyle, SelectionStyle,
71        },
72        layout_tree::{LayoutNode, LayoutNodeHot, LayoutNodeWarm, LayoutTree},
73        positioning::get_position_type,
74        scrollbar::{ScrollbarRequirements, compute_scrollbar_geometry_with_button_size},
75        LayoutContext, LayoutError, Result,
76    },
77};
78
79/// Border widths for all four sides.
80///
81/// Each field is optional to allow partial border specifications.
82/// Used in [`DisplayListItem::Border`] to specify per-side border widths.
83#[derive(Debug, Clone, Copy)]
84pub struct StyleBorderWidths {
85    /// Top border width (CSS `border-top-width`)
86    pub top: Option<CssPropertyValue<LayoutBorderTopWidth>>,
87    /// Right border width (CSS `border-right-width`)
88    pub right: Option<CssPropertyValue<LayoutBorderRightWidth>>,
89    /// Bottom border width (CSS `border-bottom-width`)
90    pub bottom: Option<CssPropertyValue<LayoutBorderBottomWidth>>,
91    /// Left border width (CSS `border-left-width`)
92    pub left: Option<CssPropertyValue<LayoutBorderLeftWidth>>,
93}
94
95/// Border colors for all four sides.
96///
97/// Each field is optional to allow partial border specifications.
98/// Used in [`DisplayListItem::Border`] to specify per-side border colors.
99#[derive(Debug, Clone, Copy)]
100pub struct StyleBorderColors {
101    /// Top border color (CSS `border-top-color`)
102    pub top: Option<CssPropertyValue<StyleBorderTopColor>>,
103    /// Right border color (CSS `border-right-color`)
104    pub right: Option<CssPropertyValue<StyleBorderRightColor>>,
105    /// Bottom border color (CSS `border-bottom-color`)
106    pub bottom: Option<CssPropertyValue<StyleBorderBottomColor>>,
107    /// Left border color (CSS `border-left-color`)
108    pub left: Option<CssPropertyValue<StyleBorderLeftColor>>,
109}
110
111/// Border styles for all four sides.
112///
113/// Each field is optional to allow partial border specifications.
114/// Used in [`DisplayListItem::Border`] to specify per-side border styles
115/// (solid, dashed, dotted, none, etc.).
116#[derive(Debug, Clone, Copy)]
117pub struct StyleBorderStyles {
118    /// Top border style (CSS `border-top-style`)
119    pub top: Option<CssPropertyValue<StyleBorderTopStyle>>,
120    /// Right border style (CSS `border-right-style`)
121    pub right: Option<CssPropertyValue<StyleBorderRightStyle>>,
122    /// Bottom border style (CSS `border-bottom-style`)
123    pub bottom: Option<CssPropertyValue<StyleBorderBottomStyle>>,
124    /// Left border style (CSS `border-left-style`)
125    pub left: Option<CssPropertyValue<StyleBorderLeftStyle>>,
126}
127
128/// A rectangle in border-box coordinates (includes padding and border).
129/// This is what layout calculates and stores in `used_size` and absolute positions.
130#[derive(Debug, Clone, Copy, PartialEq)]
131pub struct BorderBoxRect(pub LogicalRect);
132
133/// A `LogicalRect` known to be in **absolute window coordinates** (as output
134/// by the layout engine).  All spatial bounds stored in [`DisplayListItem`] use
135/// this type so that the compositor is *forced* to convert them to
136/// frame-relative coordinates before passing them to WebRender.
137///
138/// ## Coordinate-space contract
139///
140/// * **Layout engine** produces `WindowLogicalRect` values.
141/// * **Compositor** converts via `resolve_rect()` → WebRender `LayoutRect`.
142/// * Passing a `WindowLogicalRect` directly to a WebRender push function is a
143///   **type error** (it wraps `LogicalRect`, not `LayoutRect`).
144///
145/// See `doc/SCROLL_COORDINATE_ARCHITECTURE.md` for background.
146#[derive(Debug, Copy, Clone, Default, PartialEq, PartialOrd, Eq, Ord, Hash)]
147pub struct WindowLogicalRect(pub LogicalRect);
148
149impl WindowLogicalRect {
150    #[inline]
151    pub const fn new(origin: LogicalPosition, size: LogicalSize) -> Self {
152        Self(LogicalRect::new(origin, size))
153    }
154
155    #[inline]
156    pub const fn zero() -> Self {
157        Self(LogicalRect::zero())
158    }
159
160    /// Access the inner `LogicalRect` (still in window space – the caller is
161    /// responsible for applying any offset conversion).
162    #[inline]
163    pub const fn inner(&self) -> &LogicalRect {
164        &self.0
165    }
166
167    #[inline]
168    pub const fn into_inner(self) -> LogicalRect {
169        self.0
170    }
171
172    // Convenience accessors
173    #[inline] pub fn origin(&self) -> LogicalPosition { self.0.origin }
174    #[inline] pub fn size(&self)   -> LogicalSize     { self.0.size }
175}
176
177impl From<LogicalRect> for WindowLogicalRect {
178    #[inline]
179    fn from(r: LogicalRect) -> Self { Self(r) }
180}
181
182impl From<WindowLogicalRect> for LogicalRect {
183    #[inline]
184    fn from(w: WindowLogicalRect) -> Self { w.0 }
185}
186
187/// Simple struct for passing element dimensions to border-radius calculation
188#[derive(Debug, Clone, Copy)]
189pub struct PhysicalSizeImport {
190    pub width: f32,
191    pub height: f32,
192}
193
194/// Complete drawing information for a scrollbar with all visual components.
195///
196/// This contains the resolved geometry and colors for all scrollbar parts:
197/// - Track: The background area where the thumb slides
198/// - Thumb: The draggable indicator showing current scroll position
199/// - Buttons: Optional up/down or left/right arrow buttons
200/// - Corner: The area where horizontal and vertical scrollbars meet
201#[derive(Debug, Clone)]
202pub struct ScrollbarDrawInfo {
203    /// Overall bounds of the entire scrollbar (including track and buttons)
204    pub bounds: WindowLogicalRect,
205    /// Scrollbar orientation (horizontal or vertical)
206    pub orientation: ScrollbarOrientation,
207
208    // Track area (the background rail)
209    /// Bounds of the track area
210    pub track_bounds: WindowLogicalRect,
211    /// Color of the track background
212    pub track_color: ColorU,
213
214    // Thumb (the draggable part)
215    /// Bounds of the thumb
216    pub thumb_bounds: WindowLogicalRect,
217    /// Color of the thumb
218    pub thumb_color: ColorU,
219    /// Border radius for rounded thumb corners
220    pub thumb_border_radius: BorderRadius,
221
222    // Optional buttons (arrows at ends)
223    /// Optional decrement button bounds (up/left arrow)
224    pub button_decrement_bounds: Option<WindowLogicalRect>,
225    /// Optional increment button bounds (down/right arrow)
226    pub button_increment_bounds: Option<WindowLogicalRect>,
227    /// Color for buttons
228    pub button_color: ColorU,
229
230    /// Optional opacity key for GPU-side fading animation.
231    pub opacity_key: Option<OpacityKey>,
232    /// Optional transform key for GPU-side scrollbar thumb positioning.
233    /// When present, the compositor will wrap the thumb in a PushReferenceFrame
234    /// with PropertyBinding::Binding so WebRender can animate the thumb position
235    /// without rebuilding the display list.
236    pub thumb_transform_key: Option<TransformKey>,
237    /// Initial transform for the scrollbar thumb (current scroll position).
238    /// This is the transform applied when the display list is first built.
239    /// During GPU-only scroll, synchronize_gpu_values updates this dynamically.
240    pub thumb_initial_transform: ComputedTransform3D,
241    /// Optional hit-test ID for WebRender hit-testing.
242    pub hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
243    /// Whether to clip scrollbar to container's border-radius
244    pub clip_to_container_border: bool,
245    /// Container's border-radius (for clipping)
246    pub container_border_radius: BorderRadius,
247    /// Scrollbar visibility mode — used by back-registration to choose initial opacity.
248    /// `Always` → initial opacity 1.0; `WhenScrolling` → initial opacity 0.0.
249    pub visibility: azul_css::props::style::scrollbar::ScrollbarVisibilityMode,
250}
251
252impl BorderBoxRect {
253    /// Convert border-box to content-box by subtracting padding and border.
254    /// Content-box is where inline layout and text actually render.
255    pub fn to_content_box(
256        self,
257        padding: &crate::solver3::geometry::EdgeSizes,
258        border: &crate::solver3::geometry::EdgeSizes,
259    ) -> ContentBoxRect {
260        ContentBoxRect(LogicalRect {
261            origin: LogicalPosition {
262                x: self.0.origin.x + padding.left + border.left,
263                y: self.0.origin.y + padding.top + border.top,
264            },
265            size: LogicalSize {
266                width: self.0.size.width
267                    - padding.left
268                    - padding.right
269                    - border.left
270                    - border.right,
271                height: self.0.size.height
272                    - padding.top
273                    - padding.bottom
274                    - border.top
275                    - border.bottom,
276            },
277        })
278    }
279
280    /// Get the inner LogicalRect
281    pub fn rect(&self) -> LogicalRect {
282        self.0
283    }
284}
285
286/// A rectangle in content-box coordinates (excludes padding and border).
287/// This is where text and inline content is positioned by the inline formatter.
288#[derive(Debug, Clone, Copy, PartialEq)]
289pub struct ContentBoxRect(pub LogicalRect);
290
291impl ContentBoxRect {
292    /// Get the inner LogicalRect
293    pub fn rect(&self) -> LogicalRect {
294        self.0
295    }
296}
297
298/// The final, renderer-agnostic output of the layout engine.
299///
300/// This is a flat list of drawing and state-management commands, already sorted
301/// according to the CSS paint order. A renderer can consume this list directly.
302#[derive(Debug, Default, Clone)]
303pub struct DisplayList {
304    pub items: Vec<DisplayListItem>,
305    /// Optional mapping from item index to the DOM NodeId that generated it.
306    /// Used for pagination to look up CSS break properties.
307    /// Not all items have a source node (e.g., synthesized decorations).
308    pub node_mapping: Vec<Option<NodeId>>,
309    /// Y-positions where forced page breaks should occur (from break-before/break-after: always).
310    /// These are absolute Y coordinates in the infinite canvas coordinate system.
311    /// The slicer will ensure page boundaries align with these positions.
312    pub forced_page_breaks: Vec<f32>,
313    /// Index ranges (start, end) of display list items that belong to fixed-position elements.
314    /// In paged media, these items are replicated on every page (CSS Positioned Layout §2.1).
315    pub fixed_position_item_ranges: Vec<(usize, usize)>,
316}
317
318impl DisplayList {
319    /// Patch text glyph data for a specific layout node without rebuilding
320    /// the entire display list. Returns the damage rect covering all
321    /// affected text items, or None if no matching items found.
322    ///
323    /// Used for GlyphSwap incremental relayout: glyphs changed but
324    /// positions are identical, so only the glyph IDs need updating.
325    pub fn patch_text_glyphs(
326        &mut self,
327        node_index: usize,
328        new_glyphs_by_run: &[Vec<GlyphInstance>],
329    ) -> Option<LogicalRect> {
330        let mut run_idx = 0;
331        let mut damage: Option<LogicalRect> = None;
332
333        for item in &mut self.items {
334            if let DisplayListItem::Text {
335                ref mut glyphs,
336                ref clip_rect,
337                source_node_index: Some(src_idx),
338                ..
339            } = item {
340                if *src_idx == node_index {
341                    if run_idx < new_glyphs_by_run.len() {
342                        *glyphs = new_glyphs_by_run[run_idx].clone();
343                        let bounds = *clip_rect.inner();
344                        damage = Some(match damage {
345                            Some(d) => crate::cpurender::union_rect(&d, &bounds),
346                            None => bounds,
347                        });
348                        run_idx += 1;
349                    }
350                }
351            }
352        }
353
354        damage
355    }
356
357    /// Compute a damage rect from the difference between old and new text
358    /// layout results, starting from a given line index.
359    pub fn compute_text_damage_rect(
360        old_items: &[super::super::text3::cache::PositionedItem],
361        new_items: &[super::super::text3::cache::PositionedItem],
362        container_origin: LogicalPosition,
363        affected_line: usize,
364    ) -> LogicalRect {
365        let expand = |items: &[super::super::text3::cache::PositionedItem]| -> (f32, f32, f32, f32) {
366            let mut lx = f32::MAX;
367            let mut ly = f32::MAX;
368            let mut rx = f32::MIN;
369            let mut ry = f32::MIN;
370            for item in items {
371                if item.line_index >= affected_line {
372                    let bounds = item.item.bounds();
373                    let x = container_origin.x + item.position.x;
374                    let y = container_origin.y + item.position.y;
375                    lx = lx.min(x);
376                    ly = ly.min(y);
377                    rx = rx.max(x + bounds.width);
378                    ry = ry.max(y + bounds.height);
379                }
380            }
381            (lx, ly, rx, ry)
382        };
383
384        let (olx, oly, orx, ory) = expand(old_items);
385        let (nlx, nly, nrx, nry) = expand(new_items);
386        let min_x = olx.min(nlx);
387        let min_y = oly.min(nly);
388        let max_x = orx.max(nrx);
389        let max_y = ory.max(nry);
390
391        if min_x > max_x || min_y > max_y {
392            return LogicalRect::default();
393        }
394
395        LogicalRect {
396            origin: LogicalPosition { x: min_x, y: min_y },
397            size: LogicalSize { width: max_x - min_x, height: max_y - min_y },
398        }
399    }
400
401    /// Generates a JSON representation of the display list for debugging.
402    /// This includes clip chain analysis showing how clips are stacked.
403    pub fn to_debug_json(&self) -> String {
404        use std::fmt::Write;
405        let mut json = String::new();
406        writeln!(json, "{{").unwrap();
407        writeln!(json, "  \"total_items\": {},", self.items.len()).unwrap();
408        writeln!(json, "  \"items\": [").unwrap();
409
410        let mut clip_depth = 0i32;
411        let mut scroll_depth = 0i32;
412        let mut stacking_depth = 0i32;
413
414        for (i, item) in self.items.iter().enumerate() {
415            let comma = if i < self.items.len() - 1 { "," } else { "" };
416            let node_id = self.node_mapping.get(i).and_then(|n| *n);
417
418            match item {
419                DisplayListItem::PushClip {
420                    bounds,
421                    border_radius,
422                } => {
423                    clip_depth += 1;
424                    writeln!(json, "    {{").unwrap();
425                    writeln!(json, "      \"index\": {},", i).unwrap();
426                    writeln!(json, "      \"type\": \"PushClip\",").unwrap();
427                    writeln!(json, "      \"clip_depth\": {},", clip_depth).unwrap();
428                    writeln!(json, "      \"scroll_depth\": {},", scroll_depth).unwrap();
429                    writeln!(json, "      \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},", 
430                        bounds.0.origin.x, bounds.0.origin.y, bounds.0.size.width, bounds.0.size.height).unwrap();
431                    writeln!(json, "      \"border_radius\": {{ \"tl\": {:.1}, \"tr\": {:.1}, \"bl\": {:.1}, \"br\": {:.1} }},",
432                        border_radius.top_left, border_radius.top_right,
433                        border_radius.bottom_left, border_radius.bottom_right).unwrap();
434                    writeln!(json, "      \"node_id\": {:?}", node_id).unwrap();
435                    writeln!(json, "    }}{}", comma).unwrap();
436                }
437                DisplayListItem::PopClip => {
438                    writeln!(json, "    {{").unwrap();
439                    writeln!(json, "      \"index\": {},", i).unwrap();
440                    writeln!(json, "      \"type\": \"PopClip\",").unwrap();
441                    writeln!(json, "      \"clip_depth_before\": {},", clip_depth).unwrap();
442                    writeln!(json, "      \"clip_depth_after\": {}", clip_depth - 1).unwrap();
443                    writeln!(json, "    }}{}", comma).unwrap();
444                    clip_depth -= 1;
445                }
446                DisplayListItem::PushScrollFrame {
447                    clip_bounds,
448                    content_size,
449                    scroll_id,
450                } => {
451                    scroll_depth += 1;
452                    writeln!(json, "    {{").unwrap();
453                    writeln!(json, "      \"index\": {},", i).unwrap();
454                    writeln!(json, "      \"type\": \"PushScrollFrame\",").unwrap();
455                    writeln!(json, "      \"clip_depth\": {},", clip_depth).unwrap();
456                    writeln!(json, "      \"scroll_depth\": {},", scroll_depth).unwrap();
457                    writeln!(json, "      \"clip_bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},",
458                        clip_bounds.0.origin.x, clip_bounds.0.origin.y,
459                        clip_bounds.0.size.width, clip_bounds.0.size.height).unwrap();
460                    writeln!(
461                        json,
462                        "      \"content_size\": {{ \"w\": {:.1}, \"h\": {:.1} }},",
463                        content_size.width, content_size.height
464                    )
465                    .unwrap();
466                    writeln!(json, "      \"scroll_id\": {},", scroll_id).unwrap();
467                    writeln!(json, "      \"node_id\": {:?}", node_id).unwrap();
468                    writeln!(json, "    }}{}", comma).unwrap();
469                }
470                DisplayListItem::PopScrollFrame => {
471                    writeln!(json, "    {{").unwrap();
472                    writeln!(json, "      \"index\": {},", i).unwrap();
473                    writeln!(json, "      \"type\": \"PopScrollFrame\",").unwrap();
474                    writeln!(json, "      \"scroll_depth_before\": {},", scroll_depth).unwrap();
475                    writeln!(json, "      \"scroll_depth_after\": {}", scroll_depth - 1).unwrap();
476                    writeln!(json, "    }}{}", comma).unwrap();
477                    scroll_depth -= 1;
478                }
479                DisplayListItem::PushStackingContext { z_index, bounds } => {
480                    stacking_depth += 1;
481                    writeln!(json, "    {{").unwrap();
482                    writeln!(json, "      \"index\": {},", i).unwrap();
483                    writeln!(json, "      \"type\": \"PushStackingContext\",").unwrap();
484                    writeln!(json, "      \"stacking_depth\": {},", stacking_depth).unwrap();
485                    writeln!(json, "      \"z_index\": {},", z_index).unwrap();
486                    writeln!(json, "      \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }}",
487                        bounds.0.origin.x, bounds.0.origin.y, bounds.0.size.width, bounds.0.size.height).unwrap();
488                    writeln!(json, "    }}{}", comma).unwrap();
489                }
490                DisplayListItem::PopStackingContext => {
491                    writeln!(json, "    {{").unwrap();
492                    writeln!(json, "      \"index\": {},", i).unwrap();
493                    writeln!(json, "      \"type\": \"PopStackingContext\",").unwrap();
494                    writeln!(json, "      \"stacking_depth_before\": {},", stacking_depth).unwrap();
495                    writeln!(
496                        json,
497                        "      \"stacking_depth_after\": {}",
498                        stacking_depth - 1
499                    )
500                    .unwrap();
501                    writeln!(json, "    }}{}", comma).unwrap();
502                    stacking_depth -= 1;
503                }
504                DisplayListItem::Rect {
505                    bounds,
506                    color,
507                    border_radius,
508                } => {
509                    writeln!(json, "    {{").unwrap();
510                    writeln!(json, "      \"index\": {},", i).unwrap();
511                    writeln!(json, "      \"type\": \"Rect\",").unwrap();
512                    writeln!(json, "      \"clip_depth\": {},", clip_depth).unwrap();
513                    writeln!(json, "      \"scroll_depth\": {},", scroll_depth).unwrap();
514                    writeln!(json, "      \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},",
515                        bounds.0.origin.x, bounds.0.origin.y, bounds.0.size.width, bounds.0.size.height).unwrap();
516                    writeln!(
517                        json,
518                        "      \"color\": \"rgba({},{},{},{})\",",
519                        color.r, color.g, color.b, color.a
520                    )
521                    .unwrap();
522                    writeln!(json, "      \"node_id\": {:?}", node_id).unwrap();
523                    writeln!(json, "    }}{}", comma).unwrap();
524                }
525                DisplayListItem::Border { bounds, .. } => {
526                    writeln!(json, "    {{").unwrap();
527                    writeln!(json, "      \"index\": {},", i).unwrap();
528                    writeln!(json, "      \"type\": \"Border\",").unwrap();
529                    writeln!(json, "      \"clip_depth\": {},", clip_depth).unwrap();
530                    writeln!(json, "      \"scroll_depth\": {},", scroll_depth).unwrap();
531                    writeln!(json, "      \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},",
532                        bounds.0.origin.x, bounds.0.origin.y, bounds.0.size.width, bounds.0.size.height).unwrap();
533                    writeln!(json, "      \"node_id\": {:?}", node_id).unwrap();
534                    writeln!(json, "    }}{}", comma).unwrap();
535                }
536                DisplayListItem::ScrollBarStyled { info } => {
537                    writeln!(json, "    {{").unwrap();
538                    writeln!(json, "      \"index\": {},", i).unwrap();
539                    writeln!(json, "      \"type\": \"ScrollBarStyled\",").unwrap();
540                    writeln!(json, "      \"clip_depth\": {},", clip_depth).unwrap();
541                    writeln!(json, "      \"scroll_depth\": {},", scroll_depth).unwrap();
542                    writeln!(json, "      \"orientation\": \"{:?}\",", info.orientation).unwrap();
543                    writeln!(json, "      \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }}",
544                        info.bounds.0.origin.x, info.bounds.0.origin.y,
545                        info.bounds.0.size.width, info.bounds.0.size.height).unwrap();
546                    writeln!(json, "    }}{}", comma).unwrap();
547                }
548                _ => {
549                    writeln!(json, "    {{").unwrap();
550                    writeln!(json, "      \"index\": {},", i).unwrap();
551                    writeln!(
552                        json,
553                        "      \"type\": \"{:?}\",",
554                        std::mem::discriminant(item)
555                    )
556                    .unwrap();
557                    writeln!(json, "      \"clip_depth\": {},", clip_depth).unwrap();
558                    writeln!(json, "      \"scroll_depth\": {},", scroll_depth).unwrap();
559                    writeln!(json, "      \"node_id\": {:?}", node_id).unwrap();
560                    writeln!(json, "    }}{}", comma).unwrap();
561                }
562            }
563        }
564
565        writeln!(json, "  ],").unwrap();
566        writeln!(json, "  \"final_clip_depth\": {},", clip_depth).unwrap();
567        writeln!(json, "  \"final_scroll_depth\": {},", scroll_depth).unwrap();
568        writeln!(json, "  \"final_stacking_depth\": {},", stacking_depth).unwrap();
569        writeln!(
570            json,
571            "  \"balanced\": {}",
572            clip_depth == 0 && scroll_depth == 0 && stacking_depth == 0
573        )
574        .unwrap();
575        writeln!(json, "}}").unwrap();
576
577        json
578    }
579}
580
581/// A command in the display list. Can be either a drawing primitive or a
582/// state-management instruction for the renderer's graphics context.
583#[derive(Debug, Clone)]
584pub enum DisplayListItem {
585    // Drawing Primitives
586    /// A filled rectangle with optional rounded corners.
587    /// Used for backgrounds, colored boxes, and other solid fills.
588    Rect {
589        /// The rectangle bounds in absolute window coordinates
590        bounds: WindowLogicalRect,
591        /// The fill color (RGBA)
592        color: ColorU,
593        /// Corner radii for rounded rectangles
594        border_radius: BorderRadius,
595    },
596    /// A selection highlight rectangle (e.g., for text selection).
597    /// Rendered behind text to show selected regions.
598    SelectionRect {
599        /// The rectangle bounds in absolute window coordinates
600        bounds: WindowLogicalRect,
601        /// Corner radii for rounded selection
602        border_radius: BorderRadius,
603        /// The selection highlight color (typically semi-transparent)
604        color: ColorU,
605    },
606    /// A text cursor (caret) rectangle.
607    /// Typically a thin vertical line indicating text insertion point.
608    CursorRect {
609        /// The cursor bounds (usually narrow width)
610        bounds: WindowLogicalRect,
611        /// The cursor color
612        color: ColorU,
613    },
614    /// A CSS border with per-side widths, colors, and styles.
615    /// Supports different styles per side (solid, dashed, dotted, etc.).
616    Border {
617        /// The border-box bounds
618        bounds: WindowLogicalRect,
619        /// Border widths for each side
620        widths: StyleBorderWidths,
621        /// Border colors for each side
622        colors: StyleBorderColors,
623        /// Border styles for each side (solid, dashed, etc.)
624        styles: StyleBorderStyles,
625        /// Corner radii for rounded borders
626        border_radius: StyleBorderRadius,
627    },
628    /// Text layout with full metadata (for PDF, accessibility, etc.)
629    /// This is pushed BEFORE the individual Text items and contains
630    /// the original text, glyph-to-unicode mapping, and positioning info
631    TextLayout {
632        layout: Arc<dyn std::any::Any + Send + Sync>, // Type-erased UnifiedLayout
633        bounds: WindowLogicalRect,
634        font_hash: FontHash,
635        font_size_px: f32,
636        color: ColorU,
637    },
638    /// Text rendered with individual glyph positioning (for simple renderers)
639    Text {
640        glyphs: Vec<GlyphInstance>,
641        font_hash: FontHash,
642        font_size_px: f32,
643        color: ColorU,
644        clip_rect: WindowLogicalRect,
645        /// Layout node index that produced this text run.
646        /// Enables patching glyphs without full display list regeneration.
647        source_node_index: Option<usize>,
648    },
649    /// Underline decoration for text (CSS text-decoration: underline)
650    Underline {
651        bounds: WindowLogicalRect,
652        color: ColorU,
653        thickness: f32,
654    },
655    /// Strikethrough decoration for text (CSS text-decoration: line-through)
656    Strikethrough {
657        bounds: WindowLogicalRect,
658        color: ColorU,
659        thickness: f32,
660    },
661    /// Overline decoration for text (CSS text-decoration: overline)
662    Overline {
663        bounds: WindowLogicalRect,
664        color: ColorU,
665        thickness: f32,
666    },
667    Image {
668        bounds: WindowLogicalRect,
669        image: ImageRef,
670        border_radius: BorderRadius,
671    },
672    /// A dedicated primitive for a scrollbar with optional GPU-animated opacity.
673    /// This is a simple single-color scrollbar used for basic rendering.
674    ScrollBar {
675        bounds: WindowLogicalRect,
676        color: ColorU,
677        orientation: ScrollbarOrientation,
678        /// Optional opacity key for GPU-side fading animation.
679        /// If present, the renderer will use this key to look up dynamic opacity.
680        /// If None, the alpha channel of `color` is used directly.
681        opacity_key: Option<OpacityKey>,
682        /// Optional hit-test ID for WebRender hit-testing.
683        /// If present, allows event handlers to identify which scrollbar component was clicked.
684        hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
685    },
686    /// A fully styled scrollbar with separate track, thumb, and optional buttons.
687    /// Used when CSS scrollbar properties are specified.
688    ScrollBarStyled {
689        /// Complete drawing information for all scrollbar components
690        info: Box<ScrollbarDrawInfo>,
691    },
692
693    /// An embedded VirtualView that references a child DOM with its own display list.
694    /// The renderer will look up the child display list by child_dom_id and
695    /// render it within the bounds. The VirtualView viewport is rendered in parent
696    /// coordinate space (NOT inside a scroll frame) so it stays stationary.
697    /// Scroll offset is communicated to the VirtualView callback, not via WebRender.
698    VirtualView {
699        /// The DomId of the child DOM (similar to webrender's pipeline_id)
700        child_dom_id: DomId,
701        /// The bounds where the VirtualView should be rendered
702        bounds: WindowLogicalRect,
703        /// The clip rect for the VirtualView content
704        clip_rect: WindowLogicalRect,
705    },
706
707    /// Placeholder emitted during display list generation for VirtualView nodes.
708    /// `window.rs` replaces this with a real `VirtualView` item after invoking
709    /// the VirtualView callback. This avoids the need for post-hoc scroll frame
710    /// scanning — `window.rs` simply finds the placeholder by `node_id`.
711    ///
712    /// Unlike regular scrollable nodes, VirtualView nodes do NOT get a
713    /// PushScrollFrame/PopScrollFrame pair. Scroll state is managed by
714    /// `ScrollManager` and passed to the VirtualView callback as `scroll_offset`.
715    VirtualViewPlaceholder {
716        /// The DOM NodeId of the VirtualView element in the parent DOM
717        node_id: NodeId,
718        /// The layout bounds of the VirtualView container
719        bounds: WindowLogicalRect,
720        /// The clip rect (same as bounds initially, may be adjusted)
721        clip_rect: WindowLogicalRect,
722    },
723
724    // --- State-Management Commands ---
725    /// Pushes a new clipping rectangle onto the renderer's clip stack.
726    /// All subsequent primitives will be clipped by this rect until a PopClip.
727    PushClip {
728        bounds: WindowLogicalRect,
729        border_radius: BorderRadius,
730    },
731    /// Pops the current clip from the renderer's clip stack.
732    PopClip,
733
734    /// Pushes an image-based clip mask onto the renderer's clip stack.
735    /// The mask image should be R8 format: white (255) = visible, black (0) = clipped.
736    /// All subsequent primitives will be masked until PopImageMaskClip.
737    PushImageMaskClip {
738        /// The bounds of the element being clipped
739        bounds: WindowLogicalRect,
740        /// The mask image (R8 format)
741        mask_image: ImageRef,
742        /// The rect within which the mask is applied
743        mask_rect: WindowLogicalRect,
744    },
745    /// Pops the current image mask clip from the renderer's clip stack.
746    PopImageMaskClip,
747
748    /// Defines a scrollable area. This is a specialized clip that also
749    /// establishes a new coordinate system for its children, which can be offset.
750    PushScrollFrame {
751        /// The clip rect in the parent's coordinate space.
752        clip_bounds: WindowLogicalRect,
753        /// The total size of the scrollable content.
754        content_size: LogicalSize,
755        /// An ID for the renderer to track this scrollable area between frames.
756        scroll_id: LocalScrollId,
757    },
758    /// Pops the current scroll frame.
759    PopScrollFrame,
760
761    /// Pushes a new stacking context for proper z-index layering.
762    /// All subsequent primitives until PopStackingContext will be in this stacking context.
763    PushStackingContext {
764        /// The z-index for this stacking context (for debugging/validation)
765        z_index: i32,
766        /// The bounds of the stacking context root element
767        bounds: WindowLogicalRect,
768    },
769    /// Pops the current stacking context.
770    PopStackingContext,
771
772    /// Pushes a reference frame with a GPU-accelerated transform.
773    /// Used for CSS transforms and drag visual offsets.
774    /// Creates a new spatial coordinate system for all children.
775    PushReferenceFrame {
776        /// The transform key for GPU-animated property binding
777        transform_key: TransformKey,
778        /// The initial transform value (identity for drag, computed for CSS transform)
779        initial_transform: ComputedTransform3D,
780        /// The bounds of the reference frame (origin = transform origin)
781        bounds: WindowLogicalRect,
782    },
783    /// Pops the current reference frame.
784    PopReferenceFrame,
785
786    /// Defines a region for hit-testing.
787    HitTestArea {
788        bounds: WindowLogicalRect,
789        tag: DisplayListTagId, // This would be a renderer-agnostic ID type
790    },
791
792    // --- Gradient Primitives ---
793    /// A linear gradient fill.
794    LinearGradient {
795        bounds: WindowLogicalRect,
796        gradient: LinearGradient,
797        border_radius: BorderRadius,
798    },
799    /// A radial gradient fill.
800    RadialGradient {
801        bounds: WindowLogicalRect,
802        gradient: RadialGradient,
803        border_radius: BorderRadius,
804    },
805    /// A conic (angular) gradient fill.
806    ConicGradient {
807        bounds: WindowLogicalRect,
808        gradient: ConicGradient,
809        border_radius: BorderRadius,
810    },
811
812    // --- Shadow Effects ---
813    /// A box shadow (either outset or inset).
814    BoxShadow {
815        bounds: WindowLogicalRect,
816        shadow: StyleBoxShadow,
817        border_radius: BorderRadius,
818    },
819
820    // --- Filter Effects ---
821    /// Push a filter effect that applies to subsequent content.
822    PushFilter {
823        bounds: WindowLogicalRect,
824        filters: Vec<StyleFilter>,
825    },
826    /// Pop a previously pushed filter.
827    PopFilter,
828
829    /// Push a backdrop filter (applies to content behind the element).
830    PushBackdropFilter {
831        bounds: WindowLogicalRect,
832        filters: Vec<StyleFilter>,
833    },
834    /// Pop a previously pushed backdrop filter.
835    PopBackdropFilter,
836
837    /// Push an opacity layer.
838    PushOpacity {
839        bounds: WindowLogicalRect,
840        opacity: f32,
841    },
842    /// Pop an opacity layer.
843    PopOpacity,
844
845    /// Push a text shadow that applies to subsequent text content.
846    PushTextShadow {
847        shadow: azul_css::props::style::box_shadow::StyleBoxShadow,
848    },
849    /// Pop all text shadows.
850    PopTextShadow,
851}
852
853impl DisplayListItem {
854    /// Compare two display list items for visual equality (same appearance when rendered).
855    /// Used by damage computation to detect content changes within the same bounds.
856    /// Conservative: returns `false` (assumes different) for complex types like Arc<dyn Any>.
857    pub fn is_visually_equal(&self, other: &Self) -> bool {
858        if std::mem::discriminant(self) != std::mem::discriminant(other) {
859            return false;
860        }
861        match (self, other) {
862            (Self::Rect { bounds: b1, color: c1, border_radius: br1 },
863             Self::Rect { bounds: b2, color: c2, border_radius: br2 }) => {
864                b1 == b2 && c1 == c2 && br1.top_left == br2.top_left && br1.top_right == br2.top_right
865                    && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
866            }
867            (Self::SelectionRect { bounds: b1, border_radius: br1, color: c1 },
868             Self::SelectionRect { bounds: b2, border_radius: br2, color: c2 }) => {
869                b1 == b2 && c1 == c2 && br1.top_left == br2.top_left && br1.top_right == br2.top_right
870                    && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
871            }
872            (Self::CursorRect { bounds: b1, color: c1 },
873             Self::CursorRect { bounds: b2, color: c2 }) => b1 == b2 && c1 == c2,
874            (Self::Text { glyphs: g1, font_hash: fh1, font_size_px: fs1, color: c1, clip_rect: cr1, .. },
875             Self::Text { glyphs: g2, font_hash: fh2, font_size_px: fs2, color: c2, clip_rect: cr2, .. }) => {
876                cr1 == cr2 && c1 == c2 && fh1 == fh2 && fs1 == fs2 && g1.len() == g2.len()
877                    && g1.iter().zip(g2.iter()).all(|(a, b)| {
878                        a.index == b.index
879                            && a.point.x == b.point.x
880                            && a.point.y == b.point.y
881                    })
882            }
883            (Self::Underline { bounds: b1, color: c1, thickness: t1 },
884             Self::Underline { bounds: b2, color: c2, thickness: t2 }) => b1 == b2 && c1 == c2 && t1 == t2,
885            (Self::Strikethrough { bounds: b1, color: c1, thickness: t1 },
886             Self::Strikethrough { bounds: b2, color: c2, thickness: t2 }) => b1 == b2 && c1 == c2 && t1 == t2,
887            (Self::Overline { bounds: b1, color: c1, thickness: t1 },
888             Self::Overline { bounds: b2, color: c2, thickness: t2 }) => b1 == b2 && c1 == c2 && t1 == t2,
889            (Self::Border { bounds: b1, widths: w1, colors: c1, styles: s1, .. },
890             Self::Border { bounds: b2, widths: w2, colors: c2, styles: s2, .. }) => {
891                b1 == b2
892                    && w1.top == w2.top && w1.right == w2.right && w1.bottom == w2.bottom && w1.left == w2.left
893                    && c1.top == c2.top && c1.right == c2.right && c1.bottom == c2.bottom && c1.left == c2.left
894                    && s1.top == s2.top && s1.right == s2.right && s1.bottom == s2.bottom && s1.left == s2.left
895            }
896            (Self::Image { bounds: b1, image: i1, border_radius: br1 },
897             Self::Image { bounds: b2, image: i2, border_radius: br2 }) => {
898                b1 == b2
899                    && i1.data as usize == i2.data as usize // pointer identity
900                    && br1.top_left == br2.top_left && br1.top_right == br2.top_right
901                    && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
902            }
903            (Self::BoxShadow { bounds: b1, shadow: s1, border_radius: br1 },
904             Self::BoxShadow { bounds: b2, shadow: s2, border_radius: br2 }) => {
905                b1 == b2 && s1 == s2
906                    && br1.top_left == br2.top_left && br1.top_right == br2.top_right
907                    && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
908            }
909            (Self::LinearGradient { bounds: b1, gradient: g1, border_radius: br1 },
910             Self::LinearGradient { bounds: b2, gradient: g2, border_radius: br2 }) => {
911                b1 == b2 && g1 == g2
912                    && br1.top_left == br2.top_left && br1.top_right == br2.top_right
913                    && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
914            }
915            (Self::RadialGradient { bounds: b1, gradient: g1, border_radius: br1 },
916             Self::RadialGradient { bounds: b2, gradient: g2, border_radius: br2 }) => {
917                b1 == b2 && g1 == g2
918                    && br1.top_left == br2.top_left && br1.top_right == br2.top_right
919                    && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
920            }
921            (Self::ConicGradient { bounds: b1, gradient: g1, border_radius: br1 },
922             Self::ConicGradient { bounds: b2, gradient: g2, border_radius: br2 }) => {
923                b1 == b2 && g1 == g2
924                    && br1.top_left == br2.top_left && br1.top_right == br2.top_right
925                    && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
926            }
927            (Self::ScrollBar { bounds: b1, color: c1, .. },
928             Self::ScrollBar { bounds: b2, color: c2, .. }) => b1 == b2 && c1 == c2,
929            (Self::PushClip { bounds: b1, .. }, Self::PushClip { bounds: b2, .. }) => b1 == b2,
930            (Self::PushScrollFrame { clip_bounds: b1, scroll_id: s1, .. },
931             Self::PushScrollFrame { clip_bounds: b2, scroll_id: s2, .. }) => b1 == b2 && s1 == s2,
932            (Self::PushStackingContext { z_index: z1, bounds: b1 },
933             Self::PushStackingContext { z_index: z2, bounds: b2 }) => z1 == z2 && b1 == b2,
934            (Self::PushOpacity { bounds: b1, opacity: o1 },
935             Self::PushOpacity { bounds: b2, opacity: o2 }) => b1 == b2 && o1 == o2,
936            // Pop items with no fields are always equal (discriminant already matched)
937            (Self::PopClip, Self::PopClip)
938            | (Self::PopImageMaskClip, Self::PopImageMaskClip)
939            | (Self::PopScrollFrame, Self::PopScrollFrame)
940            | (Self::PopStackingContext, Self::PopStackingContext)
941            | (Self::PopReferenceFrame, Self::PopReferenceFrame)
942            | (Self::PopFilter, Self::PopFilter)
943            | (Self::PopBackdropFilter, Self::PopBackdropFilter)
944            | (Self::PopOpacity, Self::PopOpacity)
945            | (Self::PopTextShadow, Self::PopTextShadow) => true,
946            // For complex types (TextLayout with Arc, Image, gradients, etc.),
947            // conservatively assume different
948            _ => false,
949        }
950    }
951
952    /// Returns true if this item is a state-management command (Push/Pop)
953    /// that must always be processed to maintain correct stacks.
954    pub fn is_state_management(&self) -> bool {
955        matches!(self,
956            Self::PushClip { .. }
957            | Self::PopClip
958            | Self::PushImageMaskClip { .. }
959            | Self::PopImageMaskClip
960            | Self::PushScrollFrame { .. }
961            | Self::PopScrollFrame
962            | Self::PushStackingContext { .. }
963            | Self::PopStackingContext
964            | Self::PushReferenceFrame { .. }
965            | Self::PopReferenceFrame
966            | Self::PushFilter { .. }
967            | Self::PopFilter
968            | Self::PushBackdropFilter { .. }
969            | Self::PopBackdropFilter
970            | Self::PushOpacity { .. }
971            | Self::PopOpacity
972            | Self::PushTextShadow { .. }
973            | Self::PopTextShadow
974        )
975    }
976
977    /// Return the visual bounding rect including effects that extend beyond
978    /// content bounds (e.g. box-shadow spread/blur/offset). Used for damage
979    /// rect computation where we need the full repaint area.
980    pub fn visual_bounds(&self) -> Option<LogicalRect> {
981        match self {
982            Self::BoxShadow { bounds, shadow, .. } => {
983                let b = *bounds.inner();
984                // Shadow can extend beyond element bounds by offset + spread + blur
985                let ox = shadow.offset_x.to_pixels_internal(16.0, 16.0).abs();
986                let oy = shadow.offset_y.to_pixels_internal(16.0, 16.0).abs();
987                let blur = shadow.blur_radius.to_pixels_internal(16.0, 16.0).abs();
988                let spread = shadow.spread_radius.to_pixels_internal(16.0, 16.0).abs();
989                let expand = ox + oy + blur + spread;
990                Some(LogicalRect {
991                    origin: LogicalPosition {
992                        x: b.origin.x - expand,
993                        y: b.origin.y - expand,
994                    },
995                    size: LogicalSize {
996                        width: b.size.width + expand * 2.0,
997                        height: b.size.height + expand * 2.0,
998                    },
999                })
1000            }
1001            _ => self.bounds(),
1002        }
1003    }
1004
1005    /// Return the bounding rect of this item, or None for push/pop commands
1006    /// that don't have their own visual bounds.
1007    pub fn bounds(&self) -> Option<LogicalRect> {
1008        match self {
1009            Self::Rect { bounds, .. }
1010            | Self::SelectionRect { bounds, .. }
1011            | Self::CursorRect { bounds, .. }
1012            | Self::Border { bounds, .. }
1013            | Self::Text { clip_rect: bounds, .. }
1014            | Self::TextLayout { bounds, .. }
1015            | Self::Underline { bounds, .. }
1016            | Self::Strikethrough { bounds, .. }
1017            | Self::Overline { bounds, .. }
1018            | Self::Image { bounds, .. }
1019            | Self::ScrollBar { bounds, .. }
1020            | Self::LinearGradient { bounds, .. }
1021            | Self::RadialGradient { bounds, .. }
1022            | Self::ConicGradient { bounds, .. }
1023            | Self::BoxShadow { bounds, .. }
1024            | Self::VirtualView { bounds, .. }
1025            | Self::VirtualViewPlaceholder { bounds, .. }
1026            | Self::HitTestArea { bounds, .. }
1027            | Self::PushClip { bounds, .. }
1028            | Self::PushImageMaskClip { bounds, .. }
1029            | Self::PushScrollFrame { clip_bounds: bounds, .. }
1030            | Self::PushStackingContext { bounds, .. }
1031            | Self::PushReferenceFrame { bounds, .. }
1032            | Self::PushFilter { bounds, .. }
1033            | Self::PushBackdropFilter { bounds, .. }
1034            | Self::PushOpacity { bounds, .. } => Some(*bounds.inner()),
1035            Self::ScrollBarStyled { info, .. } => Some(*info.bounds.inner()),
1036            Self::PushTextShadow { .. } => None, // text shadow has no bounds, affects following text
1037            Self::PopClip
1038            | Self::PopImageMaskClip
1039            | Self::PopScrollFrame
1040            | Self::PopStackingContext
1041            | Self::PopReferenceFrame
1042            | Self::PopFilter
1043            | Self::PopBackdropFilter
1044            | Self::PopOpacity
1045            | Self::PopTextShadow => None,
1046        }
1047    }
1048}
1049
1050// Helper structs for the DisplayList
1051#[derive(Debug, Copy, Clone, Default)]
1052pub struct BorderRadius {
1053    pub top_left: f32,
1054    pub top_right: f32,
1055    pub bottom_left: f32,
1056    pub bottom_right: f32,
1057}
1058
1059impl BorderRadius {
1060    pub fn is_zero(&self) -> bool {
1061        self.top_left == 0.0
1062            && self.top_right == 0.0
1063            && self.bottom_left == 0.0
1064            && self.bottom_right == 0.0
1065    }
1066}
1067
1068// Dummy types for compilation
1069pub type LocalScrollId = u64;
1070/// Display list tag ID as (payload, type_marker) tuple.
1071/// The u16 field is used as a namespace marker:
1072/// - 0x0100 = DOM Node (regular interactive elements)
1073/// - 0x0200 = Scrollbar component
1074pub type DisplayListTagId = (u64, u16);
1075
1076/// Internal builder to accumulate display list items during generation.
1077#[derive(Debug, Default)]
1078struct DisplayListBuilder {
1079    items: Vec<DisplayListItem>,
1080    node_mapping: Vec<Option<NodeId>>,
1081    /// Current node being processed (set by generator)
1082    current_node: Option<NodeId>,
1083    /// Collected debug messages (transferred to ctx on finalize)
1084    debug_messages: Vec<LayoutDebugMessage>,
1085    /// Whether debug logging is enabled
1086    debug_enabled: bool,
1087    /// Y-positions where forced page breaks should occur
1088    forced_page_breaks: Vec<f32>,
1089    /// Index ranges of items from fixed-position elements (for paged media replication)
1090    fixed_position_item_ranges: Vec<(usize, usize)>,
1091    /// Start index of the current fixed-position element being built, if any
1092    fixed_position_start: Option<usize>,
1093}
1094
1095impl DisplayListBuilder {
1096    pub fn new() -> Self {
1097        Self::default()
1098    }
1099
1100    pub fn with_debug(debug_enabled: bool) -> Self {
1101        Self {
1102            items: Vec::new(),
1103            node_mapping: Vec::new(),
1104            current_node: None,
1105            debug_messages: Vec::new(),
1106            debug_enabled,
1107            forced_page_breaks: Vec::new(),
1108            fixed_position_item_ranges: Vec::new(),
1109            fixed_position_start: None,
1110        }
1111    }
1112
1113    /// Log a debug message if debug is enabled
1114    fn debug_log(&mut self, message: String) {
1115        if self.debug_enabled {
1116            self.debug_messages.push(LayoutDebugMessage::info(message));
1117        }
1118    }
1119
1120    /// Build the display list and transfer debug messages to the provided option
1121    pub fn build_with_debug(
1122        mut self,
1123        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1124    ) -> DisplayList {
1125        // Transfer collected debug messages to the context
1126        if let Some(msgs) = debug_messages.as_mut() {
1127            msgs.append(&mut self.debug_messages);
1128        }
1129        DisplayList {
1130            items: self.items,
1131            node_mapping: self.node_mapping,
1132            forced_page_breaks: self.forced_page_breaks,
1133            fixed_position_item_ranges: self.fixed_position_item_ranges,
1134        }
1135    }
1136
1137    /// Set the current node context for subsequent push operations
1138    pub fn set_current_node(&mut self, node_id: Option<NodeId>) {
1139        self.current_node = node_id;
1140    }
1141
1142    /// Mark the start of a fixed-position element's display items.
1143    pub fn begin_fixed_position_element(&mut self) {
1144        self.fixed_position_start = Some(self.items.len());
1145    }
1146
1147    /// Mark the end of a fixed-position element's display items.
1148    /// Records the (start, end) index range for paged media replication.
1149    pub fn end_fixed_position_element(&mut self) {
1150        if let Some(start) = self.fixed_position_start.take() {
1151            let end = self.items.len();
1152            if end > start {
1153                self.fixed_position_item_ranges.push((start, end));
1154            }
1155        }
1156    }
1157
1158    /// Register a forced page break at the given Y position.
1159    /// This is used for CSS break-before: always and break-after: always.
1160    pub fn add_forced_page_break(&mut self, y_position: f32) {
1161        // Avoid duplicates and keep sorted
1162        if !self.forced_page_breaks.contains(&y_position) {
1163            self.forced_page_breaks.push(y_position);
1164            self.forced_page_breaks.sort_by(|a, b| a.partial_cmp(b).unwrap());
1165        }
1166    }
1167
1168    /// Push an item and record its node mapping
1169    fn push_item(&mut self, item: DisplayListItem) {
1170        self.items.push(item);
1171        self.node_mapping.push(self.current_node);
1172    }
1173
1174    pub fn build(self) -> DisplayList {
1175        DisplayList {
1176            items: self.items,
1177            node_mapping: self.node_mapping,
1178            forced_page_breaks: self.forced_page_breaks,
1179            fixed_position_item_ranges: self.fixed_position_item_ranges,
1180        }
1181    }
1182
1183    pub fn push_hit_test_area(&mut self, bounds: LogicalRect, tag: DisplayListTagId) {
1184        self.push_item(DisplayListItem::HitTestArea { bounds: bounds.into(), tag });
1185    }
1186
1187    /// Push a simple single-color scrollbar (legacy method).
1188    pub fn push_scrollbar(
1189        &mut self,
1190        bounds: LogicalRect,
1191        color: ColorU,
1192        orientation: ScrollbarOrientation,
1193        opacity_key: Option<OpacityKey>,
1194        hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
1195    ) {
1196        if color.a > 0 || opacity_key.is_some() {
1197            // Optimization: Don't draw fully transparent items without opacity keys.
1198            self.push_item(DisplayListItem::ScrollBar {
1199                bounds: bounds.into(),
1200                color,
1201                orientation,
1202                opacity_key,
1203                hit_id,
1204            });
1205        }
1206    }
1207
1208    /// Push a fully styled scrollbar with track, thumb, and optional buttons.
1209    pub fn push_scrollbar_styled(&mut self, info: ScrollbarDrawInfo) {
1210        // Only push if at least the thumb or track is visible
1211        if info.thumb_color.a > 0 || info.track_color.a > 0 || info.opacity_key.is_some() {
1212            self.push_item(DisplayListItem::ScrollBarStyled {
1213                info: Box::new(info),
1214            });
1215        }
1216    }
1217
1218    pub fn push_rect(&mut self, bounds: LogicalRect, color: ColorU, border_radius: BorderRadius) {
1219        if color.a > 0 {
1220            // Optimization: Don't draw fully transparent items.
1221            self.push_item(DisplayListItem::Rect {
1222                bounds: bounds.into(),
1223                color,
1224                border_radius,
1225            });
1226        }
1227    }
1228
1229    /// Unified method to paint all background layers and border for an element.
1230    ///
1231    /// This consolidates the background/border painting logic that was previously
1232    /// duplicated across:
1233    /// - paint_node_background_and_border() for block elements
1234    /// - paint_inline_shape() for inline-block elements
1235    ///
1236    /// The backgrounds are painted in order (back to front per CSS spec), followed
1237    /// by the border.
1238    pub fn push_backgrounds_and_border(
1239        &mut self,
1240        bounds: LogicalRect,
1241        background_contents: &[azul_css::props::style::StyleBackgroundContent],
1242        border_info: &BorderInfo,
1243        simple_border_radius: BorderRadius,
1244        style_border_radius: StyleBorderRadius,
1245        image_cache: &azul_core::resources::ImageCache,
1246    ) {
1247        use azul_css::props::style::StyleBackgroundContent;
1248
1249        // Paint all background layers in order (CSS paints backgrounds back to front)
1250        for bg in background_contents {
1251            match bg {
1252                StyleBackgroundContent::Color(color) => {
1253                    self.push_rect(bounds, *color, simple_border_radius);
1254                }
1255                StyleBackgroundContent::LinearGradient(gradient) => {
1256                    self.push_linear_gradient(bounds, gradient.clone(), simple_border_radius);
1257                }
1258                StyleBackgroundContent::RadialGradient(gradient) => {
1259                    self.push_radial_gradient(bounds, gradient.clone(), simple_border_radius);
1260                }
1261                StyleBackgroundContent::ConicGradient(gradient) => {
1262                    self.push_conic_gradient(bounds, gradient.clone(), simple_border_radius);
1263                }
1264                StyleBackgroundContent::Image(image_id) => {
1265                    if let Some(image_ref) = image_cache.get_css_image_id(image_id) {
1266                        self.push_image(bounds, image_ref.clone(), simple_border_radius);
1267                    }
1268                }
1269            }
1270        }
1271
1272        // Paint border
1273        self.push_border(
1274            bounds,
1275            border_info.widths,
1276            border_info.colors,
1277            border_info.styles,
1278            style_border_radius,
1279        );
1280    }
1281
1282    /// Paint backgrounds and border for inline text elements.
1283    ///
1284    /// Similar to push_backgrounds_and_border but uses InlineBorderInfo which stores
1285    /// pre-resolved pixel values instead of CSS property values. This is used for
1286    /// inline (display: inline) elements where the border info is computed during
1287    /// text layout and stored in the glyph runs.
1288    pub fn push_inline_backgrounds_and_border(
1289        &mut self,
1290        bounds: LogicalRect,
1291        background_color: Option<ColorU>,
1292        background_contents: &[azul_css::props::style::StyleBackgroundContent],
1293        border: Option<&crate::text3::cache::InlineBorderInfo>,
1294        image_cache: &azul_core::resources::ImageCache,
1295    ) {
1296        use azul_css::props::style::StyleBackgroundContent;
1297
1298        // Paint solid background color if present
1299        if let Some(bg_color) = background_color {
1300            self.push_rect(bounds, bg_color, BorderRadius::default());
1301        }
1302
1303        // Paint all background layers in order (CSS paints backgrounds back to front)
1304        for bg in background_contents {
1305            match bg {
1306                StyleBackgroundContent::Color(color) => {
1307                    self.push_rect(bounds, *color, BorderRadius::default());
1308                }
1309                StyleBackgroundContent::LinearGradient(gradient) => {
1310                    self.push_linear_gradient(bounds, gradient.clone(), BorderRadius::default());
1311                }
1312                StyleBackgroundContent::RadialGradient(gradient) => {
1313                    self.push_radial_gradient(bounds, gradient.clone(), BorderRadius::default());
1314                }
1315                StyleBackgroundContent::ConicGradient(gradient) => {
1316                    self.push_conic_gradient(bounds, gradient.clone(), BorderRadius::default());
1317                }
1318                StyleBackgroundContent::Image(image_id) => {
1319                    if let Some(image_ref) = image_cache.get_css_image_id(image_id) {
1320                        self.push_image(bounds, image_ref.clone(), BorderRadius::default());
1321                    }
1322                }
1323            }
1324        }
1325
1326        // Paint border if present
1327        // CSS 2.2 §8.6: suppress left/right borders at split points, respecting direction
1328        if let Some(border) = border {
1329            let effective_left = if border.left_inset() > 0.0 { border.left } else { 0.0 };
1330            let effective_right = if border.right_inset() > 0.0 { border.right } else { 0.0 };
1331            if border.top > 0.0 || effective_right > 0.0 || border.bottom > 0.0 || effective_left > 0.0 {
1332                let border_widths = StyleBorderWidths {
1333                    top: Some(CssPropertyValue::Exact(LayoutBorderTopWidth {
1334                        inner: PixelValue::px(border.top),
1335                    })),
1336                    right: Some(CssPropertyValue::Exact(LayoutBorderRightWidth {
1337                        inner: PixelValue::px(effective_right),
1338                    })),
1339                    bottom: Some(CssPropertyValue::Exact(LayoutBorderBottomWidth {
1340                        inner: PixelValue::px(border.bottom),
1341                    })),
1342                    left: Some(CssPropertyValue::Exact(LayoutBorderLeftWidth {
1343                        inner: PixelValue::px(effective_left),
1344                    })),
1345                };
1346                let border_colors = StyleBorderColors {
1347                    top: Some(CssPropertyValue::Exact(StyleBorderTopColor {
1348                        inner: border.top_color,
1349                    })),
1350                    right: Some(CssPropertyValue::Exact(StyleBorderRightColor {
1351                        inner: border.right_color,
1352                    })),
1353                    bottom: Some(CssPropertyValue::Exact(StyleBorderBottomColor {
1354                        inner: border.bottom_color,
1355                    })),
1356                    left: Some(CssPropertyValue::Exact(StyleBorderLeftColor {
1357                        inner: border.left_color,
1358                    })),
1359                };
1360                let border_styles = StyleBorderStyles {
1361                    top: Some(CssPropertyValue::Exact(StyleBorderTopStyle {
1362                        inner: BorderStyle::Solid,
1363                    })),
1364                    right: Some(CssPropertyValue::Exact(StyleBorderRightStyle {
1365                        inner: BorderStyle::Solid,
1366                    })),
1367                    bottom: Some(CssPropertyValue::Exact(StyleBorderBottomStyle {
1368                        inner: BorderStyle::Solid,
1369                    })),
1370                    left: Some(CssPropertyValue::Exact(StyleBorderLeftStyle {
1371                        inner: BorderStyle::Solid,
1372                    })),
1373                };
1374                let radius_px = PixelValue::px(border.radius.unwrap_or(0.0));
1375                let border_radius = StyleBorderRadius {
1376                    top_left: radius_px,
1377                    top_right: radius_px,
1378                    bottom_left: radius_px,
1379                    bottom_right: radius_px,
1380                };
1381
1382                self.push_border(
1383                    bounds,
1384                    border_widths,
1385                    border_colors,
1386                    border_styles,
1387                    border_radius,
1388                );
1389            }
1390        }
1391    }
1392
1393    /// Push a linear gradient background
1394    pub fn push_linear_gradient(
1395        &mut self,
1396        bounds: LogicalRect,
1397        gradient: LinearGradient,
1398        border_radius: BorderRadius,
1399    ) {
1400        self.push_item(DisplayListItem::LinearGradient {
1401            bounds: bounds.into(),
1402            gradient,
1403            border_radius,
1404        });
1405    }
1406
1407    /// Push a radial gradient background
1408    pub fn push_radial_gradient(
1409        &mut self,
1410        bounds: LogicalRect,
1411        gradient: RadialGradient,
1412        border_radius: BorderRadius,
1413    ) {
1414        self.push_item(DisplayListItem::RadialGradient {
1415            bounds: bounds.into(),
1416            gradient,
1417            border_radius,
1418        });
1419    }
1420
1421    /// Push a conic gradient background
1422    pub fn push_conic_gradient(
1423        &mut self,
1424        bounds: LogicalRect,
1425        gradient: ConicGradient,
1426        border_radius: BorderRadius,
1427    ) {
1428        self.push_item(DisplayListItem::ConicGradient {
1429            bounds: bounds.into(),
1430            gradient,
1431            border_radius,
1432        });
1433    }
1434
1435    pub fn push_selection_rect(
1436        &mut self,
1437        bounds: LogicalRect,
1438        color: ColorU,
1439        border_radius: BorderRadius,
1440    ) {
1441        if color.a > 0 {
1442            self.push_item(DisplayListItem::SelectionRect {
1443                bounds: bounds.into(),
1444                color,
1445                border_radius,
1446            });
1447        }
1448    }
1449
1450    pub fn push_cursor_rect(&mut self, bounds: LogicalRect, color: ColorU) {
1451        if color.a > 0 {
1452            self.push_item(DisplayListItem::CursorRect { bounds: bounds.into(), color });
1453        }
1454    }
1455    pub fn push_clip(&mut self, bounds: LogicalRect, border_radius: BorderRadius) {
1456        self.push_item(DisplayListItem::PushClip {
1457            bounds: bounds.into(),
1458            border_radius,
1459        });
1460    }
1461    pub fn pop_clip(&mut self) {
1462        self.push_item(DisplayListItem::PopClip);
1463    }
1464    pub fn push_image_mask_clip(&mut self, bounds: LogicalRect, mask_image: ImageRef, mask_rect: LogicalRect) {
1465        self.push_item(DisplayListItem::PushImageMaskClip {
1466            bounds: bounds.into(),
1467            mask_image,
1468            mask_rect: mask_rect.into(),
1469        });
1470    }
1471    pub fn pop_image_mask_clip(&mut self) {
1472        self.push_item(DisplayListItem::PopImageMaskClip);
1473    }
1474    pub fn push_scroll_frame(
1475        &mut self,
1476        clip_bounds: LogicalRect,
1477        content_size: LogicalSize,
1478        scroll_id: LocalScrollId,
1479    ) {
1480        self.push_item(DisplayListItem::PushScrollFrame {
1481            clip_bounds: clip_bounds.into(),
1482            content_size,
1483            scroll_id,
1484        });
1485    }
1486    pub fn pop_scroll_frame(&mut self) {
1487        self.push_item(DisplayListItem::PopScrollFrame);
1488    }
1489    pub fn push_virtual_view_placeholder(
1490        &mut self,
1491        node_id: NodeId,
1492        bounds: LogicalRect,
1493        clip_rect: LogicalRect,
1494    ) {
1495        self.push_item(DisplayListItem::VirtualViewPlaceholder {
1496            node_id,
1497            bounds: bounds.into(),
1498            clip_rect: clip_rect.into(),
1499        });
1500    }
1501    pub fn push_border(
1502        &mut self,
1503        bounds: LogicalRect,
1504        widths: StyleBorderWidths,
1505        colors: StyleBorderColors,
1506        styles: StyleBorderStyles,
1507        border_radius: StyleBorderRadius,
1508    ) {
1509        // Check if any border side is visible
1510        let has_visible_border = {
1511            let has_width = widths.top.is_some()
1512                || widths.right.is_some()
1513                || widths.bottom.is_some()
1514                || widths.left.is_some();
1515            let has_style = styles.top.is_some()
1516                || styles.right.is_some()
1517                || styles.bottom.is_some()
1518                || styles.left.is_some();
1519            has_width && has_style
1520        };
1521
1522        if has_visible_border {
1523            self.push_item(DisplayListItem::Border {
1524                bounds: bounds.into(),
1525                widths,
1526                colors,
1527                styles,
1528                border_radius,
1529            });
1530        }
1531    }
1532
1533    pub fn push_stacking_context(&mut self, z_index: i32, bounds: LogicalRect) {
1534        self.push_item(DisplayListItem::PushStackingContext { z_index, bounds: bounds.into() });
1535    }
1536
1537    pub fn pop_stacking_context(&mut self) {
1538        self.push_item(DisplayListItem::PopStackingContext);
1539    }
1540
1541    pub fn push_reference_frame(
1542        &mut self,
1543        transform_key: TransformKey,
1544        initial_transform: ComputedTransform3D,
1545        bounds: LogicalRect,
1546    ) {
1547        self.push_item(DisplayListItem::PushReferenceFrame {
1548            transform_key,
1549            initial_transform,
1550            bounds: bounds.into(),
1551        });
1552    }
1553
1554    pub fn pop_reference_frame(&mut self) {
1555        self.push_item(DisplayListItem::PopReferenceFrame);
1556    }
1557
1558    pub fn push_text_run(
1559        &mut self,
1560        glyphs: Vec<GlyphInstance>,
1561        font_hash: FontHash, // Just the hash, not the full FontRef
1562        font_size_px: f32,
1563        color: ColorU,
1564        clip_rect: LogicalRect,
1565        source_node_index: Option<usize>,
1566    ) {
1567        self.debug_log(format!(
1568            "[push_text_run] {} glyphs, font_size={}px, color=({},{},{},{}), clip={:?}",
1569            glyphs.len(),
1570            font_size_px,
1571            color.r,
1572            color.g,
1573            color.b,
1574            color.a,
1575            clip_rect
1576        ));
1577
1578        if !glyphs.is_empty() && color.a > 0 {
1579            self.push_item(DisplayListItem::Text {
1580                glyphs,
1581                font_hash,
1582                font_size_px,
1583                color,
1584                clip_rect: clip_rect.into(),
1585                source_node_index,
1586            });
1587        } else {
1588            self.debug_log(format!(
1589                "[push_text_run] SKIPPED: glyphs.is_empty()={}, color.a={}",
1590                glyphs.is_empty(),
1591                color.a
1592            ));
1593        }
1594    }
1595
1596    pub fn push_text_layout(
1597        &mut self,
1598        layout: Arc<dyn std::any::Any + Send + Sync>,
1599        bounds: LogicalRect,
1600        font_hash: FontHash,
1601        font_size_px: f32,
1602        color: ColorU,
1603    ) {
1604        if color.a > 0 {
1605            self.push_item(DisplayListItem::TextLayout {
1606                layout,
1607                bounds: bounds.into(),
1608                font_hash,
1609                font_size_px,
1610                color,
1611            });
1612        }
1613    }
1614
1615    pub fn push_underline(&mut self, bounds: LogicalRect, color: ColorU, thickness: f32) {
1616        if color.a > 0 && thickness > 0.0 {
1617            self.push_item(DisplayListItem::Underline {
1618                bounds: bounds.into(),
1619                color,
1620                thickness,
1621            });
1622        }
1623    }
1624
1625    pub fn push_strikethrough(&mut self, bounds: LogicalRect, color: ColorU, thickness: f32) {
1626        if color.a > 0 && thickness > 0.0 {
1627            self.push_item(DisplayListItem::Strikethrough {
1628                bounds: bounds.into(),
1629                color,
1630                thickness,
1631            });
1632        }
1633    }
1634
1635    pub fn push_overline(&mut self, bounds: LogicalRect, color: ColorU, thickness: f32) {
1636        if color.a > 0 && thickness > 0.0 {
1637            self.push_item(DisplayListItem::Overline {
1638                bounds: bounds.into(),
1639                color,
1640                thickness,
1641            });
1642        }
1643    }
1644
1645    pub fn push_image(&mut self, bounds: LogicalRect, image: ImageRef, border_radius: BorderRadius) {
1646        self.push_item(DisplayListItem::Image { bounds: bounds.into(), image, border_radius });
1647    }
1648}
1649
1650/// Main entry point for generating the display list.
1651pub fn generate_display_list<T: ParsedFontTrait + Sync + 'static>(
1652    ctx: &mut LayoutContext<T>,
1653    tree: &LayoutTree,
1654    calculated_positions: &super::PositionVec,
1655    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
1656    scroll_ids: &HashMap<usize, u64>,
1657    gpu_value_cache: Option<&GpuValueCache>,
1658    renderer_resources: &RendererResources,
1659    id_namespace: IdNamespace,
1660    dom_id: DomId,
1661) -> Result<DisplayList> {
1662    debug_info!(
1663        ctx,
1664        "[DisplayList] generate_display_list: tree has {} nodes, {} positions calculated",
1665        tree.nodes.len(),
1666        calculated_positions.len()
1667    );
1668
1669    debug_info!(ctx, "Starting display list generation");
1670    debug_info!(
1671        ctx,
1672        "Collecting stacking contexts from root node {}",
1673        tree.root
1674    );
1675
1676    let positioned_tree = PositionedTree {
1677        tree,
1678        calculated_positions,
1679    };
1680    let mut generator = DisplayListGenerator::new(
1681        ctx,
1682        scroll_offsets,
1683        &positioned_tree,
1684        scroll_ids,
1685        gpu_value_cache,
1686        renderer_resources,
1687        id_namespace,
1688        dom_id,
1689    );
1690
1691    // Create builder with debug enabled if ctx has debug messages
1692    let debug_enabled = generator.ctx.debug_messages.is_some();
1693    let mut builder = DisplayListBuilder::with_debug(debug_enabled);
1694
1695    // 0. Canvas background propagation (CSS 2.1 § 14.2):
1696    //    "The background of the root element becomes the background of the canvas."
1697    //    If the root (html) has a transparent background, propagate from <body>.
1698    //    The canvas background fills the ENTIRE viewport, not just the root's content box.
1699    //    This is critical when <html> doesn't have height:100% — without this,
1700    //    the body's background only covers the body's content area, not the viewport.
1701    {
1702        let root_node = tree.get(tree.root);
1703        if let Some(root) = root_node {
1704            if let Some(root_dom_id) = root.dom_node_id {
1705                let root_state = generator.get_styled_node_state(root_dom_id);
1706                let canvas_bg = get_background_color(
1707                    generator.ctx.styled_dom,
1708                    root_dom_id,
1709                    &root_state,
1710                );
1711                if canvas_bg.a > 0 {
1712                    let viewport_rect = LogicalRect {
1713                        origin: LogicalPosition::zero(),
1714                        size: generator.ctx.viewport_size,
1715                    };
1716                    builder.push_rect(viewport_rect, canvas_bg, BorderRadius::default());
1717                    debug_info!(
1718                        generator.ctx,
1719                        "[DisplayList] Canvas background: color=({},{},{},{}), size={:?}",
1720                        canvas_bg.r, canvas_bg.g, canvas_bg.b, canvas_bg.a,
1721                        generator.ctx.viewport_size
1722                    );
1723                }
1724            }
1725        }
1726    }
1727
1728    // +spec:stacking-contexts:33d435 - CSS 2.2 painting order: build stacking context tree then traverse in z-order
1729    // +spec:stacking-contexts:887766 - CSS2 §9.9 stacking contexts, z-index layering, and painting order
1730    // 1. Build a tree of stacking contexts, which defines the global paint order.
1731    // +spec:display-property:9a419c - root element always forms a stacking context (it's the tree root)
1732    let stacking_context_tree = generator.collect_stacking_contexts(tree.root)?;
1733
1734    // 2. Traverse the stacking context tree to generate display items in the correct order.
1735    debug_info!(
1736        generator.ctx,
1737        "Generating display items from stacking context tree"
1738    );
1739    generator.generate_for_stacking_context(&mut builder, &stacking_context_tree)?;
1740
1741    // Build display list and transfer debug messages to context
1742    let display_list = builder.build_with_debug(generator.ctx.debug_messages);
1743    debug_info!(
1744        generator.ctx,
1745        "[DisplayList] Generated {} display items",
1746        display_list.items.len()
1747    );
1748    Ok(display_list)
1749}
1750
1751/// A helper struct that holds all necessary state and context for the generation process.
1752struct DisplayListGenerator<'a, 'b, T: ParsedFontTrait> {
1753    ctx: &'a mut LayoutContext<'b, T>,
1754    scroll_offsets: &'a BTreeMap<NodeId, ScrollPosition>,
1755    positioned_tree: &'a PositionedTree<'a>,
1756    scroll_ids: &'a HashMap<usize, u64>,
1757    gpu_value_cache: Option<&'a GpuValueCache>,
1758    renderer_resources: &'a RendererResources,
1759    id_namespace: IdNamespace,
1760    dom_id: DomId,
1761}
1762
1763// +spec:stacking-contexts:9e85a3 - Stacking context tree: hierarchical, nested, atomic painting order
1764/// Represents a node in the CSS stacking context tree, not the DOM tree.
1765#[derive(Debug)]
1766struct StackingContext {
1767    node_index: usize,
1768    z_index: i32,
1769    child_contexts: Vec<StackingContext>,
1770    /// Children that do not create their own stacking contexts and are painted in DOM order.
1771    in_flow_children: Vec<usize>,
1772}
1773
1774impl<'a, 'b, T> DisplayListGenerator<'a, 'b, T>
1775where
1776    T: ParsedFontTrait + Sync + 'static,
1777{
1778    pub fn new(
1779        ctx: &'a mut LayoutContext<'b, T>,
1780        scroll_offsets: &'a BTreeMap<NodeId, ScrollPosition>,
1781        positioned_tree: &'a PositionedTree<'a>,
1782        scroll_ids: &'a HashMap<usize, u64>,
1783        gpu_value_cache: Option<&'a GpuValueCache>,
1784        renderer_resources: &'a RendererResources,
1785        id_namespace: IdNamespace,
1786        dom_id: DomId,
1787    ) -> Self {
1788        Self {
1789            ctx,
1790            scroll_offsets,
1791            positioned_tree,
1792            scroll_ids,
1793            gpu_value_cache,
1794            renderer_resources,
1795            id_namespace,
1796            dom_id,
1797        }
1798    }
1799
1800    /// Helper to get styled node state for a node
1801    fn get_styled_node_state(&self, dom_id: NodeId) -> azul_core::styled_dom::StyledNodeState {
1802        self.ctx
1803            .styled_dom
1804            .styled_nodes
1805            .as_container()
1806            .get(dom_id)
1807            .map(|n| n.styled_node_state.clone())
1808            .unwrap_or_default()
1809    }
1810
1811    // +spec:overflow:visibility - CSS 2.2 §11.2: visibility:hidden makes the box invisible
1812    // but still affects layout. Checked per-node because visibility is inherited and a child
1813    // with visibility:visible inside a hidden parent must still be painted.
1814    fn is_node_hidden(&self, node_index: usize) -> bool {
1815        use azul_css::props::style::effects::StyleVisibility;
1816        let node = match self.positioned_tree.tree.get(node_index) {
1817            Some(n) => n,
1818            None => return false,
1819        };
1820        let dom_id = match node.dom_node_id {
1821            Some(id) => id,
1822            None => return false,
1823        };
1824        let node_state = self.get_styled_node_state(dom_id);
1825        match get_visibility(self.ctx.styled_dom, dom_id, &node_state) {
1826            crate::solver3::getters::MultiValue::Exact(StyleVisibility::Hidden)
1827            | crate::solver3::getters::MultiValue::Exact(StyleVisibility::Collapse) => true,
1828            _ => false,
1829        }
1830    }
1831
1832    /// Gets the cursor type for a text node from its CSS properties.
1833    /// Defaults to Text (I-beam) cursor if no explicit cursor is set.
1834    fn get_cursor_type_for_text_node(&self, node_id: NodeId) -> CursorType {
1835        use azul_css::props::style::effects::StyleCursor;
1836        
1837        let styled_node_state = self.get_styled_node_state(node_id);
1838        let node_data_container = self.ctx.styled_dom.node_data.as_container();
1839        let node_data = node_data_container.get(node_id);
1840        
1841        // Query the cursor CSS property for this text node
1842        if let Some(node_data) = node_data {
1843            if let Some(cursor_value) = self.ctx.styled_dom.get_css_property_cache().get_cursor(
1844                node_data,
1845                &node_id,
1846                &styled_node_state,
1847            ) {
1848                if let CssPropertyValue::Exact(cursor) = cursor_value {
1849                    return match cursor {
1850                        StyleCursor::Default => CursorType::Default,
1851                        StyleCursor::Pointer => CursorType::Pointer,
1852                        StyleCursor::Text => CursorType::Text,
1853                        StyleCursor::Crosshair => CursorType::Crosshair,
1854                        StyleCursor::Move => CursorType::Move,
1855                        StyleCursor::Help => CursorType::Help,
1856                        StyleCursor::Wait => CursorType::Wait,
1857                        StyleCursor::Progress => CursorType::Progress,
1858                        StyleCursor::NsResize => CursorType::NsResize,
1859                        StyleCursor::EwResize => CursorType::EwResize,
1860                        StyleCursor::NeswResize => CursorType::NeswResize,
1861                        StyleCursor::NwseResize => CursorType::NwseResize,
1862                        StyleCursor::NResize => CursorType::NResize,
1863                        StyleCursor::SResize => CursorType::SResize,
1864                        StyleCursor::EResize => CursorType::EResize,
1865                        StyleCursor::WResize => CursorType::WResize,
1866                        StyleCursor::Grab => CursorType::Grab,
1867                        StyleCursor::Grabbing => CursorType::Grabbing,
1868                        StyleCursor::RowResize => CursorType::RowResize,
1869                        StyleCursor::ColResize => CursorType::ColResize,
1870                        // Map less common cursors to closest available
1871                        StyleCursor::SeResize | StyleCursor::NeswResize => CursorType::NeswResize,
1872                        StyleCursor::ZoomIn | StyleCursor::ZoomOut => CursorType::Default,
1873                        StyleCursor::Copy | StyleCursor::Alias => CursorType::Default,
1874                        StyleCursor::Cell => CursorType::Crosshair,
1875                        StyleCursor::AllScroll => CursorType::Move,
1876                        StyleCursor::ContextMenu => CursorType::Default,
1877                        StyleCursor::VerticalText => CursorType::Text,
1878                        StyleCursor::Unset => CursorType::Text, // Default to text for text nodes
1879                    };
1880                }
1881            }
1882        }
1883        
1884        // Default: Text cursor (I-beam) for text nodes
1885        CursorType::Text
1886    }
1887
1888    /// Emits drawing commands for text selections only (not cursor).
1889    /// The cursor is drawn separately via `paint_cursor()`.
1890    fn paint_selections(
1891        &self,
1892        builder: &mut DisplayListBuilder,
1893        node_index: usize,
1894    ) -> Result<()> {
1895        let node = self
1896            .positioned_tree
1897            .tree
1898            .get(node_index)
1899            .ok_or(LayoutError::InvalidTree)?;
1900        let Some(dom_id) = node.dom_node_id else {
1901            return Ok(());
1902        };
1903        
1904        // Get inline layout using the unified helper that handles IFC membership
1905        // This is critical: text nodes don't have their own inline_layout_result,
1906        // but they have ifc_membership pointing to their IFC root
1907        let Some(layout) = self.positioned_tree.tree.get_inline_layout_for_node(node_index) else {
1908            return Ok(());
1909        };
1910
1911        // Get the absolute position of this node (border-box position)
1912        let node_pos = self
1913            .positioned_tree
1914            .calculated_positions
1915            .get(node_index)
1916            .copied()
1917            .unwrap_or_default();
1918
1919        // Selection rects are relative to content-box origin
1920        let bp = node.box_props.unpack();
1921        let padding = &bp.padding;
1922        let border = &bp.border;
1923        let content_box_offset_x = node_pos.x + padding.left + border.left;
1924        let content_box_offset_y = node_pos.y + padding.top + border.top;
1925
1926        // Check if text is selectable (respects CSS user-select property)
1927        let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
1928        let is_selectable = super::getters::is_text_selectable(self.ctx.styled_dom, dom_id, node_state);
1929
1930        if !is_selectable {
1931            return Ok(());
1932        }
1933
1934        // === NEW: Check text_selections first (multi-node selection support) ===
1935        if let Some(text_selection) = self.ctx.text_selections.get(&self.ctx.styled_dom.dom_id) {
1936            if let Some(range) = text_selection.affected_nodes.get(&dom_id) {
1937                let is_collapsed = text_selection.is_collapsed();
1938                
1939                // Only draw selection highlight if NOT collapsed
1940                if !is_collapsed {
1941                    let rects = layout.get_selection_rects(range);
1942                    let style = get_selection_style(self.ctx.styled_dom, Some(dom_id), self.ctx.system_style.as_ref());
1943
1944                    let border_radius = BorderRadius {
1945                        top_left: style.radius,
1946                        top_right: style.radius,
1947                        bottom_left: style.radius,
1948                        bottom_right: style.radius,
1949                    };
1950
1951                    for mut rect in rects {
1952                        rect.origin.x += content_box_offset_x;
1953                        rect.origin.y += content_box_offset_y;
1954                        builder.push_selection_rect(rect, style.bg_color, border_radius);
1955                    }
1956                }
1957                
1958                return Ok(());
1959            }
1960        }
1961
1962        Ok(())
1963    }
1964
1965    /// Emits drawing commands for all text cursors (carets).
1966    /// Iterates over `ctx.cursor_locations` to support multi-cursor rendering.
1967    /// Preedit underline is only rendered for the primary (last) cursor.
1968    fn paint_cursor(
1969        &self,
1970        builder: &mut DisplayListBuilder,
1971        node_index: usize,
1972    ) -> Result<()> {
1973        // Early exit if cursor is not visible (blinking off phase)
1974        if !self.ctx.cursor_is_visible {
1975            return Ok(());
1976        }
1977
1978        // Early exit if no cursor locations
1979        if self.ctx.cursor_locations.is_empty() {
1980            return Ok(());
1981        }
1982
1983        let node = self
1984            .positioned_tree
1985            .tree
1986            .get(node_index)
1987            .ok_or(LayoutError::InvalidTree)?;
1988        let Some(dom_id) = node.dom_node_id else {
1989            return Ok(());
1990        };
1991
1992        // Check if this node is contenteditable
1993        let is_contenteditable = super::getters::is_node_contenteditable_inherited(self.ctx.styled_dom, dom_id);
1994        if !is_contenteditable {
1995            return Ok(());
1996        }
1997
1998        // Check if text is selectable
1999        let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
2000        let is_selectable = super::getters::is_text_selectable(self.ctx.styled_dom, dom_id, node_state);
2001        if !is_selectable {
2002            return Ok(());
2003        }
2004
2005        // Get inline layout
2006        let Some(layout) = self.positioned_tree.tree.get_inline_layout_for_node(node_index) else {
2007            return Ok(());
2008        };
2009
2010        // Compute content-box offset once
2011        let node_pos = self
2012            .positioned_tree
2013            .calculated_positions
2014            .get(node_index)
2015            .copied()
2016            .unwrap_or_default();
2017        let bp = node.box_props.unpack();
2018        let padding = &bp.padding;
2019        let border = &bp.border;
2020        let content_box_offset_x = node_pos.x + padding.left + border.left;
2021        let content_box_offset_y = node_pos.y + padding.top + border.top;
2022
2023        let style = get_caret_style(self.ctx.styled_dom, Some(dom_id));
2024
2025        // Find the index of the last (primary) cursor that belongs to this DOM/node,
2026        // so preedit underline is only drawn on the actual primary cursor.
2027        let primary_idx_for_this_node = self.ctx.cursor_locations.iter().enumerate()
2028            .rev()
2029            .find(|(_, (cd, cn, _))| {
2030                *cd == self.ctx.styled_dom.dom_id && (*cn == dom_id || self.positioned_tree.tree.children(node_index).iter().any(|&child_idx| {
2031                    self.positioned_tree.tree.get(child_idx)
2032                        .and_then(|c| c.dom_node_id)
2033                        .map(|id| id == *cn)
2034                        .unwrap_or(false)
2035                }))
2036            })
2037            .map(|(i, _)| i);
2038
2039        for (i, (cursor_dom_id, cursor_node_id, cursor)) in self.ctx.cursor_locations.iter().enumerate() {
2040            // Check DOM ID matches
2041            if self.ctx.styled_dom.dom_id != *cursor_dom_id {
2042                continue;
2043            }
2044
2045            // Check this node contains the cursor
2046            if dom_id != *cursor_node_id {
2047                let is_ifc_root_of_cursor = self.positioned_tree.tree.children(node_index)
2048                    .iter()
2049                    .any(|&child_idx| {
2050                        self.positioned_tree.tree.get(child_idx)
2051                            .and_then(|c| c.dom_node_id)
2052                            .map(|id| id == *cursor_node_id)
2053                            .unwrap_or(false)
2054                    });
2055                if !is_ifc_root_of_cursor {
2056                    continue;
2057                }
2058            }
2059
2060            // Get cursor rect from text layout
2061            let Some(mut rect) = layout.get_cursor_rect(cursor) else {
2062                continue;
2063            };
2064
2065            rect.origin.x += content_box_offset_x;
2066            rect.origin.y += content_box_offset_y;
2067            rect.size.width = style.width;
2068
2069            builder.push_cursor_rect(rect, style.color);
2070
2071            // Preedit underline only on the primary cursor for this node
2072            let is_primary = primary_idx_for_this_node == Some(i);
2073            if is_primary {
2074                if let Some(ref preedit) = self.ctx.preedit_text {
2075                    if !preedit.is_empty() {
2076                        let char_count = preedit.chars().count() as f32;
2077                        let approx_char_width = style.width.max(8.0);
2078                        let preedit_width = char_count * approx_char_width;
2079                        let underline_bounds = azul_core::geom::LogicalRect {
2080                            origin: azul_core::geom::LogicalPosition {
2081                                x: rect.origin.x + rect.size.width,
2082                                y: rect.origin.y + rect.size.height - 2.0,
2083                            },
2084                            size: azul_core::geom::LogicalSize {
2085                                width: preedit_width,
2086                                height: 2.0,
2087                            },
2088                        };
2089                        builder.push_underline(underline_bounds, style.color, 2.0);
2090                    }
2091                }
2092            }
2093        }
2094
2095        Ok(())
2096    }
2097
2098    /// Emits drawing commands for selection and cursor.
2099    /// Delegates to `paint_selections()` and `paint_cursor()`.
2100    fn paint_selection_and_cursor(
2101        &self,
2102        builder: &mut DisplayListBuilder,
2103        node_index: usize,
2104    ) -> Result<()> {
2105        self.paint_selections(builder, node_index)?;
2106        self.paint_cursor(builder, node_index)?;
2107        Ok(())
2108    }
2109
2110    /// Recursively builds the tree of stacking contexts starting from a given layout node.
2111    // +spec:writing-modes:a86a28 - preorder depth-first traversal of the rendering tree in logical order
2112    fn collect_stacking_contexts(&mut self, node_index: usize) -> Result<StackingContext> {
2113        let node = self
2114            .positioned_tree
2115            .tree
2116            .get(node_index)
2117            .ok_or(LayoutError::InvalidTree)?;
2118        let z_index = get_z_index(self.ctx.styled_dom, node.dom_node_id);
2119
2120        if let Some(dom_id) = node.dom_node_id {
2121            let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
2122            debug_info!(
2123                self.ctx,
2124                "Collecting stacking context for node {} ({:?}), z-index={}",
2125                node_index,
2126                node_type.get_node_type(),
2127                z_index
2128            );
2129        }
2130
2131        let mut child_contexts = Vec::new();
2132        let mut in_flow_children = Vec::new();
2133
2134        for &child_index in self.positioned_tree.tree.children(node_index) {
2135            if self.establishes_stacking_context(child_index) {
2136                child_contexts.push(self.collect_stacking_contexts(child_index)?);
2137            } else {
2138                in_flow_children.push(child_index);
2139                // Recurse into non-stacking-context children to find nested
2140                // stacking contexts. Per CSS 2.2 Appendix E, these are promoted
2141                // to be child stacking contexts of the nearest ancestor SC.
2142                self.find_nested_stacking_contexts(child_index, &mut child_contexts)?;
2143            }
2144        }
2145
2146        Ok(StackingContext {
2147            node_index,
2148            z_index,
2149            child_contexts,
2150            in_flow_children,
2151        })
2152    }
2153
2154    /// Recursively searches non-stacking-context subtrees for nested stacking
2155    /// contexts, promoting them to the parent stacking context's child list.
2156    fn find_nested_stacking_contexts(
2157        &mut self,
2158        parent_index: usize,
2159        child_contexts: &mut Vec<StackingContext>,
2160    ) -> Result<()> {
2161        for &child_index in self.positioned_tree.tree.children(parent_index) {
2162            if self.establishes_stacking_context(child_index) {
2163                child_contexts.push(self.collect_stacking_contexts(child_index)?);
2164            } else {
2165                self.find_nested_stacking_contexts(child_index, child_contexts)?;
2166            }
2167        }
2168        Ok(())
2169    }
2170
2171    // +spec:box-model:de94ab - stacking context painting order (negative z, in-flow, z=0, positive z)
2172    // +spec:display-property:337069 - CSS 2.2 E.2 painting order: stacking contexts sorted by z-index, in-flow children in tree order
2173    // +spec:display-property:7b0a87 - CSS 2.2 E.2 painting order: negative z-index, in-flow, z-index 0/auto, positive z-index
2174    // +spec:stacking-contexts:5cbdfb - full CSS painting order (bg, neg-z, in-flow, z0, pos-z)
2175    // +spec:stacking-contexts:3ded3a - CSS 2.2 Appendix E painting order: definitions and tree order traversal
2176    // +spec:stacking-contexts:973368 - CSS 2.2 Appendix E.2 painting order: bg/border, negative z, in-flow, zero z, positive z
2177    // +spec:stacking-contexts:464bb7 - CSS 2.2 §9.9.1 painting order: negative z-index, in-flow, z-index 0, positive z-index (recursive)
2178    /// Recursively traverses the stacking context tree, emitting drawing commands to the builder
2179    /// according to the CSS Painting Algorithm specification.
2180    // +spec:display-property:39e879 - CSS 2.2 E.2 painting order for block-level and inline-level elements
2181    // +spec:display-property:de4c66 - CSS 2.2 E.2 stacking context paint order (canvas bg, negative z, in-flow, floats, inline, positive z)
2182    // +spec:overflow:6e48b4 - CSS 2.2 Appendix E painting order: bg/border, negative z-index, in-flow, floats, z-index 0/auto, positive z-index
2183    // +spec:stacking-contexts:55ca96 - CSS 2.2 E.2 painting order: backgrounds, negative z-index, in-flow, z-index 0/auto, positive z-index
2184    fn generate_for_stacking_context(
2185        &mut self,
2186        builder: &mut DisplayListBuilder,
2187        context: &StackingContext,
2188    ) -> Result<()> {
2189        // Before painting the node, check if it establishes a new clip or scroll frame.
2190        let node = self
2191            .positioned_tree
2192            .tree
2193            .get(context.node_index)
2194            .ok_or(LayoutError::InvalidTree)?;
2195
2196        if let Some(dom_id) = node.dom_node_id {
2197            let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
2198            debug_info!(
2199                self.ctx,
2200                "Painting stacking context for node {} ({:?}), z-index={}, {} child contexts, {} \
2201                 in-flow children",
2202                context.node_index,
2203                node_type.get_node_type(),
2204                context.z_index,
2205                context.child_contexts.len(),
2206                context.in_flow_children.len()
2207            );
2208        }
2209
2210        // Set current node BEFORE pushing stacking context so that
2211        // the PushStackingContext item gets the correct node_mapping entry.
2212        // This is critical for drag visual offset matching.
2213        builder.set_current_node(node.dom_node_id);
2214
2215        // Track fixed-position elements for paged media replication (CSS Positioned Layout §2.1)
2216        let is_fixed_position = node.dom_node_id
2217            .map(|dom_id| get_position_type(self.ctx.styled_dom, Some(dom_id)) == LayoutPosition::Fixed)
2218            .unwrap_or(false);
2219        if is_fixed_position {
2220            builder.begin_fixed_position_element();
2221        }
2222
2223        // Check if this node has a GPU-accelerated transform (CSS transform or drag).
2224        // If so, wrap in a reference frame so WebRender can animate it on the GPU.
2225        let has_reference_frame = node.dom_node_id.and_then(|dom_id| {
2226            self.gpu_value_cache.and_then(|cache| {
2227                let key = cache.css_transform_keys.get(&dom_id)?;
2228                let transform = cache.css_current_transform_values.get(&dom_id)?;
2229                Some((*key, *transform))
2230            })
2231        });
2232
2233        // Push a stacking context for WebRender
2234        // Get the node's bounds for the stacking context
2235        let node_pos = self
2236            .positioned_tree
2237            .calculated_positions
2238            .get(context.node_index)
2239            .copied()
2240            .unwrap_or_default();
2241        let node_size = node.used_size.unwrap_or(LogicalSize {
2242            width: 0.0,
2243            height: 0.0,
2244        });
2245        let node_bounds = LogicalRect {
2246            origin: node_pos,
2247            size: node_size,
2248        };
2249
2250        // Push reference frame BEFORE stacking context if node has a transform
2251        if let Some((transform_key, initial_transform)) = has_reference_frame {
2252            builder.push_reference_frame(transform_key, initial_transform, node_bounds);
2253        }
2254
2255        builder.push_stacking_context(context.z_index, node_bounds);
2256
2257        // Push opacity/filter effects if the node has them
2258        let mut pushed_opacity = false;
2259        let mut pushed_filter = false;
2260        let mut pushed_backdrop_filter = false;
2261
2262        if let Some(dom_id) = node.dom_node_id {
2263            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
2264            let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
2265
2266            // Opacity (GPU: fast path via compact cache)
2267            let opacity = crate::solver3::getters::get_opacity(
2268                self.ctx.styled_dom, dom_id, node_state,
2269            );
2270
2271            if opacity < 1.0 {
2272                builder.push_item(DisplayListItem::PushOpacity {
2273                    bounds: node_bounds.into(),
2274                    opacity,
2275                });
2276                pushed_opacity = true;
2277            }
2278
2279            // Filter
2280            if let Some(filter_vec_value) = self.ctx.styled_dom.css_property_cache.ptr
2281                .get_filter(node_data, &dom_id, node_state)
2282            {
2283                if let Some(filter_vec) = filter_vec_value.get_property() {
2284                    let filters: Vec<_> = filter_vec.as_ref().to_vec();
2285                    if !filters.is_empty() {
2286                        builder.push_item(DisplayListItem::PushFilter {
2287                            bounds: node_bounds.into(),
2288                            filters,
2289                        });
2290                        pushed_filter = true;
2291                    }
2292                }
2293            }
2294
2295            // Backdrop filter
2296            if let Some(backdrop_filter_value) = self.ctx.styled_dom.css_property_cache.ptr
2297                .get_backdrop_filter(node_data, &dom_id, node_state)
2298            {
2299                if let Some(filter_vec) = backdrop_filter_value.get_property() {
2300                    let filters: Vec<_> = filter_vec.as_ref().to_vec();
2301                    if !filters.is_empty() {
2302                        builder.push_item(DisplayListItem::PushBackdropFilter {
2303                            bounds: node_bounds.into(),
2304                            filters,
2305                        });
2306                        pushed_backdrop_filter = true;
2307                    }
2308                }
2309            }
2310        }
2311
2312        // 0b. Push image mask clip if this node has one.
2313        // This wraps background, border, and all children so the SVG mask clips everything.
2314        let did_push_image_mask = self.push_image_mask_clip(builder, context.node_index);
2315
2316        // +spec:box-model:84b238 - CSS 2.2 E.2 painting order: bg/border, negative z, in-flow, z=0, positive z
2317        // 1. Paint background and borders for the context's root element.
2318        // This must be BEFORE push_node_clips so the container background
2319        // is rendered in parent space (stationary), not scroll space.
2320        // +spec:overflow:40052b - backgrounds paint at border-box, scrollbars overlay on top (scrollbar-extended background positioning area)
2321        self.paint_node_background_and_border(builder, context.node_index)?;
2322
2323        // 1b. For scrollable containers, push the hit-test area BEFORE the scroll frame
2324        // so the hit-test covers the entire container box (including visible area),
2325        // not just the scrolled content. This ensures scroll wheel events hit the
2326        // container regardless of scroll position.
2327        // +spec:overflow:visibility - visibility:hidden scroll containers must not allow
2328        // interactive scrolling per CSS 2.2 §11.2
2329        if !self.is_node_hidden(context.node_index) {
2330            if let Some(dom_id) = node.dom_node_id {
2331                let styled_node_state = self.get_styled_node_state(dom_id);
2332                let overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
2333                let overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
2334                if overflow_x.is_scroll() || overflow_y.is_scroll() {
2335                    if let Some(tag_id) = get_tag_id(self.ctx.styled_dom, node.dom_node_id) {
2336                        builder.push_hit_test_area(node_bounds, tag_id);
2337                    }
2338                }
2339            }
2340        }
2341
2342        // 2. Push clips and scroll frames AFTER painting background
2343        // +spec:positioning:ddc554 - overflow clips apply to absolutely positioned descendants
2344        // when this node is their containing block (stacking contexts painted within clip scope)
2345        // TODO: CSS Overflow 3 says overflow clips should NOT apply to abs-pos descendants
2346        // whose containing block is above this clipper. Currently all descendants are clipped.
2347        // The containing_block_index field on LayoutNode is set for this purpose.
2348        let did_push_clip_or_scroll = self.push_node_clips(builder, context.node_index, node)?;
2349
2350        // +spec:display-contents:434de8 - E.2 painting order: negative z-index, in-flow, z-index 0/auto, positive z-index
2351        // 3. Paint child stacking contexts with negative z-indices.
2352        let mut negative_z_children: Vec<_> = context
2353            .child_contexts
2354            .iter()
2355            .filter(|c| c.z_index < 0)
2356            .collect();
2357        negative_z_children.sort_by_key(|c| c.z_index);
2358        for child in negative_z_children {
2359            self.generate_for_stacking_context(builder, child)?;
2360        }
2361
2362        // 4. Paint the in-flow descendants of the context root.
2363        self.paint_in_flow_descendants(builder, context.node_index, &context.in_flow_children)?;
2364
2365        // +spec:stacking-contexts:9a4eb3 - z-index:auto/0 positioned descendants painted in tree order
2366        // 5. Paint child stacking contexts with z-index: 0 / auto.
2367        for child in context.child_contexts.iter().filter(|c| c.z_index == 0) {
2368            self.generate_for_stacking_context(builder, child)?;
2369        }
2370
2371        // +spec:stacking-contexts:198fa4 - positive z-index stacking contexts painted in z-index order then tree order
2372        // 6. Paint child stacking contexts with positive z-indices.
2373        let mut positive_z_children: Vec<_> = context
2374            .child_contexts
2375            .iter()
2376            .filter(|c| c.z_index > 0)
2377            .collect();
2378
2379        positive_z_children.sort_by_key(|c| c.z_index);
2380
2381        for child in positive_z_children {
2382            self.generate_for_stacking_context(builder, child)?;
2383        }
2384
2385        // Pop image mask clip (before filter/opacity since it was pushed after them)
2386        if did_push_image_mask {
2387            builder.pop_image_mask_clip();
2388        }
2389
2390        // Pop filter/opacity effects (in reverse order of push)
2391        if pushed_backdrop_filter {
2392            builder.push_item(DisplayListItem::PopBackdropFilter);
2393        }
2394        if pushed_filter {
2395            builder.push_item(DisplayListItem::PopFilter);
2396        }
2397        if pushed_opacity {
2398            builder.push_item(DisplayListItem::PopOpacity);
2399        }
2400
2401        // Pop the stacking context for WebRender
2402        builder.pop_stacking_context();
2403
2404        // Pop reference frame if we pushed one
2405        if has_reference_frame.is_some() {
2406            builder.pop_reference_frame();
2407        }
2408
2409        // End fixed-position tracking (records the item range for paged media replication)
2410        if is_fixed_position {
2411            builder.end_fixed_position_element();
2412        }
2413
2414        // After painting the node and all its descendants, pop any contexts it pushed.
2415        // For VirtualView nodes, emit the placeholder INSIDE the clip (before PopClip)
2416        // so the virtualized view viewport is clipped to the container.
2417        if did_push_clip_or_scroll {
2418            // Emit VirtualViewPlaceholder before popping the clip so it's inside PushClip/PopClip
2419            if let Some(dom_id) = node.dom_node_id {
2420                if self.is_virtual_view_node(dom_id) {
2421                    builder.push_virtual_view_placeholder(dom_id, node_bounds, node_bounds);
2422                }
2423            }
2424            self.pop_node_clips(builder, node)?;
2425        } else {
2426            // Even without clips, emit VirtualViewPlaceholder for VirtualView nodes
2427            if let Some(dom_id) = node.dom_node_id {
2428                if self.is_virtual_view_node(dom_id) {
2429                    builder.push_virtual_view_placeholder(dom_id, node_bounds, node_bounds);
2430                }
2431            }
2432        }
2433
2434        // Paint scrollbars AFTER popping the clip, so they appear on top of content
2435        // and are not clipped by the scroll frame
2436        self.paint_scrollbars(builder, context.node_index)?;
2437
2438        Ok(())
2439    }
2440
2441    /// Paints the content and non-stacking-context children.
2442    fn paint_in_flow_descendants(
2443        &mut self,
2444        builder: &mut DisplayListBuilder,
2445        node_index: usize,
2446        children_indices: &[usize],
2447    ) -> Result<()> {
2448        // NOTE: We do NOT paint the node's background here - that was already done by
2449        // generate_for_stacking_context! Only paint selection, cursor, and content for the
2450        // current node
2451
2452        // 2. Paint selection highlights and the text cursor if applicable.
2453        self.paint_selection_and_cursor(builder, node_index)?;
2454
2455        // 3. Paint the node's own content (text, images, hit-test areas).
2456        self.paint_node_content(builder, node_index)?;
2457
2458        // +spec:display-property:86a3de - inline-level boxes painted in document order; z-index does not apply
2459        // +spec:floats:b8c494 - E.2 painting order: non-positioned floats painted after block-level descendants, in tree order
2460        // 4. Recursively paint the in-flow children in correct CSS painting order:
2461        //    - First: Non-float, non-dragging block-level children
2462        //    - Then: Float, non-dragging children (so they appear on top)
2463        //    - Finally: Dragging children (so they appear on top of everything per W3C spec)
2464
2465        // Separate children into floats, non-floats, and dragging.
2466        // Skip children that establish stacking contexts - those are painted
2467        // separately via generate_for_stacking_context with proper z-ordering.
2468        let mut non_float_children = Vec::new();
2469        let mut float_children = Vec::new();
2470        let mut dragging_children = Vec::new();
2471
2472        for &child_index in children_indices {
2473            // Skip stacking context children - they're painted by the stacking
2474            // context tree traversal, not by the in-flow descendant path.
2475            if self.establishes_stacking_context(child_index) {
2476                continue;
2477            }
2478            let child_node = self
2479                .positioned_tree
2480                .tree
2481                .get(child_index)
2482                .ok_or(LayoutError::InvalidTree)?;
2483
2484            // Check if this child is being dragged (paint last for z-order)
2485            let is_dragging = if let Some(dom_id) = child_node.dom_node_id {
2486                let styled_node_state = self.get_styled_node_state(dom_id);
2487                styled_node_state.dragging
2488            } else {
2489                false
2490            };
2491
2492            if is_dragging {
2493                dragging_children.push(child_index);
2494                continue;
2495            }
2496
2497            // Check if this child is a float
2498            let is_float = if let Some(dom_id) = child_node.dom_node_id {
2499                use crate::solver3::getters::get_float;
2500                let styled_node_state = self.get_styled_node_state(dom_id);
2501                let float_value = get_float(self.ctx.styled_dom, dom_id, &styled_node_state);
2502                !matches!(
2503                    float_value.unwrap_or_default(),
2504                    azul_css::props::layout::LayoutFloat::None
2505                )
2506            } else {
2507                false
2508            };
2509
2510            if is_float {
2511                float_children.push(child_index);
2512            } else {
2513                non_float_children.push(child_index);
2514            }
2515        }
2516
2517        // Paint non-float children first
2518        for child_index in non_float_children {
2519            let child_node = self
2520                .positioned_tree
2521                .tree
2522                .get(child_index)
2523                .ok_or(LayoutError::InvalidTree)?;
2524
2525            // Check if this child has a GPU transform (CSS transform or drag)
2526            let child_ref_frame = child_node.dom_node_id.and_then(|dom_id| {
2527                self.gpu_value_cache.and_then(|cache| {
2528                    let key = cache.css_transform_keys.get(&dom_id)?;
2529                    let transform = cache.css_current_transform_values.get(&dom_id)?;
2530                    Some((*key, *transform))
2531                })
2532            });
2533
2534            // Push reference frame if child has a transform
2535            if let Some((transform_key, initial_transform)) = child_ref_frame {
2536                let child_pos = self
2537                    .positioned_tree
2538                    .calculated_positions
2539            .get(child_index)
2540                    .copied()
2541                    .unwrap_or_default();
2542                let child_size = child_node.used_size.unwrap_or(LogicalSize {
2543                    width: 0.0,
2544                    height: 0.0,
2545                });
2546                let child_bounds = LogicalRect {
2547                    origin: child_pos,
2548                    size: child_size,
2549                };
2550                builder.set_current_node(child_node.dom_node_id);
2551                builder.push_reference_frame(transform_key, initial_transform, child_bounds);
2552            }
2553
2554            // Push image mask clip if this child has one (wraps background + children)
2555            let did_push_child_image_mask = self.push_image_mask_clip(builder, child_index);
2556
2557            // IMPORTANT: Paint background and border BEFORE pushing clips!
2558            // This ensures the container's background is in parent space (stationary),
2559            // not in scroll space. Same logic as generate_for_stacking_context.
2560            self.paint_node_background_and_border(builder, child_index)?;
2561
2562            // Push clips and scroll frames AFTER painting background
2563            let did_push_clip = self.push_node_clips(builder, child_index, child_node)?;
2564
2565            // Paint descendants inside the clip/scroll frame
2566            self.paint_in_flow_descendants(builder, child_index, self.positioned_tree.tree.children(child_index))?;
2567
2568            // For VirtualView children: emit placeholder INSIDE the clip
2569            if let Some(dom_id) = child_node.dom_node_id {
2570                if self.is_virtual_view_node(dom_id) {
2571                    let child_bounds = self.get_paint_rect(child_index).unwrap_or_default();
2572                    builder.push_virtual_view_placeholder(dom_id, child_bounds, child_bounds);
2573                }
2574            }
2575
2576            // Pop the child's clips.
2577            if did_push_clip {
2578                self.pop_node_clips(builder, child_node)?;
2579            }
2580
2581            // Pop image mask clip
2582            if did_push_child_image_mask {
2583                builder.pop_image_mask_clip();
2584            }
2585
2586            // Paint scrollbars AFTER popping clips so they appear on top of content
2587            self.paint_scrollbars(builder, child_index)?;
2588
2589            // Pop reference frame if we pushed one
2590            if child_ref_frame.is_some() {
2591                builder.pop_reference_frame();
2592            }
2593        }
2594
2595        // +spec:positioning:1bcbb5 - floats rendered in front of non-positioned in-flow blocks, but behind in-flow inlines
2596        // Paint float children AFTER non-floats (so they appear on top)
2597        for child_index in float_children {
2598            let child_node = self
2599                .positioned_tree
2600                .tree
2601                .get(child_index)
2602                .ok_or(LayoutError::InvalidTree)?;
2603
2604            // Check if this child has a GPU transform (CSS transform or drag)
2605            let child_ref_frame = child_node.dom_node_id.and_then(|dom_id| {
2606                self.gpu_value_cache.and_then(|cache| {
2607                    let key = cache.css_transform_keys.get(&dom_id)?;
2608                    let transform = cache.css_current_transform_values.get(&dom_id)?;
2609                    Some((*key, *transform))
2610                })
2611            });
2612
2613            // Push reference frame if child has a transform
2614            if let Some((transform_key, initial_transform)) = child_ref_frame {
2615                let child_pos = self
2616                    .positioned_tree
2617                    .calculated_positions
2618            .get(child_index)
2619                    .copied()
2620                    .unwrap_or_default();
2621                let child_size = child_node.used_size.unwrap_or(LogicalSize {
2622                    width: 0.0,
2623                    height: 0.0,
2624                });
2625                let child_bounds = LogicalRect {
2626                    origin: child_pos,
2627                    size: child_size,
2628                };
2629                builder.set_current_node(child_node.dom_node_id);
2630                builder.push_reference_frame(transform_key, initial_transform, child_bounds);
2631            }
2632
2633            // Same as above: push image mask, paint background, then clips
2634            let did_push_child_image_mask = self.push_image_mask_clip(builder, child_index);
2635            self.paint_node_background_and_border(builder, child_index)?;
2636            let did_push_clip = self.push_node_clips(builder, child_index, child_node)?;
2637            self.paint_in_flow_descendants(builder, child_index, self.positioned_tree.tree.children(child_index))?;
2638
2639            // For VirtualView children: emit placeholder INSIDE the clip
2640            if let Some(dom_id) = child_node.dom_node_id {
2641                if self.is_virtual_view_node(dom_id) {
2642                    let child_bounds = self.get_paint_rect(child_index).unwrap_or_default();
2643                    builder.push_virtual_view_placeholder(dom_id, child_bounds, child_bounds);
2644                }
2645            }
2646
2647            if did_push_clip {
2648                self.pop_node_clips(builder, child_node)?;
2649            }
2650            if did_push_child_image_mask {
2651                builder.pop_image_mask_clip();
2652            }
2653
2654            // Paint scrollbars AFTER popping clips so they appear on top of content
2655            self.paint_scrollbars(builder, child_index)?;
2656
2657            // Pop reference frame if we pushed one
2658            if child_ref_frame.is_some() {
2659                builder.pop_reference_frame();
2660            }
2661        }
2662
2663        // Paint dragging children LAST so they appear on top of everything (W3C spec)
2664        for child_index in dragging_children {
2665            let child_node = self
2666                .positioned_tree
2667                .tree
2668                .get(child_index)
2669                .ok_or(LayoutError::InvalidTree)?;
2670
2671            // Check if this child has a GPU transform (CSS transform or drag)
2672            let child_ref_frame = child_node.dom_node_id.and_then(|dom_id| {
2673                self.gpu_value_cache.and_then(|cache| {
2674                    let key = cache.css_transform_keys.get(&dom_id)?;
2675                    let transform = cache.css_current_transform_values.get(&dom_id)?;
2676                    Some((*key, *transform))
2677                })
2678            });
2679
2680            // Push reference frame if child has a transform
2681            if let Some((transform_key, initial_transform)) = child_ref_frame {
2682                let child_pos = self
2683                    .positioned_tree
2684                    .calculated_positions
2685            .get(child_index)
2686                    .copied()
2687                    .unwrap_or_default();
2688                let child_size = child_node.used_size.unwrap_or(LogicalSize {
2689                    width: 0.0,
2690                    height: 0.0,
2691                });
2692                let child_bounds = LogicalRect {
2693                    origin: child_pos,
2694                    size: child_size,
2695                };
2696                builder.set_current_node(child_node.dom_node_id);
2697                builder.push_reference_frame(transform_key, initial_transform, child_bounds);
2698            }
2699
2700            // Same as above: push image mask, paint background, then clips
2701            let did_push_child_image_mask = self.push_image_mask_clip(builder, child_index);
2702            self.paint_node_background_and_border(builder, child_index)?;
2703            let did_push_clip = self.push_node_clips(builder, child_index, child_node)?;
2704            self.paint_in_flow_descendants(builder, child_index, self.positioned_tree.tree.children(child_index))?;
2705
2706            // For VirtualView children: emit placeholder INSIDE the clip
2707            if let Some(dom_id) = child_node.dom_node_id {
2708                if self.is_virtual_view_node(dom_id) {
2709                    let child_bounds = self.get_paint_rect(child_index).unwrap_or_default();
2710                    builder.push_virtual_view_placeholder(dom_id, child_bounds, child_bounds);
2711                }
2712            }
2713
2714            if did_push_clip {
2715                self.pop_node_clips(builder, child_node)?;
2716            }
2717            if did_push_child_image_mask {
2718                builder.pop_image_mask_clip();
2719            }
2720
2721            // Paint scrollbars AFTER popping clips so they appear on top of content
2722            self.paint_scrollbars(builder, child_index)?;
2723
2724            // Pop reference frame if we pushed one
2725            if child_ref_frame.is_some() {
2726                builder.pop_reference_frame();
2727            }
2728        }
2729
2730        Ok(())
2731    }
2732
2733    /// Returns true if the given DOM node is a VirtualView node.
2734    fn is_virtual_view_node(&self, dom_id: NodeId) -> bool {
2735        let node_data_container = self.ctx.styled_dom.node_data.as_container();
2736        node_data_container
2737            .get(dom_id)
2738            .map(|nd| matches!(nd.get_node_type(), NodeType::VirtualView))
2739            .unwrap_or(false)
2740    }
2741
2742    /// Checks if a node has an image mask clip and pushes PushImageMaskClip if so.
2743    /// Returns true if a clip was pushed (caller must pop it).
2744    fn push_image_mask_clip(
2745        &self,
2746        builder: &mut DisplayListBuilder,
2747        node_index: usize,
2748    ) -> bool {
2749        let node = match self.positioned_tree.tree.get(node_index) {
2750            Some(n) => n,
2751            None => return false,
2752        };
2753        let dom_id = match node.dom_node_id {
2754            Some(id) => id,
2755            None => return false,
2756        };
2757        let node_data_container = self.ctx.styled_dom.node_data.as_container();
2758        let node_data = match node_data_container.get(dom_id) {
2759            Some(nd) => nd,
2760            None => return false,
2761        };
2762        match node_data.get_svg_data() {
2763            Some(azul_core::dom::SvgNodeData::ImageClipMask(clip_mask)) => {
2764                let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
2765                // Convert mask rect from element-local to window-logical coordinates
2766                let mask_rect = LogicalRect {
2767                    origin: LogicalPosition {
2768                        x: paint_rect.origin.x + clip_mask.rect.origin.x,
2769                        y: paint_rect.origin.y + clip_mask.rect.origin.y,
2770                    },
2771                    size: clip_mask.rect.size,
2772                };
2773                builder.push_image_mask_clip(
2774                    paint_rect,
2775                    clip_mask.image.clone(),
2776                    mask_rect,
2777                );
2778                true
2779            }
2780            #[cfg(feature = "cpurender")]
2781            Some(azul_core::dom::SvgNodeData::Path(svg_clip)) => {
2782                let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
2783                if let Some(mask_image) = rasterize_svg_clip_to_r8(svg_clip, &paint_rect) {
2784                    builder.push_image_mask_clip(paint_rect, mask_image, paint_rect);
2785                    true
2786                } else {
2787                    false
2788                }
2789            }
2790            #[cfg(not(feature = "cpurender"))]
2791            Some(azul_core::dom::SvgNodeData::Path(_)) => false,
2792            // Other SvgNodeData variants (shapes, gradients, etc.) don't produce clip masks
2793            Some(_) => false,
2794            None => false,
2795        }
2796    }
2797
2798    // +spec:overflow:531bd2 - ancestor clips accumulate via push_clip/pop_clip stack (cumulative intersection)
2799    // +spec:overflow:8098ec - overflow clipping/scrolling; abs-pos elements with containing block outside scroller are not scrolled
2800    /// Checks if a node requires clipping or scrolling and pushes the appropriate commands.
2801    /// Returns true if any command was pushed.
2802    ///
2803    /// // +spec:containing-block:62aa5c - overflow clipping applies to all content except
2804    /// // descendants whose containing block is the viewport or an ancestor of this element
2805    /// // (i.e. absolutely positioned elements that escape the overflow container).
2806    /// // TODO: exempt abs-pos descendants whose containing block is an ancestor of this node.
2807    ///
2808    /// For VirtualView nodes with `overflow: scroll/auto`, we intentionally skip
2809    /// `PushScrollFrame` / `PopScrollFrame`. VirtualView scroll state is managed by
2810    /// `ScrollManager`, not WebRender's APZ. Instead we emit only a `PushClip`
2811    /// and later an `VirtualViewPlaceholder` (see `generate_for_stacking_context`).
2812    fn push_node_clips(
2813        &self,
2814        builder: &mut DisplayListBuilder,
2815        node_index: usize,
2816        node: &LayoutNodeHot,
2817    ) -> Result<bool> {
2818        let Some(dom_id) = node.dom_node_id else {
2819            return Ok(false);
2820        };
2821
2822        let styled_node_state = self.get_styled_node_state(dom_id);
2823
2824        let raw_overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
2825        let raw_overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
2826        // +spec:overflow:833078 - resolve visible/clip to auto/hidden per CSS Overflow 3 §3.1
2827        let overflow_x = raw_overflow_x.resolve_computed(&raw_overflow_y);
2828        let overflow_y = raw_overflow_y.resolve_computed(&raw_overflow_x);
2829
2830        let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
2831        let element_size = PhysicalSizeImport {
2832            width: paint_rect.size.width,
2833            height: paint_rect.size.height,
2834        };
2835        let border_radius = get_border_radius(
2836            self.ctx.styled_dom,
2837            dom_id,
2838            &styled_node_state,
2839            element_size,
2840            self.ctx.viewport_size,
2841        );
2842
2843        // +spec:positioning:9c261b - clip-path (modern replacement for legacy 'clip' property)
2844        // The legacy CSS 2.2 'clip' property applied only to absolutely positioned elements;
2845        // clip-path supersedes it and applies to all elements per CSS Masking Level 1.
2846        // If present, push a clip region derived from the clip-path shape.
2847        // This is evaluated before overflow clips; both can be active simultaneously.
2848        let has_clip_path = if let Some(clip_path) = super::getters::get_clip_path(
2849            self.ctx.styled_dom, dom_id, &styled_node_state,
2850        ) {
2851            if let Some((clip_rect, radius)) = resolve_clip_path(&clip_path, paint_rect) {
2852                let br = if radius > 0.0 {
2853                    BorderRadius {
2854                        top_left: radius,
2855                        top_right: radius,
2856                        bottom_left: radius,
2857                        bottom_right: radius,
2858                    }
2859                } else {
2860                    BorderRadius::default()
2861                };
2862                builder.push_clip(clip_rect, br);
2863                true
2864            } else {
2865                false
2866            }
2867        } else {
2868            false
2869        };
2870
2871        // +spec:overflow:6890f2 - text-overflow: clip inline content at end line box edge when overflow != visible
2872        // +spec:overflow:77d7ce - clipping region defines visible portion of border box; default is not clipped
2873        let needs_clip = overflow_x.is_clipped() || overflow_y.is_clipped();
2874
2875        if !needs_clip {
2876            return Ok(has_clip_path);
2877        }
2878
2879        // +spec:overflow:c52f2a - clipping region is rounded to element's border-radius
2880        // +spec:overflow:913b23 - when both axes are clip, region is rounded per overflow-clip-margin
2881        // +spec:overflow:449d69 - when one axis is clip and the other is visible, clipping region is not rounded
2882        // +spec:overflow:449d69 - when one axis is clip and the other is visible, clipping region is not rounded
2883        let ox_clip = overflow_x.is_clipped() && !overflow_x.is_scroll() && !overflow_x.is_auto_overflow();
2884        let oy_clip = overflow_y.is_clipped() && !overflow_y.is_scroll() && !overflow_y.is_auto_overflow();
2885        let ox_visible = !overflow_x.is_clipped();
2886        let oy_visible = !overflow_y.is_clipped();
2887        let border_radius = if (ox_clip && oy_visible) || (oy_clip && ox_visible)
2888        {
2889            BorderRadius::default()
2890        } else {
2891            border_radius
2892        };
2893
2894        let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
2895
2896        let bp = node.box_props.unpack();
2897        let border = &bp.border;
2898
2899        // Get scrollbar info to adjust clip rect for content area
2900        let scrollbar_info = self.positioned_tree.tree.warm(node_index)
2901            .and_then(|w| w.scrollbar_info.clone())
2902            .unwrap_or_default();
2903
2904        // +spec:overflow:13cacb - clip rect clamped to 0 so zero-size clips hide all pixels
2905        // +spec:overflow:9207bc - clip rect computed from border-box edges (analogous to CSS 2.2 clip: rect() offsets)
2906        // +spec:overflow:3d5b53 - overflow clips to padding edge, scroll mechanism for scroll/auto
2907        // The clip rect for content should exclude the scrollbar area
2908        // Scrollbars are drawn inside the border-box, on the right/bottom edges
2909        // +spec:overflow:a825a6 - TODO: abs-pos elements with containing block outside this
2910        // element should not be clipped (currently all DOM children are clipped)
2911        let mut clip_rect = LogicalRect {
2912            origin: LogicalPosition {
2913                x: paint_rect.origin.x + border.left,
2914                y: paint_rect.origin.y + border.top,
2915            },
2916            size: LogicalSize {
2917                // Reduce width/height by scrollbar dimensions so content doesn't overlap scrollbar
2918                width: (paint_rect.size.width
2919                    - border.left
2920                    - border.right
2921                    - scrollbar_info.scrollbar_width)
2922                    .max(0.0),
2923                height: (paint_rect.size.height
2924                    - border.top
2925                    - border.bottom
2926                    - scrollbar_info.scrollbar_height)
2927                    .max(0.0),
2928            },
2929        };
2930
2931        // +spec:overflow:342f47 - overflow-clip-margin expands clip edge for overflow:clip only
2932        // Per CSS Overflow 3 §3.2: overflow-clip-margin has no effect on overflow:hidden
2933        // or overflow:scroll. It only expands the overflow clip edge when overflow:clip is used.
2934        apply_overflow_clip_margin(
2935            &mut clip_rect,
2936            &overflow_x,
2937            &overflow_y,
2938            self.ctx.styled_dom,
2939            dom_id,
2940            &styled_node_state,
2941        );
2942
2943        let is_virtual_view = self.is_virtual_view_node(dom_id);
2944
2945        // +spec:overflow:484889 - clip content in unreachable scrollable overflow region
2946        // +spec:overflow:917dae - scrollable overflow rect is a rectangle in box's own coordinate system
2947        if overflow_x.is_scroll() || overflow_y.is_scroll() {
2948            if is_virtual_view {
2949                // VirtualView nodes: only push a clip, NO scroll frame.
2950                // Scroll state is managed by ScrollManager and passed to the
2951                // VirtualView callback as scroll_offset. The VirtualViewPlaceholder is
2952                // emitted after pop_node_clips in generate_for_stacking_context.
2953                builder.push_clip(clip_rect, border_radius);
2954            } else {
2955                // Regular scrollable nodes: push clip AND scroll frame.
2956                // WebRender's APZ manages the scroll offset via define_scroll_frame.
2957                // CPU renderers use scroll_offset to translate children.
2958                builder.push_clip(clip_rect, border_radius);
2959                let scroll_id = self.scroll_ids.get(&node_index).copied().unwrap_or(0);
2960                let content_size = get_scroll_content_size(node, self.positioned_tree.tree.warm(node_index));
2961                builder.push_scroll_frame(clip_rect, content_size, scroll_id);
2962            }
2963        } else {
2964            // Simple clip for hidden/clip
2965            builder.push_clip(clip_rect, border_radius);
2966        }
2967
2968        Ok(true)
2969    }
2970
2971    /// Pops any clip/scroll commands associated with a node.
2972    fn pop_node_clips(&self, builder: &mut DisplayListBuilder, node: &LayoutNodeHot) -> Result<()> {
2973        let Some(dom_id) = node.dom_node_id else {
2974            return Ok(());
2975        };
2976
2977        let styled_node_state = self.get_styled_node_state(dom_id);
2978        let overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
2979        let overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
2980
2981        let paint_rect = self
2982            .get_paint_rect(
2983                self.positioned_tree
2984                    .tree
2985                    .nodes
2986                    .iter()
2987                    .position(|n| n.dom_node_id == Some(dom_id))
2988                    .unwrap_or(0),
2989            )
2990            .unwrap_or_default();
2991
2992        let element_size = PhysicalSizeImport {
2993            width: paint_rect.size.width,
2994            height: paint_rect.size.height,
2995        };
2996        let border_radius = get_border_radius(
2997            self.ctx.styled_dom,
2998            dom_id,
2999            &styled_node_state,
3000            element_size,
3001            self.ctx.viewport_size,
3002        );
3003
3004        let needs_clip =
3005            overflow_x.is_clipped() || overflow_y.is_clipped();
3006
3007        let is_virtual_view = self.is_virtual_view_node(dom_id);
3008
3009        if needs_clip {
3010            if (overflow_x.is_scroll() || overflow_y.is_scroll()) && !is_virtual_view {
3011                // For regular (non-VirtualView) scroll/auto, pop both scroll frame AND clip
3012                builder.pop_scroll_frame();
3013                builder.pop_clip();
3014            } else {
3015                // For hidden/clip, or VirtualView scroll (which only pushed a clip)
3016                builder.pop_clip();
3017            }
3018        }
3019
3020        // Pop the clip-path clip if one was pushed.
3021        // This mirrors the push_node_clips logic: if clip-path is set,
3022        // a PushClip was emitted before any overflow clips.
3023        // We pop it last (stack order: clip-path pushed first, popped last).
3024        if let Some(clip_path) = super::getters::get_clip_path(
3025            self.ctx.styled_dom, dom_id, &styled_node_state,
3026        ) {
3027            if resolve_clip_path(&clip_path, paint_rect).is_some() {
3028                builder.pop_clip();
3029            }
3030        }
3031
3032        Ok(())
3033    }
3034
3035    /// Calculates the final paint-time rectangle for a node.
3036    /// 
3037    /// ## Coordinate Space
3038    /// 
3039    /// Returns the node's position in **absolute window coordinates** (logical pixels).
3040    /// This is the coordinate space used throughout the display list:
3041    /// 
3042    /// - Origin: Top-left corner of the window
3043    /// - Units: Logical pixels (HiDPI scaling happens in compositor2.rs)
3044    /// - Scroll: NOT applied here - WebRender scroll frames handle scroll offset
3045    ///   transformation internally via `define_scroll_frame()`
3046    /// 
3047    /// ## Important
3048    /// 
3049    /// Do NOT manually subtract scroll offset here! WebRender's scroll spatial
3050    /// transforms handle this. Subtracting here would cause double-offset and
3051    /// parallax effects (backgrounds and text moving at different speeds).
3052    fn get_paint_rect(&self, node_index: usize) -> Option<LogicalRect> {
3053        let node = self.positioned_tree.tree.get(node_index)?;
3054        let pos = self
3055            .positioned_tree
3056            .calculated_positions
3057            .get(node_index)
3058            .copied()
3059            .unwrap_or_default();
3060        let size = node.used_size.unwrap_or_default();
3061
3062        // NOTE: Scroll offset is NOT applied here!
3063        // WebRender scroll frames handle scroll transformation.
3064        // See compositor2.rs PushScrollFrame for details.
3065
3066        Some(LogicalRect::new(pos, size))
3067    }
3068
3069    /// Emits drawing commands for the background and border of a single node.
3070    fn paint_node_background_and_border(
3071        &mut self,
3072        builder: &mut DisplayListBuilder,
3073        node_index: usize,
3074    ) -> Result<()> {
3075        let Some(paint_rect) = self.get_paint_rect(node_index) else {
3076            return Ok(());
3077        };
3078        let node = self
3079            .positioned_tree
3080            .tree
3081            .get(node_index)
3082            .ok_or(LayoutError::InvalidTree)?;
3083
3084        // Set current node for node mapping (for pagination break properties)
3085        builder.set_current_node(node.dom_node_id);
3086
3087        // Check for CSS break-before/break-after properties and register forced page breaks
3088        // This is used by the pagination slicer to insert page breaks at correct positions
3089        if let Some(dom_id) = node.dom_node_id {
3090            let break_before = get_break_before(self.ctx.styled_dom, Some(dom_id));
3091            let break_after = get_break_after(self.ctx.styled_dom, Some(dom_id));
3092
3093            // For break-before: always, insert a page break at the top of this element
3094            if is_forced_page_break(break_before) {
3095                let y_position = paint_rect.origin.y;
3096                builder.add_forced_page_break(y_position);
3097                debug_info!(
3098                    self.ctx,
3099                    "Registered forced page break BEFORE node {} at y={}",
3100                    node_index,
3101                    y_position
3102                );
3103            }
3104
3105            // For break-after: always, insert a page break at the bottom of this element
3106            if is_forced_page_break(break_after) {
3107                let y_position = paint_rect.origin.y + paint_rect.size.height;
3108                builder.add_forced_page_break(y_position);
3109                debug_info!(
3110                    self.ctx,
3111                    "Registered forced page break AFTER node {} at y={}",
3112                    node_index,
3113                    y_position
3114                );
3115            }
3116        }
3117
3118        // CSS 2.2 §11.2: visibility:hidden — box is invisible but still affects layout.
3119        // Skip painting background/border for hidden nodes, but traversal continues
3120        // so visible descendants are still painted.
3121        if self.is_node_hidden(node_index) {
3122            return Ok(());
3123        }
3124
3125        // Skip inline and inline-block elements ONLY if they participate in an IFC (Inline Formatting Context).
3126        // In Flex or Grid containers, inline-block elements are treated as flex/grid items and must be painted here.
3127        // Inline elements participate in inline formatting context and their backgrounds
3128        // must be positioned by the text layout engine, not the block layout engine
3129        //
3130        // IMPORTANT: The parent check must look at the PARENT NODE's formatting_context,
3131        // not the current node's. If parent is Flex/Grid, we paint this element as a flex/grid item.
3132        // Also check parent_formatting_context field which stores parent's FC during tree construction.
3133        let warm = self.positioned_tree.tree.warm(node_index);
3134        let parent_is_flex_or_grid = warm
3135            .and_then(|w| w.parent_formatting_context.as_ref().map(|fc| matches!(fc, FormattingContext::Flex | FormattingContext::Grid)))
3136            .unwrap_or(false);
3137        
3138        if let Some(dom_id) = node.dom_node_id {
3139            let display = {
3140                use crate::solver3::getters::get_display_property;
3141                get_display_property(self.ctx.styled_dom, Some(dom_id))
3142                    .unwrap_or(LayoutDisplay::Inline)
3143            };
3144
3145            if display == LayoutDisplay::InlineBlock || display == LayoutDisplay::Inline {
3146                debug_info!(
3147                    self.ctx,
3148                    "[paint_node] node {} has display={:?}, parent_formatting_context={:?}, parent_is_flex_or_grid={}",
3149                    node_index,
3150                    display,
3151                    warm.and_then(|w| w.parent_formatting_context.as_ref()),
3152                    parent_is_flex_or_grid
3153                );
3154
3155                if !parent_is_flex_or_grid {
3156                    // Normally, text3 handles inline/inline-block backgrounds via
3157                    // InlineShape (inline-block) or glyph runs (inline). However,
3158                    // if this inline-block establishes a stacking context (e.g.
3159                    // position:relative + z-index, opacity < 1, transform), we MUST
3160                    // paint its background here. generate_for_stacking_context paints
3161                    // background (step 1) → children (steps 3-6). If we skip the
3162                    // background, paint_inline_shape in the parent's paint_node_content
3163                    // would paint it AFTER the children, obscuring them.
3164                    if display == LayoutDisplay::InlineBlock
3165                        && self.establishes_stacking_context(node_index)
3166                    {
3167                        // Fall through to paint background/border now
3168                    } else {
3169                        return Ok(());
3170                    }
3171                }
3172                // Fall through to paint this element - it's a flex/grid item
3173            }
3174        }
3175
3176        // CSS 2.2 Section 17.5.1: Tables in the visual formatting model
3177        // Table-internal elements (row groups, rows, columns, column groups) have their
3178        // backgrounds painted by paint_table_items() in the correct 6-layer order.
3179        // Skip background painting here to avoid double-painting at wrong positions
3180        // (calculated_positions for TR elements may not reflect row offsets correctly;
3181        // paint_table_items computes row rects from cell bounding boxes instead).
3182        // Table CELLS still need content painting via paint_in_flow_descendants, so
3183        // we only skip the background/border here — content painting continues normally.
3184        if matches!(node.formatting_context,
3185            FormattingContext::TableRowGroup | FormattingContext::TableRow |
3186            FormattingContext::TableColumnGroup
3187        ) {
3188            return Ok(());
3189        }
3190
3191        // Tables have a special 6-layer background painting order
3192        if matches!(node.formatting_context, FormattingContext::Table) {
3193            debug_info!(
3194                self.ctx,
3195                "Painting table backgrounds/borders for node {} at {:?}",
3196                node_index,
3197                paint_rect
3198            );
3199            // Delegate to specialized table painting function
3200            return self.paint_table_items(builder, node_index);
3201        }
3202
3203        if let Some(dom_id) = node.dom_node_id {
3204            let styled_node_state = self.get_styled_node_state(dom_id);
3205            let background_contents =
3206                get_background_contents(self.ctx.styled_dom, dom_id, &styled_node_state);
3207            let border_info = get_border_info(self.ctx.styled_dom, dom_id, &styled_node_state);
3208
3209            let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
3210            debug_info!(
3211                self.ctx,
3212                "Painting background/border for node {} ({:?}) at {:?}, backgrounds={:?}",
3213                node_index,
3214                node_type.get_node_type(),
3215                paint_rect,
3216                background_contents.len()
3217            );
3218
3219            // Get both versions: simple BorderRadius for rect clipping and StyleBorderRadius for
3220            // border rendering
3221            let element_size = PhysicalSizeImport {
3222                width: paint_rect.size.width,
3223                height: paint_rect.size.height,
3224            };
3225            let simple_border_radius = get_border_radius(
3226                self.ctx.styled_dom,
3227                dom_id,
3228                &styled_node_state,
3229                element_size,
3230                self.ctx.viewport_size,
3231            );
3232            let style_border_radius =
3233                get_style_border_radius(self.ctx.styled_dom, dom_id, &styled_node_state);
3234
3235            // Paint box shadows before backgrounds (CSS spec: shadows render behind the element)
3236            let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
3237
3238            // +spec:overflow:bb4308 - box shadows are ink overflow: painted outside border box, not affecting layout
3239            // Check all four sides for box-shadow (azul stores them per-side).
3240            // Routed through `super::getters::*` so the compact-cache has_box_shadow
3241            // fast path fires — most nodes have no shadow and skip 4 cascade walks.
3242            for shadow in [
3243                super::getters::get_box_shadow_left(self.ctx.styled_dom, dom_id, node_state),
3244                super::getters::get_box_shadow_right(self.ctx.styled_dom, dom_id, node_state),
3245                super::getters::get_box_shadow_top(self.ctx.styled_dom, dom_id, node_state),
3246                super::getters::get_box_shadow_bottom(self.ctx.styled_dom, dom_id, node_state),
3247            ] {
3248                if let Some(shadow) = shadow {
3249                    builder.push_item(DisplayListItem::BoxShadow {
3250                        bounds: paint_rect.into(),
3251                        shadow,
3252                        border_radius: simple_border_radius,
3253                    });
3254                }
3255            }
3256
3257            // Use unified background/border painting
3258            builder.push_backgrounds_and_border(
3259                paint_rect,
3260                &background_contents,
3261                &border_info,
3262                simple_border_radius,
3263                style_border_radius,
3264                self.ctx.image_cache,
3265            );
3266
3267        }
3268
3269        Ok(())
3270    }
3271
3272    //   backgrounds are invisible, allowing table background to show through
3273    // +spec:box-model:124815 - Table layer background painting order (6 layers: table, col-group, col, row-group, row, cell)
3274    // +spec:positioning:702985 - Table background painting in 6 layers (17.5.1)
3275    // +spec:table-layout:7370dc - Table layers and transparency: 6-layer background painting order
3276    // +spec:table-layout:7a5909 - table layers: 6-layer background paint order (table/colgroup/col/rowgroup/row/cell)
3277    /// CSS 2.2 Section 17.5.1: Table background painting in 6 layers
3278    ///
3279    /// Implements the CSS 2.2 specification for table background painting order.
3280    /// Unlike regular block elements, tables paint backgrounds in layers from back to front:
3281    ///
3282    /// 1. Table background (lowest layer)
3283    /// 2. Column group backgrounds
3284    /// 3. Column backgrounds
3285    /// 4. Row group backgrounds
3286    /// 5. Row backgrounds
3287    /// 6. Cell backgrounds (topmost layer)
3288    ///
3289    /// Then borders are painted (respecting border-collapse mode).
3290    /// Finally, cell content is painted on top of everything.
3291    ///
3292    /// This function generates simple display list items (Rect, Border) in the correct
3293    /// CSS paint order, making WebRender integration trivial.
3294    fn paint_table_items(
3295        &self,
3296        builder: &mut DisplayListBuilder,
3297        table_index: usize,
3298    ) -> Result<()> {
3299        let table_node = self
3300            .positioned_tree
3301            .tree
3302            .get(table_index)
3303            .ok_or(LayoutError::InvalidTree)?;
3304
3305        let Some(table_paint_rect) = self.get_paint_rect(table_index) else {
3306            return Ok(());
3307        };
3308
3309        // Layer 1: Table background
3310        if let Some(dom_id) = table_node.dom_node_id {
3311            let styled_node_state = self.get_styled_node_state(dom_id);
3312            let bg_color = get_background_color(self.ctx.styled_dom, dom_id, &styled_node_state);
3313            let element_size = PhysicalSizeImport {
3314                width: table_paint_rect.size.width,
3315                height: table_paint_rect.size.height,
3316            };
3317            let border_radius = get_border_radius(
3318                self.ctx.styled_dom,
3319                dom_id,
3320                &styled_node_state,
3321                element_size,
3322                self.ctx.viewport_size,
3323            );
3324
3325            builder.push_rect(table_paint_rect, bg_color, border_radius);
3326        }
3327
3328        // Traverse table children to paint layers 2-6
3329
3330        // Layer 2: Column group backgrounds
3331        // Layer 3: Column backgrounds (columns are children of column groups)
3332        for &child_idx in self.positioned_tree.tree.children(table_index) {
3333            let child_node = self.positioned_tree.tree.get(child_idx);
3334            if let Some(node) = child_node {
3335                if matches!(node.formatting_context, FormattingContext::TableColumnGroup) {
3336                    // Paint column group background
3337                    self.paint_element_background(builder, child_idx)?;
3338
3339                    // Paint backgrounds of individual columns within this group
3340                    for &col_idx in self.positioned_tree.tree.children(child_idx) {
3341                        self.paint_element_background(builder, col_idx)?;
3342                    }
3343                }
3344            }
3345        }
3346
3347        // Layer 4: Row group backgrounds (tbody, thead, tfoot)
3348        // Layer 5: Row backgrounds
3349        // Layer 6: Cell backgrounds
3350        for &child_idx in self.positioned_tree.tree.children(table_index) {
3351            let child_node = self.positioned_tree.tree.get(child_idx);
3352            if let Some(node) = child_node {
3353                match node.formatting_context {
3354                    FormattingContext::TableRowGroup => {
3355                        // Paint row group background
3356                        self.paint_element_background(builder, child_idx)?;
3357
3358                        // Paint rows within this group
3359                        for &row_idx in self.positioned_tree.tree.children(child_idx) {
3360                            self.paint_table_row_and_cells(builder, row_idx)?;
3361                        }
3362                    }
3363                    FormattingContext::TableRow => {
3364                        // Direct row child (no row group wrapper)
3365                        self.paint_table_row_and_cells(builder, child_idx)?;
3366                    }
3367                    _ => {}
3368                }
3369            }
3370        }
3371
3372        // Borders are painted separately after all backgrounds
3373        // This is handled by the normal rendering flow for each element
3374        // TODO: For border-collapse: collapse tables, resolve conflicts between
3375        // adjacent cell borders using BorderInfo::resolve_conflict() from fc.rs.
3376        // Currently all cells paint their own borders (separate model behavior).
3377
3378        Ok(())
3379    }
3380
3381    /// Helper function to paint a table row's background and then its cells' backgrounds
3382    /// Layer 5: Row background
3383    /// Layer 6: Cell backgrounds (painted after row, so they appear on top)
3384    fn paint_table_row_and_cells(
3385        &self,
3386        builder: &mut DisplayListBuilder,
3387        row_idx: usize,
3388    ) -> Result<()> {
3389        // Layer 5: Paint row background.
3390        // Rows don't have entries in calculated_positions (adding them would
3391        // double-offset cells during position recursion). Compute the row rect
3392        // from the bounding box of its cell children.
3393        if let Some(row_node) = self.positioned_tree.tree.get(row_idx) {
3394            if let Some(dom_id) = row_node.dom_node_id {
3395                let styled_node_state = self.get_styled_node_state(dom_id);
3396                let bg_color = get_background_color(self.ctx.styled_dom, dom_id, &styled_node_state);
3397                if bg_color.a > 0 {
3398                    // Compute row rect from cell children
3399                    let mut min_x = f32::MAX;
3400                    let mut min_y = f32::MAX;
3401                    let mut max_x = f32::MIN;
3402                    let mut max_y = f32::MIN;
3403                    for &cell_idx in self.positioned_tree.tree.children(row_idx) {
3404                        if let Some(cell_rect) = self.get_paint_rect(cell_idx) {
3405                            min_x = min_x.min(cell_rect.origin.x);
3406                            min_y = min_y.min(cell_rect.origin.y);
3407                            max_x = max_x.max(cell_rect.origin.x + cell_rect.size.width);
3408                            max_y = max_y.max(cell_rect.origin.y + cell_rect.size.height);
3409                        }
3410                    }
3411                    if min_x < max_x && min_y < max_y {
3412                        let row_rect = LogicalRect::new(
3413                            LogicalPosition::new(min_x, min_y),
3414                            LogicalSize::new(max_x - min_x, max_y - min_y),
3415                        );
3416                        builder.push_rect(row_rect, bg_color, BorderRadius::default());
3417                    }
3418                }
3419            }
3420        }
3421
3422        // Layer 6: Paint cell backgrounds (topmost layer)
3423        if let Some(_node) = self.positioned_tree.tree.get(row_idx) {
3424            for &cell_idx in self.positioned_tree.tree.children(row_idx) {
3425                self.paint_element_background(builder, cell_idx)?;
3426            }
3427        }
3428
3429        Ok(())
3430    }
3431
3432    /// Helper function to paint an element's background (used for all table elements)
3433    /// Reads background-color and border-radius from CSS properties and emits push_rect()
3434    fn paint_element_background(
3435        &self,
3436        builder: &mut DisplayListBuilder,
3437        node_index: usize,
3438    ) -> Result<()> {
3439        let Some(paint_rect) = self.get_paint_rect(node_index) else {
3440            return Ok(());
3441        };
3442
3443        let Some(node) = self.positioned_tree.tree.get(node_index) else {
3444            return Ok(());
3445        };
3446        let Some(dom_id) = node.dom_node_id else {
3447            return Ok(());
3448        };
3449
3450        let styled_node_state = self.get_styled_node_state(dom_id);
3451        let bg_color = get_background_color(self.ctx.styled_dom, dom_id, &styled_node_state);
3452
3453        // Only paint if background color has alpha > 0 (optimization)
3454        if bg_color.a == 0 {
3455            return Ok(());
3456        }
3457
3458        let element_size = PhysicalSizeImport {
3459            width: paint_rect.size.width,
3460            height: paint_rect.size.height,
3461        };
3462        let border_radius = get_border_radius(
3463            self.ctx.styled_dom,
3464            dom_id,
3465            &styled_node_state,
3466            element_size,
3467            self.ctx.viewport_size,
3468        );
3469
3470        builder.push_rect(paint_rect, bg_color, border_radius);
3471
3472        Ok(())
3473    }
3474
3475    /// Emits drawing commands for the foreground content, including hit-test areas and scrollbars.
3476    fn paint_node_content(
3477        &mut self,
3478        builder: &mut DisplayListBuilder,
3479        node_index: usize,
3480    ) -> Result<()> {
3481        // CSS 2.2 §11.2: visibility:hidden — skip painting content for hidden nodes.
3482        if self.is_node_hidden(node_index) {
3483            return Ok(());
3484        }
3485
3486        let node = self
3487            .positioned_tree
3488            .tree
3489            .get(node_index)
3490            .ok_or(LayoutError::InvalidTree)?;
3491        let node_warm = self.positioned_tree.tree.warm(node_index);
3492
3493        // Set current node for node mapping (for pagination break properties)
3494        builder.set_current_node(node.dom_node_id);
3495
3496        let Some(mut paint_rect) = self.get_paint_rect(node_index) else {
3497            return Ok(());
3498        };
3499
3500        // For text nodes (with inline layout), the used_size might be 0x0.
3501        // In this case, compute the bounds from the inline layout result.
3502        if paint_rect.size.width == 0.0 || paint_rect.size.height == 0.0 {
3503            if let Some(cached_layout) = node_warm.and_then(|w| w.inline_layout_result.as_ref()) {
3504                let content_bounds = cached_layout.layout.bounds();
3505                paint_rect.size.width = content_bounds.width;
3506                paint_rect.size.height = content_bounds.height;
3507            }
3508        }
3509
3510        // Add a hit-test area for this node if it's interactive.
3511        // NOTE: For scrollable containers (overflow: scroll/auto), the hit-test area
3512        // was already pushed in generate_for_stacking_context BEFORE the scroll frame,
3513        // so we skip it here to avoid duplicate hit-test areas that would scroll with content.
3514        if let Some(tag_id) = get_tag_id(self.ctx.styled_dom, node.dom_node_id) {
3515            let is_scrollable = if let Some(dom_id) = node.dom_node_id {
3516                let styled_node_state = self.get_styled_node_state(dom_id);
3517                let overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
3518                let overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
3519                overflow_x.is_scroll() || overflow_y.is_scroll()
3520            } else {
3521                false
3522            };
3523
3524            // Push hit-test area for this node ONLY if it's not a scrollable container.
3525            // Scrollable containers already have their hit-test area pushed BEFORE the scroll frame
3526            // in generate_for_stacking_context, ensuring the hit-test stays stationary in parent space
3527            // while content scrolls. Pushing it again here would create a duplicate that scrolls
3528            // with content, causing hit-test failures when scrolled to the bottom.
3529            if !is_scrollable {
3530                builder.push_hit_test_area(paint_rect, tag_id);
3531            }
3532        }
3533
3534        // Paint the node's visible content.
3535        if let Some(cached_layout) = node_warm.and_then(|w| w.inline_layout_result.as_ref()) {
3536            let inline_layout = &cached_layout.layout;
3537            debug_info!(
3538                self.ctx,
3539                "[paint_node] node {} has inline_layout with {} items",
3540                node_index,
3541                inline_layout.items.len()
3542            );
3543
3544            if let Some(dom_id) = node.dom_node_id {
3545                let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
3546                debug_info!(
3547                    self.ctx,
3548                    "Painting inline content for node {} ({:?}) at {:?}, {} layout items",
3549                    node_index,
3550                    node_type.get_node_type(),
3551                    paint_rect,
3552                    inline_layout.items.len()
3553                );
3554            }
3555
3556            // paint_rect is the border-box, but inline layout positions are relative to
3557            // content-box. Use type-safe conversion to make this clear and avoid manual
3558            // calculations.
3559            let border_box = BorderBoxRect(paint_rect);
3560            let nbp = node.box_props.unpack();
3561            let mut content_box_rect =
3562                border_box.to_content_box(&nbp.padding, &nbp.border).rect();
3563
3564            // Save the viewport-sized content box for clipping BEFORE expanding
3565            // to full scroll content size. Text must be clipped to the viewport
3566            // when overflow is hidden/scroll/auto, not to the full content size.
3567            let viewport_clip_rect = content_box_rect;
3568
3569            // For scrollable containers, extend the content rect to the full content size.
3570            // The scroll frame handles clipping - we need to paint ALL content, not just
3571            // what fits in the viewport. Otherwise glyphs beyond the viewport are not rendered.
3572            let content_size = get_scroll_content_size(node, node_warm);
3573            if content_size.height > content_box_rect.size.height {
3574                content_box_rect.size.height = content_size.height;
3575            }
3576            if content_size.width > content_box_rect.size.width {
3577                content_box_rect.size.width = content_size.width;
3578            }
3579
3580            // Check for text-shadow and wrap inline content with push/pop shadow
3581            let mut pushed_text_shadow = false;
3582            if let Some(dom_id) = node.dom_node_id {
3583                let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
3584                let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
3585                if let Some(shadow_val) = self.ctx.styled_dom.css_property_cache.ptr
3586                    .get_text_shadow(node_data, &dom_id, node_state)
3587                {
3588                    if let Some(shadow) = shadow_val.get_property() {
3589                        builder.push_item(DisplayListItem::PushTextShadow {
3590                            shadow: (**shadow).clone(),
3591                        });
3592                        pushed_text_shadow = true;
3593                    }
3594                }
3595            }
3596
3597            self.paint_inline_content(builder, content_box_rect, viewport_clip_rect, inline_layout, node_index)?;
3598
3599            if pushed_text_shadow {
3600                builder.push_item(DisplayListItem::PopTextShadow);
3601            }
3602        } else if let Some(dom_id) = node.dom_node_id {
3603            // +spec:replaced-elements:edd21b - block-level replaced element painted atomically per E.2
3604            // +spec:replaced-elements:516b2a - replaced content painted atomically in painting order
3605            // This node might be a simple replaced element, like an <img> tag.
3606            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
3607            if let NodeType::Image(image_ref) = node_data.get_node_type() {
3608                debug_info!(
3609                    self.ctx,
3610                    "Painting image for node {} at {:?}",
3611                    node_index,
3612                    paint_rect
3613                );
3614                // Get border-radius so the compositor can clip the image to rounded corners
3615                let styled_node_state = self.get_styled_node_state(dom_id);
3616                let element_size = PhysicalSizeImport {
3617                    width: paint_rect.size.width,
3618                    height: paint_rect.size.height,
3619                };
3620                let border_radius = get_border_radius(
3621                    self.ctx.styled_dom,
3622                    dom_id,
3623                    &styled_node_state,
3624                    element_size,
3625                    self.ctx.viewport_size,
3626                );
3627                // Store the ImageRef directly in the display list
3628                builder.push_image(paint_rect, image_ref.as_ref().clone(), border_radius);
3629            }
3630        }
3631
3632        Ok(())
3633    }
3634
3635    /// Emits drawing commands for scrollbars. This is called AFTER popping the scroll frame
3636    /// clip so scrollbars appear on top of content and are not clipped.
3637    fn paint_scrollbars(&self, builder: &mut DisplayListBuilder, node_index: usize) -> Result<()> {
3638        // CSS 2.2 §11.2: visibility:hidden scroll containers must not paint scrollbars,
3639        // but their layout space is preserved (already handled by layout).
3640        if self.is_node_hidden(node_index) {
3641            return Ok(());
3642        }
3643
3644        let node = self
3645            .positioned_tree
3646            .tree
3647            .get(node_index)
3648            .ok_or(LayoutError::InvalidTree)?;
3649
3650        let Some(paint_rect) = self.get_paint_rect(node_index) else {
3651            return Ok(());
3652        };
3653
3654        // Check if we need to draw scrollbars for this node.
3655        let scrollbar_info = self.positioned_tree.tree.warm(node_index)
3656            .and_then(|w| w.scrollbar_info.clone())
3657            .unwrap_or_default();
3658
3659        // Get node_id for GPU cache lookup and CSS style lookup
3660        let node_id = node.dom_node_id;
3661
3662        // Get CSS scrollbar style for this node (cached per LayoutContext).
3663        let scrollbar_style = node_id
3664            .map(|nid| {
3665                let node_state =
3666                    &self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
3667                crate::solver3::getters::get_scrollbar_style_cached(self.ctx, nid, node_state)
3668            })
3669            .unwrap_or_default();
3670
3671        // Skip if scrollbar-width: none
3672        if matches!(
3673            scrollbar_style.width_mode,
3674            azul_css::props::style::scrollbar::LayoutScrollbarWidth::None
3675        ) {
3676            return Ok(());
3677        }
3678
3679        // +spec:overflow:3dfb2c - when scrollbar gutter is present but scrollbar is not,
3680        // paint the gutter background as an extension of the padding
3681        let scrollbar_gutter = node_id
3682            .and_then(|nid| {
3683                let node_state =
3684                    &self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
3685                get_scrollbar_gutter_property(self.ctx.styled_dom, nid, node_state).exact()
3686            })
3687            .unwrap_or_default();
3688        let gutter_is_stable = matches!(
3689            scrollbar_gutter,
3690            azul_css::props::layout::overflow::StyleScrollbarGutter::Stable
3691            | azul_css::props::layout::overflow::StyleScrollbarGutter::StableBothEdges
3692        );
3693        let gutter_both_edges = matches!(
3694            scrollbar_gutter,
3695            azul_css::props::layout::overflow::StyleScrollbarGutter::StableBothEdges
3696        );
3697
3698        if gutter_is_stable {
3699            let gbp = node.box_props.unpack();
3700            let border = &gbp.border;
3701            let gutter_width = scrollbar_style.visual_width_px;
3702            // Paint gutter as padding extension when scrollbar is absent
3703            let bg_color = node_id
3704                .map(|nid| {
3705                    let node_state =
3706                        &self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
3707                    get_background_color(self.ctx.styled_dom, nid, node_state)
3708                })
3709                .unwrap_or(ColorU::TRANSPARENT);
3710
3711            if !scrollbar_info.needs_vertical && gutter_width > 0.0 {
3712                // Right-side gutter (inline-end)
3713                let gutter_rect = LogicalRect {
3714                    origin: LogicalPosition::new(
3715                        paint_rect.origin.x + paint_rect.size.width - border.right - gutter_width,
3716                        paint_rect.origin.y + border.top,
3717                    ),
3718                    size: LogicalSize::new(
3719                        gutter_width,
3720                        (paint_rect.size.height - border.top - border.bottom).max(0.0),
3721                    ),
3722                };
3723                builder.push_rect(gutter_rect, bg_color, BorderRadius::default());
3724
3725                // Both-edges: also paint left-side gutter (inline-start)
3726                if gutter_both_edges {
3727                    let left_gutter_rect = LogicalRect {
3728                        origin: LogicalPosition::new(
3729                            paint_rect.origin.x + border.left,
3730                            paint_rect.origin.y + border.top,
3731                        ),
3732                        size: LogicalSize::new(
3733                            gutter_width,
3734                            (paint_rect.size.height - border.top - border.bottom).max(0.0),
3735                        ),
3736                    };
3737                    builder.push_rect(left_gutter_rect, bg_color, BorderRadius::default());
3738                }
3739            }
3740        }
3741
3742        // Get border dimensions to position scrollbar inside the border-box
3743        let sbp = node.box_props.unpack();
3744        let border = &sbp.border;
3745
3746        // Get border-radius for potential clipping
3747        let container_border_radius = node_id
3748            .map(|nid| {
3749                let node_state =
3750                    &self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
3751                let element_size = PhysicalSizeImport {
3752                    width: paint_rect.size.width,
3753                    height: paint_rect.size.height,
3754                };
3755                let viewport_size =
3756                    LogicalSize::new(self.ctx.viewport_size.width, self.ctx.viewport_size.height);
3757                get_border_radius(
3758                    self.ctx.styled_dom,
3759                    nid,
3760                    node_state,
3761                    element_size,
3762                    viewport_size,
3763                )
3764            })
3765            .unwrap_or_default();
3766
3767        // Calculate the inner rect (content-box) where scrollbars should be placed
3768        // Scrollbars are positioned inside the border, at the right/bottom edges
3769        let inner_rect = LogicalRect {
3770            origin: LogicalPosition::new(
3771                paint_rect.origin.x + border.left,
3772                paint_rect.origin.y + border.top,
3773            ),
3774            size: LogicalSize::new(
3775                (paint_rect.size.width - border.left - border.right).max(0.0),
3776                (paint_rect.size.height - border.top - border.bottom).max(0.0),
3777            ),
3778        };
3779
3780        // Get scroll position for thumb calculation
3781        // ScrollPosition contains parent_rect and children_rect
3782        // The scroll offset is the difference between children_rect.origin and parent_rect.origin
3783        let (scroll_offset_x, scroll_offset_y) = node_id
3784            .and_then(|nid| {
3785                self.scroll_offsets.get(&nid).map(|pos| {
3786                    (
3787                        pos.children_rect.origin.x - pos.parent_rect.origin.x,
3788                        pos.children_rect.origin.y - pos.parent_rect.origin.y,
3789                    )
3790                })
3791            })
3792            .unwrap_or((0.0, 0.0));
3793
3794        // Get content size for thumb proportional sizing
3795        // Use the node's get_content_size() method which returns the actual content size
3796        // from overflow_content_size (set during layout) or computes it from text/children.
3797        // For VirtualView nodes, the virtual_scroll_size (propagated through ScrollPosition.children_rect)
3798        // is more accurate than the layout-computed content size.
3799        let content_size = node_id
3800            .and_then(|nid| self.scroll_offsets.get(&nid))
3801            .map(|pos| pos.children_rect.size)
3802            .unwrap_or_else(|| self.positioned_tree.tree.get_content_size(node_index));
3803
3804        // Calculate thumb border-radius (half the scrollbar width for pill-shaped thumb)
3805        let thumb_radius = scrollbar_style.visual_width_px / 2.0;
3806        let thumb_border_radius = BorderRadius {
3807            top_left: thumb_radius,
3808            top_right: thumb_radius,
3809            bottom_left: thumb_radius,
3810            bottom_right: thumb_radius,
3811        };
3812
3813        if scrollbar_info.needs_vertical {
3814            // Look up opacity key from GPU cache for GPU-animated opacity.
3815            // If a key already exists in the cache from a previous frame, reuse it.
3816            // Otherwise, create a new unique key. The key will be registered
3817            // in the GPU cache after layout_document returns (same pattern as
3818            // transform keys). This ensures the display list ALWAYS has an
3819            // opacity binding, so GPU-only scroll updates can animate it.
3820            let opacity_key = node_id.map(|nid| {
3821                self.gpu_value_cache
3822                    .and_then(|cache| {
3823                        cache
3824                            .scrollbar_v_opacity_keys
3825                            .get(&(self.dom_id, nid))
3826                            .copied()
3827                    })
3828                    .unwrap_or_else(|| OpacityKey::unique())
3829            });
3830
3831            // Vertical scrollbar: use shared geometry computation
3832            let button_size = if scrollbar_style.show_scroll_buttons {
3833                scrollbar_style.scroll_button_size_px
3834            } else {
3835                0.0
3836            };
3837            let v_geom = compute_scrollbar_geometry_with_button_size(
3838                ScrollbarOrientation::Vertical,
3839                inner_rect,
3840                content_size,
3841                scroll_offset_y,
3842                scrollbar_style.visual_width_px,
3843                scrollbar_info.needs_horizontal,
3844                button_size,
3845            );
3846
3847            // Position thumb after the top button; GPU transform moves it within usable track
3848            let thumb_bounds = LogicalRect {
3849                origin: LogicalPosition::new(
3850                    v_geom.track_rect.origin.x,
3851                    v_geom.track_rect.origin.y + v_geom.button_size,
3852                ),
3853                size: LogicalSize::new(v_geom.width_px, v_geom.thumb_length),
3854            };
3855
3856            // Look up transform key from GPU cache for GPU-animated thumb positioning.
3857            // If a key already exists in the cache from a previous frame, reuse it.
3858            // Otherwise, create a new unique key. The key will be registered
3859            // in the GPU cache after layout_document returns.
3860            let thumb_transform_key = node_id.map(|nid| {
3861                self.gpu_value_cache
3862                    .and_then(|cache| cache.transform_keys.get(&nid).copied())
3863                    .unwrap_or_else(|| TransformKey::unique())
3864            });
3865
3866            // Initial transform: translate thumb within usable region
3867            let thumb_initial_transform =
3868                ComputedTransform3D::new_translation(0.0, v_geom.thumb_offset, 0.0);
3869
3870            // Generate hit-test ID for vertical scrollbar thumb
3871            let hit_id = node_id
3872                .map(|nid| azul_core::hit_test::ScrollbarHitId::VerticalThumb(self.dom_id, nid));
3873
3874            // Buttons at top/bottom of track (only if enabled in style)
3875            let (button_decrement_bounds, button_increment_bounds) = if scrollbar_style.show_scroll_buttons && v_geom.button_size > 0.0 {
3876                (
3877                    Some(LogicalRect {
3878                        origin: v_geom.track_rect.origin,
3879                        size: LogicalSize::new(v_geom.button_size, v_geom.button_size),
3880                    }),
3881                    Some(LogicalRect {
3882                        origin: LogicalPosition::new(
3883                            v_geom.track_rect.origin.x,
3884                            v_geom.track_rect.origin.y + v_geom.track_rect.size.height - v_geom.button_size,
3885                        ),
3886                        size: LogicalSize::new(v_geom.button_size, v_geom.button_size),
3887                    }),
3888                )
3889            } else {
3890                (None, None)
3891            };
3892            builder.push_scrollbar_styled(ScrollbarDrawInfo {
3893                bounds: v_geom.track_rect.into(),
3894                orientation: ScrollbarOrientation::Vertical,
3895                track_bounds: v_geom.track_rect.into(),
3896                track_color: scrollbar_style.track_color,
3897                thumb_bounds: thumb_bounds.into(),
3898                thumb_color: scrollbar_style.thumb_color,
3899                thumb_border_radius,
3900                button_decrement_bounds: button_decrement_bounds.map(|b| b.into()),
3901                button_increment_bounds: button_increment_bounds.map(|b| b.into()),
3902                button_color: scrollbar_style.button_color,
3903                opacity_key,
3904                thumb_transform_key,
3905                thumb_initial_transform,
3906                hit_id,
3907                clip_to_container_border: scrollbar_style.clip_to_container_border,
3908                container_border_radius,
3909                visibility: scrollbar_style.visibility,
3910            });
3911        }
3912
3913        if scrollbar_info.needs_horizontal {
3914            // Look up horizontal opacity key from GPU cache (same pattern as vertical).
3915            let opacity_key = node_id.map(|nid| {
3916                self.gpu_value_cache
3917                    .and_then(|cache| {
3918                        cache
3919                            .scrollbar_h_opacity_keys
3920                            .get(&(self.dom_id, nid))
3921                            .copied()
3922                    })
3923                    .unwrap_or_else(|| OpacityKey::unique())
3924            });
3925
3926            // Horizontal scrollbar: use shared geometry computation
3927            let h_button_size = if scrollbar_style.show_scroll_buttons {
3928                scrollbar_style.scroll_button_size_px
3929            } else {
3930                0.0
3931            };
3932            let h_geom = compute_scrollbar_geometry_with_button_size(
3933                ScrollbarOrientation::Horizontal,
3934                inner_rect,
3935                content_size,
3936                scroll_offset_x,
3937                scrollbar_style.visual_width_px,
3938                scrollbar_info.needs_vertical,
3939                h_button_size,
3940            );
3941
3942            // Position thumb after the left button; GPU transform moves it within usable track
3943            let thumb_bounds = LogicalRect {
3944                origin: LogicalPosition::new(
3945                    h_geom.track_rect.origin.x + h_geom.button_size,
3946                    h_geom.track_rect.origin.y,
3947                ),
3948                size: LogicalSize::new(h_geom.thumb_length, h_geom.width_px),
3949            };
3950
3951            // Look up horizontal transform key from GPU cache for GPU-animated thumb positioning.
3952            let thumb_transform_key = node_id.map(|nid| {
3953                self.gpu_value_cache
3954                    .and_then(|cache| cache.h_transform_keys.get(&nid).copied())
3955                    .unwrap_or_else(|| TransformKey::unique())
3956            });
3957            let thumb_initial_transform =
3958                ComputedTransform3D::new_translation(h_geom.thumb_offset, 0.0, 0.0);
3959
3960            // Generate hit-test ID for horizontal scrollbar thumb
3961            let hit_id = node_id
3962                .map(|nid| azul_core::hit_test::ScrollbarHitId::HorizontalThumb(self.dom_id, nid));
3963
3964            // Buttons at left/right of track (only if enabled in style)
3965            let (button_decrement_bounds, button_increment_bounds) = if scrollbar_style.show_scroll_buttons && h_geom.button_size > 0.0 {
3966                (
3967                    Some(LogicalRect {
3968                        origin: h_geom.track_rect.origin,
3969                        size: LogicalSize::new(h_geom.button_size, h_geom.button_size),
3970                    }),
3971                    Some(LogicalRect {
3972                        origin: LogicalPosition::new(
3973                            h_geom.track_rect.origin.x + h_geom.track_rect.size.width - h_geom.button_size,
3974                            h_geom.track_rect.origin.y,
3975                        ),
3976                        size: LogicalSize::new(h_geom.button_size, h_geom.button_size),
3977                    }),
3978                )
3979            } else {
3980                (None, None)
3981            };
3982            builder.push_scrollbar_styled(ScrollbarDrawInfo {
3983                bounds: h_geom.track_rect.into(),
3984                orientation: ScrollbarOrientation::Horizontal,
3985                track_bounds: h_geom.track_rect.into(),
3986                track_color: scrollbar_style.track_color,
3987                thumb_bounds: thumb_bounds.into(),
3988                thumb_color: scrollbar_style.thumb_color,
3989                thumb_border_radius,
3990                button_decrement_bounds: button_decrement_bounds.map(|b| b.into()),
3991                button_increment_bounds: button_increment_bounds.map(|b| b.into()),
3992                button_color: scrollbar_style.button_color,
3993                opacity_key,
3994                thumb_transform_key,
3995                thumb_initial_transform,
3996                hit_id,
3997                clip_to_container_border: scrollbar_style.clip_to_container_border,
3998                container_border_radius,
3999                visibility: scrollbar_style.visibility,
4000            });
4001        }
4002
4003        Ok(())
4004    }
4005
4006    /// Converts the rich layout information from `text3` into drawing commands.
4007    fn paint_inline_content(
4008        &self,
4009        builder: &mut DisplayListBuilder,
4010        container_rect: LogicalRect,
4011        viewport_clip_rect: LogicalRect,
4012        layout: &UnifiedLayout,
4013        source_node_index: usize,
4014    ) -> Result<()> {
4015        // TODO: This will always paint images over the glyphs
4016        // TODO: Handle z-index within inline content (e.g. background images)
4017        // NOTE: Text decorations (underline, strikethrough, overline) are handled in push_text_layout_to_display_list
4018        // TODO: Text shadows not yet implemented
4019        // NOTE: Text-overflow ellipsis is handled via apply_text_overflow_ellipsis()
4020        // which can be called as a post-processing step on the display list when
4021        // the node has overflow:hidden and text-overflow:ellipsis CSS properties.
4022        // +spec:overflow:7807b1 - text-overflow ellipsis side depends on direction (RTL clips left, LTR clips right); not yet implemented
4023        // +spec:overflow:bbf9c1 - text-overflow ellipsis should only truncate content
4024        // that is actually clipped; as content scrolls into view, show it instead of ellipsis
4025        // TODO: Handle text overflowing (based on container_rect and overflow behavior)
4026
4027        // Calculate actual content bounds from the layout
4028        // Use these bounds instead of container_rect to avoid inflated bounds
4029        // that extend beyond actual text content
4030        let layout_bounds = layout.bounds();
4031        let actual_bounds = if layout_bounds.width > 0.0 && layout_bounds.height > 0.0 {
4032            LogicalRect {
4033                origin: container_rect.origin,
4034                size: LogicalSize {
4035                    width: layout_bounds.width,
4036                    height: layout_bounds.height,
4037                },
4038            }
4039        } else {
4040            // If layout has no content, don't push TextLayout item at all
4041            // This prevents 0x0 TextLayout items that pollute height calculation
4042            LogicalRect {
4043                origin: container_rect.origin,
4044                size: LogicalSize::default(),
4045            }
4046        };
4047
4048        // Only push TextLayout if layout has actual content
4049        // This prevents empty TextLayout items with 0x0 bounds at various Y positions
4050        // from affecting pagination height calculations
4051        if layout_bounds.width > 0.0 || layout_bounds.height > 0.0 {
4052            builder.push_text_layout(
4053                Arc::new(layout.clone()) as Arc<dyn std::any::Any + Send + Sync>,
4054                actual_bounds,
4055                FontHash::from_hash(0), // Will be updated per glyph run
4056                12.0,                   // Default font size, will be updated per glyph run
4057                ColorU {
4058                    r: 0,
4059                    g: 0,
4060                    b: 0,
4061                    a: 255,
4062                }, // Default color
4063            );
4064        }
4065
4066        let glyph_runs = crate::text3::glyphs::get_glyph_runs_simple(layout);
4067
4068        // FIRST PASS: Render backgrounds (solid colors, gradients) and borders for each glyph run
4069        // This must happen BEFORE rendering text so that backgrounds appear behind text.
4070        for glyph_run in glyph_runs.iter() {
4071            // Calculate the bounding box for this glyph run
4072            if let (Some(first_glyph), Some(last_glyph)) =
4073                (glyph_run.glyphs.first(), glyph_run.glyphs.last())
4074            {
4075                // Calculate run bounds from glyph positions
4076                let run_start_x = container_rect.origin.x + first_glyph.point.x;
4077                let run_end_x = container_rect.origin.x + last_glyph.point.x;
4078                let run_width = (run_end_x - run_start_x).max(0.0);
4079
4080                // Skip if run has no width
4081                if run_width <= 0.0 {
4082                    continue;
4083                }
4084
4085                // Approximate height based on font size (baseline is at glyph.point.y)
4086                let baseline_y = container_rect.origin.y + first_glyph.point.y;
4087                let font_size = glyph_run.font_size_px;
4088                let ascent = font_size * 0.8; // Approximate ascent
4089
4090                let mut run_bounds = LogicalRect::new(
4091                    LogicalPosition::new(run_start_x, baseline_y - ascent),
4092                    LogicalSize::new(run_width, font_size),
4093                );
4094
4095                // Expand run_bounds by padding + border so the background/border
4096                // rect covers the full inline box, not just the glyph area.
4097                if let Some(border) = &glyph_run.border {
4098                    let left_inset = border.left_inset();
4099                    let right_inset = border.right_inset();
4100                    let top_inset = border.top_inset();
4101                    let bottom_inset = border.bottom_inset();
4102
4103                    run_bounds.origin.x -= left_inset;
4104                    run_bounds.origin.y -= top_inset;
4105                    run_bounds.size.width += left_inset + right_inset;
4106                    run_bounds.size.height += top_inset + bottom_inset;
4107                }
4108
4109                builder.push_inline_backgrounds_and_border(
4110                    run_bounds,
4111                    glyph_run.background_color,
4112                    &glyph_run.background_content,
4113                    glyph_run.border.as_ref(),
4114                    self.ctx.image_cache,
4115                );
4116            }
4117        }
4118
4119        // SECOND PASS: Render text runs
4120        for (_idx, glyph_run) in glyph_runs.iter().enumerate() {
4121            // Clip text to the viewport-sized content box, not the full scroll
4122            // content area. This prevents text from overflowing outside the
4123            // container when overflow is hidden/scroll/auto.
4124            let clip_rect = viewport_clip_rect;
4125
4126            // Fix: Offset glyph positions by the container origin.
4127            // Text layout is relative to (0,0) of the IFC, but we need absolute coordinates.
4128            let offset_glyphs: Vec<GlyphInstance> = glyph_run
4129                .glyphs
4130                .iter()
4131                .map(|g| {
4132                    let mut g = g.clone();
4133                    g.point.x += container_rect.origin.x;
4134                    g.point.y += container_rect.origin.y;
4135                    g
4136                })
4137                .collect();
4138
4139            // Store only the font hash in the display list to keep it lean
4140            builder.push_text_run(
4141                offset_glyphs,
4142                FontHash::from_hash(glyph_run.font_hash),
4143                glyph_run.font_size_px,
4144                glyph_run.color,
4145                clip_rect,
4146                Some(source_node_index),
4147            );
4148
4149            // Render text decorations if present OR if this is IME composition preview
4150            let needs_underline = glyph_run.text_decoration.underline || glyph_run.is_ime_preview;
4151            let needs_strikethrough = glyph_run.text_decoration.strikethrough;
4152            let needs_overline = glyph_run.text_decoration.overline;
4153
4154            if needs_underline || needs_strikethrough || needs_overline {
4155                // Calculate the bounding box for this glyph run
4156                if let (Some(first_glyph), Some(last_glyph)) =
4157                    (glyph_run.glyphs.first(), glyph_run.glyphs.last())
4158                {
4159                    let decoration_start_x = container_rect.origin.x + first_glyph.point.x;
4160                    let decoration_end_x = container_rect.origin.x + last_glyph.point.x;
4161                    let decoration_width = decoration_end_x - decoration_start_x;
4162
4163                    // Use font metrics to determine decoration positions
4164                    // Standard ratios based on CSS specification
4165                    let font_size = glyph_run.font_size_px;
4166                    let thickness = (font_size * 0.08).max(1.0); // ~8% of font size, min 1px
4167
4168                    // Baseline is at glyph.point.y
4169                    let baseline_y = container_rect.origin.y + first_glyph.point.y;
4170
4171                    if needs_underline {
4172                        // Underline is typically 10-15% below baseline
4173                        // IME composition always gets underlined
4174                        let underline_y = baseline_y + (font_size * 0.12);
4175                        let underline_bounds = LogicalRect::new(
4176                            LogicalPosition::new(decoration_start_x, underline_y),
4177                            LogicalSize::new(decoration_width, thickness),
4178                        );
4179                        builder.push_underline(underline_bounds, glyph_run.color, thickness);
4180                    }
4181
4182                    if needs_strikethrough {
4183                        // Strikethrough is typically 40% above baseline (middle of x-height)
4184                        let strikethrough_y = baseline_y - (font_size * 0.3);
4185                        let strikethrough_bounds = LogicalRect::new(
4186                            LogicalPosition::new(decoration_start_x, strikethrough_y),
4187                            LogicalSize::new(decoration_width, thickness),
4188                        );
4189                        builder.push_strikethrough(
4190                            strikethrough_bounds,
4191                            glyph_run.color,
4192                            thickness,
4193                        );
4194                    }
4195
4196                    if needs_overline {
4197                        // Overline is typically at cap-height (75% above baseline)
4198                        let overline_y = baseline_y - (font_size * 0.85);
4199                        let overline_bounds = LogicalRect::new(
4200                            LogicalPosition::new(decoration_start_x, overline_y),
4201                            LogicalSize::new(decoration_width, thickness),
4202                        );
4203                        builder.push_overline(overline_bounds, glyph_run.color, thickness);
4204                    }
4205                }
4206            }
4207        }
4208
4209        // THIRD PASS: Generate hit-test areas for text runs
4210        // This enables cursor resolution directly on text nodes instead of their containers
4211        for glyph_run in glyph_runs.iter() {
4212            // Only generate hit-test areas for runs with a source node id
4213            let Some(source_node_id) = glyph_run.source_node_id else {
4214                continue;
4215            };
4216
4217            // Calculate the bounding box for this glyph run
4218            if let (Some(first_glyph), Some(last_glyph)) =
4219                (glyph_run.glyphs.first(), glyph_run.glyphs.last())
4220            {
4221                let run_start_x = container_rect.origin.x + first_glyph.point.x;
4222                let run_end_x = container_rect.origin.x + last_glyph.point.x;
4223                let run_width = (run_end_x - run_start_x).max(0.0);
4224
4225                // Skip if run has no width
4226                if run_width <= 0.0 {
4227                    continue;
4228                }
4229
4230                // Calculate run bounds using font metrics
4231                let baseline_y = container_rect.origin.y + first_glyph.point.y;
4232                let font_size = glyph_run.font_size_px;
4233                let ascent = font_size * 0.8; // Approximate ascent
4234
4235                let run_bounds = LogicalRect::new(
4236                    LogicalPosition::new(run_start_x, baseline_y - ascent),
4237                    LogicalSize::new(run_width, font_size),
4238                );
4239
4240                // Query the cursor type for this text node from the CSS property cache
4241                // Default to Text cursor (I-beam) for text nodes
4242                let cursor_type = self.get_cursor_type_for_text_node(source_node_id);
4243
4244                // Construct the hit-test tag for cursor resolution
4245                // tag.0 = DomId (upper 32 bits) | NodeId (lower 32 bits)
4246                // tag.1 = TAG_TYPE_CURSOR | cursor_type
4247                let tag_value = ((self.dom_id.inner as u64) << 32) | (source_node_id.index() as u64);
4248                let tag_type = TAG_TYPE_CURSOR | (cursor_type as u16);
4249                let tag_id = (tag_value, tag_type);
4250
4251                builder.push_hit_test_area(run_bounds, tag_id);
4252            }
4253        }
4254
4255        // Render inline objects (images, shapes/inline-blocks, etc.)
4256        // These are positioned by the text3 engine and need to be rendered at their calculated
4257        // positions
4258        for positioned_item in &layout.items {
4259            self.paint_inline_object(builder, container_rect.origin, positioned_item)?;
4260        }
4261        Ok(())
4262    }
4263
4264    /// Paints a single inline object (image, shape, or inline-block)
4265    fn paint_inline_object(
4266        &self,
4267        builder: &mut DisplayListBuilder,
4268        base_pos: LogicalPosition,
4269        positioned_item: &PositionedItem,
4270    ) -> Result<()> {
4271        let ShapedItem::Object {
4272            content, bounds, ..
4273        } = &positioned_item.item
4274        else {
4275            // Other item types (e.g., breaks) don't produce painted output.
4276            return Ok(());
4277        };
4278
4279        // Calculate the absolute position of this object
4280        // positioned_item.position is relative to the container
4281        let object_bounds = LogicalRect::new(
4282            LogicalPosition::new(
4283                base_pos.x + positioned_item.position.x,
4284                base_pos.y + positioned_item.position.y,
4285            ),
4286            LogicalSize::new(bounds.width, bounds.height),
4287        );
4288
4289        match content {
4290            InlineContent::Image(image) => {
4291                if let Some(image_ref) = get_image_ref_for_image_source(&image.source) {
4292                    builder.push_image(object_bounds, image_ref, BorderRadius::default());
4293                }
4294            }
4295            InlineContent::Shape(shape) => {
4296                self.paint_inline_shape(builder, object_bounds, shape, bounds)?;
4297            }
4298            _ => {}
4299        }
4300        Ok(())
4301    }
4302
4303    // +spec:inline-block:a60a89 - inline-block painted atomically as pseudo-stacking-context per E.2
4304    /// Paints an inline shape (inline-block background and border)
4305    fn paint_inline_shape(
4306        &self,
4307        builder: &mut DisplayListBuilder,
4308        object_bounds: LogicalRect,
4309        shape: &InlineShape,
4310        bounds: &crate::text3::cache::Rect,
4311    ) -> Result<()> {
4312        // Render inline-block backgrounds and borders using their CSS styling
4313        // The text3 engine positions these correctly in the inline flow
4314        let Some(node_id) = shape.source_node_id else {
4315            return Ok(());
4316        };
4317
4318        // If this inline-block establishes a stacking context, its background was
4319        // already painted by paint_node_background_and_border (called from
4320        // generate_for_stacking_context). Painting again here would cause
4321        // double-rendering. Skip it.
4322        if let Some(indices) = self.positioned_tree.tree.dom_to_layout.get(&node_id) {
4323            if let Some(&idx) = indices.first() {
4324                if self.establishes_stacking_context(idx) {
4325                    return Ok(());
4326                }
4327            }
4328        }
4329
4330        let styled_node_state =
4331            &self.ctx.styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
4332
4333        // Get all background layers (colors, gradients, images)
4334        let background_contents =
4335            get_background_contents(self.ctx.styled_dom, node_id, styled_node_state);
4336
4337        // Get border information
4338        let border_info = get_border_info(self.ctx.styled_dom, node_id, styled_node_state);
4339
4340        // FIX: object_bounds is the margin-box position from text3.
4341        // We need to convert to border-box for painting backgrounds/borders.
4342        let margins = if let Some(indices) = self.positioned_tree.tree.dom_to_layout.get(&node_id) {
4343            if let Some(&idx) = indices.first() {
4344                self.positioned_tree.tree.nodes[idx].box_props.unpack().margin
4345            } else {
4346                Default::default()
4347            }
4348        } else {
4349            Default::default()
4350        };
4351
4352        // Convert margin-box bounds to border-box bounds
4353        let border_box_bounds = LogicalRect {
4354            origin: LogicalPosition {
4355                x: object_bounds.origin.x + margins.left,
4356                y: object_bounds.origin.y + margins.top,
4357            },
4358            size: LogicalSize {
4359                width: (object_bounds.size.width - margins.left - margins.right).max(0.0),
4360                height: (object_bounds.size.height - margins.top - margins.bottom).max(0.0),
4361            },
4362        };
4363
4364        let element_size = PhysicalSizeImport {
4365            width: border_box_bounds.size.width,
4366            height: border_box_bounds.size.height,
4367        };
4368
4369        // Get border radius for background clipping
4370        let simple_border_radius = get_border_radius(
4371            self.ctx.styled_dom,
4372            node_id,
4373            styled_node_state,
4374            element_size,
4375            self.ctx.viewport_size,
4376        );
4377
4378        // Get style border radius for border rendering
4379        let style_border_radius =
4380            get_style_border_radius(self.ctx.styled_dom, node_id, styled_node_state);
4381
4382        // Use unified background/border painting with border-box bounds
4383        builder.push_backgrounds_and_border(
4384            border_box_bounds,
4385            &background_contents,
4386            &border_info,
4387            simple_border_radius,
4388            style_border_radius,
4389            self.ctx.image_cache,
4390        );
4391
4392        // Push hit-test area for this inline-block element
4393        // This is critical for buttons and other inline-block elements to receive
4394        // mouse events and display the correct cursor (e.g., cursor: pointer)
4395        if let Some(tag_id) = get_tag_id(self.ctx.styled_dom, Some(node_id)) {
4396            builder.push_hit_test_area(border_box_bounds, tag_id);
4397        }
4398
4399        Ok(())
4400    }
4401
4402    // +spec:overflow:d1d5f6 - CSS 2.2 §9.9.1 stacking context creation and 7-layer paint order
4403    /// Determines if a node establishes a new stacking context based on CSS rules.
4404    // +spec:overflow:47b791 - z-index applies to positioned boxes; z-index:auto does not establish stacking context
4405    // +spec:positioning:8c6efd - Stacking contexts: positioned elements with z-index != auto establish new stacking context
4406    // +spec:positioning:b84cfa - z-index stacking context creation: integer z-index on positioned elements creates SC; auto on fixed/root creates SC
4407    // +spec:positioning:d06368 - relative/absolute with z-index:auto do not form stacking context but are painted as if they did
4408    fn establishes_stacking_context(&self, node_index: usize) -> bool {
4409        let Some(node) = self.positioned_tree.tree.get(node_index) else {
4410            return false;
4411        };
4412        let Some(dom_id) = node.dom_node_id else {
4413            return false;
4414        };
4415
4416        let position = get_position_type(self.ctx.styled_dom, Some(dom_id));
4417        let z_auto = crate::solver3::getters::is_z_index_auto(self.ctx.styled_dom, Some(dom_id));
4418
4419        // +spec:position-sticky:66ba22 - fixed and sticky positioned boxes form a stacking context
4420        if position == LayoutPosition::Fixed || position == LayoutPosition::Sticky {
4421            return true;
4422        }
4423
4424        // +spec:positioning:d06368 - relative/absolute with z-index:auto do not form stacking context
4425        // z-index:auto on position:absolute does NOT establish stacking context
4426        if position == LayoutPosition::Absolute {
4427            return !z_auto;
4428        }
4429
4430        // position:relative with explicit z-index integer establishes stacking context
4431        if position == LayoutPosition::Relative && !z_auto {
4432            return true;
4433        }
4434
4435        if let Some(styled_node) = self.ctx.styled_dom.styled_nodes.as_container().get(dom_id) {
4436            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
4437            let node_state =
4438                &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
4439
4440            // Opacity < 1 (GPU: fast path via compact cache)
4441            if crate::solver3::getters::get_opacity(
4442                self.ctx.styled_dom, dom_id, node_state,
4443            ) < 1.0 {
4444                return true;
4445            }
4446
4447            // Transform != none (GPU: has_transform bit check, then slow walk only if set)
4448            if let Some(t) = crate::solver3::getters::get_transform(
4449                self.ctx.styled_dom, dom_id, node_state,
4450            ) {
4451                if !t.is_empty() {
4452                    return true;
4453                }
4454            }
4455        }
4456
4457        false
4458    }
4459}
4460
4461/// Standalone predicate: does this element establish a new stacking context?
4462///
4463/// Centralizes the CSS 2.1 §9.9.1 and CSS3 rules for stacking context creation.
4464/// Checks: position + z-index, opacity < 1, transform != none.
4465///
4466/// This is the canonical check used by display list generation and can also
4467/// be called from other phases that need to reason about stacking contexts.
4468pub fn node_establishes_stacking_context(
4469    styled_dom: &StyledDom,
4470    dom_id: NodeId,
4471) -> bool {
4472    let position = crate::solver3::positioning::get_position_type(styled_dom, Some(dom_id));
4473    let z_auto = crate::solver3::getters::is_z_index_auto(styled_dom, Some(dom_id));
4474
4475    // +spec:position-sticky:66ba22 - fixed and sticky positioned boxes form a stacking context
4476    if position == LayoutPosition::Fixed || position == LayoutPosition::Sticky {
4477        return true;
4478    }
4479    // Absolute with explicit z-index creates stacking context
4480    if position == LayoutPosition::Absolute && !z_auto {
4481        return true;
4482    }
4483    // Relative with explicit z-index creates stacking context
4484    if position == LayoutPosition::Relative && !z_auto {
4485        return true;
4486    }
4487
4488    let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
4489
4490    // Opacity < 1 (GPU: compact-cache fast path)
4491    if crate::solver3::getters::get_opacity(styled_dom, dom_id, node_state) < 1.0 {
4492        return true;
4493    }
4494
4495    // Transform != none (GPU: has_transform bit check, then slow walk only if set)
4496    if let Some(t) = crate::solver3::getters::get_transform(styled_dom, dom_id, node_state) {
4497        if !t.is_empty() {
4498            return true;
4499        }
4500    }
4501
4502    false
4503}
4504
4505/// Helper struct to pass layout results to the display list generator.
4506///
4507/// Combines the layout tree with pre-calculated absolute positions for each node.
4508/// The positions are stored separately because they are computed in a final
4509/// positioning pass after layout is complete.
4510pub struct PositionedTree<'a> {
4511    /// The layout tree containing all nodes with their computed sizes
4512    pub tree: &'a LayoutTree,
4513    /// Map from node index to its absolute position in the document
4514    pub calculated_positions: &'a super::PositionVec,
4515}
4516
4517/// Describes how overflow content should be handled for an element.
4518///
4519/// This maps to the CSS `overflow-x` and `overflow-y` properties and determines
4520/// whether content that exceeds the element's bounds should be visible, clipped,
4521/// or scrollable.
4522#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4523pub enum OverflowBehavior {
4524    /// Content is not clipped and may render outside the element's box (default)
4525    Visible,
4526    /// Content is clipped to the padding box, no scrollbars provided
4527    Hidden,
4528    /// Content is clipped to the padding box (CSS `overflow: clip`)
4529    Clip,
4530    /// Content is clipped and scrollbars are always shown
4531    Scroll,
4532    /// Content is clipped and scrollbars appear only when needed
4533    Auto,
4534}
4535
4536impl OverflowBehavior {
4537    /// Returns `true` if this overflow behavior clips content.
4538    ///
4539    /// All behaviors except `Visible` result in content being clipped
4540    /// to the element's padding box.
4541    pub fn is_clipped(&self) -> bool {
4542        matches!(self, Self::Hidden | Self::Clip | Self::Scroll | Self::Auto)
4543    }
4544
4545    /// Returns `true` if this overflow behavior enables scrolling.
4546    ///
4547    /// Only `Scroll` and `Auto` allow the user to scroll to see
4548    /// overflowing content.
4549    pub fn is_scroll(&self) -> bool {
4550        matches!(self, Self::Scroll | Self::Auto)
4551    }
4552}
4553
4554/// Expands `clip_rect` outward by the `overflow-clip-margin` value on axes that use `overflow: clip`.
4555///
4556/// Per CSS Overflow 3 §3.2, `overflow-clip-margin` only applies to `overflow: clip` —
4557/// it has no effect on `overflow: hidden`, `scroll`, or `auto`.
4558fn apply_overflow_clip_margin(
4559    clip_rect: &mut LogicalRect,
4560    overflow_x: &super::getters::MultiValue<LayoutOverflow>,
4561    overflow_y: &super::getters::MultiValue<LayoutOverflow>,
4562    styled_dom: &StyledDom,
4563    dom_id: NodeId,
4564    styled_node_state: &azul_core::styled_dom::StyledNodeState,
4565) {
4566    if !overflow_x.is_clip() && !overflow_y.is_clip() {
4567        return;
4568    }
4569    let clip_margin = get_overflow_clip_margin_property(styled_dom, dom_id, styled_node_state);
4570    let Some(margin_val) = clip_margin.exact() else {
4571        return;
4572    };
4573    let m = margin_val.inner.to_pixels_internal(0.0, 0.0, 0.0).max(0.0);
4574    if m <= 0.0 {
4575        return;
4576    }
4577    if overflow_x.is_clip() {
4578        clip_rect.origin.x -= m;
4579        clip_rect.size.width += m * 2.0;
4580    }
4581    if overflow_y.is_clip() {
4582        clip_rect.origin.y -= m;
4583        clip_rect.size.height += m * 2.0;
4584    }
4585}
4586
4587fn get_scroll_id(id: Option<NodeId>) -> LocalScrollId {
4588    id.map(|i| i.index() as u64).unwrap_or(0)
4589}
4590
4591/// Calculates the actual content size of a node, including all children and text.
4592/// This is used to determine if scrollbars should appear for overflow: auto.
4593// +spec:overflow:c2ed94 - replaced element overflow is ink overflow (not scrollable);
4594// replaced elements (images) don't contribute scrollable overflow here
4595fn get_scroll_content_size(node: &LayoutNodeHot, warm: Option<&LayoutNodeWarm>) -> LogicalSize {
4596    // First check if we have a pre-calculated overflow_content_size (for block children)
4597    if let Some(overflow_size) = warm.and_then(|w| w.overflow_content_size) {
4598        return overflow_size;
4599    }
4600
4601    // Start with the node's own size
4602    let mut content_size = node.used_size.unwrap_or_default();
4603
4604    // If this node has text layout, calculate the bounds of all text items
4605    if let Some(cached_layout) = warm.and_then(|w| w.inline_layout_result.as_ref()) {
4606        let text_layout = &cached_layout.layout;
4607        // Find the maximum extent of all positioned items
4608        let mut max_x: f32 = 0.0;
4609        let mut max_y: f32 = 0.0;
4610
4611        for positioned_item in &text_layout.items {
4612            let item_bounds = positioned_item.item.bounds();
4613            let item_right = positioned_item.position.x + item_bounds.width;
4614            let item_bottom = positioned_item.position.y + item_bounds.height;
4615
4616            max_x = max_x.max(item_right);
4617            max_y = max_y.max(item_bottom);
4618        }
4619
4620        // Use the maximum extent as content size if it's larger
4621        content_size.width = content_size.width.max(max_x);
4622        content_size.height = content_size.height.max(max_y);
4623    }
4624
4625    content_size
4626}
4627
4628fn get_tag_id(dom: &StyledDom, id: Option<NodeId>) -> Option<DisplayListTagId> {
4629    let node_id = id?;
4630    let tag_mapping = dom.tag_ids_to_node_ids.as_ref().iter().find(|m| {
4631        m.node_id.into_crate_internal() == Some(node_id)
4632    })?;
4633    Some((tag_mapping.tag_id.inner, TAG_TYPE_DOM_NODE))
4634}
4635
4636fn get_image_ref_for_image_source(
4637    source: &ImageSource,
4638) -> Option<ImageRef> {
4639    match source {
4640        ImageSource::Ref(image_ref) => Some(image_ref.clone()),
4641        ImageSource::Url(_url) => {
4642            // TODO: Look up in ImageCache
4643            // For now, CSS url() images are not yet supported
4644            None
4645        }
4646        ImageSource::Data(_) | ImageSource::Svg(_) | ImageSource::Placeholder(_) => {
4647            // TODO: Decode raw data / SVG to ImageRef
4648            None
4649        }
4650    }
4651}
4652
4653/// Get the bounds of a display list item, if it has spatial extent.
4654fn get_display_item_bounds(item: &DisplayListItem) -> Option<WindowLogicalRect> {
4655    match item {
4656        DisplayListItem::Rect { bounds, .. } => Some(*bounds),
4657        DisplayListItem::SelectionRect { bounds, .. } => Some(*bounds),
4658        DisplayListItem::CursorRect { bounds, .. } => Some(*bounds),
4659        DisplayListItem::Border { bounds, .. } => Some(*bounds),
4660        DisplayListItem::TextLayout { bounds, .. } => Some(*bounds),
4661        DisplayListItem::Text { clip_rect, .. } => Some(*clip_rect),
4662        DisplayListItem::Underline { bounds, .. } => Some(*bounds),
4663        DisplayListItem::Strikethrough { bounds, .. } => Some(*bounds),
4664        DisplayListItem::Overline { bounds, .. } => Some(*bounds),
4665        DisplayListItem::Image { bounds, .. } => Some(*bounds),
4666        DisplayListItem::ScrollBar { bounds, .. } => Some(*bounds),
4667        DisplayListItem::ScrollBarStyled { info } => Some(info.bounds),
4668        DisplayListItem::PushClip { bounds, .. } => Some(*bounds),
4669        DisplayListItem::PushScrollFrame { clip_bounds, .. } => Some(*clip_bounds),
4670        DisplayListItem::HitTestArea { bounds, .. } => Some(*bounds),
4671        DisplayListItem::PushStackingContext { bounds, .. } => Some(*bounds),
4672        DisplayListItem::VirtualView { bounds, .. } => Some(*bounds),
4673        _ => None,
4674    }
4675}
4676
4677/// Clip a display list item to page bounds and offset to page-relative coordinates.
4678/// Returns None if the item is completely outside the page bounds.
4679fn clip_and_offset_display_item(
4680    item: &DisplayListItem,
4681    page_top: f32,
4682    page_bottom: f32,
4683) -> Option<DisplayListItem> {
4684    match item {
4685        DisplayListItem::Rect {
4686            bounds,
4687            color,
4688            border_radius,
4689        } => clip_rect_item(bounds.into_inner(), *color, *border_radius, page_top, page_bottom),
4690
4691        DisplayListItem::Border {
4692            bounds,
4693            widths,
4694            colors,
4695            styles,
4696            border_radius,
4697        } => clip_border_item(
4698            bounds.into_inner(),
4699            *widths,
4700            *colors,
4701            *styles,
4702            border_radius.clone(),
4703            page_top,
4704            page_bottom,
4705        ),
4706
4707        DisplayListItem::SelectionRect {
4708            bounds,
4709            border_radius,
4710            color,
4711        } => clip_selection_rect_item(bounds.into_inner(), *border_radius, *color, page_top, page_bottom),
4712
4713        DisplayListItem::CursorRect { bounds, color } => {
4714            clip_cursor_rect_item(bounds.into_inner(), *color, page_top, page_bottom)
4715        }
4716
4717        DisplayListItem::Image { bounds, image, border_radius } => {
4718            clip_image_item(bounds.into_inner(), image.clone(), *border_radius, page_top, page_bottom)
4719        }
4720
4721        DisplayListItem::TextLayout {
4722            layout,
4723            bounds,
4724            font_hash,
4725            font_size_px,
4726            color,
4727        } => clip_text_layout_item(
4728            layout,
4729            bounds.into_inner(),
4730            *font_hash,
4731            *font_size_px,
4732            *color,
4733            page_top,
4734            page_bottom,
4735        ),
4736
4737        DisplayListItem::Text {
4738            glyphs,
4739            font_hash,
4740            font_size_px,
4741            color,
4742            clip_rect,
4743            ..
4744        } => clip_text_item(
4745            glyphs,
4746            *font_hash,
4747            *font_size_px,
4748            *color,
4749            clip_rect.into_inner(),
4750            page_top,
4751            page_bottom,
4752        ),
4753
4754        DisplayListItem::Underline {
4755            bounds,
4756            color,
4757            thickness,
4758        } => clip_text_decoration_item(
4759            bounds.into_inner(),
4760            *color,
4761            *thickness,
4762            TextDecorationType::Underline,
4763            page_top,
4764            page_bottom,
4765        ),
4766
4767        DisplayListItem::Strikethrough {
4768            bounds,
4769            color,
4770            thickness,
4771        } => clip_text_decoration_item(
4772            bounds.into_inner(),
4773            *color,
4774            *thickness,
4775            TextDecorationType::Strikethrough,
4776            page_top,
4777            page_bottom,
4778        ),
4779
4780        DisplayListItem::Overline {
4781            bounds,
4782            color,
4783            thickness,
4784        } => clip_text_decoration_item(
4785            bounds.into_inner(),
4786            *color,
4787            *thickness,
4788            TextDecorationType::Overline,
4789            page_top,
4790            page_bottom,
4791        ),
4792
4793        DisplayListItem::ScrollBar {
4794            bounds,
4795            color,
4796            orientation,
4797            opacity_key,
4798            hit_id,
4799        } => clip_scrollbar_item(
4800            bounds.into_inner(),
4801            *color,
4802            *orientation,
4803            *opacity_key,
4804            *hit_id,
4805            page_top,
4806            page_bottom,
4807        ),
4808
4809        DisplayListItem::HitTestArea { bounds, tag } => {
4810            clip_hit_test_area_item(bounds.into_inner(), *tag, page_top, page_bottom)
4811        }
4812
4813        DisplayListItem::VirtualView {
4814            child_dom_id,
4815            bounds,
4816            clip_rect,
4817        } => clip_virtual_view_item(*child_dom_id, bounds.into_inner(), clip_rect.into_inner(), page_top, page_bottom),
4818
4819        // ScrollBarStyled - clip based on overall bounds
4820        DisplayListItem::ScrollBarStyled { info } => {
4821            let bounds = info.bounds;
4822            if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
4823                None
4824            } else {
4825                // Clone and offset all the internal bounds
4826                let mut clipped_info = (**info).clone();
4827                let y_offset = -page_top;
4828                clipped_info.bounds = offset_rect_y(clipped_info.bounds.into_inner(), y_offset).into();
4829                clipped_info.track_bounds = offset_rect_y(clipped_info.track_bounds.into_inner(), y_offset).into();
4830                clipped_info.thumb_bounds = offset_rect_y(clipped_info.thumb_bounds.into_inner(), y_offset).into();
4831                if let Some(b) = clipped_info.button_decrement_bounds {
4832                    clipped_info.button_decrement_bounds = Some(offset_rect_y(b.into_inner(), y_offset).into());
4833                }
4834                if let Some(b) = clipped_info.button_increment_bounds {
4835                    clipped_info.button_increment_bounds = Some(offset_rect_y(b.into_inner(), y_offset).into());
4836                }
4837                Some(DisplayListItem::ScrollBarStyled {
4838                    info: Box::new(clipped_info),
4839                })
4840            }
4841        }
4842
4843        // State management items - skip for now (would need proper per-page tracking)
4844        DisplayListItem::PushClip { .. }
4845        | DisplayListItem::PopClip
4846        | DisplayListItem::PushScrollFrame { .. }
4847        | DisplayListItem::PopScrollFrame
4848        | DisplayListItem::PushStackingContext { .. }
4849        | DisplayListItem::PopStackingContext
4850        | DisplayListItem::VirtualViewPlaceholder { .. } => None,
4851
4852        // Gradient items - simple bounds check
4853        DisplayListItem::LinearGradient {
4854            bounds,
4855            gradient,
4856            border_radius,
4857        } => {
4858            if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
4859                None
4860            } else {
4861                Some(DisplayListItem::LinearGradient {
4862                    bounds: offset_rect_y(bounds.into_inner(), -page_top).into(),
4863                    gradient: gradient.clone(),
4864                    border_radius: *border_radius,
4865                })
4866            }
4867        }
4868        DisplayListItem::RadialGradient {
4869            bounds,
4870            gradient,
4871            border_radius,
4872        } => {
4873            if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
4874                None
4875            } else {
4876                Some(DisplayListItem::RadialGradient {
4877                    bounds: offset_rect_y(bounds.into_inner(), -page_top).into(),
4878                    gradient: gradient.clone(),
4879                    border_radius: *border_radius,
4880                })
4881            }
4882        }
4883        DisplayListItem::ConicGradient {
4884            bounds,
4885            gradient,
4886            border_radius,
4887        } => {
4888            if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
4889                None
4890            } else {
4891                Some(DisplayListItem::ConicGradient {
4892                    bounds: offset_rect_y(bounds.into_inner(), -page_top).into(),
4893                    gradient: gradient.clone(),
4894                    border_radius: *border_radius,
4895                })
4896            }
4897        }
4898
4899        // BoxShadow - simple bounds check
4900        DisplayListItem::BoxShadow {
4901            bounds,
4902            shadow,
4903            border_radius,
4904        } => {
4905            if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
4906                None
4907            } else {
4908                Some(DisplayListItem::BoxShadow {
4909                    bounds: offset_rect_y(bounds.into_inner(), -page_top).into(),
4910                    shadow: *shadow,
4911                    border_radius: *border_radius,
4912                })
4913            }
4914        }
4915
4916        // Filter effects - skip for now (would need proper per-page tracking)
4917        DisplayListItem::PushFilter { .. }
4918        | DisplayListItem::PopFilter
4919        | DisplayListItem::PushBackdropFilter { .. }
4920        | DisplayListItem::PopBackdropFilter
4921        | DisplayListItem::PushOpacity { .. }
4922        | DisplayListItem::PopOpacity
4923        | DisplayListItem::PushReferenceFrame { .. }
4924        | DisplayListItem::PopReferenceFrame
4925        | DisplayListItem::PushTextShadow { .. }
4926        | DisplayListItem::PopTextShadow
4927        | DisplayListItem::PushImageMaskClip { .. }
4928        | DisplayListItem::PopImageMaskClip => None,
4929    }
4930}
4931
4932// Helper functions for clip_and_offset_display_item
4933
4934/// Internal enum for text decoration type dispatch
4935#[derive(Debug, Clone, Copy)]
4936enum TextDecorationType {
4937    Underline,
4938    Strikethrough,
4939    Overline,
4940}
4941
4942/// Clips a filled rectangle to page bounds.
4943fn clip_rect_item(
4944    bounds: LogicalRect,
4945    color: ColorU,
4946    border_radius: BorderRadius,
4947    page_top: f32,
4948    page_bottom: f32,
4949) -> Option<DisplayListItem> {
4950    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::Rect {
4951        bounds: clipped.into(),
4952        color,
4953        border_radius,
4954    })
4955}
4956
4957/// Clips a border to page bounds, hiding top/bottom borders when clipped.
4958fn clip_border_item(
4959    bounds: LogicalRect,
4960    widths: StyleBorderWidths,
4961    colors: StyleBorderColors,
4962    styles: StyleBorderStyles,
4963    border_radius: StyleBorderRadius,
4964    page_top: f32,
4965    page_bottom: f32,
4966) -> Option<DisplayListItem> {
4967    let original_bounds = bounds;
4968    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| {
4969        let new_widths = adjust_border_widths_for_clipping(
4970            widths,
4971            original_bounds,
4972            clipped,
4973            page_top,
4974            page_bottom,
4975        );
4976        DisplayListItem::Border {
4977            bounds: clipped.into(),
4978            widths: new_widths,
4979            colors,
4980            styles,
4981            border_radius,
4982        }
4983    })
4984}
4985
4986/// Adjusts border widths when a border is clipped at page boundaries.
4987/// Hides top border if clipped at top, bottom border if clipped at bottom.
4988fn adjust_border_widths_for_clipping(
4989    mut widths: StyleBorderWidths,
4990    original_bounds: LogicalRect,
4991    clipped: LogicalRect,
4992    page_top: f32,
4993    page_bottom: f32,
4994) -> StyleBorderWidths {
4995    // Hide top border if we clipped the top
4996    if clipped.origin.y > 0.0 && original_bounds.origin.y < page_top {
4997        widths.top = None;
4998    }
4999
5000    // Hide bottom border if we clipped the bottom
5001    let original_bottom = original_bounds.origin.y + original_bounds.size.height;
5002    let clipped_bottom = clipped.origin.y + clipped.size.height;
5003    if original_bottom > page_bottom && clipped_bottom >= page_bottom - page_top - 1.0 {
5004        widths.bottom = None;
5005    }
5006
5007    widths
5008}
5009
5010/// Clips a selection rectangle to page bounds.
5011fn clip_selection_rect_item(
5012    bounds: LogicalRect,
5013    border_radius: BorderRadius,
5014    color: ColorU,
5015    page_top: f32,
5016    page_bottom: f32,
5017) -> Option<DisplayListItem> {
5018    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::SelectionRect {
5019        bounds: clipped.into(),
5020        border_radius,
5021        color,
5022    })
5023}
5024
5025/// Clips a cursor rectangle to page bounds.
5026fn clip_cursor_rect_item(
5027    bounds: LogicalRect,
5028    color: ColorU,
5029    page_top: f32,
5030    page_bottom: f32,
5031) -> Option<DisplayListItem> {
5032    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::CursorRect {
5033        bounds: clipped.into(),
5034        color,
5035    })
5036}
5037
5038/// Clips an image to page bounds if it overlaps the page.
5039fn clip_image_item(
5040    bounds: LogicalRect,
5041    image: ImageRef,
5042    border_radius: BorderRadius,
5043    page_top: f32,
5044    page_bottom: f32,
5045) -> Option<DisplayListItem> {
5046    if !rect_intersects(&bounds, page_top, page_bottom) {
5047        return None;
5048    }
5049    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::Image {
5050        bounds: clipped.into(),
5051        image,
5052        border_radius,
5053    })
5054}
5055
5056/// Clips a text layout block to page bounds, filtering individual text items.
5057fn clip_text_layout_item(
5058    layout: &Arc<dyn std::any::Any + Send + Sync>,
5059    bounds: LogicalRect,
5060    font_hash: FontHash,
5061    font_size_px: f32,
5062    color: ColorU,
5063    page_top: f32,
5064    page_bottom: f32,
5065) -> Option<DisplayListItem> {
5066    if !rect_intersects(&bounds, page_top, page_bottom) {
5067        return None;
5068    }
5069
5070    // Try to downcast and filter UnifiedLayout items
5071    #[cfg(feature = "text_layout")]
5072    if let Some(unified_layout) = layout.downcast_ref::<crate::text3::cache::UnifiedLayout>() {
5073        return clip_unified_layout(
5074            unified_layout,
5075            bounds,
5076            font_hash,
5077            font_size_px,
5078            color,
5079            page_top,
5080            page_bottom,
5081        );
5082    }
5083
5084    // Fallback: simple bounds offset (legacy behavior)
5085    Some(DisplayListItem::TextLayout {
5086        layout: layout.clone(),
5087        bounds: offset_rect_y(bounds, -page_top).into(),
5088        font_hash,
5089        font_size_px,
5090        color,
5091    })
5092}
5093
5094/// Clips a UnifiedLayout by filtering items to those on the current page.
5095#[cfg(feature = "text_layout")]
5096fn clip_unified_layout(
5097    unified_layout: &crate::text3::cache::UnifiedLayout,
5098    bounds: LogicalRect,
5099    font_hash: FontHash,
5100    font_size_px: f32,
5101    color: ColorU,
5102    page_top: f32,
5103    page_bottom: f32,
5104) -> Option<DisplayListItem> {
5105    let layout_origin_y = bounds.origin.y;
5106    let layout_origin_x = bounds.origin.x;
5107
5108    // Filter items whose center falls within this page
5109    let filtered_items: Vec<_> = unified_layout
5110        .items
5111        .iter()
5112        .filter(|item| item_center_on_page(item, layout_origin_y, page_top, page_bottom))
5113        .cloned()
5114        .collect();
5115
5116    if filtered_items.is_empty() {
5117        return None;
5118    }
5119
5120    // Calculate new origin for page-relative positioning
5121    let new_origin_y = (layout_origin_y - page_top).max(0.0);
5122
5123    // Transform items to page-relative coordinates and calculate bounds
5124    let (offset_items, min_y, max_y, max_width) =
5125        transform_items_to_page_coords(filtered_items, layout_origin_y, page_top, new_origin_y);
5126
5127    let new_layout = crate::text3::cache::UnifiedLayout {
5128        items: offset_items,
5129        overflow: unified_layout.overflow.clone(),
5130    };
5131
5132    let new_bounds = LogicalRect {
5133        origin: LogicalPosition {
5134            x: layout_origin_x,
5135            y: new_origin_y,
5136        },
5137        size: LogicalSize {
5138            width: max_width.max(bounds.size.width),
5139            height: (max_y - min_y.min(0.0)).max(0.0),
5140        },
5141    };
5142
5143    Some(DisplayListItem::TextLayout {
5144        layout: Arc::new(new_layout) as Arc<dyn std::any::Any + Send + Sync>,
5145        bounds: new_bounds.into(),
5146        font_hash,
5147        font_size_px,
5148        color,
5149    })
5150}
5151
5152/// Checks if an item's center point falls within the page bounds.
5153#[cfg(feature = "text_layout")]
5154fn item_center_on_page(
5155    item: &crate::text3::cache::PositionedItem,
5156    layout_origin_y: f32,
5157    page_top: f32,
5158    page_bottom: f32,
5159) -> bool {
5160    let item_y_absolute = layout_origin_y + item.position.y;
5161    let item_height = item.item.bounds().height;
5162    let item_center_y = item_y_absolute + (item_height / 2.0);
5163    item_center_y >= page_top && item_center_y < page_bottom
5164}
5165
5166/// Transforms filtered items to page-relative coordinates.
5167/// Returns (items, min_y, max_y, max_width).
5168#[cfg(feature = "text_layout")]
5169fn transform_items_to_page_coords(
5170    items: Vec<crate::text3::cache::PositionedItem>,
5171    layout_origin_y: f32,
5172    page_top: f32,
5173    new_origin_y: f32,
5174) -> (Vec<crate::text3::cache::PositionedItem>, f32, f32, f32) {
5175    let mut min_y = f32::MAX;
5176    let mut max_y = f32::MIN;
5177    let mut max_width = 0.0f32;
5178
5179    let offset_items: Vec<_> = items
5180        .into_iter()
5181        .map(|mut item| {
5182            let abs_y = layout_origin_y + item.position.y;
5183            let page_y = abs_y - page_top;
5184            let new_item_y = page_y - new_origin_y;
5185
5186            let item_bounds = item.item.bounds();
5187            min_y = min_y.min(new_item_y);
5188            max_y = max_y.max(new_item_y + item_bounds.height);
5189            max_width = max_width.max(item.position.x + item_bounds.width);
5190
5191            item.position.y = new_item_y;
5192            item
5193        })
5194        .collect();
5195
5196    (offset_items, min_y, max_y, max_width)
5197}
5198
5199/// Clips a text glyph run to page bounds, filtering individual glyphs.
5200fn clip_text_item(
5201    glyphs: &[GlyphInstance],
5202    font_hash: FontHash,
5203    font_size_px: f32,
5204    color: ColorU,
5205    clip_rect: LogicalRect,
5206    page_top: f32,
5207    page_bottom: f32,
5208) -> Option<DisplayListItem> {
5209    if !rect_intersects(&clip_rect, page_top, page_bottom) {
5210        return None;
5211    }
5212
5213    // Filter glyphs using center-point decision (baseline position)
5214    let page_glyphs: Vec<_> = glyphs
5215        .iter()
5216        .filter(|g| g.point.y >= page_top && g.point.y < page_bottom)
5217        .map(|g| GlyphInstance {
5218            index: g.index,
5219            point: LogicalPosition {
5220                x: g.point.x,
5221                y: g.point.y - page_top,
5222            },
5223            size: g.size,
5224        })
5225        .collect();
5226
5227    if page_glyphs.is_empty() {
5228        return None;
5229    }
5230
5231    Some(DisplayListItem::Text {
5232        glyphs: page_glyphs,
5233        font_hash,
5234        font_size_px,
5235        color,
5236        clip_rect: offset_rect_y(clip_rect, -page_top).into(),
5237        source_node_index: None,
5238    })
5239}
5240
5241/// Clips a text decoration (underline, strikethrough, or overline) to page bounds.
5242fn clip_text_decoration_item(
5243    bounds: LogicalRect,
5244    color: ColorU,
5245    thickness: f32,
5246    decoration_type: TextDecorationType,
5247    page_top: f32,
5248    page_bottom: f32,
5249) -> Option<DisplayListItem> {
5250    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| match decoration_type {
5251        TextDecorationType::Underline => DisplayListItem::Underline {
5252            bounds: clipped.into(),
5253            color,
5254            thickness,
5255        },
5256        TextDecorationType::Strikethrough => DisplayListItem::Strikethrough {
5257            bounds: clipped.into(),
5258            color,
5259            thickness,
5260        },
5261        TextDecorationType::Overline => DisplayListItem::Overline {
5262            bounds: clipped.into(),
5263            color,
5264            thickness,
5265        },
5266    })
5267}
5268
5269/// Clips a scrollbar to page bounds.
5270fn clip_scrollbar_item(
5271    bounds: LogicalRect,
5272    color: ColorU,
5273    orientation: ScrollbarOrientation,
5274    opacity_key: Option<OpacityKey>,
5275    hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
5276    page_top: f32,
5277    page_bottom: f32,
5278) -> Option<DisplayListItem> {
5279    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::ScrollBar {
5280        bounds: clipped.into(),
5281        color,
5282        orientation,
5283        opacity_key,
5284        hit_id,
5285    })
5286}
5287
5288/// Clips a hit test area to page bounds.
5289fn clip_hit_test_area_item(
5290    bounds: LogicalRect,
5291    tag: DisplayListTagId,
5292    page_top: f32,
5293    page_bottom: f32,
5294) -> Option<DisplayListItem> {
5295    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::HitTestArea {
5296        bounds: clipped.into(),
5297        tag,
5298    })
5299}
5300
5301/// Clips a virtualized view to page bounds.
5302fn clip_virtual_view_item(
5303    child_dom_id: DomId,
5304    bounds: LogicalRect,
5305    clip_rect: LogicalRect,
5306    page_top: f32,
5307    page_bottom: f32,
5308) -> Option<DisplayListItem> {
5309    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::VirtualView {
5310        child_dom_id,
5311        bounds: clipped.into(),
5312        clip_rect: offset_rect_y(clip_rect, -page_top).into(),
5313    })
5314}
5315
5316/// Clip a rectangle to page bounds and offset to page-relative coordinates.
5317/// Returns None if the rectangle is completely outside the page.
5318fn clip_rect_bounds(bounds: LogicalRect, page_top: f32, page_bottom: f32) -> Option<LogicalRect> {
5319    let item_top = bounds.origin.y;
5320    let item_bottom = bounds.origin.y + bounds.size.height;
5321
5322    // Check if completely outside page
5323    if item_bottom <= page_top || item_top >= page_bottom {
5324        return None;
5325    }
5326
5327    // Calculate clipped bounds
5328    let clipped_top = item_top.max(page_top);
5329    let clipped_bottom = item_bottom.min(page_bottom);
5330    let clipped_height = clipped_bottom - clipped_top;
5331
5332    // Offset to page-relative coordinates
5333    let page_relative_y = clipped_top - page_top;
5334
5335    Some(LogicalRect {
5336        origin: LogicalPosition {
5337            x: bounds.origin.x,
5338            y: page_relative_y,
5339        },
5340        size: LogicalSize {
5341            width: bounds.size.width,
5342            height: clipped_height,
5343        },
5344    })
5345}
5346
5347/// Check if a rectangle intersects the page bounds.
5348fn rect_intersects(bounds: &LogicalRect, page_top: f32, page_bottom: f32) -> bool {
5349    let item_top = bounds.origin.y;
5350    let item_bottom = bounds.origin.y + bounds.size.height;
5351    item_bottom > page_top && item_top < page_bottom
5352}
5353
5354/// Offset a rectangle's Y coordinate.
5355fn offset_rect_y(bounds: LogicalRect, offset_y: f32) -> LogicalRect {
5356    LogicalRect {
5357        origin: LogicalPosition {
5358            x: bounds.origin.x,
5359            y: bounds.origin.y + offset_y,
5360        },
5361        size: bounds.size,
5362    }
5363}
5364
5365// Slicer based pagination: "Infinite Canvas with Clipping"
5366//
5367// This approach treats pages as "viewports" into a single infinite canvas:
5368//
5369// 1. Layout generates ONE display list on an infinite vertical strip
5370// 2. Each page is a clip rectangle that "views" a portion of that strip
5371// 3. Items that span page boundaries are clipped and appear on BOTH pages
5372
5373use azul_css::props::layout::fragmentation::{BreakInside, PageBreak};
5374
5375use crate::solver3::pagination::{
5376    HeaderFooterConfig, MarginBoxContent, PageInfo, TableHeaderInfo, TableHeaderTracker,
5377};
5378
5379/// Configuration for the slicer-based pagination.
5380#[derive(Debug, Clone, Default)]
5381pub struct SlicerConfig {
5382    /// Height of each page's content area (excludes margins, headers, footers)
5383    pub page_content_height: f32,
5384    /// Height of "dead zone" between pages (for margins, headers, footers)
5385    /// This represents space that content should NOT overlap with
5386    pub page_gap: f32,
5387    /// Whether to clip items that span page boundaries (true) or push them to next page (false)
5388    pub allow_clipping: bool,
5389    /// Header and footer configuration
5390    pub header_footer: HeaderFooterConfig,
5391    /// Width of the page content area (for centering headers/footers)
5392    pub page_width: f32,
5393    /// Table headers that need repetition across pages
5394    pub table_headers: TableHeaderTracker,
5395}
5396
5397impl SlicerConfig {
5398    /// Create a simple slicer config with no gaps between pages.
5399    pub fn simple(page_height: f32) -> Self {
5400        Self {
5401            page_content_height: page_height,
5402            page_gap: 0.0,
5403            allow_clipping: true,
5404            header_footer: HeaderFooterConfig::default(),
5405            page_width: 595.0, // Default A4 width in points
5406            table_headers: TableHeaderTracker::default(),
5407        }
5408    }
5409
5410    /// Create a slicer config with margins/gaps between pages.
5411    pub fn with_gap(page_height: f32, gap: f32) -> Self {
5412        Self {
5413            page_content_height: page_height,
5414            page_gap: gap,
5415            allow_clipping: true,
5416            header_footer: HeaderFooterConfig::default(),
5417            page_width: 595.0,
5418            table_headers: TableHeaderTracker::default(),
5419        }
5420    }
5421
5422    /// Add header/footer configuration.
5423    pub fn with_header_footer(mut self, config: HeaderFooterConfig) -> Self {
5424        self.header_footer = config;
5425        self
5426    }
5427
5428    /// Set the page width (for header/footer positioning).
5429    pub fn with_page_width(mut self, width: f32) -> Self {
5430        self.page_width = width;
5431        self
5432    }
5433
5434    /// Add table headers for repetition.
5435    pub fn with_table_headers(mut self, tracker: TableHeaderTracker) -> Self {
5436        self.table_headers = tracker;
5437        self
5438    }
5439
5440    /// Register a single table header.
5441    pub fn register_table_header(&mut self, info: TableHeaderInfo) {
5442        self.table_headers.register_table_header(info);
5443    }
5444
5445    /// The total height of a page "slot" including the gap.
5446    pub fn page_slot_height(&self) -> f32 {
5447        self.page_content_height + self.page_gap
5448    }
5449
5450    /// Calculate which page a Y coordinate falls on.
5451    pub fn page_for_y(&self, y: f32) -> usize {
5452        if self.page_slot_height() <= 0.0 {
5453            return 0;
5454        }
5455        (y / self.page_slot_height()).floor() as usize
5456    }
5457
5458    /// Get the Y range for a specific page (in infinite canvas coordinates).
5459    pub fn page_bounds(&self, page_index: usize) -> (f32, f32) {
5460        let start = page_index as f32 * self.page_slot_height();
5461        let end = start + self.page_content_height;
5462        (start, end)
5463    }
5464}
5465
5466/// Paginate with CSS break property support.
5467///
5468/// This function calculates page boundaries based on CSS break-before, break-after,
5469/// and break-inside properties, then clips content to those boundaries.
5470///
5471/// **Key insight**: Items are NEVER shifted. Instead, page boundaries are adjusted
5472/// to honor break properties.
5473pub fn paginate_display_list_with_slicer_and_breaks(
5474    full_display_list: DisplayList,
5475    config: &SlicerConfig,
5476) -> Result<Vec<DisplayList>> {
5477    if config.page_content_height <= 0.0 || config.page_content_height >= f32::MAX {
5478        return Ok(vec![full_display_list]);
5479    }
5480
5481    // Calculate base header/footer space (used for pages that show headers/footers)
5482    let base_header_space = if config.header_footer.show_header {
5483        config.header_footer.header_height
5484    } else {
5485        0.0
5486    };
5487    let base_footer_space = if config.header_footer.show_footer {
5488        config.header_footer.footer_height
5489    } else {
5490        0.0
5491    };
5492
5493    // Calculate effective heights for different page types
5494    let normal_page_content_height =
5495        config.page_content_height - base_header_space - base_footer_space;
5496    let first_page_content_height = if config.header_footer.skip_first_page {
5497        // First page has full height when skipping headers/footers
5498        config.page_content_height
5499    } else {
5500        normal_page_content_height
5501    };
5502
5503    // Step 1: Calculate page break positions based on CSS properties
5504    //
5505    // Instead of using regular intervals, we calculate where page breaks
5506    // should occur based on:
5507    //
5508    // - break-before: always → force break before this item
5509    // - break-after: always → force break after this item
5510    // - break-inside: avoid → don't break inside this item (push to next page if needed)
5511
5512    let page_breaks = calculate_page_break_positions(
5513        &full_display_list,
5514        first_page_content_height,
5515        normal_page_content_height,
5516    );
5517
5518    let num_pages = page_breaks.len();
5519
5520    // Create per-page display lists by slicing the master list
5521    let mut pages: Vec<DisplayList> = Vec::with_capacity(num_pages);
5522
5523    for (page_idx, &(content_start_y, content_end_y)) in page_breaks.iter().enumerate() {
5524        // Generate page info for header/footer content
5525        let page_info = PageInfo::new(page_idx + 1, num_pages);
5526
5527        // Calculate per-page header/footer space
5528        let skip_this_page = config.header_footer.skip_first_page && page_info.is_first;
5529        let header_space = if config.header_footer.show_header && !skip_this_page {
5530            config.header_footer.header_height
5531        } else {
5532            0.0
5533        };
5534        let footer_space = if config.header_footer.show_footer && !skip_this_page {
5535            config.header_footer.footer_height
5536        } else {
5537            0.0
5538        };
5539
5540        let _ = footer_space; // Currently unused but reserved for future
5541
5542        let mut page_items = Vec::new();
5543        let mut page_node_mapping = Vec::new();
5544
5545        // 1. Add header if enabled
5546        if config.header_footer.show_header && !skip_this_page {
5547            let header_text = config.header_footer.header_text(page_info);
5548            if !header_text.is_empty() {
5549                let header_items = generate_text_display_items(
5550                    &header_text,
5551                    LogicalRect {
5552                        origin: LogicalPosition { x: 0.0, y: 0.0 },
5553                        size: LogicalSize {
5554                            width: config.page_width,
5555                            height: config.header_footer.header_height,
5556                        },
5557                    },
5558                    config.header_footer.font_size,
5559                    config.header_footer.text_color,
5560                    TextAlignment::Center,
5561                );
5562                for item in header_items {
5563                    page_items.push(item);
5564                    page_node_mapping.push(None);
5565                }
5566            }
5567        }
5568
5569        // 2. Inject repeated table headers (if any)
5570        let repeated_headers = config.table_headers.get_repeated_headers_for_page(
5571            page_idx,
5572            content_start_y,
5573            content_end_y,
5574        );
5575
5576        let mut thead_total_height = 0.0f32;
5577        for (y_offset_from_page_top, thead_items, thead_height) in repeated_headers {
5578            let thead_y = header_space + y_offset_from_page_top;
5579            for item in thead_items {
5580                let translated_item = offset_display_item_y(item, thead_y);
5581                page_items.push(translated_item);
5582                page_node_mapping.push(None);
5583            }
5584            thead_total_height = thead_total_height.max(thead_height);
5585        }
5586
5587        // 3. Calculate content offset (after header and repeated table headers)
5588        let content_y_offset = header_space + thead_total_height;
5589
5590        // 4. Slice and offset content items (skip fixed-position items, they are added in step 4b)
5591        for (item_idx, item) in full_display_list.items.iter().enumerate() {
5592            // Skip items that belong to fixed-position elements (they are replicated separately)
5593            let is_fixed = full_display_list.fixed_position_item_ranges.iter()
5594                .any(|&(start, end)| item_idx >= start && item_idx < end);
5595            if is_fixed {
5596                continue;
5597            }
5598            if let Some(clipped_item) =
5599                clip_and_offset_display_item(item, content_start_y, content_end_y)
5600            {
5601                let final_item = if content_y_offset > 0.0 {
5602                    offset_display_item_y(&clipped_item, content_y_offset)
5603                } else {
5604                    clipped_item
5605                };
5606                page_items.push(final_item);
5607                let node_mapping = full_display_list
5608                    .node_mapping
5609                    .get(item_idx)
5610                    .copied()
5611                    .flatten();
5612                page_node_mapping.push(node_mapping);
5613            }
5614        }
5615
5616        // 4b. Replicate fixed-position items on every page (CSS Positioned Layout §2.1)
5617        // Fixed-position boxes are fixed relative to the page box, so they appear
5618        // at the same position on every page without Y-offset adjustment.
5619        for &(start, end) in &full_display_list.fixed_position_item_ranges {
5620            for item_idx in start..end {
5621                if let Some(item) = full_display_list.items.get(item_idx) {
5622                    let final_item = if content_y_offset > 0.0 {
5623                        offset_display_item_y(item, content_y_offset)
5624                    } else {
5625                        item.clone()
5626                    };
5627                    page_items.push(final_item);
5628                    let node_mapping = full_display_list
5629                        .node_mapping
5630                        .get(item_idx)
5631                        .copied()
5632                        .flatten();
5633                    page_node_mapping.push(node_mapping);
5634                }
5635            }
5636        }
5637
5638        // 5. Add footer if enabled
5639        if config.header_footer.show_footer && !skip_this_page {
5640            let footer_text = config.header_footer.footer_text(page_info);
5641            if !footer_text.is_empty() {
5642                let footer_y = config.page_content_height - config.header_footer.footer_height;
5643                let footer_items = generate_text_display_items(
5644                    &footer_text,
5645                    LogicalRect {
5646                        origin: LogicalPosition {
5647                            x: 0.0,
5648                            y: footer_y,
5649                        },
5650                        size: LogicalSize {
5651                            width: config.page_width,
5652                            height: config.header_footer.footer_height,
5653                        },
5654                    },
5655                    config.header_footer.font_size,
5656                    config.header_footer.text_color,
5657                    TextAlignment::Center,
5658                );
5659                for item in footer_items {
5660                    page_items.push(item);
5661                    page_node_mapping.push(None);
5662                }
5663            }
5664        }
5665
5666        pages.push(DisplayList {
5667            items: page_items,
5668            node_mapping: page_node_mapping,
5669            forced_page_breaks: Vec::new(),
5670            fixed_position_item_ranges: Vec::new(), // Already handled during pagination
5671        });
5672    }
5673
5674    // Ensure at least one page
5675    if pages.is_empty() {
5676        pages.push(DisplayList::default());
5677    }
5678
5679    Ok(pages)
5680}
5681
5682/// Calculate page break positions respecting CSS forced page breaks.
5683///
5684/// Returns a vector of (start_y, end_y) tuples representing each page's content bounds.
5685///
5686/// This function uses the `forced_page_breaks` from the DisplayList to insert
5687/// page breaks at positions specified by CSS `break-before: always` and `break-after: always`.
5688/// Regular page breaks still occur at normal intervals when no forced break is present.
5689fn calculate_page_break_positions(
5690    display_list: &DisplayList,
5691    first_page_height: f32,
5692    normal_page_height: f32,
5693) -> Vec<(f32, f32)> {
5694    let total_height = calculate_display_list_height(display_list);
5695
5696    if total_height <= 0.0 || first_page_height <= 0.0 {
5697        return vec![(0.0, total_height.max(first_page_height))];
5698    }
5699
5700    // Collect all potential break points: forced breaks + regular interval breaks
5701    let mut break_points: Vec<f32> = Vec::new();
5702
5703    // Add forced page breaks from the display list (from CSS break-before/break-after)
5704    for &forced_break_y in &display_list.forced_page_breaks {
5705        if forced_break_y > 0.0 && forced_break_y < total_height {
5706            break_points.push(forced_break_y);
5707        }
5708    }
5709
5710    // Generate regular interval break points
5711    let mut y = first_page_height;
5712    while y < total_height {
5713        break_points.push(y);
5714        y += normal_page_height;
5715    }
5716
5717    // Sort and deduplicate break points
5718    break_points.sort_by(|a, b| a.partial_cmp(b).unwrap());
5719    break_points.dedup_by(|a, b| (*a - *b).abs() < 1.0); // Merge breaks within 1px
5720
5721    // Convert break points to page ranges
5722    let mut page_breaks: Vec<(f32, f32)> = Vec::new();
5723    let mut page_start = 0.0f32;
5724
5725    for break_y in break_points {
5726        if break_y > page_start {
5727            page_breaks.push((page_start, break_y));
5728            page_start = break_y;
5729        }
5730    }
5731
5732    // Add final page if there's remaining content
5733    if page_start < total_height {
5734        page_breaks.push((page_start, total_height));
5735    }
5736
5737    // Ensure at least one page
5738    if page_breaks.is_empty() {
5739        page_breaks.push((0.0, total_height.max(first_page_height)));
5740    }
5741
5742    page_breaks
5743}
5744
5745/// Text alignment for generated header/footer text.
5746#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5747pub enum TextAlignment {
5748    Left,
5749    Center,
5750    Right,
5751}
5752
5753/// Helper to offset all Y coordinates of a display item.
5754fn offset_display_item_y(item: &DisplayListItem, y_offset: f32) -> DisplayListItem {
5755    if y_offset == 0.0 {
5756        return item.clone();
5757    }
5758
5759    match item {
5760        DisplayListItem::Rect {
5761            bounds,
5762            color,
5763            border_radius,
5764        } => DisplayListItem::Rect {
5765            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5766            color: *color,
5767            border_radius: *border_radius,
5768        },
5769        DisplayListItem::Border {
5770            bounds,
5771            widths,
5772            colors,
5773            styles,
5774            border_radius,
5775        } => DisplayListItem::Border {
5776            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5777            widths: widths.clone(),
5778            colors: *colors,
5779            styles: *styles,
5780            border_radius: border_radius.clone(),
5781        },
5782        DisplayListItem::Text {
5783            glyphs,
5784            font_hash,
5785            font_size_px,
5786            color,
5787            clip_rect,
5788            ..
5789        } => {
5790            let offset_glyphs: Vec<GlyphInstance> = glyphs
5791                .iter()
5792                .map(|g| GlyphInstance {
5793                    index: g.index,
5794                    point: LogicalPosition {
5795                        x: g.point.x,
5796                        y: g.point.y + y_offset,
5797                    },
5798                    size: g.size,
5799                })
5800                .collect();
5801            DisplayListItem::Text {
5802                glyphs: offset_glyphs,
5803                font_hash: *font_hash,
5804                font_size_px: *font_size_px,
5805                color: *color,
5806                clip_rect: offset_rect_y(clip_rect.into_inner(), y_offset).into(),
5807                source_node_index: None,
5808            }
5809        }
5810        DisplayListItem::TextLayout {
5811            layout,
5812            bounds,
5813            font_hash,
5814            font_size_px,
5815            color,
5816        } => DisplayListItem::TextLayout {
5817            layout: layout.clone(),
5818            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5819            font_hash: *font_hash,
5820            font_size_px: *font_size_px,
5821            color: *color,
5822        },
5823        DisplayListItem::Image { bounds, image, border_radius } => DisplayListItem::Image {
5824            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5825            image: image.clone(),
5826            border_radius: *border_radius,
5827        },
5828        // Pass through other items with their bounds offset
5829        DisplayListItem::SelectionRect {
5830            bounds,
5831            border_radius,
5832            color,
5833        } => DisplayListItem::SelectionRect {
5834            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5835            border_radius: *border_radius,
5836            color: *color,
5837        },
5838        DisplayListItem::CursorRect { bounds, color } => DisplayListItem::CursorRect {
5839            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5840            color: *color,
5841        },
5842        DisplayListItem::Underline {
5843            bounds,
5844            color,
5845            thickness,
5846        } => DisplayListItem::Underline {
5847            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5848            color: *color,
5849            thickness: *thickness,
5850        },
5851        DisplayListItem::Strikethrough {
5852            bounds,
5853            color,
5854            thickness,
5855        } => DisplayListItem::Strikethrough {
5856            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5857            color: *color,
5858            thickness: *thickness,
5859        },
5860        DisplayListItem::Overline {
5861            bounds,
5862            color,
5863            thickness,
5864        } => DisplayListItem::Overline {
5865            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5866            color: *color,
5867            thickness: *thickness,
5868        },
5869        DisplayListItem::ScrollBar {
5870            bounds,
5871            color,
5872            orientation,
5873            opacity_key,
5874            hit_id,
5875        } => DisplayListItem::ScrollBar {
5876            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5877            color: *color,
5878            orientation: *orientation,
5879            opacity_key: *opacity_key,
5880            hit_id: *hit_id,
5881        },
5882        DisplayListItem::HitTestArea { bounds, tag } => DisplayListItem::HitTestArea {
5883            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5884            tag: *tag,
5885        },
5886        DisplayListItem::PushClip {
5887            bounds,
5888            border_radius,
5889        } => DisplayListItem::PushClip {
5890            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5891            border_radius: *border_radius,
5892        },
5893        DisplayListItem::PushScrollFrame {
5894            clip_bounds,
5895            content_size,
5896            scroll_id,
5897        } => DisplayListItem::PushScrollFrame {
5898            clip_bounds: offset_rect_y(clip_bounds.into_inner(), y_offset).into(),
5899            content_size: *content_size,
5900            scroll_id: *scroll_id,
5901        },
5902        DisplayListItem::PushStackingContext { bounds, z_index } => {
5903            DisplayListItem::PushStackingContext {
5904                bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5905                z_index: *z_index,
5906            }
5907        }
5908        DisplayListItem::VirtualView {
5909            child_dom_id,
5910            bounds,
5911            clip_rect,
5912        } => DisplayListItem::VirtualView {
5913            child_dom_id: *child_dom_id,
5914            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5915            clip_rect: offset_rect_y(clip_rect.into_inner(), y_offset).into(),
5916        },
5917        DisplayListItem::VirtualViewPlaceholder {
5918            node_id,
5919            bounds,
5920            clip_rect,
5921        } => DisplayListItem::VirtualViewPlaceholder {
5922            node_id: *node_id,
5923            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5924            clip_rect: offset_rect_y(clip_rect.into_inner(), y_offset).into(),
5925        },
5926        // Pass through stateless items
5927        DisplayListItem::PopClip => DisplayListItem::PopClip,
5928        DisplayListItem::PopScrollFrame => DisplayListItem::PopScrollFrame,
5929        DisplayListItem::PopStackingContext => DisplayListItem::PopStackingContext,
5930
5931        // Gradient items
5932        DisplayListItem::LinearGradient {
5933            bounds,
5934            gradient,
5935            border_radius,
5936        } => DisplayListItem::LinearGradient {
5937            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5938            gradient: gradient.clone(),
5939            border_radius: *border_radius,
5940        },
5941        DisplayListItem::RadialGradient {
5942            bounds,
5943            gradient,
5944            border_radius,
5945        } => DisplayListItem::RadialGradient {
5946            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5947            gradient: gradient.clone(),
5948            border_radius: *border_radius,
5949        },
5950        DisplayListItem::ConicGradient {
5951            bounds,
5952            gradient,
5953            border_radius,
5954        } => DisplayListItem::ConicGradient {
5955            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5956            gradient: gradient.clone(),
5957            border_radius: *border_radius,
5958        },
5959
5960        // BoxShadow
5961        DisplayListItem::BoxShadow {
5962            bounds,
5963            shadow,
5964            border_radius,
5965        } => DisplayListItem::BoxShadow {
5966            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5967            shadow: *shadow,
5968            border_radius: *border_radius,
5969        },
5970
5971        // Filter effects
5972        DisplayListItem::PushFilter { bounds, filters } => DisplayListItem::PushFilter {
5973            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5974            filters: filters.clone(),
5975        },
5976        DisplayListItem::PopFilter => DisplayListItem::PopFilter,
5977        DisplayListItem::PushBackdropFilter { bounds, filters } => {
5978            DisplayListItem::PushBackdropFilter {
5979                bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5980                filters: filters.clone(),
5981            }
5982        }
5983        DisplayListItem::PopBackdropFilter => DisplayListItem::PopBackdropFilter,
5984        DisplayListItem::PushOpacity { bounds, opacity } => DisplayListItem::PushOpacity {
5985            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
5986            opacity: *opacity,
5987        },
5988        DisplayListItem::PopOpacity => DisplayListItem::PopOpacity,
5989        DisplayListItem::ScrollBarStyled { info } => {
5990            let mut offset_info = (**info).clone();
5991            offset_info.bounds = offset_rect_y(offset_info.bounds.into_inner(), y_offset).into();
5992            offset_info.track_bounds = offset_rect_y(offset_info.track_bounds.into_inner(), y_offset).into();
5993            offset_info.thumb_bounds = offset_rect_y(offset_info.thumb_bounds.into_inner(), y_offset).into();
5994            if let Some(b) = offset_info.button_decrement_bounds {
5995                offset_info.button_decrement_bounds = Some(offset_rect_y(b.into_inner(), y_offset).into());
5996            }
5997            if let Some(b) = offset_info.button_increment_bounds {
5998                offset_info.button_increment_bounds = Some(offset_rect_y(b.into_inner(), y_offset).into());
5999            }
6000            DisplayListItem::ScrollBarStyled {
6001                info: Box::new(offset_info),
6002            }
6003        }
6004
6005        // Reference frames - offset the bounds
6006        DisplayListItem::PushReferenceFrame {
6007            transform_key,
6008            initial_transform,
6009            bounds,
6010        } => DisplayListItem::PushReferenceFrame {
6011            transform_key: *transform_key,
6012            initial_transform: *initial_transform,
6013            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
6014        },
6015        DisplayListItem::PopReferenceFrame => DisplayListItem::PopReferenceFrame,
6016        DisplayListItem::PushTextShadow { shadow } => DisplayListItem::PushTextShadow {
6017            shadow: shadow.clone(),
6018        },
6019        DisplayListItem::PopTextShadow => DisplayListItem::PopTextShadow,
6020        DisplayListItem::PushImageMaskClip {
6021            bounds,
6022            mask_image,
6023            mask_rect,
6024        } => DisplayListItem::PushImageMaskClip {
6025            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
6026            mask_image: mask_image.clone(),
6027            mask_rect: offset_rect_y(mask_rect.into_inner(), y_offset).into(),
6028        },
6029        DisplayListItem::PopImageMaskClip => DisplayListItem::PopImageMaskClip,
6030    }
6031}
6032
6033/// Generate display list items for simple text (headers/footers).
6034///
6035/// This creates a simplified text rendering without full text layout.
6036/// For now, this creates a placeholder that renderers should handle specially.
6037fn generate_text_display_items(
6038    text: &str,
6039    bounds: LogicalRect,
6040    font_size: f32,
6041    color: ColorU,
6042    alignment: TextAlignment,
6043) -> Vec<DisplayListItem> {
6044    use crate::font_traits::FontHash;
6045
6046    if text.is_empty() {
6047        return Vec::new();
6048    }
6049
6050    // Calculate approximate text position based on alignment
6051    // For now, we estimate character width as 0.5 * font_size (monospace approximation)
6052    let char_width = font_size * 0.5;
6053    let text_width = text.len() as f32 * char_width;
6054
6055    let x_offset = match alignment {
6056        TextAlignment::Left => bounds.origin.x,
6057        TextAlignment::Center => bounds.origin.x + (bounds.size.width - text_width) / 2.0,
6058        TextAlignment::Right => bounds.origin.x + bounds.size.width - text_width,
6059    };
6060
6061    // Position text vertically centered in the bounds
6062    let y_pos = bounds.origin.y + (bounds.size.height + font_size) / 2.0 - font_size * 0.2;
6063
6064    // Create simple glyph instances for each character
6065    // Note: This is a simplified approach - proper text rendering should use text3
6066    let glyphs: Vec<GlyphInstance> = text
6067        .chars()
6068        .enumerate()
6069        .filter(|(_, c)| !c.is_control())
6070        .map(|(i, c)| GlyphInstance {
6071            index: c as u32, // Use Unicode codepoint as glyph index (placeholder)
6072            point: LogicalPosition {
6073                x: x_offset + i as f32 * char_width,
6074                y: y_pos,
6075            },
6076            size: LogicalSize::new(char_width, font_size),
6077        })
6078        .collect();
6079
6080    if glyphs.is_empty() {
6081        return Vec::new();
6082    }
6083
6084    vec![DisplayListItem::Text {
6085        glyphs,
6086        font_hash: FontHash::from_hash(0), // Default font hash - renderer should use default font
6087        font_size_px: font_size,
6088        color,
6089        clip_rect: bounds.into(),
6090        source_node_index: None,
6091    }]
6092}
6093
6094/// Calculate the total height of a display list (max Y + height of all items).
6095fn calculate_display_list_height(display_list: &DisplayList) -> f32 {
6096    let mut max_bottom = 0.0f32;
6097
6098    for item in &display_list.items {
6099        if let Some(bounds) = get_display_item_bounds(item) {
6100            // Skip items with zero height - they don't contribute to visible content
6101            if bounds.0.size.height < 0.1 {
6102                continue;
6103            }
6104            
6105            let item_bottom = bounds.0.origin.y + bounds.0.size.height;
6106            if item_bottom > max_bottom {
6107                max_bottom = item_bottom;
6108            }
6109        }
6110    }
6111
6112    max_bottom
6113}
6114
6115/// Break property information for pagination decisions.
6116#[derive(Debug, Clone, Copy, Default)]
6117pub struct BreakProperties {
6118    pub break_before: PageBreak,
6119    pub break_after: PageBreak,
6120    pub break_inside: BreakInside,
6121}
6122
6123// ============================================================================
6124// TEXT-OVERFLOW STUB
6125// ============================================================================
6126
6127/// Applies text-overflow ellipsis handling to a display list.
6128///
6129/// CSS UI Module Level 3, section 6.2 (text-overflow):
6130/// When inline content overflows a block container that has `overflow: hidden`
6131/// (or clip/scroll) and `text-overflow: ellipsis`, the overflowing text should
6132/// be replaced with an ellipsis character (U+2026) or a custom string.
6133///
6134/// This is a display-list post-processing step that modifies glyph runs
6135/// to show an ellipsis when text overflows its container. It operates on
6136/// the assumption that the container already has a PushClip that clips
6137/// the overflow -- this function additionally replaces the trailing glyphs
6138/// with an ellipsis so the user gets a visual indicator of truncation.
6139///
6140/// # Parameters
6141/// - `display_list`: The display list to modify (text items may be clipped/replaced)
6142/// - `container_bounds`: The bounds of the containing block (overflow boundary)
6143/// - `_ellipsis`: The ellipsis string (currently unused; U+2026 glyph index is used)
6144///
6145/// # Algorithm
6146/// 1. For each Text item in the display list, check if any glyphs extend
6147///    past the container's right edge (inline-end in LTR).
6148/// 2. If so, find the last glyph that fits entirely within the container,
6149///    accounting for the width of the ellipsis character.
6150/// 3. Remove all glyphs after that point.
6151/// 4. Append an ellipsis glyph (U+2026 = glyph index 0x2026 as a fallback;
6152///    proper glyph lookup requires font metrics not available here).
6153///
6154/// Note: This is a best-effort implementation. A pixel-perfect version would
6155/// need access to font metrics to measure the exact ellipsis glyph width and
6156/// to look up the correct glyph index for the ellipsis in each font.
6157// +spec:overflow:f175b9 - bidi ellipsis: characters visually at the end edge of the line are hidden for ellipsis
6158pub fn apply_text_overflow_ellipsis(
6159    display_list: &mut DisplayList,
6160    container_bounds: LogicalRect,
6161    _ellipsis: &str,
6162) {
6163    let container_right = container_bounds.origin.x + container_bounds.size.width;
6164
6165    // Approximate ellipsis width as ~0.6 * font_size (typical for "..." in most fonts).
6166    // This is a heuristic; proper implementation requires font metric access.
6167    for item in display_list.items.iter_mut() {
6168        match item {
6169            DisplayListItem::Text {
6170                glyphs,
6171                font_size_px,
6172                clip_rect,
6173                ..
6174            } => {
6175                if glyphs.is_empty() {
6176                    continue;
6177                }
6178
6179                // Check if any glyph extends past the container right edge
6180                let last_glyph = &glyphs[glyphs.len() - 1];
6181                let last_glyph_right = last_glyph.point.x + last_glyph.size.width;
6182
6183                if last_glyph_right <= container_right {
6184                    continue; // No overflow, nothing to do
6185                }
6186
6187                // Estimate ellipsis width
6188                let ellipsis_width = *font_size_px * 0.6;
6189                let truncation_edge = container_right - ellipsis_width;
6190
6191                // Find the last glyph that fits before the truncation edge
6192                let mut keep_count = 0;
6193                for (i, glyph) in glyphs.iter().enumerate() {
6194                    let glyph_right = glyph.point.x + glyph.size.width;
6195                    if glyph_right > truncation_edge {
6196                        break;
6197                    }
6198                    keep_count = i + 1;
6199                }
6200
6201                // Truncate the glyphs
6202                glyphs.truncate(keep_count);
6203
6204                // Append an ellipsis glyph. We use Unicode codepoint U+2026
6205                // (HORIZONTAL ELLIPSIS) as the glyph index. This is a common
6206                // convention; renderers that use proper glyph IDs will need to
6207                // map this to the font's actual glyph index.
6208                let ellipsis_x = if let Some(last) = glyphs.last() {
6209                    last.point.x + last.size.width
6210                } else {
6211                    container_bounds.origin.x
6212                };
6213
6214                let ellipsis_glyph = GlyphInstance {
6215                    index: 0x2026, // U+2026 HORIZONTAL ELLIPSIS
6216                    point: LogicalPosition::new(ellipsis_x, glyphs.first().map_or(
6217                        container_bounds.origin.y,
6218                        |g| g.point.y,
6219                    )),
6220                    size: LogicalSize::new(ellipsis_width, *font_size_px),
6221                };
6222
6223                glyphs.push(ellipsis_glyph);
6224
6225                // Update the clip rect to match the container bounds so
6226                // the ellipsis is visible but nothing past it is shown
6227                *clip_rect = container_bounds.into();
6228            }
6229            _ => {} // Only process Text items
6230        }
6231    }
6232}
6233
6234// ============================================================================
6235// CLIP-PATH STUB
6236// ============================================================================
6237
6238/// Resolves a CSS clip-path shape to a clipping rectangle.
6239///
6240/// CSS Masking Module Level 1, section 3 (clip-path):
6241/// The clip-path property creates a clipping region that determines which parts
6242/// of an element are visible. Content outside the clipping region is hidden.
6243///
6244/// Currently supported clip-path values:
6245/// - `inset()` - rectangular clip with optional rounding
6246/// - `circle()` - approximated as bounding box rectangle
6247/// - `ellipse()` - approximated as bounding box rectangle
6248/// - `polygon()` - approximated as axis-aligned bounding box
6249/// - `none` - no clipping (returns None)
6250///
6251/// # Parameters
6252/// - `clip_path`: The resolved clip-path CSS property value
6253/// - `node_bounds`: The reference box for resolving clip-path values
6254///
6255/// # Returns
6256/// A `(LogicalRect, f32)` tuple: the clip rectangle and border radius,
6257/// or `None` if no clipping should be applied.
6258///
6259/// Note: Circle, ellipse, and polygon shapes are approximated as axis-aligned
6260/// bounding boxes. A full implementation would use path-based clipping in the
6261/// renderer, but rectangular clips work for the most common use cases.
6262pub fn resolve_clip_path(
6263    clip_path: &azul_css::props::layout::shape::ClipPath,
6264    node_bounds: LogicalRect,
6265) -> Option<(LogicalRect, f32)> {
6266    use azul_css::props::layout::shape::ClipPath;
6267    use azul_css::shape::CssShape;
6268
6269    match clip_path {
6270        ClipPath::None => None,
6271        ClipPath::Shape(shape) => {
6272            match shape {
6273                CssShape::Inset(inset) => {
6274                    // CSS inset() creates a rectangular clip inset from each edge.
6275                    // inset(top right bottom left round border-radius)
6276                    let x = node_bounds.origin.x + inset.inset_left;
6277                    let y = node_bounds.origin.y + inset.inset_top;
6278                    let w = (node_bounds.size.width - inset.inset_left - inset.inset_right).max(0.0);
6279                    let h = (node_bounds.size.height - inset.inset_top - inset.inset_bottom).max(0.0);
6280                    let radius = match inset.border_radius {
6281                        azul_css::corety::OptionF32::Some(r) => r,
6282                        azul_css::corety::OptionF32::None => 0.0,
6283                    };
6284                    Some((LogicalRect {
6285                        origin: LogicalPosition::new(x, y),
6286                        size: LogicalSize::new(w, h),
6287                    }, radius))
6288                }
6289                CssShape::Circle(circle) => {
6290                    // Approximate circle as a square bounding box centered at the circle center.
6291                    // CSS circle(radius at cx cy). The center point coordinates are in
6292                    // absolute units (pre-resolved by the CSS parser).
6293                    let cx = node_bounds.origin.x + circle.center.x;
6294                    let cy = node_bounds.origin.y + circle.center.y;
6295                    let r = circle.radius;
6296                    Some((LogicalRect {
6297                        origin: LogicalPosition::new(cx - r, cy - r),
6298                        size: LogicalSize::new(r * 2.0, r * 2.0),
6299                    }, r))
6300                }
6301                CssShape::Ellipse(ellipse) => {
6302                    // Approximate ellipse as its bounding box.
6303                    let cx = node_bounds.origin.x + ellipse.center.x;
6304                    let cy = node_bounds.origin.y + ellipse.center.y;
6305                    let rx = ellipse.radius_x;
6306                    let ry = ellipse.radius_y;
6307                    let radius = rx.min(ry);
6308                    Some((LogicalRect {
6309                        origin: LogicalPosition::new(cx - rx, cy - ry),
6310                        size: LogicalSize::new(rx * 2.0, ry * 2.0),
6311                    }, radius))
6312                }
6313                CssShape::Polygon(polygon) => {
6314                    // Compute the axis-aligned bounding box of the polygon.
6315                    if polygon.points.is_empty() {
6316                        return None;
6317                    }
6318                    let mut min_x = f32::INFINITY;
6319                    let mut min_y = f32::INFINITY;
6320                    let mut max_x = f32::NEG_INFINITY;
6321                    let mut max_y = f32::NEG_INFINITY;
6322                    for point in polygon.points.iter() {
6323                        // Polygon points are in absolute coordinates (pre-resolved)
6324                        let px = node_bounds.origin.x + point.x;
6325                        let py = node_bounds.origin.y + point.y;
6326                        min_x = min_x.min(px);
6327                        min_y = min_y.min(py);
6328                        max_x = max_x.max(px);
6329                        max_y = max_y.max(py);
6330                    }
6331                    Some((LogicalRect {
6332                        origin: LogicalPosition::new(min_x, min_y),
6333                        size: LogicalSize::new((max_x - min_x).max(0.0), (max_y - min_y).max(0.0)),
6334                    }, 0.0))
6335                }
6336                CssShape::Path(_) => {
6337                    // SVG paths are not supported for clip-path yet.
6338                    // Return the full node bounds (no clipping).
6339                    None
6340                }
6341            }
6342        }
6343    }
6344}
6345
6346/// Applies a CSS clip-path to the display list by inserting PushClip/PopClip.
6347///
6348/// This is a post-processing step that wraps all items between `start_index`
6349/// and the current end of the display list in a clip region derived from
6350/// the clip-path shape.
6351///
6352/// # Parameters
6353/// - `display_list`: The display list to modify
6354/// - `start_index`: The index of the first item belonging to this node
6355/// - `clip_rect`: The resolved clip rectangle
6356/// - `border_radius`: The border radius for the clip (from inset round, or circle)
6357pub fn apply_clip_path(
6358    display_list: &mut DisplayList,
6359    start_index: usize,
6360    clip_rect: LogicalRect,
6361    border_radius: f32,
6362) {
6363    let br = if border_radius > 0.0 {
6364        BorderRadius {
6365            top_left: border_radius,
6366            top_right: border_radius,
6367            bottom_left: border_radius,
6368            bottom_right: border_radius,
6369        }
6370    } else {
6371        BorderRadius::default()
6372    };
6373
6374    // Insert PushClip at start_index
6375    display_list.items.insert(start_index, DisplayListItem::PushClip {
6376        bounds: clip_rect.into(),
6377        border_radius: br,
6378    });
6379    // Insert a corresponding None in node_mapping
6380    if display_list.node_mapping.len() >= start_index {
6381        display_list.node_mapping.insert(start_index, None);
6382    }
6383
6384    // Append PopClip at the end
6385    display_list.items.push(DisplayListItem::PopClip);
6386    display_list.node_mapping.push(None);
6387}
6388
6389/// Rasterize an `SvgMultiPolygon` clip path into an R8 image mask at the given paint rect size.
6390///
6391/// Returns `None` if the rect has zero size.
6392#[cfg(feature = "cpurender")]
6393fn rasterize_svg_clip_to_r8(
6394    svg_clip: &azul_core::svg::SvgMultiPolygon,
6395    paint_rect: &LogicalRect,
6396) -> Option<azul_core::resources::ImageRef> {
6397    use agg_rust::{
6398        basics::FillingRule,
6399        color::Rgba8,
6400        path_storage::PathStorage,
6401        pixfmt_rgba::PixfmtRgba32,
6402        rasterizer_scanline_aa::RasterizerScanlineAa,
6403        renderer_base::RendererBase,
6404        renderer_scanline::render_scanlines_aa_solid,
6405        rendering_buffer::RowAccessor,
6406        scanline_u::ScanlineU8,
6407    };
6408    use azul_core::resources::{ImageRef, RawImage, RawImageFormat, RawImageData};
6409
6410    let w = paint_rect.size.width.ceil() as u32;
6411    let h = paint_rect.size.height.ceil() as u32;
6412    if w == 0 || h == 0 {
6413        return None;
6414    }
6415
6416    // Build agg PathStorage from SvgMultiPolygon
6417    let mut path = PathStorage::new();
6418    for ring in svg_clip.rings.as_ref().iter() {
6419        let mut first = true;
6420        for item in ring.items.as_ref().iter() {
6421            match item {
6422                azul_core::svg::SvgPathElement::Line(l) => {
6423                    if first {
6424                        path.move_to(
6425                            (l.start.x - paint_rect.origin.x) as f64,
6426                            (l.start.y - paint_rect.origin.y) as f64,
6427                        );
6428                        first = false;
6429                    }
6430                    path.line_to(
6431                        (l.end.x - paint_rect.origin.x) as f64,
6432                        (l.end.y - paint_rect.origin.y) as f64,
6433                    );
6434                }
6435                azul_core::svg::SvgPathElement::QuadraticCurve(q) => {
6436                    if first {
6437                        path.move_to(
6438                            (q.start.x - paint_rect.origin.x) as f64,
6439                            (q.start.y - paint_rect.origin.y) as f64,
6440                        );
6441                        first = false;
6442                    }
6443                    path.curve3(
6444                        (q.ctrl.x - paint_rect.origin.x) as f64,
6445                        (q.ctrl.y - paint_rect.origin.y) as f64,
6446                        (q.end.x - paint_rect.origin.x) as f64,
6447                        (q.end.y - paint_rect.origin.y) as f64,
6448                    );
6449                }
6450                azul_core::svg::SvgPathElement::CubicCurve(c) => {
6451                    if first {
6452                        path.move_to(
6453                            (c.start.x - paint_rect.origin.x) as f64,
6454                            (c.start.y - paint_rect.origin.y) as f64,
6455                        );
6456                        first = false;
6457                    }
6458                    path.curve4(
6459                        (c.ctrl_1.x - paint_rect.origin.x) as f64,
6460                        (c.ctrl_1.y - paint_rect.origin.y) as f64,
6461                        (c.ctrl_2.x - paint_rect.origin.x) as f64,
6462                        (c.ctrl_2.y - paint_rect.origin.y) as f64,
6463                        (c.end.x - paint_rect.origin.x) as f64,
6464                        (c.end.y - paint_rect.origin.y) as f64,
6465                    );
6466                }
6467            }
6468        }
6469    }
6470
6471    // Rasterize to RGBA32 buffer
6472    let mut rgba_buf = vec![0u8; (w * h * 4) as usize];
6473    {
6474        let stride = (w * 4) as i32;
6475        let mut ra = unsafe {
6476            RowAccessor::new_with_buf(rgba_buf.as_mut_ptr(), w, h, stride)
6477        };
6478        let pf = PixfmtRgba32::new(&mut ra);
6479        let mut rb = RendererBase::new(pf);
6480
6481        let mut ras = RasterizerScanlineAa::new();
6482        ras.filling_rule(FillingRule::NonZero);
6483        ras.add_path(&mut path, 0);
6484
6485        let mut sl = ScanlineU8::new();
6486        let white = Rgba8 { r: 255, g: 255, b: 255, a: 255 };
6487        render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, &white);
6488    }
6489
6490    // Extract alpha channel as R8 mask
6491    let r8_data: Vec<u8> = rgba_buf.chunks_exact(4).map(|px| px[3]).collect();
6492
6493    ImageRef::new_rawimage(RawImage {
6494        pixels: RawImageData::U8(r8_data.into()),
6495        width: w as usize,
6496        height: h as usize,
6497        premultiplied_alpha: false,
6498        data_format: RawImageFormat::R8,
6499        tag: Vec::new().into(),
6500    })
6501}