2022-01-17 15:07:19 +01:00
|
|
|
/*
|
2024-10-04 13:19:50 +02:00
|
|
|
* Copyright (c) 2022, Andreas Kling <andreas@ladybird.org>
|
2022-01-17 15:07:19 +01:00
|
|
|
*
|
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
|
*/
|
|
|
|
|
|
2026-02-08 17:13:36 +01:00
|
|
|
#include <LibWeb/DOM/ShadowRoot.h>
|
2025-08-08 21:17:33 +10:00
|
|
|
#include <LibWeb/HTML/FormAssociatedElement.h>
|
2022-01-17 15:07:19 +01:00
|
|
|
#include <LibWeb/Layout/BreakNode.h>
|
2022-02-14 15:52:29 +01:00
|
|
|
#include <LibWeb/Layout/InlineFormattingContext.h>
|
2022-01-17 15:07:19 +01:00
|
|
|
#include <LibWeb/Layout/InlineLevelIterator.h>
|
|
|
|
|
#include <LibWeb/Layout/InlineNode.h>
|
2022-01-20 16:17:29 +01:00
|
|
|
#include <LibWeb/Layout/ListItemMarkerBox.h>
|
2022-01-17 15:07:19 +01:00
|
|
|
#include <LibWeb/Layout/ReplacedBox.h>
|
|
|
|
|
|
|
|
|
|
namespace Web::Layout {
|
|
|
|
|
|
2024-03-15 19:25:00 +01:00
|
|
|
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)
|
2022-02-14 15:52:29 +01:00
|
|
|
: m_inline_formatting_context(inline_formatting_context)
|
2022-07-16 23:43:48 +02:00
|
|
|
, m_layout_state(layout_state)
|
2024-03-15 19:25:00 +01:00
|
|
|
, m_containing_block(containing_block)
|
|
|
|
|
, m_containing_block_used_values(containing_block_used_values)
|
|
|
|
|
, m_next_node(containing_block.first_child())
|
2022-02-14 15:52:29 +01:00
|
|
|
, m_layout_mode(layout_mode)
|
|
|
|
|
{
|
|
|
|
|
skip_to_next();
|
2026-01-10 13:22:55 +01:00
|
|
|
generate_all_items();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InlineLevelIterator::generate_all_items()
|
|
|
|
|
{
|
|
|
|
|
for (;;) {
|
|
|
|
|
auto item = generate_next_item();
|
|
|
|
|
if (!item.has_value())
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
// Track accumulated width for tab calculations.
|
|
|
|
|
// Reset on forced breaks since tabs measure from line start.
|
|
|
|
|
if (item->type == Item::Type::ForcedBreak) {
|
|
|
|
|
m_accumulated_width_for_tabs = 0;
|
|
|
|
|
} else {
|
|
|
|
|
m_accumulated_width_for_tabs += item->border_box_width();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_items.append(item.release_value());
|
|
|
|
|
}
|
2022-02-14 15:52:29 +01:00
|
|
|
}
|
|
|
|
|
|
2022-02-20 15:51:24 +01:00
|
|
|
void InlineLevelIterator::enter_node_with_box_model_metrics(Layout::NodeWithStyleAndBoxModelMetrics const& node)
|
2022-02-14 15:52:29 +01:00
|
|
|
{
|
|
|
|
|
if (!m_extra_leading_metrics.has_value())
|
|
|
|
|
m_extra_leading_metrics = ExtraBoxMetrics {};
|
|
|
|
|
|
2022-02-20 15:51:24 +01:00
|
|
|
// FIXME: It's really weird that *this* is where we assign box model metrics for these layout nodes..
|
2022-02-14 15:52:29 +01:00
|
|
|
|
2022-07-16 23:43:48 +02:00
|
|
|
auto& used_values = m_layout_state.get_mutable(node);
|
2022-02-20 15:51:24 +01:00
|
|
|
auto const& computed_values = node.computed_values();
|
|
|
|
|
|
2025-09-01 12:51:52 +01:00
|
|
|
used_values.margin_top = computed_values.margin().top().to_px_or_zero(node, m_containing_block_used_values.content_width());
|
|
|
|
|
used_values.margin_bottom = computed_values.margin().bottom().to_px_or_zero(node, m_containing_block_used_values.content_width());
|
2025-02-22 14:59:09 +01:00
|
|
|
|
2025-09-01 12:51:52 +01:00
|
|
|
used_values.margin_left = computed_values.margin().left().to_px_or_zero(node, m_containing_block_used_values.content_width());
|
2022-07-16 23:43:48 +02:00
|
|
|
used_values.border_left = computed_values.border_left().width;
|
2025-09-01 12:51:52 +01:00
|
|
|
used_values.padding_left = computed_values.padding().left().to_px_or_zero(node, m_containing_block_used_values.content_width());
|
2022-02-20 15:51:24 +01:00
|
|
|
|
2025-09-01 12:51:52 +01:00
|
|
|
used_values.margin_right = computed_values.margin().right().to_px_or_zero(node, m_containing_block_used_values.content_width());
|
2025-02-22 14:59:09 +01:00
|
|
|
used_values.border_right = computed_values.border_right().width;
|
2025-09-01 12:51:52 +01:00
|
|
|
used_values.padding_right = computed_values.padding().right().to_px_or_zero(node, m_containing_block_used_values.content_width());
|
2025-02-22 14:59:09 +01:00
|
|
|
|
2024-10-15 20:08:48 +02:00
|
|
|
used_values.border_top = computed_values.border_top().width;
|
|
|
|
|
used_values.border_bottom = computed_values.border_bottom().width;
|
2025-09-01 12:51:52 +01:00
|
|
|
used_values.padding_bottom = computed_values.padding().bottom().to_px_or_zero(node, m_containing_block_used_values.content_width());
|
|
|
|
|
used_values.padding_top = computed_values.padding().top().to_px_or_zero(node, m_containing_block_used_values.content_width());
|
2024-04-23 13:34:09 -06:00
|
|
|
|
2022-07-16 23:43:48 +02:00
|
|
|
m_extra_leading_metrics->margin += used_values.margin_left;
|
|
|
|
|
m_extra_leading_metrics->border += used_values.border_left;
|
|
|
|
|
m_extra_leading_metrics->padding += used_values.padding_left;
|
2022-02-14 15:52:29 +01:00
|
|
|
|
2023-08-15 09:34:06 +02:00
|
|
|
// Now's our chance to resolve the inset properties for this node.
|
2024-11-11 15:54:21 +01:00
|
|
|
m_inline_formatting_context.compute_inset(node, m_inline_formatting_context.content_box_rect(m_containing_block_used_values).size());
|
2023-08-15 09:34:06 +02:00
|
|
|
|
LibWeb: Make layout nodes refcounted
Move the layout tree from GC allocation to refcounted ownership so
removed layout and paint subtrees are destroyed synchronously instead
of waiting for the next GC sweep. This dramatically reduces GC memory
usage peaks after layout tree churn and makes it easier for memory use
to fall back after large document updates.
Update layout factories, tree traversal, SVG layout node creation,
paintable back-pointers, and pseudo-element layout links to use RefPtr
ownership.
Make display: contents follow the same shape as Blink and WebKit: the
element itself does not create a layout node, and its children are
flattened into the nearest layout parent. Wrap direct non-whitespace
text in an anonymous inline node when the boxless element contributes
inherited style to that text.
Use an internal inline wrapper for display: contents pseudo-elements
so generated content can still participate in layout, painting, hit
testing, and pseudo-element queries. Keep CSSOM reporting the computed
display value from the pseudo style, not the internal wrapper.
Remove the retained out-of-tree layout node list and its testing hook,
since the flattened model does not need a side owner for boxless
elements. Add coverage for inherited text style, dynamic insertion
order, pseudo-element hit testing, and computed style queries.
2026-06-07 17:50:33 +02:00
|
|
|
m_box_model_node_stack.append(&node);
|
2022-02-14 15:52:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InlineLevelIterator::exit_node_with_box_model_metrics()
|
|
|
|
|
{
|
|
|
|
|
if (!m_extra_trailing_metrics.has_value())
|
|
|
|
|
m_extra_trailing_metrics = ExtraBoxMetrics {};
|
|
|
|
|
|
LibWeb: Make layout nodes refcounted
Move the layout tree from GC allocation to refcounted ownership so
removed layout and paint subtrees are destroyed synchronously instead
of waiting for the next GC sweep. This dramatically reduces GC memory
usage peaks after layout tree churn and makes it easier for memory use
to fall back after large document updates.
Update layout factories, tree traversal, SVG layout node creation,
paintable back-pointers, and pseudo-element layout links to use RefPtr
ownership.
Make display: contents follow the same shape as Blink and WebKit: the
element itself does not create a layout node, and its children are
flattened into the nearest layout parent. Wrap direct non-whitespace
text in an anonymous inline node when the boxless element contributes
inherited style to that text.
Use an internal inline wrapper for display: contents pseudo-elements
so generated content can still participate in layout, painting, hit
testing, and pseudo-element queries. Keep CSSOM reporting the computed
display value from the pseudo style, not the internal wrapper.
Remove the retained out-of-tree layout node list and its testing hook,
since the flattened model does not need a side owner for boxless
elements. Add coverage for inherited text style, dynamic insertion
order, pseudo-element hit testing, and computed style queries.
2026-06-07 17:50:33 +02:00
|
|
|
auto& node = *m_box_model_node_stack.last();
|
2022-07-16 23:43:48 +02:00
|
|
|
auto& used_values = m_layout_state.get_mutable(node);
|
2022-02-14 15:52:29 +01:00
|
|
|
|
2022-07-16 23:43:48 +02:00
|
|
|
m_extra_trailing_metrics->margin += used_values.margin_right;
|
|
|
|
|
m_extra_trailing_metrics->border += used_values.border_right;
|
|
|
|
|
m_extra_trailing_metrics->padding += used_values.padding_right;
|
2022-02-14 15:52:29 +01:00
|
|
|
|
|
|
|
|
m_box_model_node_stack.take_last();
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-20 12:18:25 +01:00
|
|
|
// This is similar to Layout::Node::next_in_pre_order() but will not descend into inline-block nodes.
|
2022-02-20 15:51:24 +01:00
|
|
|
Layout::Node const* InlineLevelIterator::next_inline_node_in_pre_order(Layout::Node const& current, Layout::Node const* stay_within)
|
2022-01-20 12:18:25 +01:00
|
|
|
{
|
2022-10-06 16:21:11 +02:00
|
|
|
if (current.first_child()
|
|
|
|
|
&& current.first_child()->display().is_inline_outside()
|
|
|
|
|
&& current.display().is_flow_inside()
|
|
|
|
|
&& !current.is_replaced_box()) {
|
2022-03-22 19:18:05 +01:00
|
|
|
if (!current.is_box() || !static_cast<Box const&>(current).is_out_of_flow(m_inline_formatting_context))
|
|
|
|
|
return current.first_child();
|
|
|
|
|
}
|
2022-01-20 12:18:25 +01:00
|
|
|
|
2022-02-20 15:51:24 +01:00
|
|
|
Layout::Node const* node = ¤t;
|
|
|
|
|
Layout::Node const* next = nullptr;
|
2022-01-20 12:18:25 +01:00
|
|
|
while (!(next = node->next_sibling())) {
|
|
|
|
|
node = node->parent();
|
2022-02-14 15:52:29 +01:00
|
|
|
|
|
|
|
|
// If node is the last node on the "box model node stack", pop it off.
|
|
|
|
|
if (!m_box_model_node_stack.is_empty()
|
2023-02-26 16:09:02 -07:00
|
|
|
&& m_box_model_node_stack.last() == node) {
|
2022-02-14 15:52:29 +01:00
|
|
|
exit_node_with_box_model_metrics();
|
|
|
|
|
}
|
2022-01-20 12:18:25 +01:00
|
|
|
if (!node || node == stay_within)
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-26 09:25:24 +01:00
|
|
|
// If node is the last node on the "box model node stack", pop it off.
|
|
|
|
|
if (!m_box_model_node_stack.is_empty()
|
2023-02-26 16:09:02 -07:00
|
|
|
&& m_box_model_node_stack.last() == node) {
|
2022-02-26 09:25:24 +01:00
|
|
|
exit_node_with_box_model_metrics();
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-20 12:18:25 +01:00
|
|
|
return next;
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-14 15:52:29 +01:00
|
|
|
void InlineLevelIterator::compute_next()
|
2022-01-17 15:07:19 +01:00
|
|
|
{
|
2022-02-14 15:52:29 +01:00
|
|
|
if (m_next_node == nullptr)
|
|
|
|
|
return;
|
2022-01-17 15:07:19 +01:00
|
|
|
do {
|
LibWeb: Make layout nodes refcounted
Move the layout tree from GC allocation to refcounted ownership so
removed layout and paint subtrees are destroyed synchronously instead
of waiting for the next GC sweep. This dramatically reduces GC memory
usage peaks after layout tree churn and makes it easier for memory use
to fall back after large document updates.
Update layout factories, tree traversal, SVG layout node creation,
paintable back-pointers, and pseudo-element layout links to use RefPtr
ownership.
Make display: contents follow the same shape as Blink and WebKit: the
element itself does not create a layout node, and its children are
flattened into the nearest layout parent. Wrap direct non-whitespace
text in an anonymous inline node when the boxless element contributes
inherited style to that text.
Use an internal inline wrapper for display: contents pseudo-elements
so generated content can still participate in layout, painting, hit
testing, and pseudo-element queries. Keep CSSOM reporting the computed
display value from the pseudo style, not the internal wrapper.
Remove the retained out-of-tree layout node list and its testing hook,
since the flattened model does not need a side owner for boxless
elements. Add coverage for inherited text style, dynamic insertion
order, pseudo-element hit testing, and computed style queries.
2026-06-07 17:50:33 +02:00
|
|
|
m_next_node = next_inline_node_in_pre_order(*m_next_node, &m_containing_block);
|
2024-04-25 21:10:30 +02:00
|
|
|
if (m_next_node && m_next_node->is_svg_mask_box()) {
|
|
|
|
|
// NOTE: It is possible to encounter SVGMaskBox nodes while doing layout of formatting context established by <foreignObject> with a mask.
|
|
|
|
|
// We should skip and let SVGFormattingContext take care of them.
|
|
|
|
|
m_next_node = m_next_node->next_sibling();
|
|
|
|
|
}
|
2022-03-22 19:18:05 +01:00
|
|
|
} while (m_next_node && (!m_next_node->is_inline() && !m_next_node->is_out_of_flow(m_inline_formatting_context)));
|
2022-02-14 15:52:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InlineLevelIterator::skip_to_next()
|
|
|
|
|
{
|
2022-10-06 16:21:11 +02:00
|
|
|
if (m_next_node
|
|
|
|
|
&& is<Layout::NodeWithStyleAndBoxModelMetrics>(*m_next_node)
|
|
|
|
|
&& m_next_node->display().is_flow_inside()
|
|
|
|
|
&& !m_next_node->is_out_of_flow(m_inline_formatting_context)
|
|
|
|
|
&& !m_next_node->is_replaced_box())
|
2022-02-20 15:51:24 +01:00
|
|
|
enter_node_with_box_model_metrics(static_cast<Layout::NodeWithStyleAndBoxModelMetrics const&>(*m_next_node));
|
2022-02-14 15:52:29 +01:00
|
|
|
|
|
|
|
|
m_current_node = m_next_node;
|
|
|
|
|
compute_next();
|
2022-01-17 15:07:19 +01:00
|
|
|
}
|
|
|
|
|
|
2026-05-30 12:06:04 +02:00
|
|
|
Optional<InlineLevelIterator::Item&> InlineLevelIterator::next()
|
2023-08-19 03:36:53 +00:00
|
|
|
{
|
2026-01-10 13:22:55 +01:00
|
|
|
if (m_next_item_index >= m_items.size())
|
|
|
|
|
return {};
|
|
|
|
|
return m_items[m_next_item_index++];
|
2023-08-19 03:36:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CSSPixels InlineLevelIterator::next_non_whitespace_sequence_width()
|
|
|
|
|
{
|
|
|
|
|
CSSPixels next_width = 0;
|
2026-01-10 13:22:55 +01:00
|
|
|
for (size_t i = m_next_item_index; i < m_items.size(); ++i) {
|
|
|
|
|
auto const& next_item = m_items[i];
|
2023-08-22 06:38:17 +00:00
|
|
|
if (next_item.type == InlineLevelIterator::Item::Type::ForcedBreak)
|
|
|
|
|
break;
|
2025-05-22 00:31:24 +12:00
|
|
|
if (next_item.node->computed_values().text_wrap_mode() == CSS::TextWrapMode::Wrap) {
|
2023-08-19 03:36:53 +00:00
|
|
|
if (next_item.type != InlineLevelIterator::Item::Type::Text)
|
|
|
|
|
break;
|
|
|
|
|
if (next_item.is_collapsible_whitespace)
|
|
|
|
|
break;
|
2025-08-08 21:13:35 +10:00
|
|
|
auto const& next_text_node = as<Layout::TextNode>(*(next_item.node));
|
2025-07-25 09:34:41 -04:00
|
|
|
auto next_view = next_text_node.text_for_rendering().substring_view(next_item.offset_in_node, next_item.length_in_node);
|
|
|
|
|
if (next_view.is_ascii_whitespace())
|
2023-08-19 03:36:53 +00:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
next_width += next_item.border_box_width();
|
|
|
|
|
}
|
|
|
|
|
return next_width;
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-18 17:58:05 +01:00
|
|
|
Gfx::GlyphRun::TextType InlineLevelIterator::resolve_text_direction_from_context()
|
|
|
|
|
{
|
|
|
|
|
VERIFY(m_text_node_context.has_value());
|
|
|
|
|
|
2026-01-10 12:03:54 +01:00
|
|
|
// Search forward in the pre-generated chunks array to find the next chunk with known direction.
|
|
|
|
|
// Since chunks are pre-generated, this is just O(1) array access per iteration.
|
2024-08-18 17:58:05 +01:00
|
|
|
Optional<Gfx::GlyphRun::TextType> next_known_direction;
|
2026-05-30 12:06:04 +02:00
|
|
|
auto const& chunks = m_text_node_context->chunk_list->chunks;
|
|
|
|
|
for (size_t i = m_text_node_context->next_chunk_index; i < chunks.size(); ++i) {
|
|
|
|
|
auto const& chunk = chunks[i];
|
2026-01-10 12:03:54 +01:00
|
|
|
if (chunk.text_type == Gfx::GlyphRun::TextType::Ltr || chunk.text_type == Gfx::GlyphRun::TextType::Rtl) {
|
|
|
|
|
next_known_direction = chunk.text_type;
|
2024-08-18 17:58:05 +01:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto last_known_direction = m_text_node_context->last_known_direction;
|
2026-01-10 12:03:54 +01:00
|
|
|
|
2024-08-18 17:58:05 +01:00
|
|
|
if (last_known_direction.has_value() && next_known_direction.has_value() && *last_known_direction != *next_known_direction) {
|
LibWeb: Make layout nodes refcounted
Move the layout tree from GC allocation to refcounted ownership so
removed layout and paint subtrees are destroyed synchronously instead
of waiting for the next GC sweep. This dramatically reduces GC memory
usage peaks after layout tree churn and makes it easier for memory use
to fall back after large document updates.
Update layout factories, tree traversal, SVG layout node creation,
paintable back-pointers, and pseudo-element layout links to use RefPtr
ownership.
Make display: contents follow the same shape as Blink and WebKit: the
element itself does not create a layout node, and its children are
flattened into the nearest layout parent. Wrap direct non-whitespace
text in an anonymous inline node when the boxless element contributes
inherited style to that text.
Use an internal inline wrapper for display: contents pseudo-elements
so generated content can still participate in layout, painting, hit
testing, and pseudo-element queries. Keep CSSOM reporting the computed
display value from the pseudo style, not the internal wrapper.
Remove the retained out-of-tree layout node list and its testing hook,
since the flattened model does not need a side owner for boxless
elements. Add coverage for inherited text style, dynamic insertion
order, pseudo-element hit testing, and computed style queries.
2026-06-07 17:50:33 +02:00
|
|
|
switch (m_containing_block.computed_values().direction()) {
|
2024-08-18 17:58:05 +01:00
|
|
|
case CSS::Direction::Ltr:
|
|
|
|
|
return Gfx::GlyphRun::TextType::Ltr;
|
|
|
|
|
case CSS::Direction::Rtl:
|
|
|
|
|
return Gfx::GlyphRun::TextType::Rtl;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (last_known_direction.has_value())
|
|
|
|
|
return *last_known_direction;
|
|
|
|
|
if (next_known_direction.has_value())
|
|
|
|
|
return *next_known_direction;
|
|
|
|
|
|
|
|
|
|
return Gfx::GlyphRun::TextType::ContextDependent;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-10 13:22:55 +01:00
|
|
|
Optional<InlineLevelIterator::Item> InlineLevelIterator::generate_next_item()
|
2022-01-17 15:07:19 +01:00
|
|
|
{
|
|
|
|
|
if (!m_current_node)
|
|
|
|
|
return {};
|
|
|
|
|
|
2025-09-12 10:06:27 +02:00
|
|
|
if (auto* text_node = as_if<Layout::TextNode>(*m_current_node)) {
|
2022-03-27 21:14:10 +02:00
|
|
|
if (!m_text_node_context.has_value())
|
2025-09-12 10:06:27 +02:00
|
|
|
enter_text_node(*text_node);
|
2022-01-17 15:07:19 +01:00
|
|
|
|
2026-01-10 12:03:54 +01:00
|
|
|
// Track chunk position locally
|
|
|
|
|
bool is_first_chunk = (m_text_node_context->next_chunk_index == 0);
|
|
|
|
|
|
|
|
|
|
// Get the next chunk from the pre-generated array
|
|
|
|
|
Optional<TextNode::Chunk> chunk_opt;
|
2026-05-30 12:06:04 +02:00
|
|
|
auto const& chunks = m_text_node_context->chunk_list->chunks;
|
|
|
|
|
if (m_text_node_context->next_chunk_index < chunks.size()) {
|
|
|
|
|
chunk_opt = chunks[m_text_node_context->next_chunk_index++];
|
2026-01-10 12:03:54 +01:00
|
|
|
}
|
2025-10-18 13:56:50 +05:30
|
|
|
|
2026-05-30 12:06:04 +02:00
|
|
|
bool is_last_chunk = (m_text_node_context->next_chunk_index >= chunks.size());
|
2025-08-08 21:17:33 +10:00
|
|
|
|
|
|
|
|
auto is_empty_editable = false;
|
2022-01-17 15:07:19 +01:00
|
|
|
if (!chunk_opt.has_value()) {
|
2026-01-10 12:03:54 +01:00
|
|
|
auto const is_only_chunk = is_first_chunk && is_last_chunk;
|
2025-08-08 21:17:33 +10:00
|
|
|
if (is_only_chunk && text_node->text_for_rendering().is_empty()) {
|
2026-06-07 12:44:31 +02:00
|
|
|
if (auto const* dom_text = text_node->dom_text()) {
|
|
|
|
|
if (auto const* shadow_root = as_if<DOM::ShadowRoot>(dom_text->root()))
|
|
|
|
|
if (auto const* form_associated_element = as_if<HTML::FormAssociatedTextControlElement>(shadow_root->host()))
|
|
|
|
|
is_empty_editable = form_associated_element->text_control_to_html_element().is_mutable();
|
|
|
|
|
is_empty_editable |= dom_text->parent() && dom_text->parent()->is_editing_host();
|
|
|
|
|
}
|
2025-08-08 21:17:33 +10:00
|
|
|
}
|
2022-01-17 15:07:19 +01:00
|
|
|
|
2025-08-08 21:17:33 +10:00
|
|
|
if (is_empty_editable) {
|
2026-01-10 12:03:54 +01:00
|
|
|
// Create an empty chunk for editable empty text fields
|
|
|
|
|
chunk_opt = TextNode::Chunk {
|
|
|
|
|
.view = {},
|
|
|
|
|
.font = text_node->computed_values().font_list().first(),
|
|
|
|
|
.is_all_whitespace = true,
|
|
|
|
|
.text_type = Gfx::GlyphRun::TextType::Common,
|
|
|
|
|
};
|
|
|
|
|
// Advance the index so the next call will move to the next node
|
|
|
|
|
m_text_node_context->next_chunk_index = 1;
|
2025-08-08 21:17:33 +10:00
|
|
|
} else {
|
|
|
|
|
m_text_node_context = {};
|
2026-01-20 09:23:51 +00:00
|
|
|
m_previous_chunk_can_break_after = false;
|
2025-08-08 21:17:33 +10:00
|
|
|
skip_to_next();
|
2026-01-10 13:22:55 +01:00
|
|
|
return generate_next_item();
|
2025-08-08 21:17:33 +10:00
|
|
|
}
|
|
|
|
|
}
|
2022-02-14 15:52:29 +01:00
|
|
|
|
2022-01-17 15:07:19 +01:00
|
|
|
auto& chunk = chunk_opt.value();
|
2024-08-18 17:58:05 +01:00
|
|
|
auto text_type = chunk.text_type;
|
2026-01-10 12:03:54 +01:00
|
|
|
if (text_type == Gfx::GlyphRun::TextType::Ltr || text_type == Gfx::GlyphRun::TextType::Rtl) {
|
2024-08-18 17:58:05 +01:00
|
|
|
m_text_node_context->last_known_direction = text_type;
|
2026-01-10 12:03:54 +01:00
|
|
|
}
|
2024-08-18 17:58:05 +01:00
|
|
|
|
2026-01-10 12:03:54 +01:00
|
|
|
auto do_respect_linebreak = m_text_node_context->should_respect_linebreaks;
|
2025-09-12 10:06:27 +02:00
|
|
|
if (do_respect_linebreak && chunk.has_breaking_newline) {
|
2026-01-10 12:03:54 +01:00
|
|
|
is_last_chunk = true;
|
2024-08-18 17:58:05 +01:00
|
|
|
if (chunk.is_all_whitespace)
|
|
|
|
|
text_type = Gfx::GlyphRun::TextType::EndPadding;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (text_type == Gfx::GlyphRun::TextType::ContextDependent)
|
|
|
|
|
text_type = resolve_text_direction_from_context();
|
2022-03-26 18:59:54 +01:00
|
|
|
|
2025-09-12 10:06:27 +02:00
|
|
|
if (do_respect_linebreak && chunk.has_breaking_newline)
|
|
|
|
|
return Item { .type = Item::Type::ForcedBreak };
|
2022-03-26 18:59:54 +01:00
|
|
|
|
2025-09-12 10:06:27 +02:00
|
|
|
auto letter_spacing = text_node->computed_values().letter_spacing();
|
2025-08-02 00:07:48 +12:00
|
|
|
// FIXME: We should apply word spacing to all word-separator characters not just breaking tabs
|
2025-09-12 10:06:27 +02:00
|
|
|
auto word_spacing = text_node->computed_values().word_spacing();
|
2024-11-05 07:11:34 +00:00
|
|
|
|
2024-10-15 18:21:49 +01:00
|
|
|
auto x = 0.0f;
|
|
|
|
|
if (chunk.has_breaking_tab) {
|
2026-01-10 13:22:55 +01:00
|
|
|
// Use the accumulated width we've been tracking during pre-generation.
|
|
|
|
|
// This accounts for items that would appear before this tab on the same line.
|
|
|
|
|
CSSPixels accumulated_width = m_accumulated_width_for_tabs;
|
2024-10-15 18:21:49 +01:00
|
|
|
|
|
|
|
|
// https://drafts.csswg.org/css-text/#tab-size-property
|
2025-11-09 16:28:32 +13:00
|
|
|
auto tab_width = text_node->computed_values().tab_size().visit(
|
|
|
|
|
[&](CSS::Length const& length) -> CSSPixels {
|
|
|
|
|
return length.absolute_length_to_px();
|
2024-10-15 18:21:49 +01:00
|
|
|
},
|
2025-11-09 16:28:32 +13:00
|
|
|
[&](double tab_number) -> CSSPixels {
|
2024-10-23 08:27:22 +01:00
|
|
|
return CSSPixels::nearest_value_for(tab_number * (chunk.font->glyph_width(' ') + word_spacing.to_float() + letter_spacing.to_float()));
|
2024-10-15 18:21:49 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// https://drafts.csswg.org/css-text/#white-space-phase-2
|
|
|
|
|
// if fragments have added to the width, calculate the net distance to the next tab stop, otherwise the shift will just be the tab width
|
|
|
|
|
auto tab_stop_dist = accumulated_width > 0 ? (ceil((accumulated_width / tab_width)) * tab_width) - accumulated_width : tab_width;
|
|
|
|
|
auto ch_width = chunk.font->glyph_width('0');
|
|
|
|
|
|
|
|
|
|
// If this distance is less than 0.5ch, then the subsequent tab stop is used instead
|
|
|
|
|
if (tab_stop_dist < ch_width * 0.5)
|
|
|
|
|
tab_stop_dist += tab_width;
|
|
|
|
|
|
|
|
|
|
// account for consecutive tabs
|
|
|
|
|
auto num_of_tabs = 0;
|
|
|
|
|
for (auto code_point : chunk.view) {
|
|
|
|
|
if (code_point != '\t')
|
|
|
|
|
break;
|
|
|
|
|
num_of_tabs++;
|
|
|
|
|
}
|
|
|
|
|
tab_stop_dist = tab_stop_dist * num_of_tabs;
|
|
|
|
|
|
2024-10-22 13:07:26 +01:00
|
|
|
// remove tabs, we don't want to render them when we shape the text
|
|
|
|
|
chunk.view = chunk.view.substring_view(num_of_tabs);
|
2024-10-15 18:21:49 +01:00
|
|
|
x = tab_stop_dist.to_float();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 16:16:17 +13:00
|
|
|
auto glyph_run = Gfx::shape_text({ x, 0 }, letter_spacing.to_float(), chunk.view, chunk.font, text_type);
|
2024-10-15 18:21:49 +01:00
|
|
|
|
2025-04-19 23:36:03 +02:00
|
|
|
CSSPixels chunk_width = CSSPixels::nearest_value_for(glyph_run->width() + x);
|
2023-10-28 15:39:57 +02:00
|
|
|
|
2022-10-14 12:30:44 +02:00
|
|
|
// NOTE: We never consider `content: ""` to be collapsible whitespace.
|
2025-08-08 21:17:33 +10:00
|
|
|
bool is_generated_empty_string = is_empty_editable || (text_node->is_generated_for_pseudo_element() && chunk.length == 0);
|
2026-01-10 12:03:54 +01:00
|
|
|
auto collapse_whitespace = m_text_node_context->should_collapse_whitespace;
|
2022-10-14 12:30:44 +02:00
|
|
|
|
2022-01-17 15:07:19 +01:00
|
|
|
Item item {
|
|
|
|
|
.type = Item::Type::Text,
|
2025-09-12 10:06:27 +02:00
|
|
|
.node = text_node,
|
2024-09-14 19:54:41 +02:00
|
|
|
.glyph_run = move(glyph_run),
|
2022-01-17 15:07:19 +01:00
|
|
|
.offset_in_node = chunk.start,
|
|
|
|
|
.length_in_node = chunk.length,
|
|
|
|
|
.width = chunk_width,
|
2025-09-12 10:06:27 +02:00
|
|
|
.is_collapsible_whitespace = collapse_whitespace && chunk.is_all_whitespace && !is_generated_empty_string,
|
2026-01-20 09:23:51 +00:00
|
|
|
.can_break_before = m_previous_chunk_can_break_after,
|
2022-01-17 15:07:19 +01:00
|
|
|
};
|
|
|
|
|
|
2026-01-20 09:23:51 +00:00
|
|
|
m_previous_chunk_can_break_after = chunk.can_break_after;
|
|
|
|
|
|
2026-01-10 12:03:54 +01:00
|
|
|
add_extra_box_model_metrics_to_item(item, is_first_chunk, is_last_chunk);
|
2022-01-17 15:07:19 +01:00
|
|
|
return item;
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-02 14:53:39 +01:00
|
|
|
if (m_current_node->is_absolutely_positioned()) {
|
2025-08-08 21:13:35 +10:00
|
|
|
auto const& node = *m_current_node;
|
2022-02-15 01:54:22 +01:00
|
|
|
skip_to_next();
|
2022-03-07 22:27:09 +01:00
|
|
|
return Item {
|
|
|
|
|
.type = Item::Type::AbsolutelyPositionedElement,
|
|
|
|
|
.node = &node,
|
|
|
|
|
};
|
2022-02-15 01:54:22 +01:00
|
|
|
}
|
|
|
|
|
|
2022-03-22 19:18:05 +01:00
|
|
|
if (m_current_node->is_floating()) {
|
2025-08-08 21:13:35 +10:00
|
|
|
auto const& node = *m_current_node;
|
2022-03-22 19:18:05 +01:00
|
|
|
skip_to_next();
|
|
|
|
|
return Item {
|
|
|
|
|
.type = Item::Type::FloatingElement,
|
|
|
|
|
.node = &node,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-17 15:07:19 +01:00
|
|
|
if (is<Layout::BreakNode>(*m_current_node)) {
|
2025-08-08 21:13:35 +10:00
|
|
|
auto const& node = *m_current_node;
|
2022-01-17 15:07:19 +01:00
|
|
|
skip_to_next();
|
|
|
|
|
return Item {
|
|
|
|
|
.type = Item::Type::ForcedBreak,
|
2023-07-19 23:44:40 +00:00
|
|
|
.node = &node,
|
2022-01-17 15:07:19 +01:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-20 16:17:29 +01:00
|
|
|
if (is<Layout::ListItemMarkerBox>(*m_current_node)) {
|
|
|
|
|
skip_to_next();
|
2026-01-10 13:22:55 +01:00
|
|
|
return generate_next_item();
|
2022-01-20 16:17:29 +01:00
|
|
|
}
|
|
|
|
|
|
2022-01-17 15:07:19 +01:00
|
|
|
if (!is<Layout::Box>(*m_current_node)) {
|
|
|
|
|
skip_to_next();
|
2026-01-10 13:22:55 +01:00
|
|
|
return generate_next_item();
|
2022-01-17 15:07:19 +01:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 21:13:35 +10:00
|
|
|
auto const& box = as<Layout::Box>(*m_current_node);
|
|
|
|
|
auto const& box_state = m_layout_state.get(box);
|
2022-02-14 15:52:29 +01:00
|
|
|
m_inline_formatting_context.dimension_box_on_line(box, m_layout_mode);
|
2022-01-17 15:07:19 +01:00
|
|
|
|
2022-02-14 15:52:29 +01:00
|
|
|
auto item = Item {
|
2022-01-17 15:07:19 +01:00
|
|
|
.type = Item::Type::Element,
|
|
|
|
|
.node = &box,
|
|
|
|
|
.offset_in_node = 0,
|
|
|
|
|
.length_in_node = 0,
|
2022-07-17 17:59:02 +02:00
|
|
|
.width = box_state.content_width(),
|
2022-02-20 15:51:24 +01:00
|
|
|
.padding_start = box_state.padding_left,
|
|
|
|
|
.padding_end = box_state.padding_right,
|
|
|
|
|
.border_start = box_state.border_left,
|
|
|
|
|
.border_end = box_state.border_right,
|
|
|
|
|
.margin_start = box_state.margin_left,
|
|
|
|
|
.margin_end = box_state.margin_right,
|
2022-01-17 15:07:19 +01:00
|
|
|
};
|
2022-02-14 15:52:29 +01:00
|
|
|
add_extra_box_model_metrics_to_item(item, true, true);
|
2025-06-20 00:40:15 +02:00
|
|
|
skip_to_next();
|
2022-02-14 15:52:29 +01:00
|
|
|
return item;
|
2022-01-17 15:07:19 +01:00
|
|
|
}
|
|
|
|
|
|
2022-03-27 21:14:10 +02:00
|
|
|
void InlineLevelIterator::enter_text_node(Layout::TextNode const& text_node)
|
2022-01-17 15:07:19 +01:00
|
|
|
{
|
2025-05-22 00:31:24 +12:00
|
|
|
auto white_space_collapse = text_node.computed_values().white_space_collapse();
|
|
|
|
|
auto text_wrap_mode = text_node.computed_values().text_wrap_mode();
|
|
|
|
|
|
2025-09-12 10:06:27 +02:00
|
|
|
// https://drafts.csswg.org/css-text-4/#collapse
|
2025-05-22 00:31:24 +12:00
|
|
|
bool do_wrap_lines = text_wrap_mode == CSS::TextWrapMode::Wrap;
|
2025-09-12 10:06:27 +02:00
|
|
|
bool do_respect_linebreaks = first_is_one_of(white_space_collapse, CSS::WhiteSpaceCollapse::Preserve, CSS::WhiteSpaceCollapse::PreserveBreaks, CSS::WhiteSpaceCollapse::BreakSpaces);
|
2022-11-26 00:14:36 +01:00
|
|
|
|
2026-05-24 19:52:14 +02:00
|
|
|
auto const& chunks = text_node.chunks_for_layout(do_wrap_lines, do_respect_linebreaks);
|
2026-01-10 12:03:54 +01:00
|
|
|
|
2022-01-17 15:07:19 +01:00
|
|
|
m_text_node_context = TextNodeContext {
|
2026-05-30 12:06:04 +02:00
|
|
|
// OPTIMIZATION: The chunk list is cached by the TextNode and only read by this iterator, so keep a pointer
|
|
|
|
|
// to it instead of copying every chunk when entering a text node.
|
|
|
|
|
.chunk_list = &chunks,
|
2026-01-10 12:03:54 +01:00
|
|
|
.next_chunk_index = 0,
|
2026-05-24 19:52:14 +02:00
|
|
|
.should_collapse_whitespace = chunks.should_collapse_whitespace,
|
2026-01-10 12:03:54 +01:00
|
|
|
.should_wrap_lines = do_wrap_lines,
|
|
|
|
|
.should_respect_linebreaks = do_respect_linebreaks,
|
2022-01-17 15:07:19 +01:00
|
|
|
};
|
2022-02-14 15:52:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InlineLevelIterator::add_extra_box_model_metrics_to_item(Item& item, bool add_leading_metrics, bool add_trailing_metrics)
|
|
|
|
|
{
|
|
|
|
|
if (add_leading_metrics && m_extra_leading_metrics.has_value()) {
|
|
|
|
|
item.margin_start += m_extra_leading_metrics->margin;
|
|
|
|
|
item.border_start += m_extra_leading_metrics->border;
|
|
|
|
|
item.padding_start += m_extra_leading_metrics->padding;
|
|
|
|
|
m_extra_leading_metrics = {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (add_trailing_metrics && m_extra_trailing_metrics.has_value()) {
|
|
|
|
|
item.margin_end += m_extra_trailing_metrics->margin;
|
|
|
|
|
item.border_end += m_extra_trailing_metrics->border;
|
|
|
|
|
item.padding_end += m_extra_trailing_metrics->padding;
|
|
|
|
|
m_extra_trailing_metrics = {};
|
|
|
|
|
}
|
2022-01-17 15:07:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|