GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat(line_edit): Ctrl+Y redoes
Ctrl+Y is the redo many hands expect, but it is not a DE chord (the
runner routes input.kdl's undo/redo, Ctrl+Z / Ctrl+Shift+Z), so it
arrives at handle_key as a key: it now redoes there, Ignored with
nothing to redo. It is caught before the edit recording, which would
otherwise file the redo as a fresh edit and drop the rest of the redo
branch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
src/widget/line_edit.rs | 42 ++++++++++++++++++++++++++++++++++++++----
1 file changed, 38 insertions(+), 4 deletions(-)
diff --git a/src/widget/line_edit.rs b/src/widget/line_edit.rs
index 9f19c3a..96ff869 100644
--- a/src/widget/line_edit.rs
+++ b/src/widget/line_edit.rs
@@ -378,11 +378,24 @@ impl LineEdit {
/// and Ctrl+U are each their own; and a caret move — by key or click —
/// ends a run.
///
- /// Undo and redo themselves are not keys here: the window runner routes
- /// the DE's `undo` / `redo` chords (`input.kdl`, Ctrl+Z / Ctrl+Shift+Z
- /// by default) to the app, which calls [`LineEdit::undo`] /
- /// [`LineEdit::redo`] on the field that has focus.
+ /// Undo and redo are mostly not keys here: the window runner routes the
+ /// DE's `undo` / `redo` chords (`input.kdl`, Ctrl+Z / Ctrl+Shift+Z by
+ /// default) to the app, which calls [`LineEdit::undo`] /
+ /// [`LineEdit::redo`] on the field that has focus. The one exception is
+ /// **Ctrl+Y**, the other redo hands expect, which is not a DE chord and
+ /// so arrives here as a key: it redoes, or is `Ignored` with nothing to
+ /// redo. (A DE chord bound to Ctrl+Y is offered to the app first, as
+ /// any chord is, and never gets this far.)
pub fn handle_key(&mut self, event: &KeyEvent) -> EditOutcome {
+ // Before the recording below, which would file the redo as a fresh
+ // edit and so drop everything left to redo.
+ if event.state == ElementState::Pressed
+ && event.ctrl
+ && !event.shift
+ && matches!(&event.logical_key, Key::Character(c) if c.eq_ignore_ascii_case("y"))
+ {
+ return if self.redo() { EditOutcome::Edited } else { EditOutcome::Ignored };
+ }
let before = (!self.masked).then(|| self.snapshot());
let outcome = self.apply_key(event);
if let Some(before) = before {
@@ -1016,6 +1029,27 @@ mod tests {
assert_eq!(e.text, "one six");
}
+ #[test]
+ fn ctrl_y_redoes_and_keeps_the_rest_redoable() {
+ let mut e = LineEdit::default();
+ typed(&mut e, "one two three");
+ e.undo();
+ e.undo();
+ e.undo();
+ assert_eq!(e.text, "one ");
+ assert_eq!(e.handle_key(&ctrl("y")), EditOutcome::Edited);
+ assert_eq!(e.text, "one two");
+ assert!(e.can_redo(), "a redo by key is not a new edit");
+ e.handle_key(&ctrl("y"));
+ e.handle_key(&ctrl("y"));
+ assert_eq!(e.text, "one two three");
+ assert_eq!(e.handle_key(&ctrl("y")), EditOutcome::Ignored, "nothing left to redo");
+ assert_eq!(e.text, "one two three", "and no stray y typed");
+ // Undo still steps back over what Ctrl+Y redid.
+ e.undo();
+ assert_eq!(e.text, "one two ");
+ }
+
#[test]
fn a_masked_field_keeps_no_history() {
let mut e = LineEdit::masked();