Coverage Report

Created: 2026-09-14 07:15

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibWeb/Painting/StackingContext.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2020-2022, Andreas Kling <kling@serenityos.org>
3
 * Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
4
 *
5
 * SPDX-License-Identifier: BSD-2-Clause
6
 */
7
8
#include <AK/Debug.h>
9
#include <AK/QuickSort.h>
10
#include <AK/StringBuilder.h>
11
#include <LibGfx/AffineTransform.h>
12
#include <LibGfx/Matrix4x4.h>
13
#include <LibGfx/Rect.h>
14
#include <LibWeb/CSS/ComputedValues.h>
15
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
16
#include <LibWeb/Layout/Box.h>
17
#include <LibWeb/Layout/ReplacedBox.h>
18
#include <LibWeb/Layout/Viewport.h>
19
#include <LibWeb/Painting/PaintableBox.h>
20
#include <LibWeb/Painting/SVGPaintable.h>
21
#include <LibWeb/Painting/StackingContext.h>
22
#include <LibWeb/SVG/SVGMaskElement.h>
23
24
namespace Web::Painting {
25
26
static void paint_node(Paintable const& paintable, PaintContext& context, PaintPhase phase)
27
0
{
28
0
    paintable.before_paint(context, phase);
29
0
    paintable.paint(context, phase);
30
0
    paintable.after_paint(context, phase);
31
0
}
32
33
StackingContext::StackingContext(Paintable& paintable, StackingContext* parent, size_t index_in_tree_order)
34
0
    : m_paintable(paintable)
35
0
    , m_parent(parent)
36
0
    , m_index_in_tree_order(index_in_tree_order)
37
0
{
38
0
    VERIFY(m_parent != this);
39
0
    if (m_parent)
40
0
        m_parent->m_children.append(this);
41
0
}
42
43
void StackingContext::sort()
44
0
{
45
0
    quick_sort(m_children, [](auto& a, auto& b) {
46
0
        auto a_z_index = a->paintable().computed_values().z_index().value_or(0);
47
0
        auto b_z_index = b->paintable().computed_values().z_index().value_or(0);
48
0
        if (a_z_index == b_z_index)
49
0
            return a->m_index_in_tree_order < b->m_index_in_tree_order;
50
0
        return a_z_index < b_z_index;
51
0
    });
52
53
0
    for (auto* child : m_children)
54
0
        child->sort();
55
0
}
56
57
void StackingContext::set_last_paint_generation_id(u64 generation_id)
58
0
{
59
0
    if (m_last_paint_generation_id.has_value() && m_last_paint_generation_id.value() >= generation_id) {
60
0
        dbgln("FIXME: Painting commands are recorded twice for stacking context: {}", m_paintable->layout_node().debug_description());
61
0
    }
62
0
    m_last_paint_generation_id = generation_id;
63
0
}
64
65
static PaintPhase to_paint_phase(StackingContext::StackingContextPaintPhase phase)
66
0
{
67
    // There are not a fully correct mapping since some stacking context phases are combined.
68
0
    switch (phase) {
69
0
    case StackingContext::StackingContextPaintPhase::Floats:
70
0
    case StackingContext::StackingContextPaintPhase::BackgroundAndBordersForInlineLevelAndReplaced:
71
0
    case StackingContext::StackingContextPaintPhase::BackgroundAndBorders:
72
0
        return PaintPhase::Background;
73
0
    case StackingContext::StackingContextPaintPhase::Foreground:
74
0
        return PaintPhase::Foreground;
75
0
    case StackingContext::StackingContextPaintPhase::FocusAndOverlay:
76
0
        return PaintPhase::Overlay;
77
0
    default:
78
0
        VERIFY_NOT_REACHED();
79
0
    }
80
0
}
81
82
void StackingContext::paint_node_as_stacking_context(Paintable const& paintable, PaintContext& context)
83
0
{
84
0
    paint_node(paintable, context, PaintPhase::Background);
85
0
    paint_node(paintable, context, PaintPhase::Border);
86
0
    paint_descendants(context, paintable, StackingContextPaintPhase::BackgroundAndBorders);
87
0
    paint_descendants(context, paintable, StackingContextPaintPhase::Floats);
88
0
    paint_descendants(context, paintable, StackingContextPaintPhase::BackgroundAndBordersForInlineLevelAndReplaced);
89
0
    paint_node(paintable, context, PaintPhase::Foreground);
90
0
    paint_descendants(context, paintable, StackingContextPaintPhase::Foreground);
91
0
    paint_node(paintable, context, PaintPhase::Outline);
92
0
    paint_node(paintable, context, PaintPhase::Overlay);
93
0
    paint_descendants(context, paintable, StackingContextPaintPhase::FocusAndOverlay);
94
0
}
95
96
void StackingContext::paint_descendants(PaintContext& context, Paintable const& paintable, StackingContextPaintPhase phase)
97
0
{
98
0
    paintable.before_children_paint(context, to_paint_phase(phase));
99
100
0
    paintable.for_each_child([&context, phase](auto& child) {
101
0
        auto* stacking_context = child.stacking_context();
102
0
        auto const& z_index = child.computed_values().z_index();
103
104
        // NOTE: Grid specification https://www.w3.org/TR/css-grid-2/#z-order says that grid items should be treated
105
        //       the same way as CSS2 defines for inline-blocks:
106
        //       "For each one of these, treat the element as if it created a new stacking context, but any positioned
107
        //       descendants and descendants which actually create a new stacking context should be considered part of
108
        //       the parent stacking context, not this new one."
109
0
        auto should_be_treated_as_stacking_context = child.layout_node().is_grid_item() && !z_index.has_value();
110
0
        if (should_be_treated_as_stacking_context) {
111
            // FIXME: This may not be fully correct with respect to the paint phases.
112
0
            if (phase == StackingContextPaintPhase::Foreground)
113
0
                paint_node_as_stacking_context(child, context);
114
0
            return IterationDecision::Continue;
115
0
        }
116
117
0
        if (stacking_context && z_index.value_or(0) != 0)
118
0
            return IterationDecision::Continue;
119
0
        if (child.is_positioned() && z_index.value_or(0) == 0)
120
0
            return IterationDecision::Continue;
121
122
0
        if (stacking_context) {
123
            // FIXME: This may not be fully correct with respect to the paint phases.
124
0
            if (phase == StackingContextPaintPhase::Foreground) {
125
0
                paint_child(context, *stacking_context);
126
0
            }
127
            // Note: Don't further recurse into descendants as paint_child() will do that.
128
0
            return IterationDecision::Continue;
129
0
        }
130
131
0
        bool child_is_inline_or_replaced = child.is_inline() || is<Layout::ReplacedBox>(child.layout_node());
132
0
        switch (phase) {
133
0
        case StackingContextPaintPhase::BackgroundAndBorders:
134
0
            if (!child_is_inline_or_replaced && !child.is_floating()) {
135
0
                paint_node(child, context, PaintPhase::Background);
136
0
                paint_node(child, context, PaintPhase::Border);
137
0
                paint_descendants(context, child, phase);
138
0
                paint_node(child, context, PaintPhase::TableCollapsedBorder);
139
0
            }
140
0
            break;
141
0
        case StackingContextPaintPhase::Floats:
142
0
            if (child.is_floating()) {
143
0
                paint_node(child, context, PaintPhase::Background);
144
0
                paint_node(child, context, PaintPhase::Border);
145
0
                paint_descendants(context, child, StackingContextPaintPhase::BackgroundAndBorders);
146
0
            }
147
0
            paint_descendants(context, child, phase);
148
0
            break;
149
0
        case StackingContextPaintPhase::BackgroundAndBordersForInlineLevelAndReplaced:
150
0
            if (child_is_inline_or_replaced) {
151
0
                paint_node(child, context, PaintPhase::Background);
152
0
                paint_node(child, context, PaintPhase::Border);
153
0
                paint_node(child, context, PaintPhase::TableCollapsedBorder);
154
0
                paint_descendants(context, child, StackingContextPaintPhase::BackgroundAndBorders);
155
0
            }
156
0
            paint_descendants(context, child, phase);
157
0
            break;
158
0
        case StackingContextPaintPhase::Foreground:
159
0
            paint_node(child, context, PaintPhase::Foreground);
160
0
            paint_descendants(context, child, phase);
161
0
            break;
162
0
        case StackingContextPaintPhase::FocusAndOverlay:
163
0
            paint_node(child, context, PaintPhase::Outline);
164
0
            paint_node(child, context, PaintPhase::Overlay);
165
0
            paint_descendants(context, child, phase);
166
0
            break;
167
0
        }
168
169
0
        return IterationDecision::Continue;
170
0
    });
171
172
0
    paintable.after_children_paint(context, to_paint_phase(phase));
173
0
}
174
175
void StackingContext::paint_child(PaintContext& context, StackingContext const& child)
176
0
{
177
0
    const_cast<StackingContext&>(child).set_last_paint_generation_id(context.paint_generation_id());
178
179
0
    auto parent_paintable = child.paintable().parent();
180
0
    if (parent_paintable)
181
0
        parent_paintable->before_children_paint(context, PaintPhase::Foreground);
182
183
0
    child.paint(context);
184
185
0
    if (parent_paintable)
186
0
        parent_paintable->after_children_paint(context, PaintPhase::Foreground);
187
0
}
188
189
void StackingContext::paint_internal(PaintContext& context) const
190
0
{
191
    // For a more elaborate description of the algorithm, see CSS 2.1 Appendix E
192
    // Draw the background and borders for the context root (steps 1, 2)
193
0
    paint_node(paintable(), context, PaintPhase::Background);
194
0
    paint_node(paintable(), context, PaintPhase::Border);
195
196
    // Stacking contexts formed by positioned descendants with negative z-indices (excluding 0) in z-index order
197
    // (most negative first) then tree order. (step 3)
198
    // NOTE: This doesn't check if a descendant is positioned as modern CSS allows for alternative methods to establish stacking contexts.
199
0
    for (auto* child : m_children) {
200
0
        if (child->paintable().computed_values().z_index().has_value() && child->paintable().computed_values().z_index().value() < 0)
201
0
            paint_child(context, *child);
202
0
    }
203
204
    // Draw the background and borders for block-level children (step 4)
205
0
    paint_descendants(context, paintable(), StackingContextPaintPhase::BackgroundAndBorders);
206
    // Draw the non-positioned floats (step 5)
207
0
    paint_descendants(context, paintable(), StackingContextPaintPhase::Floats);
208
    // Draw inline content, replaced content, etc. (steps 6, 7)
209
0
    paint_descendants(context, paintable(), StackingContextPaintPhase::BackgroundAndBordersForInlineLevelAndReplaced);
210
0
    paint_node(paintable(), context, PaintPhase::Foreground);
211
0
    paint_descendants(context, paintable(), StackingContextPaintPhase::Foreground);
212
213
    // Draw positioned descendants with z-index `0` or `auto` in tree order. (step 8)
214
    // FIXME: There's more to this step that we have yet to understand and implement.
215
0
    for (auto const& paintable : m_positioned_descendants_with_stack_level_0_and_stacking_contexts) {
216
0
        if (!paintable->is_positioned())
217
0
            continue;
218
219
        // At this point, `paintable_box` is a positioned descendant with z-index: auto.
220
        // FIXME: This is basically duplicating logic found elsewhere in this same function. Find a way to make this more elegant.
221
0
        auto* parent_paintable = paintable->parent();
222
0
        if (parent_paintable)
223
0
            parent_paintable->before_children_paint(context, PaintPhase::Foreground);
224
0
        if (auto* child = paintable->stacking_context()) {
225
0
            paint_child(context, *child);
226
0
        } else {
227
0
            paint_node_as_stacking_context(paintable, context);
228
0
        }
229
0
        if (parent_paintable)
230
0
            parent_paintable->after_children_paint(context, PaintPhase::Foreground);
231
0
    };
232
233
    // Stacking contexts formed by positioned descendants with z-indices greater than or equal to 1 in z-index order
234
    // (smallest first) then tree order. (Step 9)
235
    // NOTE: This doesn't check if a descendant is positioned as modern CSS allows for alternative methods to establish stacking contexts.
236
0
    for (auto* child : m_children) {
237
0
        if (child->paintable().computed_values().z_index().has_value() && child->paintable().computed_values().z_index().value() >= 1)
238
0
            paint_child(context, *child);
239
0
    }
240
241
0
    paint_node(paintable(), context, PaintPhase::Outline);
242
243
0
    if (context.should_paint_overlay()) {
244
0
        paint_node(paintable(), context, PaintPhase::Overlay);
245
0
        paint_descendants(context, paintable(), StackingContextPaintPhase::FocusAndOverlay);
246
0
    }
247
0
}
248
249
// FIXME: This extracts the affine 2D part of the full transformation matrix.
250
//  Use the whole matrix when we get better transformation support in LibGfx or use LibGL for drawing the bitmap
251
Gfx::AffineTransform StackingContext::affine_transform_matrix() const
252
0
{
253
0
    if (paintable().is_paintable_box())
254
0
        return Gfx::extract_2d_affine_transform(paintable_box().transform());
255
0
    return Gfx::AffineTransform {};
256
0
}
257
258
static Gfx::FloatMatrix4x4 matrix_with_scaled_translation(Gfx::FloatMatrix4x4 matrix, float scale)
259
0
{
260
0
    auto* m = matrix.elements();
261
0
    m[0][3] *= scale;
262
0
    m[1][3] *= scale;
263
0
    m[2][3] *= scale;
264
0
    return matrix;
265
0
}
266
267
void StackingContext::paint(PaintContext& context) const
268
0
{
269
0
    auto opacity = paintable().computed_values().opacity();
270
0
    if (opacity == 0.0f)
271
0
        return;
272
273
0
    DisplayListRecorderStateSaver saver(context.display_list_recorder());
274
275
0
    auto to_device_pixels_scale = float(context.device_pixels_per_css_pixel());
276
0
    Gfx::IntRect source_paintable_rect;
277
0
    if (paintable().is_paintable_box()) {
278
0
        source_paintable_rect = context.enclosing_device_rect(paintable_box().absolute_paint_rect()).to_type<int>();
279
0
    } else if (paintable().is_inline()) {
280
0
        source_paintable_rect = context.enclosing_device_rect(inline_paintable().bounding_rect()).to_type<int>();
281
0
    } else {
282
0
        VERIFY_NOT_REACHED();
283
0
    }
284
285
0
    auto transform_matrix = Gfx::FloatMatrix4x4::identity();
286
0
    Gfx::FloatPoint transform_origin;
287
0
    if (paintable().is_paintable_box()) {
288
0
        transform_matrix = paintable_box().transform();
289
0
        transform_origin = paintable_box().transform_origin().to_type<float>();
290
0
    }
291
292
0
    DisplayListRecorder::PushStackingContextParams push_stacking_context_params {
293
0
        .opacity = opacity,
294
0
        .is_fixed_position = paintable().is_fixed_position(),
295
0
        .source_paintable_rect = source_paintable_rect,
296
0
        .image_rendering = paintable().computed_values().image_rendering(),
297
0
        .transform = {
298
0
            .origin = transform_origin.scaled(to_device_pixels_scale),
299
0
            .matrix = matrix_with_scaled_translation(transform_matrix, to_device_pixels_scale),
300
0
        },
301
0
    };
302
303
0
    if (paintable().is_paintable_box()) {
304
0
        if (auto masking_area = paintable_box().get_masking_area(); masking_area.has_value()) {
305
0
            if (masking_area->is_empty())
306
0
                return;
307
0
            auto mask_bitmap = paintable_box().calculate_mask(context, *masking_area);
308
0
            if (mask_bitmap) {
309
0
                auto source_paintable_rect = context.enclosing_device_rect(*masking_area).to_type<int>();
310
0
                push_stacking_context_params.source_paintable_rect = source_paintable_rect;
311
0
                push_stacking_context_params.mask = StackingContextMask {
312
0
                    .mask_bitmap = mask_bitmap.release_nonnull(),
313
0
                    .mask_kind = *paintable_box().get_mask_type()
314
0
                };
315
0
            }
316
0
        }
317
0
    }
318
319
0
    context.display_list_recorder().save();
320
0
    if (paintable().is_paintable_box() && paintable_box().scroll_frame_id().has_value())
321
0
        context.display_list_recorder().set_scroll_frame_id(*paintable_box().scroll_frame_id());
322
0
    context.display_list_recorder().push_stacking_context(push_stacking_context_params);
323
0
    paint_internal(context);
324
0
    context.display_list_recorder().pop_stacking_context();
325
0
    context.display_list_recorder().restore();
326
0
}
327
328
TraversalDecision StackingContext::hit_test(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const
329
0
{
330
0
    if (!paintable().is_visible())
331
0
        return TraversalDecision::Continue;
332
333
0
    CSSPixelPoint transform_origin { 0, 0 };
334
0
    if (paintable().is_paintable_box())
335
0
        transform_origin = paintable_box().transform_origin();
336
    // NOTE: This CSSPixels -> Float -> CSSPixels conversion is because we can't AffineTransform::map() a CSSPixelPoint.
337
0
    Gfx::FloatPoint offset_position {
338
0
        (position.x() - transform_origin.x()).to_float(),
339
0
        (position.y() - transform_origin.y()).to_float()
340
0
    };
341
0
    auto transformed_position = affine_transform_matrix().inverse().value_or({}).map(offset_position).to_type<CSSPixels>() + transform_origin;
342
343
0
    if (paintable().is_fixed_position()) {
344
0
        auto scroll_offset = paintable().document().navigable()->viewport_scroll_offset();
345
0
        transformed_position.translate_by(-scroll_offset);
346
0
    }
347
348
    // NOTE: Hit testing basically happens in reverse painting order.
349
    // https://www.w3.org/TR/CSS22/visuren.html#z-index
350
351
    // 7. the child stacking contexts with positive stack levels (least positive first).
352
    // NOTE: Hit testing follows reverse painting order, that's why the conditions here are reversed.
353
0
    for (ssize_t i = m_children.size() - 1; i >= 0; --i) {
354
0
        auto const& child = *m_children[i];
355
0
        if (child.paintable().computed_values().z_index().value_or(0) <= 0)
356
0
            break;
357
0
        if (child.hit_test(transformed_position, type, callback) == TraversalDecision::Break)
358
0
            return TraversalDecision::Break;
359
0
    }
360
361
    // 6. the child stacking contexts with stack level 0 and the positioned descendants with stack level 0.
362
0
    for (auto const& paintable : m_positioned_descendants_with_stack_level_0_and_stacking_contexts.in_reverse()) {
363
0
        if (paintable->stacking_context()) {
364
0
            if (paintable->stacking_context()->hit_test(transformed_position, type, callback) == TraversalDecision::Break)
365
0
                return TraversalDecision::Break;
366
0
        } else {
367
0
            if (paintable->hit_test(transformed_position, type, callback) == TraversalDecision::Break)
368
0
                return TraversalDecision::Break;
369
0
        }
370
0
    }
371
372
    // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
373
0
    if (paintable().layout_node().children_are_inline() && is<Layout::BlockContainer>(paintable().layout_node())) {
374
0
        for (auto const* child = paintable().last_child(); child; child = child->previous_sibling()) {
375
0
            if (child->is_inline() && !child->is_absolutely_positioned() && !child->stacking_context()) {
376
0
                if (child->hit_test(transformed_position, type, callback) == TraversalDecision::Break)
377
0
                    return TraversalDecision::Break;
378
0
            }
379
0
        }
380
0
    }
381
382
    // 4. the non-positioned floats.
383
0
    for (auto const& paintable : m_non_positioned_floating_descendants.in_reverse()) {
384
0
        if (paintable->hit_test(transformed_position, type, callback) == TraversalDecision::Break)
385
0
            return TraversalDecision::Break;
386
0
    }
387
388
    // 3. the in-flow, non-inline-level, non-positioned descendants.
389
0
    if (!paintable().layout_node().children_are_inline()) {
390
0
        for (auto const* child = paintable().last_child(); child; child = child->previous_sibling()) {
391
0
            if (!child->is_paintable_box())
392
0
                continue;
393
394
0
            auto const& paintable_box = verify_cast<PaintableBox>(*child);
395
0
            if (!paintable_box.is_absolutely_positioned() && !paintable_box.is_floating() && !paintable_box.stacking_context()) {
396
0
                if (paintable_box.hit_test(transformed_position, type, callback) == TraversalDecision::Break)
397
0
                    return TraversalDecision::Break;
398
0
            }
399
0
        }
400
0
    }
401
402
    // 2. the child stacking contexts with negative stack levels (most negative first).
403
    // NOTE: Hit testing follows reverse painting order, that's why the conditions here are reversed.
404
0
    for (ssize_t i = m_children.size() - 1; i >= 0; --i) {
405
0
        auto const& child = *m_children[i];
406
0
        if (child.paintable().computed_values().z_index().value_or(0) >= 0)
407
0
            break;
408
0
        if (child.hit_test(transformed_position, type, callback) == TraversalDecision::Break)
409
0
            return TraversalDecision::Break;
410
0
    }
411
412
    // 1. the background and borders of the element forming the stacking context.
413
0
    if (paintable().is_paintable_box()) {
414
0
        if (paintable_box().absolute_border_box_rect().contains(transformed_position.x(), transformed_position.y())) {
415
0
            auto hit_test_result = HitTestResult { .paintable = const_cast<PaintableBox&>(paintable_box()) };
416
0
            if (callback(hit_test_result) == TraversalDecision::Break)
417
0
                return TraversalDecision::Break;
418
0
        }
419
0
    }
420
421
0
    return TraversalDecision::Continue;
422
0
}
423
424
void StackingContext::dump(int indent) const
425
0
{
426
0
    StringBuilder builder;
427
0
    for (int i = 0; i < indent; ++i)
428
0
        builder.append(' ');
429
0
    CSSPixelRect rect;
430
0
    if (paintable().is_paintable_box()) {
431
0
        rect = paintable_box().absolute_rect();
432
0
    } else if (paintable().is_inline_paintable()) {
433
0
        rect = inline_paintable().bounding_rect();
434
0
    } else {
435
0
        VERIFY_NOT_REACHED();
436
0
    }
437
0
    builder.appendff("SC for {} {} [children: {}] (z-index: ", paintable().layout_node().debug_description(), rect, m_children.size());
438
439
0
    if (paintable().computed_values().z_index().has_value())
440
0
        builder.appendff("{}", paintable().computed_values().z_index().value());
441
0
    else
442
0
        builder.append("auto"sv);
443
0
    builder.append(')');
444
445
0
    auto affine_transform = affine_transform_matrix();
446
0
    if (!affine_transform.is_identity()) {
447
0
        builder.appendff(", transform: {}", affine_transform);
448
0
    }
449
0
    dbgln("{}", builder.string_view());
450
0
    for (auto& child : m_children)
451
0
        child->dump(indent + 1);
452
0
}
453
454
}