/src/serenity/Userland/Libraries/LibWeb/Painting/PaintableBox.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2022-2023, Andreas Kling <kling@serenityos.org> |
3 | | * Copyright (c) 2022-2023, Sam Atkins <atkinssj@serenityos.org> |
4 | | * |
5 | | * SPDX-License-Identifier: BSD-2-Clause |
6 | | */ |
7 | | |
8 | | #include <AK/GenericShorthands.h> |
9 | | #include <LibGfx/Font/ScaledFont.h> |
10 | | #include <LibGfx/Painter.h> |
11 | | #include <LibUnicode/CharacterTypes.h> |
12 | | #include <LibWeb/CSS/SystemColor.h> |
13 | | #include <LibWeb/DOM/Document.h> |
14 | | #include <LibWeb/DOM/Range.h> |
15 | | #include <LibWeb/HTML/HTMLHtmlElement.h> |
16 | | #include <LibWeb/HTML/Window.h> |
17 | | #include <LibWeb/Layout/BlockContainer.h> |
18 | | #include <LibWeb/Layout/Viewport.h> |
19 | | #include <LibWeb/Painting/BackgroundPainting.h> |
20 | | #include <LibWeb/Painting/FilterPainting.h> |
21 | | #include <LibWeb/Painting/PaintableBox.h> |
22 | | #include <LibWeb/Painting/SVGPaintable.h> |
23 | | #include <LibWeb/Painting/SVGSVGPaintable.h> |
24 | | #include <LibWeb/Painting/StackingContext.h> |
25 | | #include <LibWeb/Painting/TableBordersPainting.h> |
26 | | #include <LibWeb/Painting/TextPaintable.h> |
27 | | #include <LibWeb/Painting/ViewportPaintable.h> |
28 | | #include <LibWeb/Platform/FontPlugin.h> |
29 | | #include <LibWeb/Selection/Selection.h> |
30 | | |
31 | | namespace Web::Painting { |
32 | | |
33 | | JS::NonnullGCPtr<PaintableWithLines> PaintableWithLines::create(Layout::BlockContainer const& block_container) |
34 | 0 | { |
35 | 0 | return block_container.heap().allocate_without_realm<PaintableWithLines>(block_container); |
36 | 0 | } |
37 | | |
38 | | JS::NonnullGCPtr<PaintableBox> PaintableBox::create(Layout::Box const& layout_box) |
39 | 0 | { |
40 | 0 | return layout_box.heap().allocate_without_realm<PaintableBox>(layout_box); |
41 | 0 | } |
42 | | |
43 | | PaintableBox::PaintableBox(Layout::Box const& layout_box) |
44 | 0 | : Paintable(layout_box) |
45 | 0 | { |
46 | 0 | } |
47 | | |
48 | | PaintableBox::~PaintableBox() |
49 | 0 | { |
50 | 0 | } |
51 | | |
52 | | PaintableWithLines::PaintableWithLines(Layout::BlockContainer const& layout_box) |
53 | 0 | : PaintableBox(layout_box) |
54 | 0 | { |
55 | 0 | } |
56 | | |
57 | | PaintableWithLines::~PaintableWithLines() |
58 | 0 | { |
59 | 0 | } |
60 | | |
61 | | CSSPixelPoint PaintableBox::scroll_offset() const |
62 | 0 | { |
63 | 0 | if (is_viewport()) { |
64 | 0 | auto navigable = document().navigable(); |
65 | 0 | VERIFY(navigable); |
66 | 0 | return navigable->viewport_scroll_offset(); |
67 | 0 | } |
68 | | |
69 | 0 | auto const& node = layout_node(); |
70 | 0 | if (node.is_generated_for_before_pseudo_element()) |
71 | 0 | return node.pseudo_element_generator()->scroll_offset(DOM::Element::ScrollOffsetFor::PseudoBefore); |
72 | 0 | if (node.is_generated_for_after_pseudo_element()) |
73 | 0 | return node.pseudo_element_generator()->scroll_offset(DOM::Element::ScrollOffsetFor::PseudoAfter); |
74 | | |
75 | 0 | if (!(dom_node() && is<DOM::Element>(*dom_node()))) |
76 | 0 | return {}; |
77 | | |
78 | 0 | return static_cast<DOM::Element const*>(dom_node())->scroll_offset(DOM::Element::ScrollOffsetFor::Self); |
79 | 0 | } |
80 | | |
81 | | void PaintableBox::set_scroll_offset(CSSPixelPoint offset) |
82 | 0 | { |
83 | 0 | auto scrollable_overflow_rect = this->scrollable_overflow_rect(); |
84 | 0 | if (!scrollable_overflow_rect.has_value()) |
85 | 0 | return; |
86 | | |
87 | 0 | document().set_needs_to_refresh_clip_state(true); |
88 | 0 | document().set_needs_to_refresh_scroll_state(true); |
89 | |
|
90 | 0 | auto padding_rect = absolute_padding_box_rect(); |
91 | 0 | auto max_x_offset = max(scrollable_overflow_rect->width() - padding_rect.width(), 0); |
92 | 0 | auto max_y_offset = max(scrollable_overflow_rect->height() - padding_rect.height(), 0); |
93 | |
|
94 | 0 | offset.set_x(clamp(offset.x(), 0, max_x_offset)); |
95 | 0 | offset.set_y(clamp(offset.y(), 0, max_y_offset)); |
96 | | |
97 | | // FIXME: If there is horizontal and vertical scroll ignore only part of the new offset |
98 | 0 | if (offset.y() < 0 || scroll_offset() == offset) |
99 | 0 | return; |
100 | | |
101 | 0 | auto& node = layout_node(); |
102 | 0 | if (node.is_generated_for_before_pseudo_element()) { |
103 | 0 | node.pseudo_element_generator()->set_scroll_offset(DOM::Element::ScrollOffsetFor::PseudoBefore, offset); |
104 | 0 | } else if (node.is_generated_for_after_pseudo_element()) { |
105 | 0 | node.pseudo_element_generator()->set_scroll_offset(DOM::Element::ScrollOffsetFor::PseudoAfter, offset); |
106 | 0 | } else if (is<DOM::Element>(*dom_node())) { |
107 | 0 | static_cast<DOM::Element*>(dom_node())->set_scroll_offset(DOM::Element::ScrollOffsetFor::Self, offset); |
108 | 0 | } else { |
109 | 0 | return; |
110 | 0 | } |
111 | | |
112 | | // https://drafts.csswg.org/cssom-view-1/#scrolling-events |
113 | | // Whenever an element gets scrolled (whether in response to user interaction or by an API), |
114 | | // the user agent must run these steps: |
115 | | |
116 | | // 1. Let doc be the element’s node document. |
117 | 0 | auto& document = layout_box().document(); |
118 | | |
119 | | // FIXME: 2. If the element is a snap container, run the steps to update snapchanging targets for the element with |
120 | | // the element’s eventual snap target in the block axis as newBlockTarget and the element’s eventual snap |
121 | | // target in the inline axis as newInlineTarget. |
122 | |
|
123 | 0 | JS::NonnullGCPtr<DOM::EventTarget> const event_target = *dom_node(); |
124 | | |
125 | | // 3. If the element is already in doc’s pending scroll event targets, abort these steps. |
126 | 0 | if (document.pending_scroll_event_targets().contains_slow(event_target)) |
127 | 0 | return; |
128 | | |
129 | | // 4. Append the element to doc’s pending scroll event targets. |
130 | 0 | document.pending_scroll_event_targets().append(*layout_box().dom_node()); |
131 | |
|
132 | 0 | set_needs_display(); |
133 | 0 | } |
134 | | |
135 | | void PaintableBox::scroll_by(int delta_x, int delta_y) |
136 | 0 | { |
137 | 0 | set_scroll_offset(scroll_offset().translated(delta_x, delta_y)); |
138 | 0 | } |
139 | | |
140 | | void PaintableBox::set_offset(CSSPixelPoint offset) |
141 | 0 | { |
142 | 0 | m_offset = offset; |
143 | 0 | } |
144 | | |
145 | | void PaintableBox::set_content_size(CSSPixelSize size) |
146 | 0 | { |
147 | 0 | m_content_size = size; |
148 | 0 | layout_box().did_set_content_size(); |
149 | 0 | } |
150 | | |
151 | | CSSPixelPoint PaintableBox::offset() const |
152 | 0 | { |
153 | 0 | return m_offset; |
154 | 0 | } |
155 | | |
156 | | CSSPixelRect PaintableBox::compute_absolute_rect() const |
157 | 0 | { |
158 | 0 | CSSPixelRect rect { offset(), content_size() }; |
159 | 0 | for (auto const* block = containing_block(); block; block = block->containing_block()) |
160 | 0 | rect.translate_by(block->offset()); |
161 | 0 | return rect; |
162 | 0 | } |
163 | | |
164 | | CSSPixelRect PaintableBox::compute_absolute_padding_rect_with_css_transform_applied() const |
165 | 0 | { |
166 | 0 | auto rect = absolute_rect(); |
167 | 0 | auto scroll_offset = this->enclosing_scroll_frame_offset(); |
168 | 0 | if (scroll_offset.has_value()) |
169 | 0 | rect.translate_by(scroll_offset.value()); |
170 | 0 | rect.translate_by(combined_css_transform().translation().to_type<CSSPixels>()); |
171 | |
|
172 | 0 | CSSPixelRect padding_rect; |
173 | 0 | padding_rect.set_x(rect.x() - box_model().padding.left); |
174 | 0 | padding_rect.set_width(content_width() + box_model().padding.left + box_model().padding.right); |
175 | 0 | padding_rect.set_y(rect.y() - box_model().padding.top); |
176 | 0 | padding_rect.set_height(content_height() + box_model().padding.top + box_model().padding.bottom); |
177 | 0 | return padding_rect; |
178 | 0 | } |
179 | | |
180 | | CSSPixelRect PaintableBox::absolute_rect() const |
181 | 0 | { |
182 | 0 | if (!m_absolute_rect.has_value()) |
183 | 0 | m_absolute_rect = compute_absolute_rect(); |
184 | 0 | return *m_absolute_rect; |
185 | 0 | } |
186 | | |
187 | | CSSPixelRect PaintableBox::compute_absolute_paint_rect() const |
188 | 0 | { |
189 | | // FIXME: This likely incomplete: |
190 | 0 | auto rect = absolute_border_box_rect(); |
191 | 0 | if (has_scrollable_overflow()) { |
192 | 0 | auto scrollable_overflow_rect = this->scrollable_overflow_rect().value(); |
193 | 0 | if (computed_values().overflow_x() == CSS::Overflow::Visible) |
194 | 0 | rect.unite_horizontally(scrollable_overflow_rect); |
195 | 0 | if (computed_values().overflow_y() == CSS::Overflow::Visible) |
196 | 0 | rect.unite_vertically(scrollable_overflow_rect); |
197 | 0 | } |
198 | 0 | for (auto const& shadow : box_shadow_data()) { |
199 | 0 | if (shadow.placement == ShadowPlacement::Inner) |
200 | 0 | continue; |
201 | 0 | auto inflate = shadow.spread_distance + shadow.blur_radius; |
202 | 0 | auto shadow_rect = rect.inflated(inflate, inflate, inflate, inflate).translated(shadow.offset_x, shadow.offset_y); |
203 | 0 | rect = rect.united(shadow_rect); |
204 | 0 | } |
205 | 0 | return rect; |
206 | 0 | } |
207 | | |
208 | | CSSPixelRect PaintableBox::absolute_paint_rect() const |
209 | 0 | { |
210 | 0 | if (!m_absolute_paint_rect.has_value()) |
211 | 0 | m_absolute_paint_rect = compute_absolute_paint_rect(); |
212 | 0 | return *m_absolute_paint_rect; |
213 | 0 | } |
214 | | |
215 | | Optional<CSSPixelRect> PaintableBox::get_clip_rect() const |
216 | 0 | { |
217 | 0 | auto clip = computed_values().clip(); |
218 | 0 | if (clip.is_rect() && layout_box().is_absolutely_positioned()) { |
219 | 0 | auto border_box = absolute_border_box_rect(); |
220 | 0 | return clip.to_rect().resolved(layout_node(), border_box); |
221 | 0 | } |
222 | 0 | return {}; |
223 | 0 | } |
224 | | |
225 | | bool PaintableBox::wants_mouse_events() const |
226 | 0 | { |
227 | 0 | if (scroll_thumb_rect(ScrollDirection::Vertical).has_value()) |
228 | 0 | return true; |
229 | 0 | if (scroll_thumb_rect(ScrollDirection::Horizontal).has_value()) |
230 | 0 | return true; |
231 | 0 | return false; |
232 | 0 | } |
233 | | |
234 | | void PaintableBox::before_paint(PaintContext& context, [[maybe_unused]] PaintPhase phase) const |
235 | 0 | { |
236 | 0 | if (!is_visible()) |
237 | 0 | return; |
238 | | |
239 | 0 | apply_clip_overflow_rect(context, phase); |
240 | 0 | apply_scroll_offset(context, phase); |
241 | 0 | } |
242 | | |
243 | | void PaintableBox::after_paint(PaintContext& context, [[maybe_unused]] PaintPhase phase) const |
244 | 0 | { |
245 | 0 | if (!is_visible()) |
246 | 0 | return; |
247 | | |
248 | 0 | reset_scroll_offset(context, phase); |
249 | 0 | clear_clip_overflow_rect(context, phase); |
250 | 0 | } |
251 | | |
252 | | bool PaintableBox::is_scrollable(ScrollDirection direction) const |
253 | 0 | { |
254 | 0 | auto overflow = direction == ScrollDirection::Horizontal ? computed_values().overflow_x() : computed_values().overflow_y(); |
255 | 0 | auto scrollable_overflow_size = direction == ScrollDirection::Horizontal ? scrollable_overflow_rect()->width() : scrollable_overflow_rect()->height(); |
256 | 0 | auto scrollport_size = direction == ScrollDirection::Horizontal ? absolute_padding_box_rect().width() : absolute_padding_box_rect().height(); |
257 | 0 | if (is_viewport() || overflow == CSS::Overflow::Auto) |
258 | 0 | return scrollable_overflow_size > scrollport_size; |
259 | 0 | return overflow == CSS::Overflow::Scroll; |
260 | 0 | } |
261 | | |
262 | | static constexpr CSSPixels scrollbar_thumb_thickness = 8; |
263 | | |
264 | | Optional<CSSPixelRect> PaintableBox::scroll_thumb_rect(ScrollDirection direction) const |
265 | 0 | { |
266 | 0 | if (!is_scrollable(direction)) |
267 | 0 | return {}; |
268 | | |
269 | 0 | auto padding_rect = absolute_padding_box_rect(); |
270 | 0 | auto scrollable_overflow_rect = this->scrollable_overflow_rect().value(); |
271 | 0 | auto scroll_overflow_size = direction == ScrollDirection::Horizontal ? scrollable_overflow_rect.width() : scrollable_overflow_rect.height(); |
272 | 0 | auto scrollport_size = direction == ScrollDirection::Horizontal ? padding_rect.width() : padding_rect.height(); |
273 | 0 | auto scroll_offset = direction == ScrollDirection::Horizontal ? this->scroll_offset().x() : this->scroll_offset().y(); |
274 | 0 | if (scroll_overflow_size == 0) |
275 | 0 | return {}; |
276 | | |
277 | 0 | auto thumb_size = scrollport_size * (scrollport_size / scroll_overflow_size); |
278 | 0 | CSSPixels thumb_position = 0; |
279 | 0 | if (scroll_overflow_size > scrollport_size) |
280 | 0 | thumb_position = scroll_offset * (scrollport_size - thumb_size) / (scroll_overflow_size - scrollport_size); |
281 | |
|
282 | 0 | CSSPixelRect thumb_rect; |
283 | 0 | if (direction == ScrollDirection::Horizontal) { |
284 | 0 | thumb_rect = { |
285 | 0 | padding_rect.left() + thumb_position, |
286 | 0 | padding_rect.bottom() - scrollbar_thumb_thickness, |
287 | 0 | thumb_size, |
288 | 0 | scrollbar_thumb_thickness |
289 | 0 | }; |
290 | 0 | } else { |
291 | 0 | thumb_rect = { |
292 | 0 | padding_rect.right() - scrollbar_thumb_thickness, |
293 | 0 | padding_rect.top() + thumb_position, |
294 | 0 | scrollbar_thumb_thickness, |
295 | 0 | thumb_size |
296 | 0 | }; |
297 | 0 | } |
298 | |
|
299 | 0 | if (is_viewport()) |
300 | 0 | thumb_rect.translate_by(this->scroll_offset()); |
301 | |
|
302 | 0 | return thumb_rect; |
303 | 0 | } |
304 | | |
305 | | void PaintableBox::paint(PaintContext& context, PaintPhase phase) const |
306 | 0 | { |
307 | 0 | if (!is_visible()) |
308 | 0 | return; |
309 | | |
310 | 0 | if (phase == PaintPhase::Background) { |
311 | 0 | paint_backdrop_filter(context); |
312 | 0 | paint_background(context); |
313 | 0 | paint_box_shadow(context); |
314 | 0 | } |
315 | |
|
316 | 0 | auto const is_table_with_collapsed_borders = display().is_table_inside() && computed_values().border_collapse() == CSS::BorderCollapse::Collapse; |
317 | 0 | if (!display().is_table_cell() && !is_table_with_collapsed_borders && phase == PaintPhase::Border) { |
318 | 0 | paint_border(context); |
319 | 0 | } |
320 | |
|
321 | 0 | if ((display().is_table_inside() || computed_values().border_collapse() == CSS::BorderCollapse::Collapse) && phase == PaintPhase::TableCollapsedBorder) { |
322 | 0 | paint_table_borders(context, *this); |
323 | 0 | } |
324 | |
|
325 | 0 | if (phase == PaintPhase::Outline) { |
326 | 0 | auto const& outline_data = this->outline_data(); |
327 | 0 | if (outline_data.has_value()) { |
328 | 0 | auto outline_offset = this->outline_offset(); |
329 | 0 | auto border_radius_data = normalized_border_radii_data(ShrinkRadiiForBorders::No); |
330 | 0 | auto borders_rect = absolute_border_box_rect(); |
331 | |
|
332 | 0 | auto outline_offset_x = outline_offset; |
333 | 0 | auto outline_offset_y = outline_offset; |
334 | | // "Both the height and the width of the outside of the shape drawn by the outline should not |
335 | | // become smaller than twice the computed value of the outline-width property to make sure |
336 | | // that an outline can be rendered even with large negative values." |
337 | | // https://www.w3.org/TR/css-ui-4/#outline-offset |
338 | | // So, if the horizontal outline offset is > half the borders_rect's width then we set it to that. |
339 | | // (And the same for y) |
340 | 0 | if ((borders_rect.width() / 2) + outline_offset_x < 0) |
341 | 0 | outline_offset_x = -borders_rect.width() / 2; |
342 | 0 | if ((borders_rect.height() / 2) + outline_offset_y < 0) |
343 | 0 | outline_offset_y = -borders_rect.height() / 2; |
344 | |
|
345 | 0 | border_radius_data.inflate(outline_data->top.width + outline_offset_y, outline_data->right.width + outline_offset_x, outline_data->bottom.width + outline_offset_y, outline_data->left.width + outline_offset_x); |
346 | 0 | borders_rect.inflate(outline_data->top.width + outline_offset_y, outline_data->right.width + outline_offset_x, outline_data->bottom.width + outline_offset_y, outline_data->left.width + outline_offset_x); |
347 | |
|
348 | 0 | paint_all_borders(context.display_list_recorder(), context.rounded_device_rect(borders_rect), border_radius_data.as_corners(context), outline_data->to_device_pixels(context)); |
349 | 0 | } |
350 | 0 | } |
351 | |
|
352 | 0 | auto scrollbar_width = computed_values().scrollbar_width(); |
353 | 0 | if (phase == PaintPhase::Overlay && scrollbar_width != CSS::ScrollbarWidth::None) { |
354 | 0 | auto color = Color(Color::NamedColor::DarkGray).with_alpha(128); |
355 | 0 | auto border_color = Color(Color::NamedColor::LightGray).with_alpha(128); |
356 | 0 | auto borders_data = BordersDataDevicePixels { |
357 | 0 | .top = BorderDataDevicePixels { border_color, CSS::LineStyle::Solid, 1 }, |
358 | 0 | .right = BorderDataDevicePixels { border_color, CSS::LineStyle::Solid, 1 }, |
359 | 0 | .bottom = BorderDataDevicePixels { border_color, CSS::LineStyle::Solid, 1 }, |
360 | 0 | .left = BorderDataDevicePixels { border_color, CSS::LineStyle::Solid, 1 }, |
361 | 0 | }; |
362 | 0 | int thumb_corner_radius = static_cast<int>(context.rounded_device_pixels(scrollbar_thumb_thickness / 2)); |
363 | 0 | CornerRadii corner_radii = { |
364 | 0 | .top_left = Gfx::CornerRadius { thumb_corner_radius, thumb_corner_radius }, |
365 | 0 | .top_right = Gfx::CornerRadius { thumb_corner_radius, thumb_corner_radius }, |
366 | 0 | .bottom_right = Gfx::CornerRadius { thumb_corner_radius, thumb_corner_radius }, |
367 | 0 | .bottom_left = Gfx::CornerRadius { thumb_corner_radius, thumb_corner_radius }, |
368 | 0 | }; |
369 | 0 | if (auto thumb_rect = scroll_thumb_rect(ScrollDirection::Horizontal); thumb_rect.has_value()) { |
370 | 0 | auto thumb_device_rect = context.enclosing_device_rect(thumb_rect.value()); |
371 | 0 | paint_all_borders(context.display_list_recorder(), thumb_device_rect, corner_radii, borders_data); |
372 | 0 | context.display_list_recorder().fill_rect_with_rounded_corners(thumb_device_rect.to_type<int>(), color, thumb_corner_radius); |
373 | 0 | } |
374 | 0 | if (auto thumb_rect = scroll_thumb_rect(ScrollDirection::Vertical); thumb_rect.has_value()) { |
375 | 0 | auto thumb_device_rect = context.enclosing_device_rect(thumb_rect.value()); |
376 | 0 | paint_all_borders(context.display_list_recorder(), thumb_device_rect, corner_radii, borders_data); |
377 | 0 | context.display_list_recorder().fill_rect_with_rounded_corners(thumb_device_rect.to_type<int>(), color, thumb_corner_radius); |
378 | 0 | } |
379 | 0 | } |
380 | |
|
381 | 0 | if (phase == PaintPhase::Overlay && layout_box().document().inspected_layout_node() == &layout_box()) { |
382 | 0 | auto content_rect = absolute_rect(); |
383 | |
|
384 | 0 | auto margin_box = box_model().margin_box(); |
385 | 0 | CSSPixelRect margin_rect; |
386 | 0 | margin_rect.set_x(absolute_x() - margin_box.left); |
387 | 0 | margin_rect.set_width(content_width() + margin_box.left + margin_box.right); |
388 | 0 | margin_rect.set_y(absolute_y() - margin_box.top); |
389 | 0 | margin_rect.set_height(content_height() + margin_box.top + margin_box.bottom); |
390 | |
|
391 | 0 | auto border_rect = absolute_border_box_rect(); |
392 | 0 | auto padding_rect = absolute_padding_box_rect(); |
393 | |
|
394 | 0 | auto paint_inspector_rect = [&](CSSPixelRect const& rect, Color color) { |
395 | 0 | auto device_rect = context.enclosing_device_rect(rect).to_type<int>(); |
396 | 0 | context.display_list_recorder().fill_rect(device_rect, Color(color).with_alpha(100)); |
397 | 0 | context.display_list_recorder().draw_rect(device_rect, Color(color)); |
398 | 0 | }; |
399 | |
|
400 | 0 | paint_inspector_rect(margin_rect, Color::Yellow); |
401 | 0 | paint_inspector_rect(padding_rect, Color::Cyan); |
402 | 0 | paint_inspector_rect(border_rect, Color::Green); |
403 | 0 | paint_inspector_rect(content_rect, Color::Magenta); |
404 | |
|
405 | 0 | auto& font = Platform::FontPlugin::the().default_font(); |
406 | |
|
407 | 0 | StringBuilder builder; |
408 | 0 | if (layout_box().dom_node()) |
409 | 0 | builder.append(layout_box().dom_node()->debug_description()); |
410 | 0 | else |
411 | 0 | builder.append(layout_box().debug_description()); |
412 | 0 | builder.appendff(" {}x{} @ {},{}", border_rect.width(), border_rect.height(), border_rect.x(), border_rect.y()); |
413 | 0 | auto size_text = MUST(builder.to_string()); |
414 | 0 | auto size_text_rect = border_rect; |
415 | 0 | size_text_rect.set_y(border_rect.y() + border_rect.height()); |
416 | 0 | size_text_rect.set_top(size_text_rect.top()); |
417 | 0 | size_text_rect.set_width(CSSPixels::nearest_value_for(font.width(size_text)) + 4); |
418 | 0 | size_text_rect.set_height(CSSPixels::nearest_value_for(font.pixel_size()) + 4); |
419 | 0 | auto size_text_device_rect = context.enclosing_device_rect(size_text_rect).to_type<int>(); |
420 | 0 | context.display_list_recorder().fill_rect(size_text_device_rect, context.palette().color(Gfx::ColorRole::Tooltip)); |
421 | 0 | context.display_list_recorder().draw_rect(size_text_device_rect, context.palette().threed_shadow1()); |
422 | 0 | context.display_list_recorder().draw_text(size_text_device_rect, size_text, font.with_size(font.point_size() * static_cast<float>(context.device_pixels_per_css_pixel())), Gfx::TextAlignment::Center, context.palette().color(Gfx::ColorRole::TooltipText)); |
423 | 0 | } |
424 | 0 | } |
425 | | |
426 | | BordersData PaintableBox::remove_element_kind_from_borders_data(PaintableBox::BordersDataWithElementKind borders_data) |
427 | 0 | { |
428 | 0 | return { |
429 | 0 | .top = borders_data.top.border_data, |
430 | 0 | .right = borders_data.right.border_data, |
431 | 0 | .bottom = borders_data.bottom.border_data, |
432 | 0 | .left = borders_data.left.border_data, |
433 | 0 | }; |
434 | 0 | } |
435 | | |
436 | | void PaintableBox::paint_border(PaintContext& context) const |
437 | 0 | { |
438 | 0 | auto borders_data = m_override_borders_data.has_value() ? remove_element_kind_from_borders_data(m_override_borders_data.value()) : BordersData { |
439 | 0 | .top = box_model().border.top == 0 ? CSS::BorderData() : computed_values().border_top(), |
440 | 0 | .right = box_model().border.right == 0 ? CSS::BorderData() : computed_values().border_right(), |
441 | 0 | .bottom = box_model().border.bottom == 0 ? CSS::BorderData() : computed_values().border_bottom(), |
442 | 0 | .left = box_model().border.left == 0 ? CSS::BorderData() : computed_values().border_left(), |
443 | 0 | }; |
444 | 0 | paint_all_borders(context.display_list_recorder(), context.rounded_device_rect(absolute_border_box_rect()), normalized_border_radii_data().as_corners(context), borders_data.to_device_pixels(context)); |
445 | 0 | } |
446 | | |
447 | | void PaintableBox::paint_backdrop_filter(PaintContext& context) const |
448 | 0 | { |
449 | 0 | auto& backdrop_filter = computed_values().backdrop_filter(); |
450 | 0 | if (!backdrop_filter.is_none()) |
451 | 0 | apply_backdrop_filter(context, absolute_border_box_rect(), normalized_border_radii_data(), backdrop_filter); |
452 | 0 | } |
453 | | |
454 | | void PaintableBox::paint_background(PaintContext& context) const |
455 | 0 | { |
456 | | // If the body's background properties were propagated to the root element, do no re-paint the body's background. |
457 | 0 | if (layout_box().is_body() && document().html_element()->should_use_body_background_properties()) |
458 | 0 | return; |
459 | | |
460 | 0 | CSSPixelRect background_rect; |
461 | 0 | Color background_color = computed_values().background_color(); |
462 | 0 | auto* background_layers = &computed_values().background_layers(); |
463 | |
|
464 | 0 | if (layout_box().is_root_element()) { |
465 | | // CSS 2.1 Appendix E.2: If the element is a root element, paint the background over the entire canvas. |
466 | 0 | background_rect = context.css_viewport_rect(); |
467 | | |
468 | | // Section 2.11.2: If the computed value of background-image on the root element is none and its background-color is transparent, |
469 | | // user agents must instead propagate the computed values of the background properties from that element’s first HTML BODY child element. |
470 | 0 | if (document().html_element()->should_use_body_background_properties()) { |
471 | 0 | background_layers = document().background_layers(); |
472 | 0 | background_color = document().background_color(); |
473 | 0 | } |
474 | 0 | } else { |
475 | 0 | background_rect = absolute_padding_box_rect(); |
476 | 0 | } |
477 | | |
478 | | // HACK: If the Box has a border, use the bordered_rect to paint the background. |
479 | | // This way if we have a border-radius there will be no gap between the filling and actual border. |
480 | 0 | if (computed_values().border_top().width != 0 || computed_values().border_right().width != 0 || computed_values().border_bottom().width != 0 || computed_values().border_left().width != 0) |
481 | 0 | background_rect = absolute_border_box_rect(); |
482 | |
|
483 | 0 | Painting::paint_background(context, layout_box(), background_rect, background_color, computed_values().image_rendering(), background_layers, normalized_border_radii_data()); |
484 | 0 | } |
485 | | |
486 | | void PaintableBox::paint_box_shadow(PaintContext& context) const |
487 | 0 | { |
488 | 0 | auto const& resolved_box_shadow_data = box_shadow_data(); |
489 | 0 | if (resolved_box_shadow_data.is_empty()) |
490 | 0 | return; |
491 | 0 | auto borders_data = BordersData { |
492 | 0 | .top = computed_values().border_top(), |
493 | 0 | .right = computed_values().border_right(), |
494 | 0 | .bottom = computed_values().border_bottom(), |
495 | 0 | .left = computed_values().border_left(), |
496 | 0 | }; |
497 | 0 | Painting::paint_box_shadow(context, absolute_border_box_rect(), absolute_padding_box_rect(), |
498 | 0 | borders_data, normalized_border_radii_data(), resolved_box_shadow_data); |
499 | 0 | } |
500 | | |
501 | | BorderRadiiData PaintableBox::normalized_border_radii_data(ShrinkRadiiForBorders shrink) const |
502 | 0 | { |
503 | 0 | auto border_radii_data = this->border_radii_data(); |
504 | 0 | if (shrink == ShrinkRadiiForBorders::Yes) |
505 | 0 | border_radii_data.shrink(computed_values().border_top().width, computed_values().border_right().width, computed_values().border_bottom().width, computed_values().border_left().width); |
506 | 0 | return border_radii_data; |
507 | 0 | } |
508 | | |
509 | | void PaintableBox::apply_scroll_offset(PaintContext& context, PaintPhase) const |
510 | 0 | { |
511 | 0 | if (scroll_frame_id().has_value()) { |
512 | 0 | context.display_list_recorder().save(); |
513 | 0 | context.display_list_recorder().set_scroll_frame_id(scroll_frame_id().value()); |
514 | 0 | } |
515 | 0 | } |
516 | | |
517 | | void PaintableBox::reset_scroll_offset(PaintContext& context, PaintPhase) const |
518 | 0 | { |
519 | 0 | if (scroll_frame_id().has_value()) |
520 | 0 | context.display_list_recorder().restore(); |
521 | 0 | } |
522 | | |
523 | | void PaintableBox::apply_clip_overflow_rect(PaintContext& context, PaintPhase phase) const |
524 | 0 | { |
525 | 0 | if (!AK::first_is_one_of(phase, PaintPhase::Background, PaintPhase::Border, PaintPhase::TableCollapsedBorder, PaintPhase::Foreground, PaintPhase::Outline)) |
526 | 0 | return; |
527 | | |
528 | 0 | if (clip_rect().has_value()) { |
529 | 0 | auto overflow_clip_rect = clip_rect().value(); |
530 | 0 | m_clipping_overflow = true; |
531 | 0 | context.display_list_recorder().save(); |
532 | 0 | context.display_list_recorder().add_clip_rect(context.enclosing_device_rect(overflow_clip_rect).to_type<int>()); |
533 | 0 | auto const& border_radii_clips = this->border_radii_clips(); |
534 | 0 | m_corner_clipper_ids.resize(border_radii_clips.size()); |
535 | 0 | auto const& combined_transform = combined_css_transform(); |
536 | 0 | for (size_t corner_clip_index = 0; corner_clip_index < border_radii_clips.size(); ++corner_clip_index) { |
537 | 0 | auto const& corner_clip = border_radii_clips[corner_clip_index]; |
538 | 0 | auto corners = corner_clip.radii.as_corners(context); |
539 | 0 | if (!corners.has_any_radius()) |
540 | 0 | continue; |
541 | 0 | auto corner_clipper_id = context.allocate_corner_clipper_id(); |
542 | 0 | m_corner_clipper_ids[corner_clip_index] = corner_clipper_id; |
543 | 0 | auto rect = corner_clip.rect.translated(-combined_transform.translation().to_type<CSSPixels>()); |
544 | 0 | context.display_list_recorder().sample_under_corners(corner_clipper_id, corner_clip.radii.as_corners(context), context.rounded_device_rect(rect).to_type<int>(), CornerClip::Outside); |
545 | 0 | } |
546 | 0 | } |
547 | 0 | } |
548 | | |
549 | | void PaintableBox::clear_clip_overflow_rect(PaintContext& context, PaintPhase phase) const |
550 | 0 | { |
551 | 0 | if (!AK::first_is_one_of(phase, PaintPhase::Background, PaintPhase::Border, PaintPhase::TableCollapsedBorder, PaintPhase::Foreground, PaintPhase::Outline)) |
552 | 0 | return; |
553 | | |
554 | 0 | if (m_clipping_overflow) { |
555 | 0 | m_clipping_overflow = false; |
556 | 0 | auto const& border_radii_clips = this->border_radii_clips(); |
557 | 0 | for (int corner_clip_index = border_radii_clips.size() - 1; corner_clip_index >= 0; --corner_clip_index) { |
558 | 0 | auto const& corner_clip = border_radii_clips[corner_clip_index]; |
559 | 0 | auto corners = corner_clip.radii.as_corners(context); |
560 | 0 | if (!corners.has_any_radius()) |
561 | 0 | continue; |
562 | 0 | auto corner_clipper_id = m_corner_clipper_ids[corner_clip_index]; |
563 | 0 | m_corner_clipper_ids[corner_clip_index] = corner_clipper_id; |
564 | 0 | context.display_list_recorder().blit_corner_clipping(corner_clipper_id); |
565 | 0 | } |
566 | 0 | context.display_list_recorder().restore(); |
567 | 0 | } |
568 | 0 | } |
569 | | |
570 | | void paint_cursor_if_needed(PaintContext& context, TextPaintable const& paintable, PaintableFragment const& fragment) |
571 | 0 | { |
572 | 0 | auto const& navigable = *paintable.navigable(); |
573 | 0 | auto const& document = paintable.document(); |
574 | |
|
575 | 0 | if (!navigable.is_focused()) |
576 | 0 | return; |
577 | | |
578 | 0 | if (!document.cursor_blink_state()) |
579 | 0 | return; |
580 | | |
581 | 0 | if (document.cursor_position()->node() != paintable.dom_node()) |
582 | 0 | return; |
583 | | |
584 | | // NOTE: This checks if the cursor is before the start or after the end of the fragment. If it is at the end, after all text, it should still be painted. |
585 | 0 | if (document.cursor_position()->offset() < (unsigned)fragment.start() || document.cursor_position()->offset() > (unsigned)(fragment.start() + fragment.length())) |
586 | 0 | return; |
587 | | |
588 | 0 | if (!fragment.layout_node().dom_node() || !fragment.layout_node().dom_node()->is_editable()) |
589 | 0 | return; |
590 | | |
591 | 0 | auto fragment_rect = fragment.absolute_rect(); |
592 | |
|
593 | 0 | auto text = fragment.string_view(); |
594 | 0 | CSSPixelRect cursor_rect { |
595 | 0 | fragment_rect.x() + CSSPixels::nearest_value_for(paintable.layout_node().first_available_font().width(text.substring_view(0, document.cursor_position()->offset() - fragment.start()))), |
596 | 0 | fragment_rect.top(), |
597 | 0 | 1, |
598 | 0 | fragment_rect.height() |
599 | 0 | }; |
600 | |
|
601 | 0 | auto cursor_device_rect = context.rounded_device_rect(cursor_rect).to_type<int>(); |
602 | |
|
603 | 0 | context.display_list_recorder().draw_rect(cursor_device_rect, paintable.computed_values().color()); |
604 | 0 | } |
605 | | |
606 | | void paint_text_decoration(PaintContext& context, TextPaintable const& paintable, PaintableFragment const& fragment) |
607 | 0 | { |
608 | 0 | auto& painter = context.display_list_recorder(); |
609 | 0 | auto& font = fragment.layout_node().first_available_font(); |
610 | 0 | auto fragment_box = fragment.absolute_rect(); |
611 | 0 | CSSPixels glyph_height = CSSPixels::nearest_value_for(font.pixel_size()); |
612 | 0 | auto baseline = fragment.baseline(); |
613 | |
|
614 | 0 | auto line_color = paintable.computed_values().text_decoration_color(); |
615 | 0 | auto const& text_paintable = static_cast<TextPaintable const&>(fragment.paintable()); |
616 | 0 | auto device_line_thickness = context.rounded_device_pixels(text_paintable.text_decoration_thickness()); |
617 | |
|
618 | 0 | auto text_decoration_lines = paintable.computed_values().text_decoration_line(); |
619 | 0 | for (auto line : text_decoration_lines) { |
620 | 0 | DevicePixelPoint line_start_point {}; |
621 | 0 | DevicePixelPoint line_end_point {}; |
622 | |
|
623 | 0 | switch (line) { |
624 | 0 | case CSS::TextDecorationLine::None: |
625 | 0 | return; |
626 | 0 | case CSS::TextDecorationLine::Underline: |
627 | 0 | line_start_point = context.rounded_device_point(fragment_box.top_left().translated(0, baseline + 2)); |
628 | 0 | line_end_point = context.rounded_device_point(fragment_box.top_right().translated(-1, baseline + 2)); |
629 | 0 | break; |
630 | 0 | case CSS::TextDecorationLine::Overline: |
631 | 0 | line_start_point = context.rounded_device_point(fragment_box.top_left().translated(0, baseline - glyph_height)); |
632 | 0 | line_end_point = context.rounded_device_point(fragment_box.top_right().translated(-1, baseline - glyph_height)); |
633 | 0 | break; |
634 | 0 | case CSS::TextDecorationLine::LineThrough: { |
635 | 0 | auto x_height = font.x_height(); |
636 | 0 | line_start_point = context.rounded_device_point(fragment_box.top_left().translated(0, baseline - x_height * CSSPixels(0.5f))); |
637 | 0 | line_end_point = context.rounded_device_point(fragment_box.top_right().translated(-1, baseline - x_height * CSSPixels(0.5f))); |
638 | 0 | break; |
639 | 0 | } |
640 | 0 | case CSS::TextDecorationLine::Blink: |
641 | | // Conforming user agents may simply not blink the text |
642 | 0 | return; |
643 | 0 | } |
644 | | |
645 | 0 | switch (paintable.computed_values().text_decoration_style()) { |
646 | 0 | case CSS::TextDecorationStyle::Solid: |
647 | 0 | painter.draw_line(line_start_point.to_type<int>(), line_end_point.to_type<int>(), line_color, device_line_thickness.value(), Gfx::LineStyle::Solid); |
648 | 0 | break; |
649 | 0 | case CSS::TextDecorationStyle::Double: |
650 | 0 | switch (line) { |
651 | 0 | case CSS::TextDecorationLine::Underline: |
652 | 0 | break; |
653 | 0 | case CSS::TextDecorationLine::Overline: |
654 | 0 | line_start_point.translate_by(0, -device_line_thickness - context.rounded_device_pixels(1)); |
655 | 0 | line_end_point.translate_by(0, -device_line_thickness - context.rounded_device_pixels(1)); |
656 | 0 | break; |
657 | 0 | case CSS::TextDecorationLine::LineThrough: |
658 | 0 | line_start_point.translate_by(0, -device_line_thickness / 2); |
659 | 0 | line_end_point.translate_by(0, -device_line_thickness / 2); |
660 | 0 | break; |
661 | 0 | default: |
662 | 0 | VERIFY_NOT_REACHED(); |
663 | 0 | } |
664 | | |
665 | 0 | painter.draw_line(line_start_point.to_type<int>(), line_end_point.to_type<int>(), line_color, device_line_thickness.value()); |
666 | 0 | painter.draw_line(line_start_point.translated(0, device_line_thickness + 1).to_type<int>(), line_end_point.translated(0, device_line_thickness + 1).to_type<int>(), line_color, device_line_thickness.value()); |
667 | 0 | break; |
668 | 0 | case CSS::TextDecorationStyle::Dashed: |
669 | 0 | painter.draw_line(line_start_point.to_type<int>(), line_end_point.to_type<int>(), line_color, device_line_thickness.value(), Gfx::LineStyle::Dashed); |
670 | 0 | break; |
671 | 0 | case CSS::TextDecorationStyle::Dotted: |
672 | 0 | painter.draw_line(line_start_point.to_type<int>(), line_end_point.to_type<int>(), line_color, device_line_thickness.value(), Gfx::LineStyle::Dotted); |
673 | 0 | break; |
674 | 0 | case CSS::TextDecorationStyle::Wavy: |
675 | 0 | painter.draw_triangle_wave(line_start_point.to_type<int>(), line_end_point.to_type<int>(), line_color, device_line_thickness.value() + 1, device_line_thickness.value()); |
676 | 0 | break; |
677 | 0 | } |
678 | 0 | } |
679 | 0 | } |
680 | | |
681 | | void paint_text_fragment(PaintContext& context, TextPaintable const& paintable, PaintableFragment const& fragment, PaintPhase phase) |
682 | 0 | { |
683 | 0 | if (!paintable.is_visible()) |
684 | 0 | return; |
685 | | |
686 | 0 | auto& painter = context.display_list_recorder(); |
687 | |
|
688 | 0 | if (phase == PaintPhase::Foreground) { |
689 | 0 | auto fragment_absolute_rect = fragment.absolute_rect(); |
690 | 0 | auto fragment_absolute_device_rect = context.enclosing_device_rect(fragment_absolute_rect); |
691 | |
|
692 | 0 | if (paintable.document().inspected_layout_node() == &paintable.layout_node()) |
693 | 0 | context.display_list_recorder().draw_rect(fragment_absolute_device_rect.to_type<int>(), Color::Magenta); |
694 | |
|
695 | 0 | auto text = paintable.text_for_rendering(); |
696 | |
|
697 | 0 | auto glyph_run = fragment.glyph_run(); |
698 | 0 | if (!glyph_run) |
699 | 0 | return; |
700 | | |
701 | 0 | DevicePixelPoint baseline_start { fragment_absolute_device_rect.x(), fragment_absolute_device_rect.y() + context.rounded_device_pixels(fragment.baseline()) }; |
702 | 0 | auto scale = context.device_pixels_per_css_pixel(); |
703 | 0 | painter.draw_text_run(baseline_start.to_type<int>(), *glyph_run, paintable.computed_values().webkit_text_fill_color(), fragment_absolute_device_rect.to_type<int>(), scale); |
704 | |
|
705 | 0 | auto selection_rect = context.enclosing_device_rect(fragment.selection_rect(paintable.layout_node().first_available_font())).to_type<int>(); |
706 | 0 | if (!selection_rect.is_empty()) { |
707 | 0 | painter.fill_rect(selection_rect, CSS::SystemColor::highlight()); |
708 | 0 | DisplayListRecorderStateSaver saver(painter); |
709 | 0 | painter.add_clip_rect(selection_rect); |
710 | 0 | painter.draw_text_run(baseline_start.to_type<int>(), *glyph_run, CSS::SystemColor::highlight_text(), fragment_absolute_device_rect.to_type<int>(), scale); |
711 | 0 | } |
712 | |
|
713 | 0 | paint_text_decoration(context, paintable, fragment); |
714 | 0 | paint_cursor_if_needed(context, paintable, fragment); |
715 | 0 | } |
716 | 0 | } |
717 | | |
718 | | void PaintableWithLines::paint(PaintContext& context, PaintPhase phase) const |
719 | 0 | { |
720 | 0 | if (!is_visible()) |
721 | 0 | return; |
722 | | |
723 | 0 | PaintableBox::paint(context, phase); |
724 | |
|
725 | 0 | if (fragments().is_empty()) |
726 | 0 | return; |
727 | | |
728 | 0 | bool should_clip_overflow = computed_values().overflow_x() != CSS::Overflow::Visible && computed_values().overflow_y() != CSS::Overflow::Visible; |
729 | 0 | Optional<u32> corner_clip_id; |
730 | |
|
731 | 0 | auto clip_box = absolute_padding_box_rect(); |
732 | 0 | if (get_clip_rect().has_value()) { |
733 | 0 | clip_box.intersect(get_clip_rect().value()); |
734 | 0 | should_clip_overflow = true; |
735 | 0 | } |
736 | 0 | if (should_clip_overflow) { |
737 | 0 | context.display_list_recorder().save(); |
738 | | // FIXME: Handle overflow-x and overflow-y being different values. |
739 | 0 | auto clip_box_with_enclosing_scroll_frame_offset = clip_box; |
740 | 0 | if (enclosing_scroll_frame_offset().has_value()) |
741 | 0 | clip_box_with_enclosing_scroll_frame_offset.translate_by(enclosing_scroll_frame_offset().value()); |
742 | 0 | context.display_list_recorder().add_clip_rect(context.rounded_device_rect(clip_box_with_enclosing_scroll_frame_offset).to_type<int>()); |
743 | |
|
744 | 0 | auto border_radii = normalized_border_radii_data(ShrinkRadiiForBorders::Yes); |
745 | 0 | CornerRadii corner_radii { |
746 | 0 | .top_left = border_radii.top_left.as_corner(context), |
747 | 0 | .top_right = border_radii.top_right.as_corner(context), |
748 | 0 | .bottom_right = border_radii.bottom_right.as_corner(context), |
749 | 0 | .bottom_left = border_radii.bottom_left.as_corner(context) |
750 | 0 | }; |
751 | 0 | if (corner_radii.has_any_radius()) { |
752 | 0 | corner_clip_id = context.allocate_corner_clipper_id(); |
753 | 0 | context.display_list_recorder().sample_under_corners(*corner_clip_id, corner_radii, context.rounded_device_rect(clip_box).to_type<int>(), CornerClip::Outside); |
754 | 0 | } |
755 | |
|
756 | 0 | context.display_list_recorder().save(); |
757 | 0 | auto scroll_offset = context.rounded_device_point(this->scroll_offset()); |
758 | 0 | context.display_list_recorder().translate(-scroll_offset.to_type<int>()); |
759 | 0 | } |
760 | | |
761 | | // Text shadows |
762 | | // This is yet another loop, but done here because all shadows should appear under all text. |
763 | | // So, we paint the shadows before painting any text. |
764 | | // FIXME: Find a smarter way to do this? |
765 | 0 | if (phase == PaintPhase::Foreground) { |
766 | 0 | for (auto& fragment : fragments()) { |
767 | 0 | paint_text_shadow(context, fragment, fragment.shadows()); |
768 | 0 | } |
769 | 0 | } |
770 | |
|
771 | 0 | for (auto const& fragment : m_fragments) { |
772 | 0 | auto fragment_absolute_rect = fragment.absolute_rect(); |
773 | 0 | auto fragment_absolute_device_rect = context.enclosing_device_rect(fragment_absolute_rect); |
774 | 0 | if (context.should_show_line_box_borders()) { |
775 | 0 | context.display_list_recorder().draw_rect(fragment_absolute_device_rect.to_type<int>(), Color::Green); |
776 | 0 | context.display_list_recorder().draw_line( |
777 | 0 | context.rounded_device_point(fragment_absolute_rect.top_left().translated(0, fragment.baseline())).to_type<int>(), |
778 | 0 | context.rounded_device_point(fragment_absolute_rect.top_right().translated(-1, fragment.baseline())).to_type<int>(), Color::Red); |
779 | 0 | } |
780 | 0 | if (is<TextPaintable>(fragment.paintable())) |
781 | 0 | paint_text_fragment(context, static_cast<TextPaintable const&>(fragment.paintable()), fragment, phase); |
782 | 0 | } |
783 | |
|
784 | 0 | if (should_clip_overflow) { |
785 | 0 | context.display_list_recorder().restore(); |
786 | 0 | if (corner_clip_id.has_value()) { |
787 | 0 | context.display_list_recorder().blit_corner_clipping(*corner_clip_id); |
788 | 0 | corner_clip_id = {}; |
789 | 0 | } |
790 | 0 | context.display_list_recorder().restore(); |
791 | 0 | } |
792 | 0 | } |
793 | | |
794 | | Paintable::DispatchEventOfSameName PaintableBox::handle_mousedown(Badge<EventHandler>, CSSPixelPoint position, unsigned, unsigned) |
795 | 0 | { |
796 | 0 | auto vertical_scroll_thumb_rect = scroll_thumb_rect(ScrollDirection::Vertical); |
797 | 0 | auto horizontal_scroll_thumb_rect = scroll_thumb_rect(ScrollDirection::Horizontal); |
798 | 0 | if (vertical_scroll_thumb_rect.has_value() && vertical_scroll_thumb_rect.value().contains(position)) { |
799 | 0 | if (is_viewport()) |
800 | 0 | position.translate_by(-scroll_offset()); |
801 | 0 | m_last_mouse_tracking_position = position; |
802 | 0 | m_scroll_thumb_dragging_direction = ScrollDirection::Vertical; |
803 | 0 | const_cast<HTML::Navigable&>(*navigable()).event_handler().set_mouse_event_tracking_paintable(this); |
804 | 0 | } else if (horizontal_scroll_thumb_rect.has_value() && horizontal_scroll_thumb_rect.value().contains(position)) { |
805 | 0 | if (is_viewport()) |
806 | 0 | position.translate_by(-scroll_offset()); |
807 | 0 | m_last_mouse_tracking_position = position; |
808 | 0 | m_scroll_thumb_dragging_direction = ScrollDirection::Horizontal; |
809 | 0 | const_cast<HTML::Navigable&>(*navigable()).event_handler().set_mouse_event_tracking_paintable(this); |
810 | 0 | } |
811 | 0 | return Paintable::DispatchEventOfSameName::Yes; |
812 | 0 | } |
813 | | |
814 | | Paintable::DispatchEventOfSameName PaintableBox::handle_mouseup(Badge<EventHandler>, CSSPixelPoint, unsigned, unsigned) |
815 | 0 | { |
816 | 0 | if (m_last_mouse_tracking_position.has_value()) { |
817 | 0 | m_last_mouse_tracking_position.clear(); |
818 | 0 | m_scroll_thumb_dragging_direction.clear(); |
819 | 0 | const_cast<HTML::Navigable&>(*navigable()).event_handler().set_mouse_event_tracking_paintable(nullptr); |
820 | 0 | } |
821 | 0 | return Paintable::DispatchEventOfSameName::Yes; |
822 | 0 | } |
823 | | |
824 | | Paintable::DispatchEventOfSameName PaintableBox::handle_mousemove(Badge<EventHandler>, CSSPixelPoint position, unsigned, unsigned) |
825 | 0 | { |
826 | 0 | if (m_last_mouse_tracking_position.has_value()) { |
827 | 0 | if (is_viewport()) |
828 | 0 | position.translate_by(-scroll_offset()); |
829 | |
|
830 | 0 | Gfx::Point<double> scroll_delta; |
831 | 0 | if (m_scroll_thumb_dragging_direction == ScrollDirection::Horizontal) |
832 | 0 | scroll_delta.set_x((position.x() - m_last_mouse_tracking_position->x()).to_double()); |
833 | 0 | else |
834 | 0 | scroll_delta.set_y((position.y() - m_last_mouse_tracking_position->y()).to_double()); |
835 | |
|
836 | 0 | auto padding_rect = absolute_padding_box_rect(); |
837 | 0 | auto scrollable_overflow_rect = this->scrollable_overflow_rect().value(); |
838 | 0 | auto scroll_overflow_size = m_scroll_thumb_dragging_direction == ScrollDirection::Horizontal ? scrollable_overflow_rect.width() : scrollable_overflow_rect.height(); |
839 | 0 | auto scrollport_size = m_scroll_thumb_dragging_direction == ScrollDirection::Horizontal ? padding_rect.width() : padding_rect.height(); |
840 | 0 | auto scroll_px_per_mouse_position_delta_px = scroll_overflow_size.to_double() / scrollport_size.to_double(); |
841 | 0 | scroll_delta *= scroll_px_per_mouse_position_delta_px; |
842 | |
|
843 | 0 | if (is_viewport()) { |
844 | 0 | document().window()->scroll_by(scroll_delta.x(), scroll_delta.y()); |
845 | 0 | } else { |
846 | 0 | scroll_by(scroll_delta.x(), scroll_delta.y()); |
847 | 0 | } |
848 | |
|
849 | 0 | m_last_mouse_tracking_position = position; |
850 | 0 | return Paintable::DispatchEventOfSameName::No; |
851 | 0 | } |
852 | 0 | return Paintable::DispatchEventOfSameName::Yes; |
853 | 0 | } |
854 | | |
855 | | bool PaintableBox::handle_mousewheel(Badge<EventHandler>, CSSPixelPoint, unsigned, unsigned, int wheel_delta_x, int wheel_delta_y) |
856 | 0 | { |
857 | 0 | if (!layout_box().is_user_scrollable()) |
858 | 0 | return false; |
859 | | |
860 | | // TODO: Vertical and horizontal scroll overflow should be handled separately. |
861 | 0 | if (!has_scrollable_overflow()) |
862 | 0 | return false; |
863 | | |
864 | 0 | scroll_by(wheel_delta_x, wheel_delta_y); |
865 | 0 | return true; |
866 | 0 | } |
867 | | |
868 | | Layout::BlockContainer const& PaintableWithLines::layout_box() const |
869 | 0 | { |
870 | 0 | return static_cast<Layout::BlockContainer const&>(PaintableBox::layout_box()); |
871 | 0 | } |
872 | | |
873 | | Layout::BlockContainer& PaintableWithLines::layout_box() |
874 | 0 | { |
875 | 0 | return static_cast<Layout::BlockContainer&>(PaintableBox::layout_box()); |
876 | 0 | } |
877 | | |
878 | | TraversalDecision PaintableBox::hit_test_scrollbars(CSSPixelPoint position, Function<TraversalDecision(HitTestResult)> const& callback) const |
879 | 0 | { |
880 | 0 | auto vertical_scroll_thumb_rect = scroll_thumb_rect(ScrollDirection::Vertical); |
881 | 0 | if (vertical_scroll_thumb_rect.has_value() && vertical_scroll_thumb_rect.value().contains(position)) |
882 | 0 | return callback(HitTestResult { const_cast<PaintableBox&>(*this) }); |
883 | 0 | auto horizontal_scroll_thumb_rect = scroll_thumb_rect(ScrollDirection::Horizontal); |
884 | 0 | if (horizontal_scroll_thumb_rect.has_value() && horizontal_scroll_thumb_rect.value().contains(position)) |
885 | 0 | return callback(HitTestResult { const_cast<PaintableBox&>(*this) }); |
886 | 0 | return TraversalDecision::Continue; |
887 | 0 | } |
888 | | |
889 | | TraversalDecision PaintableBox::hit_test(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const |
890 | 0 | { |
891 | 0 | if (clip_rect().has_value() && !clip_rect()->contains(position)) |
892 | 0 | return TraversalDecision::Continue; |
893 | | |
894 | 0 | auto position_adjusted_by_scroll_offset = position; |
895 | 0 | if (enclosing_scroll_frame_offset().has_value()) |
896 | 0 | position_adjusted_by_scroll_offset.translate_by(-enclosing_scroll_frame_offset().value()); |
897 | |
|
898 | 0 | if (!is_visible()) |
899 | 0 | return TraversalDecision::Continue; |
900 | | |
901 | 0 | if (hit_test_scrollbars(position_adjusted_by_scroll_offset, callback) == TraversalDecision::Break) |
902 | 0 | return TraversalDecision::Break; |
903 | | |
904 | 0 | if (layout_box().is_viewport()) { |
905 | 0 | auto& viewport_paintable = const_cast<ViewportPaintable&>(static_cast<ViewportPaintable const&>(*this)); |
906 | 0 | viewport_paintable.build_stacking_context_tree_if_needed(); |
907 | 0 | viewport_paintable.document().update_paint_and_hit_testing_properties_if_needed(); |
908 | 0 | viewport_paintable.refresh_scroll_state(); |
909 | 0 | viewport_paintable.refresh_clip_state(); |
910 | 0 | return stacking_context()->hit_test(position, type, callback); |
911 | 0 | } |
912 | | |
913 | 0 | for (auto const* child = last_child(); child; child = child->previous_sibling()) { |
914 | 0 | auto z_index = child->computed_values().z_index(); |
915 | 0 | if (child->layout_node().is_positioned() && z_index.value_or(0) == 0) |
916 | 0 | continue; |
917 | 0 | if (child->hit_test(position, type, callback) == TraversalDecision::Break) |
918 | 0 | return TraversalDecision::Break; |
919 | 0 | } |
920 | | |
921 | 0 | if (!absolute_border_box_rect().contains(position_adjusted_by_scroll_offset.x(), position_adjusted_by_scroll_offset.y())) |
922 | 0 | return TraversalDecision::Continue; |
923 | | |
924 | 0 | if (!visible_for_hit_testing()) |
925 | 0 | return TraversalDecision::Continue; |
926 | | |
927 | 0 | return callback(HitTestResult { const_cast<PaintableBox&>(*this) }); |
928 | 0 | } |
929 | | |
930 | | Optional<HitTestResult> PaintableBox::hit_test(CSSPixelPoint position, HitTestType type) const |
931 | 0 | { |
932 | 0 | Optional<HitTestResult> result; |
933 | 0 | (void)PaintableBox::hit_test(position, type, [&](HitTestResult candidate) { |
934 | 0 | if (candidate.paintable->visible_for_hit_testing()) { |
935 | 0 | if (!result.has_value() |
936 | 0 | || candidate.vertical_distance.value_or(CSSPixels::max_integer_value) < result->vertical_distance.value_or(CSSPixels::max_integer_value) |
937 | 0 | || candidate.horizontal_distance.value_or(CSSPixels::max_integer_value) < result->horizontal_distance.value_or(CSSPixels::max_integer_value)) { |
938 | 0 | result = move(candidate); |
939 | 0 | } |
940 | 0 | } |
941 | |
|
942 | 0 | if (result.has_value() && (type == HitTestType::Exact || (result->vertical_distance == 0 && result->horizontal_distance == 0))) |
943 | 0 | return TraversalDecision::Break; |
944 | 0 | return TraversalDecision::Continue; |
945 | 0 | }); |
946 | 0 | return result; |
947 | 0 | } |
948 | | |
949 | | TraversalDecision PaintableWithLines::hit_test(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const |
950 | 0 | { |
951 | 0 | if (clip_rect().has_value() && !clip_rect()->contains(position)) |
952 | 0 | return TraversalDecision::Continue; |
953 | | |
954 | 0 | auto position_adjusted_by_scroll_offset = position; |
955 | 0 | if (enclosing_scroll_frame_offset().has_value()) |
956 | 0 | position_adjusted_by_scroll_offset.translate_by(-enclosing_scroll_frame_offset().value()); |
957 | |
|
958 | 0 | if (!layout_box().children_are_inline() || m_fragments.is_empty()) { |
959 | 0 | return PaintableBox::hit_test(position, type, callback); |
960 | 0 | } |
961 | | |
962 | 0 | if (hit_test_scrollbars(position_adjusted_by_scroll_offset, callback) == TraversalDecision::Break) |
963 | 0 | return TraversalDecision::Break; |
964 | | |
965 | 0 | for (auto const* child = last_child(); child; child = child->previous_sibling()) { |
966 | 0 | if (child->hit_test(position, type, callback) == TraversalDecision::Break) |
967 | 0 | return TraversalDecision::Break; |
968 | 0 | } |
969 | | |
970 | 0 | for (auto const& fragment : fragments()) { |
971 | 0 | if (fragment.paintable().stacking_context()) |
972 | 0 | continue; |
973 | 0 | auto fragment_absolute_rect = fragment.absolute_rect(); |
974 | 0 | if (fragment_absolute_rect.contains(position_adjusted_by_scroll_offset)) { |
975 | 0 | if (fragment.paintable().hit_test(position, type, callback) == TraversalDecision::Break) |
976 | 0 | return TraversalDecision::Break; |
977 | 0 | HitTestResult hit_test_result { const_cast<Paintable&>(fragment.paintable()), fragment.text_index_at(position_adjusted_by_scroll_offset.x()), 0, 0 }; |
978 | 0 | if (callback(hit_test_result) == TraversalDecision::Break) |
979 | 0 | return TraversalDecision::Break; |
980 | 0 | } else if (type == HitTestType::TextCursor) { |
981 | 0 | auto const* common_ancestor_parent = [&]() -> DOM::Node const* { |
982 | 0 | auto selection = document().get_selection(); |
983 | 0 | if (!selection) |
984 | 0 | return nullptr; |
985 | 0 | auto range = selection->range(); |
986 | 0 | if (!range) |
987 | 0 | return nullptr; |
988 | 0 | auto common_ancestor = range->common_ancestor_container(); |
989 | 0 | if (common_ancestor->parent()) |
990 | 0 | return common_ancestor->parent(); |
991 | 0 | return common_ancestor; |
992 | 0 | }(); |
993 | |
|
994 | 0 | auto const* fragment_dom_node = fragment.layout_node().dom_node(); |
995 | 0 | if (common_ancestor_parent && fragment_dom_node && common_ancestor_parent->is_ancestor_of(*fragment_dom_node)) { |
996 | | // If we reached this point, the position is not within the fragment. However, the fragment start or end might be |
997 | | // the place to place the cursor. To determine the best place, we first find the closest fragment horizontally to |
998 | | // the cursor. If we could not find one, then find for the closest vertically above the cursor. |
999 | | // If we knew the direction of selection, we would look above if selecting upward. |
1000 | 0 | if (fragment_absolute_rect.bottom() - 1 <= position_adjusted_by_scroll_offset.y()) { // fully below the fragment |
1001 | 0 | HitTestResult hit_test_result { |
1002 | 0 | .paintable = const_cast<Paintable&>(fragment.paintable()), |
1003 | 0 | .index_in_node = fragment.start() + fragment.length(), |
1004 | 0 | .vertical_distance = position_adjusted_by_scroll_offset.y() - fragment_absolute_rect.bottom(), |
1005 | 0 | }; |
1006 | 0 | if (callback(hit_test_result) == TraversalDecision::Break) |
1007 | 0 | return TraversalDecision::Break; |
1008 | 0 | } else if (fragment_absolute_rect.top() <= position_adjusted_by_scroll_offset.y()) { // vertically within the fragment |
1009 | 0 | if (position_adjusted_by_scroll_offset.x() < fragment_absolute_rect.left()) { |
1010 | 0 | HitTestResult hit_test_result { |
1011 | 0 | .paintable = const_cast<Paintable&>(fragment.paintable()), |
1012 | 0 | .index_in_node = fragment.start(), |
1013 | 0 | .vertical_distance = 0, |
1014 | 0 | .horizontal_distance = fragment_absolute_rect.left() - position_adjusted_by_scroll_offset.x(), |
1015 | 0 | }; |
1016 | 0 | if (callback(hit_test_result) == TraversalDecision::Break) |
1017 | 0 | return TraversalDecision::Break; |
1018 | 0 | } else if (position_adjusted_by_scroll_offset.x() > fragment_absolute_rect.right()) { |
1019 | 0 | HitTestResult hit_test_result { |
1020 | 0 | .paintable = const_cast<Paintable&>(fragment.paintable()), |
1021 | 0 | .index_in_node = fragment.start() + fragment.length(), |
1022 | 0 | .vertical_distance = 0, |
1023 | 0 | .horizontal_distance = position_adjusted_by_scroll_offset.x() - fragment_absolute_rect.right(), |
1024 | 0 | }; |
1025 | 0 | if (callback(hit_test_result) == TraversalDecision::Break) |
1026 | 0 | return TraversalDecision::Break; |
1027 | 0 | } |
1028 | 0 | } |
1029 | 0 | } |
1030 | 0 | } |
1031 | 0 | } |
1032 | | |
1033 | 0 | if (!stacking_context() && is_visible() && absolute_border_box_rect().contains(position_adjusted_by_scroll_offset.x(), position_adjusted_by_scroll_offset.y())) { |
1034 | 0 | if (callback(HitTestResult { const_cast<PaintableWithLines&>(*this) }) == TraversalDecision::Break) |
1035 | 0 | return TraversalDecision::Break; |
1036 | 0 | } |
1037 | | |
1038 | 0 | return TraversalDecision::Continue; |
1039 | 0 | } |
1040 | | |
1041 | | void PaintableBox::set_needs_display() const |
1042 | 0 | { |
1043 | 0 | if (auto navigable = this->navigable()) |
1044 | 0 | navigable->set_needs_display(absolute_rect()); |
1045 | 0 | } |
1046 | | |
1047 | | Optional<CSSPixelRect> PaintableBox::get_masking_area() const |
1048 | 0 | { |
1049 | | // FIXME: Support clip-paths with transforms. |
1050 | 0 | if (!combined_css_transform().is_identity_or_translation()) |
1051 | 0 | return {}; |
1052 | 0 | auto clip_path = computed_values().clip_path(); |
1053 | | // FIXME: Support other clip sources. |
1054 | 0 | if (!clip_path.has_value() || !clip_path->is_basic_shape()) |
1055 | 0 | return {}; |
1056 | | // FIXME: Support other geometry boxes. See: https://drafts.fxtf.org/css-masking/#typedef-geometry-box |
1057 | 0 | return absolute_border_box_rect(); |
1058 | 0 | } |
1059 | | |
1060 | | Optional<Gfx::Bitmap::MaskKind> PaintableBox::get_mask_type() const |
1061 | 0 | { |
1062 | | // Always an alpha mask as only basic shapes are supported right now. |
1063 | 0 | return Gfx::Bitmap::MaskKind::Alpha; |
1064 | 0 | } |
1065 | | |
1066 | | RefPtr<Gfx::Bitmap> PaintableBox::calculate_mask(PaintContext& context, CSSPixelRect const& masking_area) const |
1067 | 0 | { |
1068 | 0 | VERIFY(computed_values().clip_path()->is_basic_shape()); |
1069 | 0 | auto const& basic_shape = computed_values().clip_path()->basic_shape(); |
1070 | 0 | auto path = basic_shape.to_path(masking_area, layout_node()); |
1071 | 0 | auto device_pixel_scale = context.device_pixels_per_css_pixel(); |
1072 | 0 | path = path.copy_transformed(Gfx::AffineTransform {}.set_scale(device_pixel_scale, device_pixel_scale)); |
1073 | 0 | auto mask_rect = context.enclosing_device_rect(masking_area); |
1074 | 0 | auto maybe_bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, mask_rect.size().to_type<int>()); |
1075 | 0 | if (maybe_bitmap.is_error()) |
1076 | 0 | return {}; |
1077 | 0 | auto bitmap = maybe_bitmap.release_value(); |
1078 | 0 | Gfx::Painter painter(*bitmap); |
1079 | 0 | Gfx::AntiAliasingPainter aa_painter(painter); |
1080 | 0 | aa_painter.fill_path(path, Color::Black); |
1081 | 0 | return bitmap; |
1082 | 0 | } |
1083 | | |
1084 | | void PaintableBox::resolve_paint_properties() |
1085 | 0 | { |
1086 | 0 | auto const& computed_values = this->computed_values(); |
1087 | 0 | auto const& layout_node = this->layout_node(); |
1088 | | |
1089 | | // Border radii |
1090 | 0 | BorderRadiiData radii_data {}; |
1091 | 0 | if (computed_values.has_noninitial_border_radii()) { |
1092 | 0 | CSSPixelRect const border_rect { 0, 0, border_box_width(), border_box_height() }; |
1093 | |
|
1094 | 0 | auto const& border_top_left_radius = computed_values.border_top_left_radius(); |
1095 | 0 | auto const& border_top_right_radius = computed_values.border_top_right_radius(); |
1096 | 0 | auto const& border_bottom_right_radius = computed_values.border_bottom_right_radius(); |
1097 | 0 | auto const& border_bottom_left_radius = computed_values.border_bottom_left_radius(); |
1098 | |
|
1099 | 0 | radii_data = normalize_border_radii_data(layout_node, border_rect, border_top_left_radius, |
1100 | 0 | border_top_right_radius, border_bottom_right_radius, |
1101 | 0 | border_bottom_left_radius); |
1102 | 0 | } |
1103 | 0 | set_border_radii_data(radii_data); |
1104 | | |
1105 | | // Box shadows |
1106 | 0 | auto const& box_shadow_data = computed_values.box_shadow(); |
1107 | 0 | Vector<Painting::ShadowData> resolved_box_shadow_data; |
1108 | 0 | resolved_box_shadow_data.ensure_capacity(box_shadow_data.size()); |
1109 | 0 | for (auto const& layer : box_shadow_data) { |
1110 | 0 | resolved_box_shadow_data.empend( |
1111 | 0 | layer.color, |
1112 | 0 | layer.offset_x.to_px(layout_node), |
1113 | 0 | layer.offset_y.to_px(layout_node), |
1114 | 0 | layer.blur_radius.to_px(layout_node), |
1115 | 0 | layer.spread_distance.to_px(layout_node), |
1116 | 0 | layer.placement == CSS::ShadowPlacement::Outer ? Painting::ShadowPlacement::Outer |
1117 | 0 | : Painting::ShadowPlacement::Inner); |
1118 | 0 | } |
1119 | 0 | set_box_shadow_data(move(resolved_box_shadow_data)); |
1120 | |
|
1121 | 0 | auto const& transformations = computed_values.transformations(); |
1122 | 0 | if (!transformations.is_empty()) { |
1123 | 0 | auto matrix = Gfx::FloatMatrix4x4::identity(); |
1124 | 0 | for (auto const& transform : transformations) |
1125 | 0 | matrix = matrix * transform.to_matrix(*this).release_value(); |
1126 | 0 | set_transform(matrix); |
1127 | 0 | } |
1128 | |
|
1129 | 0 | auto const& transform_origin = computed_values.transform_origin(); |
1130 | | // https://www.w3.org/TR/css-transforms-1/#transform-box |
1131 | 0 | auto transform_box = computed_values.transform_box(); |
1132 | | // For SVG elements without associated CSS layout box, the used value for content-box is fill-box and for |
1133 | | // border-box is stroke-box. |
1134 | | // FIXME: This currently detects any SVG element except the <svg> one. Is that correct? |
1135 | | // And is it correct to use `else` below? |
1136 | 0 | if (is<Painting::SVGPaintable>(*this)) { |
1137 | 0 | switch (transform_box) { |
1138 | 0 | case CSS::TransformBox::ContentBox: |
1139 | 0 | transform_box = CSS::TransformBox::FillBox; |
1140 | 0 | break; |
1141 | 0 | case CSS::TransformBox::BorderBox: |
1142 | 0 | transform_box = CSS::TransformBox::StrokeBox; |
1143 | 0 | break; |
1144 | 0 | default: |
1145 | 0 | break; |
1146 | 0 | } |
1147 | 0 | } |
1148 | | // For elements with associated CSS layout box, the used value for fill-box is content-box and for |
1149 | | // stroke-box and view-box is border-box. |
1150 | 0 | else { |
1151 | 0 | switch (transform_box) { |
1152 | 0 | case CSS::TransformBox::FillBox: |
1153 | 0 | transform_box = CSS::TransformBox::ContentBox; |
1154 | 0 | break; |
1155 | 0 | case CSS::TransformBox::StrokeBox: |
1156 | 0 | case CSS::TransformBox::ViewBox: |
1157 | 0 | transform_box = CSS::TransformBox::BorderBox; |
1158 | 0 | break; |
1159 | 0 | default: |
1160 | 0 | break; |
1161 | 0 | } |
1162 | 0 | } |
1163 | | |
1164 | 0 | CSSPixelRect reference_box = [&]() { |
1165 | 0 | switch (transform_box) { |
1166 | 0 | case CSS::TransformBox::ContentBox: |
1167 | | // Uses the content box as reference box. |
1168 | | // FIXME: The reference box of a table is the border box of its table wrapper box, not its table box. |
1169 | 0 | return absolute_rect(); |
1170 | 0 | case CSS::TransformBox::BorderBox: |
1171 | | // Uses the border box as reference box. |
1172 | | // FIXME: The reference box of a table is the border box of its table wrapper box, not its table box. |
1173 | 0 | return absolute_border_box_rect(); |
1174 | 0 | case CSS::TransformBox::FillBox: |
1175 | | // Uses the object bounding box as reference box. |
1176 | | // FIXME: For now we're using the content rect as an approximation. |
1177 | 0 | return absolute_rect(); |
1178 | 0 | case CSS::TransformBox::StrokeBox: |
1179 | | // Uses the stroke bounding box as reference box. |
1180 | | // FIXME: For now we're using the border rect as an approximation. |
1181 | 0 | return absolute_border_box_rect(); |
1182 | 0 | case CSS::TransformBox::ViewBox: |
1183 | | // Uses the nearest SVG viewport as reference box. |
1184 | | // FIXME: If a viewBox attribute is specified for the SVG viewport creating element: |
1185 | | // - The reference box is positioned at the origin of the coordinate system established by the viewBox attribute. |
1186 | | // - The dimension of the reference box is set to the width and height values of the viewBox attribute. |
1187 | 0 | auto* svg_paintable = first_ancestor_of_type<Painting::SVGSVGPaintable>(); |
1188 | 0 | if (!svg_paintable) |
1189 | 0 | return absolute_border_box_rect(); |
1190 | 0 | return svg_paintable->absolute_rect(); |
1191 | 0 | } |
1192 | 0 | VERIFY_NOT_REACHED(); |
1193 | 0 | }(); |
1194 | 0 | auto x = reference_box.left() + transform_origin.x.to_px(layout_node, reference_box.width()); |
1195 | 0 | auto y = reference_box.top() + transform_origin.y.to_px(layout_node, reference_box.height()); |
1196 | 0 | set_transform_origin({ x, y }); |
1197 | 0 | set_transform_origin({ x, y }); |
1198 | | |
1199 | | // Outlines |
1200 | 0 | auto outline_width = computed_values.outline_width().to_px(layout_node); |
1201 | 0 | auto outline_data = borders_data_for_outline(layout_node, computed_values.outline_color(), computed_values.outline_style(), outline_width); |
1202 | 0 | auto outline_offset = computed_values.outline_offset().to_px(layout_node); |
1203 | 0 | set_outline_data(outline_data); |
1204 | 0 | set_outline_offset(outline_offset); |
1205 | |
|
1206 | 0 | auto combined_transform = compute_combined_css_transform(); |
1207 | 0 | set_combined_css_transform(combined_transform); |
1208 | 0 | } |
1209 | | |
1210 | | void PaintableWithLines::resolve_paint_properties() |
1211 | 0 | { |
1212 | 0 | PaintableBox::resolve_paint_properties(); |
1213 | |
|
1214 | 0 | auto const& layout_node = this->layout_node(); |
1215 | 0 | for (auto const& fragment : fragments()) { |
1216 | 0 | auto const& text_shadow = fragment.m_layout_node->computed_values().text_shadow(); |
1217 | 0 | if (!text_shadow.is_empty()) { |
1218 | 0 | Vector<Painting::ShadowData> resolved_shadow_data; |
1219 | 0 | resolved_shadow_data.ensure_capacity(text_shadow.size()); |
1220 | 0 | for (auto const& layer : text_shadow) { |
1221 | 0 | resolved_shadow_data.empend( |
1222 | 0 | layer.color, |
1223 | 0 | layer.offset_x.to_px(layout_node), |
1224 | 0 | layer.offset_y.to_px(layout_node), |
1225 | 0 | layer.blur_radius.to_px(layout_node), |
1226 | 0 | layer.spread_distance.to_px(layout_node), |
1227 | 0 | Painting::ShadowPlacement::Outer); |
1228 | 0 | } |
1229 | 0 | const_cast<Painting::PaintableFragment&>(fragment).set_shadows(move(resolved_shadow_data)); |
1230 | 0 | } |
1231 | 0 | } |
1232 | 0 | } |
1233 | | |
1234 | | } |