diff options
author | Andreas Kling <kling@serenityos.org> | 2021-04-27 13:05:50 +0200 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2021-04-27 19:11:59 +0200 |
commit | 5074aaea69f7cec9673aa57ba277686c153d7862 (patch) | |
tree | 2d9f15065ae169087ba3920cc4c97c62d7736a5b /Userland/Libraries/LibWeb/Layout | |
parent | 03d8ee1082d11fafae58f6697edb7e7a04ec6b82 (diff) | |
download | serenity-5074aaea69f7cec9673aa57ba277686c153d7862.zip |
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.
Diffstat (limited to 'Userland/Libraries/LibWeb/Layout')
-rw-r--r-- | Userland/Libraries/LibWeb/Layout/TextNode.cpp | 162 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/Layout/TextNode.h | 30 |
2 files changed, 115 insertions, 77 deletions
diff --git a/Userland/Libraries/LibWeb/Layout/TextNode.cpp b/Userland/Libraries/LibWeb/Layout/TextNode.cpp index df362475e2..e10a57bd8a 100644 --- a/Userland/Libraries/LibWeb/Layout/TextNode.cpp +++ b/Userland/Libraries/LibWeb/Layout/TextNode.cpp @@ -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 {}; +} + } diff --git a/Userland/Libraries/LibWeb/Layout/TextNode.h b/Userland/Libraries/LibWeb/Layout/TextNode.h index ded15114d3..5597f42df0 100644 --- a/Userland/Libraries/LibWeb/Layout/TextNode.h +++ b/Userland/Libraries/LibWeb/Layout/TextNode.h @@ -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; }; |