EditBox caret keys, selection and focus blink follow 1.12.1 (#750)

Several EditBox keys differed from 1.12.1:
- Up/Down did nothing in a multi-line box;
- Home/End went to the text's ends;
- `HighlightText` and Ctrl+A moved the caret;
- focus did not restart the blink;
- a non-digit kept a numeric box's selection;
- Option+Up could not recall chat history on a Mac.

In 1.12.1, Up/Down move a wrapped row, keeping the letter column (`0x77cb20`), or recall history in a single-line box with any modifier but Shift. Home/End stop at a newline (`0x77c980`, `0x77c9f0`). `HighlightText` writes only the selection (`0x77cca0`), and Shift extends by `0x77cd10`. Focus restarts the blink (`0x77dd68`), and Insert deletes the selection before its digit test (`0x77bf41`).

benilla now does each. Down onto the last row lands at the end, and history stays shell-style, both deviations.

Tests cover each key, in the engine and the stock mail and ticket boxes.

Fixes #735
Fixes #736
Fixes #737
Fixes #738
Fixes #745
Fixes #747

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-10-01 10:56:03 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent be9e3d8aac
commit f22ec0016c
17 changed files with 1269 additions and 214 deletions
+151 -27
View File
@@ -1,6 +1,7 @@
//! The per-OS text-editing keymap: a keypress and modifiers to an engine [`EditAction`] or a
//! host-side clipboard operation. The Windows/Linux table is the reference's key handler
//! (`0x77b160`: Ctrl+arrows by word, Ctrl+A/C/X/V, Ctrl/Shift+Insert, Shift+Delete).
//! (`0x77b160`: Ctrl+arrows by word, Ctrl+HOME/END to the text's edge, Ctrl+A/C/X/V,
//! Ctrl/Shift+Insert, Shift+Delete).
//! Deviation: macOS takes the Cocoa text-field chords and Ctrl+Backspace/Delete delete a word,
//! because editing follows each OS's own text fields.
@@ -56,10 +57,11 @@ pub(crate) fn chord(key: KeyCode, name: Option<KeyName>, m: Mods, mac: bool) ->
}
}
/// macOS, the Cocoa text-field chords: Option is a word, Cmd the line edge; plain Up/Down recall
/// history. Cocoa's Emacs Ctrl set is left unbound.
/// macOS, the Cocoa text-field chords: Option+Left/Right is a word and Cmd+Left/Right the line's
/// edge; Cmd+Up/Down and Shift+Up/Down go to the text's edge, and Up/Down, plain or with Option,
/// move a row, or recall history in a single-line box. Cocoa's Emacs Ctrl set is left unbound.
fn chord_mac(key: KeyCode, m: Mods) -> Option<Chord> {
use EditUnit::{Char, Edge, Word};
use EditUnit::{Char, Edge, Line, Row, Word};
let mv = |unit, back| {
Some(Chord::Edit(EditAction::Move {
unit,
@@ -72,24 +74,25 @@ fn chord_mac(key: KeyCode, m: Mods) -> Option<Chord> {
KeyCode::ArrowLeft | KeyCode::ArrowRight => {
let back = key == KeyCode::ArrowLeft;
if m.sup {
mv(Edge, back)
mv(Line, back)
} else if m.alt {
mv(Word, back)
} else {
mv(Char, back)
}
}
// Cmd+Up/Down is the box's start/end; Shift extends there.
// Cmd+Up/Down go to the text's start or end and Shift+Up/Down select to it, as a Cocoa
// text field's do. Option is the reference's Alt, which its UP and DOWN arms do not read
// (`0x77b64e`, `0x77b675`), and the one modifier that keeps the arrows in an alt-arrow box
// (`0x77b1b3`): the chat box's history recall.
KeyCode::ArrowUp | KeyCode::ArrowDown => {
let back = key == KeyCode::ArrowUp;
if m.sup || m.shift {
mv(Edge, back)
} else if m.alt || m.ctrl {
} else if m.ctrl {
None
} else if back {
Some(Chord::Edit(EditAction::HistoryPrev))
} else {
Some(Chord::Edit(EditAction::HistoryNext))
mv(Row, back)
}
}
KeyCode::Home => mv(Edge, true),
@@ -118,7 +121,7 @@ fn chord_mac(key: KeyCode, m: Mods) -> Option<Chord> {
/// Windows/Linux: the reference's chords (`0x77b160`) plus the Ctrl word deletes.
fn chord_pc(key: KeyCode, m: Mods) -> Option<Chord> {
use EditUnit::{Char, Edge, Word};
use EditUnit::{Char, Edge, Line, Row, Word};
let mv = |unit, back| {
Some(Chord::Edit(EditAction::Move {
unit,
@@ -137,15 +140,11 @@ fn chord_pc(key: KeyCode, m: Mods) -> Option<Chord> {
mv(Char, back)
}
}
// Plain Up/Down only: history recall.
KeyCode::ArrowUp if !(m.ctrl || m.alt || m.shift || m.sup) => {
Some(Chord::Edit(EditAction::HistoryPrev))
}
KeyCode::ArrowDown if !(m.ctrl || m.alt || m.shift || m.sup) => {
Some(Chord::Edit(EditAction::HistoryNext))
}
KeyCode::Home => mv(Edge, true),
KeyCode::End => mv(Edge, false),
// UP/DOWN read only Shift, as the extend flag (`0x77b65d`, `0x77b684`): a row in a
// multi-line box, history in a single-line one.
KeyCode::ArrowUp | KeyCode::ArrowDown if !m.sup => mv(Row, key == KeyCode::ArrowUp),
// HOME/END stop at a newline; Ctrl goes to the text's edge (`0x77b49e`, `0x77b4de`).
KeyCode::Home | KeyCode::End => mv(if m.ctrl { Edge } else { Line }, key == KeyCode::Home),
// Shift+Delete cuts, as the reference does (`0x77b160`).
KeyCode::Backspace => {
if m.ctrl {
@@ -233,7 +232,7 @@ mod tests {
assert_eq!(
edit(chord(KeyCode::ArrowLeft, us(KeyCode::ArrowLeft), SUP, true)),
Move {
unit: Edge,
unit: Line,
back: true,
extend: false
}
@@ -253,7 +252,11 @@ mod tests {
);
assert_eq!(
edit(chord(KeyCode::ArrowUp, us(KeyCode::ArrowUp), NONE, true)),
HistoryPrev
Move {
unit: Row,
back: true,
extend: false
}
);
assert_eq!(
edit(chord(
@@ -262,7 +265,11 @@ mod tests {
NONE,
true
)),
HistoryNext
Move {
unit: Row,
back: false,
extend: false
}
);
assert_eq!(
edit(chord(KeyCode::ArrowUp, us(KeyCode::ArrowUp), SHIFT, true)),
@@ -353,16 +360,29 @@ mod tests {
);
assert_eq!(
edit(chord(KeyCode::ArrowUp, us(KeyCode::ArrowUp), NONE, false)),
HistoryPrev
Move {
unit: Row,
back: true,
extend: false
}
);
assert_eq!(
chord(KeyCode::ArrowUp, us(KeyCode::ArrowUp), SHIFT, false),
None
edit(chord(
KeyCode::ArrowDown,
us(KeyCode::ArrowDown),
SHIFT,
false
)),
Move {
unit: Row,
back: false,
extend: true
}
);
assert_eq!(
edit(chord(KeyCode::End, us(KeyCode::End), SHIFT, false)),
Move {
unit: Edge,
unit: Line,
back: false,
extend: true
}
@@ -413,6 +433,110 @@ mod tests {
assert_eq!(chord(KeyCode::KeyA, us(KeyCode::KeyA), SUP, false), None);
}
/// On a Mac, Cmd+Left/Right go to the caret's line's start or end, as a Cocoa text view's do;
/// in a single-line box, with no newline typed, that is the text's edge.
#[test]
fn mac_cmd_left_right_go_to_the_lines_edge() {
use EditAction::Move;
use EditUnit::Line;
for (key, back) in [(KeyCode::ArrowLeft, true), (KeyCode::ArrowRight, false)] {
assert_eq!(
edit(chord(key, us(key), SUP, true)),
Move {
unit: Line,
back,
extend: false
}
);
assert_eq!(
edit(chord(key, us(key), Mods { shift: true, ..SUP }, true)),
Move {
unit: Line,
back,
extend: true
}
);
}
}
/// On a Mac, Option is the reference's Alt: Option+Up/Down reach the UP and DOWN arms
/// (`0x77b64e`, `0x77b675`), which read only Shift, so the alt-arrow chat box recalls its
/// history with them (`0x77b1b3`, then `0x77d030`/`0x77cfd0`). Ctrl stays unbound.
#[test]
fn mac_option_up_down_move_a_row_or_recall_history() {
use EditAction::Move;
use EditUnit::Row;
for (key, back) in [(KeyCode::ArrowUp, true), (KeyCode::ArrowDown, false)] {
assert_eq!(
edit(chord(key, us(key), ALT, true)),
Move {
unit: Row,
back,
extend: false
}
);
assert_eq!(chord(key, us(key), CTRL, true), None);
}
}
/// The reference's UP and DOWN arms read only Shift (`0x77b64e`, `0x77b675`), so Ctrl and Alt
/// reach them too: Alt+Up is how the alt-arrow chat box recalls its history. HOME and END
/// stop at a newline and Ctrl+HOME/END go to the text's edge (`0x77b499`, `0x77b4d9`).
#[test]
fn pc_up_down_take_any_modifier_and_home_end_split_on_ctrl() {
use EditAction::Move;
use EditUnit::*;
for m in [NONE, CTRL, ALT, Mods { shift: true, ..ALT }] {
assert_eq!(
edit(chord(KeyCode::ArrowUp, us(KeyCode::ArrowUp), m, false)),
Move {
unit: Row,
back: true,
extend: m.shift
}
);
}
assert_eq!(
chord(KeyCode::ArrowDown, us(KeyCode::ArrowDown), SUP, false),
None
);
for (key, back) in [(KeyCode::Home, true), (KeyCode::End, false)] {
assert_eq!(
edit(chord(key, us(key), NONE, false)),
Move {
unit: Line,
back,
extend: false
}
);
assert_eq!(
edit(chord(
key,
us(key),
Mods {
shift: true,
..CTRL
},
false
)),
Move {
unit: Edge,
back,
extend: true
}
);
// macOS keeps the text-field chord: HOME and END go to the text's edge.
assert_eq!(
edit(chord(key, us(key), NONE, true)),
Move {
unit: Edge,
back,
extend: false
}
);
}
}
/// AltGr arrives as Ctrl+Alt and types letters on European layouts (Polish `ą`, `ć`, `ź`).
#[test]
fn altgr_letters_are_not_clipboard_chords() {