/src/serenity/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2021-2023, Andreas Kling <kling@serenityos.org> |
3 | | * Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org> |
4 | | * Copyright (c) 2022, Tobias Christiansen <tobyase@serenityos.org> |
5 | | * Copyright (c) 2023, MacDue <macdue@dueutil.tech> |
6 | | * |
7 | | * SPDX-License-Identifier: BSD-2-Clause |
8 | | */ |
9 | | |
10 | | #include <AK/Debug.h> |
11 | | #include <LibGfx/BoundingBox.h> |
12 | | #include <LibGfx/Font/ScaledFont.h> |
13 | | #include <LibGfx/TextLayout.h> |
14 | | #include <LibWeb/Layout/BlockFormattingContext.h> |
15 | | #include <LibWeb/Layout/SVGClipBox.h> |
16 | | #include <LibWeb/Layout/SVGFormattingContext.h> |
17 | | #include <LibWeb/Layout/SVGGeometryBox.h> |
18 | | #include <LibWeb/Layout/SVGImageBox.h> |
19 | | #include <LibWeb/Layout/SVGMaskBox.h> |
20 | | #include <LibWeb/Layout/Viewport.h> |
21 | | #include <LibWeb/SVG/SVGAElement.h> |
22 | | #include <LibWeb/SVG/SVGClipPathElement.h> |
23 | | #include <LibWeb/SVG/SVGForeignObjectElement.h> |
24 | | #include <LibWeb/SVG/SVGGElement.h> |
25 | | #include <LibWeb/SVG/SVGImageElement.h> |
26 | | #include <LibWeb/SVG/SVGMaskElement.h> |
27 | | #include <LibWeb/SVG/SVGSVGElement.h> |
28 | | #include <LibWeb/SVG/SVGSymbolElement.h> |
29 | | #include <LibWeb/SVG/SVGUseElement.h> |
30 | | |
31 | | namespace Web::Layout { |
32 | | |
33 | | SVGFormattingContext::SVGFormattingContext(LayoutState& state, LayoutMode layout_mode, Box const& box, FormattingContext* parent, Gfx::AffineTransform parent_viewbox_transform) |
34 | 0 | : FormattingContext(Type::SVG, layout_mode, state, box, parent) |
35 | 0 | , m_parent_viewbox_transform(parent_viewbox_transform) |
36 | 0 | { |
37 | 0 | } |
38 | | |
39 | 0 | SVGFormattingContext::~SVGFormattingContext() = default; |
40 | | |
41 | | CSSPixels SVGFormattingContext::automatic_content_width() const |
42 | 0 | { |
43 | 0 | return 0; |
44 | 0 | } |
45 | | |
46 | | CSSPixels SVGFormattingContext::automatic_content_height() const |
47 | 0 | { |
48 | 0 | return 0; |
49 | 0 | } |
50 | | |
51 | | struct ViewBoxTransform { |
52 | | CSSPixelPoint offset; |
53 | | double scale_factor_x; |
54 | | double scale_factor_y; |
55 | | }; |
56 | | |
57 | | // https://svgwg.org/svg2-draft/coords.html#PreserveAspectRatioAttribute |
58 | | static ViewBoxTransform scale_and_align_viewbox_content(SVG::PreserveAspectRatio const& preserve_aspect_ratio, |
59 | | SVG::ViewBox const& view_box, Gfx::FloatSize viewbox_scale, auto const& svg_box_state) |
60 | 0 | { |
61 | 0 | ViewBoxTransform viewbox_transform {}; |
62 | |
|
63 | 0 | if (preserve_aspect_ratio.align == SVG::PreserveAspectRatio::Align::None) { |
64 | 0 | viewbox_transform.scale_factor_x = viewbox_scale.width(); |
65 | 0 | viewbox_transform.scale_factor_y = viewbox_scale.height(); |
66 | 0 | viewbox_transform.offset = {}; |
67 | 0 | return viewbox_transform; |
68 | 0 | } |
69 | | |
70 | 0 | switch (preserve_aspect_ratio.meet_or_slice) { |
71 | 0 | case SVG::PreserveAspectRatio::MeetOrSlice::Meet: |
72 | | // meet (the default) - Scale the graphic such that: |
73 | | // - aspect ratio is preserved |
74 | | // - the entire ‘viewBox’ is visible within the SVG viewport |
75 | | // - the ‘viewBox’ is scaled up as much as possible, while still meeting the other criteria |
76 | 0 | viewbox_transform.scale_factor_x = viewbox_transform.scale_factor_y = min(viewbox_scale.width(), viewbox_scale.height()); |
77 | 0 | break; |
78 | 0 | case SVG::PreserveAspectRatio::MeetOrSlice::Slice: |
79 | | // slice - Scale the graphic such that: |
80 | | // aspect ratio is preserved |
81 | | // the entire SVG viewport is covered by the ‘viewBox’ |
82 | | // the ‘viewBox’ is scaled down as much as possible, while still meeting the other criteria |
83 | 0 | viewbox_transform.scale_factor_x = viewbox_transform.scale_factor_y = max(viewbox_scale.width(), viewbox_scale.height()); |
84 | 0 | break; |
85 | 0 | default: |
86 | 0 | VERIFY_NOT_REACHED(); |
87 | 0 | } |
88 | | |
89 | | // Handle X alignment: |
90 | 0 | if (svg_box_state.has_definite_width()) { |
91 | 0 | switch (preserve_aspect_ratio.align) { |
92 | 0 | case SVG::PreserveAspectRatio::Align::xMinYMin: |
93 | 0 | case SVG::PreserveAspectRatio::Align::xMinYMid: |
94 | 0 | case SVG::PreserveAspectRatio::Align::xMinYMax: |
95 | | // Align the <min-x> of the element's ‘viewBox’ with the smallest X value of the SVG viewport. |
96 | 0 | viewbox_transform.offset.translate_by(0, 0); |
97 | 0 | break; |
98 | 0 | case SVG::PreserveAspectRatio::Align::None: { |
99 | | // Do not force uniform scaling. Scale the graphic content of the given element non-uniformly |
100 | | // if necessary such that the element's bounding box exactly matches the SVG viewport rectangle. |
101 | | // FIXME: None is unimplemented (treat as xMidYMid) |
102 | 0 | [[fallthrough]]; |
103 | 0 | } |
104 | 0 | case SVG::PreserveAspectRatio::Align::xMidYMin: |
105 | 0 | case SVG::PreserveAspectRatio::Align::xMidYMid: |
106 | 0 | case SVG::PreserveAspectRatio::Align::xMidYMax: |
107 | | // Align the midpoint X value of the element's ‘viewBox’ with the midpoint X value of the SVG viewport. |
108 | 0 | viewbox_transform.offset.translate_by((svg_box_state.content_width() - CSSPixels::nearest_value_for(view_box.width * viewbox_transform.scale_factor_x)) / 2, 0); |
109 | 0 | break; |
110 | 0 | case SVG::PreserveAspectRatio::Align::xMaxYMin: |
111 | 0 | case SVG::PreserveAspectRatio::Align::xMaxYMid: |
112 | 0 | case SVG::PreserveAspectRatio::Align::xMaxYMax: |
113 | | // Align the <min-x>+<width> of the element's ‘viewBox’ with the maximum X value of the SVG viewport. |
114 | 0 | viewbox_transform.offset.translate_by((svg_box_state.content_width() - CSSPixels::nearest_value_for(view_box.width * viewbox_transform.scale_factor_x)), 0); |
115 | 0 | break; |
116 | 0 | default: |
117 | 0 | VERIFY_NOT_REACHED(); |
118 | 0 | } |
119 | 0 | } |
120 | | |
121 | 0 | if (svg_box_state.has_definite_width()) { |
122 | 0 | switch (preserve_aspect_ratio.align) { |
123 | 0 | case SVG::PreserveAspectRatio::Align::xMinYMin: |
124 | 0 | case SVG::PreserveAspectRatio::Align::xMidYMin: |
125 | 0 | case SVG::PreserveAspectRatio::Align::xMaxYMin: |
126 | | // Align the <min-y> of the element's ‘viewBox’ with the smallest Y value of the SVG viewport. |
127 | 0 | viewbox_transform.offset.translate_by(0, 0); |
128 | 0 | break; |
129 | 0 | case SVG::PreserveAspectRatio::Align::None: { |
130 | | // Do not force uniform scaling. Scale the graphic content of the given element non-uniformly |
131 | | // if necessary such that the element's bounding box exactly matches the SVG viewport rectangle. |
132 | | // FIXME: None is unimplemented (treat as xMidYMid) |
133 | 0 | [[fallthrough]]; |
134 | 0 | } |
135 | 0 | case SVG::PreserveAspectRatio::Align::xMinYMid: |
136 | 0 | case SVG::PreserveAspectRatio::Align::xMidYMid: |
137 | 0 | case SVG::PreserveAspectRatio::Align::xMaxYMid: |
138 | | // Align the midpoint Y value of the element's ‘viewBox’ with the midpoint Y value of the SVG viewport. |
139 | 0 | viewbox_transform.offset.translate_by(0, (svg_box_state.content_height() - CSSPixels::nearest_value_for(view_box.height * viewbox_transform.scale_factor_y)) / 2); |
140 | 0 | break; |
141 | 0 | case SVG::PreserveAspectRatio::Align::xMinYMax: |
142 | 0 | case SVG::PreserveAspectRatio::Align::xMidYMax: |
143 | 0 | case SVG::PreserveAspectRatio::Align::xMaxYMax: |
144 | | // Align the <min-y>+<height> of the element's ‘viewBox’ with the maximum Y value of the SVG viewport. |
145 | 0 | viewbox_transform.offset.translate_by(0, (svg_box_state.content_height() - CSSPixels::nearest_value_for(view_box.height * viewbox_transform.scale_factor_y))); |
146 | 0 | break; |
147 | 0 | default: |
148 | 0 | VERIFY_NOT_REACHED(); |
149 | 0 | } |
150 | 0 | } |
151 | | |
152 | 0 | return viewbox_transform; |
153 | 0 | } |
154 | | |
155 | | static bool is_container_element(Node const& node) |
156 | 0 | { |
157 | | // https://svgwg.org/svg2-draft/struct.html#GroupsOverview |
158 | 0 | auto* dom_node = node.dom_node(); |
159 | 0 | if (!dom_node) |
160 | 0 | return false; |
161 | 0 | if (is<SVG::SVGAElement>(dom_node)) |
162 | 0 | return true; |
163 | 0 | if (is<SVG::SVGUseElement>(dom_node)) |
164 | 0 | return true; |
165 | 0 | if (is<SVG::SVGSymbolElement>(dom_node)) |
166 | 0 | return true; |
167 | 0 | if (is<SVG::SVGGElement>(dom_node)) |
168 | 0 | return true; |
169 | 0 | if (is<SVG::SVGMaskElement>(dom_node)) |
170 | 0 | return true; |
171 | 0 | return false; |
172 | 0 | } |
173 | | |
174 | | void SVGFormattingContext::run(AvailableSpace const& available_space) |
175 | 0 | { |
176 | | // NOTE: SVG doesn't have a "formatting context" in the spec, but this is the most |
177 | | // obvious way to drive SVG layout in our engine at the moment. |
178 | |
|
179 | 0 | auto& svg_viewport = dynamic_cast<SVG::SVGViewport const&>(*context_box().dom_node()); |
180 | 0 | auto& svg_box_state = m_state.get_mutable(context_box()); |
181 | |
|
182 | 0 | if (!this->context_box().root().document().is_decoded_svg()) { |
183 | | // Overwrite the content width/height with the styled node width/height (from <svg width height ...>) |
184 | | |
185 | | // NOTE: If a height had not been provided by the svg element, it was set to the height of the container |
186 | | // (see BlockFormattingContext::layout_viewport) |
187 | 0 | if (svg_box_state.node().computed_values().width().is_length()) |
188 | 0 | svg_box_state.set_content_width(svg_box_state.node().computed_values().width().length().to_px(svg_box_state.node())); |
189 | 0 | if (svg_box_state.node().computed_values().height().is_length()) |
190 | 0 | svg_box_state.set_content_height(svg_box_state.node().computed_values().height().length().to_px(svg_box_state.node())); |
191 | | // FIXME: In SVG 2, length can also be a percentage. We'll need to support that. |
192 | 0 | } |
193 | | |
194 | | // NOTE: We consider all SVG root elements to have definite size in both axes. |
195 | | // I'm not sure if this is good or bad, but our viewport transform logic depends on it. |
196 | 0 | svg_box_state.set_has_definite_width(true); |
197 | 0 | svg_box_state.set_has_definite_height(true); |
198 | |
|
199 | 0 | auto viewbox = svg_viewport.view_box(); |
200 | | // https://svgwg.org/svg2-draft/coords.html#ViewBoxAttribute |
201 | 0 | if (viewbox.has_value()) { |
202 | 0 | if (viewbox->width < 0 || viewbox->height < 0) { |
203 | | // A negative value for <width> or <height> is an error and invalidates the ‘viewBox’ attribute. |
204 | 0 | viewbox = {}; |
205 | 0 | } else if (viewbox->width == 0 || viewbox->height == 0) { |
206 | | // A value of zero disables rendering of the element. |
207 | 0 | return; |
208 | 0 | } |
209 | 0 | } |
210 | | |
211 | 0 | m_current_viewbox_transform = m_parent_viewbox_transform; |
212 | 0 | if (viewbox.has_value()) { |
213 | | // FIXME: This should allow just one of width or height to be specified. |
214 | | // E.g. We should be able to layout <svg width="100%"> where height is unspecified/auto. |
215 | 0 | if (!svg_box_state.has_definite_width() || !svg_box_state.has_definite_height()) { |
216 | 0 | dbgln_if(LIBWEB_CSS_DEBUG, "FIXME: Attempting to layout indefinitely sized SVG with a viewbox -- this likely won't work!"); |
217 | 0 | } |
218 | |
|
219 | 0 | auto scale_width = svg_box_state.has_definite_width() ? svg_box_state.content_width() / viewbox->width : 1; |
220 | 0 | auto scale_height = svg_box_state.has_definite_height() ? svg_box_state.content_height() / viewbox->height : 1; |
221 | | |
222 | | // The initial value for preserveAspectRatio is xMidYMid meet. |
223 | 0 | auto preserve_aspect_ratio = svg_viewport.preserve_aspect_ratio().value_or(SVG::PreserveAspectRatio {}); |
224 | 0 | auto viewbox_offset_and_scale = scale_and_align_viewbox_content(preserve_aspect_ratio, *viewbox, { scale_width, scale_height }, svg_box_state); |
225 | |
|
226 | 0 | CSSPixelPoint offset = viewbox_offset_and_scale.offset; |
227 | 0 | m_current_viewbox_transform = Gfx::AffineTransform { m_current_viewbox_transform }.multiply(Gfx::AffineTransform {} |
228 | 0 | .translate(offset.to_type<float>()) |
229 | 0 | .scale(viewbox_offset_and_scale.scale_factor_x, viewbox_offset_and_scale.scale_factor_y) |
230 | 0 | .translate({ -viewbox->min_x, -viewbox->min_y })); |
231 | 0 | } |
232 | |
|
233 | 0 | if (svg_box_state.has_definite_width() && svg_box_state.has_definite_height()) { |
234 | | // Scale the box of the viewport based on the parent's viewBox transform. |
235 | | // The viewBox transform is always just a simple scale + offset. |
236 | | // FIXME: Avoid converting SVG box to floats. |
237 | 0 | Gfx::FloatRect svg_rect = { svg_box_state.offset.to_type<float>(), |
238 | 0 | { float(svg_box_state.content_width()), float(svg_box_state.content_height()) } }; |
239 | 0 | svg_rect = m_parent_viewbox_transform.map(svg_rect); |
240 | 0 | svg_box_state.set_content_offset(svg_rect.location().to_type<CSSPixels>()); |
241 | 0 | svg_box_state.set_content_width(CSSPixels(svg_rect.width())); |
242 | 0 | svg_box_state.set_content_height(CSSPixels(svg_rect.height())); |
243 | 0 | svg_box_state.set_has_definite_width(true); |
244 | 0 | svg_box_state.set_has_definite_height(true); |
245 | 0 | } |
246 | |
|
247 | 0 | auto viewport_width = [&] { |
248 | 0 | if (viewbox.has_value()) |
249 | 0 | return CSSPixels::nearest_value_for(viewbox->width); |
250 | 0 | if (svg_box_state.has_definite_width()) |
251 | 0 | return svg_box_state.content_width(); |
252 | 0 | dbgln_if(LIBWEB_CSS_DEBUG, "FIXME: Failed to resolve width of SVG viewport!"); |
253 | 0 | return CSSPixels {}; |
254 | 0 | }(); |
255 | |
|
256 | 0 | auto viewport_height = [&] { |
257 | 0 | if (viewbox.has_value()) |
258 | 0 | return CSSPixels::nearest_value_for(viewbox->height); |
259 | 0 | if (svg_box_state.has_definite_height()) |
260 | 0 | return svg_box_state.content_height(); |
261 | 0 | dbgln_if(LIBWEB_CSS_DEBUG, "FIXME: Failed to resolve height of SVG viewport!"); |
262 | 0 | return CSSPixels {}; |
263 | 0 | }(); |
264 | |
|
265 | 0 | m_available_space = available_space; |
266 | 0 | m_svg_offset = svg_box_state.offset; |
267 | 0 | m_viewport_size = { viewport_width, viewport_height }; |
268 | |
|
269 | 0 | context_box().for_each_child_of_type<Box>([&](Box const& child) { |
270 | 0 | layout_svg_element(child); |
271 | 0 | return IterationDecision::Continue; |
272 | 0 | }); |
273 | 0 | } |
274 | | |
275 | | void SVGFormattingContext::layout_svg_element(Box const& child) |
276 | 0 | { |
277 | 0 | if (is<SVG::SVGViewport>(child.dom_node())) { |
278 | 0 | layout_nested_viewport(child); |
279 | 0 | } else if (is<SVG::SVGForeignObjectElement>(child.dom_node()) && is<BlockContainer>(child)) { |
280 | 0 | Layout::BlockFormattingContext bfc(m_state, LayoutMode::Normal, static_cast<BlockContainer const&>(child), this); |
281 | 0 | bfc.run(*m_available_space); |
282 | 0 | auto& child_state = m_state.get_mutable(child); |
283 | 0 | child_state.set_content_offset(child_state.offset.translated(m_svg_offset)); |
284 | 0 | child.for_each_child_of_type<SVGMaskBox>([&](SVGMaskBox const& child) { |
285 | 0 | layout_svg_element(child); |
286 | 0 | return IterationDecision::Continue; |
287 | 0 | }); |
288 | 0 | } else if (is<SVGGraphicsBox>(child)) { |
289 | 0 | layout_graphics_element(static_cast<SVGGraphicsBox const&>(child)); |
290 | 0 | } |
291 | 0 | } |
292 | | |
293 | | void SVGFormattingContext::layout_nested_viewport(Box const& viewport) |
294 | 0 | { |
295 | | // Layout for a nested SVG viewport. |
296 | | // https://svgwg.org/svg2-draft/coords.html#EstablishingANewSVGViewport. |
297 | 0 | SVGFormattingContext nested_context(m_state, LayoutMode::Normal, viewport, this, m_current_viewbox_transform); |
298 | 0 | auto& nested_viewport_state = m_state.get_mutable(viewport); |
299 | 0 | auto resolve_dimension = [](auto& node, auto size, auto reference_value) { |
300 | | // The value auto for width and height on the ‘svg’ element is treated as 100%. |
301 | | // https://svgwg.org/svg2-draft/geometry.html#Sizing |
302 | 0 | if (size.is_auto()) |
303 | 0 | return reference_value; |
304 | 0 | return size.to_px(node, reference_value); |
305 | 0 | }; |
306 | |
|
307 | 0 | auto nested_viewport_x = viewport.computed_values().x().to_px(viewport, m_viewport_size.width()); |
308 | 0 | auto nested_viewport_y = viewport.computed_values().y().to_px(viewport, m_viewport_size.height()); |
309 | 0 | auto nested_viewport_width = resolve_dimension(viewport, viewport.computed_values().width(), m_viewport_size.width()); |
310 | 0 | auto nested_viewport_height = resolve_dimension(viewport, viewport.computed_values().height(), m_viewport_size.height()); |
311 | 0 | nested_viewport_state.set_content_offset({ nested_viewport_x, nested_viewport_y }); |
312 | 0 | nested_viewport_state.set_content_width(nested_viewport_width); |
313 | 0 | nested_viewport_state.set_content_height(nested_viewport_height); |
314 | 0 | nested_viewport_state.set_has_definite_width(true); |
315 | 0 | nested_viewport_state.set_has_definite_height(true); |
316 | 0 | nested_context.run(*m_available_space); |
317 | 0 | } |
318 | | |
319 | | Gfx::Path SVGFormattingContext::compute_path_for_text(SVGTextBox const& text_box) |
320 | 0 | { |
321 | 0 | auto& text_element = static_cast<SVG::SVGTextPositioningElement const&>(text_box.dom_node()); |
322 | 0 | auto& font = text_box.first_available_font(); |
323 | 0 | auto text_contents = text_element.text_contents(); |
324 | 0 | Utf8View text_utf8 { text_contents }; |
325 | 0 | auto text_width = font.width(text_utf8); |
326 | 0 | auto text_offset = text_element.get_offset(m_viewport_size); |
327 | | |
328 | | // https://svgwg.org/svg2-draft/text.html#TextAnchoringProperties |
329 | 0 | switch (text_element.text_anchor().value_or(SVG::TextAnchor::Start)) { |
330 | 0 | case SVG::TextAnchor::Start: |
331 | | // The rendered characters are aligned such that the start of the resulting rendered text is at the initial |
332 | | // current text position. |
333 | 0 | break; |
334 | 0 | case SVG::TextAnchor::Middle: { |
335 | | // The rendered characters are shifted such that the geometric middle of the resulting rendered text |
336 | | // (determined from the initial and final current text position before applying the text-anchor property) |
337 | | // is at the initial current text position. |
338 | 0 | text_offset.translate_by(-text_width / 2, 0); |
339 | 0 | break; |
340 | 0 | } |
341 | 0 | case SVG::TextAnchor::End: { |
342 | | // The rendered characters are shifted such that the end of the resulting rendered text (final current text |
343 | | // position before applying the text-anchor property) is at the initial current text position. |
344 | 0 | text_offset.translate_by(-text_width, 0); |
345 | 0 | break; |
346 | 0 | } |
347 | 0 | default: |
348 | 0 | VERIFY_NOT_REACHED(); |
349 | 0 | } |
350 | | |
351 | 0 | Gfx::Path path; |
352 | 0 | path.move_to(text_offset); |
353 | 0 | path.text(text_utf8, font); |
354 | 0 | return path; |
355 | 0 | } |
356 | | |
357 | | Gfx::Path SVGFormattingContext::compute_path_for_text_path(SVGTextPathBox const& text_path_box) |
358 | 0 | { |
359 | 0 | auto& text_path_element = static_cast<SVG::SVGTextPathElement const&>(text_path_box.dom_node()); |
360 | 0 | auto path_or_shape = text_path_element.path_or_shape(); |
361 | 0 | if (!path_or_shape) |
362 | 0 | return {}; |
363 | | |
364 | 0 | auto& font = text_path_box.first_available_font(); |
365 | 0 | auto text_contents = text_path_element.text_contents(); |
366 | 0 | Utf8View text_utf8 { text_contents }; |
367 | |
|
368 | 0 | auto shape_path = const_cast<SVG::SVGGeometryElement&>(*path_or_shape).get_path(m_viewport_size); |
369 | 0 | return shape_path.place_text_along(text_utf8, font); |
370 | 0 | } |
371 | | |
372 | | void SVGFormattingContext::layout_path_like_element(SVGGraphicsBox const& graphics_box) |
373 | 0 | { |
374 | 0 | auto& graphics_box_state = m_state.get_mutable(graphics_box); |
375 | 0 | VERIFY(graphics_box_state.computed_svg_transforms().has_value()); |
376 | | |
377 | 0 | auto to_css_pixels_transform = Gfx::AffineTransform {} |
378 | 0 | .multiply(m_current_viewbox_transform) |
379 | 0 | .multiply(graphics_box_state.computed_svg_transforms()->svg_transform()); |
380 | |
|
381 | 0 | Gfx::Path path; |
382 | 0 | if (is<SVGGeometryBox>(graphics_box)) { |
383 | 0 | auto& geometry_box = static_cast<SVGGeometryBox const&>(graphics_box); |
384 | 0 | path = const_cast<SVGGeometryBox&>(geometry_box).dom_node().get_path(m_viewport_size); |
385 | 0 | } else if (is<SVGTextBox>(graphics_box)) { |
386 | 0 | auto& text_box = static_cast<SVGTextBox const&>(graphics_box); |
387 | 0 | path = compute_path_for_text(text_box); |
388 | | // <text> and <tspan> elements can contain more text elements. |
389 | 0 | text_box.for_each_child_of_type<SVGGraphicsBox>([&](auto& child) { |
390 | 0 | if (is<SVGTextBox>(child) || is<SVGTextPathBox>(child)) |
391 | 0 | layout_graphics_element(child); |
392 | 0 | return IterationDecision::Continue; |
393 | 0 | }); |
394 | 0 | } else if (is<SVGTextPathBox>(graphics_box)) { |
395 | | // FIXME: Support <tspan> in <textPath>. |
396 | 0 | path = compute_path_for_text_path(static_cast<SVGTextPathBox const&>(graphics_box)); |
397 | 0 | } |
398 | |
|
399 | 0 | auto path_bounding_box = to_css_pixels_transform.map(path.bounding_box()).to_type<CSSPixels>(); |
400 | | // Stroke increases the path's size by stroke_width/2 per side. |
401 | 0 | CSSPixels stroke_width = CSSPixels::nearest_value_for(graphics_box.dom_node().visible_stroke_width() * m_current_viewbox_transform.x_scale()); |
402 | 0 | path_bounding_box.inflate(stroke_width, stroke_width); |
403 | 0 | graphics_box_state.set_content_offset(path_bounding_box.top_left()); |
404 | 0 | graphics_box_state.set_content_width(path_bounding_box.width()); |
405 | 0 | graphics_box_state.set_content_height(path_bounding_box.height()); |
406 | 0 | graphics_box_state.set_has_definite_width(true); |
407 | 0 | graphics_box_state.set_has_definite_height(true); |
408 | 0 | graphics_box_state.set_computed_svg_path(move(path)); |
409 | 0 | } |
410 | | |
411 | | void SVGFormattingContext::layout_graphics_element(SVGGraphicsBox const& graphics_box) |
412 | 0 | { |
413 | 0 | auto& graphics_box_state = m_state.get_mutable(graphics_box); |
414 | 0 | auto svg_transform = const_cast<SVGGraphicsBox&>(graphics_box).dom_node().get_transform(); |
415 | 0 | graphics_box_state.set_computed_svg_transforms(Painting::SVGGraphicsPaintable::ComputedTransforms(m_current_viewbox_transform, svg_transform)); |
416 | |
|
417 | 0 | if (is_container_element(graphics_box)) { |
418 | | // https://svgwg.org/svg2-draft/struct.html#Groups |
419 | | // 5.2. Grouping: the ‘g’ element |
420 | | // The ‘g’ element is a container element for grouping together related graphics elements. |
421 | 0 | layout_container_element(graphics_box); |
422 | 0 | } else if (is<SVGImageBox>(graphics_box)) { |
423 | 0 | layout_image_element(static_cast<SVGImageBox const&>(graphics_box)); |
424 | 0 | } else { |
425 | | // Assume this is a path-like element. |
426 | 0 | layout_path_like_element(graphics_box); |
427 | 0 | } |
428 | |
|
429 | 0 | if (auto* mask_box = graphics_box.first_child_of_type<SVGMaskBox>()) |
430 | 0 | layout_mask_or_clip(*mask_box); |
431 | |
|
432 | 0 | if (auto* clip_box = graphics_box.first_child_of_type<SVGClipBox>()) |
433 | 0 | layout_mask_or_clip(*clip_box); |
434 | 0 | } |
435 | | |
436 | | void SVGFormattingContext::layout_image_element(SVGImageBox const& image_box) |
437 | 0 | { |
438 | 0 | auto& box_state = m_state.get_mutable(image_box); |
439 | 0 | auto bounding_box = image_box.dom_node().bounding_box(); |
440 | 0 | box_state.set_content_x(bounding_box.x()); |
441 | 0 | box_state.set_content_y(bounding_box.y()); |
442 | 0 | box_state.set_content_width(bounding_box.width()); |
443 | 0 | box_state.set_content_height(bounding_box.height()); |
444 | 0 | box_state.set_has_definite_width(true); |
445 | 0 | box_state.set_has_definite_height(true); |
446 | 0 | } |
447 | | |
448 | | void SVGFormattingContext::layout_mask_or_clip(SVGBox const& mask_or_clip) |
449 | 0 | { |
450 | 0 | SVG::SVGUnits content_units {}; |
451 | 0 | if (is<SVGMaskBox>(mask_or_clip)) |
452 | 0 | content_units = static_cast<SVGMaskBox const&>(mask_or_clip).dom_node().mask_content_units(); |
453 | 0 | else if (is<SVGClipBox>(mask_or_clip)) |
454 | 0 | content_units = static_cast<SVGClipBox const&>(mask_or_clip).dom_node().clip_path_units(); |
455 | 0 | else |
456 | 0 | VERIFY_NOT_REACHED(); |
457 | | // FIXME: Somehow limit <clipPath> contents to: shape elements, <text>, and <use>. |
458 | 0 | auto& layout_state = m_state.get_mutable(mask_or_clip); |
459 | 0 | auto parent_viewbox_transform = m_current_viewbox_transform; |
460 | 0 | if (content_units == SVG::SVGUnits::ObjectBoundingBox) { |
461 | 0 | auto* parent_node = mask_or_clip.parent(); |
462 | 0 | auto& parent_node_state = m_state.get(*parent_node); |
463 | 0 | layout_state.set_content_width(parent_node_state.content_width()); |
464 | 0 | layout_state.set_content_height(parent_node_state.content_height()); |
465 | 0 | parent_viewbox_transform = Gfx::AffineTransform {}.translate(parent_node_state.offset.to_type<float>()); |
466 | 0 | } else { |
467 | 0 | layout_state.set_content_width(m_viewport_size.width()); |
468 | 0 | layout_state.set_content_height(m_viewport_size.height()); |
469 | 0 | } |
470 | | // Pretend masks/clips are a viewport so we can scale the contents depending on the `contentUnits`. |
471 | 0 | SVGFormattingContext nested_context(m_state, LayoutMode::Normal, mask_or_clip, this, parent_viewbox_transform); |
472 | 0 | layout_state.set_has_definite_width(true); |
473 | 0 | layout_state.set_has_definite_height(true); |
474 | 0 | nested_context.run(*m_available_space); |
475 | 0 | } |
476 | | |
477 | | void SVGFormattingContext::layout_container_element(SVGBox const& container) |
478 | 0 | { |
479 | 0 | auto& box_state = m_state.get_mutable(container); |
480 | 0 | Gfx::BoundingBox<CSSPixels> bounding_box; |
481 | 0 | container.for_each_child_of_type<Box>([&](Box const& child) { |
482 | | // Masks/clips do not change the bounding box of their parents. |
483 | 0 | if (is<SVGMaskBox>(child) || is<SVGClipBox>(child)) |
484 | 0 | return IterationDecision::Continue; |
485 | 0 | layout_svg_element(child); |
486 | 0 | auto& child_state = m_state.get(child); |
487 | 0 | bounding_box.add_point(child_state.offset); |
488 | 0 | bounding_box.add_point(child_state.offset.translated(child_state.content_width(), child_state.content_height())); |
489 | 0 | return IterationDecision::Continue; |
490 | 0 | }); |
491 | 0 | box_state.set_content_x(bounding_box.x()); |
492 | 0 | box_state.set_content_y(bounding_box.y()); |
493 | 0 | box_state.set_content_width(bounding_box.width()); |
494 | 0 | box_state.set_content_height(bounding_box.height()); |
495 | 0 | box_state.set_has_definite_width(true); |
496 | 0 | box_state.set_has_definite_height(true); |
497 | 0 | } |
498 | | |
499 | | } |