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 crate::solver3::layout_tree::LayoutNodeId;
17
use std::{collections::{BTreeMap, HashMap}, sync::Arc};
18

            
19
use azul_core::{
20
    dom::{DomId, FormattingContext, NodeId, NodeType, ScrollbarOrientation},
21
    geom::{LogicalPosition, LogicalRect, LogicalSize},
22
    gpu::GpuValueCache,
23
    hit_test::{CursorType, ScrollPosition, 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
    codegen::format::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
const APPROX_ASCENT_RATIO: f32 = 0.8;
80
const APPROX_UNDERLINE_THICKNESS_RATIO: f32 = 0.08;
81
const APPROX_UNDERLINE_OFFSET_RATIO: f32 = 0.12;
82
const APPROX_STRIKETHROUGH_OFFSET_RATIO: f32 = 0.3;
83
const APPROX_OVERLINE_OFFSET_RATIO: f32 = 0.85;
84
const APPROX_ELLIPSIS_WIDTH_RATIO: f32 = 0.6;
85
const DEFAULT_A4_WIDTH_PT: f32 = 595.0;
86
const DEFAULT_SHADOW_FONT_SIZE_PX: f32 = 16.0;
87

            
88
/// Border widths for all four sides.
89
///
90
/// Each field is optional to allow partial border specifications.
91
/// Used in [`DisplayListItem::Border`] to specify per-side border widths.
92
#[derive(Debug, Clone, Copy)]
93
pub struct StyleBorderWidths {
94
    /// Top border width (CSS `border-top-width`)
95
    pub top: Option<CssPropertyValue<LayoutBorderTopWidth>>,
96
    /// Right border width (CSS `border-right-width`)
97
    pub right: Option<CssPropertyValue<LayoutBorderRightWidth>>,
98
    /// Bottom border width (CSS `border-bottom-width`)
99
    pub bottom: Option<CssPropertyValue<LayoutBorderBottomWidth>>,
100
    /// Left border width (CSS `border-left-width`)
101
    pub left: Option<CssPropertyValue<LayoutBorderLeftWidth>>,
102
}
103

            
104
/// Border colors for all four sides.
105
///
106
/// Each field is optional to allow partial border specifications.
107
/// Used in [`DisplayListItem::Border`] to specify per-side border colors.
108
#[derive(Debug, Clone, Copy)]
109
pub struct StyleBorderColors {
110
    /// Top border color (CSS `border-top-color`)
111
    pub top: Option<CssPropertyValue<StyleBorderTopColor>>,
112
    /// Right border color (CSS `border-right-color`)
113
    pub right: Option<CssPropertyValue<StyleBorderRightColor>>,
114
    /// Bottom border color (CSS `border-bottom-color`)
115
    pub bottom: Option<CssPropertyValue<StyleBorderBottomColor>>,
116
    /// Left border color (CSS `border-left-color`)
117
    pub left: Option<CssPropertyValue<StyleBorderLeftColor>>,
118
}
119

            
120
/// Border styles for all four sides.
121
///
122
/// Each field is optional to allow partial border specifications.
123
/// Used in [`DisplayListItem::Border`] to specify per-side border styles
124
/// (solid, dashed, dotted, none, etc.).
125
#[derive(Debug, Clone, Copy)]
126
pub struct StyleBorderStyles {
127
    /// Top border style (CSS `border-top-style`)
128
    pub top: Option<CssPropertyValue<StyleBorderTopStyle>>,
129
    /// Right border style (CSS `border-right-style`)
130
    pub right: Option<CssPropertyValue<StyleBorderRightStyle>>,
131
    /// Bottom border style (CSS `border-bottom-style`)
132
    pub bottom: Option<CssPropertyValue<StyleBorderBottomStyle>>,
133
    /// Left border style (CSS `border-left-style`)
134
    pub left: Option<CssPropertyValue<StyleBorderLeftStyle>>,
135
}
136

            
137
/// A rectangle in border-box coordinates (includes padding and border).
138
/// This is what layout calculates and stores in `used_size` and absolute positions.
139
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140
pub struct BorderBoxRect(pub LogicalRect);
141

            
142
/// A `LogicalRect` known to be in **absolute window coordinates** (as output
143
/// by the layout engine).
144
///
145
/// All spatial bounds stored in [`DisplayListItem`] use
146
/// this type so that the compositor is *forced* to convert them to
147
/// frame-relative coordinates before passing them to `WebRender`.
148
///
149
/// ## Coordinate-space contract
150
///
151
/// * **Layout engine** produces `WindowLogicalRect` values.
152
/// * **Compositor** converts via `resolve_rect()` → `WebRender` `LayoutRect`.
153
/// * Passing a `WindowLogicalRect` directly to a `WebRender` push function is a
154
///   **type error** (it wraps `LogicalRect`, not `LayoutRect`).
155
///
156
/// See `doc/SCROLL_COORDINATE_ARCHITECTURE.md` for background.
157
#[derive(Debug, Copy, Clone, Default, PartialEq, PartialOrd, Eq, Ord, Hash)]
158
pub struct WindowLogicalRect(pub LogicalRect);
159

            
160
/// Anything at or below this is the unassigned-position sentinel, not geometry.
161
///
162
/// The sentinel is exactly `f32::MIN`; the threshold is deliberately a little
163
/// looser so a sentinel that has been through one offset addition (which barely
164
/// moves a value of that magnitude) is still recognised.
165
const UNASSIGNED_POSITION_LIMIT: f32 = f32::MIN / 2.0;
166

            
167

            
168
impl WindowLogicalRect {
169
    #[inline]
170
50
    #[must_use] pub const fn new(origin: LogicalPosition, size: LogicalSize) -> Self {
171
50
        Self(LogicalRect::new(origin, size))
172
50
    }
173

            
174
    #[inline]
175
8
    #[must_use] pub const fn zero() -> Self {
176
8
        Self(LogicalRect::zero())
177
8
    }
178

            
179
    /// Access the inner `LogicalRect` (still in window space – the caller is
180
    /// responsible for applying any offset conversion).
181
    #[inline]
182
2373610
    #[must_use] pub const fn inner(&self) -> &LogicalRect {
183
2373610
        &self.0
184
2373610
    }
185

            
186
    #[inline]
187
29581
    #[must_use] pub const fn into_inner(self) -> LogicalRect {
188
29581
        self.0
189
29581
    }
190

            
191
    // Convenience accessors
192
118
    #[inline] #[must_use] pub const fn origin(&self) -> LogicalPosition { self.0.origin }
193
138
    #[inline] #[must_use] pub const fn size(&self)   -> LogicalSize     { self.0.size }
194
}
195

            
196
impl From<LogicalRect> for WindowLogicalRect {
197
    #[inline]
198
2233153
    fn from(r: LogicalRect) -> Self { Self(r) }
199
}
200

            
201
impl From<WindowLogicalRect> for LogicalRect {
202
    #[inline]
203
1
    fn from(w: WindowLogicalRect) -> Self { w.0 }
204
}
205

            
206
/// Simple struct for passing element dimensions to border-radius calculation
207
#[derive(Debug, Clone, Copy)]
208
pub struct PhysicalSizeImport {
209
    pub width: f32,
210
    pub height: f32,
211
}
212

            
213
/// Complete drawing information for a scrollbar with all visual components.
214
///
215
/// This contains the resolved geometry and colors for all scrollbar parts:
216
/// - Track: The background area where the thumb slides
217
/// - Thumb: The draggable indicator showing current scroll position
218
/// - Buttons: Optional up/down or left/right arrow buttons
219
/// - Corner: The area where horizontal and vertical scrollbars meet
220
#[derive(Debug, Clone, PartialEq)]
221
pub struct ScrollbarDrawInfo {
222
    /// Overall bounds of the entire scrollbar (including track and buttons)
223
    pub bounds: WindowLogicalRect,
224
    /// Scrollbar orientation (horizontal or vertical)
225
    pub orientation: ScrollbarOrientation,
226

            
227
    // Track area (the background rail)
228
    /// Bounds of the track area
229
    pub track_bounds: WindowLogicalRect,
230
    /// Color of the track background
231
    pub track_color: ColorU,
232

            
233
    // Thumb (the draggable part)
234
    /// Bounds of the thumb
235
    pub thumb_bounds: WindowLogicalRect,
236
    /// Color of the thumb
237
    pub thumb_color: ColorU,
238
    /// Border radius for rounded thumb corners
239
    pub thumb_border_radius: BorderRadius,
240

            
241
    // Optional buttons (arrows at ends)
242
    /// Optional decrement button bounds (up/left arrow)
243
    pub button_decrement_bounds: Option<WindowLogicalRect>,
244
    /// Optional increment button bounds (down/right arrow)
245
    pub button_increment_bounds: Option<WindowLogicalRect>,
246
    /// Color for buttons
247
    pub button_color: ColorU,
248

            
249
    /// Optional opacity key for GPU-side fading animation.
250
    pub opacity_key: Option<OpacityKey>,
251
    /// Optional transform key for GPU-side scrollbar thumb positioning.
252
    /// When present, the compositor will wrap the thumb in a `PushReferenceFrame`
253
    /// with `PropertyBinding::Binding` so `WebRender` can animate the thumb position
254
    /// without rebuilding the display list.
255
    pub thumb_transform_key: Option<TransformKey>,
256
    /// Initial transform for the scrollbar thumb (current scroll position).
257
    /// This is the transform applied when the display list is first built.
258
    /// During GPU-only scroll, `synchronize_gpu_values` updates this dynamically.
259
    pub thumb_initial_transform: ComputedTransform3D,
260
    /// Optional hit-test ID for `WebRender` hit-testing.
261
    pub hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
262
    /// Whether to clip scrollbar to container's border-radius
263
    pub clip_to_container_border: bool,
264
    /// Container's border-radius (for clipping)
265
    pub container_border_radius: BorderRadius,
266
    /// Scrollbar visibility mode — used by back-registration to choose initial opacity.
267
    /// `Always` → initial opacity 1.0; `WhenScrolling` → initial opacity 0.0.
268
    pub visibility: azul_css::props::style::scrollbar::ScrollbarVisibilityMode,
269
}
270

            
271
impl BorderBoxRect {
272
    /// Convert border-box to content-box by subtracting padding and border.
273
    /// Content-box is where inline layout and text actually render.
274
286096
    #[must_use] pub fn to_content_box(
275
286096
        self,
276
286096
        padding: &crate::solver3::geometry::EdgeSizes,
277
286096
        border: &crate::solver3::geometry::EdgeSizes,
278
286096
    ) -> ContentBoxRect {
279
286096
        ContentBoxRect(LogicalRect {
280
286096
            origin: LogicalPosition {
281
286096
                x: self.0.origin.x + padding.left + border.left,
282
286096
                y: self.0.origin.y + padding.top + border.top,
283
286096
            },
284
286096
            size: LogicalSize {
285
286096
                width: self.0.size.width
286
286096
                    - padding.left
287
286096
                    - padding.right
288
286096
                    - border.left
289
286096
                    - border.right,
290
286096
                height: self.0.size.height
291
286096
                    - padding.top
292
286096
                    - padding.bottom
293
286096
                    - border.top
294
286096
                    - border.bottom,
295
286096
            },
296
286096
        })
297
286096
    }
298

            
299
    /// Get the inner `LogicalRect`
300
2
    #[must_use] pub const fn rect(&self) -> LogicalRect {
301
2
        self.0
302
2
    }
303
}
304

            
305
/// A rectangle in content-box coordinates (excludes padding and border).
306
/// This is where text and inline content is positioned by the inline formatter.
307
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
308
pub struct ContentBoxRect(pub LogicalRect);
309

            
310
impl ContentBoxRect {
311
    /// Get the inner `LogicalRect`
312
286099
    #[must_use] pub const fn rect(&self) -> LogicalRect {
313
286099
        self.0
314
286099
    }
315
}
316

            
317
/// The final, renderer-agnostic output of the layout engine.
318
///
319
/// This is a flat list of drawing and state-management commands, already sorted
320
/// according to the CSS paint order. A renderer can consume this list directly.
321
/// A CSS-forced page break recorded while building the display list.
322
#[derive(Debug, Clone, Copy, PartialEq)]
323
pub struct ForcedBreak {
324
    /// Absolute document-space Y of the break.
325
    pub y: f32,
326
    /// The node whose `break-before`/`break-after` property forced it.
327
    /// `None` only for synthetic inputs (tests, hand-built lists).
328
    pub causing_node: Option<NodeId>,
329
}
330

            
331
#[derive(Debug, Default, Clone)]
332
pub struct DisplayList {
333
    pub items: Vec<DisplayListItem>,
334
    /// Optional mapping from item index to the DOM `NodeId` that generated it.
335
    /// Used for pagination to look up CSS break properties.
336
    /// Not all items have a source node (e.g., synthesized decorations).
337
    pub node_mapping: Vec<Option<NodeId>>,
338
    /// Forced page breaks (from break-before/break-after: always), with the
339
    /// node whose break property caused each one. Y coordinates are absolute
340
    /// in the infinite canvas coordinate system; the slicer aligns page
341
    /// boundaries with these positions.
342
    pub forced_page_breaks: Vec<ForcedBreak>,
343
    /// Index ranges (start, end) of display list items that belong to fixed-position elements.
344
    /// In paged media, these items are replicated on every page (CSS Positioned Layout §2.1).
345
    pub fixed_position_item_ranges: Vec<(usize, usize)>,
346
    /// Per-item PROVEN uniform background (Text items only, None elsewhere).
347
    /// Parallel to `items` like `node_mapping`. Lives on the LIST rather
348
    /// than in the `Text` variant because printpdf (crates.io, immutable)
349
    /// pattern-matches the variant exhaustively — a new variant field is a
350
    /// downstream semver break; a new list field is not. See
351
    /// `compute_uniform_text_bg` for what "proven" means and the raster's
352
    /// pre-blended tile path for what it buys (~6 ms/repaint of per-pixel
353
    /// linear LCD compositing).
354
    pub uniform_text_bgs: Vec<Option<(ColorU, WindowLogicalRect)>>,
355
    /// Per-item (layout-tree index, emission phase) — the DL-PATCHING key.
356
    /// `node_mapping` attributes items to DOM ids, but patching substitutes
357
    /// per LAYOUT-NODE PAINT CALL: on a resize-skip pass the tree object and
358
    /// its indices are unchanged, so a node's cached items can replace a
359
    /// paint call — IF the run for the bg/border call can be told apart from
360
    /// the run for the content call and from items the SC walk itself pushes
361
    /// (scroll-frame hit areas, clips, scrollbars — those always re-emit
362
    /// fresh). Parallel to `items`, like `node_mapping`.
363
    pub layout_node_mapping: Vec<Option<(usize, EmitPhase)>>,
364
}
365

            
366
impl DisplayList {
367
    /// Approximate heap bytes retained by this display list — the
368
    /// item vec + parallel vecs + every item's owned heap. The memory
369
    /// report used to charge a FLAT 2 KiB for a cached display list;
370
    /// on a paginated document the `Text` items alone hold an offset
371
    /// COPY of every painted glyph (20 B each), which made the single
372
    /// biggest DL cost invisible ("a zero is not a measurement" — and
373
    /// so is a constant).
374
    ///
375
    /// Returns `(bytes, text_instances, item_count)` — the counters that
376
    /// explain the byte figure (glyph copies vs item slots), same pattern
377
    /// as the cluster counters on the tree report.
378
    #[must_use]
379
2
    pub fn retained_bytes(&self) -> (usize, usize, usize) {
380
        use core::mem::size_of;
381
2
        let mut bytes = self.items.capacity() * size_of::<DisplayListItem>()
382
2
            + self.node_mapping.capacity() * size_of::<Option<NodeId>>()
383
2
            + self.forced_page_breaks.capacity() * size_of::<ForcedBreak>()
384
2
            + self.fixed_position_item_ranges.capacity() * size_of::<(usize, usize)>()
385
2
            + self
386
2
                .uniform_text_bgs
387
2
                .capacity()
388
2
                * size_of::<Option<(ColorU, WindowLogicalRect)>>()
389
2
            + self
390
2
                .layout_node_mapping
391
2
                .capacity()
392
2
                * size_of::<Option<(usize, EmitPhase)>>();
393
2
        let mut text_instances = 0usize;
394
4
        for item in &self.items {
395
2
            bytes += Self::item_heap_bytes(item, &mut text_instances);
396
2
        }
397
2
        (bytes, text_instances, self.items.len())
398
2
    }
399

            
400
    /// Owned heap hanging off one item. EXHAUSTIVE on purpose — a new
401
    /// variant must fail this match so its heap gets classified rather
402
    /// than silently uncounted. `Arc`/`ImageRef` fields are CLONES of
403
    /// allocations owned (and counted) elsewhere → 0 here.
404
2
    const fn item_heap_bytes(item: &DisplayListItem, text_instances: &mut usize) -> usize {
405
        use core::mem::size_of;
406
        use DisplayListItem as I;
407
2
        match item {
408
2
            I::Text { glyphs, .. } => {
409
2
                *text_instances += glyphs.len();
410
2
                glyphs.capacity() * size_of::<GlyphInstance>()
411
            }
412
            I::ScrollBarStyled { .. } => size_of::<ScrollbarDrawInfo>(),
413
            I::LinearGradient { gradient, .. } => {
414
                gradient.stops.len()
415
                    * size_of::<azul_css::props::style::NormalizedLinearColorStop>()
416
            }
417
            I::RadialGradient { gradient, .. } => {
418
                gradient.stops.len()
419
                    * size_of::<azul_css::props::style::NormalizedLinearColorStop>()
420
            }
421
            I::ConicGradient { gradient, .. } => {
422
                gradient.stops.len()
423
                    * size_of::<azul_css::props::style::NormalizedRadialColorStop>()
424
            }
425
            I::PushFilter { filters, .. } | I::PushBackdropFilter { filters, .. } => {
426
                filters.capacity() * size_of::<StyleFilter>()
427
            }
428
            // Arc / ImageRef clones — owned and counted elsewhere.
429
            I::TextLayout { .. } | I::Image { .. } | I::PushImageMaskClip { .. } => 0,
430
            // Flat payloads: everything lives inline in the enum variant,
431
            // already covered by `items.capacity() * size_of::<DisplayListItem>()`.
432
            I::Rect { .. }
433
            | I::SelectionRect { .. }
434
            | I::CursorRect { .. }
435
            | I::Border { .. }
436
            | I::Underline { .. }
437
            | I::Strikethrough { .. }
438
            | I::Overline { .. }
439
            | I::ScrollBar { .. }
440
            | I::VirtualView { .. }
441
            | I::VirtualViewPlaceholder { .. }
442
            | I::PushClip { .. }
443
            | I::PopClip
444
            | I::PopImageMaskClip
445
            | I::PushScrollFrame { .. }
446
            | I::PopScrollFrame
447
            | I::PushStackingContext { .. }
448
            | I::PopStackingContext
449
            | I::PushReferenceFrame { .. }
450
            | I::PopReferenceFrame
451
            | I::HitTestArea { .. }
452
            | I::BoxShadow { .. }
453
            | I::PopFilter
454
            | I::PopBackdropFilter
455
            | I::PushOpacity { .. }
456
            | I::PopOpacity
457
            | I::PushTextShadow { .. }
458
            | I::PopTextShadow => 0,
459
        }
460
2
    }
461
}
462

            
463
/// Translate a display-list item's geometry by `delta` (DL patching:
464
/// same item content, moved node). Every variant the two paint fns can
465
/// emit is handled; variants outside `patchable_item` never reach this
466
/// (`PatchState::build` forces their node into the re-emit set).
467
#[must_use]
468
382
pub(crate) fn translate_item(mut item: DisplayListItem, delta: LogicalPosition) -> DisplayListItem {
469
    use DisplayListItem as I;
470
382
    if delta.x == 0.0 && delta.y == 0.0 {
471
378
        return item;
472
4
    }
473
4
    let shift = |b: &mut WindowLogicalRect| {
474
4
        b.0.origin.x += delta.x;
475
4
        b.0.origin.y += delta.y;
476
4
    };
477
4
    match &mut item {
478
        I::Rect { bounds, .. }
479
        | I::SelectionRect { bounds, .. }
480
        | I::CursorRect { bounds, .. }
481
1
        | I::Border { bounds, .. }
482
        | I::TextLayout { bounds, .. }
483
        | I::Underline { bounds, .. }
484
        | I::Strikethrough { bounds, .. }
485
        | I::Overline { bounds, .. }
486
        | I::Image { bounds, .. }
487
        | I::LinearGradient { bounds, .. }
488
        | I::RadialGradient { bounds, .. }
489
        | I::ConicGradient { bounds, .. }
490
        | I::BoxShadow { bounds, .. }
491
3
        | I::HitTestArea { bounds, .. } => shift(bounds),
492
1
        I::Text { glyphs, clip_rect, .. } => {
493
40
            for g in glyphs.iter_mut() {
494
40
                g.point.x += delta.x;
495
40
                g.point.y += delta.y;
496
40
            }
497
1
            shift(clip_rect);
498
        }
499
        // No geometry of their own.
500
        I::PushTextShadow { .. } | I::PopTextShadow => {}
501
        // Anything else is untranslatable by contract (see patchable_item).
502
        _ => {}
503
    }
504
4
    item
505
382
}
506

            
507
/// Whether an item may appear in a PATCHED (copied+translated) run. The
508
/// build step forces nodes emitting anything else into the re-emit set —
509
/// scroll frames, clips, stacking contexts, virtual views, filters and
510
/// scrollbars all carry state beyond a rect and must re-emit fresh.
511
#[must_use]
512
1467
pub(crate) const fn patchable_item(item: &DisplayListItem) -> bool {
513
    use DisplayListItem as I;
514
    matches!(
515
1467
        item,
516
        I::Rect { .. }
517
            | I::SelectionRect { .. }
518
            | I::CursorRect { .. }
519
            | I::Border { .. }
520
            | I::TextLayout { .. }
521
            | I::Text { .. }
522
            | I::Underline { .. }
523
            | I::Strikethrough { .. }
524
            | I::Overline { .. }
525
            | I::Image { .. }
526
            | I::LinearGradient { .. }
527
            | I::RadialGradient { .. }
528
            | I::ConicGradient { .. }
529
            | I::BoxShadow { .. }
530
            | I::HitTestArea { .. }
531
            | I::PushTextShadow { .. }
532
            | I::PopTextShadow
533
    )
534
1467
}
535

            
536
/// Runtime toggle for DL patching. 0 = uninitialized (resolve from
537
/// `AZ_NO_DL_PATCH` on first read), 1 = enabled, 2 = disabled. A runtime
538
/// atomic rather than a latched `OnceLock` so tests can A/B the patched and
539
/// full paths in one process (the golden-equality gate), and so a live
540
/// session can be flipped for diagnosis.
541
static DL_PATCHING: core::sync::atomic::AtomicU8 = core::sync::atomic::AtomicU8::new(0);
542

            
543
#[must_use]
544
4711
pub fn dl_patching_enabled() -> bool {
545
    use core::sync::atomic::Ordering;
546
4711
    match DL_PATCHING.load(Ordering::Relaxed) {
547
4610
        1 => true,
548
72
        2 => false,
549
        _ => {
550
29
            let on = std::env::var_os("AZ_NO_DL_PATCH").is_none();
551
29
            DL_PATCHING.store(if on { 1 } else { 2 }, Ordering::Relaxed);
552
29
            on
553
        }
554
    }
555
4711
}
556

            
557
/// Force DL patching on/off (tests + live diagnosis; overrides the env).
558
27
pub fn set_dl_patching_enabled(on: bool) {
559
    use core::sync::atomic::Ordering;
560
27
    DL_PATCHING.store(if on { 1 } else { 2 }, Ordering::Relaxed);
561
27
}
562

            
563
/// Which emission site produced a display-list item. See
564
/// [`DisplayList::layout_node_mapping`].
565
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
566
pub enum EmitPhase {
567
    /// Pushed by the stacking-context walk itself (clips, scroll frames,
568
    /// scrollable hit areas, scrollbars) — never substituted from cache.
569
    ScWalk,
570
    /// Pushed by `paint_node_background_and_border`.
571
    BgBorder,
572
    /// Pushed by `paint_node_content`.
573
    Content,
574
}
575

            
576
impl DisplayList {
577
    /// THE display-list ↔ DOM identity invariant, checkable after every
578
    /// build or patch.
579
    ///
580
    /// `node_mapping` is how thirty-plus consumers (damage attribution,
581
    /// pagination break lookup, hit-testing, the coming display-list PATCHING)
582
    /// resolve an item back to the DOM. A stale or out-of-range id does not
583
    /// fail — it succeeds at describing the WRONG node, which is the silent
584
    /// corruption class `assert_dom_ids_are_in_range` documents on the tree
585
    /// side (a ribbon tab click once repainted rects of unrelated nodes; it
586
    /// was only caught because an index happened to trip a bounds check).
587
    /// This is the missing display-list-side counterpart, and the hard gate
588
    /// the DL-patching work patches AGAINST: every patch must leave this
589
    /// invariant intact.
590
    ///
591
    /// Checks: (1) `node_mapping` covers `items` 1:1; (2) every mapped id
592
    /// indexes into `styled_dom`; (3) every item's bounds, where it has any,
593
    /// is finite (a NaN origin poisons every damage union it touches).
594
    /// Deliberately NOT checked: bounds-within-node-rect — decorations
595
    /// (shadows, outlines, selection) legitimately paint outside their node.
596
    ///
597
    /// # Errors
598
    /// Returns a message naming the first offending item index.
599
45
    pub fn validate_node_mapping(
600
45
        &self,
601
45
        styled_dom: &StyledDom,
602
45
    ) -> core::result::Result<(), String> {
603
45
        if self.node_mapping.len() != self.items.len() {
604
9
            return Err(format!(
605
9
                "node_mapping covers {} items but the list has {}",
606
9
                self.node_mapping.len(),
607
9
                self.items.len()
608
9
            ));
609
36
        }
610
36
        let dom_len = styled_dom.node_data.as_ref().len();
611
963
        for (i, (item, mapping)) in self.items.iter().zip(self.node_mapping.iter()).enumerate() {
612
963
            if let Some(node_id) = mapping {
613
900
                if node_id.index() >= dom_len {
614
9
                    return Err(format!(
615
9
                        "item {i} maps to NodeId {} but the DOM has {dom_len} nodes",
616
9
                        node_id.index()
617
9
                    ));
618
891
                }
619
63
            }
620
954
            if let Some(b) = item.bounds() {
621
927
                if !b.origin.x.is_finite()
622
927
                    || !b.origin.y.is_finite()
623
927
                    || !b.size.width.is_finite()
624
927
                    || !b.size.height.is_finite()
625
                {
626
                    return Err(format!("item {i} has non-finite bounds {b:?}"));
627
927
                }
628
27
            }
629
        }
630
27
        Ok(())
631
45
    }
632

            
633
    /// Patch text glyph data for a specific layout node without rebuilding
634
    /// the entire display list. Returns the damage rect covering all
635
    /// affected text items, or None if no matching items found.
636
    ///
637
    /// Used for `GlyphSwap` incremental relayout: glyphs changed but
638
    /// positions are identical, so only the glyph IDs need updating.
639
7
    pub(crate) fn patch_text_glyphs(
640
7
        &mut self,
641
7
        node_index: usize,
642
7
        new_glyphs_by_run: &[Vec<GlyphInstance>],
643
7
    ) -> Option<LogicalRect> {
644
7
        let mut run_idx = 0;
645
7
        let mut damage: Option<LogicalRect> = None;
646

            
647
23
        for item in &mut self.items {
648
            if let DisplayListItem::Text {
649
10
                ref mut glyphs,
650
10
                ref clip_rect,
651
10
                source_node_index: Some(src_idx),
652
                ..
653
13
            } = item {
654
10
                if *src_idx == node_index
655
8
                    && run_idx < new_glyphs_by_run.len() {
656
5
                        glyphs.clone_from(&new_glyphs_by_run[run_idx]);
657
5
                        let bounds = *clip_rect.inner();
658
5
                        damage = Some(damage.map_or(bounds, |d| {
659
                                // rect union (was crate::cpurender::union_rect, which
660
                                // is gated behind the `cpurender` feature; inlined here
661
                                // so display-list damage tracking works without it / on WASM)
662
1
                                let x = d.origin.x.min(bounds.origin.x);
663
1
                                let y = d.origin.y.min(bounds.origin.y);
664
1
                                let right = (d.origin.x + d.size.width)
665
1
                                    .max(bounds.origin.x + bounds.size.width);
666
1
                                let bottom = (d.origin.y + d.size.height)
667
1
                                    .max(bounds.origin.y + bounds.size.height);
668
1
                                LogicalRect {
669
1
                                    origin: LogicalPosition { x, y },
670
1
                                    size: LogicalSize { width: right - x, height: bottom - y },
671
1
                                }
672
1
                            }));
673
5
                        run_idx += 1;
674
5
                    }
675
6
            }
676
        }
677

            
678
7
        damage
679
7
    }
680

            
681
    /// Rewrite the `ImageRef` of every `Image` item generated by `node`
682
    /// without rebuilding the display list, returning the union of the
683
    /// patched bounds (None if the node produced no image items).
684
    ///
685
    /// The single caller is the content chokepoint
686
    /// (`LayoutWindow::apply_content_change`, paint tier): geometry is
687
    /// unchanged, only the content identity moves — the backend display-list
688
    /// diff sees the `ImageRef` id change and damages exactly these bounds.
689
    /// Patch PAINT COLOURS in place for a node's SUBTREE — the per-tick fast
690
    /// path of a paint-only `animation` transition. Items attributed (via
691
    /// `node_mapping`) to a node id in `range` whose colour equals `from`
692
    /// take `to`; the from-match is what implements inheritance without a
693
    /// cascade pass (a descendant with its OWN explicit colour differs from
694
    /// `from` and is untouched). Returns the damage union, `None` when
695
    /// nothing matched (the caller falls back to a full rebuild).
696
58
    pub fn patch_paint_colors(
697
58
        &mut self,
698
58
        range: core::ops::Range<usize>,
699
58
        from: ColorU,
700
58
        to: ColorU,
701
58
        patch_text: bool,
702
58
        patch_background: bool,
703
58
    ) -> Option<LogicalRect> {
704
58
        let mut damage: Option<LogicalRect> = None;
705
58
        let mut grow = |bounds: LogicalRect| {
706
58
            damage = Some(damage.map_or(bounds, |d| {
707
                let x = d.origin.x.min(bounds.origin.x);
708
                let y = d.origin.y.min(bounds.origin.y);
709
                let right = (d.origin.x + d.size.width).max(bounds.origin.x + bounds.size.width);
710
                let bottom = (d.origin.y + d.size.height).max(bounds.origin.y + bounds.size.height);
711
                LogicalRect {
712
                    origin: LogicalPosition { x, y },
713
                    size: LogicalSize {
714
                        width: right - x,
715
                        height: bottom - y,
716
                    },
717
                }
718
            }));
719
58
        };
720
1218
        for (idx, item) in self.items.iter_mut().enumerate() {
721
1218
            let Some(node) = self.node_mapping.get(idx).copied().flatten() else {
722
116
                continue;
723
            };
724
1102
            if !range.contains(&node.index()) {
725
870
                continue;
726
232
            }
727
            match item {
728
                DisplayListItem::Text {
729
58
                    color, clip_rect, ..
730
58
                } if patch_text && *color == from => {
731
58
                    *color = to;
732
58
                    grow(*clip_rect.inner());
733
58
                }
734
                DisplayListItem::Underline { color, bounds, .. }
735
                | DisplayListItem::Strikethrough { color, bounds, .. }
736
                    if patch_text && *color == from =>
737
                {
738
                    *color = to;
739
                    grow(*bounds.inner());
740
                }
741
                DisplayListItem::Rect { color, bounds, .. }
742
                    if patch_background && *color == from =>
743
                {
744
                    *color = to;
745
                    grow(*bounds.inner());
746
                }
747
174
                _ => {}
748
            }
749
        }
750
58
        damage
751
58
    }
752

            
753
1
    pub fn patch_node_image(&mut self, node: NodeId, image: &ImageRef) -> Option<LogicalRect> {
754
1
        let mut damage: Option<LogicalRect> = None;
755

            
756
13
        for (idx, item) in self.items.iter_mut().enumerate() {
757
13
            if self.node_mapping.get(idx).copied().flatten() != Some(node) {
758
11
                continue;
759
2
            }
760
            if let DisplayListItem::Image {
761
1
                bounds,
762
1
                image: item_image,
763
                ..
764
2
            } = item
765
            {
766
1
                *item_image = image.clone();
767
1
                let bounds = *bounds.inner();
768
1
                damage = Some(damage.map_or(bounds, |d| {
769
                    let x = d.origin.x.min(bounds.origin.x);
770
                    let y = d.origin.y.min(bounds.origin.y);
771
                    let right =
772
                        (d.origin.x + d.size.width).max(bounds.origin.x + bounds.size.width);
773
                    let bottom =
774
                        (d.origin.y + d.size.height).max(bounds.origin.y + bounds.size.height);
775
                    LogicalRect {
776
                        origin: LogicalPosition { x, y },
777
                        size: LogicalSize {
778
                            width: right - x,
779
                            height: bottom - y,
780
                        },
781
                    }
782
                }));
783
1
            }
784
        }
785

            
786
1
        damage
787
1
    }
788

            
789
    /// Compute a damage rect from the difference between old and new text
790
    /// layout results, starting from a given line index.
791
    #[allow(clippy::similar_names)] // domain-standard coordinate/geometry/short-lived names
792
7
    pub(crate) fn compute_text_damage_rect(
793
7
        old_items: &[PositionedItem],
794
7
        new_items: &[PositionedItem],
795
7
        container_origin: LogicalPosition,
796
7
        affected_line: usize,
797
7
    ) -> LogicalRect {
798
14
        let expand = |items: &[PositionedItem]| -> (f32, f32, f32, f32) {
799
14
            let mut lx = f32::MAX;
800
14
            let mut ly = f32::MAX;
801
14
            let mut rx = f32::MIN;
802
14
            let mut ry = f32::MIN;
803
28
            for item in items {
804
14
                if item.line_index >= affected_line {
805
8
                    let bounds = item.item.bounds();
806
8
                    let x = container_origin.x + item.position.x;
807
8
                    let y = container_origin.y + item.position.y;
808
8
                    lx = lx.min(x);
809
8
                    ly = ly.min(y);
810
8
                    rx = rx.max(x + bounds.width);
811
8
                    ry = ry.max(y + bounds.height);
812
8
                }
813
            }
814
14
            (lx, ly, rx, ry)
815
14
        };
816

            
817
7
        let (olx, oly, orx, ory) = expand(old_items);
818
7
        let (nlx, nly, nrx, nry) = expand(new_items);
819
7
        let min_x = olx.min(nlx);
820
7
        let min_y = oly.min(nly);
821
7
        let max_x = orx.max(nrx);
822
7
        let max_y = ory.max(nry);
823

            
824
7
        if min_x > max_x || min_y > max_y {
825
3
            return LogicalRect::default();
826
4
        }
827

            
828
4
        LogicalRect {
829
4
            origin: LogicalPosition { x: min_x, y: min_y },
830
4
            size: LogicalSize { width: max_x - min_x, height: max_y - min_y },
831
4
        }
832
7
    }
833

            
834
    /// Generates a JSON representation of the display list for debugging.
835
    /// This includes clip chain analysis showing how clips are stacked.
836
    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
837
4
    pub(crate) fn to_debug_json(&self) -> String {
838
        use std::fmt::Write;
839
4
        let mut json = String::new();
840
4
        writeln!(json, "{{").unwrap();
841
4
        writeln!(json, "  \"total_items\": {},", self.items.len()).unwrap();
842
4
        writeln!(json, "  \"items\": [").unwrap();
843

            
844
4
        let mut clip_depth = 0i32;
845
4
        let mut scroll_depth = 0i32;
846
4
        let mut stacking_depth = 0i32;
847

            
848
9
        for (i, item) in self.items.iter().enumerate() {
849
9
            let comma = if i < self.items.len() - 1 { "," } else { "" };
850
9
            let node_id = self.node_mapping.get(i).and_then(|n| *n);
851

            
852
9
            match item {
853
                DisplayListItem::PushClip {
854
2
                    bounds,
855
2
                    border_radius,
856
2
                } => {
857
2
                    clip_depth += 1;
858
2
                    writeln!(json, "    {{").unwrap();
859
2
                    writeln!(json, "      \"index\": {i},").unwrap();
860
2
                    writeln!(json, "      \"type\": \"PushClip\",").unwrap();
861
2
                    writeln!(json, "      \"clip_depth\": {clip_depth},").unwrap();
862
2
                    writeln!(json, "      \"scroll_depth\": {scroll_depth},").unwrap();
863
2
                    writeln!(json, "      \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},", 
864
2
                        bounds.0.origin.x, bounds.0.origin.y, bounds.0.size.width, bounds.0.size.height).unwrap();
865
2
                    writeln!(json, "      \"border_radius\": {{ \"tl\": {:.1}, \"tr\": {:.1}, \"bl\": {:.1}, \"br\": {:.1} }},",
866
2
                        border_radius.top_left, border_radius.top_right,
867
2
                        border_radius.bottom_left, border_radius.bottom_right).unwrap();
868
2
                    writeln!(json, "      \"node_id\": {node_id:?}").unwrap();
869
2
                    writeln!(json, "    }}{comma}").unwrap();
870
2
                }
871
2
                DisplayListItem::PopClip => {
872
2
                    writeln!(json, "    {{").unwrap();
873
2
                    writeln!(json, "      \"index\": {i},").unwrap();
874
2
                    writeln!(json, "      \"type\": \"PopClip\",").unwrap();
875
2
                    writeln!(json, "      \"clip_depth_before\": {clip_depth},").unwrap();
876
2
                    writeln!(json, "      \"clip_depth_after\": {}", clip_depth - 1).unwrap();
877
2
                    writeln!(json, "    }}{comma}").unwrap();
878
2
                    clip_depth -= 1;
879
2
                }
880
                DisplayListItem::PushScrollFrame {
881
1
                    clip_bounds,
882
1
                    content_size,
883
1
                    scroll_id,
884
1
                } => {
885
1
                    scroll_depth += 1;
886
1
                    writeln!(json, "    {{").unwrap();
887
1
                    writeln!(json, "      \"index\": {i},").unwrap();
888
1
                    writeln!(json, "      \"type\": \"PushScrollFrame\",").unwrap();
889
1
                    writeln!(json, "      \"clip_depth\": {clip_depth},").unwrap();
890
1
                    writeln!(json, "      \"scroll_depth\": {scroll_depth},").unwrap();
891
1
                    writeln!(json, "      \"clip_bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},",
892
1
                        clip_bounds.0.origin.x, clip_bounds.0.origin.y,
893
1
                        clip_bounds.0.size.width, clip_bounds.0.size.height).unwrap();
894
1
                    writeln!(
895
1
                        json,
896
1
                        "      \"content_size\": {{ \"w\": {:.1}, \"h\": {:.1} }},",
897
1
                        content_size.width, content_size.height
898
1
                    )
899
1
                    .unwrap();
900
1
                    writeln!(json, "      \"scroll_id\": {scroll_id},").unwrap();
901
1
                    writeln!(json, "      \"node_id\": {node_id:?}").unwrap();
902
1
                    writeln!(json, "    }}{comma}").unwrap();
903
1
                }
904
1
                DisplayListItem::PopScrollFrame => {
905
1
                    writeln!(json, "    {{").unwrap();
906
1
                    writeln!(json, "      \"index\": {i},").unwrap();
907
1
                    writeln!(json, "      \"type\": \"PopScrollFrame\",").unwrap();
908
1
                    writeln!(json, "      \"scroll_depth_before\": {scroll_depth},").unwrap();
909
1
                    writeln!(json, "      \"scroll_depth_after\": {}", scroll_depth - 1).unwrap();
910
1
                    writeln!(json, "    }}{comma}").unwrap();
911
1
                    scroll_depth -= 1;
912
1
                }
913
1
                DisplayListItem::PushStackingContext { z_index, bounds } => {
914
1
                    stacking_depth += 1;
915
1
                    writeln!(json, "    {{").unwrap();
916
1
                    writeln!(json, "      \"index\": {i},").unwrap();
917
1
                    writeln!(json, "      \"type\": \"PushStackingContext\",").unwrap();
918
1
                    writeln!(json, "      \"stacking_depth\": {stacking_depth},").unwrap();
919
1
                    writeln!(json, "      \"z_index\": {z_index},").unwrap();
920
1
                    writeln!(json, "      \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }}",
921
1
                        bounds.0.origin.x, bounds.0.origin.y, bounds.0.size.width, bounds.0.size.height).unwrap();
922
1
                    writeln!(json, "    }}{comma}").unwrap();
923
1
                }
924
1
                DisplayListItem::PopStackingContext => {
925
1
                    writeln!(json, "    {{").unwrap();
926
1
                    writeln!(json, "      \"index\": {i},").unwrap();
927
1
                    writeln!(json, "      \"type\": \"PopStackingContext\",").unwrap();
928
1
                    writeln!(json, "      \"stacking_depth_before\": {stacking_depth},").unwrap();
929
1
                    writeln!(
930
1
                        json,
931
1
                        "      \"stacking_depth_after\": {}",
932
1
                        stacking_depth - 1
933
1
                    )
934
1
                    .unwrap();
935
1
                    writeln!(json, "    }}{comma}").unwrap();
936
1
                    stacking_depth -= 1;
937
1
                }
938
                DisplayListItem::Rect {
939
                    bounds,
940
                    color,
941
                    border_radius,
942
                } => {
943
                    writeln!(json, "    {{").unwrap();
944
                    writeln!(json, "      \"index\": {i},").unwrap();
945
                    writeln!(json, "      \"type\": \"Rect\",").unwrap();
946
                    writeln!(json, "      \"clip_depth\": {clip_depth},").unwrap();
947
                    writeln!(json, "      \"scroll_depth\": {scroll_depth},").unwrap();
948
                    writeln!(json, "      \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},",
949
                        bounds.0.origin.x, bounds.0.origin.y, bounds.0.size.width, bounds.0.size.height).unwrap();
950
                    writeln!(
951
                        json,
952
                        "      \"color\": \"rgba({},{},{},{})\",",
953
                        color.r, color.g, color.b, color.a
954
                    )
955
                    .unwrap();
956
                    writeln!(json, "      \"node_id\": {node_id:?}").unwrap();
957
                    writeln!(json, "    }}{comma}").unwrap();
958
                }
959
                DisplayListItem::Border { bounds, .. } => {
960
                    writeln!(json, "    {{").unwrap();
961
                    writeln!(json, "      \"index\": {i},").unwrap();
962
                    writeln!(json, "      \"type\": \"Border\",").unwrap();
963
                    writeln!(json, "      \"clip_depth\": {clip_depth},").unwrap();
964
                    writeln!(json, "      \"scroll_depth\": {scroll_depth},").unwrap();
965
                    writeln!(json, "      \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},",
966
                        bounds.0.origin.x, bounds.0.origin.y, bounds.0.size.width, bounds.0.size.height).unwrap();
967
                    writeln!(json, "      \"node_id\": {node_id:?}").unwrap();
968
                    writeln!(json, "    }}{comma}").unwrap();
969
                }
970
                DisplayListItem::ScrollBarStyled { info } => {
971
                    writeln!(json, "    {{").unwrap();
972
                    writeln!(json, "      \"index\": {i},").unwrap();
973
                    writeln!(json, "      \"type\": \"ScrollBarStyled\",").unwrap();
974
                    writeln!(json, "      \"clip_depth\": {clip_depth},").unwrap();
975
                    writeln!(json, "      \"scroll_depth\": {scroll_depth},").unwrap();
976
                    writeln!(json, "      \"orientation\": \"{:?}\",", info.orientation).unwrap();
977
                    writeln!(json, "      \"bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }}",
978
                        info.bounds.0.origin.x, info.bounds.0.origin.y,
979
                        info.bounds.0.size.width, info.bounds.0.size.height).unwrap();
980
                    writeln!(json, "    }}{comma}").unwrap();
981
                }
982
1
                _ => {
983
1
                    writeln!(json, "    {{").unwrap();
984
1
                    writeln!(json, "      \"index\": {i},").unwrap();
985
1
                    writeln!(
986
1
                        json,
987
1
                        "      \"type\": \"{:?}\",",
988
1
                        std::mem::discriminant(item)
989
1
                    )
990
1
                    .unwrap();
991
1
                    writeln!(json, "      \"clip_depth\": {clip_depth},").unwrap();
992
1
                    writeln!(json, "      \"scroll_depth\": {scroll_depth},").unwrap();
993
1
                    writeln!(json, "      \"node_id\": {node_id:?}").unwrap();
994
1
                    writeln!(json, "    }}{comma}").unwrap();
995
1
                }
996
            }
997
        }
998

            
999
4
        writeln!(json, "  ],").unwrap();
4
        writeln!(json, "  \"final_clip_depth\": {clip_depth},").unwrap();
4
        writeln!(json, "  \"final_scroll_depth\": {scroll_depth},").unwrap();
4
        writeln!(json, "  \"final_stacking_depth\": {stacking_depth},").unwrap();
4
        writeln!(
4
            json,
4
            "  \"balanced\": {}",
4
            clip_depth == 0 && scroll_depth == 0 && stacking_depth == 0
        )
4
        .unwrap();
4
        writeln!(json, "}}").unwrap();
4
        json
4
    }
}
/// A command in the display list. Can be either a drawing primitive or a
/// state-management instruction for the renderer's graphics context.
#[derive(Debug, Clone)]
pub enum DisplayListItem {
    // Drawing Primitives
    /// A filled rectangle with optional rounded corners.
    /// Used for backgrounds, colored boxes, and other solid fills.
    Rect {
        /// The rectangle bounds in absolute window coordinates
        bounds: WindowLogicalRect,
        /// The fill color (RGBA)
        color: ColorU,
        /// Corner radii for rounded rectangles
        border_radius: BorderRadius,
    },
    /// A selection highlight rectangle (e.g., for text selection).
    /// Rendered behind text to show selected regions.
    SelectionRect {
        /// The rectangle bounds in absolute window coordinates
        bounds: WindowLogicalRect,
        /// Corner radii for rounded selection
        border_radius: BorderRadius,
        /// The selection highlight color (typically semi-transparent)
        color: ColorU,
    },
    /// A text cursor (caret) rectangle.
    /// Typically a thin vertical line indicating text insertion point.
    CursorRect {
        /// The cursor bounds (usually narrow width)
        bounds: WindowLogicalRect,
        /// The cursor color
        color: ColorU,
    },
    /// A CSS border with per-side widths, colors, and styles.
    /// Supports different styles per side (solid, dashed, dotted, etc.).
    Border {
        /// The border-box bounds
        bounds: WindowLogicalRect,
        /// Border widths for each side
        widths: StyleBorderWidths,
        /// Border colors for each side
        colors: StyleBorderColors,
        /// Border styles for each side (solid, dashed, etc.)
        styles: StyleBorderStyles,
        /// Corner radii for rounded borders
        border_radius: StyleBorderRadius,
    },
    /// Text layout with full metadata (for PDF, accessibility, etc.)
    /// This is pushed BEFORE the individual Text items and contains
    /// the original text, glyph-to-unicode mapping, and positioning info
    TextLayout {
        layout: Arc<dyn std::any::Any + Send + Sync>, // Type-erased UnifiedLayout
        bounds: WindowLogicalRect,
        font_hash: FontHash,
        font_size_px: f32,
        color: ColorU,
    },
    /// Text rendered with individual glyph positioning (for simple renderers)
    Text {
        glyphs: Vec<GlyphInstance>,
        font_hash: FontHash,
        font_size_px: f32,
        color: ColorU,
        clip_rect: WindowLogicalRect,
        /// Layout node index that produced this text run.
        /// Enables patching glyphs without full display list regeneration.
        source_node_index: Option<usize>,
    },
    /// Underline decoration for text (CSS text-decoration: underline)
    Underline {
        bounds: WindowLogicalRect,
        color: ColorU,
        thickness: f32,
    },
    /// Strikethrough decoration for text (CSS text-decoration: line-through)
    Strikethrough {
        bounds: WindowLogicalRect,
        color: ColorU,
        thickness: f32,
    },
    /// Overline decoration for text (CSS text-decoration: overline)
    Overline {
        bounds: WindowLogicalRect,
        color: ColorU,
        thickness: f32,
    },
    Image {
        bounds: WindowLogicalRect,
        image: ImageRef,
        border_radius: BorderRadius,
    },
    /// A dedicated primitive for a scrollbar with optional GPU-animated opacity.
    /// This is a simple single-color scrollbar used for basic rendering.
    ScrollBar {
        bounds: WindowLogicalRect,
        color: ColorU,
        orientation: ScrollbarOrientation,
        /// Optional opacity key for GPU-side fading animation.
        /// If present, the renderer will use this key to look up dynamic opacity.
        /// If None, the alpha channel of `color` is used directly.
        opacity_key: Option<OpacityKey>,
        /// Optional hit-test ID for `WebRender` hit-testing.
        /// If present, allows event handlers to identify which scrollbar component was clicked.
        hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
    },
    /// A fully styled scrollbar with separate track, thumb, and optional buttons.
    /// Used when CSS scrollbar properties are specified.
    ScrollBarStyled {
        /// Complete drawing information for all scrollbar components
        info: Box<ScrollbarDrawInfo>,
    },
    /// An embedded `VirtualView` that references a child DOM with its own display list.
    /// The renderer will look up the child display list by `child_dom_id` and
    /// render it within the bounds. The `VirtualView` viewport is rendered in parent
    /// coordinate space (NOT inside a scroll frame) so it stays stationary.
    /// Scroll offset is communicated to the `VirtualView` callback, not via `WebRender`.
    VirtualView {
        /// The `DomId` of the child DOM (similar to webrender's `pipeline_id`)
        child_dom_id: DomId,
        /// The bounds where the `VirtualView` should be rendered
        bounds: WindowLogicalRect,
        /// The clip rect for the `VirtualView` content
        clip_rect: WindowLogicalRect,
        /// How far to shift the child content inside `bounds`, in logical px:
        /// `materialized_window_origin - current_scroll_offset`.
        ///
        /// This is what makes a `VirtualView` SCROLL rather than merely
        /// re-materialize. It is deliberately derived from the materialized
        /// window's origin and the live scroll offset ONLY — never from the
        /// virtual document size — so refining the document estimate (as
        /// background pagination lands) resizes the scrollbar and moves
        /// nothing on screen.
        content_offset: LogicalPosition,
    },
    /// Placeholder emitted during display list generation for `VirtualView` nodes.
    /// `window.rs` replaces this with a real `VirtualView` item after invoking
    /// the `VirtualView` callback. This avoids the need for post-hoc scroll frame
    /// scanning — `window.rs` simply finds the placeholder by `node_id`.
    ///
    /// Unlike regular scrollable nodes, `VirtualView` nodes do NOT get a
    /// PushScrollFrame/PopScrollFrame pair. Scroll state is managed by
    /// `ScrollManager` and passed to the `VirtualView` callback as `scroll_offset`.
    VirtualViewPlaceholder {
        /// The DOM `NodeId` of the `VirtualView` element in the parent DOM
        node_id: NodeId,
        /// The layout bounds of the `VirtualView` container
        bounds: WindowLogicalRect,
        /// The clip rect (same as bounds initially, may be adjusted)
        clip_rect: WindowLogicalRect,
    },
    // --- State-Management Commands ---
    /// Pushes a new clipping rectangle onto the renderer's clip stack.
    /// All subsequent primitives will be clipped by this rect until a `PopClip`.
    PushClip {
        bounds: WindowLogicalRect,
        border_radius: BorderRadius,
    },
    /// Pops the current clip from the renderer's clip stack.
    PopClip,
    /// Pushes an image-based clip mask onto the renderer's clip stack.
    /// The mask image should be R8 format: white (255) = visible, black (0) = clipped.
    /// All subsequent primitives will be masked until `PopImageMaskClip`.
    PushImageMaskClip {
        /// The bounds of the element being clipped
        bounds: WindowLogicalRect,
        /// The mask image (R8 format)
        mask_image: ImageRef,
        /// The rect within which the mask is applied
        mask_rect: WindowLogicalRect,
    },
    /// Pops the current image mask clip from the renderer's clip stack.
    PopImageMaskClip,
    /// Defines a scrollable area. This is a specialized clip that also
    /// establishes a new coordinate system for its children, which can be offset.
    PushScrollFrame {
        /// The clip rect in the parent's coordinate space.
        clip_bounds: WindowLogicalRect,
        /// The total size of the scrollable content.
        content_size: LogicalSize,
        /// An ID for the renderer to track this scrollable area between frames.
        scroll_id: LocalScrollId,
    },
    /// Pops the current scroll frame.
    PopScrollFrame,
    /// Pushes a new stacking context for proper z-index layering.
    /// All subsequent primitives until `PopStackingContext` will be in this stacking context.
    PushStackingContext {
        /// The z-index for this stacking context (for debugging/validation)
        z_index: i32,
        /// The bounds of the stacking context root element
        bounds: WindowLogicalRect,
    },
    /// Pops the current stacking context.
    PopStackingContext,
    /// Pushes a reference frame with a GPU-accelerated transform.
    /// Used for CSS transforms and drag visual offsets.
    /// Creates a new spatial coordinate system for all children.
    PushReferenceFrame {
        /// The transform key for GPU-animated property binding
        transform_key: TransformKey,
        /// The initial transform value (identity for drag, computed for CSS transform)
        initial_transform: ComputedTransform3D,
        /// The bounds of the reference frame (origin = transform origin)
        bounds: WindowLogicalRect,
    },
    /// Pops the current reference frame.
    PopReferenceFrame,
    /// Defines a region for hit-testing.
    HitTestArea {
        bounds: WindowLogicalRect,
        tag: DisplayListTagId, // This would be a renderer-agnostic ID type
    },
    // --- Gradient Primitives ---
    /// A linear gradient fill.
    LinearGradient {
        bounds: WindowLogicalRect,
        gradient: LinearGradient,
        border_radius: BorderRadius,
    },
    /// A radial gradient fill.
    RadialGradient {
        bounds: WindowLogicalRect,
        gradient: RadialGradient,
        border_radius: BorderRadius,
    },
    /// A conic (angular) gradient fill.
    ConicGradient {
        bounds: WindowLogicalRect,
        gradient: ConicGradient,
        border_radius: BorderRadius,
    },
    // --- Shadow Effects ---
    /// A box shadow (either outset or inset).
    BoxShadow {
        bounds: WindowLogicalRect,
        shadow: StyleBoxShadow,
        border_radius: BorderRadius,
    },
    // --- Filter Effects ---
    /// Push a filter effect that applies to subsequent content.
    PushFilter {
        bounds: WindowLogicalRect,
        filters: Vec<StyleFilter>,
    },
    /// Pop a previously pushed filter.
    PopFilter,
    /// Push a backdrop filter (applies to content behind the element).
    PushBackdropFilter {
        bounds: WindowLogicalRect,
        filters: Vec<StyleFilter>,
    },
    /// Pop a previously pushed backdrop filter.
    PopBackdropFilter,
    /// Push an opacity layer.
    PushOpacity {
        bounds: WindowLogicalRect,
        /// The BAKED (CSS) opacity — the value at display-list build time.
        opacity: f32,
        /// GPU binding for ANIMATED opacity (enter/exit fades, diff-driven
        /// animation). Same contract as `PushReferenceFrame.transform_key`:
        /// the display list carries the KEY, the per-tick value lives in the
        /// `GpuValueCache` animation channel, so the cached list serves every
        /// tick while the fade advances. `None` for plain CSS opacity.
        opacity_key: Option<OpacityKey>,
    },
    /// Pop an opacity layer.
    PopOpacity,
    /// Push a text shadow that applies to subsequent text content.
    PushTextShadow {
        shadow: StyleBoxShadow,
    },
    /// Pop all text shadows.
    PopTextShadow,
}
/// Intersect `a` with `b`; if they do not overlap, fall back to `b`.
///
/// Used to clamp a computed ink extent to its clip rect. The fallback is
/// deliberately the CLIP and not an empty rect: a non-overlapping result
/// means the extent was computed wrongly, and in that case damaging too
/// much is recoverable while damaging nothing leaves stale pixels.
5380
fn intersect_or(a: LogicalRect, b: LogicalRect) -> LogicalRect {
5380
    let x0 = a.origin.x.max(b.origin.x);
5380
    let y0 = a.origin.y.max(b.origin.y);
5380
    let x1 = (a.origin.x + a.size.width).min(b.origin.x + b.size.width);
5380
    let y1 = (a.origin.y + a.size.height).min(b.origin.y + b.size.height);
5380
    if x1 > x0 && y1 > y0 {
5353
        LogicalRect {
5353
            origin: LogicalPosition { x: x0, y: y0 },
5353
            size: LogicalSize {
5353
                width: x1 - x0,
5353
                height: y1 - y0,
5353
            },
5353
        }
    } else {
27
        b
    }
5380
}
impl DisplayListItem {
    /// Compare two display list items for visual equality (same appearance when rendered).
    /// Used by damage computation to detect content changes within the same bounds.
    /// Conservative: returns `false` (assumes different) for complex types like Arc<dyn Any>.
    // Exact float equality is intentional: this is frame-to-frame damage detection,
    // so any bit-level change in a coordinate/color/thickness SHOULD force a redraw.
    // An epsilon comparison would wrongly skip sub-epsilon visual updates.
    #[allow(clippy::float_cmp)]
    #[allow(clippy::similar_names)] // domain-standard coordinate/geometry/short-lived names
    #[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
    /// Is this a structure-opening marker (clip / scroll frame / stacking
    /// context / text shadow / image mask)? Paired with [`Self::is_pop_marker`]
    /// and [`Self::matching_pop`] by the page slicer's E17 re-derivation.
34735
    #[must_use] pub const fn is_push_marker(&self) -> bool {
33156
        matches!(
34735
            self,
            Self::PushClip { .. }
                | Self::PushScrollFrame { .. }
                | Self::PushStackingContext { .. }
                | Self::PushTextShadow { .. }
                | Self::PushImageMaskClip { .. }
        )
34735
    }
    /// Is this a structure-closing marker?
33156
    #[must_use] pub const fn is_pop_marker(&self) -> bool {
31577
        matches!(
33156
            self,
            Self::PopClip
                | Self::PopScrollFrame
                | Self::PopStackingContext
                | Self::PopTextShadow
                | Self::PopImageMaskClip
        )
33156
    }
    /// The Pop item that closes this Push marker (None for non-markers).
    #[must_use] pub const fn matching_pop(&self) -> Option<Self> {
        match self {
            Self::PushClip { .. } => Some(Self::PopClip),
            Self::PushScrollFrame { .. } => Some(Self::PopScrollFrame),
            Self::PushStackingContext { .. } => Some(Self::PopStackingContext),
            Self::PushTextShadow { .. } => Some(Self::PopTextShadow),
            Self::PushImageMaskClip { .. } => Some(Self::PopImageMaskClip),
            _ => None,
        }
    }
1138218
    #[must_use] pub fn is_visually_equal(&self, other: &Self) -> bool {
1138218
        if std::mem::discriminant(self) != std::mem::discriminant(other) {
3
            return false;
1138215
        }
1138215
        match (self, other) {
126323
            (Self::Rect { bounds: b1, color: c1, border_radius: br1 },
126323
             Self::Rect { bounds: b2, color: c2, border_radius: br2 }) => {
126323
                b1 == b2 && c1 == c2 && br1.top_left == br2.top_left && br1.top_right == br2.top_right
126295
                    && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
            }
            (Self::SelectionRect { bounds: b1, border_radius: br1, color: c1 },
             Self::SelectionRect { bounds: b2, border_radius: br2, color: c2 }) => {
                b1 == b2 && c1 == c2 && br1.top_left == br2.top_left && br1.top_right == br2.top_right
                    && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
            }
564
            (Self::CursorRect { bounds: b1, color: c1 },
564
             Self::CursorRect { bounds: b2, color: c2 }) => b1 == b2 && c1 == c2,
226106
            (Self::Text { glyphs: g1, font_hash: fh1, font_size_px: fs1, color: c1, clip_rect: cr1, .. },
226106
             Self::Text { glyphs: g2, font_hash: fh2, font_size_px: fs2, color: c2, clip_rect: cr2, .. }) => {
226106
                cr1 == cr2 && c1 == c2 && fh1 == fh2 && fs1 == fs2 && g1.len() == g2.len()
6395891
                    && g1.iter().zip(g2.iter()).all(|(a, b)| {
6395891
                        a.index == b.index
6395887
                            && a.point.x == b.point.x
6395886
                            && a.point.y == b.point.y
6395891
                    })
            }
1
            (Self::Underline { bounds: b1, color: c1, thickness: t1 },
1
             Self::Underline { bounds: b2, color: c2, thickness: t2 }) => b1 == b2 && c1 == c2 && t1 == t2,
            (Self::Strikethrough { bounds: b1, color: c1, thickness: t1 },
             Self::Strikethrough { bounds: b2, color: c2, thickness: t2 }) => b1 == b2 && c1 == c2 && t1 == t2,
            (Self::Overline { bounds: b1, color: c1, thickness: t1 },
             Self::Overline { bounds: b2, color: c2, thickness: t2 }) => b1 == b2 && c1 == c2 && t1 == t2,
291339
            (Self::Border { bounds: b1, widths: w1, colors: c1, styles: s1, .. },
291339
             Self::Border { bounds: b2, widths: w2, colors: c2, styles: s2, .. }) => {
291339
                b1 == b2
291311
                    && w1.top == w2.top && w1.right == w2.right && w1.bottom == w2.bottom && w1.left == w2.left
291311
                    && c1.top == c2.top && c1.right == c2.right && c1.bottom == c2.bottom && c1.left == c2.left
291311
                    && s1.top == s2.top && s1.right == s2.right && s1.bottom == s2.bottom && s1.left == s2.left
            }
4
            (Self::Image { bounds: b1, image: i1, border_radius: br1 },
4
             Self::Image { bounds: b2, image: i2, border_radius: br2 }) => {
4
                b1 == b2
                    // Compare the never-reused ImageRef identity, NOT the data
                    // pointer: `id` exists precisely because heap addresses
                    // get reused (core/resources.rs), and pointer identity
                    // also can't see "same image object, new pixels" swaps
                    // (per-frame producers replacing an ImageRef with an
                    // equal-address reallocation reported no damage).
4
                    && i1.get_hash() == i2.get_hash()
1
                    && br1.top_left == br2.top_left && br1.top_right == br2.top_right
1
                    && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
            }
            (Self::BoxShadow { bounds: b1, shadow: s1, border_radius: br1 },
             Self::BoxShadow { bounds: b2, shadow: s2, border_radius: br2 }) => {
                b1 == b2 && s1 == s2
                    && br1.top_left == br2.top_left && br1.top_right == br2.top_right
                    && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
            }
            (Self::LinearGradient { bounds: b1, gradient: g1, border_radius: br1 },
             Self::LinearGradient { bounds: b2, gradient: g2, border_radius: br2 }) => {
                b1 == b2 && g1 == g2
                    && br1.top_left == br2.top_left && br1.top_right == br2.top_right
                    && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
            }
            (Self::RadialGradient { bounds: b1, gradient: g1, border_radius: br1 },
             Self::RadialGradient { bounds: b2, gradient: g2, border_radius: br2 }) => {
                b1 == b2 && g1 == g2
                    && br1.top_left == br2.top_left && br1.top_right == br2.top_right
                    && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
            }
            (Self::ConicGradient { bounds: b1, gradient: g1, border_radius: br1 },
             Self::ConicGradient { bounds: b2, gradient: g2, border_radius: br2 }) => {
                b1 == b2 && g1 == g2
                    && br1.top_left == br2.top_left && br1.top_right == br2.top_right
                    && br1.bottom_left == br2.bottom_left && br1.bottom_right == br2.bottom_right
            }
            (Self::ScrollBar { bounds: b1, color: c1, .. },
             Self::ScrollBar { bounds: b2, color: c2, .. }) => b1 == b2 && c1 == c2,
16437
            (Self::PushClip { bounds: b1, .. }, Self::PushClip { bounds: b2, .. }) => b1 == b2,
16438
            (Self::PushScrollFrame { clip_bounds: b1, scroll_id: s1, .. },
16438
             Self::PushScrollFrame { clip_bounds: b2, scroll_id: s2, .. }) => b1 == b2 && s1 == s2,
17492
            (Self::PushStackingContext { z_index: z1, bounds: b1 },
17492
             Self::PushStackingContext { z_index: z2, bounds: b2 }) => z1 == z2 && b1 == b2,
4
            (Self::PushOpacity { bounds: b1, opacity: o1, opacity_key: k1 },
4
             Self::PushOpacity { bounds: b2, opacity: o2, opacity_key: k2 }) => {
                // The key participates: two lists binding different keys draw
                // from different animation channels and are NOT interchangeable
                // (same rule the GPU-damage diff relies on).
4
                b1 == b2 && o1 == o2 && k1 == k2
            }
            // Pop items with no fields are always equal (discriminant already matched)
            (Self::PopClip, Self::PopClip)
            | (Self::PopImageMaskClip, Self::PopImageMaskClip)
            | (Self::PopScrollFrame, Self::PopScrollFrame)
            | (Self::PopStackingContext, Self::PopStackingContext)
            | (Self::PopReferenceFrame, Self::PopReferenceFrame)
            | (Self::PopFilter, Self::PopFilter)
            | (Self::PopBackdropFilter, Self::PopBackdropFilter)
            | (Self::PopOpacity, Self::PopOpacity)
50424
            | (Self::PopTextShadow, Self::PopTextShadow) => true,
            // HitTestArea paints NO pixels (hit-testing only), so two of them are
            // always visually equal — a moved/changed hit region never needs a
            // repaint on its own. Without this it hit `_ => false` and forced
            // false-positive damage on every relayout (#12).
376595
            (Self::HitTestArea { .. }, Self::HitTestArea { .. }) => true,
            // TextLayout: visually equal iff same box / font / colour AND the same
            // underlying (type-erased) layout allocation. A no-op relayout reuses
            // the cached layout Arc (pointer identity holds); a real text change
            // reshapes into a new Arc. Without this it hit `_ => false` and
            // reported damage every frame (#12).
2
            (Self::TextLayout { layout: l1, bounds: b1, font_hash: fh1, font_size_px: fs1, color: c1 },
2
             Self::TextLayout { layout: l2, bounds: b2, font_hash: fh2, font_size_px: fs2, color: c2 }) => {
2
                b1 == b2
2
                    && fh1 == fh2
2
                    && fs1 == fs2
2
                    && c1 == c2
2
                    && Arc::ptr_eq(l1, l2)
            }
            // ScrollBarStyled: equal iff the STATIC drawing info matches. The
            // LIVE thumb position/opacity are read from the GPU value cache at
            // raster time (thumb_transform_key/opacity_key) — value changes are
            // damaged by the render_frame GPU-value diff, NOT by this item
            // comparison. Without this arm every scrollbar'd window re-damaged
            // its bar every frame (`_ => false`), so `FrameDamage::None` was
            // unreachable and idle windows re-rendered + re-presented forever.
16437
            (Self::ScrollBarStyled { info: i1 }, Self::ScrollBarStyled { info: i2 }) => i1 == i2,
            // VirtualView: the item only carries WHERE the child renders; the
            // child DOM's content changes are detected by
            // compute_virtual_view_damage (child display-list diff).
            (Self::VirtualView { child_dom_id: d1, bounds: b1, clip_rect: c1, .. },
             Self::VirtualView { child_dom_id: d2, bounds: b2, clip_rect: c2, .. }) => {
                d1 == d2 && b1 == b2 && c1 == c2
            }
            (Self::VirtualViewPlaceholder { bounds: b1, .. },
             Self::VirtualViewPlaceholder { bounds: b2, .. }) => b1 == b2,
            // PushReferenceFrame: the LIVE transform (drag, animation) is a GPU
            // cache value keyed by transform_key — covered by the GPU-value
            // diff, same as the scrollbar thumb.
49
            (Self::PushReferenceFrame { transform_key: k1, initial_transform: t1, bounds: b1 },
49
             Self::PushReferenceFrame { transform_key: k2, initial_transform: t2, bounds: b2 }) => {
49
                k1 == k2 && t1 == t2 && b1 == b2
            }
            (Self::PushFilter { bounds: b1, filters: f1 },
             Self::PushFilter { bounds: b2, filters: f2 }) => b1 == b2 && f1 == f2,
            (Self::PushBackdropFilter { bounds: b1, filters: f1 },
             Self::PushBackdropFilter { bounds: b2, filters: f2 }) => b1 == b2 && f1 == f2,
            // PushImageMaskClip: ImageRef comparison is by underlying data hash
            // (cheap identity), so a swapped mask image reports unequal.
            (Self::PushImageMaskClip { bounds: b1, mask_image: m1, mask_rect: r1 },
             Self::PushImageMaskClip { bounds: b2, mask_image: m2, mask_rect: r2 }) => {
                b1 == b2 && r1 == r2 && m1.get_hash() == m2.get_hash()
            }
            (Self::PushTextShadow { shadow: s1 }, Self::PushTextShadow { shadow: s2 }) => s1 == s2,
            // For other complex types (Image, gradients, etc.),
            // conservatively assume different
            _ => false,
        }
1138218
    }
    /// Returns true if this item is a state-management command (Push/Pop)
    /// that must always be processed to maintain correct stacks.
21118
    #[must_use] pub const fn is_state_management(&self) -> bool {
21118
        matches!(self,
            Self::PushClip { .. }
            | Self::PopClip
            | Self::PushImageMaskClip { .. }
            | Self::PopImageMaskClip
            | Self::PushScrollFrame { .. }
            | Self::PopScrollFrame
            | Self::PushStackingContext { .. }
            | Self::PopStackingContext
            | Self::PushReferenceFrame { .. }
            | Self::PopReferenceFrame
            | Self::PushFilter { .. }
            | Self::PopFilter
            | Self::PushBackdropFilter { .. }
            | Self::PopBackdropFilter
            | Self::PushOpacity { .. }
            | Self::PopOpacity
            | Self::PushTextShadow { .. }
            | Self::PopTextShadow
        )
21118
    }
    /// Return the visual bounding rect including effects that extend beyond
    /// content bounds (e.g. box-shadow spread/blur/offset). Used for damage
    /// rect computation where we need the full repaint area.
    #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
30777
    #[must_use] pub fn visual_bounds(&self) -> Option<LogicalRect> {
30777
        match self {
            // TextLayout paints NOTHING — `render_single_item` skips it
            // outright ("metadata for PDF/accessibility"). So it must not
            // contribute damage either.
            //
            // It did. Its bounds is the enclosing node's whole content box
            // and its payload (the unified layout) changes on every edit, so
            // `is_visually_equal` said "changed" and the damage diff unioned
            // that box in — repainting the entire editor for a one-character
            // keystroke. It dominated the union completely: measured on a
            // 400x300 contenteditable, the two items that differ per
            // keystroke are this one at 356x65 (identical before and after)
            // and the caret at 2x22. Tightening TEXT damage could never show
            // up while a non-painting item was damaging everything.
            //
            // `None` here is what push/pop items already return: no pixels,
            // no damage. `bounds()` is left alone — it is the item filter in
            // the damaged renderer, and the render arm for TextLayout is a
            // no-op either way.
4059
            Self::TextLayout { .. } => None,
            // Text damages only where its GLYPHS are, not its whole clip box.
            //
            // `bounds()` reports a text run's `clip_rect`, and the emission
            // site sets that to the node's entire viewport-sized content box.
            // Since `compute_display_list_damage` unions `visual_bounds()`,
            // that made damage for a text change as coarse as the enclosing
            // text node however little of it changed — measured at 384x48 for
            // a one-character edit, i.e. every line of the paragraph.
            //
            // `bounds()` deliberately keeps returning the clip rect: it is the
            // item filter in `render_display_list_damaged`, where being too
            // WIDE only costs a redundant intersection test, while being too
            // narrow would skip an item that should have painted. Only the
            // damage side is tightened here.
            //
            // The extent is padded generously and then clamped to the clip.
            // Ink routinely exceeds the advance box — diacritics, italic
            // overhang, `f`/`j` descenders, emoji — and under-damaging leaves
            // stale pixels on screen, which is worse than the coarseness this
            // replaces. The padding is in em, so it scales with the font.
            Self::Text {
5380
                glyphs,
5380
                clip_rect,
5380
                font_size_px,
                ..
            } => {
5380
                let clip = *clip_rect.inner();
5380
                if glyphs.is_empty() {
                    return Some(clip);
5380
                }
5380
                let em = *font_size_px;
5380
                let mut min_x = f32::MAX;
5380
                let mut max_x = f32::MIN;
5380
                let mut min_y = f32::MAX;
5380
                let mut max_y = f32::MIN;
45967
                for g in glyphs {
40587
                    min_x = min_x.min(g.point.x);
40587
                    max_x = max_x.max(g.point.x);
40587
                    min_y = min_y.min(g.point.y);
40587
                    max_y = max_y.max(g.point.y);
40587
                }
                // `point` is the pen position ON the baseline, so the box has
                // to grow upward by the ascent and downward by the descent;
                // 1.5em/0.75em covers every Latin face plus stacked marks.
                // Horizontally the last glyph's own advance is still to come,
                // hence the extra em on the right.
5380
                let ink = LogicalRect {
5380
                    origin: LogicalPosition {
5380
                        x: min_x - em,
5380
                        y: min_y - em * 1.5,
5380
                    },
5380
                    size: LogicalSize {
5380
                        width: (max_x - min_x) + em * 2.0,
5380
                        height: (max_y - min_y) + em * 2.25,
5380
                    },
5380
                };
5380
                Some(intersect_or(ink, clip))
            }
2
            Self::BoxShadow { bounds, shadow, .. } => {
2
                let b = *bounds.inner();
                // Shadow can extend beyond element bounds by offset + spread + blur
2
                let ox = shadow
2
                    .offset_x
2
                    .to_pixels_internal(DEFAULT_SHADOW_FONT_SIZE_PX, DEFAULT_SHADOW_FONT_SIZE_PX)
2
                    .abs();
2
                let oy = shadow
2
                    .offset_y
2
                    .to_pixels_internal(DEFAULT_SHADOW_FONT_SIZE_PX, DEFAULT_SHADOW_FONT_SIZE_PX)
2
                    .abs();
2
                let blur = shadow
2
                    .blur_radius
2
                    .to_pixels_internal(DEFAULT_SHADOW_FONT_SIZE_PX, DEFAULT_SHADOW_FONT_SIZE_PX)
2
                    .abs();
2
                let spread = shadow
2
                    .spread_radius
2
                    .to_pixels_internal(DEFAULT_SHADOW_FONT_SIZE_PX, DEFAULT_SHADOW_FONT_SIZE_PX)
2
                    .abs();
2
                let expand = ox + oy + blur + spread;
2
                Some(LogicalRect {
2
                    origin: LogicalPosition {
2
                        x: b.origin.x - expand,
2
                        y: b.origin.y - expand,
2
                    },
2
                    size: LogicalSize {
2
                        width: b.size.width + expand * 2.0,
2
                        height: b.size.height + expand * 2.0,
2
                    },
2
                })
            }
21336
            _ => self.bounds(),
        }
30777
    }
    /// Return the bounding rect of this item, or None for push/pop commands
    /// that don't have their own visual bounds.
    #[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
2003842
    #[must_use] pub fn bounds(&self) -> Option<LogicalRect> {
2003842
        match self {
33231
            Self::Rect { bounds, .. }
551
            | Self::SelectionRect { bounds, .. }
2282
            | Self::CursorRect { bounds, .. }
426394
            | Self::Border { bounds, .. }
564169
            | Self::Text { clip_rect: bounds, .. }
29268
            | Self::TextLayout { bounds, .. }
595
            | Self::Underline { bounds, .. }
            | Self::Strikethrough { bounds, .. }
            | Self::Overline { bounds, .. }
55
            | Self::Image { bounds, .. }
1
            | Self::ScrollBar { bounds, .. }
190
            | Self::LinearGradient { bounds, .. }
46
            | Self::RadialGradient { bounds, .. }
1
            | Self::ConicGradient { bounds, .. }
182
            | Self::BoxShadow { bounds, .. }
9
            | Self::VirtualView { bounds, .. }
244
            | Self::VirtualViewPlaceholder { bounds, .. }
911057
            | Self::HitTestArea { bounds, .. }
3587
            | Self::PushClip { bounds, .. }
46
            | Self::PushImageMaskClip { bounds, .. }
465
            | Self::PushScrollFrame { clip_bounds: bounds, .. }
13173
            | Self::PushStackingContext { bounds, .. }
242
            | Self::PushReferenceFrame { bounds, .. }
            | Self::PushFilter { bounds, .. }
            | Self::PushBackdropFilter { bounds, .. }
1985814
            | Self::PushOpacity { bounds, .. } => Some(*bounds.inner()),
628
            Self::ScrollBarStyled { info, .. } => Some(*info.bounds.inner()),
1
            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
17399
            | Self::PopTextShadow => None,
        }
2003842
    }
}
// Helper structs for the DisplayList
#[derive(Debug, Copy, Clone, Default, PartialEq)]
pub struct BorderRadius {
    pub top_left: f32,
    pub top_right: f32,
    pub bottom_left: f32,
    pub bottom_right: f32,
}
impl BorderRadius {
6474
    #[must_use] pub fn is_zero(&self) -> bool {
6474
        self.top_left == 0.0
6247
            && self.top_right == 0.0
6246
            && self.bottom_left == 0.0
6245
            && self.bottom_right == 0.0
6474
    }
}
// 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(crate) 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<ForcedBreak>,
    /// 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>,
    /// Current (layout index, phase) for `layout_node_mapping` — see
    /// [`DisplayList::layout_node_mapping`].
    current_layout: Option<(usize, EmitPhase)>,
    layout_node_mapping: Vec<Option<(usize, EmitPhase)>>,
    /// One-shot: the uniform background for the NEXT pushed item (set by
    /// `push_text_run` / the patch copy just before pushing a Text item).
    next_text_bg: Option<(ColorU, WindowLogicalRect)>,
    uniform_text_bgs: Vec<Option<(ColorU, WindowLogicalRect)>>,
}
impl DisplayListBuilder {
22
    pub(crate) fn new() -> Self {
22
        Self::default()
22
    }
9171
    pub(crate) const fn with_debug(debug_enabled: bool) -> Self {
9171
        Self {
9171
            items: Vec::new(),
9171
            node_mapping: Vec::new(),
9171
            current_node: None,
9171
            debug_messages: Vec::new(),
9171
            debug_enabled,
9171
            forced_page_breaks: Vec::new(),
9171
            fixed_position_item_ranges: Vec::new(),
9171
            fixed_position_start: None,
9171
            current_layout: None,
9171
            layout_node_mapping: Vec::new(),
9171
            next_text_bg: None,
9171
            uniform_text_bgs: Vec::new(),
9171
        }
9171
    }
    /// Log a debug message if debug is enabled
524490
    fn debug_log(&mut self, message: String) {
524490
        if self.debug_enabled {
55272
            self.debug_messages.push(LayoutDebugMessage::info(message));
469243
        }
524490
    }
    /// Build the display list and transfer debug messages to the provided option
9169
    pub(crate) fn build_with_debug(
9169
        mut self,
9169
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
9169
    ) -> DisplayList {
        // Transfer collected debug messages to the context
9169
        if let Some(msgs) = debug_messages.as_mut() {
5343
            msgs.append(&mut self.debug_messages);
5402
        }
9169
        DisplayList {
9169
            items: self.items,
9169
            node_mapping: self.node_mapping,
9169
            forced_page_breaks: self.forced_page_breaks,
9169
            fixed_position_item_ranges: self.fixed_position_item_ranges,
9169
            layout_node_mapping: self.layout_node_mapping,
9169
            uniform_text_bgs: self.uniform_text_bgs,
9169
        }
9169
    }
    /// Set the current node context for subsequent push operations
1334800
    pub(crate) const fn set_current_node(&mut self, node_id: Option<NodeId>) {
1334800
        self.current_node = node_id;
1334800
    }
    /// Tag subsequent pushes with (layout index, phase). `None` = untagged
    /// (equivalent to `ScWalk` for patching purposes — never substituted).
2650310
    pub(crate) const fn set_current_layout(&mut self, tag: Option<(usize, EmitPhase)>) {
2650310
        self.current_layout = tag;
2650310
    }
    /// The current node context (to save/restore around pushes that must be
    /// attributed to a DIFFERENT node, e.g. inline images inside an IFC).
    pub(crate) const fn current_node(&self) -> Option<NodeId> {
        self.current_node
    }
    /// Mark the start of a fixed-position element's display items.
2
    pub(crate) const fn begin_fixed_position_element(&mut self) {
2
        self.fixed_position_start = Some(self.items.len());
2
    }
    /// Mark the end of a fixed-position element's display items.
    /// Records the (start, end) index range for paged media replication.
4
    pub(crate) fn end_fixed_position_element(&mut self) {
4
        if let Some(start) = self.fixed_position_start.take() {
2
            let end = self.items.len();
2
            if end > start {
1
                self.fixed_position_item_ranges.push((start, end));
1
            }
2
        }
4
    }
    /// Register a forced page break at the given Y position, remembering the
    /// node whose break property caused it (diagnostics: surfaces in
    /// `PageBreakPosition::causing_node`).
    /// This is used for CSS break-before: always and break-after: always.
65
    pub(crate) fn add_forced_page_break(&mut self, y_position: f32, causing_node: Option<NodeId>) {
        // Avoid duplicates and keep sorted by Y
80
        if !self.forced_page_breaks.iter().any(|b| b.y == y_position) {
64
            self.forced_page_breaks.push(ForcedBreak { y: y_position, causing_node });
64
            self.forced_page_breaks
89
                .sort_by(|a, b| a.y.partial_cmp(&b.y).unwrap_or(core::cmp::Ordering::Equal));
1
        }
65
    }
    /// Push an item and record its node mapping.
    ///
    /// Drops items whose geometry is still the UNASSIGNED-POSITION SENTINEL.
    ///
    /// `calculated_positions` seeds entries with `f32::MIN`, and an inline text
    /// node's own box position is never assigned — see the comment on
    /// `paint_text_selection`, which anchors to the IFC root precisely because
    /// "the node's OWN box position is never assigned (stays the `f32::MIN`
    /// sentinel)". That workaround is local to one caller; the other thirteen
    /// readers of `calculated_positions` get the raw sentinel.
    ///
    /// The published 0.2.0 azul-self-test-linux display list carried 24 of
    /// them, e.g.
    ///
    ///     Border      { bounds: WindowLogicalRect(624x0 @ (-3.4028235e38, -3.4028235e38)) }
    ///     HitTestArea { bounds: WindowLogicalRect(624x0 @ (-3.4028235e38, -3.4028235e38)) }
    ///
    /// A `Border` at -3.4e38 is merely invisible. A `HitTestArea` there is
    /// ACTIVELY WRONG: it is a live hit-test rectangle at a coordinate no
    /// pointer can reach, and any arithmetic on it (offsetting into a parent
    /// space, unioning a bounding box) produces infinities that then propagate
    /// into rects that ARE on screen.
    ///
    /// Dropping is right rather than clamping to the origin: the item has no
    /// position, and inventing (0,0) would put a phantom border and a live
    /// hit target in the top-left corner of the window. An item nobody can
    /// see and nobody can click is exactly what an unpositioned node should
    /// contribute.
1804897
    fn push_item(&mut self, item: DisplayListItem) {
        // A TextLayout carrying `FontHash::invalid()` (0) describes text in a
        // font that does not exist. Verified before dropping it, because
        // "harmless metadata" is a claim that has to be checked:
        //   * compositor2.rs   — explicit no-op, "handled elsewhere (via
        //                        PushCachedTextRuns)"; the GPU path never draws it
        //   * cpurender/raster — "TextLayout is metadata for PDF/accessibility -
        //                        skip in CPU rendering"
        //   * hit-testing      — uses separate HitTestArea items
        //   * selection        — uses get_selection_rects on the inline layout,
        //                        not display-list items
        //   * scan_used_fonts  — reads font_hash (fixed separately: 0 was being
        //                        turned into a phantom FontKey and marked live)
        //   * PDF export       — cannot render text in a font it cannot resolve
        // So nothing draws, hits, or selects it, and the one consumer that read
        // it was harmed by doing so.
1804897
        if let DisplayListItem::TextLayout { font_hash, .. } = &item {
8604
            if font_hash.font_hash == 0 {
                #[cfg(debug_assertions)]
                eprintln!(
                    "[azul][displaylist] dropping a TextLayout with \
                     FontHash::invalid() — its font never resolved, so no \
                     renderer would have drawn it"
                );
153
                return;
8451
            }
1796293
        }
1804744
        if let Some(b) = item.bounds() {
3580624
            let unassigned = |v: f32| !v.is_finite() || v <= UNASSIGNED_POSITION_LIMIT;
1791561
            if unassigned(b.origin.x) || unassigned(b.origin.y) {
                #[cfg(debug_assertions)]
                eprintln!(
                    "[azul][displaylist] dropping {item:?} at an unassigned \
                     position ({}, {}) — its layout node never received a \
                     computed position",
                    b.origin.x, b.origin.y,
                );
2498
                return;
1789063
            }
13183
        }
        // Structural push/pop items get NO node attribution. Before this,
        // they inherited whatever `current_node` the LAST paint call leaked —
        // a PopStackingContext attributed to some text node that happened to
        // paint last. No consumer legitimately reads a break property or a
        // damage source off a Push/Pop, and the leak made attribution
        // pass-history-dependent (the DL-patch golden test caught patched
        // and full passes disagreeing about it).
1802246
        let attr = if matches!(
1802246
            item,
            DisplayListItem::PushClip { .. }
                | DisplayListItem::PopClip
                | DisplayListItem::PushImageMaskClip { .. }
                | DisplayListItem::PopImageMaskClip
                | DisplayListItem::PushScrollFrame { .. }
                | DisplayListItem::PopScrollFrame
                | DisplayListItem::PushStackingContext { .. }
                | DisplayListItem::PopStackingContext
                | DisplayListItem::PushReferenceFrame { .. }
                | DisplayListItem::PopReferenceFrame
                | DisplayListItem::PushFilter { .. }
                | DisplayListItem::PopFilter
                | DisplayListItem::PushBackdropFilter { .. }
                | DisplayListItem::PopBackdropFilter
                | DisplayListItem::PushOpacity { .. }
                | DisplayListItem::PopOpacity
        ) {
26362
            None
        } else {
1775884
            self.current_node
        };
1802246
        self.items.push(item);
1802246
        self.node_mapping.push(attr);
1802246
        self.layout_node_mapping.push(self.current_layout);
1802246
        self.uniform_text_bgs.push(self.next_text_bg.take());
1804897
    }
3
    pub(crate) fn build(self) -> DisplayList {
3
        DisplayList {
3
            items: self.items,
3
            node_mapping: self.node_mapping,
3
            forced_page_breaks: self.forced_page_breaks,
3
            fixed_position_item_ranges: self.fixed_position_item_ranges,
3
            layout_node_mapping: self.layout_node_mapping,
3
            uniform_text_bgs: self.uniform_text_bgs,
3
        }
3
    }
834955
    pub(crate) fn push_hit_test_area(&mut self, bounds: LogicalRect, tag: DisplayListTagId) {
834955
        self.push_item(DisplayListItem::HitTestArea { bounds: bounds.into(), tag });
834955
    }
    /// Push a simple single-color scrollbar (legacy method).
2
    pub(crate) fn push_scrollbar(
2
        &mut self,
2
        bounds: LogicalRect,
2
        color: ColorU,
2
        orientation: ScrollbarOrientation,
2
        opacity_key: Option<OpacityKey>,
2
        hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
2
    ) {
2
        if color.a > 0 || opacity_key.is_some() {
1
            // Optimization: Don't draw fully transparent items without opacity keys.
1
            self.push_item(DisplayListItem::ScrollBar {
1
                bounds: bounds.into(),
1
                color,
1
                orientation,
1
                opacity_key,
1
                hit_id,
1
            });
1
        }
2
    }
    /// Push a fully styled scrollbar with track, thumb, and optional buttons.
569
    pub(crate) fn push_scrollbar_styled(&mut self, info: ScrollbarDrawInfo) {
        // Only push if at least the thumb or track is visible
569
        if info.thumb_color.a > 0 || info.track_color.a > 0 || info.opacity_key.is_some() {
569
            self.push_item(DisplayListItem::ScrollBarStyled {
569
                info: Box::new(info),
569
            });
569
        }
569
    }
42176
    pub(crate) fn push_rect(&mut self, bounds: LogicalRect, color: ColorU, border_radius: BorderRadius) {
42176
        if color.a > 0 {
27208
            // Optimization: Don't draw fully transparent items.
27208
            self.push_item(DisplayListItem::Rect {
27208
                bounds: bounds.into(),
27208
                color,
27208
                border_radius,
27208
            });
27208
        }
42176
    }
    /// 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.
378488
    pub(crate) fn push_backgrounds_and_border(
378488
        &mut self,
378488
        bounds: LogicalRect,
378488
        background_contents: &[azul_css::props::style::StyleBackgroundContent],
378488
        border_info: &BorderInfo,
378488
        simple_border_radius: BorderRadius,
378488
        style_border_radius: StyleBorderRadius,
378488
        image_cache: &azul_core::resources::ImageCache,
378488
    ) {
        use azul_css::props::style::StyleBackgroundContent;
        // Paint all background layers in order (CSS paints backgrounds back to front)
417923
        for bg in background_contents {
39435
            match bg {
39360
                StyleBackgroundContent::Color(color) => {
39360
                    self.push_rect(bounds, *color, simple_border_radius);
39360
                }
36
                StyleBackgroundContent::LinearGradient(gradient) => {
36
                    self.push_linear_gradient(bounds, gradient.clone(), simple_border_radius);
36
                }
9
                StyleBackgroundContent::RadialGradient(gradient) => {
9
                    self.push_radial_gradient(bounds, gradient.clone(), simple_border_radius);
9
                }
                StyleBackgroundContent::ConicGradient(gradient) => {
                    self.push_conic_gradient(bounds, gradient.clone(), simple_border_radius);
                }
3
                StyleBackgroundContent::Image(image_id) => {
3
                    if let Some(image_ref) = image_cache.get_css_image_id(image_id) {
1
                        self.push_image(bounds, image_ref.clone(), simple_border_radius);
2
                    }
                }
27
                StyleBackgroundContent::SystemColor(_s) => {
27
                    // TODO(superplan g8): resolve via SystemColorRef::resolve(&SystemColors,
27
                    // fallback) and push_rect. SystemColors is not threaded into the
27
                    // display-list builder yet, so `background: system:<name>` currently
27
                    // parses but paints nothing (graceful no-op rather than a wrong color).
27
                }
            }
        }
        // Paint border
378488
        self.push_border(
378488
            bounds,
378488
            border_info.widths,
378488
            border_info.colors,
378488
            border_info.styles,
378488
            style_border_radius,
        );
378488
    }
    /// 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.
520149
    pub(crate) fn push_inline_backgrounds_and_border(
520149
        &mut self,
520149
        bounds: LogicalRect,
520149
        background_color: Option<ColorU>,
520149
        background_contents: &[azul_css::props::style::StyleBackgroundContent],
520149
        border: Option<&crate::text3::cache::InlineBorderInfo>,
520149
        image_cache: &azul_core::resources::ImageCache,
520149
    ) {
        use azul_css::props::style::StyleBackgroundContent;
        // Paint solid background color if present
520149
        if let Some(bg_color) = background_color {
27
            self.push_rect(bounds, bg_color, BorderRadius::default());
520122
        }
        // Paint all background layers in order (CSS paints backgrounds back to front)
520185
        for bg in background_contents {
36
            match bg {
27
                StyleBackgroundContent::Color(color) => {
27
                    self.push_rect(bounds, *color, BorderRadius::default());
27
                }
9
                StyleBackgroundContent::LinearGradient(gradient) => {
9
                    self.push_linear_gradient(bounds, gradient.clone(), BorderRadius::default());
9
                }
                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());
                    }
                }
                StyleBackgroundContent::SystemColor(_s) => {
                    // TODO(superplan g8): resolve via SystemColorRef::resolve(&SystemColors,
                    // fallback) and push_rect. SystemColors is not threaded into the
                    // display-list builder yet, so `background: system:<name>` currently
                    // parses but paints nothing (graceful no-op rather than a wrong color).
                }
            }
        }
        // Paint border if present
        // CSS 2.2 §8.6: suppress left/right borders at split points, respecting direction
520149
        if let Some(border) = border {
27
            let effective_left = if border.left_inset() > 0.0 { border.left } else { 0.0 };
27
            let effective_right = if border.right_inset() > 0.0 { border.right } else { 0.0 };
27
            if border.top > 0.0 || effective_right > 0.0 || border.bottom > 0.0 || effective_left > 0.0 {
9
                let border_widths = StyleBorderWidths {
9
                    top: Some(CssPropertyValue::Exact(LayoutBorderTopWidth {
9
                        inner: PixelValue::px(border.top),
9
                    })),
9
                    right: Some(CssPropertyValue::Exact(LayoutBorderRightWidth {
9
                        inner: PixelValue::px(effective_right),
9
                    })),
9
                    bottom: Some(CssPropertyValue::Exact(LayoutBorderBottomWidth {
9
                        inner: PixelValue::px(border.bottom),
9
                    })),
9
                    left: Some(CssPropertyValue::Exact(LayoutBorderLeftWidth {
9
                        inner: PixelValue::px(effective_left),
9
                    })),
9
                };
9
                let border_colors = StyleBorderColors {
9
                    top: Some(CssPropertyValue::Exact(StyleBorderTopColor {
9
                        inner: border.top_color,
9
                    })),
9
                    right: Some(CssPropertyValue::Exact(StyleBorderRightColor {
9
                        inner: border.right_color,
9
                    })),
9
                    bottom: Some(CssPropertyValue::Exact(StyleBorderBottomColor {
9
                        inner: border.bottom_color,
9
                    })),
9
                    left: Some(CssPropertyValue::Exact(StyleBorderLeftColor {
9
                        inner: border.left_color,
9
                    })),
9
                };
9
                let border_styles = StyleBorderStyles {
9
                    top: Some(CssPropertyValue::Exact(StyleBorderTopStyle {
9
                        inner: BorderStyle::Solid,
9
                    })),
9
                    right: Some(CssPropertyValue::Exact(StyleBorderRightStyle {
9
                        inner: BorderStyle::Solid,
9
                    })),
9
                    bottom: Some(CssPropertyValue::Exact(StyleBorderBottomStyle {
9
                        inner: BorderStyle::Solid,
9
                    })),
9
                    left: Some(CssPropertyValue::Exact(StyleBorderLeftStyle {
9
                        inner: BorderStyle::Solid,
9
                    })),
9
                };
9
                let radius_px = PixelValue::px(border.radius.unwrap_or(0.0));
9
                let border_radius = StyleBorderRadius {
9
                    top_left: radius_px,
9
                    top_right: radius_px,
9
                    bottom_left: radius_px,
9
                    bottom_right: radius_px,
9
                };
9

            
9
                self.push_border(
9
                    bounds,
9
                    border_widths,
9
                    border_colors,
9
                    border_styles,
9
                    border_radius,
9
                );
18
            }
520122
        }
520149
    }
    /// Push a linear gradient background
46
    pub(crate) fn push_linear_gradient(
46
        &mut self,
46
        bounds: LogicalRect,
46
        gradient: LinearGradient,
46
        border_radius: BorderRadius,
46
    ) {
46
        self.push_item(DisplayListItem::LinearGradient {
46
            bounds: bounds.into(),
46
            gradient,
46
            border_radius,
46
        });
46
    }
    /// Push a radial gradient background
10
    pub(crate) fn push_radial_gradient(
10
        &mut self,
10
        bounds: LogicalRect,
10
        gradient: RadialGradient,
10
        border_radius: BorderRadius,
10
    ) {
10
        self.push_item(DisplayListItem::RadialGradient {
10
            bounds: bounds.into(),
10
            gradient,
10
            border_radius,
10
        });
10
    }
    /// Push a conic gradient background
1
    pub(crate) fn push_conic_gradient(
1
        &mut self,
1
        bounds: LogicalRect,
1
        gradient: ConicGradient,
1
        border_radius: BorderRadius,
1
    ) {
1
        self.push_item(DisplayListItem::ConicGradient {
1
            bounds: bounds.into(),
1
            gradient,
1
            border_radius,
1
        });
1
    }
552
    pub(crate) fn push_selection_rect(
552
        &mut self,
552
        bounds: LogicalRect,
552
        color: ColorU,
552
        border_radius: BorderRadius,
552
    ) {
552
        if color.a > 0 {
551
            self.push_item(DisplayListItem::SelectionRect {
551
                bounds: bounds.into(),
551
                color,
551
                border_radius,
551
            });
551
        }
552
    }
2255
    pub(crate) fn push_cursor_rect(&mut self, bounds: LogicalRect, color: ColorU) {
        // Always emit the caret item — even with alpha == 0 in the blink-off phase — so
        // the display-list item COUNT stays stable across blink phases. That lets
        // compute_display_list_damage diff it down to a caret-sized rect instead of
        // falling back to a full-window repaint every ~530ms.
2255
        self.push_item(DisplayListItem::CursorRect { bounds: bounds.into(), color });
2255
    }
3369
    pub(crate) fn push_clip(&mut self, bounds: LogicalRect, border_radius: BorderRadius) {
3369
        self.push_item(DisplayListItem::PushClip {
3369
            bounds: bounds.into(),
3369
            border_radius,
3369
        });
3369
    }
3371
    pub(crate) fn pop_clip(&mut self) {
3371
        self.push_item(DisplayListItem::PopClip);
3371
    }
46
    pub(crate) fn push_image_mask_clip(&mut self, bounds: LogicalRect, mask_image: ImageRef, mask_rect: LogicalRect) {
46
        self.push_item(DisplayListItem::PushImageMaskClip {
46
            bounds: bounds.into(),
46
            mask_image,
46
            mask_rect: mask_rect.into(),
46
        });
46
    }
46
    pub(crate) fn pop_image_mask_clip(&mut self) {
46
        self.push_item(DisplayListItem::PopImageMaskClip);
46
    }
462
    pub(crate) fn push_scroll_frame(
462
        &mut self,
462
        clip_bounds: LogicalRect,
462
        content_size: LogicalSize,
462
        scroll_id: LocalScrollId,
462
    ) {
462
        self.push_item(DisplayListItem::PushScrollFrame {
462
            clip_bounds: clip_bounds.into(),
462
            content_size,
462
            scroll_id,
462
        });
462
    }
462
    pub(crate) fn pop_scroll_frame(&mut self) {
462
        self.push_item(DisplayListItem::PopScrollFrame);
462
    }
190
    pub(crate) fn push_virtual_view_placeholder(
190
        &mut self,
190
        node_id: NodeId,
190
        bounds: LogicalRect,
190
        clip_rect: LogicalRect,
190
    ) {
190
        self.push_item(DisplayListItem::VirtualViewPlaceholder {
190
            node_id,
190
            bounds: bounds.into(),
190
            clip_rect: clip_rect.into(),
190
        });
190
    }
378501
    pub(crate) fn push_border(
378501
        &mut self,
378501
        bounds: LogicalRect,
378501
        widths: StyleBorderWidths,
378501
        colors: StyleBorderColors,
378501
        styles: StyleBorderStyles,
378501
        border_radius: StyleBorderRadius,
378501
    ) {
        // Check if any border side is visible
378501
        let has_visible_border = {
378501
            let has_width = widths.top.is_some()
8
                || widths.right.is_some()
8
                || widths.bottom.is_some()
8
                || widths.left.is_some();
378501
            let has_style = styles.top.is_some()
8
                || styles.right.is_some()
8
                || styles.bottom.is_some()
8
                || styles.left.is_some();
378501
            has_width && has_style
        };
378501
        if has_visible_border {
378492
            self.push_item(DisplayListItem::Border {
378492
                bounds: bounds.into(),
378492
                widths,
378492
                colors,
378492
                styles,
378492
                border_radius,
378492
            });
378492
        }
378501
    }
9192
    pub(crate) fn push_stacking_context(&mut self, z_index: i32, bounds: LogicalRect) {
9192
        self.push_item(DisplayListItem::PushStackingContext { z_index, bounds: bounds.into() });
9192
    }
9191
    pub(crate) fn pop_stacking_context(&mut self) {
9191
        self.push_item(DisplayListItem::PopStackingContext);
9191
    }
91
    pub(crate) fn push_reference_frame(
91
        &mut self,
91
        transform_key: TransformKey,
91
        initial_transform: ComputedTransform3D,
91
        bounds: LogicalRect,
91
    ) {
91
        self.push_item(DisplayListItem::PushReferenceFrame {
91
            transform_key,
91
            initial_transform,
91
            bounds: bounds.into(),
91
        });
91
    }
91
    pub(crate) fn pop_reference_frame(&mut self) {
91
        self.push_item(DisplayListItem::PopReferenceFrame);
91
    }
524482
    pub(crate) fn push_text_run(
524482
        &mut self,
524482
        glyphs: Vec<GlyphInstance>,
524482
        font_hash: FontHash, // Just the hash, not the full FontRef
524482
        font_size_px: f32,
524482
        color: ColorU,
524482
        clip_rect: LogicalRect,
524482
        source_node_index: Option<usize>,
524482
        uniform_bg: Option<(ColorU, WindowLogicalRect)>,
524482
    ) {
524482
        self.debug_log(format!(
524482
            "[push_text_run] {} glyphs, font_size={}px, color=({},{},{},{}), clip={:?}",
524482
            glyphs.len(),
            font_size_px,
            color.r,
            color.g,
            color.b,
            color.a,
            clip_rect
        ));
524482
        if !glyphs.is_empty() && color.a > 0 {
524480
            self.next_text_bg = uniform_bg;
524480
            self.push_item(DisplayListItem::Text {
524480
                glyphs,
524480
                font_hash,
524480
                font_size_px,
524480
                color,
524480
                clip_rect: clip_rect.into(),
524480
                source_node_index,
524480
            });
524480
        } else {
2
            self.debug_log(format!(
2
                "[push_text_run] SKIPPED: glyphs.is_empty()={}, color.a={}",
2
                glyphs.is_empty(),
2
                color.a
2
            ));
2
        }
524482
    }
8604
    pub(crate) fn push_text_layout(
8604
        &mut self,
8604
        layout: Arc<dyn std::any::Any + Send + Sync>,
8604
        bounds: LogicalRect,
8604
        font_hash: FontHash,
8604
        font_size_px: f32,
8604
        color: ColorU,
8604
    ) {
8604
        if color.a > 0 {
8604
            self.push_item(DisplayListItem::TextLayout {
8604
                layout,
8604
                bounds: bounds.into(),
8604
                font_hash,
8604
                font_size_px,
8604
                color,
8604
            });
8604
        }
8604
    }
599
    pub(crate) fn push_underline(&mut self, bounds: LogicalRect, color: ColorU, thickness: f32) {
599
        if color.a > 0 && thickness > 0.0 {
595
            self.push_item(DisplayListItem::Underline {
595
                bounds: bounds.into(),
595
                color,
595
                thickness,
595
            });
598
        }
599
    }
4
    pub(crate) fn push_strikethrough(&mut self, bounds: LogicalRect, color: ColorU, thickness: f32) {
4
        if color.a > 0 && thickness > 0.0 {
            self.push_item(DisplayListItem::Strikethrough {
                bounds: bounds.into(),
                color,
                thickness,
            });
4
        }
4
    }
5
    pub(crate) fn push_overline(&mut self, bounds: LogicalRect, color: ColorU, thickness: f32) {
5
        if color.a > 0 && thickness > 0.0 {
            self.push_item(DisplayListItem::Overline {
                bounds: bounds.into(),
                color,
                thickness,
            });
5
        }
5
    }
49
    pub(crate) fn push_image(&mut self, bounds: LogicalRect, image: ImageRef, border_radius: BorderRadius) {
49
        self.push_item(DisplayListItem::Image { bounds: bounds.into(), image, border_radius });
49
    }
}
/// Main entry point for generating the display list.
#[allow(clippy::implicit_hasher)] // internal helper; only ever called with the default-hasher HashMap/HashSet
/// # Errors
///
/// Returns a `LayoutError` if display-list generation fails.
3016
pub fn generate_display_list<T: ParsedFontTrait + Sync + 'static>(
3016
    ctx: &mut LayoutContext<'_, T>,
3016
    tree: &LayoutTree,
3016
    calculated_positions: &super::PositionVec,
3016
    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
3016
    scroll_ids: &HashMap<LayoutNodeId, u64>,
3016
    gpu_value_cache: Option<&GpuValueCache>,
3016
    renderer_resources: &RendererResources,
3016
    id_namespace: IdNamespace,
3016
    dom_id: DomId,
3016
) -> Result<DisplayList> {
3016
    generate_display_list_impl(
3016
        ctx, tree, calculated_positions, scroll_offsets, scroll_ids,
3016
        gpu_value_cache, renderer_resources, id_namespace, dom_id, None,
    )
3016
}
/// [`generate_display_list`] + the DL-PATCHING source. With `patch` armed
/// (resize-skip passes), unchanged nodes' paint calls are substituted by
/// their previous items translated by the node's position delta — only
/// re-flowed IFCs and size-changed nodes actually re-emit.
#[allow(clippy::too_many_arguments)]
7727
pub fn generate_display_list_impl<T: ParsedFontTrait + Sync + 'static>(
7727
    ctx: &mut LayoutContext<'_, T>,
7727
    tree: &LayoutTree,
7727
    calculated_positions: &super::PositionVec,
7727
    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
7727
    scroll_ids: &HashMap<LayoutNodeId, u64>,
7727
    gpu_value_cache: Option<&GpuValueCache>,
7727
    renderer_resources: &RendererResources,
7727
    id_namespace: IdNamespace,
7727
    dom_id: DomId,
7727
    patch: Option<PatchState<'_>>,
7727
) -> Result<DisplayList> {
7727
    debug_info!(
4198
        ctx,
4198
        "[DisplayList] generate_display_list: tree has {} nodes, {} positions calculated",
4198
        tree.nodes.len(),
4198
        calculated_positions.len()
    );
    // AZ_PROFILE=cpu breakdown of the DL build. Inert (one relaxed load)
    // unless recording is on — see probe::set_recording.
7727
    let _dl_span = crate::probe::Probe::span("dl_generate");
7727
    debug_info!(ctx, "Starting display list generation");
7727
    debug_info!(
4198
        ctx,
4198
        "Collecting stacking contexts from root node {}",
        tree.root
    );
7727
    let positioned_tree = PositionedTree {
7727
        tree,
7727
        calculated_positions,
7727
    };
7727
    let mut generator = DisplayListGenerator::new(
7727
        ctx,
7727
        scroll_offsets,
7727
        &positioned_tree,
7727
        scroll_ids,
7727
        gpu_value_cache,
7727
        renderer_resources,
7727
        id_namespace,
7727
        dom_id,
    );
7727
    generator.patch = patch;
    // Create builder with debug enabled if ctx has debug messages
7727
    let debug_enabled = generator.ctx.debug_messages.is_some();
7727
    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.
    {
7727
        let root_node = tree.get(LayoutNodeId::new(tree.root));
7727
        if let Some(root) = root_node {
7727
            if let Some(root_dom_id) = root.dom_node_id {
7727
                let root_state = generator.get_styled_node_state(root_dom_id);
7727
                let canvas_bg = get_background_color(
7727
                    generator.ctx.styled_dom,
7727
                    root_dom_id,
7727
                    &root_state,
                );
7727
                if canvas_bg.a > 0 {
2183
                    let viewport_rect = LogicalRect {
2183
                        origin: LogicalPosition::zero(),
2183
                        size: generator.ctx.viewport_size,
2183
                    };
2183
                    builder.push_rect(viewport_rect, canvas_bg, BorderRadius::default());
2183
                    debug_info!(
166
                        generator.ctx,
166
                        "[DisplayList] Canvas background: color=({},{},{},{}), size={:?}",
                        canvas_bg.r, canvas_bg.g, canvas_bg.b, canvas_bg.a,
                        generator.ctx.viewport_size
                    );
5544
                }
            }
        }
    }
    // +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)
7727
    let stacking_context_tree = {
7727
        let _p = crate::probe::Probe::span("dl_collect_stacking");
7727
        generator.collect_stacking_contexts(tree.root)?
    };
    // 2. Traverse the stacking context tree to generate display items in the correct order.
7727
    debug_info!(
4198
        generator.ctx,
4198
        "Generating display items from stacking context tree"
    );
7727
    generator.generate_for_stacking_context(&mut builder, &stacking_context_tree)?;
    // Build display list and transfer debug messages to context
7727
    let display_list = builder.build_with_debug(generator.ctx.debug_messages);
7727
    debug_info!(
4198
        generator.ctx,
4198
        "[DisplayList] Generated {} display items",
4198
        display_list.items.len()
    );
7727
    Ok(display_list)
7727
}
/// 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<LayoutNodeId, u64>,
    gpu_value_cache: Option<&'a GpuValueCache>,
    renderer_resources: &'a RendererResources,
    id_namespace: IdNamespace,
    dom_id: DomId,
    /// DL-PATCHING source (resize-skip passes only): the previous pass's
    /// display list plus per-node deltas. `None` = full generation.
    patch: Option<PatchState<'a>>,
}
/// State for patched display-list generation. Built by `layout_document`
/// on a resize-skip pass — the tree object and its indices are unchanged,
/// so per-(layout node, phase) item runs from the PREVIOUS list can replace
/// the paint calls of every node that neither re-flowed its inline content
/// nor changed size; their geometry moves by the node's position delta.
pub(crate) struct PatchState<'a> {
    prev: &'a DisplayList,
    /// Per (layout idx, phase): queue of item ranges in `prev`, walk order.
    runs: HashMap<(usize, u8), std::collections::VecDeque<(usize, usize)>>,
    /// Per layout idx: (`new_pos` - `old_pos`).
    deltas: Vec<LogicalPosition>,
    /// Nodes that MUST re-emit (re-flowed IFC, size changed).
    reemit: std::collections::BTreeSet<usize>,
}
/// Presentation summary of a patched pass: which single translation moved
/// most of the frame, and which rects must be repainted anyway. Feeds the
/// CPU compositor's translate-aware presentation (blit the retained pixmap
/// by `dominant_delta`, repaint `exceptions` + exposed strips) — the round-3
/// path that makes a drag frame memory-bandwidth instead of rendering.
#[derive(Debug, Clone)]
pub struct PatchMoveSummary {
    /// The translation shared by the largest moved item AREA (logical px).
    pub dominant_delta: LogicalPosition,
    /// OLD rects of the dominant movers, one blit each. Per-rect (not a
    /// union): the union would include the gaps BETWEEN movers — static
    /// backdrop that must not be dragged. Only movers that paint an OPAQUE
    /// background over their own rect qualify (a translucent mover's pixels
    /// are a blend with the static backdrop, which does not translate).
    pub mover_rects_old: Vec<LogicalRect>,
    /// Union of `mover_rects_old` — the region the translated DIFF may
    /// classify as moves.
    pub moved_region_old: LogicalRect,
    /// Rects that must repaint regardless: re-emitted nodes, off-delta
    /// movers, non-opaque movers, and NON-ANCESTOR static nodes overlapping
    /// a mover (ancestors paint BELOW their descendants — the root
    /// background under a page is covered by the opaque page and never
    /// dragged visibly; a static sibling painted ABOVE would be) — each as
    /// old∪new bounds.
    pub exceptions: Vec<LogicalRect>,
}
/// Compute a [`PatchMoveSummary`] from the same inputs `PatchState::build`
/// consumes. Returns `None` when the frame has no dominant translation
/// (nothing moved, or the exception list would exceed its cap — at which
/// point a plain full repaint is the better deal).
#[allow(clippy::too_many_lines)]
44
#[must_use] pub fn compute_patch_move_summary(
44
    prev_positions: &[LogicalPosition],
44
    new_positions: &[LogicalPosition],
44
    prev_sizes: &[Option<LogicalSize>],
44
    new_sizes: &[Option<LogicalSize>],
44
    reemit: &std::collections::BTreeSet<usize>,
44
    // parents[i] = layout parent of node i (ancestor exclusion);
44
    // opaque_bg[i] = node i paints a fully opaque background over its own
44
    // rect (blit eligibility for movers).
44
    parents: &[Option<usize>],
44
    opaque_bg: &[bool],
44
) -> Option<PatchMoveSummary> {
    const EPS: f32 = 0.01;
    const MAX_EXCEPTIONS: usize = 64;
44
    let n = prev_positions
44
        .len()
44
        .min(new_positions.len())
44
        .min(prev_sizes.len())
44
        .min(new_sizes.len());
    // Group movers by exact delta bits, weighted by new area.
44
    let mut groups: HashMap<(u32, u32), f32> =
44
        HashMap::new();
588
    for i in 0..n {
588
        if reemit.contains(&i) {
239
            continue;
349
        }
349
        let dx = new_positions[i].x - prev_positions[i].x;
349
        let dy = new_positions[i].y - prev_positions[i].y;
349
        if dx.abs() < EPS && dy.abs() < EPS {
347
            continue;
2
        }
2
        let area = new_sizes[i]
2
            .map_or(1.0, |s| (s.width * s.height).max(1.0));
2
        *groups.entry((dx.to_bits(), dy.to_bits())).or_insert(0.0) += area;
    }
44
    let ((dxb, dyb), _) = groups
44
        .into_iter()
44
        .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(core::cmp::Ordering::Equal))?;
1
    let dominant = LogicalPosition {
1
        x: f32::from_bits(dxb),
1
        y: f32::from_bits(dyb),
1
    };
1
    let rect_of = |pos: LogicalPosition, size: Option<LogicalSize>| -> Option<LogicalRect> {
        let s = size?;
        if s.width <= 0.0 || s.height <= 0.0 || !pos.x.is_finite() || !pos.y.is_finite() {
            return None;
        }
        Some(LogicalRect { origin: pos, size: s })
    };
1
    let union = |a: Option<LogicalRect>, b: LogicalRect| -> LogicalRect {
        match a {
            None => b,
            Some(a) => {
                let x0 = a.origin.x.min(b.origin.x);
                let y0 = a.origin.y.min(b.origin.y);
                let x1 = (a.origin.x + a.size.width).max(b.origin.x + b.size.width);
                let y1 = (a.origin.y + a.size.height).max(b.origin.y + b.size.height);
                LogicalRect {
                    origin: LogicalPosition { x: x0, y: y0 },
                    size: LogicalSize { width: x1 - x0, height: y1 - y0 },
                }
            }
        }
    };
1
    let intersects = |a: &LogicalRect, b: &LogicalRect| -> bool {
        a.origin.x < b.origin.x + b.size.width
            && b.origin.x < a.origin.x + a.size.width
            && a.origin.y < b.origin.y + b.size.height
            && b.origin.y < a.origin.y + a.size.height
    };
    // Pass 1: dominant movers. Only OPAQUE-background movers may blit;
    // the rest become exceptions in pass 2. Keep only TOP-LEVEL mover
    // rects (drop movers whose ancestor is already a collected mover —
    // the ancestor's blit carries them).
    const MAX_MOVERS: usize = 32;
1
    let mut is_dominant_mover = vec![false; n];
7
    for i in 0..n {
7
        if reemit.contains(&i) {
1
            continue;
6
        }
6
        let dx = new_positions[i].x - prev_positions[i].x;
6
        let dy = new_positions[i].y - prev_positions[i].y;
6
        if (dx - dominant.x).abs() < EPS && (dy - dominant.y).abs() < EPS
2
            && (dx.abs() >= EPS || dy.abs() >= EPS)
2
        {
2
            is_dominant_mover[i] = true;
4
        }
    }
1
    let covered_by_mover_ancestor = |i: usize| -> bool {
        let mut cur = parents.get(i).copied().flatten();
        while let Some(p) = cur {
            if is_dominant_mover.get(p).copied().unwrap_or(false)
                && opaque_bg.get(p).copied().unwrap_or(false)
            {
                return true;
            }
            cur = parents.get(p).copied().flatten();
        }
        false
    };
1
    let mut mover_rects_old: Vec<LogicalRect> = Vec::new();
1
    let mut moved_region: Option<LogicalRect> = None;
7
    for i in 0..n {
7
        if !is_dominant_mover[i]
2
            || !opaque_bg.get(i).copied().unwrap_or(false)
            || covered_by_mover_ancestor(i)
        {
7
            continue;
        }
        if let Some(r) = rect_of(prev_positions[i], prev_sizes[i]) {
            mover_rects_old.push(r);
            if mover_rects_old.len() > MAX_MOVERS {
                return None; // too many independent blits — full repaint
            }
            moved_region = Some(union(moved_region, r));
        }
    }
1
    let moved_region_old = moved_region?;
    // Descendant-of-blitted-mover test for pass 2: a static (or any) node
    // whose OPAQUE mover ancestor blits is carried by that blit and needs
    // no exception; equally, dominant movers inside a blitted ancestor are
    // fine. Everything else that touches a mover rect must repaint.
    let inside_blit = |i: usize| -> bool {
        is_dominant_mover[i] && opaque_bg.get(i).copied().unwrap_or(false)
            || covered_by_mover_ancestor(i)
    };
    // Pass 2: exceptions — re-emitted nodes, off-delta movers, non-opaque
    // dominant movers outside any blitted ancestor, and NON-ANCESTOR
    // static nodes overlapping a blitted mover. Ancestors of a mover paint
    // BELOW it (covered by the mover's opaque background) — no exception.
    let is_ancestor_of_any_mover = |i: usize| -> bool {
        // ancestor test via the parent chains of the collected movers is
        // O(movers × depth); movers are ≤32 and depth is small.
        for (j, dom) in is_dominant_mover.iter().enumerate() {
            if !dom || !opaque_bg.get(j).copied().unwrap_or(false) {
                continue;
            }
            let mut cur = parents.get(j).copied().flatten();
            while let Some(p) = cur {
                if p == i {
                    return true;
                }
                cur = parents.get(p).copied().flatten();
            }
        }
        false
    };
    let mut exceptions: Vec<LogicalRect> = Vec::new();
    for i in 0..n {
        let dx = new_positions[i].x - prev_positions[i].x;
        let dy = new_positions[i].y - prev_positions[i].y;
        let is_static = dx.abs() < EPS && dy.abs() < EPS;
        let old_r = rect_of(prev_positions[i], prev_sizes[i]);
        let new_r = rect_of(new_positions[i], new_sizes[i]);
        let needs_exception = if reemit.contains(&i) {
            // NOT an exception: a re-emitted node's items genuinely differ
            // (fresh content / new size), so the translated DIFF damages
            // them itself — old∪new bounds — which also repaints over any
            // pixels a blit dragged through them. Listing them here
            // double-covered the damage and, for the size-changed ROOT
            // (width:100% on every resize), turned the whole viewport into
            // an exception — the blit was decorative and the NC could not
            // go red.
            false
        } else if inside_blit(i) {
            false
        } else if is_static {
            // Static content BELOW the movers (their ancestors) is covered
            // by the opaque blits; anything else touching a mover rect
            // would be visibly dragged or stamped over — repaint it.
            !is_ancestor_of_any_mover(i)
                && old_r.is_some_and(|r| {
                    mover_rects_old.iter().any(|m| intersects(&r, m))
                })
        } else {
            true // off-delta mover (or non-opaque dominant outside a blit)
        };
        if needs_exception {
            let mut acc: Option<LogicalRect> = None;
            if let Some(r) = old_r {
                acc = Some(union(acc, r));
            }
            if let Some(r) = new_r {
                acc = Some(union(acc, r));
            }
            if let Some(r) = acc {
                exceptions.push(r);
                if exceptions.len() > MAX_EXCEPTIONS {
                    return None; // too fragmented — full repaint is cheaper
                }
            }
        }
    }
    Some(PatchMoveSummary {
        dominant_delta: dominant,
        mover_rects_old,
        moved_region_old,
        exceptions,
    })
44
}
impl<'a> PatchState<'a> {
44
    pub(crate) fn build(
44
        prev: &'a DisplayList,
44
        prev_positions: &[LogicalPosition],
44
        new_positions: &[LogicalPosition],
44
        prev_sizes: &[Option<LogicalSize>],
44
        new_sizes: &[Option<LogicalSize>],
44
        reflowed_ifcs: &std::collections::BTreeSet<usize>,
44
    ) -> Option<Self> {
        // The tagging is as old as the patcher — a cached list from before
        // the tagging (or a page slice) cannot be patched.
44
        if prev.layout_node_mapping.len() != prev.items.len() {
            return None;
44
        }
44
        let n = new_positions.len().min(prev_positions.len());
44
        let mut reemit: std::collections::BTreeSet<usize> = reflowed_ifcs.clone();
588
        for i in 0..n.min(prev_sizes.len()).min(new_sizes.len()) {
588
            if prev_sizes[i] != new_sizes[i] {
28
                reemit.insert(i);
560
            }
        }
44
        let mut deltas = vec![LogicalPosition::zero(); new_positions.len()];
588
        for i in 0..n {
588
            deltas[i] = LogicalPosition {
588
                x: new_positions[i].x - prev_positions[i].x,
588
                y: new_positions[i].y - prev_positions[i].y,
588
            };
588
        }
        // Contiguous same-(node,phase) runs, in list order.
44
        let mut runs: HashMap<(usize, u8), std::collections::VecDeque<(usize, usize)>> =
44
            HashMap::new();
44
        let mut i = 0usize;
820
        while i < prev.items.len() {
776
            let tag = prev.layout_node_mapping[i];
776
            let start = i;
2445
            while i < prev.items.len() && prev.layout_node_mapping[i] == tag {
1669
                i += 1;
1669
            }
776
            if let Some((node, phase)) = tag {
667
                if !matches!(phase, EmitPhase::ScWalk) {
667
                    if prev.items[start..i].iter().all(patchable_item) {
667
                        runs.entry((node, phase as u8))
667
                            .or_default()
667
                            .push_back((start, i));
667
                    } else {
                        // The run carries state beyond translatable geometry
                        // (virtual view, filter, …) — this node re-emits.
                        reemit.insert(node);
                    }
                }
109
            }
        }
44
        Some(Self { prev, runs, deltas, reemit })
44
    }
}
// +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,
{
7727
    pub(crate) const fn new(
7727
        ctx: &'a mut LayoutContext<'b, T>,
7727
        scroll_offsets: &'a BTreeMap<NodeId, ScrollPosition>,
7727
        positioned_tree: &'a PositionedTree<'a>,
7727
        scroll_ids: &'a HashMap<LayoutNodeId, u64>,
7727
        gpu_value_cache: Option<&'a GpuValueCache>,
7727
        renderer_resources: &'a RendererResources,
7727
        id_namespace: IdNamespace,
7727
        dom_id: DomId,
7727
    ) -> Self {
7727
        Self {
7727
            ctx,
7727
            scroll_offsets,
7727
            positioned_tree,
7727
            scroll_ids,
7727
            gpu_value_cache,
7727
            renderer_resources,
7727
            id_namespace,
7727
            dom_id,
7727
            patch: None,
7727
        }
7727
    }
    /// Prove the SOLID OPAQUE color a text run inside this IFC sits on, or
    /// `None`. Correct-by-construction: only claims a background when the
    /// nearest layout ancestor that paints ANY background paints exactly one
    /// solid opaque color (no gradients/images, no translucency) and no
    /// selection touches the node (selection rects paint between background
    /// and glyphs). No claim for canvas-direct text — the clear color is a
    /// renderer detail (`AZ_DEBUG_FILL` can change it).
277387
    fn compute_uniform_text_bg(
277387
        &self,
277387
        source_node_index: usize,
277387
    ) -> Option<(ColorU, WindowLogicalRect)> {
277387
        let tree = self.positioned_tree.tree;
277387
        let dom_id_opt = tree.get(LayoutNodeId::new(source_node_index)).and_then(|n| n.dom_node_id);
1416
        if let (Some(sel), Some(dom_id)) = (
277387
            self.ctx.text_selections.get(&self.ctx.styled_dom.dom_id),
277387
            dom_id_opt,
        ) {
1416
            if sel.affected_nodes.contains_key(&dom_id) {
542
                return None;
874
            }
275971
        }
276845
        let mut cur = Some(source_node_index);
656338
        while let Some(idx) = cur {
637909
            if let Some(dom_id) = tree.get(LayoutNodeId::new(idx)).and_then(|n| n.dom_node_id) {
637779
                let state = self.get_styled_node_state(dom_id);
637779
                let contents = get_background_contents(
637779
                    self.ctx.styled_dom,
637779
                    dom_id,
637779
                    &state,
                );
637779
                let mut saw_any = false;
648822
                for c in &contents {
                    use azul_css::props::style::StyleBackgroundContent as B;
11043
                    match c {
269317
                        B::Color(col) if col.a == 255 => {
                            // The PROOF's extent: this node's painted rect.
                            // The LCD fringe of a glyph at the very edge of
                            // it spills onto UNPROVEN pixels — the raster
                            // must sweep those glyphs (a pre-blended tile
                            // stamped across the boundary bakes the wrong
                            // neighbour into the fringe; caught by the
                            // round-3 blit gate as an 8-pixel divergence at
                            // the page edge).
258274
                            let rect = self.get_paint_rect(idx)?;
258274
                            return Some((*col, rect.into()));
                        }
11043
                        B::Color(col) if col.a == 0 => {}
                        // translucent color, gradient or image — unprovable
142
                        _ => return None,
                    }
11043
                    saw_any = true;
                }
379363
                let _ = saw_any;
130
            }
379493
            cur = tree.get(LayoutNodeId::new(idx)).and_then(|n| n.parent);
        }
18429
        None
277387
    }
    /// DL-PATCHING substitution: if a patch source is armed and this node's
    /// (phase) run can be reused, copy it translated by the node's position
    /// delta and report `true` (the caller skips painting). `false` = paint
    /// normally (no patch, node in the re-emit set, or no cached run left —
    /// the last one falls back to a fresh paint, which is always correct).
1281294
    fn try_copy_cached_run(
1281294
        &mut self,
1281294
        builder: &mut DisplayListBuilder,
1281294
        node_index: usize,
1281294
        phase: EmitPhase,
1281294
    ) -> bool {
1281294
        let Some(patch) = self.patch.as_mut() else {
1280082
            return false;
        };
1212
        if patch.reemit.contains(&node_index) {
478
            return false;
734
        }
734
        let Some(run) = patch
734
            .runs
734
            .get_mut(&(node_index, phase as u8))
734
            .and_then(std::collections::VecDeque::pop_front)
        else {
            // No cached run for this call. For a node that emitted nothing
            // last pass this is the COMMON case — and emitting nothing again
            // is exactly right, so treat it as a successful (empty) copy.
            // A node that DID emit but whose run count changed cannot occur
            // without a reflow/size change (both force re-emit).
523
            drop(crate::probe::Probe::span("dl_patch_empty"));
523
            return true;
        };
211
        let delta = patch.deltas.get(node_index).copied().unwrap_or_else(LogicalPosition::zero);
211
        drop(crate::probe::Probe::span("dl_patch_copy"));
        // DOM attribution is PER ITEM, not per run: a content run can switch
        // attribution mid-run (e.g. an IFC root's items ending with a text
        // child's id) — the golden test caught a first-item flattening here.
211
        let saved_node = builder.current_node;
211
        builder.set_current_layout(Some((node_index, phase)));
382
        for i in run.0..run.1 {
382
            builder.set_current_node(patch.prev.node_mapping.get(i).copied().flatten());
382
            builder.next_text_bg = patch.prev.uniform_text_bgs.get(i).copied().flatten();
382
            let item = translate_item(patch.prev.items[i].clone(), delta);
382
            builder.push_item(item);
382
        }
211
        builder.set_current_layout(None);
211
        builder.set_current_node(saved_node);
211
        true
1281294
    }
    /// Helper to get styled node state for a node
5671438
    fn get_styled_node_state(&self, dom_id: NodeId) -> azul_core::styled_dom::StyledNodeState {
5671438
        self.ctx
5671438
            .styled_dom
5671438
            .styled_nodes
5671438
            .as_container()
5671438
            .get(dom_id)
5671438
            .map(|n| n.styled_node_state)
5671438
            .unwrap_or_default()
5671438
    }
    // +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.
1928957
    fn is_node_hidden(&self, node_index: usize) -> bool {
        use azul_css::props::style::effects::StyleVisibility;
1928957
        let Some(node) = self.positioned_tree.tree.get(LayoutNodeId::new(node_index)) else {
            return false;
        };
1928957
        let Some(dom_id) = node.dom_node_id else {
393
            return false;
        };
1928564
        let node_state = self.get_styled_node_state(dom_id);
1928564
        matches!(
1928564
            get_visibility(self.ctx.styled_dom, dom_id, &node_state),
            crate::solver3::getters::MultiValue::Exact(
                StyleVisibility::Hidden | StyleVisibility::Collapse
            )
        )
1928957
    }
    /// Gets the cursor type for a text node from its CSS properties.
    /// Defaults to Text (I-beam) cursor if no explicit cursor is set.
    #[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
507853
    fn get_cursor_type_for_text_node(&self, node_id: NodeId) -> CursorType {
        use azul_css::props::style::effects::StyleCursor;
507853
        let styled_node_state = self.get_styled_node_state(node_id);
507853
        let node_data_container = self.ctx.styled_dom.node_data.as_container();
507853
        let node_data = node_data_container.get(node_id);
        // Query the cursor CSS property for this text node
507853
        if let Some(node_data) = node_data {
507853
            if let Some(CssPropertyValue::Exact(cursor)) = self.ctx.styled_dom.get_css_property_cache().get_cursor(
507853
                node_data,
507853
                &node_id,
507853
                &styled_node_state,
            ) {
507853
                    return match cursor {
21267
                        StyleCursor::Default => CursorType::Default,
9183
                        StyleCursor::Pointer => CursorType::Pointer,
477403
                        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
507853
    }
    /// Emits drawing commands for text selections only (not cursor).
    /// The cursor is drawn separately via `paint_cursor()`.
640647
    fn paint_selections(
640647
        &self,
640647
        builder: &mut DisplayListBuilder,
640647
        node_index: usize,
640647
    ) -> Result<()> {
640647
        let node = self
640647
            .positioned_tree
640647
            .tree
640647
            .get(LayoutNodeId::new(node_index))
640647
            .ok_or(LayoutError::InvalidTree)?;
640647
        let Some(dom_id) = node.dom_node_id else {
131
            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
        // (d6h) Materialized: sentinel-safe caret/selection geometry.
640516
        let Some(layout) = self.positioned_tree.tree.materialized_inline_layout_for_node(node_index) else {
363246
            return Ok(());
        };
        // Get the absolute position of this node (border-box position)
277270
        let node_pos = self
277270
            .positioned_tree
277270
            .calculated_positions
277270
            .get(node_index)
277270
            .copied()
277270
            .unwrap_or_default();
        // Selection rects from `get_selection_rects` are in the IFC root's content-box
        // coordinate space. For an inline text node, the node's OWN box position is never
        // assigned (stays the `f32::MIN` sentinel), so we must anchor to the IFC root's
        // position + padding/border — exactly the box that owns the inline layout. (The
        // caret avoids this because paint_cursor runs on the IFC-root node directly.)
277270
        let anchor = self.ifc_content_box_origin(node_index, node_pos);
277270
        let content_box_offset_x = anchor.x;
277270
        let content_box_offset_y = anchor.y;
        // Check if text is selectable (respects CSS user-select property)
277270
        let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
277270
        let is_selectable = super::getters::is_text_selectable(self.ctx.styled_dom, dom_id, node_state);
277270
        if !is_selectable {
36099
            return Ok(());
241171
        }
        // === NEW: Check text_selections first (multi-node selection support) ===
241171
        if let Some(text_selection) = self.ctx.text_selections.get(&self.ctx.styled_dom.dom_id) {
1416
            if let Some(ranges) = text_selection.affected_nodes.get(&dom_id) {
542
                let is_collapsed = text_selection.is_collapsed();
                // Only draw selection highlight if NOT collapsed
542
                if !is_collapsed {
                    // A multi-cursor session (Ctrl+D) puts EVERY occurrence on
                    // this one node, so every range gets its own rects.
542
                    let rects: Vec<LogicalRect> = ranges
542
                        .iter()
551
                        .flat_map(|range| layout.get_selection_rects(range))
542
                        .collect();
542
                    let style = get_selection_style(self.ctx.styled_dom, Some(dom_id), self.ctx.system_style.as_ref());
542
                    let border_radius = BorderRadius {
542
                        top_left: style.radius,
542
                        top_right: style.radius,
542
                        bottom_left: style.radius,
542
                        bottom_right: style.radius,
542
                    };
1093
                    for mut rect in rects {
551
                        rect.origin.x += content_box_offset_x;
551
                        rect.origin.y += content_box_offset_y;
551
                        builder.push_selection_rect(rect, style.bg_color, border_radius);
551
                    }
                }
542
                return Ok(());
874
            }
239755
        }
240629
        Ok(())
640647
    }
    /// Selection rects covering this IFC, already translated into the same
    /// space as the offset glyph positions, plus the `::selection` text colour
    /// — or `None` when there is nothing to recolour.
    ///
    /// Resolves the selection through `ifc_root_owns_dom_node` rather than off
    /// the IFC root's own `dom_node_id`: an editing session keys `affected_nodes`
    /// on the TEXT node (`initialize_editing` puts the caret there), while this
    /// pass runs on the root that owns the inline layout. A cross-block
    /// selection keys on the block, which is its own IFC root — both resolve.
    /// The `user-select` gate matches `paint_selections` on purpose: the
    /// recolour must cover exactly the glyphs the highlight covers.
    /// The origin of the content box that owns an IFC's inline layout.
    ///
    /// Selection rects come out of `get_selection_rects` in this space. An
    /// inline text node's own box position is never assigned (it keeps the
    /// `f32::MIN` sentinel), so the anchor has to be the IFC ROOT's box — which
    /// is why both the highlight and the recolour resolve it here rather than
    /// each computing its own.
277279
    fn ifc_content_box_origin(
277279
        &self,
277279
        node_index: usize,
277279
        fallback: LogicalPosition,
277279
    ) -> LogicalPosition {
277279
        let ifc_root_index = self.positioned_tree.tree.get_ifc_root_layout_index(node_index);
277279
        let pos = self
277279
            .positioned_tree
277279
            .calculated_positions
277279
            .get(ifc_root_index)
277279
            .copied()
277279
            .unwrap_or(fallback);
277279
        let bp = self
277279
            .positioned_tree
277279
            .tree
277279
            .get(LayoutNodeId::new(ifc_root_index))
277279
            .map(|n| n.box_props.unpack());
277279
        let (pad_left, pad_top, bor_left, bor_top) = bp.map_or((0.0, 0.0, 0.0, 0.0), |b| {
277279
            (b.padding.left, b.padding.top, b.border.left, b.border.top)
277279
        });
277279
        LogicalPosition::new(
277279
            pos.x + pad_left + bor_left,
277279
            pos.y + pad_top + bor_top,
        )
277279
    }
277387
    fn selection_recolour_for_ifc(
277387
        &self,
277387
        source_node_index: usize,
277387
    ) -> Option<(Vec<LogicalRect>, ColorU)> {
277387
        let sel = self.ctx.text_selections.get(&self.ctx.styled_dom.dom_id)?;
1416
        if sel.is_collapsed() {
            return None;
1416
        }
1416
        let (dom_id, ranges) = sel
1416
            .affected_nodes
1416
            .iter()
9563
            .find(|&(node, _)| self.ifc_root_owns_dom_node(source_node_index, *node))?;
542
        let node_state = self.get_styled_node_state(*dom_id);
542
        if !super::getters::is_text_selectable(self.ctx.styled_dom, *dom_id, &node_state) {
            return None;
542
        }
542
        let style = get_selection_style(
542
            self.ctx.styled_dom,
542
            Some(*dom_id),
542
            self.ctx.system_style.as_ref(),
        );
542
        let text_color = style.text_color?;
        // The SAME geometry the highlight is painted from: the sentinel-safe
        // materialized layout, anchored on the IFC root's content box. Reading
        // the raw cached layout here yielded no rects at all, so the recolour
        // silently never applied.
9
        let layout = self
9
            .positioned_tree
9
            .tree
9
            .materialized_inline_layout_for_node(source_node_index)?;
9
        let node_pos = self
9
            .positioned_tree
9
            .calculated_positions
9
            .get(source_node_index)
9
            .copied()
9
            .unwrap_or_default();
9
        let anchor = self.ifc_content_box_origin(source_node_index, node_pos);
9
        let rects: Vec<LogicalRect> = ranges
9
            .iter()
9
            .flat_map(|r| layout.get_selection_rects(r))
9
            .map(|mut r| {
9
                r.origin.x += anchor.x;
9
                r.origin.y += anchor.y;
9
                r
9
            })
9
            .collect();
9
        if rects.is_empty() {
            return None;
9
        }
9
        Some((rects, text_color))
277387
    }
    /// Does the IFC rooted at `node_index` own the inline content of `dom_id`?
    ///
    /// The editing session's text node need not be a DIRECT child of the IFC
    /// root — `p > span > text` puts it one level deeper — so resolve it through
    /// `ifc_membership`, which every inline descendant of the root carries,
    /// instead of scanning the root's children.
14071
    fn ifc_root_owns_dom_node(&self, node_index: usize, dom_id: NodeId) -> bool {
14071
        let tree = self.positioned_tree.tree;
14071
        tree.dom_to_layout.get(&dom_id).is_some_and(|indices| {
14053
            indices
14053
                .iter()
14053
                .any(|&idx| tree.get_ifc_root_layout_index(idx.index()) == node_index)
14053
        })
14071
    }
    /// 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.
    #[allow(clippy::cast_precision_loss)] // bounded graphics/coord/font/fixed-point/debug-marker cast
640647
    fn paint_cursor(
640647
        &self,
640647
        builder: &mut DisplayListBuilder,
640647
        node_index: usize,
640647
    ) -> Result<()> {
        // NOTE: we deliberately do NOT early-return in the blink-off phase. Emitting the
        // caret item every frame — with alpha forced to 0 when invisible (see caret_color
        // below) — keeps the display-list item COUNT stable across blink phases, so
        // compute_display_list_damage yields a tiny caret-sized damage rect instead of
        // bailing to a full-window repaint on every ~530ms blink toggle.
        // Early exit if no cursor locations
640647
        if self.ctx.cursor_locations.is_empty() {
195152
            return Ok(());
445495
        }
445495
        let node = self
445495
            .positioned_tree
445495
            .tree
445495
            .get(LayoutNodeId::new(node_index))
445495
            .ok_or(LayoutError::InvalidTree)?;
445495
        let Some(dom_id) = node.dom_node_id else {
9
            return Ok(());
        };
        // Check if this node is contenteditable
445486
        let is_contenteditable = super::getters::is_node_contenteditable_inherited(self.ctx.styled_dom, dom_id);
445486
        if !is_contenteditable {
439799
            return Ok(());
5687
        }
        // Check if text is selectable
5687
        let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
5687
        let is_selectable = super::getters::is_text_selectable(self.ctx.styled_dom, dom_id, node_state);
5687
        if !is_selectable {
            return Ok(());
5687
        }
        // Get inline layout
        // (d6h) Materialized: sentinel-safe caret/selection geometry.
5687
        let Some(layout) = self.positioned_tree.tree.materialized_inline_layout_for_node(node_index) else {
3001
            return Ok(());
        };
        // Compute content-box offset once
2686
        let node_pos = self
2686
            .positioned_tree
2686
            .calculated_positions
2686
            .get(node_index)
2686
            .copied()
2686
            .unwrap_or_default();
2686
        let bp = node.box_props.unpack();
2686
        let padding = &bp.padding;
2686
        let border = &bp.border;
2686
        let content_box_offset_x = node_pos.x + padding.left + border.left;
2686
        let content_box_offset_y = node_pos.y + padding.top + border.top;
2686
        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.
2686
        let primary_idx_for_this_node = self.ctx.cursor_locations.iter().enumerate()
2686
            .rev()
2686
            .find(|(_, (cd, cn, _))| {
2686
                *cd == self.ctx.styled_dom.dom_id
2686
                    && (*cn == dom_id || self.ifc_root_owns_dom_node(node_index, *cn))
2686
            })
2686
            .map(|(i, _)| i);
2686
        for (i, (cursor_dom_id, cursor_node_id, cursor)) in self.ctx.cursor_locations.iter().enumerate() {
            // Check DOM ID matches
2686
            if self.ctx.styled_dom.dom_id != *cursor_dom_id {
                continue;
2686
            }
            // Check this node contains the cursor
2686
            if dom_id != *cursor_node_id
2254
                && !self.ifc_root_owns_dom_node(node_index, *cursor_node_id)
            {
189
                continue;
2497
            }
            // Get cursor rect from text layout
2497
            let Some(mut rect) = layout.get_cursor_rect(cursor) else {
243
                continue;
            };
2254
            rect.origin.x += content_box_offset_x;
2254
            rect.origin.y += content_box_offset_y;
2254
            rect.size.width = style.width;
            // Blink: keep the caret item present every frame (stable item count for
            // incremental damage) but make it invisible in the off phase by zeroing alpha.
2254
            let caret_color = if self.ctx.cursor_is_visible {
2250
                style.color
            } else {
4
                ColorU { a: 0, ..style.color }
            };
2254
            builder.push_cursor_rect(rect, caret_color);
            // Preedit underline only on the primary cursor for this node
2254
            let is_primary = primary_idx_for_this_node == Some(i);
2254
            if is_primary {
2254
                if let Some(ref preedit) = self.ctx.preedit_text {
216
                    if !preedit.is_empty() {
                        // The composition IS in this layout: apply_preedit_to_
                        // text_cache splices it into the run at the cursor's
                        // byte offset and re-shapes, so the underline can span
                        // the MEASURED advance of the composed clusters. The
                        // old `char_count × max(caret_width, 8px)` guess was
                        // wrong for every script whose advances aren't ~8px —
                        // a CJK cluster is roughly double that.
216
                        let run = cursor.cluster_id.source_run;
216
                        let start_byte = cursor.cluster_id.start_byte_in_run;
216
                        let end_byte = start_byte.saturating_add(
216
                            u32::try_from(preedit.len()).unwrap_or(u32::MAX),
                        );
                        // (line_index, min_x, max_x) of the composed clusters.
                        // Clusters on a later line belong to a composition that
                        // wrapped; they would need their own rect, so the span
                        // stays on the line the caret sits on.
216
                        let mut span: Option<(usize, f32, f32)> = None;
918
                        for item in &layout.items {
918
                            let Some(cluster) = item.item.as_cluster() else {
                                continue;
                            };
918
                            let id = cluster.source_cluster_id;
918
                            if id.source_run != run
918
                                || id.start_byte_in_run < start_byte
828
                                || id.start_byte_in_run >= end_byte
                            {
486
                                continue;
432
                            }
432
                            let edge = item.position.x + cluster.advance;
432
                            let (lo, hi) =
432
                                (item.position.x.min(edge), item.position.x.max(edge));
216
                            span = Some(match span {
216
                                Some((line, min_x, max_x)) if line == item.line_index => {
216
                                    (line, min_x.min(lo), max_x.max(hi))
                                }
                                Some(other_line) => other_line,
216
                                None => (item.line_index, lo, hi),
                            });
                        }
216
                        let (underline_x, preedit_width) = if let Some((_, lo, hi)) = span {
216
                            (lo + content_box_offset_x, hi - lo)
                        } else {
                            // Composition not (yet) in the cache — keep the
                            // old estimate rather than drawing nothing.
                            let char_count = preedit.chars().count() as f32;
                            (
                                rect.origin.x + rect.size.width,
                                char_count * style.width.max(8.0),
                            )
                        };
216
                        let underline_bounds = LogicalRect {
216
                            origin: LogicalPosition {
216
                                x: underline_x,
216
                                y: rect.origin.y + rect.size.height - 2.0,
216
                            },
216
                            size: LogicalSize {
216
                                width: preedit_width,
216
                                height: 2.0,
216
                            },
216
                        };
216
                        builder.push_underline(underline_bounds, style.color, 2.0);
                    }
2038
                }
            }
        }
2686
        Ok(())
640647
    }
    /// Emits drawing commands for selection and cursor.
    /// Delegates to `paint_selections()` and `paint_cursor()`.
640647
    fn paint_selection_and_cursor(
640647
        &self,
640647
        builder: &mut DisplayListBuilder,
640647
        node_index: usize,
640647
    ) -> Result<()> {
640647
        self.paint_selections(builder, node_index)?;
640647
        self.paint_cursor(builder, node_index)?;
640647
        Ok(())
640647
    }
    /// 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
7750
    fn collect_stacking_contexts(&mut self, node_index: usize) -> Result<StackingContext> {
7750
        let node = self
7750
            .positioned_tree
7750
            .tree
7750
            .get(LayoutNodeId::new(node_index))
7750
            .ok_or(LayoutError::InvalidTree)?;
7750
        let z_index = get_z_index(self.ctx.styled_dom, node.dom_node_id);
7750
        if let Some(dom_id) = node.dom_node_id {
7750
            let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
7750
            debug_info!(
4220
                self.ctx,
4220
                "Collecting stacking context for node {} ({:?}), z-index={}",
                node_index,
4220
                node_type.get_node_type(),
                z_index
            );
        }
7750
        let mut child_contexts = Vec::new();
7750
        let mut in_flow_children = Vec::new();
229602
        for &child_index in self.positioned_tree.tree.children(node_index) {
229602
            if self.establishes_stacking_context(child_index) {
18
                child_contexts.push(self.collect_stacking_contexts(child_index)?);
            } else {
229584
                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.
229584
                self.find_nested_stacking_contexts(child_index, &mut child_contexts)?;
            }
        }
7750
        Ok(StackingContext {
7750
            node_index,
7750
            z_index,
7750
            child_contexts,
7750
            in_flow_children,
7750
        })
7750
    }
    /// Recursively searches non-stacking-context subtrees for nested stacking
    /// contexts, promoting them to the parent stacking context's child list.
632897
    fn find_nested_stacking_contexts(
632897
        &mut self,
632897
        parent_index: usize,
632897
        child_contexts: &mut Vec<StackingContext>,
632897
    ) -> Result<()> {
632897
        for &child_index in self.positioned_tree.tree.children(parent_index) {
403318
            if self.establishes_stacking_context(child_index) {
5
                child_contexts.push(self.collect_stacking_contexts(child_index)?);
            } else {
403313
                self.find_nested_stacking_contexts(child_index, child_contexts)?;
            }
        }
632897
        Ok(())
632897
    }
    // +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
    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
7750
    fn generate_for_stacking_context(
7750
        &mut self,
7750
        builder: &mut DisplayListBuilder,
7750
        context: &StackingContext,
7750
    ) -> Result<()> {
        // Before painting the node, check if it establishes a new clip or scroll frame.
7750
        let node = self
7750
            .positioned_tree
7750
            .tree
7750
            .get(LayoutNodeId::new(context.node_index))
7750
            .ok_or(LayoutError::InvalidTree)?;
7750
        if let Some(dom_id) = node.dom_node_id {
7750
            let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
7750
            debug_info!(
4220
                self.ctx,
4220
                "Painting stacking context for node {} ({:?}), z-index={}, {} child contexts, {} \
4220
                 in-flow children",
                context.node_index,
4220
                node_type.get_node_type(),
                context.z_index,
4220
                context.child_contexts.len(),
4220
                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.
7750
        builder.set_current_node(node.dom_node_id);
        // Track fixed-position elements for paged media replication (CSS Positioned Layout §2.1)
7750
        let is_fixed_position = node.dom_node_id
7750
            .is_some_and(|dom_id| get_position_type(self.ctx.styled_dom, Some(dom_id)) == LayoutPosition::Fixed);
7750
        if is_fixed_position {
            builder.begin_fixed_position_element();
7750
        }
        // 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.
7750
        let has_reference_frame = node.dom_node_id.and_then(|dom_id| {
7750
            self.gpu_value_cache.and_then(|cache| {
                // CSS transform first, then the ANIMATION channel. An
                // engine-driven transition animates nodes that have no CSS
                // `transform` of their own, so without this second lookup they
                // get no reference frame and the element jumps to its
                // destination instead of travelling there.
7225
                let (key, transform) = cache
7225
                    .css_transform_keys
7225
                    .get(&dom_id)
7225
                    .zip(cache.css_current_transform_values.get(&dom_id))
7225
                    .or_else(|| {
7206
                        cache
7206
                            .anim_transform_keys
7206
                            .get(&dom_id)
7206
                            .zip(cache.anim_current_transform_values.get(&dom_id))
7206
                    })?;
19
                Some((*key, *transform))
7225
            })
7750
        });
        // Push a stacking context for WebRender
        // Get the node's bounds for the stacking context
7750
        let node_pos = self
7750
            .positioned_tree
7750
            .calculated_positions
7750
            .get(context.node_index)
7750
            .copied()
7750
            .unwrap_or_default();
7750
        let node_size = node.used_size.unwrap_or(LogicalSize {
7750
            width: 0.0,
7750
            height: 0.0,
7750
        });
7750
        let node_bounds = LogicalRect {
7750
            origin: node_pos,
7750
            size: node_size,
7750
        };
        // Push reference frame BEFORE stacking context if node has a transform
7750
        if let Some((transform_key, initial_transform)) = has_reference_frame {
19
            builder.push_reference_frame(transform_key, initial_transform, node_bounds);
7731
        }
7750
        builder.push_stacking_context(context.z_index, node_bounds);
        // Push opacity/filter effects if the node has them
7750
        let mut pushed_opacity = false;
7750
        let mut pushed_filter = false;
7750
        let mut pushed_backdrop_filter = false;
7750
        if let Some(dom_id) = node.dom_node_id {
7750
            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
7750
            let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
            // Opacity (GPU: fast path via compact cache)
7750
            let opacity = crate::solver3::getters::get_opacity(
7750
                self.ctx.styled_dom, dom_id, node_state,
            );
            // ANIMATED opacity binds a key from the animation channel — the
            // exact twin of `has_reference_frame` for transforms: the list
            // carries the key, the value flows per tick through the GPU cache,
            // and an enter/exit fade needs a layer even while the BAKED CSS
            // opacity is 1.0.
7750
            let anim_opacity_key = self.gpu_value_cache.and_then(|cache| {
7225
                cache
7225
                    .anim_opacity_keys
7225
                    .get(&dom_id)
7225
                    .zip(cache.anim_current_opacity_values.get(&dom_id))
7225
                    .map(|(k, _)| *k)
7225
            });
7750
            if opacity < 1.0 || anim_opacity_key.is_some() {
22
                builder.push_item(DisplayListItem::PushOpacity {
22
                    bounds: node_bounds.into(),
22
                    opacity,
22
                    opacity_key: anim_opacity_key,
22
                });
22
                pushed_opacity = true;
7728
            }
            // Filter
7750
            if let Some(filter_vec_value) = self.ctx.styled_dom.css_property_cache.ptr
7750
                .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;
                    }
                }
7750
            }
            // Backdrop filter
7750
            if let Some(backdrop_filter_value) = self.ctx.styled_dom.css_property_cache.ptr
7750
                .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;
                    }
                }
7750
            }
        }
        // 0b. Push image mask clip if this node has one.
        // This wraps background, border, and all children so the SVG mask clips everything.
7750
        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)
7750
        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
7750
        if !self.is_node_hidden(context.node_index) {
7750
            if let Some(dom_id) = node.dom_node_id {
7750
                let styled_node_state = self.get_styled_node_state(dom_id);
7750
                let overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
7750
                let overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
7750
                if overflow_x.is_scroll() || overflow_y.is_scroll() {
153
                    if let Some(tag_id) = get_tag_id(self.ctx.styled_dom, node.dom_node_id) {
153
                        builder.push_hit_test_area(node_bounds, tag_id);
153
                    }
7597
                }
            }
        }
        // 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.
7750
        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.
7750
        let mut negative_z_children: Vec<_> = context
7750
            .child_contexts
7750
            .iter()
7750
            .filter(|c| c.z_index < 0)
7750
            .collect();
7750
        negative_z_children.sort_by_key(|c| c.z_index);
7750
        for child in negative_z_children {
            self.generate_for_stacking_context(builder, child)?;
        }
        // 4. Paint the in-flow descendants of the context root.
7750
        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.
7750
        for child in context.child_contexts.iter().filter(|c| c.z_index == 0) {
23
            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.
7750
        let mut positive_z_children: Vec<_> = context
7750
            .child_contexts
7750
            .iter()
7750
            .filter(|c| c.z_index > 0)
7750
            .collect();
7750
        positive_z_children.sort_by_key(|c| c.z_index);
7750
        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)
7750
        if did_push_image_mask {
45
            builder.pop_image_mask_clip();
7705
        }
        // Pop filter/opacity effects (in reverse order of push)
7750
        if pushed_backdrop_filter {
            builder.push_item(DisplayListItem::PopBackdropFilter);
7750
        }
7750
        if pushed_filter {
            builder.push_item(DisplayListItem::PopFilter);
7750
        }
7750
        if pushed_opacity {
22
            builder.push_item(DisplayListItem::PopOpacity);
7728
        }
        // Pop the stacking context for WebRender
7750
        builder.pop_stacking_context();
        // Pop reference frame if we pushed one
7750
        if has_reference_frame.is_some() {
19
            builder.pop_reference_frame();
7731
        }
        // End fixed-position tracking (records the item range for paged media replication)
7750
        if is_fixed_position {
            builder.end_fixed_position_element();
7750
        }
        // 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.
7750
        if did_push_clip_or_scroll {
            // Emit VirtualViewPlaceholder before popping the clip so it's inside PushClip/PopClip
225
            if let Some(dom_id) = node.dom_node_id {
225
                if self.is_virtual_view_node(dom_id) {
                    builder.push_virtual_view_placeholder(dom_id, node_bounds, node_bounds);
225
                }
            }
225
            self.pop_node_clips(builder, node);
        } else {
            // Even without clips, emit VirtualViewPlaceholder for VirtualView nodes
7525
            if let Some(dom_id) = node.dom_node_id {
7525
                if self.is_virtual_view_node(dom_id) {
                    builder.push_virtual_view_placeholder(dom_id, node_bounds, node_bounds);
7525
                }
            }
        }
        // Paint scrollbars AFTER popping the clip, so they appear on top of content
        // and are not clipped by the scroll frame
7750
        self.paint_scrollbars(builder, context.node_index)?;
7750
        Ok(())
7750
    }
    /// Paints the content and non-stacking-context children.
    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
640647
    fn paint_in_flow_descendants(
640647
        &mut self,
640647
        builder: &mut DisplayListBuilder,
640647
        node_index: usize,
640647
        children_indices: &[usize],
640647
    ) -> 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.
640647
        self.paint_selection_and_cursor(builder, node_index)?;
        // 3. Paint the node's own content (text, images, hit-test areas).
640647
        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.
640647
        let mut non_float_children = Vec::new();
640647
        let mut float_children = Vec::new();
640647
        let mut dragging_children = Vec::new();
1273549
        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.
632902
            if self.establishes_stacking_context(child_index) {
5
                continue;
632897
            }
632897
            let child_node = self
632897
                .positioned_tree
632897
                .tree
632897
                .get(LayoutNodeId::new(child_index))
632897
                .ok_or(LayoutError::InvalidTree)?;
            // Check if this child is being dragged (paint last for z-order)
632897
            let is_dragging = child_node.dom_node_id.is_some_and(|dom_id| {
632766
                let styled_node_state = self.get_styled_node_state(dom_id);
632766
                styled_node_state.dragging
632766
            });
632897
            if is_dragging {
                dragging_children.push(child_index);
                continue;
632897
            }
            // Check if this child is a float
632897
            let is_float = if let Some(dom_id) = child_node.dom_node_id {
                use crate::solver3::getters::get_float;
632766
                let styled_node_state = self.get_styled_node_state(dom_id);
632766
                let float_value = get_float(self.ctx.styled_dom, dom_id, &styled_node_state);
50
                !matches!(
632766
                    float_value.unwrap_or_default(),
                    azul_css::props::layout::LayoutFloat::None
                )
            } else {
131
                false
            };
632897
            if is_float {
50
                float_children.push(child_index);
632847
            } else {
632847
                non_float_children.push(child_index);
632847
            }
        }
        // Paint non-float children first
1273494
        for child_index in non_float_children {
632847
            let child_node = self
632847
                .positioned_tree
632847
                .tree
632847
                .get(LayoutNodeId::new(child_index))
632847
                .ok_or(LayoutError::InvalidTree)?;
            // Check if this child has a GPU transform (CSS transform or drag)
632847
            let child_ref_frame = child_node.dom_node_id.and_then(|dom_id| {
632716
                self.gpu_value_cache.and_then(|cache| {
                    // CSS transform first, then the ANIMATION channel — an
                    // engine-driven transition animates nodes that have no CSS
                    // `transform` of their own, and without this they get no
                    // reference frame and jump to their destination.
628405
                    let (key, transform) = cache
628405
                        .css_transform_keys
628405
                        .get(&dom_id)
628405
                        .zip(cache.css_current_transform_values.get(&dom_id))
628405
                        .or_else(|| {
628405
                            cache
628405
                                .anim_transform_keys
628405
                                .get(&dom_id)
628405
                                .zip(cache.anim_current_transform_values.get(&dom_id))
628405
                        })?;
71
                    Some((*key, *transform))
628405
                })
632716
            });
            // Push reference frame if child has a transform
632847
            if let Some((transform_key, initial_transform)) = child_ref_frame {
71
                let child_pos = self
71
                    .positioned_tree
71
                    .calculated_positions
71
            .get(child_index)
71
                    .copied()
71
                    .unwrap_or_default();
71
                let child_size = child_node.used_size.unwrap_or(LogicalSize {
71
                    width: 0.0,
71
                    height: 0.0,
71
                });
71
                let child_bounds = LogicalRect {
71
                    origin: child_pos,
71
                    size: child_size,
71
                };
71
                builder.set_current_node(child_node.dom_node_id);
71
                builder.push_reference_frame(transform_key, initial_transform, child_bounds);
632776
            }
            // Push image mask clip if this child has one (wraps background + children)
632847
            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.
632847
            self.paint_node_background_and_border(builder, child_index)?;
            // Push clips and scroll frames AFTER painting background
632847
            let did_push_clip = self.push_node_clips(builder, child_index, child_node);
            // Paint descendants inside the clip/scroll frame
632847
            self.paint_in_flow_descendants(builder, child_index, self.positioned_tree.tree.children(child_index))?;
            // For VirtualView children: emit placeholder INSIDE the clip
632847
            if let Some(dom_id) = child_node.dom_node_id {
632716
                if self.is_virtual_view_node(dom_id) {
173
                    let child_bounds = self.get_paint_rect(child_index).unwrap_or_default();
173
                    builder.push_virtual_view_placeholder(dom_id, child_bounds, child_bounds);
632543
                }
131
            }
            // Pop the child's clips.
632847
            if did_push_clip {
3079
                self.pop_node_clips(builder, child_node);
629768
            }
            // Pop image mask clip
632847
            if did_push_child_image_mask {
                builder.pop_image_mask_clip();
632847
            }
            // Paint scrollbars AFTER popping clips so they appear on top of content
632847
            self.paint_scrollbars(builder, child_index)?;
            // Pop reference frame if we pushed one
632847
            if child_ref_frame.is_some() {
71
                builder.pop_reference_frame();
632776
            }
        }
        // +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)
640697
        for child_index in float_children {
50
            let child_node = self
50
                .positioned_tree
50
                .tree
50
                .get(LayoutNodeId::new(child_index))
50
                .ok_or(LayoutError::InvalidTree)?;
            // Check if this child has a GPU transform (CSS transform or drag)
50
            let child_ref_frame = child_node.dom_node_id.and_then(|dom_id| {
50
                self.gpu_value_cache.and_then(|cache| {
                    // CSS transform first, then the ANIMATION channel — an
                    // engine-driven transition animates nodes that have no CSS
                    // `transform` of their own, and without this they get no
                    // reference frame and jump to their destination.
                    let (key, transform) = cache
                        .css_transform_keys
                        .get(&dom_id)
                        .zip(cache.css_current_transform_values.get(&dom_id))
                        .or_else(|| {
                            cache
                                .anim_transform_keys
                                .get(&dom_id)
                                .zip(cache.anim_current_transform_values.get(&dom_id))
                        })?;
                    Some((*key, *transform))
                })
50
            });
            // Push reference frame if child has a transform
50
            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);
50
            }
            // Same as above: push image mask, paint background, then clips
50
            let did_push_child_image_mask = self.push_image_mask_clip(builder, child_index);
50
            self.paint_node_background_and_border(builder, child_index)?;
50
            let did_push_clip = self.push_node_clips(builder, child_index, child_node);
50
            self.paint_in_flow_descendants(builder, child_index, self.positioned_tree.tree.children(child_index))?;
            // For VirtualView children: emit placeholder INSIDE the clip
50
            if let Some(dom_id) = child_node.dom_node_id {
50
                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);
50
                }
            }
50
            if did_push_clip {
                self.pop_node_clips(builder, child_node);
50
            }
50
            if did_push_child_image_mask {
                builder.pop_image_mask_clip();
50
            }
            // Paint scrollbars AFTER popping clips so they appear on top of content
50
            self.paint_scrollbars(builder, child_index)?;
            // Pop reference frame if we pushed one
50
            if child_ref_frame.is_some() {
                builder.pop_reference_frame();
50
            }
        }
        // Paint dragging children LAST so they appear on top of everything (W3C spec)
640647
        for child_index in dragging_children {
            let child_node = self
                .positioned_tree
                .tree
                .get(LayoutNodeId::new(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| {
                    // CSS transform first, then the ANIMATION channel — an
                    // engine-driven transition animates nodes that have no CSS
                    // `transform` of their own, and without this they get no
                    // reference frame and jump to their destination.
                    let (key, transform) = cache
                        .css_transform_keys
                        .get(&dom_id)
                        .zip(cache.css_current_transform_values.get(&dom_id))
                        .or_else(|| {
                            cache
                                .anim_transform_keys
                                .get(&dom_id)
                                .zip(cache.anim_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();
            }
        }
640647
        Ok(())
640647
    }
    /// Returns true if the given DOM node is a `VirtualView` node.
647124
    fn is_virtual_view_node(&self, dom_id: NodeId) -> bool {
647124
        let node_data_container = self.ctx.styled_dom.node_data.as_container();
647124
        node_data_container
647124
            .get(dom_id)
647124
            .is_some_and(|nd| matches!(nd.get_node_type(), NodeType::VirtualView))
647124
    }
    /// 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).
640647
    fn push_image_mask_clip(
640647
        &self,
640647
        builder: &mut DisplayListBuilder,
640647
        node_index: usize,
640647
    ) -> bool {
640647
        let Some(node) = self.positioned_tree.tree.get(LayoutNodeId::new(node_index)) else {
            return false;
        };
640647
        let Some(dom_id) = node.dom_node_id else {
131
            return false;
        };
640516
        let node_data_container = self.ctx.styled_dom.node_data.as_container();
640516
        let Some(node_data) = node_data_container.get(dom_id) else {
            return false;
        };
640516
        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")]
45
            Some(azul_core::dom::SvgNodeData::Path(svg_clip)) => {
45
                let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
45
                rasterize_svg_clip_to_r8(svg_clip, &paint_rect).is_some_and(|mask_image| {
45
                    builder.push_image_mask_clip(paint_rect, mask_image, paint_rect);
45
                    true
45
                })
            }
            #[cfg(not(feature = "cpurender"))]
            Some(azul_core::dom::SvgNodeData::Path(_)) => {
                // The DOM asked for an SVG clip mask; without `cpurender` the
                // clip silently does not clip. Say so once.
                static ANNOUNCE: std::sync::Once = std::sync::Once::new();
                ANNOUNCE.call_once(|| {
                    eprintln!(
                        "[azul][svg] an SVG clip-path is present, but this build has \
                         no `cpurender` feature — SVG clips will NOT clip"
                    );
                });
                false
            }
            // Other SvgNodeData variants (shapes, gradients, etc.) don't produce clip masks
            Some(_) => false,
640471
            None => false,
        }
640647
    }
    // +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`).
    #[allow(clippy::similar_names)] // domain-standard coordinate/geometry/short-lived names
640647
    fn push_node_clips(
640647
        &self,
640647
        builder: &mut DisplayListBuilder,
640647
        node_index: usize,
640647
        node: &LayoutNodeHot,
640647
    ) -> bool {
640647
        let Some(dom_id) = node.dom_node_id else {
131
            return false;
        };
640516
        let styled_node_state = self.get_styled_node_state(dom_id);
640516
        let raw_overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
640516
        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
640516
        let overflow_x = raw_overflow_x.resolve_computed(&raw_overflow_y);
640516
        let overflow_y = raw_overflow_y.resolve_computed(&raw_overflow_x);
640516
        let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
640516
        let element_size = PhysicalSizeImport {
640516
            width: paint_rect.size.width,
640516
            height: paint_rect.size.height,
640516
        };
640516
        let border_radius = get_border_radius(
640516
            self.ctx.styled_dom,
640516
            dom_id,
640516
            &styled_node_state,
640516
            element_size,
640516
            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.
640516
        let has_clip_path = super::getters::get_clip_path(
640516
            self.ctx.styled_dom, dom_id, &styled_node_state,
640516
        ).is_some_and(|clip_path| 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
            });
        // +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
640516
        let needs_clip = overflow_x.is_clipped() || overflow_y.is_clipped();
640516
        if !needs_clip {
637212
            return has_clip_path;
3304
        }
        // +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
3304
        let ox_clip = overflow_x.is_clipped() && !overflow_x.is_scroll() && !overflow_x.is_auto_overflow();
3304
        let oy_clip = overflow_y.is_clipped() && !overflow_y.is_scroll() && !overflow_y.is_auto_overflow();
3304
        let ox_visible = !overflow_x.is_clipped();
3304
        let oy_visible = !overflow_y.is_clipped();
3304
        let border_radius = if (ox_clip && oy_visible) || (oy_clip && ox_visible)
        {
            BorderRadius::default()
        } else {
3304
            border_radius
        };
3304
        let paint_rect = self.get_paint_rect(node_index).unwrap_or_default();
3304
        let bp = node.box_props.unpack();
3304
        let border = &bp.border;
        // Get scrollbar info to adjust clip rect for content area
3304
        let scrollbar_info = self.positioned_tree.tree.warm(LayoutNodeId::new(node_index))
3304
            .and_then(|w| w.scrollbar_info)
3304
            .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)
3304
        let mut clip_rect = LogicalRect {
3304
            origin: LogicalPosition {
3304
                x: paint_rect.origin.x + border.left,
3304
                y: paint_rect.origin.y + border.top,
3304
            },
3304
            size: LogicalSize {
3304
                // Reduce width/height by scrollbar dimensions so content doesn't overlap scrollbar
3304
                width: (paint_rect.size.width
3304
                    - border.left
3304
                    - border.right
3304
                    - scrollbar_info.scrollbar_width)
3304
                    .max(0.0),
3304
                height: (paint_rect.size.height
3304
                    - border.top
3304
                    - border.bottom
3304
                    - scrollbar_info.scrollbar_height)
3304
                    .max(0.0),
3304
            },
3304
        };
        // +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.
3304
        apply_overflow_clip_margin(
3304
            &mut clip_rect,
3304
            &overflow_x,
3304
            &overflow_y,
3304
            self.ctx.styled_dom,
3304
            dom_id,
3304
            &styled_node_state,
        );
3304
        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
        // Every clipped node pushes a clip (scrollable, hidden, or clip alike).
3304
        builder.push_clip(clip_rect, border_radius);
        // Regular scrollable nodes ALSO push a scroll frame: WebRender's APZ
        // manages the offset via define_scroll_frame, CPU renderers translate
        // children by scroll_offset. VirtualView scroll state is instead managed
        // by ScrollManager and passed to the callback as scroll_offset, with the
        // VirtualViewPlaceholder emitted after pop_node_clips in
        // generate_for_stacking_context — so VirtualView nodes get only the clip.
3304
        if (overflow_x.is_scroll() || overflow_y.is_scroll()) && !is_virtual_view {
461
            let scroll_id = self.scroll_ids.get(&LayoutNodeId::new(node_index)).copied().unwrap_or(0);
461
            let content_size = get_scroll_content_size(node, self.positioned_tree.tree.warm(LayoutNodeId::new(node_index)));
461
            builder.push_scroll_frame(clip_rect, content_size, scroll_id);
2872
        }
3304
        true
640647
    }
    /// Pops any clip/scroll commands associated with a node.
3304
    fn pop_node_clips(&self, builder: &mut DisplayListBuilder, node: &LayoutNodeHot) {
3304
        let Some(dom_id) = node.dom_node_id else {
            return;
        };
3304
        let styled_node_state = self.get_styled_node_state(dom_id);
        // Mirror push_node_clips EXACTLY: resolve visible/clip → auto/hidden per
        // CSS Overflow 3 §3.1 (an axis computes to auto/hidden when the *other*
        // axis is a scroll container). push_node_clips decides whether to emit a
        // scroll frame from the RESOLVED values; popping from the RAW values can
        // disagree. Concretely: the auto-injected titlebar title has
        // overflow-x:hidden, overflow-y:visible → push resolves y→auto (a scroll
        // container, since is_scroll() counts Auto) and emits PushClip +
        // PushScrollFrame, but pop saw raw y=visible (is_scroll=false) and emitted
        // only PopClip → an unbalanced PushScrollFrame. The layer allocator then
        // extends the titlebar's scroll layer to the end of the list, swallowing
        // the document body into the titlebar's clip rect (blank window) and
        // underflowing the clip stack. Resolving here keeps push/pop symmetric.
3304
        let raw_overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
3304
        let raw_overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
3304
        let overflow_x = raw_overflow_x.resolve_computed(&raw_overflow_y);
3304
        let overflow_y = raw_overflow_y.resolve_computed(&raw_overflow_x);
3304
        let paint_rect = self
3304
            .get_paint_rect(
3304
                self.positioned_tree
3304
                    .tree
3304
                    .nodes
3304
                    .iter()
214239
                    .position(|n| n.dom_node_id == Some(dom_id))
3304
                    .unwrap_or(0),
            )
3304
            .unwrap_or_default();
3304
        let element_size = PhysicalSizeImport {
3304
            width: paint_rect.size.width,
3304
            height: paint_rect.size.height,
3304
        };
3304
        let border_radius = get_border_radius(
3304
            self.ctx.styled_dom,
3304
            dom_id,
3304
            &styled_node_state,
3304
            element_size,
3304
            self.ctx.viewport_size,
        );
3304
        let needs_clip =
3304
            overflow_x.is_clipped() || overflow_y.is_clipped();
3304
        let is_virtual_view = self.is_virtual_view_node(dom_id);
3304
        if needs_clip {
            // Regular (non-VirtualView) scroll/auto also pushed a scroll frame;
            // pop it first (LIFO) before the shared clip. Hidden/clip and
            // VirtualView scroll only pushed a clip.
3304
            if (overflow_x.is_scroll() || overflow_y.is_scroll()) && !is_virtual_view {
461
                builder.pop_scroll_frame();
2872
            }
3304
            builder.pop_clip();
        }
        // 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).
3304
        if let Some(clip_path) = super::getters::get_clip_path(
3304
            self.ctx.styled_dom, dom_id, &styled_node_state,
3304
        ) {
            if resolve_clip_path(&clip_path, paint_rect).is_some() {
                builder.pop_clip();
            }
3304
        }
3304
    }
    /// 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).
2828110
    fn get_paint_rect(&self, node_index: usize) -> Option<LogicalRect> {
2828110
        let node = self.positioned_tree.tree.get(LayoutNodeId::new(node_index))?;
2828110
        let pos = self
2828110
            .positioned_tree
2828110
            .calculated_positions
2828110
            .get(node_index)
2828110
            .copied()
2828110
            .unwrap_or_default();
2828110
        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.
2828110
        Some(LogicalRect::new(pos, size))
2828110
    }
    /// Emits drawing commands for the background and border of a single node.
    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
640647
    fn paint_node_background_and_border(
640647
        &mut self,
640647
        builder: &mut DisplayListBuilder,
640647
        node_index: usize,
640647
    ) -> Result<()> {
640647
        let _p = crate::probe::Probe::span("dl_bg_border");
640647
        if self.try_copy_cached_run(builder, node_index, EmitPhase::BgBorder) {
367
            return Ok(());
640280
        }
640280
        builder.set_current_layout(Some((node_index, EmitPhase::BgBorder)));
640280
        let result = self.paint_node_background_and_border_inner(builder, node_index);
640280
        builder.set_current_layout(None);
640280
        result
640647
    }
640280
    fn paint_node_background_and_border_inner(
640280
        &mut self,
640280
        builder: &mut DisplayListBuilder,
640280
        node_index: usize,
640280
    ) -> Result<()> {
640280
        let Some(paint_rect) = self.get_paint_rect(node_index) else {
            return Ok(());
        };
640280
        let node = self
640280
            .positioned_tree
640280
            .tree
640280
            .get(LayoutNodeId::new(node_index))
640280
            .ok_or(LayoutError::InvalidTree)?;
        // Set current node for node mapping (for pagination break properties)
640280
        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
640280
        if let Some(dom_id) = node.dom_node_id {
640149
            let break_before = get_break_before(self.ctx.styled_dom, Some(dom_id));
640149
            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
640149
            if is_forced_page_break(break_before) {
14
                let y_position = paint_rect.origin.y;
14
                builder.add_forced_page_break(y_position, Some(dom_id));
14
                debug_info!(
14
                    self.ctx,
14
                    "Registered forced page break BEFORE node {} at y={}",
                    node_index,
                    y_position
                );
640135
            }
            // For break-after: always, insert a page break at the bottom of this element
640149
            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, Some(dom_id));
                debug_info!(
                    self.ctx,
                    "Registered forced page break AFTER node {} at y={}",
                    node_index,
                    y_position
                );
640149
            }
131
        }
        // 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.
640280
        if self.is_node_hidden(node_index) {
            return Ok(());
640280
        }
        // 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.
640280
        let warm = self.positioned_tree.tree.warm(LayoutNodeId::new(node_index));
640280
        let parent_is_flex_or_grid = warm
640280
            .and_then(|w| w.parent_formatting_context.as_ref().map(|fc| matches!(fc, FormattingContext::Flex | FormattingContext::Grid)))
640280
            .unwrap_or(false);
640280
        if let Some(dom_id) = node.dom_node_id {
640149
            let display = {
                use crate::solver3::getters::get_display_property;
640149
                get_display_property(self.ctx.styled_dom, Some(dom_id))
640149
                    .unwrap_or(LayoutDisplay::Inline)
            };
640149
            if display == LayoutDisplay::InlineBlock || display == LayoutDisplay::Inline {
294214
                debug_info!(
61583
                    self.ctx,
61583
                    "[paint_node] node {} has display={:?}, parent_formatting_context={:?}, parent_is_flex_or_grid={}",
                    node_index,
                    display,
61583
                    warm.and_then(|w| w.parent_formatting_context.as_ref()),
                    parent_is_flex_or_grid
                );
294214
                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.
274139
                    if display == LayoutDisplay::InlineBlock
114
                        && self.establishes_stacking_context(node_index)
                    {
                        // Fall through to paint background/border now
                    } else {
274139
                        return Ok(());
                    }
20075
                }
                // Fall through to paint this element - it's a flex/grid item
345935
            }
131
        }
        // 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.
366141
        if matches!(node.formatting_context,
            FormattingContext::TableRowGroup | FormattingContext::TableRow |
            FormattingContext::TableColumnGroup
        ) {
378
            return Ok(());
365763
        }
        // Tables have a special 6-layer background painting order
365763
        if matches!(node.formatting_context, FormattingContext::Table) {
333
            debug_info!(
333
                self.ctx,
333
                "Painting table backgrounds/borders for node {} at {:?}",
                node_index,
                paint_rect
            );
            // Delegate to specialized table painting function
333
            return self.paint_table_items(builder, node_index);
365430
        }
        // CSS 2.2 section 17.5.1: a cell's BACKGROUND belongs to layer 6 of
        // the table paint order and is painted by paint_table_items — never
        // here, or it re-paints ON TOP of whatever the table painted after
        // layer 6 (the resolved collapsed borders). The cell's own BORDER is
        // painted here in the separate-borders model only; in the collapsing
        // model the table paints one resolved border per grid edge
        // (paint_collapsed_table_borders), so the cell contributes nothing.
365430
        let is_table_cell = matches!(node.formatting_context, FormattingContext::TableCell);
365430
        if is_table_cell && self.is_inside_collapsed_table(node_index) {
81
            return Ok(());
365349
        }
365349
        if let Some(dom_id) = node.dom_node_id {
365218
            let styled_node_state = self.get_styled_node_state(dom_id);
365218
            let background_contents = if is_table_cell {
765
                Vec::new()
            } else {
364453
                get_background_contents(self.ctx.styled_dom, dom_id, &styled_node_state)
            };
365218
            let border_info = get_border_info(self.ctx.styled_dom, dom_id, &styled_node_state);
365218
            let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
365218
            debug_info!(
127392
                self.ctx,
127392
                "Painting background/border for node {} ({:?}) at {:?}, backgrounds={:?}",
                node_index,
127392
                node_type.get_node_type(),
                paint_rect,
127392
                background_contents.len()
            );
            // Get both versions: simple BorderRadius for rect clipping and StyleBorderRadius for
            // border rendering
365218
            let element_size = PhysicalSizeImport {
365218
                width: paint_rect.size.width,
365218
                height: paint_rect.size.height,
365218
            };
365218
            let simple_border_radius = get_border_radius(
365218
                self.ctx.styled_dom,
365218
                dom_id,
365218
                &styled_node_state,
365218
                element_size,
365218
                self.ctx.viewport_size,
            );
365218
            let style_border_radius =
365218
                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)
365218
            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.
365218
            for shadow in [
365218
                super::getters::get_box_shadow_left(self.ctx.styled_dom, dom_id, node_state),
365218
                super::getters::get_box_shadow_right(self.ctx.styled_dom, dom_id, node_state),
365218
                super::getters::get_box_shadow_top(self.ctx.styled_dom, dom_id, node_state),
365218
                super::getters::get_box_shadow_bottom(self.ctx.styled_dom, dom_id, node_state),
365218
            ].into_iter().flatten() {
144
                builder.push_item(DisplayListItem::BoxShadow {
144
                    bounds: paint_rect.into(),
144
                    shadow,
144
                    border_radius: simple_border_radius,
144
                });
144
            }
            // Use unified background/border painting
365218
            builder.push_backgrounds_and_border(
365218
                paint_rect,
365218
                &background_contents,
365218
                &border_info,
365218
                simple_border_radius,
365218
                style_border_radius,
365218
                self.ctx.image_cache,
            );
131
        }
365349
        Ok(())
640280
    }
    //   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.
333
    fn paint_table_items(
333
        &self,
333
        builder: &mut DisplayListBuilder,
333
        table_index: usize,
333
    ) -> Result<()> {
333
        let table_node = self
333
            .positioned_tree
333
            .tree
333
            .get(LayoutNodeId::new(table_index))
333
            .ok_or(LayoutError::InvalidTree)?;
333
        let Some(table_paint_rect) = self.get_paint_rect(table_index) else {
            return Ok(());
        };
        // Layer 1: Table background
333
        if let Some(dom_id) = table_node.dom_node_id {
333
            let styled_node_state = self.get_styled_node_state(dom_id);
333
            let bg_color = get_background_color(self.ctx.styled_dom, dom_id, &styled_node_state);
333
            let element_size = PhysicalSizeImport {
333
                width: table_paint_rect.size.width,
333
                height: table_paint_rect.size.height,
333
            };
333
            let border_radius = get_border_radius(
333
                self.ctx.styled_dom,
333
                dom_id,
333
                &styled_node_state,
333
                element_size,
333
                self.ctx.viewport_size,
333
            );
333

            
333
            builder.push_rect(table_paint_rect, bg_color, border_radius);
333
        }
        // Traverse table children to paint layers 2-6
        // Layer 2: Column group backgrounds
        // Layer 3: Column backgrounds (columns are children of column groups)
351
        for &child_idx in self.positioned_tree.tree.children(table_index) {
351
            let child_node = self.positioned_tree.tree.get(LayoutNodeId::new(child_idx));
351
            if let Some(node) = child_node {
351
                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);
                    }
351
                }
            }
        }
        // Layer 4: Row group backgrounds (tbody, thead, tfoot)
        // Layer 5: Row backgrounds
        // Layer 6: Cell backgrounds
351
        for &child_idx in self.positioned_tree.tree.children(table_index) {
351
            let child_node = self.positioned_tree.tree.get(LayoutNodeId::new(child_idx));
351
            if let Some(node) = child_node {
351
                match node.formatting_context {
                    FormattingContext::TableRowGroup => {
                        // Paint row group background
27
                        self.paint_element_background(builder, child_idx);
                        // Paint rows within this group
27
                        for &row_idx in self.positioned_tree.tree.children(child_idx) {
27
                            self.paint_table_row_and_cells(builder, row_idx);
27
                        }
                    }
324
                    FormattingContext::TableRow => {
324
                        // Direct row child (no row group wrapper)
324
                        self.paint_table_row_and_cells(builder, child_idx);
324
                    }
                    _ => {}
                }
            }
        }
        // Borders: separate model cells paint their own borders in the normal
        // flow; the collapsing model paints ONE resolved border per grid edge
        // here, on top of all table backgrounds (cell borders are suppressed
        // in paint_node_background_and_border for collapsed tables).
333
        if self.table_is_border_collapsed(table_index) {
36
            self.paint_collapsed_table_borders(builder, table_index);
297
        }
333
        Ok(())
333
    }
    /// Whether `table_index` (a `FormattingContext::Table` node) uses the
    /// collapsing border model. Anonymous wrapper tables have no DOM node and
    /// keep the initial value (separate).
1179
    fn table_is_border_collapsed(&self, table_index: usize) -> bool {
        use azul_css::props::layout::table::StyleBorderCollapse;
1179
        let Some(node) = self.positioned_tree.tree.get(LayoutNodeId::new(table_index)) else {
            return false;
        };
1179
        let Some(dom_id) = node.dom_node_id else {
            return false;
        };
1179
        if let Some(ref cc) = self.ctx.styled_dom.css_property_cache.ptr.compact_cache {
1179
            return cc.get_border_collapse(dom_id.index()) == StyleBorderCollapse::Collapse;
        }
        let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
        let node_state = self.get_styled_node_state(dom_id);
        self.ctx
            .styled_dom
            .css_property_cache
            .ptr
            .get_border_collapse(node_data, &dom_id, &node_state)
            .and_then(|prop| prop.get_property().copied())
            .unwrap_or(StyleBorderCollapse::Separate)
            == StyleBorderCollapse::Collapse
1179
    }
    /// Whether a node lives inside a `border-collapse: collapse` table
    /// (walks the layout-tree parent chain to the nearest Table node).
846
    fn is_inside_collapsed_table(&self, node_index: usize) -> bool {
846
        let mut cur = self.positioned_tree.tree.get(LayoutNodeId::new(node_index)).and_then(|n| n.parent);
1746
        while let Some(idx) = cur {
1746
            let Some(node) = self.positioned_tree.tree.get(LayoutNodeId::new(idx)) else {
                return false;
            };
1746
            if matches!(node.formatting_context, FormattingContext::Table) {
846
                return self.table_is_border_collapsed(idx);
900
            }
900
            cur = node.parent;
        }
        false
846
    }
    /// CSS 2.2 section 17.6.2: paint the collapsing-border grid.
    ///
    /// For every grid edge the participating borders (the two adjacent cells
    /// on interior edges; cell + table on perimeter edges; the row's own
    /// border on horizontal edges) compete via
    /// `BorderInfo::resolve_conflict` (hidden wins, then wider, then style
    /// priority, then source priority) and the single winner is painted as a
    /// strip CENTERED on the grid line — perimeter borders deliberately
    /// straddle the table edge, exactly like browsers render them.
    ///
    /// v1 limitations, acceptable for the current corpus and safe (worst
    /// case: a border strip at a slightly wrong offset, never a double
    /// border): cells are paired positionally per row (colspan/rowspan
    /// pairing is approximate), column/column-group borders do not
    /// participate, and non-solid winners (dashed/dotted/double) paint as a
    /// solid strip of the winning color.
36
    fn paint_collapsed_table_borders(
36
        &self,
36
        builder: &mut DisplayListBuilder,
36
        table_index: usize,
36
    ) {
        use crate::solver3::fc::{
            get_border_info as collapsed_border_info, BorderInfo as CollapsedBorder, BorderSource,
        };
        use azul_css::props::style::border::BorderStyle;
        // (cell layout-tree index, paint rect, owning row index) per row
36
        let mut rows: Vec<(usize, Vec<(usize, LogicalRect)>)> = Vec::new();
45
        for &child_idx in self.positioned_tree.tree.children(table_index) {
45
            let Some(child) = self.positioned_tree.tree.get(LayoutNodeId::new(child_idx)) else {
                continue;
            };
45
            match child.formatting_context {
                FormattingContext::TableRowGroup => {
27
                    for &row_idx in self.positioned_tree.tree.children(child_idx) {
27
                        rows.push((row_idx, self.collect_row_cells(row_idx)));
27
                    }
                }
18
                FormattingContext::TableRow => {
18
                    rows.push((child_idx, self.collect_row_cells(child_idx)));
18
                }
                _ => {}
            }
        }
45
        rows.retain(|(_, cells)| !cells.is_empty());
36
        if rows.is_empty() {
            return;
36
        }
324
        let cell_border = |idx: usize| -> Option<[CollapsedBorder; 4]> {
324
            let node = self.positioned_tree.tree.get(LayoutNodeId::new(idx))?;
324
            Some(collapsed_border_info(self.ctx, node, BorderSource::Cell).into())
324
        };
162
        let row_border = |idx: usize| -> Option<[CollapsedBorder; 4]> {
162
            let node = self.positioned_tree.tree.get(LayoutNodeId::new(idx))?;
162
            Some(collapsed_border_info(self.ctx, node, BorderSource::Row).into())
162
        };
36
        let table_border: Option<[CollapsedBorder; 4]> = self
36
            .positioned_tree
36
            .tree
36
            .get(LayoutNodeId::new(table_index))
36
            .map(|node| collapsed_border_info(self.ctx, node, BorderSource::Table).into());
        const TOP: usize = 0;
        const RIGHT: usize = 1;
        const BOTTOM: usize = 2;
        const LEFT: usize = 3;
        // Fold the participants down to one winner. `resolve_conflict`
        // returning None means `hidden` participated: paint nothing.
270
        let resolve = |participants: &[Option<CollapsedBorder>]| -> Option<CollapsedBorder> {
270
            let mut winner: Option<CollapsedBorder> = None;
648
            for b in participants.iter().flatten() {
648
                winner = match winner {
270
                    None => Some(*b),
378
                    Some(w) => Some(CollapsedBorder::resolve_conflict(&w, b)?),
                };
            }
171
            let w = winner?;
171
            (w.width > 0.0 && !matches!(w.style, BorderStyle::None | BorderStyle::Hidden))
171
                .then_some(w)
270
        };
        // Vertical edges, one strip per (row, boundary).
81
        for (row_idx, cells) in &rows {
45
            let _ = row_idx;
45
            let n = cells.len();
126
            for boundary in 0..=n {
126
                let left_cell = boundary.checked_sub(1).and_then(|i| cells.get(i));
126
                let right_cell = cells.get(boundary);
126
                let winner = resolve(&[
126
                    left_cell.and_then(|(i, _)| cell_border(*i)).map(|b| b[RIGHT]),
126
                    right_cell.and_then(|(i, _)| cell_border(*i)).map(|b| b[LEFT]),
                    // table border participates on the perimeter only
126
                    if boundary == 0 {
45
                        table_border.map(|b| b[LEFT])
81
                    } else if boundary == n {
45
                        table_border.map(|b| b[RIGHT])
                    } else {
36
                        None
                    },
                ]);
126
                let Some(w) = winner else { continue };
81
                let (x, y0, y1) = match (left_cell, right_cell) {
54
                    (Some((_, lr)), _) => (
54
                        lr.origin.x + lr.size.width,
54
                        lr.origin.y,
54
                        lr.origin.y + lr.size.height,
54
                    ),
27
                    (None, Some((_, rr))) => {
27
                        (rr.origin.x, rr.origin.y, rr.origin.y + rr.size.height)
                    }
                    (None, None) => continue,
                };
81
                builder.push_rect(
81
                    LogicalRect::new(
81
                        LogicalPosition::new(w.width.mul_add(-0.5, x), y0),
81
                        LogicalSize::new(w.width, y1 - y0),
                    ),
81
                    w.color,
81
                    BorderRadius::default(),
                );
            }
        }
        // Horizontal edges: for each row boundary, one strip per column
        // segment (positional pairing of the cells above/below).
36
        let n_rows = rows.len();
81
        for boundary in 0..=n_rows {
81
            let above = boundary.checked_sub(1).and_then(|i| rows.get(i));
81
            let below = rows.get(boundary);
81
            let segments: &[(usize, LogicalRect)] = match below.or(above) {
81
                Some((_, cells)) => cells,
                None => continue,
            };
144
            for (col, (_, seg_rect)) in segments.iter().enumerate() {
144
                let above_cell = above.and_then(|(_, cells)| cells.get(col));
144
                let below_cell = below.and_then(|(_, cells)| cells.get(col));
144
                let winner = resolve(&[
144
                    above_cell.and_then(|(i, _)| cell_border(*i)).map(|b| b[BOTTOM]),
144
                    above.and_then(|(r, _)| row_border(*r)).map(|b| b[BOTTOM]),
144
                    below_cell.and_then(|(i, _)| cell_border(*i)).map(|b| b[TOP]),
144
                    below.and_then(|(r, _)| row_border(*r)).map(|b| b[TOP]),
144
                    if boundary == 0 {
63
                        table_border.map(|b| b[TOP])
81
                    } else if boundary == n_rows {
63
                        table_border.map(|b| b[BOTTOM])
                    } else {
18
                        None
                    },
                ]);
144
                let Some(w) = winner else { continue };
90
                let y = match above_cell.or(below_cell) {
90
                    Some((_, r)) if above_cell.is_some() => r.origin.y + r.size.height,
36
                    Some((_, r)) => r.origin.y,
                    None => continue,
                };
                // Extend the strip by its own half-width at both ends so the
                // corners where a wider horizontal border meets a narrower
                // vertical one are filled.
90
                let x0 = w.width.mul_add(-0.5, seg_rect.origin.x);
90
                let x1 = w.width.mul_add(0.5, seg_rect.origin.x + seg_rect.size.width);
90
                builder.push_rect(
90
                    LogicalRect::new(
90
                        LogicalPosition::new(x0, w.width.mul_add(-0.5, y)),
90
                        LogicalSize::new(x1 - x0, w.width),
                    ),
90
                    w.color,
90
                    BorderRadius::default(),
                );
            }
        }
36
    }
    /// Paint rects of a row's cell children, in tree order.
45
    fn collect_row_cells(&self, row_idx: usize) -> Vec<(usize, LogicalRect)> {
45
        self.positioned_tree
45
            .tree
45
            .children(row_idx)
45
            .iter()
81
            .filter_map(|&cell_idx| {
81
                let rect = self.get_paint_rect(cell_idx)?;
81
                Some((cell_idx, rect))
81
            })
45
            .collect()
45
    }
    /// 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)
351
    fn paint_table_row_and_cells(
351
        &self,
351
        builder: &mut DisplayListBuilder,
351
        row_idx: usize,
351
    ) {
        // 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.
351
        if let Some(row_node) = self.positioned_tree.tree.get(LayoutNodeId::new(row_idx)) {
351
            if let Some(dom_id) = row_node.dom_node_id {
351
                let styled_node_state = self.get_styled_node_state(dom_id);
351
                let bg_color = get_background_color(self.ctx.styled_dom, dom_id, &styled_node_state);
351
                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());
                    }
351
                }
            }
        }
        // Layer 6: Paint cell backgrounds (topmost layer)
351
        if let Some(_node) = self.positioned_tree.tree.get(LayoutNodeId::new(row_idx)) {
846
            for &cell_idx in self.positioned_tree.tree.children(row_idx) {
846
                self.paint_element_background(builder, cell_idx);
846
            }
        }
351
    }
    /// 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()`
873
    fn paint_element_background(
873
        &self,
873
        builder: &mut DisplayListBuilder,
873
        node_index: usize,
873
    ) {
873
        let Some(paint_rect) = self.get_paint_rect(node_index) else {
            return;
        };
873
        let Some(node) = self.positioned_tree.tree.get(LayoutNodeId::new(node_index)) else {
            return;
        };
873
        let Some(dom_id) = node.dom_node_id else {
            return;
        };
873
        let styled_node_state = self.get_styled_node_state(dom_id);
873
        let bg_color = get_background_color(self.ctx.styled_dom, dom_id, &styled_node_state);
        // Only paint if background color has alpha > 0 (optimization)
873
        if bg_color.a == 0 {
819
            return;
54
        }
54
        let element_size = PhysicalSizeImport {
54
            width: paint_rect.size.width,
54
            height: paint_rect.size.height,
54
        };
54
        let border_radius = get_border_radius(
54
            self.ctx.styled_dom,
54
            dom_id,
54
            &styled_node_state,
54
            element_size,
54
            self.ctx.viewport_size,
        );
54
        builder.push_rect(paint_rect, bg_color, border_radius);
873
    }
    /// Emits drawing commands for the foreground content, including hit-test areas and scrollbars.
    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
640647
    fn paint_node_content(
640647
        &mut self,
640647
        builder: &mut DisplayListBuilder,
640647
        node_index: usize,
640647
    ) -> Result<()> {
640647
        let _p = crate::probe::Probe::span("dl_content");
640647
        if self.try_copy_cached_run(builder, node_index, EmitPhase::Content) {
367
            return Ok(());
640280
        }
640280
        builder.set_current_layout(Some((node_index, EmitPhase::Content)));
640280
        let result = self.paint_node_content_inner(builder, node_index);
640280
        builder.set_current_layout(None);
640280
        result
640647
    }
640280
    fn paint_node_content_inner(
640280
        &mut self,
640280
        builder: &mut DisplayListBuilder,
640280
        node_index: usize,
640280
    ) -> Result<()> {
        // CSS 2.2 §11.2: visibility:hidden — skip painting content for hidden nodes.
640280
        if self.is_node_hidden(node_index) {
            return Ok(());
640280
        }
640280
        let node = self
640280
            .positioned_tree
640280
            .tree
640280
            .get(LayoutNodeId::new(node_index))
640280
            .ok_or(LayoutError::InvalidTree)?;
640280
        let node_warm = self.positioned_tree.tree.warm(LayoutNodeId::new(node_index));
        // Set current node for node mapping (for pagination break properties)
640280
        builder.set_current_node(node.dom_node_id);
640280
        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.
640280
        if paint_rect.size.width == 0.0 || paint_rect.size.height == 0.0 {
289363
            if let Some(cached_layout) = node_warm.and_then(|w| w.inline_layout_result.as_ref()) {
5
                let content_bounds = cached_layout.layout.bounds();
5
                paint_rect.size.width = content_bounds.width;
5
                paint_rect.size.height = content_bounds.height;
289358
            }
350917
        }
        // 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.
640280
        if let Some(tag_id) = get_tag_id(self.ctx.styled_dom, node.dom_node_id) {
305049
            let is_scrollable = if let Some(dom_id) = node.dom_node_id {
305049
                let styled_node_state = self.get_styled_node_state(dom_id);
305049
                let overflow_x = get_overflow_x(self.ctx.styled_dom, dom_id, &styled_node_state);
305049
                let overflow_y = get_overflow_y(self.ctx.styled_dom, dom_id, &styled_node_state);
305049
                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.
305049
            if !is_scrollable {
304465
                builder.push_hit_test_area(paint_rect, tag_id);
304465
            }
335231
        }
        // Paint the node's visible content.
640280
        if let Some(cached_layout) = node_warm.and_then(|w| w.inline_layout_result.as_ref()) {
277387
            let inline_layout = &cached_layout.layout;
            // (d6h) Dense-aware count: the stored sparse may be the
            // retirement sentinel; logs should report the real content.
277387
            let logged_item_count = cached_layout
277387
                .dense
277387
                .as_deref()
277387
                .filter(|d| !d.clusters.is_empty())
277387
                .map_or(inline_layout.items.len(), |d| d.clusters.len());
277387
            debug_info!(
45585
                self.ctx,
45585
                "[paint_node] node {} has inline_layout with {} items",
                node_index,
                logged_item_count
            );
277387
            if let Some(dom_id) = node.dom_node_id {
277259
                let node_type = &self.ctx.styled_dom.node_data.as_container()[dom_id];
277259
                debug_info!(
45467
                    self.ctx,
45467
                    "Painting inline content for node {} ({:?}) at {:?}, {} layout items",
                    node_index,
45467
                    node_type.get_node_type(),
                    paint_rect,
                    logged_item_count
                );
128
            }
            // 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.
277387
            let border_box = BorderBoxRect(paint_rect);
277387
            let nbp = node.box_props.unpack();
277387
            let mut content_box_rect =
277387
                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.
277387
            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.
277387
            let content_size = get_scroll_content_size(node, node_warm);
277387
            if content_size.height > content_box_rect.size.height {
1
                content_box_rect.size.height = content_size.height;
277386
            }
277387
            if content_size.width > content_box_rect.size.width {
                content_box_rect.size.width = content_size.width;
277387
            }
            // Check for text-shadow and wrap inline content with push/pop shadow
277387
            let mut pushed_text_shadow = false;
277387
            if let Some(dom_id) = node.dom_node_id {
277259
                let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
277259
                let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
277259
                if let Some(shadow_val) = self.ctx.styled_dom.css_property_cache.ptr
277259
                    .get_text_shadow(node_data, &dom_id, node_state)
                {
                    if let Some(shadow) = shadow_val.get_property() {
                        builder.push_item(DisplayListItem::PushTextShadow {
                            shadow: (**shadow),
                        });
                        pushed_text_shadow = true;
                    }
277259
                }
128
            }
277387
            self.paint_inline_content(
277387
                builder,
277387
                content_box_rect,
277387
                viewport_clip_rect,
277387
                inline_layout,
277387
                cached_layout.dense.as_deref(),
277387
                &cached_layout.payload,
277387
                &cached_layout.glyph_runs,
277387
                node_index,
            );
277387
            if pushed_text_shadow {
                builder.push_item(DisplayListItem::PopTextShadow);
277387
            }
362893
        } 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.
            // Content resolves overlay→DOM: a runtime-swapped image or produced
            // callback frame (overlay) wins over the immutable DOM's ImageRef.
362890
            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
362890
            if matches!(node_data.get_node_type(), NodeType::Image(_)) {
47
                if let Some(image_ref) = self.ctx.resolved_content().image_for_paint(dom_id) {
47
                    debug_info!(
20
                        self.ctx,
20
                        "Painting image for node {} at {:?}",
                        node_index,
                        paint_rect
                    );
                    // Get border-radius so the compositor can clip the image to rounded corners
47
                    let styled_node_state = self.get_styled_node_state(dom_id);
47
                    let element_size = PhysicalSizeImport {
47
                        width: paint_rect.size.width,
47
                        height: paint_rect.size.height,
47
                    };
47
                    let border_radius = get_border_radius(
47
                        self.ctx.styled_dom,
47
                        dom_id,
47
                        &styled_node_state,
47
                        element_size,
47
                        self.ctx.viewport_size,
                    );
                    // Store the ImageRef directly in the display list
47
                    builder.push_image(paint_rect, image_ref, border_radius);
                }
362843
            }
3
        }
640280
        Ok(())
640280
    }
    /// 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.
    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
640647
    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).
640647
        if self.is_node_hidden(node_index) {
            return Ok(());
640647
        }
640647
        let node = self
640647
            .positioned_tree
640647
            .tree
640647
            .get(LayoutNodeId::new(node_index))
640647
            .ok_or(LayoutError::InvalidTree)?;
640647
        let Some(paint_rect) = self.get_paint_rect(node_index) else {
            return Ok(());
        };
        // Check if we need to draw scrollbars for this node.
640647
        let mut scrollbar_info = self.positioned_tree.tree.warm(LayoutNodeId::new(node_index))
640647
            .and_then(|w| w.scrollbar_info)
640647
            .unwrap_or_default();
        // Get node_id for GPU cache lookup and CSS style lookup
640647
        let node_id = node.dom_node_id;
        // A VirtualView is a replaced element with NO flow content, so the
        // layout-side necessity test (`check_scrollbar_necessity`: laid-out
        // content > container) can never fire for it and `overflow: auto` would
        // stay bar-less no matter how large the virtualized document is. The
        // virtual size does reach the display list — the ScrollManager puts the
        // callback's `virtual_scroll_size` into `ScrollPosition::children_rect`,
        // which the thumb geometry below already prefers — so decide from that,
        // through the one function that owns the rule.
        //
        // The answer cannot simply be read off `warm.scrollbar_info` here:
        // `register_scroll_nodes` writes the same amendment back into the tree
        // for the GPU/hit-test consumers, but it runs AFTER this display list is
        // built, and the layout pass that precedes this one has already
        // recomputed `warm.scrollbar_info` from the laid-out sizes.
640647
        if let Some(nid) = node_id {
640516
            if let Some(pos) = self.scroll_offsets.get(&nid) {
125
                let bp = node.box_props.unpack();
125
                let border = &bp.border;
125
                let padding_box_size = LogicalSize::new(
125
                    (paint_rect.size.width - border.left - border.right).max(0.0),
125
                    (paint_rect.size.height - border.top - border.bottom).max(0.0),
125
                );
125
                crate::solver3::cache::apply_virtual_scroll_necessity(
125
                    self.ctx.styled_dom,
125
                    nid,
125
                    pos.children_rect.size,
125
                    padding_box_size,
125
                    &mut scrollbar_info,
125
                );
640391
            }
131
        }
        // Get CSS scrollbar style for this node (cached per LayoutContext).
640647
        let scrollbar_style = node_id
640647
            .map(|nid| {
640516
                let node_state =
640516
                    &self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
640516
                crate::solver3::getters::get_scrollbar_style_cached(self.ctx, nid, node_state)
640516
            })
640647
            .unwrap_or_default();
        // Skip if scrollbar-width: none
640647
        if matches!(
640647
            scrollbar_style.width_mode,
            azul_css::props::style::scrollbar::LayoutScrollbarWidth::None
        ) {
            return Ok(());
640647
        }
        // +spec:overflow:3dfb2c - when scrollbar gutter is present but scrollbar is not,
        // paint the gutter background as an extension of the padding
640647
        let scrollbar_gutter = node_id
640647
            .and_then(|nid| {
640516
                let node_state =
640516
                    &self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
640516
                get_scrollbar_gutter_property(self.ctx.styled_dom, nid, node_state).exact()
640516
            })
640647
            .unwrap_or_default();
640647
        let gutter_is_stable = matches!(
640647
            scrollbar_gutter,
            azul_css::props::layout::overflow::StyleScrollbarGutter::Stable
            | azul_css::props::layout::overflow::StyleScrollbarGutter::StableBothEdges
        );
640647
        let gutter_both_edges = matches!(
640647
            scrollbar_gutter,
            azul_css::props::layout::overflow::StyleScrollbarGutter::StableBothEdges
        );
640647
        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_or(ColorU::TRANSPARENT, |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)
                });
            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());
                }
            }
640647
        }
        // Get border dimensions to position scrollbar inside the border-box
640647
        let sbp = node.box_props.unpack();
640647
        let border = &sbp.border;
        // Get border-radius for potential clipping
640647
        let container_border_radius = node_id
640647
            .map(|nid| {
640516
                let node_state =
640516
                    &self.ctx.styled_dom.styled_nodes.as_container()[nid].styled_node_state;
640516
                let element_size = PhysicalSizeImport {
640516
                    width: paint_rect.size.width,
640516
                    height: paint_rect.size.height,
640516
                };
640516
                let viewport_size =
640516
                    LogicalSize::new(self.ctx.viewport_size.width, self.ctx.viewport_size.height);
640516
                get_border_radius(
640516
                    self.ctx.styled_dom,
640516
                    nid,
640516
                    node_state,
640516
                    element_size,
640516
                    viewport_size,
                )
640516
            })
640647
            .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
640647
        let inner_rect = LogicalRect {
640647
            origin: LogicalPosition::new(
640647
                paint_rect.origin.x + border.left,
640647
                paint_rect.origin.y + border.top,
640647
            ),
640647
            size: LogicalSize::new(
640647
                (paint_rect.size.width - border.left - border.right).max(0.0),
640647
                (paint_rect.size.height - border.top - border.bottom).max(0.0),
640647
            ),
640647
        };
        // Get scroll position for thumb calculation.
        // `children_rect.origin` IS the scroll offset (see
        // `ScrollManager::get_scroll_states_for_dom`); `parent_rect.origin` is
        // an ABSOLUTE window coordinate, so subtracting it here mixed two
        // spaces and started the thumb `container.y` px down its own track for
        // any scroller not at the window origin. The GPU-only scroll path
        // (`GpuStateManager::update_scrollbar_transforms`) feeds the raw
        // `current_offset` into the same geometry fn — these two MUST agree,
        // because the transform written here is the initial value of the key
        // that path later overwrites.
640647
        let (scroll_offset_x, scroll_offset_y) = node_id
640647
            .and_then(|nid| {
640516
                self.scroll_offsets
640516
                    .get(&nid)
640516
                    .map(|pos| (pos.children_rect.origin.x, pos.children_rect.origin.y))
640516
            })
640647
            .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.
640647
        let content_size = node_id
640647
            .and_then(|nid| self.scroll_offsets.get(&nid)).map_or_else(|| self.positioned_tree.tree.get_content_size(LayoutNodeId::new(node_index)), |pos| pos.children_rect.size);
        // Calculate thumb border-radius (half the scrollbar width for pill-shaped thumb)
640647
        let thumb_radius = scrollbar_style.visual_width_px / 2.0;
640647
        let thumb_border_radius = BorderRadius {
640647
            top_left: thumb_radius,
640647
            top_right: thumb_radius,
640647
            bottom_left: thumb_radius,
640647
            bottom_right: thumb_radius,
640647
        };
640647
        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.
434
            let opacity_key = node_id.map(|nid| {
434
                self.gpu_value_cache
434
                    .and_then(|cache| {
434
                        cache
434
                            .scrollbar_v_opacity_keys
434
                            .get(&(self.dom_id, nid))
434
                            .copied()
434
                    })
434
                    .unwrap_or_else(OpacityKey::unique)
434
            });
            // Vertical scrollbar: use shared geometry computation
434
            let button_size = if scrollbar_style.show_scroll_buttons {
434
                scrollbar_style.scroll_button_size_px
            } else {
                0.0
            };
434
            let v_geom = compute_scrollbar_geometry_with_button_size(
434
                ScrollbarOrientation::Vertical,
434
                inner_rect,
434
                content_size,
434
                scroll_offset_y,
434
                scrollbar_style.visual_width_px,
434
                scrollbar_info.needs_horizontal,
434
                button_size,
            );
            // Position thumb after the top button; GPU transform moves it within usable track
434
            let thumb_bounds = LogicalRect {
434
                origin: LogicalPosition::new(
434
                    v_geom.track_rect.origin.x,
434
                    v_geom.track_rect.origin.y + v_geom.button_size,
434
                ),
434
                size: LogicalSize::new(v_geom.width_px, v_geom.thumb_length),
434
            };
            // 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.
434
            let thumb_transform_key = node_id.map(|nid| {
434
                self.gpu_value_cache
434
                    .and_then(|cache| cache.transform_keys.get(&nid).copied())
434
                    .unwrap_or_else(TransformKey::unique)
434
            });
            // Initial transform: translate thumb within usable region.
            // Quantised — see `quantize_thumb_offset`; the GPU updater that
            // overwrites this value rounds identically.
434
            let thumb_initial_transform = ComputedTransform3D::new_translation(
                0.0,
434
                crate::solver3::scrollbar::quantize_thumb_offset(v_geom.thumb_offset),
                0.0,
            );
            // Generate hit-test ID for vertical scrollbar thumb
434
            let hit_id = node_id
434
                .map(|nid| azul_core::hit_test::ScrollbarHitId::VerticalThumb(self.dom_id, nid));
            // Buttons at top/bottom of track (only if enabled in style)
434
            let (button_decrement_bounds, button_increment_bounds) = if scrollbar_style.show_scroll_buttons && v_geom.button_size > 0.0 {
434
                (
434
                    Some(LogicalRect {
434
                        origin: v_geom.track_rect.origin,
434
                        size: LogicalSize::new(v_geom.button_size, v_geom.button_size),
434
                    }),
434
                    Some(LogicalRect {
434
                        origin: LogicalPosition::new(
434
                            v_geom.track_rect.origin.x,
434
                            v_geom.track_rect.origin.y + v_geom.track_rect.size.height - v_geom.button_size,
434
                        ),
434
                        size: LogicalSize::new(v_geom.button_size, v_geom.button_size),
434
                    }),
434
                )
            } else {
                (None, None)
            };
434
            builder.push_scrollbar_styled(ScrollbarDrawInfo {
434
                bounds: v_geom.track_rect.into(),
434
                orientation: ScrollbarOrientation::Vertical,
434
                track_bounds: v_geom.track_rect.into(),
434
                track_color: scrollbar_style.track_color,
434
                thumb_bounds: thumb_bounds.into(),
434
                thumb_color: scrollbar_style.thumb_color,
434
                thumb_border_radius,
434
                button_decrement_bounds: button_decrement_bounds.map(Into::into),
434
                button_increment_bounds: button_increment_bounds.map(Into::into),
434
                button_color: scrollbar_style.button_color,
434
                opacity_key,
434
                thumb_transform_key,
434
                thumb_initial_transform,
434
                hit_id,
434
                clip_to_container_border: scrollbar_style.clip_to_container_border,
434
                container_border_radius,
434
                visibility: scrollbar_style.visibility,
434
            });
640213
        }
640647
        if scrollbar_info.needs_horizontal {
            // Look up horizontal opacity key from GPU cache (same pattern as vertical).
135
            let opacity_key = node_id.map(|nid| {
135
                self.gpu_value_cache
135
                    .and_then(|cache| {
135
                        cache
135
                            .scrollbar_h_opacity_keys
135
                            .get(&(self.dom_id, nid))
135
                            .copied()
135
                    })
135
                    .unwrap_or_else(OpacityKey::unique)
135
            });
            // Horizontal scrollbar: use shared geometry computation
135
            let h_button_size = if scrollbar_style.show_scroll_buttons {
135
                scrollbar_style.scroll_button_size_px
            } else {
                0.0
            };
135
            let h_geom = compute_scrollbar_geometry_with_button_size(
135
                ScrollbarOrientation::Horizontal,
135
                inner_rect,
135
                content_size,
135
                scroll_offset_x,
135
                scrollbar_style.visual_width_px,
135
                scrollbar_info.needs_vertical,
135
                h_button_size,
            );
            // Position thumb after the left button; GPU transform moves it within usable track
135
            let thumb_bounds = LogicalRect {
135
                origin: LogicalPosition::new(
135
                    h_geom.track_rect.origin.x + h_geom.button_size,
135
                    h_geom.track_rect.origin.y,
135
                ),
135
                size: LogicalSize::new(h_geom.thumb_length, h_geom.width_px),
135
            };
            // Look up horizontal transform key from GPU cache for GPU-animated thumb positioning.
135
            let thumb_transform_key = node_id.map(|nid| {
135
                self.gpu_value_cache
135
                    .and_then(|cache| cache.h_transform_keys.get(&nid).copied())
135
                    .unwrap_or_else(TransformKey::unique)
135
            });
135
            let thumb_initial_transform = ComputedTransform3D::new_translation(
135
                crate::solver3::scrollbar::quantize_thumb_offset(h_geom.thumb_offset),
                0.0,
                0.0,
            );
            // Generate hit-test ID for horizontal scrollbar thumb
135
            let hit_id = node_id
135
                .map(|nid| azul_core::hit_test::ScrollbarHitId::HorizontalThumb(self.dom_id, nid));
            // Buttons at left/right of track (only if enabled in style)
135
            let (button_decrement_bounds, button_increment_bounds) = if scrollbar_style.show_scroll_buttons && h_geom.button_size > 0.0 {
135
                (
135
                    Some(LogicalRect {
135
                        origin: h_geom.track_rect.origin,
135
                        size: LogicalSize::new(h_geom.button_size, h_geom.button_size),
135
                    }),
135
                    Some(LogicalRect {
135
                        origin: LogicalPosition::new(
135
                            h_geom.track_rect.origin.x + h_geom.track_rect.size.width - h_geom.button_size,
135
                            h_geom.track_rect.origin.y,
135
                        ),
135
                        size: LogicalSize::new(h_geom.button_size, h_geom.button_size),
135
                    }),
135
                )
            } else {
                (None, None)
            };
135
            builder.push_scrollbar_styled(ScrollbarDrawInfo {
135
                bounds: h_geom.track_rect.into(),
135
                orientation: ScrollbarOrientation::Horizontal,
135
                track_bounds: h_geom.track_rect.into(),
135
                track_color: scrollbar_style.track_color,
135
                thumb_bounds: thumb_bounds.into(),
135
                thumb_color: scrollbar_style.thumb_color,
135
                thumb_border_radius,
135
                button_decrement_bounds: button_decrement_bounds.map(Into::into),
135
                button_increment_bounds: button_increment_bounds.map(Into::into),
135
                button_color: scrollbar_style.button_color,
135
                opacity_key,
135
                thumb_transform_key,
135
                thumb_initial_transform,
135
                hit_id,
135
                clip_to_container_border: scrollbar_style.clip_to_container_border,
135
                container_border_radius,
135
                visibility: scrollbar_style.visibility,
135
            });
640512
        }
640647
        Ok(())
640647
    }
    /// Converts the rich layout information from `text3` into drawing commands.
    #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
277387
    fn paint_inline_content(
277387
        &self,
277387
        builder: &mut DisplayListBuilder,
277387
        container_rect: LogicalRect,
277387
        viewport_clip_rect: LogicalRect,
277387
        layout: &Arc<UnifiedLayout>,
277387
        dense_view: Option<&crate::text3::dense::DenseText>,
277387
        payload: &Arc<dyn std::any::Any + Send + Sync>,
277387
        glyph_runs: &[crate::text3::glyphs::CompactGlyphRun],
277387
        source_node_index: usize,
277387
    ) {
277387
        let _p = crate::probe::Probe::span("dl_inline_text");
        // 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
        // (d6h) Sentinel-aware: bounds() over the empty retirement
        // sentinel gated EVERY TextLayout DL item off — textless PDF
        // export and missing a11y metadata (caught by
        // dom_to_pdf_embeds_text_fonts under the d7 default flip). The
        // dense extent is the same math as the scroll-extent arm.
277387
        let layout_bounds = if layout.items.is_empty() {
277311
            match dense_view.filter(|d| !d.clusters.is_empty()) {
277309
                Some(d) => {
277309
                    let w = d
277309
                        .clusters
277309
                        .iter()
14807521
                        .map(|c| c.x + c.advance)
277309
                        .fold(0.0f32, f32::max);
277309
                    let h = d
277309
                        .lines
277309
                        .iter()
507807
                        .map(|l| l.top_y + l.height)
277309
                        .fold(0.0f32, f32::max);
277309
                    crate::text3::cache::Rect {
277309
                        x: 0.0,
277309
                        y: 0.0,
277309
                        width: w,
277309
                        height: h,
277309
                    }
                }
2
                None => layout.bounds(),
            }
        } else {
76
            layout.bounds()
        };
277387
        let actual_bounds = if layout_bounds.width > 0.0 && layout_bounds.height > 0.0 {
277384
            LogicalRect {
277384
                origin: container_rect.origin,
277384
                size: LogicalSize {
277384
                    width: layout_bounds.width,
277384
                    height: layout_bounds.height,
277384
                },
277384
            }
        } else {
            // If layout has no content, don't push TextLayout item at all
            // This prevents 0x0 TextLayout items that pollute height calculation
3
            LogicalRect {
3
                origin: container_rect.origin,
3
                size: LogicalSize::default(),
3
            }
        };
        // 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
        //
        // (#25 / user ruling 2026-08-12) AND only for PAGED (PDF-export)
        // display lists: `TextLayout` is the type-erased shaped-layout
        // payload printpdf's bridge and the font-embedding walk consume.
        // The screen renderers all no-op it (raster: "metadata for
        // PDF/accessibility — skip"; compositor2: logs and moves on), so
        // a screen DL was carrying one dead item + payload-Arc clone per
        // painted IFC. `fragmentation_context.is_some()` IS the
        // screen-vs-paged discriminator (paged entries own it; the window
        // path never sets it) — no new target input needed.
277387
        let is_paged_target = self.ctx.fragmentation_context.is_some();
277387
        if is_paged_target && (layout_bounds.width > 0.0 || layout_bounds.height > 0.0) {
            // The item-level font is the layout's PRIMARY font: the first
            // shaped glyph that resolved one. This used to be a hardcoded
            // `FontHash::from_hash(0)` placeholder ("will be updated per
            // glyph run" — nothing ever did), so every TextLayout shipped
            // `font_hash: 0`, and once `push_item` began dropping hash-0
            // TextLayouts as unresolved, ALL of them vanished — text
            // disappeared from the PDF export and a11y metadata. Per-glyph
            // consumers still read each glyph's own hash; staying at 0 here
            // is now reserved for a layout in which NO glyph resolved, which
            // is exactly the case the drop guard exists for.
            // §3.2 (d3): from the dense runs when retained (runs carry
            // font_hash + style directly; hash-0 runs = unresolved, skipped
            // exactly like the sparse scan). Falls back to the sparse walk
            // for mixed layouts or when no dense view is retained.
956
            let mut primary: Option<(u64, f32)> = None;
956
            if let Some(d) = dense_view {
                // (d6h) Sentinel-aware: empty stored items + non-empty
                // dense = the retirement form, dense is authoritative.
956
                if !d.clusters.is_empty()
939
                    && (layout.items.is_empty() || d.clusters.len() == layout.items.len())
                {
925
                    primary = d
925
                        .runs
925
                        .iter()
925
                        .find(|r| r.font_hash != 0)
925
                        .map(|r| (r.font_hash, r.style.font_size_px));
31
                }
            }
956
            if primary.is_none() {
35
                for positioned in &layout.items {
35
                    let (glyphs, arm_style) = match &positioned.item {
14
                        ShapedItem::Cluster(c) => (&c.glyphs, &c.style),
                        ShapedItem::CombinedBlock { glyphs, style, .. } => (glyphs, style),
21
                        _ => continue,
                    };
14
                    if let Some(g) = glyphs.iter().find(|g| g.font_hash != 0) {
14
                        primary = Some((g.font_hash, arm_style.font_size_px));
14
                        break;
                    }
                }
925
            }
956
            let (primary_hash, primary_size) = primary.unwrap_or((0, 12.0));
            // Clone the CACHED Arc, do not re-wrap a deep clone: TextLayout
            // damage diffing is Arc::ptr_eq, so a fresh Arc per rebuild made
            // every blink / tween tick repaint the whole text run (and deep-
            // cloned all shaped glyphs per frame). Real text changes replace
            // the cached Arc, so ptr_eq still fires damage then.
956
            builder.push_text_layout(
                // (d5) The CACHED payload Arc — TextPayload{dense,sparse}
                // when the dense view is retained, the bare layout Arc
                // otherwise. Cloned from the cache entry, so ptr_eq damage
                // diffing sees the same allocation across paints exactly
                // as before.
956
                payload.clone(),
956
                actual_bounds,
956
                FontHash::from_hash(primary_hash),
956
                primary_size,
956
                ColorU {
956
                    r: 0,
956
                    g: 0,
956
                    b: 0,
956
                    a: 255,
956
                }, // Default color
            );
276431
        }
        // Precomputed at CachedInlineLayout store time — the run-grouping
        // walk used to re-run here on EVERY paint of every IFC.
        // 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.
788088
        for glyph_run in glyph_runs {
            // Calculate the bounding box for this glyph run
510701
            if let (Some(first_glyph), Some(last_glyph)) =
510701
                (glyph_run.glyphs.first(), glyph_run.glyphs.last())
            {
                // Calculate run bounds from glyph positions
510701
                let run_start_x = container_rect.origin.x + first_glyph.point.x;
510701
                let run_end_x = container_rect.origin.x + last_glyph.point.x;
510701
                let run_width = (run_end_x - run_start_x).max(0.0);
                // Skip if run has no width
510701
                if run_width <= 0.0 {
2696
                    continue;
508005
                }
                // Approximate height based on font size (baseline is at glyph.point.y)
508005
                let baseline_y = container_rect.origin.y + first_glyph.point.y;
508005
                let font_size = glyph_run.font_size_px;
508005
                let ascent = font_size * APPROX_ASCENT_RATIO;
508005
                let mut run_bounds = LogicalRect::new(
508005
                    LogicalPosition::new(run_start_x, baseline_y - ascent),
508005
                    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.
508005
                if let Some(border) = &glyph_run.border {
3
                    let left_inset = border.left_inset();
3
                    let right_inset = border.right_inset();
3
                    let top_inset = border.top_inset();
3
                    let bottom_inset = border.bottom_inset();
3

            
3
                    run_bounds.origin.x -= left_inset;
3
                    run_bounds.origin.y -= top_inset;
3
                    run_bounds.size.width += left_inset + right_inset;
3
                    run_bounds.size.height += top_inset + bottom_inset;
508002
                }
508005
                builder.push_inline_backgrounds_and_border(
508005
                    run_bounds,
508005
                    glyph_run.background_color,
508005
                    &glyph_run.background_content,
508005
                    glyph_run.border.as_ref(),
508005
                    self.ctx.image_cache,
                );
            }
        }
        // The IFC-level background proof, refined per run below (a run
        // carrying its OWN background paints it directly underneath).
277387
        let ifc_uniform_bg = self.compute_uniform_text_bg(source_node_index);
        // The `::selection` recolour band, resolved once for the whole IFC.
277387
        let selection_recolour = self.selection_recolour_for_ifc(source_node_index);
        // SECOND PASS: Render text runs
788088
        for glyph_run in glyph_runs {
            // 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.
510701
            let clip_rect = viewport_clip_rect;
            // Offset glyph positions by the container origin (text layout is
            // relative to (0,0) of the IFC). (#25) The runs are stored
            // compact; this expansion builds the same Vec the pre-#25 code
            // built by copy-then-offset — construct instead of memcpy.
510701
            let offset_glyphs: Vec<GlyphInstance> = glyph_run
510701
                .glyphs
510701
                .to_vec_offset(container_rect.origin.x, container_rect.origin.y);
            // Store only the font hash in the display list to keep it lean
510701
            let uniform_bg = if glyph_run.background_content.is_empty() {
                match glyph_run.background_color {
                    // A span's own opaque background covers only the run —
                    // no cheap proven-RECT for the fringe boundary here, so
                    // the run takes the sweep. (The common body-text case is
                    // "no span background" → the ancestor proof below.)
                    Some(c) if c.a == 255 => {
                        let _ = c;
                        None
                    }
                    Some(c) if c.a > 0 => None, // translucent span bg
510697
                    _ => ifc_uniform_bg,
                }
            } else {
4
                None // gradient/image span background — unprovable
            };
            // Colour is paint-only and deliberately excluded from the text
            // layout hash, so a cached run can outlive the cascade that
            // resolved its colour (the deactivated-ribbon-tab KNOWN GAP:
            // a colour-only change never re-lays the IFC owner, and the
            // baked value painted stale). Re-resolve against the CURRENT
            // cascade at build time; the baked colour only serves runs
            // without a source node (markers, synthesized content).
510701
            let live_color = glyph_run
510701
                .source_node_id
510701
                .and_then(|nid| {
508749
                    let sd = self.ctx.styled_dom;
508749
                    let styled_nodes = sd.styled_nodes.as_container();
508749
                    if nid.index() >= styled_nodes.len() {
                        return None;
508749
                    }
508749
                    let cache = &sd.css_property_cache.ptr;
508749
                    let node_data = sd.node_data.as_container();
                    // ANCESTOR USER OVERRIDES participate in inheritance: an
                    // `animation: color ..` transition overrides `color` on a
                    // CONTAINER, and the precomputed inherited tables cannot
                    // see it — the text painted the stale colour (found by
                    // the css_anim_perf_transition damage law: the "colour
                    // transition" repainted nothing). Walk self -> root: the
                    // nearest override wins unless a closer node declares its
                    // OWN colour, which re-roots inheritance below it.
508749
                    let hierarchy = sd.node_hierarchy.as_container();
508749
                    let ty = azul_css::props::property::CssPropertyType::TextColor;
508749
                    let mut cur = Some(nid);
2004289
                    while let Some(n) = cur {
1
                        if let Some(azul_css::props::property::CssProperty::TextColor(v)) =
1548590
                            cache.get_user_override(&n, &ty)
                        {
1
                            if let Some(c) = v.get_property() {
1
                                return Some(c.inner);
                            }
                            break;
1548589
                        }
1548589
                        if n.index() < node_data.len()
1548589
                            && cache.has_own_declaration(&node_data[n], &n, &ty)
                        {
53049
                            break;
1495540
                        }
1495540
                        cur = hierarchy
1495540
                            .get(n)
1495540
                            .and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id);
                    }
508748
                    let node_state = &styled_nodes[nid].styled_node_state;
508748
                    Some(
508748
                        cache
508748
                            .get_text_color_or_default(&node_data[nid], &nid, node_state)
508748
                            .inner,
508748
                    )
508749
                })
510701
                .unwrap_or(glyph_run.color);
510701
            match &selection_recolour {
9
                Some((rects, selected_color)) => {
                    // A glyph's `point` is its pen position ON THE BASELINE at
                    // the left edge of its advance. A selection rect covers
                    // [start_x, end_x) of the clusters it spans and the whole
                    // line box vertically, so the origin of a selected glyph
                    // falls inside it while the origin of the first glyph
                    // AFTER the selection sits exactly on its right edge —
                    // hence the half-open x test. (A zero-advance mark sitting
                    // exactly on the right edge stays unselected; it is one
                    // combining mark at the very end of a selection.)
126
                    let inside = |g: &GlyphInstance| {
126
                        rects.iter().any(|r| {
126
                            g.point.x >= r.min_x() - 0.5
126
                                && g.point.x < r.max_x() - 0.5
36
                                && g.point.y >= r.min_y()
36
                                && g.point.y <= r.max_y()
126
                        })
126
                    };
9
                    let (selected, normal): (Vec<GlyphInstance>, Vec<GlyphInstance>) =
9
                        offset_glyphs.into_iter().partition(inside);
9
                    if !normal.is_empty() {
9
                        builder.push_text_run(
9
                            normal,
9
                            FontHash::from_hash(glyph_run.font_hash),
9
                            glyph_run.font_size_px,
9
                            live_color,
9
                            clip_rect,
9
                            Some(source_node_index),
9
                            uniform_bg,
9
                        );
9
                    }
9
                    if !selected.is_empty() {
9
                        builder.push_text_run(
9
                            selected,
9
                            FontHash::from_hash(glyph_run.font_hash),
9
                            glyph_run.font_size_px,
9
                            *selected_color,
9
                            clip_rect,
9
                            Some(source_node_index),
9
                            // The proven background under a selected glyph is
9
                            // the HIGHLIGHT, not the ancestor's colour.
9
                            None,
9
                        );
9
                    }
                }
510692
                None => {
510692
                    builder.push_text_run(
510692
                        offset_glyphs,
510692
                        FontHash::from_hash(glyph_run.font_hash),
510692
                        glyph_run.font_size_px,
510692
                        live_color,
510692
                        clip_rect,
510692
                        Some(source_node_index),
510692
                        uniform_bg,
510692
                    );
510692
                }
            }
            // Render text decorations if present OR if this is IME composition preview
510701
            let needs_underline = glyph_run.text_decoration.underline || glyph_run.is_ime_preview;
510701
            let needs_strikethrough = glyph_run.text_decoration.strikethrough;
510701
            let needs_overline = glyph_run.text_decoration.overline;
510701
            if needs_underline || needs_strikethrough || needs_overline {
                // Calculate the bounding box for this glyph run
378
                if let (Some(first_glyph), Some(last_glyph)) =
378
                    (glyph_run.glyphs.first(), glyph_run.glyphs.last())
                {
378
                    let decoration_start_x = container_rect.origin.x + first_glyph.point.x;
378
                    let decoration_end_x = container_rect.origin.x + last_glyph.point.x;
378
                    let decoration_width = decoration_end_x - decoration_start_x;
                    // Use font metrics to determine decoration positions
                    // Standard ratios based on CSS specification
378
                    let font_size = glyph_run.font_size_px;
378
                    let thickness = (font_size * APPROX_UNDERLINE_THICKNESS_RATIO).max(1.0);
                    // Baseline is at glyph.point.y
378
                    let baseline_y = container_rect.origin.y + first_glyph.point.y;
378
                    if needs_underline {
378
                        // Underline is typically 10-15% below baseline
378
                        // IME composition always gets underlined
378
                        let underline_y = baseline_y + (font_size * APPROX_UNDERLINE_OFFSET_RATIO);
378
                        let underline_bounds = LogicalRect::new(
378
                            LogicalPosition::new(decoration_start_x, underline_y),
378
                            LogicalSize::new(decoration_width, thickness),
378
                        );
378
                        builder.push_underline(underline_bounds, glyph_run.color, thickness);
378
                    }
378
                    if needs_strikethrough {
                        // Strikethrough is typically 40% above baseline (middle of x-height)
                        let strikethrough_y = baseline_y - (font_size * APPROX_STRIKETHROUGH_OFFSET_RATIO);
                        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,
                        );
378
                    }
378
                    if needs_overline {
                        // Overline is typically at cap-height (75% above baseline)
                        let overline_y = baseline_y - (font_size * APPROX_OVERLINE_OFFSET_RATIO);
                        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);
378
                    }
                }
510323
            }
        }
        // THIRD PASS: Generate hit-test areas for text runs
        // This enables cursor resolution directly on text nodes instead of their containers
788088
        for glyph_run in glyph_runs {
            // Only generate hit-test areas for runs with a source node id
510701
            let Some(source_node_id) = glyph_run.source_node_id else {
1952
                continue;
            };
            // Calculate the bounding box for this glyph run
508749
            if let (Some(first_glyph), Some(last_glyph)) =
508749
                (glyph_run.glyphs.first(), glyph_run.glyphs.last())
            {
508749
                let run_start_x = container_rect.origin.x + first_glyph.point.x;
508749
                let run_end_x = container_rect.origin.x + last_glyph.point.x;
508749
                let run_width = (run_end_x - run_start_x).max(0.0);
                // Skip if run has no width
508749
                if run_width <= 0.0 {
896
                    continue;
507853
                }
                // Calculate run bounds using font metrics
507853
                let baseline_y = container_rect.origin.y + first_glyph.point.y;
507853
                let font_size = glyph_run.font_size_px;
507853
                let ascent = font_size * APPROX_ASCENT_RATIO;
507853
                let run_bounds = LogicalRect::new(
507853
                    LogicalPosition::new(run_start_x, baseline_y - ascent),
507853
                    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
507853
                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
507853
                let tag_value = ((self.dom_id.inner as u64) << 32) | (source_node_id.index() as u64);
507853
                let tag_type = TAG_TYPE_CURSOR | (cursor_type as u16);
507853
                let tag_id = (tag_value, tag_type);
507853
                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. §3.2 (d3): a pure-cluster layout (dense len == items
        // len) has no objects BY CONSTRUCTION — skip the walk entirely.
        // (d6h) Sentinel-aware: the retirement form (empty items, dense
        // non-empty) is pure-cluster by construction.
277387
        let pure_clusters = dense_view.is_some_and(|d| {
277387
            !d.clusters.is_empty()
277332
                && (layout.items.is_empty() || d.clusters.len() == layout.items.len())
277387
        });
277387
        if !pure_clusters {
414
            for positioned_item in &layout.items {
414
                self.paint_inline_object(builder, container_rect.origin, positioned_item);
414
            }
277309
        }
277387
    }
    /// Paints a single inline object (image, shape, or inline-block)
414
    fn paint_inline_object(
414
        &self,
414
        builder: &mut DisplayListBuilder,
414
        base_pos: LogicalPosition,
414
        positioned_item: &PositionedItem,
414
    ) {
        let ShapedItem::Object {
86
            content, bounds, ..
414
        } = &positioned_item.item
        else {
            // Other item types (e.g., breaks) don't produce painted output.
328
            return;
        };
        // Calculate the absolute position of this object
        // positioned_item.position is relative to the container
86
        let object_bounds = LogicalRect::new(
86
            LogicalPosition::new(
86
                base_pos.x + positioned_item.position.x,
86
                base_pos.y + positioned_item.position.y,
            ),
86
            LogicalSize::new(bounds.width, bounds.height),
        );
86
        match content {
            InlineContent::Image(image) => {
                if let ImageSource::Node(image_node) = &image.source {
                    // Live overlay→DOM resolution: the IFC snapshotted only the
                    // NODE (see fc.rs), so a runtime image swap is visible on
                    // every DL build. Attribute the item to the image node —
                    // not the IFC root — so the chokepoint's in-place patch
                    // (`DisplayList::patch_node_image`) finds it.
                    if let Some(image_ref) =
                        self.ctx.resolved_content().image_for_paint(*image_node)
                    {
                        let prev_node = builder.current_node();
                        builder.set_current_node(Some(*image_node));
                        builder.push_image(object_bounds, image_ref, BorderRadius::default());
                        builder.set_current_node(prev_node);
                    }
                } else if let Some(image_ref) = get_image_ref_for_image_source(
                    &image.source,
                    self.ctx.image_cache,
                    object_bounds.size,
                ) {
                    builder.push_image(object_bounds, image_ref, BorderRadius::default());
                }
            }
86
            InlineContent::Shape(shape) => {
86
                self.paint_inline_shape(builder, object_bounds, shape, bounds);
86
            }
            _ => {}
        }
414
    }
    // +spec:inline-block:a60a89 - inline-block painted atomically as pseudo-stacking-context per E.2
    /// Paints an inline shape (inline-block background and border)
86
    fn paint_inline_shape(
86
        &self,
86
        builder: &mut DisplayListBuilder,
86
        object_bounds: LogicalRect,
86
        shape: &InlineShape,
86
        bounds: &crate::text3::cache::Rect,
86
    ) {
        // Render inline-block backgrounds and borders using their CSS styling
        // The text3 engine positions these correctly in the inline flow
86
        let Some(node_id) = shape.source_node_id else {
            return;
        };
        // 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.
86
        if let Some(indices) = self.positioned_tree.tree.dom_to_layout.get(&node_id) {
86
            if let Some(&idx) = indices.first() {
86
                if self.establishes_stacking_context(idx.index()) {
                    return;
86
                }
            }
        }
86
        let styled_node_state =
86
            &self.ctx.styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
        // Get all background layers (colors, gradients, images)
86
        let background_contents =
86
            get_background_contents(self.ctx.styled_dom, node_id, styled_node_state);
        // Get border information
86
        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.
86
        let margins = self.positioned_tree.tree.dom_to_layout.get(&node_id).map_or_else(
            crate::solver3::geometry::EdgeSizes::default,
86
            |indices| indices.first().map_or_else(
                crate::solver3::geometry::EdgeSizes::default,
86
                |&idx| self.positioned_tree.tree.nodes[idx.index()].box_props.unpack().margin,
            ),
        );
        // Convert margin-box bounds to border-box bounds
86
        let border_box_bounds = LogicalRect {
86
            origin: LogicalPosition {
86
                x: object_bounds.origin.x + margins.left,
86
                y: object_bounds.origin.y + margins.top,
86
            },
86
            size: LogicalSize {
86
                width: (object_bounds.size.width - margins.left - margins.right).max(0.0),
86
                height: (object_bounds.size.height - margins.top - margins.bottom).max(0.0),
86
            },
86
        };
86
        let element_size = PhysicalSizeImport {
86
            width: border_box_bounds.size.width,
86
            height: border_box_bounds.size.height,
86
        };
        // Get border radius for background clipping
86
        let simple_border_radius = get_border_radius(
86
            self.ctx.styled_dom,
86
            node_id,
86
            styled_node_state,
86
            element_size,
86
            self.ctx.viewport_size,
        );
        // Get style border radius for border rendering
86
        let style_border_radius =
86
            get_style_border_radius(self.ctx.styled_dom, node_id, styled_node_state);
        // Use unified background/border painting with border-box bounds
86
        builder.push_backgrounds_and_border(
86
            border_box_bounds,
86
            &background_contents,
86
            &border_info,
86
            simple_border_radius,
86
            style_border_radius,
86
            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)
86
        if let Some(tag_id) = get_tag_id(self.ctx.styled_dom, Some(node_id)) {
59
            builder.push_hit_test_area(border_box_bounds, tag_id);
59
        }
86
    }
    // +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
1266022
    fn establishes_stacking_context(&self, node_index: usize) -> bool {
1266022
        let Some(node) = self.positioned_tree.tree.get(LayoutNodeId::new(node_index)) else {
            return false;
        };
1266022
        let Some(dom_id) = node.dom_node_id else {
262
            return false;
        };
1265760
        let position = get_position_type(self.ctx.styled_dom, Some(dom_id));
1265760
        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
1265760
        if position == LayoutPosition::Fixed || position == LayoutPosition::Sticky {
            return true;
1265760
        }
        // +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
1265760
        if position == LayoutPosition::Absolute {
5174
            return !z_auto;
1260586
        }
        // position:relative with explicit z-index integer establishes stacking context
1260586
        if position == LayoutPosition::Relative && !z_auto {
            return true;
1260586
        }
1260586
        if let Some(styled_node) = self.ctx.styled_dom.styled_nodes.as_container().get(dom_id) {
1260586
            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
1260586
            let node_state =
1260586
                &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
            // Opacity < 1 (GPU: fast path via compact cache)
1260586
            if crate::solver3::getters::get_opacity(
1260586
                self.ctx.styled_dom, dom_id, node_state,
1260586
            ) < 1.0 {
26
                return true;
1260560
            }
            // Transform != none (GPU: has_transform bit check, then slow walk only if set)
1260560
            if let Some(t) = crate::solver3::getters::get_transform(
1260560
                self.ctx.styled_dom, dom_id, node_state,
1260560
            ) {
2
                if !t.is_empty() {
2
                    return true;
                }
1260558
            }
        }
1260558
        false
1266022
    }
}
/// 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.
#[derive(Debug)]
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,
}
/// 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`.
#[allow(clippy::trivially_copy_pass_by_ref)] // <=8B Copy param kept by-ref intentionally (hot pixel/coord path or to avoid churning call sites for a perf-neutral change)
3368
fn apply_overflow_clip_margin(
3368
    clip_rect: &mut LogicalRect,
3368
    overflow_x: &super::getters::MultiValue<LayoutOverflow>,
3368
    overflow_y: &super::getters::MultiValue<LayoutOverflow>,
3368
    styled_dom: &StyledDom,
3368
    dom_id: NodeId,
3368
    styled_node_state: &azul_core::styled_dom::StyledNodeState,
3368
) {
3368
    if !overflow_x.is_clip() && !overflow_y.is_clip() {
3368
        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;
    }
3368
}
5
fn get_scroll_id(id: Option<NodeId>) -> LocalScrollId {
5
    id.map_or(0, |i| i.index() as u64)
5
}
/// 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
286552
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)
286552
    if let Some(overflow_size) = warm.and_then(|w| w.overflow_content_size) {
286552
        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;
        // §3.2 (d3): the extent from the DENSE arrays when retained and the
        // layout is pure clusters — width from base advances (== the sparse
        // bounds().width since d2), height from the d3-filled line heights.
        // Mixed layouts (objects/tabs) keep the sparse walk.
        let dense_extent = cached_layout.dense.as_ref().and_then(|d| {
            // (d6h) Sentinel-aware: post-retirement the stored items are
            // EMPTY and dense IS the content — the old equality guard
            // read n != 0 and rejected the dense arm, zeroing the
            // extent (no scrollbar, dead caret-reveal; caught by
            // caret_scroll_glide under the d7 default flip). Mixed
            // layouts (flag-off with objects/tabs) still fall back.
            if d.clusters.is_empty() {
                return None;
            }
            if !text_layout.items.is_empty() && d.clusters.len() != text_layout.items.len() {
                return None;
            }
            let max_x = d
                .clusters
                .iter()
                .map(|c| c.x + c.advance)
                .fold(0.0f32, f32::max);
            let max_y = d
                .lines
                .iter()
                .map(|l| l.top_y + l.height)
                .fold(0.0f32, f32::max);
            Some((max_x, max_y))
        });
        let (max_x, max_y) = if let Some((dx, dy)) = dense_extent {
            if std::env::var("AZ_DENSE_TEXT").as_deref() == Ok("verify") {
                let mut sx: f32 = 0.0;
                let mut sy: f32 = 0.0;
                for positioned_item in &text_layout.items {
                    let item_bounds = positioned_item.item.bounds();
                    sx = sx.max(positioned_item.position.x + item_bounds.width);
                    sy = sy.max(positioned_item.position.y + item_bounds.height);
                }
                assert!(
                    (sx - dx).abs() < 0.01 && (sy - dy).abs() < 0.01,
                    "d3 verify: scroll extent diverged (sparse {sx}x{sy} vs dense {dx}x{dy})"
                );
            }
            (dx, dy)
        } else {
            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);
            }
            (max_x, max_y)
        };
        // 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
286552
}
662895
fn get_tag_id(dom: &StyledDom, id: Option<NodeId>) -> Option<DisplayListTagId> {
662895
    let node_id = id?;
74397394
    let tag_mapping = dom.tag_ids_to_node_ids.as_ref().iter().find(|m| {
74397175
        m.node_id.into_crate_internal() == Some(node_id)
74397374
    })?;
315605
    Some((tag_mapping.tag_id.inner, TAG_TYPE_DOM_NODE))
662895
}
/// Resolve an [`ImageSource`] (as carried by an inline `InlineContent::Image`)
/// to a concrete [`ImageRef`] ready for `push_image`.
///
/// `target_size` is the object's logical box size, used only to size the raster
/// when rasterizing an SVG source.
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // bounded graphics/coord/font/fixed-point/debug-marker cast
fn get_image_ref_for_image_source(
    source: &ImageSource,
    image_cache: &azul_core::resources::ImageCache,
    target_size: LogicalSize,
) -> Option<ImageRef> {
    match source {
        ImageSource::Ref(image_ref) => Some(image_ref.clone()),
        // Node-scoped content resolves through the overlay→DOM resolver, which
        // needs the LayoutContext — `paint_inline_object` handles that arm
        // BEFORE calling here. A caller without a resolver cannot paint it.
        ImageSource::Node(_) => None,
        ImageSource::Url(url) => {
            // CSS url() image — resolved exactly like `background-image`: look it
            // up in the ImageCache by its CSS id (see push_backgrounds_and_border).
            let css_id: azul_css::AzString = url.clone().into();
            image_cache.get_css_image_id(&css_id).cloned()
        }
        ImageSource::Data(bytes) => {
            // Encoded image bytes (PNG/JPEG/…): decode to a RawImage, then build
            // an ImageRef. The `decode` module is gated on `std` and the decoder
            // itself needs the `image` crate (`image_decoding`).
            #[cfg(all(feature = "std", feature = "image_decoding"))]
            {
                use crate::image::decode::{
                    decode_raw_image_from_any_bytes, ResultRawImageDecodeImageError,
                };
                if let ResultRawImageDecodeImageError::Ok(raw) =
                    decode_raw_image_from_any_bytes(bytes)
                {
                    return ImageRef::new_rawimage(raw);
                }
                None
            }
            #[cfg(not(all(feature = "std", feature = "image_decoding")))]
            {
                // The document handed us encoded image bytes and this build
                // cannot decode them — the image just vanishes. Say so once.
                static ANNOUNCE: std::sync::Once = std::sync::Once::new();
                ANNOUNCE.call_once(|| {
                    eprintln!(
                        "[azul][image] encoded image data present, but this build \
                         lacks the `image_decoding` (+`std`) feature — images from \
                         encoded bytes will NOT appear"
                    );
                });
                let _ = bytes;
                None
            }
        }
        ImageSource::Svg(svg) => {
            // Rasterize the SVG source to the object's box size using the CPU SVG
            // renderer. Needs the `cpurender` feature.
            #[cfg(feature = "cpurender")]
            {
                let w = (target_size.width.round() as u32).max(1);
                let h = (target_size.height.round() as u32).max(1);
                crate::cpurender::render_svg_to_imageref(svg.as_bytes(), w, h).ok()
            }
            #[cfg(not(feature = "cpurender"))]
            {
                // Inline SVG images rasterize through the CPU renderer; without
                // it they silently vanish from the frame. Say so once.
                static ANNOUNCE: std::sync::Once = std::sync::Once::new();
                ANNOUNCE.call_once(|| {
                    eprintln!(
                        "[azul][svg] an SVG image source is present, but this build \
                         has no `cpurender` feature — SVG images will NOT appear"
                    );
                });
                let _ = (svg, target_size);
                None
            }
        }
        ImageSource::Placeholder(_) => {
            // Layout-only placeholder: reserves space, paints nothing.
            None
        }
    }
}
/// Get the bounds of a display list item in window-logical coordinates.
156238
fn get_display_item_bounds(item: &DisplayListItem) -> Option<WindowLogicalRect> {
156238
    item.bounds().map(WindowLogicalRect::from)
156238
}
/// 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.
#[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
29574
fn clip_and_offset_display_item(
29574
    item: &DisplayListItem,
29574
    page_top: f32,
29574
    page_bottom: f32,
29574
) -> Option<DisplayListItem> {
29574
    match item {
        DisplayListItem::Rect {
2085
            bounds,
2085
            color,
2085
            border_radius,
2085
        } => clip_rect_item(bounds.into_inner(), *color, *border_radius, page_top, page_bottom),
        DisplayListItem::Border {
8902
            bounds,
8902
            widths,
8902
            colors,
8902
            styles,
8902
            border_radius,
8902
        } => clip_border_item(
8902
            bounds.into_inner(),
8902
            *widths,
8902
            *colors,
8902
            *styles,
8902
            *border_radius,
8902
            page_top,
8902
            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 {
4248
            layout,
4248
            bounds,
4248
            font_hash,
4248
            font_size_px,
4248
            color,
4248
        } => clip_text_layout_item(
4248
            layout,
4248
            bounds.into_inner(),
4248
            *font_hash,
4248
            *font_size_px,
4248
            *color,
4248
            page_top,
4248
            page_bottom,
        ),
        DisplayListItem::Text {
4689
            glyphs,
4689
            font_hash,
4689
            font_size_px,
4689
            color,
4689
            clip_rect,
            ..
4689
        } => clip_text_item(
4689
            glyphs,
4689
            *font_hash,
4689
            *font_size_px,
4689
            *color,
4689
            clip_rect.into_inner(),
4689
            page_top,
4689
            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,
        ),
9567
        DisplayListItem::HitTestArea { bounds, tag } => {
9567
            clip_hit_test_area_item(bounds.into_inner(), *tag, page_top, page_bottom)
        }
        DisplayListItem::VirtualView {
            child_dom_id,
            bounds,
            clip_rect,
            content_offset,
        } => clip_virtual_view_item(*child_dom_id, bounds.into_inner(), clip_rect.into_inner(), *content_offset, 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
34
        | DisplayListItem::VirtualViewPlaceholder { .. } => None,
        // Gradient items - simple bounds check
        DisplayListItem::LinearGradient {
38
            bounds,
38
            gradient,
38
            border_radius,
        } => {
38
            if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
1
                None
            } else {
37
                Some(DisplayListItem::LinearGradient {
37
                    bounds: offset_rect_y(bounds.into_inner(), -page_top).into(),
37
                    gradient: gradient.clone(),
37
                    border_radius: *border_radius,
37
                })
            }
        }
        DisplayListItem::RadialGradient {
9
            bounds,
9
            gradient,
9
            border_radius,
        } => {
9
            if bounds.0.origin.y + bounds.0.size.height < page_top || bounds.0.origin.y > page_bottom {
                None
            } else {
9
                Some(DisplayListItem::RadialGradient {
9
                    bounds: offset_rect_y(bounds.into_inner(), -page_top).into(),
9
                    gradient: gradient.clone(),
9
                    border_radius: *border_radius,
9
                })
            }
        }
        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 { .. }
2
        | DisplayListItem::PopImageMaskClip => None,
    }
29574
}
// 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.
2087
fn clip_rect_item(
2087
    bounds: LogicalRect,
2087
    color: ColorU,
2087
    border_radius: BorderRadius,
2087
    page_top: f32,
2087
    page_bottom: f32,
2087
) -> Option<DisplayListItem> {
2087
    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::Rect {
1220
        bounds: clipped.into(),
1220
        color,
1220
        border_radius,
1220
    })
2087
}
/// Clips a border to page bounds, hiding top/bottom borders when clipped.
8903
fn clip_border_item(
8903
    bounds: LogicalRect,
8903
    widths: StyleBorderWidths,
8903
    colors: StyleBorderColors,
8903
    styles: StyleBorderStyles,
8903
    border_radius: StyleBorderRadius,
8903
    page_top: f32,
8903
    page_bottom: f32,
8903
) -> Option<DisplayListItem> {
8903
    let original_bounds = bounds;
8903
    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| {
7785
        let new_widths = adjust_border_widths_for_clipping(
7785
            widths,
7785
            original_bounds,
7785
            clipped,
7785
            page_top,
7785
            page_bottom,
        );
7785
        DisplayListItem::Border {
7785
            bounds: clipped.into(),
7785
            widths: new_widths,
7785
            colors,
7785
            styles,
7785
            border_radius,
7785
        }
7785
    })
8903
}
/// Adjusts border widths when a border is clipped at page boundaries.
/// Hides top border if clipped at top, bottom border if clipped at bottom.
7787
fn adjust_border_widths_for_clipping(
7787
    mut widths: StyleBorderWidths,
7787
    original_bounds: LogicalRect,
7787
    clipped: LogicalRect,
7787
    page_top: f32,
7787
    page_bottom: f32,
7787
) -> StyleBorderWidths {
    // Hide top border if we clipped the top
7787
    if clipped.origin.y > 0.0 && original_bounds.origin.y < page_top {
        widths.top = None;
7787
    }
    // Hide bottom border if we clipped the bottom
7787
    let original_bottom = original_bounds.origin.y + original_bounds.size.height;
7787
    let clipped_bottom = clipped.origin.y + clipped.size.height;
7787
    if original_bottom > page_bottom && clipped_bottom >= page_bottom - page_top - 1.0 {
371
        widths.bottom = None;
7481
    }
7787
    widths
7787
}
/// Clips a selection rectangle to page bounds.
2
fn clip_selection_rect_item(
2
    bounds: LogicalRect,
2
    border_radius: BorderRadius,
2
    color: ColorU,
2
    page_top: f32,
2
    page_bottom: f32,
2
) -> Option<DisplayListItem> {
2
    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::SelectionRect {
1
        bounds: clipped.into(),
1
        border_radius,
1
        color,
1
    })
2
}
/// Clips a cursor rectangle to page bounds.
2
fn clip_cursor_rect_item(
2
    bounds: LogicalRect,
2
    color: ColorU,
2
    page_top: f32,
2
    page_bottom: f32,
2
) -> Option<DisplayListItem> {
2
    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::CursorRect {
1
        bounds: clipped.into(),
1
        color,
1
    })
2
}
/// Clips an image to page bounds if it overlaps the page.
2
fn clip_image_item(
2
    bounds: LogicalRect,
2
    image: ImageRef,
2
    border_radius: BorderRadius,
2
    page_top: f32,
2
    page_bottom: f32,
2
) -> Option<DisplayListItem> {
2
    if !rect_intersects(&bounds, page_top, page_bottom) {
1
        return None;
1
    }
1
    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::Image {
1
        bounds: clipped.into(),
1
        image,
1
        border_radius,
1
    })
2
}
/// Clips a text layout block to page bounds, filtering individual text items.
4250
fn clip_text_layout_item(
4250
    layout: &Arc<dyn std::any::Any + Send + Sync>,
4250
    bounds: LogicalRect,
4250
    font_hash: FontHash,
4250
    font_size_px: f32,
4250
    color: ColorU,
4250
    page_top: f32,
4250
    page_bottom: f32,
4250
) -> Option<DisplayListItem> {
4250
    if !rect_intersects(&bounds, page_top, page_bottom) {
333
        return None;
3917
    }
    // (d5/d6h) TextPayload carries both forms. Post-retirement the
    // sparse half is the EMPTY sentinel — the clipper materializes the
    // items from the dense arrays on demand (transient, export-time
    // only; exactness pinned by the d6h expansion gate).
    #[cfg(feature = "text_layout")]
3917
    if let Some(p) = layout.downcast_ref::<crate::solver3::layout_tree::TextPayload>() {
3916
        if p.sparse.items.is_empty() && !p.dense.clusters.is_empty() {
3789
            let expanded = UnifiedLayout {
3789
                items: p.dense.to_unified_items(),
3789
                overflow: p.sparse.overflow.clone(),
3789
            };
3789
            return clip_unified_layout(
3789
                &expanded,
3789
                bounds,
3789
                font_hash,
3789
                font_size_px,
3789
                color,
3789
                page_top,
3789
                page_bottom,
            );
127
        }
127
        return clip_unified_layout(
127
            &p.sparse,
127
            bounds,
127
            font_hash,
127
            font_size_px,
127
            color,
127
            page_top,
127
            page_bottom,
        );
1
    }
    // Try to downcast and filter UnifiedLayout items
    #[cfg(feature = "text_layout")]
1
    if let Some(unified_layout) = layout.downcast_ref::<UnifiedLayout>() {
1
        return clip_unified_layout(
1
            unified_layout,
1
            bounds,
1
            font_hash,
1
            font_size_px,
1
            color,
1
            page_top,
1
            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,
    })
4250
}
/// Clips a `UnifiedLayout` by filtering items to those on the current page.
#[cfg(feature = "text_layout")]
3917
fn clip_unified_layout(
3917
    unified_layout: &UnifiedLayout,
3917
    bounds: LogicalRect,
3917
    font_hash: FontHash,
3917
    font_size_px: f32,
3917
    color: ColorU,
3917
    page_top: f32,
3917
    page_bottom: f32,
3917
) -> Option<DisplayListItem> {
3917
    let layout_origin_y = bounds.origin.y;
3917
    let layout_origin_x = bounds.origin.x;
    // Filter items whose center falls within this page
3917
    let filtered_items: Vec<_> = unified_layout
3917
        .items
3917
        .iter()
32762
        .filter(|item| item_center_on_page(item, layout_origin_y, page_top, page_bottom))
3917
        .cloned()
3917
        .collect();
3917
    if filtered_items.is_empty() {
        return None;
3917
    }
    // Calculate new origin for page-relative positioning
3917
    let new_origin_y = (layout_origin_y - page_top).max(0.0);
    // Transform items to page-relative coordinates and calculate bounds
3917
    let (offset_items, min_y, max_y, max_width) =
3917
        transform_items_to_page_coords(filtered_items, layout_origin_y, page_top, new_origin_y);
3917
    let new_layout = UnifiedLayout {
3917
        items: offset_items,
3917
        overflow: unified_layout.overflow.clone(),
3917
    };
3917
    let new_bounds = LogicalRect {
3917
        origin: LogicalPosition {
3917
            x: layout_origin_x,
3917
            y: new_origin_y,
3917
        },
3917
        size: LogicalSize {
3917
            width: max_width.max(bounds.size.width),
3917
            height: (max_y - min_y.min(0.0)).max(0.0),
3917
        },
3917
    };
3917
    Some(DisplayListItem::TextLayout {
3917
        layout: Arc::new(new_layout),
3917
        bounds: new_bounds.into(),
3917
        font_hash,
3917
        font_size_px,
3917
        color,
3917
    })
3917
}
/// Checks if an item's center point falls within the page bounds.
#[cfg(feature = "text_layout")]
32769
fn item_center_on_page(
32769
    item: &PositionedItem,
32769
    layout_origin_y: f32,
32769
    page_top: f32,
32769
    page_bottom: f32,
32769
) -> bool {
32769
    let item_y_absolute = layout_origin_y + item.position.y;
32769
    let item_height = item.item.bounds().height;
32769
    let item_center_y = item_y_absolute + (item_height / 2.0);
32769
    item_center_y >= page_top && item_center_y < page_bottom
32769
}
/// Transforms filtered items to page-relative coordinates.
/// Returns (items, `min_y`, `max_y`, `max_width`).
#[cfg(feature = "text_layout")]
3919
fn transform_items_to_page_coords(
3919
    items: Vec<PositionedItem>,
3919
    layout_origin_y: f32,
3919
    page_top: f32,
3919
    new_origin_y: f32,
3919
) -> (Vec<PositionedItem>, f32, f32, f32) {
3919
    let mut min_y = f32::MAX;
3919
    let mut max_y = f32::MIN;
3919
    let mut max_width = 0.0f32;
3919
    let offset_items: Vec<_> = items
3919
        .into_iter()
32764
        .map(|mut item| {
32764
            let abs_y = layout_origin_y + item.position.y;
32764
            let page_y = abs_y - page_top;
32764
            let new_item_y = page_y - new_origin_y;
32764
            let item_bounds = item.item.bounds();
32764
            min_y = min_y.min(new_item_y);
32764
            max_y = max_y.max(new_item_y + item_bounds.height);
32764
            max_width = max_width.max(item.position.x + item_bounds.width);
32764
            item.position.y = new_item_y;
32764
            item
32764
        })
3919
        .collect();
3919
    (offset_items, min_y, max_y, max_width)
3919
}
/// Clips a text glyph run to page bounds, filtering individual glyphs.
4694
fn clip_text_item(
4694
    glyphs: &[GlyphInstance],
4694
    font_hash: FontHash,
4694
    font_size_px: f32,
4694
    color: ColorU,
4694
    clip_rect: LogicalRect,
4694
    page_top: f32,
4694
    page_bottom: f32,
4694
) -> Option<DisplayListItem> {
4694
    if !rect_intersects(&clip_rect, page_top, page_bottom) {
343
        return None;
4351
    }
    // Filter glyphs using center-point decision (baseline position)
4351
    let page_glyphs: Vec<_> = glyphs
4351
        .iter()
32588
        .filter(|g| g.point.y >= page_top && g.point.y < page_bottom)
4351
        .map(|g| GlyphInstance {
32582
            index: g.index,
32582
            point: LogicalPosition {
32582
                x: g.point.x,
32582
                y: g.point.y - page_top,
32582
            },
32582
            size: g.size,
32582
        })
4351
        .collect();
4351
    if page_glyphs.is_empty() {
3
        return None;
4348
    }
4348
    Some(DisplayListItem::Text {
4348
        glyphs: page_glyphs,
4348
        font_hash,
4348
        font_size_px,
4348
        color,
4348
        clip_rect: offset_rect_y(clip_rect, -page_top).into(),
4348
        source_node_index: None,
4348
    })
4694
}
/// Clips a text decoration (underline, strikethrough, or overline) to page bounds.
4
fn clip_text_decoration_item(
4
    bounds: LogicalRect,
4
    color: ColorU,
4
    thickness: f32,
4
    decoration_type: TextDecorationType,
4
    page_top: f32,
4
    page_bottom: f32,
4
) -> Option<DisplayListItem> {
4
    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| match decoration_type {
1
        TextDecorationType::Underline => DisplayListItem::Underline {
1
            bounds: clipped.into(),
1
            color,
1
            thickness,
1
        },
1
        TextDecorationType::Strikethrough => DisplayListItem::Strikethrough {
1
            bounds: clipped.into(),
1
            color,
1
            thickness,
1
        },
1
        TextDecorationType::Overline => DisplayListItem::Overline {
1
            bounds: clipped.into(),
1
            color,
1
            thickness,
1
        },
3
    })
4
}
/// Clips a scrollbar to page bounds.
2
fn clip_scrollbar_item(
2
    bounds: LogicalRect,
2
    color: ColorU,
2
    orientation: ScrollbarOrientation,
2
    opacity_key: Option<OpacityKey>,
2
    hit_id: Option<azul_core::hit_test::ScrollbarHitId>,
2
    page_top: f32,
2
    page_bottom: f32,
2
) -> Option<DisplayListItem> {
2
    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::ScrollBar {
1
        bounds: clipped.into(),
1
        color,
1
        orientation,
1
        opacity_key,
1
        hit_id,
1
    })
2
}
/// Clips a hit test area to page bounds.
9569
fn clip_hit_test_area_item(
9569
    bounds: LogicalRect,
9569
    tag: DisplayListTagId,
9569
    page_top: f32,
9569
    page_bottom: f32,
9569
) -> Option<DisplayListItem> {
9569
    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::HitTestArea {
9055
        bounds: clipped.into(),
9055
        tag,
9055
    })
9569
}
/// Clips a virtualized view to page bounds.
2
fn clip_virtual_view_item(
2
    child_dom_id: DomId,
2
    bounds: LogicalRect,
2
    clip_rect: LogicalRect,
2
    content_offset: LogicalPosition,
2
    page_top: f32,
2
    page_bottom: f32,
2
) -> Option<DisplayListItem> {
2
    clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::VirtualView {
1
        child_dom_id,
1
        bounds: clipped.into(),
1
        clip_rect: offset_rect_y(clip_rect, -page_top).into(),
        // A page break moves the BOX, not the window the content sits in.
1
        content_offset,
1
    })
2
}
/// Clip a rectangle to page bounds and offset to page-relative coordinates.
/// Returns None if the rectangle is completely outside the page.
20587
fn clip_rect_bounds(bounds: LogicalRect, page_top: f32, page_bottom: f32) -> Option<LogicalRect> {
20587
    let item_top = bounds.origin.y;
20587
    let item_bottom = bounds.origin.y + bounds.size.height;
    // Check if completely outside page
20587
    if item_bottom <= page_top || item_top >= page_bottom {
2509
        return None;
18078
    }
    // Calculate clipped bounds
18078
    let clipped_top = item_top.max(page_top);
18078
    let clipped_bottom = item_bottom.min(page_bottom);
18078
    let clipped_height = clipped_bottom - clipped_top;
    // Offset to page-relative coordinates
18078
    let page_relative_y = clipped_top - page_top;
18078
    Some(LogicalRect {
18078
        origin: LogicalPosition {
18078
            x: bounds.origin.x,
18078
            y: page_relative_y,
18078
        },
18078
        size: LogicalSize {
18078
            width: bounds.size.width,
18078
            height: clipped_height,
18078
        },
18078
    })
20587
}
/// Check if a rectangle intersects the page bounds.
8959
fn rect_intersects(bounds: &LogicalRect, page_top: f32, page_bottom: f32) -> bool {
8959
    let item_top = bounds.origin.y;
8959
    let item_bottom = bounds.origin.y + bounds.size.height;
8959
    item_bottom > page_top && item_top < page_bottom
8959
}
/// Offset a rectangle's Y coordinate.
4437
fn offset_rect_y(bounds: LogicalRect, offset_y: f32) -> LogicalRect {
4437
    LogicalRect {
4437
        origin: LogicalPosition {
4437
            x: bounds.origin.x,
4437
            y: bounds.origin.y + offset_y,
4437
        },
4437
        size: bounds.size,
4437
    }
4437
}
// 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::page_breaks::{self, BreakPolicy};
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,
    /// Break-awareness policy (all-off default = plain interval slicing).
    pub break_policy: BreakPolicy,
    /// office-suite-style per-page setup (default + overrides + parity). `None`
    /// = every page uses `header_footer` / `page_content_height` uniformly.
    pub page_sequence: Option<crate::solver3::pagination::PageSequence>,
}
impl SlicerConfig {
    /// Create a simple slicer config with no gaps between pages.
23
    #[must_use] pub fn simple(page_height: f32) -> Self {
23
        Self {
23
            page_content_height: page_height,
23
            page_gap: 0.0,
23
            allow_clipping: true,
23
            header_footer: HeaderFooterConfig::default(),
23
            page_width: DEFAULT_A4_WIDTH_PT, // Default A4 width in points
23
            table_headers: TableHeaderTracker::default(),
23
            break_policy: BreakPolicy::default(),
23
            page_sequence: None,
23
        }
23
    }
    /// Create a slicer config with margins/gaps between pages.
6
    #[must_use] pub fn with_gap(page_height: f32, gap: f32) -> Self {
6
        Self {
6
            page_content_height: page_height,
6
            page_gap: gap,
6
            allow_clipping: true,
6
            header_footer: HeaderFooterConfig::default(),
6
            page_width: DEFAULT_A4_WIDTH_PT,
6
            table_headers: TableHeaderTracker::default(),
6
            break_policy: BreakPolicy::default(),
6
            page_sequence: None,
6
        }
6
    }
    /// Set the break-awareness policy.
    #[must_use] pub const fn with_break_policy(
        mut self,
        policy: BreakPolicy,
    ) -> Self {
        self.break_policy = policy;
        self
    }
    /// Add header/footer configuration.
    #[must_use] pub fn with_header_footer(mut self, config: HeaderFooterConfig) -> Self {
        self.header_footer = config;
        self
    }
    /// Set the page width (for header/footer positioning).
1
    #[must_use] pub const fn with_page_width(mut self, width: f32) -> Self {
1
        self.page_width = width;
1
        self
1
    }
    /// Add table headers for repetition.
    #[must_use] 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.
73
    #[must_use] pub fn page_slot_height(&self) -> f32 {
73
        self.page_content_height + self.page_gap
73
    }
    /// Calculate which page a Y coordinate falls on.
    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // bounded graphics/coord/font/fixed-point/debug-marker cast
27
    #[must_use] pub fn page_for_y(&self, y: f32) -> usize {
27
        if self.page_slot_height() <= 0.0 {
6
            return 0;
21
        }
21
        (y / self.page_slot_height()).floor() as usize
27
    }
    /// Get the Y range for a specific page (in infinite canvas coordinates).
    #[allow(clippy::cast_precision_loss)] // bounded graphics/coord/font/fixed-point/debug-marker cast
17
    #[must_use] pub fn page_bounds(&self, page_index: usize) -> (f32, f32) {
17
        let start = page_index as f32 * self.page_slot_height();
17
        let end = start + self.page_content_height;
17
        (start, end)
17
    }
}
/// 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.
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
/// # Errors
///
/// Returns a `LayoutError` if paginating the display list fails.
8
pub fn paginate_display_list_with_slicer_and_breaks(
8
    full_display_list: DisplayList,
8
    config: &SlicerConfig,
8
    renderer_resources: &RendererResources,
8
) -> Result<Vec<DisplayList>> {
8
    if config.page_content_height <= 0.0 || config.page_content_height >= f32::MAX {
4
        return Ok(vec![full_display_list]);
4
    }
    // Step 1: Calculate page break positions based on CSS properties
    // (forced break-before/after positions + regular interval breaks).
4
    let constraints = page_breaks::PageConstraints::from_slicer_config(config);
4
    let breaks =
4
        page_breaks::compute_page_breaks_from_display_list(&full_display_list, &constraints);
4
    paginate_display_list_with_breaks(full_display_list, config, &breaks, renderer_resources)
8
}
/// Paginate against a PRE-COMPUTED break analysis (see
/// [`page_breaks::compute_page_breaks_from_display_list`]).
///
/// The sibling of [`paginate_display_list_with_slicer_and_breaks`], which
/// computes the breaks itself and delegates here. Lets embedders analyze
/// pagination once and materialize pages separately.
/// # Errors
///
/// Returns a `LayoutError` if paginating the display list fails.
1139
pub fn paginate_display_list_with_breaks(
1139
    full_display_list: DisplayList,
1139
    config: &SlicerConfig,
1139
    breaks: &[page_breaks::PageBreakPosition],
1139
    renderer_resources: &RendererResources,
1139
) -> Result<Vec<DisplayList>> {
1139
    Ok(paginate_pages_impl(
1139
        full_display_list,
1139
        config,
1139
        breaks,
1139
        renderer_resources,
1139
        None,
1139
    ))
1139
}
/// Materialize ONE page of a paginated document — the lazy-viewer entry.
///
/// A document editor computes breaks once (`compute_page_breaks*`), then
/// materializes only the visible pages. Headers/footers still show the
/// correct "page N of TOTAL" because the total comes from `breaks`, not from
/// how many pages were materialized.
///
/// Returns an empty display list when `page_index` is out of range.
/// # Errors
///
/// Returns a `LayoutError` if paginating the display list fails.
5
pub fn paginate_single_page(
5
    full_display_list: DisplayList,
5
    config: &SlicerConfig,
5
    breaks: &[page_breaks::PageBreakPosition],
5
    renderer_resources: &RendererResources,
5
    page_index: usize,
5
) -> Result<DisplayList> {
5
    let mut pages = paginate_pages_impl(
5
        full_display_list,
5
        config,
5
        breaks,
5
        renderer_resources,
5
        Some(page_index),
    );
5
    Ok(pages.pop().unwrap_or_default())
5
}
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
1144
fn paginate_pages_impl(
1144
    full_display_list: DisplayList,
1144
    config: &SlicerConfig,
1144
    breaks: &[page_breaks::PageBreakPosition],
1144
    renderer_resources: &RendererResources,
1144
    only_page: Option<usize>,
1144
) -> Vec<DisplayList> {
1144
    if config.page_content_height <= 0.0 || config.page_content_height >= f32::MAX {
7
        return vec![full_display_list];
1137
    }
1137
    let total_height = calculate_display_list_height(&full_display_list);
1137
    let mut page_spans = page_breaks::page_spans(breaks, total_height);
1137
    if page_spans.is_empty() {
        // An empty/zero-height document still produces one page tall enough
        // for the first page, so headers/footers render on it.
        let constraints = page_breaks::PageConstraints::from_slicer_config(config);
        page_spans.push((
            0.0,
            total_height.max(constraints.first_page_content_height),
        ));
1137
    }
1137
    let num_pages = page_spans.len();
    // Create per-page display lists by slicing the master list
1137
    let mut pages: Vec<DisplayList> = Vec::with_capacity(num_pages.min(only_page.map_or(usize::MAX, |_| 1)));
1328
    for (page_idx, &(content_start_y, content_end_y)) in page_spans.iter().enumerate() {
        // Lazy single-page materialization: skip every other span. PageInfo
        // below still receives the TRUE total (num_pages).
1328
        if only_page.is_some_and(|p| p != page_idx) {
16
            continue;
1312
        }
        // Generate page info for header/footer content
1312
        let page_info = PageInfo::new(page_idx + 1, num_pages);
        // Calculate per-page header/footer space. With a page SEQUENCE the
        // decoration comes from THIS page's setup (override/first/parity/
        // default) — the classic office suites model.
1312
        let hf = config
1312
            .page_sequence
1312
            .as_ref()
1312
            .map_or(&config.header_footer, |s| {
                &s.setup_for_page(page_idx).header_footer
            });
1312
        let skip_this_page = hf.skip_first_page && page_info.is_first;
1312
        let header_space = if hf.show_header && !skip_this_page {
4
            hf.header_height
        } else {
1308
            0.0
        };
1312
        let footer_space = if hf.show_footer && !skip_this_page {
16
            hf.footer_height
        } else {
1296
            0.0
        };
1312
        let _ = footer_space; // Currently unused but reserved for future
1312
        let mut page_items = Vec::new();
1312
        let mut page_node_mapping = Vec::new();
        // 1. Add header if enabled
1312
        if hf.show_header && !skip_this_page {
4
            let header_text = hf.header_text(page_info);
4
            if !header_text.is_empty() {
4
                let header_items = generate_text_display_items(
4
                    &header_text,
4
                    LogicalRect {
4
                        origin: LogicalPosition { x: 0.0, y: 0.0 },
4
                        size: LogicalSize {
4
                            width: config.page_width,
4
                            height: hf.header_height,
4
                        },
4
                    },
4
                    hf.font_size,
4
                    hf.text_color,
4
                    TextAlignment::Center,
4
                    renderer_resources,
                );
4
                for item in header_items {
                    page_items.push(item);
                    page_node_mapping.push(None);
                }
            }
1308
        }
        // 2. Inject repeated table headers (if any). Placement is X-AWARE:
        // two sibling tables can only both straddle a page top when they sit
        // SIDE BY SIDE (columns) — each repeated thead then stays in its own
        // x-band at the page top, and each table's continued rows shift down
        // by the summed heights of the theads whose x-band OVERLAPS theirs
        // (side-by-side: their own thead only; nested tables: outer + inner
        // stack). The old code stacked every thead vertically AND shifted
        // ALL content by max(height) — two straddling tables overlapped.
        struct TheadBand {
            x_start: f32,
            x_end: f32,
            /// Cumulative shift for content whose x overlaps this band
            /// (this table's thead + every x-overlapping one injected
            /// above it).
            shift: f32,
            /// Rebased end of the table's content on this page (below it
            /// the SUM of all thead heights applies — matching the break
            /// pass's per-page reservation).
            table_end_page_local: f32,
        }
1312
        let straddlers = config.table_headers.straddling_tables_for_page(
1312
            page_idx,
1312
            content_start_y,
1312
            content_end_y,
        );
1312
        let mut bands: Vec<TheadBand> = Vec::new();
1312
        let mut sum_all_theads = 0.0f32;
1315
        for table in &straddlers {
            // The table's x-extent, from its thead's own geometry.
3
            let (mut x_start, mut x_end) = (f32::MAX, f32::MIN);
6
            for item in &table.thead_items {
3
                if let Some(b) = item.visual_bounds() {
3
                    x_start = x_start.min(b.origin.x);
3
                    x_end = x_end.max(b.origin.x + b.size.width);
3
                }
            }
3
            if x_start > x_end {
                (x_start, x_end) = (f32::MIN, f32::MAX); // no geometry: full width
3
            }
            // Stack below previously injected theads whose x-band overlaps
            // (nested tables); disjoint (side-by-side) theads sit at the top.
3
            let stacked_above: f32 = bands
3
                .iter()
3
                .filter(|b| b.x_start < x_end && x_start < b.x_end)
3
                .map(|b| b.shift)
3
                .fold(0.0, f32::max);
3
            let thead_y = header_space + stacked_above;
6
            for item in &table.thead_items {
3
                page_items.push(offset_display_item_y(item, thead_y));
3
                page_node_mapping.push(None);
3
            }
3
            bands.push(TheadBand {
3
                x_start,
3
                x_end,
3
                shift: stacked_above + table.thead_height,
3
                table_end_page_local: table.table_end_y - content_start_y,
3
            });
3
            sum_all_theads += table.thead_height;
        }
        // 3. Band-dependent shift for content items: inside a straddling
        // table's x-band and y-range → that band's shift; below EVERY
        // straddling table → the full sum (the break pass reserved it).
1312
        let below_all_y = bands
1312
            .iter()
1312
            .map(|b| b.table_end_page_local)
1312
            .fold(0.0f32, f32::max);
22448
        let thead_shift_for = |b: LogicalRect| -> f32 {
22448
            if bands.is_empty() {
22443
                return 0.0;
5
            }
5
            if b.origin.y >= below_all_y {
2
                return sum_all_theads;
3
            }
3
            let x_center = b.size.width.mul_add(0.5, b.origin.x);
3
            bands
3
                .iter()
5
                .filter(|band| x_center >= band.x_start && x_center < band.x_end)
3
                .map(|band| band.shift)
3
                .fold(0.0, f32::max)
22448
        };
        // 4. Slice and offset content items (skip fixed-position items, they are added in step 4b)
        //
        // E17: the clip / stacking / scroll-frame / text-shadow / image-mask
        // STRUCTURE is re-derived per page. Push/Pop markers are tracked in
        // a live stack and emitted LAZILY: a marker chain materializes only
        // when the first content item UNDER it survives this page's Y-band
        // (so pages without a subtree's content carry none of its markers,
        // and marker-free lists produce byte-identical output to before).
        // Balancing Pops are appended at the page end. Marker bounds get the
        // same rebase as the content they wrap (clip rects intersected with
        // the page band, so a partially-on-page clip keeps clipping).
        //
        // Previously EVERY marker was dropped at the page boundary: exports
        // showed unclipped overflow bleed and flattened z-order / opacity on
        // pages after the first affected one.
        struct OpenMarker {
            item_idx: usize,
            /// Already emitted into `page_items`?
            emitted: bool,
        }
1312
        let mut marker_stack: Vec<OpenMarker> = Vec::new();
        // Rebase a MARKER item into page-local space: intersect its rect
        // with the page band (a clip partially on this page keeps clipping;
        // an off-page clip collapses to an empty rect — its content is
        // off-page anyway), rebase to page-local Y, apply the header offset.
1384
        let rebase_band = |r: LogicalRect| -> LogicalRect {
1369
            let top = r.origin.y.max(content_start_y);
1369
            let bottom = (r.origin.y + r.size.height).min(content_end_y);
1369
            LogicalRect {
1369
                origin: LogicalPosition {
1369
                    x: r.origin.x,
1369
                    y: (top - content_start_y) + header_space,
1369
                },
1369
                size: LogicalSize {
1369
                    width: r.size.width,
1369
                    height: (bottom - top).max(0.0),
1369
                },
1369
            }
1369
        };
1384
        let rebase_marker = |item: &DisplayListItem| -> DisplayListItem {
1369
            match item {
                DisplayListItem::PushClip {
74
                    bounds,
74
                    border_radius,
74
                } => DisplayListItem::PushClip {
74
                    bounds: rebase_band(bounds.0).into(),
74
                    border_radius: *border_radius,
74
                },
1295
                DisplayListItem::PushStackingContext { z_index, bounds } => {
1295
                    DisplayListItem::PushStackingContext {
1295
                        z_index: *z_index,
1295
                        bounds: rebase_band(bounds.0).into(),
1295
                    }
                }
                DisplayListItem::PushScrollFrame {
                    clip_bounds,
                    content_size,
                    scroll_id,
                } => DisplayListItem::PushScrollFrame {
                    clip_bounds: rebase_band(clip_bounds.0).into(),
                    content_size: *content_size,
                    scroll_id: *scroll_id,
                },
                DisplayListItem::PushImageMaskClip {
                    bounds,
                    mask_image,
                    mask_rect,
                } => DisplayListItem::PushImageMaskClip {
                    bounds: rebase_band(bounds.0).into(),
                    mask_image: mask_image.clone(),
                    mask_rect: rebase_band(mask_rect.0).into(),
                },
                // PushTextShadow carries only the shadow definition.
                other => other.clone(),
            }
1369
        };
32359
        for (item_idx, item) in full_display_list.items.iter().enumerate() {
            // Maintain the live marker stack for EVERY item, retained or not.
32359
            if item.is_push_marker() {
1399
                marker_stack.push(OpenMarker {
1399
                    item_idx,
1399
                    emitted: false,
1399
                });
1399
                continue;
30960
            }
30960
            if item.is_pop_marker() {
1399
                if let Some(open) = marker_stack.pop() {
1399
                    if open.emitted {
1369
                        page_items.push(item.clone());
1369
                        page_node_mapping.push(None);
1369
                    }
                }
1399
                continue;
29561
            }
            // Skip items that belong to fixed-position elements (they are replicated separately)
29561
            let is_fixed = full_display_list.fixed_position_item_ranges.iter()
29561
                .any(|&(start, end)| item_idx >= start && item_idx < end);
29561
            if is_fixed {
                continue;
29561
            }
26363
            if let Some(clipped_item) =
29561
                clip_and_offset_display_item(item, content_start_y, content_end_y)
            {
                // The item survives this page: materialize its (not yet
                // emitted) enclosing marker chain, outermost first.
52891
                for open in &mut marker_stack {
26528
                    if !open.emitted {
1369
                        page_items.push(rebase_marker(
1369
                            &full_display_list.items[open.item_idx],
1369
                        ));
1369
                        page_node_mapping.push(None);
1369
                        open.emitted = true;
25159
                    }
                }
                // Page-local y of the clipped item decides which thead band
                // shifts it (items without geometry keep the base offset).
26363
                let item_shift = clipped_item
26363
                    .visual_bounds()
26363
                    .map_or(0.0, thead_shift_for);
26363
                let content_y_offset = header_space + item_shift;
26363
                let final_item = if content_y_offset > 0.0 {
31
                    offset_display_item_y(&clipped_item, content_y_offset)
                } else {
26332
                    clipped_item
                };
26363
                page_items.push(final_item);
26363
                let node_mapping = full_display_list
26363
                    .node_mapping
26363
                    .get(item_idx)
26363
                    .copied()
26363
                    .flatten();
26363
                page_node_mapping.push(node_mapping);
3198
            }
        }
        // Balance: close every marker still open AND emitted at the page end
        // (innermost first).
1312
        for open in marker_stack.iter().rev() {
            if open.emitted {
                if let Some(pop) = full_display_list.items[open.item_idx].matching_pop() {
                    page_items.push(pop);
                    page_node_mapping.push(None);
                }
            }
        }
        // 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.
1312
        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) {
                    // Fixed-position boxes anchor to the PAGE box: offset by
                    // the page header only, never by content thead shifts.
                    let final_item = if header_space > 0.0 {
                        offset_display_item_y(item, header_space)
                    } 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
1312
        if hf.show_footer && !skip_this_page {
16
            let footer_text = hf.footer_text(page_info);
16
            if !footer_text.is_empty() {
                // THIS page's content height: under a PageSequence pages
                // differ (first/parity/override); the uniform config height
                // is only the no-sequence fallback.
16
                let page_h = config
16
                    .page_sequence
16
                    .as_ref()
16
                    .map_or(config.page_content_height, |s| {
                        s.setup_for_page(page_idx).content_height()
                    });
16
                let footer_y = page_h - hf.footer_height;
16
                let footer_items = generate_text_display_items(
16
                    &footer_text,
16
                    LogicalRect {
16
                        origin: LogicalPosition {
16
                            x: 0.0,
16
                            y: footer_y,
16
                        },
16
                        size: LogicalSize {
16
                            width: config.page_width,
16
                            height: hf.footer_height,
16
                        },
16
                    },
16
                    hf.font_size,
16
                    hf.text_color,
16
                    TextAlignment::Center,
16
                    renderer_resources,
                );
16
                for item in footer_items {
                    page_items.push(item);
                    page_node_mapping.push(None);
                }
            }
1296
        }
1312
        pages.push(DisplayList {
1312
            items: page_items,
1312
            node_mapping: page_node_mapping,
1312
            forced_page_breaks: Vec::new(),
1312
            fixed_position_item_ranges: Vec::new(), // Already handled during pagination
1312
            // Page slices are consumed by renderers only — patching operates
1312
            // on the WINDOWED cached DL, never on page slices.
1312
            layout_node_mapping: Vec::new(),
1312
            uniform_text_bgs: Vec::new(),
1312
        });
    }
    // Ensure at least one page
1137
    if pages.is_empty() {
1
        pages.push(DisplayList::default());
1136
    }
1137
    pages
1144
}
/// Calculate page break positions respecting CSS forced page breaks.
///
/// Returns a vector of (`start_y`, `end_y`) tuples representing each page's content bounds.
///
/// Shim over [`page_breaks::compute_page_breaks_from_display_list`] +
/// [`page_breaks::page_spans`], kept so the pre-extraction tests keep pinning
/// the observable spans through the same signature.
#[cfg(test)]
8
fn calculate_page_break_positions(
8
    display_list: &DisplayList,
8
    first_page_height: f32,
8
    normal_page_height: f32,
8
) -> Vec<(f32, f32)> {
8
    let total_height = calculate_display_list_height(display_list);
8
    if total_height <= 0.0 || first_page_height <= 0.0 {
3
        return vec![(0.0, total_height.max(first_page_height))];
5
    }
5
    let constraints = page_breaks::PageConstraints {
5
        first_page_content_height: first_page_height,
5
        normal_page_content_height: normal_page_height,
5
    };
5
    let breaks = page_breaks::compute_page_breaks_from_display_list(display_list, &constraints);
5
    let mut spans = page_breaks::page_spans(&breaks, total_height);
5
    if spans.is_empty() {
        spans.push((0.0, total_height.max(first_page_height)));
5
    }
5
    spans
8
}
/// Text alignment for generated header/footer text.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TextAlignment {
    Left,
    Center,
    Right,
}
/// Helper to offset all Y coordinates of a display item.
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
41
pub(crate) fn offset_display_item_y(item: &DisplayListItem, y_offset: f32) -> DisplayListItem {
41
    if y_offset == 0.0 {
6
        return item.clone();
35
    }
35
    match item {
        DisplayListItem::Rect {
19
            bounds,
19
            color,
19
            border_radius,
19
        } => DisplayListItem::Rect {
19
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
19
            color: *color,
19
            border_radius: *border_radius,
19
        },
        DisplayListItem::Border {
15
            bounds,
15
            widths,
15
            colors,
15
            styles,
15
            border_radius,
15
        } => DisplayListItem::Border {
15
            bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
15
            widths: *widths,
15
            colors: *colors,
15
            styles: *styles,
15
            border_radius: *border_radius,
15
        },
        DisplayListItem::Text {
1
            glyphs,
1
            font_hash,
1
            font_size_px,
1
            color,
1
            clip_rect,
            ..
        } => {
1
            let offset_glyphs: Vec<GlyphInstance> = glyphs
1
                .iter()
1
                .map(|g| GlyphInstance {
2
                    index: g.index,
2
                    point: LogicalPosition {
2
                        x: g.point.x,
2
                        y: g.point.y + y_offset,
2
                    },
2
                    size: g.size,
2
                })
1
                .collect();
1
            DisplayListItem::Text {
1
                glyphs: offset_glyphs,
1
                font_hash: *font_hash,
1
                font_size_px: *font_size_px,
1
                color: *color,
1
                clip_rect: offset_rect_y(clip_rect.into_inner(), y_offset).into(),
1
                source_node_index: None,
1
            }
        }
        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,
            content_offset,
        } => 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(),
            // Paginating shifts the box down the page; the content's position
            // WITHIN its window is unrelated and must not move.
            content_offset: *content_offset,
        },
        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, opacity_key } => {
            DisplayListItem::PushOpacity {
                bounds: offset_rect_y(bounds.into_inner(), y_offset).into(),
                opacity: *opacity,
                opacity_key: *opacity_key,
            }
        }
        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,
        },
        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,
    }
41
}
/// Generate display list items for simple text (paginated headers/footers).
///
/// Shapes `text` against a registered font (chosen from `renderer_resources`)
/// and emits a single [`DisplayListItem::Text`] whose glyph indices are the
/// font's real GIDs and whose `font_hash` is the font's registered hash, so the
/// renderer (`cpurender::render_text`) can resolve and paint it.
///
/// This is a deliberately simple shaper for short, single-line running
/// headers/footers (e.g. "Page 1 of 3"): per-character cmap lookup + horizontal
/// advance, no complex shaping / kerning / bidi. The full text pipeline is not
/// used because the pagination call site does not carry a styled run — only the
/// header/footer string and a font size/color.
///
/// History: a previous stub fabricated glyphs whose `index` was the Unicode
/// *codepoint* and whose `font_hash` was `0`, which matched no registered font
/// (the renderer logged "Font hash 0 not found" and painted nothing). A later
/// revision returned an empty list. Both rendered no header/footer text; this
/// emits real glyphs.
///
/// Returns an empty list only when `text` is empty, no font is registered, or
/// the chosen font has degenerate metrics (`units_per_em == 0`).
22
fn generate_text_display_items(
22
    text: &str,
22
    bounds: LogicalRect,
22
    font_size: f32,
22
    color: ColorU,
22
    alignment: TextAlignment,
22
    renderer_resources: &RendererResources,
22
) -> Vec<DisplayListItem> {
22
    if text.is_empty() || font_size <= 0.0 {
        return Vec::new();
22
    }
    // Pick the first registered font. Running headers/footers do not carry a
    // styled run, so there is no per-node font family to resolve; the document's
    // registered font is a reasonable choice for the page furniture.
1
    let Some((_font_key, (font_ref, _instances))) =
22
        renderer_resources.currently_registered_fonts.iter().next()
    else {
21
        return Vec::new();
    };
1
    let parsed = crate::font_ref_to_parsed_font(font_ref);
1
    let units_per_em = f32::from(parsed.font_metrics.units_per_em);
1
    if units_per_em <= 0.0 {
        return Vec::new();
1
    }
1
    let scale = font_size / units_per_em;
1
    let font_hash = parsed.hash;
    // First pass: shape (cmap lookup + advance) and accumulate total width.
1
    let mut shaped: Vec<(u16, f32)> = Vec::new(); // (glyph_id, advance_px)
1
    let mut total_width = 0.0f32;
11
    for c in text.chars() {
11
        let gid = parsed.lookup_glyph_index(c as u32).unwrap_or(0);
11
        let advance = f32::from(parsed.get_horizontal_advance(gid)) * scale;
11
        shaped.push((gid, advance));
11
        total_width += advance;
11
    }
1
    if shaped.is_empty() {
        return Vec::new();
1
    }
    // Horizontal placement within the box.
1
    let start_x = match alignment {
1
        TextAlignment::Center => (bounds.size.width - total_width).mul_add(0.5, bounds.origin.x),
        TextAlignment::Right => bounds.origin.x + (bounds.size.width - total_width),
        TextAlignment::Left => bounds.origin.x,
    };
    // Vertical placement: center the text's em-box in the header/footer band and
    // place the baseline accordingly (point.y is the glyph baseline).
1
    let ascent_px = parsed.font_metrics.ascent * scale;
1
    let descent_px = parsed.font_metrics.descent * scale; // hhea descender, usually negative
1
    let text_height = ascent_px - descent_px;
1
    let baseline_y =
1
        bounds.origin.y + (bounds.size.height - text_height).mul_add(0.5, ascent_px);
1
    let mut pen_x = start_x;
1
    let mut glyphs: Vec<GlyphInstance> = Vec::with_capacity(shaped.len());
12
    for (gid, advance) in shaped {
11
        let size = parsed
11
            .get_glyph_size(gid, font_size)
11
            .unwrap_or(LogicalSize {
11
                width: advance,
11
                height: font_size,
11
            });
11
        glyphs.push(GlyphInstance {
11
            index: u32::from(gid),
11
            point: LogicalPosition {
11
                x: pen_x,
11
                y: baseline_y,
11
            },
11
            size,
11
        });
11
        pen_x += advance;
11
    }
1
    vec![DisplayListItem::Text {
1
        glyphs,
1
        font_hash: FontHash::from_hash(font_hash),
1
        font_size_px: font_size,
1
        color,
1
        clip_rect: bounds.into(),
1
        source_node_index: None,
1
    }]
22
}
/// Calculate the total height of a display list (max Y + height of all items).
3995
pub(crate) fn calculate_display_list_height(display_list: &DisplayList) -> f32 {
3995
    let mut max_bottom = 0.0f32;
160231
    for item in &display_list.items {
156236
        if let Some(bounds) = get_display_item_bounds(item) {
            // Skip items with zero height - they don't contribute to visible content
152073
            if bounds.0.size.height < 0.1 {
3133
                continue;
148940
            }
148940
            let item_bottom = bounds.0.origin.y + bounds.0.size.height;
148940
            if item_bottom > max_bottom {
4746
                max_bottom = item_bottom;
144194
            }
4163
        }
    }
3995
    max_bottom
3995
}
/// Break property information for pagination decisions.
#[derive(Debug, Clone, Copy, Default)]
// fields mirror the CSS break-before / break-after / break-inside properties
#[allow(clippy::struct_field_names)]
struct BreakProperties {
    break_before: PageBreak,
    break_after: PageBreak,
    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
5
pub(crate) fn apply_text_overflow_ellipsis(
5
    display_list: &mut DisplayList,
5
    container_bounds: LogicalRect,
5
    _ellipsis: &str,
5
) {
5
    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.
12
    for item in &mut display_list.items {
        if let DisplayListItem::Text {
5
            glyphs,
5
            font_size_px,
5
            clip_rect,
            ..
7
        } = item {
5
                if glyphs.is_empty() {
1
                    continue;
4
                }
                // Check if any glyph extends past the container right edge
4
                let last_glyph = &glyphs[glyphs.len() - 1];
4
                let last_glyph_right = last_glyph.point.x + last_glyph.size.width;
4
                if last_glyph_right <= container_right {
1
                    continue; // No overflow, nothing to do
3
                }
                // Estimate ellipsis width
3
                let ellipsis_width = *font_size_px * APPROX_ELLIPSIS_WIDTH_RATIO;
3
                let truncation_edge = container_right - ellipsis_width;
                // Find the last glyph that fits before the truncation edge
3
                let mut keep_count = 0;
8
                for (i, glyph) in glyphs.iter().enumerate() {
8
                    let glyph_right = glyph.point.x + glyph.size.width;
8
                    if glyph_right > truncation_edge {
2
                        break;
6
                    }
6
                    keep_count = i + 1;
                }
                // Truncate the glyphs
3
                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.
3
                let ellipsis_x = glyphs.last().map_or(container_bounds.origin.x, |last| last.point.x + last.size.width);
3
                let ellipsis_glyph = GlyphInstance {
                    index: 0x2026, // U+2026 HORIZONTAL ELLIPSIS
3
                    point: LogicalPosition::new(ellipsis_x, glyphs.first().map_or(
3
                        container_bounds.origin.y,
                        |g| g.point.y,
                    )),
3
                    size: LogicalSize::new(ellipsis_width, *font_size_px),
                };
3
                glyphs.push(ellipsis_glyph);
                // Update the clip rect to match the container bounds so
                // the ellipsis is visible but nothing past it is shown
3
                *clip_rect = container_bounds.into();
2
            }
    }
5
}
// ============================================================================
// 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.
#[allow(clippy::many_single_char_names)] // domain-standard coordinate/geometry/short-lived names
14
pub(crate) fn resolve_clip_path(
14
    clip_path: &azul_css::props::layout::shape::ClipPath,
14
    node_bounds: LogicalRect,
14
) -> Option<(LogicalRect, f32)> {
    use azul_css::props::layout::shape::ClipPath;
    use azul_css::shape::CssShape;
14
    match clip_path {
2
        ClipPath::None => None,
12
        ClipPath::Shape(shape) => {
12
            match shape {
2
                CssShape::Inset(inset) => {
                    // CSS inset() creates a rectangular clip inset from each edge.
                    // inset(top right bottom left round border-radius)
2
                    let x = node_bounds.origin.x + inset.inset_left;
2
                    let y = node_bounds.origin.y + inset.inset_top;
2
                    let w = (node_bounds.size.width - inset.inset_left - inset.inset_right).max(0.0);
2
                    let h = (node_bounds.size.height - inset.inset_top - inset.inset_bottom).max(0.0);
2
                    let radius = match inset.border_radius {
1
                        azul_css::corety::OptionF32::Some(r) => r,
1
                        azul_css::corety::OptionF32::None => 0.0,
                    };
2
                    Some((LogicalRect {
2
                        origin: LogicalPosition::new(x, y),
2
                        size: LogicalSize::new(w, h),
2
                    }, radius))
                }
5
                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).
5
                    let cx = node_bounds.origin.x + circle.center.x;
5
                    let cy = node_bounds.origin.y + circle.center.y;
5
                    let r = circle.radius;
5
                    Some((LogicalRect {
5
                        origin: LogicalPosition::new(cx - r, cy - r),
5
                        size: LogicalSize::new(r * 2.0, r * 2.0),
5
                    }, r))
                }
1
                CssShape::Ellipse(ellipse) => {
                    // Approximate ellipse as its bounding box.
1
                    let cx = node_bounds.origin.x + ellipse.center.x;
1
                    let cy = node_bounds.origin.y + ellipse.center.y;
1
                    let rx = ellipse.radius_x;
1
                    let ry = ellipse.radius_y;
1
                    let radius = rx.min(ry);
1
                    Some((LogicalRect {
1
                        origin: LogicalPosition::new(cx - rx, cy - ry),
1
                        size: LogicalSize::new(rx * 2.0, ry * 2.0),
1
                    }, radius))
                }
3
                CssShape::Polygon(polygon) => {
                    // Compute the axis-aligned bounding box of the polygon.
3
                    if polygon.points.is_empty() {
1
                        return None;
2
                    }
2
                    let mut min_x = f32::INFINITY;
2
                    let mut min_y = f32::INFINITY;
2
                    let mut max_x = f32::NEG_INFINITY;
2
                    let mut max_y = f32::NEG_INFINITY;
6
                    for point in &polygon.points {
4
                        // Polygon points are in absolute coordinates (pre-resolved)
4
                        let px = node_bounds.origin.x + point.x;
4
                        let py = node_bounds.origin.y + point.y;
4
                        min_x = min_x.min(px);
4
                        min_y = min_y.min(py);
4
                        max_x = max_x.max(px);
4
                        max_y = max_y.max(py);
4
                    }
2
                    Some((LogicalRect {
2
                        origin: LogicalPosition::new(min_x, min_y),
2
                        size: LogicalSize::new((max_x - min_x).max(0.0), (max_y - min_y).max(0.0)),
2
                    }, 0.0))
                }
                CssShape::Path(_) => {
                    // SVG paths are not supported for clip-path yet.
                    // Return the full node bounds (no clipping).
1
                    None
                }
            }
        }
    }
14
}
/// 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)
6
pub(crate) fn apply_clip_path(
6
    display_list: &mut DisplayList,
6
    start_index: usize,
6
    clip_rect: LogicalRect,
6
    border_radius: f32,
6
) {
6
    let br = if border_radius > 0.0 {
1
        BorderRadius {
1
            top_left: border_radius,
1
            top_right: border_radius,
1
            bottom_left: border_radius,
1
            bottom_right: border_radius,
1
        }
    } else {
5
        BorderRadius::default()
    };
    // Insert PushClip at start_index
6
    display_list.items.insert(start_index, DisplayListItem::PushClip {
6
        bounds: clip_rect.into(),
6
        border_radius: br,
6
    });
    // Insert a corresponding None in node_mapping
6
    if display_list.node_mapping.len() >= start_index {
5
        display_list.node_mapping.insert(start_index, None);
5
    }
    // Append PopClip at the end
6
    display_list.items.push(DisplayListItem::PopClip);
6
    display_list.node_mapping.push(None);
6
}
/// 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")]
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] // bounded graphics/coord/font/fixed-point/debug-marker cast
45
fn rasterize_svg_clip_to_r8(
45
    svg_clip: &azul_core::svg::SvgMultiPolygon,
45
    paint_rect: &LogicalRect,
45
) -> Option<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};
45
    let w = paint_rect.size.width.ceil() as u32;
45
    let h = paint_rect.size.height.ceil() as u32;
45
    if w == 0 || h == 0 {
        return None;
45
    }
    // Build agg PathStorage from SvgMultiPolygon
45
    let mut path = PathStorage::new();
45
    for ring in svg_clip.rings.as_ref() {
45
        let mut first = true;
270
        for item in ring.items.as_ref() {
270
            match item {
198
                azul_core::svg::SvgPathElement::Line(l) => {
198
                    if first {
36
                        path.move_to(
36
                            f64::from(l.start.x - paint_rect.origin.x),
36
                            f64::from(l.start.y - paint_rect.origin.y),
36
                        );
36
                        first = false;
162
                    }
198
                    path.line_to(
198
                        f64::from(l.end.x - paint_rect.origin.x),
198
                        f64::from(l.end.y - paint_rect.origin.y),
                    );
                }
                azul_core::svg::SvgPathElement::QuadraticCurve(q) => {
                    if first {
                        path.move_to(
                            f64::from(q.start.x - paint_rect.origin.x),
                            f64::from(q.start.y - paint_rect.origin.y),
                        );
                        first = false;
                    }
                    path.curve3(
                        f64::from(q.ctrl.x - paint_rect.origin.x),
                        f64::from(q.ctrl.y - paint_rect.origin.y),
                        f64::from(q.end.x - paint_rect.origin.x),
                        f64::from(q.end.y - paint_rect.origin.y),
                    );
                }
72
                azul_core::svg::SvgPathElement::CubicCurve(c) => {
72
                    if first {
9
                        path.move_to(
9
                            f64::from(c.start.x - paint_rect.origin.x),
9
                            f64::from(c.start.y - paint_rect.origin.y),
9
                        );
9
                        first = false;
63
                    }
72
                    path.curve4(
72
                        f64::from(c.ctrl_1.x - paint_rect.origin.x),
72
                        f64::from(c.ctrl_1.y - paint_rect.origin.y),
72
                        f64::from(c.ctrl_2.x - paint_rect.origin.x),
72
                        f64::from(c.ctrl_2.y - paint_rect.origin.y),
72
                        f64::from(c.end.x - paint_rect.origin.x),
72
                        f64::from(c.end.y - paint_rect.origin.y),
                    );
                }
            }
        }
    }
    // Rasterize to RGBA32 buffer
45
    let mut rgba_buf = vec![0u8; (w * h * 4) as usize];
45
    {
45
        let stride = (w * 4) as i32;
45
        let mut ra = unsafe {
45
            RowAccessor::new_with_buf(rgba_buf.as_mut_ptr(), w, h, stride)
45
        };
45
        let pf = PixfmtRgba32::new(&mut ra);
45
        let mut rb = RendererBase::new(pf);
45

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

            
45
        let mut sl = ScanlineU8::new();
45
        let white = Rgba8 { r: 255, g: 255, b: 255, a: 255 };
45
        render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, &white);
45
    }
    // Extract alpha channel as R8 mask
540000
    let r8_data: Vec<u8> = rgba_buf.chunks_exact(4).map(|px| px[3]).collect();
45
    ImageRef::new_rawimage(RawImage {
45
        pixels: RawImageData::U8(r8_data.into()),
45
        width: w as usize,
45
        height: h as usize,
45
        premultiplied_alpha: false,
45
        data_format: RawImageFormat::R8,
45
        tag: Vec::new().into(),
45
    })
45
}
#[cfg(test)]
mod pagination_text_tests {
    use super::*;
    use crate::font::parsed::ParsedFont;
    use azul_core::resources::{FontKey, IdNamespace};
    /// Loads a system font for testing, retaining source bytes so advances work.
1
    fn load_test_font() -> Option<ParsedFont> {
1
        let candidates = [
1
            "/System/Library/Fonts/Supplemental/Times New Roman.ttf",
1
            "/System/Library/Fonts/Helvetica.ttc",
1
            "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
1
            "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
1
            "C:/Windows/Fonts/arial.ttf",
1
        ];
3
        for path in candidates {
3
            if let Ok(bytes) = std::fs::read(path) {
1
                let arc = Arc::new(rust_fontconfig::FontBytes::Owned(
1
                    Arc::from(bytes.as_slice()),
1
                ));
1
                if let Some(font) =
1
                    ParsedFont::from_bytes(&bytes, 0, &mut Vec::new()).map(|f| f.with_source_bytes(arc))
                {
1
                    return Some(font);
                }
2
            }
        }
        None
1
    }
1
    fn renderer_resources_with(font: ParsedFont) -> RendererResources {
1
        let mut rr = RendererResources::default();
1
        let font_ref = crate::parsed_font_to_font_ref(font);
1
        let key = FontKey::unique(IdNamespace(0));
1
        let hash = crate::font_ref_to_parsed_font(&font_ref).hash;
1
        rr.font_hash_map.insert(hash, key);
1
        rr.currently_registered_fonts
1
            .insert(key, (font_ref, BTreeMap::default()));
1
        rr
1
    }
    /// The pagination header/footer text path must emit real glyph display items
    /// (the audit flagged it as a no-op rendering nothing).
    #[test]
1
    fn generate_text_display_items_emits_glyphs() {
1
        let Some(font) = load_test_font() else {
            eprintln!("[skip] no system font available");
            return;
        };
1
        let expected_hash = font.hash;
1
        let rr = renderer_resources_with(font);
1
        let bounds = LogicalRect {
1
            origin: LogicalPosition { x: 0.0, y: 0.0 },
1
            size: LogicalSize { width: 400.0, height: 30.0 },
1
        };
1
        let items = generate_text_display_items(
1
            "Page 1 of 3",
1
            bounds,
            14.0,
1
            ColorU { r: 0, g: 0, b: 0, a: 255 },
1
            TextAlignment::Center,
1
            &rr,
        );
1
        assert_eq!(items.len(), 1, "expected exactly one Text item");
1
        match &items[0] {
1
            DisplayListItem::Text { glyphs, font_hash, color, .. } => {
1
                assert_eq!(glyphs.len(), "Page 1 of 3".chars().count());
1
                assert_eq!(font_hash.font_hash, expected_hash, "must use registered font hash");
1
                assert_ne!(font_hash.font_hash, 0, "hash 0 resolves no font");
1
                assert_eq!(color.a, 255);
                // Glyph IDs must be real (cmap-resolved), not raw codepoints.
1
                let p_gid = glyphs[0].index;
1
                assert_ne!(p_gid, 'P' as u32, "glyph index must be a GID, not a codepoint");
                // Pen must advance: x coordinates strictly increase across the run.
1
                assert!(glyphs[1].point.x > glyphs[0].point.x, "pen did not advance");
            }
            other => panic!("expected DisplayListItem::Text, got {other:?}"),
        }
1
    }
    /// With no registered fonts there is nothing to shape against -> empty.
    #[test]
1
    fn generate_text_display_items_empty_without_font() {
1
        let rr = RendererResources::default();
1
        let bounds = LogicalRect {
1
            origin: LogicalPosition { x: 0.0, y: 0.0 },
1
            size: LogicalSize { width: 400.0, height: 30.0 },
1
        };
1
        let items = generate_text_display_items(
1
            "Header",
1
            bounds,
            14.0,
1
            ColorU { r: 0, g: 0, b: 0, a: 255 },
1
            TextAlignment::Center,
1
            &rr,
        );
1
        assert!(items.is_empty());
1
    }
}
#[cfg(test)]
#[allow(clippy::float_cmp, clippy::too_many_lines)]
mod autotest_generated {
    use super::*;
    // ---------------------------------------------------------------------
    // Construction helpers
    // ---------------------------------------------------------------------
    fn rect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
        LogicalRect::new(LogicalPosition::new(x, y), LogicalSize::new(w, h))
    }
    fn opaque() -> ColorU {
        ColorU { r: 10, g: 20, b: 30, a: 255 }
    }
    fn glyph(index: u32, x: f32, y: f32) -> GlyphInstance {
        GlyphInstance {
            index,
            point: LogicalPosition::new(x, y),
            size: LogicalSize::new(8.0, 12.0),
        }
    }
    fn no_widths() -> StyleBorderWidths {
        StyleBorderWidths { top: None, right: None, bottom: None, left: None }
    }
    fn all_widths() -> StyleBorderWidths {
        StyleBorderWidths {
            top: Some(CssPropertyValue::Exact(LayoutBorderTopWidth::default())),
            right: Some(CssPropertyValue::Exact(LayoutBorderRightWidth::default())),
            bottom: Some(CssPropertyValue::Exact(LayoutBorderBottomWidth::default())),
            left: Some(CssPropertyValue::Exact(LayoutBorderLeftWidth::default())),
        }
    }
    fn no_colors() -> StyleBorderColors {
        StyleBorderColors { top: None, right: None, bottom: None, left: None }
    }
    fn no_styles() -> StyleBorderStyles {
        StyleBorderStyles { top: None, right: None, bottom: None, left: None }
    }
    fn all_styles() -> StyleBorderStyles {
        StyleBorderStyles {
            top: Some(CssPropertyValue::Exact(StyleBorderTopStyle::default())),
            right: Some(CssPropertyValue::Exact(StyleBorderRightStyle::default())),
            bottom: Some(CssPropertyValue::Exact(StyleBorderBottomStyle::default())),
            left: Some(CssPropertyValue::Exact(StyleBorderLeftStyle::default())),
        }
    }
    fn zero_style_radius() -> StyleBorderRadius {
        StyleBorderRadius {
            top_left: PixelValue::zero(),
            top_right: PixelValue::zero(),
            bottom_left: PixelValue::zero(),
            bottom_right: PixelValue::zero(),
        }
    }
    fn test_image() -> ImageRef {
        ImageRef::null_image(4, 4, azul_core::resources::RawImageFormat::RGBA8, Vec::new())
    }
    fn text_item(src: Option<usize>, clip: LogicalRect, glyphs: Vec<GlyphInstance>) -> DisplayListItem {
        DisplayListItem::Text {
            glyphs,
            font_hash: FontHash::from_hash(7),
            font_size_px: 16.0,
            color: opaque(),
            clip_rect: clip.into(),
            source_node_index: src,
        }
    }
    fn list_of(items: Vec<DisplayListItem>) -> DisplayList {
        let node_mapping = vec![None; items.len()];
        DisplayList { items, node_mapping, ..DisplayList::default() }
    }
    #[cfg(feature = "text_layout")]
    fn positioned(line_index: usize, x: f32, y: f32, w: f32, h: f32) -> PositionedItem {
        PositionedItem {
            item: ShapedItem::Tab {
                source: azul_core::selection::ContentIndex { run_index: 0, item_index: 0 },
                bounds: text3::cache::Rect { x: 0.0, y: 0.0, width: w, height: h },
            },
            position: text3::cache::Point { x, y },
            line_index,
        }
    }
    // ---------------------------------------------------------------------
    // WindowLogicalRect / BorderBoxRect / ContentBoxRect
    // ---------------------------------------------------------------------
    #[test]
    fn window_logical_rect_accessors_roundtrip() {
        let origin = LogicalPosition::new(-3.5, 12.25);
        let size = LogicalSize::new(100.0, 40.0);
        let w = WindowLogicalRect::new(origin, size);
        assert_eq!(w.origin(), origin);
        assert_eq!(w.size(), size);
        assert_eq!(*w.inner(), LogicalRect::new(origin, size));
        assert_eq!(w.into_inner(), LogicalRect::new(origin, size));
        // From/Into must be the identity on the wrapped rect.
        assert_eq!(WindowLogicalRect::from(w.into_inner()), w);
        assert_eq!(LogicalRect::from(w), w.into_inner());
    }
    #[test]
    fn window_logical_rect_zero_is_neutral() {
        let z = WindowLogicalRect::zero();
        assert_eq!(z, WindowLogicalRect::default());
        assert_eq!(z.origin(), LogicalPosition::zero());
        assert_eq!(z.size(), LogicalSize::zero());
        assert_eq!(z.into_inner(), LogicalRect::zero());
    }
    #[test]
    fn window_logical_rect_extreme_values_do_not_panic() {
        for (x, y, w, h) in [
            (f32::MAX, f32::MAX, f32::MAX, f32::MAX),
            (f32::MIN, f32::MIN, 0.0, 0.0),
            (f32::INFINITY, f32::NEG_INFINITY, f32::INFINITY, 0.0),
            (f32::NAN, f32::NAN, f32::NAN, f32::NAN),
        ] {
            let r = WindowLogicalRect::new(LogicalPosition::new(x, y), LogicalSize::new(w, h));
            // Accessors must round-trip the raw bits regardless of how odd they are.
            assert_eq!(r.origin().x.to_bits(), x.to_bits());
            assert_eq!(r.size().height.to_bits(), h.to_bits());
            let _ = format!("{r:?}");
        }
    }
    #[test]
    fn border_box_to_content_box_subtracts_padding_and_border() {
        let bb = BorderBoxRect(rect(10.0, 20.0, 100.0, 50.0));
        let padding = crate::solver3::geometry::EdgeSizes { top: 1.0, right: 2.0, bottom: 3.0, left: 4.0 };
        let border = crate::solver3::geometry::EdgeSizes { top: 5.0, right: 6.0, bottom: 7.0, left: 8.0 };
        let cb = bb.to_content_box(&padding, &border);
        assert_eq!(cb.rect(), rect(22.0, 26.0, 80.0, 34.0));
        assert_eq!(bb.rect(), rect(10.0, 20.0, 100.0, 50.0), "receiver copy is unchanged");
    }
    #[test]
    fn border_box_to_content_box_zero_edges_is_identity() {
        let bb = BorderBoxRect(rect(1.0, 2.0, 3.0, 4.0));
        let zero = crate::solver3::geometry::EdgeSizes::default();
        assert_eq!(bb.to_content_box(&zero, &zero).rect(), bb.rect());
    }
    #[test]
    fn border_box_to_content_box_overinset_yields_negative_size_not_a_panic() {
        // padding + border exceed the box: the result is a NEGATIVE content box.
        // Nothing clamps it, so downstream code must tolerate it. Pin that here so
        // a future clamp is a deliberate, visible change.
        let bb = BorderBoxRect(rect(0.0, 0.0, 10.0, 10.0));
        let big = crate::solver3::geometry::EdgeSizes { top: 50.0, right: 50.0, bottom: 50.0, left: 50.0 };
        let cb = bb.to_content_box(&big, &big);
        assert!(cb.rect().size.width < 0.0);
        assert!(cb.rect().size.height < 0.0);
    }
    #[test]
    fn border_box_to_content_box_nan_and_inf_do_not_panic() {
        let bb = BorderBoxRect(rect(0.0, 0.0, f32::MAX, f32::MAX));
        let nan = crate::solver3::geometry::EdgeSizes {
            top: f32::NAN, right: f32::NAN, bottom: f32::NAN, left: f32::NAN,
        };
        let inf = crate::solver3::geometry::EdgeSizes {
            top: f32::INFINITY, right: f32::INFINITY, bottom: f32::INFINITY, left: f32::INFINITY,
        };
        assert!(bb.to_content_box(&nan, &nan).rect().size.width.is_nan());
        // MAX - inf - inf ... = -inf (defined, not a trap)
        assert!(bb.to_content_box(&inf, &inf).rect().size.width.is_infinite());
    }
    #[test]
    fn content_box_rect_getter_returns_wrapped_rect() {
        let r = rect(-1.0, -2.0, 0.0, 0.0);
        assert_eq!(ContentBoxRect(r).rect(), r);
        assert_eq!(ContentBoxRect(LogicalRect::zero()).rect(), LogicalRect::zero());
    }
    // ---------------------------------------------------------------------
    // BorderRadius::is_zero
    // ---------------------------------------------------------------------
    #[test]
    fn border_radius_is_zero_basic() {
        assert!(BorderRadius::default().is_zero());
        assert!(!BorderRadius { top_left: 1.0, ..BorderRadius::default() }.is_zero());
        assert!(!BorderRadius { bottom_right: 0.001, ..BorderRadius::default() }.is_zero());
    }
    #[test]
    fn border_radius_is_zero_edge_floats() {
        // -0.0 == 0.0 in IEEE-754, so a negative zero radius still counts as zero.
        assert!(BorderRadius { top_left: -0.0, top_right: -0.0, bottom_left: -0.0, bottom_right: -0.0 }.is_zero());
        // NaN != 0.0, so a NaN radius is (conservatively) *not* zero — no panic.
        assert!(!BorderRadius { top_left: f32::NAN, ..BorderRadius::default() }.is_zero());
        assert!(!BorderRadius { top_right: f32::INFINITY, ..BorderRadius::default() }.is_zero());
        // A negative radius is not zero either.
        assert!(!BorderRadius { bottom_left: -5.0, ..BorderRadius::default() }.is_zero());
    }
    // ---------------------------------------------------------------------
    // DisplayListItem predicates / getters
    // ---------------------------------------------------------------------
    #[test]
    fn is_state_management_true_for_push_pop_only() {
        let state = [
            DisplayListItem::PushClip { bounds: rect(0.0, 0.0, 1.0, 1.0).into(), border_radius: BorderRadius::default() },
            DisplayListItem::PopClip,
            DisplayListItem::PopImageMaskClip,
            DisplayListItem::PopScrollFrame,
            DisplayListItem::PopStackingContext,
            DisplayListItem::PopReferenceFrame,
            DisplayListItem::PopFilter,
            DisplayListItem::PopBackdropFilter,
            DisplayListItem::PopOpacity,
            DisplayListItem::PopTextShadow,
            DisplayListItem::PushStackingContext { z_index: 0, bounds: WindowLogicalRect::zero() },
            DisplayListItem::PushOpacity { bounds: WindowLogicalRect::zero(), opacity: 0.5, opacity_key: None },
            DisplayListItem::PushTextShadow { shadow: StyleBoxShadow::default() },
        ];
        for item in &state {
            assert!(item.is_state_management(), "{item:?} must be state management");
        }
        let drawing = [
            DisplayListItem::Rect { bounds: WindowLogicalRect::zero(), color: opaque(), border_radius: BorderRadius::default() },
            DisplayListItem::CursorRect { bounds: WindowLogicalRect::zero(), color: opaque() },
            // HitTestArea paints nothing but is NOT a stack command — it must not be forced through.
            DisplayListItem::HitTestArea { bounds: WindowLogicalRect::zero(), tag: (0, TAG_TYPE_DOM_NODE) },
            text_item(None, LogicalRect::zero(), Vec::new()),
        ];
        for item in &drawing {
            assert!(!item.is_state_management(), "{item:?} must NOT be state management");
        }
    }
    #[test]
    fn bounds_reports_none_only_for_pop_and_text_shadow() {
        let r = rect(1.0, 2.0, 3.0, 4.0);
        assert_eq!(
            DisplayListItem::Rect { bounds: r.into(), color: opaque(), border_radius: BorderRadius::default() }.bounds(),
            Some(r)
        );
        // Text reports its CLIP rect as its bounds, not a glyph hull.
        assert_eq!(text_item(Some(0), r, vec![glyph(1, 999.0, 999.0)]).bounds(), Some(r));
        assert_eq!(
            DisplayListItem::PushScrollFrame {
                clip_bounds: r.into(),
                content_size: LogicalSize::new(9.0, 9.0),
                scroll_id: 3,
            }.bounds(),
            Some(r)
        );
        assert_eq!(DisplayListItem::PopClip.bounds(), None);
        assert_eq!(DisplayListItem::PopOpacity.bounds(), None);
        assert_eq!(
            DisplayListItem::PushTextShadow { shadow: StyleBoxShadow::default() }.bounds(),
            None,
            "a text shadow has no bounds of its own"
        );
    }
    #[test]
    fn bounds_on_degenerate_rects_does_not_panic() {
        for r in [
            LogicalRect::zero(),
            rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN),
            rect(f32::MIN, f32::MIN, f32::MAX, f32::MAX),
            rect(0.0, 0.0, -10.0, -10.0),
        ] {
            let item = DisplayListItem::Rect { bounds: r.into(), color: opaque(), border_radius: BorderRadius::default() };
            assert!(item.bounds().is_some());
            assert!(item.visual_bounds().is_some());
        }
    }
    #[test]
    fn visual_bounds_matches_bounds_for_non_shadow_items() {
        let r = rect(5.0, 6.0, 7.0, 8.0);
        let item = DisplayListItem::Rect { bounds: r.into(), color: opaque(), border_radius: BorderRadius::default() };
        assert_eq!(item.visual_bounds(), item.bounds());
        assert_eq!(DisplayListItem::PopClip.visual_bounds(), None);
    }
    #[test]
    fn visual_bounds_expands_box_shadow_by_offset_blur_and_spread() {
        use azul_css::props::basic::pixel::PixelValueNoPercent;
        let shadow = StyleBoxShadow {
            offset_x: PixelValueNoPercent { inner: PixelValue::const_px(2) },
            offset_y: PixelValueNoPercent { inner: PixelValue::const_px(3) },
            blur_radius: PixelValueNoPercent { inner: PixelValue::const_px(4) },
            spread_radius: PixelValueNoPercent { inner: PixelValue::const_px(5) },
            clip_mode: BoxShadowClipMode::default(),
            color: ColorU::BLACK,
        };
        let item = DisplayListItem::BoxShadow {
            bounds: rect(100.0, 100.0, 50.0, 50.0).into(),
            shadow,
            border_radius: BorderRadius::default(),
        };
        // expand = |2| + |3| + |4| + |5| = 14, applied on every side.
        let vb = item.visual_bounds().expect("box shadow has visual bounds");
        assert_eq!(vb, rect(86.0, 86.0, 78.0, 78.0));
        // The visual bounds must strictly contain the paint bounds.
        let b = item.bounds().unwrap();
        assert!(vb.origin.x < b.origin.x && vb.size.width > b.size.width);
    }
    #[test]
    fn visual_bounds_box_shadow_with_negative_offsets_uses_absolute_values() {
        use azul_css::props::basic::pixel::PixelValueNoPercent;
        let shadow = StyleBoxShadow {
            offset_x: PixelValueNoPercent { inner: PixelValue::const_px(-10) },
            offset_y: PixelValueNoPercent { inner: PixelValue::const_px(-10) },
            blur_radius: PixelValueNoPercent { inner: PixelValue::const_px(0) },
            spread_radius: PixelValueNoPercent { inner: PixelValue::const_px(0) },
            clip_mode: BoxShadowClipMode::default(),
            color: ColorU::BLACK,
        };
        let item = DisplayListItem::BoxShadow {
            bounds: rect(0.0, 0.0, 10.0, 10.0).into(),
            shadow,
            border_radius: BorderRadius::default(),
        };
        // .abs() is applied, so the shadow expands symmetrically by 20 in each direction.
        assert_eq!(item.visual_bounds().unwrap(), rect(-20.0, -20.0, 50.0, 50.0));
    }
    // ---------------------------------------------------------------------
    // DisplayListItem::is_visually_equal
    // ---------------------------------------------------------------------
    #[test]
    fn is_visually_equal_reflexive_and_discriminant_guarded() {
        let a = DisplayListItem::Rect {
            bounds: rect(0.0, 0.0, 10.0, 10.0).into(),
            color: opaque(),
            border_radius: BorderRadius::default(),
        };
        assert!(a.is_visually_equal(&a));
        assert!(!a.is_visually_equal(&DisplayListItem::PopClip), "different variants are never equal");
        assert!(!DisplayListItem::PopClip.is_visually_equal(&a));
    }
    #[test]
    fn is_visually_equal_detects_field_changes() {
        let base = DisplayListItem::Rect {
            bounds: rect(0.0, 0.0, 10.0, 10.0).into(),
            color: opaque(),
            border_radius: BorderRadius::default(),
        };
        let moved = DisplayListItem::Rect {
            bounds: rect(1.0, 0.0, 10.0, 10.0).into(),
            color: opaque(),
            border_radius: BorderRadius::default(),
        };
        let recolored = DisplayListItem::Rect {
            bounds: rect(0.0, 0.0, 10.0, 10.0).into(),
            color: ColorU { r: 255, g: 0, b: 0, a: 255 },
            border_radius: BorderRadius::default(),
        };
        let rounded = DisplayListItem::Rect {
            bounds: rect(0.0, 0.0, 10.0, 10.0).into(),
            color: opaque(),
            border_radius: BorderRadius { top_left: 4.0, ..BorderRadius::default() },
        };
        assert!(!base.is_visually_equal(&moved));
        assert!(!base.is_visually_equal(&recolored));
        assert!(!base.is_visually_equal(&rounded));
    }
    #[test]
    fn is_visually_equal_pops_are_always_equal() {
        for (a, b) in [
            (DisplayListItem::PopClip, DisplayListItem::PopClip),
            (DisplayListItem::PopScrollFrame, DisplayListItem::PopScrollFrame),
            (DisplayListItem::PopOpacity, DisplayListItem::PopOpacity),
            (DisplayListItem::PopTextShadow, DisplayListItem::PopTextShadow),
        ] {
            assert!(a.is_visually_equal(&b));
        }
        assert!(!DisplayListItem::PopClip.is_visually_equal(&DisplayListItem::PopOpacity));
    }
    #[test]
    fn is_visually_equal_hit_test_areas_never_damage() {
        // Documented: hit-test areas paint no pixels, so ANY two are visually equal
        // (regression guard for issue #12 — a moved hit region must not force a repaint).
        let a = DisplayListItem::HitTestArea { bounds: rect(0.0, 0.0, 1.0, 1.0).into(), tag: (1, TAG_TYPE_DOM_NODE) };
        let b = DisplayListItem::HitTestArea { bounds: rect(500.0, 900.0, 7.0, 7.0).into(), tag: (99, TAG_TYPE_CURSOR) };
        assert!(a.is_visually_equal(&b));
    }
    #[test]
    fn is_visually_equal_text_layout_uses_arc_pointer_identity() {
        let shared: Arc<dyn std::any::Any + Send + Sync> = Arc::new(42u32);
        let other: Arc<dyn std::any::Any + Send + Sync> = Arc::new(42u32);
        let make = |layout: Arc<dyn std::any::Any + Send + Sync>| DisplayListItem::TextLayout {
            layout,
            bounds: rect(0.0, 0.0, 10.0, 10.0).into(),
            font_hash: FontHash::from_hash(1),
            font_size_px: 16.0,
            color: opaque(),
        };
        assert!(make(shared.clone()).is_visually_equal(&make(shared)), "same Arc => reuse => no damage");
        assert!(
            !make(Arc::new(42u32)).is_visually_equal(&make(other)),
            "distinct allocations => conservatively different"
        );
    }
    #[test]
    fn is_visually_equal_image_uses_pointer_identity_not_content() {
        let img = test_image();
        let a = DisplayListItem::Image {
            bounds: rect(0.0, 0.0, 4.0, 4.0).into(),
            image: img.clone(),
            border_radius: BorderRadius::default(),
        };
        let same_alloc = DisplayListItem::Image {
            bounds: rect(0.0, 0.0, 4.0, 4.0).into(),
            image: img,
            border_radius: BorderRadius::default(),
        };
        assert!(a.is_visually_equal(&same_alloc));
        // A byte-identical but separately allocated image is conservatively "different".
        let b = DisplayListItem::Image {
            bounds: rect(0.0, 0.0, 4.0, 4.0).into(),
            image: test_image(),
            border_radius: BorderRadius::default(),
        };
        assert!(!a.is_visually_equal(&b));
    }
    #[test]
    fn is_visually_equal_text_compares_glyph_ids_and_positions() {
        let clip = rect(0.0, 0.0, 100.0, 20.0);
        let a = text_item(Some(1), clip, vec![glyph(5, 0.0, 10.0), glyph(6, 8.0, 10.0)]);
        let same = text_item(Some(999), clip, vec![glyph(5, 0.0, 10.0), glyph(6, 8.0, 10.0)]);
        let diff_gid = text_item(Some(1), clip, vec![glyph(5, 0.0, 10.0), glyph(7, 8.0, 10.0)]);
        let diff_pos = text_item(Some(1), clip, vec![glyph(5, 0.0, 10.0), glyph(6, 9.0, 10.0)]);
        let shorter = text_item(Some(1), clip, vec![glyph(5, 0.0, 10.0)]);
        let empty = text_item(Some(1), clip, Vec::new());
        assert!(a.is_visually_equal(&same), "source_node_index is not a visual property");
        assert!(!a.is_visually_equal(&diff_gid));
        assert!(!a.is_visually_equal(&diff_pos));
        assert!(!a.is_visually_equal(&shorter), "glyph count mismatch => different");
        assert!(!a.is_visually_equal(&empty));
        assert!(empty.is_visually_equal(&empty), "empty glyph runs are equal to each other");
    }
    #[test]
    fn is_visually_equal_nan_thickness_is_conservatively_unequal() {
        // Raw f32 `==` on thickness: NaN != NaN, so even a self-comparison of a NaN
        // thickness reports "changed". That is the SAFE direction (forces a repaint),
        // and it must not panic.
        let a = DisplayListItem::Underline {
            bounds: rect(0.0, 0.0, 10.0, 1.0).into(),
            color: opaque(),
            thickness: f32::NAN,
        };
        assert!(!a.is_visually_equal(&a));
    }
    #[test]
    fn is_visually_equal_bounds_are_quantized_to_a_thousandth_of_a_pixel() {
        // LogicalRect equality is fixed-point (1/1000 px). Sub-quantum jitter is
        // deliberately treated as "no visual change" so float noise cannot damage.
        let a = DisplayListItem::Rect {
            bounds: rect(0.0, 0.0, 10.0, 10.0).into(),
            color: opaque(),
            border_radius: BorderRadius::default(),
        };
        let jittered = DisplayListItem::Rect {
            bounds: rect(0.000_01, 0.0, 10.0, 10.0).into(),
            color: opaque(),
            border_radius: BorderRadius::default(),
        };
        assert!(a.is_visually_equal(&jittered));
        // A whole-pixel move is above the quantum and *is* reported.
        let moved = DisplayListItem::Rect {
            bounds: rect(1.0, 0.0, 10.0, 10.0).into(),
            color: opaque(),
            border_radius: BorderRadius::default(),
        };
        assert!(!a.is_visually_equal(&moved));
    }
    // ---------------------------------------------------------------------
    // DisplayListBuilder
    // ---------------------------------------------------------------------
    #[test]
    fn builder_new_is_empty_and_matches_with_debug_false() {
        let b = DisplayListBuilder::new();
        assert!(b.items.is_empty());
        assert!(b.node_mapping.is_empty());
        assert!(!b.debug_enabled);
        assert!(b.forced_page_breaks.is_empty());
        assert!(b.fixed_position_item_ranges.is_empty());
        assert!(b.fixed_position_start.is_none());
        let dl = DisplayListBuilder::new().build();
        assert!(dl.items.is_empty());
        assert!(dl.node_mapping.is_empty());
    }
    #[test]
    fn builder_with_debug_toggles_message_collection() {
        let mut off = DisplayListBuilder::with_debug(false);
        off.debug_log("dropped".to_string());
        assert!(off.debug_messages.is_empty());
        let mut on = DisplayListBuilder::with_debug(true);
        on.debug_log(String::new());
        on.debug_log("x".repeat(100_000)); // huge message must not panic
        on.debug_log("🦀 unicode \u{0}\u{FFFD} nul".to_string());
        assert_eq!(on.debug_messages.len(), 3);
    }
    #[test]
    fn builder_push_item_keeps_node_mapping_in_lockstep() {
        let mut b = DisplayListBuilder::new();
        b.set_current_node(Some(NodeId::new(4)));
        b.push_rect(rect(0.0, 0.0, 1.0, 1.0), opaque(), BorderRadius::default());
        b.set_current_node(None);
        b.pop_clip();
        let dl = b.build();
        assert_eq!(dl.items.len(), 2);
        assert_eq!(dl.items.len(), dl.node_mapping.len(), "node_mapping must parallel items");
        assert_eq!(dl.node_mapping[0], Some(NodeId::new(4)));
        assert_eq!(dl.node_mapping[1], None);
    }
    #[test]
    fn builder_skips_fully_transparent_fills() {
        let mut b = DisplayListBuilder::new();
        b.push_rect(rect(0.0, 0.0, 10.0, 10.0), ColorU::TRANSPARENT, BorderRadius::default());
        b.push_selection_rect(rect(0.0, 0.0, 10.0, 10.0), ColorU::TRANSPARENT, BorderRadius::default());
        b.push_scrollbar(rect(0.0, 0.0, 10.0, 10.0), ColorU::TRANSPARENT, ScrollbarOrientation::Vertical, None, None);
        assert!(b.items.is_empty(), "alpha == 0 with no opacity key paints nothing");
        // An opacity key means the alpha is animated on the GPU — it must still be pushed.
        b.push_scrollbar(
            rect(0.0, 0.0, 10.0, 10.0),
            ColorU::TRANSPARENT,
            ScrollbarOrientation::Vertical,
            Some(OpacityKey::unique()),
            None,
        );
        assert_eq!(b.items.len(), 1);
    }
    #[test]
    fn builder_cursor_rect_is_emitted_even_when_invisible() {
        // Blink-off carets MUST still emit an item so the item count stays stable
        // across blink phases (otherwise damage falls back to a full-window repaint).
        let mut b = DisplayListBuilder::new();
        b.push_cursor_rect(rect(0.0, 0.0, 1.0, 16.0), ColorU::TRANSPARENT);
        assert_eq!(b.items.len(), 1);
        assert!(matches!(b.items[0], DisplayListItem::CursorRect { .. }));
    }
    #[test]
    fn builder_text_decorations_require_positive_thickness_and_alpha() {
        let bounds = rect(0.0, 0.0, 10.0, 2.0);
        for thickness in [0.0, -1.0, f32::NAN, f32::NEG_INFINITY] {
            let mut b = DisplayListBuilder::new();
            b.push_underline(bounds, opaque(), thickness);
            b.push_strikethrough(bounds, opaque(), thickness);
            b.push_overline(bounds, opaque(), thickness);
            assert!(b.items.is_empty(), "thickness {thickness} must not paint");
        }
        // +inf is > 0.0, so it *is* pushed (defined, no panic).
        let mut b = DisplayListBuilder::new();
        b.push_underline(bounds, opaque(), f32::INFINITY);
        assert_eq!(b.items.len(), 1);
        // Transparent decorations are skipped regardless of thickness.
        let mut b = DisplayListBuilder::new();
        b.push_overline(bounds, ColorU::TRANSPARENT, 3.0);
        assert!(b.items.is_empty());
    }
    #[test]
    fn builder_text_run_skips_empty_glyphs_and_transparent_color() {
        let clip = rect(0.0, 0.0, 100.0, 20.0);
        let mut b = DisplayListBuilder::new();
        b.push_text_run(Vec::new(), FontHash::invalid(), 16.0, opaque(), clip, Some(0), None);
        b.push_text_run(vec![glyph(1, 0.0, 0.0)], FontHash::invalid(), 16.0, ColorU::TRANSPARENT, clip, Some(0), None);
        assert!(b.items.is_empty());
        // NaN / huge font sizes are pass-through values, not a panic.
        b.push_text_run(vec![glyph(1, 0.0, 0.0)], FontHash::invalid(), f32::NAN, opaque(), clip, None, None);
        b.push_text_run(vec![glyph(2, 0.0, 0.0)], FontHash::invalid(), f32::MAX, opaque(), clip, None, None);
        assert_eq!(b.items.len(), 2);
    }
    #[test]
    fn builder_border_requires_both_a_width_and_a_style() {
        let bounds = rect(0.0, 0.0, 10.0, 10.0);
        let mut b = DisplayListBuilder::new();
        b.push_border(bounds, no_widths(), no_colors(), no_styles(), zero_style_radius());
        assert!(b.items.is_empty(), "no widths + no styles => nothing to draw");
        let mut b = DisplayListBuilder::new();
        b.push_border(bounds, all_widths(), no_colors(), no_styles(), zero_style_radius());
        assert!(b.items.is_empty(), "width without style => nothing to draw");
        let mut b = DisplayListBuilder::new();
        b.push_border(bounds, no_widths(), no_colors(), all_styles(), zero_style_radius());
        assert!(b.items.is_empty(), "style without width => nothing to draw");
        let mut b = DisplayListBuilder::new();
        b.push_border(bounds, all_widths(), no_colors(), all_styles(), zero_style_radius());
        assert_eq!(b.items.len(), 1);
    }
    #[test]
    fn builder_forced_page_breaks_are_deduped_and_sorted() {
        let mut b = DisplayListBuilder::new();
        b.add_forced_page_break(300.0, None);
        b.add_forced_page_break(100.0, None);
        b.add_forced_page_break(300.0, None); // exact duplicate
        b.add_forced_page_break(200.0, None);
        b.add_forced_page_break(0.0, None);
        b.add_forced_page_break(-50.0, None); // negative is accepted verbatim
        b.add_forced_page_break(f32::INFINITY, None);
        b.add_forced_page_break(f32::NEG_INFINITY, None);
        assert_eq!(
            b.forced_page_breaks.iter().map(|fb| fb.y).collect::<Vec<_>>(),
            vec![f32::NEG_INFINITY, -50.0, 0.0, 100.0, 200.0, 300.0, f32::INFINITY]
        );
    }
    #[test]
    fn builder_forced_page_break_nan_is_never_deduped() {
        // `contains(&NaN)` is always false (NaN != NaN), so NaN breaks are NOT deduped —
        // repeated calls accumulate. The sort uses partial_cmp().unwrap_or(Equal), so it
        // survives the non-total order rather than panicking on an `unwrap`.
        let mut b = DisplayListBuilder::new();
        b.add_forced_page_break(f32::NAN, None);
        b.add_forced_page_break(f32::NAN, None);
        b.add_forced_page_break(100.0, None);
        assert_eq!(b.forced_page_breaks.len(), 3, "a NaN break is never deduped");
        assert_eq!(b.forced_page_breaks.iter().filter(|v| v.y.is_nan()).count(), 2);
    }
    #[test]
    fn builder_fixed_position_ranges_need_a_begin_and_at_least_one_item() {
        // end without begin: no-op, no panic.
        let mut b = DisplayListBuilder::new();
        b.end_fixed_position_element();
        assert!(b.fixed_position_item_ranges.is_empty());
        // begin + end with zero items in between: nothing recorded (end > start is false).
        let mut b = DisplayListBuilder::new();
        b.begin_fixed_position_element();
        b.end_fixed_position_element();
        assert!(b.fixed_position_item_ranges.is_empty());
        // begin + items + end: the half-open [start, end) range is recorded.
        let mut b = DisplayListBuilder::new();
        b.pop_clip(); // one pre-existing item at index 0
        b.begin_fixed_position_element();
        b.push_rect(rect(0.0, 0.0, 1.0, 1.0), opaque(), BorderRadius::default());
        b.push_rect(rect(0.0, 0.0, 2.0, 2.0), opaque(), BorderRadius::default());
        b.end_fixed_position_element();
        assert_eq!(b.fixed_position_item_ranges, vec![(1, 3)]);
        // A second end() without a matching begin() must not re-record.
        b.end_fixed_position_element();
        assert_eq!(b.fixed_position_item_ranges.len(), 1);
    }
    #[test]
    fn builder_build_with_debug_transfers_messages() {
        let mut b = DisplayListBuilder::with_debug(true);
        b.debug_log("hello".to_string());
        b.push_rect(rect(0.0, 0.0, 1.0, 1.0), opaque(), BorderRadius::default());
        let mut sink = Some(vec![LayoutDebugMessage::info("pre-existing".to_string())]);
        let dl = b.build_with_debug(&mut sink);
        let msgs = sink.expect("sink stays Some");
        assert_eq!(msgs.len(), 2, "messages are appended, not replaced");
        assert_eq!(dl.items.len(), 1);
        // A None sink must swallow the messages rather than panic.
        let mut b = DisplayListBuilder::with_debug(true);
        b.debug_log("dropped".to_string());
        let mut none_sink: Option<Vec<LayoutDebugMessage>> = None;
        let dl = b.build_with_debug(&mut none_sink);
        assert!(none_sink.is_none());
        assert!(dl.items.is_empty());
    }
    #[test]
    fn builder_stack_pushes_accept_extreme_arguments() {
        let mut b = DisplayListBuilder::new();
        // i32 extremes for z-index, degenerate bounds, and an all-NaN clip.
        b.push_stacking_context(i32::MIN, rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN));
        b.push_stacking_context(i32::MAX, rect(0.0, 0.0, -1.0, -1.0));
        b.pop_stacking_context();
        b.push_clip(rect(f32::MIN, f32::MIN, f32::MAX, f32::MAX), BorderRadius { top_left: f32::INFINITY, ..BorderRadius::default() });
        b.pop_clip();
        b.push_scroll_frame(LogicalRect::zero(), LogicalSize::new(f32::MAX, f32::MAX), u64::MAX);
        b.pop_scroll_frame();
        b.push_image_mask_clip(LogicalRect::zero(), test_image(), rect(0.0, 0.0, -5.0, -5.0));
        b.pop_image_mask_clip();
        b.push_reference_frame(TransformKey::unique(), ComputedTransform3D::IDENTITY, LogicalRect::zero());
        b.pop_reference_frame();
        b.push_virtual_view_placeholder(NodeId::ZERO, LogicalRect::zero(), LogicalRect::zero());
        b.push_hit_test_area(rect(0.0, 0.0, 1.0, 1.0), (u64::MAX, TAG_TYPE_CURSOR));
        b.push_image(LogicalRect::zero(), test_image(), BorderRadius::default());
        b.push_linear_gradient(LogicalRect::zero(), LinearGradient::default(), BorderRadius::default());
        b.push_radial_gradient(LogicalRect::zero(), RadialGradient::default(), BorderRadius::default());
        b.push_conic_gradient(LogicalRect::zero(), ConicGradient::default(), BorderRadius::default());
        let dl = b.build();
        // 17 until push_item started dropping items at the unassigned-position
        // sentinel. Two pushes above use NaN / f32::MIN ORIGINS — the NaN
        // stacking context and the f32::MIN clip — and those are exactly what
        // the drop exists to remove, so they no longer reach the list. The
        // point of this test is that extreme arguments do not PANIC and do not
        // desync the node mapping; both still hold.
        assert_eq!(
            dl.items.len(),
            15,
            "expected the NaN-origin stacking context and the f32::MIN-origin \
             clip to be dropped as unassigned positions; everything else is \
             real geometry and must survive"
        );
        assert_eq!(dl.items.len(), dl.node_mapping.len());
        // The mapping must stay in lockstep — dropping an item without dropping
        // its node entry would misattribute every later item to the wrong node.
        assert!(
            !dl.items.is_empty(),
            "the drop must not swallow well-formed items"
        );
    }
    // ---------------------------------------------------------------------
    // Free geometry helpers: rect_intersects / clip_rect_bounds / offset_rect_y
    // ---------------------------------------------------------------------
    #[test]
    fn rect_intersects_uses_a_half_open_page_interval() {
        let page = (100.0f32, 200.0f32);
        // Fully inside.
        assert!(rect_intersects(&rect(0.0, 120.0, 10.0, 10.0), page.0, page.1));
        // Straddling both edges.
        assert!(rect_intersects(&rect(0.0, 50.0, 10.0, 300.0), page.0, page.1));
        // Entirely above / below.
        assert!(!rect_intersects(&rect(0.0, 0.0, 10.0, 10.0), page.0, page.1));
        assert!(!rect_intersects(&rect(0.0, 500.0, 10.0, 10.0), page.0, page.1));
        // Touching exactly: bottom edge == page_top is NOT an intersection...
        assert!(!rect_intersects(&rect(0.0, 90.0, 10.0, 10.0), page.0, page.1));
        // ...and top edge == page_bottom is NOT either.
        assert!(!rect_intersects(&rect(0.0, 200.0, 10.0, 10.0), page.0, page.1));
        // A zero-height rect strictly inside DOES intersect (its single edge is in range).
        assert!(rect_intersects(&rect(0.0, 150.0, 10.0, 0.0), page.0, page.1));
        // ...but a zero-height rect sitting exactly on either page edge does not.
        assert!(!rect_intersects(&rect(0.0, 100.0, 10.0, 0.0), page.0, page.1));
        assert!(!rect_intersects(&rect(0.0, 200.0, 10.0, 0.0), page.0, page.1));
    }
    #[test]
    fn rect_intersects_with_nan_is_false_not_a_panic() {
        assert!(!rect_intersects(&rect(0.0, f32::NAN, 10.0, 10.0), 0.0, 100.0));
        assert!(!rect_intersects(&rect(0.0, 10.0, 10.0, f32::NAN), 0.0, 100.0));
        assert!(!rect_intersects(&rect(0.0, 10.0, 10.0, 10.0), f32::NAN, f32::NAN));
        // Infinite page bounds cover everything finite.
        assert!(rect_intersects(&rect(0.0, 10.0, 10.0, 10.0), f32::NEG_INFINITY, f32::INFINITY));
    }
    #[test]
    fn clip_rect_bounds_clips_and_rebases_to_page_relative_coords() {
        // Item straddles the top of page [100, 200): keep the visible slice, rebase to y=0.
        let clipped = clip_rect_bounds(rect(5.0, 50.0, 20.0, 100.0), 100.0, 200.0).unwrap();
        assert_eq!(clipped, rect(5.0, 0.0, 20.0, 50.0));
        // Item straddles the bottom: kept slice starts at its own offset into the page.
        let clipped = clip_rect_bounds(rect(5.0, 180.0, 20.0, 100.0), 100.0, 200.0).unwrap();
        assert_eq!(clipped, rect(5.0, 80.0, 20.0, 20.0));
        // Item strictly inside: only rebased, never resized.
        let clipped = clip_rect_bounds(rect(5.0, 120.0, 20.0, 30.0), 100.0, 200.0).unwrap();
        assert_eq!(clipped, rect(5.0, 20.0, 20.0, 30.0));
        // Item larger than the page on both sides: clamped to exactly the page height.
        let clipped = clip_rect_bounds(rect(0.0, 0.0, 20.0, 10_000.0), 100.0, 200.0).unwrap();
        assert_eq!(clipped, rect(0.0, 0.0, 20.0, 100.0));
    }
    #[test]
    fn clip_rect_bounds_rejects_off_page_and_edge_touching_rects() {
        assert_eq!(clip_rect_bounds(rect(0.0, 0.0, 10.0, 10.0), 100.0, 200.0), None);
        assert_eq!(clip_rect_bounds(rect(0.0, 300.0, 10.0, 10.0), 100.0, 200.0), None);
        // bottom == page_top -> outside (half-open interval).
        assert_eq!(clip_rect_bounds(rect(0.0, 90.0, 10.0, 10.0), 100.0, 200.0), None);
        // top == page_bottom -> outside.
        assert_eq!(clip_rect_bounds(rect(0.0, 200.0, 10.0, 10.0), 100.0, 200.0), None);
    }
    #[test]
    fn clip_rect_bounds_zero_height_rects() {
        // A zero-height rect sitting exactly on page_top is rejected (bottom <= top).
        assert_eq!(clip_rect_bounds(rect(0.0, 100.0, 10.0, 0.0), 100.0, 200.0), None);
        // A zero-height rect strictly inside survives as a zero-height slice.
        assert_eq!(
            clip_rect_bounds(rect(0.0, 150.0, 10.0, 0.0), 100.0, 200.0),
            Some(rect(0.0, 50.0, 10.0, 0.0))
        );
    }
    #[test]
    fn clip_rect_bounds_with_an_inverted_page_produces_a_degenerate_rect_not_a_panic() {
        // page_top > page_bottom is never produced by calculate_page_break_positions,
        // but nothing rejects it here: the height goes NEGATIVE. Pinned so the missing
        // guard is visible rather than silently feeding a negative rect downstream.
        let out = clip_rect_bounds(rect(0.0, 0.0, 10.0, 500.0), 200.0, 100.0)
            .expect("an inverted page is not rejected");
        assert!(out.size.height < 0.0, "no clamp: an inverted page yields a negative height");
    }
    #[test]
    fn clip_rect_bounds_with_extreme_pages_does_not_panic() {
        // An unbounded page keeps the item intact (no clipping).
        let full = clip_rect_bounds(rect(0.0, 10.0, 10.0, 10.0), f32::NEG_INFINITY, f32::INFINITY)
            .expect("an infinite page contains everything");
        assert_eq!(full.size, LogicalSize::new(10.0, 10.0));
        // NaN page bounds: every comparison is false, so the rect is kept. f32::max/min
        // drop the NaN operand, so the SIZE stays clean and only the rebased origin goes
        // NaN. Defined, total, and crucially NOT a panic.
        let nan_page = clip_rect_bounds(rect(0.0, 10.0, 10.0, 10.0), f32::NAN, f32::NAN)
            .expect("NaN bounds fail both rejection tests");
        assert!(nan_page.origin.y.is_nan(), "the page-relative rebase propagates NaN");
        assert_eq!(nan_page.size.height, 10.0, "min/max ignore NaN, so the height survives");
        // f32::MAX height must not overflow into a panic.
        assert_eq!(
            clip_rect_bounds(rect(0.0, 0.0, 10.0, f32::MAX), 0.0, 100.0),
            Some(rect(0.0, 0.0, 10.0, 100.0))
        );
    }
    #[test]
    fn offset_rect_y_only_moves_y_and_preserves_size() {
        assert_eq!(offset_rect_y(rect(1.0, 2.0, 3.0, 4.0), 0.0), rect(1.0, 2.0, 3.0, 4.0));
        assert_eq!(offset_rect_y(rect(1.0, 2.0, 3.0, 4.0), 10.0), rect(1.0, 12.0, 3.0, 4.0));
        assert_eq!(offset_rect_y(rect(1.0, 2.0, 3.0, 4.0), -10.0), rect(1.0, -8.0, 3.0, 4.0));
        // Non-finite offsets are propagated, never trapped.
        assert!(offset_rect_y(rect(0.0, 0.0, 1.0, 1.0), f32::INFINITY).origin.y.is_infinite());
        assert!(offset_rect_y(rect(0.0, 0.0, 1.0, 1.0), f32::NAN).origin.y.is_nan());
        // MAX + MAX saturates to +inf under IEEE-754, not a wrap.
        assert!(offset_rect_y(rect(0.0, f32::MAX, 1.0, 1.0), f32::MAX).origin.y.is_infinite());
        // The size is untouched in every case.
        assert_eq!(offset_rect_y(rect(0.0, 0.0, 3.0, 4.0), f32::NAN).size, LogicalSize::new(3.0, 4.0));
    }
    // ---------------------------------------------------------------------
    // Per-item page clipping
    // ---------------------------------------------------------------------
    #[test]
    fn clip_rect_item_drops_off_page_and_rebases_on_page() {
        assert!(clip_rect_item(rect(0.0, 0.0, 10.0, 10.0), opaque(), BorderRadius::default(), 100.0, 200.0).is_none());
        let item = clip_rect_item(rect(0.0, 150.0, 10.0, 100.0), opaque(), BorderRadius::default(), 100.0, 200.0)
            .expect("overlaps the page");
        match item {
            DisplayListItem::Rect { bounds, color, .. } => {
                assert_eq!(bounds.into_inner(), rect(0.0, 50.0, 10.0, 50.0));
                assert_eq!(color, opaque());
            }
            other => panic!("expected Rect, got {other:?}"),
        }
    }
    #[test]
    fn clip_cursor_selection_hittest_and_scrollbar_items_share_the_page_test() {
        let off = rect(0.0, 0.0, 10.0, 10.0);
        let on = rect(0.0, 120.0, 10.0, 10.0);
        let (top, bottom) = (100.0, 200.0);
        assert!(clip_cursor_rect_item(off, opaque(), top, bottom).is_none());
        assert!(clip_cursor_rect_item(on, opaque(), top, bottom).is_some());
        assert!(clip_selection_rect_item(off, BorderRadius::default(), opaque(), top, bottom).is_none());
        assert!(clip_selection_rect_item(on, BorderRadius::default(), opaque(), top, bottom).is_some());
        assert!(clip_hit_test_area_item(off, (1, TAG_TYPE_DOM_NODE), top, bottom).is_none());
        assert!(clip_hit_test_area_item(on, (1, TAG_TYPE_DOM_NODE), top, bottom).is_some());
        assert!(clip_scrollbar_item(off, opaque(), ScrollbarOrientation::Vertical, None, None, top, bottom).is_none());
        assert!(clip_scrollbar_item(on, opaque(), ScrollbarOrientation::Vertical, None, None, top, bottom).is_some());
        assert!(clip_image_item(off, test_image(), BorderRadius::default(), top, bottom).is_none());
        assert!(clip_image_item(on, test_image(), BorderRadius::default(), top, bottom).is_some());
        let no_shift = LogicalPosition::zero();
        assert!(clip_virtual_view_item(DomId::ROOT_ID, off, off, no_shift, top, bottom).is_none());
        assert!(clip_virtual_view_item(DomId::ROOT_ID, on, on, no_shift, top, bottom).is_some());
    }
    #[test]
    fn clip_text_decoration_item_preserves_the_decoration_kind() {
        let on = rect(0.0, 120.0, 10.0, 2.0);
        let (top, bottom) = (100.0, 200.0);
        assert!(matches!(
            clip_text_decoration_item(on, opaque(), 1.0, TextDecorationType::Underline, top, bottom),
            Some(DisplayListItem::Underline { .. })
        ));
        assert!(matches!(
            clip_text_decoration_item(on, opaque(), 1.0, TextDecorationType::Strikethrough, top, bottom),
            Some(DisplayListItem::Strikethrough { .. })
        ));
        assert!(matches!(
            clip_text_decoration_item(on, opaque(), 1.0, TextDecorationType::Overline, top, bottom),
            Some(DisplayListItem::Overline { .. })
        ));
        // Off-page decorations are dropped even with a NaN thickness.
        assert!(clip_text_decoration_item(
            rect(0.0, 0.0, 10.0, 2.0), opaque(), f32::NAN, TextDecorationType::Underline, top, bottom
        ).is_none());
    }
    #[test]
    fn clip_text_item_filters_glyphs_by_baseline_into_a_half_open_page() {
        let clip = rect(0.0, 90.0, 200.0, 120.0);
        let glyphs = vec![
            glyph(1, 0.0, 99.0),   // above page  -> dropped
            glyph(2, 8.0, 100.0),  // exactly page_top -> KEPT (>= top)
            glyph(3, 16.0, 150.0), // inside -> kept
            glyph(4, 24.0, 200.0), // exactly page_bottom -> dropped (< bottom)
            glyph(5, 32.0, 250.0), // below -> dropped
        ];
        let item = clip_text_item(&glyphs, FontHash::from_hash(9), 16.0, opaque(), clip, 100.0, 200.0)
            .expect("some glyphs are on the page");
        match item {
            DisplayListItem::Text { glyphs: kept, clip_rect, source_node_index, .. } => {
                assert_eq!(kept.iter().map(|g| g.index).collect::<Vec<_>>(), vec![2, 3]);
                // Kept glyphs are rebased to page-relative Y.
                assert_eq!(kept[0].point.y, 0.0);
                assert_eq!(kept[1].point.y, 50.0);
                // X is never touched.
                assert_eq!(kept[0].point.x, 8.0);
                assert_eq!(clip_rect.into_inner().origin.y, -10.0);
                assert_eq!(source_node_index, None, "the paginated copy loses its source node");
            }
            other => panic!("expected Text, got {other:?}"),
        }
    }
    #[test]
    fn clip_text_item_returns_none_when_nothing_survives() {
        let clip = rect(0.0, 90.0, 200.0, 120.0);
        // The clip rect overlaps the page but no glyph baseline does.
        let outside = vec![glyph(1, 0.0, 95.0), glyph(2, 8.0, 400.0)];
        assert!(clip_text_item(&outside, FontHash::from_hash(1), 16.0, opaque(), clip, 100.0, 200.0).is_none());
        // An empty glyph run is dropped too.
        assert!(clip_text_item(&[], FontHash::from_hash(1), 16.0, opaque(), clip, 100.0, 200.0).is_none());
        // A clip rect entirely off the page short-circuits before glyph filtering.
        assert!(clip_text_item(
            &[glyph(1, 0.0, 150.0)], FontHash::from_hash(1), 16.0, opaque(),
            rect(0.0, 0.0, 10.0, 10.0), 100.0, 200.0
        ).is_none());
        // NaN glyph baselines never satisfy either bound -> filtered out -> None.
        let nan_glyphs = vec![glyph(1, 0.0, f32::NAN)];
        assert!(clip_text_item(&nan_glyphs, FontHash::from_hash(1), 16.0, opaque(), clip, 100.0, 200.0).is_none());
    }
    #[test]
    fn clip_border_item_hides_the_bottom_border_when_clipped_at_the_bottom() {
        let (top, bottom) = (100.0f32, 200.0f32);
        // Spans past the bottom of the page.
        let original = rect(0.0, 50.0, 100.0, 200.0);
        let item = clip_border_item(original, all_widths(), no_colors(), all_styles(), zero_style_radius(), top, bottom)
            .expect("overlaps the page");
        match item {
            DisplayListItem::Border { widths, .. } => {
                assert!(widths.bottom.is_none(), "a border cut by the page edge must not draw its bottom rule");
                assert!(widths.left.is_some() && widths.right.is_some(), "side borders survive");
            }
            other => panic!("expected Border, got {other:?}"),
        }
    }
    #[test]
    fn adjust_border_widths_keeps_every_side_for_a_fully_contained_border() {
        let (top, bottom) = (100.0f32, 200.0f32);
        let original = rect(0.0, 110.0, 100.0, 50.0); // strictly inside the page
        let clipped = clip_rect_bounds(original, top, bottom).unwrap();
        assert_eq!(clipped, rect(0.0, 10.0, 100.0, 50.0), "unclipped => only rebased");
        let widths = adjust_border_widths_for_clipping(all_widths(), original, clipped, top, bottom);
        assert!(widths.top.is_some());
        assert!(widths.bottom.is_some());
        assert!(widths.left.is_some());
        assert!(widths.right.is_some());
    }
    #[test]
    fn adjust_border_widths_with_nan_page_bounds_does_not_panic() {
        let original = rect(0.0, 0.0, 10.0, 10.0);
        let clipped = rect(0.0, 0.0, 10.0, 10.0);
        let w = adjust_border_widths_for_clipping(all_widths(), original, clipped, f32::NAN, f32::NAN);
        // Every NaN comparison is false, so nothing is hidden — defined and total.
        assert!(w.top.is_some() && w.bottom.is_some());
    }
    #[test]
    fn clip_and_offset_display_item_drops_state_management_commands() {
        // Pagination cannot re-derive a clip/scroll/stacking stack per page, so those
        // items are deliberately dropped. Pin it so a change is visible.
        for item in [
            DisplayListItem::PushClip { bounds: rect(0.0, 120.0, 10.0, 10.0).into(), border_radius: BorderRadius::default() },
            DisplayListItem::PopClip,
            DisplayListItem::PushScrollFrame {
                clip_bounds: rect(0.0, 120.0, 10.0, 10.0).into(),
                content_size: LogicalSize::new(10.0, 10.0),
                scroll_id: 1,
            },
            DisplayListItem::PopScrollFrame,
            DisplayListItem::PushStackingContext { z_index: 0, bounds: rect(0.0, 120.0, 10.0, 10.0).into() },
            DisplayListItem::PopStackingContext,
            DisplayListItem::PopOpacity,
            DisplayListItem::PopTextShadow,
            DisplayListItem::VirtualViewPlaceholder {
                node_id: NodeId::ZERO,
                bounds: rect(0.0, 120.0, 10.0, 10.0).into(),
                clip_rect: rect(0.0, 120.0, 10.0, 10.0).into(),
            },
        ] {
            assert!(
                clip_and_offset_display_item(&item, 100.0, 200.0).is_none(),
                "{item:?} must be dropped by the paginator"
            );
        }
    }
    #[test]
    fn clip_and_offset_display_item_dispatches_drawing_items() {
        let on_page = DisplayListItem::Rect {
            bounds: rect(0.0, 150.0, 10.0, 10.0).into(),
            color: opaque(),
            border_radius: BorderRadius::default(),
        };
        let clipped = clip_and_offset_display_item(&on_page, 100.0, 200.0).expect("on page");
        assert_eq!(clipped.bounds(), Some(rect(0.0, 50.0, 10.0, 10.0)));
        let off_page = DisplayListItem::Rect {
            bounds: rect(0.0, 0.0, 10.0, 10.0).into(),
            color: opaque(),
            border_radius: BorderRadius::default(),
        };
        assert!(clip_and_offset_display_item(&off_page, 100.0, 200.0).is_none());
        // Gradients use a plain bounds test (no sub-item filtering).
        let grad = DisplayListItem::LinearGradient {
            bounds: rect(0.0, 150.0, 10.0, 10.0).into(),
            gradient: LinearGradient::default(),
            border_radius: BorderRadius::default(),
        };
        assert!(clip_and_offset_display_item(&grad, 100.0, 200.0).is_some());
        assert!(clip_and_offset_display_item(&grad, 1000.0, 2000.0).is_none());
    }
    // ---------------------------------------------------------------------
    // get_display_item_bounds / offset_display_item_y / heights
    // ---------------------------------------------------------------------
    #[test]
    fn get_display_item_bounds_mirrors_item_bounds() {
        let r = rect(1.0, 2.0, 3.0, 4.0);
        let item = DisplayListItem::Rect { bounds: r.into(), color: opaque(), border_radius: BorderRadius::default() };
        assert_eq!(get_display_item_bounds(&item), Some(WindowLogicalRect::from(r)));
        assert_eq!(get_display_item_bounds(&DisplayListItem::PopClip), None);
    }
    #[test]
    fn offset_display_item_y_zero_offset_is_a_pure_clone() {
        let item = text_item(Some(3), rect(0.0, 10.0, 100.0, 20.0), vec![glyph(1, 5.0, 15.0)]);
        let same = offset_display_item_y(&item, 0.0);
        assert!(item.is_visually_equal(&same));
        assert_eq!(same.bounds(), item.bounds());
    }
    #[test]
    fn offset_display_item_y_moves_glyphs_and_clip_together() {
        let item = text_item(Some(3), rect(0.0, 10.0, 100.0, 20.0), vec![glyph(1, 5.0, 15.0), glyph(2, 13.0, 15.0)]);
        match offset_display_item_y(&item, -10.0) {
            DisplayListItem::Text { glyphs, clip_rect, .. } => {
                assert_eq!(clip_rect.into_inner(), rect(0.0, 0.0, 100.0, 20.0));
                assert_eq!(glyphs[0].point.y, 5.0);
                assert_eq!(glyphs[1].point.y, 5.0);
                assert_eq!(glyphs[0].point.x, 5.0, "X must not move");
            }
            other => panic!("expected Text, got {other:?}"),
        }
    }
    #[test]
    fn offset_display_item_y_with_nonfinite_offsets_does_not_panic() {
        let item = DisplayListItem::Rect {
            bounds: rect(0.0, 10.0, 5.0, 5.0).into(),
            color: opaque(),
            border_radius: BorderRadius::default(),
        };
        assert!(offset_display_item_y(&item, f32::NAN).bounds().unwrap().origin.y.is_nan());
        assert!(offset_display_item_y(&item, f32::INFINITY).bounds().unwrap().origin.y.is_infinite());
        assert!(offset_display_item_y(&item, f32::MIN).bounds().unwrap().origin.y.is_finite());
    }
    #[test]
    fn calculate_display_list_height_ignores_hairline_items() {
        assert_eq!(calculate_display_list_height(&DisplayList::default()), 0.0);
        // Items thinner than 0.1px do not contribute (they are not "visible content").
        let dl = list_of(vec![
            DisplayListItem::Rect {
                bounds: rect(0.0, 5000.0, 10.0, 0.05).into(),
                color: opaque(),
                border_radius: BorderRadius::default(),
            },
            DisplayListItem::Rect {
                bounds: rect(0.0, 10.0, 10.0, 40.0).into(),
                color: opaque(),
                border_radius: BorderRadius::default(),
            },
        ]);
        assert_eq!(calculate_display_list_height(&dl), 50.0);
    }
    #[test]
    fn calculate_display_list_height_never_returns_negative_or_nan() {
        // Negative and NaN geometry must floor at 0.0 rather than poison the height
        // (a NaN height would panic the paginator's `partial_cmp().unwrap()` sort).
        let dl = list_of(vec![
            DisplayListItem::Rect {
                bounds: rect(0.0, -500.0, 10.0, 100.0).into(),
                color: opaque(),
                border_radius: BorderRadius::default(),
            },
            DisplayListItem::Rect {
                bounds: rect(0.0, f32::NAN, 10.0, f32::NAN).into(),
                color: opaque(),
                border_radius: BorderRadius::default(),
            },
            DisplayListItem::PopClip, // no bounds at all
        ]);
        let h = calculate_display_list_height(&dl);
        assert!(!h.is_nan(), "a NaN total height would panic the page-break sort");
        assert_eq!(h, 0.0);
    }
    // ---------------------------------------------------------------------
    // get_scroll_id
    // ---------------------------------------------------------------------
    #[test]
    fn get_scroll_id_maps_node_index_and_collides_none_with_node_zero() {
        assert_eq!(get_scroll_id(None), 0);
        assert_eq!(get_scroll_id(Some(NodeId::new(5))), 5);
        assert_eq!(get_scroll_id(Some(NodeId::new(usize::MAX))), usize::MAX as u64);
        // NOTE: the "no node" sentinel and the root node both map to 0.
        assert_eq!(get_scroll_id(Some(NodeId::ZERO)), get_scroll_id(None));
    }
    // ---------------------------------------------------------------------
    // SlicerConfig
    // ---------------------------------------------------------------------
    #[test]
    fn slicer_config_builders_set_the_fields_they_name() {
        let simple = SlicerConfig::simple(800.0);
        assert_eq!(simple.page_content_height, 800.0);
        assert_eq!(simple.page_gap, 0.0);
        assert!(simple.allow_clipping);
        assert_eq!(simple.page_width, DEFAULT_A4_WIDTH_PT);
        assert_eq!(simple.page_slot_height(), 800.0);
        let gapped = SlicerConfig::with_gap(800.0, 40.0);
        assert_eq!(gapped.page_gap, 40.0);
        assert_eq!(gapped.page_slot_height(), 840.0);
        let wide = SlicerConfig::simple(800.0).with_page_width(1000.0);
        assert_eq!(wide.page_width, 1000.0);
        assert_eq!(wide.page_content_height, 800.0, "with_page_width must not disturb the height");
    }
    #[test]
    fn slicer_config_builders_accept_extreme_values() {
        for h in [0.0, -100.0, f32::MAX, f32::INFINITY, f32::NAN] {
            let c = SlicerConfig::simple(h);
            assert_eq!(c.page_content_height.to_bits(), h.to_bits());
            let _ = c.page_slot_height();
            let _ = c.page_for_y(10.0);
            let _ = c.page_bounds(0);
        }
        // NaN gap propagates into the slot height without panicking.
        assert!(SlicerConfig::with_gap(100.0, f32::NAN).page_slot_height().is_nan());
    }
    #[test]
    fn page_for_y_basic_and_boundary() {
        let c = SlicerConfig::simple(100.0);
        assert_eq!(c.page_for_y(0.0), 0);
        assert_eq!(c.page_for_y(99.999), 0);
        assert_eq!(c.page_for_y(100.0), 1, "the page boundary belongs to the next page");
        assert_eq!(c.page_for_y(250.0), 2);
        // The gap counts towards the slot: page 1 starts at 120, not 100.
        let g = SlicerConfig::with_gap(100.0, 20.0);
        assert_eq!(g.page_for_y(119.0), 0);
        assert_eq!(g.page_for_y(120.0), 1);
    }
    #[test]
    fn page_for_y_saturates_instead_of_wrapping_or_trapping() {
        let c = SlicerConfig::simple(100.0);
        // Negative Y floors to a negative page; the f32->usize cast SATURATES to 0
        // (Rust >= 1.45), it does not wrap to usize::MAX.
        assert_eq!(c.page_for_y(-1.0), 0);
        assert_eq!(c.page_for_y(-1e30), 0);
        assert_eq!(c.page_for_y(f32::NEG_INFINITY), 0);
        // NaN saturates to 0 as well.
        assert_eq!(c.page_for_y(f32::NAN), 0);
        // Enormous Y saturates to usize::MAX rather than overflowing.
        assert_eq!(c.page_for_y(f32::INFINITY), usize::MAX);
        assert_eq!(c.page_for_y(f32::MAX), usize::MAX);
    }
    #[test]
    fn page_for_y_with_a_nonpositive_slot_is_always_page_zero() {
        // Guard against a division by zero / infinite page index.
        assert_eq!(SlicerConfig::default().page_for_y(f32::MAX), 0);
        assert_eq!(SlicerConfig::simple(0.0).page_for_y(500.0), 0);
        assert_eq!(SlicerConfig::with_gap(100.0, -100.0).page_for_y(500.0), 0, "slot == 0");
        assert_eq!(SlicerConfig::with_gap(100.0, -500.0).page_for_y(500.0), 0, "slot < 0");
        // A NaN slot is not > 0.0, but it also fails the `<= 0.0` guard; the cast still
        // saturates NaN to 0 rather than trapping.
        assert_eq!(SlicerConfig::simple(f32::NAN).page_for_y(500.0), 0);
    }
    #[test]
    fn page_bounds_are_contiguous_without_a_gap_and_spaced_with_one() {
        let c = SlicerConfig::simple(100.0);
        assert_eq!(c.page_bounds(0), (0.0, 100.0));
        assert_eq!(c.page_bounds(1), (100.0, 200.0));
        assert_eq!(c.page_bounds(3), (300.0, 400.0));
        let g = SlicerConfig::with_gap(100.0, 20.0);
        assert_eq!(g.page_bounds(0), (0.0, 100.0));
        assert_eq!(g.page_bounds(2), (240.0, 340.0), "the gap is dead space between pages");
        // page_for_y and page_bounds must agree.
        for page in 0..5usize {
            let (start, _end) = g.page_bounds(page);
            assert_eq!(g.page_for_y(start), page);
        }
    }
    #[test]
    fn page_bounds_at_extreme_indices_do_not_panic() {
        let c = SlicerConfig::simple(100.0);
        let (start, end) = c.page_bounds(usize::MAX);
        assert!(start.is_finite() && end.is_finite(), "usize::MAX as f32 * 100 stays in f32 range");
        assert!(start > 0.0);
        // A zero-height config collapses every page onto (0, 0).
        assert_eq!(SlicerConfig::default().page_bounds(9999), (0.0, 0.0));
    }
    // ---------------------------------------------------------------------
    // calculate_page_break_positions
    // ---------------------------------------------------------------------
    #[test]
    fn page_breaks_for_an_empty_or_zero_height_list_is_a_single_page() {
        let pages = calculate_page_break_positions(&DisplayList::default(), 100.0, 100.0);
        assert_eq!(pages, vec![(0.0, 100.0)], "an empty document still has one page");
        // first_page_height <= 0 short-circuits to a single page too.
        let dl = list_of(vec![DisplayListItem::Rect {
            bounds: rect(0.0, 0.0, 10.0, 250.0).into(),
            color: opaque(),
            border_radius: BorderRadius::default(),
        }]);
        assert_eq!(calculate_page_break_positions(&dl, 0.0, 100.0), vec![(0.0, 250.0)]);
        assert_eq!(calculate_page_break_positions(&dl, -50.0, 100.0), vec![(0.0, 250.0)]);
    }
    #[test]
    fn page_breaks_split_at_regular_intervals() {
        let dl = list_of(vec![DisplayListItem::Rect {
            bounds: rect(0.0, 0.0, 10.0, 250.0).into(),
            color: opaque(),
            border_radius: BorderRadius::default(),
        }]);
        let pages = calculate_page_break_positions(&dl, 100.0, 100.0);
        assert_eq!(pages, vec![(0.0, 100.0), (100.0, 200.0), (200.0, 250.0)]);
        // Pages must tile the document with no gaps and no overlaps.
        for w in pages.windows(2) {
            assert_eq!(w[0].1, w[1].0);
        }
        assert_eq!(pages.first().unwrap().0, 0.0);
        assert_eq!(pages.last().unwrap().1, 250.0);
    }
    #[test]
    fn page_breaks_honour_forced_breaks_and_merge_near_duplicates() {
        let mut dl = list_of(vec![DisplayListItem::Rect {
            bounds: rect(0.0, 0.0, 10.0, 250.0).into(),
            color: opaque(),
            border_radius: BorderRadius::default(),
        }]);
        dl.forced_page_breaks = vec![ForcedBreak { y: 50.0, causing_node: None }];
        assert_eq!(
            calculate_page_break_positions(&dl, 100.0, 100.0),
            vec![(0.0, 50.0), (50.0, 100.0), (100.0, 200.0), (200.0, 250.0)]
        );
        // A forced break within 1px of a regular break is merged, not duplicated
        // (a duplicate would emit a zero-height page) — and the FORCED break's
        // position wins the merge (CSS Fragmentation: forced breaks always
        // apply; before the page_breaks extraction the interval break at 100.0
        // silently swallowed the author's break at 100.5).
        dl.forced_page_breaks = vec![ForcedBreak { y: 100.5, causing_node: None }];
        let pages = calculate_page_break_positions(&dl, 100.0, 100.0);
        assert_eq!(pages, vec![(0.0, 100.5), (100.5, 200.0), (200.0, 250.0)]);
        assert!(pages.iter().all(|(s, e)| e > s), "no zero-height pages");
    }
    #[test]
    fn page_breaks_ignore_out_of_range_and_nan_forced_breaks() {
        // A NaN forced break is filtered by the range check (NaN fails both
        // comparisons); the sort itself is total_cmp since the page_breaks
        // extraction, so even an unfiltered NaN could no longer panic.
        let mut dl = list_of(vec![DisplayListItem::Rect {
            bounds: rect(0.0, 0.0, 10.0, 250.0).into(),
            color: opaque(),
            border_radius: BorderRadius::default(),
        }]);
        dl.forced_page_breaks = [f32::NAN, -10.0, 0.0, 250.0, 9999.0, f32::INFINITY]
            .into_iter()
            .map(|y| ForcedBreak { y, causing_node: None })
            .collect();
        let pages = calculate_page_break_positions(&dl, 100.0, 100.0);
        // Only the regular interval breaks survive.
        assert_eq!(pages, vec![(0.0, 100.0), (100.0, 200.0), (200.0, 250.0)]);
    }
    #[test]
    fn page_breaks_with_a_nan_first_page_height_still_yields_one_page() {
        let dl = list_of(vec![DisplayListItem::Rect {
            bounds: rect(0.0, 0.0, 10.0, 250.0).into(),
            color: opaque(),
            border_radius: BorderRadius::default(),
        }]);
        // `while NaN < total` is immediately false, so no regular breaks are produced
        // and the whole document lands on one page. Defined, and NOT a hang.
        let pages = calculate_page_break_positions(&dl, f32::NAN, 100.0);
        assert_eq!(pages, vec![(0.0, 250.0)]);
    }
    // ---------------------------------------------------------------------
    // paginate_display_list_with_slicer_and_breaks (public entry point)
    // ---------------------------------------------------------------------
    #[test]
    fn paginate_with_a_degenerate_page_height_returns_the_list_unsliced() {
        let rr = RendererResources::default();
        let dl = list_of(vec![DisplayListItem::Rect {
            bounds: rect(0.0, 0.0, 10.0, 250.0).into(),
            color: opaque(),
            border_radius: BorderRadius::default(),
        }]);
        for h in [0.0, -1.0, f32::MAX, f32::INFINITY] {
            let pages = paginate_display_list_with_slicer_and_breaks(dl.clone(), &SlicerConfig::simple(h), &rr)
                .expect("degenerate page height must not error");
            assert_eq!(pages.len(), 1, "page height {h} => no slicing");
            assert_eq!(pages[0].items.len(), 1);
        }
    }
    #[test]
    fn paginate_single_page_matches_the_full_pagination_page_for_page() {
        let rr = RendererResources::default();
        let mut dl = list_of(vec![DisplayListItem::Rect {
            bounds: rect(0.0, 0.0, 10.0, 250.0).into(),
            color: opaque(),
            border_radius: BorderRadius::default(),
        }]);
        dl.forced_page_breaks = vec![ForcedBreak { y: 50.0, causing_node: None }];
        let cfg = SlicerConfig::simple(100.0);
        let constraints = page_breaks::PageConstraints::from_slicer_config(&cfg);
        let breaks = page_breaks::compute_page_breaks_from_display_list(&dl, &constraints);
        let all = paginate_display_list_with_breaks(dl.clone(), &cfg, &breaks, &rr)
            .expect("full pagination");
        assert_eq!(all.len(), 4);
        for (idx, expected_page) in all.iter().enumerate() {
            let single = paginate_single_page(dl.clone(), &cfg, &breaks, &rr, idx)
                .expect("single page");
            assert_eq!(
                single.items.len(),
                expected_page.items.len(),
                "page {idx}: lazy materialization must produce the same page"
            );
            let bounds = |p: &DisplayList| {
                p.items.iter().find_map(DisplayListItem::bounds)
            };
            assert_eq!(bounds(&single), bounds(expected_page), "page {idx} geometry");
        }
        // Out of range: empty, not a panic and not page 0.
        let oob = paginate_single_page(dl, &cfg, &breaks, &rr, 99).expect("oob");
        assert!(oob.items.is_empty());
    }
    #[test]
    fn paginate_slices_content_into_pages() {
        let rr = RendererResources::default();
        let dl = list_of(vec![DisplayListItem::Rect {
            bounds: rect(0.0, 0.0, 10.0, 250.0).into(),
            color: opaque(),
            border_radius: BorderRadius::default(),
        }]);
        let pages = paginate_display_list_with_slicer_and_breaks(dl, &SlicerConfig::simple(100.0), &rr)
            .expect("pagination succeeds");
        assert_eq!(pages.len(), 3);
        // The tall rect is clipped onto every page it crosses, always rebased to y=0.
        for page in &pages {
            let r = page.items.iter().find_map(DisplayListItem::bounds).expect("each page keeps a slice");
            assert_eq!(r.origin.y, 0.0);
            assert!(r.size.height > 0.0 && r.size.height <= 100.0);
        }
    }
    // ---------------------------------------------------------------------
    // DisplayList::patch_text_glyphs
    // ---------------------------------------------------------------------
    #[test]
    fn patch_text_glyphs_replaces_matching_runs_and_returns_their_damage() {
        let clip = rect(10.0, 20.0, 100.0, 30.0);
        let mut dl = list_of(vec![text_item(Some(7), clip, vec![glyph(1, 0.0, 0.0)])]);
        let damage = dl.patch_text_glyphs(7, &[vec![glyph(42, 5.0, 6.0), glyph(43, 13.0, 6.0)]]);
        assert_eq!(damage, Some(clip), "damage covers the run's clip rect");
        match &dl.items[0] {
            DisplayListItem::Text { glyphs, .. } => {
                assert_eq!(glyphs.iter().map(|g| g.index).collect::<Vec<_>>(), vec![42, 43]);
            }
            other => panic!("expected Text, got {other:?}"),
        }
    }
    #[test]
    fn patch_text_glyphs_unions_damage_across_runs() {
        let a = rect(0.0, 0.0, 50.0, 10.0);
        let b = rect(100.0, 200.0, 50.0, 10.0);
        let mut dl = list_of(vec![
            text_item(Some(3), a, vec![glyph(1, 0.0, 0.0)]),
            text_item(Some(3), b, vec![glyph(2, 0.0, 0.0)]),
        ]);
        let damage = dl.patch_text_glyphs(3, &[vec![glyph(9, 0.0, 0.0)], vec![glyph(10, 0.0, 0.0)]])
            .expect("both runs matched");
        // The union spans from a's top-left to b's bottom-right.
        assert_eq!(damage, rect(0.0, 0.0, 150.0, 210.0));
    }
    #[test]
    fn patch_text_glyphs_returns_none_when_nothing_matches() {
        let clip = rect(0.0, 0.0, 10.0, 10.0);
        let mut dl = list_of(vec![
            text_item(Some(7), clip, vec![glyph(1, 0.0, 0.0)]),
            text_item(None, clip, vec![glyph(2, 0.0, 0.0)]), // no source node => never patched
            DisplayListItem::PopClip,
        ]);
        assert_eq!(dl.patch_text_glyphs(8, &[vec![glyph(9, 0.0, 0.0)]]), None, "wrong node index");
        assert_eq!(dl.patch_text_glyphs(usize::MAX, &[vec![glyph(9, 0.0, 0.0)]]), None);
        assert_eq!(dl.patch_text_glyphs(7, &[]), None, "no replacement runs => nothing to do");
        // Nothing was mutated by any of the failed patches.
        match &dl.items[0] {
            DisplayListItem::Text { glyphs, .. } => assert_eq!(glyphs[0].index, 1),
            other => panic!("expected Text, got {other:?}"),
        }
    }
    #[test]
    fn patch_text_glyphs_stops_when_it_runs_out_of_replacement_runs() {
        let clip = rect(0.0, 0.0, 10.0, 10.0);
        let mut dl = list_of(vec![
            text_item(Some(1), clip, vec![glyph(100, 0.0, 0.0)]),
            text_item(Some(1), clip, vec![glyph(200, 0.0, 0.0)]),
            text_item(Some(1), clip, vec![glyph(300, 0.0, 0.0)]),
        ]);
        // Only one replacement run for three matching items: patch the first, leave the rest.
        assert!(dl.patch_text_glyphs(1, &[vec![glyph(999, 0.0, 0.0)]]).is_some());
        let ids: Vec<u32> = dl.items.iter().map(|i| match i {
            DisplayListItem::Text { glyphs, .. } => glyphs[0].index,
            other => panic!("expected Text, got {other:?}"),
        }).collect();
        assert_eq!(ids, vec![999, 200, 300]);
    }
    #[test]
    fn patch_text_glyphs_accepts_an_empty_replacement_run() {
        let clip = rect(0.0, 0.0, 10.0, 10.0);
        let mut dl = list_of(vec![text_item(Some(0), clip, vec![glyph(1, 0.0, 0.0)])]);
        // An empty run erases the glyphs but still reports damage over the old area.
        assert_eq!(dl.patch_text_glyphs(0, &[Vec::new()]), Some(clip));
        match &dl.items[0] {
            DisplayListItem::Text { glyphs, .. } => assert!(glyphs.is_empty()),
            other => panic!("expected Text, got {other:?}"),
        }
    }
    // ---------------------------------------------------------------------
    // DisplayList::compute_text_damage_rect (text_layout)
    // ---------------------------------------------------------------------
    #[cfg(feature = "text_layout")]
    #[test]
    fn compute_text_damage_rect_empty_inputs_yield_the_zero_rect() {
        let r = DisplayList::compute_text_damage_rect(&[], &[], LogicalPosition::new(100.0, 200.0), 0);
        assert_eq!(r, LogicalRect::zero(), "no items => no damage (not a MAX..MIN garbage rect)");
    }
    #[cfg(feature = "text_layout")]
    #[test]
    fn compute_text_damage_rect_translates_by_the_container_origin() {
        let old = vec![positioned(0, 10.0, 20.0, 30.0, 40.0)];
        let r = DisplayList::compute_text_damage_rect(&old, &[], LogicalPosition::new(100.0, 200.0), 0);
        assert_eq!(r, rect(110.0, 220.0, 30.0, 40.0));
    }
    #[cfg(feature = "text_layout")]
    #[test]
    fn compute_text_damage_rect_unions_old_and_new() {
        let old = vec![positioned(0, 0.0, 0.0, 10.0, 10.0)];
        let new = vec![positioned(0, 90.0, 190.0, 10.0, 10.0)];
        let r = DisplayList::compute_text_damage_rect(&old, &new, LogicalPosition::zero(), 0);
        assert_eq!(r, rect(0.0, 0.0, 100.0, 200.0), "damage must cover both the before and after ink");
    }
    #[cfg(feature = "text_layout")]
    #[test]
    fn compute_text_damage_rect_skips_lines_before_the_affected_line() {
        let items = vec![
            positioned(0, 0.0, 0.0, 1000.0, 10.0),  // line 0 — untouched, must be excluded
            positioned(5, 10.0, 50.0, 20.0, 10.0),  // line 5 — the reflowed line
        ];
        let r = DisplayList::compute_text_damage_rect(&items, &items, LogicalPosition::zero(), 5);
        assert_eq!(r, rect(10.0, 50.0, 20.0, 10.0));
        // An affected_line past every line damages nothing.
        let none = DisplayList::compute_text_damage_rect(&items, &items, LogicalPosition::zero(), usize::MAX);
        assert_eq!(none, LogicalRect::zero());
    }
    #[cfg(feature = "text_layout")]
    #[test]
    fn compute_text_damage_rect_nan_positions_are_ignored_not_propagated() {
        // f32::min/max drop NaN operands, so a NaN-positioned item contributes nothing.
        let nan_only = vec![positioned(0, f32::NAN, f32::NAN, 10.0, 10.0)];
        let r = DisplayList::compute_text_damage_rect(&nan_only, &[], LogicalPosition::zero(), 0);
        assert_eq!(r, LogicalRect::zero(), "an all-NaN run must not produce a NaN damage rect");
        // Mixed: min/max are applied PER AXIS, so the half-NaN item is not dropped
        // wholesale -- its NaN x contributes nothing, but its finite y=0.0 still
        // widens the union. The result is a safe non-NaN superset: y spans [0,15].
        let mixed = vec![positioned(0, f32::NAN, 0.0, 10.0, 10.0), positioned(0, 5.0, 5.0, 10.0, 10.0)];
        let r = DisplayList::compute_text_damage_rect(&mixed, &[], LogicalPosition::zero(), 0);
        assert!(!r.origin.x.is_nan() && !r.size.width.is_nan());
        assert_eq!(r, rect(5.0, 0.0, 10.0, 15.0));
    }
    // ---------------------------------------------------------------------
    // item_center_on_page / transform_items_to_page_coords (text_layout)
    // ---------------------------------------------------------------------
    #[cfg(feature = "text_layout")]
    #[test]
    fn item_center_on_page_uses_the_item_midpoint_half_open() {
        let item = positioned(0, 0.0, 0.0, 10.0, 20.0); // height 20 => center at +10
        // layout_origin_y 90 => absolute y 90, center 100 == page_top => on page.
        assert!(item_center_on_page(&item, 90.0, 100.0, 200.0));
        // center 99.99 => just above the page.
        assert!(!item_center_on_page(&item, 89.99, 100.0, 200.0));
        // center exactly page_bottom => OFF page (half-open interval).
        assert!(!item_center_on_page(&item, 190.0, 100.0, 200.0));
        assert!(item_center_on_page(&item, 189.0, 100.0, 200.0));
    }
    #[cfg(feature = "text_layout")]
    #[test]
    fn item_center_on_page_with_nonfinite_values_is_false_not_a_panic() {
        let item = positioned(0, 0.0, f32::NAN, 10.0, 20.0);
        assert!(!item_center_on_page(&item, 0.0, 100.0, 200.0));
        let inf = positioned(0, 0.0, f32::INFINITY, 10.0, 20.0);
        assert!(!item_center_on_page(&inf, 0.0, 100.0, 200.0));
        let ok = positioned(0, 0.0, 150.0, 10.0, 20.0);
        assert!(!item_center_on_page(&ok, 0.0, f32::NAN, f32::NAN));
    }
    #[cfg(feature = "text_layout")]
    #[test]
    fn transform_items_to_page_coords_rebases_and_reports_extents() {
        let items = vec![
            positioned(0, 0.0, 10.0, 30.0, 20.0),
            positioned(1, 5.0, 40.0, 50.0, 20.0),
        ];
        // layout starts at y=100; the page starts at y=100, so new_origin_y = 0.
        let (out, min_y, max_y, max_width) = transform_items_to_page_coords(items, 100.0, 100.0, 0.0);
        assert_eq!(out.len(), 2);
        assert_eq!(out[0].position.y, 10.0);
        assert_eq!(out[1].position.y, 40.0);
        assert_eq!(out[0].position.x, 0.0, "X is never rebased");
        assert_eq!(min_y, 10.0);
        assert_eq!(max_y, 60.0);
        assert_eq!(max_width, 55.0, "max_width is position.x + item width");
    }
    #[cfg(feature = "text_layout")]
    #[test]
    fn transform_items_to_page_coords_on_an_empty_input_returns_sentinel_extents() {
        let (out, min_y, max_y, max_width) = transform_items_to_page_coords(Vec::new(), 0.0, 0.0, 0.0);
        assert!(out.is_empty());
        // The seeds are returned untouched — callers MUST NOT treat these as real bounds.
        assert_eq!(min_y, f32::MAX);
        assert_eq!(max_y, f32::MIN);
        assert_eq!(max_width, 0.0);
    }
    // ---------------------------------------------------------------------
    // DisplayList::to_debug_json
    // ---------------------------------------------------------------------
    #[test]
    fn to_debug_json_on_an_empty_list_reports_a_balanced_zero_item_list() {
        let json = DisplayList::default().to_debug_json();
        assert!(json.contains("\"total_items\": 0"));
        assert!(json.contains("\"balanced\": true"));
        assert!(json.contains("\"final_clip_depth\": 0"));
    }
    #[test]
    fn to_debug_json_flags_an_unbalanced_clip_stack() {
        // A PushClip with no PopClip must be reported as unbalanced.
        let dl = list_of(vec![DisplayListItem::PushClip {
            bounds: rect(0.0, 0.0, 10.0, 10.0).into(),
            border_radius: BorderRadius::default(),
        }]);
        let json = dl.to_debug_json();
        assert!(json.contains("\"final_clip_depth\": 1"));
        assert!(json.contains("\"balanced\": false"));
        // A stray PopClip drives the depth negative — also unbalanced, not a panic.
        let json = list_of(vec![DisplayListItem::PopClip]).to_debug_json();
        assert!(json.contains("\"final_clip_depth\": -1"));
        assert!(json.contains("\"balanced\": false"));
    }
    #[test]
    fn to_debug_json_survives_a_truncated_node_mapping_and_extreme_values() {
        // node_mapping shorter than items must not index out of bounds.
        let dl = DisplayList {
            items: vec![
                DisplayListItem::PushClip {
                    bounds: rect(f32::NAN, f32::INFINITY, f32::MAX, -1.0).into(),
                    border_radius: BorderRadius { top_left: f32::NAN, ..BorderRadius::default() },
                },
                DisplayListItem::PopClip,
                DisplayListItem::PushStackingContext { z_index: i32::MIN, bounds: WindowLogicalRect::zero() },
                DisplayListItem::PopStackingContext,
                DisplayListItem::PushScrollFrame {
                    clip_bounds: WindowLogicalRect::zero(),
                    content_size: LogicalSize::new(f32::MAX, f32::MAX),
                    scroll_id: u64::MAX,
                },
                DisplayListItem::PopScrollFrame,
                DisplayListItem::PopTextShadow, // exercises the `_ =>` fallback arm
            ],
            node_mapping: Vec::new(), // deliberately desynced
            ..DisplayList::default()
        };
        let json = dl.to_debug_json();
        assert!(json.contains("\"total_items\": 7"));
        assert!(json.contains("\"balanced\": true"), "every push is matched by a pop");
    }
    // ---------------------------------------------------------------------
    // apply_text_overflow_ellipsis
    // ---------------------------------------------------------------------
    #[test]
    fn ellipsis_leaves_non_overflowing_text_alone() {
        let container = rect(0.0, 0.0, 100.0, 20.0);
        let glyphs = vec![glyph(1, 0.0, 10.0), glyph(2, 10.0, 10.0)]; // right edge 18 < 100
        let mut dl = list_of(vec![text_item(Some(0), rect(0.0, 0.0, 100.0, 20.0), glyphs.clone())]);
        apply_text_overflow_ellipsis(&mut dl, container, "…");
        match &dl.items[0] {
            DisplayListItem::Text { glyphs: g, .. } => {
                assert_eq!(g.len(), glyphs.len());
                assert_eq!(g.iter().map(|x| x.index).collect::<Vec<_>>(), vec![1, 2]);
            }
            other => panic!("expected Text, got {other:?}"),
        }
    }
    #[test]
    fn ellipsis_truncates_overflowing_text_and_appends_u2026() {
        let container = rect(0.0, 0.0, 50.0, 20.0);
        // Glyphs at x = 0,10,20,30,40,50 each 8 wide => right edges 8,18,28,38,48,58.
        let glyphs: Vec<_> = (0..6).map(|i| glyph(i + 1, (i as f32) * 10.0, 10.0)).collect();
        let mut dl = list_of(vec![text_item(Some(0), rect(0.0, 0.0, 500.0, 20.0), glyphs)]);
        apply_text_overflow_ellipsis(&mut dl, container, "…");
        match &dl.items[0] {
            DisplayListItem::Text { glyphs: g, clip_rect, .. } => {
                // font_size 16 => ellipsis width 9.6 => truncation edge 40.4;
                // glyph right edges 8/18/28/38 fit, 48 does not.
                assert_eq!(g.len(), 5, "4 kept glyphs + 1 ellipsis");
                assert_eq!(g.last().unwrap().index, 0x2026, "U+2026 HORIZONTAL ELLIPSIS");
                assert_eq!(g[3].index, 4, "the last kept glyph");
                // The clip rect is retargeted to the container so nothing spills past it.
                assert_eq!(clip_rect.into_inner(), container);
            }
            other => panic!("expected Text, got {other:?}"),
        }
    }
    #[test]
    fn ellipsis_on_a_container_too_narrow_for_any_glyph_leaves_only_the_ellipsis() {
        // keep_count == 0 => glyphs.truncate(0) => `glyphs.last()` is None. The fallback
        // must anchor the ellipsis to the container origin instead of panicking.
        let container = rect(3.0, 4.0, 1.0, 20.0);
        let glyphs = vec![glyph(1, 0.0, 10.0), glyph(2, 10.0, 10.0)];
        let mut dl = list_of(vec![text_item(Some(0), rect(0.0, 0.0, 500.0, 20.0), glyphs)]);
        apply_text_overflow_ellipsis(&mut dl, container, "…");
        match &dl.items[0] {
            DisplayListItem::Text { glyphs: g, .. } => {
                assert_eq!(g.len(), 1);
                assert_eq!(g[0].index, 0x2026);
                assert_eq!(g[0].point, LogicalPosition::new(3.0, 4.0), "anchored to the container origin");
            }
            other => panic!("expected Text, got {other:?}"),
        }
    }
    #[test]
    fn ellipsis_skips_empty_runs_and_non_text_items() {
        let container = rect(0.0, 0.0, 1.0, 20.0);
        let mut dl = list_of(vec![
            text_item(Some(0), rect(0.0, 0.0, 500.0, 20.0), Vec::new()),
            DisplayListItem::PopClip,
            DisplayListItem::Rect {
                bounds: rect(0.0, 0.0, 900.0, 20.0).into(),
                color: opaque(),
                border_radius: BorderRadius::default(),
            },
        ]);
        apply_text_overflow_ellipsis(&mut dl, container, "…");
        match &dl.items[0] {
            DisplayListItem::Text { glyphs, .. } => assert!(glyphs.is_empty(), "an empty run is left alone"),
            other => panic!("expected Text, got {other:?}"),
        }
        assert_eq!(dl.items.len(), 3, "no items added or removed");
    }
    #[test]
    fn ellipsis_with_a_nan_font_size_does_not_panic() {
        // A NaN ellipsis width makes every `glyph_right > truncation_edge` comparison
        // false, so nothing is truncated — but an ellipsis is still appended.
        let container = rect(0.0, 0.0, 50.0, 20.0);
        let glyphs = vec![glyph(1, 0.0, 10.0), glyph(2, 100.0, 10.0)];
        let mut dl = list_of(vec![DisplayListItem::Text {
            glyphs,
            font_hash: FontHash::invalid(),
            font_size_px: f32::NAN,
            color: opaque(),
            clip_rect: rect(0.0, 0.0, 500.0, 20.0).into(),
            source_node_index: None,
        }]);
        apply_text_overflow_ellipsis(&mut dl, container, "…");
        match &dl.items[0] {
            DisplayListItem::Text { glyphs: g, .. } => {
                assert_eq!(g.len(), 3, "both glyphs kept + ellipsis");
                assert_eq!(g.last().unwrap().index, 0x2026);
                assert!(g.last().unwrap().size.width.is_nan());
            }
            other => panic!("expected Text, got {other:?}"),
        }
    }
    // ---------------------------------------------------------------------
    // resolve_clip_path
    // ---------------------------------------------------------------------
    #[test]
    fn resolve_clip_path_none_is_no_clip() {
        use azul_css::props::layout::shape::ClipPath;
        assert_eq!(resolve_clip_path(&ClipPath::None, rect(0.0, 0.0, 100.0, 100.0)), None);
        assert_eq!(resolve_clip_path(&ClipPath::default(), rect(0.0, 0.0, 100.0, 100.0)), None);
    }
    #[test]
    fn resolve_clip_path_inset_shrinks_from_every_edge() {
        use azul_css::{corety::OptionF32, props::layout::shape::ClipPath, shape::{CssShape, ShapeInset}};
        let path = ClipPath::Shape(CssShape::Inset(ShapeInset {
            inset_top: 10.0,
            inset_right: 20.0,
            inset_bottom: 30.0,
            inset_left: 40.0,
            border_radius: OptionF32::Some(6.0),
        }));
        let (r, radius) = resolve_clip_path(&path, rect(100.0, 200.0, 300.0, 400.0)).expect("inset clips");
        assert_eq!(r, rect(140.0, 210.0, 240.0, 360.0));
        assert_eq!(radius, 6.0);
    }
    #[test]
    fn resolve_clip_path_over_inset_clamps_the_size_to_zero() {
        use azul_css::{corety::OptionF32, props::layout::shape::ClipPath, shape::{CssShape, ShapeInset}};
        let path = ClipPath::Shape(CssShape::Inset(ShapeInset {
            inset_top: 500.0,
            inset_right: 500.0,
            inset_bottom: 500.0,
            inset_left: 500.0,
            border_radius: OptionF32::None,
        }));
        let (r, radius) = resolve_clip_path(&path, rect(0.0, 0.0, 100.0, 100.0)).expect("still returns a rect");
        assert_eq!(r.size, LogicalSize::zero(), "insets larger than the box collapse, they do not go negative");
        assert_eq!(radius, 0.0, "OptionF32::None => radius 0");
    }
    #[test]
    fn resolve_clip_path_circle_and_ellipse_use_their_bounding_box() {
        use azul_css::{props::layout::shape::ClipPath, shape::{CssShape, ShapeCircle, ShapeEllipse, ShapePoint}};
        let circle = ClipPath::Shape(CssShape::Circle(ShapeCircle {
            center: ShapePoint { x: 50.0, y: 50.0 },
            radius: 20.0,
        }));
        let (r, radius) = resolve_clip_path(&circle, rect(100.0, 100.0, 200.0, 200.0)).expect("circle clips");
        assert_eq!(r, rect(130.0, 130.0, 40.0, 40.0), "centre is relative to the node origin");
        assert_eq!(radius, 20.0);
        let ellipse = ClipPath::Shape(CssShape::Ellipse(ShapeEllipse {
            center: ShapePoint { x: 50.0, y: 50.0 },
            radius_x: 10.0,
            radius_y: 30.0,
        }));
        let (r, radius) = resolve_clip_path(&ellipse, rect(0.0, 0.0, 100.0, 100.0)).expect("ellipse clips");
        assert_eq!(r, rect(40.0, 20.0, 20.0, 60.0));
        assert_eq!(radius, 10.0, "the rounding uses the SMALLER of the two radii");
    }
    #[test]
    fn resolve_clip_path_circle_with_a_degenerate_radius_does_not_panic() {
        use azul_css::{props::layout::shape::ClipPath, shape::{CssShape, ShapeCircle, ShapePoint}};
        for radius in [0.0, -10.0, f32::NAN, f32::INFINITY] {
            let path = ClipPath::Shape(CssShape::Circle(ShapeCircle {
                center: ShapePoint { x: 0.0, y: 0.0 },
                radius,
            }));
            // The value is passed through unclamped; the contract here is only that it
            // returns a value deterministically instead of panicking.
            let out = resolve_clip_path(&path, rect(0.0, 0.0, 100.0, 100.0));
            assert!(out.is_some(), "radius {radius} must still resolve");
        }
    }
    #[test]
    fn resolve_clip_path_polygon_bbox_and_empty_polygon() {
        use azul_css::{props::layout::shape::ClipPath, shape::{CssShape, ShapePoint, ShapePolygon}};
        // An empty polygon has no bounding box => no clip.
        let empty = ClipPath::Shape(CssShape::Polygon(ShapePolygon { points: Vec::new().into() }));
        assert_eq!(resolve_clip_path(&empty, rect(0.0, 0.0, 100.0, 100.0)), None);
        // A real polygon collapses to its axis-aligned bounding box.
        let tri = ClipPath::Shape(CssShape::Polygon(ShapePolygon {
            points: vec![
                ShapePoint { x: 10.0, y: 90.0 },
                ShapePoint { x: 50.0, y: 10.0 },
                ShapePoint { x: 90.0, y: 90.0 },
            ].into(),
        }));
        let (r, radius) = resolve_clip_path(&tri, rect(1000.0, 2000.0, 100.0, 100.0)).expect("polygon clips");
        assert_eq!(r, rect(1010.0, 2010.0, 80.0, 80.0));
        assert_eq!(radius, 0.0, "a polygon bbox is never rounded");
    }
    #[test]
    fn resolve_clip_path_polygon_with_nan_points_collapses_instead_of_panicking() {
        use azul_css::{props::layout::shape::ClipPath, shape::{CssShape, ShapePoint, ShapePolygon}};
        let nan_poly = ClipPath::Shape(CssShape::Polygon(ShapePolygon {
            points: vec![ShapePoint { x: f32::NAN, y: f32::NAN }].into(),
        }));
        let (r, _) = resolve_clip_path(&nan_poly, rect(0.0, 0.0, 100.0, 100.0)).expect("still resolves");
        // f32::min/max drop NaN, leaving the ±INFINITY seeds; the `.max(0.0)` on the size
        // keeps the result degenerate-but-finite rather than negative.
        assert_eq!(r.size, LogicalSize::zero());
    }
    #[test]
    fn resolve_clip_path_svg_path_is_unsupported_and_does_not_clip() {
        use azul_css::{props::layout::shape::ClipPath, shape::{CssShape, ShapePath}};
        let path = ClipPath::Shape(CssShape::Path(ShapePath {
            data: String::from("M 0 0 L 10 10 Z").into(),
        }));
        assert_eq!(
            resolve_clip_path(&path, rect(0.0, 0.0, 100.0, 100.0)),
            None,
            "path() clip-paths are not implemented => no clipping (rather than a wrong clip)"
        );
    }
    // ---------------------------------------------------------------------
    // apply_clip_path
    // ---------------------------------------------------------------------
    #[test]
    fn apply_clip_path_wraps_the_tail_of_the_list_and_keeps_the_mapping_in_sync() {
        let mut dl = list_of(vec![
            DisplayListItem::Rect {
                bounds: rect(0.0, 0.0, 10.0, 10.0).into(),
                color: opaque(),
                border_radius: BorderRadius::default(),
            },
            DisplayListItem::Rect {
                bounds: rect(0.0, 0.0, 20.0, 20.0).into(),
                color: opaque(),
                border_radius: BorderRadius::default(),
            },
        ]);
        apply_clip_path(&mut dl, 1, rect(5.0, 5.0, 50.0, 50.0), 8.0);
        assert_eq!(dl.items.len(), 4, "PushClip inserted + PopClip appended");
        assert_eq!(dl.items.len(), dl.node_mapping.len(), "node_mapping must stay parallel to items");
        assert!(matches!(dl.items[1], DisplayListItem::PushClip { .. }));
        assert!(matches!(dl.items[3], DisplayListItem::PopClip));
        assert_eq!(dl.node_mapping[1], None);
        assert_eq!(dl.node_mapping[3], None);
        match &dl.items[1] {
            DisplayListItem::PushClip { bounds, border_radius } => {
                assert_eq!(bounds.into_inner(), rect(5.0, 5.0, 50.0, 50.0));
                // A positive radius is applied uniformly to all four corners.
                assert_eq!(border_radius.top_left, 8.0);
                assert_eq!(border_radius.bottom_right, 8.0);
            }
            other => panic!("expected PushClip, got {other:?}"),
        }
    }
    #[test]
    fn apply_clip_path_with_a_nonpositive_radius_uses_square_corners() {
        for radius in [0.0, -5.0, f32::NAN] {
            let mut dl = list_of(vec![DisplayListItem::PopClip]);
            apply_clip_path(&mut dl, 0, rect(0.0, 0.0, 10.0, 10.0), radius);
            match &dl.items[0] {
                DisplayListItem::PushClip { border_radius, .. } => {
                    assert!(border_radius.is_zero(), "radius {radius} must not round the clip");
                }
                other => panic!("expected PushClip, got {other:?}"),
            }
        }
    }
    #[test]
    fn apply_clip_path_at_the_end_index_appends_rather_than_panicking() {
        let mut dl = list_of(vec![DisplayListItem::PopClip]);
        // start_index == items.len() is the boundary case and must be accepted.
        apply_clip_path(&mut dl, 1, rect(0.0, 0.0, 10.0, 10.0), 0.0);
        assert_eq!(dl.items.len(), 3);
        assert_eq!(dl.items.len(), dl.node_mapping.len());
        assert!(matches!(dl.items[1], DisplayListItem::PushClip { .. }));
        assert!(matches!(dl.items[2], DisplayListItem::PopClip));
    }
    #[test]
    #[should_panic(expected = "insertion index")]
    fn apply_clip_path_past_the_end_panics_on_the_vec_insert() {
        // BUG: `start_index` is not validated against `items.len()`, so an out-of-range
        // index panics inside `Vec::insert` instead of being rejected. Pinned so the
        // panic is a deliberate, visible contract rather than a latent crash.
        let mut dl = DisplayList::default();
        apply_clip_path(&mut dl, 3, rect(0.0, 0.0, 10.0, 10.0), 0.0);
    }
    // ======================================================================
    // E18: repeated theads for MULTIPLE straddling tables (side-by-side)
    // ======================================================================
    fn plain_rect(x: f32, y: f32, w: f32, h: f32) -> DisplayListItem {
        DisplayListItem::Rect {
            bounds: rect(x, y, w, h).into(),
            color: ColorU { r: 0, g: 0, b: 0, a: 255 },
            border_radius: BorderRadius::default(),
        }
    }
    fn item_y(item: &DisplayListItem) -> f32 {
        item.visual_bounds().map(|b| b.origin.y).unwrap_or(f32::NAN)
    }
    fn item_x(item: &DisplayListItem) -> f32 {
        item.visual_bounds().map(|b| b.origin.x).unwrap_or(f32::NAN)
    }
    /// Two side-by-side tables (columns) straddling the same page top: each
    /// repeated thead stays in its own x-band AT the page top, each table's
    /// continued rows shift by ITS OWN thead height, and content below both
    /// shifts by the SUM (the break pass reserves the sum per page).
    #[test]
    fn side_by_side_straddling_tables_keep_their_own_thead_offsets() {
        use crate::solver3::pagination::{TableHeaderInfo, TableHeaderTracker};
        // Table A: x 0..280, thead 20 tall. Table B: x 320..600, thead 30.
        // Both span content y 0..350 over 200-tall pages: page 1 (y 200..400)
        // is a continuation page for both.
        let thead_a = vec![plain_rect(0.0, 0.0, 280.0, 20.0)];
        let thead_b = vec![plain_rect(320.0, 0.0, 280.0, 30.0)];
        let mut tracker = TableHeaderTracker::new();
        tracker.register_table_header(TableHeaderInfo {
            table_node_index: 1,
            table_start_y: 0.0,
            table_end_y: 350.0,
            thead_items: thead_a,
            thead_height: 20.0,
            thead_offset_y: 0.0,
        });
        tracker.register_table_header(TableHeaderInfo {
            table_node_index: 2,
            table_start_y: 0.0,
            table_end_y: 350.0,
            thead_items: thead_b,
            thead_height: 30.0,
            thead_offset_y: 0.0,
        });
        // Content: one row of each table living on page 1 (content y
        // 200..240), plus a full-width paragraph BELOW both tables
        // (content y 360..380).
        let full = DisplayList {
            items: vec![
                plain_rect(0.0, 200.0, 280.0, 40.0),   // A's continued row
                plain_rect(320.0, 200.0, 280.0, 40.0), // B's continued row
                plain_rect(0.0, 360.0, 600.0, 20.0),   // below both
            ],
            node_mapping: vec![None, None, None],
            ..DisplayList::default()
        };
        let config = SlicerConfig {
            table_headers: tracker,
            ..SlicerConfig::simple(200.0)
        };
        let pages = paginate_display_list_with_slicer_and_breaks(
            full,
            &config,
            &RendererResources::default(),
        )
        .expect("paginate");
        assert!(pages.len() >= 2, "content spans two pages, got {}", pages.len());
        let page1 = &pages[1];
        // Both theads sit AT the page top, in their own x-bands.
        let thead_a_y = page1
            .items
            .iter()
            .filter(|i| item_x(i) < 300.0 && i.visual_bounds().is_some_and(|b| b.size.height == 20.0))
            .map(item_y)
            .fold(f32::NAN, f32::min);
        let thead_b_y = page1
            .items
            .iter()
            .filter(|i| item_x(i) >= 300.0 && i.visual_bounds().is_some_and(|b| b.size.height == 30.0))
            .map(item_y)
            .fold(f32::NAN, f32::min);
        assert_eq!(thead_a_y, 0.0, "thead A at its column's page top");
        assert_eq!(
            thead_b_y, 0.0,
            "thead B at its OWN column's page top - NOT stacked below A"
        );
        // Each table's continued row shifts by ITS OWN thead height.
        let row_a = page1
            .items
            .iter()
            .find(|i| item_x(i) < 300.0 && i.visual_bounds().is_some_and(|b| b.size.height == 40.0))
            .expect("A's row on page 1");
        let row_b = page1
            .items
            .iter()
            .find(|i| item_x(i) >= 300.0 && i.visual_bounds().is_some_and(|b| b.size.height == 40.0))
            .expect("B's row on page 1");
        assert_eq!(item_y(row_a), 20.0, "A's row below A's 20px thead only");
        assert_eq!(
            item_y(row_b),
            30.0,
            "B's row below B's 30px thead only - the old max() collapse \
             shifted both by 30 and overlapped A"
        );
        // Content below BOTH tables shifts by the SUM (matches the break
        // pass's per-page reservation).
        let below = page1
            .items
            .iter()
            .find(|i| i.visual_bounds().is_some_and(|b| b.size.width == 600.0))
            .expect("below-both paragraph on page 1");
        assert_eq!(item_y(below), 160.0 + 50.0, "sum of both thead heights");
    }
    // ======================================================================
    // E17: clip / stacking structure is re-derived per page slice
    // ======================================================================
    /// A clipped, stacked subtree spanning pages 0-1: BOTH pages re-open the
    /// chain around the content that lands on them, balanced; a page without
    /// any of the subtree's content carries none of its markers.
    #[test]
    fn clip_and_stacking_chain_is_rederived_on_every_page_it_touches() {
        // Subtree: PushStackingContext > PushClip(y 50..350) > rect(60..340),
        // then a plain rect on page 2 (y 450..470) OUTSIDE the subtree.
        let full = DisplayList {
            items: vec![
                DisplayListItem::PushStackingContext {
                    z_index: 3,
                    bounds: rect(0.0, 50.0, 600.0, 300.0).into(),
                },
                DisplayListItem::PushClip {
                    bounds: rect(0.0, 50.0, 600.0, 300.0).into(),
                    border_radius: BorderRadius::default(),
                },
                plain_rect(0.0, 60.0, 600.0, 280.0),
                DisplayListItem::PopClip,
                DisplayListItem::PopStackingContext,
                plain_rect(0.0, 450.0, 600.0, 20.0),
            ],
            node_mapping: vec![None; 6],
            ..DisplayList::default()
        };
        let config = SlicerConfig::simple(200.0);
        let pages = paginate_display_list_with_slicer_and_breaks(
            full,
            &config,
            &RendererResources::default(),
        )
        .expect("paginate");
        assert!(pages.len() >= 3, "3 pages of content, got {}", pages.len());
        let structure = |page: &DisplayList| -> Vec<&'static str> {
            page.items
                .iter()
                .map(|i| match i {
                    DisplayListItem::PushStackingContext { .. } => "push_sc",
                    DisplayListItem::PopStackingContext => "pop_sc",
                    DisplayListItem::PushClip { .. } => "push_clip",
                    DisplayListItem::PopClip => "pop_clip",
                    DisplayListItem::Rect { .. } => "rect",
                    _ => "other",
                })
                .collect()
        };
        // Page 0: chain opens before the rect slice, closes after.
        assert_eq!(
            structure(&pages[0]),
            vec!["push_sc", "push_clip", "rect", "pop_clip", "pop_sc"],
            "page 0: {:?}",
            structure(&pages[0])
        );
        // Page 1: the SAME chain re-opens for the continued slice — this is
        // what the slicer used to drop entirely (unclipped bleed, flattened
        // z-order on continuation pages).
        assert_eq!(
            structure(&pages[1]),
            vec!["push_sc", "push_clip", "rect", "pop_clip", "pop_sc"],
            "page 1: {:?}",
            structure(&pages[1])
        );
        // The re-opened clip is INTERSECTED with page 1's band and rebased:
        // original clip 50..350 ∩ page [200, 400) → page-local 0..150.
        let clip_bounds = pages[1]
            .items
            .iter()
            .find_map(|i| match i {
                DisplayListItem::PushClip { bounds, .. } => Some(bounds.0),
                _ => None,
            })
            .expect("clip on page 1");
        assert_eq!(
            (clip_bounds.origin.y, clip_bounds.size.height),
            (0.0, 150.0),
            "clip clipped to the page band and rebased"
        );
        // Page 2: no subtree content — NO markers, just the plain rect.
        assert_eq!(
            structure(&pages[2]),
            vec!["rect"],
            "a page without the subtree's content carries none of its markers"
        );
        // The stacking context keeps its z-index on every page it touches.
        for page in &pages[..2] {
            let z = page.items.iter().find_map(|i| match i {
                DisplayListItem::PushStackingContext { z_index, .. } => Some(*z_index),
                _ => None,
            });
            assert_eq!(z, Some(3));
        }
    }
    /// One straddling table: everything shifts by exactly its thead height —
    /// the pre-E18 behavior, unchanged.
    #[test]
    fn single_straddling_table_keeps_the_uniform_shift() {
        use crate::solver3::pagination::{TableHeaderInfo, TableHeaderTracker};
        let mut tracker = TableHeaderTracker::new();
        tracker.register_table_header(TableHeaderInfo {
            table_node_index: 1,
            table_start_y: 0.0,
            table_end_y: 350.0,
            thead_items: vec![plain_rect(0.0, 0.0, 600.0, 25.0)],
            thead_height: 25.0,
            thead_offset_y: 0.0,
        });
        let full = DisplayList {
            items: vec![
                plain_rect(0.0, 200.0, 600.0, 40.0), // continued row
                plain_rect(0.0, 360.0, 600.0, 20.0), // below the table
            ],
            node_mapping: vec![None, None],
            ..DisplayList::default()
        };
        let config = SlicerConfig {
            table_headers: tracker,
            ..SlicerConfig::simple(200.0)
        };
        let pages = paginate_display_list_with_slicer_and_breaks(
            full,
            &config,
            &RendererResources::default(),
        )
        .expect("paginate");
        let page1 = &pages[1];
        let row = page1
            .items
            .iter()
            .find(|i| i.visual_bounds().is_some_and(|b| b.size.height == 40.0))
            .expect("row");
        let below = page1
            .items
            .iter()
            .find(|i| i.visual_bounds().is_some_and(|b| b.size.height == 20.0 && b.size.width == 600.0))
            .expect("below");
        assert_eq!(item_y(row), 25.0);
        assert_eq!(item_y(below), 160.0 + 25.0);
    }
}
#[cfg(test)]
mod dense_scroll_extent_tests {
    use super::*;
    use crate::text3::cache::{
        BidiDirection, ClusterFlags, ContentIndex, GraphemeClusterId, LayoutFontMetrics,
        OverflowInfo, PositionedItem, Point, ShapedCluster, ShapedGlyph, ShapedItem,
        StyleProperties, UnifiedLayout,
    };
    use crate::text3::dense::DenseText;
    use alloc::sync::Arc;
    /// One real cluster with real metrics so the dense extent is NON-zero
    /// and the sparse-vs-dense A/B is meaningful (an empty layout passes
    /// the guard vacuously at 0x0 — that is exactly the coverage hole the
    /// d3 NC exposed: no lib/corpus test reached this path with a dense
    /// view, so the verify assert could not fire).
2
    fn one_cluster_layout() -> UnifiedLayout {
2
        let metrics = LayoutFontMetrics {
2
            ascent: 800.0,
2
            descent: -200.0,
2
            cap_height: None,
2
            x_height: None,
2
            line_gap: 0.0,
2
            units_per_em: 1000,
2
        };
2
        let glyph = ShapedGlyph {
2
            kind: text3::cache::GlyphKind::Character,
2
            glyph_id: 7,
2
            cluster_offset: 0,
2
            advance: 10.0,
2
            kerning: 0.0,
2
            offset: Point::default(),
2
            vertical_advance: 0.0,
2
            vertical_offset: Point::default(),
2
            script: text3::script::Script::Latin,
2
            font_hash: 42,
2
            font_metrics: metrics,
2
        };
2
        let cluster = ShapedCluster {
2
            flags: ClusterFlags::classify("a"),
2
            source_text: std::sync::Arc::from("a"),
            source_byte_len: 1,
2
            source_cluster_id: GraphemeClusterId { source_run: 0, start_byte_in_run: 0 },
2
            source_content_index: ContentIndex { run_index: 0, item_index: 0 },
2
            source_node_id: None,
2
            glyphs: smallvec::smallvec![glyph],
            advance: 10.0,
2
            direction: BidiDirection::Ltr,
2
            style: Arc::new(StyleProperties { font_size_px: 16.0, ..StyleProperties::default() }),
2
            marker_position_outside: None,
            is_first_fragment: true,
            is_last_fragment: true,
        };
2
        UnifiedLayout {
2
            items: vec![PositionedItem {
2
                item: ShapedItem::Cluster(cluster),
2
                position: Point { x: 5.0, y: 3.0 },
2
                line_index: 0,
2
            }],
2
            overflow: OverflowInfo::default(),
2
        }
2
    }
    /// (d5) The pagination clipper must treat a TextPayload payload
    /// EXACTLY like a bare UnifiedLayout payload — same clip decision,
    /// same output bounds. This is the only coverage the TextPayload
    /// arm has (it activates under AZ_DENSE_TEXT only, and no verify
    /// run paginates), so its NC is required to go red here.
    #[test]
1
    fn clip_text_layout_item_reads_through_the_text_payload() {
        use crate::solver3::layout_tree::TextPayload;
1
        let layout = Arc::new(one_cluster_layout());
1
        let dense = Arc::new(DenseText::from_unified(&layout));
1
        let bounds = LogicalRect::new(
1
            LogicalPosition::new(0.0, 0.0),
1
            LogicalSize::new(100.0, 40.0),
        );
1
        let bare: Arc<dyn std::any::Any + Send + Sync> = layout.clone();
1
        let wrapped: Arc<dyn std::any::Any + Send + Sync> =
1
            Arc::new(TextPayload { dense, sparse: layout });
1
        let via_bare = clip_text_layout_item(
1
            &bare, bounds, FontHash::from_hash(42), 16.0,
1
            ColorU { r: 0, g: 0, b: 0, a: 255 }, 0.0, 1000.0,
        );
1
        let via_payload = clip_text_layout_item(
1
            &wrapped, bounds, FontHash::from_hash(42), 16.0,
1
            ColorU { r: 0, g: 0, b: 0, a: 255 }, 0.0, 1000.0,
        );
1
        match (via_bare, via_payload) {
1
            (Some(DisplayListItem::TextLayout { bounds: a, .. }),
1
             Some(DisplayListItem::TextLayout { bounds: b, .. })) => {
1
                assert_eq!(a, b, "payload route must produce identical clip bounds");
            }
            other => panic!("both routes must clip to TextLayout items, got {other:?}"),
        }
1
    }
    #[test]
1
    fn scroll_extent_from_dense_matches_the_sparse_walk() {
1
        let layout = one_cluster_layout();
1
        let dense = DenseText::from_unified(&layout);
1
        assert_eq!(dense.clusters.len(), layout.items.len(), "pure-cluster guard");
        // Sparse reference extent.
1
        let mut sx: f32 = 0.0;
1
        let mut sy: f32 = 0.0;
2
        for it in &layout.items {
1
            let b = it.item.bounds();
1
            sx = sx.max(it.position.x + b.width);
1
            sy = sy.max(it.position.y + b.height);
1
        }
1
        assert!(sx > 14.0 && sy > 10.0, "non-trivial extent ({sx}x{sy})");
        // Dense extent — the exact computation get_scroll_content_size uses.
1
        let dx = dense.clusters.iter().map(|c| c.x + c.advance).fold(0.0f32, f32::max);
1
        let dy = dense.lines.iter().map(|l| l.top_y + l.height).fold(0.0f32, f32::max);
1
        assert!((sx - dx).abs() < 0.01, "width: sparse {sx} vs dense {dx}");
1
        assert!((sy - dy).abs() < 0.01, "height: sparse {sy} vs dense {dy}");
1
    }
}