From ea3c840983cbe0b739039f8aad8c4ff86b6ead47 Mon Sep 17 00:00:00 2001 From: Clement Miao Date: Fri, 7 Apr 2017 01:05:13 -0700 Subject: [PATCH] new keyboard shortcuts inside text input --- Cargo.lock | 1 + components/script/Cargo.toml | 1 + components/script/lib.rs | 1 + components/script/textinput.rs | 190 +++++++++++++++++++++++++++++++-- tests/unit/script/textinput.rs | 114 +++++++++++++++++++- 5 files changed, 292 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5dd44828a3a..847034e2fb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2291,6 +2291,7 @@ dependencies = [ "style_traits 0.0.1", "time 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", "tinyfiledialogs 2.5.9 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-segmentation 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "uuid 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "webrender_traits 0.16.0 (git+https://github.com/servo/webrender)", diff --git a/components/script/Cargo.toml b/components/script/Cargo.toml index 4007e7ae040..bb59a12f0cb 100644 --- a/components/script/Cargo.toml +++ b/components/script/Cargo.toml @@ -84,6 +84,7 @@ smallvec = "0.1" style = {path = "../style"} style_traits = {path = "../style_traits"} time = "0.1.12" +unicode-segmentation = "1.1.0" url = {version = "1.2", features = ["heap_size", "query_encoding"]} uuid = {version = "0.4", features = ["v4"]} websocket = "0.17" diff --git a/components/script/lib.rs b/components/script/lib.rs index 92be1272d6f..5d07d14a4c5 100644 --- a/components/script/lib.rs +++ b/components/script/lib.rs @@ -97,6 +97,7 @@ extern crate style_traits; extern crate time; #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] extern crate tinyfiledialogs; +extern crate unicode_segmentation; extern crate url; extern crate uuid; extern crate webrender_traits; diff --git a/components/script/textinput.rs b/components/script/textinput.rs index 5f04b5b187e..d600b819e4c 100644 --- a/components/script/textinput.rs +++ b/components/script/textinput.rs @@ -14,6 +14,7 @@ use std::cmp::{max, min}; use std::default::Default; use std::ops::Range; use std::usize; +use unicode_segmentation::UnicodeSegmentation; #[derive(Copy, Clone, PartialEq)] pub enum Selection { @@ -468,6 +469,111 @@ impl TextInput { self.selection_begin = None; } + pub fn adjust_horizontal_by_word(&mut self, direction: Direction, select: Selection) { + if self.adjust_selection_for_horizontal_change(direction, select) { + return + } + let shift_increment: isize = { + let input: &str; + match direction { + Direction::Backward => { + let remaining = self.edit_point.index; + let current_line = self.edit_point.line; + let mut newline_adjustment = 0; + if remaining == 0 && current_line > 0 { + input = &self + .lines[current_line-1]; + newline_adjustment = 1; + } else { + input = &self + .lines[current_line] + [..remaining]; + } + + let mut iter = input.split_word_bounds().rev(); + let mut shift_temp: isize = 0; + loop { + match iter.next() { + None => break, + Some(x) => { + shift_temp += - (x.len() as isize); + if x.chars().any(|x| x.is_alphabetic() || x.is_numeric()) { + break; + } + } + } + } + shift_temp - newline_adjustment + } + Direction::Forward => { + let remaining = self.current_line_length() - self.edit_point.index; + let current_line = self.edit_point.line; + let mut newline_adjustment = 0; + if remaining == 0 && self.lines.len() > self.edit_point.line + 1 { + input = &self + .lines[current_line + 1]; + newline_adjustment = 1; + } else { + input = &self + .lines[current_line] + [self.edit_point.index..]; + } + + let mut iter = input.split_word_bounds(); + let mut shift_temp: isize = 0; + loop { + match iter.next() { + None => break, + Some(x) => { + shift_temp += x.len() as isize; + if x.chars().any(|x| x.is_alphabetic() || x.is_numeric()) { + break; + } + } + } + } + shift_temp + newline_adjustment + } + } + }; + + self.adjust_horizontal(shift_increment, select); + } + + pub fn adjust_horizontal_to_line_end(&mut self, direction: Direction, select: Selection) { + if self.adjust_selection_for_horizontal_change(direction, select) { + return + } + let shift: isize = { + let current_line = &self.lines[self.edit_point.line]; + match direction { + Direction::Backward => { + - (current_line[..self.edit_point.index].len() as isize) + }, + Direction::Forward => { + current_line[self.edit_point.index..].len() as isize + } + } + }; + self.perform_horizontal_adjustment(shift, select); + } + + pub fn adjust_horizontal_to_limit(&mut self, direction: Direction, select: Selection) { + if self.adjust_selection_for_horizontal_change(direction, select) { + return + } + match direction { + Direction::Backward => { + self.edit_point.line = 0; + self.edit_point.index = 0; + }, + Direction::Forward => { + self.edit_point.line = &self.lines.len() - 1; + self.edit_point.index = (&self.lines[&self.lines.len() - 1]).len(); + } + } + } + /// Process a given `KeyboardEvent` and return an action for the caller to execute. pub fn handle_keydown(&mut self, event: &KeyboardEvent) -> KeyReaction { if let Some(key) = event.get_key() { @@ -483,6 +589,32 @@ impl TextInput { mods: KeyModifiers) -> KeyReaction { let maybe_select = if mods.contains(SHIFT) { Selection::Selected } else { Selection::NotSelected }; match (printable, key) { + (_, Key::B) if mods.contains(CONTROL | ALT) => { + self.adjust_horizontal_by_word(Direction::Backward, maybe_select); + KeyReaction::RedrawSelection + }, + (_, Key::F) if mods.contains(CONTROL | ALT) => { + self.adjust_horizontal_by_word(Direction::Forward, maybe_select); + KeyReaction::RedrawSelection + }, + (_, Key::A) if mods.contains(CONTROL | ALT) => { + self.adjust_horizontal_to_line_end(Direction::Backward, maybe_select); + KeyReaction::RedrawSelection + }, + (_, Key::E) if mods.contains(CONTROL | ALT) => { + self.adjust_horizontal_to_line_end(Direction::Forward, maybe_select); + KeyReaction::RedrawSelection + }, + #[cfg(target_os = "macos")] + (None, Key::A) if mods == CONTROL => { + self.adjust_horizontal_to_line_end(Direction::Backward, maybe_select); + KeyReaction::RedrawSelection + }, + #[cfg(target_os = "macos")] + (None, Key::E) if mods == CONTROL => { + self.adjust_horizontal_to_line_end(Direction::Forward, maybe_select); + KeyReaction::RedrawSelection + }, (Some('a'), _) if is_control_key(mods) => { self.select_all(); KeyReaction::RedrawSelection @@ -501,49 +633,85 @@ impl TextInput { (Some(c), _) => { self.insert_char(c); KeyReaction::DispatchInput - } + }, + #[cfg(target_os = "macos")] + (None, Key::Home) => { + KeyReaction::RedrawSelection + }, + #[cfg(target_os = "macos")] + (None, Key::End) => { + KeyReaction::RedrawSelection + }, (None, Key::Delete) => { self.delete_char(Direction::Forward); KeyReaction::DispatchInput - } + }, (None, Key::Backspace) => { self.delete_char(Direction::Backward); KeyReaction::DispatchInput - } + }, + #[cfg(target_os = "macos")] + (None, Key::Left) if mods.contains(SUPER) => { + self.adjust_horizontal_to_line_end(Direction::Backward, maybe_select); + KeyReaction::RedrawSelection + }, + #[cfg(target_os = "macos")] + (None, Key::Right) if mods.contains(SUPER) => { + self.adjust_horizontal_to_line_end(Direction::Forward, maybe_select); + KeyReaction::RedrawSelection + }, + #[cfg(target_os = "macos")] + (None, Key::Up) if mods.contains(SUPER) => { + self.adjust_horizontal_to_limit(Direction::Backward, maybe_select); + KeyReaction::RedrawSelection + }, + #[cfg(target_os = "macos")] + (None, Key::Down) if mods.contains(SUPER) => { + self.adjust_horizontal_to_limit(Direction::Forward, maybe_select); + KeyReaction::RedrawSelection + }, + (None, Key::Left) if mods.contains(ALT) => { + self.adjust_horizontal_by_word(Direction::Backward, maybe_select); + KeyReaction::RedrawSelection + }, + (None, Key::Right) if mods.contains(ALT) => { + self.adjust_horizontal_by_word(Direction::Forward, maybe_select); + KeyReaction::RedrawSelection + }, (None, Key::Left) => { self.adjust_horizontal_by_one(Direction::Backward, maybe_select); KeyReaction::RedrawSelection - } + }, (None, Key::Right) => { self.adjust_horizontal_by_one(Direction::Forward, maybe_select); KeyReaction::RedrawSelection - } + }, (None, Key::Up) => { self.adjust_vertical(-1, maybe_select); KeyReaction::RedrawSelection - } + }, (None, Key::Down) => { self.adjust_vertical(1, maybe_select); KeyReaction::RedrawSelection - } + }, (None, Key::Enter) | (None, Key::KpEnter) => self.handle_return(), (None, Key::Home) => { self.edit_point.index = 0; KeyReaction::RedrawSelection - } + }, (None, Key::End) => { self.edit_point.index = self.current_line_length(); self.assert_ok_selection(); KeyReaction::RedrawSelection - } + }, (None, Key::PageUp) => { self.adjust_vertical(-28, maybe_select); KeyReaction::RedrawSelection - } + }, (None, Key::PageDown) => { self.adjust_vertical(28, maybe_select); KeyReaction::RedrawSelection - } + }, _ => KeyReaction::Nothing, } } diff --git a/tests/unit/script/textinput.rs b/tests/unit/script/textinput.rs index 01e28433ed6..a9e5a9fc08b 100644 --- a/tests/unit/script/textinput.rs +++ b/tests/unit/script/textinput.rs @@ -7,11 +7,8 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. +use msg::constellation_msg::{ALT, CONTROL, SUPER}; use msg::constellation_msg::{Key, KeyModifiers}; -#[cfg(not(target_os = "macos"))] -use msg::constellation_msg::CONTROL; -#[cfg(target_os = "macos")] -use msg::constellation_msg::SUPER; use script::clipboard_provider::DummyClipboardContext; use script::test::DOMString; use script::textinput::{TextInput, TextPoint, Selection, Lines, Direction, SelectionDirection}; @@ -349,6 +346,115 @@ fn test_textinput_adjust_horizontal() { assert_eq!(textinput.edit_point.index, 2); } +#[test] +fn test_textinput_adjust_horizontal_by_word() { + // Test basic case of movement word by word based on UAX#29 rules + let mut textinput = text_input(Lines::Single, "abc def"); + textinput.adjust_horizontal_by_word(Direction::Forward, Selection::NotSelected); + textinput.adjust_horizontal_by_word(Direction::Forward, Selection::NotSelected); + assert_eq!(textinput.edit_point.line, 0); + assert_eq!(textinput.edit_point.index, 7); + textinput.adjust_horizontal_by_word(Direction::Backward, Selection::NotSelected); + assert_eq!(textinput.edit_point.line, 0); + assert_eq!(textinput.edit_point.index, 4); + textinput.adjust_horizontal_by_word(Direction::Backward, Selection::NotSelected); + assert_eq!(textinput.edit_point.line, 0); + assert_eq!(textinput.edit_point.index, 0); + + // Test new line case of movement word by word based on UAX#29 rules + let mut textinput_2 = text_input(Lines::Multiple, "abc\ndef"); + textinput_2.adjust_horizontal_by_word(Direction::Forward, Selection::NotSelected); + textinput_2.adjust_horizontal_by_word(Direction::Forward, Selection::NotSelected); + assert_eq!(textinput_2.edit_point.line, 1); + assert_eq!(textinput_2.edit_point.index, 3); + textinput_2.adjust_horizontal_by_word(Direction::Backward, Selection::NotSelected); + assert_eq!(textinput_2.edit_point.line, 1); + assert_eq!(textinput_2.edit_point.index, 0); + textinput_2.adjust_horizontal_by_word(Direction::Backward, Selection::NotSelected); + assert_eq!(textinput_2.edit_point.line, 0); + assert_eq!(textinput_2.edit_point.index, 0); + + // Test non-standard sized characters case of movement word by word based on UAX#29 rules + let mut textinput_3 = text_input(Lines::Single, "áéc d🌠bc"); + textinput_3.adjust_horizontal_by_word(Direction::Forward, Selection::NotSelected); + assert_eq!(textinput_3.edit_point.line, 0); + assert_eq!(textinput_3.edit_point.index, 5); + textinput_3.adjust_horizontal_by_word(Direction::Forward, Selection::NotSelected); + assert_eq!(textinput_3.edit_point.line, 0); + assert_eq!(textinput_3.edit_point.index, 7); + textinput_3.adjust_horizontal_by_word(Direction::Forward, Selection::NotSelected); + assert_eq!(textinput_3.edit_point.line, 0); + assert_eq!(textinput_3.edit_point.index, 13); + textinput_3.adjust_horizontal_by_word(Direction::Backward, Selection::NotSelected); + assert_eq!(textinput_3.edit_point.line, 0); + assert_eq!(textinput_3.edit_point.index, 11); + textinput_3.adjust_horizontal_by_word(Direction::Backward, Selection::NotSelected); + assert_eq!(textinput_3.edit_point.line, 0); + assert_eq!(textinput_3.edit_point.index, 6); +} + +#[test] +fn test_textinput_adjust_horizontal_to_line_end() { + // Test standard case of movement to end based on UAX#29 rules + let mut textinput = text_input(Lines::Single, "abc def"); + textinput.adjust_horizontal_to_line_end(Direction::Forward, Selection::NotSelected); + assert_eq!(textinput.edit_point.line, 0); + assert_eq!(textinput.edit_point.index, 7); + + // Test new line case of movement to end based on UAX#29 rules + let mut textinput_2 = text_input(Lines::Multiple, "abc\ndef"); + textinput_2.adjust_horizontal_to_line_end(Direction::Forward, Selection::NotSelected); + assert_eq!(textinput_2.edit_point.line, 0); + assert_eq!(textinput_2.edit_point.index, 3); + textinput_2.adjust_horizontal_to_line_end(Direction::Forward, Selection::NotSelected); + assert_eq!(textinput_2.edit_point.line, 0); + assert_eq!(textinput_2.edit_point.index, 3); + textinput_2.adjust_horizontal_to_line_end(Direction::Backward, Selection::NotSelected); + assert_eq!(textinput_2.edit_point.line, 0); + assert_eq!(textinput_2.edit_point.index, 0); + + // Test non-standard sized characters case of movement to end based on UAX#29 rules + let mut textinput_3 = text_input(Lines::Single, "áéc d🌠bc"); + textinput_3.adjust_horizontal_to_line_end(Direction::Forward, Selection::NotSelected); + assert_eq!(textinput_3.edit_point.line, 0); + assert_eq!(textinput_3.edit_point.index, 13); + textinput_3.adjust_horizontal_to_line_end(Direction::Backward, Selection::NotSelected); + assert_eq!(textinput_3.edit_point.line, 0); + assert_eq!(textinput_3.edit_point.index, 0); +} + +#[test] +#[cfg(target_os = "macos")] +fn test_navigation_keyboard_shortcuts() { + let mut textinput = text_input(Lines::Multiple, "hello áéc"); + + // Test that CMD + Right moves to the end of the current line. + textinput.handle_keydown_aux(None, Key::Right, SUPER); + assert_eq!(textinput.edit_point.index, 11); + // Test that CMD + Right moves to the beginning of the current line. + textinput.handle_keydown_aux(None, Key::Left, SUPER); + assert_eq!(textinput.edit_point.index, 0); + // Test that CTRL + ALT + E moves to the end of the current line also. + textinput.handle_keydown_aux(None, Key::E, CONTROL | ALT); + assert_eq!(textinput.edit_point.index, 11); + // Test that CTRL + ALT + A moves to the beginning of the current line also. + textinput.handle_keydown_aux(None, Key::A, CONTROL | ALT); + assert_eq!(textinput.edit_point.index, 0); + + // Test that ALT + Right moves to the end of the word. + textinput.handle_keydown_aux(None, Key::Right, ALT); + assert_eq!(textinput.edit_point.index, 5); + // Test that CTRL + ALT + F moves to the end of the word also. + textinput.handle_keydown_aux(None, Key::F, CONTROL | ALT); + assert_eq!(textinput.edit_point.index, 11); + // Test that ALT + Left moves to the end of the word. + textinput.handle_keydown_aux(None, Key::Left, ALT); + assert_eq!(textinput.edit_point.index, 6); + // Test that CTRL + ALT + B moves to the end of the word also. + textinput.handle_keydown_aux(None, Key::B, CONTROL | ALT); + assert_eq!(textinput.edit_point.index, 0); +} + #[test] fn test_textinput_handle_return() { let mut single_line_textinput = text_input(Lines::Single, "abcdef");