From 33d08680096a71ef7be7159965338281a4344582 Mon Sep 17 00:00:00 2001 From: grom Date: Mon, 2 Nov 2026 10:00:30 -0700 Subject: [PATCH] ui: SetEnabled, the one setter retail put in place of Enable/Disable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A later-era addon writes `SetEnabled` **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; `:305` twelve pages up is the same file writing `control.SetEnabled and …` about a control it did not make. The raise at 4176 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, leaving the traceback in chat and the window still reading "Nothing runs until you pick one." That is the chat error grom recorded on 2026-11-02. Composed from the stock `Enable`/`Disable` pair (`0x780160`, `0x7801b0`) on Button's table, which CheckButton and LootButton reach down the same chain the pair travels. EditBox takes it too, written out rather than composed because the reference gives a box no enabled state at all: it writes the mouse and keyboard the frame already owns and drops focus, so input is refused and the text is NOT greyed, which 1.12 has no slot for — `Options.lua:1629` is the call that needs it. A Slider still answers neither this nor the trio: addons duck-type on them to tell a control from a bar, which `tests/slider.rs` pins. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- .../src/script/compat/classic_api/frame.rs | 94 +++++++++++++++++++ .../src/script/tests/widget_surface.rs | 14 +++ 2 files changed, 108 insertions(+) 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",