Fix cursor navigation when using combining characters

The cursor navigation left/right (and subsequently text selection) needs
to respect grapheme boundaries. Since we already depend on the
unicode-segmentation crate through femtovg, we might as well use the
functionality for determining grapheme boundaries from there.

The only place where the cursor navigation is allowed to break that is
when using backspace, as that allows the user to break glyph clusters.
This commit is contained in:
Simon Hausmann 2021-07-30 15:17:49 +02:00 committed by Simon Hausmann
parent 35541cffd9
commit 17b3fbc7cf
3 changed files with 68 additions and 8 deletions

View file

@ -40,6 +40,7 @@ auto_enums = "0.7"
weak-table = "0.3"
scopeguard = "1.1.0"
cfg-if = "1"
unicode-segmentation = "1.8.0"
[target.'cfg(target_arch = "wasm32")'.dependencies]
instant = { version = "0.1", features = [ "wasm-bindgen", "now" ] }

View file

@ -432,6 +432,7 @@ impl ItemConsts for TextInput {
enum TextCursorDirection {
Forward,
Backward,
PreviousCharacter, // breaks grapheme boundaries, so only used by delete-previous-char
StartOfLine,
EndOfLine,
}
@ -487,17 +488,17 @@ impl TextInput {
let last_cursor_pos = (self.cursor_position() as usize).max(0).min(text.len());
let mut grapheme_cursor =
unicode_segmentation::GraphemeCursor::new(last_cursor_pos, text.len(), true);
let new_cursor_pos = match direction {
TextCursorDirection::Forward => {
let mut i = last_cursor_pos;
loop {
i = i.checked_add(1).unwrap_or_default().min(text.len());
if text.is_char_boundary(i) {
break i;
}
}
grapheme_cursor.next_boundary(&text, 0).ok().flatten().unwrap_or_else(|| text.len())
}
TextCursorDirection::Backward => {
grapheme_cursor.prev_boundary(&text, 0).ok().flatten().unwrap_or(0)
}
TextCursorDirection::PreviousCharacter => {
let mut i = last_cursor_pos;
loop {
i = i.checked_sub(1).unwrap_or_default();
@ -538,7 +539,8 @@ impl TextInput {
self.delete_selection();
return;
}
if self.move_cursor(TextCursorDirection::Backward, AnchorMode::MoveAnchor, window) {
if self.move_cursor(TextCursorDirection::PreviousCharacter, AnchorMode::MoveAnchor, window)
{
self.delete_char(window);
}
}

View file

@ -0,0 +1,57 @@
/* LICENSE BEGIN
This file is part of the SixtyFPS Project -- https://sixtyfps.io
Copyright (c) 2021 Olivier Goffart <olivier.goffart@sixtyfps.io>
Copyright (c) 2021 Simon Hausmann <simon.hausmann@sixtyfps.io>
SPDX-License-Identifier: GPL-3.0-only
This file is also available under commercial licensing terms.
Please contact info@sixtyfps.io for more information.
LICENSE END */
TestCase := TextInput {
width: 100phx;
height: 100phx;
property<string> test_text: self.text;
property<int> test_cursor_pos: self.cursor_position;
property<int> test_anchor_pos: self.anchor_position;
property<bool> has_selection: self.cursor_position != self.anchor_position;
property<bool> input_focused: self.has_focus;
}
/*
```rust
// from input.rs
const LEFT_CODE: char = '\u{000E}'; // shift out
const BACK_CODE: char = '\u{0007}'; // backspace \b
let shift_modifier = sixtyfps::re_exports::KeyboardModifiers {
shift: true,
..Default::default()
};
let instance = TestCase::new();
sixtyfps::testing::send_mouse_click(&instance, 50., 50.);
assert!(instance.get_input_focused());
assert_eq!(instance.get_test_text(), "");
sixtyfps::testing::send_keyboard_string_sequence(&instance, "e\u{0301}");
assert_eq!(instance.get_test_text(), "e\u{0301}");
assert!(!instance.get_has_selection());
// Test that selecting the grapheme works
sixtyfps::testing::set_current_keyboard_modifiers(&instance, shift_modifier);
sixtyfps::testing::send_keyboard_string_sequence(&instance, &LEFT_CODE.to_string());
sixtyfps::testing::set_current_keyboard_modifiers(&instance, sixtyfps::re_exports::KeyboardModifiers::default());
assert!(instance.get_has_selection());
sixtyfps::testing::send_keyboard_string_sequence(&instance, &BACK_CODE.to_string());
assert_eq!(instance.get_test_text(), "");
sixtyfps::testing::send_keyboard_string_sequence(&instance, "e\u{0301}");
// Test that backspace does not operate on the grapheme and just removes the
// diacritic.
sixtyfps::testing::send_keyboard_string_sequence(&instance, &BACK_CODE.to_string());
assert_eq!(instance.get_test_cursor_pos(), 1);
assert_eq!(instance.get_test_text(), "e");
```
*/