GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
fix: multiline TextBox focus click places caret instead of select-all
The focusing press on a multiline box now positions the caret at the click
(single-line boxes keep select-all-on-focus), and FocusIn no longer re-runs
begin_editing when already editing — the app-side press-then-set_focused
sequence was re-arming select-all over the caret the press just placed,
so the first keystroke in a reply wiped the entire quoted body.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/widget/input/text_box.rs | 44 ++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 42 insertions(+), 2 deletions(-)
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index aa5d8fd..d61fc9d 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -1352,6 +1352,16 @@ impl Input for TextBox {
if !self.editing {
self.begin_editing();
ectx.request_focus();
+ if self.multiline {
+ // A multiline box is a document, not a form field: the focusing
+ // click places the caret instead of arming select-all, so the
+ // first keystroke can't wipe prefilled content (e.g. a reply quote).
+ let idx = self.position_to_idx(*px, *py, true);
+ self.cursor_idx = idx;
+ self.select_anchor = Some(idx);
+ self.all_selected = false;
+ self.sync_editor_state();
+ }
} else {
let idx = self.position_to_idx(*px, *py, true);
self.cursor_idx = idx;
@@ -1402,8 +1412,10 @@ impl Input for TextBox {
Event::KeyInput(key_event) => self.handle_key(key_event),
Event::FocusIn => {
// The legacy `focus()`: enter editing and claim the global slot (unless
- // disabled — legacy early-returned before `set_focused`).
- if !self.disabled {
+ // disabled — legacy early-returned before `set_focused`). Skipped when
+ // already editing: a press-then-set_focused sequence must not re-arm
+ // select-all over the caret the press just placed.
+ if !self.disabled && !self.editing {
self.begin_editing();
ectx.request_focus();
}
@@ -1548,6 +1560,34 @@ mod tests {
assert!(tb.take_change());
}
+ #[test]
+ fn multiline_focus_click_places_caret_instead_of_select_all() {
+ let mut dummy = crate::context::UiContext::new();
+ let mut tb = TextBox::new("abcdef".to_string()).with_multiline(true);
+ tb.set_rect(10.0, 10.0, 200.0, 100.0);
+
+ // The focusing click must NOT arm select-all (a first keystroke would wipe
+ // prefilled content, e.g. a reply quote) — it places the caret like any click.
+ let clicked = tb.mouse_input(MouseButton::Left, ElementState::Pressed, 12.0, 20.0, &mut dummy);
+ assert!(clicked);
+ assert!(tb.editing);
+ assert!(!tb.all_selected);
+ let caret_after_click = tb.cursor_idx;
+ let key_ev = KeyEvent {
+ state: ElementState::Pressed,
+ logical_key: Key::Character("X".to_string()),
+ text: Some("X".to_string()),
+ repeat: false,
+ ctrl: false,
+ shift: false,
+ };
+ let handled = tb.keyboard_input(&key_ev, &mut dummy);
+ assert!(handled);
+ assert_eq!(tb.edit_buffer.chars().count(), 7, "typing must insert, not replace the buffer");
+ assert!(tb.edit_buffer.contains("X"));
+ assert_eq!(tb.cursor_idx, caret_after_click + 1);
+ }
+
#[test]
fn test_textbox_drag_and_modifier_selection() {
let mut dummy = crate::context::UiContext::new();