text: Implement advanced horizontal text controls for EditText

This commit is contained in:
Kamil Jarosz 2024-01-30 03:04:24 +01:00 committed by Nathan Adams
parent 0657d2daa2
commit 4154a0945d
3 changed files with 187 additions and 18 deletions

51
Cargo.lock generated
View File

@ -4270,6 +4270,7 @@ dependencies = [
"thiserror",
"tracing",
"ttf-parser",
"unic-segment",
"url",
"wasm-bindgen-futures",
"weak-table",
@ -5510,6 +5511,27 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unic-char-property"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
dependencies = [
"unic-char-range",
]
[[package]]
name = "unic-char-range"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
[[package]]
name = "unic-common"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
[[package]]
name = "unic-langid"
version = "0.9.4"
@ -5553,6 +5575,35 @@ dependencies = [
"unic-langid-impl",
]
[[package]]
name = "unic-segment"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23"
dependencies = [
"unic-ucd-segment",
]
[[package]]
name = "unic-ucd-segment"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700"
dependencies = [
"unic-char-property",
"unic-char-range",
"unic-ucd-version",
]
[[package]]
name = "unic-ucd-version"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
dependencies = [
"unic-common",
]
[[package]]
name = "unicase"
version = "2.7.0"

View File

@ -64,6 +64,7 @@ image = { version = "0.24.8", default-features = false, features = ["tiff", "dxt
enum-map = "2.7.3"
ttf-parser = "0.20"
num-bigint = "0.4"
unic-segment = "0.9.0"
[target.'cfg(not(target_family = "wasm"))'.dependencies.futures]
version = "0.3.30"

View File

@ -35,9 +35,11 @@ use gc_arena::{Collect, Gc, GcCell, Mutation};
use ruffle_render::commands::CommandHandler;
use ruffle_render::shape_utils::DrawCommand;
use ruffle_render::transform::Transform;
use ruffle_wstr::WStrToUtf8;
use std::collections::VecDeque;
use std::{cell::Ref, cell::RefMut, sync::Arc};
use swf::{Color, ColorTransform, Twips};
use unic_segment::WordBoundIndices;
use super::interactive::Avm2MousePick;
@ -1368,9 +1370,12 @@ impl<'gc> EditText<'gc> {
let mut changed = false;
let is_selectable = self.is_selectable();
match control_code {
TextControlCode::MoveLeft => {
let new_pos = if selection.is_caret() && selection.to > 0 {
string_utils::prev_char_boundary(&self.text(), selection.to)
TextControlCode::MoveLeft
| TextControlCode::MoveLeftWord
| TextControlCode::MoveLeftLine
| TextControlCode::MoveLeftDocument => {
let new_pos = if selection.is_caret() {
self.find_new_position(control_code, selection.to)
} else {
selection.start()
};
@ -1379,9 +1384,12 @@ impl<'gc> EditText<'gc> {
context.gc_context,
);
}
TextControlCode::MoveRight => {
TextControlCode::MoveRight
| TextControlCode::MoveRightWord
| TextControlCode::MoveRightLine
| TextControlCode::MoveRightDocument => {
let new_pos = if selection.is_caret() && selection.to < self.text().len() {
string_utils::next_char_boundary(&self.text(), selection.to)
self.find_new_position(control_code, selection.to)
} else {
selection.end()
};
@ -1390,18 +1398,24 @@ impl<'gc> EditText<'gc> {
context.gc_context,
);
}
TextControlCode::SelectLeft => {
TextControlCode::SelectLeft
| TextControlCode::SelectLeftWord
| TextControlCode::SelectLeftLine
| TextControlCode::SelectLeftDocument => {
if is_selectable && selection.to > 0 {
let new_pos = string_utils::prev_char_boundary(&self.text(), selection.to);
let new_pos = self.find_new_position(control_code, selection.to);
self.set_selection(
Some(TextSelection::for_range(selection.from, new_pos)),
context.gc_context,
);
}
}
TextControlCode::SelectRight => {
TextControlCode::SelectRight
| TextControlCode::SelectRightWord
| TextControlCode::SelectRightLine
| TextControlCode::SelectRightDocument => {
if is_selectable && selection.to < self.text().len() {
let new_pos = string_utils::next_char_boundary(&self.text(), selection.to);
let new_pos = self.find_new_position(control_code, selection.to);
self.set_selection(
Some(TextSelection::for_range(selection.from, new_pos)),
context.gc_context,
@ -1477,7 +1491,12 @@ impl<'gc> EditText<'gc> {
changed = true;
}
}
TextControlCode::Backspace | TextControlCode::Delete if !selection.is_caret() => {
TextControlCode::Backspace
| TextControlCode::BackspaceWord
| TextControlCode::Delete
| TextControlCode::DeleteWord
if !selection.is_caret() =>
{
// Backspace or delete with multiple characters selected
self.replace_text(selection.start(), selection.end(), WStr::empty(), context);
self.set_selection(
@ -1486,12 +1505,11 @@ impl<'gc> EditText<'gc> {
);
changed = true;
}
TextControlCode::Backspace => {
TextControlCode::Backspace | TextControlCode::BackspaceWord => {
// Backspace with caret
if selection.start() > 0 {
// Delete previous character
let text = self.text();
let start = string_utils::prev_char_boundary(&text, selection.start());
// Delete previous character(s)
let start = self.find_new_position(control_code, selection.start());
self.replace_text(start, selection.start(), WStr::empty(), context);
self.set_selection(
Some(TextSelection::for_position(start)),
@ -1500,12 +1518,11 @@ impl<'gc> EditText<'gc> {
changed = true;
}
}
TextControlCode::Delete => {
TextControlCode::Delete | TextControlCode::DeleteWord => {
// Delete with caret
if selection.end() < self.text_length() {
// Delete next character
let text = self.text();
let end = string_utils::next_char_boundary(&text, selection.start());
// Delete next character(s)
let end = self.find_new_position(control_code, selection.start());
self.replace_text(selection.start(), end, WStr::empty(), context);
// No need to change selection, reset it to prevent caret from blinking
self.reset_selection_blinking(context.gc_context);
@ -1526,6 +1543,106 @@ impl<'gc> EditText<'gc> {
}
}
/// Find the new position in the text for the given control code.
///
/// * For selection codes it will represent the "to" part of the selection.
/// * For left/right moves it will represent the final caret position.
/// * For backspace/delete it will represent the position to which the text should be deleted.
fn find_new_position(self, control_code: TextControlCode, current_pos: usize) -> usize {
match control_code {
TextControlCode::SelectRight | TextControlCode::MoveRight | TextControlCode::Delete => {
string_utils::next_char_boundary(&self.text(), current_pos)
}
TextControlCode::SelectLeft
| TextControlCode::MoveLeft
| TextControlCode::Backspace => {
string_utils::prev_char_boundary(&self.text(), current_pos)
}
TextControlCode::SelectRightWord
| TextControlCode::MoveRightWord
| TextControlCode::DeleteWord => self.find_next_word_boundary(current_pos),
TextControlCode::SelectLeftWord
| TextControlCode::MoveLeftWord
| TextControlCode::BackspaceWord => self.find_prev_word_boundary(current_pos),
TextControlCode::SelectRightLine | TextControlCode::MoveRightLine => {
self.find_next_line_boundary(current_pos)
}
TextControlCode::SelectLeftLine | TextControlCode::MoveLeftLine => {
self.find_prev_line_boundary(current_pos)
}
TextControlCode::SelectRightDocument | TextControlCode::MoveRightDocument => {
self.text().len()
}
TextControlCode::SelectLeftDocument | TextControlCode::MoveLeftDocument => 0,
_ => unreachable!(),
}
}
/// Find the nearest word boundary before `pos`,
/// which is applicable for selection.
///
/// This algorithm is based on [UAX #29](https://unicode.org/reports/tr29/).
fn find_prev_word_boundary(self, pos: usize) -> usize {
let head = &self.text()[..pos];
let to_utf8 = WStrToUtf8::new(head);
WordBoundIndices::new(&to_utf8.to_utf8_lossy())
.rev()
.find(|(_, span)| !span.trim().is_empty())
.map(|(position, _)| position)
.and_then(|utf8_index| to_utf8.utf16_index(utf8_index))
.unwrap_or(0)
}
/// Find the nearest word boundary after `pos`,
/// which is applicable for selection.
///
/// This algorithm is based on [UAX #29](https://unicode.org/reports/tr29/).
fn find_next_word_boundary(self, pos: usize) -> usize {
let tail = &self.text()[pos..];
let to_utf8 = WStrToUtf8::new(tail);
WordBoundIndices::new(&to_utf8.to_utf8_lossy())
.skip_while(|(_, span)| span.trim().is_empty())
.nth(1)
.map(|p| p.0)
.and_then(|utf8_index| to_utf8.utf16_index(utf8_index))
.map(|utf16_index| pos + utf16_index)
.unwrap_or_else(|| self.text().len())
}
/// Find the nearest line boundary before or at `pos`.
fn find_prev_line_boundary(self, pos: usize) -> usize {
// TODO take into account the text layout instead of relying on newlines only
if pos == 0 {
return 0;
}
let mut line_break_pos = pos;
while line_break_pos > 0 && !self.is_newline_at(line_break_pos - 1) {
line_break_pos -= 1;
}
line_break_pos
}
/// Find the nearest line boundary after or at `pos`.
fn find_next_line_boundary(self, pos: usize) -> usize {
// TODO take into account the text layout instead of relying on newlines only
let len = self.text().len();
if pos >= len {
return len;
}
let mut line_break_pos = pos;
while line_break_pos < len && !self.is_newline_at(line_break_pos) {
line_break_pos += 1;
}
line_break_pos
}
fn is_newline_at(self, pos: usize) -> bool {
self.text().get(pos).unwrap_or(0) == '\n' as u16
}
pub fn text_input(self, character: char, context: &mut UpdateContext<'_, 'gc>) {
if self.0.read().flags.contains(EditTextFlag::READ_ONLY)
|| character.is_control()