git.lucas.co / cce-ui
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 }