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:
co-authored by
Claude Opus 5.5
parent
be9e3d8aac
commit
f22ec0016c
@@ -8,8 +8,8 @@
|
||||
//! - Routing: a focused box consumes every key and char and fires only its specialized scripts
|
||||
//! (Enter, Escape, Space, Tab, TextChanged, TextSet, focus), never a generic `OnKeyDown`; an
|
||||
//! unfocused box that is not `autoFocus` ignores input.
|
||||
//! - Editing: an insert replaces the selection; `numeric` aborts a whole insert on any non-digit;
|
||||
//! caps trim from the end (`maxBytes`, then `maxLetters`).
|
||||
//! - Editing: an insert deletes the selection, then `numeric` aborts a whole insert on any
|
||||
//! non-digit; caps trim from the end (`maxBytes`, then `maxLetters`).
|
||||
//! - Events: an edit, a caret move, a focus change or a re-seat of the text only raises the box's
|
||||
//! dirty bits (`[E+0x31c]`); the box's flush, run after its own `OnUpdate` in the tick's walk
|
||||
//! ([`update`]) and before a key or click acts on it, fires `OnCursorChanged` and then
|
||||
@@ -108,22 +108,32 @@ pub(super) fn action(lua: &Lua, a: EditAction) -> bool {
|
||||
EditUnit::Char => move_horizontal(lua, h, !back, extend),
|
||||
// Ctrl (Option on macOS) moves by word (the client's Ctrl check, `0x41f8f0(1)`).
|
||||
EditUnit::Word => interact::move_word(lua, h, !back, extend),
|
||||
EditUnit::Line => {
|
||||
with_eb(lua, h, |eb| eb.move_to_line_edge(!back, extend));
|
||||
}
|
||||
EditUnit::Edge => move_to_edge(lua, h, !back, extend),
|
||||
// UP/DOWN fork on `multiLine` (`0x77b64e`, `0x77b675`): a row in a multi-line box,
|
||||
// else history recall (`0x77d030`, `0x77cfd0`; benilla's is shell-style, see
|
||||
// `EditBoxState::history_step`). On an alt-arrow box, as the stock chat box is, that is
|
||||
// Alt+Up/Down.
|
||||
EditUnit::Row => {
|
||||
if with_eb(lua, h, |eb| eb.multi_line).unwrap_or(false) {
|
||||
// The rows the move reads, measured now when an engine is installed.
|
||||
seam::advances_current(lua, h);
|
||||
with_eb(lua, h, |eb| eb.move_by_row(!back, extend));
|
||||
} else {
|
||||
history_step_key(lua, h, back);
|
||||
}
|
||||
}
|
||||
},
|
||||
EditAction::Delete { unit, back } => match unit {
|
||||
EditUnit::Char => delete_dir(lua, h, !back),
|
||||
EditUnit::Word => delete_span(lua, h, |eb| eb.word_boundary(!back)),
|
||||
EditUnit::Edge => delete_span(lua, h, |eb| if back { 0 } else { eb.text.len() }),
|
||||
// No keymap deletes by line or row.
|
||||
EditUnit::Line | EditUnit::Row => {}
|
||||
},
|
||||
EditAction::SelectAll => highlight_text(lua, h, 0, -1),
|
||||
// History recall (`historyLines`): older, newer, then back to the live draft. A multiLine
|
||||
// box consumes it inert (it has no vertical caret movement). On an alt-arrow box, as the
|
||||
// stock chat box is, recall is Alt+Up/Down; the reference's history controller is untraced.
|
||||
EditAction::HistoryPrev | EditAction::HistoryNext => {
|
||||
if !with_eb(lua, h, |eb| eb.multi_line).unwrap_or(false) {
|
||||
history_step_key(lua, h, a == EditAction::HistoryPrev);
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -260,17 +270,21 @@ fn clear_focus_handle(lua: &Lua, h: FrameHandle) {
|
||||
// Editing primitives: mutate under one borrow, then sync and fire
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// `Insert` (`0x77bee0`): replace the selection, abort on a non-digit when `numeric`, splice,
|
||||
/// `Insert` (`0x77bee0`): delete the selection, abort on a non-digit when `numeric`, splice,
|
||||
/// enforce caps, raise the dirty bits for the flush, and with `fire_space` fire one
|
||||
/// `OnSpacePressed` per space.
|
||||
fn insert(lua: &Lua, h: FrameHandle, ins: &str, fire_space: bool) {
|
||||
let Some(out) = with_eb(lua, h, |eb| eb.insert(ins)) else {
|
||||
return; // not an EditBox
|
||||
};
|
||||
if !out.text_changed {
|
||||
return; // numeric-aborted
|
||||
if out.text_changed {
|
||||
sync_text_region(lua, h);
|
||||
}
|
||||
if !out.inserted {
|
||||
// Refused: a numeric abort leaves at most the selection's deletion, whose text bit fires
|
||||
// `OnTextChanged` at the flush, and returns before `OnChar` (`0x77bf6d` → `0x77c1b8`).
|
||||
return;
|
||||
}
|
||||
sync_text_region(lua, h);
|
||||
let id = frame_id_of(lua, h);
|
||||
// Insert fires the generic `OnChar` with the spliced string as `arg1` (`0x77c13c`, through the
|
||||
// varargs firer `0x7026f0`), ahead of the flush's `OnTextChanged`; `SetText` does not.
|
||||
@@ -329,12 +343,12 @@ fn history_step_key(lua: &Lua, h: FrameHandle, older: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Left/Right: `shift` extends from the anchor, else collapse the selection or move one char.
|
||||
/// Left/Right: `shift` extends the selection, else collapse it or move one char.
|
||||
fn move_horizontal(lua: &Lua, h: FrameHandle, right: bool, shift: bool) {
|
||||
with_eb(lua, h, |eb| eb.move_by_char(right, shift));
|
||||
}
|
||||
|
||||
/// Home/End; `shift` extends from the anchor.
|
||||
/// Ctrl+Home/End and Cmd+Up/Down, to the text's edge; `shift` extends the selection.
|
||||
fn move_to_edge(lua: &Lua, h: FrameHandle, end: bool, shift: bool) {
|
||||
with_eb(lua, h, |eb| eb.move_to_edge(end, shift));
|
||||
}
|
||||
|
||||
@@ -14,11 +14,12 @@ pub(in crate::script) fn update(lua: &Lua, h: FrameHandle, dt: f32) {
|
||||
}
|
||||
|
||||
/// The flush `0x77d3e0`. It samples bit 0 at entry (`0x77d3f2`); on bit 0 the relayout runs
|
||||
/// (`0x77d447`) and the bit clears (`0x77d44c`); on bit 2 the caret leg runs (`0x77d475`), firing
|
||||
/// `OnCursorChanged`, and only then does the bit clear (`0x77d47a`); last, `OnTextChanged` fires
|
||||
/// if bit 0 was set at entry (`0x77d481`–`0x77d498`). So a handler's edit raises bits for the next
|
||||
/// flush, a frame later, and never loops inside one: an `OnTextChanged` that sets its own text
|
||||
/// fires once more, next frame, and a caret moved by `OnCursorChanged` is cleared with the bit.
|
||||
/// (`0x77d447`) and the bit clears (`0x77d44c`); on bit 2 the caret leg runs (`0x77d475`), with
|
||||
/// the blink and `OnCursorChanged`, and only then does the bit clear (`0x77d47a`); last,
|
||||
/// `OnTextChanged` fires if bit 0 was set at entry (`0x77d481`–`0x77d498`). So a handler's edit
|
||||
/// raises bits for the next flush, a frame later, and never loops inside one: an `OnTextChanged`
|
||||
/// that sets its own text fires once more, next frame, and a caret moved by `OnCursorChanged` is
|
||||
/// cleared with the bit.
|
||||
/// Besides the walk, the box's `OnKeyDown` (`0x77b1e2`) and `OnMouseDown` (`0x77b819`) run it
|
||||
/// before the key or click acts.
|
||||
///
|
||||
@@ -47,17 +48,33 @@ pub(super) fn flush(lua: &Lua, h: FrameHandle) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The caret leg `0x77da80`: `OnCursorChanged(x, y, w, h)` (`0x77de1b`, in UI units via
|
||||
/// `0x77dd5f`), `x` the caret's advance along its line, `y` minus the row index times the row
|
||||
/// pitch, `w` the constant 4.0 and `h` the line height. The arguments are computed only for a box
|
||||
/// with the script (`0x77dd8c`), from the box's advance table, which the installed font engine
|
||||
/// measures here when the text or font moved. `false` while that table is pending, with no engine
|
||||
/// installed, for the next flush to retry: the host answers the focused box's a tick later.
|
||||
/// The caret leg `0x77da80`: it restarts the blink (`0x77dd68`), fires `OnCursorChanged`, then
|
||||
/// shows the caret in the focused box (`0x77de35`) and hides it elsewhere (`0x77de51`). So a box
|
||||
/// that takes the keyboard, whose focus change raises bit 2 (`0x77afa8`), starts its caret solid.
|
||||
/// `false` while the advance table is pending, for the next flush to retry.
|
||||
fn caret(lua: &Lua, h: FrameHandle) -> bool {
|
||||
with_eb(lua, h, |eb| eb.blink_accum = 0.0);
|
||||
let placed = cursor_changed(lua, h);
|
||||
let focused = lua
|
||||
.app_data_ref::<Model>()
|
||||
.expect("model app_data")
|
||||
.focused_editbox
|
||||
== Some(h);
|
||||
with_eb(lua, h, |eb| eb.caret_shown = focused);
|
||||
placed
|
||||
}
|
||||
|
||||
/// `OnCursorChanged(x, y, w, h)` (`0x77de1b`, in UI units via `0x77dd5f`), `x` the caret's advance
|
||||
/// along its line, `y` minus the row index times the row pitch, `w` the constant 4.0 and `h` the
|
||||
/// line height. The arguments are computed only for a box with the script (`0x77dd8c`), from the
|
||||
/// box's advance table, which the installed font engine measures here when the text or font
|
||||
/// moved. `false` while that table is pending, with no engine installed: the host answers the
|
||||
/// focused box's a tick later.
|
||||
///
|
||||
/// Deviation: `h` is the row pitch the advance answer carries, where the reference passes the line
|
||||
/// height (`0x7727b0`): 0 for a single-line box, whose answer has no pitch, and for a multi-line
|
||||
/// box taller than the line height only for a font with extra spacing (none in the stock UI).
|
||||
fn caret(lua: &Lua, h: FrameHandle) -> bool {
|
||||
fn cursor_changed(lua: &Lua, h: FrameHandle) -> bool {
|
||||
let id = frame_id_of(lua, h);
|
||||
if !event::has_widget_handler(lua, id, "OnCursorChanged") {
|
||||
return true;
|
||||
@@ -77,7 +94,10 @@ fn caret(lua: &Lua, h: FrameHandle) -> bool {
|
||||
(0, eb.advances[cursor_d])
|
||||
};
|
||||
let pitch = eb.cell_h;
|
||||
Some((x, -(row as f32) * pitch, pitch))
|
||||
// `y` starts at 0 and loses one pitch per row above the caret (`0x77dae1`, `0x77db31`),
|
||||
// so row 0 reports +0, never the -0 that Lua prints as "-0".
|
||||
let y = 0.0 - row as f32 * pitch;
|
||||
Some((x, y, pitch))
|
||||
})
|
||||
.flatten();
|
||||
let Some((x, y, pitch)) = args else {
|
||||
|
||||
@@ -6,7 +6,8 @@ use crate::script::{
|
||||
};
|
||||
|
||||
/// A stand-in font engine: each byte 7 wide and each line 14 tall, answering the advance table
|
||||
/// inline as the host's engine does.
|
||||
/// inline as the host's engine does. A multi-line box's rows start after each newline, a trailing
|
||||
/// one opening an empty row, as the reference's break routine adds one (`0x5c250b`–`0x5c2522`).
|
||||
struct Mono;
|
||||
|
||||
impl TextMeasure for Mono {
|
||||
@@ -20,11 +21,12 @@ impl TextMeasure for Mono {
|
||||
req: &EditBoxAdvanceRequest,
|
||||
) -> Option<(Vec<f32>, Vec<usize>, f32)> {
|
||||
let cum = (0..=req.text.len()).map(|i| i as f32 * 7.0).collect();
|
||||
Some((
|
||||
cum,
|
||||
vec![0],
|
||||
if req.wrap_width.is_some() { 14.0 } else { 0.0 },
|
||||
))
|
||||
let Some(_) = req.wrap_width else {
|
||||
return Some((cum, vec![0], 0.0));
|
||||
};
|
||||
let breaks = req.text.match_indices('\n').map(|(i, _)| i + 1);
|
||||
let rows = std::iter::once(0).chain(breaks).collect();
|
||||
Some((cum, rows, 14.0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,3 +375,137 @@ fn a_key_flushes_the_box_before_it_acts() {
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// `HighlightText` writes only the selection (`0x77cca0`), and Ctrl+A is that call (`0x77b239`):
|
||||
/// the caret bit stays down, so no `OnCursorChanged` fires at the flush.
|
||||
#[test]
|
||||
fn highlight_text_and_ctrl_a_fire_no_on_cursor_changed() {
|
||||
let mut s = script();
|
||||
logged_box(&mut s);
|
||||
s.run(r#"E:SetText("hello") E:SetFocus()"#).unwrap();
|
||||
s.tick(0.016);
|
||||
take_log(&s);
|
||||
assert!(s.editbox_action(EditAction::Move {
|
||||
unit: EditUnit::Edge,
|
||||
back: true,
|
||||
extend: false,
|
||||
}));
|
||||
s.tick(0.016);
|
||||
assert_eq!(take_log(&s), "early update cursor@0 late");
|
||||
s.run("E:HighlightText()").unwrap();
|
||||
s.tick(0.016);
|
||||
assert!(s.editbox_action(EditAction::SelectAll));
|
||||
s.tick(0.016);
|
||||
assert_eq!(
|
||||
take_log(&s),
|
||||
"early update late early update late",
|
||||
"the caret stayed at 0"
|
||||
);
|
||||
s.char_input("X");
|
||||
assert_eq!(
|
||||
s.eval::<String>("return E:GetText()").unwrap(),
|
||||
"X",
|
||||
"and the selection is all of it"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// A focus change raises bit 2 (`0x77afa8`), and the caret leg restarts the blink (`0x77dd68`)
|
||||
/// and shows the caret in the focused box (`0x77de35`): a box that takes the keyboard back starts
|
||||
/// solid, whatever phase it lost it in.
|
||||
#[test]
|
||||
fn taking_the_keyboard_restarts_the_caret_blink() {
|
||||
let mut s = script();
|
||||
logged_box(&mut s);
|
||||
s.run(r#"E:SetText("ab") E:SetFocus()"#).unwrap();
|
||||
s.tick(0.016);
|
||||
s.tick(0.6);
|
||||
s.resolve();
|
||||
assert!(
|
||||
!s.focused_editbox_text_ui().unwrap().caret_on,
|
||||
"past the half-period: off"
|
||||
);
|
||||
s.run("E:ClearFocus()").unwrap();
|
||||
s.tick(0.3);
|
||||
s.run("E:SetFocus()").unwrap();
|
||||
s.tick(0.016);
|
||||
assert!(
|
||||
s.focused_editbox_text_ui().unwrap().caret_on,
|
||||
"the focus's flush shows it"
|
||||
);
|
||||
s.tick(0.45);
|
||||
assert!(
|
||||
s.focused_editbox_text_ui().unwrap().caret_on,
|
||||
"with a whole half-period ahead"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// UP and DOWN in a multi-line box move a row (`0x77cb20`), and `OnCursorChanged` reports it a
|
||||
/// row pitch down (`0x77db31`); a single-line box would recall history instead.
|
||||
#[test]
|
||||
fn up_and_down_move_a_multi_line_boxs_caret_by_rows() {
|
||||
let mut s = script();
|
||||
logged_box(&mut s);
|
||||
s.run(
|
||||
r#"
|
||||
E:SetScript("OnCursorChanged", function()
|
||||
table.insert(LOG, format("cursor@%d,%d", arg1, arg2))
|
||||
end)
|
||||
E:SetMultiLine(true)
|
||||
E:SetText("abc\ndefg")
|
||||
E:SetFocus()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.tick(0.016);
|
||||
take_log(&s);
|
||||
let row = |back| EditAction::Move {
|
||||
unit: EditUnit::Row,
|
||||
back,
|
||||
extend: false,
|
||||
};
|
||||
assert!(s.editbox_action(row(true)));
|
||||
s.tick(0.016);
|
||||
assert!(s.editbox_action(row(false)));
|
||||
s.tick(0.016);
|
||||
assert_eq!(
|
||||
take_log(&s),
|
||||
"early update cursor@21,0 late early update cursor@21,-14 late",
|
||||
"column 4 holds before the newline above, then comes back down to column 3"
|
||||
);
|
||||
s.char_input("|");
|
||||
assert_eq!(
|
||||
s.eval::<String>("return E:GetText()").unwrap(),
|
||||
"abc\ndef||g"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// `OnCursorChanged`'s `y` starts at 0 and loses one row pitch per row above the caret
|
||||
/// (`0x77dae1`, `0x77db31`): a single-line box, and a multi-line box's first row, report +0, which
|
||||
/// Lua prints as "0", never "-0".
|
||||
#[test]
|
||||
fn on_cursor_changed_reports_the_first_row_at_plus_zero() {
|
||||
let mut s = script();
|
||||
logged_box(&mut s);
|
||||
s.run(
|
||||
r#"
|
||||
E:SetScript("OnCursorChanged", function() table.insert(LOG, tostring(arg2)) end)
|
||||
E:SetText("ab")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(take_log(&s), "early update 0 text:ab late", "single-line");
|
||||
s.run(r#"E:SetMultiLine(true) E:SetText("a\nb")"#).unwrap();
|
||||
s.tick(0.016);
|
||||
s.run(r#"E:SetText("c")"#).unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(
|
||||
take_log(&s),
|
||||
"early update -14 text:a\nb late early update 0 text:c late",
|
||||
"multi-line, the second row and then the first"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ pub(in crate::script) fn click(lua: &Lua, id: u32, x: f32, y: f32) {
|
||||
}
|
||||
|
||||
/// Mouse move with the button held (`0x77a860`): a dragging focused box extends its selection to
|
||||
/// the hovered index through the Shift+arrow helper (`0x77cd10`).
|
||||
/// the hovered index ([`crate::widget::EditBoxState::move_caret_to`]).
|
||||
pub(in crate::script) fn drag_update(lua: &Lua, x: f32, y: f32) {
|
||||
let Some(h) = focused(lua) else { return };
|
||||
if with_eb(lua, h, |eb| eb.drag_active) != Some(true) {
|
||||
@@ -72,7 +72,7 @@ pub(in crate::script) fn drag_end(lua: &Lua) {
|
||||
}
|
||||
|
||||
/// Ctrl+Left/Right, the word move: the client walks its per-byte class array (`0x41f8f0(1)`),
|
||||
/// benilla stops at alphanumeric runs. `shift` extends the selection from its anchor.
|
||||
/// benilla stops at alphanumeric runs. `shift` extends the selection.
|
||||
pub(in crate::script) fn move_word(lua: &Lua, h: FrameHandle, right: bool, shift: bool) {
|
||||
with_eb(lua, h, |eb| eb.move_by_word(right, shift));
|
||||
}
|
||||
|
||||
@@ -6,6 +6,18 @@ fn script() -> UiScript {
|
||||
UiScript::new().expect("construct UiScript")
|
||||
}
|
||||
|
||||
/// UP and DOWN, which recall history in a single-line box.
|
||||
const UP: EditAction = EditAction::Move {
|
||||
unit: EditUnit::Row,
|
||||
back: true,
|
||||
extend: false,
|
||||
};
|
||||
const DOWN: EditAction = EditAction::Move {
|
||||
unit: EditUnit::Row,
|
||||
back: false,
|
||||
extend: false,
|
||||
};
|
||||
|
||||
fn text_quad(s: &UiScript) -> Option<String> {
|
||||
s.extract().into_iter().find_map(|q| match q.content {
|
||||
QuadContent::Text { text, .. } => text,
|
||||
@@ -291,6 +303,41 @@ fn numeric_aborts_a_mixed_insert_wholesale() {
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "12");
|
||||
}
|
||||
|
||||
/// `Insert` deletes the selection before its `numeric` test (`0x77bf13`, then `0x77bf41`): a
|
||||
/// letter typed over a numeric box's selection empties it, so `OnTextChanged` fires at the flush,
|
||||
/// while the abort comes before `OnChar` (`0x77c13c`).
|
||||
#[test]
|
||||
fn a_non_digit_typed_over_a_numeric_selection_deletes_it() {
|
||||
let mut s = script();
|
||||
s.run(
|
||||
r#"
|
||||
CHARS, CHANGES = 0, 0
|
||||
E = CreateFrame("EditBox", "E")
|
||||
E:SetNumeric(true)
|
||||
E:SetText("125")
|
||||
E:SetScript("OnChar", function() CHARS = CHARS + 1 end)
|
||||
E:SetScript("OnTextChanged", function() CHANGES = CHANGES + 1 end)
|
||||
E:SetFocus()
|
||||
E:HighlightText()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.tick(0.016);
|
||||
s.run("CHANGES = 0").unwrap();
|
||||
assert!(s.char_input("g"), "focused: consumed");
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "");
|
||||
s.tick(0.016);
|
||||
assert_eq!(
|
||||
s.eval::<(i64, i64)>("return CHARS, CHANGES").unwrap(),
|
||||
(0, 1)
|
||||
);
|
||||
assert_eq!(
|
||||
text_quad(&s).as_deref(),
|
||||
Some(""),
|
||||
"the text region follows"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_letters_trims_from_the_end() {
|
||||
let s = script();
|
||||
@@ -687,19 +734,19 @@ fn history_recall_walks_up_and_down_and_restores_the_draft() {
|
||||
)
|
||||
.unwrap();
|
||||
// UP recalls newest-first; a second UP walks older; at the oldest it holds.
|
||||
assert!(s.editbox_action(EditAction::HistoryPrev));
|
||||
assert!(s.editbox_action(UP));
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "/g two");
|
||||
s.editbox_action(EditAction::HistoryPrev);
|
||||
s.editbox_action(UP);
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "/say one");
|
||||
s.editbox_action(EditAction::HistoryPrev);
|
||||
s.editbox_action(UP);
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "/say one");
|
||||
// DOWN walks newer; past the newest the stashed live draft comes back.
|
||||
s.editbox_action(EditAction::HistoryNext);
|
||||
s.editbox_action(DOWN);
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "/g two");
|
||||
s.editbox_action(EditAction::HistoryNext);
|
||||
s.editbox_action(DOWN);
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "draft");
|
||||
// Not browsing: DOWN does nothing.
|
||||
s.editbox_action(EditAction::HistoryNext);
|
||||
s.editbox_action(DOWN);
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "draft");
|
||||
}
|
||||
|
||||
@@ -716,15 +763,15 @@ fn typing_ends_the_history_browse() {
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.editbox_action(EditAction::HistoryPrev);
|
||||
s.editbox_action(UP);
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "newer");
|
||||
// A typed char turns the recalled line into a draft: the next UP starts a fresh browse from
|
||||
// the newest entry, stashing the edited line as the draft.
|
||||
s.char_input("!");
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "newer!");
|
||||
s.editbox_action(EditAction::HistoryPrev);
|
||||
s.editbox_action(UP);
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "newer");
|
||||
s.editbox_action(EditAction::HistoryNext);
|
||||
s.editbox_action(DOWN);
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "newer!");
|
||||
}
|
||||
|
||||
@@ -743,14 +790,14 @@ fn programmatic_set_text_keeps_the_browse_and_focus_gain_resets_it() {
|
||||
.unwrap();
|
||||
// Rewrite the recalled line as the chat parser does ("/g two" → Guild + "two"); the next UP
|
||||
// must reach the older entry, not the newest again.
|
||||
s.editbox_action(EditAction::HistoryPrev);
|
||||
s.editbox_action(UP);
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "/g two");
|
||||
s.run(r#"E:SetText("two")"#).unwrap();
|
||||
s.editbox_action(EditAction::HistoryPrev);
|
||||
s.editbox_action(UP);
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "/say one");
|
||||
// Refocusing starts a fresh session: the stale walk drops, UP recalls the newest again.
|
||||
s.run(r#"E:ClearFocus() E:SetFocus()"#).unwrap();
|
||||
s.editbox_action(EditAction::HistoryPrev);
|
||||
s.editbox_action(UP);
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "/g two");
|
||||
}
|
||||
|
||||
@@ -770,11 +817,11 @@ fn history_caps_at_history_lines_drop_oldest() {
|
||||
.unwrap();
|
||||
assert_eq!(s.eval::<i64>("return E:GetHistoryLines()").unwrap(), 2);
|
||||
// Only b/c survive: two UPs land on 'b', a third holds there.
|
||||
s.editbox_action(EditAction::HistoryPrev);
|
||||
s.editbox_action(UP);
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "c");
|
||||
s.editbox_action(EditAction::HistoryPrev);
|
||||
s.editbox_action(UP);
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "b");
|
||||
s.editbox_action(EditAction::HistoryPrev);
|
||||
s.editbox_action(UP);
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "b");
|
||||
}
|
||||
|
||||
@@ -786,7 +833,7 @@ fn history_off_by_default_and_up_is_still_consumed() {
|
||||
// No historyLines: AddHistoryLine does nothing, and UP is consumed but inert, since a focused
|
||||
// box eats every key (both handlers return 1, `0x77a900`/`0x77b160`).
|
||||
s.run(r#"E:AddHistoryLine("x")"#).unwrap();
|
||||
assert!(s.editbox_action(EditAction::HistoryPrev));
|
||||
assert!(s.editbox_action(UP));
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "t");
|
||||
}
|
||||
|
||||
@@ -1242,9 +1289,17 @@ fn shift_arrow_selects_the_whole_link_and_typing_replaces_it() {
|
||||
|
||||
#[test]
|
||||
fn typing_strictly_inside_a_link_is_refused() {
|
||||
// Only the mouse can put the caret there, so place it the way a click would.
|
||||
let mut s = box_with_link("");
|
||||
s.run("E:HighlightText(35, 35)").unwrap(); // mid-"Ironfoe"
|
||||
// Only the mouse can put the caret there: a click, which stops on each visible character.
|
||||
let mut s = script();
|
||||
seam_rig(&mut s, "");
|
||||
s.run(&format!("E:SetWidth(400) E:Insert(\"{LINK}\")"))
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
answer_advances(&mut s);
|
||||
// Byte 35, mid-"Ironfoe", at 7 px per byte from the box's left edge at 100.
|
||||
assert!(s.mouse_button(100.0 + 35.0 * 7.0, 60.0, "LeftButton", true));
|
||||
s.mouse_button(100.0 + 35.0 * 7.0, 60.0, "LeftButton", false);
|
||||
assert_eq!(s.focused_editbox_text_ui().unwrap().caret_x, 245.0);
|
||||
s.char_input("x");
|
||||
assert_eq!(
|
||||
text_of(&s),
|
||||
@@ -1252,7 +1307,11 @@ fn typing_strictly_inside_a_link_is_refused() {
|
||||
"the client swallows it rather than splitting the link"
|
||||
);
|
||||
// At the link's leading edge the keystroke lands: the guard also tests the previous token.
|
||||
s.run("E:HighlightText(0, 0)").unwrap();
|
||||
s.editbox_action(EditAction::Move {
|
||||
unit: EditUnit::Edge,
|
||||
back: true,
|
||||
extend: false,
|
||||
});
|
||||
s.char_input("x");
|
||||
assert_eq!(text_of(&s), format!("x{LINK}"));
|
||||
}
|
||||
|
||||
@@ -1058,7 +1058,7 @@ impl UiScript {
|
||||
/// Whether the focused EditBox is in alt-arrow mode (XML `ignoreArrows`, Lua
|
||||
/// `SetAltArrowKeyMode`, `[editbox+0x318] & 0x10`): without ALT the reference declines the four
|
||||
/// arrows (`0x77b1c4`), so they reach the world's bindings and turn the player while chat has
|
||||
/// focus. The gate is on the key, not the [`EditAction`]: HOME and END also move to an edge.
|
||||
/// focus. The gate is on the key, not the [`EditAction`]: HOME and END also move the caret.
|
||||
pub fn editbox_alt_arrow_mode(&self) -> bool {
|
||||
let model = self.model_ref();
|
||||
model.focused_editbox.is_some_and(|h| {
|
||||
|
||||
Reference in New Issue
Block a user