mirror of
https://github.com/RGBCube/serenity
synced 2025-07-27 22:57:44 +00:00
LibWeb: Improve the line breaking algorithm
Check the width of the next token after white space to decide line breaks. The next width can also be the total width of multiple tokens. This better follows the CSS Text specification and matches behavior of other browsers. Fixes #20388.
This commit is contained in:
parent
33b133d3f4
commit
3d7e788981
9 changed files with 227 additions and 13 deletions
|
@ -250,8 +250,14 @@ void InlineFormattingContext::generate_line_boxes(LayoutMode layout_mode)
|
|||
auto& item = item_opt.value();
|
||||
|
||||
// Ignore collapsible whitespace chunks at the start of line, and if the last fragment already ends in whitespace.
|
||||
if (item.is_collapsible_whitespace && (line_boxes.is_empty() || line_boxes.last().is_empty_or_ends_in_whitespace()))
|
||||
if (item.is_collapsible_whitespace && (line_boxes.is_empty() || line_boxes.last().is_empty_or_ends_in_whitespace())) {
|
||||
if (item.node->computed_values().white_space() != CSS::WhiteSpace::Nowrap) {
|
||||
auto next_width = iterator.next_non_whitespace_sequence_width();
|
||||
if (next_width > 0)
|
||||
line_builder.break_if_needed(next_width);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (item.type) {
|
||||
case InlineLevelIterator::Item::Type::ForcedBreak: {
|
||||
|
@ -287,17 +293,24 @@ void InlineFormattingContext::generate_line_boxes(LayoutMode layout_mode)
|
|||
case InlineLevelIterator::Item::Type::Text: {
|
||||
auto& text_node = verify_cast<Layout::TextNode>(*item.node);
|
||||
|
||||
if (text_node.computed_values().white_space() != CSS::WhiteSpace::Nowrap && line_builder.break_if_needed(item.border_box_width())) {
|
||||
if (text_node.computed_values().white_space() != CSS::WhiteSpace::Nowrap) {
|
||||
bool is_whitespace = false;
|
||||
CSSPixels next_width = 0;
|
||||
// If we're in a whitespace-collapsing context, we can simply check the flag.
|
||||
if (item.is_collapsible_whitespace) {
|
||||
is_whitespace = true;
|
||||
next_width = iterator.next_non_whitespace_sequence_width();
|
||||
} else {
|
||||
// In whitespace-preserving contexts (white-space: pre*), we have to check manually.
|
||||
auto view = text_node.text_for_rendering().substring_view(item.offset_in_node, item.length_in_node);
|
||||
is_whitespace = view.is_whitespace();
|
||||
if (is_whitespace)
|
||||
next_width = iterator.next_non_whitespace_sequence_width();
|
||||
}
|
||||
|
||||
// If whitespace caused us to break, we swallow the whitespace instead of
|
||||
// putting it on the next line.
|
||||
|
||||
// If we're in a whitespace-collapsing context, we can simply check the flag.
|
||||
if (item.is_collapsible_whitespace)
|
||||
break;
|
||||
|
||||
// In whitespace-preserving contexts (white-space: pre*), we have to check manually.
|
||||
auto view = text_node.text_for_rendering().substring_view(item.offset_in_node, item.length_in_node);
|
||||
if (view.is_whitespace())
|
||||
if (is_whitespace && next_width > 0 && line_builder.break_if_needed(item.border_box_width() + next_width))
|
||||
break;
|
||||
}
|
||||
line_builder.append_text_chunk(
|
||||
|
|
|
@ -125,6 +125,37 @@ void InlineLevelIterator::skip_to_next()
|
|||
}
|
||||
|
||||
Optional<InlineLevelIterator::Item> InlineLevelIterator::next()
|
||||
{
|
||||
if (m_lookahead_items.is_empty())
|
||||
return next_without_lookahead();
|
||||
return m_lookahead_items.dequeue();
|
||||
}
|
||||
|
||||
CSSPixels InlineLevelIterator::next_non_whitespace_sequence_width()
|
||||
{
|
||||
CSSPixels next_width = 0;
|
||||
for (;;) {
|
||||
auto next_item_opt = next_without_lookahead();
|
||||
if (!next_item_opt.has_value())
|
||||
break;
|
||||
m_lookahead_items.enqueue(next_item_opt.release_value());
|
||||
auto& next_item = m_lookahead_items.tail();
|
||||
if (next_item.node->computed_values().white_space() != CSS::WhiteSpace::Nowrap) {
|
||||
if (next_item.type != InlineLevelIterator::Item::Type::Text)
|
||||
break;
|
||||
if (next_item.is_collapsible_whitespace)
|
||||
break;
|
||||
auto& next_text_node = verify_cast<Layout::TextNode>(*(next_item.node));
|
||||
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_whitespace())
|
||||
break;
|
||||
}
|
||||
next_width += next_item.border_box_width();
|
||||
}
|
||||
return next_width;
|
||||
}
|
||||
|
||||
Optional<InlineLevelIterator::Item> InlineLevelIterator::next_without_lookahead()
|
||||
{
|
||||
if (!m_current_node)
|
||||
return {};
|
||||
|
@ -139,7 +170,7 @@ Optional<InlineLevelIterator::Item> InlineLevelIterator::next()
|
|||
if (!chunk_opt.has_value()) {
|
||||
m_text_node_context = {};
|
||||
skip_to_next();
|
||||
return next();
|
||||
return next_without_lookahead();
|
||||
}
|
||||
|
||||
m_text_node_context->next_chunk = m_text_node_context->chunk_iterator.next();
|
||||
|
@ -200,12 +231,12 @@ Optional<InlineLevelIterator::Item> InlineLevelIterator::next()
|
|||
|
||||
if (is<Layout::ListItemMarkerBox>(*m_current_node)) {
|
||||
skip_to_next();
|
||||
return next();
|
||||
return next_without_lookahead();
|
||||
}
|
||||
|
||||
if (!is<Layout::Box>(*m_current_node)) {
|
||||
skip_to_next();
|
||||
return next();
|
||||
return next_without_lookahead();
|
||||
}
|
||||
|
||||
if (is<Layout::ReplacedBox>(*m_current_node)) {
|
||||
|
|
|
@ -52,8 +52,10 @@ public:
|
|||
InlineLevelIterator(Layout::InlineFormattingContext&, LayoutState&, Layout::BlockContainer const&, LayoutMode);
|
||||
|
||||
Optional<Item> next();
|
||||
CSSPixels next_non_whitespace_sequence_width();
|
||||
|
||||
private:
|
||||
Optional<Item> next_without_lookahead();
|
||||
void skip_to_next();
|
||||
void compute_next();
|
||||
|
||||
|
@ -96,6 +98,7 @@ private:
|
|||
Optional<ExtraBoxMetrics> m_extra_trailing_metrics;
|
||||
|
||||
Vector<JS::NonnullGCPtr<NodeWithStyleAndBoxModelMetrics const>> m_box_model_node_stack;
|
||||
Queue<InlineLevelIterator::Item> m_lookahead_items;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue