mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-04-19 10:20:22 +00:00
Add unsafe_layout_node(), unsafe_paintable(), and unsafe_paintable_box() accessors that skip layout-staleness verification. These are for use in contexts where accessing layout/paintable data is legitimate despite layout not being up to date: tree construction, style recalculation, painting, animation interpolation, DOM mutation, and invalidation propagation. Also add wrapper APIs on Node to centralize common patterns: - set_needs_display() wraps if (unsafe_paintable()) ...set_needs_display - set_needs_paint_only_properties_update() wraps similar - set_needs_layout_update() wraps if (unsafe_layout_node()) ... And add Document::layout_is_up_to_date() which checks whether layout tree update flags are all clear.
119 lines
4.4 KiB
C++
119 lines
4.4 KiB
C++
/*
|
|
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/Utf16View.h>
|
|
#include <LibGfx/TextLayout.h>
|
|
#include <LibWeb/DOM/Text.h>
|
|
#include <LibWeb/GraphemeEdgeTracker.h>
|
|
#include <LibWeb/Layout/TextNode.h>
|
|
|
|
namespace Web {
|
|
|
|
// FIXME: Using newline characters to determine line breaks is insufficient. If a line is wrapped due space constraints,
|
|
// we want to consider each segment of the wrapped line as its own line in the algorithms below.
|
|
|
|
size_t find_line_start(Utf16View const& view, size_t offset)
|
|
{
|
|
while (offset != 0 && view.code_unit_at(offset - 1) != '\n')
|
|
--offset;
|
|
return offset;
|
|
}
|
|
|
|
size_t find_line_end(Utf16View const& view, size_t offset)
|
|
{
|
|
auto length = view.length_in_code_units();
|
|
while (offset < length && view.code_unit_at(offset) != '\n')
|
|
++offset;
|
|
return offset;
|
|
}
|
|
|
|
static float measure_text_width(Layout::TextNode const& text_node, Utf16View const& text)
|
|
{
|
|
if (text.is_empty())
|
|
return 0;
|
|
|
|
auto grapheme_segmenter = text_node.grapheme_segmenter().clone();
|
|
grapheme_segmenter->set_segmented_text(text);
|
|
|
|
auto line_segmenter = text_node.line_segmenter().clone();
|
|
line_segmenter->set_segmented_text(text);
|
|
|
|
Layout::TextNode::ChunkIterator iterator { text_node, text, *grapheme_segmenter, *line_segmenter, CSS::WordBreak::Normal, false, false };
|
|
auto letter_spacing = text_node.computed_values().letter_spacing().to_float();
|
|
float width = 0;
|
|
|
|
for (auto chunk = iterator.next(); chunk.has_value(); chunk = iterator.next())
|
|
width += Gfx::measure_text_width(chunk->view, *chunk->font, letter_spacing);
|
|
|
|
return width;
|
|
}
|
|
|
|
static size_t translate_position_across_lines(Layout::TextNode const& text_node, Utf16View const& source_line, Utf16View const& target_line)
|
|
{
|
|
GraphemeEdgeTracker tracker(measure_text_width(text_node, source_line));
|
|
auto previous_index = 0uz;
|
|
|
|
text_node.grapheme_segmenter().clone()->for_each_boundary(target_line, [&](auto index) {
|
|
auto current_width = measure_text_width(text_node, target_line.substring_view(previous_index, index - previous_index));
|
|
|
|
if (tracker.update(index - previous_index, current_width) == IterationDecision::Break)
|
|
return IterationDecision::Break;
|
|
|
|
previous_index = index;
|
|
return IterationDecision::Continue;
|
|
});
|
|
|
|
return tracker.resolve();
|
|
}
|
|
|
|
Optional<size_t> compute_cursor_position_on_next_line(DOM::Text const& dom_node, size_t current_offset)
|
|
{
|
|
// NB: Called during text editing, layout may not be current.
|
|
auto const* layout_node = as_if<Layout::TextNode>(dom_node.unsafe_layout_node());
|
|
if (!layout_node)
|
|
return {};
|
|
|
|
auto text = dom_node.data().utf16_view();
|
|
auto new_offset = text.length_in_code_units();
|
|
|
|
if (auto current_line_end = find_line_end(text, current_offset); current_line_end < text.length_in_code_units()) {
|
|
auto current_line_start = find_line_start(text, current_offset);
|
|
auto current_line_text = text.substring_view(current_line_start, current_offset - current_line_start);
|
|
|
|
auto next_line_start = current_line_end + 1;
|
|
auto next_line_length = find_line_end(text, next_line_start) - next_line_start;
|
|
auto next_line_text = text.substring_view(next_line_start, next_line_length);
|
|
|
|
new_offset = next_line_start + translate_position_across_lines(*layout_node, current_line_text, next_line_text);
|
|
}
|
|
|
|
return new_offset;
|
|
}
|
|
|
|
Optional<size_t> compute_cursor_position_on_previous_line(DOM::Text const& dom_node, size_t current_offset)
|
|
{
|
|
// NB: Called during text editing, layout may not be current.
|
|
auto const* layout_node = as_if<Layout::TextNode>(dom_node.unsafe_layout_node());
|
|
if (!layout_node)
|
|
return {};
|
|
|
|
auto text = dom_node.data().utf16_view();
|
|
auto new_offset = 0uz;
|
|
|
|
if (auto current_line_start = find_line_start(text, current_offset); current_line_start != 0) {
|
|
auto current_line_text = text.substring_view(current_line_start, current_offset - current_line_start);
|
|
|
|
auto previous_line_start = find_line_start(text, current_line_start - 1);
|
|
auto previous_line_length = current_line_start - previous_line_start - 1;
|
|
auto previous_line_text = text.substring_view(previous_line_start, previous_line_length);
|
|
|
|
new_offset = previous_line_start + translate_position_across_lines(*layout_node, current_line_text, previous_line_text);
|
|
}
|
|
|
|
return new_offset;
|
|
}
|
|
|
|
}
|