/src/serenity/Userland/Libraries/LibWeb/Layout/InlineLevelIterator.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright (c) 2022, Andreas Kling <kling@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <LibWeb/Layout/BreakNode.h> |
8 | | #include <LibWeb/Layout/InlineFormattingContext.h> |
9 | | #include <LibWeb/Layout/InlineLevelIterator.h> |
10 | | #include <LibWeb/Layout/InlineNode.h> |
11 | | #include <LibWeb/Layout/ListItemMarkerBox.h> |
12 | | #include <LibWeb/Layout/ReplacedBox.h> |
13 | | |
14 | | namespace Web::Layout { |
15 | | |
16 | | InlineLevelIterator::InlineLevelIterator(Layout::InlineFormattingContext& inline_formatting_context, Layout::LayoutState& layout_state, Layout::BlockContainer const& containing_block, LayoutState::UsedValues const& containing_block_used_values, LayoutMode layout_mode) |
17 | 0 | : m_inline_formatting_context(inline_formatting_context) |
18 | 0 | , m_layout_state(layout_state) |
19 | 0 | , m_containing_block(containing_block) |
20 | 0 | , m_containing_block_used_values(containing_block_used_values) |
21 | 0 | , m_next_node(containing_block.first_child()) |
22 | 0 | , m_layout_mode(layout_mode) |
23 | 0 | { |
24 | 0 | skip_to_next(); |
25 | 0 | } |
26 | | |
27 | | void InlineLevelIterator::enter_node_with_box_model_metrics(Layout::NodeWithStyleAndBoxModelMetrics const& node) |
28 | 0 | { |
29 | 0 | if (!m_extra_leading_metrics.has_value()) |
30 | 0 | m_extra_leading_metrics = ExtraBoxMetrics {}; |
31 | | |
32 | | // FIXME: It's really weird that *this* is where we assign box model metrics for these layout nodes.. |
33 | |
|
34 | 0 | auto& used_values = m_layout_state.get_mutable(node); |
35 | 0 | auto const& computed_values = node.computed_values(); |
36 | |
|
37 | 0 | used_values.margin_left = computed_values.margin().left().to_px(node, m_containing_block_used_values.content_width()); |
38 | 0 | used_values.border_left = computed_values.border_left().width; |
39 | 0 | used_values.padding_left = computed_values.padding().left().to_px(node, m_containing_block_used_values.content_width()); |
40 | |
|
41 | 0 | used_values.padding_bottom = computed_values.padding().bottom().to_px(node, m_containing_block_used_values.content_width()); |
42 | 0 | used_values.padding_top = computed_values.padding().top().to_px(node, m_containing_block_used_values.content_width()); |
43 | |
|
44 | 0 | m_extra_leading_metrics->margin += used_values.margin_left; |
45 | 0 | m_extra_leading_metrics->border += used_values.border_left; |
46 | 0 | m_extra_leading_metrics->padding += used_values.padding_left; |
47 | | |
48 | | // Now's our chance to resolve the inset properties for this node. |
49 | 0 | m_inline_formatting_context.compute_inset(node); |
50 | |
|
51 | 0 | m_box_model_node_stack.append(node); |
52 | 0 | } |
53 | | |
54 | | void InlineLevelIterator::exit_node_with_box_model_metrics() |
55 | 0 | { |
56 | 0 | if (!m_extra_trailing_metrics.has_value()) |
57 | 0 | m_extra_trailing_metrics = ExtraBoxMetrics {}; |
58 | |
|
59 | 0 | auto& node = m_box_model_node_stack.last(); |
60 | 0 | auto& used_values = m_layout_state.get_mutable(node); |
61 | 0 | auto const& computed_values = node->computed_values(); |
62 | |
|
63 | 0 | used_values.margin_right = computed_values.margin().right().to_px(node, m_containing_block_used_values.content_width()); |
64 | 0 | used_values.border_right = computed_values.border_right().width; |
65 | 0 | used_values.padding_right = computed_values.padding().right().to_px(node, m_containing_block_used_values.content_width()); |
66 | |
|
67 | 0 | m_extra_trailing_metrics->margin += used_values.margin_right; |
68 | 0 | m_extra_trailing_metrics->border += used_values.border_right; |
69 | 0 | m_extra_trailing_metrics->padding += used_values.padding_right; |
70 | |
|
71 | 0 | m_box_model_node_stack.take_last(); |
72 | 0 | } |
73 | | |
74 | | // This is similar to Layout::Node::next_in_pre_order() but will not descend into inline-block nodes. |
75 | | Layout::Node const* InlineLevelIterator::next_inline_node_in_pre_order(Layout::Node const& current, Layout::Node const* stay_within) |
76 | 0 | { |
77 | 0 | if (current.first_child() |
78 | 0 | && current.first_child()->display().is_inline_outside() |
79 | 0 | && current.display().is_flow_inside() |
80 | 0 | && !current.is_replaced_box()) { |
81 | 0 | if (!current.is_box() || !static_cast<Box const&>(current).is_out_of_flow(m_inline_formatting_context)) |
82 | 0 | return current.first_child(); |
83 | 0 | } |
84 | | |
85 | 0 | Layout::Node const* node = ¤t; |
86 | 0 | Layout::Node const* next = nullptr; |
87 | 0 | while (!(next = node->next_sibling())) { |
88 | 0 | node = node->parent(); |
89 | | |
90 | | // If node is the last node on the "box model node stack", pop it off. |
91 | 0 | if (!m_box_model_node_stack.is_empty() |
92 | 0 | && m_box_model_node_stack.last() == node) { |
93 | 0 | exit_node_with_box_model_metrics(); |
94 | 0 | } |
95 | 0 | if (!node || node == stay_within) |
96 | 0 | return nullptr; |
97 | 0 | } |
98 | | |
99 | | // If node is the last node on the "box model node stack", pop it off. |
100 | 0 | if (!m_box_model_node_stack.is_empty() |
101 | 0 | && m_box_model_node_stack.last() == node) { |
102 | 0 | exit_node_with_box_model_metrics(); |
103 | 0 | } |
104 | |
|
105 | 0 | return next; |
106 | 0 | } |
107 | | |
108 | | void InlineLevelIterator::compute_next() |
109 | 0 | { |
110 | 0 | if (m_next_node == nullptr) |
111 | 0 | return; |
112 | 0 | do { |
113 | 0 | m_next_node = next_inline_node_in_pre_order(*m_next_node, m_containing_block); |
114 | 0 | if (m_next_node && m_next_node->is_svg_mask_box()) { |
115 | | // NOTE: It is possible to encounter SVGMaskBox nodes while doing layout of formatting context established by <foreignObject> with a mask. |
116 | | // We should skip and let SVGFormattingContext take care of them. |
117 | 0 | m_next_node = m_next_node->next_sibling(); |
118 | 0 | } |
119 | 0 | } while (m_next_node && (!m_next_node->is_inline() && !m_next_node->is_out_of_flow(m_inline_formatting_context))); |
120 | 0 | } |
121 | | |
122 | | void InlineLevelIterator::skip_to_next() |
123 | 0 | { |
124 | 0 | if (m_next_node |
125 | 0 | && is<Layout::NodeWithStyleAndBoxModelMetrics>(*m_next_node) |
126 | 0 | && m_next_node->display().is_flow_inside() |
127 | 0 | && !m_next_node->is_out_of_flow(m_inline_formatting_context) |
128 | 0 | && !m_next_node->is_replaced_box()) |
129 | 0 | enter_node_with_box_model_metrics(static_cast<Layout::NodeWithStyleAndBoxModelMetrics const&>(*m_next_node)); |
130 | |
|
131 | 0 | m_current_node = m_next_node; |
132 | 0 | compute_next(); |
133 | 0 | } |
134 | | |
135 | | Optional<InlineLevelIterator::Item> InlineLevelIterator::next() |
136 | 0 | { |
137 | 0 | if (m_lookahead_items.is_empty()) |
138 | 0 | return next_without_lookahead(); |
139 | 0 | return m_lookahead_items.dequeue(); |
140 | 0 | } |
141 | | |
142 | | CSSPixels InlineLevelIterator::next_non_whitespace_sequence_width() |
143 | 0 | { |
144 | 0 | CSSPixels next_width = 0; |
145 | 0 | for (;;) { |
146 | 0 | auto next_item_opt = next_without_lookahead(); |
147 | 0 | if (!next_item_opt.has_value()) |
148 | 0 | break; |
149 | 0 | m_lookahead_items.enqueue(next_item_opt.release_value()); |
150 | 0 | auto& next_item = m_lookahead_items.tail(); |
151 | 0 | if (next_item.type == InlineLevelIterator::Item::Type::ForcedBreak) |
152 | 0 | break; |
153 | 0 | if (next_item.node->computed_values().white_space() != CSS::WhiteSpace::Nowrap) { |
154 | 0 | if (next_item.type != InlineLevelIterator::Item::Type::Text) |
155 | 0 | break; |
156 | 0 | if (next_item.is_collapsible_whitespace) |
157 | 0 | break; |
158 | 0 | auto& next_text_node = verify_cast<Layout::TextNode>(*(next_item.node)); |
159 | 0 | auto next_view = next_text_node.text_for_rendering().bytes_as_string_view().substring_view(next_item.offset_in_node, next_item.length_in_node); |
160 | 0 | if (next_view.is_whitespace()) |
161 | 0 | break; |
162 | 0 | } |
163 | 0 | next_width += next_item.border_box_width(); |
164 | 0 | } |
165 | 0 | return next_width; |
166 | 0 | } |
167 | | |
168 | | Gfx::GlyphRun::TextType InlineLevelIterator::resolve_text_direction_from_context() |
169 | 0 | { |
170 | 0 | VERIFY(m_text_node_context.has_value()); |
171 | | |
172 | 0 | Optional<Gfx::GlyphRun::TextType> next_known_direction; |
173 | 0 | for (size_t i = 0;; ++i) { |
174 | 0 | auto peek = m_text_node_context->chunk_iterator.peek(i); |
175 | 0 | if (!peek.has_value()) |
176 | 0 | break; |
177 | 0 | if (peek->text_type == Gfx::GlyphRun::TextType::Ltr || peek->text_type == Gfx::GlyphRun::TextType::Rtl) { |
178 | 0 | next_known_direction = peek->text_type; |
179 | 0 | break; |
180 | 0 | } |
181 | 0 | } |
182 | |
|
183 | 0 | auto last_known_direction = m_text_node_context->last_known_direction; |
184 | 0 | if (last_known_direction.has_value() && next_known_direction.has_value() && *last_known_direction != *next_known_direction) { |
185 | 0 | switch (m_containing_block->computed_values().direction()) { |
186 | 0 | case CSS::Direction::Ltr: |
187 | 0 | return Gfx::GlyphRun::TextType::Ltr; |
188 | 0 | case CSS::Direction::Rtl: |
189 | 0 | return Gfx::GlyphRun::TextType::Rtl; |
190 | 0 | } |
191 | 0 | } |
192 | | |
193 | 0 | if (last_known_direction.has_value()) |
194 | 0 | return *last_known_direction; |
195 | 0 | if (next_known_direction.has_value()) |
196 | 0 | return *next_known_direction; |
197 | | |
198 | 0 | return Gfx::GlyphRun::TextType::ContextDependent; |
199 | 0 | } |
200 | | |
201 | | Optional<InlineLevelIterator::Item> InlineLevelIterator::next_without_lookahead() |
202 | 0 | { |
203 | 0 | if (!m_current_node) |
204 | 0 | return {}; |
205 | | |
206 | 0 | if (is<Layout::TextNode>(*m_current_node)) { |
207 | 0 | auto& text_node = static_cast<Layout::TextNode const&>(*m_current_node); |
208 | |
|
209 | 0 | if (!m_text_node_context.has_value()) |
210 | 0 | enter_text_node(text_node); |
211 | |
|
212 | 0 | auto chunk_opt = m_text_node_context->chunk_iterator.next(); |
213 | 0 | if (!chunk_opt.has_value()) { |
214 | 0 | m_text_node_context = {}; |
215 | 0 | skip_to_next(); |
216 | 0 | return next_without_lookahead(); |
217 | 0 | } |
218 | | |
219 | 0 | if (!m_text_node_context->chunk_iterator.peek(0).has_value()) |
220 | 0 | m_text_node_context->is_last_chunk = true; |
221 | |
|
222 | 0 | auto& chunk = chunk_opt.value(); |
223 | 0 | auto text_type = chunk.text_type; |
224 | 0 | if (text_type == Gfx::GlyphRun::TextType::Ltr || text_type == Gfx::GlyphRun::TextType::Rtl) |
225 | 0 | m_text_node_context->last_known_direction = text_type; |
226 | |
|
227 | 0 | if (m_text_node_context->do_respect_linebreaks && chunk.has_breaking_newline) { |
228 | 0 | m_text_node_context->is_last_chunk = true; |
229 | 0 | if (chunk.is_all_whitespace) |
230 | 0 | text_type = Gfx::GlyphRun::TextType::EndPadding; |
231 | 0 | } |
232 | |
|
233 | 0 | if (text_type == Gfx::GlyphRun::TextType::ContextDependent) |
234 | 0 | text_type = resolve_text_direction_from_context(); |
235 | |
|
236 | 0 | if (m_text_node_context->do_respect_linebreaks && chunk.has_breaking_newline) { |
237 | 0 | return Item { |
238 | 0 | .type = Item::Type::ForcedBreak, |
239 | 0 | }; |
240 | 0 | } |
241 | | |
242 | 0 | Vector<Gfx::DrawGlyphOrEmoji> glyph_run; |
243 | 0 | float glyph_run_width = 0; |
244 | 0 | Gfx::for_each_glyph_position( |
245 | 0 | { 0, 0 }, chunk.view, chunk.font, [&](Gfx::DrawGlyphOrEmoji const& glyph_or_emoji) { |
246 | 0 | glyph_run.append(glyph_or_emoji); |
247 | 0 | return IterationDecision::Continue; |
248 | 0 | }, |
249 | 0 | Gfx::IncludeLeftBearing::No, glyph_run_width); |
250 | |
|
251 | 0 | if (!m_text_node_context->is_last_chunk) |
252 | 0 | glyph_run_width += text_node.first_available_font().glyph_spacing(); |
253 | |
|
254 | 0 | CSSPixels chunk_width = CSSPixels::nearest_value_for(glyph_run_width); |
255 | | |
256 | | // NOTE: We never consider `content: ""` to be collapsible whitespace. |
257 | 0 | bool is_generated_empty_string = text_node.is_generated() && chunk.length == 0; |
258 | |
|
259 | 0 | Item item { |
260 | 0 | .type = Item::Type::Text, |
261 | 0 | .node = &text_node, |
262 | 0 | .glyph_run = adopt_ref(*new Gfx::GlyphRun(move(glyph_run), chunk.font, text_type)), |
263 | 0 | .offset_in_node = chunk.start, |
264 | 0 | .length_in_node = chunk.length, |
265 | 0 | .width = chunk_width, |
266 | 0 | .is_collapsible_whitespace = m_text_node_context->do_collapse && chunk.is_all_whitespace && !is_generated_empty_string, |
267 | 0 | }; |
268 | |
|
269 | 0 | add_extra_box_model_metrics_to_item(item, m_text_node_context->is_first_chunk, m_text_node_context->is_last_chunk); |
270 | 0 | return item; |
271 | 0 | } |
272 | | |
273 | 0 | if (m_current_node->is_absolutely_positioned()) { |
274 | 0 | auto& node = *m_current_node; |
275 | 0 | skip_to_next(); |
276 | 0 | return Item { |
277 | 0 | .type = Item::Type::AbsolutelyPositionedElement, |
278 | 0 | .node = &node, |
279 | 0 | }; |
280 | 0 | } |
281 | | |
282 | 0 | if (m_current_node->is_floating()) { |
283 | 0 | auto& node = *m_current_node; |
284 | 0 | skip_to_next(); |
285 | 0 | return Item { |
286 | 0 | .type = Item::Type::FloatingElement, |
287 | 0 | .node = &node, |
288 | 0 | }; |
289 | 0 | } |
290 | | |
291 | 0 | if (is<Layout::BreakNode>(*m_current_node)) { |
292 | 0 | auto& node = *m_current_node; |
293 | 0 | skip_to_next(); |
294 | 0 | return Item { |
295 | 0 | .type = Item::Type::ForcedBreak, |
296 | 0 | .node = &node, |
297 | 0 | }; |
298 | 0 | } |
299 | | |
300 | 0 | if (is<Layout::ListItemMarkerBox>(*m_current_node)) { |
301 | 0 | skip_to_next(); |
302 | 0 | return next_without_lookahead(); |
303 | 0 | } |
304 | | |
305 | 0 | if (!is<Layout::Box>(*m_current_node)) { |
306 | 0 | skip_to_next(); |
307 | 0 | return next_without_lookahead(); |
308 | 0 | } |
309 | | |
310 | 0 | if (is<Layout::ReplacedBox>(*m_current_node)) { |
311 | 0 | auto& replaced_box = static_cast<Layout::ReplacedBox const&>(*m_current_node); |
312 | | // FIXME: This const_cast is gross. |
313 | 0 | const_cast<Layout::ReplacedBox&>(replaced_box).prepare_for_replaced_layout(); |
314 | 0 | } |
315 | |
|
316 | 0 | auto& box = verify_cast<Layout::Box>(*m_current_node); |
317 | 0 | auto& box_state = m_layout_state.get(box); |
318 | 0 | m_inline_formatting_context.dimension_box_on_line(box, m_layout_mode); |
319 | |
|
320 | 0 | skip_to_next(); |
321 | 0 | auto item = Item { |
322 | 0 | .type = Item::Type::Element, |
323 | 0 | .node = &box, |
324 | 0 | .offset_in_node = 0, |
325 | 0 | .length_in_node = 0, |
326 | 0 | .width = box_state.content_width(), |
327 | 0 | .padding_start = box_state.padding_left, |
328 | 0 | .padding_end = box_state.padding_right, |
329 | 0 | .border_start = box_state.border_left, |
330 | 0 | .border_end = box_state.border_right, |
331 | 0 | .margin_start = box_state.margin_left, |
332 | 0 | .margin_end = box_state.margin_right, |
333 | 0 | }; |
334 | 0 | add_extra_box_model_metrics_to_item(item, true, true); |
335 | 0 | return item; |
336 | 0 | } |
337 | | |
338 | | void InlineLevelIterator::enter_text_node(Layout::TextNode const& text_node) |
339 | 0 | { |
340 | 0 | bool do_collapse = true; |
341 | 0 | bool do_wrap_lines = true; |
342 | 0 | bool do_respect_linebreaks = false; |
343 | |
|
344 | 0 | if (text_node.computed_values().white_space() == CSS::WhiteSpace::Nowrap) { |
345 | 0 | do_collapse = true; |
346 | 0 | do_wrap_lines = false; |
347 | 0 | do_respect_linebreaks = false; |
348 | 0 | } else if (text_node.computed_values().white_space() == CSS::WhiteSpace::Pre) { |
349 | 0 | do_collapse = false; |
350 | 0 | do_wrap_lines = false; |
351 | 0 | do_respect_linebreaks = true; |
352 | 0 | } else if (text_node.computed_values().white_space() == CSS::WhiteSpace::PreLine) { |
353 | 0 | do_collapse = true; |
354 | 0 | do_wrap_lines = true; |
355 | 0 | do_respect_linebreaks = true; |
356 | 0 | } else if (text_node.computed_values().white_space() == CSS::WhiteSpace::PreWrap) { |
357 | 0 | do_collapse = false; |
358 | 0 | do_wrap_lines = true; |
359 | 0 | do_respect_linebreaks = true; |
360 | 0 | } |
361 | |
|
362 | 0 | if (text_node.dom_node().is_editable() && !text_node.dom_node().is_uninteresting_whitespace_node()) |
363 | 0 | do_collapse = false; |
364 | |
|
365 | 0 | m_text_node_context = TextNodeContext { |
366 | 0 | .do_collapse = do_collapse, |
367 | 0 | .do_wrap_lines = do_wrap_lines, |
368 | 0 | .do_respect_linebreaks = do_respect_linebreaks, |
369 | 0 | .is_first_chunk = true, |
370 | 0 | .is_last_chunk = false, |
371 | 0 | .chunk_iterator = TextNode::ChunkIterator { text_node, do_wrap_lines, do_respect_linebreaks }, |
372 | 0 | }; |
373 | 0 | } |
374 | | |
375 | | void InlineLevelIterator::add_extra_box_model_metrics_to_item(Item& item, bool add_leading_metrics, bool add_trailing_metrics) |
376 | 0 | { |
377 | 0 | if (add_leading_metrics && m_extra_leading_metrics.has_value()) { |
378 | 0 | item.margin_start += m_extra_leading_metrics->margin; |
379 | 0 | item.border_start += m_extra_leading_metrics->border; |
380 | 0 | item.padding_start += m_extra_leading_metrics->padding; |
381 | 0 | m_extra_leading_metrics = {}; |
382 | 0 | } |
383 | |
|
384 | 0 | if (add_trailing_metrics && m_extra_trailing_metrics.has_value()) { |
385 | 0 | item.margin_end += m_extra_trailing_metrics->margin; |
386 | 0 | item.border_end += m_extra_trailing_metrics->border; |
387 | 0 | item.padding_end += m_extra_trailing_metrics->padding; |
388 | 0 | m_extra_trailing_metrics = {}; |
389 | 0 | } |
390 | 0 | } |
391 | | |
392 | | } |