A from-scratch World of Warcraft 1.12.1 client in Rust + Bevy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
222 lines
7.7 KiB
Rust
222 lines
7.7 KiB
Rust
//! The EditBox Lua method surface — `install` builds the method table
|
|
//! (`REG_EDITBOX_METHODS`) the dispatcher consults before the shared frame table for EditBox
|
|
//! frames: text/cursor/selection accessors, focus, history, the config setters, and the blink
|
|
//! dial. Every method routes through the mother module's primitives (`set_text`,
|
|
//! `highlight_text`, …), so the byte-verified law lives exactly once.
|
|
|
|
use mlua::{Lua, Table, Value};
|
|
|
|
use crate::script::object::frame_handle_of;
|
|
use crate::script::Model;
|
|
use crate::widget::EditBoxState;
|
|
|
|
use super::{
|
|
clear_focus_handle, highlight_text, insert, set_focus_handle, set_text, set_text_insets,
|
|
with_eb, REG_EDITBOX_METHODS,
|
|
};
|
|
|
|
/// Run `f` over a frame's EditBox state for a Lua method call; errors if `this` is not a live EditBox.
|
|
fn with_editbox<T>(
|
|
lua: &Lua,
|
|
this: &Table,
|
|
f: impl FnOnce(&mut EditBoxState) -> T,
|
|
) -> mlua::Result<T> {
|
|
let h = frame_handle_of(lua, this)?;
|
|
with_eb(lua, h, f).ok_or_else(|| mlua::Error::runtime("not an EditBox"))
|
|
}
|
|
|
|
pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
|
let m = lua.create_table()?;
|
|
|
|
m.set(
|
|
"SetText",
|
|
lua.create_function(|lua, (this, s): (Table, Option<String>)| {
|
|
let h = frame_handle_of(lua, &this)?;
|
|
// Programmatic SetText ends a history browse (the engine-internal recall path calls
|
|
// `set_text` directly and keeps its browse position).
|
|
with_eb(lua, h, EditBoxState::end_history_browse);
|
|
set_text(lua, h, &s.unwrap_or_default());
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
m.set(
|
|
"GetText",
|
|
lua.create_function(|lua, this: Table| with_editbox(lua, &this, |eb| eb.text.clone()))?,
|
|
)?;
|
|
// GetNumber: atof of the real text (0 on failure), matching `0x798790`.
|
|
m.set(
|
|
"GetNumber",
|
|
lua.create_function(|lua, this: Table| {
|
|
let text = with_editbox(lua, &this, |eb| eb.text.clone())?;
|
|
Ok(text.trim().parse::<f64>().unwrap_or(0.0))
|
|
})?,
|
|
)?;
|
|
m.set(
|
|
"Insert",
|
|
lua.create_function(|lua, (this, s): (Table, String)| {
|
|
let h = frame_handle_of(lua, &this)?;
|
|
insert(lua, h, &s, true);
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
|
|
m.set(
|
|
"SetFocus",
|
|
lua.create_function(|lua, this: Table| {
|
|
let h = frame_handle_of(lua, &this)?;
|
|
set_focus_handle(lua, h);
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
m.set(
|
|
"ClearFocus",
|
|
lua.create_function(|lua, this: Table| {
|
|
let h = frame_handle_of(lua, &this)?;
|
|
clear_focus_handle(lua, h);
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
m.set(
|
|
"HasFocus",
|
|
lua.create_function(|lua, this: Table| {
|
|
let h = frame_handle_of(lua, &this)?;
|
|
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
|
Ok(model.focused_editbox == Some(h))
|
|
})?,
|
|
)?;
|
|
|
|
// HighlightText([start [, end]]) — defaults (0, -1) = select-all.
|
|
m.set(
|
|
"HighlightText",
|
|
lua.create_function(
|
|
|lua, (this, start, end): (Table, Option<i64>, Option<i64>)| {
|
|
let h = frame_handle_of(lua, &this)?;
|
|
highlight_text(lua, h, start.unwrap_or(0), end.unwrap_or(-1));
|
|
Ok(())
|
|
},
|
|
)?,
|
|
)?;
|
|
|
|
m.set(
|
|
"SetMaxLetters",
|
|
lua.create_function(|lua, (this, n): (Table, i64)| {
|
|
with_editbox(lua, &this, |eb| eb.max_letters = n.max(0) as usize)
|
|
})?,
|
|
)?;
|
|
// The submitted-line history (`historyLines`): FrameXML pushes each sent line
|
|
// (ChatEdit_AddHistory), UP/DOWN recall it (see `key_input`).
|
|
m.set(
|
|
"AddHistoryLine",
|
|
lua.create_function(|lua, (this, line): (Table, Option<String>)| {
|
|
with_editbox(lua, &this, |eb| {
|
|
eb.add_history_line(line.as_deref().unwrap_or(""));
|
|
})
|
|
})?,
|
|
)?;
|
|
m.set(
|
|
"SetHistoryLines",
|
|
lua.create_function(|lua, (this, n): (Table, i64)| {
|
|
with_editbox(lua, &this, |eb| {
|
|
eb.history_max = n.max(0) as usize;
|
|
let over = eb.history.len().saturating_sub(eb.history_max);
|
|
if over > 0 {
|
|
eb.history.drain(..over);
|
|
}
|
|
})
|
|
})?,
|
|
)?;
|
|
m.set(
|
|
"GetHistoryLines",
|
|
lua.create_function(|lua, this: Table| {
|
|
with_editbox(lua, &this, |eb| eb.history_max as i64)
|
|
})?,
|
|
)?;
|
|
// SetBlinkSpeed/GetBlinkSpeed — the caret half-period (`E+0x370`, XML `blinkSpeed`; ctor 0.5).
|
|
m.set(
|
|
"SetBlinkSpeed",
|
|
lua.create_function(|lua, (this, s): (Table, f32)| {
|
|
with_editbox(lua, &this, |eb| eb.blink_period = s)
|
|
})?,
|
|
)?;
|
|
m.set(
|
|
"GetBlinkSpeed",
|
|
lua.create_function(|lua, this: Table| with_editbox(lua, &this, |eb| eb.blink_period))?,
|
|
)?;
|
|
m.set(
|
|
"SetTextInsets",
|
|
lua.create_function(
|
|
|lua,
|
|
(this, l, r, t, b): (
|
|
Table,
|
|
Option<f32>,
|
|
Option<f32>,
|
|
Option<f32>,
|
|
Option<f32>,
|
|
)| {
|
|
let h = frame_handle_of(lua, &this)?;
|
|
set_text_insets(
|
|
lua,
|
|
h,
|
|
l.unwrap_or(0.0),
|
|
r.unwrap_or(0.0),
|
|
t.unwrap_or(0.0),
|
|
b.unwrap_or(0.0),
|
|
);
|
|
Ok(())
|
|
},
|
|
)?,
|
|
)?;
|
|
m.set(
|
|
"GetTextInsets",
|
|
lua.create_function(|lua, this: Table| {
|
|
let ins = with_editbox(lua, &this, |eb| eb.text_insets)?;
|
|
Ok((ins[0], ins[1], ins[2], ins[3]))
|
|
})?,
|
|
)?;
|
|
// GetNumLetters: the CHARACTER count (not bytes) — `0x7992c0` walks the class array.
|
|
m.set(
|
|
"GetNumLetters",
|
|
lua.create_function(|lua, this: Table| {
|
|
with_editbox(lua, &this, |eb| eb.text.chars().count() as i64)
|
|
})?,
|
|
)?;
|
|
|
|
// The config flags. SetAutoFocus/SetNumeric/SetPassword/SetMultiLine mirror the live API;
|
|
// SetIgnoreArrows has no client method (the flag is XML-only) — a benilla convenience so the
|
|
// loader can drive every flag through one method surface.
|
|
for (name, set) in flag_setters() {
|
|
let refresh_justify = name == "SetMultiLine";
|
|
m.set(
|
|
name,
|
|
lua.create_function(move |lua, (this, v): (Table, Value)| {
|
|
let on = !matches!(v, Value::Nil | Value::Boolean(false));
|
|
with_editbox(lua, &this, |eb| set(eb, on))?;
|
|
if refresh_justify {
|
|
// The text-anchoring law reads multiLine (TOP vs MIDDLE), and the loader
|
|
// wires the declared `<FontString>` BEFORE the editbox flags (LoadXML order
|
|
// 5·b vs 5b) — re-seat an already-wired region so `multiLine="true"` lands.
|
|
super::refresh_text_region_justify(lua, &this)?;
|
|
}
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
}
|
|
|
|
lua.set_named_registry_value(REG_EDITBOX_METHODS, m)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// A `Set<Flag>` name paired with the EditBoxState mutator it drives (Lua truthiness).
|
|
type FlagSetter = (&'static str, fn(&mut EditBoxState, bool));
|
|
|
|
/// The config-flag setters, in one place so the loader and the Lua surface share the list.
|
|
fn flag_setters() -> [FlagSetter; 5] {
|
|
[
|
|
("SetAutoFocus", |eb, on| eb.auto_focus = on),
|
|
("SetNumeric", |eb, on| eb.numeric = on),
|
|
("SetPassword", |eb, on| eb.password = on),
|
|
("SetMultiLine", |eb, on| eb.multi_line = on),
|
|
("SetIgnoreArrows", |eb, on| eb.ignore_arrows = on),
|
|
]
|
|
}
|