GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/widget/line_edit.rs (53.5K)
1 //! A one-line text field's model: the text, a caret, and a selection — and
2 //! the keymap that edits them.
3 //!
4 //! For an app that draws its own field rather than using [`TextBox`]: the
5 //! app keeps a `LineEdit`, feeds it keys with [`LineEdit::handle_key`], and
6 //! draws [`LineEdit::display`] with the caret and selection wherever its
7 //! own layout puts them. Enter and Escape come back as [`EditOutcome`]s for
8 //! the caller to act on, as does any chord the field does not own, so the
9 //! app's own shortcuts still reach it. The pointer selects by dragging:
10 //! the app hit-tests a press and each motion to a byte offset and calls
11 //! [`LineEdit::press`] / [`LineEdit::drag_to`] / [`LineEdit::release`]. cce-browser's URL bar and its dialog
12 //! fields (HTTP auth, JS prompts) are built on it.
13 //!
14 //! **An input method's composition** (see `crate::ime`) is SHOWN, never
15 //! held: `text` keeps what was typed, and [`LineEdit::display`] splices the
16 //! composition in at the caret, with [`LineEdit::display_index`] /
17 //! [`LineEdit::text_index`] mapping across it (the caret lands where the
18 //! input method has its cursor; a press inside the composition is the caret)
19 //! and [`LineEdit::composition_range`] the span to underline. The app calls
20 //! [`LineEdit::sync_ime`] each frame the field has the keyboard, before
21 //! drawing it, reports the caret it draws with `ime::report_caret` (that is
22 //! also what tells the shell text input is wanted), and calls
23 //! [`LineEdit::drop_composition`] when the field loses the keyboard. The
24 //! commit arrives as typed text through [`LineEdit::handle_key`], which
25 //! takes no other key while a composition is up — they are the input
26 //! method's.
27 //!
28 //! Indices are **byte** offsets into `text`, always on a char boundary
29 //! ([`prev_boundary`] / [`next_boundary`] step them), which is what slicing
30 //! and shaping want. [`TextEditorState`](super::TextEditorState), the model
31 //! behind `TextBox`, counts chars and leaves the keymap to its widget;
32 //! the two are not interchangeable.
33 //!
34 //! [`TextBox`]: super::TextBox
35
36 use std::time::Duration;
37 use web_time::Instant;
38
39 use crate::history::History;
40 use crate::ime::{self, Preedit};
41 use crate::widget::{ElementState, Key, KeyEvent, NamedKey};
42
43 /// How close two presses at the same offset must be to count as one double
44 /// (or triple) click — `DocEditor`'s figure, so the two editors agree.
45 const DOUBLE_CLICK: Duration = Duration::from_millis(400);
46
47 /// What a keystroke meant, beyond editing the text.
48 #[derive(Debug, PartialEq)]
49 pub enum EditOutcome {
50 /// Nothing structural — redraw and carry on.
51 Edited,
52 /// Enter: the caller commits.
53 Submit,
54 /// Escape: the caller cancels.
55 Cancel,
56 /// Not ours (a chord the chrome owns).
57 Ignored,
58 }
59
60 #[derive(Default)]
61 pub struct LineEdit {
62 pub text: String,
63 pub cursor: usize,
64 /// Normalized (start < end). Any edit replaces or drops it.
65 pub selection: Option<(usize, usize)>,
66 /// Render as bullets. Set for password fields.
67 pub masked: bool,
68 /// While a pointer button is held after [`LineEdit::press`]: what the
69 /// press selected and how a drag grows it. `None` when no drag is in
70 /// progress.
71 drag: Option<Drag>,
72 /// The last press — when, where, and how many presses in a row landed
73 /// there — for telling a double or triple click from two clicks.
74 clicks: Option<(Instant, usize, u8)>,
75 /// Undo and redo for what [`LineEdit::handle_key`] changes. Never kept
76 /// for a masked field (see [`LineEdit::undo`]).
77 history: History<Snapshot>,
78 /// The input method's composition, shown at the caret (see the module
79 /// doc), and the `ime::generation` it was taken at.
80 composition: Option<Preedit>,
81 ime_seen: u64,
82 }
83
84 /// What an undo step restores: the text, and where the caret and selection
85 /// were, so undoing a deletion puts the caret back where it was.
86 #[derive(Clone, Debug)]
87 struct Snapshot {
88 text: String,
89 cursor: usize,
90 selection: Option<(usize, usize)>,
91 }
92
93 /// A press being dragged: the span the press selected (empty for a single
94 /// click, the word for a double, everything for a triple) and the unit the
95 /// selection grows by as the pointer moves.
96 #[derive(Clone, Copy, Debug)]
97 struct Drag {
98 lo: usize,
99 hi: usize,
100 unit: Unit,
101 }
102
103 #[derive(Clone, Copy, Debug, PartialEq)]
104 enum Unit {
105 Char,
106 Word,
107 All,
108 }
109
110 /// A word character for double-click and Ctrl+arrow purposes: letters and
111 /// digits, and `_` (as `DocEditor` counts them).
112 fn is_word_char(c: char) -> bool {
113 c.is_alphanumeric() || c == '_'
114 }
115
116 /// The char boundary before byte `i` in `s` (0 at the start). For stepping a
117 /// caret, or trimming text to fit without splitting a character.
118 pub fn prev_boundary(s: &str, i: usize) -> usize {
119 let mut j = i;
120 while j > 0 {
121 j -= 1;
122 if s.is_char_boundary(j) {
123 return j;
124 }
125 }
126 0
127 }
128
129 /// The char boundary after byte `i` in `s` (`s.len()` at the end).
130 pub fn next_boundary(s: &str, i: usize) -> usize {
131 let mut j = i;
132 while j < s.len() {
133 j += 1;
134 if s.is_char_boundary(j) {
135 return j;
136 }
137 }
138 s.len()
139 }
140
141 impl LineEdit {
142 pub fn with_text(text: impl Into<String>) -> Self {
143 let text = text.into();
144 Self { cursor: text.len(), text, ..Self::default() }
145 }
146
147 pub fn masked() -> Self {
148 Self { masked: true, ..Self::default() }
149 }
150
151 /// What to draw: the text, with an input method's composition at the
152 /// caret. Never returns the password itself.
153 pub fn display(&self) -> String {
154 let mut shown = if self.masked {
155 "\u{2022}".repeat(self.text.chars().count())
156 } else {
157 self.text.clone()
158 };
159 if let Some(p) = &self.composition {
160 shown.insert_str(self.display_index_held(self.cursor), &self.shown(p));
161 }
162 shown
163 }
164
165 /// A composition as drawn: its text, or a bullet a char when masked.
166 fn shown(&self, p: &Preedit) -> String {
167 if self.masked {
168 "\u{2022}".repeat(p.text.chars().count())
169 } else {
170 p.text.clone()
171 }
172 }
173
174 // ---- the input method ----
175
176 /// Take up the input method's composition, if it has moved since this
177 /// field last did. Call each frame the field has the keyboard, before
178 /// drawing it; true when what [`LineEdit::display`] shows changed. A
179 /// composition begun over a selection replaces it, as typing would.
180 pub fn sync_ime(&mut self) -> bool {
181 let generation = ime::generation();
182 if generation == self.ime_seen {
183 return false;
184 }
185 self.ime_seen = generation;
186 let next = ime::preedit();
187 if next.is_some() && self.composition.is_none() && self.selection.is_some_and(|(a, b)| a < b) {
188 let before = self.snapshot();
189 self.take_selection();
190 if !self.masked {
191 self.history.record(before);
192 }
193 }
194 if next.is_some() {
195 self.selection = None;
196 }
197 let changed = next != self.composition;
198 self.composition = next;
199 changed
200 }
201
202 /// The field lost the keyboard, or a press moved its caret: a
203 /// composition it was showing is dropped, and the input method asked to
204 /// cancel it.
205 pub fn drop_composition(&mut self) {
206 if self.composition.take().is_some() {
207 ime::request_reset();
208 }
209 self.ime_seen = ime::generation();
210 }
211
212 /// Whether an input method's composition is showing.
213 pub fn composing(&self) -> bool {
214 self.composition.is_some()
215 }
216
217 /// The composition's span in [`LineEdit::display`], to underline.
218 pub fn composition_range(&self) -> Option<(usize, usize)> {
219 let p = self.composition.as_ref()?;
220 let at = self.display_index_held(self.cursor);
221 Some((at, at + self.shown(p).len()))
222 }
223
224 pub fn select_all(&mut self) {
225 self.cursor = self.text.len();
226 self.selection = (self.cursor > 0).then_some((0, self.cursor));
227 }
228
229 // ---- the pointer ----
230 //
231 // The field does not know where its characters are drawn; the app does.
232 // So the pointer arrives as a byte offset into `text` that the app has
233 // already hit-tested (nearest char boundary to the pointer's x on the
234 // same shaped run it drew), and for a masked field converted with
235 // `text_index`, since what was drawn there is bullets.
236
237 /// A press at byte `at`: the caret goes there and a drag starts from it.
238 /// With `extend` (Shift held) the selection's far end stays put and the
239 /// selection runs from it to `at` instead — shift+click.
240 ///
241 /// Presses at the same offset within [`DOUBLE_CLICK`] of each other count
242 /// up: the second selects the word there (see [`LineEdit::word_at`]) and
243 /// a drag from it grows by whole words; the third selects everything —
244 /// the field is one line; a fourth starts over. A masked field selects
245 /// everything on the second press too: picking out a "word" would show
246 /// where the password's spaces and symbols are. Shift+click never counts.
247 pub fn press(&mut self, at: usize, extend: bool) {
248 self.press_at(at, extend, Instant::now());
249 }
250
251 fn press_at(&mut self, at: usize, extend: bool, now: Instant) {
252 // The caret moves: a composition is left where it was, cancelled.
253 self.drop_composition();
254 // A click moves the caret: typing after it is a new undo step.
255 self.history.break_group();
256 let at = self.boundary(at);
257 let count = match self.clicks {
258 Some((t, p, n)) if !extend && p == at && now.duration_since(t) < DOUBLE_CLICK => n % 3 + 1,
259 _ => 1,
260 };
261 self.clicks = Some((now, at, count));
262 let unit = match count {
263 1 => Unit::Char,
264 2 if !self.masked => Unit::Word,
265 _ => Unit::All,
266 };
267 let (lo, hi) = match unit {
268 Unit::Char => {
269 let anchor = if extend { self.anchor() } else { at };
270 self.select_between(anchor, at);
271 (anchor, anchor)
272 }
273 Unit::Word => {
274 let (a, b) = self.word_at(at);
275 self.select_between(a, b);
276 (a, b)
277 }
278 Unit::All => {
279 self.select_all();
280 (0, self.text.len())
281 }
282 };
283 self.drag = Some(Drag { lo, hi, unit });
284 }
285
286 /// The pointer moved to byte `at` with the button still held: the
287 /// selection is now everything between the press and here, and the caret
288 /// is here — or, after a double-click, everything from the pressed word
289 /// to the whole word here. True when that changed anything (a repaint is
290 /// due); false, and nothing happens, when no press is in progress.
291 pub fn drag_to(&mut self, at: usize) -> bool {
292 let Some(drag) = self.drag else {
293 return false;
294 };
295 let before = (self.cursor, self.selection);
296 let at = self.boundary(at);
297 match drag.unit {
298 Unit::Char => self.select_between(drag.lo, at),
299 Unit::Word if at < drag.lo => self.select_between(drag.hi, self.word_at(at).0),
300 Unit::Word if at > drag.hi => self.select_between(drag.lo, self.word_at(at).1),
301 Unit::Word => self.select_between(drag.lo, drag.hi),
302 Unit::All => {}
303 }
304 (self.cursor, self.selection) != before
305 }
306
307 /// The button came up: the drag is over, and what it selected stays
308 /// selected.
309 pub fn release(&mut self) {
310 self.drag = None;
311 }
312
313 /// Whether a press is being dragged — the app routes pointer motion to
314 /// [`LineEdit::drag_to`] while this holds, wherever the pointer is.
315 pub fn dragging(&self) -> bool {
316 self.drag.is_some()
317 }
318
319 /// The "word" a double-click at byte `at` selects: the run of characters
320 /// of one kind around it — letters and digits (with `_`, as `DocEditor`
321 /// counts them), spaces, or anything else. So in a URL `example` is a
322 /// word and so is `://`. On the edge of a word the word wins, so a click
323 /// just past a word's last letter still selects the word.
324 /// Where Ctrl+Left goes from byte `at`: back over anything that is not
325 /// a word character, then to the start of the word before it — the same
326 /// word characters as [`LineEdit::word_at`] (letters, digits, `_`). On a
327 /// masked field, straight to the start: stopping at word edges would show
328 /// where a password's spaces and symbols are.
329 pub fn word_left(&self, at: usize) -> usize {
330 if self.masked {
331 return 0;
332 }
333 let s = &self.text;
334 let mut i = self.boundary(at);
335 for want_word in [false, true] {
336 while let Some(c) = s[..i].chars().next_back() {
337 if is_word_char(c) != want_word {
338 break;
339 }
340 i -= c.len_utf8();
341 }
342 }
343 i
344 }
345
346 /// Where Ctrl+Right goes from byte `at`: forward over anything that is
347 /// not a word character, then to the end of the word after it. On a
348 /// masked field, straight to the end (see [`LineEdit::word_left`]).
349 pub fn word_right(&self, at: usize) -> usize {
350 if self.masked {
351 return self.text.len();
352 }
353 let s = &self.text;
354 let mut i = self.boundary(at);
355 for want_word in [false, true] {
356 while let Some(c) = s[i..].chars().next() {
357 if is_word_char(c) != want_word {
358 break;
359 }
360 i += c.len_utf8();
361 }
362 }
363 i
364 }
365
366 pub fn word_at(&self, at: usize) -> (usize, usize) {
367 let at = self.boundary(at);
368 let s = &self.text;
369 let kind = |c: char| {
370 if is_word_char(c) {
371 0
372 } else if c.is_whitespace() {
373 1
374 } else {
375 2
376 }
377 };
378 let after = s[at..].chars().next();
379 let before = s[..at].chars().next_back();
380 let k = match (before.map(kind), after.map(kind)) {
381 (_, Some(0)) | (Some(0), _) => 0,
382 (_, Some(k)) | (Some(k), None) => k,
383 (None, None) => return (at, at),
384 };
385 let mut a = at;
386 while let Some(c) = s[..a].chars().next_back() {
387 if kind(c) != k {
388 break;
389 }
390 a -= c.len_utf8();
391 }
392 let mut b = at;
393 while let Some(c) = s[b..].chars().next() {
394 if kind(c) != k {
395 break;
396 }
397 b += c.len_utf8();
398 }
399 (a, b)
400 }
401
402 /// A byte offset into [`LineEdit::display`] as the offset into `text` it
403 /// stands for: the same offset unless the field is masked, where each
404 /// bullet stands for one character of the text.
405 /// Across a composition: a point inside it is the caret, one after it
406 /// the text it is drawn after.
407 pub fn text_index(&self, display_at: usize) -> usize {
408 match self.composition_range() {
409 Some((a, _)) if display_at <= a => self.text_index_held(display_at),
410 Some((_, b)) if display_at < b => self.cursor,
411 Some((a, b)) => self.text_index_held(display_at - (b - a)),
412 None => self.text_index_held(display_at),
413 }
414 }
415
416 /// [`LineEdit::text_index`] over the held text alone.
417 fn text_index_held(&self, display_at: usize) -> usize {
418 if !self.masked {
419 return self.boundary(display_at);
420 }
421 let n = display_at / '\u{2022}'.len_utf8();
422 self.text.char_indices().nth(n).map_or(self.text.len(), |(i, _)| i)
423 }
424
425 /// The other direction: a byte offset into `text` (the caret, a selection
426 /// edge) as the offset into [`LineEdit::display`] where it is drawn — the
427 /// same offset unless masked, where it is that many bullets in.
428 ///
429 /// Across a composition: the caret is drawn where the input method has
430 /// its cursor in it, and what follows the caret after it.
431 pub fn display_index(&self, at: usize) -> usize {
432 let held = self.display_index_held(at);
433 let Some(p) = &self.composition else { return held };
434 let at = self.boundary(at);
435 if at > self.cursor {
436 held + self.shown(p).len()
437 } else if at == self.cursor {
438 let caret = p.caret_chars();
439 held + if self.masked { caret * '\u{2022}'.len_utf8() } else { p.text.char_indices().nth(caret).map_or(p.text.len(), |(b, _)| b) }
440 } else {
441 held
442 }
443 }
444
445 /// [`LineEdit::display_index`] over the held text alone.
446 fn display_index_held(&self, at: usize) -> usize {
447 let at = self.boundary(at);
448 if !self.masked {
449 return at;
450 }
451 self.text[..at].chars().count() * '\u{2022}'.len_utf8()
452 }
453
454 /// The end of the selection the caret is not at — what a shift+click
455 /// keeps — or the caret itself with nothing selected.
456 fn anchor(&self) -> usize {
457 match self.selection {
458 Some((a, b)) if self.cursor == a => b,
459 Some((a, _)) => a,
460 None => self.cursor,
461 }
462 }
463
464 fn select_between(&mut self, anchor: usize, at: usize) {
465 self.cursor = at;
466 self.selection = (anchor != at).then(|| (anchor.min(at), anchor.max(at)));
467 }
468
469 /// `at` clamped into the text and onto a char boundary.
470 fn boundary(&self, at: usize) -> usize {
471 let at = at.min(self.text.len());
472 if self.text.is_char_boundary(at) {
473 at
474 } else {
475 prev_boundary(&self.text, at)
476 }
477 }
478
479 fn take_selection(&mut self) -> bool {
480 match self.selection.take() {
481 Some((a, b)) if a < b && b <= self.text.len() => {
482 self.text.replace_range(a..b, "");
483 self.cursor = a;
484 true
485 }
486 _ => false,
487 }
488 }
489
490 /// One key. Edits are recorded for [`LineEdit::undo`] the way `TextBox`
491 /// records them: a typed run is one step and whitespace starts the next,
492 /// so undo walks back a word at a time; a run of Backspace (or Delete)
493 /// is one step; replacing a selection, a paste, a cut, a Ctrl word-delete
494 /// and Ctrl+U are each their own; and a caret move — by key or click —
495 /// ends a run.
496 ///
497 /// Undo and redo are mostly not keys here: the window runner routes the
498 /// DE's `undo` / `redo` chords (`input.kdl`, Ctrl+Z / Ctrl+Shift+Z by
499 /// default) to the app, which calls [`LineEdit::undo`] /
500 /// [`LineEdit::redo`] on the field that has focus. The one exception is
501 /// **Ctrl+Y**, the other redo hands expect, which is not a DE chord and
502 /// so arrives here as a key: it redoes, or is `Ignored` with nothing to
503 /// redo. (A DE chord bound to Ctrl+Y is offered to the app first, as
504 /// any chord is, and never gets this far.)
505 pub fn handle_key(&mut self, event: &KeyEvent) -> EditOutcome {
506 // While an input method composes, its keys are its own: a shell does
507 // not deliver them, and one that does is not editing this text.
508 self.sync_ime();
509 if self.composition.is_some() {
510 return EditOutcome::Edited;
511 }
512 // Before the recording below, which would file the redo as a fresh
513 // edit and so drop everything left to redo.
514 if event.state == ElementState::Pressed
515 && event.ctrl
516 && !event.shift
517 && matches!(&event.logical_key, Key::Character(c) if c.eq_ignore_ascii_case("y"))
518 {
519 return if self.redo() { EditOutcome::Edited } else { EditOutcome::Ignored };
520 }
521 let before = (!self.masked).then(|| self.snapshot());
522 let outcome = self.apply_key(event);
523 if let Some(before) = before {
524 if self.text != before.text {
525 let replaced = before.selection.is_some_and(|(a, b)| a < b);
526 let group = match &event.logical_key {
527 _ if replaced || event.ctrl => None,
528 Key::Named(NamedKey::Backspace) => Some(2),
529 Key::Named(NamedKey::Delete) => Some(3),
530 Key::Named(NamedKey::Space) => Some(4),
531 _ => {
532 let ws = event.text.as_deref().is_some_and(|t| t.chars().all(char::is_whitespace));
533 Some(if ws { 4 } else { 1 })
534 }
535 };
536 match group {
537 Some(g) => self.history.record_grouped(before, g),
538 None => self.history.record(before),
539 }
540 } else if outcome == EditOutcome::Edited {
541 // A caret or selection move between keystrokes splits the
542 // run: "abc", Left, "d" undoes as two steps.
543 self.history.break_group();
544 }
545 }
546 outcome
547 }
548
549 /// Step back to before the last edit; true when there was one. The caret
550 /// and selection come back with the text. A masked field keeps no
551 /// history — holding past versions of a password in memory is a cost
552 /// with nothing to show for it — so this is always false there.
553 pub fn undo(&mut self) -> bool {
554 let current = self.snapshot();
555 let Some(prev) = self.history.undo(current) else {
556 return false;
557 };
558 self.restore(prev);
559 true
560 }
561
562 /// Step forward again after [`LineEdit::undo`]; true when there was a
563 /// step to redo. Any new edit after an undo drops what could be redone.
564 pub fn redo(&mut self) -> bool {
565 let current = self.snapshot();
566 let Some(next) = self.history.redo(current) else {
567 return false;
568 };
569 self.restore(next);
570 true
571 }
572
573 /// The field for a screen reader (`a11y::AppNodes::text_field`): what it holds — a
574 /// masked field as bullets, never an input method's composition — and, while it has the
575 /// keyboard (`editing`), its selection and caret. The caller adds what the field does
576 /// not know: its placeholder, whether it may be set.
577 pub fn a11y_text(&self, editing: bool) -> crate::a11y::A11yText {
578 let chars = |byte: usize| self.text[..byte.min(self.text.len())].chars().count();
579 let len = self.text.chars().count();
580 let text = if self.masked { "\u{2022}".repeat(len) } else { self.text.clone() };
581 let selection = editing.then(|| match self.selection {
582 // A selection is normalized; the caret is at one of its ends.
583 Some((a, b)) if self.cursor == a => (chars(b), chars(a)),
584 Some((a, b)) => (chars(a), chars(b)),
585 None => (chars(self.cursor), chars(self.cursor)),
586 });
587 crate::a11y::A11yText { text, selection, password: self.masked, editable: true, ..Default::default() }
588 }
589
590 /// Replace the text with what a screen reader set (AT-SPI's `SetTextContents`), as the
591 /// user replacing it would: undoable (a masked field keeps no history), the caret at its
592 /// end, any composition dropped. Whether it changed.
593 pub fn a11y_set_text(&mut self, text: &str) -> bool {
594 self.drop_composition();
595 if self.text == text {
596 return false;
597 }
598 if !self.masked {
599 let before = self.snapshot();
600 self.history.record(before);
601 }
602 self.text = text.to_string();
603 self.cursor = self.text.len();
604 self.selection = None;
605 true
606 }
607
608 pub fn can_undo(&self) -> bool {
609 self.history.can_undo()
610 }
611
612 pub fn can_redo(&self) -> bool {
613 self.history.can_redo()
614 }
615
616 fn snapshot(&self) -> Snapshot {
617 Snapshot { text: self.text.clone(), cursor: self.cursor, selection: self.selection }
618 }
619
620 fn restore(&mut self, s: Snapshot) {
621 self.text = s.text;
622 self.cursor = s.cursor;
623 self.selection = s.selection;
624 self.drag = None;
625 self.clicks = None;
626 }
627
628 fn apply_key(&mut self, event: &KeyEvent) -> EditOutcome {
629 if event.state != ElementState::Pressed {
630 return EditOutcome::Ignored;
631 }
632 // Typing between two clicks makes them two clicks, not a double.
633 self.clicks = None;
634 match &event.logical_key {
635 Key::Named(NamedKey::Enter) => return EditOutcome::Submit,
636 Key::Named(NamedKey::Escape) => return EditOutcome::Cancel,
637 // Ctrl deletes a word: back to where Ctrl+Left would go, or on to
638 // where Ctrl+Right would. A selection goes instead, as it does
639 // for the plain keys. On a masked field the word edges are the
640 // ends, so these clear to the start or the end.
641 Key::Named(NamedKey::Backspace) if event.ctrl => {
642 if !self.take_selection() {
643 let from = self.word_left(self.cursor);
644 self.text.replace_range(from..self.cursor, "");
645 self.cursor = from;
646 }
647 }
648 Key::Named(NamedKey::Delete) if event.ctrl => {
649 if !self.take_selection() {
650 let to = self.word_right(self.cursor);
651 self.text.replace_range(self.cursor..to, "");
652 }
653 }
654 Key::Named(NamedKey::Backspace) => {
655 if !self.take_selection() && self.cursor > 0 {
656 let prev = prev_boundary(&self.text, self.cursor);
657 self.text.replace_range(prev..self.cursor, "");
658 self.cursor = prev;
659 }
660 }
661 Key::Named(NamedKey::Delete) => {
662 if !self.take_selection() && self.cursor < self.text.len() {
663 let next = next_boundary(&self.text, self.cursor);
664 self.text.replace_range(self.cursor..next, "");
665 }
666 }
667 // Arrows collapse a selection to the edge they move toward.
668 // With Shift the caret moves and the selection follows it from
669 // its anchor — the end the caret is not at, the same one a
670 // shift+click keeps — so it grows, shrinks, or flips past the
671 // anchor as the caret goes.
672 // Ctrl moves by word, plain or with Shift. Plain, it starts from
673 // the selection's edge in the direction it goes, as a plain
674 // arrow collapses to that edge.
675 Key::Named(NamedKey::ArrowLeft) if event.ctrl => {
676 if event.shift {
677 let to = self.word_left(self.cursor);
678 self.select_between(self.anchor(), to);
679 } else {
680 let from = self.selection.take().map_or(self.cursor, |(a, _)| a);
681 self.cursor = self.word_left(from);
682 }
683 }
684 Key::Named(NamedKey::ArrowRight) if event.ctrl => {
685 if event.shift {
686 let to = self.word_right(self.cursor);
687 self.select_between(self.anchor(), to);
688 } else {
689 let from = self.selection.take().map_or(self.cursor, |(_, b)| b);
690 self.cursor = self.word_right(from);
691 }
692 }
693 Key::Named(NamedKey::ArrowLeft) if event.shift => {
694 let to = prev_boundary(&self.text, self.cursor);
695 self.select_between(self.anchor(), to);
696 }
697 Key::Named(NamedKey::ArrowRight) if event.shift => {
698 let to = next_boundary(&self.text, self.cursor);
699 self.select_between(self.anchor(), to);
700 }
701 Key::Named(NamedKey::Home) if event.shift => self.select_between(self.anchor(), 0),
702 Key::Named(NamedKey::End) if event.shift => self.select_between(self.anchor(), self.text.len()),
703 Key::Named(NamedKey::ArrowLeft) => {
704 self.cursor = match self.selection.take() {
705 Some((a, _)) => a,
706 None => prev_boundary(&self.text, self.cursor),
707 };
708 }
709 Key::Named(NamedKey::ArrowRight) => {
710 self.cursor = match self.selection.take() {
711 Some((_, b)) => b,
712 None => next_boundary(&self.text, self.cursor),
713 };
714 }
715 Key::Named(NamedKey::Home) => {
716 self.selection = None;
717 self.cursor = 0;
718 }
719 Key::Named(NamedKey::End) => {
720 self.selection = None;
721 self.cursor = self.text.len();
722 }
723 Key::Character(c) if event.ctrl => match c.as_str() {
724 "a" => self.select_all(),
725 "u" => {
726 self.text.clear();
727 self.cursor = 0;
728 self.selection = None;
729 }
730 // Copy and cut are deliberately absent on a masked field:
731 // a password should not leave through the clipboard by a
732 // chord the user may not have meant. Paste is allowed, since
733 // that is how password managers hand one over.
734 "v" => {
735 if let Some(t) = crate::widget::clipboard::read_from_clipboard() {
736 let flat: String = t.chars().filter(|c| !c.is_control()).collect();
737 if !flat.is_empty() {
738 self.take_selection();
739 self.text.insert_str(self.cursor, &flat);
740 self.cursor += flat.len();
741 }
742 }
743 }
744 "c" | "x" if !self.masked => {
745 if let Some((a, b)) = self.selection.filter(|&(a, b)| a < b) {
746 crate::widget::clipboard::copy_to_clipboard(&self.text[a..b]);
747 if c == "x" {
748 self.take_selection();
749 }
750 }
751 }
752 _ => return EditOutcome::Ignored,
753 },
754 _ => {
755 let insert = match (&event.text, &event.logical_key) {
756 (Some(t), _) if !event.ctrl && !t.chars().any(char::is_control) => {
757 Some(t.clone())
758 }
759 (None, Key::Named(NamedKey::Space)) => Some(" ".to_string()),
760 (None, Key::Character(c)) if !event.ctrl => Some(c.clone()),
761 _ => return EditOutcome::Ignored,
762 };
763 if let Some(t) = insert {
764 self.take_selection();
765 self.text.insert_str(self.cursor, &t);
766 self.cursor += t.len();
767 }
768 }
769 }
770 EditOutcome::Edited
771 }
772 }
773
774 #[cfg(test)]
775 mod tests {
776 use super::*;
777
778 fn ev(key: Key, ctrl: bool) -> KeyEvent {
779 let text = match &key {
780 Key::Character(c) if !ctrl => Some(c.clone()),
781 _ => None,
782 };
783 KeyEvent {
784 state: ElementState::Pressed,
785 logical_key: key,
786 text,
787 repeat: false,
788 ctrl,
789 shift: false,
790 alt: false,
791 }
792 }
793 fn ch(c: &str) -> KeyEvent {
794 ev(Key::Character(c.into()), false)
795 }
796 fn ctrl(c: &str) -> KeyEvent {
797 ev(Key::Character(c.into()), true)
798 }
799 fn named(n: NamedKey) -> KeyEvent {
800 ev(Key::Named(n), false)
801 }
802
803 /// A composition is shown, not held: `display` has it at the caret, the
804 /// indices map across it, `text` never sees it; keys wait while it is up;
805 /// the commit is typed; a press drops it and asks the input method to
806 /// cancel; a masked field shows it as bullets.
807 #[test]
808 fn a_composition_is_shown_at_the_caret_and_never_held() {
809 let mut e = LineEdit::with_text("ab");
810 e.cursor = 1;
811 let _ = ime::take_reset();
812 // "にほ", the input method's cursor after "に" (three bytes in).
813 ime::set_preedit(Some(Preedit::new("にほ", Some((3, 3)))));
814 assert!(e.sync_ime());
815 assert_eq!(e.text, "ab");
816 assert_eq!(e.display(), "aにほb");
817 assert_eq!(e.composition_range(), Some((1, 7)));
818 assert_eq!(e.display_index(1), 4, "the caret after に");
819 assert_eq!(e.display_index(2), 8, "what follows, after the composition");
820 assert_eq!(e.text_index(5), 1, "inside the composition is the caret");
821 assert_eq!(e.text_index(8), 2);
822 assert_eq!(e.handle_key(&ch("x")), EditOutcome::Edited);
823 assert_eq!(e.text, "ab", "keys wait while the input method composes");
824
825 // The commit: the composition ends, its text is typed.
826 ime::set_preedit(None);
827 e.handle_key(&ch("日本"));
828 assert_eq!((e.text.as_str(), e.cursor), ("a日本b", 7));
829 assert_eq!(e.display(), "a日本b");
830
831 // A press drops a composition and asks for it to be cancelled.
832 ime::set_preedit(Some(Preedit::new("か", None)));
833 e.sync_ime();
834 assert!(e.composing());
835 e.press(0, false);
836 assert!(!e.composing());
837 assert!(ime::take_reset());
838 assert_eq!(e.text, "a日本b");
839
840 // Masked: bullets for the composition too.
841 let mut m = LineEdit::masked();
842 ime::set_preedit(Some(Preedit::new("ab", None)));
843 m.sync_ime();
844 assert_eq!(m.display(), "\u{2022}\u{2022}");
845 assert_eq!(m.text, "");
846 ime::set_preedit(None);
847 }
848
849 fn typed(e: &mut LineEdit, s: &str) {
850 for c in s.chars() {
851 e.handle_key(&ch(&c.to_string()));
852 }
853 }
854
855 #[test]
856 fn typing_inserts_at_the_caret() {
857 let mut e = LineEdit::default();
858 typed(&mut e, "abc");
859 assert_eq!(e.text, "abc");
860 assert_eq!(e.cursor, 3);
861 }
862
863 /// The URL bar's defining behaviour: entering it selects everything, so
864 /// the next keystroke replaces the address rather than appending to it.
865 #[test]
866 fn typing_over_a_selection_replaces_it() {
867 let mut e = LineEdit::with_text("https://example.com");
868 e.select_all();
869 typed(&mut e, "x");
870 assert_eq!(e.text, "x");
871 assert_eq!(e.selection, None);
872 }
873
874 #[test]
875 fn ctrl_a_selects_all_and_ctrl_u_clears() {
876 let mut e = LineEdit::with_text("abc");
877 e.handle_key(&ctrl("a"));
878 assert_eq!(e.selection, Some((0, 3)));
879 e.handle_key(&ctrl("u"));
880 assert_eq!(e.text, "");
881 assert_eq!(e.selection, None);
882 }
883
884 #[test]
885 fn backspace_deletes_a_selection_whole_or_one_char() {
886 let mut e = LineEdit::with_text("abc");
887 e.select_all();
888 e.handle_key(&named(NamedKey::Backspace));
889 assert_eq!(e.text, "");
890
891 let mut e = LineEdit::with_text("abc");
892 e.handle_key(&named(NamedKey::Backspace));
893 assert_eq!(e.text, "ab");
894 }
895
896 /// Arrows collapse to the edge they move toward rather than stepping from
897 /// the caret — otherwise Left after a select-all lands in the wrong place.
898 #[test]
899 fn arrows_collapse_a_selection_to_its_edge() {
900 let mut e = LineEdit::with_text("abc");
901 e.select_all();
902 e.handle_key(&named(NamedKey::ArrowLeft));
903 assert_eq!((e.cursor, e.selection), (0, None));
904
905 e.select_all();
906 e.handle_key(&named(NamedKey::ArrowRight));
907 assert_eq!((e.cursor, e.selection), (3, None));
908 }
909
910 #[test]
911 fn enter_and_escape_are_reported_not_swallowed() {
912 let mut e = LineEdit::with_text("x");
913 assert_eq!(e.handle_key(&named(NamedKey::Enter)), EditOutcome::Submit);
914 assert_eq!(e.handle_key(&named(NamedKey::Escape)), EditOutcome::Cancel);
915 }
916
917 /// Multi-byte text must not be split mid-character.
918 #[test]
919 fn caret_moves_by_character_not_byte() {
920 let mut e = LineEdit::with_text("é1");
921 e.handle_key(&named(NamedKey::Home));
922 e.handle_key(&named(NamedKey::ArrowRight));
923 assert_eq!(e.cursor, 2, "é is two bytes");
924 e.handle_key(&named(NamedKey::Backspace));
925 assert_eq!(e.text, "1");
926 }
927
928 #[test]
929 fn a_drag_selects_from_the_press_to_the_pointer_either_way() {
930 let mut e = LineEdit::with_text("hello world");
931 e.press(2, false);
932 assert!(e.dragging());
933 assert_eq!((e.cursor, e.selection), (2, None), "a press alone selects nothing");
934 assert!(e.drag_to(7));
935 assert_eq!((e.cursor, e.selection), (7, Some((2, 7))));
936 assert!(!e.drag_to(7), "no move, no repaint");
937 // Back past the press: the selection flips to the other side of it.
938 e.drag_to(0);
939 assert_eq!((e.cursor, e.selection), (0, Some((0, 2))));
940 // Back onto the press point: nothing selected, caret there.
941 e.drag_to(2);
942 assert_eq!((e.cursor, e.selection), (2, None));
943 e.drag_to(11);
944 e.release();
945 assert!(!e.dragging());
946 assert_eq!(e.selection, Some((2, 11)), "release keeps what was dragged");
947 assert!(!e.drag_to(4), "motion after release is not a drag");
948 assert_eq!(e.selection, Some((2, 11)));
949 }
950
951 #[test]
952 fn shift_click_extends_from_the_far_end() {
953 let mut e = LineEdit::with_text("hello world");
954 e.press(3, false);
955 e.release();
956 e.press(8, true);
957 assert_eq!((e.cursor, e.selection), (8, Some((3, 8))));
958 e.release();
959 // Shift+click on the other side of the anchor keeps the anchor.
960 e.press(1, true);
961 assert_eq!((e.cursor, e.selection), (1, Some((1, 3))));
962 e.release();
963 // After a select-all the caret is at the end, so the start is kept.
964 e.select_all();
965 e.press(5, true);
966 assert_eq!(e.selection, Some((0, 5)));
967 }
968
969 #[test]
970 fn a_dragged_selection_is_edited_like_any_other() {
971 let mut e = LineEdit::with_text("hello world");
972 e.press(0, false);
973 e.drag_to(6);
974 e.release();
975 typed(&mut e, "big ");
976 assert_eq!(e.text, "big world");
977 assert_eq!(e.cursor, 4);
978 }
979
980 /// The app's offset is clamped into the text and never splits a char.
981 #[test]
982 fn pointer_offsets_land_on_char_boundaries() {
983 let mut e = LineEdit::with_text("aé");
984 e.press(2, false); // inside é's two bytes
985 assert_eq!(e.cursor, 1);
986 e.drag_to(99);
987 assert_eq!((e.cursor, e.selection), (3, Some((1, 3))));
988 }
989
990 /// Each bullet is three bytes of display and one char of text.
991 #[test]
992 fn a_masked_field_maps_bullets_back_to_the_text() {
993 let mut e = LineEdit::masked();
994 typed(&mut e, "pé!");
995 let bullet = '\u{2022}'.len_utf8();
996 assert_eq!(e.text_index(0), 0);
997 assert_eq!(e.text_index(bullet), 1);
998 assert_eq!(e.text_index(2 * bullet), 3, "past é's two bytes");
999 assert_eq!(e.text_index(3 * bullet), 4);
1000 assert_eq!(e.text_index(99), 4);
1001 assert_eq!(LineEdit::with_text("abc").text_index(2), 2, "unmasked is the identity");
1002 // And back: every text boundary round-trips through the bullets.
1003 for at in [0, 1, 3, 4] {
1004 assert_eq!(e.text_index(e.display_index(at)), at, "{at}");
1005 }
1006 assert_eq!(e.display_index(3), 2 * bullet);
1007 assert_eq!(LineEdit::with_text("abc").display_index(2), 2);
1008 }
1009
1010 fn shifted(n: NamedKey) -> KeyEvent {
1011 KeyEvent { shift: true, ..named(n) }
1012 }
1013
1014 #[test]
1015 fn shift_arrows_grow_and_shrink_the_selection_from_its_anchor() {
1016 let mut e = LineEdit::with_text("hello");
1017 e.handle_key(&named(NamedKey::Home));
1018 e.handle_key(&shifted(NamedKey::ArrowRight));
1019 e.handle_key(&shifted(NamedKey::ArrowRight));
1020 assert_eq!((e.cursor, e.selection), (2, Some((0, 2))));
1021 e.handle_key(&shifted(NamedKey::ArrowLeft));
1022 assert_eq!((e.cursor, e.selection), (1, Some((0, 1))), "shrinks back toward the anchor");
1023 e.handle_key(&shifted(NamedKey::ArrowLeft));
1024 assert_eq!((e.cursor, e.selection), (0, None), "back on the anchor: nothing selected");
1025 // Past the anchor it flips to the other side.
1026 let mut e = LineEdit::with_text("hello");
1027 e.handle_key(&named(NamedKey::ArrowLeft)); // caret 4
1028 e.handle_key(&shifted(NamedKey::ArrowRight));
1029 e.handle_key(&shifted(NamedKey::ArrowLeft));
1030 e.handle_key(&shifted(NamedKey::ArrowLeft));
1031 assert_eq!((e.cursor, e.selection), (3, Some((3, 4))));
1032 // At the ends it stops.
1033 e.handle_key(&shifted(NamedKey::End));
1034 e.handle_key(&shifted(NamedKey::ArrowRight));
1035 assert_eq!((e.cursor, e.selection), (5, Some((4, 5))));
1036 }
1037
1038 #[test]
1039 fn shift_home_and_end_select_to_the_ends() {
1040 let mut e = LineEdit::with_text("hello world");
1041 e.press(6, false);
1042 e.release();
1043 e.handle_key(&shifted(NamedKey::End));
1044 assert_eq!((e.cursor, e.selection), (11, Some((6, 11))));
1045 e.handle_key(&shifted(NamedKey::Home));
1046 assert_eq!((e.cursor, e.selection), (0, Some((0, 6))), "the anchor stays where the caret was");
1047 // A select-all keeps its start; Shift+Left then trims its end.
1048 e.select_all();
1049 e.handle_key(&shifted(NamedKey::ArrowLeft));
1050 assert_eq!(e.selection, Some((0, 10)));
1051 // A plain arrow still collapses to the edge it points at.
1052 e.handle_key(&named(NamedKey::ArrowLeft));
1053 assert_eq!((e.cursor, e.selection), (0, None));
1054 }
1055
1056 /// Multi-byte text: Shift+arrow steps a character, not a byte.
1057 #[test]
1058 fn shift_arrows_step_whole_characters() {
1059 let mut e = LineEdit::with_text("aé");
1060 e.handle_key(&shifted(NamedKey::ArrowLeft));
1061 assert_eq!((e.cursor, e.selection), (1, Some((1, 3))));
1062 }
1063
1064 fn ctrl_key(n: NamedKey, shift: bool) -> KeyEvent {
1065 KeyEvent { ctrl: true, shift, ..named(n) }
1066 }
1067
1068 #[test]
1069 fn ctrl_arrows_jump_by_word() {
1070 let mut e = LineEdit::with_text("https://example.com/a_b c");
1071 e.handle_key(&named(NamedKey::Home));
1072 let mut stops = Vec::new();
1073 for _ in 0..6 {
1074 e.handle_key(&ctrl_key(NamedKey::ArrowRight, false));
1075 stops.push(e.cursor);
1076 }
1077 assert_eq!(stops, [5, 15, 19, 23, 26, 26], "ends of https, example, com, a_b, c; then stays");
1078 let mut back = Vec::new();
1079 for _ in 0..6 {
1080 e.handle_key(&ctrl_key(NamedKey::ArrowLeft, false));
1081 back.push(e.cursor);
1082 }
1083 assert_eq!(back, [25, 20, 16, 8, 0, 0], "starts of c, a_b, com, example, https; then stays");
1084 // From inside a word: to that word's own edge.
1085 e.cursor = 11;
1086 e.handle_key(&ctrl_key(NamedKey::ArrowLeft, false));
1087 assert_eq!(e.cursor, 8);
1088 }
1089
1090 #[test]
1091 fn ctrl_shift_arrows_select_by_word() {
1092 let mut e = LineEdit::with_text("one two three");
1093 e.handle_key(&named(NamedKey::Home));
1094 e.handle_key(&ctrl_key(NamedKey::ArrowRight, true));
1095 e.handle_key(&ctrl_key(NamedKey::ArrowRight, true));
1096 assert_eq!((e.cursor, e.selection), (7, Some((0, 7))));
1097 e.handle_key(&ctrl_key(NamedKey::ArrowLeft, true));
1098 assert_eq!((e.cursor, e.selection), (4, Some((0, 4))), "shrinks a word back");
1099 // A plain Ctrl+arrow leaves from the selection's edge and drops it.
1100 e.handle_key(&ctrl_key(NamedKey::ArrowRight, false));
1101 assert_eq!((e.cursor, e.selection), (7, None));
1102 e.select_all();
1103 e.handle_key(&ctrl_key(NamedKey::ArrowLeft, false));
1104 assert_eq!((e.cursor, e.selection), (0, None));
1105 }
1106
1107 #[test]
1108 fn ctrl_arrows_in_a_password_go_to_the_ends() {
1109 let mut e = LineEdit::masked();
1110 typed(&mut e, "pass word");
1111 e.handle_key(&ctrl_key(NamedKey::ArrowLeft, false));
1112 assert_eq!(e.cursor, 0, "no stop at the space");
1113 e.handle_key(&ctrl_key(NamedKey::ArrowRight, true));
1114 assert_eq!(e.selection, Some((0, 9)));
1115 }
1116
1117 #[test]
1118 fn ctrl_backspace_and_delete_take_a_word() {
1119 let mut e = LineEdit::with_text("https://example.com/drag");
1120 e.handle_key(&ctrl_key(NamedKey::Backspace, false));
1121 assert_eq!((e.text.as_str(), e.cursor), ("https://example.com/", 20));
1122 e.handle_key(&ctrl_key(NamedKey::Backspace, false));
1123 assert_eq!((e.text.as_str(), e.cursor), ("https://example.", 16), "the / and com go together");
1124 // From inside a word: just its first half.
1125 let mut e = LineEdit::with_text("hello world");
1126 e.cursor = 8;
1127 e.handle_key(&ctrl_key(NamedKey::Backspace, false));
1128 assert_eq!((e.text.as_str(), e.cursor), ("hello rld", 6));
1129 // Ctrl+Delete: forward to the end of the word.
1130 e.cursor = 0;
1131 e.handle_key(&ctrl_key(NamedKey::Delete, false));
1132 assert_eq!((e.text.as_str(), e.cursor), (" rld", 0));
1133 // At the ends nothing happens.
1134 let mut e = LineEdit::with_text("abc");
1135 e.handle_key(&ctrl_key(NamedKey::Delete, false));
1136 assert_eq!(e.text, "abc");
1137 e.cursor = 0;
1138 e.handle_key(&ctrl_key(NamedKey::Backspace, false));
1139 assert_eq!(e.text, "abc");
1140 }
1141
1142 #[test]
1143 fn ctrl_backspace_takes_a_selection_not_a_word() {
1144 let mut e = LineEdit::with_text("one two three");
1145 e.press(4, false);
1146 e.drag_to(6);
1147 e.release();
1148 e.handle_key(&ctrl_key(NamedKey::Backspace, false));
1149 assert_eq!((e.text.as_str(), e.cursor), ("one o three", 4));
1150 }
1151
1152 #[test]
1153 fn ctrl_backspace_in_a_password_clears_to_the_start() {
1154 let mut e = LineEdit::masked();
1155 typed(&mut e, "pass word");
1156 e.handle_key(&named(NamedKey::ArrowLeft));
1157 e.handle_key(&named(NamedKey::ArrowLeft)); // before "rd"
1158 e.handle_key(&ctrl_key(NamedKey::Backspace, false));
1159 assert_eq!((e.text.as_str(), e.cursor), ("rd", 0), "no stop at the space");
1160 }
1161
1162 #[test]
1163 fn undo_walks_back_a_word_at_a_time_and_redo_forward() {
1164 let mut e = LineEdit::default();
1165 typed(&mut e, "hello world");
1166 assert!(e.can_undo());
1167 assert!(e.undo());
1168 assert_eq!((e.text.as_str(), e.cursor), ("hello ", 6), "the word after the space");
1169 assert!(e.undo());
1170 assert_eq!(e.text, "hello", "then the space");
1171 assert!(e.undo());
1172 assert_eq!((e.text.as_str(), e.cursor), ("", 0));
1173 assert!(!e.undo(), "nothing left");
1174 assert!(e.redo());
1175 assert!(e.redo());
1176 assert!(e.redo());
1177 assert_eq!((e.text.as_str(), e.cursor), ("hello world", 11));
1178 assert!(!e.redo());
1179 }
1180
1181 #[test]
1182 fn a_caret_move_or_click_splits_a_run() {
1183 let mut e = LineEdit::default();
1184 typed(&mut e, "abc");
1185 e.handle_key(&named(NamedKey::ArrowLeft));
1186 typed(&mut e, "X");
1187 e.undo();
1188 assert_eq!((e.text.as_str(), e.cursor), ("abc", 2), "X alone, caret back before c");
1189 let mut e = LineEdit::default();
1190 typed(&mut e, "abc");
1191 e.press(1, false);
1192 e.release();
1193 typed(&mut e, "X");
1194 e.undo();
1195 assert_eq!(e.text, "abc");
1196 }
1197
1198 #[test]
1199 fn deleting_runs_and_replacements_are_their_own_steps() {
1200 let mut e = LineEdit::with_text("hello world");
1201 for _ in 0..3 {
1202 e.handle_key(&named(NamedKey::Backspace));
1203 }
1204 assert_eq!(e.text, "hello wo");
1205 e.undo();
1206 assert_eq!(e.text, "hello world", "three Backspaces, one step");
1207 // Typing over a selection: the replacement is one step, then the
1208 // rest of the typed run another.
1209 e.select_all();
1210 typed(&mut e, "xyz");
1211 e.undo();
1212 assert_eq!(e.text, "x");
1213 e.undo();
1214 assert_eq!((e.text.as_str(), e.selection), ("hello world", Some((0, 11))), "the selection comes back too");
1215 // Ctrl word-delete is a step of its own.
1216 e.selection = None;
1217 e.handle_key(&ctrl_key(NamedKey::Backspace, false));
1218 e.handle_key(&ctrl_key(NamedKey::Backspace, false));
1219 assert_eq!(e.text, "");
1220 e.undo();
1221 assert_eq!(e.text, "hello ");
1222 }
1223
1224 #[test]
1225 fn a_new_edit_after_undo_drops_the_redo() {
1226 let mut e = LineEdit::default();
1227 typed(&mut e, "one two");
1228 e.undo();
1229 typed(&mut e, "six");
1230 assert!(!e.can_redo());
1231 assert!(!e.redo());
1232 assert_eq!(e.text, "one six");
1233 }
1234
1235 #[test]
1236 fn ctrl_y_redoes_and_keeps_the_rest_redoable() {
1237 let mut e = LineEdit::default();
1238 typed(&mut e, "one two three");
1239 e.undo();
1240 e.undo();
1241 e.undo();
1242 assert_eq!(e.text, "one ");
1243 assert_eq!(e.handle_key(&ctrl("y")), EditOutcome::Edited);
1244 assert_eq!(e.text, "one two");
1245 assert!(e.can_redo(), "a redo by key is not a new edit");
1246 e.handle_key(&ctrl("y"));
1247 e.handle_key(&ctrl("y"));
1248 assert_eq!(e.text, "one two three");
1249 assert_eq!(e.handle_key(&ctrl("y")), EditOutcome::Ignored, "nothing left to redo");
1250 assert_eq!(e.text, "one two three", "and no stray y typed");
1251 // Undo still steps back over what Ctrl+Y redid.
1252 e.undo();
1253 assert_eq!(e.text, "one two ");
1254 }
1255
1256 #[test]
1257 fn a_masked_field_keeps_no_history() {
1258 let mut e = LineEdit::masked();
1259 typed(&mut e, "hunter2");
1260 assert!(!e.can_undo());
1261 assert!(!e.undo());
1262 assert_eq!(e.text, "hunter2");
1263 }
1264
1265 fn ms(t0: Instant, n: u64) -> Instant {
1266 t0 + Duration::from_millis(n)
1267 }
1268
1269 fn click(e: &mut LineEdit, at: usize, now: Instant) {
1270 e.press_at(at, false, now);
1271 e.release();
1272 }
1273
1274 #[test]
1275 fn a_double_click_selects_the_word_and_a_triple_everything() {
1276 let t0 = Instant::now();
1277 let mut e = LineEdit::with_text("https://example.com/drag");
1278 click(&mut e, 10, t0);
1279 assert_eq!(e.selection, None);
1280 click(&mut e, 10, ms(t0, 150));
1281 assert_eq!((e.selection, e.cursor), (Some((8, 15)), 15), "example");
1282 click(&mut e, 10, ms(t0, 300));
1283 assert_eq!(e.selection, Some((0, 24)), "the third click takes the line");
1284 click(&mut e, 10, ms(t0, 450));
1285 assert_eq!((e.selection, e.cursor), (None, 10), "the fourth starts over");
1286 }
1287
1288 #[test]
1289 fn slow_or_moved_clicks_are_two_clicks() {
1290 let t0 = Instant::now();
1291 let mut e = LineEdit::with_text("hello world");
1292 click(&mut e, 2, t0);
1293 click(&mut e, 2, ms(t0, 500));
1294 assert_eq!(e.selection, None, "too slow");
1295 click(&mut e, 3, ms(t0, 600));
1296 assert_eq!(e.selection, None, "somewhere else");
1297 // Shift+click is never half of a double-click.
1298 let mut e = LineEdit::with_text("hello world");
1299 click(&mut e, 2, t0);
1300 e.press_at(2, true, ms(t0, 100));
1301 assert_eq!(e.selection, None);
1302 // Nor is a click after typing.
1303 let mut e = LineEdit::with_text("hello world");
1304 click(&mut e, 11, t0);
1305 typed(&mut e, "!");
1306 e.handle_key(&named(NamedKey::Backspace));
1307 click(&mut e, 11, ms(t0, 100));
1308 assert_eq!(e.selection, None);
1309 }
1310
1311 #[test]
1312 fn a_word_is_a_run_of_one_kind() {
1313 let e = LineEdit::with_text("https://example.com/a_b c");
1314 assert_eq!(e.word_at(1), (0, 5), "https");
1315 assert_eq!(e.word_at(6), (5, 8), "the :// between words");
1316 assert_eq!(e.word_at(15), (8, 15), "just past a word's end is still the word");
1317 assert_eq!(e.word_at(21), (20, 23), "_ joins a word");
1318 assert_eq!(e.word_at(24), (23, 25), "a run of spaces");
1319 assert_eq!(e.word_at(26), (25, 26), "the end of the text");
1320 assert_eq!(LineEdit::default().word_at(0), (0, 0));
1321 // Multi-byte letters are letters.
1322 assert_eq!(LineEdit::with_text("é1 x").word_at(1), (0, 3));
1323 }
1324
1325 #[test]
1326 fn dragging_a_double_click_grows_by_words() {
1327 let t0 = Instant::now();
1328 let mut e = LineEdit::with_text("one two three four");
1329 click(&mut e, 5, t0);
1330 e.press_at(5, false, ms(t0, 100));
1331 assert_eq!(e.selection, Some((4, 7)), "two");
1332 e.drag_to(10); // into "three"
1333 assert_eq!((e.selection, e.cursor), (Some((4, 13)), 13));
1334 e.drag_to(6); // back inside "two": just the word again
1335 assert_eq!(e.selection, Some((4, 7)));
1336 e.drag_to(1); // into "one": from its start to the end of "two"
1337 assert_eq!((e.selection, e.cursor), (Some((0, 7)), 0));
1338 e.release();
1339 assert!(!e.drag_to(16));
1340 }
1341
1342 #[test]
1343 fn a_masked_double_click_selects_it_all() {
1344 let t0 = Instant::now();
1345 let mut e = LineEdit::masked();
1346 typed(&mut e, "pass word");
1347 click(&mut e, 2, t0);
1348 click(&mut e, 2, ms(t0, 100));
1349 assert_eq!(e.selection, Some((0, 9)), "no word boundaries in a password");
1350 }
1351
1352 /// A password must not leave through a chord the user may not have meant.
1353 #[test]
1354 fn a_masked_field_hides_its_text_and_refuses_copy() {
1355 let mut e = LineEdit::masked();
1356 typed(&mut e, "hunter2");
1357 assert_eq!(e.display(), "•".repeat(7));
1358 assert_ne!(e.display(), e.text);
1359 e.select_all();
1360 assert_eq!(e.handle_key(&ctrl("c")), EditOutcome::Ignored);
1361 assert_eq!(e.handle_key(&ctrl("x")), EditOutcome::Ignored);
1362 assert_eq!(e.text, "hunter2", "cut must not have removed it");
1363 }
1364
1365 /// A reader's edit replaces the text as the user's would: the caret at its end, a
1366 /// selection dropped, one undo step back — and none for a masked field, which keeps no
1367 /// history. The same text again changes nothing.
1368 #[test]
1369 fn a_readers_edit_replaces_the_text_undoably() {
1370 let mut e = LineEdit::with_text("old");
1371 e.selection = Some((0, 3));
1372 assert!(e.a11y_set_text("new.org"));
1373 assert_eq!((e.text.as_str(), e.cursor, e.selection), ("new.org", 7, None));
1374 assert!(!e.a11y_set_text("new.org"));
1375 assert!(e.undo());
1376 assert_eq!(e.text, "old");
1377 let mut p = LineEdit::masked();
1378 assert!(p.a11y_set_text("hunter2"));
1379 assert!(!p.undo(), "no history for a secret");
1380 assert_eq!(p.a11y_text(false).text, "\u{2022}".repeat(7));
1381 }
1382 }