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