diff --git a/crates/benilla-ui/src/script/compat/classic_api/frame.rs b/crates/benilla-ui/src/script/compat/classic_api/frame.rs index 56b2e59c..112c68e3 100644 --- a/crates/benilla-ui/src/script/compat/classic_api/frame.rs +++ b/crates/benilla-ui/src/script/compat/classic_api/frame.rs @@ -124,6 +124,46 @@ end end "#; +/// Button-only: `SetEnabled(enabled)` over 1.12's own `Enable`/`Disable` pair (`0x780160`, +/// `0x7801b0`), which retail replaced with the one setter. Every later-era addon writes the +/// setter, and writes it **unguarded** on its own controls even where it guards on someone +/// else's: `Cinematic/Options.lua:4176` is a tick's own `Refresh`, greying it when a rule owns +/// the row. The raise there unwound `RefreshOptions` → `SettingsChanged` → `SetProfile` → +/// `Welcome.lua:43`, so picking a profile from the addon's welcome window applied the profile and +/// then abandoned the panel refresh, with the traceback in chat. +/// +/// Button's table only. A CheckButton reaches it down the same chain the stock pair travels +/// (`object.rs`: CheckButton → Button), and a Slider answers none of the trio on purpose +/// (`tests/slider.rs`) — addons duck-type on it to tell a control from a bar, so a Slider must +/// not start answering this either. +const ENABLED_DELEGATES: &str = r#" +return function(m) +function m.SetEnabled(self, enabled) + if enabled then self:Enable() else self:Disable() end +end +end +"#; + +/// EditBox-only: the same setter for a box, which 1.12 gives no enabled state at all — there is +/// no `Enable`/`Disable` pair on `REG_EDITBOX_METHODS` to compose, because the reference has +/// none. What a disabled box *is* for a caller is a box that takes no input, so that is what this +/// writes: the mouse and keyboard the frame already owns, plus dropping focus on the way down so +/// a box disabled while being typed into does not keep the caret. +/// +/// Deviation, stated rather than faked: retail also greys the text through the box's disabled +/// font, and 1.12's EditBox has no such slot, so a disabled box here reads as live and simply +/// refuses input. `Cinematic/Options.lua:1629` is the call that needs it — the ignore-list page's +/// entry box, greyed with its Add button when the feature above it is off. +const EDITBOX_ENABLED_DELEGATES: &str = r#" +return function(m) +function m.SetEnabled(self, enabled) + self:EnableMouse(enabled and true or false) + self:EnableKeyboard(enabled and true or false) + if not enabled then self:ClearFocus() end +end +end +"#; + /// FontString-only: `SetFormattedText` is `SetText(format(...))`. 1.12's `string.format` is the /// `format` global here, as `lua50.rs` leaves it. const FONTSTRING_DELEGATES: &str = r#" @@ -224,6 +264,16 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { } delegate(lua, REG_TEXTURE_METHODS, TEXTURE_DELEGATES)?; delegate(lua, REG_FONTSTRING_METHODS, FONTSTRING_DELEGATES)?; + delegate( + lua, + crate::script::button::REG_BUTTON_METHODS, + ENABLED_DELEGATES, + )?; + delegate( + lua, + crate::script::editbox::REG_EDITBOX_METHODS, + EDITBOX_ENABLED_DELEGATES, + )?; // `GetStringHeight()`: the laid-out height of the current text — wrapped to the declared // width, so a three-line paragraph answers three lines tall, where the font's own height @@ -714,6 +764,50 @@ mod tests { assert_eq!(s.eval::("return f:IsShown()").unwrap(), 1); } + #[test] + fn set_enabled_drives_the_stock_enable_and_disable_pair() { + let s = UiScript::new().unwrap(); + s.run( + r#" + b = CreateFrame("Button", "CAEnableBtn") + chk = CreateFrame("CheckButton", "CAEnableChk") + box = CreateFrame("EditBox", "CAEnableBox") + "#, + ) + .unwrap(); + + // The stock predicate is the NUMBER 1 / the NUMBER 0, and stays so. + s.run("b:SetEnabled(false)").unwrap(); + assert_eq!(s.eval::("return b:IsEnabled()").unwrap(), 0); + s.run("b:SetEnabled(true)").unwrap(); + assert_eq!(s.eval::("return b:IsEnabled()").unwrap(), 1); + + // A CheckButton reaches it down Button's chain, which is the one Cinematic calls it on + // (`Options.lua:4176`, a tick's own Refresh). + s.run("chk:SetEnabled(false)").unwrap(); + assert_eq!(s.eval::("return chk:IsEnabled()").unwrap(), 0); + + // An EditBox has no pair to compose: it answers the setter by refusing input. + s.run("box:SetEnabled(false)").unwrap(); + assert_eq!( + s.eval::>("return box:IsMouseEnabled()") + .unwrap(), + None, + "a disabled box takes no mouse" + ); + s.run("box:SetEnabled(true)").unwrap(); + assert_eq!(s.eval::("return box:IsMouseEnabled()").unwrap(), 1); + + // A Slider answers neither the trio nor this: addons duck-type on them. + s.run(r#"sl = CreateFrame("Slider", "CAEnableSlider")"#) + .unwrap(); + assert_eq!( + s.eval::("return type(sl.SetEnabled)").unwrap(), + "nil", + "a Slider is not a control" + ); + } + #[test] fn the_geometry_four_compose_the_stock_getters_on_a_frame_and_a_region() { let s = UiScript::new().unwrap(); diff --git a/crates/benilla-ui/src/script/tests/widget_surface.rs b/crates/benilla-ui/src/script/tests/widget_surface.rs index 14694a68..45b26595 100644 --- a/crates/benilla-ui/src/script/tests/widget_surface.rs +++ b/crates/benilla-ui/src/script/tests/widget_surface.rs @@ -603,6 +603,20 @@ const ALLOWED_BEYOND: &[(&str, &str, &str)] = &[ addon writes this one — Cinematic/Options.lua:701 paints its list border with it and \ died at session start without it, and OpenMapClassic its map chrome", ), + ( + "SetEnabled", + "Button CheckButton EditBox LootButton", + "ClassicAPI: retail replaced 1.12's Enable/Disable pair (0x780160, 0x7801b0) with the one \ + setter, and later-era addons write it unguarded on their OWN controls — \ + Cinematic/Options.lua:4176 is a tick's own Refresh, and the raise there unwound \ + RefreshOptions -> SettingsChanged -> SetProfile, so picking a profile left the options \ + panel half-refreshed with a traceback in chat. Composed from the stock pair on Button's \ + table, which CheckButton and LootButton inherit. EditBox has no pair to compose (the \ + reference gives a box no enabled state at all), so there it writes the mouse and \ + keyboard and drops focus — input refused, text NOT greyed, which 1.12 has no slot for. \ + A Slider still answers neither this nor the trio: addons duck-type on it to tell a \ + control from a bar (tests/slider.rs)", + ), ( "SetShown", "Button CheckButton ColorSelect DressUpModel EditBox FontString Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel Texture WorldFrame",