LibWeb: Prevent cursor movement when collapsing text selection

Previously if you had a selection and you pressed the left/right arrow
key it would collapse the selection *and* perform the movement. This is
not how most text editors work and felt unnatural.
This commit is contained in:
zac 2026-03-07 15:18:57 +10:00 committed by Shannon Booth
parent dc381c4ba7
commit 2e930e83ce
3 changed files with 120 additions and 10 deletions

View file

@ -1034,7 +1034,12 @@ void FormAssociatedTextControlElement::increment_cursor_position_offset(Collapse
auto const text_node = form_associated_element_to_text_node();
if (!text_node)
return;
if (auto offset = text_node->grapheme_segmenter().next_boundary(m_selection_end); offset.has_value()) {
// If there is a selection range, collapse to the end (max) of that range without moving forward
if (collapse == CollapseSelection::Yes && (m_selection_start != m_selection_end)) {
collapse_selection_to_offset(max(m_selection_start, m_selection_end));
}
// Otherwise, move forward if possible
else if (auto offset = text_node->grapheme_segmenter().next_boundary(m_selection_end); offset.has_value()) {
if (collapse == CollapseSelection::Yes) {
collapse_selection_to_offset(*offset);
} else {
@ -1049,7 +1054,12 @@ void FormAssociatedTextControlElement::decrement_cursor_position_offset(Collapse
auto const text_node = form_associated_element_to_text_node();
if (!text_node)
return;
if (auto offset = text_node->grapheme_segmenter().previous_boundary(m_selection_end); offset.has_value()) {
// If there is a selection range, collapse to the start (min) of that range without moving backward
if (collapse == CollapseSelection::Yes && (m_selection_start != m_selection_end)) {
collapse_selection_to_offset(min(m_selection_start, m_selection_end));
}
// Otherwise, move backward if possible
else if (auto offset = text_node->grapheme_segmenter().previous_boundary(m_selection_end); offset.has_value()) {
if (collapse == CollapseSelection::Yes) {
collapse_selection_to_offset(*offset);
} else {

View file

@ -597,15 +597,21 @@ void Selection::move_offset_to_next_character(bool collapse_selection)
if (!text_node)
return;
if (auto offset = text_node->grapheme_segmenter().next_boundary(focus_offset()); offset.has_value()) {
// If there is a selection range, collapse to the end (max) of that range without moving forward
if (collapse_selection && !is_collapsed()) {
MUST(collapse(text_node, max(anchor_offset(), focus_offset())));
m_document->reset_cursor_blink_cycle();
}
// Otherwise, move forward if possible
else if (auto offset = text_node->grapheme_segmenter().next_boundary(focus_offset()); offset.has_value()) {
if (collapse_selection) {
MUST(collapse(text_node, *offset));
m_document->reset_cursor_blink_cycle();
} else {
MUST(set_base_and_extent(*text_node, anchor_offset(), *text_node, *offset));
}
scroll_focus_into_view();
}
scroll_focus_into_view();
}
void Selection::move_offset_to_previous_character(bool collapse_selection)
@ -614,15 +620,21 @@ void Selection::move_offset_to_previous_character(bool collapse_selection)
if (!text_node)
return;
if (auto offset = text_node->grapheme_segmenter().previous_boundary(focus_offset()); offset.has_value()) {
// If there is a selection range, collapse to the start (min) of that range without moving backward
if (collapse_selection && !is_collapsed()) {
MUST(collapse(text_node, min(anchor_offset(), focus_offset())));
m_document->reset_cursor_blink_cycle();
}
// Otherwise, move backward if possible
else if (auto offset = text_node->grapheme_segmenter().previous_boundary(focus_offset()); offset.has_value()) {
if (collapse_selection) {
MUST(collapse(text_node, *offset));
m_document->reset_cursor_blink_cycle();
} else {
MUST(set_base_and_extent(*text_node, anchor_offset(), *text_node, *offset));
}
scroll_focus_into_view();
}
scroll_focus_into_view();
}
void Selection::move_offset_to_next_word(bool collapse_selection)
@ -637,13 +649,13 @@ void Selection::move_offset_to_next_word(bool collapse_selection)
break;
if (auto offset = text_node->word_segmenter().next_boundary(focus_offset); offset.has_value()) {
auto word = text_node->data().substring_view(focus_offset, *offset - focus_offset);
if (collapse_selection) {
MUST(collapse(text_node, *offset));
m_document->reset_cursor_blink_cycle();
} else {
MUST(set_base_and_extent(*text_node, anchor_offset(), *text_node, *offset));
}
auto word = text_node->data().substring_view(focus_offset, *offset - focus_offset);
if (Unicode::Segmenter::should_continue_beyond_word(word))
continue;
}
@ -661,13 +673,13 @@ void Selection::move_offset_to_previous_word(bool collapse_selection)
while (true) {
auto focus_offset = this->focus_offset();
if (auto offset = text_node->word_segmenter().previous_boundary(focus_offset); offset.has_value()) {
auto word = text_node->data().substring_view(*offset, focus_offset - *offset);
if (collapse_selection) {
MUST(collapse(text_node, *offset));
m_document->reset_cursor_blink_cycle();
} else {
MUST(set_base_and_extent(*text_node, anchor_offset(), *text_node, *offset));
}
auto word = text_node->data().substring_view(*offset, focus_offset - *offset);
if (Unicode::Segmenter::should_continue_beyond_word(word))
continue;
}

View file

@ -24,16 +24,104 @@
return;
}
selection.modify("extend", "backward", "word");
if (selection.toString() !== "Well ") {
println("FAIL: selection is not what we expected after extending by word");
return;
}
// backward by character on a non-collapsed selection
selection.modify("move", "backward", "character");
if (
selection.anchorNode !== a.firstChild || selection.anchorOffset !== 9 ||
selection.focusNode !== a.firstChild || selection.focusOffset !== 9 ||
selection.anchorNode !== a.firstChild || selection.anchorOffset !== 0 ||
selection.focusNode !== a.firstChild || selection.focusOffset !== 0 ||
!selection.isCollapsed
) {
println("FAIL: selection is not what we expected after moving backward by character on non-collapsed selection");
return;
}
// forward by character on a non-collapsed selection
selection.setBaseAndExtent(a.firstChild, 0, a.firstChild, 10);
selection.modify("move", "forward", "character");
if (
selection.anchorNode !== a.firstChild || selection.anchorOffset !== 10 ||
selection.focusNode !== a.firstChild || selection.focusOffset !== 10 ||
!selection.isCollapsed
) {
println("FAIL: selection is not what we expected after moving forward by character on non-collapsed selection");
return;
}
// backward by word on a non-collapsed selection
selection.setBaseAndExtent(a.firstChild, 8, a.firstChild, 15);
selection.modify("move", "backward", "word");
if (
selection.anchorNode !== a.firstChild || selection.anchorOffset !== 11 ||
selection.focusNode !== a.firstChild || selection.focusOffset !== 11 ||
!selection.isCollapsed
) {
println("FAIL: selection is not what we expected after moving backward by word on non-collapsed selection");
return;
}
// forward by word on a non-collapsed selection
selection.setBaseAndExtent(a.firstChild, 0, a.firstChild, 10);
selection.modify("move", "forward", "word");
if (
selection.anchorNode !== a.firstChild || selection.anchorOffset !== 18 ||
selection.focusNode !== a.firstChild || selection.focusOffset !== 18 ||
!selection.isCollapsed
) {
println("FAIL: selection is not what we expected after moving forward by word on non-collapsed selection");
return;
}
// backward by character on a collapsed selection
selection.modify("move", "backward", "character");
if (
selection.anchorNode !== a.firstChild || selection.anchorOffset !== 17 ||
selection.focusNode !== a.firstChild || selection.focusOffset !== 17 ||
!selection.isCollapsed
) {
println("FAIL: selection is not what we expected after moving backward by character");
return;
}
// forward by character on a collapsed selection
selection.modify("move", "forward", "character");
if (
selection.anchorNode !== a.firstChild || selection.anchorOffset !== 18 ||
selection.focusNode !== a.firstChild || selection.focusOffset !== 18 ||
!selection.isCollapsed
) {
println("FAIL: selection is not what we expected after moving forward by character");
return;
}
// backward by word (twice) on a collapsed selection
selection.modify("move", "backward", "word");
selection.modify("move", "backward", "word");
if (
selection.anchorNode !== a.firstChild || selection.anchorOffset !== 5 ||
selection.focusNode !== a.firstChild || selection.focusOffset !== 5 ||
!selection.isCollapsed
) {
println("FAIL: selection is not what we expected after moving backward by word");
return;
}
// forward by word on a collapsed selection
selection.modify("move", "forward", "word");
if (
selection.anchorNode !== a.firstChild || selection.anchorOffset !== 10 ||
selection.focusNode !== a.firstChild || selection.focusOffset !== 10 ||
!selection.isCollapsed
) {
println("FAIL: selection is not what we expected after moving forward by word");
return;
}
println("PASS");
});
</script>