LibWeb: Teach line layout to collapse whitespace across fragments

This kind of HTML now produces a single piece of whitespace:

<span> </span> <span> </span> <span> </span>

We achieve this by checking if the last fragment on the last line ends
in whitespace. If so, we either don't add a fragment at all (for the
current chunk) or we simply skip over all whitespace at the head of
the current chunk (instead of collapsing it to a single ' '.)
This commit is contained in:
Andreas Kling 2020-06-13 14:59:17 +02:00
parent 4ab1b0b436
commit 07ccaa1934
Notes: sideshowbarker 2024-07-19 05:40:25 +09:00
5 changed files with 46 additions and 15 deletions

View file

@ -46,7 +46,7 @@ LayoutText::~LayoutText()
{
}
static bool is_all_whitespace(const String& string)
static bool is_all_whitespace(const StringView& string)
{
for (size_t i = 0; i < string.length(); ++i) {
if (!isspace(string[i]))
@ -111,7 +111,8 @@ void LayoutText::for_each_chunk(Callback callback, LayoutMode layout_mode, bool
int length = view.byte_offset_of(it) - view.byte_offset_of(start_of_chunk);
if (has_breaking_newline || length > 0) {
callback(view.substring_view(start, length), start, length, has_breaking_newline);
auto chunk_view = view.substring_view(start, length);
callback(chunk_view, start, length, has_breaking_newline, is_all_whitespace(chunk_view.as_string()));
}
start_of_chunk = it;
@ -160,17 +161,23 @@ void LayoutText::split_into_lines_by_rules(LayoutBlock& container, LayoutMode la
if (do_collapse) {
auto utf8_view = Utf8View(node().data());
StringBuilder builder(node().data().length());
for (auto it = utf8_view.begin(); it != utf8_view.end(); ++it) {
auto it = utf8_view.begin();
auto skip_over_whitespace = [&] {
auto prev = it;
while (it != utf8_view.end() && isspace(*it)) {
prev = it;
++it;
}
it = prev;
};
if (line_boxes.last().ends_in_whitespace())
skip_over_whitespace();
for (; it != utf8_view.end(); ++it) {
if (!isspace(*it)) {
builder.append(utf8_view.as_string().characters_without_null_termination() + utf8_view.byte_offset_of(it), it.codepoint_length_in_bytes());
} else {
builder.append(' ');
auto prev = it;
while (it != utf8_view.end() && isspace(*it)) {
prev = it;
++it;
}
it = prev;
skip_over_whitespace();
}
}
m_text_for_rendering = builder.to_string();
@ -182,25 +189,30 @@ void LayoutText::split_into_lines_by_rules(LayoutBlock& container, LayoutMode la
// !do_wrap_lines => chunks_are_lines
struct Chunk {
Utf8View view;
int start;
int length;
bool is_break;
int start { 0 };
int length { 0 };
bool is_break { false };
bool is_all_whitespace { false };
};
Vector<Chunk> chunks;
for_each_chunk(
[&](const Utf8View& view, int start, int length, bool is_break) {
chunks.append({ Utf8View(view), start, length, is_break });
[&](const Utf8View& view, int start, int length, bool is_break, bool is_all_whitespace) {
chunks.append({ Utf8View(view), start, length, is_break, is_all_whitespace });
},
layout_mode, do_wrap_lines, do_wrap_breaks);
for (size_t i = 0; i < chunks.size(); ++i) {
auto& chunk = chunks[i];
// Collapse entire fragment into non-existence if previous fragment on line ended in whitespace.
if (do_collapse && line_boxes.last().ends_in_whitespace() && chunk.is_all_whitespace)
continue;
float chunk_width;
bool need_collapse = false;
if (do_wrap_lines) {
bool need_collapse = do_collapse && isspace(*chunk.view.begin());
need_collapse = do_collapse && isspace(*chunk.view.begin()) && line_boxes.last().ends_in_whitespace();
if (need_collapse)
chunk_width = space_width;

View file

@ -73,4 +73,11 @@ void LineBox::trim_trailing_whitespace()
}
}
bool LineBox::ends_in_whitespace() const
{
if (m_fragments.is_empty())
return false;
return m_fragments.last().ends_in_whitespace();
}
}

View file

@ -45,6 +45,8 @@ public:
void trim_trailing_whitespace();
bool ends_in_whitespace() const;
private:
friend class LayoutBlock;
NonnullOwnPtrVector<LineBoxFragment> m_fragments;

View file

@ -30,6 +30,7 @@
#include <LibWeb/Layout/LayoutText.h>
#include <LibWeb/Layout/LineBoxFragment.h>
#include <LibWeb/RenderingContext.h>
#include <ctype.h>
namespace Web {
@ -45,6 +46,14 @@ void LineBoxFragment::render(RenderingContext& context)
}
}
bool LineBoxFragment::ends_in_whitespace() const
{
auto text = this->text();
if (text.is_empty())
return false;
return isspace(text[text.length() - 1]);
}
bool LineBoxFragment::is_justifiable_whitespace() const
{
return text() == " ";

View file

@ -62,6 +62,7 @@ public:
void render(RenderingContext&);
bool ends_in_whitespace() const;
bool is_justifiable_whitespace() const;
StringView text() const;