/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 | | } |