Coverage Report

Created: 2026-02-14 08:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibWeb/Painting/ViewportPaintable.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2023, Andreas Kling <kling@serenityos.org>
3
 *
4
 * SPDX-License-Identifier: BSD-2-Clause
5
 */
6
7
#include <LibWeb/DOM/Range.h>
8
#include <LibWeb/Layout/Viewport.h>
9
#include <LibWeb/Painting/StackingContext.h>
10
#include <LibWeb/Painting/ViewportPaintable.h>
11
#include <LibWeb/Selection/Selection.h>
12
13
namespace Web::Painting {
14
15
JS_DEFINE_ALLOCATOR(ViewportPaintable);
16
17
JS::NonnullGCPtr<ViewportPaintable> ViewportPaintable::create(Layout::Viewport const& layout_viewport)
18
0
{
19
0
    return layout_viewport.heap().allocate_without_realm<ViewportPaintable>(layout_viewport);
20
0
}
21
22
ViewportPaintable::ViewportPaintable(Layout::Viewport const& layout_viewport)
23
0
    : PaintableWithLines(layout_viewport)
24
0
{
25
0
}
26
27
0
ViewportPaintable::~ViewportPaintable() = default;
28
29
void ViewportPaintable::build_stacking_context_tree_if_needed()
30
0
{
31
0
    if (stacking_context())
32
0
        return;
33
0
    build_stacking_context_tree();
34
0
}
35
36
void ViewportPaintable::build_stacking_context_tree()
37
0
{
38
0
    set_stacking_context(make<StackingContext>(*this, nullptr, 0));
39
40
0
    size_t index_in_tree_order = 1;
41
0
    for_each_in_subtree([&](Paintable const& paintable) {
42
0
        const_cast<Paintable&>(paintable).invalidate_stacking_context();
43
0
        auto* parent_context = const_cast<Paintable&>(paintable).enclosing_stacking_context();
44
0
        auto establishes_stacking_context = paintable.layout_node().establishes_stacking_context();
45
0
        if ((paintable.is_positioned() || establishes_stacking_context) && paintable.computed_values().z_index().value_or(0) == 0)
46
0
            parent_context->m_positioned_descendants_with_stack_level_0_and_stacking_contexts.append(paintable);
47
0
        if (!paintable.is_positioned() && paintable.is_floating())
48
0
            parent_context->m_non_positioned_floating_descendants.append(paintable);
49
0
        if (!establishes_stacking_context) {
50
0
            VERIFY(!paintable.stacking_context());
51
0
            return TraversalDecision::Continue;
52
0
        }
53
0
        VERIFY(parent_context);
54
0
        const_cast<Paintable&>(paintable).set_stacking_context(make<Painting::StackingContext>(const_cast<Paintable&>(paintable), parent_context, index_in_tree_order++));
55
0
        return TraversalDecision::Continue;
56
0
    });
57
58
0
    stacking_context()->sort();
59
0
}
60
61
void ViewportPaintable::paint_all_phases(PaintContext& context)
62
0
{
63
0
    build_stacking_context_tree_if_needed();
64
0
    context.display_list_recorder().translate(-context.device_viewport_rect().location().to_type<int>());
65
0
    stacking_context()->paint(context);
66
0
}
67
68
void ViewportPaintable::assign_scroll_frames()
69
0
{
70
0
    int next_id = 0;
71
0
    for_each_in_subtree_of_type<PaintableBox>([&](auto const& paintable_box) {
72
0
        if (paintable_box.has_scrollable_overflow()) {
73
0
            auto scroll_frame = adopt_ref(*new ScrollFrame());
74
0
            scroll_frame->id = next_id++;
75
0
            scroll_state.set(paintable_box, move(scroll_frame));
76
0
        }
77
0
        return TraversalDecision::Continue;
78
0
    });
79
80
0
    for_each_in_subtree([&](auto const& paintable) {
81
0
        for (auto block = paintable.containing_block(); !block->is_viewport(); block = block->containing_block()) {
82
0
            if (auto scroll_frame = scroll_state.get(block); scroll_frame.has_value()) {
83
0
                if (paintable.is_paintable_box()) {
84
0
                    auto const& paintable_box = static_cast<PaintableBox const&>(paintable);
85
0
                    const_cast<PaintableBox&>(paintable_box).set_enclosing_scroll_frame(scroll_frame.value());
86
0
                } else if (paintable.is_inline_paintable()) {
87
0
                    auto const& inline_paintable = static_cast<InlinePaintable const&>(paintable);
88
0
                    const_cast<InlinePaintable&>(inline_paintable).set_enclosing_scroll_frame(scroll_frame.value());
89
0
                }
90
0
                break;
91
0
            }
92
0
        }
93
0
        return TraversalDecision::Continue;
94
0
    });
95
0
}
96
97
void ViewportPaintable::assign_clip_frames()
98
0
{
99
0
    for_each_in_subtree_of_type<PaintableBox>([&](auto const& paintable_box) {
100
0
        auto overflow_x = paintable_box.computed_values().overflow_x();
101
0
        auto overflow_y = paintable_box.computed_values().overflow_y();
102
0
        auto has_hidden_overflow = overflow_x != CSS::Overflow::Visible && overflow_y != CSS::Overflow::Visible;
103
0
        if (has_hidden_overflow || paintable_box.get_clip_rect().has_value()) {
104
0
            auto clip_frame = adopt_ref(*new ClipFrame());
105
0
            clip_state.set(paintable_box, move(clip_frame));
106
0
        }
107
0
        return TraversalDecision::Continue;
108
0
    });
109
110
0
    for_each_in_subtree([&](auto const& paintable) {
111
0
        for (auto block = paintable.containing_block(); !block->is_viewport(); block = block->containing_block()) {
112
0
            if (auto clip_frame = clip_state.get(block); clip_frame.has_value()) {
113
0
                if (paintable.is_paintable_box()) {
114
0
                    auto const& paintable_box = static_cast<PaintableBox const&>(paintable);
115
0
                    const_cast<PaintableBox&>(paintable_box).set_enclosing_clip_frame(clip_frame.value());
116
0
                } else if (paintable.is_inline_paintable()) {
117
0
                    auto const& inline_paintable = static_cast<InlinePaintable const&>(paintable);
118
0
                    const_cast<InlinePaintable&>(inline_paintable).set_enclosing_clip_frame(clip_frame.value());
119
0
                }
120
0
                break;
121
0
            }
122
0
        }
123
0
        return TraversalDecision::Continue;
124
0
    });
125
0
}
126
127
void ViewportPaintable::refresh_scroll_state()
128
0
{
129
0
    if (!m_needs_to_refresh_scroll_state)
130
0
        return;
131
0
    m_needs_to_refresh_scroll_state = false;
132
133
0
    for (auto& it : scroll_state) {
134
0
        auto const& paintable_box = *it.key;
135
0
        auto& scroll_frame = *it.value;
136
0
        CSSPixelPoint offset;
137
0
        for (auto const* block = &paintable_box.layout_box(); !block->is_viewport(); block = block->containing_block()) {
138
0
            auto const& block_paintable_box = *block->paintable_box();
139
0
            offset.translate_by(block_paintable_box.scroll_offset());
140
0
        }
141
0
        scroll_frame.offset = -offset;
142
0
    }
143
0
}
144
145
void ViewportPaintable::refresh_clip_state()
146
0
{
147
0
    if (!m_needs_to_refresh_clip_state)
148
0
        return;
149
0
    m_needs_to_refresh_clip_state = false;
150
151
0
    for (auto& it : clip_state) {
152
0
        auto const& paintable_box = *it.key;
153
0
        auto& clip_frame = *it.value;
154
0
        auto overflow_x = paintable_box.computed_values().overflow_x();
155
0
        auto overflow_y = paintable_box.computed_values().overflow_y();
156
        // Start from CSS clip property if it exists.
157
0
        Optional<CSSPixelRect> clip_rect = paintable_box.get_clip_rect();
158
159
0
        clip_frame.clear_border_radii_clips();
160
0
        if (overflow_x != CSS::Overflow::Visible && overflow_y != CSS::Overflow::Visible) {
161
0
            auto overflow_clip_rect = paintable_box.compute_absolute_padding_rect_with_css_transform_applied();
162
0
            for (auto const* block = &paintable_box.layout_box(); !block->is_viewport(); block = block->containing_block()) {
163
0
                auto const& block_paintable_box = *block->paintable_box();
164
0
                auto block_overflow_x = block_paintable_box.computed_values().overflow_x();
165
0
                auto block_overflow_y = block_paintable_box.computed_values().overflow_y();
166
0
                if (block_overflow_x != CSS::Overflow::Visible && block_overflow_y != CSS::Overflow::Visible) {
167
0
                    auto rect = block_paintable_box.compute_absolute_padding_rect_with_css_transform_applied();
168
0
                    overflow_clip_rect.intersect(rect);
169
0
                    auto border_radii_data = block_paintable_box.normalized_border_radii_data(ShrinkRadiiForBorders::Yes);
170
0
                    if (border_radii_data.has_any_radius()) {
171
0
                        BorderRadiiClip border_radii_clip { .rect = rect, .radii = border_radii_data };
172
0
                        clip_frame.add_border_radii_clip(border_radii_clip);
173
0
                    }
174
0
                }
175
0
                if (auto css_clip_property_rect = block->paintable_box()->get_clip_rect(); css_clip_property_rect.has_value())
176
0
                    overflow_clip_rect.intersect(css_clip_property_rect.value());
177
0
            }
178
0
            clip_rect = overflow_clip_rect;
179
0
        }
180
181
0
        clip_frame.set_rect(*clip_rect);
182
0
    }
183
0
}
184
185
void ViewportPaintable::resolve_paint_only_properties()
186
0
{
187
    // Resolves layout-dependent properties not handled during layout and stores them in the paint tree.
188
    // Properties resolved include:
189
    // - Border radii
190
    // - Box shadows
191
    // - Text shadows
192
    // - Transforms
193
    // - Transform origins
194
    // - Outlines
195
0
    for_each_in_inclusive_subtree([&](Paintable& paintable) {
196
0
        paintable.resolve_paint_properties();
197
0
        return TraversalDecision::Continue;
198
0
    });
199
0
}
200
201
JS::GCPtr<Selection::Selection> ViewportPaintable::selection() const
202
0
{
203
0
    return const_cast<DOM::Document&>(document()).get_selection();
204
0
}
205
206
void ViewportPaintable::update_selection()
207
0
{
208
    // 1. Start by setting all layout nodes to unselected.
209
0
    for_each_in_inclusive_subtree([&](auto& layout_node) {
210
0
        layout_node.set_selected(false);
211
0
        return TraversalDecision::Continue;
212
0
    });
213
214
    // 2. If there is no active Selection or selected Range, return.
215
0
    auto selection = document().get_selection();
216
0
    if (!selection)
217
0
        return;
218
0
    auto range = selection->range();
219
0
    if (!range)
220
0
        return;
221
222
0
    auto* start_container = range->start_container();
223
0
    auto* end_container = range->end_container();
224
225
    // 3. Mark the nodes included in range selected.
226
0
    for (auto* node = start_container; node && node != end_container->next_in_pre_order(); node = node->next_in_pre_order()) {
227
0
        if (auto* paintable = node->paintable())
228
0
            paintable->set_selected(true);
229
0
    }
230
0
}
231
232
void ViewportPaintable::recompute_selection_states(DOM::Range& range)
233
0
{
234
    // 1. Start by resetting the selection state of all layout nodes to None.
235
0
    for_each_in_inclusive_subtree([&](auto& layout_node) {
236
0
        layout_node.set_selection_state(SelectionState::None);
237
0
        return TraversalDecision::Continue;
238
0
    });
239
240
0
    auto* start_container = range.start_container();
241
0
    auto* end_container = range.end_container();
242
243
    // 2. If the selection starts and ends in the same node:
244
0
    if (start_container == end_container) {
245
        // 1. If the selection starts and ends at the same offset, return.
246
0
        if (range.start_offset() == range.end_offset()) {
247
            // NOTE: A zero-length selection should not be visible.
248
0
            return;
249
0
        }
250
251
        // 2. If it's a text node, mark it as StartAndEnd and return.
252
0
        if (is<DOM::Text>(*start_container)) {
253
0
            if (auto* paintable = start_container->paintable())
254
0
                paintable->set_selection_state(SelectionState::StartAndEnd);
255
0
            return;
256
0
        }
257
0
    }
258
259
    // 3. Mark the selection start node as Start (if text) or Full (if anything else).
260
0
    if (auto* paintable = start_container->paintable()) {
261
0
        if (is<DOM::Text>(*start_container))
262
0
            paintable->set_selection_state(SelectionState::Start);
263
0
        else
264
0
            paintable->set_selection_state(SelectionState::Full);
265
0
    }
266
267
    // 4. Mark the selection end node as End (if text) or Full (if anything else).
268
0
    if (auto* paintable = end_container->paintable()) {
269
0
        if (is<DOM::Text>(*end_container))
270
0
            paintable->set_selection_state(SelectionState::End);
271
0
        else
272
0
            paintable->set_selection_state(SelectionState::Full);
273
0
    }
274
275
    // 5. Mark the nodes between start node and end node (in tree order) as Full.
276
0
    for (auto* node = start_container->next_in_pre_order(); node && node != end_container; node = node->next_in_pre_order()) {
277
0
        if (auto* paintable = node->paintable())
278
0
            paintable->set_selection_state(SelectionState::Full);
279
0
    }
280
0
}
281
282
bool ViewportPaintable::handle_mousewheel(Badge<EventHandler>, CSSPixelPoint, unsigned, unsigned, int, int)
283
0
{
284
0
    return false;
285
0
}
286
287
void ViewportPaintable::visit_edges(Visitor& visitor)
288
0
{
289
0
    Base::visit_edges(visitor);
290
0
    visitor.visit(scroll_state);
291
0
    visitor.visit(clip_state);
292
0
}
293
294
}