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

            
16
use std::{collections::{BTreeMap, HashMap}, sync::Arc};
17

            
18
use 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
};
32
use 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")]
54
use crate::text3;
55
#[cfg(feature = "text_layout")]
56
use crate::text3::cache::{InlineShape, PositionedItem};
57
use 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)]
84
pub 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)]
100
pub 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)]
117
pub 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)]
131
pub 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)]
147
pub struct WindowLogicalRect(pub LogicalRect);
148

            
149
impl 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
7525
    pub const fn inner(&self) -> &LogicalRect {
164
7525
        &self.0
165
7525
    }
166

            
167
    #[inline]
168
16380
    pub const fn into_inner(self) -> LogicalRect {
169
16380
        self.0
170
16380
    }
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

            
177
impl From<LogicalRect> for WindowLogicalRect {
178
    #[inline]
179
66815
    fn from(r: LogicalRect) -> Self { Self(r) }
180
}
181

            
182
impl 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)]
189
pub 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)]
202
pub 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

            
252
impl 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
5425
    pub fn to_content_box(
256
5425
        self,
257
5425
        padding: &crate::solver3::geometry::EdgeSizes,
258
5425
        border: &crate::solver3::geometry::EdgeSizes,
259
5425
    ) -> ContentBoxRect {
260
5425
        ContentBoxRect(LogicalRect {
261
5425
            origin: LogicalPosition {
262
5425
                x: self.0.origin.x + padding.left + border.left,
263
5425
                y: self.0.origin.y + padding.top + border.top,
264
5425
            },
265
5425
            size: LogicalSize {
266
5425
                width: self.0.size.width
267
5425
                    - padding.left
268
5425
                    - padding.right
269
5425
                    - border.left
270
5425
                    - border.right,
271
5425
                height: self.0.size.height
272
5425
                    - padding.top
273
5425
                    - padding.bottom
274
5425
                    - border.top
275
5425
                    - border.bottom,
276
5425
            },
277
5425
        })
278
5425
    }
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)]
289
pub struct ContentBoxRect(pub LogicalRect);
290

            
291
impl ContentBoxRect {
292
    /// Get the inner LogicalRect
293
5425
    pub fn rect(&self) -> LogicalRect {
294
5425
        self.0
295
5425
    }
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)]
303
pub 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

            
318
impl 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)]
598
pub 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

            
867
impl 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();
                let oy = shadow.offset_y.to_pixels_internal(16.0, 16.0).abs();
                let blur = shadow.blur_radius.to_pixels_internal(16.0, 16.0).abs();
                let spread = shadow.spread_radius.to_pixels_internal(16.0, 16.0).abs();
                let expand = ox + oy + blur + spread;
                Some(LogicalRect {
                    origin: LogicalPosition {
                        x: b.origin.x - expand,
                        y: b.origin.y - expand,
                    },
                    size: LogicalSize {
                        width: b.size.width + expand * 2.0,
                        height: b.size.height + expand * 2.0,
                    },
                })
            }
            _ => self.bounds(),
        }
    }
    /// Return the bounding rect of this item, or None for push/pop commands
    /// that don't have their own visual bounds.
    pub fn bounds(&self) -> Option<LogicalRect> {
        match self {
            Self::Rect { bounds, .. }
            | Self::SelectionRect { bounds, .. }
            | Self::CursorRect { bounds, .. }
            | Self::Border { bounds, .. }
            | Self::Text { clip_rect: bounds, .. }
            | Self::TextLayout { bounds, .. }
            | Self::Underline { bounds, .. }
            | Self::Strikethrough { bounds, .. }
            | Self::Overline { bounds, .. }
            | Self::Image { bounds, .. }
            | Self::ScrollBar { bounds, .. }
            | Self::LinearGradient { bounds, .. }
            | Self::RadialGradient { bounds, .. }
            | Self::ConicGradient { bounds, .. }
            | Self::BoxShadow { bounds, .. }
            | Self::VirtualView { bounds, .. }
            | Self::VirtualViewPlaceholder { bounds, .. }
            | Self::HitTestArea { bounds, .. }
            | Self::PushClip { bounds, .. }
            | Self::PushImageMaskClip { bounds, .. }
            | Self::PushScrollFrame { clip_bounds: bounds, .. }
            | Self::PushStackingContext { bounds, .. }
            | Self::PushReferenceFrame { bounds, .. }
            | Self::PushFilter { bounds, .. }
            | Self::PushBackdropFilter { bounds, .. }
            | Self::PushOpacity { bounds, .. } => Some(*bounds.inner()),
            Self::ScrollBarStyled { info, .. } => Some(*info.bounds.inner()),
            Self::PushTextShadow { .. } => None, // text shadow has no bounds, affects following text
            Self::PopClip
            | Self::PopImageMaskClip
            | Self::PopScrollFrame
            | Self::PopStackingContext
            | Self::PopReferenceFrame
            | Self::PopFilter
            | Self::PopBackdropFilter
            | Self::PopOpacity
            | Self::PopTextShadow => None,
        }
    }
}
// Helper structs for the DisplayList
#[derive(Debug, Copy, Clone, Default)]
pub struct BorderRadius {
    pub top_left: f32,
    pub top_right: f32,
    pub bottom_left: f32,
    pub bottom_right: f32,
}
impl BorderRadius {
4830
    pub fn is_zero(&self) -> bool {
4830
        self.top_left == 0.0
4830
            && self.top_right == 0.0
4830
            && self.bottom_left == 0.0
4830
            && self.bottom_right == 0.0
4830
    }
}
// Dummy types for compilation
pub type LocalScrollId = u64;
/// Display list tag ID as (payload, type_marker) tuple.
/// The u16 field is used as a namespace marker:
/// - 0x0100 = DOM Node (regular interactive elements)
/// - 0x0200 = Scrollbar component
pub type DisplayListTagId = (u64, u16);
/// Internal builder to accumulate display list items during generation.
#[derive(Debug, Default)]
struct DisplayListBuilder {
    items: Vec<DisplayListItem>,
    node_mapping: Vec<Option<NodeId>>,
    /// Current node being processed (set by generator)
    current_node: Option<NodeId>,
    /// Collected debug messages (transferred to ctx on finalize)
    debug_messages: Vec<LayoutDebugMessage>,
    /// Whether debug logging is enabled
    debug_enabled: bool,
    /// Y-positions where forced page breaks should occur
    forced_page_breaks: Vec<f32>,
    /// Index ranges of items from fixed-position elements (for paged media replication)
    fixed_position_item_ranges: Vec<(usize, usize)>,
    /// Start index of the current fixed-position element being built, if any
    fixed_position_start: Option<usize>,
}
impl DisplayListBuilder {
    pub fn new() -> Self {
        Self::default()
    }
5110
    pub fn with_debug(debug_enabled: bool) -> Self {
5110
        Self {
5110
            items: Vec::new(),
5110
            node_mapping: Vec::new(),
5110
            current_node: None,
5110
            debug_messages: Vec::new(),
5110
            debug_enabled,
5110
            forced_page_breaks: Vec::new(),
5110
            fixed_position_item_ranges: Vec::new(),
5110
            fixed_position_start: None,
5110
        }
5110
    }
    /// Log a debug message if debug is enabled
5250
    fn debug_log(&mut self, message: String) {
5250
        if self.debug_enabled {
4690
            self.debug_messages.push(LayoutDebugMessage::info(message));
4690
        }
5250
    }
    /// Build the display list and transfer debug messages to the provided option
5110
    pub fn build_with_debug(
5110
        mut self,
5110
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
5110
    ) -> DisplayList {
        // Transfer collected debug messages to the context
5110
        if let Some(msgs) = debug_messages.as_mut() {
4165
            msgs.append(&mut self.debug_messages);
4165
        }
5110
        DisplayList {
5110
            items: self.items,
5110
            node_mapping: self.node_mapping,
5110
            forced_page_breaks: self.forced_page_breaks,
5110
            fixed_position_item_ranges: self.fixed_position_item_ranges,
5110
        }
5110
    }
    /// Set the current node context for subsequent push operations
59220
    pub fn set_current_node(&mut self, node_id: Option<NodeId>) {
59220
        self.current_node = node_id;
59220
    }
    /// Mark the start of a fixed-position element's display items.
    pub fn begin_fixed_position_element(&mut self) {
        self.fixed_position_start = Some(self.items.len());
    }
    /// Mark the end of a fixed-position element's display items.
    /// Records the (start, end) index range for paged media replication.
    pub fn end_fixed_position_element(&mut self) {
        if let Some(start) = self.fixed_position_start.take() {
            let end = self.items.len();
            if end > start {
                self.fixed_position_item_ranges.push((start, end));
            }
        }
    }
    /// Register a forced page break at the given Y position.
    /// This is used for CSS break-before: always and break-after: always.
    pub fn add_forced_page_break(&mut self, y_position: f32) {
        // Avoid duplicates and keep sorted
        if !self.forced_page_breaks.contains(&y_position) {
            self.forced_page_breaks.push(y_position);
            self.forced_page_breaks.sort_by(|a, b| a.partial_cmp(b).unwrap());
        }
    }
    /// Push an item and record its node mapping
56490
    fn push_item(&mut self, item: DisplayListItem) {
56490
        self.items.push(item);
56490
        self.node_mapping.push(self.current_node);
56490
    }
    pub fn build(self) -> DisplayList {
        DisplayList {
            items: self.items,
            node_mapping: self.node_mapping,
            forced_page_breaks: self.forced_page_breaks,
            fixed_position_item_ranges: self.fixed_position_item_ranges,
        }
    }
12005
    pub fn push_hit_test_area(&mut self, bounds: LogicalRect, tag: DisplayListTagId) {
12005
        self.push_item(DisplayListItem::HitTestArea { bounds: bounds.into(), tag });
12005
    }
    /// Push a simple single-color scrollbar (legacy method).
    pub fn push_scrollbar(
        &mut self,
        bounds: LogicalRect,
        color: ColorU,
        orientation: ScrollbarOrientation,
        opacity_key: Option<OpacityKey>,
        hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
    ) {
        if color.a > 0 || opacity_key.is_some() {
            // Optimization: Don't draw fully transparent items without opacity keys.
            self.push_item(DisplayListItem::ScrollBar {
                bounds: bounds.into(),
                color,
                orientation,
                opacity_key,
                hit_id,
            });
        }
    }
    /// Push a fully styled scrollbar with track, thumb, and optional buttons.
    pub fn push_scrollbar_styled(&mut self, info: ScrollbarDrawInfo) {
        // Only push if at least the thumb or track is visible
        if info.thumb_color.a > 0 || info.track_color.a > 0 || info.opacity_key.is_some() {
            self.push_item(DisplayListItem::ScrollBarStyled {
                info: Box::new(info),
            });
        }
    }
7385
    pub fn push_rect(&mut self, bounds: LogicalRect, color: ColorU, border_radius: BorderRadius) {
7385
        if color.a > 0 {
6230
            // Optimization: Don't draw fully transparent items.
6230
            self.push_item(DisplayListItem::Rect {
6230
                bounds: bounds.into(),
6230
                color,
6230
                border_radius,
6230
            });
6230
        }
7385
    }
    /// Unified method to paint all background layers and border for an element.
    ///
    /// This consolidates the background/border painting logic that was previously
    /// duplicated across:
    /// - paint_node_background_and_border() for block elements
    /// - paint_inline_shape() for inline-block elements
    ///
    /// The backgrounds are painted in order (back to front per CSS spec), followed
    /// by the border.
15470
    pub fn push_backgrounds_and_border(
15470
        &mut self,
15470
        bounds: LogicalRect,
15470
        background_contents: &[azul_css::props::style::StyleBackgroundContent],
15470
        border_info: &BorderInfo,
15470
        simple_border_radius: BorderRadius,
15470
        style_border_radius: StyleBorderRadius,
15470
        image_cache: &azul_core::resources::ImageCache,
15470
    ) {
        use azul_css::props::style::StyleBackgroundContent;
        // Paint all background layers in order (CSS paints backgrounds back to front)
20440
        for bg in background_contents {
4970
            match bg {
4935
                StyleBackgroundContent::Color(color) => {
4935
                    self.push_rect(bounds, *color, simple_border_radius);
4935
                }
35
                StyleBackgroundContent::LinearGradient(gradient) => {
35
                    self.push_linear_gradient(bounds, gradient.clone(), simple_border_radius);
35
                }
                StyleBackgroundContent::RadialGradient(gradient) => {
                    self.push_radial_gradient(bounds, gradient.clone(), simple_border_radius);
                }
                StyleBackgroundContent::ConicGradient(gradient) => {
                    self.push_conic_gradient(bounds, gradient.clone(), simple_border_radius);
                }
                StyleBackgroundContent::Image(image_id) => {
                    if let Some(image_ref) = image_cache.get_css_image_id(image_id) {
                        self.push_image(bounds, image_ref.clone(), simple_border_radius);
                    }
                }
            }
        }
        // Paint border
15470
        self.push_border(
15470
            bounds,
15470
            border_info.widths,
15470
            border_info.colors,
15470
            border_info.styles,
15470
            style_border_radius,
        );
15470
    }
    /// Paint backgrounds and border for inline text elements.
    ///
    /// Similar to push_backgrounds_and_border but uses InlineBorderInfo which stores
    /// pre-resolved pixel values instead of CSS property values. This is used for
    /// inline (display: inline) elements where the border info is computed during
    /// text layout and stored in the glyph runs.
5040
    pub fn push_inline_backgrounds_and_border(
5040
        &mut self,
5040
        bounds: LogicalRect,
5040
        background_color: Option<ColorU>,
5040
        background_contents: &[azul_css::props::style::StyleBackgroundContent],
5040
        border: Option<&crate::text3::cache::InlineBorderInfo>,
5040
        image_cache: &azul_core::resources::ImageCache,
5040
    ) {
        use azul_css::props::style::StyleBackgroundContent;
        // Paint solid background color if present
5040
        if let Some(bg_color) = background_color {
            self.push_rect(bounds, bg_color, BorderRadius::default());
5040
        }
        // Paint all background layers in order (CSS paints backgrounds back to front)
5040
        for bg in background_contents {
            match bg {
                StyleBackgroundContent::Color(color) => {
                    self.push_rect(bounds, *color, BorderRadius::default());
                }
                StyleBackgroundContent::LinearGradient(gradient) => {
                    self.push_linear_gradient(bounds, gradient.clone(), BorderRadius::default());
                }
                StyleBackgroundContent::RadialGradient(gradient) => {
                    self.push_radial_gradient(bounds, gradient.clone(), BorderRadius::default());
                }
                StyleBackgroundContent::ConicGradient(gradient) => {
                    self.push_conic_gradient(bounds, gradient.clone(), BorderRadius::default());
                }
                StyleBackgroundContent::Image(image_id) => {
                    if let Some(image_ref) = image_cache.get_css_image_id(image_id) {
                        self.push_image(bounds, image_ref.clone(), BorderRadius::default());
                    }
                }
            }
        }
        // Paint border if present
        // CSS 2.2 §8.6: suppress left/right borders at split points, respecting direction
5040
        if let Some(border) = border {
            let effective_left = if border.left_inset() > 0.0 { border.left } else { 0.0 };
            let effective_right = if border.right_inset() > 0.0 { border.right } else { 0.0 };
            if border.top > 0.0 || effective_right > 0.0 || border.bottom > 0.0 || effective_left > 0.0 {
                let border_widths = StyleBorderWidths {
                    top: Some(CssPropertyValue::Exact(LayoutBorderTopWidth {
                        inner: PixelValue::px(border.top),
                    })),
                    right: Some(CssPropertyValue::Exact(LayoutBorderRightWidth {
                        inner: PixelValue::px(effective_right),
                    })),
                    bottom: Some(CssPropertyValue::Exact(LayoutBorderBottomWidth {
                        inner: PixelValue::px(border.bottom),
                    })),
                    left: Some(CssPropertyValue::Exact(LayoutBorderLeftWidth {
                        inner: PixelValue::px(effective_left),
                    })),
                };
                let border_colors = StyleBorderColors {
                    top: Some(CssPropertyValue::Exact(StyleBorderTopColor {
                        inner: border.top_color,
                    })),
                    right: Some(CssPropertyValue::Exact(StyleBorderRightColor {
                        inner: border.right_color,
                    })),
                    bottom: Some(CssPropertyValue::Exact(StyleBorderBottomColor {
                        inner: border.bottom_color,
                    })),
                    left: Some(CssPropertyValue::Exact(StyleBorderLeftColor {
                        inner: border.left_color,
                    })),
                };
                let border_styles = StyleBorderStyles {
                    top: Some(CssPropertyValue::Exact(StyleBorderTopStyle {
                        inner: BorderStyle::Solid,
                    })),
                    right: Some(CssPropertyValue::Exact(StyleBorderRightStyle {
                        inner: BorderStyle::Solid,
                    })),
                    bottom: Some(CssPropertyValue::Exact(StyleBorderBottomStyle {
                        inner: BorderStyle::Solid,
                    })),
                    left: Some(CssPropertyValue::Exact(StyleBorderLeftStyle {
                        inner: BorderStyle::Solid,
                    })),
                };
                let radius_px = PixelValue::px(border.radius.unwrap_or(0.0));
                let border_radius = StyleBorderRadius {
                    top_left: radius_px,
                    top_right: radius_px,
                    bottom_left: radius_px,
                    bottom_right: radius_px,
                };
                self.push_border(
                    bounds,
                    border_widths,
                    border_colors,
                    border_styles,
                    border_radius,
                );
            }
5040
        }
5040
    }
    /// Push a linear gradient background
35
    pub fn push_linear_gradient(
35
        &mut self,
35
        bounds: LogicalRect,
35
        gradient: LinearGradient,
35
        border_radius: BorderRadius,
35
    ) {
35
        self.push_item(DisplayListItem::LinearGradient {
35
            bounds: bounds.into(),
35
            gradient,
35
            border_radius,
35
        });
35
    }
    /// Push a radial gradient background
    pub fn push_radial_gradient(
        &mut self,
        bounds: LogicalRect,
        gradient: RadialGradient,
        border_radius: BorderRadius,
    ) {
        self.push_item(DisplayListItem::RadialGradient {
            bounds: bounds.into(),
            gradient,
            border_radius,
        });
    }
    /// Push a conic gradient background
    pub fn push_conic_gradient(
        &mut self,
        bounds: LogicalRect,
        gradient: ConicGradient,
        border_radius: BorderRadius,
    ) {
        self.push_item(DisplayListItem::ConicGradient {
            bounds: bounds.into(),
            gradient,
            border_radius,
        });
    }
    pub fn push_selection_rect(
        &mut self,
        bounds: LogicalRect,
        color: ColorU,
        border_radius: BorderRadius,
    ) {
        if color.a > 0 {
            self.push_item(DisplayListItem::SelectionRect {
                bounds: bounds.into(),
                color,
                border_radius,
            });
        }
    }
910
    pub fn push_cursor_rect(&mut self, bounds: LogicalRect, color: ColorU) {
910
        if color.a > 0 {
910
            self.push_item(DisplayListItem::CursorRect { bounds: bounds.into(), color });
910
        }
910
    }
105
    pub fn push_clip(&mut self, bounds: LogicalRect, border_radius: BorderRadius) {
105
        self.push_item(DisplayListItem::PushClip {
105
            bounds: bounds.into(),
105
            border_radius,
105
        });
105
    }
105
    pub fn pop_clip(&mut self) {
105
        self.push_item(DisplayListItem::PopClip);
105
    }
175
    pub fn push_image_mask_clip(&mut self, bounds: LogicalRect, mask_image: ImageRef, mask_rect: LogicalRect) {
175
        self.push_item(DisplayListItem::PushImageMaskClip {
175
            bounds: bounds.into(),
175
            mask_image,
175
            mask_rect: mask_rect.into(),
175
        });
175
    }
175
    pub fn pop_image_mask_clip(&mut self) {
175
        self.push_item(DisplayListItem::PopImageMaskClip);
175
    }
    pub fn push_scroll_frame(
        &mut self,
        clip_bounds: LogicalRect,
        content_size: LogicalSize,
        scroll_id: LocalScrollId,
    ) {
        self.push_item(DisplayListItem::PushScrollFrame {
            clip_bounds: clip_bounds.into(),
            content_size,
            scroll_id,
        });
    }
    pub fn pop_scroll_frame(&mut self) {
        self.push_item(DisplayListItem::PopScrollFrame);
    }
    pub fn push_virtual_view_placeholder(
        &mut self,
        node_id: NodeId,
        bounds: LogicalRect,
        clip_rect: LogicalRect,
    ) {
        self.push_item(DisplayListItem::VirtualViewPlaceholder {
            node_id,
            bounds: bounds.into(),
            clip_rect: clip_rect.into(),
        });
    }
15470
    pub fn push_border(
15470
        &mut self,
15470
        bounds: LogicalRect,
15470
        widths: StyleBorderWidths,
15470
        colors: StyleBorderColors,
15470
        styles: StyleBorderStyles,
15470
        border_radius: StyleBorderRadius,
15470
    ) {
        // Check if any border side is visible
15470
        let has_visible_border = {
15470
            let has_width = widths.top.is_some()
                || widths.right.is_some()
                || widths.bottom.is_some()
                || widths.left.is_some();
15470
            let has_style = styles.top.is_some()
                || styles.right.is_some()
                || styles.bottom.is_some()
                || styles.left.is_some();
15470
            has_width && has_style
        };
15470
        if has_visible_border {
15470
            self.push_item(DisplayListItem::Border {
15470
                bounds: bounds.into(),
15470
                widths,
15470
                colors,
15470
                styles,
15470
                border_radius,
15470
            });
15470
        }
15470
    }
5180
    pub fn push_stacking_context(&mut self, z_index: i32, bounds: LogicalRect) {
5180
        self.push_item(DisplayListItem::PushStackingContext { z_index, bounds: bounds.into() });
5180
    }
5180
    pub fn pop_stacking_context(&mut self) {
5180
        self.push_item(DisplayListItem::PopStackingContext);
5180
    }
70
    pub fn push_reference_frame(
70
        &mut self,
70
        transform_key: TransformKey,
70
        initial_transform: ComputedTransform3D,
70
        bounds: LogicalRect,
70
    ) {
70
        self.push_item(DisplayListItem::PushReferenceFrame {
70
            transform_key,
70
            initial_transform,
70
            bounds: bounds.into(),
70
        });
70
    }
70
    pub fn pop_reference_frame(&mut self) {
70
        self.push_item(DisplayListItem::PopReferenceFrame);
70
    }
5250
    pub fn push_text_run(
5250
        &mut self,
5250
        glyphs: Vec<GlyphInstance>,
5250
        font_hash: FontHash, // Just the hash, not the full FontRef
5250
        font_size_px: f32,
5250
        color: ColorU,
5250
        clip_rect: LogicalRect,
5250
        source_node_index: Option<usize>,
5250
    ) {
5250
        self.debug_log(format!(
5250
            "[push_text_run] {} glyphs, font_size={}px, color=({},{},{},{}), clip={:?}",
5250
            glyphs.len(),
            font_size_px,
            color.r,
            color.g,
            color.b,
            color.a,
            clip_rect
        ));
5250
        if !glyphs.is_empty() && color.a > 0 {
5250
            self.push_item(DisplayListItem::Text {
5250
                glyphs,
5250
                font_hash,
5250
                font_size_px,
5250
                color,
5250
                clip_rect: clip_rect.into(),
5250
                source_node_index,
5250
            });
5250
        } else {
            self.debug_log(format!(
                "[push_text_run] SKIPPED: glyphs.is_empty()={}, color.a={}",
                glyphs.is_empty(),
                color.a
            ));
        }
5250
    }
5250
    pub fn push_text_layout(
5250
        &mut self,
5250
        layout: Arc<dyn std::any::Any + Send + Sync>,
5250
        bounds: LogicalRect,
5250
        font_hash: FontHash,
5250
        font_size_px: f32,
5250
        color: ColorU,
5250
    ) {
5250
        if color.a > 0 {
5250
            self.push_item(DisplayListItem::TextLayout {
5250
                layout,
5250
                bounds: bounds.into(),
5250
                font_hash,
5250
                font_size_px,
5250
                color,
5250
            });
5250
        }
5250
    }
    pub fn push_underline(&mut self, bounds: LogicalRect, color: ColorU, thickness: f32) {
        if color.a > 0 && thickness > 0.0 {
            self.push_item(DisplayListItem::Underline {
                bounds: bounds.into(),
                color,
                thickness,
            });
        }
    }
    pub fn push_strikethrough(&mut self, bounds: LogicalRect, color: ColorU, thickness: f32) {
        if color.a > 0 && thickness > 0.0 {
            self.push_item(DisplayListItem::Strikethrough {
                bounds: bounds.into(),
                color,
                thickness,
            });
        }
    }
    pub fn push_overline(&mut self, bounds: LogicalRect, color: ColorU, thickness: f32) {
        if color.a > 0 && thickness > 0.0 {
            self.push_item(DisplayListItem::Overline {
                bounds: bounds.into(),
                color,
                thickness,
            });
        }
    }
    pub fn push_image(&mut self, bounds: LogicalRect, image: ImageRef, border_radius: BorderRadius) {
        self.push_item(DisplayListItem::Image { bounds: bounds.into(), image, border_radius });
    }
}
/// Main entry point for generating the display list.
3240
pub fn generate_display_list<T: ParsedFontTrait + Sync + 'static>(
3240
    ctx: &mut LayoutContext<T>,
3240
    tree: &LayoutTree,
3240
    calculated_positions: &super::PositionVec,
3240
    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
3240
    scroll_ids: &HashMap<usize, u64>,
3240
    gpu_value_cache: Option<&GpuValueCache>,
3240
    renderer_resources: &RendererResources,
3240
    id_namespace: IdNamespace,
3240
    dom_id: DomId,
3240
) -> Result<DisplayList> {
3240
    debug_info!(
2295
        ctx,
2295
        "[DisplayList] generate_display_list: tree has {} nodes, {} positions calculated",
2295
        tree.nodes.len(),
2295
        calculated_positions.len()
    );
3240
    debug_info!(ctx, "Starting display list generation");
3240
    debug_info!(
2295
        ctx,
2295
        "Collecting stacking contexts from root node {}",
        tree.root
    );
3240
    let positioned_tree = PositionedTree {
3240
        tree,
3240
        calculated_positions,
3240
    };
3240
    let mut generator = DisplayListGenerator::new(
3240
        ctx,
3240
        scroll_offsets,
3240
        &positioned_tree,
3240
        scroll_ids,
3240
        gpu_value_cache,
3240
        renderer_resources,
3240
        id_namespace,
3240
        dom_id,
    );
    // Create builder with debug enabled if ctx has debug messages
3240
    let debug_enabled = generator.ctx.debug_messages.is_some();
3240
    let mut builder = DisplayListBuilder::with_debug(debug_enabled);
    // 0. Canvas background propagation (CSS 2.1 § 14.2):
    //    "The background of the root element becomes the background of the canvas."
    //    If the root (html) has a transparent background, propagate from <body>.
    //    The canvas background fills the ENTIRE viewport, not just the root's content box.
    //    This is critical when <html> doesn't have height:100% — without this,
    //    the body's background only covers the body's content area, not the viewport.
    {
3240
        let root_node = tree.get(tree.root);
3240
        if let Some(root) = root_node {
3240
            if let Some(root_dom_id) = root.dom_node_id {
3240
                let root_state = generator.get_styled_node_state(root_dom_id);
3240
                let canvas_bg = get_background_color(
3240
                    generator.ctx.styled_dom,
3240
                    root_dom_id,
3240
                    &root_state,
                );
3240
                if canvas_bg.a > 0 {
1261
                    let viewport_rect = LogicalRect {
1261
                        origin: LogicalPosition::zero(),
1261
                        size: generator.ctx.viewport_size,
1261
                    };
1261
                    builder.push_rect(viewport_rect, canvas_bg, BorderRadius::default());
1261
                    debug_info!(
386
                        generator.ctx,
386
                        "[DisplayList] Canvas background: color=({},{},{},{}), size={:?}",
                        canvas_bg.r, canvas_bg.g, canvas_bg.b, canvas_bg.a,
                        generator.ctx.viewport_size
                    );
1979
                }
            }
        }
    }
    // +spec:stacking-contexts:33d435 - CSS 2.2 painting order: build stacking context tree then traverse in z-order
    // +spec:stacking-contexts:887766 - CSS2 §9.9 stacking contexts, z-index layering, and painting order
    // 1. Build a tree of stacking contexts, which defines the global paint order.
    // +spec:display-property:9a419c - root element always forms a stacking context (it's the tree root)
3240
    let stacking_context_tree = generator.collect_stacking_contexts(tree.root)?;
    // 2. Traverse the stacking context tree to generate display items in the correct order.
3240
    debug_info!(
2295
        generator.ctx,
2295
        "Generating display items from stacking context tree"
    );
3240
    generator.generate_for_stacking_context(&mut builder, &stacking_context_tree)?;
    // Build display list and transfer debug messages to context
3240
    let display_list = builder.build_with_debug(generator.ctx.debug_messages);
3240
    debug_info!(
2295
        generator.ctx,
2295
        "[DisplayList] Generated {} display items",
2295
        display_list.items.len()
    );
3240
    Ok(display_list)
3240
}
/// A helper struct that holds all necessary state and context for the generation process.
struct DisplayListGenerator<'a, 'b, T: ParsedFontTrait> {
    ctx: &'a mut LayoutContext<'b, T>,
    scroll_offsets: &'a BTreeMap<NodeId, ScrollPosition>,
    positioned_tree: &'a PositionedTree<'a>,
    scroll_ids: &'a HashMap<usize, u64>,
    gpu_value_cache: Option<&'a GpuValueCache>,
    renderer_resources: &'a RendererResources,
    id_namespace: IdNamespace,
    dom_id: DomId,
}
// +spec:stacking-contexts:9e85a3 - Stacking context tree: hierarchical, nested, atomic painting order
/// Represents a node in the CSS stacking context tree, not the DOM tree.
#[derive(Debug)]
struct StackingContext {
    node_index: usize,
    z_index: i32,
    child_contexts: Vec<StackingContext>,
    /// Children that do not create their own stacking contexts and are painted in DOM order.
    in_flow_children: Vec<usize>,
}
impl<'a, 'b, T> DisplayListGenerator<'a, 'b, T>
where
    T: ParsedFontTrait + Sync + 'static,
{
3240
    pub fn new(
3240
        ctx: &'a mut LayoutContext<'b, T>,
3240
        scroll_offsets: &'a BTreeMap<NodeId, ScrollPosition>,
3240
        positioned_tree: &'a PositionedTree<'a>,
3240
        scroll_ids: &'a HashMap<usize, u64>,
3240
        gpu_value_cache: Option<&'a GpuValueCache>,
3240
        renderer_resources: &'a RendererResources,
3240
        id_namespace: IdNamespace,
3240
        dom_id: DomId,
3240
    ) -> Self {
3240
        Self {
3240
            ctx,
3240
            scroll_offsets,
3240
            positioned_tree,
3240
            scroll_ids,
3240
            gpu_value_cache,
3240
            renderer_resources,
3240
            id_namespace,
3240
            dom_id,
3240
        }
3240
    }
    /// Helper to get styled node state for a node
130254
    fn get_styled_node_state(&self, dom_id: NodeId) -> azul_core::styled_dom::StyledNodeState {
130254
        self.ctx
130254
            .styled_dom
130254
            .styled_nodes
130254
            .as_container()
130254
            .get(dom_id)
130254
            .map(|n| n.styled_node_state.clone())
130254
            .unwrap_or_default()
130254
    }
    // +spec:overflow:visibility - CSS 2.2 §11.2: visibility:hidden makes the box invisible
    // but still affects layout. Checked per-node because visibility is inherited and a child
    // with visibility:visible inside a hidden parent must still be painted.
56422
    fn is_node_hidden(&self, node_index: usize) -> bool {
        use azul_css::props::style::effects::StyleVisibility;
56422
        let node = match self.positioned_tree.tree.get(node_index) {
56422
            Some(n) => n,
            None => return false,
        };
56422
        let dom_id = match node.dom_node_id {
56200
            Some(id) => id,
222
            None => return false,
        };
56200
        let node_state = self.get_styled_node_state(dom_id);
56200
        match get_visibility(self.ctx.styled_dom, dom_id, &node_state) {
            crate::solver3::getters::MultiValue::Exact(StyleVisibility::Hidden)
            | crate::solver3::getters::MultiValue::Exact(StyleVisibility::Collapse) => true,
56200
            _ => false,
        }
56422
    }
    /// Gets the cursor type for a text node from its CSS properties.
    /// Defaults to Text (I-beam) cursor if no explicit cursor is set.
4088
    fn get_cursor_type_for_text_node(&self, node_id: NodeId) -> CursorType {
        use azul_css::props::style::effects::StyleCursor;
4088
        let styled_node_state = self.get_styled_node_state(node_id);
4088
        let node_data_container = self.ctx.styled_dom.node_data.as_container();
4088
        let node_data = node_data_container.get(node_id);
        // Query the cursor CSS property for this text node
4088
        if let Some(node_data) = node_data {
4088
            if let Some(cursor_value) = self.ctx.styled_dom.get_css_property_cache().get_cursor(
4088
                node_data,
4088
                &node_id,
4088
                &styled_node_state,
4088
            ) {
4088
                if let CssPropertyValue::Exact(cursor) = cursor_value {
4088
                    return match cursor {
                        StyleCursor::Default => CursorType::Default,
                        StyleCursor::Pointer => CursorType::Pointer,
4088
                        StyleCursor::Text => CursorType::Text,
                        StyleCursor::Crosshair => CursorType::Crosshair,
                        StyleCursor::Move => CursorType::Move,
                        StyleCursor::Help => CursorType::Help,
                        StyleCursor::Wait => CursorType::Wait,
                        StyleCursor::Progress => CursorType::Progress,
                        StyleCursor::NsResize => CursorType::NsResize,
                        StyleCursor::EwResize => CursorType::EwResize,
                        StyleCursor::NeswResize => CursorType::NeswResize,
                        StyleCursor::NwseResize => CursorType::NwseResize,
                        StyleCursor::NResize => CursorType::NResize,
                        StyleCursor::SResize => CursorType::SResize,
                        StyleCursor::EResize => CursorType::EResize,
                        StyleCursor::WResize => CursorType::WResize,
                        StyleCursor::Grab => CursorType::Grab,
                        StyleCursor::Grabbing => CursorType::Grabbing,
                        StyleCursor::RowResize => CursorType::RowResize,
                        StyleCursor::ColResize => CursorType::ColResize,
                        // Map less common cursors to closest available
                        StyleCursor::SeResize | StyleCursor::NeswResize => CursorType::NeswResize,
                        StyleCursor::ZoomIn | StyleCursor::ZoomOut => CursorType::Default,
                        StyleCursor::Copy | StyleCursor::Alias => CursorType::Default,
                        StyleCursor::Cell => CursorType::Crosshair,
                        StyleCursor::AllScroll => CursorType::Move,
                        StyleCursor::ContextMenu => CursorType::Default,
                        StyleCursor::VerticalText => CursorType::Text,
                        StyleCursor::Unset => CursorType::Text, // Default to text for text nodes
                    };
                }
            }
        }
        // Default: Text cursor (I-beam) for text nodes
        CursorType::Text
4088
    }
    /// Emits drawing commands for text selections only (not cursor).
    /// The cursor is drawn separately via `paint_cursor()`.
17704
    fn paint_selections(
17704
        &self,
17704
        builder: &mut DisplayListBuilder,
17704
        node_index: usize,
17704
    ) -> Result<()> {
17704
        let node = self
17704
            .positioned_tree
17704
            .tree
17704
            .get(node_index)
17704
            .ok_or(LayoutError::InvalidTree)?;
17704
        let Some(dom_id) = node.dom_node_id else {
74
            return Ok(());
        };
        // Get inline layout using the unified helper that handles IFC membership
        // This is critical: text nodes don't have their own inline_layout_result,
        // but they have ifc_membership pointing to their IFC root
17630
        let Some(layout) = self.positioned_tree.tree.get_inline_layout_for_node(node_index) else {
10641
            return Ok(());
        };
        // Get the absolute position of this node (border-box position)
6989
        let node_pos = self
6989
            .positioned_tree
6989
            .calculated_positions
6989
            .get(node_index)
6989
            .copied()
6989
            .unwrap_or_default();
        // Selection rects are relative to content-box origin
6989
        let bp = node.box_props.unpack();
6989
        let padding = &bp.padding;
6989
        let border = &bp.border;
6989
        let content_box_offset_x = node_pos.x + padding.left + border.left;
6989
        let content_box_offset_y = node_pos.y + padding.top + border.top;
        // Check if text is selectable (respects CSS user-select property)
6989
        let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
6989
        let is_selectable = super::getters::is_text_selectable(self.ctx.styled_dom, dom_id, node_state);
6989
        if !is_selectable {
            return Ok(());
6989
        }
        // === NEW: Check text_selections first (multi-node selection support) ===
6989
        if let Some(text_selection) = self.ctx.text_selections.get(&self.ctx.styled_dom.dom_id) {
            if let Some(range) = text_selection.affected_nodes.get(&dom_id) {
                let is_collapsed = text_selection.is_collapsed();
                // Only draw selection highlight if NOT collapsed
                if !is_collapsed {
                    let rects = layout.get_selection_rects(range);
                    let style = get_selection_style(self.ctx.styled_dom, Some(dom_id), self.ctx.system_style.as_ref());
                    let border_radius = BorderRadius {
                        top_left: style.radius,
                        top_right: style.radius,
                        bottom_left: style.radius,
                        bottom_right: style.radius,
                    };
                    for mut rect in rects {
                        rect.origin.x += content_box_offset_x;
                        rect.origin.y += content_box_offset_y;
                        builder.push_selection_rect(rect, style.bg_color, border_radius);
                    }
                }
                return Ok(());
            }
6989
        }
6989
        Ok(())
17704
    }
    /// Emits drawing commands for all text cursors (carets).
    /// Iterates over `ctx.cursor_locations` to support multi-cursor rendering.
    /// Preedit underline is only rendered for the primary (last) cursor.
17704
    fn paint_cursor(
17704
        &self,
17704
        builder: &mut DisplayListBuilder,
17704
        node_index: usize,
17704
    ) -> Result<()> {
        // Early exit if cursor is not visible (blinking off phase)
17704
        if !self.ctx.cursor_is_visible {
15855
            return Ok(());
1849
        }
        // Early exit if no cursor locations
1849
        if self.ctx.cursor_locations.is_empty() {
274
            return Ok(());
1575
        }
1575
        let node = self
1575
            .positioned_tree
1575
            .tree
1575
            .get(node_index)
1575
            .ok_or(LayoutError::InvalidTree)?;
1575
        let Some(dom_id) = node.dom_node_id else {
35
            return Ok(());
        };
        // Check if this node is contenteditable
1540
        let is_contenteditable = super::getters::is_node_contenteditable_inherited(self.ctx.styled_dom, dom_id);
1540
        if !is_contenteditable {
490
            return Ok(());
1050
        }
        // Check if text is selectable
1050
        let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
1050
        let is_selectable = super::getters::is_text_selectable(self.ctx.styled_dom, dom_id, node_state);
1050
        if !is_selectable {
            return Ok(());
1050
        }
        // Get inline layout
1050
        let Some(layout) = self.positioned_tree.tree.get_inline_layout_for_node(node_index) else {
            return Ok(());
        };
        // Compute content-box offset once
1050
        let node_pos = self
1050
            .positioned_tree
1050
            .calculated_positions
1050
            .get(node_index)
1050
            .copied()
1050
            .unwrap_or_default();
1050
        let bp = node.box_props.unpack();
1050
        let padding = &bp.padding;
1050
        let border = &bp.border;
1050
        let content_box_offset_x = node_pos.x + padding.left + border.left;
1050
        let content_box_offset_y = node_pos.y + padding.top + border.top;
1050
        let style = get_caret_style(self.ctx.styled_dom, Some(dom_id));
        // Find the index of the last (primary) cursor that belongs to this DOM/node,
        // so preedit underline is only drawn on the actual primary cursor.
1050
        let primary_idx_for_this_node = self.ctx.cursor_locations.iter().enumerate()
1050
            .rev()
1050
            .find(|(_, (cd, cn, _))| {
1050
                *cd == self.ctx.styled_dom.dom_id && (*cn == dom_id || self.positioned_tree.tree.children(node_index).iter().any(|&child_idx| {
525
                    self.positioned_tree.tree.get(child_idx)
525
                        .and_then(|c| c.dom_node_id)
525
                        .map(|id| id == *cn)
525
                        .unwrap_or(false)
525
                }))
1050
            })
1050
            .map(|(i, _)| i);
1050
        for (i, (cursor_dom_id, cursor_node_id, cursor)) in self.ctx.cursor_locations.iter().enumerate() {
            // Check DOM ID matches
1050
            if self.ctx.styled_dom.dom_id != *cursor_dom_id {
                continue;
1050
            }
            // Check this node contains the cursor
1050
            if dom_id != *cursor_node_id {
595
                let is_ifc_root_of_cursor = self.positioned_tree.tree.children(node_index)
595
                    .iter()
595
                    .any(|&child_idx| {
525
                        self.positioned_tree.tree.get(child_idx)
525
                            .and_then(|c| c.dom_node_id)
525
                            .map(|id| id == *cursor_node_id)
525
                            .unwrap_or(false)
525
                    });
595
                if !is_ifc_root_of_cursor {
140
                    continue;
455
                }
455
            }
            // Get cursor rect from text layout
910
            let Some(mut rect) = layout.get_cursor_rect(cursor) else {
                continue;
            };
910
            rect.origin.x += content_box_offset_x;
910
            rect.origin.y += content_box_offset_y;
910
            rect.size.width = style.width;
910
            builder.push_cursor_rect(rect, style.color);
            // Preedit underline only on the primary cursor for this node
910
            let is_primary = primary_idx_for_this_node == Some(i);
910
            if is_primary {
910
                if let Some(ref preedit) = self.ctx.preedit_text {
                    if !preedit.is_empty() {
                        let char_count = preedit.chars().count() as f32;
                        let approx_char_width = style.width.max(8.0);
                        let preedit_width = char_count * approx_char_width;
                        let underline_bounds = azul_core::geom::LogicalRect {
                            origin: azul_core::geom::LogicalPosition {
                                x: rect.origin.x + rect.size.width,
                                y: rect.origin.y + rect.size.height - 2.0,
                            },
                            size: azul_core::geom::LogicalSize {
                                width: preedit_width,
                                height: 2.0,
                            },
                        };
                        builder.push_underline(underline_bounds, style.color, 2.0);
                    }
910
                }
            }
        }
1050
        Ok(())
17704
    }
    /// Emits drawing commands for selection and cursor.
    /// Delegates to `paint_selections()` and `paint_cursor()`.
17704
    fn paint_selection_and_cursor(
17704
        &self,
17704
        builder: &mut DisplayListBuilder,
17704
        node_index: usize,
17704
    ) -> Result<()> {
17704
        self.paint_selections(builder, node_index)?;
17704
        self.paint_cursor(builder, node_index)?;
17704
        Ok(())
17704
    }
    /// Recursively builds the tree of stacking contexts starting from a given layout node.
    // +spec:writing-modes:a86a28 - preorder depth-first traversal of the rendering tree in logical order
3310
    fn collect_stacking_contexts(&mut self, node_index: usize) -> Result<StackingContext> {
3310
        let node = self
3310
            .positioned_tree
3310
            .tree
3310
            .get(node_index)
3310
            .ok_or(LayoutError::InvalidTree)?;
3310
        let z_index = get_z_index(self.ctx.styled_dom, node.dom_node_id);
3310
        if let Some(dom_id) = node.dom_node_id {
3310
            let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
3310
            debug_info!(
2365
                self.ctx,
2365
                "Collecting stacking context for node {} ({:?}), z-index={}",
                node_index,
2365
                node_type.get_node_type(),
                z_index
            );
        }
3310
        let mut child_contexts = Vec::new();
3310
        let mut in_flow_children = Vec::new();
3310
        for &child_index in self.positioned_tree.tree.children(node_index) {
3030
            if self.establishes_stacking_context(child_index) {
70
                child_contexts.push(self.collect_stacking_contexts(child_index)?);
            } else {
2960
                in_flow_children.push(child_index);
                // Recurse into non-stacking-context children to find nested
                // stacking contexts. Per CSS 2.2 Appendix E, these are promoted
                // to be child stacking contexts of the nearest ancestor SC.
2960
                self.find_nested_stacking_contexts(child_index, &mut child_contexts)?;
            }
        }
3310
        Ok(StackingContext {
3310
            node_index,
3310
            z_index,
3310
            child_contexts,
3310
            in_flow_children,
3310
        })
3310
    }
    /// Recursively searches non-stacking-context subtrees for nested stacking
    /// contexts, promoting them to the parent stacking context's child list.
14394
    fn find_nested_stacking_contexts(
14394
        &mut self,
14394
        parent_index: usize,
14394
        child_contexts: &mut Vec<StackingContext>,
14394
    ) -> Result<()> {
14394
        for &child_index in self.positioned_tree.tree.children(parent_index) {
11434
            if self.establishes_stacking_context(child_index) {
                child_contexts.push(self.collect_stacking_contexts(child_index)?);
            } else {
11434
                self.find_nested_stacking_contexts(child_index, child_contexts)?;
            }
        }
14394
        Ok(())
14394
    }
    // +spec:box-model:de94ab - stacking context painting order (negative z, in-flow, z=0, positive z)
    // +spec:display-property:337069 - CSS 2.2 E.2 painting order: stacking contexts sorted by z-index, in-flow children in tree order
    // +spec:display-property:7b0a87 - CSS 2.2 E.2 painting order: negative z-index, in-flow, z-index 0/auto, positive z-index
    // +spec:stacking-contexts:5cbdfb - full CSS painting order (bg, neg-z, in-flow, z0, pos-z)
    // +spec:stacking-contexts:3ded3a - CSS 2.2 Appendix E painting order: definitions and tree order traversal
    // +spec:stacking-contexts:973368 - CSS 2.2 Appendix E.2 painting order: bg/border, negative z, in-flow, zero z, positive z
    // +spec:stacking-contexts:464bb7 - CSS 2.2 §9.9.1 painting order: negative z-index, in-flow, z-index 0, positive z-index (recursive)
    /// Recursively traverses the stacking context tree, emitting drawing commands to the builder
    /// according to the CSS Painting Algorithm specification.
    // +spec:display-property:39e879 - CSS 2.2 E.2 painting order for block-level and inline-level elements
    // +spec:display-property:de4c66 - CSS 2.2 E.2 stacking context paint order (canvas bg, negative z, in-flow, floats, inline, positive z)
    // +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
    // +spec:stacking-contexts:55ca96 - CSS 2.2 E.2 painting order: backgrounds, negative z-index, in-flow, z-index 0/auto, positive z-index
3310
    fn generate_for_stacking_context(
3310
        &mut self,
3310
        builder: &mut DisplayListBuilder,
3310
        context: &StackingContext,
3310
    ) -> Result<()> {
        // Before painting the node, check if it establishes a new clip or scroll frame.
3310
        let node = self
3310
            .positioned_tree
3310
            .tree
3310
            .get(context.node_index)
3310
            .ok_or(LayoutError::InvalidTree)?;
3310
        if let Some(dom_id) = node.dom_node_id {
3310
            let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
3310
            debug_info!(
2365
                self.ctx,
2365
                "Painting stacking context for node {} ({:?}), z-index={}, {} child contexts, {} \
2365
                 in-flow children",
                context.node_index,
2365
                node_type.get_node_type(),
                context.z_index,
2365
                context.child_contexts.len(),
2365
                context.in_flow_children.len()
            );
        }
        // Set current node BEFORE pushing stacking context so that
        // the PushStackingContext item gets the correct node_mapping entry.
        // This is critical for drag visual offset matching.
3310
        builder.set_current_node(node.dom_node_id);
        // Track fixed-position elements for paged media replication (CSS Positioned Layout §2.1)
3310
        let is_fixed_position = node.dom_node_id
3310
            .map(|dom_id| get_position_type(self.ctx.styled_dom, Some(dom_id)) == LayoutPosition::Fixed)
3310
            .unwrap_or(false);
3310
        if is_fixed_position {
            builder.begin_fixed_position_element();
3310
        }
        // Check if this node has a GPU-accelerated transform (CSS transform or drag).
        // If so, wrap in a reference frame so WebRender can animate it on the GPU.
3310
        let has_reference_frame = node.dom_node_id.and_then(|dom_id| {
3310
            self.gpu_value_cache.and_then(|cache| {
2765
                let key = cache.css_transform_keys.get(&dom_id)?;
70
                let transform = cache.css_current_transform_values.get(&dom_id)?;
70
                Some((*key, *transform))
2765
            })
3310
        });
        // Push a stacking context for WebRender
        // Get the node's bounds for the stacking context
3310
        let node_pos = self
3310
            .positioned_tree
3310
            .calculated_positions
3310
            .get(context.node_index)
3310
            .copied()
3310
            .unwrap_or_default();
3310
        let node_size = node.used_size.unwrap_or(LogicalSize {
3310
            width: 0.0,
3310
            height: 0.0,
3310
        });
3310
        let node_bounds = LogicalRect {
3310
            origin: node_pos,
3310
            size: node_size,
3310
        };
        // Push reference frame BEFORE stacking context if node has a transform
3310
        if let Some((transform_key, initial_transform)) = has_reference_frame {
70
            builder.push_reference_frame(transform_key, initial_transform, node_bounds);
3240
        }
3310
        builder.push_stacking_context(context.z_index, node_bounds);
        // Push opacity/filter effects if the node has them
3310
        let mut pushed_opacity = false;
3310
        let mut pushed_filter = false;
3310
        let mut pushed_backdrop_filter = false;
3310
        if let Some(dom_id) = node.dom_node_id {
3310
            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
3310
            let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
            // Opacity (GPU: fast path via compact cache)
3310
            let opacity = crate::solver3::getters::get_opacity(
3310
                self.ctx.styled_dom, dom_id, node_state,
            );
3310
            if opacity < 1.0 {
70
                builder.push_item(DisplayListItem::PushOpacity {
70
                    bounds: node_bounds.into(),
70
                    opacity,
70
                });
70
                pushed_opacity = true;
3240
            }
            // Filter
3310
            if let Some(filter_vec_value) = self.ctx.styled_dom.css_property_cache.ptr
3310
                .get_filter(node_data, &dom_id, node_state)
            {
                if let Some(filter_vec) = filter_vec_value.get_property() {
                    let filters: Vec<_> = filter_vec.as_ref().to_vec();
                    if !filters.is_empty() {
                        builder.push_item(DisplayListItem::PushFilter {
                            bounds: node_bounds.into(),
                            filters,
                        });
                        pushed_filter = true;
                    }
                }
3310
            }
            // Backdrop filter
3310
            if let Some(backdrop_filter_value) = self.ctx.styled_dom.css_property_cache.ptr
3310
                .get_backdrop_filter(node_data, &dom_id, node_state)
            {
                if let Some(filter_vec) = backdrop_filter_value.get_property() {
                    let filters: Vec<_> = filter_vec.as_ref().to_vec();
                    if !filters.is_empty() {
                        builder.push_item(DisplayListItem::PushBackdropFilter {
                            bounds: node_bounds.into(),
                            filters,
                        });
                        pushed_backdrop_filter = true;
                    }
                }
3310
            }
        }
        // 0b. Push image mask clip if this node has one.
        // This wraps background, border, and all children so the SVG mask clips everything.
3310
        let did_push_image_mask = self.push_image_mask_clip(builder, context.node_index);
        // +spec:box-model:84b238 - CSS 2.2 E.2 painting order: bg/border, negative z, in-flow, z=0, positive z
        // 1. Paint background and borders for the context's root element.
        // This must be BEFORE push_node_clips so the container background
        // is rendered in parent space (stationary), not scroll space.
        // +spec:overflow:40052b - backgrounds paint at border-box, scrollbars overlay on top (scrollbar-extended background positioning area)
3310
        self.paint_node_background_and_border(builder, context.node_index)?;
        // 1b. For scrollable containers, push the hit-test area BEFORE the scroll frame
        // so the hit-test covers the entire container box (including visible area),
        // not just the scrolled content. This ensures scroll wheel events hit the
        // container regardless of scroll position.
        // +spec:overflow:visibility - visibility:hidden scroll containers must not allow
        // interactive scrolling per CSS 2.2 §11.2
3310
        if !self.is_node_hidden(context.node_index) {
3310
            if let Some(dom_id) = node.dom_node_id {
3310
                let styled_node_state = self.get_styled_node_state(dom_id);
3310
                let overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
3310
                let overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
3310
                if overflow_x.is_scroll() || overflow_y.is_scroll() {
                    if let Some(tag_id) = get_tag_id(self.ctx.styled_dom, node.dom_node_id) {
                        builder.push_hit_test_area(node_bounds, tag_id);
                    }
3310
                }
            }
        }
        // 2. Push clips and scroll frames AFTER painting background
        // +spec:positioning:ddc554 - overflow clips apply to absolutely positioned descendants
        // when this node is their containing block (stacking contexts painted within clip scope)
        // TODO: CSS Overflow 3 says overflow clips should NOT apply to abs-pos descendants
        // whose containing block is above this clipper. Currently all descendants are clipped.
        // The containing_block_index field on LayoutNode is set for this purpose.
3310
        let did_push_clip_or_scroll = self.push_node_clips(builder, context.node_index, node)?;
        // +spec:display-contents:434de8 - E.2 painting order: negative z-index, in-flow, z-index 0/auto, positive z-index
        // 3. Paint child stacking contexts with negative z-indices.
3310
        let mut negative_z_children: Vec<_> = context
3310
            .child_contexts
3310
            .iter()
3310
            .filter(|c| c.z_index < 0)
3310
            .collect();
3310
        negative_z_children.sort_by_key(|c| c.z_index);
3310
        for child in negative_z_children {
            self.generate_for_stacking_context(builder, child)?;
        }
        // 4. Paint the in-flow descendants of the context root.
3310
        self.paint_in_flow_descendants(builder, context.node_index, &context.in_flow_children)?;
        // +spec:stacking-contexts:9a4eb3 - z-index:auto/0 positioned descendants painted in tree order
        // 5. Paint child stacking contexts with z-index: 0 / auto.
3310
        for child in context.child_contexts.iter().filter(|c| c.z_index == 0) {
70
            self.generate_for_stacking_context(builder, child)?;
        }
        // +spec:stacking-contexts:198fa4 - positive z-index stacking contexts painted in z-index order then tree order
        // 6. Paint child stacking contexts with positive z-indices.
3310
        let mut positive_z_children: Vec<_> = context
3310
            .child_contexts
3310
            .iter()
3310
            .filter(|c| c.z_index > 0)
3310
            .collect();
3310
        positive_z_children.sort_by_key(|c| c.z_index);
3310
        for child in positive_z_children {
            self.generate_for_stacking_context(builder, child)?;
        }
        // Pop image mask clip (before filter/opacity since it was pushed after them)
3310
        if did_push_image_mask {
175
            builder.pop_image_mask_clip();
3135
        }
        // Pop filter/opacity effects (in reverse order of push)
3310
        if pushed_backdrop_filter {
            builder.push_item(DisplayListItem::PopBackdropFilter);
3310
        }
3310
        if pushed_filter {
            builder.push_item(DisplayListItem::PopFilter);
3310
        }
3310
        if pushed_opacity {
70
            builder.push_item(DisplayListItem::PopOpacity);
3240
        }
        // Pop the stacking context for WebRender
3310
        builder.pop_stacking_context();
        // Pop reference frame if we pushed one
3310
        if has_reference_frame.is_some() {
70
            builder.pop_reference_frame();
3240
        }
        // End fixed-position tracking (records the item range for paged media replication)
3310
        if is_fixed_position {
            builder.end_fixed_position_element();
3310
        }
        // After painting the node and all its descendants, pop any contexts it pushed.
        // For VirtualView nodes, emit the placeholder INSIDE the clip (before PopClip)
        // so the virtualized view viewport is clipped to the container.
3310
        if did_push_clip_or_scroll {
            // Emit VirtualViewPlaceholder before popping the clip so it's inside PushClip/PopClip
            if let Some(dom_id) = node.dom_node_id {
                if self.is_virtual_view_node(dom_id) {
                    builder.push_virtual_view_placeholder(dom_id, node_bounds, node_bounds);
                }
            }
            self.pop_node_clips(builder, node)?;
        } else {
            // Even without clips, emit VirtualViewPlaceholder for VirtualView nodes
3310
            if let Some(dom_id) = node.dom_node_id {
3310
                if self.is_virtual_view_node(dom_id) {
                    builder.push_virtual_view_placeholder(dom_id, node_bounds, node_bounds);
3310
                }
            }
        }
        // Paint scrollbars AFTER popping the clip, so they appear on top of content
        // and are not clipped by the scroll frame
3310
        self.paint_scrollbars(builder, context.node_index)?;
3310
        Ok(())
3310
    }
    /// Paints the content and non-stacking-context children.
17704
    fn paint_in_flow_descendants(
17704
        &mut self,
17704
        builder: &mut DisplayListBuilder,
17704
        node_index: usize,
17704
        children_indices: &[usize],
17704
    ) -> Result<()> {
        // NOTE: We do NOT paint the node's background here - that was already done by
        // generate_for_stacking_context! Only paint selection, cursor, and content for the
        // current node
        // 2. Paint selection highlights and the text cursor if applicable.
17704
        self.paint_selection_and_cursor(builder, node_index)?;
        // 3. Paint the node's own content (text, images, hit-test areas).
17704
        self.paint_node_content(builder, node_index)?;
        // +spec:display-property:86a3de - inline-level boxes painted in document order; z-index does not apply
        // +spec:floats:b8c494 - E.2 painting order: non-positioned floats painted after block-level descendants, in tree order
        // 4. Recursively paint the in-flow children in correct CSS painting order:
        //    - First: Non-float, non-dragging block-level children
        //    - Then: Float, non-dragging children (so they appear on top)
        //    - Finally: Dragging children (so they appear on top of everything per W3C spec)
        // Separate children into floats, non-floats, and dragging.
        // Skip children that establish stacking contexts - those are painted
        // separately via generate_for_stacking_context with proper z-ordering.
17704
        let mut non_float_children = Vec::new();
17704
        let mut float_children = Vec::new();
17704
        let mut dragging_children = Vec::new();
32098
        for &child_index in children_indices {
            // Skip stacking context children - they're painted by the stacking
            // context tree traversal, not by the in-flow descendant path.
14394
            if self.establishes_stacking_context(child_index) {
                continue;
14394
            }
14394
            let child_node = self
14394
                .positioned_tree
14394
                .tree
14394
                .get(child_index)
14394
                .ok_or(LayoutError::InvalidTree)?;
            // Check if this child is being dragged (paint last for z-order)
14394
            let is_dragging = if let Some(dom_id) = child_node.dom_node_id {
14320
                let styled_node_state = self.get_styled_node_state(dom_id);
14320
                styled_node_state.dragging
            } else {
74
                false
            };
14394
            if is_dragging {
                dragging_children.push(child_index);
                continue;
14394
            }
            // Check if this child is a float
14394
            let is_float = if let Some(dom_id) = child_node.dom_node_id {
                use crate::solver3::getters::get_float;
14320
                let styled_node_state = self.get_styled_node_state(dom_id);
14320
                let float_value = get_float(self.ctx.styled_dom, dom_id, &styled_node_state);
26
                !matches!(
14320
                    float_value.unwrap_or_default(),
                    azul_css::props::layout::LayoutFloat::None
                )
            } else {
74
                false
            };
14394
            if is_float {
26
                float_children.push(child_index);
14368
            } else {
14368
                non_float_children.push(child_index);
14368
            }
        }
        // Paint non-float children first
32072
        for child_index in non_float_children {
14368
            let child_node = self
14368
                .positioned_tree
14368
                .tree
14368
                .get(child_index)
14368
                .ok_or(LayoutError::InvalidTree)?;
            // Check if this child has a GPU transform (CSS transform or drag)
14368
            let child_ref_frame = child_node.dom_node_id.and_then(|dom_id| {
14294
                self.gpu_value_cache.and_then(|cache| {
13965
                    let key = cache.css_transform_keys.get(&dom_id)?;
                    let transform = cache.css_current_transform_values.get(&dom_id)?;
                    Some((*key, *transform))
13965
                })
14294
            });
            // Push reference frame if child has a transform
14368
            if let Some((transform_key, initial_transform)) = child_ref_frame {
                let child_pos = self
                    .positioned_tree
                    .calculated_positions
            .get(child_index)
                    .copied()
                    .unwrap_or_default();
                let child_size = child_node.used_size.unwrap_or(LogicalSize {
                    width: 0.0,
                    height: 0.0,
                });
                let child_bounds = LogicalRect {
                    origin: child_pos,
                    size: child_size,
                };
                builder.set_current_node(child_node.dom_node_id);
                builder.push_reference_frame(transform_key, initial_transform, child_bounds);
14368
            }
            // Push image mask clip if this child has one (wraps background + children)
14368
            let did_push_child_image_mask = self.push_image_mask_clip(builder, child_index);
            // IMPORTANT: Paint background and border BEFORE pushing clips!
            // This ensures the container's background is in parent space (stationary),
            // not in scroll space. Same logic as generate_for_stacking_context.
14368
            self.paint_node_background_and_border(builder, child_index)?;
            // Push clips and scroll frames AFTER painting background
14368
            let did_push_clip = self.push_node_clips(builder, child_index, child_node)?;
            // Paint descendants inside the clip/scroll frame
14368
            self.paint_in_flow_descendants(builder, child_index, self.positioned_tree.tree.children(child_index))?;
            // For VirtualView children: emit placeholder INSIDE the clip
14368
            if let Some(dom_id) = child_node.dom_node_id {
14294
                if self.is_virtual_view_node(dom_id) {
                    let child_bounds = self.get_paint_rect(child_index).unwrap_or_default();
                    builder.push_virtual_view_placeholder(dom_id, child_bounds, child_bounds);
14294
                }
74
            }
            // Pop the child's clips.
14368
            if did_push_clip {
37
                self.pop_node_clips(builder, child_node)?;
14331
            }
            // Pop image mask clip
14368
            if did_push_child_image_mask {
                builder.pop_image_mask_clip();
14368
            }
            // Paint scrollbars AFTER popping clips so they appear on top of content
14368
            self.paint_scrollbars(builder, child_index)?;
            // Pop reference frame if we pushed one
14368
            if child_ref_frame.is_some() {
                builder.pop_reference_frame();
14368
            }
        }
        // +spec:positioning:1bcbb5 - floats rendered in front of non-positioned in-flow blocks, but behind in-flow inlines
        // Paint float children AFTER non-floats (so they appear on top)
17730
        for child_index in float_children {
26
            let child_node = self
26
                .positioned_tree
26
                .tree
26
                .get(child_index)
26
                .ok_or(LayoutError::InvalidTree)?;
            // Check if this child has a GPU transform (CSS transform or drag)
26
            let child_ref_frame = child_node.dom_node_id.and_then(|dom_id| {
26
                self.gpu_value_cache.and_then(|cache| {
                    let key = cache.css_transform_keys.get(&dom_id)?;
                    let transform = cache.css_current_transform_values.get(&dom_id)?;
                    Some((*key, *transform))
                })
26
            });
            // Push reference frame if child has a transform
26
            if let Some((transform_key, initial_transform)) = child_ref_frame {
                let child_pos = self
                    .positioned_tree
                    .calculated_positions
            .get(child_index)
                    .copied()
                    .unwrap_or_default();
                let child_size = child_node.used_size.unwrap_or(LogicalSize {
                    width: 0.0,
                    height: 0.0,
                });
                let child_bounds = LogicalRect {
                    origin: child_pos,
                    size: child_size,
                };
                builder.set_current_node(child_node.dom_node_id);
                builder.push_reference_frame(transform_key, initial_transform, child_bounds);
26
            }
            // Same as above: push image mask, paint background, then clips
26
            let did_push_child_image_mask = self.push_image_mask_clip(builder, child_index);
26
            self.paint_node_background_and_border(builder, child_index)?;
26
            let did_push_clip = self.push_node_clips(builder, child_index, child_node)?;
26
            self.paint_in_flow_descendants(builder, child_index, self.positioned_tree.tree.children(child_index))?;
            // For VirtualView children: emit placeholder INSIDE the clip
26
            if let Some(dom_id) = child_node.dom_node_id {
26
                if self.is_virtual_view_node(dom_id) {
                    let child_bounds = self.get_paint_rect(child_index).unwrap_or_default();
                    builder.push_virtual_view_placeholder(dom_id, child_bounds, child_bounds);
26
                }
            }
26
            if did_push_clip {
                self.pop_node_clips(builder, child_node)?;
26
            }
26
            if did_push_child_image_mask {
                builder.pop_image_mask_clip();
26
            }
            // Paint scrollbars AFTER popping clips so they appear on top of content
26
            self.paint_scrollbars(builder, child_index)?;
            // Pop reference frame if we pushed one
26
            if child_ref_frame.is_some() {
                builder.pop_reference_frame();
26
            }
        }
        // Paint dragging children LAST so they appear on top of everything (W3C spec)
17704
        for child_index in dragging_children {
            let child_node = self
                .positioned_tree
                .tree
                .get(child_index)
                .ok_or(LayoutError::InvalidTree)?;
            // Check if this child has a GPU transform (CSS transform or drag)
            let child_ref_frame = child_node.dom_node_id.and_then(|dom_id| {
                self.gpu_value_cache.and_then(|cache| {
                    let key = cache.css_transform_keys.get(&dom_id)?;
                    let transform = cache.css_current_transform_values.get(&dom_id)?;
                    Some((*key, *transform))
                })
            });
            // Push reference frame if child has a transform
            if let Some((transform_key, initial_transform)) = child_ref_frame {
                let child_pos = self
                    .positioned_tree
                    .calculated_positions
            .get(child_index)
                    .copied()
                    .unwrap_or_default();
                let child_size = child_node.used_size.unwrap_or(LogicalSize {
                    width: 0.0,
                    height: 0.0,
                });
                let child_bounds = LogicalRect {
                    origin: child_pos,
                    size: child_size,
                };
                builder.set_current_node(child_node.dom_node_id);
                builder.push_reference_frame(transform_key, initial_transform, child_bounds);
            }
            // Same as above: push image mask, paint background, then clips
            let did_push_child_image_mask = self.push_image_mask_clip(builder, child_index);
            self.paint_node_background_and_border(builder, child_index)?;
            let did_push_clip = self.push_node_clips(builder, child_index, child_node)?;
            self.paint_in_flow_descendants(builder, child_index, self.positioned_tree.tree.children(child_index))?;
            // For VirtualView children: emit placeholder INSIDE the clip
            if let Some(dom_id) = child_node.dom_node_id {
                if self.is_virtual_view_node(dom_id) {
                    let child_bounds = self.get_paint_rect(child_index).unwrap_or_default();
                    builder.push_virtual_view_placeholder(dom_id, child_bounds, child_bounds);
                }
            }
            if did_push_clip {
                self.pop_node_clips(builder, child_node)?;
            }
            if did_push_child_image_mask {
                builder.pop_image_mask_clip();
            }
            // Paint scrollbars AFTER popping clips so they appear on top of content
            self.paint_scrollbars(builder, child_index)?;
            // Pop reference frame if we pushed one
            if child_ref_frame.is_some() {
                builder.pop_reference_frame();
            }
        }
17704
        Ok(())
17704
    }
    /// Returns true if the given DOM node is a VirtualView node.
17704
    fn is_virtual_view_node(&self, dom_id: NodeId) -> bool {
17704
        let node_data_container = self.ctx.styled_dom.node_data.as_container();
17704
        node_data_container
17704
            .get(dom_id)
17704
            .map(|nd| matches!(nd.get_node_type(), NodeType::VirtualView))
17704
            .unwrap_or(false)
17704
    }
    /// Checks if a node has an image mask clip and pushes PushImageMaskClip if so.
    /// Returns true if a clip was pushed (caller must pop it).
17704
    fn push_image_mask_clip(
17704
        &self,
17704
        builder: &mut DisplayListBuilder,
17704
        node_index: usize,
17704
    ) -> bool {
17704
        let node = match self.positioned_tree.tree.get(node_index) {
17704
            Some(n) => n,
            None => return false,
        };
17704
        let dom_id = match node.dom_node_id {
17630
            Some(id) => id,
74
            None => return false,
        };
17630
        let node_data_container = self.ctx.styled_dom.node_data.as_container();
17630
        let node_data = match node_data_container.get(dom_id) {
17630
            Some(nd) => nd,
            None => return false,
        };
17630
        match node_data.get_svg_data() {
            Some(azul_core::dom::SvgNodeData::ImageClipMask(clip_mask)) => {
                let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
                // Convert mask rect from element-local to window-logical coordinates
                let mask_rect = LogicalRect {
                    origin: LogicalPosition {
                        x: paint_rect.origin.x + clip_mask.rect.origin.x,
                        y: paint_rect.origin.y + clip_mask.rect.origin.y,
                    },
                    size: clip_mask.rect.size,
                };
                builder.push_image_mask_clip(
                    paint_rect,
                    clip_mask.image.clone(),
                    mask_rect,
                );
                true
            }
            #[cfg(feature = "cpurender")]
175
            Some(azul_core::dom::SvgNodeData::Path(svg_clip)) => {
175
                let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
175
                if let Some(mask_image) = rasterize_svg_clip_to_r8(svg_clip, &paint_rect) {
175
                    builder.push_image_mask_clip(paint_rect, mask_image, paint_rect);
175
                    true
                } else {
                    false
                }
            }
            #[cfg(not(feature = "cpurender"))]
            Some(azul_core::dom::SvgNodeData::Path(_)) => false,
            // Other SvgNodeData variants (shapes, gradients, etc.) don't produce clip masks
            Some(_) => false,
17455
            None => false,
        }
17704
    }
    // +spec:overflow:531bd2 - ancestor clips accumulate via push_clip/pop_clip stack (cumulative intersection)
    // +spec:overflow:8098ec - overflow clipping/scrolling; abs-pos elements with containing block outside scroller are not scrolled
    /// Checks if a node requires clipping or scrolling and pushes the appropriate commands.
    /// Returns true if any command was pushed.
    ///
    /// // +spec:containing-block:62aa5c - overflow clipping applies to all content except
    /// // descendants whose containing block is the viewport or an ancestor of this element
    /// // (i.e. absolutely positioned elements that escape the overflow container).
    /// // TODO: exempt abs-pos descendants whose containing block is an ancestor of this node.
    ///
    /// For VirtualView nodes with `overflow: scroll/auto`, we intentionally skip
    /// `PushScrollFrame` / `PopScrollFrame`. VirtualView scroll state is managed by
    /// `ScrollManager`, not WebRender's APZ. Instead we emit only a `PushClip`
    /// and later an `VirtualViewPlaceholder` (see `generate_for_stacking_context`).
17704
    fn push_node_clips(
17704
        &self,
17704
        builder: &mut DisplayListBuilder,
17704
        node_index: usize,
17704
        node: &LayoutNodeHot,
17704
    ) -> Result<bool> {
17704
        let Some(dom_id) = node.dom_node_id else {
74
            return Ok(false);
        };
17630
        let styled_node_state = self.get_styled_node_state(dom_id);
17630
        let raw_overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
17630
        let raw_overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
        // +spec:overflow:833078 - resolve visible/clip to auto/hidden per CSS Overflow 3 §3.1
17630
        let overflow_x = raw_overflow_x.resolve_computed(&raw_overflow_y);
17630
        let overflow_y = raw_overflow_y.resolve_computed(&raw_overflow_x);
17630
        let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
17630
        let element_size = PhysicalSizeImport {
17630
            width: paint_rect.size.width,
17630
            height: paint_rect.size.height,
17630
        };
17630
        let border_radius = get_border_radius(
17630
            self.ctx.styled_dom,
17630
            dom_id,
17630
            &styled_node_state,
17630
            element_size,
17630
            self.ctx.viewport_size,
        );
        // +spec:positioning:9c261b - clip-path (modern replacement for legacy 'clip' property)
        // The legacy CSS 2.2 'clip' property applied only to absolutely positioned elements;
        // clip-path supersedes it and applies to all elements per CSS Masking Level 1.
        // If present, push a clip region derived from the clip-path shape.
        // This is evaluated before overflow clips; both can be active simultaneously.
17630
        let has_clip_path = if let Some(clip_path) = super::getters::get_clip_path(
17630
            self.ctx.styled_dom, dom_id, &styled_node_state,
17630
        ) {
            if let Some((clip_rect, radius)) = resolve_clip_path(&clip_path, paint_rect) {
                let br = if radius > 0.0 {
                    BorderRadius {
                        top_left: radius,
                        top_right: radius,
                        bottom_left: radius,
                        bottom_right: radius,
                    }
                } else {
                    BorderRadius::default()
                };
                builder.push_clip(clip_rect, br);
                true
            } else {
                false
            }
        } else {
17630
            false
        };
        // +spec:overflow:6890f2 - text-overflow: clip inline content at end line box edge when overflow != visible
        // +spec:overflow:77d7ce - clipping region defines visible portion of border box; default is not clipped
17630
        let needs_clip = overflow_x.is_clipped() || overflow_y.is_clipped();
17630
        if !needs_clip {
17593
            return Ok(has_clip_path);
37
        }
        // +spec:overflow:c52f2a - clipping region is rounded to element's border-radius
        // +spec:overflow:913b23 - when both axes are clip, region is rounded per overflow-clip-margin
        // +spec:overflow:449d69 - when one axis is clip and the other is visible, clipping region is not rounded
        // +spec:overflow:449d69 - when one axis is clip and the other is visible, clipping region is not rounded
37
        let ox_clip = overflow_x.is_clipped() && !overflow_x.is_scroll() && !overflow_x.is_auto_overflow();
37
        let oy_clip = overflow_y.is_clipped() && !overflow_y.is_scroll() && !overflow_y.is_auto_overflow();
37
        let ox_visible = !overflow_x.is_clipped();
37
        let oy_visible = !overflow_y.is_clipped();
37
        let border_radius = if (ox_clip && oy_visible) || (oy_clip && ox_visible)
        {
            BorderRadius::default()
        } else {
37
            border_radius
        };
37
        let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
37
        let bp = node.box_props.unpack();
37
        let border = &bp.border;
        // Get scrollbar info to adjust clip rect for content area
37
        let scrollbar_info = self.positioned_tree.tree.warm(node_index)
37
            .and_then(|w| w.scrollbar_info.clone())
37
            .unwrap_or_default();
        // +spec:overflow:13cacb - clip rect clamped to 0 so zero-size clips hide all pixels
        // +spec:overflow:9207bc - clip rect computed from border-box edges (analogous to CSS 2.2 clip: rect() offsets)
        // +spec:overflow:3d5b53 - overflow clips to padding edge, scroll mechanism for scroll/auto
        // The clip rect for content should exclude the scrollbar area
        // Scrollbars are drawn inside the border-box, on the right/bottom edges
        // +spec:overflow:a825a6 - TODO: abs-pos elements with containing block outside this
        // element should not be clipped (currently all DOM children are clipped)
37
        let mut clip_rect = LogicalRect {
37
            origin: LogicalPosition {
37
                x: paint_rect.origin.x + border.left,
37
                y: paint_rect.origin.y + border.top,
37
            },
37
            size: LogicalSize {
37
                // Reduce width/height by scrollbar dimensions so content doesn't overlap scrollbar
37
                width: (paint_rect.size.width
37
                    - border.left
37
                    - border.right
37
                    - scrollbar_info.scrollbar_width)
37
                    .max(0.0),
37
                height: (paint_rect.size.height
37
                    - border.top
37
                    - border.bottom
37
                    - scrollbar_info.scrollbar_height)
37
                    .max(0.0),
37
            },
37
        };
        // +spec:overflow:342f47 - overflow-clip-margin expands clip edge for overflow:clip only
        // Per CSS Overflow 3 §3.2: overflow-clip-margin has no effect on overflow:hidden
        // or overflow:scroll. It only expands the overflow clip edge when overflow:clip is used.
37
        apply_overflow_clip_margin(
37
            &mut clip_rect,
37
            &overflow_x,
37
            &overflow_y,
37
            self.ctx.styled_dom,
37
            dom_id,
37
            &styled_node_state,
        );
37
        let is_virtual_view = self.is_virtual_view_node(dom_id);
        // +spec:overflow:484889 - clip content in unreachable scrollable overflow region
        // +spec:overflow:917dae - scrollable overflow rect is a rectangle in box's own coordinate system
37
        if overflow_x.is_scroll() || overflow_y.is_scroll() {
            if is_virtual_view {
                // VirtualView nodes: only push a clip, NO scroll frame.
                // Scroll state is managed by ScrollManager and passed to the
                // VirtualView callback as scroll_offset. The VirtualViewPlaceholder is
                // emitted after pop_node_clips in generate_for_stacking_context.
                builder.push_clip(clip_rect, border_radius);
            } else {
                // Regular scrollable nodes: push clip AND scroll frame.
                // WebRender's APZ manages the scroll offset via define_scroll_frame.
                // CPU renderers use scroll_offset to translate children.
                builder.push_clip(clip_rect, border_radius);
                let scroll_id = self.scroll_ids.get(&node_index).copied().unwrap_or(0);
                let content_size = get_scroll_content_size(node, self.positioned_tree.tree.warm(node_index));
                builder.push_scroll_frame(clip_rect, content_size, scroll_id);
            }
37
        } else {
37
            // Simple clip for hidden/clip
37
            builder.push_clip(clip_rect, border_radius);
37
        }
37
        Ok(true)
17704
    }
    /// Pops any clip/scroll commands associated with a node.
37
    fn pop_node_clips(&self, builder: &mut DisplayListBuilder, node: &LayoutNodeHot) -> Result<()> {
37
        let Some(dom_id) = node.dom_node_id else {
            return Ok(());
        };
37
        let styled_node_state = self.get_styled_node_state(dom_id);
37
        let overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
37
        let overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
37
        let paint_rect = self
37
            .get_paint_rect(
37
                self.positioned_tree
37
                    .tree
37
                    .nodes
37
                    .iter()
78
                    .position(|n| n.dom_node_id == Some(dom_id))
37
                    .unwrap_or(0),
            )
37
            .unwrap_or_default();
37
        let element_size = PhysicalSizeImport {
37
            width: paint_rect.size.width,
37
            height: paint_rect.size.height,
37
        };
37
        let border_radius = get_border_radius(
37
            self.ctx.styled_dom,
37
            dom_id,
37
            &styled_node_state,
37
            element_size,
37
            self.ctx.viewport_size,
        );
37
        let needs_clip =
37
            overflow_x.is_clipped() || overflow_y.is_clipped();
37
        let is_virtual_view = self.is_virtual_view_node(dom_id);
37
        if needs_clip {
37
            if (overflow_x.is_scroll() || overflow_y.is_scroll()) && !is_virtual_view {
                // For regular (non-VirtualView) scroll/auto, pop both scroll frame AND clip
                builder.pop_scroll_frame();
                builder.pop_clip();
37
            } else {
37
                // For hidden/clip, or VirtualView scroll (which only pushed a clip)
37
                builder.pop_clip();
37
            }
        }
        // Pop the clip-path clip if one was pushed.
        // This mirrors the push_node_clips logic: if clip-path is set,
        // a PushClip was emitted before any overflow clips.
        // We pop it last (stack order: clip-path pushed first, popped last).
37
        if let Some(clip_path) = super::getters::get_clip_path(
37
            self.ctx.styled_dom, dom_id, &styled_node_state,
37
        ) {
            if resolve_clip_path(&clip_path, paint_rect).is_some() {
                builder.pop_clip();
            }
37
        }
37
        Ok(())
37
    }
    /// Calculates the final paint-time rectangle for a node.
    /// 
    /// ## Coordinate Space
    /// 
    /// Returns the node's position in **absolute window coordinates** (logical pixels).
    /// This is the coordinate space used throughout the display list:
    /// 
    /// - Origin: Top-left corner of the window
    /// - Units: Logical pixels (HiDPI scaling happens in compositor2.rs)
    /// - Scroll: NOT applied here - WebRender scroll frames handle scroll offset
    ///   transformation internally via `define_scroll_frame()`
    /// 
    /// ## Important
    /// 
    /// Do NOT manually subtract scroll offset here! WebRender's scroll spatial
    /// transforms handle this. Subtracting here would cause double-offset and
    /// parallax effects (backgrounds and text moving at different speeds).
75121
    fn get_paint_rect(&self, node_index: usize) -> Option<LogicalRect> {
75121
        let node = self.positioned_tree.tree.get(node_index)?;
75121
        let pos = self
75121
            .positioned_tree
75121
            .calculated_positions
75121
            .get(node_index)
75121
            .copied()
75121
            .unwrap_or_default();
75121
        let size = node.used_size.unwrap_or_default();
        // NOTE: Scroll offset is NOT applied here!
        // WebRender scroll frames handle scroll transformation.
        // See compositor2.rs PushScrollFrame for details.
75121
        Some(LogicalRect::new(pos, size))
75121
    }
    /// Emits drawing commands for the background and border of a single node.
17704
    fn paint_node_background_and_border(
17704
        &mut self,
17704
        builder: &mut DisplayListBuilder,
17704
        node_index: usize,
17704
    ) -> Result<()> {
17704
        let Some(paint_rect) = self.get_paint_rect(node_index) else {
            return Ok(());
        };
17704
        let node = self
17704
            .positioned_tree
17704
            .tree
17704
            .get(node_index)
17704
            .ok_or(LayoutError::InvalidTree)?;
        // Set current node for node mapping (for pagination break properties)
17704
        builder.set_current_node(node.dom_node_id);
        // Check for CSS break-before/break-after properties and register forced page breaks
        // This is used by the pagination slicer to insert page breaks at correct positions
17704
        if let Some(dom_id) = node.dom_node_id {
17630
            let break_before = get_break_before(self.ctx.styled_dom, Some(dom_id));
17630
            let break_after = get_break_after(self.ctx.styled_dom, Some(dom_id));
            // For break-before: always, insert a page break at the top of this element
17630
            if is_forced_page_break(break_before) {
                let y_position = paint_rect.origin.y;
                builder.add_forced_page_break(y_position);
                debug_info!(
                    self.ctx,
                    "Registered forced page break BEFORE node {} at y={}",
                    node_index,
                    y_position
                );
17630
            }
            // For break-after: always, insert a page break at the bottom of this element
17630
            if is_forced_page_break(break_after) {
                let y_position = paint_rect.origin.y + paint_rect.size.height;
                builder.add_forced_page_break(y_position);
                debug_info!(
                    self.ctx,
                    "Registered forced page break AFTER node {} at y={}",
                    node_index,
                    y_position
                );
17630
            }
74
        }
        // CSS 2.2 §11.2: visibility:hidden — box is invisible but still affects layout.
        // Skip painting background/border for hidden nodes, but traversal continues
        // so visible descendants are still painted.
17704
        if self.is_node_hidden(node_index) {
            return Ok(());
17704
        }
        // Skip inline and inline-block elements ONLY if they participate in an IFC (Inline Formatting Context).
        // In Flex or Grid containers, inline-block elements are treated as flex/grid items and must be painted here.
        // Inline elements participate in inline formatting context and their backgrounds
        // must be positioned by the text layout engine, not the block layout engine
        //
        // IMPORTANT: The parent check must look at the PARENT NODE's formatting_context,
        // not the current node's. If parent is Flex/Grid, we paint this element as a flex/grid item.
        // Also check parent_formatting_context field which stores parent's FC during tree construction.
17704
        let warm = self.positioned_tree.tree.warm(node_index);
17704
        let parent_is_flex_or_grid = warm
17704
            .and_then(|w| w.parent_formatting_context.as_ref().map(|fc| matches!(fc, FormattingContext::Flex | FormattingContext::Grid)))
17704
            .unwrap_or(false);
17704
        if let Some(dom_id) = node.dom_node_id {
17630
            let display = {
                use crate::solver3::getters::get_display_property;
17630
                get_display_property(self.ctx.styled_dom, Some(dom_id))
17630
                    .unwrap_or(LayoutDisplay::Inline)
            };
17630
            if display == LayoutDisplay::InlineBlock || display == LayoutDisplay::Inline {
7807
                debug_info!(
7247
                    self.ctx,
7247
                    "[paint_node] node {} has display={:?}, parent_formatting_context={:?}, parent_is_flex_or_grid={}",
                    node_index,
                    display,
7247
                    warm.and_then(|w| w.parent_formatting_context.as_ref()),
                    parent_is_flex_or_grid
                );
7807
                if !parent_is_flex_or_grid {
                    // Normally, text3 handles inline/inline-block backgrounds via
                    // InlineShape (inline-block) or glyph runs (inline). However,
                    // if this inline-block establishes a stacking context (e.g.
                    // position:relative + z-index, opacity < 1, transform), we MUST
                    // paint its background here. generate_for_stacking_context paints
                    // background (step 1) → children (steps 3-6). If we skip the
                    // background, paint_inline_shape in the parent's paint_node_content
                    // would paint it AFTER the children, obscuring them.
7807
                    if display == LayoutDisplay::InlineBlock
70
                        && self.establishes_stacking_context(node_index)
                    {
                        // Fall through to paint background/border now
                    } else {
7807
                        return Ok(());
                    }
                }
                // Fall through to paint this element - it's a flex/grid item
9823
            }
74
        }
        // CSS 2.2 Section 17.5.1: Tables in the visual formatting model
        // Table-internal elements (row groups, rows, columns, column groups) have their
        // backgrounds painted by paint_table_items() in the correct 6-layer order.
        // Skip background painting here to avoid double-painting at wrong positions
        // (calculated_positions for TR elements may not reflect row offsets correctly;
        // paint_table_items computes row rects from cell bounding boxes instead).
        // Table CELLS still need content painting via paint_in_flow_descendants, so
        // we only skip the background/border here — content painting continues normally.
9897
        if matches!(node.formatting_context,
            FormattingContext::TableRowGroup | FormattingContext::TableRow |
            FormattingContext::TableColumnGroup
        ) {
1190
            return Ok(());
8707
        }
        // Tables have a special 6-layer background painting order
8707
        if matches!(node.formatting_context, FormattingContext::Table) {
1155
            debug_info!(
1155
                self.ctx,
1155
                "Painting table backgrounds/borders for node {} at {:?}",
                node_index,
                paint_rect
            );
            // Delegate to specialized table painting function
1155
            return self.paint_table_items(builder, node_index);
7552
        }
7552
        if let Some(dom_id) = node.dom_node_id {
7478
            let styled_node_state = self.get_styled_node_state(dom_id);
7478
            let background_contents =
7478
                get_background_contents(self.ctx.styled_dom, dom_id, &styled_node_state);
7478
            let border_info = get_border_info(self.ctx.styled_dom, dom_id, &styled_node_state);
7478
            let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
7478
            debug_info!(
5868
                self.ctx,
5868
                "Painting background/border for node {} ({:?}) at {:?}, backgrounds={:?}",
                node_index,
5868
                node_type.get_node_type(),
                paint_rect,
5868
                background_contents.len()
            );
            // Get both versions: simple BorderRadius for rect clipping and StyleBorderRadius for
            // border rendering
7478
            let element_size = PhysicalSizeImport {
7478
                width: paint_rect.size.width,
7478
                height: paint_rect.size.height,
7478
            };
7478
            let simple_border_radius = get_border_radius(
7478
                self.ctx.styled_dom,
7478
                dom_id,
7478
                &styled_node_state,
7478
                element_size,
7478
                self.ctx.viewport_size,
            );
7478
            let style_border_radius =
7478
                get_style_border_radius(self.ctx.styled_dom, dom_id, &styled_node_state);
            // Paint box shadows before backgrounds (CSS spec: shadows render behind the element)
7478
            let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
            // +spec:overflow:bb4308 - box shadows are ink overflow: painted outside border box, not affecting layout
            // Check all four sides for box-shadow (azul stores them per-side).
            // Routed through `super::getters::*` so the compact-cache has_box_shadow
            // fast path fires — most nodes have no shadow and skip 4 cascade walks.
29912
            for shadow in [
7478
                super::getters::get_box_shadow_left(self.ctx.styled_dom, dom_id, node_state),
7478
                super::getters::get_box_shadow_right(self.ctx.styled_dom, dom_id, node_state),
7478
                super::getters::get_box_shadow_top(self.ctx.styled_dom, dom_id, node_state),
7478
                super::getters::get_box_shadow_bottom(self.ctx.styled_dom, dom_id, node_state),
            ] {
29912
                if let Some(shadow) = shadow {
140
                    builder.push_item(DisplayListItem::BoxShadow {
140
                        bounds: paint_rect.into(),
140
                        shadow,
140
                        border_radius: simple_border_radius,
140
                    });
29772
                }
            }
            // Use unified background/border painting
7478
            builder.push_backgrounds_and_border(
7478
                paint_rect,
7478
                &background_contents,
7478
                &border_info,
7478
                simple_border_radius,
7478
                style_border_radius,
7478
                self.ctx.image_cache,
            );
74
        }
7552
        Ok(())
17704
    }
    //   backgrounds are invisible, allowing table background to show through
    // +spec:box-model:124815 - Table layer background painting order (6 layers: table, col-group, col, row-group, row, cell)
    // +spec:positioning:702985 - Table background painting in 6 layers (17.5.1)
    // +spec:table-layout:7370dc - Table layers and transparency: 6-layer background painting order
    // +spec:table-layout:7a5909 - table layers: 6-layer background paint order (table/colgroup/col/rowgroup/row/cell)
    /// CSS 2.2 Section 17.5.1: Table background painting in 6 layers
    ///
    /// Implements the CSS 2.2 specification for table background painting order.
    /// Unlike regular block elements, tables paint backgrounds in layers from back to front:
    ///
    /// 1. Table background (lowest layer)
    /// 2. Column group backgrounds
    /// 3. Column backgrounds
    /// 4. Row group backgrounds
    /// 5. Row backgrounds
    /// 6. Cell backgrounds (topmost layer)
    ///
    /// Then borders are painted (respecting border-collapse mode).
    /// Finally, cell content is painted on top of everything.
    ///
    /// This function generates simple display list items (Rect, Border) in the correct
    /// CSS paint order, making WebRender integration trivial.
1155
    fn paint_table_items(
1155
        &self,
1155
        builder: &mut DisplayListBuilder,
1155
        table_index: usize,
1155
    ) -> Result<()> {
1155
        let table_node = self
1155
            .positioned_tree
1155
            .tree
1155
            .get(table_index)
1155
            .ok_or(LayoutError::InvalidTree)?;
1155
        let Some(table_paint_rect) = self.get_paint_rect(table_index) else {
            return Ok(());
        };
        // Layer 1: Table background
1155
        if let Some(dom_id) = table_node.dom_node_id {
1155
            let styled_node_state = self.get_styled_node_state(dom_id);
1155
            let bg_color = get_background_color(self.ctx.styled_dom, dom_id, &styled_node_state);
1155
            let element_size = PhysicalSizeImport {
1155
                width: table_paint_rect.size.width,
1155
                height: table_paint_rect.size.height,
1155
            };
1155
            let border_radius = get_border_radius(
1155
                self.ctx.styled_dom,
1155
                dom_id,
1155
                &styled_node_state,
1155
                element_size,
1155
                self.ctx.viewport_size,
1155
            );
1155

            
1155
            builder.push_rect(table_paint_rect, bg_color, border_radius);
1155
        }
        // Traverse table children to paint layers 2-6
        // Layer 2: Column group backgrounds
        // Layer 3: Column backgrounds (columns are children of column groups)
1190
        for &child_idx in self.positioned_tree.tree.children(table_index) {
1190
            let child_node = self.positioned_tree.tree.get(child_idx);
1190
            if let Some(node) = child_node {
1190
                if matches!(node.formatting_context, FormattingContext::TableColumnGroup) {
                    // Paint column group background
                    self.paint_element_background(builder, child_idx)?;
                    // Paint backgrounds of individual columns within this group
                    for &col_idx in self.positioned_tree.tree.children(child_idx) {
                        self.paint_element_background(builder, col_idx)?;
                    }
1190
                }
            }
        }
        // Layer 4: Row group backgrounds (tbody, thead, tfoot)
        // Layer 5: Row backgrounds
        // Layer 6: Cell backgrounds
1190
        for &child_idx in self.positioned_tree.tree.children(table_index) {
1190
            let child_node = self.positioned_tree.tree.get(child_idx);
1190
            if let Some(node) = child_node {
1190
                match node.formatting_context {
                    FormattingContext::TableRowGroup => {
                        // Paint row group background
                        self.paint_element_background(builder, child_idx)?;
                        // Paint rows within this group
                        for &row_idx in self.positioned_tree.tree.children(child_idx) {
                            self.paint_table_row_and_cells(builder, row_idx)?;
                        }
                    }
                    FormattingContext::TableRow => {
                        // Direct row child (no row group wrapper)
1190
                        self.paint_table_row_and_cells(builder, child_idx)?;
                    }
                    _ => {}
                }
            }
        }
        // Borders are painted separately after all backgrounds
        // This is handled by the normal rendering flow for each element
        // TODO: For border-collapse: collapse tables, resolve conflicts between
        // adjacent cell borders using BorderInfo::resolve_conflict() from fc.rs.
        // Currently all cells paint their own borders (separate model behavior).
1155
        Ok(())
1155
    }
    /// Helper function to paint a table row's background and then its cells' backgrounds
    /// Layer 5: Row background
    /// Layer 6: Cell backgrounds (painted after row, so they appear on top)
1190
    fn paint_table_row_and_cells(
1190
        &self,
1190
        builder: &mut DisplayListBuilder,
1190
        row_idx: usize,
1190
    ) -> Result<()> {
        // Layer 5: Paint row background.
        // Rows don't have entries in calculated_positions (adding them would
        // double-offset cells during position recursion). Compute the row rect
        // from the bounding box of its cell children.
1190
        if let Some(row_node) = self.positioned_tree.tree.get(row_idx) {
1190
            if let Some(dom_id) = row_node.dom_node_id {
1190
                let styled_node_state = self.get_styled_node_state(dom_id);
1190
                let bg_color = get_background_color(self.ctx.styled_dom, dom_id, &styled_node_state);
1190
                if bg_color.a > 0 {
                    // Compute row rect from cell children
                    let mut min_x = f32::MAX;
                    let mut min_y = f32::MAX;
                    let mut max_x = f32::MIN;
                    let mut max_y = f32::MIN;
                    for &cell_idx in self.positioned_tree.tree.children(row_idx) {
                        if let Some(cell_rect) = self.get_paint_rect(cell_idx) {
                            min_x = min_x.min(cell_rect.origin.x);
                            min_y = min_y.min(cell_rect.origin.y);
                            max_x = max_x.max(cell_rect.origin.x + cell_rect.size.width);
                            max_y = max_y.max(cell_rect.origin.y + cell_rect.size.height);
                        }
                    }
                    if min_x < max_x && min_y < max_y {
                        let row_rect = LogicalRect::new(
                            LogicalPosition::new(min_x, min_y),
                            LogicalSize::new(max_x - min_x, max_y - min_y),
                        );
                        builder.push_rect(row_rect, bg_color, BorderRadius::default());
                    }
1190
                }
            }
        }
        // Layer 6: Paint cell backgrounds (topmost layer)
1190
        if let Some(_node) = self.positioned_tree.tree.get(row_idx) {
2975
            for &cell_idx in self.positioned_tree.tree.children(row_idx) {
2975
                self.paint_element_background(builder, cell_idx)?;
            }
        }
1190
        Ok(())
1190
    }
    /// Helper function to paint an element's background (used for all table elements)
    /// Reads background-color and border-radius from CSS properties and emits push_rect()
2975
    fn paint_element_background(
2975
        &self,
2975
        builder: &mut DisplayListBuilder,
2975
        node_index: usize,
2975
    ) -> Result<()> {
2975
        let Some(paint_rect) = self.get_paint_rect(node_index) else {
            return Ok(());
        };
2975
        let Some(node) = self.positioned_tree.tree.get(node_index) else {
            return Ok(());
        };
2975
        let Some(dom_id) = node.dom_node_id else {
            return Ok(());
        };
2975
        let styled_node_state = self.get_styled_node_state(dom_id);
2975
        let bg_color = get_background_color(self.ctx.styled_dom, dom_id, &styled_node_state);
        // Only paint if background color has alpha > 0 (optimization)
2975
        if bg_color.a == 0 {
2975
            return Ok(());
        }
        let element_size = PhysicalSizeImport {
            width: paint_rect.size.width,
            height: paint_rect.size.height,
        };
        let border_radius = get_border_radius(
            self.ctx.styled_dom,
            dom_id,
            &styled_node_state,
            element_size,
            self.ctx.viewport_size,
        );
        builder.push_rect(paint_rect, bg_color, border_radius);
        Ok(())
2975
    }
    /// Emits drawing commands for the foreground content, including hit-test areas and scrollbars.
17704
    fn paint_node_content(
17704
        &mut self,
17704
        builder: &mut DisplayListBuilder,
17704
        node_index: usize,
17704
    ) -> Result<()> {
        // CSS 2.2 §11.2: visibility:hidden — skip painting content for hidden nodes.
17704
        if self.is_node_hidden(node_index) {
            return Ok(());
17704
        }
17704
        let node = self
17704
            .positioned_tree
17704
            .tree
17704
            .get(node_index)
17704
            .ok_or(LayoutError::InvalidTree)?;
17704
        let node_warm = self.positioned_tree.tree.warm(node_index);
        // Set current node for node mapping (for pagination break properties)
17704
        builder.set_current_node(node.dom_node_id);
17704
        let Some(mut paint_rect) = self.get_paint_rect(node_index) else {
            return Ok(());
        };
        // For text nodes (with inline layout), the used_size might be 0x0.
        // In this case, compute the bounds from the inline layout result.
17704
        if paint_rect.size.width == 0.0 || paint_rect.size.height == 0.0 {
4895
            if let Some(cached_layout) = node_warm.and_then(|w| w.inline_layout_result.as_ref()) {
6
                let content_bounds = cached_layout.layout.bounds();
6
                paint_rect.size.width = content_bounds.width;
6
                paint_rect.size.height = content_bounds.height;
4889
            }
12809
        }
        // Add a hit-test area for this node if it's interactive.
        // NOTE: For scrollable containers (overflow: scroll/auto), the hit-test area
        // was already pushed in generate_for_stacking_context BEFORE the scroll frame,
        // so we skip it here to avoid duplicate hit-test areas that would scroll with content.
17704
        if let Some(tag_id) = get_tag_id(self.ctx.styled_dom, node.dom_node_id) {
4311
            let is_scrollable = if let Some(dom_id) = node.dom_node_id {
4311
                let styled_node_state = self.get_styled_node_state(dom_id);
4311
                let overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
4311
                let overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
4311
                overflow_x.is_scroll() || overflow_y.is_scroll()
            } else {
                false
            };
            // Push hit-test area for this node ONLY if it's not a scrollable container.
            // Scrollable containers already have their hit-test area pushed BEFORE the scroll frame
            // in generate_for_stacking_context, ensuring the hit-test stays stationary in parent space
            // while content scrolls. Pushing it again here would create a duplicate that scrolls
            // with content, causing hit-test failures when scrolled to the bottom.
4311
            if !is_scrollable {
4311
                builder.push_hit_test_area(paint_rect, tag_id);
4311
            }
13393
        }
        // Paint the node's visible content.
17704
        if let Some(cached_layout) = node_warm.and_then(|w| w.inline_layout_result.as_ref()) {
4337
            let inline_layout = &cached_layout.layout;
4337
            debug_info!(
3777
                self.ctx,
3777
                "[paint_node] node {} has inline_layout with {} items",
                node_index,
3777
                inline_layout.items.len()
            );
4337
            if let Some(dom_id) = node.dom_node_id {
4267
                let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
4267
                debug_info!(
3742
                    self.ctx,
3742
                    "Painting inline content for node {} ({:?}) at {:?}, {} layout items",
                    node_index,
3742
                    node_type.get_node_type(),
                    paint_rect,
3742
                    inline_layout.items.len()
                );
70
            }
            // paint_rect is the border-box, but inline layout positions are relative to
            // content-box. Use type-safe conversion to make this clear and avoid manual
            // calculations.
4337
            let border_box = BorderBoxRect(paint_rect);
4337
            let nbp = node.box_props.unpack();
4337
            let mut content_box_rect =
4337
                border_box.to_content_box(&nbp.padding, &nbp.border).rect();
            // Save the viewport-sized content box for clipping BEFORE expanding
            // to full scroll content size. Text must be clipped to the viewport
            // when overflow is hidden/scroll/auto, not to the full content size.
4337
            let viewport_clip_rect = content_box_rect;
            // For scrollable containers, extend the content rect to the full content size.
            // The scroll frame handles clipping - we need to paint ALL content, not just
            // what fits in the viewport. Otherwise glyphs beyond the viewport are not rendered.
4337
            let content_size = get_scroll_content_size(node, node_warm);
4337
            if content_size.height > content_box_rect.size.height {
1
                content_box_rect.size.height = content_size.height;
4336
            }
4337
            if content_size.width > content_box_rect.size.width {
                content_box_rect.size.width = content_size.width;
4337
            }
            // Check for text-shadow and wrap inline content with push/pop shadow
4337
            let mut pushed_text_shadow = false;
4337
            if let Some(dom_id) = node.dom_node_id {
4267
                let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
4267
                let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
4267
                if let Some(shadow_val) = self.ctx.styled_dom.css_property_cache.ptr
4267
                    .get_text_shadow(node_data, &dom_id, node_state)
                {
                    if let Some(shadow) = shadow_val.get_property() {
                        builder.push_item(DisplayListItem::PushTextShadow {
                            shadow: (**shadow).clone(),
                        });
                        pushed_text_shadow = true;
                    }
4267
                }
70
            }
4337
            self.paint_inline_content(builder, content_box_rect, viewport_clip_rect, inline_layout, node_index)?;
4337
            if pushed_text_shadow {
                builder.push_item(DisplayListItem::PopTextShadow);
4337
            }
13367
        } else if let Some(dom_id) = node.dom_node_id {
            // +spec:replaced-elements:edd21b - block-level replaced element painted atomically per E.2
            // +spec:replaced-elements:516b2a - replaced content painted atomically in painting order
            // This node might be a simple replaced element, like an <img> tag.
13363
            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
13363
            if let NodeType::Image(image_ref) = node_data.get_node_type() {
                debug_info!(
                    self.ctx,
                    "Painting image for node {} at {:?}",
                    node_index,
                    paint_rect
                );
                // Get border-radius so the compositor can clip the image to rounded corners
                let styled_node_state = self.get_styled_node_state(dom_id);
                let element_size = PhysicalSizeImport {
                    width: paint_rect.size.width,
                    height: paint_rect.size.height,
                };
                let border_radius = get_border_radius(
                    self.ctx.styled_dom,
                    dom_id,
                    &styled_node_state,
                    element_size,
                    self.ctx.viewport_size,
                );
                // Store the ImageRef directly in the display list
                builder.push_image(paint_rect, image_ref.as_ref().clone(), border_radius);
13363
            }
4
        }
17704
        Ok(())
17704
    }
    /// Emits drawing commands for scrollbars. This is called AFTER popping the scroll frame
    /// clip so scrollbars appear on top of content and are not clipped.
17704
    fn paint_scrollbars(&self, builder: &mut DisplayListBuilder, node_index: usize) -> Result<()> {
        // CSS 2.2 §11.2: visibility:hidden scroll containers must not paint scrollbars,
        // but their layout space is preserved (already handled by layout).
17704
        if self.is_node_hidden(node_index) {
            return Ok(());
17704
        }
17704
        let node = self
17704
            .positioned_tree
17704
            .tree
17704
            .get(node_index)
17704
            .ok_or(LayoutError::InvalidTree)?;
17704
        let Some(paint_rect) = self.get_paint_rect(node_index) else {
            return Ok(());
        };
        // Check if we need to draw scrollbars for this node.
17704
        let scrollbar_info = self.positioned_tree.tree.warm(node_index)
17704
            .and_then(|w| w.scrollbar_info.clone())
17704
            .unwrap_or_default();
        // Get node_id for GPU cache lookup and CSS style lookup
17704
        let node_id = node.dom_node_id;
        // Get CSS scrollbar style for this node (cached per LayoutContext).
17704
        let scrollbar_style = node_id
17704
            .map(|nid| {
17630
                let node_state =
17630
                    &self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
17630
                crate::solver3::getters::get_scrollbar_style_cached(self.ctx, nid, node_state)
17630
            })
17704
            .unwrap_or_default();
        // Skip if scrollbar-width: none
17704
        if matches!(
17704
            scrollbar_style.width_mode,
            azul_css::props::style::scrollbar::LayoutScrollbarWidth::None
        ) {
            return Ok(());
17704
        }
        // +spec:overflow:3dfb2c - when scrollbar gutter is present but scrollbar is not,
        // paint the gutter background as an extension of the padding
17704
        let scrollbar_gutter = node_id
17704
            .and_then(|nid| {
17630
                let node_state =
17630
                    &self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
17630
                get_scrollbar_gutter_property(self.ctx.styled_dom, nid, node_state).exact()
17630
            })
17704
            .unwrap_or_default();
17704
        let gutter_is_stable = matches!(
17704
            scrollbar_gutter,
            azul_css::props::layout::overflow::StyleScrollbarGutter::Stable
            | azul_css::props::layout::overflow::StyleScrollbarGutter::StableBothEdges
        );
17704
        let gutter_both_edges = matches!(
17704
            scrollbar_gutter,
            azul_css::props::layout::overflow::StyleScrollbarGutter::StableBothEdges
        );
17704
        if gutter_is_stable {
            let gbp = node.box_props.unpack();
            let border = &gbp.border;
            let gutter_width = scrollbar_style.visual_width_px;
            // Paint gutter as padding extension when scrollbar is absent
            let bg_color = node_id
                .map(|nid| {
                    let node_state =
                        &self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
                    get_background_color(self.ctx.styled_dom, nid, node_state)
                })
                .unwrap_or(ColorU::TRANSPARENT);
            if !scrollbar_info.needs_vertical && gutter_width > 0.0 {
                // Right-side gutter (inline-end)
                let gutter_rect = LogicalRect {
                    origin: LogicalPosition::new(
                        paint_rect.origin.x + paint_rect.size.width - border.right - gutter_width,
                        paint_rect.origin.y + border.top,
                    ),
                    size: LogicalSize::new(
                        gutter_width,
                        (paint_rect.size.height - border.top - border.bottom).max(0.0),
                    ),
                };
                builder.push_rect(gutter_rect, bg_color, BorderRadius::default());
                // Both-edges: also paint left-side gutter (inline-start)
                if gutter_both_edges {
                    let left_gutter_rect = LogicalRect {
                        origin: LogicalPosition::new(
                            paint_rect.origin.x + border.left,
                            paint_rect.origin.y + border.top,
                        ),
                        size: LogicalSize::new(
                            gutter_width,
                            (paint_rect.size.height - border.top - border.bottom).max(0.0),
                        ),
                    };
                    builder.push_rect(left_gutter_rect, bg_color, BorderRadius::default());
                }
            }
17704
        }
        // Get border dimensions to position scrollbar inside the border-box
17704
        let sbp = node.box_props.unpack();
17704
        let border = &sbp.border;
        // Get border-radius for potential clipping
17704
        let container_border_radius = node_id
17704
            .map(|nid| {
17630
                let node_state =
17630
                    &self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
17630
                let element_size = PhysicalSizeImport {
17630
                    width: paint_rect.size.width,
17630
                    height: paint_rect.size.height,
17630
                };
17630
                let viewport_size =
17630
                    LogicalSize::new(self.ctx.viewport_size.width, self.ctx.viewport_size.height);
17630
                get_border_radius(
17630
                    self.ctx.styled_dom,
17630
                    nid,
17630
                    node_state,
17630
                    element_size,
17630
                    viewport_size,
                )
17630
            })
17704
            .unwrap_or_default();
        // Calculate the inner rect (content-box) where scrollbars should be placed
        // Scrollbars are positioned inside the border, at the right/bottom edges
17704
        let inner_rect = LogicalRect {
17704
            origin: LogicalPosition::new(
17704
                paint_rect.origin.x + border.left,
17704
                paint_rect.origin.y + border.top,
17704
            ),
17704
            size: LogicalSize::new(
17704
                (paint_rect.size.width - border.left - border.right).max(0.0),
17704
                (paint_rect.size.height - border.top - border.bottom).max(0.0),
17704
            ),
17704
        };
        // Get scroll position for thumb calculation
        // ScrollPosition contains parent_rect and children_rect
        // The scroll offset is the difference between children_rect.origin and parent_rect.origin
17704
        let (scroll_offset_x, scroll_offset_y) = node_id
17704
            .and_then(|nid| {
17630
                self.scroll_offsets.get(&nid).map(|pos| {
                    (
                        pos.children_rect.origin.x - pos.parent_rect.origin.x,
                        pos.children_rect.origin.y - pos.parent_rect.origin.y,
                    )
                })
17630
            })
17704
            .unwrap_or((0.0, 0.0));
        // Get content size for thumb proportional sizing
        // Use the node's get_content_size() method which returns the actual content size
        // from overflow_content_size (set during layout) or computes it from text/children.
        // For VirtualView nodes, the virtual_scroll_size (propagated through ScrollPosition.children_rect)
        // is more accurate than the layout-computed content size.
17704
        let content_size = node_id
17704
            .and_then(|nid| self.scroll_offsets.get(&nid))
17704
            .map(|pos| pos.children_rect.size)
17704
            .unwrap_or_else(|| self.positioned_tree.tree.get_content_size(node_index));
        // Calculate thumb border-radius (half the scrollbar width for pill-shaped thumb)
17704
        let thumb_radius = scrollbar_style.visual_width_px / 2.0;
17704
        let thumb_border_radius = BorderRadius {
17704
            top_left: thumb_radius,
17704
            top_right: thumb_radius,
17704
            bottom_left: thumb_radius,
17704
            bottom_right: thumb_radius,
17704
        };
17704
        if scrollbar_info.needs_vertical {
            // Look up opacity key from GPU cache for GPU-animated opacity.
            // If a key already exists in the cache from a previous frame, reuse it.
            // Otherwise, create a new unique key. The key will be registered
            // in the GPU cache after layout_document returns (same pattern as
            // transform keys). This ensures the display list ALWAYS has an
            // opacity binding, so GPU-only scroll updates can animate it.
            let opacity_key = node_id.map(|nid| {
                self.gpu_value_cache
                    .and_then(|cache| {
                        cache
                            .scrollbar_v_opacity_keys
                            .get(&(self.dom_id, nid))
                            .copied()
                    })
                    .unwrap_or_else(|| OpacityKey::unique())
            });
            // Vertical scrollbar: use shared geometry computation
            let button_size = if scrollbar_style.show_scroll_buttons {
                scrollbar_style.scroll_button_size_px
            } else {
                0.0
            };
            let v_geom = compute_scrollbar_geometry_with_button_size(
                ScrollbarOrientation::Vertical,
                inner_rect,
                content_size,
                scroll_offset_y,
                scrollbar_style.visual_width_px,
                scrollbar_info.needs_horizontal,
                button_size,
            );
            // Position thumb after the top button; GPU transform moves it within usable track
            let thumb_bounds = LogicalRect {
                origin: LogicalPosition::new(
                    v_geom.track_rect.origin.x,
                    v_geom.track_rect.origin.y + v_geom.button_size,
                ),
                size: LogicalSize::new(v_geom.width_px, v_geom.thumb_length),
            };
            // Look up transform key from GPU cache for GPU-animated thumb positioning.
            // If a key already exists in the cache from a previous frame, reuse it.
            // Otherwise, create a new unique key. The key will be registered
            // in the GPU cache after layout_document returns.
            let thumb_transform_key = node_id.map(|nid| {
                self.gpu_value_cache
                    .and_then(|cache| cache.transform_keys.get(&nid).copied())
                    .unwrap_or_else(|| TransformKey::unique())
            });
            // Initial transform: translate thumb within usable region
            let thumb_initial_transform =
                ComputedTransform3D::new_translation(0.0, v_geom.thumb_offset, 0.0);
            // Generate hit-test ID for vertical scrollbar thumb
            let hit_id = node_id
                .map(|nid| azul_core::hit_test::ScrollbarHitId::VerticalThumb(self.dom_id, nid));
            // Buttons at top/bottom of track (only if enabled in style)
            let (button_decrement_bounds, button_increment_bounds) = if scrollbar_style.show_scroll_buttons && v_geom.button_size > 0.0 {
                (
                    Some(LogicalRect {
                        origin: v_geom.track_rect.origin,
                        size: LogicalSize::new(v_geom.button_size, v_geom.button_size),
                    }),
                    Some(LogicalRect {
                        origin: LogicalPosition::new(
                            v_geom.track_rect.origin.x,
                            v_geom.track_rect.origin.y + v_geom.track_rect.size.height - v_geom.button_size,
                        ),
                        size: LogicalSize::new(v_geom.button_size, v_geom.button_size),
                    }),
                )
            } else {
                (None, None)
            };
            builder.push_scrollbar_styled(ScrollbarDrawInfo {
                bounds: v_geom.track_rect.into(),
                orientation: ScrollbarOrientation::Vertical,
                track_bounds: v_geom.track_rect.into(),
                track_color: scrollbar_style.track_color,
                thumb_bounds: thumb_bounds.into(),
                thumb_color: scrollbar_style.thumb_color,
                thumb_border_radius,
                button_decrement_bounds: button_decrement_bounds.map(|b| b.into()),
                button_increment_bounds: button_increment_bounds.map(|b| b.into()),
                button_color: scrollbar_style.button_color,
                opacity_key,
                thumb_transform_key,
                thumb_initial_transform,
                hit_id,
                clip_to_container_border: scrollbar_style.clip_to_container_border,
                container_border_radius,
                visibility: scrollbar_style.visibility,
            });
17704
        }
17704
        if scrollbar_info.needs_horizontal {
            // Look up horizontal opacity key from GPU cache (same pattern as vertical).
            let opacity_key = node_id.map(|nid| {
                self.gpu_value_cache
                    .and_then(|cache| {
                        cache
                            .scrollbar_h_opacity_keys
                            .get(&(self.dom_id, nid))
                            .copied()
                    })
                    .unwrap_or_else(|| OpacityKey::unique())
            });
            // Horizontal scrollbar: use shared geometry computation
            let h_button_size = if scrollbar_style.show_scroll_buttons {
                scrollbar_style.scroll_button_size_px
            } else {
                0.0
            };
            let h_geom = compute_scrollbar_geometry_with_button_size(
                ScrollbarOrientation::Horizontal,
                inner_rect,
                content_size,
                scroll_offset_x,
                scrollbar_style.visual_width_px,
                scrollbar_info.needs_vertical,
                h_button_size,
            );
            // Position thumb after the left button; GPU transform moves it within usable track
            let thumb_bounds = LogicalRect {
                origin: LogicalPosition::new(
                    h_geom.track_rect.origin.x + h_geom.button_size,
                    h_geom.track_rect.origin.y,
                ),
                size: LogicalSize::new(h_geom.thumb_length, h_geom.width_px),
            };
            // Look up horizontal transform key from GPU cache for GPU-animated thumb positioning.
            let thumb_transform_key = node_id.map(|nid| {
                self.gpu_value_cache
                    .and_then(|cache| cache.h_transform_keys.get(&nid).copied())
                    .unwrap_or_else(|| TransformKey::unique())
            });
            let thumb_initial_transform =
                ComputedTransform3D::new_translation(h_geom.thumb_offset, 0.0, 0.0);
            // Generate hit-test ID for horizontal scrollbar thumb
            let hit_id = node_id
                .map(|nid| azul_core::hit_test::ScrollbarHitId::HorizontalThumb(self.dom_id, nid));
            // Buttons at left/right of track (only if enabled in style)
            let (button_decrement_bounds, button_increment_bounds) = if scrollbar_style.show_scroll_buttons && h_geom.button_size > 0.0 {
                (
                    Some(LogicalRect {
                        origin: h_geom.track_rect.origin,
                        size: LogicalSize::new(h_geom.button_size, h_geom.button_size),
                    }),
                    Some(LogicalRect {
                        origin: LogicalPosition::new(
                            h_geom.track_rect.origin.x + h_geom.track_rect.size.width - h_geom.button_size,
                            h_geom.track_rect.origin.y,
                        ),
                        size: LogicalSize::new(h_geom.button_size, h_geom.button_size),
                    }),
                )
            } else {
                (None, None)
            };
            builder.push_scrollbar_styled(ScrollbarDrawInfo {
                bounds: h_geom.track_rect.into(),
                orientation: ScrollbarOrientation::Horizontal,
                track_bounds: h_geom.track_rect.into(),
                track_color: scrollbar_style.track_color,
                thumb_bounds: thumb_bounds.into(),
                thumb_color: scrollbar_style.thumb_color,
                thumb_border_radius,
                button_decrement_bounds: button_decrement_bounds.map(|b| b.into()),
                button_increment_bounds: button_increment_bounds.map(|b| b.into()),
                button_color: scrollbar_style.button_color,
                opacity_key,
                thumb_transform_key,
                thumb_initial_transform,
                hit_id,
                clip_to_container_border: scrollbar_style.clip_to_container_border,
                container_border_radius,
                visibility: scrollbar_style.visibility,
            });
17704
        }
17704
        Ok(())
17704
    }
    /// Converts the rich layout information from `text3` into drawing commands.
4337
    fn paint_inline_content(
4337
        &self,
4337
        builder: &mut DisplayListBuilder,
4337
        container_rect: LogicalRect,
4337
        viewport_clip_rect: LogicalRect,
4337
        layout: &UnifiedLayout,
4337
        source_node_index: usize,
4337
    ) -> Result<()> {
        // TODO: This will always paint images over the glyphs
        // TODO: Handle z-index within inline content (e.g. background images)
        // NOTE: Text decorations (underline, strikethrough, overline) are handled in push_text_layout_to_display_list
        // TODO: Text shadows not yet implemented
        // NOTE: Text-overflow ellipsis is handled via apply_text_overflow_ellipsis()
        // which can be called as a post-processing step on the display list when
        // the node has overflow:hidden and text-overflow:ellipsis CSS properties.
        // +spec:overflow:7807b1 - text-overflow ellipsis side depends on direction (RTL clips left, LTR clips right); not yet implemented
        // +spec:overflow:bbf9c1 - text-overflow ellipsis should only truncate content
        // that is actually clipped; as content scrolls into view, show it instead of ellipsis
        // TODO: Handle text overflowing (based on container_rect and overflow behavior)
        // Calculate actual content bounds from the layout
        // Use these bounds instead of container_rect to avoid inflated bounds
        // that extend beyond actual text content
4337
        let layout_bounds = layout.bounds();
4337
        let actual_bounds = if layout_bounds.width > 0.0 && layout_bounds.height > 0.0 {
4332
            LogicalRect {
4332
                origin: container_rect.origin,
4332
                size: LogicalSize {
4332
                    width: layout_bounds.width,
4332
                    height: layout_bounds.height,
4332
                },
4332
            }
        } else {
            // If layout has no content, don't push TextLayout item at all
            // This prevents 0x0 TextLayout items that pollute height calculation
5
            LogicalRect {
5
                origin: container_rect.origin,
5
                size: LogicalSize::default(),
5
            }
        };
        // Only push TextLayout if layout has actual content
        // This prevents empty TextLayout items with 0x0 bounds at various Y positions
        // from affecting pagination height calculations
4337
        if layout_bounds.width > 0.0 || layout_bounds.height > 0.0 {
4332
            builder.push_text_layout(
4332
                Arc::new(layout.clone()) as Arc<dyn std::any::Any + Send + Sync>,
4332
                actual_bounds,
4332
                FontHash::from_hash(0), // Will be updated per glyph run
4332
                12.0,                   // Default font size, will be updated per glyph run
4332
                ColorU {
4332
                    r: 0,
4332
                    g: 0,
4332
                    b: 0,
4332
                    a: 255,
4332
                }, // Default color
4332
            );
4332
        }
4337
        let glyph_runs = crate::text3::glyphs::get_glyph_runs_simple(layout);
        // FIRST PASS: Render backgrounds (solid colors, gradients) and borders for each glyph run
        // This must happen BEFORE rendering text so that backgrounds appear behind text.
4337
        for glyph_run in glyph_runs.iter() {
            // Calculate the bounding box for this glyph run
4264
            if let (Some(first_glyph), Some(last_glyph)) =
4264
                (glyph_run.glyphs.first(), glyph_run.glyphs.last())
            {
                // Calculate run bounds from glyph positions
4264
                let run_start_x = container_rect.origin.x + first_glyph.point.x;
4264
                let run_end_x = container_rect.origin.x + last_glyph.point.x;
4264
                let run_width = (run_end_x - run_start_x).max(0.0);
                // Skip if run has no width
4264
                if run_width <= 0.0 {
176
                    continue;
4088
                }
                // Approximate height based on font size (baseline is at glyph.point.y)
4088
                let baseline_y = container_rect.origin.y + first_glyph.point.y;
4088
                let font_size = glyph_run.font_size_px;
4088
                let ascent = font_size * 0.8; // Approximate ascent
4088
                let mut run_bounds = LogicalRect::new(
4088
                    LogicalPosition::new(run_start_x, baseline_y - ascent),
4088
                    LogicalSize::new(run_width, font_size),
                );
                // Expand run_bounds by padding + border so the background/border
                // rect covers the full inline box, not just the glyph area.
4088
                if let Some(border) = &glyph_run.border {
                    let left_inset = border.left_inset();
                    let right_inset = border.right_inset();
                    let top_inset = border.top_inset();
                    let bottom_inset = border.bottom_inset();
                    run_bounds.origin.x -= left_inset;
                    run_bounds.origin.y -= top_inset;
                    run_bounds.size.width += left_inset + right_inset;
                    run_bounds.size.height += top_inset + bottom_inset;
4088
                }
4088
                builder.push_inline_backgrounds_and_border(
4088
                    run_bounds,
4088
                    glyph_run.background_color,
4088
                    &glyph_run.background_content,
4088
                    glyph_run.border.as_ref(),
4088
                    self.ctx.image_cache,
                );
            }
        }
        // SECOND PASS: Render text runs
4337
        for (_idx, glyph_run) in glyph_runs.iter().enumerate() {
            // Clip text to the viewport-sized content box, not the full scroll
            // content area. This prevents text from overflowing outside the
            // container when overflow is hidden/scroll/auto.
4264
            let clip_rect = viewport_clip_rect;
            // Fix: Offset glyph positions by the container origin.
            // Text layout is relative to (0,0) of the IFC, but we need absolute coordinates.
4264
            let offset_glyphs: Vec<GlyphInstance> = glyph_run
4264
                .glyphs
4264
                .iter()
37744
                .map(|g| {
37744
                    let mut g = g.clone();
37744
                    g.point.x += container_rect.origin.x;
37744
                    g.point.y += container_rect.origin.y;
37744
                    g
37744
                })
4264
                .collect();
            // Store only the font hash in the display list to keep it lean
4264
            builder.push_text_run(
4264
                offset_glyphs,
4264
                FontHash::from_hash(glyph_run.font_hash),
4264
                glyph_run.font_size_px,
4264
                glyph_run.color,
4264
                clip_rect,
4264
                Some(source_node_index),
            );
            // Render text decorations if present OR if this is IME composition preview
4264
            let needs_underline = glyph_run.text_decoration.underline || glyph_run.is_ime_preview;
4264
            let needs_strikethrough = glyph_run.text_decoration.strikethrough;
4264
            let needs_overline = glyph_run.text_decoration.overline;
4264
            if needs_underline || needs_strikethrough || needs_overline {
                // Calculate the bounding box for this glyph run
                if let (Some(first_glyph), Some(last_glyph)) =
                    (glyph_run.glyphs.first(), glyph_run.glyphs.last())
                {
                    let decoration_start_x = container_rect.origin.x + first_glyph.point.x;
                    let decoration_end_x = container_rect.origin.x + last_glyph.point.x;
                    let decoration_width = decoration_end_x - decoration_start_x;
                    // Use font metrics to determine decoration positions
                    // Standard ratios based on CSS specification
                    let font_size = glyph_run.font_size_px;
                    let thickness = (font_size * 0.08).max(1.0); // ~8% of font size, min 1px
                    // Baseline is at glyph.point.y
                    let baseline_y = container_rect.origin.y + first_glyph.point.y;
                    if needs_underline {
                        // Underline is typically 10-15% below baseline
                        // IME composition always gets underlined
                        let underline_y = baseline_y + (font_size * 0.12);
                        let underline_bounds = LogicalRect::new(
                            LogicalPosition::new(decoration_start_x, underline_y),
                            LogicalSize::new(decoration_width, thickness),
                        );
                        builder.push_underline(underline_bounds, glyph_run.color, thickness);
                    }
                    if needs_strikethrough {
                        // Strikethrough is typically 40% above baseline (middle of x-height)
                        let strikethrough_y = baseline_y - (font_size * 0.3);
                        let strikethrough_bounds = LogicalRect::new(
                            LogicalPosition::new(decoration_start_x, strikethrough_y),
                            LogicalSize::new(decoration_width, thickness),
                        );
                        builder.push_strikethrough(
                            strikethrough_bounds,
                            glyph_run.color,
                            thickness,
                        );
                    }
                    if needs_overline {
                        // Overline is typically at cap-height (75% above baseline)
                        let overline_y = baseline_y - (font_size * 0.85);
                        let overline_bounds = LogicalRect::new(
                            LogicalPosition::new(decoration_start_x, overline_y),
                            LogicalSize::new(decoration_width, thickness),
                        );
                        builder.push_overline(overline_bounds, glyph_run.color, thickness);
                    }
                }
4264
            }
        }
        // THIRD PASS: Generate hit-test areas for text runs
        // This enables cursor resolution directly on text nodes instead of their containers
4337
        for glyph_run in glyph_runs.iter() {
            // Only generate hit-test areas for runs with a source node id
4264
            let Some(source_node_id) = glyph_run.source_node_id else {
                continue;
            };
            // Calculate the bounding box for this glyph run
4264
            if let (Some(first_glyph), Some(last_glyph)) =
4264
                (glyph_run.glyphs.first(), glyph_run.glyphs.last())
            {
4264
                let run_start_x = container_rect.origin.x + first_glyph.point.x;
4264
                let run_end_x = container_rect.origin.x + last_glyph.point.x;
4264
                let run_width = (run_end_x - run_start_x).max(0.0);
                // Skip if run has no width
4264
                if run_width <= 0.0 {
176
                    continue;
4088
                }
                // Calculate run bounds using font metrics
4088
                let baseline_y = container_rect.origin.y + first_glyph.point.y;
4088
                let font_size = glyph_run.font_size_px;
4088
                let ascent = font_size * 0.8; // Approximate ascent
4088
                let run_bounds = LogicalRect::new(
4088
                    LogicalPosition::new(run_start_x, baseline_y - ascent),
4088
                    LogicalSize::new(run_width, font_size),
                );
                // Query the cursor type for this text node from the CSS property cache
                // Default to Text cursor (I-beam) for text nodes
4088
                let cursor_type = self.get_cursor_type_for_text_node(source_node_id);
                // Construct the hit-test tag for cursor resolution
                // tag.0 = DomId (upper 32 bits) | NodeId (lower 32 bits)
                // tag.1 = TAG_TYPE_CURSOR | cursor_type
4088
                let tag_value = ((self.dom_id.inner as u64) << 32) | (source_node_id.index() as u64);
4088
                let tag_type = TAG_TYPE_CURSOR | (cursor_type as u16);
4088
                let tag_id = (tag_value, tag_type);
4088
                builder.push_hit_test_area(run_bounds, tag_id);
            }
        }
        // Render inline objects (images, shapes/inline-blocks, etc.)
        // These are positioned by the text3 engine and need to be rendered at their calculated
        // positions
42161
        for positioned_item in &layout.items {
37824
            self.paint_inline_object(builder, container_rect.origin, positioned_item)?;
        }
4337
        Ok(())
4337
    }
    /// Paints a single inline object (image, shape, or inline-block)
37824
    fn paint_inline_object(
37824
        &self,
37824
        builder: &mut DisplayListBuilder,
37824
        base_pos: LogicalPosition,
37824
        positioned_item: &PositionedItem,
37824
    ) -> Result<()> {
        let ShapedItem::Object {
70
            content, bounds, ..
37824
        } = &positioned_item.item
        else {
            // Other item types (e.g., breaks) don't produce painted output.
37754
            return Ok(());
        };
        // Calculate the absolute position of this object
        // positioned_item.position is relative to the container
70
        let object_bounds = LogicalRect::new(
70
            LogicalPosition::new(
70
                base_pos.x + positioned_item.position.x,
70
                base_pos.y + positioned_item.position.y,
            ),
70
            LogicalSize::new(bounds.width, bounds.height),
        );
70
        match content {
            InlineContent::Image(image) => {
                if let Some(image_ref) = get_image_ref_for_image_source(&image.source) {
                    builder.push_image(object_bounds, image_ref, BorderRadius::default());
                }
            }
70
            InlineContent::Shape(shape) => {
70
                self.paint_inline_shape(builder, object_bounds, shape, bounds)?;
            }
            _ => {}
        }
70
        Ok(())
37824
    }
    // +spec:inline-block:a60a89 - inline-block painted atomically as pseudo-stacking-context per E.2
    /// Paints an inline shape (inline-block background and border)
70
    fn paint_inline_shape(
70
        &self,
70
        builder: &mut DisplayListBuilder,
70
        object_bounds: LogicalRect,
70
        shape: &InlineShape,
70
        bounds: &crate::text3::cache::Rect,
70
    ) -> Result<()> {
        // Render inline-block backgrounds and borders using their CSS styling
        // The text3 engine positions these correctly in the inline flow
70
        let Some(node_id) = shape.source_node_id else {
            return Ok(());
        };
        // If this inline-block establishes a stacking context, its background was
        // already painted by paint_node_background_and_border (called from
        // generate_for_stacking_context). Painting again here would cause
        // double-rendering. Skip it.
70
        if let Some(indices) = self.positioned_tree.tree.dom_to_layout.get(&node_id) {
70
            if let Some(&idx) = indices.first() {
70
                if self.establishes_stacking_context(idx) {
                    return Ok(());
70
                }
            }
        }
70
        let styled_node_state =
70
            &self.ctx.styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
        // Get all background layers (colors, gradients, images)
70
        let background_contents =
70
            get_background_contents(self.ctx.styled_dom, node_id, styled_node_state);
        // Get border information
70
        let border_info = get_border_info(self.ctx.styled_dom, node_id, styled_node_state);
        // FIX: object_bounds is the margin-box position from text3.
        // We need to convert to border-box for painting backgrounds/borders.
70
        let margins = if let Some(indices) = self.positioned_tree.tree.dom_to_layout.get(&node_id) {
70
            if let Some(&idx) = indices.first() {
70
                self.positioned_tree.tree.nodes[idx].box_props.unpack().margin
            } else {
                Default::default()
            }
        } else {
            Default::default()
        };
        // Convert margin-box bounds to border-box bounds
70
        let border_box_bounds = LogicalRect {
70
            origin: LogicalPosition {
70
                x: object_bounds.origin.x + margins.left,
70
                y: object_bounds.origin.y + margins.top,
70
            },
70
            size: LogicalSize {
70
                width: (object_bounds.size.width - margins.left - margins.right).max(0.0),
70
                height: (object_bounds.size.height - margins.top - margins.bottom).max(0.0),
70
            },
70
        };
70
        let element_size = PhysicalSizeImport {
70
            width: border_box_bounds.size.width,
70
            height: border_box_bounds.size.height,
70
        };
        // Get border radius for background clipping
70
        let simple_border_radius = get_border_radius(
70
            self.ctx.styled_dom,
70
            node_id,
70
            styled_node_state,
70
            element_size,
70
            self.ctx.viewport_size,
        );
        // Get style border radius for border rendering
70
        let style_border_radius =
70
            get_style_border_radius(self.ctx.styled_dom, node_id, styled_node_state);
        // Use unified background/border painting with border-box bounds
70
        builder.push_backgrounds_and_border(
70
            border_box_bounds,
70
            &background_contents,
70
            &border_info,
70
            simple_border_radius,
70
            style_border_radius,
70
            self.ctx.image_cache,
        );
        // Push hit-test area for this inline-block element
        // This is critical for buttons and other inline-block elements to receive
        // mouse events and display the correct cursor (e.g., cursor: pointer)
70
        if let Some(tag_id) = get_tag_id(self.ctx.styled_dom, Some(node_id)) {
70
            builder.push_hit_test_area(border_box_bounds, tag_id);
70
        }
70
        Ok(())
70
    }
    // +spec:overflow:d1d5f6 - CSS 2.2 §9.9.1 stacking context creation and 7-layer paint order
    /// Determines if a node establishes a new stacking context based on CSS rules.
    // +spec:overflow:47b791 - z-index applies to positioned boxes; z-index:auto does not establish stacking context
    // +spec:positioning:8c6efd - Stacking contexts: positioned elements with z-index != auto establish new stacking context
    // +spec:positioning:b84cfa - z-index stacking context creation: integer z-index on positioned elements creates SC; auto on fixed/root creates SC
    // +spec:positioning:d06368 - relative/absolute with z-index:auto do not form stacking context but are painted as if they did
28998
    fn establishes_stacking_context(&self, node_index: usize) -> bool {
28998
        let Some(node) = self.positioned_tree.tree.get(node_index) else {
            return false;
        };
28998
        let Some(dom_id) = node.dom_node_id else {
148
            return false;
        };
28850
        let position = get_position_type(self.ctx.styled_dom, Some(dom_id));
28850
        let z_auto = crate::solver3::getters::is_z_index_auto(self.ctx.styled_dom, Some(dom_id));
        // +spec:position-sticky:66ba22 - fixed and sticky positioned boxes form a stacking context
28850
        if position == LayoutPosition::Fixed || position == LayoutPosition::Sticky {
            return true;
28850
        }
        // +spec:positioning:d06368 - relative/absolute with z-index:auto do not form stacking context
        // z-index:auto on position:absolute does NOT establish stacking context
28850
        if position == LayoutPosition::Absolute {
70
            return !z_auto;
28780
        }
        // position:relative with explicit z-index integer establishes stacking context
28780
        if position == LayoutPosition::Relative && !z_auto {
            return true;
28780
        }
28780
        if let Some(styled_node) = self.ctx.styled_dom.styled_nodes.as_container().get(dom_id) {
28780
            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
28780
            let node_state =
28780
                &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
            // Opacity < 1 (GPU: fast path via compact cache)
28780
            if crate::solver3::getters::get_opacity(
28780
                self.ctx.styled_dom, dom_id, node_state,
28780
            ) < 1.0 {
70
                return true;
28710
            }
            // Transform != none (GPU: has_transform bit check, then slow walk only if set)
28710
            if let Some(t) = crate::solver3::getters::get_transform(
28710
                self.ctx.styled_dom, dom_id, node_state,
28710
            ) {
                if !t.is_empty() {
                    return true;
                }
28710
            }
        }
28710
        false
28998
    }
}
/// Standalone predicate: does this element establish a new stacking context?
///
/// Centralizes the CSS 2.1 §9.9.1 and CSS3 rules for stacking context creation.
/// Checks: position + z-index, opacity < 1, transform != none.
///
/// This is the canonical check used by display list generation and can also
/// be called from other phases that need to reason about stacking contexts.
pub fn node_establishes_stacking_context(
    styled_dom: &StyledDom,
    dom_id: NodeId,
) -> bool {
    let position = crate::solver3::positioning::get_position_type(styled_dom, Some(dom_id));
    let z_auto = crate::solver3::getters::is_z_index_auto(styled_dom, Some(dom_id));
    // +spec:position-sticky:66ba22 - fixed and sticky positioned boxes form a stacking context
    if position == LayoutPosition::Fixed || position == LayoutPosition::Sticky {
        return true;
    }
    // Absolute with explicit z-index creates stacking context
    if position == LayoutPosition::Absolute && !z_auto {
        return true;
    }
    // Relative with explicit z-index creates stacking context
    if position == LayoutPosition::Relative && !z_auto {
        return true;
    }
    let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
    // Opacity < 1 (GPU: compact-cache fast path)
    if crate::solver3::getters::get_opacity(styled_dom, dom_id, node_state) < 1.0 {
        return true;
    }
    // Transform != none (GPU: has_transform bit check, then slow walk only if set)
    if let Some(t) = crate::solver3::getters::get_transform(styled_dom, dom_id, node_state) {
        if !t.is_empty() {
            return true;
        }
    }
    false
}
/// Helper struct to pass layout results to the display list generator.
///
/// Combines the layout tree with pre-calculated absolute positions for each node.
/// The positions are stored separately because they are computed in a final
/// positioning pass after layout is complete.
pub struct PositionedTree<'a> {
    /// The layout tree containing all nodes with their computed sizes
    pub tree: &'a LayoutTree,
    /// Map from node index to its absolute position in the document
    pub calculated_positions: &'a super::PositionVec,
}
/// Describes how overflow content should be handled for an element.
///
/// This maps to the CSS `overflow-x` and `overflow-y` properties and determines
/// whether content that exceeds the element's bounds should be visible, clipped,
/// or scrollable.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OverflowBehavior {
    /// Content is not clipped and may render outside the element's box (default)
    Visible,
    /// Content is clipped to the padding box, no scrollbars provided
    Hidden,
    /// Content is clipped to the padding box (CSS `overflow: clip`)
    Clip,
    /// Content is clipped and scrollbars are always shown
    Scroll,
    /// Content is clipped and scrollbars appear only when needed
    Auto,
}
impl OverflowBehavior {
    /// Returns `true` if this overflow behavior clips content.
    ///
    /// All behaviors except `Visible` result in content being clipped
    /// to the element's padding box.
    pub fn is_clipped(&self) -> bool {
        matches!(self, Self::Hidden | Self::Clip | Self::Scroll | Self::Auto)
    }
    /// Returns `true` if this overflow behavior enables scrolling.
    ///
    /// Only `Scroll` and `Auto` allow the user to scroll to see
    /// overflowing content.
    pub fn is_scroll(&self) -> bool {
        matches!(self, Self::Scroll | Self::Auto)
    }
}
/// Expands `clip_rect` outward by the `overflow-clip-margin` value on axes that use `overflow: clip`.
///
/// Per CSS Overflow 3 §3.2, `overflow-clip-margin` only applies to `overflow: clip` —
/// it has no effect on `overflow: hidden`, `scroll`, or `auto`.
105
fn apply_overflow_clip_margin(
105
    clip_rect: &mut LogicalRect,
105
    overflow_x: &super::getters::MultiValue<LayoutOverflow>,
105
    overflow_y: &super::getters::MultiValue<LayoutOverflow>,
105
    styled_dom: &StyledDom,
105
    dom_id: NodeId,
105
    styled_node_state: &azul_core::styled_dom::StyledNodeState,
105
) {
105
    if !overflow_x.is_clip() && !overflow_y.is_clip() {
105
        return;
    }
    let clip_margin = get_overflow_clip_margin_property(styled_dom, dom_id, styled_node_state);
    let Some(margin_val) = clip_margin.exact() else {
        return;
    };
    let m = margin_val.inner.to_pixels_internal(0.0, 0.0, 0.0).max(0.0);
    if m <= 0.0 {
        return;
    }
    if overflow_x.is_clip() {
        clip_rect.origin.x -= m;
        clip_rect.size.width += m * 2.0;
    }
    if overflow_y.is_clip() {
        clip_rect.origin.y -= m;
        clip_rect.size.height += m * 2.0;
    }
105
}
fn get_scroll_id(id: Option<NodeId>) -> LocalScrollId {
    id.map(|i| i.index() as u64).unwrap_or(0)
}
/// Calculates the actual content size of a node, including all children and text.
/// This is used to determine if scrollbars should appear for overflow: auto.
// +spec:overflow:c2ed94 - replaced element overflow is ink overflow (not scrollable);
// replaced elements (images) don't contribute scrollable overflow here
5425
fn get_scroll_content_size(node: &LayoutNodeHot, warm: Option<&LayoutNodeWarm>) -> LogicalSize {
    // First check if we have a pre-calculated overflow_content_size (for block children)
5425
    if let Some(overflow_size) = warm.and_then(|w| w.overflow_content_size) {
5425
        return overflow_size;
    }
    // Start with the node's own size
    let mut content_size = node.used_size.unwrap_or_default();
    // If this node has text layout, calculate the bounds of all text items
    if let Some(cached_layout) = warm.and_then(|w| w.inline_layout_result.as_ref()) {
        let text_layout = &cached_layout.layout;
        // Find the maximum extent of all positioned items
        let mut max_x: f32 = 0.0;
        let mut max_y: f32 = 0.0;
        for positioned_item in &text_layout.items {
            let item_bounds = positioned_item.item.bounds();
            let item_right = positioned_item.position.x + item_bounds.width;
            let item_bottom = positioned_item.position.y + item_bounds.height;
            max_x = max_x.max(item_right);
            max_y = max_y.max(item_bottom);
        }
        // Use the maximum extent as content size if it's larger
        content_size.width = content_size.width.max(max_x);
        content_size.height = content_size.height.max(max_y);
    }
    content_size
5425
}
27090
fn get_tag_id(dom: &StyledDom, id: Option<NodeId>) -> Option<DisplayListTagId> {
27090
    let node_id = id?;
47915
    let tag_mapping = dom.tag_ids_to_node_ids.as_ref().iter().find(|m| {
47915
        m.node_id.into_crate_internal() == Some(node_id)
47915
    })?;
6965
    Some((tag_mapping.tag_id.inner, TAG_TYPE_DOM_NODE))
27090
}
fn get_image_ref_for_image_source(
    source: &ImageSource,
) -> Option<ImageRef> {
    match source {
        ImageSource::Ref(image_ref) => Some(image_ref.clone()),
        ImageSource::Url(_url) => {
            // TODO: Look up in ImageCache
            // For now, CSS url() images are not yet supported
            None
        }
        ImageSource::Data(_) | ImageSource::Svg(_) | ImageSource::Placeholder(_) => {
            // TODO: Decode raw data / SVG to ImageRef
            None
        }
    }
}
/// Get the bounds of a display list item, if it has spatial extent.
20370
fn get_display_item_bounds(item: &DisplayListItem) -> Option<WindowLogicalRect> {
20370
    match item {
2625
        DisplayListItem::Rect { bounds, .. } => Some(*bounds),
        DisplayListItem::SelectionRect { bounds, .. } => Some(*bounds),
        DisplayListItem::CursorRect { bounds, .. } => Some(*bounds),
8155
        DisplayListItem::Border { bounds, .. } => Some(*bounds),
945
        DisplayListItem::TextLayout { bounds, .. } => Some(*bounds),
1015
        DisplayListItem::Text { clip_rect, .. } => Some(*clip_rect),
        DisplayListItem::Underline { bounds, .. } => Some(*bounds),
        DisplayListItem::Strikethrough { bounds, .. } => Some(*bounds),
        DisplayListItem::Overline { bounds, .. } => Some(*bounds),
        DisplayListItem::Image { bounds, .. } => Some(*bounds),
        DisplayListItem::ScrollBar { bounds, .. } => Some(*bounds),
        DisplayListItem::ScrollBarStyled { info } => Some(info.bounds),
70
        DisplayListItem::PushClip { bounds, .. } => Some(*bounds),
        DisplayListItem::PushScrollFrame { clip_bounds, .. } => Some(*clip_bounds),
3640
        DisplayListItem::HitTestArea { bounds, .. } => Some(*bounds),
1925
        DisplayListItem::PushStackingContext { bounds, .. } => Some(*bounds),
        DisplayListItem::VirtualView { bounds, .. } => Some(*bounds),
1995
        _ => None,
    }
20370
}
/// Clip a display list item to page bounds and offset to page-relative coordinates.
/// Returns None if the item is completely outside the page bounds.
20370
fn clip_and_offset_display_item(
20370
    item: &DisplayListItem,
20370
    page_top: f32,
20370
    page_bottom: f32,
20370
) -> Option<DisplayListItem> {
20370
    match item {
        DisplayListItem::Rect {
2625
            bounds,
2625
            color,
2625
            border_radius,
2625
        } => clip_rect_item(bounds.into_inner(), *color, *border_radius, page_top, page_bottom),
        DisplayListItem::Border {
8155
            bounds,
8155
            widths,
8155
            colors,
8155
            styles,
8155
            border_radius,
8155
        } => clip_border_item(
8155
            bounds.into_inner(),
8155
            *widths,
8155
            *colors,
8155
            *styles,
8155
            border_radius.clone(),
8155
            page_top,
8155
            page_bottom,
        ),
        DisplayListItem::SelectionRect {
            bounds,
            border_radius,
            color,
        } => clip_selection_rect_item(bounds.into_inner(), *border_radius, *color, page_top, page_bottom),
        DisplayListItem::CursorRect { bounds, color } => {
            clip_cursor_rect_item(bounds.into_inner(), *color, page_top, page_bottom)
        }
        DisplayListItem::Image { bounds, image, border_radius } => {
            clip_image_item(bounds.into_inner(), image.clone(), *border_radius, page_top, page_bottom)
        }
        DisplayListItem::TextLayout {
945
            layout,
945
            bounds,
945
            font_hash,
945
            font_size_px,
945
            color,
945
        } => clip_text_layout_item(
945
            layout,
945
            bounds.into_inner(),
945
            *font_hash,
945
            *font_size_px,
945
            *color,
945
            page_top,
945
            page_bottom,
        ),
        DisplayListItem::Text {
1015
            glyphs,
1015
            font_hash,
1015
            font_size_px,
1015
            color,
1015
            clip_rect,
            ..
1015
        } => clip_text_item(
1015
            glyphs,
1015
            *font_hash,
1015
            *font_size_px,
1015
            *color,
1015
            clip_rect.into_inner(),
1015
            page_top,
1015
            page_bottom,
        ),
        DisplayListItem::Underline {
            bounds,
            color,
            thickness,
        } => clip_text_decoration_item(
            bounds.into_inner(),
            *color,
            *thickness,
            TextDecorationType::Underline,
            page_top,
            page_bottom,
        ),
        DisplayListItem::Strikethrough {
            bounds,
            color,
            thickness,
        } => clip_text_decoration_item(
            bounds.into_inner(),
            *color,
            *thickness,
            TextDecorationType::Strikethrough,
            page_top,
            page_bottom,
        ),
        DisplayListItem::Overline {
            bounds,
            color,
            thickness,
        } => clip_text_decoration_item(
            bounds.into_inner(),
            *color,
            *thickness,
            TextDecorationType::Overline,
            page_top,
            page_bottom,
        ),
        DisplayListItem::ScrollBar {
            bounds,
            color,
            orientation,
            opacity_key,
            hit_id,
        } => clip_scrollbar_item(
            bounds.into_inner(),
            *color,
            *orientation,
            *opacity_key,
            *hit_id,
            page_top,
            page_bottom,
        ),
3640
        DisplayListItem::HitTestArea { bounds, tag } => {
3640
            clip_hit_test_area_item(bounds.into_inner(), *tag, page_top, page_bottom)
        }
        DisplayListItem::VirtualView {
            child_dom_id,
            bounds,
            clip_rect,
        } => clip_virtual_view_item(*child_dom_id, bounds.into_inner(), clip_rect.into_inner(), page_top, page_bottom),
        // ScrollBarStyled - clip based on overall bounds
        DisplayListItem::ScrollBarStyled { info } => {
            let bounds = info.bounds;
            if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
                None
            } else {
                // Clone and offset all the internal bounds
                let mut clipped_info = (**info).clone();
                let y_offset = -page_top;
                clipped_info.bounds = offset_rect_y(clipped_info.bounds.into_inner(), y_offset).into();
                clipped_info.track_bounds = offset_rect_y(clipped_info.track_bounds.into_inner(), y_offset).into();
                clipped_info.thumb_bounds = offset_rect_y(clipped_info.thumb_bounds.into_inner(), y_offset).into();
                if let Some(b) = clipped_info.button_decrement_bounds {
                    clipped_info.button_decrement_bounds = Some(offset_rect_y(b.into_inner(), y_offset).into());
                }
                if let Some(b) = clipped_info.button_increment_bounds {
                    clipped_info.button_increment_bounds = Some(offset_rect_y(b.into_inner(), y_offset).into());
                }
                Some(DisplayListItem::ScrollBarStyled {
                    info: Box::new(clipped_info),
                })
            }
        }
        // State management items - skip for now (would need proper per-page tracking)
        DisplayListItem::PushClip { .. }
        | DisplayListItem::PopClip
        | DisplayListItem::PushScrollFrame { .. }
        | DisplayListItem::PopScrollFrame
        | DisplayListItem::PushStackingContext { .. }
        | DisplayListItem::PopStackingContext
3990
        | DisplayListItem::VirtualViewPlaceholder { .. } => None,
        // Gradient items - simple bounds check
        DisplayListItem::LinearGradient {
            bounds,
            gradient,
            border_radius,
        } => {
            if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
                None
            } else {
                Some(DisplayListItem::LinearGradient {
                    bounds: offset_rect_y(bounds.into_inner(), -page_top).into(),
                    gradient: gradient.clone(),
                    border_radius: *border_radius,
                })
            }
        }
        DisplayListItem::RadialGradient {
            bounds,
            gradient,
            border_radius,
        } => {
            if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
                None
            } else {
                Some(DisplayListItem::RadialGradient {
                    bounds: offset_rect_y(bounds.into_inner(), -page_top).into(),
                    gradient: gradient.clone(),
                    border_radius: *border_radius,
                })
            }
        }
        DisplayListItem::ConicGradient {
            bounds,
            gradient,
            border_radius,
        } => {
            if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
                None
            } else {
                Some(DisplayListItem::ConicGradient {
                    bounds: offset_rect_y(bounds.into_inner(), -page_top).into(),
                    gradient: gradient.clone(),
                    border_radius: *border_radius,
                })
            }
        }
        // BoxShadow - simple bounds check
        DisplayListItem::BoxShadow {
            bounds,
            shadow,
            border_radius,
        } => {
            if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
                None
            } else {
                Some(DisplayListItem::BoxShadow {
                    bounds: offset_rect_y(bounds.into_inner(), -page_top).into(),
                    shadow: *shadow,
                    border_radius: *border_radius,
                })
            }
        }
        // Filter effects - skip for now (would need proper per-page tracking)
        DisplayListItem::PushFilter { .. }
        | DisplayListItem::PopFilter
        | DisplayListItem::PushBackdropFilter { .. }
        | DisplayListItem::PopBackdropFilter
        | DisplayListItem::PushOpacity { .. }
        | DisplayListItem::PopOpacity
        | DisplayListItem::PushReferenceFrame { .. }
        | DisplayListItem::PopReferenceFrame
        | DisplayListItem::PushTextShadow { .. }
        | DisplayListItem::PopTextShadow
        | DisplayListItem::PushImageMaskClip { .. }
        | DisplayListItem::PopImageMaskClip => None,
    }
20370
}
// Helper functions for clip_and_offset_display_item
/// Internal enum for text decoration type dispatch
#[derive(Debug, Clone, Copy)]
enum TextDecorationType {
    Underline,
    Strikethrough,
    Overline,
}
/// Clips a filled rectangle to page bounds.
2625
fn clip_rect_item(
2625
    bounds: LogicalRect,
2625
    color: ColorU,
2625
    border_radius: BorderRadius,
2625
    page_top: f32,
2625
    page_bottom: f32,
2625
) -> Option<DisplayListItem> {
2625
    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::Rect {
2625
        bounds: clipped.into(),
2625
        color,
2625
        border_radius,
2625
    })
2625
}
/// Clips a border to page bounds, hiding top/bottom borders when clipped.
8155
fn clip_border_item(
8155
    bounds: LogicalRect,
8155
    widths: StyleBorderWidths,
8155
    colors: StyleBorderColors,
8155
    styles: StyleBorderStyles,
8155
    border_radius: StyleBorderRadius,
8155
    page_top: f32,
8155
    page_bottom: f32,
8155
) -> Option<DisplayListItem> {
8155
    let original_bounds = bounds;
8155
    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| {
7665
        let new_widths = adjust_border_widths_for_clipping(
7665
            widths,
7665
            original_bounds,
7665
            clipped,
7665
            page_top,
7665
            page_bottom,
        );
7665
        DisplayListItem::Border {
7665
            bounds: clipped.into(),
7665
            widths: new_widths,
7665
            colors,
7665
            styles,
7665
            border_radius,
7665
        }
7665
    })
8155
}
/// Adjusts border widths when a border is clipped at page boundaries.
/// Hides top border if clipped at top, bottom border if clipped at bottom.
7665
fn adjust_border_widths_for_clipping(
7665
    mut widths: StyleBorderWidths,
7665
    original_bounds: LogicalRect,
7665
    clipped: LogicalRect,
7665
    page_top: f32,
7665
    page_bottom: f32,
7665
) -> StyleBorderWidths {
    // Hide top border if we clipped the top
7665
    if clipped.origin.y > 0.0 && original_bounds.origin.y < page_top {
        widths.top = None;
7665
    }
    // Hide bottom border if we clipped the bottom
7665
    let original_bottom = original_bounds.origin.y + original_bounds.size.height;
7665
    let clipped_bottom = clipped.origin.y + clipped.size.height;
7665
    if original_bottom > page_bottom && clipped_bottom >= page_bottom - page_top - 1.0 {
        widths.bottom = None;
7665
    }
7665
    widths
7665
}
/// Clips a selection rectangle to page bounds.
fn clip_selection_rect_item(
    bounds: LogicalRect,
    border_radius: BorderRadius,
    color: ColorU,
    page_top: f32,
    page_bottom: f32,
) -> Option<DisplayListItem> {
    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::SelectionRect {
        bounds: clipped.into(),
        border_radius,
        color,
    })
}
/// Clips a cursor rectangle to page bounds.
fn clip_cursor_rect_item(
    bounds: LogicalRect,
    color: ColorU,
    page_top: f32,
    page_bottom: f32,
) -> Option<DisplayListItem> {
    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::CursorRect {
        bounds: clipped.into(),
        color,
    })
}
/// Clips an image to page bounds if it overlaps the page.
fn clip_image_item(
    bounds: LogicalRect,
    image: ImageRef,
    border_radius: BorderRadius,
    page_top: f32,
    page_bottom: f32,
) -> Option<DisplayListItem> {
    if !rect_intersects(&bounds, page_top, page_bottom) {
        return None;
    }
    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::Image {
        bounds: clipped.into(),
        image,
        border_radius,
    })
}
/// Clips a text layout block to page bounds, filtering individual text items.
945
fn clip_text_layout_item(
945
    layout: &Arc<dyn std::any::Any + Send + Sync>,
945
    bounds: LogicalRect,
945
    font_hash: FontHash,
945
    font_size_px: f32,
945
    color: ColorU,
945
    page_top: f32,
945
    page_bottom: f32,
945
) -> Option<DisplayListItem> {
945
    if !rect_intersects(&bounds, page_top, page_bottom) {
        return None;
945
    }
    // Try to downcast and filter UnifiedLayout items
    #[cfg(feature = "text_layout")]
945
    if let Some(unified_layout) = layout.downcast_ref::<crate::text3::cache::UnifiedLayout>() {
945
        return clip_unified_layout(
945
            unified_layout,
945
            bounds,
945
            font_hash,
945
            font_size_px,
945
            color,
945
            page_top,
945
            page_bottom,
        );
    }
    // Fallback: simple bounds offset (legacy behavior)
    Some(DisplayListItem::TextLayout {
        layout: layout.clone(),
        bounds: offset_rect_y(bounds, -page_top).into(),
        font_hash,
        font_size_px,
        color,
    })
945
}
/// Clips a UnifiedLayout by filtering items to those on the current page.
#[cfg(feature = "text_layout")]
945
fn clip_unified_layout(
945
    unified_layout: &crate::text3::cache::UnifiedLayout,
945
    bounds: LogicalRect,
945
    font_hash: FontHash,
945
    font_size_px: f32,
945
    color: ColorU,
945
    page_top: f32,
945
    page_bottom: f32,
945
) -> Option<DisplayListItem> {
945
    let layout_origin_y = bounds.origin.y;
945
    let layout_origin_x = bounds.origin.x;
    // Filter items whose center falls within this page
945
    let filtered_items: Vec<_> = unified_layout
945
        .items
945
        .iter()
19145
        .filter(|item| item_center_on_page(item, layout_origin_y, page_top, page_bottom))
945
        .cloned()
945
        .collect();
945
    if filtered_items.is_empty() {
        return None;
945
    }
    // Calculate new origin for page-relative positioning
945
    let new_origin_y = (layout_origin_y - page_top).max(0.0);
    // Transform items to page-relative coordinates and calculate bounds
945
    let (offset_items, min_y, max_y, max_width) =
945
        transform_items_to_page_coords(filtered_items, layout_origin_y, page_top, new_origin_y);
945
    let new_layout = crate::text3::cache::UnifiedLayout {
945
        items: offset_items,
945
        overflow: unified_layout.overflow.clone(),
945
    };
945
    let new_bounds = LogicalRect {
945
        origin: LogicalPosition {
945
            x: layout_origin_x,
945
            y: new_origin_y,
945
        },
945
        size: LogicalSize {
945
            width: max_width.max(bounds.size.width),
945
            height: (max_y - min_y.min(0.0)).max(0.0),
945
        },
945
    };
945
    Some(DisplayListItem::TextLayout {
945
        layout: Arc::new(new_layout) as Arc<dyn std::any::Any + Send + Sync>,
945
        bounds: new_bounds.into(),
945
        font_hash,
945
        font_size_px,
945
        color,
945
    })
945
}
/// Checks if an item's center point falls within the page bounds.
#[cfg(feature = "text_layout")]
19145
fn item_center_on_page(
19145
    item: &crate::text3::cache::PositionedItem,
19145
    layout_origin_y: f32,
19145
    page_top: f32,
19145
    page_bottom: f32,
19145
) -> bool {
19145
    let item_y_absolute = layout_origin_y + item.position.y;
19145
    let item_height = item.item.bounds().height;
19145
    let item_center_y = item_y_absolute + (item_height / 2.0);
19145
    item_center_y >= page_top && item_center_y < page_bottom
19145
}
/// Transforms filtered items to page-relative coordinates.
/// Returns (items, min_y, max_y, max_width).
#[cfg(feature = "text_layout")]
945
fn transform_items_to_page_coords(
945
    items: Vec<crate::text3::cache::PositionedItem>,
945
    layout_origin_y: f32,
945
    page_top: f32,
945
    new_origin_y: f32,
945
) -> (Vec<crate::text3::cache::PositionedItem>, f32, f32, f32) {
945
    let mut min_y = f32::MAX;
945
    let mut max_y = f32::MIN;
945
    let mut max_width = 0.0f32;
945
    let offset_items: Vec<_> = items
945
        .into_iter()
19145
        .map(|mut item| {
19145
            let abs_y = layout_origin_y + item.position.y;
19145
            let page_y = abs_y - page_top;
19145
            let new_item_y = page_y - new_origin_y;
19145
            let item_bounds = item.item.bounds();
19145
            min_y = min_y.min(new_item_y);
19145
            max_y = max_y.max(new_item_y + item_bounds.height);
19145
            max_width = max_width.max(item.position.x + item_bounds.width);
19145
            item.position.y = new_item_y;
19145
            item
19145
        })
945
        .collect();
945
    (offset_items, min_y, max_y, max_width)
945
}
/// Clips a text glyph run to page bounds, filtering individual glyphs.
1015
fn clip_text_item(
1015
    glyphs: &[GlyphInstance],
1015
    font_hash: FontHash,
1015
    font_size_px: f32,
1015
    color: ColorU,
1015
    clip_rect: LogicalRect,
1015
    page_top: f32,
1015
    page_bottom: f32,
1015
) -> Option<DisplayListItem> {
1015
    if !rect_intersects(&clip_rect, page_top, page_bottom) {
        return None;
1015
    }
    // Filter glyphs using center-point decision (baseline position)
1015
    let page_glyphs: Vec<_> = glyphs
1015
        .iter()
18865
        .filter(|g| g.point.y >= page_top && g.point.y < page_bottom)
1015
        .map(|g| GlyphInstance {
18865
            index: g.index,
18865
            point: LogicalPosition {
18865
                x: g.point.x,
18865
                y: g.point.y - page_top,
18865
            },
18865
            size: g.size,
18865
        })
1015
        .collect();
1015
    if page_glyphs.is_empty() {
        return None;
1015
    }
1015
    Some(DisplayListItem::Text {
1015
        glyphs: page_glyphs,
1015
        font_hash,
1015
        font_size_px,
1015
        color,
1015
        clip_rect: offset_rect_y(clip_rect, -page_top).into(),
1015
        source_node_index: None,
1015
    })
1015
}
/// Clips a text decoration (underline, strikethrough, or overline) to page bounds.
fn clip_text_decoration_item(
    bounds: LogicalRect,
    color: ColorU,
    thickness: f32,
    decoration_type: TextDecorationType,
    page_top: f32,
    page_bottom: f32,
) -> Option<DisplayListItem> {
    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| match decoration_type {
        TextDecorationType::Underline => DisplayListItem::Underline {
            bounds: clipped.into(),
            color,
            thickness,
        },
        TextDecorationType::Strikethrough => DisplayListItem::Strikethrough {
            bounds: clipped.into(),
            color,
            thickness,
        },
        TextDecorationType::Overline => DisplayListItem::Overline {
            bounds: clipped.into(),
            color,
            thickness,
        },
    })
}
/// Clips a scrollbar to page bounds.
fn clip_scrollbar_item(
    bounds: LogicalRect,
    color: ColorU,
    orientation: ScrollbarOrientation,
    opacity_key: Option<OpacityKey>,
    hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
    page_top: f32,
    page_bottom: f32,
) -> Option<DisplayListItem> {
    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::ScrollBar {
        bounds: clipped.into(),
        color,
        orientation,
        opacity_key,
        hit_id,
    })
}
/// Clips a hit test area to page bounds.
3640
fn clip_hit_test_area_item(
3640
    bounds: LogicalRect,
3640
    tag: DisplayListTagId,
3640
    page_top: f32,
3640
    page_bottom: f32,
3640
) -> Option<DisplayListItem> {
3640
    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::HitTestArea {
3500
        bounds: clipped.into(),
3500
        tag,
3500
    })
3640
}
/// Clips a virtualized view to page bounds.
fn clip_virtual_view_item(
    child_dom_id: DomId,
    bounds: LogicalRect,
    clip_rect: LogicalRect,
    page_top: f32,
    page_bottom: f32,
) -> Option<DisplayListItem> {
    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::VirtualView {
        child_dom_id,
        bounds: clipped.into(),
        clip_rect: offset_rect_y(clip_rect, -page_top).into(),
    })
}
/// Clip a rectangle to page bounds and offset to page-relative coordinates.
/// Returns None if the rectangle is completely outside the page.
14420
fn clip_rect_bounds(bounds: LogicalRect, page_top: f32, page_bottom: f32) -> Option<LogicalRect> {
14420
    let item_top = bounds.origin.y;
14420
    let item_bottom = bounds.origin.y + bounds.size.height;
    // Check if completely outside page
14420
    if item_bottom <= page_top || item_top >= page_bottom {
630
        return None;
13790
    }
    // Calculate clipped bounds
13790
    let clipped_top = item_top.max(page_top);
13790
    let clipped_bottom = item_bottom.min(page_bottom);
13790
    let clipped_height = clipped_bottom - clipped_top;
    // Offset to page-relative coordinates
13790
    let page_relative_y = clipped_top - page_top;
13790
    Some(LogicalRect {
13790
        origin: LogicalPosition {
13790
            x: bounds.origin.x,
13790
            y: page_relative_y,
13790
        },
13790
        size: LogicalSize {
13790
            width: bounds.size.width,
13790
            height: clipped_height,
13790
        },
13790
    })
14420
}
/// Check if a rectangle intersects the page bounds.
1960
fn rect_intersects(bounds: &LogicalRect, page_top: f32, page_bottom: f32) -> bool {
1960
    let item_top = bounds.origin.y;
1960
    let item_bottom = bounds.origin.y + bounds.size.height;
1960
    item_bottom > page_top && item_top < page_bottom
1960
}
/// Offset a rectangle's Y coordinate.
1015
fn offset_rect_y(bounds: LogicalRect, offset_y: f32) -> LogicalRect {
1015
    LogicalRect {
1015
        origin: LogicalPosition {
1015
            x: bounds.origin.x,
1015
            y: bounds.origin.y + offset_y,
1015
        },
1015
        size: bounds.size,
1015
    }
1015
}
// Slicer based pagination: "Infinite Canvas with Clipping"
//
// This approach treats pages as "viewports" into a single infinite canvas:
//
// 1. Layout generates ONE display list on an infinite vertical strip
// 2. Each page is a clip rectangle that "views" a portion of that strip
// 3. Items that span page boundaries are clipped and appear on BOTH pages
use azul_css::props::layout::fragmentation::{BreakInside, PageBreak};
use crate::solver3::pagination::{
    HeaderFooterConfig, MarginBoxContent, PageInfo, TableHeaderInfo, TableHeaderTracker,
};
/// Configuration for the slicer-based pagination.
#[derive(Debug, Clone, Default)]
pub struct SlicerConfig {
    /// Height of each page's content area (excludes margins, headers, footers)
    pub page_content_height: f32,
    /// Height of "dead zone" between pages (for margins, headers, footers)
    /// This represents space that content should NOT overlap with
    pub page_gap: f32,
    /// Whether to clip items that span page boundaries (true) or push them to next page (false)
    pub allow_clipping: bool,
    /// Header and footer configuration
    pub header_footer: HeaderFooterConfig,
    /// Width of the page content area (for centering headers/footers)
    pub page_width: f32,
    /// Table headers that need repetition across pages
    pub table_headers: TableHeaderTracker,
}
impl SlicerConfig {
    /// Create a simple slicer config with no gaps between pages.
    pub fn simple(page_height: f32) -> Self {
        Self {
            page_content_height: page_height,
            page_gap: 0.0,
            allow_clipping: true,
            header_footer: HeaderFooterConfig::default(),
            page_width: 595.0, // Default A4 width in points
            table_headers: TableHeaderTracker::default(),
        }
    }
    /// Create a slicer config with margins/gaps between pages.
    pub fn with_gap(page_height: f32, gap: f32) -> Self {
        Self {
            page_content_height: page_height,
            page_gap: gap,
            allow_clipping: true,
            header_footer: HeaderFooterConfig::default(),
            page_width: 595.0,
            table_headers: TableHeaderTracker::default(),
        }
    }
    /// Add header/footer configuration.
    pub fn with_header_footer(mut self, config: HeaderFooterConfig) -> Self {
        self.header_footer = config;
        self
    }
    /// Set the page width (for header/footer positioning).
    pub fn with_page_width(mut self, width: f32) -> Self {
        self.page_width = width;
        self
    }
    /// Add table headers for repetition.
    pub fn with_table_headers(mut self, tracker: TableHeaderTracker) -> Self {
        self.table_headers = tracker;
        self
    }
    /// Register a single table header.
    pub fn register_table_header(&mut self, info: TableHeaderInfo) {
        self.table_headers.register_table_header(info);
    }
    /// The total height of a page "slot" including the gap.
    pub fn page_slot_height(&self) -> f32 {
        self.page_content_height + self.page_gap
    }
    /// Calculate which page a Y coordinate falls on.
    pub fn page_for_y(&self, y: f32) -> usize {
        if self.page_slot_height() <= 0.0 {
            return 0;
        }
        (y / self.page_slot_height()).floor() as usize
    }
    /// Get the Y range for a specific page (in infinite canvas coordinates).
    pub fn page_bounds(&self, page_index: usize) -> (f32, f32) {
        let start = page_index as f32 * self.page_slot_height();
        let end = start + self.page_content_height;
        (start, end)
    }
}
/// Paginate with CSS break property support.
///
/// This function calculates page boundaries based on CSS break-before, break-after,
/// and break-inside properties, then clips content to those boundaries.
///
/// **Key insight**: Items are NEVER shifted. Instead, page boundaries are adjusted
/// to honor break properties.
1925
pub fn paginate_display_list_with_slicer_and_breaks(
1925
    full_display_list: DisplayList,
1925
    config: &SlicerConfig,
1925
) -> Result<Vec<DisplayList>> {
1925
    if config.page_content_height <= 0.0 || config.page_content_height >= f32::MAX {
        return Ok(vec![full_display_list]);
1925
    }
    // Calculate base header/footer space (used for pages that show headers/footers)
1925
    let base_header_space = if config.header_footer.show_header {
        config.header_footer.header_height
    } else {
1925
        0.0
    };
1925
    let base_footer_space = if config.header_footer.show_footer {
        config.header_footer.footer_height
    } else {
1925
        0.0
    };
    // Calculate effective heights for different page types
1925
    let normal_page_content_height =
1925
        config.page_content_height - base_header_space - base_footer_space;
1925
    let first_page_content_height = if config.header_footer.skip_first_page {
        // First page has full height when skipping headers/footers
        config.page_content_height
    } else {
1925
        normal_page_content_height
    };
    // Step 1: Calculate page break positions based on CSS properties
    //
    // Instead of using regular intervals, we calculate where page breaks
    // should occur based on:
    //
    // - break-before: always → force break before this item
    // - break-after: always → force break after this item
    // - break-inside: avoid → don't break inside this item (push to next page if needed)
1925
    let page_breaks = calculate_page_break_positions(
1925
        &full_display_list,
1925
        first_page_content_height,
1925
        normal_page_content_height,
    );
1925
    let num_pages = page_breaks.len();
    // Create per-page display lists by slicing the master list
1925
    let mut pages: Vec<DisplayList> = Vec::with_capacity(num_pages);
1925
    for (page_idx, &(content_start_y, content_end_y)) in page_breaks.iter().enumerate() {
        // Generate page info for header/footer content
1925
        let page_info = PageInfo::new(page_idx + 1, num_pages);
        // Calculate per-page header/footer space
1925
        let skip_this_page = config.header_footer.skip_first_page && page_info.is_first;
1925
        let header_space = if config.header_footer.show_header && !skip_this_page {
            config.header_footer.header_height
        } else {
1925
            0.0
        };
1925
        let footer_space = if config.header_footer.show_footer && !skip_this_page {
            config.header_footer.footer_height
        } else {
1925
            0.0
        };
1925
        let _ = footer_space; // Currently unused but reserved for future
1925
        let mut page_items = Vec::new();
1925
        let mut page_node_mapping = Vec::new();
        // 1. Add header if enabled
1925
        if config.header_footer.show_header && !skip_this_page {
            let header_text = config.header_footer.header_text(page_info);
            if !header_text.is_empty() {
                let header_items = generate_text_display_items(
                    &header_text,
                    LogicalRect {
                        origin: LogicalPosition { x: 0.0, y: 0.0 },
                        size: LogicalSize {
                            width: config.page_width,
                            height: config.header_footer.header_height,
                        },
                    },
                    config.header_footer.font_size,
                    config.header_footer.text_color,
                    TextAlignment::Center,
                );
                for item in header_items {
                    page_items.push(item);
                    page_node_mapping.push(None);
                }
            }
1925
        }
        // 2. Inject repeated table headers (if any)
1925
        let repeated_headers = config.table_headers.get_repeated_headers_for_page(
1925
            page_idx,
1925
            content_start_y,
1925
            content_end_y,
        );
1925
        let mut thead_total_height = 0.0f32;
1925
        for (y_offset_from_page_top, thead_items, thead_height) in repeated_headers {
            let thead_y = header_space + y_offset_from_page_top;
            for item in thead_items {
                let translated_item = offset_display_item_y(item, thead_y);
                page_items.push(translated_item);
                page_node_mapping.push(None);
            }
            thead_total_height = thead_total_height.max(thead_height);
        }
        // 3. Calculate content offset (after header and repeated table headers)
1925
        let content_y_offset = header_space + thead_total_height;
        // 4. Slice and offset content items (skip fixed-position items, they are added in step 4b)
20370
        for (item_idx, item) in full_display_list.items.iter().enumerate() {
            // Skip items that belong to fixed-position elements (they are replicated separately)
20370
            let is_fixed = full_display_list.fixed_position_item_ranges.iter()
20370
                .any(|&(start, end)| item_idx >= start && item_idx < end);
20370
            if is_fixed {
                continue;
20370
            }
15750
            if let Some(clipped_item) =
20370
                clip_and_offset_display_item(item, content_start_y, content_end_y)
            {
15750
                let final_item = if content_y_offset > 0.0 {
                    offset_display_item_y(&clipped_item, content_y_offset)
                } else {
15750
                    clipped_item
                };
15750
                page_items.push(final_item);
15750
                let node_mapping = full_display_list
15750
                    .node_mapping
15750
                    .get(item_idx)
15750
                    .copied()
15750
                    .flatten();
15750
                page_node_mapping.push(node_mapping);
4620
            }
        }
        // 4b. Replicate fixed-position items on every page (CSS Positioned Layout §2.1)
        // Fixed-position boxes are fixed relative to the page box, so they appear
        // at the same position on every page without Y-offset adjustment.
1925
        for &(start, end) in &full_display_list.fixed_position_item_ranges {
            for item_idx in start..end {
                if let Some(item) = full_display_list.items.get(item_idx) {
                    let final_item = if content_y_offset > 0.0 {
                        offset_display_item_y(item, content_y_offset)
                    } else {
                        item.clone()
                    };
                    page_items.push(final_item);
                    let node_mapping = full_display_list
                        .node_mapping
                        .get(item_idx)
                        .copied()
                        .flatten();
                    page_node_mapping.push(node_mapping);
                }
            }
        }
        // 5. Add footer if enabled
1925
        if config.header_footer.show_footer && !skip_this_page {
            let footer_text = config.header_footer.footer_text(page_info);
            if !footer_text.is_empty() {
                let footer_y = config.page_content_height - config.header_footer.footer_height;
                let footer_items = generate_text_display_items(
                    &footer_text,
                    LogicalRect {
                        origin: LogicalPosition {
                            x: 0.0,
                            y: footer_y,
                        },
                        size: LogicalSize {
                            width: config.page_width,
                            height: config.header_footer.footer_height,
                        },
                    },
                    config.header_footer.font_size,
                    config.header_footer.text_color,
                    TextAlignment::Center,
                );
                for item in footer_items {
                    page_items.push(item);
                    page_node_mapping.push(None);
                }
            }
1925
        }
1925
        pages.push(DisplayList {
1925
            items: page_items,
1925
            node_mapping: page_node_mapping,
1925
            forced_page_breaks: Vec::new(),
1925
            fixed_position_item_ranges: Vec::new(), // Already handled during pagination
1925
        });
    }
    // Ensure at least one page
1925
    if pages.is_empty() {
        pages.push(DisplayList::default());
1925
    }
1925
    Ok(pages)
1925
}
/// Calculate page break positions respecting CSS forced page breaks.
///
/// Returns a vector of (start_y, end_y) tuples representing each page's content bounds.
///
/// This function uses the `forced_page_breaks` from the DisplayList to insert
/// page breaks at positions specified by CSS `break-before: always` and `break-after: always`.
/// Regular page breaks still occur at normal intervals when no forced break is present.
1925
fn calculate_page_break_positions(
1925
    display_list: &DisplayList,
1925
    first_page_height: f32,
1925
    normal_page_height: f32,
1925
) -> Vec<(f32, f32)> {
1925
    let total_height = calculate_display_list_height(display_list);
1925
    if total_height <= 0.0 || first_page_height <= 0.0 {
        return vec![(0.0, total_height.max(first_page_height))];
1925
    }
    // Collect all potential break points: forced breaks + regular interval breaks
1925
    let mut break_points: Vec<f32> = Vec::new();
    // Add forced page breaks from the display list (from CSS break-before/break-after)
1925
    for &forced_break_y in &display_list.forced_page_breaks {
        if forced_break_y > 0.0 && forced_break_y < total_height {
            break_points.push(forced_break_y);
        }
    }
    // Generate regular interval break points
1925
    let mut y = first_page_height;
1925
    while y < total_height {
        break_points.push(y);
        y += normal_page_height;
    }
    // Sort and deduplicate break points
1925
    break_points.sort_by(|a, b| a.partial_cmp(b).unwrap());
1925
    break_points.dedup_by(|a, b| (*a - *b).abs() < 1.0); // Merge breaks within 1px
    // Convert break points to page ranges
1925
    let mut page_breaks: Vec<(f32, f32)> = Vec::new();
1925
    let mut page_start = 0.0f32;
1925
    for break_y in break_points {
        if break_y > page_start {
            page_breaks.push((page_start, break_y));
            page_start = break_y;
        }
    }
    // Add final page if there's remaining content
1925
    if page_start < total_height {
1925
        page_breaks.push((page_start, total_height));
1925
    }
    // Ensure at least one page
1925
    if page_breaks.is_empty() {
        page_breaks.push((0.0, total_height.max(first_page_height)));
1925
    }
1925
    page_breaks
1925
}
/// Text alignment for generated header/footer text.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextAlignment {
    Left,
    Center,
    Right,
}
/// Helper to offset all Y coordinates of a display item.
fn offset_display_item_y(item: &DisplayListItem, y_offset: f32) -> DisplayListItem {
    if y_offset == 0.0 {
        return item.clone();
    }
    match item {
        DisplayListItem::Rect {
            bounds,
            color,
            border_radius,
        } => DisplayListItem::Rect {
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            color: *color,
            border_radius: *border_radius,
        },
        DisplayListItem::Border {
            bounds,
            widths,
            colors,
            styles,
            border_radius,
        } => DisplayListItem::Border {
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            widths: widths.clone(),
            colors: *colors,
            styles: *styles,
            border_radius: border_radius.clone(),
        },
        DisplayListItem::Text {
            glyphs,
            font_hash,
            font_size_px,
            color,
            clip_rect,
            ..
        } => {
            let offset_glyphs: Vec<GlyphInstance> = glyphs
                .iter()
                .map(|g| GlyphInstance {
                    index: g.index,
                    point: LogicalPosition {
                        x: g.point.x,
                        y: g.point.y + y_offset,
                    },
                    size: g.size,
                })
                .collect();
            DisplayListItem::Text {
                glyphs: offset_glyphs,
                font_hash: *font_hash,
                font_size_px: *font_size_px,
                color: *color,
                clip_rect: offset_rect_y(clip_rect.into_inner(), y_offset).into(),
                source_node_index: None,
            }
        }
        DisplayListItem::TextLayout {
            layout,
            bounds,
            font_hash,
            font_size_px,
            color,
        } => DisplayListItem::TextLayout {
            layout: layout.clone(),
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            font_hash: *font_hash,
            font_size_px: *font_size_px,
            color: *color,
        },
        DisplayListItem::Image { bounds, image, border_radius } => DisplayListItem::Image {
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            image: image.clone(),
            border_radius: *border_radius,
        },
        // Pass through other items with their bounds offset
        DisplayListItem::SelectionRect {
            bounds,
            border_radius,
            color,
        } => DisplayListItem::SelectionRect {
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            border_radius: *border_radius,
            color: *color,
        },
        DisplayListItem::CursorRect { bounds, color } => DisplayListItem::CursorRect {
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            color: *color,
        },
        DisplayListItem::Underline {
            bounds,
            color,
            thickness,
        } => DisplayListItem::Underline {
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            color: *color,
            thickness: *thickness,
        },
        DisplayListItem::Strikethrough {
            bounds,
            color,
            thickness,
        } => DisplayListItem::Strikethrough {
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            color: *color,
            thickness: *thickness,
        },
        DisplayListItem::Overline {
            bounds,
            color,
            thickness,
        } => DisplayListItem::Overline {
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            color: *color,
            thickness: *thickness,
        },
        DisplayListItem::ScrollBar {
            bounds,
            color,
            orientation,
            opacity_key,
            hit_id,
        } => DisplayListItem::ScrollBar {
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            color: *color,
            orientation: *orientation,
            opacity_key: *opacity_key,
            hit_id: *hit_id,
        },
        DisplayListItem::HitTestArea { bounds, tag } => DisplayListItem::HitTestArea {
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            tag: *tag,
        },
        DisplayListItem::PushClip {
            bounds,
            border_radius,
        } => DisplayListItem::PushClip {
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            border_radius: *border_radius,
        },
        DisplayListItem::PushScrollFrame {
            clip_bounds,
            content_size,
            scroll_id,
        } => DisplayListItem::PushScrollFrame {
            clip_bounds: offset_rect_y(clip_bounds.into_inner(), y_offset).into(),
            content_size: *content_size,
            scroll_id: *scroll_id,
        },
        DisplayListItem::PushStackingContext { bounds, z_index } => {
            DisplayListItem::PushStackingContext {
                bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
                z_index: *z_index,
            }
        }
        DisplayListItem::VirtualView {
            child_dom_id,
            bounds,
            clip_rect,
        } => DisplayListItem::VirtualView {
            child_dom_id: *child_dom_id,
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            clip_rect: offset_rect_y(clip_rect.into_inner(), y_offset).into(),
        },
        DisplayListItem::VirtualViewPlaceholder {
            node_id,
            bounds,
            clip_rect,
        } => DisplayListItem::VirtualViewPlaceholder {
            node_id: *node_id,
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            clip_rect: offset_rect_y(clip_rect.into_inner(), y_offset).into(),
        },
        // Pass through stateless items
        DisplayListItem::PopClip => DisplayListItem::PopClip,
        DisplayListItem::PopScrollFrame => DisplayListItem::PopScrollFrame,
        DisplayListItem::PopStackingContext => DisplayListItem::PopStackingContext,
        // Gradient items
        DisplayListItem::LinearGradient {
            bounds,
            gradient,
            border_radius,
        } => DisplayListItem::LinearGradient {
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            gradient: gradient.clone(),
            border_radius: *border_radius,
        },
        DisplayListItem::RadialGradient {
            bounds,
            gradient,
            border_radius,
        } => DisplayListItem::RadialGradient {
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            gradient: gradient.clone(),
            border_radius: *border_radius,
        },
        DisplayListItem::ConicGradient {
            bounds,
            gradient,
            border_radius,
        } => DisplayListItem::ConicGradient {
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            gradient: gradient.clone(),
            border_radius: *border_radius,
        },
        // BoxShadow
        DisplayListItem::BoxShadow {
            bounds,
            shadow,
            border_radius,
        } => DisplayListItem::BoxShadow {
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            shadow: *shadow,
            border_radius: *border_radius,
        },
        // Filter effects
        DisplayListItem::PushFilter { bounds, filters } => DisplayListItem::PushFilter {
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            filters: filters.clone(),
        },
        DisplayListItem::PopFilter => DisplayListItem::PopFilter,
        DisplayListItem::PushBackdropFilter { bounds, filters } => {
            DisplayListItem::PushBackdropFilter {
                bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
                filters: filters.clone(),
            }
        }
        DisplayListItem::PopBackdropFilter => DisplayListItem::PopBackdropFilter,
        DisplayListItem::PushOpacity { bounds, opacity } => DisplayListItem::PushOpacity {
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            opacity: *opacity,
        },
        DisplayListItem::PopOpacity => DisplayListItem::PopOpacity,
        DisplayListItem::ScrollBarStyled { info } => {
            let mut offset_info = (**info).clone();
            offset_info.bounds = offset_rect_y(offset_info.bounds.into_inner(), y_offset).into();
            offset_info.track_bounds = offset_rect_y(offset_info.track_bounds.into_inner(), y_offset).into();
            offset_info.thumb_bounds = offset_rect_y(offset_info.thumb_bounds.into_inner(), y_offset).into();
            if let Some(b) = offset_info.button_decrement_bounds {
                offset_info.button_decrement_bounds = Some(offset_rect_y(b.into_inner(), y_offset).into());
            }
            if let Some(b) = offset_info.button_increment_bounds {
                offset_info.button_increment_bounds = Some(offset_rect_y(b.into_inner(), y_offset).into());
            }
            DisplayListItem::ScrollBarStyled {
                info: Box::new(offset_info),
            }
        }
        // Reference frames - offset the bounds
        DisplayListItem::PushReferenceFrame {
            transform_key,
            initial_transform,
            bounds,
        } => DisplayListItem::PushReferenceFrame {
            transform_key: *transform_key,
            initial_transform: *initial_transform,
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
        },
        DisplayListItem::PopReferenceFrame => DisplayListItem::PopReferenceFrame,
        DisplayListItem::PushTextShadow { shadow } => DisplayListItem::PushTextShadow {
            shadow: shadow.clone(),
        },
        DisplayListItem::PopTextShadow => DisplayListItem::PopTextShadow,
        DisplayListItem::PushImageMaskClip {
            bounds,
            mask_image,
            mask_rect,
        } => DisplayListItem::PushImageMaskClip {
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
            mask_image: mask_image.clone(),
            mask_rect: offset_rect_y(mask_rect.into_inner(), y_offset).into(),
        },
        DisplayListItem::PopImageMaskClip => DisplayListItem::PopImageMaskClip,
    }
}
/// Generate display list items for simple text (headers/footers).
///
/// This creates a simplified text rendering without full text layout.
/// For now, this creates a placeholder that renderers should handle specially.
fn generate_text_display_items(
    text: &str,
    bounds: LogicalRect,
    font_size: f32,
    color: ColorU,
    alignment: TextAlignment,
) -> Vec<DisplayListItem> {
    use crate::font_traits::FontHash;
    if text.is_empty() {
        return Vec::new();
    }
    // Calculate approximate text position based on alignment
    // For now, we estimate character width as 0.5 * font_size (monospace approximation)
    let char_width = font_size * 0.5;
    let text_width = text.len() as f32 * char_width;
    let x_offset = match alignment {
        TextAlignment::Left => bounds.origin.x,
        TextAlignment::Center => bounds.origin.x + (bounds.size.width - text_width) / 2.0,
        TextAlignment::Right => bounds.origin.x + bounds.size.width - text_width,
    };
    // Position text vertically centered in the bounds
    let y_pos = bounds.origin.y + (bounds.size.height + font_size) / 2.0 - font_size * 0.2;
    // Create simple glyph instances for each character
    // Note: This is a simplified approach - proper text rendering should use text3
    let glyphs: Vec<GlyphInstance> = text
        .chars()
        .enumerate()
        .filter(|(_, c)| !c.is_control())
        .map(|(i, c)| GlyphInstance {
            index: c as u32, // Use Unicode codepoint as glyph index (placeholder)
            point: LogicalPosition {
                x: x_offset + i as f32 * char_width,
                y: y_pos,
            },
            size: LogicalSize::new(char_width, font_size),
        })
        .collect();
    if glyphs.is_empty() {
        return Vec::new();
    }
    vec![DisplayListItem::Text {
        glyphs,
        font_hash: FontHash::from_hash(0), // Default font hash - renderer should use default font
        font_size_px: font_size,
        color,
        clip_rect: bounds.into(),
        source_node_index: None,
    }]
}
/// Calculate the total height of a display list (max Y + height of all items).
1925
fn calculate_display_list_height(display_list: &DisplayList) -> f32 {
1925
    let mut max_bottom = 0.0f32;
22295
    for item in &display_list.items {
20370
        if let Some(bounds) = get_display_item_bounds(item) {
            // Skip items with zero height - they don't contribute to visible content
18375
            if bounds.0.size.height < 0.1 {
1365
                continue;
17010
            }
17010
            let item_bottom = bounds.0.origin.y + bounds.0.size.height;
17010
            if item_bottom > max_bottom {
1925
                max_bottom = item_bottom;
15085
            }
1995
        }
    }
1925
    max_bottom
1925
}
/// Break property information for pagination decisions.
#[derive(Debug, Clone, Copy, Default)]
pub struct BreakProperties {
    pub break_before: PageBreak,
    pub break_after: PageBreak,
    pub break_inside: BreakInside,
}
// ============================================================================
// TEXT-OVERFLOW STUB
// ============================================================================
/// Applies text-overflow ellipsis handling to a display list.
///
/// CSS UI Module Level 3, section 6.2 (text-overflow):
/// When inline content overflows a block container that has `overflow: hidden`
/// (or clip/scroll) and `text-overflow: ellipsis`, the overflowing text should
/// be replaced with an ellipsis character (U+2026) or a custom string.
///
/// This is a display-list post-processing step that modifies glyph runs
/// to show an ellipsis when text overflows its container. It operates on
/// the assumption that the container already has a PushClip that clips
/// the overflow -- this function additionally replaces the trailing glyphs
/// with an ellipsis so the user gets a visual indicator of truncation.
///
/// # Parameters
/// - `display_list`: The display list to modify (text items may be clipped/replaced)
/// - `container_bounds`: The bounds of the containing block (overflow boundary)
/// - `_ellipsis`: The ellipsis string (currently unused; U+2026 glyph index is used)
///
/// # Algorithm
/// 1. For each Text item in the display list, check if any glyphs extend
///    past the container's right edge (inline-end in LTR).
/// 2. If so, find the last glyph that fits entirely within the container,
///    accounting for the width of the ellipsis character.
/// 3. Remove all glyphs after that point.
/// 4. Append an ellipsis glyph (U+2026 = glyph index 0x2026 as a fallback;
///    proper glyph lookup requires font metrics not available here).
///
/// Note: This is a best-effort implementation. A pixel-perfect version would
/// need access to font metrics to measure the exact ellipsis glyph width and
/// to look up the correct glyph index for the ellipsis in each font.
// +spec:overflow:f175b9 - bidi ellipsis: characters visually at the end edge of the line are hidden for ellipsis
pub fn apply_text_overflow_ellipsis(
    display_list: &mut DisplayList,
    container_bounds: LogicalRect,
    _ellipsis: &str,
) {
    let container_right = container_bounds.origin.x + container_bounds.size.width;
    // Approximate ellipsis width as ~0.6 * font_size (typical for "..." in most fonts).
    // This is a heuristic; proper implementation requires font metric access.
    for item in display_list.items.iter_mut() {
        match item {
            DisplayListItem::Text {
                glyphs,
                font_size_px,
                clip_rect,
                ..
            } => {
                if glyphs.is_empty() {
                    continue;
                }
                // Check if any glyph extends past the container right edge
                let last_glyph = &glyphs[glyphs.len() - 1];
                let last_glyph_right = last_glyph.point.x + last_glyph.size.width;
                if last_glyph_right <= container_right {
                    continue; // No overflow, nothing to do
                }
                // Estimate ellipsis width
                let ellipsis_width = *font_size_px * 0.6;
                let truncation_edge = container_right - ellipsis_width;
                // Find the last glyph that fits before the truncation edge
                let mut keep_count = 0;
                for (i, glyph) in glyphs.iter().enumerate() {
                    let glyph_right = glyph.point.x + glyph.size.width;
                    if glyph_right > truncation_edge {
                        break;
                    }
                    keep_count = i + 1;
                }
                // Truncate the glyphs
                glyphs.truncate(keep_count);
                // Append an ellipsis glyph. We use Unicode codepoint U+2026
                // (HORIZONTAL ELLIPSIS) as the glyph index. This is a common
                // convention; renderers that use proper glyph IDs will need to
                // map this to the font's actual glyph index.
                let ellipsis_x = if let Some(last) = glyphs.last() {
                    last.point.x + last.size.width
                } else {
                    container_bounds.origin.x
                };
                let ellipsis_glyph = GlyphInstance {
                    index: 0x2026, // U+2026 HORIZONTAL ELLIPSIS
                    point: LogicalPosition::new(ellipsis_x, glyphs.first().map_or(
                        container_bounds.origin.y,
                        |g| g.point.y,
                    )),
                    size: LogicalSize::new(ellipsis_width, *font_size_px),
                };
                glyphs.push(ellipsis_glyph);
                // Update the clip rect to match the container bounds so
                // the ellipsis is visible but nothing past it is shown
                *clip_rect = container_bounds.into();
            }
            _ => {} // Only process Text items
        }
    }
}
// ============================================================================
// CLIP-PATH STUB
// ============================================================================
/// Resolves a CSS clip-path shape to a clipping rectangle.
///
/// CSS Masking Module Level 1, section 3 (clip-path):
/// The clip-path property creates a clipping region that determines which parts
/// of an element are visible. Content outside the clipping region is hidden.
///
/// Currently supported clip-path values:
/// - `inset()` - rectangular clip with optional rounding
/// - `circle()` - approximated as bounding box rectangle
/// - `ellipse()` - approximated as bounding box rectangle
/// - `polygon()` - approximated as axis-aligned bounding box
/// - `none` - no clipping (returns None)
///
/// # Parameters
/// - `clip_path`: The resolved clip-path CSS property value
/// - `node_bounds`: The reference box for resolving clip-path values
///
/// # Returns
/// A `(LogicalRect, f32)` tuple: the clip rectangle and border radius,
/// or `None` if no clipping should be applied.
///
/// Note: Circle, ellipse, and polygon shapes are approximated as axis-aligned
/// bounding boxes. A full implementation would use path-based clipping in the
/// renderer, but rectangular clips work for the most common use cases.
pub fn resolve_clip_path(
    clip_path: &azul_css::props::layout::shape::ClipPath,
    node_bounds: LogicalRect,
) -> Option<(LogicalRect, f32)> {
    use azul_css::props::layout::shape::ClipPath;
    use azul_css::shape::CssShape;
    match clip_path {
        ClipPath::None => None,
        ClipPath::Shape(shape) => {
            match shape {
                CssShape::Inset(inset) => {
                    // CSS inset() creates a rectangular clip inset from each edge.
                    // inset(top right bottom left round border-radius)
                    let x = node_bounds.origin.x + inset.inset_left;
                    let y = node_bounds.origin.y + inset.inset_top;
                    let w = (node_bounds.size.width - inset.inset_left - inset.inset_right).max(0.0);
                    let h = (node_bounds.size.height - inset.inset_top - inset.inset_bottom).max(0.0);
                    let radius = match inset.border_radius {
                        azul_css::corety::OptionF32::Some(r) => r,
                        azul_css::corety::OptionF32::None => 0.0,
                    };
                    Some((LogicalRect {
                        origin: LogicalPosition::new(x, y),
                        size: LogicalSize::new(w, h),
                    }, radius))
                }
                CssShape::Circle(circle) => {
                    // Approximate circle as a square bounding box centered at the circle center.
                    // CSS circle(radius at cx cy). The center point coordinates are in
                    // absolute units (pre-resolved by the CSS parser).
                    let cx = node_bounds.origin.x + circle.center.x;
                    let cy = node_bounds.origin.y + circle.center.y;
                    let r = circle.radius;
                    Some((LogicalRect {
                        origin: LogicalPosition::new(cx - r, cy - r),
                        size: LogicalSize::new(r * 2.0, r * 2.0),
                    }, r))
                }
                CssShape::Ellipse(ellipse) => {
                    // Approximate ellipse as its bounding box.
                    let cx = node_bounds.origin.x + ellipse.center.x;
                    let cy = node_bounds.origin.y + ellipse.center.y;
                    let rx = ellipse.radius_x;
                    let ry = ellipse.radius_y;
                    let radius = rx.min(ry);
                    Some((LogicalRect {
                        origin: LogicalPosition::new(cx - rx, cy - ry),
                        size: LogicalSize::new(rx * 2.0, ry * 2.0),
                    }, radius))
                }
                CssShape::Polygon(polygon) => {
                    // Compute the axis-aligned bounding box of the polygon.
                    if polygon.points.is_empty() {
                        return None;
                    }
                    let mut min_x = f32::INFINITY;
                    let mut min_y = f32::INFINITY;
                    let mut max_x = f32::NEG_INFINITY;
                    let mut max_y = f32::NEG_INFINITY;
                    for point in polygon.points.iter() {
                        // Polygon points are in absolute coordinates (pre-resolved)
                        let px = node_bounds.origin.x + point.x;
                        let py = node_bounds.origin.y + point.y;
                        min_x = min_x.min(px);
                        min_y = min_y.min(py);
                        max_x = max_x.max(px);
                        max_y = max_y.max(py);
                    }
                    Some((LogicalRect {
                        origin: LogicalPosition::new(min_x, min_y),
                        size: LogicalSize::new((max_x - min_x).max(0.0), (max_y - min_y).max(0.0)),
                    }, 0.0))
                }
                CssShape::Path(_) => {
                    // SVG paths are not supported for clip-path yet.
                    // Return the full node bounds (no clipping).
                    None
                }
            }
        }
    }
}
/// Applies a CSS clip-path to the display list by inserting PushClip/PopClip.
///
/// This is a post-processing step that wraps all items between `start_index`
/// and the current end of the display list in a clip region derived from
/// the clip-path shape.
///
/// # Parameters
/// - `display_list`: The display list to modify
/// - `start_index`: The index of the first item belonging to this node
/// - `clip_rect`: The resolved clip rectangle
/// - `border_radius`: The border radius for the clip (from inset round, or circle)
pub fn apply_clip_path(
    display_list: &mut DisplayList,
    start_index: usize,
    clip_rect: LogicalRect,
    border_radius: f32,
) {
    let br = if border_radius > 0.0 {
        BorderRadius {
            top_left: border_radius,
            top_right: border_radius,
            bottom_left: border_radius,
            bottom_right: border_radius,
        }
    } else {
        BorderRadius::default()
    };
    // Insert PushClip at start_index
    display_list.items.insert(start_index, DisplayListItem::PushClip {
        bounds: clip_rect.into(),
        border_radius: br,
    });
    // Insert a corresponding None in node_mapping
    if display_list.node_mapping.len() >= start_index {
        display_list.node_mapping.insert(start_index, None);
    }
    // Append PopClip at the end
    display_list.items.push(DisplayListItem::PopClip);
    display_list.node_mapping.push(None);
}
/// Rasterize an `SvgMultiPolygon` clip path into an R8 image mask at the given paint rect size.
///
/// Returns `None` if the rect has zero size.
#[cfg(feature = "cpurender")]
175
fn rasterize_svg_clip_to_r8(
175
    svg_clip: &azul_core::svg::SvgMultiPolygon,
175
    paint_rect: &LogicalRect,
175
) -> Option<azul_core::resources::ImageRef> {
    use agg_rust::{
        basics::FillingRule,
        color::Rgba8,
        path_storage::PathStorage,
        pixfmt_rgba::PixfmtRgba32,
        rasterizer_scanline_aa::RasterizerScanlineAa,
        renderer_base::RendererBase,
        renderer_scanline::render_scanlines_aa_solid,
        rendering_buffer::RowAccessor,
        scanline_u::ScanlineU8,
    };
    use azul_core::resources::{ImageRef, RawImage, RawImageFormat, RawImageData};
175
    let w = paint_rect.size.width.ceil() as u32;
175
    let h = paint_rect.size.height.ceil() as u32;
175
    if w == 0 || h == 0 {
        return None;
175
    }
    // Build agg PathStorage from SvgMultiPolygon
175
    let mut path = PathStorage::new();
175
    for ring in svg_clip.rings.as_ref().iter() {
175
        let mut first = true;
1050
        for item in ring.items.as_ref().iter() {
1050
            match item {
770
                azul_core::svg::SvgPathElement::Line(l) => {
770
                    if first {
140
                        path.move_to(
140
                            (l.start.x - paint_rect.origin.x) as f64,
140
                            (l.start.y - paint_rect.origin.y) as f64,
140
                        );
140
                        first = false;
630
                    }
770
                    path.line_to(
770
                        (l.end.x - paint_rect.origin.x) as f64,
770
                        (l.end.y - paint_rect.origin.y) as f64,
                    );
                }
                azul_core::svg::SvgPathElement::QuadraticCurve(q) => {
                    if first {
                        path.move_to(
                            (q.start.x - paint_rect.origin.x) as f64,
                            (q.start.y - paint_rect.origin.y) as f64,
                        );
                        first = false;
                    }
                    path.curve3(
                        (q.ctrl.x - paint_rect.origin.x) as f64,
                        (q.ctrl.y - paint_rect.origin.y) as f64,
                        (q.end.x - paint_rect.origin.x) as f64,
                        (q.end.y - paint_rect.origin.y) as f64,
                    );
                }
280
                azul_core::svg::SvgPathElement::CubicCurve(c) => {
280
                    if first {
35
                        path.move_to(
35
                            (c.start.x - paint_rect.origin.x) as f64,
35
                            (c.start.y - paint_rect.origin.y) as f64,
35
                        );
35
                        first = false;
245
                    }
280
                    path.curve4(
280
                        (c.ctrl_1.x - paint_rect.origin.x) as f64,
280
                        (c.ctrl_1.y - paint_rect.origin.y) as f64,
280
                        (c.ctrl_2.x - paint_rect.origin.x) as f64,
280
                        (c.ctrl_2.y - paint_rect.origin.y) as f64,
280
                        (c.end.x - paint_rect.origin.x) as f64,
280
                        (c.end.y - paint_rect.origin.y) as f64,
                    );
                }
            }
        }
    }
    // Rasterize to RGBA32 buffer
175
    let mut rgba_buf = vec![0u8; (w * h * 4) as usize];
175
    {
175
        let stride = (w * 4) as i32;
175
        let mut ra = unsafe {
175
            RowAccessor::new_with_buf(rgba_buf.as_mut_ptr(), w, h, stride)
175
        };
175
        let pf = PixfmtRgba32::new(&mut ra);
175
        let mut rb = RendererBase::new(pf);
175

            
175
        let mut ras = RasterizerScanlineAa::new();
175
        ras.filling_rule(FillingRule::NonZero);
175
        ras.add_path(&mut path, 0);
175

            
175
        let mut sl = ScanlineU8::new();
175
        let white = Rgba8 { r: 255, g: 255, b: 255, a: 255 };
175
        render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, &white);
175
    }
    // Extract alpha channel as R8 mask
2100000
    let r8_data: Vec<u8> = rgba_buf.chunks_exact(4).map(|px| px[3]).collect();
175
    ImageRef::new_rawimage(RawImage {
175
        pixels: RawImageData::U8(r8_data.into()),
175
        width: w as usize,
175
        height: h as usize,
175
        premultiplied_alpha: false,
175
        data_format: RawImageFormat::R8,
175
        tag: Vec::new().into(),
175
    })
175
}