1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 //! Common handling of keyboard input and state management for text input controls
6
7 use clipboard_provider::ClipboardProvider;
8 use dom::bindings::str::DOMString;
9 use dom::keyboardevent::KeyboardEvent;
10 use msg::constellation_msg::{Key, KeyModifiers};
11 use std::borrow::ToOwned;
12 use std::cmp::{max, min};
13 use std::default::Default;
14 use std::ops::Range;
15 use std::usize;
16 use unicode_segmentation::UnicodeSegmentation;
17
18 #[derive(Clone, Copy, PartialEq)]
19 pub enum Selection {
20 Selected,
21 NotSelected
22 }
23
24 #[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)]
25 pub enum SelectionDirection {
26 Forward,
27 Backward,
28 None,
29 }
30
31 impl From<DOMString> for SelectionDirection {
from(direction: DOMString) -> SelectionDirection32 fn from(direction: DOMString) -> SelectionDirection {
33 match direction.as_ref() {
34 "forward" => SelectionDirection::Forward,
35 "backward" => SelectionDirection::Backward,
36 _ => SelectionDirection::None,
37 }
38 }
39 }
40
41 impl From<SelectionDirection> for DOMString {
from(direction: SelectionDirection) -> DOMString42 fn from(direction: SelectionDirection) -> DOMString {
43 match direction {
44 SelectionDirection::Forward => DOMString::from("forward"),
45 SelectionDirection::Backward => DOMString::from("backward"),
46 SelectionDirection::None => DOMString::from("none"),
47 }
48 }
49 }
50
51 #[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq, PartialOrd)]
52 pub struct TextPoint {
53 /// 0-based line number
54 pub line: usize,
55 /// 0-based column number in UTF-8 bytes
56 pub index: usize,
57 }
58
59 impl TextPoint {
60 /// Returns a TextPoint constrained to be a valid location within lines
constrain_to(&self, lines: &[DOMString]) -> TextPoint61 fn constrain_to(&self, lines: &[DOMString]) -> TextPoint {
62 let line = min(self.line, lines.len() - 1);
63
64 TextPoint {
65 line,
66 index: min(self.index, lines[line].len()),
67 }
68 }
69 }
70
71 #[derive(Clone, Copy, PartialEq)]
72 pub struct SelectionState {
73 start: TextPoint,
74 end: TextPoint,
75 direction: SelectionDirection,
76 }
77
78 /// Encapsulated state for handling keyboard input in a single or multiline text input control.
79 #[derive(JSTraceable, MallocSizeOf)]
80 pub struct TextInput<T: ClipboardProvider> {
81 /// Current text input content, split across lines without trailing '\n'
82 lines: Vec<DOMString>,
83
84 /// Current cursor input point
85 edit_point: TextPoint,
86
87 /// The current selection goes from the selection_origin until the edit_point. Note that the
88 /// selection_origin may be after the edit_point, in the case of a backward selection.
89 selection_origin: Option<TextPoint>,
90 selection_direction: SelectionDirection,
91
92 /// Is this a multiline input?
93 multiline: bool,
94
95 #[ignore_malloc_size_of = "Can't easily measure this generic type"]
96 clipboard_provider: T,
97
98 /// The maximum number of UTF-16 code units this text input is allowed to hold.
99 ///
100 /// <https://html.spec.whatwg.org/multipage/#attr-fe-maxlength>
101 max_length: Option<usize>,
102 min_length: Option<usize>,
103 }
104
105 /// Resulting action to be taken by the owner of a text input that is handling an event.
106 pub enum KeyReaction {
107 TriggerDefaultAction,
108 DispatchInput,
109 RedrawSelection,
110 Nothing,
111 }
112
113 impl Default for TextPoint {
default() -> TextPoint114 fn default() -> TextPoint {
115 TextPoint {
116 line: 0,
117 index: 0,
118 }
119 }
120 }
121
122 /// Control whether this control should allow multiple lines.
123 #[derive(Eq, PartialEq)]
124 pub enum Lines {
125 Single,
126 Multiple,
127 }
128
129 /// The direction in which to delete a character.
130 #[derive(Clone, Copy, Eq, PartialEq)]
131 pub enum Direction {
132 Forward,
133 Backward
134 }
135
136
137 /// Was the keyboard event accompanied by the standard control modifier,
138 /// i.e. cmd on Mac OS or ctrl on other platforms.
139 #[cfg(target_os = "macos")]
is_control_key(mods: KeyModifiers) -> bool140 fn is_control_key(mods: KeyModifiers) -> bool {
141 mods.contains(KeyModifiers::SUPER) && !mods.contains(KeyModifiers::CONTROL | KeyModifiers::ALT)
142 }
143
144 #[cfg(not(target_os = "macos"))]
is_control_key(mods: KeyModifiers) -> bool145 fn is_control_key(mods: KeyModifiers) -> bool {
146 mods.contains(KeyModifiers::CONTROL) && !mods.contains(KeyModifiers::SUPER | KeyModifiers::ALT)
147 }
148
149 /// The length in bytes of the first n characters in a UTF-8 string.
150 ///
151 /// If the string has fewer than n characters, returns the length of the whole string.
len_of_first_n_chars(text: &str, n: usize) -> usize152 fn len_of_first_n_chars(text: &str, n: usize) -> usize {
153 match text.char_indices().take(n).last() {
154 Some((index, ch)) => index + ch.len_utf8(),
155 None => 0
156 }
157 }
158
159 /// The length in bytes of the first n code units a string when encoded in UTF-16.
160 ///
161 /// If the string is fewer than n code units, returns the length of the whole string.
len_of_first_n_code_units(text: &str, n: usize) -> usize162 fn len_of_first_n_code_units(text: &str, n: usize) -> usize {
163 let mut utf8_len = 0;
164 let mut utf16_len = 0;
165 for c in text.chars() {
166 utf16_len += c.len_utf16();
167 if utf16_len > n {
168 break;
169 }
170 utf8_len += c.len_utf8();
171 }
172 utf8_len
173 }
174
175 impl<T: ClipboardProvider> TextInput<T> {
176 /// Instantiate a new text input control
new(lines: Lines, initial: DOMString, clipboard_provider: T, max_length: Option<usize>, min_length: Option<usize>, selection_direction: SelectionDirection) -> TextInput<T>177 pub fn new(lines: Lines, initial: DOMString,
178 clipboard_provider: T, max_length: Option<usize>,
179 min_length: Option<usize>,
180 selection_direction: SelectionDirection) -> TextInput<T> {
181 let mut i = TextInput {
182 lines: vec!(),
183 edit_point: Default::default(),
184 selection_origin: None,
185 multiline: lines == Lines::Multiple,
186 clipboard_provider: clipboard_provider,
187 max_length: max_length,
188 min_length: min_length,
189 selection_direction: selection_direction,
190 };
191 i.set_content(initial);
192 i
193 }
194
edit_point(&self) -> TextPoint195 pub fn edit_point(&self) -> TextPoint {
196 self.edit_point
197 }
198
selection_origin(&self) -> Option<TextPoint>199 pub fn selection_origin(&self) -> Option<TextPoint> {
200 self.selection_origin
201 }
202
203 /// The selection origin, or the edit point if there is no selection. Note that the selection
204 /// origin may be after the edit point, in the case of a backward selection.
selection_origin_or_edit_point(&self) -> TextPoint205 pub fn selection_origin_or_edit_point(&self) -> TextPoint {
206 self.selection_origin.unwrap_or(self.edit_point)
207 }
208
selection_direction(&self) -> SelectionDirection209 pub fn selection_direction(&self) -> SelectionDirection {
210 self.selection_direction
211 }
212
set_max_length(&mut self, length: Option<usize>)213 pub fn set_max_length(&mut self, length: Option<usize>) {
214 self.max_length = length;
215 }
216
set_min_length(&mut self, length: Option<usize>)217 pub fn set_min_length(&mut self, length: Option<usize>) {
218 self.min_length = length;
219 }
220
221 /// Remove a character at the current editing point
delete_char(&mut self, dir: Direction)222 pub fn delete_char(&mut self, dir: Direction) {
223 if self.selection_origin.is_none() || self.selection_origin == Some(self.edit_point) {
224 self.adjust_horizontal_by_one(dir, Selection::Selected);
225 }
226 self.replace_selection(DOMString::new());
227 }
228
229 /// Insert a character at the current editing point
insert_char(&mut self, ch: char)230 pub fn insert_char(&mut self, ch: char) {
231 self.insert_string(ch.to_string());
232 }
233
234 /// Insert a string at the current editing point
insert_string<S: Into<String>>(&mut self, s: S)235 pub fn insert_string<S: Into<String>>(&mut self, s: S) {
236 if self.selection_origin.is_none() {
237 self.selection_origin = Some(self.edit_point);
238 }
239 self.replace_selection(DOMString::from(s.into()));
240 }
241
242 /// The start of the selection (or the edit point, if there is no selection). Always less than
243 /// or equal to selection_end(), regardless of the selection direction.
selection_start(&self) -> TextPoint244 pub fn selection_start(&self) -> TextPoint {
245 match self.selection_direction {
246 SelectionDirection::None | SelectionDirection::Forward => self.selection_origin_or_edit_point(),
247 SelectionDirection::Backward => self.edit_point,
248 }
249 }
250
251 /// The UTF-8 byte offset of the selection_start()
selection_start_offset(&self) -> usize252 pub fn selection_start_offset(&self) -> usize {
253 self.text_point_to_offset(&self.selection_start())
254 }
255
256 /// The end of the selection (or the edit point, if there is no selection). Always greater
257 /// than or equal to selection_start(), regardless of the selection direction.
selection_end(&self) -> TextPoint258 pub fn selection_end(&self) -> TextPoint {
259 match self.selection_direction {
260 SelectionDirection::None | SelectionDirection::Forward => self.edit_point,
261 SelectionDirection::Backward => self.selection_origin_or_edit_point(),
262 }
263 }
264
265 /// The UTF-8 byte offset of the selection_end()
selection_end_offset(&self) -> usize266 pub fn selection_end_offset(&self) -> usize {
267 self.text_point_to_offset(&self.selection_end())
268 }
269
270 /// Whether or not there is an active selection (the selection may be zero-length)
271 #[inline]
has_selection(&self) -> bool272 pub fn has_selection(&self) -> bool {
273 self.selection_origin.is_some()
274 }
275
276 /// Returns a tuple of (start, end) giving the bounds of the current selection. start is always
277 /// less than or equal to end.
sorted_selection_bounds(&self) -> (TextPoint, TextPoint)278 pub fn sorted_selection_bounds(&self) -> (TextPoint, TextPoint) {
279 (self.selection_start(), self.selection_end())
280 }
281
282 /// Return the selection range as UTF-8 byte offsets from the start of the content.
283 ///
284 /// If there is no selection, returns an empty range at the edit point.
sorted_selection_offsets_range(&self) -> Range<usize>285 pub fn sorted_selection_offsets_range(&self) -> Range<usize> {
286 self.selection_start_offset() .. self.selection_end_offset()
287 }
288
289 /// The state of the current selection. Can be used to compare whether selection state has changed.
selection_state(&self) -> SelectionState290 pub fn selection_state(&self) -> SelectionState {
291 SelectionState {
292 start: self.selection_start(),
293 end: self.selection_end(),
294 direction: self.selection_direction,
295 }
296 }
297
298 // Check that the selection is valid.
assert_ok_selection(&self)299 fn assert_ok_selection(&self) {
300 if let Some(begin) = self.selection_origin {
301 debug_assert!(begin.line < self.lines.len());
302 debug_assert!(begin.index <= self.lines[begin.line].len());
303
304 match self.selection_direction {
305 SelectionDirection::None | SelectionDirection::Forward => {
306 debug_assert!(begin <= self.edit_point)
307 },
308
309 SelectionDirection::Backward => {
310 debug_assert!(self.edit_point <= begin)
311 },
312 }
313 }
314
315 debug_assert!(self.edit_point.line < self.lines.len());
316 debug_assert!(self.edit_point.index <= self.lines[self.edit_point.line].len());
317 }
318
get_selection_text(&self) -> Option<String>319 pub fn get_selection_text(&self) -> Option<String> {
320 let text = self.fold_selection_slices(String::new(), |s, slice| s.push_str(slice));
321 if text.is_empty() {
322 return None
323 }
324 Some(text)
325 }
326
327 /// The length of the selected text in UTF-16 code units.
selection_utf16_len(&self) -> usize328 fn selection_utf16_len(&self) -> usize {
329 self.fold_selection_slices(0usize,
330 |len, slice| *len += slice.chars().map(char::len_utf16).sum::<usize>())
331 }
332
333 /// Run the callback on a series of slices that, concatenated, make up the selected text.
334 ///
335 /// The accumulator `acc` can be mutated by the callback, and will be returned at the end.
fold_selection_slices<B, F: FnMut(&mut B, &str)>(&self, mut acc: B, mut f: F) -> B336 fn fold_selection_slices<B, F: FnMut(&mut B, &str)>(&self, mut acc: B, mut f: F) -> B {
337 if self.has_selection() {
338 let (start, end) = self.sorted_selection_bounds();
339
340 if start.line == end.line {
341 f(&mut acc, &self.lines[start.line][start.index..end.index])
342 } else {
343 f(&mut acc, &self.lines[start.line][start.index..]);
344 for line in &self.lines[start.line + 1 .. end.line] {
345 f(&mut acc, "\n");
346 f(&mut acc, line);
347 }
348 f(&mut acc, "\n");
349 f(&mut acc, &self.lines[end.line][..end.index])
350 }
351 }
352
353 acc
354 }
355
replace_selection(&mut self, insert: DOMString)356 pub fn replace_selection(&mut self, insert: DOMString) {
357 if !self.has_selection() {
358 return
359 }
360
361 let (start, end) = self.sorted_selection_bounds();
362
363 let allowed_to_insert_count = if let Some(max_length) = self.max_length {
364 let len_after_selection_replaced = self.utf16_len() - self.selection_utf16_len();
365 if len_after_selection_replaced >= max_length {
366 // If, after deleting the selection, the len is still greater than the max
367 // length, then don't delete/insert anything
368 return
369 }
370
371 max_length - len_after_selection_replaced
372 } else {
373 usize::MAX
374 };
375
376 let last_char_index = len_of_first_n_code_units(&*insert, allowed_to_insert_count);
377 let chars_to_insert = &insert[..last_char_index];
378
379 self.clear_selection();
380
381 let new_lines = {
382 let prefix = &self.lines[start.line][..start.index];
383 let suffix = &self.lines[end.line][end.index..];
384 let lines_prefix = &self.lines[..start.line];
385 let lines_suffix = &self.lines[end.line + 1..];
386
387 let mut insert_lines = if self.multiline {
388 chars_to_insert.split('\n').map(|s| DOMString::from(s)).collect()
389 } else {
390 vec!(DOMString::from(chars_to_insert))
391 };
392
393 // FIXME(ajeffrey): effecient append for DOMStrings
394 let mut new_line = prefix.to_owned();
395
396 new_line.push_str(&insert_lines[0]);
397 insert_lines[0] = DOMString::from(new_line);
398
399 let last_insert_lines_index = insert_lines.len() - 1;
400 self.edit_point.index = insert_lines[last_insert_lines_index].len();
401 self.edit_point.line = start.line + last_insert_lines_index;
402
403 // FIXME(ajeffrey): effecient append for DOMStrings
404 insert_lines[last_insert_lines_index].push_str(suffix);
405
406 let mut new_lines = vec!();
407 new_lines.extend_from_slice(lines_prefix);
408 new_lines.extend_from_slice(&insert_lines);
409 new_lines.extend_from_slice(lines_suffix);
410 new_lines
411 };
412
413 self.lines = new_lines;
414 self.assert_ok_selection();
415 }
416
417 /// Return the length in UTF-8 bytes of the current line under the editing point.
current_line_length(&self) -> usize418 pub fn current_line_length(&self) -> usize {
419 self.lines[self.edit_point.line].len()
420 }
421
422 /// Adjust the editing point position by a given of lines. The resulting column is
423 /// as close to the original column position as possible.
adjust_vertical(&mut self, adjust: isize, select: Selection)424 pub fn adjust_vertical(&mut self, adjust: isize, select: Selection) {
425 if !self.multiline {
426 return;
427 }
428
429 if select == Selection::Selected {
430 if self.selection_origin.is_none() {
431 self.selection_origin = Some(self.edit_point);
432 }
433 } else {
434 self.clear_selection();
435 }
436
437 assert!(self.edit_point.line < self.lines.len());
438
439 let target_line: isize = self.edit_point.line as isize + adjust;
440
441 if target_line < 0 {
442 self.edit_point.index = 0;
443 self.edit_point.line = 0;
444 return;
445 } else if target_line as usize >= self.lines.len() {
446 self.edit_point.line = self.lines.len() - 1;
447 self.edit_point.index = self.current_line_length();
448 return;
449 }
450
451 let col = self.lines[self.edit_point.line][..self.edit_point.index].chars().count();
452 self.edit_point.line = target_line as usize;
453 self.edit_point.index = len_of_first_n_chars(&self.lines[self.edit_point.line], col);
454 self.assert_ok_selection();
455 }
456
457 /// Adjust the editing point position by a given number of bytes. If the adjustment
458 /// requested is larger than is available in the current line, the editing point is
459 /// adjusted vertically and the process repeats with the remaining adjustment requested.
adjust_horizontal(&mut self, adjust: isize, select: Selection)460 pub fn adjust_horizontal(&mut self, adjust: isize, select: Selection) {
461 let direction = if adjust >= 0 { Direction::Forward } else { Direction::Backward };
462 if self.adjust_selection_for_horizontal_change(direction, select) {
463 return
464 }
465 self.perform_horizontal_adjustment(adjust, select);
466 }
467
adjust_horizontal_by_one(&mut self, direction: Direction, select: Selection)468 pub fn adjust_horizontal_by_one(&mut self, direction: Direction, select: Selection) {
469 if self.adjust_selection_for_horizontal_change(direction, select) {
470 return
471 }
472 let adjust = {
473 let current_line = &self.lines[self.edit_point.line];
474 match direction {
475 Direction::Forward => {
476 match current_line[self.edit_point.index..].graphemes(true).next() {
477 Some(c) => c.len() as isize,
478 None => 1, // Going to the next line is a "one byte" offset
479 }
480 }
481 Direction::Backward => {
482 match current_line[..self.edit_point.index].graphemes(true).next_back() {
483 Some(c) => -(c.len() as isize),
484 None => -1, // Going to the previous line is a "one byte" offset
485 }
486 }
487 }
488 };
489 self.perform_horizontal_adjustment(adjust, select);
490 }
491
492 /// Return whether to cancel the caret move
adjust_selection_for_horizontal_change(&mut self, adjust: Direction, select: Selection) -> bool493 fn adjust_selection_for_horizontal_change(&mut self, adjust: Direction, select: Selection)
494 -> bool {
495 if select == Selection::Selected {
496 if self.selection_origin.is_none() {
497 self.selection_origin = Some(self.edit_point);
498 }
499
500 self.selection_direction = match adjust {
501 Direction::Backward => SelectionDirection::Backward,
502 Direction::Forward => SelectionDirection::Forward,
503 };
504 } else {
505 if self.has_selection() {
506 self.edit_point = match adjust {
507 Direction::Backward => self.selection_start(),
508 Direction::Forward => self.selection_end(),
509 };
510 self.clear_selection();
511 return true
512 }
513 }
514 false
515 }
516
perform_horizontal_adjustment(&mut self, adjust: isize, select: Selection)517 fn perform_horizontal_adjustment(&mut self, adjust: isize, select: Selection) {
518 if adjust < 0 {
519 let remaining = self.edit_point.index;
520 if adjust.abs() as usize > remaining && self.edit_point.line > 0 {
521 self.adjust_vertical(-1, select);
522 self.edit_point.index = self.current_line_length();
523 self.adjust_horizontal(adjust + remaining as isize + 1, select);
524 } else {
525 self.edit_point.index = max(0, self.edit_point.index as isize + adjust) as usize;
526 }
527 } else {
528 let remaining = self.current_line_length() - self.edit_point.index;
529 if adjust as usize > remaining && self.lines.len() > self.edit_point.line + 1 {
530 self.adjust_vertical(1, select);
531 self.edit_point.index = 0;
532 // one shift is consumed by the change of line, hence the -1
533 self.adjust_horizontal(adjust - remaining as isize - 1, select);
534 } else {
535 self.edit_point.index = min(self.current_line_length(),
536 self.edit_point.index + adjust as usize);
537 }
538 }
539 self.assert_ok_selection();
540 }
541
542 /// Deal with a newline input.
handle_return(&mut self) -> KeyReaction543 pub fn handle_return(&mut self) -> KeyReaction {
544 if !self.multiline {
545 KeyReaction::TriggerDefaultAction
546 } else {
547 self.insert_char('\n');
548 KeyReaction::DispatchInput
549 }
550 }
551
552 /// Select all text in the input control.
select_all(&mut self)553 pub fn select_all(&mut self) {
554 self.selection_origin = Some(TextPoint {
555 line: 0,
556 index: 0,
557 });
558 let last_line = self.lines.len() - 1;
559 self.edit_point.line = last_line;
560 self.edit_point.index = self.lines[last_line].len();
561 self.assert_ok_selection();
562 }
563
564 /// Remove the current selection.
clear_selection(&mut self)565 pub fn clear_selection(&mut self) {
566 self.selection_origin = None;
567 self.selection_direction = SelectionDirection::None;
568 }
569
570 /// Remove the current selection and set the edit point to the end of the content.
clear_selection_to_limit(&mut self, direction: Direction)571 pub fn clear_selection_to_limit(&mut self, direction: Direction) {
572 self.clear_selection();
573 self.adjust_horizontal_to_limit(direction, Selection::NotSelected);
574 }
575
adjust_horizontal_by_word(&mut self, direction: Direction, select: Selection)576 pub fn adjust_horizontal_by_word(&mut self, direction: Direction, select: Selection) {
577 if self.adjust_selection_for_horizontal_change(direction, select) {
578 return
579 }
580 let shift_increment: isize = {
581 let input: &str;
582 match direction {
583 Direction::Backward => {
584 let remaining = self.edit_point.index;
585 let current_line = self.edit_point.line;
586 let mut newline_adjustment = 0;
587 if remaining == 0 && current_line > 0 {
588 input = &self
589 .lines[current_line-1];
590 newline_adjustment = 1;
591 } else {
592 input = &self
593 .lines[current_line]
594 [..remaining];
595 }
596
597 let mut iter = input.split_word_bounds().rev();
598 let mut shift_temp: isize = 0;
599 loop {
600 match iter.next() {
601 None => break,
602 Some(x) => {
603 shift_temp += - (x.len() as isize);
604 if x.chars().any(|x| x.is_alphabetic() || x.is_numeric()) {
605 break;
606 }
607 }
608 }
609 }
610 shift_temp - newline_adjustment
611 }
612 Direction::Forward => {
613 let remaining = self.current_line_length() - self.edit_point.index;
614 let current_line = self.edit_point.line;
615 let mut newline_adjustment = 0;
616 if remaining == 0 && self.lines.len() > self.edit_point.line + 1 {
617 input = &self
618 .lines[current_line + 1];
619 newline_adjustment = 1;
620 } else {
621 input = &self
622 .lines[current_line]
623 [self.edit_point.index..];
624 }
625
626 let mut iter = input.split_word_bounds();
627 let mut shift_temp: isize = 0;
628 loop {
629 match iter.next() {
630 None => break,
631 Some(x) => {
632 shift_temp += x.len() as isize;
633 if x.chars().any(|x| x.is_alphabetic() || x.is_numeric()) {
634 break;
635 }
636 }
637 }
638 }
639 shift_temp + newline_adjustment
640 }
641 }
642 };
643
644 self.adjust_horizontal(shift_increment, select);
645 }
646
adjust_horizontal_to_line_end(&mut self, direction: Direction, select: Selection)647 pub fn adjust_horizontal_to_line_end(&mut self, direction: Direction, select: Selection) {
648 if self.adjust_selection_for_horizontal_change(direction, select) {
649 return
650 }
651 let shift: isize = {
652 let current_line = &self.lines[self.edit_point.line];
653 match direction {
654 Direction::Backward => {
655 - (current_line[..self.edit_point.index].len() as isize)
656 },
657 Direction::Forward => {
658 current_line[self.edit_point.index..].len() as isize
659 }
660 }
661 };
662 self.perform_horizontal_adjustment(shift, select);
663 }
664
adjust_horizontal_to_limit(&mut self, direction: Direction, select: Selection)665 pub fn adjust_horizontal_to_limit(&mut self, direction: Direction, select: Selection) {
666 if self.adjust_selection_for_horizontal_change(direction, select) {
667 return
668 }
669 match direction {
670 Direction::Backward => {
671 self.edit_point.line = 0;
672 self.edit_point.index = 0;
673 },
674 Direction::Forward => {
675 self.edit_point.line = &self.lines.len() - 1;
676 self.edit_point.index = (&self.lines[&self.lines.len() - 1]).len();
677 }
678 }
679 }
680
681 /// Process a given `KeyboardEvent` and return an action for the caller to execute.
handle_keydown(&mut self, event: &KeyboardEvent) -> KeyReaction682 pub fn handle_keydown(&mut self, event: &KeyboardEvent) -> KeyReaction {
683 if let Some(key) = event.get_key() {
684 self.handle_keydown_aux(event.printable(), key, event.get_key_modifiers())
685 } else {
686 KeyReaction::Nothing
687 }
688 }
689
handle_keydown_aux(&mut self, printable: Option<char>, key: Key, mods: KeyModifiers) -> KeyReaction690 pub fn handle_keydown_aux(&mut self,
691 printable: Option<char>,
692 key: Key,
693 mods: KeyModifiers) -> KeyReaction {
694 let maybe_select = if mods.contains(KeyModifiers::SHIFT) {
695 Selection::Selected
696 } else {
697 Selection::NotSelected
698 };
699
700 match (printable, key) {
701 (_, Key::B) if mods.contains(KeyModifiers::CONTROL | KeyModifiers::ALT) => {
702 self.adjust_horizontal_by_word(Direction::Backward, maybe_select);
703 KeyReaction::RedrawSelection
704 },
705 (_, Key::F) if mods.contains(KeyModifiers::CONTROL | KeyModifiers::ALT) => {
706 self.adjust_horizontal_by_word(Direction::Forward, maybe_select);
707 KeyReaction::RedrawSelection
708 },
709 (_, Key::A) if mods.contains(KeyModifiers::CONTROL | KeyModifiers::ALT) => {
710 self.adjust_horizontal_to_line_end(Direction::Backward, maybe_select);
711 KeyReaction::RedrawSelection
712 },
713 (_, Key::E) if mods.contains(KeyModifiers::CONTROL | KeyModifiers::ALT) => {
714 self.adjust_horizontal_to_line_end(Direction::Forward, maybe_select);
715 KeyReaction::RedrawSelection
716 },
717 #[cfg(target_os = "macos")]
718 (None, Key::A) if mods == KeyModifiers::CONTROL => {
719 self.adjust_horizontal_to_line_end(Direction::Backward, maybe_select);
720 KeyReaction::RedrawSelection
721 },
722 #[cfg(target_os = "macos")]
723 (None, Key::E) if mods == KeyModifiers::CONTROL => {
724 self.adjust_horizontal_to_line_end(Direction::Forward, maybe_select);
725 KeyReaction::RedrawSelection
726 },
727 (_, Key::A) if is_control_key(mods) => {
728 self.select_all();
729 KeyReaction::RedrawSelection
730 },
731 (_, Key::C) if is_control_key(mods) => {
732 if let Some(text) = self.get_selection_text() {
733 self.clipboard_provider.set_clipboard_contents(text);
734 }
735 KeyReaction::DispatchInput
736 },
737 (_, Key::V) if is_control_key(mods) => {
738 let contents = self.clipboard_provider.clipboard_contents();
739 self.insert_string(contents);
740 KeyReaction::DispatchInput
741 },
742 (Some(c), _) => {
743 self.insert_char(c);
744 KeyReaction::DispatchInput
745 },
746 (None, Key::Delete) => {
747 self.delete_char(Direction::Forward);
748 KeyReaction::DispatchInput
749 },
750 (None, Key::Backspace) => {
751 self.delete_char(Direction::Backward);
752 KeyReaction::DispatchInput
753 },
754 #[cfg(target_os = "macos")]
755 (None, Key::Left) if mods.contains(KeyModifiers::SUPER) => {
756 self.adjust_horizontal_to_line_end(Direction::Backward, maybe_select);
757 KeyReaction::RedrawSelection
758 },
759 #[cfg(target_os = "macos")]
760 (None, Key::Right) if mods.contains(KeyModifiers::SUPER) => {
761 self.adjust_horizontal_to_line_end(Direction::Forward, maybe_select);
762 KeyReaction::RedrawSelection
763 },
764 #[cfg(target_os = "macos")]
765 (None, Key::Up) if mods.contains(KeyModifiers::SUPER) => {
766 self.adjust_horizontal_to_limit(Direction::Backward, maybe_select);
767 KeyReaction::RedrawSelection
768 },
769 #[cfg(target_os = "macos")]
770 (None, Key::Down) if mods.contains(KeyModifiers::SUPER) => {
771 self.adjust_horizontal_to_limit(Direction::Forward, maybe_select);
772 KeyReaction::RedrawSelection
773 },
774 (None, Key::Left) if mods.contains(KeyModifiers::ALT) => {
775 self.adjust_horizontal_by_word(Direction::Backward, maybe_select);
776 KeyReaction::RedrawSelection
777 },
778 (None, Key::Right) if mods.contains(KeyModifiers::ALT) => {
779 self.adjust_horizontal_by_word(Direction::Forward, maybe_select);
780 KeyReaction::RedrawSelection
781 },
782 (None, Key::Left) => {
783 self.adjust_horizontal_by_one(Direction::Backward, maybe_select);
784 KeyReaction::RedrawSelection
785 },
786 (None, Key::Right) => {
787 self.adjust_horizontal_by_one(Direction::Forward, maybe_select);
788 KeyReaction::RedrawSelection
789 },
790 (None, Key::Up) => {
791 self.adjust_vertical(-1, maybe_select);
792 KeyReaction::RedrawSelection
793 },
794 (None, Key::Down) => {
795 self.adjust_vertical(1, maybe_select);
796 KeyReaction::RedrawSelection
797 },
798 (None, Key::Enter) | (None, Key::KpEnter) => self.handle_return(),
799 (None, Key::Home) => {
800 #[cfg(not(target_os = "macos"))]
801 {
802 self.edit_point.index = 0;
803 }
804 KeyReaction::RedrawSelection
805 },
806 (None, Key::End) => {
807 #[cfg(not(target_os = "macos"))]
808 {
809 self.edit_point.index = self.current_line_length();
810 self.assert_ok_selection();
811 }
812 KeyReaction::RedrawSelection
813 },
814 (None, Key::PageUp) => {
815 self.adjust_vertical(-28, maybe_select);
816 KeyReaction::RedrawSelection
817 },
818 (None, Key::PageDown) => {
819 self.adjust_vertical(28, maybe_select);
820 KeyReaction::RedrawSelection
821 },
822 _ => KeyReaction::Nothing,
823 }
824 }
825
826 /// Whether the content is empty.
is_empty(&self) -> bool827 pub fn is_empty(&self) -> bool {
828 self.lines.len() <= 1 && self.lines.get(0).map_or(true, |line| line.is_empty())
829 }
830
831 /// The length of the content in bytes.
len(&self) -> usize832 pub fn len(&self) -> usize {
833 self.lines.iter().fold(0, |m, l| {
834 m + l.len() + 1 // + 1 for the '\n'
835 }) - 1
836 }
837
838 /// The length of the content in bytes.
utf16_len(&self) -> usize839 pub fn utf16_len(&self) -> usize {
840 self.lines.iter().fold(0, |m, l| {
841 m + l.chars().map(char::len_utf16).sum::<usize>() + 1 // + 1 for the '\n'
842 }) - 1
843 }
844
845 /// The length of the content in chars.
char_count(&self) -> usize846 pub fn char_count(&self) -> usize {
847 self.lines.iter().fold(0, |m, l| {
848 m + l.chars().count() + 1 // + 1 for the '\n'
849 }) - 1
850 }
851
852 /// Get the current contents of the text input. Multiple lines are joined by \n.
get_content(&self) -> DOMString853 pub fn get_content(&self) -> DOMString {
854 let mut content = "".to_owned();
855 for (i, line) in self.lines.iter().enumerate() {
856 content.push_str(&line);
857 if i < self.lines.len() - 1 {
858 content.push('\n');
859 }
860 }
861 DOMString::from(content)
862 }
863
864 /// Get a reference to the contents of a single-line text input. Panics if self is a multiline input.
single_line_content(&self) -> &DOMString865 pub fn single_line_content(&self) -> &DOMString {
866 assert!(!self.multiline);
867 &self.lines[0]
868 }
869
870 /// Set the current contents of the text input. If this is control supports multiple lines,
871 /// any \n encountered will be stripped and force a new logical line.
set_content(&mut self, content: DOMString)872 pub fn set_content(&mut self, content: DOMString) {
873 self.lines = if self.multiline {
874 // https://html.spec.whatwg.org/multipage/#textarea-line-break-normalisation-transformation
875 content.replace("\r\n", "\n")
876 .split(|c| c == '\n' || c == '\r')
877 .map(DOMString::from)
878 .collect()
879 } else {
880 vec!(content)
881 };
882
883 self.edit_point = self.edit_point.constrain_to(&self.lines);
884
885 if let Some(origin) = self.selection_origin {
886 self.selection_origin = Some(origin.constrain_to(&self.lines));
887 }
888 self.assert_ok_selection();
889 }
890
891 /// Convert a TextPoint into a byte offset from the start of the content.
text_point_to_offset(&self, text_point: &TextPoint) -> usize892 fn text_point_to_offset(&self, text_point: &TextPoint) -> usize {
893 self.lines.iter().enumerate().fold(0, |acc, (i, val)| {
894 if i < text_point.line {
895 acc + val.len() + 1 // +1 for the \n
896 } else {
897 acc
898 }
899 }) + text_point.index
900 }
901
902 /// Convert a byte offset from the start of the content into a TextPoint.
offset_to_text_point(&self, abs_point: usize) -> TextPoint903 fn offset_to_text_point(&self, abs_point: usize) -> TextPoint {
904 let mut index = abs_point;
905 let mut line = 0;
906 let last_line_idx = self.lines.len() - 1;
907 self.lines.iter().enumerate().fold(0, |acc, (i, val)| {
908 if i != last_line_idx {
909 let line_end = val.len();
910 let new_acc = acc + line_end + 1;
911 if abs_point >= new_acc && index > line_end {
912 index -= line_end + 1;
913 line += 1;
914 }
915 new_acc
916 } else {
917 acc
918 }
919 });
920
921 TextPoint {
922 line: line, index: index
923 }
924 }
925
set_selection_range(&mut self, start: u32, end: u32, direction: SelectionDirection)926 pub fn set_selection_range(&mut self, start: u32, end: u32, direction: SelectionDirection) {
927 let mut start = start as usize;
928 let mut end = end as usize;
929 let text_end = self.get_content().len();
930
931 if end > text_end {
932 end = text_end;
933 }
934 if start > end {
935 start = end;
936 }
937
938 self.selection_direction = direction;
939
940 match direction {
941 SelectionDirection::None |
942 SelectionDirection::Forward => {
943 self.selection_origin = Some(self.offset_to_text_point(start));
944 self.edit_point = self.offset_to_text_point(end);
945 },
946 SelectionDirection::Backward => {
947 self.selection_origin = Some(self.offset_to_text_point(end));
948 self.edit_point = self.offset_to_text_point(start);
949 }
950 }
951 self.assert_ok_selection();
952 }
953
set_edit_point_index(&mut self, index: usize)954 pub fn set_edit_point_index(&mut self, index: usize) {
955 let byte_size = self.lines[self.edit_point.line]
956 .graphemes(true)
957 .take(index)
958 .fold(0, |acc, x| acc + x.len());
959 self.edit_point.index = byte_size;
960 }
961 }
962