Serenity Operating System
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
14namespace Web::Layout {
15
16InlineLevelIterator::InlineLevelIterator(Layout::InlineFormattingContext& inline_formatting_context, Layout::LayoutState& layout_state, Layout::BlockContainer const& container, LayoutMode layout_mode)
17 : m_inline_formatting_context(inline_formatting_context)
18 , m_layout_state(layout_state)
19 , m_container(container)
20 , m_container_state(layout_state.get(container))
21 , m_next_node(container.first_child())
22 , m_layout_mode(layout_mode)
23{
24 skip_to_next();
25}
26
27void InlineLevelIterator::enter_node_with_box_model_metrics(Layout::NodeWithStyleAndBoxModelMetrics const& node)
28{
29 if (!m_extra_leading_metrics.has_value())
30 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 auto& used_values = m_layout_state.get_mutable(node);
35 auto const& computed_values = node.computed_values();
36
37 used_values.margin_left = computed_values.margin().left().resolved(node, CSS::Length::make_px(m_container_state.content_width())).to_px(node);
38 used_values.border_left = computed_values.border_left().width;
39 used_values.padding_left = computed_values.padding().left().resolved(node, CSS::Length::make_px(m_container_state.content_width())).to_px(node);
40
41 m_extra_leading_metrics->margin += used_values.margin_left;
42 m_extra_leading_metrics->border += used_values.border_left;
43 m_extra_leading_metrics->padding += used_values.padding_left;
44
45 m_box_model_node_stack.append(node);
46}
47
48void InlineLevelIterator::exit_node_with_box_model_metrics()
49{
50 if (!m_extra_trailing_metrics.has_value())
51 m_extra_trailing_metrics = ExtraBoxMetrics {};
52
53 auto& node = m_box_model_node_stack.last();
54 auto& used_values = m_layout_state.get_mutable(node);
55 auto const& computed_values = node.computed_values();
56
57 used_values.margin_right = computed_values.margin().right().resolved(node, CSS::Length::make_px(m_container_state.content_width())).to_px(node);
58 used_values.border_right = computed_values.border_right().width;
59 used_values.padding_right = computed_values.padding().right().resolved(node, CSS::Length::make_px(m_container_state.content_width())).to_px(node);
60
61 m_extra_trailing_metrics->margin += used_values.margin_right;
62 m_extra_trailing_metrics->border += used_values.border_right;
63 m_extra_trailing_metrics->padding += used_values.padding_right;
64
65 m_box_model_node_stack.take_last();
66}
67
68// This is similar to Layout::Node::next_in_pre_order() but will not descend into inline-block nodes.
69Layout::Node const* InlineLevelIterator::next_inline_node_in_pre_order(Layout::Node const& current, Layout::Node const* stay_within)
70{
71 if (current.first_child()
72 && current.first_child()->display().is_inline_outside()
73 && current.display().is_flow_inside()
74 && !current.is_replaced_box()) {
75 if (!current.is_box() || !static_cast<Box const&>(current).is_out_of_flow(m_inline_formatting_context))
76 return current.first_child();
77 }
78
79 Layout::Node const* node = ¤t;
80 Layout::Node const* next = nullptr;
81 while (!(next = node->next_sibling())) {
82 node = node->parent();
83
84 // If node is the last node on the "box model node stack", pop it off.
85 if (!m_box_model_node_stack.is_empty()
86 && &m_box_model_node_stack.last() == node) {
87 exit_node_with_box_model_metrics();
88 }
89 if (!node || node == stay_within)
90 return nullptr;
91 }
92
93 // If node is the last node on the "box model node stack", pop it off.
94 if (!m_box_model_node_stack.is_empty()
95 && &m_box_model_node_stack.last() == node) {
96 exit_node_with_box_model_metrics();
97 }
98
99 return next;
100}
101
102void InlineLevelIterator::compute_next()
103{
104 if (m_next_node == nullptr)
105 return;
106 do {
107 m_next_node = next_inline_node_in_pre_order(*m_next_node, &m_container);
108 } while (m_next_node && (!m_next_node->is_inline() && !m_next_node->is_out_of_flow(m_inline_formatting_context)));
109}
110
111void InlineLevelIterator::skip_to_next()
112{
113 if (m_next_node
114 && is<Layout::NodeWithStyleAndBoxModelMetrics>(*m_next_node)
115 && m_next_node->display().is_flow_inside()
116 && !m_next_node->is_out_of_flow(m_inline_formatting_context)
117 && !m_next_node->is_replaced_box())
118 enter_node_with_box_model_metrics(static_cast<Layout::NodeWithStyleAndBoxModelMetrics const&>(*m_next_node));
119
120 m_current_node = m_next_node;
121 compute_next();
122}
123
124Optional<InlineLevelIterator::Item> InlineLevelIterator::next(CSSPixels available_width)
125{
126 if (!m_current_node)
127 return {};
128
129 if (is<Layout::TextNode>(*m_current_node)) {
130 auto& text_node = static_cast<Layout::TextNode const&>(*m_current_node);
131
132 if (!m_text_node_context.has_value())
133 enter_text_node(text_node);
134
135 auto chunk_opt = m_text_node_context->next_chunk;
136 if (!chunk_opt.has_value()) {
137 m_text_node_context = {};
138 skip_to_next();
139 return next(available_width);
140 }
141
142 m_text_node_context->next_chunk = m_text_node_context->chunk_iterator.next();
143 if (!m_text_node_context->next_chunk.has_value())
144 m_text_node_context->is_last_chunk = true;
145
146 auto& chunk = chunk_opt.value();
147 CSSPixels chunk_width = text_node.font().width(chunk.view) + text_node.font().glyph_spacing();
148
149 if (m_text_node_context->do_respect_linebreaks && chunk.has_breaking_newline) {
150 return Item {
151 .type = Item::Type::ForcedBreak,
152 };
153 }
154
155 // NOTE: We never consider `content: ""` to be collapsible whitespace.
156 bool is_generated_empty_string = text_node.is_generated() && chunk.length == 0;
157
158 Item item {
159 .type = Item::Type::Text,
160 .node = &text_node,
161 .offset_in_node = chunk.start,
162 .length_in_node = chunk.length,
163 .width = chunk_width,
164 .is_collapsible_whitespace = m_text_node_context->do_collapse && chunk.is_all_whitespace && !is_generated_empty_string,
165 };
166
167 add_extra_box_model_metrics_to_item(item, m_text_node_context->is_first_chunk, m_text_node_context->is_last_chunk);
168 return item;
169 }
170
171 if (m_current_node->is_absolutely_positioned()) {
172 auto& node = *m_current_node;
173 skip_to_next();
174 return Item {
175 .type = Item::Type::AbsolutelyPositionedElement,
176 .node = &node,
177 };
178 }
179
180 if (m_current_node->is_floating()) {
181 auto& node = *m_current_node;
182 skip_to_next();
183 return Item {
184 .type = Item::Type::FloatingElement,
185 .node = &node,
186 };
187 }
188
189 if (is<Layout::BreakNode>(*m_current_node)) {
190 skip_to_next();
191 return Item {
192 .type = Item::Type::ForcedBreak,
193 };
194 }
195
196 if (is<Layout::ListItemMarkerBox>(*m_current_node)) {
197 skip_to_next();
198 return next(available_width);
199 }
200
201 if (!is<Layout::Box>(*m_current_node)) {
202 skip_to_next();
203 return next(available_width);
204 }
205
206 if (is<Layout::ReplacedBox>(*m_current_node)) {
207 auto& replaced_box = static_cast<Layout::ReplacedBox const&>(*m_current_node);
208 // FIXME: This const_cast is gross.
209 const_cast<Layout::ReplacedBox&>(replaced_box).prepare_for_replaced_layout();
210 }
211
212 auto& box = verify_cast<Layout::Box>(*m_current_node);
213 auto& box_state = m_layout_state.get(box);
214 m_inline_formatting_context.dimension_box_on_line(box, m_layout_mode);
215
216 skip_to_next();
217 auto item = Item {
218 .type = Item::Type::Element,
219 .node = &box,
220 .offset_in_node = 0,
221 .length_in_node = 0,
222 .width = box_state.content_width(),
223 .padding_start = box_state.padding_left,
224 .padding_end = box_state.padding_right,
225 .border_start = box_state.border_left,
226 .border_end = box_state.border_right,
227 .margin_start = box_state.margin_left,
228 .margin_end = box_state.margin_right,
229 };
230 add_extra_box_model_metrics_to_item(item, true, true);
231 return item;
232}
233
234void InlineLevelIterator::enter_text_node(Layout::TextNode const& text_node)
235{
236 bool do_collapse = true;
237 bool do_wrap_lines = true;
238 bool do_respect_linebreaks = false;
239
240 if (text_node.computed_values().white_space() == CSS::WhiteSpace::Nowrap) {
241 do_collapse = true;
242 do_wrap_lines = false;
243 do_respect_linebreaks = false;
244 } else if (text_node.computed_values().white_space() == CSS::WhiteSpace::Pre) {
245 do_collapse = false;
246 do_wrap_lines = false;
247 do_respect_linebreaks = true;
248 } else if (text_node.computed_values().white_space() == CSS::WhiteSpace::PreLine) {
249 do_collapse = true;
250 do_wrap_lines = true;
251 do_respect_linebreaks = true;
252 } else if (text_node.computed_values().white_space() == CSS::WhiteSpace::PreWrap) {
253 do_collapse = false;
254 do_wrap_lines = true;
255 do_respect_linebreaks = true;
256 }
257
258 if (text_node.dom_node().is_editable() && !text_node.dom_node().is_uninteresting_whitespace_node())
259 do_collapse = false;
260
261 // FIXME: The const_cast here is gross.
262 const_cast<TextNode&>(text_node).compute_text_for_rendering(do_collapse);
263
264 m_text_node_context = TextNodeContext {
265 .do_collapse = do_collapse,
266 .do_wrap_lines = do_wrap_lines,
267 .do_respect_linebreaks = do_respect_linebreaks,
268 .is_first_chunk = true,
269 .is_last_chunk = false,
270 .chunk_iterator = TextNode::ChunkIterator { text_node.text_for_rendering(), do_wrap_lines, do_respect_linebreaks, text_node.is_generated() && text_node.text_for_rendering().is_empty() },
271 };
272 m_text_node_context->next_chunk = m_text_node_context->chunk_iterator.next();
273}
274
275void InlineLevelIterator::add_extra_box_model_metrics_to_item(Item& item, bool add_leading_metrics, bool add_trailing_metrics)
276{
277 if (add_leading_metrics && m_extra_leading_metrics.has_value()) {
278 item.margin_start += m_extra_leading_metrics->margin;
279 item.border_start += m_extra_leading_metrics->border;
280 item.padding_start += m_extra_leading_metrics->padding;
281 m_extra_leading_metrics = {};
282 }
283
284 if (add_trailing_metrics && m_extra_trailing_metrics.has_value()) {
285 item.margin_end += m_extra_trailing_metrics->margin;
286 item.border_end += m_extra_trailing_metrics->border;
287 item.padding_end += m_extra_trailing_metrics->padding;
288 m_extra_trailing_metrics = {};
289 }
290}
291
292}