/src/serenity/Userland/Libraries/LibWeb/Painting/InlinePaintable.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <LibGfx/AntiAliasingPainter.h> |
8 | | #include <LibWeb/DOM/Document.h> |
9 | | #include <LibWeb/DOM/Range.h> |
10 | | #include <LibWeb/Layout/BlockContainer.h> |
11 | | #include <LibWeb/Painting/BackgroundPainting.h> |
12 | | #include <LibWeb/Painting/InlinePaintable.h> |
13 | | #include <LibWeb/Painting/TextPaintable.h> |
14 | | #include <LibWeb/Selection/Selection.h> |
15 | | |
16 | | namespace Web::Painting { |
17 | | |
18 | | JS_DEFINE_ALLOCATOR(InlinePaintable); |
19 | | |
20 | | JS::NonnullGCPtr<InlinePaintable> InlinePaintable::create(Layout::InlineNode const& layout_node) |
21 | 0 | { |
22 | 0 | return layout_node.heap().allocate_without_realm<InlinePaintable>(layout_node); |
23 | 0 | } |
24 | | |
25 | | InlinePaintable::InlinePaintable(Layout::InlineNode const& layout_node) |
26 | 0 | : Paintable(layout_node) |
27 | 0 | { |
28 | 0 | } |
29 | | |
30 | | Layout::InlineNode const& InlinePaintable::layout_node() const |
31 | 0 | { |
32 | 0 | return static_cast<Layout::InlineNode const&>(Paintable::layout_node()); |
33 | 0 | } |
34 | | |
35 | | void InlinePaintable::before_paint(PaintContext& context, PaintPhase) const |
36 | 0 | { |
37 | 0 | if (scroll_frame_id().has_value()) { |
38 | 0 | context.display_list_recorder().save(); |
39 | 0 | context.display_list_recorder().set_scroll_frame_id(scroll_frame_id().value()); |
40 | 0 | } |
41 | 0 | if (clip_rect().has_value()) { |
42 | 0 | context.display_list_recorder().save(); |
43 | 0 | context.display_list_recorder().add_clip_rect(context.enclosing_device_rect(*clip_rect()).to_type<int>()); |
44 | 0 | } |
45 | 0 | } |
46 | | |
47 | | void InlinePaintable::after_paint(PaintContext& context, PaintPhase) const |
48 | 0 | { |
49 | 0 | if (clip_rect().has_value()) |
50 | 0 | context.display_list_recorder().restore(); |
51 | 0 | if (scroll_frame_id().has_value()) |
52 | 0 | context.display_list_recorder().restore(); |
53 | 0 | } |
54 | | |
55 | | void InlinePaintable::paint(PaintContext& context, PaintPhase phase) const |
56 | 0 | { |
57 | 0 | auto& display_list_recorder = context.display_list_recorder(); |
58 | |
|
59 | 0 | if (phase == PaintPhase::Background) { |
60 | 0 | auto containing_block_position_in_absolute_coordinates = containing_block()->absolute_position(); |
61 | |
|
62 | 0 | for_each_fragment([&](auto const& fragment, bool is_first_fragment, bool is_last_fragment) { |
63 | 0 | CSSPixelRect absolute_fragment_rect { containing_block_position_in_absolute_coordinates.translated(fragment.offset()), fragment.size() }; |
64 | |
|
65 | 0 | if (is_first_fragment) { |
66 | 0 | auto extra_start_width = box_model().padding.left; |
67 | 0 | absolute_fragment_rect.translate_by(-extra_start_width, 0); |
68 | 0 | absolute_fragment_rect.set_width(absolute_fragment_rect.width() + extra_start_width); |
69 | 0 | } |
70 | |
|
71 | 0 | if (is_last_fragment) { |
72 | 0 | auto extra_end_width = box_model().padding.right; |
73 | 0 | absolute_fragment_rect.set_width(absolute_fragment_rect.width() + extra_end_width); |
74 | 0 | } |
75 | |
|
76 | 0 | absolute_fragment_rect.translate_by(0, -box_model().padding.top); |
77 | 0 | absolute_fragment_rect.set_height(absolute_fragment_rect.height() + box_model().padding.top + box_model().padding.bottom); |
78 | |
|
79 | 0 | auto const& border_radii_data = fragment.border_radii_data(); |
80 | 0 | paint_background(context, layout_node(), absolute_fragment_rect, computed_values().background_color(), computed_values().image_rendering(), &computed_values().background_layers(), border_radii_data); |
81 | |
|
82 | 0 | if (!box_shadow_data().is_empty()) { |
83 | 0 | auto borders_data = BordersData { |
84 | 0 | .top = computed_values().border_top(), |
85 | 0 | .right = computed_values().border_right(), |
86 | 0 | .bottom = computed_values().border_bottom(), |
87 | 0 | .left = computed_values().border_left(), |
88 | 0 | }; |
89 | 0 | auto absolute_fragment_rect_bordered = absolute_fragment_rect.inflated( |
90 | 0 | borders_data.top.width, borders_data.right.width, |
91 | 0 | borders_data.bottom.width, borders_data.left.width); |
92 | 0 | paint_box_shadow(context, absolute_fragment_rect_bordered, absolute_fragment_rect, |
93 | 0 | borders_data, border_radii_data, box_shadow_data()); |
94 | 0 | } |
95 | |
|
96 | 0 | return IterationDecision::Continue; |
97 | 0 | }); |
98 | 0 | } |
99 | |
|
100 | 0 | auto paint_border_or_outline = [&](Optional<BordersData> outline_data = {}, CSSPixels outline_offset = 0) { |
101 | 0 | auto borders_data = BordersData { |
102 | 0 | .top = computed_values().border_top(), |
103 | 0 | .right = computed_values().border_right(), |
104 | 0 | .bottom = computed_values().border_bottom(), |
105 | 0 | .left = computed_values().border_left(), |
106 | 0 | }; |
107 | |
|
108 | 0 | auto containing_block_position_in_absolute_coordinates = containing_block()->absolute_position(); |
109 | |
|
110 | 0 | for_each_fragment([&](auto const& fragment, bool is_first_fragment, bool is_last_fragment) { |
111 | 0 | CSSPixelRect absolute_fragment_rect { containing_block_position_in_absolute_coordinates.translated(fragment.offset()), fragment.size() }; |
112 | |
|
113 | 0 | if (is_first_fragment) { |
114 | 0 | auto extra_start_width = box_model().padding.left; |
115 | 0 | absolute_fragment_rect.translate_by(-extra_start_width, 0); |
116 | 0 | absolute_fragment_rect.set_width(absolute_fragment_rect.width() + extra_start_width); |
117 | 0 | } |
118 | |
|
119 | 0 | if (is_last_fragment) { |
120 | 0 | auto extra_end_width = box_model().padding.right; |
121 | 0 | absolute_fragment_rect.set_width(absolute_fragment_rect.width() + extra_end_width); |
122 | 0 | } |
123 | |
|
124 | 0 | absolute_fragment_rect.translate_by(0, -box_model().padding.top); |
125 | 0 | absolute_fragment_rect.set_height(absolute_fragment_rect.height() + box_model().padding.top + box_model().padding.bottom); |
126 | |
|
127 | 0 | auto borders_rect = absolute_fragment_rect.inflated(borders_data.top.width, borders_data.right.width, borders_data.bottom.width, borders_data.left.width); |
128 | 0 | auto border_radii_data = fragment.border_radii_data(); |
129 | |
|
130 | 0 | if (outline_data.has_value()) { |
131 | 0 | auto outline_offset_x = outline_offset; |
132 | 0 | auto outline_offset_y = outline_offset; |
133 | | // "Both the height and the width of the outside of the shape drawn by the outline should not |
134 | | // become smaller than twice the computed value of the outline-width property to make sure |
135 | | // that an outline can be rendered even with large negative values." |
136 | | // https://www.w3.org/TR/css-ui-4/#outline-offset |
137 | | // So, if the horizontal outline offset is > half the borders_rect's width then we set it to that. |
138 | | // (And the same for y) |
139 | 0 | if ((borders_rect.width() / 2) + outline_offset_x < 0) |
140 | 0 | outline_offset_x = -borders_rect.width() / 2; |
141 | 0 | if ((borders_rect.height() / 2) + outline_offset_y < 0) |
142 | 0 | outline_offset_y = -borders_rect.height() / 2; |
143 | |
|
144 | 0 | border_radii_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); |
145 | 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); |
146 | 0 | paint_all_borders(context.display_list_recorder(), context.rounded_device_rect(borders_rect), border_radii_data.as_corners(context), outline_data->to_device_pixels(context)); |
147 | 0 | } else { |
148 | 0 | paint_all_borders(context.display_list_recorder(), context.rounded_device_rect(borders_rect), border_radii_data.as_corners(context), borders_data.to_device_pixels(context)); |
149 | 0 | } |
150 | |
|
151 | 0 | return IterationDecision::Continue; |
152 | 0 | }); |
153 | 0 | }; |
154 | |
|
155 | 0 | if (phase == PaintPhase::Border) { |
156 | 0 | paint_border_or_outline(); |
157 | 0 | } |
158 | |
|
159 | 0 | if (phase == PaintPhase::Outline) { |
160 | 0 | auto maybe_outline_data = this->outline_data(); |
161 | 0 | if (maybe_outline_data.has_value()) |
162 | 0 | paint_border_or_outline(maybe_outline_data.value(), computed_values().outline_offset().to_px(layout_node())); |
163 | 0 | } |
164 | |
|
165 | 0 | if (phase == PaintPhase::Foreground) { |
166 | 0 | for_each_fragment([&](auto const& fragment, bool, bool) { |
167 | 0 | if (is<TextPaintable>(fragment.paintable())) |
168 | 0 | paint_text_fragment(context, static_cast<TextPaintable const&>(fragment.paintable()), fragment, phase); |
169 | 0 | }); |
170 | 0 | } |
171 | |
|
172 | 0 | if (phase == PaintPhase::Overlay && layout_node().document().inspected_layout_node() == &layout_node()) { |
173 | | // FIXME: This paints a double-thick border between adjacent fragments, where ideally there |
174 | | // would be none. Once we implement non-rectangular outlines for the `outline` CSS |
175 | | // property, we can use that here instead. |
176 | 0 | for_each_fragment([&](auto const& fragment, bool, bool) { |
177 | 0 | display_list_recorder.draw_rect(context.enclosing_device_rect(fragment.absolute_rect()).template to_type<int>(), Color::Magenta); |
178 | 0 | return IterationDecision::Continue; |
179 | 0 | }); |
180 | 0 | } |
181 | 0 | } |
182 | | |
183 | | template<typename Callback> |
184 | | void InlinePaintable::for_each_fragment(Callback callback) const |
185 | 0 | { |
186 | 0 | for (size_t i = 0; i < m_fragments.size(); ++i) { |
187 | 0 | auto const& fragment = m_fragments[i]; |
188 | 0 | callback(fragment, i == 0, i == m_fragments.size() - 1); |
189 | 0 | } |
190 | 0 | } Unexecuted instantiation: InlinePaintable.cpp:void Web::Painting::InlinePaintable::for_each_fragment<Web::Painting::InlinePaintable::paint(Web::PaintContext&, Web::Painting::PaintPhase) const::$_1::operator()(AK::Optional<Web::Painting::BordersData>, Web::CSSPixels) const::{lambda(auto:1 const&, bool, bool)#1}>(Web::Painting::InlinePaintable::paint(Web::PaintContext&, Web::Painting::PaintPhase) const::$_1::operator()(AK::Optional<Web::Painting::BordersData>, Web::CSSPixels) const::{lambda(auto:1 const&, bool, bool)#1}) constUnexecuted instantiation: InlinePaintable.cpp:void Web::Painting::InlinePaintable::for_each_fragment<Web::Painting::InlinePaintable::paint(Web::PaintContext&, Web::Painting::PaintPhase) const::$_0>(Web::Painting::InlinePaintable::paint(Web::PaintContext&, Web::Painting::PaintPhase) const::$_0) const Unexecuted instantiation: InlinePaintable.cpp:void Web::Painting::InlinePaintable::for_each_fragment<Web::Painting::InlinePaintable::paint(Web::PaintContext&, Web::Painting::PaintPhase) const::$_2>(Web::Painting::InlinePaintable::paint(Web::PaintContext&, Web::Painting::PaintPhase) const::$_2) const Unexecuted instantiation: InlinePaintable.cpp:void Web::Painting::InlinePaintable::for_each_fragment<Web::Painting::InlinePaintable::paint(Web::PaintContext&, Web::Painting::PaintPhase) const::$_3>(Web::Painting::InlinePaintable::paint(Web::PaintContext&, Web::Painting::PaintPhase) const::$_3) const Unexecuted instantiation: InlinePaintable.cpp:void Web::Painting::InlinePaintable::for_each_fragment<Web::Painting::InlinePaintable::bounding_rect() const::$_0>(Web::Painting::InlinePaintable::bounding_rect() const::$_0) const |
191 | | |
192 | | TraversalDecision InlinePaintable::hit_test(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const |
193 | 0 | { |
194 | 0 | if (clip_rect().has_value() && !clip_rect().value().contains(position)) |
195 | 0 | return TraversalDecision::Continue; |
196 | | |
197 | 0 | auto position_adjusted_by_scroll_offset = position; |
198 | 0 | if (enclosing_scroll_frame_offset().has_value()) |
199 | 0 | position_adjusted_by_scroll_offset.translate_by(-enclosing_scroll_frame_offset().value()); |
200 | |
|
201 | 0 | for (auto const& fragment : m_fragments) { |
202 | 0 | if (fragment.paintable().stacking_context()) |
203 | 0 | continue; |
204 | 0 | auto fragment_absolute_rect = fragment.absolute_rect(); |
205 | 0 | if (fragment_absolute_rect.contains(position_adjusted_by_scroll_offset)) { |
206 | 0 | if (fragment.paintable().hit_test(position, type, callback) == TraversalDecision::Break) |
207 | 0 | return TraversalDecision::Break; |
208 | 0 | auto hit_test_result = HitTestResult { const_cast<Paintable&>(fragment.paintable()), fragment.text_index_at(position_adjusted_by_scroll_offset.x()) }; |
209 | 0 | if (callback(hit_test_result) == TraversalDecision::Break) |
210 | 0 | return TraversalDecision::Break; |
211 | 0 | } else if (type == HitTestType::TextCursor) { |
212 | 0 | auto const* common_ancestor_parent = [&]() -> DOM::Node const* { |
213 | 0 | auto selection = document().get_selection(); |
214 | 0 | if (!selection) |
215 | 0 | return nullptr; |
216 | 0 | auto range = selection->range(); |
217 | 0 | if (!range) |
218 | 0 | return nullptr; |
219 | 0 | auto common_ancestor = range->common_ancestor_container(); |
220 | 0 | if (common_ancestor->parent()) |
221 | 0 | return common_ancestor->parent(); |
222 | 0 | return common_ancestor; |
223 | 0 | }(); |
224 | |
|
225 | 0 | auto const* fragment_dom_node = fragment.layout_node().dom_node(); |
226 | 0 | if (common_ancestor_parent && fragment_dom_node && common_ancestor_parent->is_ancestor_of(*fragment_dom_node)) { |
227 | | // If we reached this point, the position is not within the fragment. However, the fragment start or end might be |
228 | | // the place to place the cursor. To determine the best place, we first find the closest fragment horizontally to |
229 | | // the cursor. If we could not find one, then find for the closest vertically above the cursor. |
230 | | // If we knew the direction of selection, we would look above if selecting upward. |
231 | 0 | if (fragment_absolute_rect.bottom() - 1 <= position_adjusted_by_scroll_offset.y()) { // fully below the fragment |
232 | 0 | HitTestResult hit_test_result { |
233 | 0 | .paintable = const_cast<Paintable&>(fragment.paintable()), |
234 | 0 | .index_in_node = fragment.start() + fragment.length(), |
235 | 0 | .vertical_distance = position_adjusted_by_scroll_offset.y() - fragment_absolute_rect.bottom(), |
236 | 0 | }; |
237 | 0 | if (callback(hit_test_result) == TraversalDecision::Break) |
238 | 0 | return TraversalDecision::Break; |
239 | 0 | } else if (fragment_absolute_rect.top() <= position_adjusted_by_scroll_offset.y()) { // vertically within the fragment |
240 | 0 | if (position_adjusted_by_scroll_offset.x() < fragment_absolute_rect.left()) { |
241 | 0 | HitTestResult hit_test_result { |
242 | 0 | .paintable = const_cast<Paintable&>(fragment.paintable()), |
243 | 0 | .index_in_node = fragment.start(), |
244 | 0 | .vertical_distance = 0, |
245 | 0 | .horizontal_distance = fragment_absolute_rect.left() - position_adjusted_by_scroll_offset.x(), |
246 | 0 | }; |
247 | 0 | if (callback(hit_test_result) == TraversalDecision::Break) |
248 | 0 | return TraversalDecision::Break; |
249 | 0 | } else if (position_adjusted_by_scroll_offset.x() > fragment_absolute_rect.right()) { |
250 | 0 | HitTestResult hit_test_result { |
251 | 0 | .paintable = const_cast<Paintable&>(fragment.paintable()), |
252 | 0 | .index_in_node = fragment.start() + fragment.length(), |
253 | 0 | .vertical_distance = 0, |
254 | 0 | .horizontal_distance = position_adjusted_by_scroll_offset.x() - fragment_absolute_rect.right(), |
255 | 0 | }; |
256 | 0 | if (callback(hit_test_result) == TraversalDecision::Break) |
257 | 0 | return TraversalDecision::Break; |
258 | 0 | } |
259 | 0 | } |
260 | 0 | } |
261 | 0 | } |
262 | 0 | } |
263 | | |
264 | 0 | bool should_exit = false; |
265 | 0 | for_each_child([&](Paintable const& child) { |
266 | 0 | if (child.stacking_context()) |
267 | 0 | return IterationDecision::Continue; |
268 | 0 | if (child.hit_test(position, type, callback) == TraversalDecision::Break) { |
269 | 0 | should_exit = true; |
270 | 0 | return IterationDecision::Break; |
271 | 0 | } |
272 | 0 | return IterationDecision::Continue; |
273 | 0 | }); |
274 | |
|
275 | 0 | if (should_exit) |
276 | 0 | return TraversalDecision::Break; |
277 | | |
278 | 0 | return TraversalDecision::Continue; |
279 | 0 | } |
280 | | |
281 | | CSSPixelRect InlinePaintable::bounding_rect() const |
282 | 0 | { |
283 | 0 | CSSPixelRect bounding_rect; |
284 | 0 | for_each_fragment([&](auto const& fragment, bool, bool) { |
285 | 0 | auto fragment_absolute_rect = fragment.absolute_rect(); |
286 | 0 | bounding_rect = bounding_rect.united(fragment_absolute_rect); |
287 | 0 | }); |
288 | |
|
289 | 0 | if (bounding_rect.is_empty()) { |
290 | | // FIXME: This is adhoc, and we should return rect of empty fragment instead. |
291 | 0 | auto containing_block_position_in_absolute_coordinates = containing_block()->absolute_position(); |
292 | 0 | return { containing_block_position_in_absolute_coordinates, { 0, 0 } }; |
293 | 0 | } |
294 | 0 | return bounding_rect; |
295 | 0 | } |
296 | | |
297 | | void InlinePaintable::resolve_paint_properties() |
298 | 0 | { |
299 | 0 | auto const& computed_values = this->computed_values(); |
300 | 0 | auto const& layout_node = this->layout_node(); |
301 | 0 | auto& fragments = this->fragments(); |
302 | | |
303 | | // Border radii |
304 | 0 | auto const& top_left_border_radius = computed_values.border_top_left_radius(); |
305 | 0 | auto const& top_right_border_radius = computed_values.border_top_right_radius(); |
306 | 0 | auto const& bottom_right_border_radius = computed_values.border_bottom_right_radius(); |
307 | 0 | auto const& bottom_left_border_radius = computed_values.border_bottom_left_radius(); |
308 | 0 | auto containing_block_position_in_absolute_coordinates = containing_block()->absolute_position(); |
309 | 0 | for (size_t i = 0; i < fragments.size(); ++i) { |
310 | 0 | auto is_first_fragment = i == 0; |
311 | 0 | auto is_last_fragment = i == fragments.size() - 1; |
312 | 0 | auto& fragment = fragments[i]; |
313 | 0 | CSSPixelRect absolute_fragment_rect { |
314 | 0 | containing_block_position_in_absolute_coordinates.translated(fragment.offset()), |
315 | 0 | fragment.size() |
316 | 0 | }; |
317 | 0 | if (is_first_fragment) { |
318 | 0 | auto extra_start_width = box_model().padding.left; |
319 | 0 | absolute_fragment_rect.translate_by(-extra_start_width, 0); |
320 | 0 | absolute_fragment_rect.set_width(absolute_fragment_rect.width() + extra_start_width); |
321 | 0 | } |
322 | 0 | if (is_last_fragment) { |
323 | 0 | auto extra_end_width = box_model().padding.right; |
324 | 0 | absolute_fragment_rect.set_width(absolute_fragment_rect.width() + extra_end_width); |
325 | 0 | } |
326 | 0 | auto border_radii_data = normalize_border_radii_data(layout_node, |
327 | 0 | absolute_fragment_rect, top_left_border_radius, |
328 | 0 | top_right_border_radius, |
329 | 0 | bottom_right_border_radius, |
330 | 0 | bottom_left_border_radius); |
331 | 0 | fragment.set_border_radii_data(border_radii_data); |
332 | 0 | } |
333 | |
|
334 | 0 | auto const& box_shadow_data = computed_values.box_shadow(); |
335 | 0 | Vector<Painting::ShadowData> resolved_box_shadow_data; |
336 | 0 | resolved_box_shadow_data.ensure_capacity(box_shadow_data.size()); |
337 | 0 | for (auto const& layer : box_shadow_data) { |
338 | 0 | resolved_box_shadow_data.empend( |
339 | 0 | layer.color, |
340 | 0 | layer.offset_x.to_px(layout_node), |
341 | 0 | layer.offset_y.to_px(layout_node), |
342 | 0 | layer.blur_radius.to_px(layout_node), |
343 | 0 | layer.spread_distance.to_px(layout_node), |
344 | 0 | layer.placement == CSS::ShadowPlacement::Outer ? Painting::ShadowPlacement::Outer |
345 | 0 | : Painting::ShadowPlacement::Inner); |
346 | 0 | } |
347 | 0 | set_box_shadow_data(move(resolved_box_shadow_data)); |
348 | |
|
349 | 0 | for (auto const& fragment : fragments) { |
350 | 0 | auto const& text_shadow = fragment.m_layout_node->computed_values().text_shadow(); |
351 | 0 | if (!text_shadow.is_empty()) { |
352 | 0 | Vector<Painting::ShadowData> resolved_shadow_data; |
353 | 0 | resolved_shadow_data.ensure_capacity(text_shadow.size()); |
354 | 0 | for (auto const& layer : text_shadow) { |
355 | 0 | resolved_shadow_data.empend( |
356 | 0 | layer.color, |
357 | 0 | layer.offset_x.to_px(layout_node), |
358 | 0 | layer.offset_y.to_px(layout_node), |
359 | 0 | layer.blur_radius.to_px(layout_node), |
360 | 0 | layer.spread_distance.to_px(layout_node), |
361 | 0 | Painting::ShadowPlacement::Outer); |
362 | 0 | } |
363 | 0 | const_cast<Painting::PaintableFragment&>(fragment).set_shadows(move(resolved_shadow_data)); |
364 | 0 | } |
365 | 0 | } |
366 | | |
367 | | // Outlines |
368 | 0 | auto outline_width = computed_values.outline_width().to_px(layout_node); |
369 | 0 | auto outline_data = borders_data_for_outline(layout_node, computed_values.outline_color(), computed_values.outline_style(), outline_width); |
370 | 0 | auto outline_offset = computed_values.outline_offset().to_px(layout_node); |
371 | 0 | set_outline_data(outline_data); |
372 | 0 | set_outline_offset(outline_offset); |
373 | |
|
374 | 0 | auto combined_transform = compute_combined_css_transform(); |
375 | 0 | set_combined_css_transform(combined_transform); |
376 | 0 | } |
377 | | |
378 | | } |