LibWeb: Refactor Layout::TextNode splitting into a chunk iterator

Creating a ChunkIterator allows you to iterate over the text in a
Layout::TextNode at your leisure by calling next() when you want
another chunk.

This is one of many steps towards improving inline layout.
This commit is contained in:
Andreas Kling 2021-04-27 13:05:50 +02:00
parent 03d8ee1082
commit 5074aaea69
Notes: sideshowbarker 2024-07-18 19:01:36 +09:00
2 changed files with 115 additions and 77 deletions

View file

@ -1,11 +1,11 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ScopeGuard.h>
#include <AK/StringBuilder.h>
#include <AK/Utf8View.h>
#include <LibGfx/Painter.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/Layout/BlockBox.h>
@ -102,59 +102,6 @@ void TextNode::paint_cursor_if_needed(PaintContext& context, const LineBoxFragme
context.painter().draw_rect(cursor_rect, computed_values().color());
}
template<typename Callback>
void TextNode::for_each_chunk(Callback callback, LayoutMode layout_mode, bool do_wrap_lines, bool do_wrap_breaks) const
{
Utf8View view(m_text_for_rendering);
if (view.is_empty())
return;
auto start_of_chunk = view.begin();
auto commit_chunk = [&](auto it, bool has_breaking_newline, bool must_commit = false) {
if (layout_mode == LayoutMode::OnlyRequiredLineBreaks && !must_commit)
return;
int start = view.byte_offset_of(start_of_chunk);
int length = view.byte_offset_of(it) - view.byte_offset_of(start_of_chunk);
if (has_breaking_newline || length > 0) {
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;
};
bool last_was_space = isspace(*view.begin());
bool last_was_newline = false;
for (auto it = view.begin(); it != view.end();) {
if (layout_mode == LayoutMode::AllPossibleLineBreaks) {
commit_chunk(it, false);
}
if (last_was_newline) {
last_was_newline = false;
commit_chunk(it, true);
}
if (do_wrap_breaks && *it == '\n') {
last_was_newline = true;
commit_chunk(it, false);
}
if (do_wrap_lines) {
bool is_space = isspace(*it);
if (is_space != last_was_space) {
last_was_space = is_space;
commit_chunk(it, false);
}
}
++it;
}
if (last_was_newline)
commit_chunk(view.end(), true);
if (start_of_chunk != view.end())
commit_chunk(view.end(), false, true);
}
void TextNode::split_into_lines_by_rules(InlineFormattingContext& context, LayoutMode layout_mode, bool do_collapse, bool do_wrap_lines, bool do_wrap_breaks)
{
auto& containing_block = context.containing_block();
@ -193,22 +140,15 @@ void TextNode::split_into_lines_by_rules(InlineFormattingContext& context, Layou
m_text_for_rendering = dom_node().data();
}
// do_wrap_lines => chunks_are_words
// !do_wrap_lines => chunks_are_lines
struct Chunk {
Utf8View view;
int start { 0 };
int length { 0 };
bool is_break { false };
bool is_all_whitespace { false };
};
Vector<Chunk, 128> chunks;
ChunkIterator iterator(m_text_for_rendering, layout_mode, do_wrap_lines, do_wrap_breaks);
for_each_chunk(
[&](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 (;;) {
auto chunk = iterator.next();
if (!chunk.has_value())
break;
chunks.append(chunk.release_value());
}
for (size_t i = 0; i < chunks.size(); ++i) {
auto& chunk = chunks[i];
@ -251,11 +191,9 @@ void TextNode::split_into_lines_by_rules(InlineFormattingContext& context, Layou
}
}
if (do_wrap_breaks) {
if (chunk.is_break) {
containing_block.add_line_box();
available_width = context.available_width_at_line(line_boxes.size() - 1);
}
if (do_wrap_breaks && chunk.has_breaking_newline) {
containing_block.add_line_box();
available_width = context.available_width_at_line(line_boxes.size() - 1);
}
}
}
@ -319,4 +257,80 @@ void TextNode::handle_mousemove(Badge<EventHandler>, const Gfx::IntPoint& positi
downcast<Label>(*parent()).handle_mousemove_on_label({}, position, button);
}
TextNode::ChunkIterator::ChunkIterator(StringView const& text, LayoutMode layout_mode, bool wrap_lines, bool wrap_breaks)
: m_layout_mode(layout_mode)
, m_wrap_lines(wrap_lines)
, m_wrap_breaks(wrap_breaks)
, m_utf8_view(text)
, m_start_of_chunk(m_utf8_view.begin())
, m_iterator(m_utf8_view.begin())
{
m_last_was_space = !text.is_empty() && isspace(*m_utf8_view.begin());
}
Optional<TextNode::Chunk> TextNode::ChunkIterator::next()
{
while (m_iterator != m_utf8_view.end()) {
auto guard = ScopeGuard([&] { ++m_iterator; });
if (m_layout_mode == LayoutMode::AllPossibleLineBreaks) {
if (auto result = try_commit_chunk(m_iterator, false); result.has_value())
return result.release_value();
}
if (m_last_was_newline) {
m_last_was_newline = false;
if (auto result = try_commit_chunk(m_iterator, true); result.has_value())
return result.release_value();
}
if (m_wrap_breaks && *m_iterator == '\n') {
m_last_was_newline = true;
if (auto result = try_commit_chunk(m_iterator, false); result.has_value())
return result.release_value();
}
if (m_wrap_lines) {
bool is_space = isspace(*m_iterator);
if (is_space != m_last_was_space) {
m_last_was_space = is_space;
if (auto result = try_commit_chunk(m_iterator, false); result.has_value())
return result.release_value();
}
}
}
if (m_last_was_newline) {
m_last_was_newline = false;
if (auto result = try_commit_chunk(m_utf8_view.end(), true); result.has_value())
return result.release_value();
}
if (m_start_of_chunk != m_utf8_view.end()) {
if (auto result = try_commit_chunk(m_utf8_view.end(), false, true); result.has_value())
return result.release_value();
}
return {};
}
Optional<TextNode::Chunk> TextNode::ChunkIterator::try_commit_chunk(Utf8View::Iterator const& it, bool has_breaking_newline, bool must_commit)
{
if (m_layout_mode == LayoutMode::OnlyRequiredLineBreaks && !must_commit)
return {};
auto start = m_utf8_view.byte_offset_of(m_start_of_chunk);
auto length = m_utf8_view.byte_offset_of(it) - m_utf8_view.byte_offset_of(m_start_of_chunk);
if (has_breaking_newline || length > 0) {
auto chunk_view = m_utf8_view.substring_view(start, length);
m_start_of_chunk = it;
return Chunk {
.view = chunk_view,
.start = start,
.length = length,
.has_breaking_newline = has_breaking_newline,
.is_all_whitespace = is_all_whitespace(chunk_view.as_string()),
};
}
m_start_of_chunk = it;
return {};
}
}

View file

@ -6,6 +6,7 @@
#pragma once
#include <AK/Utf8View.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/Layout/Node.h>
@ -26,6 +27,32 @@ public:
virtual void split_into_lines(InlineFormattingContext&, LayoutMode) override;
struct Chunk {
Utf8View view;
size_t start { 0 };
size_t length { 0 };
bool has_breaking_newline { false };
bool is_all_whitespace { false };
};
class ChunkIterator {
public:
ChunkIterator(StringView const& text, LayoutMode, bool wrap_lines, bool wrap_breaks);
Optional<Chunk> next();
private:
Optional<Chunk> try_commit_chunk(Utf8View::Iterator const&, bool has_breaking_newline, bool must_commit = false);
const LayoutMode m_layout_mode;
const bool m_wrap_lines;
const bool m_wrap_breaks;
bool m_last_was_space { false };
bool m_last_was_newline { false };
Utf8View m_utf8_view;
Utf8View::Iterator m_start_of_chunk;
Utf8View::Iterator m_iterator;
};
private:
virtual bool is_text_node() const final { return true; }
virtual bool wants_mouse_events() const override;
@ -35,9 +62,6 @@ private:
void split_into_lines_by_rules(InlineFormattingContext&, LayoutMode, bool do_collapse, bool do_wrap_lines, bool do_wrap_breaks);
void paint_cursor_if_needed(PaintContext&, const LineBoxFragment&) const;
template<typename Callback>
void for_each_chunk(Callback, LayoutMode, bool do_wrap_lines, bool do_wrap_breaks) const;
String m_text_for_rendering;
};