- the inspect and talent windows are the reference's own: `Blizzard_InspectUI` and `Blizzard_TalentUI` are LoadOnDemand addons whose real `.xml`/`.lua` live inside `patch.MPQ` — the loose `Interface\AddOns` folder holds only a `.pub` signature, which three notes in this tree had read as "packed, no source to migrate from", one wrong inference that kept five windows on hand-written transcriptions; 2300 lines of ours out, no engine work needed, and three things our talent transcription had quietly invented fell out (there was no level-10 gate, every tree scrolled where a shallow one does not, and a nil `TalentMicroButton` threw out of OnShow before the window painted) - a nil unit token RAISES in 53 of 83 bindings: wow-re censused the registrar table at `0x850438` — `UnitName(nil)` raises and `UnitIsVisible(nil)` is quiet, one line apart in stock files, so there is no rule to derive, the table is the rule; ours answered a default and our own test had pinned it; both token positions of the seven two-token predicates are gated, `GetRaidTargetIndex` too, and `UnitIsAFK`/`UnitIsDND` are confirmed not to exist in 5875 at all (zero occurrences under a positive control) — we invented them, and they stay deliberately ungated - `EditBox:SetNumber` did not exist (the only chain file calling it is `MoneyInputFrame.lua`, on the chain since the money kit): it is byte-identical to `SetText`, formats through `%.14g` in MSVC's CRT (`1e+020`, `1.#INF`), and `OnTextChanged` never fires from an edit — it fires from the dirty-word drain inside the box's own OnUpdate, coalesced to one per box per frame, where ours fired synchronously and `MoneyInputFrame_SetCopper` raised on it; `PanelTemplates_TabResize`'s `tab` argument is optional as the reference's is (stock macro tabs call it with `0`) - the skull or star on your CURRENT target never appeared or cleared until you re-targeted — `RAID_TARGET_UPDATE` was never fired, and stock `TargetFrame.lua` hangs its repaint off it; a raid-to-party conversion now empties the mark board as `0x4ba550` does; and the group-loot roll popups were already on the chain inside stock `LootFrame.xml` — our 546-line `GroupLootFrame.xml` had been shadowing all four of them Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
445 lines
21 KiB
Rust
445 lines
21 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 KEEPS a history browse in progress: the chat live parse
|
||
// rewrites the box on every recalled slash line ("/s hi" → Say + "hi"), and ending
|
||
// the browse there reset every UP to the newest entry — "history only goes back 1"
|
||
// (director's report, 2026-07-26). The browse ends on typed edits, AddHistoryLine,
|
||
// and focus gain (`set_focus_handle`) — the fresh-session reset.
|
||
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()))?,
|
||
)?;
|
||
// SetNumber(v) — **the same function as SetText**. `0x798690` and `0x7984c0` are byte-identical
|
||
// (245 bytes each, zero differences after masking rel32 and absolute-VA operands; only the
|
||
// usage string differs), so this does no numeric work of its own: it hands the argument to the
|
||
// shared `lua_tostring` marshalling and sets the result as text. Decision 1831.
|
||
//
|
||
// The GATE is `lua_isstring 0x6f3510`, a pure type test over {number, string} — NOT
|
||
// `lua_isnumber` and NOT `luaL_checknumber`. So a STRING is accepted and passed through
|
||
// VERBATIM, never parsed: `SetNumber("abc")` sets the text "abc" and does not raise. Anything
|
||
// else — nil, boolean, table, or an ABSENT argument — raises the usage string and abandons the
|
||
// caller's statement.
|
||
//
|
||
// The live consumer is `MoneyInputFrame.lua:47/52/57`, on the chain since 1751, whose three
|
||
// boxes are `numeric="true"`. Our numeric filter already models the reference's wholesale
|
||
// abort, which matters here: on such a box `SetNumber(-5)` or `SetNumber(0.8)` leaves the box
|
||
// EMPTY rather than partially filled, because the sign or the point fails the digit test after
|
||
// the clear-all has already run. The money path only ever passes non-negative integers
|
||
// (`floor`/`mod` results), so it never takes that branch.
|
||
m.set(
|
||
"SetNumber",
|
||
lua.create_function(|lua, (this, v): (Table, Value)| {
|
||
let text = match &v {
|
||
Value::Integer(i) => crate::script::binding_abi::lua_number_text(*i as f64),
|
||
Value::Number(n) => crate::script::binding_abi::lua_number_text(*n),
|
||
// A string is not parsed — it is the text.
|
||
Value::String(s) => s.to_str()?.to_string(),
|
||
_ => return Err(mlua::Error::runtime("Usage: EditBox:SetNumber(number)")),
|
||
};
|
||
let h = frame_handle_of(lua, &this)?;
|
||
set_text(lua, h, &text);
|
||
Ok(())
|
||
})?,
|
||
)?;
|
||
// 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))
|
||
})?,
|
||
)?;
|
||
// Insert(text) — **a nil is a no-op, not an error.** The reference's C `Insert` reads its
|
||
// argument through `lua_tostring`, which answers NULL for a nil and leaves the buffer alone;
|
||
// stock FrameXML relies on that. `LootFrame.lua:152` is the plain case:
|
||
//
|
||
// ChatFrameEditBox:Insert(GetLootSlotLink(this.slot));
|
||
//
|
||
// with no guard, on a coin row whose `GetLootSlotLink` is nil. Typed as `String`, this raised
|
||
// — which the loot window only survived while we owned the file and could add a guard the
|
||
// reference does not have. It bit the moment `LootFrame.xml` came off the player's chain
|
||
// (1751), and it would bite any addon writing the same unguarded line.
|
||
//
|
||
// A NUMBER still inserts its digits: `lua_tostring` converts one in place, and mlua's
|
||
// `Option<String>` coercion follows it.
|
||
m.set(
|
||
"Insert",
|
||
lua.create_function(|lua, (this, s): (Table, Option<String>)| {
|
||
let h = frame_handle_of(lua, &this)?;
|
||
if let Some(s) = s {
|
||
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(
|
||
// `GetMaxLetters 0x79929f` — the read half, which we were missing. Not published off the
|
||
// `strings` hit alone (that is what put `SetUnit` on the wrong widget earlier today):
|
||
// wow-re's numeric-arg round identified `0x79929f` as the GETTER of `[widget+0x340]`,
|
||
// the same field the setter below writes, which is table-level evidence rather than a
|
||
// name that happens to be in the image.
|
||
//
|
||
// Its sibling `GetMaxBytes` is in the image too and is NOT added: we do not model
|
||
// `maxBytes` at all (a separate field with a `-1` sentinel), and a getter for a field we
|
||
// do not have would answer confidently with a number that means nothing.
|
||
"GetMaxLetters",
|
||
lua.create_function(|lua, this: Table| {
|
||
with_editbox(lua, &this, |eb| eb.max_letters as i64)
|
||
})?,
|
||
)?;
|
||
m.set(
|
||
// `SetMaxLetters 0x799110` — one of only FOUR widget bindings in the whole registrar that
|
||
// calls `lua_gettop` (wow-re `numeric-arg-coercion-law.md` Q3), and its gate is EXACT:
|
||
// `cmp eax,2`. So the count is checked and the type is not, which is the opposite of the
|
||
// usual pairing and the reason this needs its own body:
|
||
//
|
||
// SetMaxLetters() -> RAISES `Usage:` (too few)
|
||
// SetMaxLetters(50, 60) -> RAISES `Usage:` (too MANY — an exact gate, not a minimum)
|
||
// SetMaxLetters(nil) -> completes, stores 0
|
||
// SetMaxLetters("12") -> completes, stores 12 (a numeric string coerces)
|
||
// SetMaxLetters({}) -> completes, stores 0
|
||
//
|
||
// and **0 is "no limit"**, not "no letters": the insert path's trim block is skipped
|
||
// whole on zero (`0x77c085 test edi,edi; je`), which is what `max_letters == 0` already
|
||
// means here. So `SetMaxLetters(nil)` is `SetMaxLetters(0)` is unlimited — aux-addon's
|
||
// `gui/core.lua:288` writes exactly that, and benilla raised on it and killed the addon at
|
||
// load. (Its neighbour `SetMaxBytes` uses **-1** for the same idea; two adjacent fields,
|
||
// two different sentinels.)
|
||
"SetMaxLetters",
|
||
lua.create_function(|lua, (this, args): (Table, mlua::MultiValue)| {
|
||
let args: Vec<Value> = args.into_iter().collect();
|
||
if args.len() != 1 {
|
||
return Err(mlua::Error::runtime(
|
||
"Usage: <unnamed>:SetMaxLetters(maxLetters)",
|
||
));
|
||
}
|
||
let n = crate::script::binding_abi::coerced_number(lua, args.first().cloned());
|
||
// `__ftol` truncates toward zero; a negative stores as itself there, but our field is
|
||
// a `usize` and the trim only ever tests `> 0`, so the two agree on every value that
|
||
// can change behaviour.
|
||
with_editbox(lua, &this, |eb| eb.max_letters = (n as i64).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 LETTER count — `0x7992c0` walks the class array (`0x77bc80`), which
|
||
// counts classes 2, 3 and 6 only, so escapes are free and a 43-byte item link reports 9
|
||
// (decision 1077). Not bytes, and not chars either.
|
||
m.set(
|
||
"GetNumLetters",
|
||
lua.create_function(|lua, this: Table| {
|
||
with_editbox(lua, &this, |eb| {
|
||
crate::markup::ClassMap::new(&eb.text).num_letters() as i64
|
||
})
|
||
})?,
|
||
)?;
|
||
|
||
// The config flags. All four mirror the live API. The fifth flag — bit4, the XML's
|
||
// `ignoreArrows` — is NOT here: its Lua surface is `SetAltArrowKeyMode`/`GetAltArrowKeyMode`
|
||
// below, which take a different argument coercion and answer a number rather than a boolean,
|
||
// so it cannot share this loop. benilla used to register a `SetIgnoreArrows` "convenience"
|
||
// for the loader to drive; 5875 has no such method (the 48-entry table
|
||
// `[0x87bb68, 0x87bce8)` carries no entry whose name contains "Ignore"), so publishing it was
|
||
// exactly the error decision 1189 names — and it stood in for the two real names, which were
|
||
// missing. The loader drives the XML attribute through `SetAltArrowKeyMode` now.
|
||
// ── SetAltArrowKeyMode / GetAltArrowKeyMode (`0x7996e0` / `0x799790`) ────────────────────
|
||
//
|
||
// **The setter's argument is `GetBoolOrDefault(L, 2, default = 1)`** (`0x6f1c10`), not Lua
|
||
// truthiness and not a plain numeric coercion — `0x7996e0` pushes the default `1` before the
|
||
// call, so **an absent argument ENABLES** the mode. `nil` disables; a number goes through
|
||
// `__ftol` so `0` and `0.5` are false and `-1` is true; `""` matches nothing in the
|
||
// off/disabled/on/enabled chain and falls to the default, so it ENABLES; `"0"` disables.
|
||
// [`crate::script::binding_abi::bool_or_default`] already models every arm.
|
||
//
|
||
// **The getter answers the NUMBER 1 or nil**, never a boolean (`0x799815`: the set arm pushes
|
||
// the double 1.0 via `0x6f3810`, the clear arm pushes nil via `0x6f37f0`) — the same idiom as
|
||
// `IsShiftKeyDown`. An addon writing `if box:GetAltArrowKeyMode() then` reads either the same;
|
||
// one writing `== 1` only reads the number.
|
||
m.set(
|
||
"SetAltArrowKeyMode",
|
||
lua.create_function(|lua, (this, args): (Table, mlua::MultiValue)| {
|
||
// `MultiValue`, not `Value`, because the reference DISTINGUISHES absent from nil here
|
||
// and mlua's `Value` cannot: `0x7996e0` pushes the default `1` before the call, so
|
||
// `SetAltArrowKeyMode()` enables while `SetAltArrowKeyMode(nil)` disables.
|
||
let args: Vec<Value> = args.into_iter().collect();
|
||
let on = crate::script::binding_abi::bool_or_default(args.first(), true);
|
||
with_editbox(lua, &this, |eb| eb.alt_arrow_key_mode = on)?;
|
||
Ok(())
|
||
})?,
|
||
)?;
|
||
m.set(
|
||
"GetAltArrowKeyMode",
|
||
lua.create_function(|lua, this: Table| {
|
||
let on = with_editbox(lua, &this, |eb| eb.alt_arrow_key_mode)?;
|
||
Ok(if on { Value::Integer(1) } else { Value::Nil })
|
||
})?,
|
||
)?;
|
||
|
||
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(())
|
||
})?,
|
||
)?;
|
||
}
|
||
|
||
install_font_block(lua, &m)?;
|
||
|
||
lua.set_named_registry_value(REG_EDITBOX_METHODS, m)?;
|
||
Ok(())
|
||
}
|
||
|
||
/// **The font block — entries #0–#15 of the EditBox method table**, and the largest single gap the
|
||
/// per-kind widget-method census found in the 218-addon corpus (decision 1229).
|
||
///
|
||
/// `EditBox` is one of the six text-bearing types, so it re-declares the whole font block in its own
|
||
/// flat table (`.data 0x87bb68`, 48 entries, count read from `mov edx,0x30` at `0x799ab5`; there is
|
||
/// no `FontInstance` class in the 1.12 Lua chain). We had written every one of these verbs already —
|
||
/// on `FontString` and on the `<Font>` object — and never wired them to the kind that wanted them.
|
||
/// The census row is `63 EditBox:SetFontObject (on Texture, FontString)`, and its `(on …)` tail is
|
||
/// exactly that: *a verb we have, on the wrong kinds*.
|
||
///
|
||
/// **The 63 is one library, not 63 independent addons.** Every one of those call sites is the same
|
||
/// three lines of an embedded `Dewdrop-2.0.lua` —
|
||
/// ```lua
|
||
/// local editBox = CreateFrame("EditBox", nil, editBoxFrame)
|
||
/// editBoxFrame.editBox = editBox
|
||
/// editBox:SetFontObject(ChatFontNormal)
|
||
/// ```
|
||
/// — vendored into 63 addon folders (65 copies of the file: `FuBar` and its ~50 plugins, `BigWigs`,
|
||
/// `AtlasLoot`, `oRA2`, …). One library replicated, so 63 chances to hit the *same* next wall
|
||
/// (decision 1207).
|
||
///
|
||
/// Ten of the sixteen are the shared block and come from [`super::super::font_block`], which carries
|
||
/// the per-verb byte evidence and the return-shape traps. Two are deliberately absent and four are
|
||
/// installed here:
|
||
///
|
||
/// - **`SetSpacing`/`GetSpacing` (#10–#11) are NOT installed.** Nothing in this client models line
|
||
/// spacing, so they could only store a number no renderer reads — the silently-ignored-setter
|
||
/// failure of 1203/1205/1211 — while their absence raises a nil-value call that names itself.
|
||
/// Corpus demand is zero: `:SetSpacing(`/`:GetSpacing(` appear in **0** of 218 addons.
|
||
/// `script::font` withholds the same pair for the same reason.
|
||
/// - **The justify four (#12–#15) are installed here**, against the box's own
|
||
/// [`EditBoxState::justify`] word rather than its text region — the field's doc has the why (our
|
||
/// editbox draw law seats that region LEFT unconditionally) and states the divergence.
|
||
fn install_font_block(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||
// Every font method acts on the box's implicit FontString: each binding's shim loads
|
||
// `[this+0x324]` and hands it to the shared implementation, and that offset is
|
||
// `EditBoxState::text_region`. Created on demand, exactly like every other text-touching path.
|
||
crate::script::font_block::install(
|
||
lua,
|
||
m,
|
||
|lua, this| {
|
||
let h = frame_handle_of(lua, this)?;
|
||
super::ensure_text_region(lua, h).ok_or_else(|| mlua::Error::runtime("not an EditBox"))
|
||
},
|
||
"EditBox",
|
||
)?;
|
||
|
||
// SetJustifyH("LEFT"|"CENTER"|"RIGHT") / SetJustifyV("TOP"|"MIDDLE"|"BOTTOM") → 0 values.
|
||
//
|
||
// Two verified traps, both of which a plausible implementation gets wrong. This table was the
|
||
// only one that got them right; the FontString and `<Font>` copies coerced anything unknown to
|
||
// CENTER/MIDDLE until 1237 lifted this law into [`crate::justify`], which all three now share:
|
||
// · an **unrecognised token RAISES** `Usage: %s:SetJustifyH("justify")` (`0x87c77c`), rather
|
||
// than falling back to a default;
|
||
// · a token from the **other axis** parses fine and then masks to nothing — `SetJustifyH("TOP")`
|
||
// yields 0x08, `0x08 & 0x07 == 0`, so it CLEARS justifyH and `GetJustifyH()` answers
|
||
// `"UNKNOWN"`. No error either way.
|
||
//
|
||
// `AceGUIWidget-Slider.lua:210` (`editbox:SetJustifyH("CENTER")`, three addons) is the demand.
|
||
for (name, mask) in [
|
||
("SetJustifyH", EditBoxState::JUSTIFY_H_MASK),
|
||
("SetJustifyV", EditBoxState::JUSTIFY_V_MASK),
|
||
] {
|
||
m.set(
|
||
name,
|
||
lua.create_function(move |lua, (this, token): (Table, String)| {
|
||
let bits = EditBoxState::justify_bit(&token).ok_or_else(|| {
|
||
mlua::Error::runtime(format!("Usage: <EditBox>:{name}(\"justify\")"))
|
||
})?;
|
||
with_editbox(lua, &this, |eb| eb.set_justify_axis(mask, bits))
|
||
})?,
|
||
)?;
|
||
}
|
||
// GetJustifyH()/GetJustifyV() → exactly 1 string, the first set bit's token in the reference's
|
||
// own table order, or the literal "UNKNOWN".
|
||
for (name, mask) in [
|
||
("GetJustifyH", EditBoxState::JUSTIFY_H_MASK),
|
||
("GetJustifyV", EditBoxState::JUSTIFY_V_MASK),
|
||
] {
|
||
m.set(
|
||
name,
|
||
lua.create_function(move |lua, this: Table| {
|
||
with_editbox(lua, &this, |eb| eb.justify_token(mask))
|
||
})?,
|
||
)?;
|
||
}
|
||
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; 4] {
|
||
[
|
||
("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),
|
||
]
|
||
}
|