From 11bf8d1f674c646ef0846209d231924c02bed223 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 8 Oct 2026 16:50:10 +0200 Subject: [PATCH] :bug: Collapse text selection to edge on Left/Right (#12226) With a range selected, Left and Right were moving one character from the focus instead of collapsing to the selection start or end. Match standard text-field behavior and keep Shift+arrows extending. Closes #12207 --- render-wasm/src/state/text_editor.rs | 114 +++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/render-wasm/src/state/text_editor.rs b/render-wasm/src/state/text_editor.rs index 8d4ff4fca6..aaec3e90fd 100644 --- a/render-wasm/src/state/text_editor.rs +++ b/render-wasm/src/state/text_editor.rs @@ -887,6 +887,23 @@ impl TextEditorState { return false; } + // With a range selected, Left/Right collapse to that edge and stop. + // Extending (Shift) keeps the normal move-from-focus path below. + if self.selection.is_selection() && !extend_selection { + let edge = match direction { + CursorDirection::Backward => Some(self.selection.start()), + CursorDirection::Forward => Some(self.selection.end()), + _ => None, + }; + if let Some(caret) = edge { + self.selection.set_caret(caret); + self.update_styles(text_content); + self.reset_blink(); + self.push_event(TextEditorEvent::SelectionChanged); + return true; + } + } + let focus = self.selection.focus; // Get the text direction of the span at the current cursor position @@ -998,6 +1015,21 @@ mod tests { content } + fn content(text: &str) -> TextContent { + let mut content = + TextContent::new(Rect::from_xywh(0.0, 0.0, 200.0, 100.0), GrowType::Fixed); + content.add_paragraph(Paragraph::new( + TextAlign::Left, + TextDirection::LTR, + None, + None, + 1.2, + 0.0, + vec![span(text, 14.0, Fill::Solid(SolidColor(Color::BLACK)))], + )); + content + } + fn drain_events(editor: &mut TextEditorState) -> Vec { let mut events = Vec::new(); loop { @@ -1010,6 +1042,10 @@ mod tests { events } + fn pos(offset: usize) -> TextPositionWithAffinity { + TextPositionWithAffinity::new_downstream_affinity(0, offset) + } + #[test] fn set_caret_from_position_alone_does_not_emit_styles_changed() { let content = hello_world_content(); @@ -1076,4 +1112,82 @@ mod tests { vec![Fill::Solid(SolidColor(Color::RED))] ); } + + #[test] + fn move_backward_with_selection_collapses_to_start() { + // "Third paragraph" — select "aph" at the end, then Left. + let content = content("Third paragraph"); + let mut editor = TextEditorState::new(); + editor.selection.set_caret(pos(12)); + editor.selection.extend_to(pos(15)); + + editor.move_cursor(&content, CursorDirection::Backward, false, false); + + assert!(editor.selection.is_collapsed()); + assert_eq!(editor.selection.focus.offset, 12); + } + + #[test] + fn move_forward_with_selection_collapses_to_end() { + // Caret between "parag" and "raph", Shift+Left twice selects "ag", then Right. + let content = content("Third paragraph"); + let mut editor = TextEditorState::new(); + editor.selection.set_caret(pos(11)); + editor.selection.extend_to(pos(9)); + + editor.move_cursor(&content, CursorDirection::Forward, false, false); + + assert!(editor.selection.is_collapsed()); + assert_eq!(editor.selection.focus.offset, 11); + } + + #[test] + fn move_backward_with_full_selection_collapses_to_document_start() { + let content = content("Hello"); + let mut editor = TextEditorState::new(); + editor.select_all(&content); + + editor.move_cursor(&content, CursorDirection::Backward, false, false); + + assert!(editor.selection.is_collapsed()); + assert_eq!(editor.selection.focus.offset, 0); + } + + #[test] + fn move_forward_with_full_selection_collapses_to_document_end() { + let content = content("Hello"); + let mut editor = TextEditorState::new(); + editor.select_all(&content); + + editor.move_cursor(&content, CursorDirection::Forward, false, false); + + assert!(editor.selection.is_collapsed()); + assert_eq!(editor.selection.focus.offset, 5); + } + + #[test] + fn move_backward_while_extending_still_moves_from_focus() { + let content = content("Third paragraph"); + let mut editor = TextEditorState::new(); + editor.selection.set_caret(pos(15)); + editor.selection.extend_to(pos(12)); + + editor.move_cursor(&content, CursorDirection::Backward, false, true); + + assert!(editor.selection.is_selection()); + assert_eq!(editor.selection.anchor.offset, 15); + assert_eq!(editor.selection.focus.offset, 11); + } + + #[test] + fn move_backward_with_collapsed_caret_still_moves() { + let content = content("Hello"); + let mut editor = TextEditorState::new(); + editor.selection.set_caret(pos(3)); + + editor.move_cursor(&content, CursorDirection::Backward, false, false); + + assert!(editor.selection.is_collapsed()); + assert_eq!(editor.selection.focus.offset, 2); + } }