Key input follows the client's walks: key-down (`0x765f10`), char (`0x765df0`), key-up to the recorded consumer (`0x765fd0`) and repeat (`0x766070`). - EditBoxes are in the walk from creation (`0x779ced`), so the stock login's boxes take typing; only a focused box consumes. A box declines PRINTSCREEN and unaltered arrows on an `ignoreArrows` box, so stack-split steps under a focused chat box. A binding's press records the world frame. - Repeats follow the pressed-key list, so a held key resumes after alt-tab. Chords reach a box only through the walk. Keys the client can't name walk nowhere. - The stock login reads the agreements before its first screen. Cinematics returns to login (no movie player; Deviation). The Realmlist box has its middle border. - Alt+F4 closes the window on Windows and Linux (Deviation: the reference relies on the OS). Fixes #179 Fixes #1003 Fixes #1004 Fixes #1005 Fixes #1019 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
170 lines
6.5 KiB
Rust
170 lines
6.5 KiB
Rust
//! Keys on the stock glue screens, end to end: the host's key and char events into the stock
|
|
//! GlueXML in the glue VM.
|
|
|
|
use benilla_ui::script::{GlueNotice, NoticeState, UiScript};
|
|
|
|
use super::glue::{load_glue, set_glue_screen, GlueScreen};
|
|
|
|
/// The stock glue VM on `screen` with both agreements accepted, as a returning player's is.
|
|
fn glue_on(screen: GlueScreen) -> UiScript {
|
|
let mut s = UiScript::new_glue().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
let accepted = NoticeState {
|
|
accepted: true,
|
|
show_notice: false,
|
|
};
|
|
for notice in [GlueNotice::Eula, GlueNotice::Tos] {
|
|
s.set_glue_notice(notice, accepted);
|
|
}
|
|
let _ = load_glue(&mut s);
|
|
set_glue_screen(&mut s, screen);
|
|
s.resolve();
|
|
let _ = s.take_errors();
|
|
s
|
|
}
|
|
|
|
/// One typed key as the host feeds it (`ui_script::input`): the key-down by name, then its char.
|
|
fn type_key(s: &mut UiScript, name: &str, text: &str) {
|
|
s.frame_key_input(name);
|
|
s.char_input(text);
|
|
s.key_up(name);
|
|
}
|
|
|
|
fn text(s: &UiScript, frame: &str) -> String {
|
|
s.eval::<String>(&format!("return {frame}:GetText() or ''"))
|
|
.unwrap()
|
|
}
|
|
|
|
/// The login boxes take the keys typed into them. `AccountLogin` is keyboard-enabled with an
|
|
/// `OnChar` and an `OnKeyDown`, and walks ahead of any box that is not itself in the walk; the
|
|
/// EditBox ctor enables key input (`0x779ced`, `0x779cf8`) so the focused box answers at its own
|
|
/// level above its parent.
|
|
#[test]
|
|
fn the_stock_login_boxes_take_typed_keys() {
|
|
let _ = benilla_formats::wow_data_or_skip!();
|
|
let mut s = glue_on(GlueScreen::Login);
|
|
assert!(
|
|
s.eval::<bool>("return AccountLoginUI:IsShown() == 1")
|
|
.unwrap(),
|
|
"accepted agreements put the login boxes up"
|
|
);
|
|
s.run("AccountLoginAccountEdit:SetFocus()").unwrap();
|
|
for (name, c) in [("P", "p"), ("R", "r"), ("O", "o"), ("B", "b"), ("E", "e")] {
|
|
type_key(&mut s, name, c);
|
|
}
|
|
assert_eq!(text(&s, "AccountLoginAccountEdit"), "probe");
|
|
|
|
// Tab moves to the password box (`AccountLoginAccountEdit`'s `OnTabPressed`), which then types.
|
|
assert!(s.key_input("TAB"));
|
|
assert_eq!(
|
|
s.focused_editbox_name().as_deref(),
|
|
Some("AccountLoginPasswordEdit")
|
|
);
|
|
for (name, c) in [("S", "s"), ("E", "e"), ("C", "c")] {
|
|
type_key(&mut s, name, c);
|
|
}
|
|
assert_eq!(text(&s, "AccountLoginPasswordEdit"), "sec");
|
|
assert_eq!(text(&s, "AccountLoginAccountEdit"), "probe");
|
|
assert!(s.take_errors().is_empty());
|
|
}
|
|
|
|
/// The login screen's Cinematics button shows `MovieFrame`; no movie plays here, so
|
|
/// `StartMovie` answers nil and the stock `MovieFrame_PlayMovie` hides the frame, whose `OnHide`
|
|
/// puts the login screen back and the pointer with it (`MovieFrame.lua:18-23,40-44`).
|
|
#[test]
|
|
fn the_cinematics_button_comes_back_to_the_login_screen() {
|
|
let _ = benilla_formats::wow_data_or_skip!();
|
|
let mut s = glue_on(GlueScreen::Login);
|
|
s.register_cvars([("movieSubtitle", "1"), ("gxResolution", "1024x768")]);
|
|
s.run("AccountLoginCinematicsButton:Click()").unwrap();
|
|
s.resolve();
|
|
let (movie, login, cursor_hidden) = (
|
|
s.eval::<bool>("return MovieFrame:IsShown() == 1").unwrap(),
|
|
s.eval::<bool>("return AccountLoginUI:IsVisible() == 1")
|
|
.unwrap(),
|
|
s.glue_cursor_hidden(),
|
|
);
|
|
assert_eq!(
|
|
(movie, login, cursor_hidden),
|
|
(false, true, false),
|
|
"the movie frame is gone, the login boxes are back and the pointer shows"
|
|
);
|
|
assert_eq!(s.take_errors(), Vec::<String>::new());
|
|
}
|
|
|
|
/// A frame whose only key script is `OnKeyUp` takes the key-down and runs its `OnKeyUp` on the
|
|
/// release (`0x76b7d0`, `0x765fd0`, `0x76bba0`): Escape on a playing movie hides the frame
|
|
/// (`MovieFrame_OnKeyUp`), which returns to login.
|
|
#[test]
|
|
fn escape_on_a_playing_movie_returns_to_login_on_the_key_up() {
|
|
let _ = benilla_formats::wow_data_or_skip!();
|
|
let mut s = glue_on(GlueScreen::Login);
|
|
s.register_cvars([("movieSubtitle", "0"), ("gxResolution", "1024x768")]);
|
|
// A movie that starts, as the reference's player would.
|
|
s.run("MovieFrame.StartMovie = function() return 1 end SetGlueScreen('movie')")
|
|
.unwrap();
|
|
s.resolve();
|
|
assert!(s.eval::<bool>("return MovieFrame:IsShown() == 1").unwrap());
|
|
assert!(s.glue_cursor_hidden(), "a playing movie hides the pointer");
|
|
|
|
assert!(
|
|
s.frame_key_input("SPACE"),
|
|
"the movie swallows the key-down"
|
|
);
|
|
assert!(
|
|
s.eval::<bool>("return MovieFrame:IsShown() == 1").unwrap(),
|
|
"…and nothing happens until the key is up"
|
|
);
|
|
assert!(s.key_input("ESCAPE"));
|
|
assert!(s.eval::<bool>("return MovieFrame:IsShown() == 1").unwrap());
|
|
s.key_up("ESCAPE");
|
|
s.resolve();
|
|
assert!(!s.eval::<bool>("return MovieFrame:IsShown() == 1").unwrap());
|
|
assert!(s
|
|
.eval::<bool>("return AccountLoginUI:IsVisible() == 1")
|
|
.unwrap());
|
|
assert!(!s.glue_cursor_hidden());
|
|
assert_eq!(s.take_errors(), Vec::<String>::new());
|
|
}
|
|
|
|
/// The Realmlist dialog widens the stock edit box, whose border is two 75-pixel caps that meet at
|
|
/// its own 130 pixels: while it is wide the stretched middle fills the gap between the caps, as the
|
|
/// chat box's does (`ChatFrame.xml:41-55`), and the stock box comes back untouched.
|
|
#[test]
|
|
fn the_widened_realmlist_box_has_a_border_between_its_caps() {
|
|
let _ = benilla_formats::wow_data_or_skip!();
|
|
let mut s = glue_on(GlueScreen::Login);
|
|
s.run("BENILLA_REALMLIST = 'logon.example.org' BenillaRealmlistButton:Click()")
|
|
.unwrap();
|
|
s.resolve();
|
|
let span = |s: &UiScript| {
|
|
s.eval::<(f64, f64, f64, f64)>(
|
|
"return GlueDialogEditBox:GetLeft(), GlueDialogEditBox:GetRight(), \
|
|
BenillaRealmlist_Middle:GetLeft(), BenillaRealmlist_Middle:GetRight()",
|
|
)
|
|
.unwrap()
|
|
};
|
|
let (box_left, box_right, mid_left, mid_right) = span(&s);
|
|
assert_eq!(box_right - box_left, 300.0);
|
|
assert_eq!(
|
|
(mid_left - box_left, box_right - mid_right),
|
|
(65.0, 65.0),
|
|
"the middle starts where the left cap ends and ends where the right one starts"
|
|
);
|
|
assert!(s
|
|
.eval::<bool>("return BenillaRealmlist_Middle:IsShown() == 1")
|
|
.unwrap());
|
|
|
|
s.run("GlueDialogButton2:Click()").unwrap();
|
|
s.resolve();
|
|
assert!(!s
|
|
.eval::<bool>("return BenillaRealmlist_Middle:IsShown() == 1")
|
|
.unwrap());
|
|
assert_eq!(
|
|
s.eval::<f64>("return GlueDialogEditBox:GetWidth()")
|
|
.unwrap(),
|
|
130.0
|
|
);
|
|
assert_eq!(s.take_errors(), Vec::<String>::new());
|
|
}
|