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
+10 -2
View File
@@ -244,7 +244,11 @@ fn chat_box_arrows_edit_and_history_recalls() {
);
assert!(s.focus_editbox("ChatFrameEditBox"));
s.char_input("dra");
s.editbox_action(EditAction::HistoryPrev);
s.editbox_action(EditAction::Move {
unit: EditUnit::Row,
back: true,
extend: false,
});
// `ChatEdit_AddHistory` filed "/y hi" (`ChatFrame.lua:1916-1937`); the recall's `OnTextSet`
// parse (l.2077-2079) turns it back into YELL with "hi".
assert!(
@@ -258,7 +262,11 @@ fn chat_box_arrows_edit_and_history_recalls() {
s.eval::<String>("return ChatFrameEditBox:GetText()")
.unwrap()
);
s.editbox_action(EditAction::HistoryNext);
s.editbox_action(EditAction::Move {
unit: EditUnit::Row,
back: false,
extend: false,
});
assert_eq!(
s.eval::<String>("return ChatFrameEditBox:GetText()")
.unwrap(),
@@ -1,7 +1,10 @@
//! The stock GM help window (`HelpFrame.xml`): the two faces of `UPDATE_TICKET`, the ticket
//! toast and its repoll, and the status ask on show.
use benilla_ui::script::{GmTicketIntent, ScriptValue, UiScript};
use benilla_ui::script::{
EditAction, EditBoxAdvanceRequest, EditUnit, GmTicketIntent, MeasureRequest, ScriptValue,
TextMeasure, UiScript,
};
use super::test_ui::load_ui as load_xml;
@@ -237,3 +240,62 @@ fn the_ticket_text_grows_with_its_lines_and_its_scroll_frame_ranges_over_them()
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// A stand-in font engine: each byte 7 wide, each line 14 tall, a multi-line box's rows starting
/// after each newline, a trailing one opening an empty row as the reference's break routine does
/// (`0x5c250b`–`0x5c2522`); the texts here never wrap.
struct Rows;
impl TextMeasure for Rows {
fn measure(&mut self, req: &MeasureRequest) -> (f32, f32, f32) {
let natural = req.text.split('\n').map(str::len).max().unwrap_or(0) as f32 * 7.0;
(natural, req.text.split('\n').count() as f32 * 14.0, natural)
}
fn editbox_advances(
&mut self,
req: &EditBoxAdvanceRequest,
) -> Option<(Vec<f32>, Vec<usize>, f32)> {
let cum = (0..=req.text.len()).map(|i| i as f32 * 7.0).collect();
if req.wrap_width.is_none() {
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))
}
}
/// The ticket text is multi-line: UP moves the caret a row at its letter column (`0x77cb20`), and
/// Shift+HOME selects back to the line's start (`0x77c980`), not the ticket's.
#[test]
fn the_ticket_texts_caret_moves_by_rows_and_lines() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.set_text_measurer(Box::new(Rows));
s.run("ShowUIPanel(HelpFrame) HelpFrame_ShowFrame(\"OpenTicket\")")
.unwrap();
s.resolve();
s.run(
"HelpFrameOpenTicketText:SetText('Stuck in\\nthe mine') \
HelpFrameOpenTicketText:SetFocus()",
)
.unwrap();
s.tick(0.016);
s.resolve();
let mv = |s: &mut UiScript, unit, back, extend| {
assert!(s.editbox_action(EditAction::Move { unit, back, extend }));
};
// From the end, column 8: the end of "Stuck in".
mv(&mut s, EditUnit::Row, true, false);
s.char_input(",");
mv(&mut s, EditUnit::Edge, false, false);
mv(&mut s, EditUnit::Line, true, true);
s.char_input("a cave");
assert_eq!(
s.eval::<String>("return HelpFrameOpenTicketText:GetText()")
.unwrap(),
"Stuck in,\na cave"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
+1 -1
View File
@@ -285,7 +285,7 @@ pub(super) fn feed_ui_input(
}
} else if let Some(chord) = chord {
// The gate is on the key, not the action: a gated arrow never reaches the box (the
// reference's `return 0`), while HOME/END, which also make `Move { unit: Edge }`, do.
// reference's `return 0`), while HOME/END, which also make a `Move`, do.
let gated_arrow = capture.arrows_fall_through
&& matches!(
ev.key_code,