LibWeb: Exclude trailing whitespace from line width when placing floats

When generating line boxes, we place floats simultaneously with the
other items on the same line. The CSS text spec requires us to trim the
whitespace at the end of each line, but we only did so after laying out
all the line boxes.

This changes the way we calculate the current line box width for floats
by subtracting the amount of pixels that the current trailing whitespace
is using.

Fixes #4050.
This commit is contained in:
Jelle Raaijmakers 2025-03-26 16:11:50 +00:00
parent 85e28a29f0
commit ae21b3a971
5 changed files with 99 additions and 19 deletions

View file

@ -5,7 +5,6 @@
*/
#include <AK/CharacterTypes.h>
#include <AK/TypeCasts.h>
#include <AK/Utf8View.h>
#include <LibWeb/DOM/Position.h>
#include <LibWeb/Layout/Box.h>
@ -54,50 +53,70 @@ void LineBox::add_fragment(Node const& layout_node, int start, int length, CSSPi
m_block_length = max(m_block_length, content_height + border_box_top + border_box_bottom);
}
void LineBox::trim_trailing_whitespace()
CSSPixels LineBox::calculate_or_trim_trailing_whitespace(RemoveTrailingWhitespace should_remove)
{
auto should_trim = [](LineBoxFragment* fragment) {
auto ws = fragment->layout_node().computed_values().white_space();
return ws == CSS::WhiteSpace::Normal || ws == CSS::WhiteSpace::Nowrap || ws == CSS::WhiteSpace::PreLine;
};
CSSPixels whitespace_width = 0;
LineBoxFragment* last_fragment = nullptr;
size_t fragment_index = m_fragments.size();
for (;;) {
if (m_fragments.is_empty())
return;
// last_fragment cannot be null from here on down, as m_fragments is not empty.
last_fragment = &m_fragments.last();
if (fragment_index == 0)
return whitespace_width;
last_fragment = &m_fragments[--fragment_index];
auto const* dom_node = last_fragment->layout_node().dom_node();
if (dom_node) {
auto cursor_position = dom_node->document().cursor_position();
if (cursor_position && cursor_position->node() == dom_node)
return;
return whitespace_width;
}
if (!should_trim(last_fragment))
return;
if (last_fragment->is_justifiable_whitespace()) {
m_inline_length -= last_fragment->inline_length();
m_fragments.remove(m_fragments.size() - 1);
} else {
return whitespace_width;
if (!last_fragment->is_justifiable_whitespace())
break;
whitespace_width += last_fragment->inline_length();
if (should_remove == RemoveTrailingWhitespace::Yes) {
m_inline_length -= last_fragment->inline_length();
m_fragments.remove(fragment_index);
}
}
auto last_text = last_fragment->text();
if (last_text.is_null())
return;
return whitespace_width;
while (last_fragment->length()) {
auto last_character = last_text[last_fragment->length() - 1];
size_t last_fragment_length = last_fragment->length();
while (last_fragment_length) {
auto last_character = last_text[--last_fragment_length];
if (!is_ascii_space(last_character))
break;
auto const& font = last_fragment->glyph_run() ? last_fragment->glyph_run()->font() : last_fragment->layout_node().first_available_font();
int last_character_width = font.glyph_width(last_character);
last_fragment->m_length -= 1;
last_fragment->set_inline_length(last_fragment->inline_length() - last_character_width);
m_inline_length -= last_character_width;
whitespace_width += last_character_width;
if (should_remove == RemoveTrailingWhitespace::Yes) {
last_fragment->m_length -= 1;
last_fragment->set_inline_length(last_fragment->inline_length() - last_character_width);
m_inline_length -= last_character_width;
}
}
return whitespace_width;
}
CSSPixels LineBox::get_trailing_whitespace_width() const
{
return const_cast<LineBox&>(*this).calculate_or_trim_trailing_whitespace(RemoveTrailingWhitespace::No);
}
void LineBox::trim_trailing_whitespace()
{
calculate_or_trim_trailing_whitespace(RemoveTrailingWhitespace::Yes);
}
bool LineBox::is_empty_or_ends_in_whitespace() const

View file

@ -32,6 +32,7 @@ public:
Vector<LineBoxFragment> const& fragments() const { return m_fragments; }
Vector<LineBoxFragment>& fragments() { return m_fragments; }
CSSPixels get_trailing_whitespace_width() const;
void trim_trailing_whitespace();
bool is_empty_or_ends_in_whitespace() const;
@ -44,6 +45,13 @@ private:
friend class InlineFormattingContext;
friend class LineBuilder;
enum class RemoveTrailingWhitespace : u8 {
Yes,
No,
};
CSSPixels calculate_or_trim_trailing_whitespace(RemoveTrailingWhitespace);
Vector<LineBoxFragment> m_fragments;
CSSPixels m_inline_length { 0 };
CSSPixels m_block_length { 0 };

View file

@ -117,10 +117,14 @@ CSSPixels LineBuilder::y_for_float_to_be_inserted_here(Box const& box)
CSSPixels candidate_block_offset = m_current_block_offset;
// Determine the current line width and subtract trailing whitespace, since those have not yet been removed while
// placing floating boxes.
auto const& current_line = ensure_last_line_box();
auto current_line_width = current_line.width() - current_line.get_trailing_whitespace_width();
// If there's already inline content on the current line, check if the new float can fit
// alongside the content. If not, place it on the next line.
if (current_line.width() > 0 && (current_line.width() + width) > m_available_width_for_current_line)
if (current_line_width > 0 && (current_line_width + width) > m_available_width_for_current_line)
candidate_block_offset += current_line.height();
// Then, look for the next Y position where we can fit the new float.

View file

@ -0,0 +1,28 @@
Viewport <#document> at (0,0) content-size 800x600 children: not-inline
BlockContainer <html> at (0,0) content-size 800x33 [BFC] children: not-inline
BlockContainer <body> at (8,8) content-size 784x17 children: not-inline
BlockContainer <div.a> at (8,8) content-size 100x17 children: inline
frag 0 from BlockContainer start: 0, length: 0, rect: [8,8 50x17] baseline: 13.296875
TextNode <#text>
BlockContainer <div.b> at (8,8) content-size 50x17 inline-block [BFC] children: inline
frag 0 from TextNode start: 0, length: 3, rect: [8,8 27.15625x17] baseline: 13.296875
"foo"
TextNode <#text>
TextNode <#text>
BlockContainer <div.c> at (58,8) content-size 50x17 floating [BFC] children: inline
frag 0 from TextNode start: 0, length: 3, rect: [58,8 27.640625x17] baseline: 13.296875
"bar"
TextNode <#text>
TextNode <#text>
BlockContainer <(anonymous)> at (8,25) content-size 784x0 children: inline
TextNode <#text>
ViewportPaintable (Viewport<#document>) [0,0 800x600]
PaintableWithLines (BlockContainer<HTML>) [0,0 800x33]
PaintableWithLines (BlockContainer<BODY>) [8,8 784x17]
PaintableWithLines (BlockContainer<DIV>.a) [8,8 100x17]
PaintableWithLines (BlockContainer<DIV>.b) [8,8 50x17]
TextPaintable (TextNode<#text>)
PaintableWithLines (BlockContainer<DIV>.c) [58,8 50x17]
TextPaintable (TextNode<#text>)
PaintableWithLines (BlockContainer(anonymous)) [8,25 784x0]

View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<style>
.a {
background-color: red;
width: 100px;
}
.b {
background-color: green;
display: inline-block;
width: 50px;
}
.c {
background-color: blue;
float: right;
width: 50px;
}
</style>
<div class="a">
<div class="b">foo</div>
<div class="c">bar</div>
</div>