LibWeb: Treat DOM::Range offsets as UTF-16 code unit offsets

We generated `PaintableFragment`s with a start and length represented in
UTF-8 byte offsets, but failed to consider that the offsets in a
`DOM::Range` are actually expressed in UTF-16 code units.

This is a bit of a mess: almost all web specs use UTF-16 code units as
the unit for indexing into text nodes, but we almost exclusively use
UTF-8 in our code base. Arguably the best thing would for us to use
UTF-16 everywhere as well: it prevents these mismatches in our
implementations for the price of a bit more memory usage - and even that
could potentially be optimized for.

But for now, try to do the correct thing and lazily allocate UTF-16 data
in a `PaintableFragment` whenever we need to index into it or if we're
asked to determine the code unit offset of a pixel position.
This commit is contained in:
Jelle Raaijmakers 2025-06-12 13:19:52 +02:00 committed by Jelle Raaijmakers
parent dbbdf2cebc
commit 3df83dade8
Notes: github-actions[bot] 2025-06-13 13:09:49 +00:00
6 changed files with 110 additions and 36 deletions

View file

@ -670,7 +670,14 @@ void paint_cursor_if_needed(PaintContext& context, TextPaintable const& paintabl
return;
// NOTE: This checks if the cursor is before the start or after the end of the fragment. If it is at the end, after all text, it should still be painted.
if (cursor_position->offset() < (unsigned)fragment.start() || cursor_position->offset() > (unsigned)(fragment.start() + fragment.length()))
size_t cursor_position_byte_offset = 0;
if (cursor_position->offset() == fragment.utf16_view().length_in_code_units()) {
cursor_position_byte_offset = fragment.utf8_view().byte_length();
} else {
auto cursor_position_code_point_offset = fragment.utf16_view().code_point_offset_of(cursor_position->offset());
cursor_position_byte_offset = fragment.utf8_view().byte_offset_of(cursor_position_code_point_offset);
}
if (cursor_position_byte_offset < fragment.start() || cursor_position_byte_offset > (fragment.start() + fragment.length()))
return;
auto active_element = document.active_element();
@ -687,10 +694,12 @@ void paint_cursor_if_needed(PaintContext& context, TextPaintable const& paintabl
auto fragment_rect = fragment.absolute_rect();
auto text = fragment.string_view();
auto const& font = fragment.glyph_run() ? fragment.glyph_run()->font() : fragment.layout_node().first_available_font();
auto utf8_text = fragment.utf8_view();
auto cursor_offset = font.width(utf8_text.substring_view(fragment.start(), cursor_position_byte_offset - fragment.start()));
CSSPixelRect cursor_rect {
fragment_rect.x() + CSSPixels::nearest_value_for(font.width(text.substring_view(0, document.cursor_position()->offset() - fragment.start()))),
fragment_rect.x() + CSSPixels::nearest_value_for(cursor_offset),
fragment_rect.top(),
1,
fragment_rect.height()
@ -1182,7 +1191,7 @@ TraversalDecision PaintableWithLines::hit_test(CSSPixelPoint position, HitTestTy
if (fragment_absolute_rect.contains(transformed_position_adjusted_by_scroll_offset)) {
if (fragment.paintable().hit_test(transformed_position_adjusted_by_scroll_offset, type, callback) == TraversalDecision::Break)
return TraversalDecision::Break;
HitTestResult hit_test_result { const_cast<Paintable&>(fragment.paintable()), fragment.text_index_at(transformed_position_adjusted_by_scroll_offset), 0, 0 };
HitTestResult hit_test_result { const_cast<Paintable&>(fragment.paintable()), fragment.index_in_node_for_point(transformed_position_adjusted_by_scroll_offset), 0, 0 };
if (callback(hit_test_result) == TraversalDecision::Break)
return TraversalDecision::Break;
} else if (type == HitTestType::TextCursor) {
@ -1208,7 +1217,7 @@ TraversalDecision PaintableWithLines::hit_test(CSSPixelPoint position, HitTestTy
if (fragment_absolute_rect.bottom() - 1 <= transformed_position_adjusted_by_scroll_offset.y()) { // fully below the fragment
HitTestResult hit_test_result {
.paintable = const_cast<Paintable&>(fragment.paintable()),
.index_in_node = fragment.start() + fragment.length(),
.index_in_node = fragment.index_in_node_for_byte_offset(fragment.start() + fragment.length()),
.vertical_distance = transformed_position_adjusted_by_scroll_offset.y() - fragment_absolute_rect.bottom(),
};
if (callback(hit_test_result) == TraversalDecision::Break)
@ -1217,7 +1226,7 @@ TraversalDecision PaintableWithLines::hit_test(CSSPixelPoint position, HitTestTy
if (transformed_position_adjusted_by_scroll_offset.x() < fragment_absolute_rect.left()) {
HitTestResult hit_test_result {
.paintable = const_cast<Paintable&>(fragment.paintable()),
.index_in_node = fragment.start(),
.index_in_node = fragment.index_in_node_for_byte_offset(fragment.start()),
.vertical_distance = 0,
.horizontal_distance = fragment_absolute_rect.left() - transformed_position_adjusted_by_scroll_offset.x(),
};
@ -1226,7 +1235,7 @@ TraversalDecision PaintableWithLines::hit_test(CSSPixelPoint position, HitTestTy
} else if (transformed_position_adjusted_by_scroll_offset.x() > fragment_absolute_rect.right()) {
HitTestResult hit_test_result {
.paintable = const_cast<Paintable&>(fragment.paintable()),
.index_in_node = fragment.start() + fragment.length(),
.index_in_node = fragment.index_in_node_for_byte_offset(fragment.start() + fragment.length()),
.vertical_distance = 0,
.horizontal_distance = transformed_position_adjusted_by_scroll_offset.x() - fragment_absolute_rect.right(),
};