diff --git a/crates/benilla-app/assets/ui/ContainerFrameAdapters.xml b/crates/benilla-app/assets/ui/ContainerFrameAdapters.xml index 26466581..8a7882ff 100644 --- a/crates/benilla-app/assets/ui/ContainerFrameAdapters.xml +++ b/crates/benilla-app/assets/ui/ContainerFrameAdapters.xml @@ -10,6 +10,8 @@ -- The stock body, captured before it is shadowed; nil when no client data was sourced. local refItemButtonOnEnter = ContainerFrameItemButton_OnEnter + -- The stock name, redefined: the stock item buttons call it, and our keyring slot needs + -- the bag-slot tooltip. function ContainerFrameItemButton_OnEnter(button) if not button then button = this @@ -72,7 +74,8 @@ end end - -- After the stock ContainerFrame.lua has loaded, so its callers reach these. + -- The stock names, redefined after the stock ContainerFrame.lua has loaded, so its callers + -- (the vendor, bank, mail and trade windows) reach these. OpenBackpack = BenillaOpenBackpack CloseBackpack = BenillaCloseBackpack CloseAllBags = BenillaCloseAllBags diff --git a/crates/benilla-app/assets/ui/GameMenuAdapters.xml b/crates/benilla-app/assets/ui/GameMenuAdapters.xml index 6962cddf..43cf5fea 100644 --- a/crates/benilla-app/assets/ui/GameMenuAdapters.xml +++ b/crates/benilla-app/assets/ui/GameMenuAdapters.xml @@ -24,11 +24,11 @@ end end - -- ERA_WINDOW_SCALE (OptionsFrame.xml) stands in for the era's UIParent scale; read at - -- every show, then the stock OnShow. + -- BENILLA_ERA_WINDOW_SCALE (OptionsFrame.xml) stands in for the era's UIParent scale; + -- read at every show, then the stock OnShow. local Ref_GameMenuFrame_OnShow = GameMenuFrame:GetScript("OnShow") GameMenuFrame:SetScript("OnShow", function() - this:SetScale(ERA_WINDOW_SCALE or 1) + this:SetScale(BENILLA_ERA_WINDOW_SCALE or 1) Ref_GameMenuFrame_OnShow() end) diff --git a/crates/benilla-app/assets/ui/KeyBindingsPage.xml b/crates/benilla-app/assets/ui/KeyBindingsPage.xml index a89fa332..03d4de7e 100644 --- a/crates/benilla-app/assets/ui/KeyBindingsPage.xml +++ b/crates/benilla-app/assets/ui/KeyBindingsPage.xml @@ -4,14 +4,16 @@ OptionsFrame.xml, which loads after. Only commands the engine dispatches appear. Deviation: the layout is the era Settings panel's Keybindings page (collapsible category sections), because 1.12's settings screens are much worse to use. Capture follows 1.12's - Blizzard_BindingUI: the next key or mouse 3/4/5 press binds, left and right clicks only - select, and ESC while armed binds Escape (its OnKeyDown has no ESCAPE filter). + Blizzard_BindingUI: while a capsule is selected the window is keyboard-enabled, so the + next key binds through its OnKeyDown, and a mouse 3/4/5 click or a wheel notch binds; + left and right clicks only select, and ESC while armed binds Escape (its OnKeyDown has no + ESCAPE filter). Deviation: every bind, unbind and reset saves at once, as the era panel commits, where 1.12 had Okay and Cancel; only Reset To Default and leaving the character set confirm. --> - - - + @@ -61,13 +63,13 @@ - KeyBindings_ToggleSection(this.sectionIndex) + BenillaKeyBindings_ToggleSection(this.sectionIndex) - - @@ -75,7 +77,7 @@ - - - + - + @@ -1032,7 +1032,7 @@ - - + @@ -1096,21 +1096,21 @@ - OptionsCheckbox_OnClick(this) - OptionsRow_Hover(this:GetParent(), 1) - OptionsRow_Hover(this:GetParent(), nil) + BenillaOptionsCheckbox_OnClick(this) + BenillaOptionsRow_Hover(this:GetParent(), 1) + BenillaOptionsRow_Hover(this:GetParent(), nil) - OptionsRow_Hover(this, 1) - OptionsRow_Hover(this, nil) + BenillaOptionsRow_Hover(this, 1) + BenillaOptionsRow_Hover(this, nil) - + @@ -1172,22 +1172,22 @@ - OptionsSlider_OnValueChanged(this, arg1) - OptionsRow_Hover(this:GetParent():GetParent(), 1) - OptionsRow_Hover(this:GetParent():GetParent(), nil) + BenillaOptionsSlider_OnValueChanged(this, arg1) + BenillaOptionsRow_Hover(this:GetParent():GetParent(), 1) + BenillaOptionsRow_Hover(this:GetParent():GetParent(), nil) - OptionsRow_Hover(this, 1) - OptionsRow_Hover(this, nil) + BenillaOptionsRow_Hover(this, 1) + BenillaOptionsRow_Hover(this, nil) - + @@ -1221,8 +1221,8 @@ - OptionsRow_Hover(this, 1) - OptionsRow_Hover(this, nil) + BenillaOptionsRow_Hover(this, 1) + BenillaOptionsRow_Hover(this, nil) @@ -1327,7 +1327,7 @@ - OptionsTab_FitWidth(self) + BenillaOptionsTab_FitWidth(this) @@ -1370,56 +1370,56 @@ - + - getglobal(self:GetName() .. "Label"):SetText("Gameplay") + getglobal(this:GetName() .. "Label"):SetText("Gameplay") - - - - - - - - + - getglobal(self:GetName() .. "Label"):SetText("System") + getglobal(this:GetName() .. "Label"):SetText("System") - - @@ -1444,37 +1444,37 @@ - + - - @@ -1486,9 +1486,16 @@ - self:SetScrollChild(BenillaOptionsFrameContainerBody) - OptionsScroll_Fit() - BenillaScroll_OnMouseWheel(this, arg1) + this:SetScrollChild(BenillaOptionsFrameContainerBody) + BenillaOptionsScroll_Fit() + + + if BenillaKeyBindingsPage.selected then + BenillaKeyBindings_OnMouseWheel(arg1) + else + BenillaScroll_OnMouseWheel(this, arg1) + end + BenillaScroll_SyncBar(this) BenillaScroll_SyncBar(this) @@ -1496,7 +1503,7 @@ + BenillaOptionsScroll_Fit sizes it. --> @@ -1516,185 +1523,185 @@ - + - OptionsRow_OnLoad(self, "deselectOnClick", "Sticky Targeting", + BenillaOptionsRow_OnLoad(this, "deselectOnClick", "Sticky Targeting", "OPTION_TOOLTIP_GAMEFIELD_DESELECT") - self.invert = 1 + this.invert = 1 - + - OptionsRow_OnLoad(self, "assistAttack", "Attack on assist", + BenillaOptionsRow_OnLoad(this, "assistAttack", "Attack on assist", "OPTION_TOOLTIP_ASSIST_ATTACK") - + - OptionsRow_OnLoad(self, "autoClearAFK", "Auto Clear AFK", + BenillaOptionsRow_OnLoad(this, "autoClearAFK", "Auto Clear AFK", "OPTION_TOOLTIP_CLEAR_AFK") - + - OptionsRow_OnLoad(self, "BlockTrades", "Block Trades", + BenillaOptionsRow_OnLoad(this, "BlockTrades", "Block Trades", "OPTION_TOOLTIP_BLOCK_TRADES") - + - OptionsRow_OnLoad(self, "autoSelfCast", "Auto Self Cast", + BenillaOptionsRow_OnLoad(this, "autoSelfCast", "Auto Self Cast", "OPTION_TOOLTIP_AUTO_SELF_CAST") - + - OptionsRow_OnLoad(self, "autoLootDefault", "Auto Loot") + BenillaOptionsRow_OnLoad(this, "autoLootDefault", "Auto Loot") - + - OptionsRow_OnLoad(self, "mouseInvertPitch", "Invert Mouse", + BenillaOptionsRow_OnLoad(this, "mouseInvertPitch", "Invert Mouse", "OPTION_TOOLTIP_INVERT_MOUSE") - + - OptionsRow_OnLoad(self, "mousespeed", "Mouse Sensitivity", + BenillaOptionsRow_OnLoad(this, "mousespeed", "Mouse Sensitivity", "OPTION_TOOLTIP_MOUSE_SENSITIVITY") - OptionsSliderRow_Setup(self, 0.5, 1.5, 0.05) + BenillaOptionsSliderRow_Setup(this, 0.5, 1.5, 0.05) - + - OptionsRow_OnLoad(self, "cameraSmoothStyle", + BenillaOptionsRow_OnLoad(this, "cameraSmoothStyle", "Camera Following Style", "OPTION_TOOLTIP_CAMERA1") - OptionsDropdownRow_Setup(self, { + BenillaOptionsDropdownRow_Setup(this, { { text = "Smart", value = "1", tip = "OPTION_TOOLTIP_CAMERA1" }, { text = "Always", value = "2", tip = "OPTION_TOOLTIP_CAMERA2" }, { text = "Never", value = "0", alt = "3", tip = "OPTION_TOOLTIP_CAMERA3" }, }) - + - OptionsRow_OnLoad(self, "cameraTerrainTilt", "Follow Terrain", + BenillaOptionsRow_OnLoad(this, "cameraTerrainTilt", "Follow Terrain", "OPTION_TOOLTIP_FOLLOW_TERRAIN") - + - OptionsRow_OnLoad(self, "cameraBobbing", "Head Bob", + BenillaOptionsRow_OnLoad(this, "cameraBobbing", "Head Bob", "OPTION_TOOLTIP_HEAD_BOB") - + - OptionsRow_OnLoad(self, "cameraYawSmoothSpeed", "Auto-Follow Speed", + BenillaOptionsRow_OnLoad(this, "cameraYawSmoothSpeed", "Auto-Follow Speed", "OPTION_TOOLTIP_AUTO_FOLLOW_SPEED") - OptionsSliderRow_Setup(self, 90, 270, 10, "raw") + BenillaOptionsSliderRow_Setup(this, 90, 270, 10, "raw") - + - OptionsRow_OnLoad(self, "cameraWaterCollision", "Water Collision", + BenillaOptionsRow_OnLoad(this, "cameraWaterCollision", "Water Collision", "OPTION_TOOLTIP_WATER_COLLISION") - + - OptionsRow_OnLoad(self, "cameraPivot", "Smart Pivot", + BenillaOptionsRow_OnLoad(this, "cameraPivot", "Smart Pivot", "OPTION_TOOLTIP_SMART_PIVOT") - + - OptionsRow_OnLoad(self, "cameraYawMoveSpeed", "Mouse Look Speed", + BenillaOptionsRow_OnLoad(this, "cameraYawMoveSpeed", "Mouse Look Speed", "OPTION_TOOLTIP_MOUSE_LOOK_SPEED") - OptionsSliderRow_Setup(self, 90, 270, 10, "raw") - self.partner = { cvar = "cameraPitchMoveSpeed", scale = 0.5 } + BenillaOptionsSliderRow_Setup(this, 90, 270, 10, "raw") + this.partner = { cvar = "cameraPitchMoveSpeed", scale = 0.5 } - + - OptionsRow_OnLoad(self, "cameraDistanceMaxFactor", + BenillaOptionsRow_OnLoad(this, "cameraDistanceMaxFactor", "Max Camera Distance", "OPTION_TOOLTIP_MAX_FOLLOW_DIST") - OptionsSliderRow_Setup(self, 1.0, 2.0, 0.1, "yards") + BenillaOptionsSliderRow_Setup(this, 1.0, 2.0, 0.1, "yards") @@ -1706,104 +1713,104 @@ - + - OptionsRow_OnLoad(self, "MasterSoundEffects", "Enable All Sound", + BenillaOptionsRow_OnLoad(this, "MasterSoundEffects", "Enable All Sound", "OPTION_TOOLTIP_ENABLE_ALL_SOUND") - self.soundQuirk = 1 + this.soundQuirk = 1 - + - OptionsRow_OnLoad(self, "MasterVolume", "Master Volume", + BenillaOptionsRow_OnLoad(this, "MasterVolume", "Master Volume", "OPTION_TOOLTIP_MASTER_VOLUME") - + - OptionsRow_OnLoad(self, "SoundVolume", "Sound Volume", + BenillaOptionsRow_OnLoad(this, "SoundVolume", "Sound Volume", "OPTION_TOOLTIP_SOUND_VOLUME", 1) - self.parentRow = "RowMaster" + this.parentRow = "RowMaster" - + - OptionsRow_OnLoad(self, "MusicVolume", "Music Volume", + BenillaOptionsRow_OnLoad(this, "MusicVolume", "Music Volume", "OPTION_TOOLTIP_MUSIC_VOLUME", 1) - self.parentRow = "RowMaster" + this.parentRow = "RowMaster" - + - OptionsRow_OnLoad(self, "AmbienceVolume", "Ambience Volume", + BenillaOptionsRow_OnLoad(this, "AmbienceVolume", "Ambience Volume", "OPTION_TOOLTIP_AMBIENCE_VOLUME", 1) - self.parentRow = "RowMaster" + this.parentRow = "RowMaster" - + - OptionsRow_OnLoad(self, "EnableMusic", "Enable Music", + BenillaOptionsRow_OnLoad(this, "EnableMusic", "Enable Music", "OPTION_TOOLTIP_ENABLE_MUSIC") - self.soundQuirk = 1 + this.soundQuirk = 1 - + - OptionsRow_OnLoad(self, "EnableAmbience", "Enable Ambience", + BenillaOptionsRow_OnLoad(this, "EnableAmbience", "Enable Ambience", "OPTION_TOOLTIP_ENABLE_AMBIENCE") - self.soundQuirk = 1 + this.soundQuirk = 1 - + - OptionsRow_OnLoad(self, "EnableErrorSpeech", "Enable Error Speech", + BenillaOptionsRow_OnLoad(this, "EnableErrorSpeech", "Enable Error Speech", "OPTION_TOOLTIP_ENABLE_ERROR_SPEECH") - self.soundQuirk = 1 + this.soundQuirk = 1 - + - OptionsRow_OnLoad(self, "Sound_EnableSoundWhenGameIsInBG", + BenillaOptionsRow_OnLoad(this, "Sound_EnableSoundWhenGameIsInBG", "Enable Sound in Background", "BENILLA_TOOLTIP_BACKGROUND_SOUND") - self.soundQuirk = 1 + this.soundQuirk = 1 @@ -1817,44 +1824,44 @@ - + - OptionsRow_OnLoad(self, "uiScale", "UI Scale", + BenillaOptionsRow_OnLoad(this, "uiScale", "UI Scale", "OPTION_TOOLTIP_UI_SCALE") - OptionsSliderRow_Setup(self, OptionsFrameSliders[1].minValue, + BenillaOptionsSliderRow_Setup(this, OptionsFrameSliders[1].minValue, OptionsFrameSliders[1].maxValue, OptionsFrameSliders[1].valueStep) -- A live write would rescale the slider mid-drag. - self.deferred = 1 + this.deferred = 1 - + - OptionsRow_OnLoad(self, "farclip", "Terrain Distance", + BenillaOptionsRow_OnLoad(this, "farclip", "Terrain Distance", "OPTION_TOOLTIP_FARCLIP") - OptionsSliderRow_Setup(self, OptionsFrameSliders[2].minValue, + BenillaOptionsSliderRow_Setup(this, OptionsFrameSliders[2].minValue, OptionsFrameSliders[2].maxValue, OptionsFrameSliders[2].valueStep, "raw") - + - OptionsRow_OnLoad(self, "WorldDetail", "Environment Detail", + BenillaOptionsRow_OnLoad(this, "WorldDetail", "Environment Detail", "OPTION_TOOLTIP_ENVIRONMENT_DETAIL") -- Stop names from the era quality ladder. - OptionsSliderRow_Setup(self, OptionsFrameSliders[3].minValue, + BenillaOptionsSliderRow_Setup(this, OptionsFrameSliders[3].minValue, OptionsFrameSliders[3].maxValue, OptionsFrameSliders[3].valueStep, nil, { "Low", "Medium", "High" }) @@ -1863,7 +1870,7 @@ - + @@ -1872,33 +1879,33 @@ -- OptionsFrame_SetDefaults' default (l.444-445); -- `or 1` covers a VM with no CVars. local gd = tonumber(GetCVarDefault("gamma")) or 1 - OptionsNumFuncRow_OnLoad(self, GetGamma, SetGamma, + BenillaOptionsNumFuncRow_OnLoad(this, GetGamma, SetGamma, tostring(1 - gd), "Brightness", "BENILLA_TOOLTIP_BRIGHTNESS") - OptionsSliderRow_Setup(self, OptionsFrameSliders[6].minValue, + BenillaOptionsSliderRow_Setup(this, OptionsFrameSliders[6].minValue, OptionsFrameSliders[6].maxValue, OptionsFrameSliders[6].valueStep, "travel") - + - OptionsRow_OnLoad(self, "weatherDensity", "Weather Intensity", + BenillaOptionsRow_OnLoad(this, "weatherDensity", "Weather Intensity", "OPTION_TOOLTIP_WEATHER_DETAIL") -- Our names for `0x67b870`'s spawn-rate gains -- `{0.1, 0.33, 0.66, 1.0}` (`[0x8680ec]`). - OptionsSliderRow_Setup(self, OptionsFrameSliders[9].minValue, + BenillaOptionsSliderRow_Setup(this, OptionsFrameSliders[9].minValue, OptionsFrameSliders[9].maxValue, OptionsFrameSliders[9].valueStep, nil, { "Minimal", "Low", "Medium", "Full" }) - + @@ -1907,15 +1914,15 @@ -- Deviation: modern Classic's two-entry Display -- Mode, as there is no exclusive fullscreen; -- gxWindow "0" is the borderless window. - OptionsRow_OnLoad(self, "gxWindow", "Display Mode", + BenillaOptionsRow_OnLoad(this, "gxWindow", "Display Mode", "BENILLA_TOOLTIP_DISPLAY_MODE") - OptionsDropdownRow_Setup(self, { + BenillaOptionsDropdownRow_Setup(this, { { text = "Windowed (Fullscreen)", value = "0" }, { text = "Windowed", value = "1" }, }) - + @@ -1923,11 +1930,11 @@ -- Not deferred despite gxRestart: vsync changes -- live. - OptionsRow_OnLoad(self, "gxVSync", "Vertical Sync", + BenillaOptionsRow_OnLoad(this, "gxVSync", "Vertical Sync", "OPTION_TOOLTIP_VERTICAL_SYNC") - + @@ -1937,10 +1944,10 @@ -- the backbuffer), and no Multisampling, which -- misses alpha-tested cutouts. Deferred: a write -- rebuilds the render target. - OptionsRow_OnLoad(self, "renderScale", "Render Scale", + BenillaOptionsRow_OnLoad(this, "renderScale", "Render Scale", "BENILLA_TOOLTIP_RENDER_SCALE") - OptionsSliderRow_Setup(self, 0.5, 2.0, 0.05) - self.deferred = 1 + BenillaOptionsSliderRow_Setup(this, 0.5, 2.0, 0.05) + this.deferred = 1 @@ -1952,7 +1959,7 @@ local info = OptionsFrameSliders[i] local row = getglobal(this:GetName() .. name) if ( info and row ) then - OptionsSliderRow_Setup(row, info.minValue, info.maxValue, + BenillaOptionsSliderRow_Setup(row, info.minValue, info.maxValue, info.valueStep, row.fmt, row.stops) end end @@ -1966,227 +1973,227 @@ - + - OptionsGlobalRow_OnLoad(self, "SHOW_COMBAT_TEXT", + BenillaOptionsGlobalRow_OnLoad(this, "SHOW_COMBAT_TEXT", "Enable Floating Combat Text", "OPTION_TOOLTIP_SHOW_COMBAT_TEXT") - self.applyFunc = "CombatText_UpdateOrLoad" + this.applyFunc = "BenillaCombatText_UpdateOrLoad" - + - OptionsGlobalRow_OnLoad(self, "COMBAT_TEXT_FLOAT_MODE", + BenillaOptionsGlobalRow_OnLoad(this, "COMBAT_TEXT_FLOAT_MODE", "Combat Text Float Mode", "OPTION_TOOLTIP_COMBAT_TEXT_MODE", 1) - OptionsDropdownRow_Setup(self, { + BenillaOptionsDropdownRow_Setup(this, { { text = "Scroll Up", value = "1" }, { text = "Scroll Down", value = "2" }, { text = "Arc", value = "3" }, }) - self.parentRow = "RowCombatText" - self.applyFunc = "CombatText_UpdateOrLoad" + this.parentRow = "RowCombatText" + this.applyFunc = "BenillaCombatText_UpdateOrLoad" - + - OptionsGlobalRow_OnLoad(self, "COMBAT_TEXT_SHOW_LOW_HEALTH_MANA", + BenillaOptionsGlobalRow_OnLoad(this, "COMBAT_TEXT_SHOW_LOW_HEALTH_MANA", "Show Low Mana and Health", "OPTION_TOOLTIP_COMBAT_TEXT_SHOW_LOW_HEALTH_MANA", 1) - self.parentRow = "RowCombatText" - self.applyFunc = "CombatText_UpdateOrLoad" + this.parentRow = "RowCombatText" + this.applyFunc = "BenillaCombatText_UpdateOrLoad" - + - OptionsGlobalRow_OnLoad(self, "COMBAT_TEXT_SHOW_AURAS", + BenillaOptionsGlobalRow_OnLoad(this, "COMBAT_TEXT_SHOW_AURAS", "Show Auras", "OPTION_TOOLTIP_COMBAT_TEXT_SHOW_AURAS", 1) - self.parentRow = "RowCombatText" - self.applyFunc = "CombatText_UpdateOrLoad" + this.parentRow = "RowCombatText" + this.applyFunc = "BenillaCombatText_UpdateOrLoad" - + - OptionsGlobalRow_OnLoad(self, "COMBAT_TEXT_SHOW_AURA_FADE", + BenillaOptionsGlobalRow_OnLoad(this, "COMBAT_TEXT_SHOW_AURA_FADE", "Show Fading Auras", "OPTION_TOOLTIP_COMBAT_TEXT_SHOW_AURA_FADE", 1) - self.parentRow = "RowCombatText" - self.applyFunc = "CombatText_UpdateOrLoad" + this.parentRow = "RowCombatText" + this.applyFunc = "BenillaCombatText_UpdateOrLoad" - + - OptionsGlobalRow_OnLoad(self, "COMBAT_TEXT_SHOW_COMBAT_STATE", + BenillaOptionsGlobalRow_OnLoad(this, "COMBAT_TEXT_SHOW_COMBAT_STATE", "Show Combat State", "OPTION_TOOLTIP_COMBAT_TEXT_SHOW_COMBAT_STATE", 1) - self.parentRow = "RowCombatText" - self.applyFunc = "CombatText_UpdateOrLoad" + this.parentRow = "RowCombatText" + this.applyFunc = "BenillaCombatText_UpdateOrLoad" - + - OptionsGlobalRow_OnLoad(self, "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS", + BenillaOptionsGlobalRow_OnLoad(this, "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS", "Show Dodge/Parries/Misses", "OPTION_TOOLTIP_COMBAT_TEXT_SHOW_DODGE_PARRY_MISS", 1) - self.parentRow = "RowCombatText" - self.applyFunc = "CombatText_UpdateOrLoad" + this.parentRow = "RowCombatText" + this.applyFunc = "BenillaCombatText_UpdateOrLoad" - + - OptionsGlobalRow_OnLoad(self, "COMBAT_TEXT_SHOW_RESISTANCES", + BenillaOptionsGlobalRow_OnLoad(this, "COMBAT_TEXT_SHOW_RESISTANCES", "Show Damage Reduction", "OPTION_TOOLTIP_COMBAT_TEXT_SHOW_RESISTANCES", 1) - self.parentRow = "RowCombatText" - self.applyFunc = "CombatText_UpdateOrLoad" + this.parentRow = "RowCombatText" + this.applyFunc = "BenillaCombatText_UpdateOrLoad" - + - OptionsGlobalRow_OnLoad(self, "COMBAT_TEXT_SHOW_REPUTATION", + BenillaOptionsGlobalRow_OnLoad(this, "COMBAT_TEXT_SHOW_REPUTATION", "Show Reputation", "OPTION_TOOLTIP_COMBAT_TEXT_SHOW_REPUTATION", 1) - self.parentRow = "RowCombatText" - self.applyFunc = "CombatText_UpdateOrLoad" + this.parentRow = "RowCombatText" + this.applyFunc = "BenillaCombatText_UpdateOrLoad" - + - OptionsGlobalRow_OnLoad(self, "COMBAT_TEXT_SHOW_REACTIVES", + BenillaOptionsGlobalRow_OnLoad(this, "COMBAT_TEXT_SHOW_REACTIVES", "Show Reactive Spells and Abilities", "OPTION_TOOLTIP_COMBAT_TEXT_SHOW_REACTIVES", 1) - self.parentRow = "RowCombatText" - self.applyFunc = "CombatText_UpdateOrLoad" + this.parentRow = "RowCombatText" + this.applyFunc = "BenillaCombatText_UpdateOrLoad" - + - OptionsGlobalRow_OnLoad(self, "COMBAT_TEXT_SHOW_FRIENDLY_NAMES", + BenillaOptionsGlobalRow_OnLoad(this, "COMBAT_TEXT_SHOW_FRIENDLY_NAMES", "Show Friendly Healer Names", "OPTION_TOOLTIP_COMBAT_TEXT_SHOW_FRIENDLY_NAMES", 1) - self.parentRow = "RowCombatText" - self.applyFunc = "CombatText_UpdateOrLoad" + this.parentRow = "RowCombatText" + this.applyFunc = "BenillaCombatText_UpdateOrLoad" - + - OptionsGlobalRow_OnLoad(self, "COMBAT_TEXT_SHOW_COMBO_POINTS", + BenillaOptionsGlobalRow_OnLoad(this, "COMBAT_TEXT_SHOW_COMBO_POINTS", "Show Combo Points", "OPTION_TOOLTIP_COMBAT_TEXT_SHOW_COMBO_POINTS", 1) - self.parentRow = "RowCombatText" - self.applyFunc = "CombatText_UpdateOrLoad" + this.parentRow = "RowCombatText" + this.applyFunc = "BenillaCombatText_UpdateOrLoad" - + - OptionsGlobalRow_OnLoad(self, "COMBAT_TEXT_SHOW_MANA", + BenillaOptionsGlobalRow_OnLoad(this, "COMBAT_TEXT_SHOW_MANA", "Show Energy Gains", "OPTION_TOOLTIP_COMBAT_TEXT_SHOW_MANA", 1) - self.parentRow = "RowCombatText" - self.applyFunc = "CombatText_UpdateOrLoad" + this.parentRow = "RowCombatText" + this.applyFunc = "BenillaCombatText_UpdateOrLoad" - + - OptionsGlobalRow_OnLoad(self, "COMBAT_TEXT_SHOW_HONOR_GAINED", + BenillaOptionsGlobalRow_OnLoad(this, "COMBAT_TEXT_SHOW_HONOR_GAINED", "Show Honor Gained", "OPTION_TOOLTIP_COMBAT_TEXT_SHOW_HONOR_GAINED", 1) - self.parentRow = "RowCombatText" - self.applyFunc = "CombatText_UpdateOrLoad" + this.parentRow = "RowCombatText" + this.applyFunc = "BenillaCombatText_UpdateOrLoad" - + - OptionsRow_OnLoad(self, "CombatDamage", "Show Target Damage", + BenillaOptionsRow_OnLoad(this, "CombatDamage", "Show Target Damage", "OPTION_TOOLTIP_SHOW_DAMAGE") - + - OptionsRow_OnLoad(self, "CombatLogPeriodicSpells", "Periodic Damage", + BenillaOptionsRow_OnLoad(this, "CombatLogPeriodicSpells", "Periodic Damage", "OPTION_TOOLTIP_LOG_PERIODIC_EFFECTS", 1) - self.parentRow = "RowShowDamage" + this.parentRow = "RowShowDamage" - + - OptionsRow_OnLoad(self, "PetMeleeDamage", "Pet Damage", + BenillaOptionsRow_OnLoad(this, "PetMeleeDamage", "Pet Damage", "OPTION_TOOLTIP_SHOW_PET_MELEE_DAMAGE", 1) - self.parentRow = "RowShowDamage" - self.partner = { cvar = "PetSpellDamage" } + this.parentRow = "RowShowDamage" + this.partner = { cvar = "PetSpellDamage" } @@ -2200,13 +2207,13 @@ - + - OptionsFuncRow_OnLoad(self, + BenillaOptionsFuncRow_OnLoad(this, function() return SHOW_MULTI_ACTIONBAR_1 end, function(value) local on @@ -2220,13 +2227,13 @@ "0", "Show Bottom Left ActionBar", "OPTION_TOOLTIP_SHOW_MULTIBAR1") - + - OptionsFuncRow_OnLoad(self, + BenillaOptionsFuncRow_OnLoad(this, function() return SHOW_MULTI_ACTIONBAR_2 end, function(value) local on @@ -2240,13 +2247,13 @@ "0", "Show Bottom Right ActionBar", "OPTION_TOOLTIP_SHOW_MULTIBAR2") - + - OptionsFuncRow_OnLoad(self, + BenillaOptionsFuncRow_OnLoad(this, function() return SHOW_MULTI_ACTIONBAR_3 end, function(value) local on @@ -2261,13 +2268,13 @@ - + - OptionsFuncRow_OnLoad(self, + BenillaOptionsFuncRow_OnLoad(this, function() return SHOW_MULTI_ACTIONBAR_4 end, function(value) local on @@ -2281,25 +2288,25 @@ "0", "Show Right ActionBar 2", "OPTION_TOOLTIP_SHOW_MULTIBAR4", 1) - + - OptionsGlobalRow_OnLoad(self, "ALWAYS_SHOW_MULTIBARS", + BenillaOptionsGlobalRow_OnLoad(this, "ALWAYS_SHOW_MULTIBARS", "Always Show ActionBars", "OPTION_TOOLTIP_ALWAYS_SHOW_MULTIBARS") -- UIOptionsFrame.xml:680-683's click arm. - self.applyFunc = "MultiActionBar_UpdateGridVisibility" + this.applyFunc = "MultiActionBar_UpdateGridVisibility" - + - OptionsGlobalRow_OnLoad(self, "LOCK_ACTIONBAR", + BenillaOptionsGlobalRow_OnLoad(this, "LOCK_ACTIONBAR", "Lock ActionBars", "OPTION_TOOLTIP_LOCK_ACTIONBAR") @@ -2314,153 +2321,153 @@ - + - OptionsRow_OnLoad(self, "UberTooltips", "Enhanced Tooltips", + BenillaOptionsRow_OnLoad(this, "UberTooltips", "Enhanced Tooltips", "OPTION_TOOLTIP_USE_UBERTOOLTIPS") - + - OptionsRow_OnLoad(self, "statusBarText", + BenillaOptionsRow_OnLoad(this, "statusBarText", "Status Bar Text", "OPTION_TOOLTIP_STATUS_BAR") - self.cvarEvent = "STATUS_BAR_TEXT" + this.cvarEvent = "STATUS_BAR_TEXT" - + - OptionsGlobalRow_OnLoad(self, "SHOW_TARGET_OF_TARGET", + BenillaOptionsGlobalRow_OnLoad(this, "SHOW_TARGET_OF_TARGET", "Show Target of Target", "OPTION_TOOLTIP_SHOW_TARGET_OF_TARGET") - self.applyFunc = "TargetofTarget_Update" + this.applyFunc = "TargetofTarget_Update" - + - OptionsGlobalRow_OnLoad(self, "SHOW_TARGET_OF_TARGET_STATE", + BenillaOptionsGlobalRow_OnLoad(this, "SHOW_TARGET_OF_TARGET_STATE", "Show When", "OPTION_TOOLTIP_TARGETOFTARGET5", 1) - OptionsDropdownRow_Setup(self, { + BenillaOptionsDropdownRow_Setup(this, { { text = "Raid", value = "1", tip = "OPTION_TOOLTIP_TARGETOFTARGET1" }, { text = "Party", value = "2", tip = "OPTION_TOOLTIP_TARGETOFTARGET2" }, { text = "Solo", value = "3", tip = "OPTION_TOOLTIP_TARGETOFTARGET3" }, { text = "Party or Raid", value = "4", tip = "OPTION_TOOLTIP_TARGETOFTARGET4" }, { text = "Always", value = "5", tip = "OPTION_TOOLTIP_TARGETOFTARGET5" }, }) - self.applyFunc = "TargetofTarget_Update" + this.applyFunc = "TargetofTarget_Update" - + - OptionsFuncRow_OnLoad(self, ShowingCloak, ShowCloak, "1", + BenillaOptionsFuncRow_OnLoad(this, ShowingCloak, ShowCloak, "1", "Show Cloak", "OPTION_TOOLTIP_SHOW_CLOAK") - + - OptionsFuncRow_OnLoad(self, ShowingHelm, ShowHelm, "1", + BenillaOptionsFuncRow_OnLoad(this, ShowingHelm, ShowHelm, "1", "Show Helm", "OPTION_TOOLTIP_SHOW_HELM") - + - OptionsGlobalRow_OnLoad(self, "QUEST_FADING_DISABLE", + BenillaOptionsGlobalRow_OnLoad(this, "QUEST_FADING_DISABLE", "Instant Quest Text", "OPTION_TOOLTIP_SHOW_QUEST_FADING") - + - OptionsGlobalRow_OnLoad(self, "SHOW_BUFF_DURATIONS", + BenillaOptionsGlobalRow_OnLoad(this, "SHOW_BUFF_DURATIONS", "Buff Durations", "OPTION_TOOLTIP_SHOW_BUFF_DURATION") - self.applyFunc = "BuffButtons_UpdatePositions" + this.applyFunc = "BuffButtons_UpdatePositions" - + - OptionsGlobalRow_OnLoad(self, "AUTO_QUEST_WATCH", + BenillaOptionsGlobalRow_OnLoad(this, "AUTO_QUEST_WATCH", "Automatic Quest Tracking", "OPTION_TOOLTIP_AUTO_QUEST_WATCH") - + - OptionsRow_OnLoad(self, "profanityFilter", "Profanity Filter", + BenillaOptionsRow_OnLoad(this, "profanityFilter", "Profanity Filter", "OPTION_TOOLTIP_PROFANITY_FILTER") - + - OptionsFuncRow_OnLoad(self, TutorialsEnabled, + BenillaOptionsFuncRow_OnLoad(this, TutorialsEnabled, BenillaOptionsFrame_SetTutorialsEnabled, "1", "Show Tutorials", "OPTION_TOOLTIP_SHOW_TUTORIALS") - + - OptionsRow_OnLoad(self, "showGameTips", + BenillaOptionsRow_OnLoad(this, "showGameTips", "Show Loading Screen Tips", "OPTION_TOOLTIP_SHOW_TIPOFTHEDAY") - + - OptionsGlobalRow_OnLoad(self, "SHOW_NEWBIE_TIPS", + BenillaOptionsGlobalRow_OnLoad(this, "SHOW_NEWBIE_TIPS", "Show Detailed Tooltips", "OPTION_TOOLTIP_SHOW_NEWBIE_TIPS") @@ -2474,19 +2481,19 @@ - + - OptionsRow_OnLoad(self, "guildMemberNotify", + BenillaOptionsRow_OnLoad(this, "guildMemberNotify", "Guild Member Alert", "OPTION_TOOLTIP_GUILDMEMBER_ALERT") - + @@ -2494,41 +2501,41 @@ - OptionsGlobalRow_OnLoad(self, "REMOVE_CHAT_DELAY", + BenillaOptionsGlobalRow_OnLoad(this, "REMOVE_CHAT_DELAY", "Remove Chat Hover Delay", "OPTION_TOOLTIP_REMOVE_CHAT_DELAY") - self.applyFunc = "ChatFrame_ApplyMouseOverDelay" + this.applyFunc = "BenillaChatFrame_ApplyMouseOverDelay" - + - OptionsRow_OnLoad(self, "ChatBubbles", + BenillaOptionsRow_OnLoad(this, "ChatBubbles", "Show Chat Bubbles", "OPTION_TOOLTIP_CHAT_BUBBLES") - + - OptionsRow_OnLoad(self, "showLootSpam", + BenillaOptionsRow_OnLoad(this, "showLootSpam", "Detailed Loot Information", "OPTION_TOOLTIP_SHOW_LOOT_SPAM") - + - OptionsRow_OnLoad(self, "ChatBubblesParty", + BenillaOptionsRow_OnLoad(this, "ChatBubblesParty", "Show Party Chat Bubbles", "OPTION_TOOLTIP_PARTY_CHAT_BUBBLES") @@ -2536,15 +2543,15 @@ and writes it (l.337). The filter drops a SpamMessages.dbc match in every chat type but CHAT_MSG_FILTERED (`0x49aad2`). --> - + - OptionsRow_OnLoad(self, "spamFilter", "Disable Spam Filter", + BenillaOptionsRow_OnLoad(this, "spamFilter", "Disable Spam Filter", "OPTION_TOOLTIP_DISABLE_SPAM_FILTER") - self.invert = 1 + this.invert = 1 @@ -2558,56 +2565,56 @@ - + - OptionsRow_OnLoad(self, "nameplateShowEnemies", + BenillaOptionsRow_OnLoad(this, "nameplateShowEnemies", "Enemy Name Plates") - + - OptionsRow_OnLoad(self, "nameplateShowFriends", + BenillaOptionsRow_OnLoad(this, "nameplateShowFriends", "Friendly Name Plates") - + - OptionsRow_OnLoad(self, "UnitNamePlayer", "Player Names", + BenillaOptionsRow_OnLoad(this, "UnitNamePlayer", "Player Names", "OPTION_TOOLTIP_SHOW_PLAYER_NAMES") - + - OptionsRow_OnLoad(self, "UnitNamePlayerGuild", "Player Guild Names", + BenillaOptionsRow_OnLoad(this, "UnitNamePlayerGuild", "Player Guild Names", "OPTION_TOOLTIP_SHOW_GUILD_NAMES", 1) - self.parentRow = "RowPlayerNames" + this.parentRow = "RowPlayerNames" - + - OptionsRow_OnLoad(self, "UnitNameNPC", "NPC Names", + BenillaOptionsRow_OnLoad(this, "UnitNameNPC", "NPC Names", "OPTION_TOOLTIP_SHOW_NPC_NAMES") - + - OptionsRow_OnLoad(self, "UnitNameOwn", "Show Own Name", + BenillaOptionsRow_OnLoad(this, "UnitNameOwn", "Show Own Name", "OPTION_TOOLTIP_SHOW_OWN_NAME") @@ -2653,10 +2660,10 @@ - KeyBindings_CharacterToggle(this) + BenillaKeyBindings_CharacterToggle(this) GameTooltip:SetOwner(this, "ANCHOR_RIGHT") - GameTooltip:SetText(KeyBindings_String("CHARACTER_SPECIFIC_KEYBINDING_TOOLTIP", + GameTooltip:SetText(BenillaKeyBindings_String("CHARACTER_SPECIFIC_KEYBINDING_TOOLTIP", "Click this to toggle between general key bindings and key bindings specific to this character."), nil, nil, nil, nil, 1) GameTooltip:Hide() @@ -2666,119 +2673,119 @@ getglobal(this:GetName() .. "Label"):SetText( - KeyBindings_String("CHARACTER_SPECIFIC_KEYBINDINGS", "Character Specific Key Bindings")) + BenillaKeyBindings_String("CHARACTER_SPECIFIC_KEYBINDINGS", "Character Specific Key Bindings")) - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -2796,92 +2803,98 @@ ScrollFrame_OnLoad() - - FauxScrollFrame_OnVerticalScroll(KEYBINDINGS_ROW_HEIGHT, KeyBindingsPage_Update) + FauxScrollFrame_OnVerticalScroll(BENILLA_KEYBINDINGS_ROW_HEIGHT, BenillaKeyBindingsPage_Update) - if not KeyBindingsPage.selected then + if BenillaKeyBindingsPage.selected then + BenillaKeyBindings_OnMouseWheel(arg1) + else ScrollFrameTemplate_OnMouseWheel(arg1) end - + - if not KeyBindingsPage.selected then + if BenillaKeyBindingsPage.selected then + BenillaKeyBindings_OnMouseWheel(arg1) + else BenillaFauxScrollFrame_OnMouseWheel(getglobal(this:GetName() .. "ScrollFrame"), arg1) end + BenillaKeyBindings_OnKeyDown(arg1) - - - - - - - - - - - - - - - - - - - + + + + + + + + + - - @@ -3003,13 +3016,22 @@ BenillaOptionsFrame_SelectCategory(this.selectedCategory or "Controls") if UpdateMicroButtons then UpdateMicroButtons() end - + + + this:EnableKeyboard(false) + this:EnableMouseWheel(false) + + BenillaKeyBindings_OnKeyDown() + BenillaKeyBindings_OnKeyDown(arg1) + BenillaKeyBindings_OnMouseWheel(arg1) + BenillaOptionsFrameSearchBox:SetText("") - OptionsPage_DiscardPending() - KeyBindings_SetSelected(nil) - KeyBindings_Output("") + BenillaOptionsPage_DiscardPending() + BenillaKeyBindings_SetSelected(nil) + BenillaKeyBindings_Output("") if UpdateMicroButtons then UpdateMicroButtons() end diff --git a/crates/benilla-app/assets/ui/ScriptLogFrame.xml b/crates/benilla-app/assets/ui/ScriptLogFrame.xml index 375c1dcc..ef460f43 100644 --- a/crates/benilla-app/assets/ui/ScriptLogFrame.xml +++ b/crates/benilla-app/assets/ui/ScriptLogFrame.xml @@ -1,10 +1,10 @@ diff --git a/crates/benilla-app/assets/ui/ScrollTemplates.xml b/crates/benilla-app/assets/ui/ScrollTemplates.xml index 0a7cfd07..19a3f3e8 100644 --- a/crates/benilla-app/assets/ui/ScrollTemplates.xml +++ b/crates/benilla-app/assets/ui/ScrollTemplates.xml @@ -183,7 +183,7 @@ - BenillaFauxScrollBar_OnValueChanged(self, arg1) + BenillaFauxScrollBar_OnValueChanged(this, arg1) diff --git a/crates/benilla-app/src/bindings.rs b/crates/benilla-app/src/bindings.rs index 7981c3c1..b61d58d8 100644 --- a/crates/benilla-app/src/bindings.rs +++ b/crates/benilla-app/src/bindings.rs @@ -15,10 +15,6 @@ //! An addon's `Bindings.xml` rows ([`benilla_ui::bindings_xml`]) dispatch here too: a resolved //! chord names a [`Bound`], either a registry [`Cmd`] or an index into the addon table. //! -//! While the Keybindings page has a capsule selected (`BenillaBindCapture`), raw input is -//! canonicalized (`ALT-CTRL-SHIFT-`) and handed to the page, by the 1.12 law: lone -//! modifiers ignored, left and right clicks stay UI clicks, ESC binds like any key. -//! //! Persistence: `benilla-config/bindings/account.txt` and `-.txt` ([`store`]); the //! character file's existence is the character-set state, as in the reference. @@ -442,7 +438,6 @@ fn latch_and_dispatch( // affordances: a player build keeps the reference's `CTRL-SHIFT-P` fallback to `SHIFT-P`. let dev_plane = ctrl && shift && !alt && crate::run_mode::dev_affordances(); - let armed = script.as_ref().is_some_and(|s| s.bind_capture_armed()); // A binding body runs as `ExecuteBinding` runs it, so the movement functions' gate passes. let run_lua = |script: &mut Option>, lua: &str, tag: &str| { if let Some(s) = script.as_mut() { @@ -452,47 +447,6 @@ fn latch_and_dispatch( } }; - // ── The capture seam ── a Keybindings capsule is selected: swallow raw input and hand the - // chord string to the page's Lua. The 1.12 law: lone modifiers and unknown keys ignored, - // left and right stay UI clicks, the wheel is a chord. Super is not a 1.12 modifier, so a - // Super press is ignored outright. - if armed { - let mut captured: Option = None; - for ev in keyboard.read() { - if ev.state != ButtonState::Pressed || ev.repeat || sup { - continue; - } - if let Some(token) = chord::key_token(ev.key_code) { - captured = Some(chord::chord_string(alt, ctrl, shift, token)); - } - } - for b in [MouseButton::Middle, MouseButton::Forward, MouseButton::Back] { - if buttons.just_pressed(b) && !sup { - if let Some(token) = chord::mouse_token(b) { - captured = Some(chord::chord_string(alt, ctrl, shift, token)); - } - } - } - if scroll.delta.y != 0.0 && !sup { - let token = if scroll.delta.y > 0.0 { - "MOUSEWHEELUP" - } else { - "MOUSEWHEELDOWN" - }; - captured = Some(chord::chord_string(alt, ctrl, shift, token)); - } - if let Some(chord_str) = captured { - // The Keybindings page's own handler, not a binding body. - if let Some(s) = script.as_mut() { - if let Err(e) = s.run(&crate::ui_script::host_key_capture(&chord_str)) { - warn!("bindings(capture): {e}"); - } - } - } - // Releases still unlatch below so a key held across the arm cannot stick; nothing new - // latches or fires while armed. - } - // ── Who owns this frame's keys ── a focused EditBox eats every key while it holds focus; a // shown keyboard frame ate the keys in `capture.consumed`. Both suppress a press and nothing // else: in the reference a focused box makes the movement handlers no-ops, so the direction @@ -533,7 +487,7 @@ fn latch_and_dispatch( if !repeat { state.down.push(key); } - if armed || (typing && !arrow_exempt) || eaten || sup || repeat { + if (typing && !arrow_exempt) || eaten || sup || repeat { continue; } if state.latched.iter().any(|&(k, _)| k == BindKey::Key(key)) { @@ -579,7 +533,6 @@ fn latch_and_dispatch( MouseButton::Back, ] { if buttons.just_pressed(b) - && !armed && !sup && hover.0.is_none() && !state.latched.iter().any(|&(k, _)| k == BindKey::Mouse(b)) @@ -619,7 +572,7 @@ fn latch_and_dispatch( let wheel = wheel_lines(scroll.unit, scroll.delta.y); // Over chrome only (`PointerOverUiPanel`): the wheel still zooms with the cursor on a // nameplate, a mouse-enabled widget but not a panel. - if wheel != 0.0 && !armed && !sup && !over_ui.0 { + if wheel != 0.0 && !sup && !over_ui.0 { let (key, amount) = if wheel > 0.0 { (BindKey::WheelUp, wheel) } else { @@ -1071,59 +1024,9 @@ mod tests { ); } - /// The armed capture seam, driven by a real wheel event. - #[test] - fn an_armed_capture_takes_a_wheel_notch() { - let mut script = UiScript::new().expect("VM"); - script.register_bindings(®istry_commands()); - script - .run( - r#"CAPTURED = nil - function KeyBindings_OnHostKey(chord) CAPTURED = chord end - BenillaBindCapture(true)"#, - ) - .expect("arm"); - let mut app = vm_harness(script); - - app.world_mut().write_message(MouseWheel { - unit: MouseScrollUnit::Line, - x: 0.0, - y: 1.0, - window: Entity::PLACEHOLDER, - }); - app.update(); - assert_eq!( - lua_str(&app, "tostring(CAPTURED)"), - "MOUSEWHEELUP", - "a wheel notch while armed is a binding key" - ); - assert!( - !state(&app).fired(cmd::CAMERA_ZOOM_IN), - "the armed seam swallows the notch — it must not also zoom" - ); - - // Down, and a modified notch. - app.world_mut().write_message(MouseWheel { - unit: MouseScrollUnit::Line, - x: 0.0, - y: -1.0, - window: Entity::PLACEHOLDER, - }); - app.update(); - assert_eq!(lua_str(&app, "tostring(CAPTURED)"), "MOUSEWHEELDOWN"); - press_key(&mut app, KeyCode::ShiftLeft); - app.world_mut().write_message(MouseWheel { - unit: MouseScrollUnit::Line, - x: 0.0, - y: 1.0, - window: Entity::PLACEHOLDER, - }); - app.update(); - assert_eq!(lua_str(&app, "tostring(CAPTURED)"), "SHIFT-MOUSEWHEELUP"); - } - - /// The whole wheel-bind path: the real Keybindings page, a capsule armed by a click, a real - /// notch, then the bound chord dispatching. + /// The whole wheel-bind path: the real Keybindings page, a capsule armed by a click, a notch + /// over the page (its OnMouseWheel, as 1.12's window binds it), then the bound chord + /// dispatching. #[test] fn a_wheel_notch_binds_through_the_real_page_and_then_dispatches() { benilla_formats::wow_data_or_skip!(); @@ -1141,27 +1044,27 @@ mod tests { .unwrap(), crate::ui_script::keybindings_tests::label(&s, "BINDING_NAME_JUMP", "JUMP") ); - assert!(s.bind_capture_armed()); + s.resolve(); + let (x, y): (f32, f32) = s + .eval(&format!( + "return ({ROW}5:GetLeft() + {ROW}5:GetRight()) / 2, \ + ({ROW}5:GetTop() + {ROW}5:GetBottom()) / 2" + )) + .unwrap(); + s.mouse_wheel(x, y, 1.0); + assert_eq!( + s.eval::(r#"return GetBindingAction("MOUSEWHEELUP")"#) + .unwrap(), + "JUMP", + "a notch on an armed capsule is a bind" + ); + assert!( + s.eval::("return BenillaKeyBindingsPage.selected == nil") + .unwrap(), + "the completed bind disarms" + ); let mut app = vm_harness(s); - app.world_mut().write_message(MouseWheel { - unit: MouseScrollUnit::Line, - x: 0.0, - y: 1.0, - window: Entity::PLACEHOLDER, - }); - app.update(); - { - let s = app.world().non_send_resource::(); - assert_eq!( - s.eval::(r#"return GetBindingAction("MOUSEWHEELUP")"#) - .unwrap(), - "JUMP", - "a notch on an armed capsule is a bind" - ); - assert!(!s.bind_capture_armed(), "the completed bind disarms"); - } - // The bound chord now dispatches: the next notch jumps rather than binding. app.world_mut().write_message(MouseWheel { unit: MouseScrollUnit::Line, diff --git a/crates/benilla-app/src/bindings/chord.rs b/crates/benilla-app/src/bindings/chord.rs index f64e9a75..e0af1768 100644 --- a/crates/benilla-app/src/bindings/chord.rs +++ b/crates/benilla-app/src/bindings/chord.rs @@ -78,22 +78,6 @@ impl Chord { } } -/// Build the canonical chord string from live modifier state and a base token. -pub(crate) fn chord_string(alt: bool, ctrl: bool, shift: bool, token: &str) -> String { - let mut s = String::new(); - if alt { - s.push_str("ALT-"); - } - if ctrl { - s.push_str("CTRL-"); - } - if shift { - s.push_str("SHIFT-"); - } - s.push_str(token); - s -} - /// The 1.12 token for a physical key; `None` for a key the reference names `UNKNOWN` and for the /// modifiers, which are only prefixes (`IsKeyPressIgnoredForBinding`). Both Enters are `ENTER`. pub(crate) fn key_token(k: KeyCode) -> Option<&'static str> { @@ -235,29 +219,17 @@ pub(crate) fn normalize_key(k: KeyCode) -> KeyCode { } } -/// The 1.12 token for a mouse button: BUTTON4 is winit's `Forward` and BUTTON5 its `Back`, so the -/// 1.12 default `BUTTON4 TOGGLEAUTORUN` sits on `Forward`. Which physical button the reference -/// calls BUTTON4 is untraced. -pub(crate) fn mouse_token(b: MouseButton) -> Option<&'static str> { - Some(match b { - MouseButton::Left => "BUTTON1", - MouseButton::Right => "BUTTON2", - MouseButton::Middle => "BUTTON3", - MouseButton::Forward => "BUTTON4", - MouseButton::Back => "BUTTON5", - // The reference names further buttons `BUTTON` (`0x4b6aa0`'s bit-scan fallback). - // winit passes the platform's button number, so the sixth button is `Other(5)`. - MouseButton::Other(n) => return EXTRA_BUTTONS.get(usize::from(n).wrapping_sub(5)).copied(), - }) -} - -/// `BUTTON6` to `BUTTON20`, indexed by `Other(n) - 5`. +/// `BUTTON6` to `BUTTON20`, indexed by `Other(n) - 5`: the reference names further buttons +/// `BUTTON` (`0x4b6aa0`'s bit-scan fallback), and winit passes the platform's button number, so +/// the sixth button is `Other(5)`. const EXTRA_BUTTONS: &[&str] = &[ "BUTTON6", "BUTTON7", "BUTTON8", "BUTTON9", "BUTTON10", "BUTTON11", "BUTTON12", "BUTTON13", "BUTTON14", "BUTTON15", "BUTTON16", "BUTTON17", "BUTTON18", "BUTTON19", "BUTTON20", ]; -/// Token to base input: the inverse of [`key_token`] and [`mouse_token`], plus the wheel pair. +/// Token to base input: the inverse of [`key_token`], the mouse buttons and the wheel pair. +/// BUTTON4 is winit's `Forward` and BUTTON5 its `Back`, so the 1.12 default `BUTTON4 +/// TOGGLEAUTORUN` sits on `Forward`; which physical button the reference calls BUTTON4 is untraced. fn token_key(t: &str) -> Option { use KeyCode::*; if let Some(b) = match t { @@ -455,15 +427,11 @@ mod tests { "{k:?} names '{token}', which SetBinding refuses" ); } - for b in [ - MouseButton::Left, - MouseButton::Right, - MouseButton::Middle, - MouseButton::Forward, - MouseButton::Back, + for token in [ + "BUTTON1", "BUTTON2", "BUTTON3", "BUTTON4", "BUTTON5", "BUTTON20", ] { - let token = mouse_token(b).expect("named"); - assert!(normalize_binding_key(token).is_some(), "{b:?} → '{token}'"); + assert!(token_key(token).is_some(), "'{token}' names no button"); + assert!(normalize_binding_key(token).is_some(), "'{token}'"); } for token in ["MOUSEWHEELUP", "MOUSEWHEELDOWN"] { assert!(normalize_binding_key(token).is_some()); @@ -626,10 +594,6 @@ mod tests { }) ); assert_eq!(Chord::parse("BOGUS"), None); - assert_eq!( - chord_string(true, false, true, "PAGEDOWN"), - "ALT-SHIFT-PAGEDOWN" - ); assert_eq!( Chord::parse("ALT-SHIFT-PAGEDOWN").unwrap(), Chord { diff --git a/crates/benilla-app/src/bindings/commands.rs b/crates/benilla-app/src/bindings/commands.rs index 6f71d030..b8a32735 100644 --- a/crates/benilla-app/src/bindings/commands.rs +++ b/crates/benilla-app/src/bindings/commands.rs @@ -1129,9 +1129,9 @@ const TABLE: &[Spec] = &[ None ), // ── MultiActionBar (BINDING_HEADER_MULTIACTIONBAR) ────────────────────────────────── - // The four multibars' `runOnUp` pairs (`Bindings.xml:799-1134`), stock MultiActionBars.lua's. - // Deviation: 1.12 files bars 2-4 under the spacer headers `BLANK`, `BLANK2` and `BLANK3`; they - // sit under MULTIACTIONBAR here because this page's sections are named. + // The four multibars' `runOnUp` pairs (`Bindings.xml:799-1134`), stock MultiActionBars.lua's; + // bars 2-4 under the spacer headers `BLANK`, `BLANK2` and `BLANK3`, as `Bindings.xml:883`, + // `:967` and `:1051` file them. spec!( "MULTIACTIONBAR1BUTTON1", MULTIACTIONBAR, @@ -1254,7 +1254,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR2BUTTON1", - MULTIACTIONBAR, + BLANK, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarBottomRight", 1)"#, r#"MultiActionButtonUp("MultiBarBottomRight", 1)"# @@ -1264,7 +1264,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR2BUTTON2", - MULTIACTIONBAR, + BLANK, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarBottomRight", 2)"#, r#"MultiActionButtonUp("MultiBarBottomRight", 2)"# @@ -1274,7 +1274,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR2BUTTON3", - MULTIACTIONBAR, + BLANK, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarBottomRight", 3)"#, r#"MultiActionButtonUp("MultiBarBottomRight", 3)"# @@ -1284,7 +1284,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR2BUTTON4", - MULTIACTIONBAR, + BLANK, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarBottomRight", 4)"#, r#"MultiActionButtonUp("MultiBarBottomRight", 4)"# @@ -1294,7 +1294,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR2BUTTON5", - MULTIACTIONBAR, + BLANK, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarBottomRight", 5)"#, r#"MultiActionButtonUp("MultiBarBottomRight", 5)"# @@ -1304,7 +1304,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR2BUTTON6", - MULTIACTIONBAR, + BLANK, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarBottomRight", 6)"#, r#"MultiActionButtonUp("MultiBarBottomRight", 6)"# @@ -1314,7 +1314,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR2BUTTON7", - MULTIACTIONBAR, + BLANK, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarBottomRight", 7)"#, r#"MultiActionButtonUp("MultiBarBottomRight", 7)"# @@ -1324,7 +1324,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR2BUTTON8", - MULTIACTIONBAR, + BLANK, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarBottomRight", 8)"#, r#"MultiActionButtonUp("MultiBarBottomRight", 8)"# @@ -1334,7 +1334,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR2BUTTON9", - MULTIACTIONBAR, + BLANK, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarBottomRight", 9)"#, r#"MultiActionButtonUp("MultiBarBottomRight", 9)"# @@ -1344,7 +1344,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR2BUTTON10", - MULTIACTIONBAR, + BLANK, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarBottomRight", 10)"#, r#"MultiActionButtonUp("MultiBarBottomRight", 10)"# @@ -1354,7 +1354,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR2BUTTON11", - MULTIACTIONBAR, + BLANK, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarBottomRight", 11)"#, r#"MultiActionButtonUp("MultiBarBottomRight", 11)"# @@ -1364,7 +1364,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR2BUTTON12", - MULTIACTIONBAR, + BLANK, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarBottomRight", 12)"#, r#"MultiActionButtonUp("MultiBarBottomRight", 12)"# @@ -1375,7 +1375,7 @@ const TABLE: &[Spec] = &[ // The vertical bars: `MultiBarRight` is bar 3, `MultiBarLeft` bar 4. spec!( "MULTIACTIONBAR3BUTTON1", - MULTIACTIONBAR, + BLANK2, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarRight", 1)"#, r#"MultiActionButtonUp("MultiBarRight", 1)"# @@ -1385,7 +1385,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR3BUTTON2", - MULTIACTIONBAR, + BLANK2, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarRight", 2)"#, r#"MultiActionButtonUp("MultiBarRight", 2)"# @@ -1395,7 +1395,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR3BUTTON3", - MULTIACTIONBAR, + BLANK2, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarRight", 3)"#, r#"MultiActionButtonUp("MultiBarRight", 3)"# @@ -1405,7 +1405,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR3BUTTON4", - MULTIACTIONBAR, + BLANK2, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarRight", 4)"#, r#"MultiActionButtonUp("MultiBarRight", 4)"# @@ -1415,7 +1415,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR3BUTTON5", - MULTIACTIONBAR, + BLANK2, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarRight", 5)"#, r#"MultiActionButtonUp("MultiBarRight", 5)"# @@ -1425,7 +1425,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR3BUTTON6", - MULTIACTIONBAR, + BLANK2, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarRight", 6)"#, r#"MultiActionButtonUp("MultiBarRight", 6)"# @@ -1435,7 +1435,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR3BUTTON7", - MULTIACTIONBAR, + BLANK2, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarRight", 7)"#, r#"MultiActionButtonUp("MultiBarRight", 7)"# @@ -1445,7 +1445,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR3BUTTON8", - MULTIACTIONBAR, + BLANK2, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarRight", 8)"#, r#"MultiActionButtonUp("MultiBarRight", 8)"# @@ -1455,7 +1455,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR3BUTTON9", - MULTIACTIONBAR, + BLANK2, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarRight", 9)"#, r#"MultiActionButtonUp("MultiBarRight", 9)"# @@ -1465,7 +1465,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR3BUTTON10", - MULTIACTIONBAR, + BLANK2, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarRight", 10)"#, r#"MultiActionButtonUp("MultiBarRight", 10)"# @@ -1475,7 +1475,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR3BUTTON11", - MULTIACTIONBAR, + BLANK2, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarRight", 11)"#, r#"MultiActionButtonUp("MultiBarRight", 11)"# @@ -1485,7 +1485,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR3BUTTON12", - MULTIACTIONBAR, + BLANK2, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarRight", 12)"#, r#"MultiActionButtonUp("MultiBarRight", 12)"# @@ -1495,7 +1495,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR4BUTTON1", - MULTIACTIONBAR, + BLANK3, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarLeft", 1)"#, r#"MultiActionButtonUp("MultiBarLeft", 1)"# @@ -1505,7 +1505,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR4BUTTON2", - MULTIACTIONBAR, + BLANK3, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarLeft", 2)"#, r#"MultiActionButtonUp("MultiBarLeft", 2)"# @@ -1515,7 +1515,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR4BUTTON3", - MULTIACTIONBAR, + BLANK3, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarLeft", 3)"#, r#"MultiActionButtonUp("MultiBarLeft", 3)"# @@ -1525,7 +1525,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR4BUTTON4", - MULTIACTIONBAR, + BLANK3, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarLeft", 4)"#, r#"MultiActionButtonUp("MultiBarLeft", 4)"# @@ -1535,7 +1535,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR4BUTTON5", - MULTIACTIONBAR, + BLANK3, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarLeft", 5)"#, r#"MultiActionButtonUp("MultiBarLeft", 5)"# @@ -1545,7 +1545,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR4BUTTON6", - MULTIACTIONBAR, + BLANK3, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarLeft", 6)"#, r#"MultiActionButtonUp("MultiBarLeft", 6)"# @@ -1555,7 +1555,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR4BUTTON7", - MULTIACTIONBAR, + BLANK3, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarLeft", 7)"#, r#"MultiActionButtonUp("MultiBarLeft", 7)"# @@ -1565,7 +1565,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR4BUTTON8", - MULTIACTIONBAR, + BLANK3, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarLeft", 8)"#, r#"MultiActionButtonUp("MultiBarLeft", 8)"# @@ -1575,7 +1575,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR4BUTTON9", - MULTIACTIONBAR, + BLANK3, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarLeft", 9)"#, r#"MultiActionButtonUp("MultiBarLeft", 9)"# @@ -1585,7 +1585,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR4BUTTON10", - MULTIACTIONBAR, + BLANK3, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarLeft", 10)"#, r#"MultiActionButtonUp("MultiBarLeft", 10)"# @@ -1595,7 +1595,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR4BUTTON11", - MULTIACTIONBAR, + BLANK3, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarLeft", 11)"#, r#"MultiActionButtonUp("MultiBarLeft", 11)"# @@ -1605,7 +1605,7 @@ const TABLE: &[Spec] = &[ ), spec!( "MULTIACTIONBAR4BUTTON12", - MULTIACTIONBAR, + BLANK3, Kind::EdgeUpDown( r#"MultiActionButtonDown("MultiBarLeft", 12)"#, r#"MultiActionButtonUp("MultiBarLeft", 12)"# @@ -2101,6 +2101,35 @@ mod tests { ); } + /// Each registered command sits under the header the install's `Bindings.xml` files it under: + /// the last `header=` at or before its row, so the host's `HEADER_*` rows (a category change + /// opens one) are the reference's, the spacers over multibars 2-4 included. + #[test] + fn every_command_sits_under_its_bindings_xml_header() { + let Some(binds) = install_bindings() else { + return; + }; + let mut section = String::new(); + let mut under = std::collections::HashMap::new(); + for b in &binds { + if b.platform.as_deref().is_some_and(|p| p != "windows") { + continue; + } + if let Some(h) = &b.header { + section = format!("BINDING_HEADER_{h}"); + } + under.insert(b.name.clone(), section.clone()); + } + for spec in SPECS { + assert_eq!( + under.get(spec.name).map(String::as_str), + Some(spec.category), + "{} is filed under the wrong header", + spec.name + ); + } + } + /// The install's own `Bindings.xml`, parsed. fn install_bindings() -> Option> { let data = benilla_formats::wow_data_or_skip!(None); diff --git a/crates/benilla-app/src/capture/fixtures.rs b/crates/benilla-app/src/capture/fixtures.rs index 88ef42a1..36f377d1 100644 --- a/crates/benilla-app/src/capture/fixtures.rs +++ b/crates/benilla-app/src/capture/fixtures.rs @@ -1036,7 +1036,7 @@ pub(super) fn seed_ui_fixture( if let Err(e) = script.run( "ShowUIPanel(BenillaOptionsFrame); \ BenillaOptionsFrameCategoryListRowKeybindings:Click(); \ - KeyBindings_ExpandSection(1, true); KeyBindingsPage_Update()", + BenillaKeyBindings_ExpandSection(1, true); BenillaKeyBindingsPage_Update()", ) { warn!("capture: ui-keybindings seed failed: {e}"); } diff --git a/crates/benilla-app/src/ui_chat/commands.rs b/crates/benilla-app/src/ui_chat/commands.rs index 48fa6fdb..8f187250 100644 --- a/crates/benilla-app/src/ui_chat/commands.rs +++ b/crates/benilla-app/src/ui_chat/commands.rs @@ -2,7 +2,9 @@ //! player's own strings define, read off the UI VM's globals the way `ChatEdit_ParseText` walks //! them (`ChatFrame.lua:2164-2200`): `SLASH_` for the actions, `EMOTE_CMD` → //! `EMOTE_TOKEN` for the emotes. An action wins over an emote alias of the same name, as the -//! reference tries `SlashCmdList` first; benilla's own commands, literals, go in after both. +//! reference tries `SlashCmdList` first; benilla's dev instruments, literals, go in after both. +//! benilla's player commands (`/reload`, `/errors`) are its layer's `SlashCmdList` rows, which a +//! line this table does not know reaches through the drain's unknown-command arm. use std::collections::HashMap; @@ -50,16 +52,6 @@ pub(crate) enum SlashIndex { MacroHelp, /// `/console `: `ConsoleExec(msg)`, as the reference's handler (`ChatFrame.lua:671`). Console, - /// `/reload`, the rebuild `/console reloadUI` runs. Deviation: 1.12 has no such alias (later - /// clients added it); kept so a player can reload the interface after toggling addons. - ReloadUi, - /// `/errors`, `/err`: opens benilla's script error log. Deviation: 1.12 has only the - /// `ScriptErrors` modal, which shows a burst's first error and keeps none; addon users need - /// the list, so it is in every build. - ScriptErrors, - /// `/convertraid`: convert the party to a raid (`CMSG_GROUP_RAID_CONVERT`, leader only). - /// Deviation: 1.12's only trigger is the Raid tab's Convert button (`RaidFrame.xml`). - ConvertRaid, } impl SlashIndex { @@ -101,15 +93,11 @@ impl SlashIndex { Self::MacroUi => "MACRO", Self::MacroHelp => "MACROHELP", Self::Console => "CONSOLE", - // No shipped strings under these three keys: their aliases are literals in `build`. - Self::ReloadUi => "RELOADUI", - Self::ScriptErrors => "BENILLASCRIPTERRORS", - Self::ConvertRaid => "BENILLACONVERTRAID", } } /// Every registered index; any other command answers `HELP_TEXT_SIMPLE`, as unknown ones do. - const ALL: [Self; 38] = [ + const ALL: [Self; 35] = [ Self::Reply, Self::Join, Self::Leave, @@ -145,9 +133,6 @@ impl SlashIndex { Self::MacroUi, Self::MacroHelp, Self::Console, - Self::ReloadUi, - Self::ScriptErrors, - Self::ConvertRaid, ]; } @@ -199,8 +184,8 @@ pub(crate) enum Command { #[derive(Resource, Default)] pub(crate) struct SlashCommands { by_alias: HashMap, - /// Aliases per source, logged at boot: `(slash, emote, benilla additions, dev instruments)`. - counts: (usize, usize, usize, usize), + /// Aliases per source, logged at boot: `(slash, emote, dev instruments)`. + counts: (usize, usize, usize), } impl SlashCommands { @@ -246,27 +231,7 @@ impl SlashCommands { } let emote_aliases = by_alias.len() - slash_aliases; - // 3 · benilla's player-facing additions, in every build; after the walks, so shipped wins. - insert( - &mut by_alias, - "reload", - Command::Slash(SlashIndex::ReloadUi), - ); - for alias in ["errors", "err"] { - insert( - &mut by_alias, - alias, - Command::Slash(SlashIndex::ScriptErrors), - ); - } - insert( - &mut by_alias, - "convertraid", - Command::Slash(SlashIndex::ConvertRaid), - ); - let added_aliases = by_alias.len() - slash_aliases - emote_aliases; - - // 4 · the dev instruments, dev builds only; a player build answers them as unknown. + // 3 · the dev instruments, dev builds only; a player build answers them as unknown. let before_dev = by_alias.len(); if crate::run_mode::dev_affordances() { for dev in DevCmd::ALL { @@ -279,13 +244,13 @@ impl SlashCommands { Self { by_alias, - counts: (slash_aliases, emote_aliases, added_aliases, dev_aliases), + counts: (slash_aliases, emote_aliases, dev_aliases), } } /// Distinct aliases per source (the shipped strings repeat: `EMOTE87_CMD1` and `_CMD2` are /// both `"/sit"`); a player build reports 0 dev aliases. - pub(super) fn counts(&self) -> (usize, usize, usize, usize) { + pub(super) fn counts(&self) -> (usize, usize, usize) { self.counts } } @@ -298,6 +263,23 @@ fn insert(map: &mut HashMap, alias: &str, cmd: Command) { } } +/// The dev instruments as `SlashCmdList` rows (`BENILLA_`), so a typed one reaches the +/// drain through the stock `ChatEdit_ParseText`; registered on a freshly loaded VM in a build with +/// dev affordances only, so a player build answers them `HELP_TEXT_SIMPLE`, as 1.12 does. +pub(crate) fn register_dev_commands(script: &benilla_ui::script::UiScript, dev: bool) { + if !dev { + return; + } + for cmd in DevCmd::ALL { + let aliases: Vec = cmd.aliases().iter().map(|a| format!("/{a}")).collect(); + let aliases: Vec<&str> = aliases.iter().map(String::as_str).collect(); + let key = format!("BENILLA_{}", cmd.aliases()[0].to_ascii_uppercase()); + if let Err(e) = script.register_host_slash_command(&key, &aliases) { + warn!("chat: registering /{}: {e}", cmd.aliases()[0]); + } + } +} + /// Build the table in `PostStartup`, after `Startup` has inserted the VM and the emote catalog. pub(crate) fn build_slash_commands( mut commands: Commands, @@ -314,11 +296,11 @@ pub(crate) fn build_slash_commands( |name| globals.get::(name).ok().filter(|s| !s.is_empty()), |token| emotes.text_id(token), ); - let (slash, emote, added, dev) = table.counts(); + let (slash, emote, dev) = table.counts(); // The shipped 1.12 data gives the counts `real_alias_table_resolves_the_shipped_commands` pins. info!( "chat: slash table — {slash} command aliases, {emote} emote aliases, \ - {added} benilla additions, {dev} instrument aliases" + {dev} instrument aliases" ); if emote == 0 { error!("chat: NO emote aliases — every /wave-style command is dead (GlobalStrings?)"); diff --git a/crates/benilla-app/src/ui_chat/input/mod.rs b/crates/benilla-app/src/ui_chat/input/mod.rs index 418eca08..8d26b348 100644 --- a/crates/benilla-app/src/ui_chat/input/mod.rs +++ b/crates/benilla-app/src/ui_chat/input/mod.rs @@ -169,8 +169,8 @@ pub(super) fn drain_chat_input( return; }; let mut queue = engine_verbs(&mut script, emotes.as_deref()); - // benilla's own commands, from the `SlashCmdList` entries in `ScriptLogFrame.xml` once the - // stock `ChatEdit_ParseText` has found no built-in, and probe lines. + // The dev instruments, from their host `SlashCmdList` rows once the stock `ChatEdit_ParseText` + // has found no built-in (dev builds), and probe lines. for raw in script.take_chat_input() { let msg = raw.trim(); if msg.is_empty() { @@ -537,11 +537,6 @@ pub(super) fn drain_chat_input( let _ = commands.0.send(ClientCommand::GroupInvite { name }); } } - // The server judges it: a raid-typed `SMSG_GROUP_LIST`, or an - // `SMSG_PARTY_COMMAND_RESULT` error. - ParsedChat::ConvertRaid => { - let _ = commands.0.send(ClientCommand::GroupRaidConvert); - } ParsedChat::Uninvite { name } => { if let Some(name) = name.or_else(|| target_player_name(&selection, &names, &commands)) @@ -800,10 +795,6 @@ pub(super) fn drain_chat_input( ParsedChat::Quit => { script.queue_session_request(benilla_ui::script::SessionRequest::Quit) } - // The deferred rebuild `ReloadUI()` queues. - ParsedChat::ReloadUi => { - script.queue_session_request(benilla_ui::script::SessionRequest::ReloadUi) - } // Deferred to the world, since a command is `fn(&mut World, &str)`; its lines print // as system text. ParsedChat::Console { line } => { diff --git a/crates/benilla-app/src/ui_chat/input/parse.rs b/crates/benilla-app/src/ui_chat/input/parse.rs index f000aa5a..59928bb1 100644 --- a/crates/benilla-app/src/ui_chat/input/parse.rs +++ b/crates/benilla-app/src/ui_chat/input/parse.rs @@ -1,8 +1,8 @@ //! What a submitted slash line means: [`parse_line`] resolves it through the boot-built command //! table ([`super::super::commands`], the shipped `GlobalStrings.lua` aliases) into a -//! [`ParsedChat`] for [`super`] to execute. A typed line reaches it only through benilla's own -//! `SlashCmdList` entries, once the stock `ChatEdit_ParseText` finds no built-in; a probe's line -//! reaches it directly. +//! [`ParsedChat`] for [`super`] to execute. A typed line reaches it only through the dev +//! instruments' `SlashCmdList` rows (dev builds), once the stock `ChatEdit_ParseText` finds no +//! built-in; a probe's line reaches it directly. use crate::ui_chat::commands::{Command, DevCmd, SlashCommands, SlashIndex}; @@ -49,9 +49,6 @@ pub(in crate::ui_chat) enum ParsedChat { Liquid, /// `/reaction [name]`: every input and rung of the reaction ladder for one unit. Reaction { name: Option }, - /// `/convertraid` (`CMSG_GROUP_RAID_CONVERT`), benilla's own: 1.12 converts only from the - /// RaidFrame's Convert button. - ConvertRaid, /// `/help`. Help, /// An `EmotesText` command (`/wave` is 101), sent as `CMSG_TEXT_EMOTE` at the selection. @@ -102,8 +99,6 @@ pub(in crate::ui_chat) enum ParsedChat { Follow { name: Option }, /// `/macrohelp`: `ChatFrame_DisplayMacroHelpText`'s five `MACRO_HELP_TEXT_LINE`s. MacroHelp, - /// `/reload`, benilla's own: the in-world rebuild `ReloadUI()` queues. - ReloadUi, /// A `/console` line the CVar store did not consume, for the `ConsoleCommand` registry /// ([`crate::console::execute`]). Console { line: String }, @@ -288,18 +283,6 @@ fn slash_command(index: SlashIndex, args: &str) -> ParsedChat { body: "ShowMacroFrame()".into(), }, S::MacroHelp => ParsedChat::MacroHelp, - // `/reload` is the rebuild `/console reloadUI` runs (`0x4035f0`, which reads no arguments). - S::ReloadUi => ParsedChat::ReloadUi, - S::ConvertRaid => ParsedChat::ConvertRaid, - // `/errors`, benilla's own: toggles the layer's script error log; `/errors clear` empties - // it. A no-op when the run boots without the layer. - S::ScriptErrors => ParsedChat::Lua { - body: if args.trim().eq_ignore_ascii_case("clear") { - crate::ui_script::ERRORS_CLEAR.into() - } else { - crate::ui_script::ERRORS_TOGGLE.into() - }, - }, // `SlashCmdList["CONSOLE"]` (`ChatFrame.lua:671`) is `ConsoleExec(msg)`. A typed line runs // it; one that skipped the edit box calls the same verb, so a CVar line writes the CVar // and anything else comes back through `engine_verbs`. diff --git a/crates/benilla-app/src/ui_chat/tests.rs b/crates/benilla-app/src/ui_chat/tests.rs index 195755f6..7bb9ecca 100644 --- a/crates/benilla-app/src/ui_chat/tests.rs +++ b/crates/benilla-app/src/ui_chat/tests.rs @@ -1437,7 +1437,11 @@ fn real_alias_table_resolves_the_shipped_commands() { ); } assert_eq!(parse_line("/macrohelp"), ParsedChat::MacroHelp); - assert_eq!(parse_line("/convertraid"), ParsedChat::ConvertRaid); + // 1.12 has none of these: `/convertraid` (the Raid tab's button converts), and benilla's + // `/reload` and `/errors`, which are its layer's `SlashCmdList` rows, not this table's. + for line in ["/convertraid", "/reload", "/errors", "/err"] { + assert_eq!(parse_line(line), ParsedChat::Unknown, "{line}"); + } // `/console` from a line that skipped the stock edit box forwards to the stock handler's verb. assert_eq!( parse_line("/console fpsJournal 1"), @@ -1476,9 +1480,8 @@ fn real_alias_table_resolves_the_shipped_commands() { } } // The shipped surface: 68 distinct aliases over 36 `SlashCmdList` indices and 225 emote - // commands over 169 `EmotesText` names (aliases repeat; EMOTE27 "UNUSED" has no row). Then - // benilla's own `/reload`, `/errors`, `/err` and `/convertraid`, which are not 1.12 commands, - // in every build, and 7 instrument aliases, in dev builds only. + // commands over 169 `EmotesText` names (aliases repeat; EMOTE27 "UNUSED" has no row), then 7 + // instrument aliases, in dev builds only. let instruments = if crate::run_mode::dev_affordances() { 7 } else { @@ -1486,8 +1489,8 @@ fn real_alias_table_resolves_the_shipped_commands() { }; assert_eq!( table.counts(), - (68, 225, 4, instruments), - "(slash, emote, benilla addition, instrument) aliases" + (68, 225, instruments), + "(slash, emote, instrument) aliases" ); } diff --git a/crates/benilla-app/src/ui_script/game_menu_tests.rs b/crates/benilla-app/src/ui_script/game_menu_tests.rs index cf5b07aa..e3a3e243 100644 --- a/crates/benilla-app/src/ui_script/game_menu_tests.rs +++ b/crates/benilla-app/src/ui_script/game_menu_tests.rs @@ -719,8 +719,8 @@ fn backpack() -> ContainerState { } } -/// The menu wears the options window's `ERA_WINDOW_SCALE` on show, as the era client draws the two -/// at one density; loaded with OptionsFrame.xml, which defines it. +/// The menu wears the options window's `BENILLA_ERA_WINDOW_SCALE` on show, as the era client draws +/// the two at one density; loaded with OptionsFrame.xml, which defines it. #[test] fn the_menu_rides_the_shared_era_window_scale() { let _data = benilla_formats::wow_data_or_skip!(); @@ -737,7 +737,7 @@ fn the_menu_rides_the_shared_era_window_scale() { s.resolve(); assert!(s.errors().is_empty(), "script errors: {:?}", s.errors()); let k = s.eval::("return GameMenuFrame:GetScale()").unwrap(); - let want = s.eval::("return ERA_WINDOW_SCALE").unwrap(); + let want = s.eval::("return BENILLA_ERA_WINDOW_SCALE").unwrap(); assert!((k - want).abs() < 1e-6, "menu scale {k} != knob {want}"); assert!((want - 0.78).abs() < 1e-6, "the knob itself moved: {want}"); } diff --git a/crates/benilla-app/src/ui_script/input.rs b/crates/benilla-app/src/ui_script/input.rs index c2f6897e..55a88a6b 100644 --- a/crates/benilla-app/src/ui_script/input.rs +++ b/crates/benilla-app/src/ui_script/input.rs @@ -47,6 +47,18 @@ impl PointerFeed<'_> { } } +/// The buttons the pointer feed hands the UI, by the names its handlers read (`arg1` of `OnClick`, +/// `OnMouseDown` and `OnMouseUp`, and the ones `RegisterForClicks` takes, +/// `Blizzard_BindingUI.xml:10`); buttons 4 and 5 on the physical buttons the binding chords call +/// BUTTON4 and BUTTON5 (`bindings::chord`). +const UI_MOUSE_BUTTONS: [(MouseButton, &str); 5] = [ + (MouseButton::Left, "LeftButton"), + (MouseButton::Right, "RightButton"), + (MouseButton::Middle, "MiddleButton"), + (MouseButton::Forward, "Button4"), + (MouseButton::Back, "Button5"), +]; + /// One `OnMouseWheel` call per whole notch, `arg1 = ±1`, the fraction carried in `notches`: a /// trackpad gesture arrives as a `Pixel` trickle, and the stock handlers act on the sign alone /// (`ScrollFrameTemplate_OnMouseWheel` scrolls half a pane per call, `UIPanelTemplates.lua:150`). @@ -162,11 +174,7 @@ pub(super) fn feed_ui_input( ); } } - for (btn, name) in [ - (MouseButton::Left, "LeftButton"), - (MouseButton::Right, "RightButton"), - (MouseButton::Middle, "MiddleButton"), - ] { + for (btn, name) in UI_MOUSE_BUTTONS { if buttons.just_pressed(btn) { // A left press that would drop into the world (`world_drop_click`: any payload over // terrain or nothing) is consumed now: the drop fires on the release, but the world @@ -390,6 +398,32 @@ mod tests { assert!(sum <= 1.0, "…and they may move at most one notch: {sum}"); } + /// Every button 1.12's frames take reaches the UI, under the name its binding chord agrees + /// with, so the Key Bindings page binds mouse 4 and 5 from a click. + #[test] + fn the_ui_takes_all_five_buttons_under_the_chords_names() { + let names: Vec<&str> = UI_MOUSE_BUTTONS.iter().map(|(_, n)| *n).collect(); + assert_eq!( + names, + [ + "LeftButton", + "RightButton", + "MiddleButton", + "Button4", + "Button5" + ] + ); + for (i, (button, _)) in UI_MOUSE_BUTTONS.iter().enumerate() { + let chord = crate::bindings::chord::Chord::parse(&format!("BUTTON{}", i + 1)); + assert_eq!( + chord.map(|c| c.key), + Some(crate::bindings::chord::BindKey::Mouse(*button)), + "BUTTON{} is {button:?}", + i + 1 + ); + } + } + #[test] fn a_line_notch_fires_once() { let mut s = wheel_pane(); diff --git a/crates/benilla-app/src/ui_script/keybindings_tests.rs b/crates/benilla-app/src/ui_script/keybindings_tests.rs index f1f44410..46ead745 100644 --- a/crates/benilla-app/src/ui_script/keybindings_tests.rs +++ b/crates/benilla-app/src/ui_script/keybindings_tests.rs @@ -11,17 +11,7 @@ use crate::bindings::commands::SPECS; /// does. pub(crate) fn harness() -> UiScript { let mut s = UiScript::new().unwrap(); - let cmds: Vec = SPECS - .iter() - .map(|spec| KeybindCommand { - name: spec.name, - category: spec.category, - run_on_up: spec.run_on_up(), - default1: spec.d1, - default2: spec.d2, - }) - .collect(); - s.register_bindings(&cmds); + register_specs(&mut s); s.set_screen_size(1024.0, 768.0); for file in [ "Interface\\FrameXML\\GlobalStrings.lua", @@ -54,10 +44,27 @@ pub(crate) fn harness() -> UiScript { s } +/// The host's command registry, as `seed_bindings_for_vm` registers it. +pub(crate) fn register_specs(s: &mut UiScript) { + let cmds: Vec = SPECS + .iter() + .map(|spec| KeybindCommand { + name: spec.name, + category: spec.category, + run_on_up: spec.run_on_up(), + default1: spec.d1, + default2: spec.d2, + }) + .collect(); + s.register_bindings(&cmds); +} + /// A label as the page resolves it: the GlobalStrings global named `token`, else `raw`. pub(crate) fn label(s: &UiScript, token: &str, raw: &str) -> String { - s.eval::(&format!(r#"return KeyBindings_String("{token}", "{raw}")"#)) - .unwrap() + s.eval::(&format!( + r#"return BenillaKeyBindings_String("{token}", "{raw}")"# + )) + .unwrap() } /// Open the options window on the Keybindings page. @@ -69,6 +76,53 @@ pub(crate) fn on_page(s: &mut UiScript) { const ROW: &str = "BenillaOptionsFrameContainerBodyKeybindingsRow"; +/// Whether the window takes keys and the wheel: a selected capsule arms it, as 1.12's +/// keyboard-enabled `KeyBindingFrame` (`Blizzard_BindingUI.xml:73`). +pub(crate) fn armed(s: &UiScript) -> bool { + s.eval::( + "return BenillaOptionsFrame:IsKeyboardEnabled() == 1 \ + and BenillaOptionsFrame:IsMouseWheelEnabled() == 1 \ + and BenillaKeyBindingsPage.selected ~= nil", + ) + .unwrap() +} + +/// A key press as the host feeds it (`ui_script::input`): ENTER, ESCAPE and TAB through the named +/// walk, every other key through the frame walk by its 1.12 name. Answers whether a frame took it. +pub(crate) fn press(s: &mut UiScript, key: &str) -> bool { + match key { + "ENTER" | "ESCAPE" | "TAB" => s.key_input(key), + _ => s.frame_key_input(key), + } +} + +/// A frame's centre in screen pixels, where the pointer feed hits it. +pub(crate) fn centre(s: &mut UiScript, frame: &str) -> (f32, f32) { + s.resolve(); + let (l, r, t, b, k): (f64, f64, f64, f64, f64) = s + .eval(&format!( + "local f = {frame} \ + local k = (f.GetEffectiveScale and f or f:GetParent()):GetEffectiveScale() \ + return f:GetLeft(), f:GetRight(), f:GetTop(), f:GetBottom(), k" + )) + .unwrap(); + (((l + r) * 0.5 * k) as f32, ((t + b) * 0.5 * k) as f32) +} + +/// A click of `button` on `frame` through the pointer feed. +pub(crate) fn click(s: &mut UiScript, frame: &str, button: &str) { + let (x, y) = centre(s, frame); + s.mouse_move(x, y); + s.mouse_button(x, y, button, true); + s.mouse_button(x, y, button, false); +} + +/// One wheel notch over the page, through the pointer feed. +pub(crate) fn wheel(s: &mut UiScript, delta: f32) { + let (x, y) = centre(s, "BenillaOptionsFrameContainerBodyKeybindings"); + s.mouse_wheel(x, y, delta); +} + #[test] fn the_page_is_an_options_category_with_the_collapsed_honest_tree() { benilla_formats::wow_data_or_skip!(); @@ -90,10 +144,12 @@ fn the_page_is_an_options_category_with_the_collapsed_honest_tree() { assert!(s .eval::("return BenillaOptionsFrameContainerDefaults:IsEnabled() ~= 0") .unwrap()); - // The categories in `Bindings.xml` order, each a header collapsed by default, as in the era. + // The categories in `Bindings.xml` order, each a header collapsed by default, as in the era; + // the spacer headers over multibars 2-4 continue the section above (the page's deviation). let mut expected: Vec<&str> = Vec::new(); for spec in SPECS { - if !expected.contains(&spec.category) { + if !expected.contains(&spec.category) && !spec.category.starts_with("BINDING_HEADER_BLANK") + { expected.push(spec.category); } } @@ -125,7 +181,7 @@ fn the_page_is_an_options_category_with_the_collapsed_honest_tree() { r#"BINDING_HEADER_MOVEMENT = "Movement Keys" BINDING_NAME_MOVEANDSTEER = "Move and Steer" KEY_BUTTON3 = "Middle Mouse" - KeyBindingsPage_Update()"#, + BenillaKeyBindingsPage_Update()"#, ) .unwrap(); assert_eq!( @@ -168,21 +224,23 @@ fn the_capture_flow_binds_steals_and_refuses_like_112() { on_page(&mut s); s.run(&format!("{ROW}1Header:Click()")).unwrap(); // expand Movement s.take_keybind_requests(); - // Row 3 is MOVEFORWARD (W, UP); selecting its Key 1 capsule arms the capture. - assert!(!s.bind_capture_armed()); + // Unarmed, the window takes no key: it reaches the game's bindings. + assert!(!armed(&s)); + assert!(!press(&mut s, "J"), "an unarmed page lets a key through"); + // Row 3 is MOVEFORWARD (W, UP); selecting its Key 1 capsule arms the window. s.run(&format!("{ROW}3Key1Button:Click()")).unwrap(); - assert!( - s.bind_capture_armed(), - "a selected capsule arms the capture" - ); + assert!(armed(&s), "a selected capsule arms the window"); assert!( s.eval::("return BenillaOptionsFrameContainerUnbind:IsEnabled() ~= 0") .unwrap(), "Unbind arms with the selection" ); + // A lone modifier is no key (`Blizzard_BindingUI.lua:172-174`): still armed. + assert!(press(&mut s, "SHIFT"), "the armed window takes it"); + assert!(armed(&s), "…and waits for a real key"); // J, a key no command holds, takes slot 1 from W; UP stays in slot 2 and the bind saves. - s.run(r#"KeyBindings_OnHostKey("J")"#).unwrap(); - assert!(!s.bind_capture_armed(), "a completed bind disarms"); + assert!(press(&mut s, "J"), "the armed window takes the key"); + assert!(!armed(&s), "a completed bind disarms"); assert!(s .eval::( r#"local k1, k2 = GetBindingKey("MOVEFORWARD"); return k1 == "J" and k2 == "UP""# @@ -202,7 +260,7 @@ fn the_capture_flow_binds_steals_and_refuses_like_112() { // T is ATTACKTARGET's only key, so taking it names the victim in red (1.12's // `KEY_UNBOUND_ERROR`, `Blizzard_BindingUI.lua:185-190`). s.run(&format!("{ROW}3Key2Button:Click()")).unwrap(); - s.run(r#"KeyBindings_OnHostKey("T")"#).unwrap(); + press(&mut s, "T"); assert_eq!( s.eval::(r#"return GetBindingAction("T")"#).unwrap(), "MOVEFORWARD" @@ -214,9 +272,28 @@ fn the_capture_flow_binds_steals_and_refuses_like_112() { .contains(&victim), "the newly-bare victim is named" ); + // A chord: the modifiers held at the press prefix the key, ALT-CTRL-SHIFT order + // (`Blizzard_BindingUI.lua:175-183`). + s.run(&format!("{ROW}3Key2Button:Click()")).unwrap(); + s.set_modifiers(true, true, false); + press(&mut s, "K"); + s.set_modifiers(false, false, false); + assert_eq!( + s.eval::(r#"return GetBindingAction("CTRL-SHIFT-K")"#) + .unwrap(), + "MOVEFORWARD" + ); + // ESC while armed binds Escape: 1.12's armed branch has no ESCAPE filter. + s.run(&format!("{ROW}3Key2Button:Click()")).unwrap(); + assert!(press(&mut s, "ESCAPE")); + assert_eq!( + s.eval::(r#"return GetBindingAction("ESCAPE")"#) + .unwrap(), + "MOVEFORWARD" + ); // The wheel binds onto a press+release command too: `0x4b7490` never reads the command. s.run(&format!("{ROW}3Key1Button:Click()")).unwrap(); - s.run(r#"KeyBindings_OnHostKey("MOUSEWHEELUP")"#).unwrap(); + wheel(&mut s, 1.0); assert!( s.eval::(r#"local k1 = GetBindingKey("MOVEFORWARD"); return k1 == "MOUSEWHEELUP""#) .unwrap(), @@ -234,7 +311,7 @@ fn the_capture_flow_binds_steals_and_refuses_like_112() { // A key string the engine rejects restores the slot's old key and shows the only refusal text // 1.12 has, the wheel's (`KeyBindingFrame_SetBinding`, `Blizzard_BindingUI.lua:260-270`). s.run(&format!("{ROW}3Key1Button:Click()")).unwrap(); - s.run(r#"KeyBindings_OnHostKey("SCROLLLOCK")"#).unwrap(); + press(&mut s, "SCROLLLOCK"); assert!( s.eval::(r#"local k1 = GetBindingKey("MOVEFORWARD"); return k1 == "MOUSEWHEELUP""#) .unwrap(), @@ -246,15 +323,130 @@ fn the_capture_flow_binds_steals_and_refuses_like_112() { "Can't bind mousewheel to actions with up and down states" ); s.run(&format!("{ROW}3Key1Button:Click()")).unwrap(); - assert!(s.bind_capture_armed()); + assert!(armed(&s)); s.run(&format!(r#"{ROW}3Key1Button:Click("RightButton")"#)) .unwrap(); - assert!(!s.bind_capture_armed(), "right-click deselects"); - // Hiding the window disarms: an armed capture with no window would swallow all input. + assert!(!armed(&s), "right-click deselects"); + // Hiding the window disarms. s.run(&format!("{ROW}3Key1Button:Click()")).unwrap(); - assert!(s.bind_capture_armed()); + assert!(armed(&s)); s.run("HideUIPanel(BenillaOptionsFrame)").unwrap(); - assert!(!s.bind_capture_armed(), "OnHide disarms"); + assert!(!armed(&s), "OnHide disarms"); + assert!(s.errors().is_empty(), "{:?}", s.errors()); +} + +/// Mouse buttons 3-5 bind through the clicks 1.12's window takes: a capsule's own `OnClick` +/// (`Blizzard_BindingUI.lua:246`) and the window's (`Blizzard_BindingUI.xml:538-540`), here the +/// page's `OnMouseUp`; left and right clicks only select. +#[test] +fn mouse_buttons_bind_through_the_capsules_and_the_page() { + benilla_formats::wow_data_or_skip!(); + let mut s = harness(); + on_page(&mut s); + s.run(&format!("{ROW}1Header:Click()")).unwrap(); // expand Movement + let capsule = format!("{ROW}3Key1Button"); + click(&mut s, &capsule, "LeftButton"); + assert!(armed(&s), "a left click selects"); + click(&mut s, &capsule, "MiddleButton"); + assert!(!armed(&s)); + assert_eq!( + s.eval::(r#"return GetBindingAction("BUTTON3")"#) + .unwrap(), + "MOVEFORWARD", + "a middle click on the armed capsule binds BUTTON3" + ); + // Over the page, away from any capsule: mouse 4 with SHIFT held. + click(&mut s, &capsule, "LeftButton"); + s.set_modifiers(true, false, false); + click(&mut s, &format!("{ROW}3Description"), "Button4"); + s.set_modifiers(false, false, false); + assert_eq!( + s.eval::(r#"return GetBindingAction("SHIFT-BUTTON4")"#) + .unwrap(), + "MOVEFORWARD" + ); + // Unarmed, a middle click on a capsule selects it, as 1.12's does. + click(&mut s, &format!("{ROW}4Key2Button"), "MiddleButton"); + assert!(armed(&s)); + s.run("BenillaKeyBindings_SetSelected(nil)").unwrap(); + assert!(s.errors().is_empty(), "{:?}", s.errors()); +} + +/// Loading the stock `Blizzard_BindingUI` (an addon manager's Load button does) defines its own +/// `KeyBindingButton_OnClick` and popup (`Blizzard_BindingUI.lua:228`, `:10`) and changes no other +/// window: the page keeps binding and leaving the character set. The stock window reads the 1.12 +/// list: a `HEADER_*` row draws a section header, a command's keys fill Key 1 and Key 2. +#[test] +fn the_page_works_after_the_stock_binding_ui_loads_and_the_stock_window_reads_the_list() { + benilla_formats::wow_data_or_skip!(); + let _l = crate::local_state::test_env::ENV_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let (mut s, failures) = + super::layer_tests::production_load_with("bindingui", false, "", |_| {}); + assert!(failures.is_empty(), "load failures: {failures:#?}"); + register_specs(&mut s); + s.run(r#"assert(LoadAddOn("Blizzard_BindingUI") == 1)"#) + .unwrap(); + assert!(s + .eval::("return KeyBindingFrame ~= nil and KeyBindingButton_OnClick ~= nil") + .unwrap()); + + on_page(&mut s); + s.run(&format!("{ROW}1Header:Click()")).unwrap(); // expand Movement + s.take_keybind_requests(); + click(&mut s, &format!("{ROW}3Key1Button"), "LeftButton"); + assert!(armed(&s), "the page's capsule still arms its own window"); + assert!( + s.eval::("return KeyBindingFrame.selected == nil") + .unwrap(), + "the stock window is untouched" + ); + press(&mut s, "J"); + assert_eq!( + s.eval::(r#"return GetBindingAction("J")"#).unwrap(), + "MOVEFORWARD" + ); + assert_eq!(s.take_keybind_requests(), vec![KeybindRequest::Save(1)]); + // The character set, both ways, through the page's own popup. + let check = "BenillaOptionsFrameContainerBodyKeybindingsCharacterRowCheck"; + s.run(&format!("{check}:Click()")).unwrap(); + assert_eq!(s.current_binding_set(), 2); + s.run(&format!("{check}:Click()")).unwrap(); + s.run("StaticPopup1Button1:Click()").unwrap(); + assert_eq!( + s.current_binding_set(), + 1, + "leaving the character set works" + ); + assert!(s.errors().is_empty(), "{:?}", s.errors()); + s.run("HideUIPanel(BenillaOptionsFrame)").unwrap(); + + // The stock window over the same list. + s.run("ShowUIPanel(KeyBindingFrame)").unwrap(); + assert!(s + .eval::("return KeyBindingFrame:IsVisible()") + .unwrap()); + assert!(s + .eval::( + "return KeyBindingFrameBinding1Header:IsVisible() \ + and KeyBindingFrameBinding1Header:GetText() == BINDING_HEADER_MOVEMENT" + ) + .unwrap()); + assert_eq!( + s.eval::("return KeyBindingFrameBinding2Key1Button:GetText()") + .unwrap(), + s.eval::(r#"return GetBindingText("BUTTON3", "KEY_")"#) + .unwrap(), + "Move and Steer's first key in the Key 1 column" + ); + assert_eq!( + s.eval::("return KeyBindingFrameBinding3Key2Button:GetText()") + .unwrap(), + s.eval::(r#"return GetBindingText("UP", "KEY_")"#) + .unwrap(), + "Move Forward's second key in the Key 2 column" + ); assert!(s.errors().is_empty(), "{:?}", s.errors()); } @@ -283,7 +475,7 @@ fn unbind_reset_and_the_live_commit_replace_okay_cancel() { // Deviation: a bind saves at once, as the era panel commits, where 1.12 had Okay and Cancel; // closing the window keeps it. s.run(&format!("{ROW}9Key1Button:Click()")).unwrap(); - s.run(r#"KeyBindings_OnHostKey("G")"#).unwrap(); + press(&mut s, "G"); assert_eq!(s.take_keybind_requests(), vec![KeybindRequest::Save(1)]); s.run("BenillaOptionsFrameCloseButton:Click()").unwrap(); assert_eq!( @@ -384,8 +576,8 @@ fn search_surfaces_bindings_as_live_rows_under_the_redirect_head() { .unwrap()); s.run("BenillaOptionsFrameContainerBodyKeybindSearch1Key1Button:Click()") .unwrap(); - assert!(s.bind_capture_armed()); - s.run(r#"KeyBindings_OnHostKey("H")"#).unwrap(); + assert!(armed(&s)); + press(&mut s, "H"); assert_eq!( s.eval::(r#"return GetBindingAction("H")"#).unwrap(), "JUMP" @@ -474,8 +666,8 @@ fn the_wheel_bubbles_from_the_rows_and_the_bar_rides_the_gutter() { on_page(&mut s); // Every section open: over 100 rows overflow the list's 19 slots. s.run( - r#"for i = 1, table.getn(KeyBindingsPage.sections) do KeyBindings_ExpandSection(i, true) end - KeyBindingsPage_Update()"#, + r#"for i = 1, table.getn(BenillaKeyBindingsPage.sections) do BenillaKeyBindings_ExpandSection(i, true) end + BenillaKeyBindingsPage_Update()"#, ) .unwrap(); const SF: &str = "BenillaOptionsFrameContainerBodyKeybindingsScrollFrame"; @@ -536,8 +728,8 @@ fn the_wheel_bubbles_from_the_rows_and_the_bar_rides_the_gutter() { ); let set_all = |s: &UiScript, open: bool| { s.run(&format!( - "for i = 1, table.getn(KeyBindingsPage.sections) do KeyBindings_ExpandSection(i, {}) end - KeyBindingsPage_Update()", + "for i = 1, table.getn(BenillaKeyBindingsPage.sections) do BenillaKeyBindings_ExpandSection(i, {}) end + BenillaKeyBindingsPage_Update()", if open { "true" } else { "nil" } )) .unwrap(); @@ -603,9 +795,11 @@ fn the_wheel_bubbles_from_the_rows_and_the_bar_rides_the_gutter() { after_capsule > after_name, "a spin over a CAPSULE bubbles to the page too: {after_name} -> {after_capsule}" ); - // While a capsule is armed a spin binds, never scrolls; the page's guard catches a spin the - // host lets through. + // While a capsule is armed a spin binds, never scrolls. s.run(&format!("{ROW}2Key1Button:Click()")).unwrap(); + let armed_command = s + .eval::(&format!("return {ROW}2Key1Button.commandName")) + .unwrap(); s.mouse_wheel(wx, wy, -1.0); assert_eq!( s.eval::(&format!("return FauxScrollFrame_GetOffset({SF})")) @@ -613,7 +807,12 @@ fn the_wheel_bubbles_from_the_rows_and_the_bar_rides_the_gutter() { after_capsule, "armed: the wheel must not scroll" ); - s.run("KeyBindings_SetSelected(nil)").unwrap(); + assert_eq!( + s.eval::(r#"return GetBindingAction("MOUSEWHEELDOWN")"#) + .unwrap(), + armed_command, + "armed: the spin binds" + ); assert!(s.errors().is_empty(), "{:?}", s.errors()); } @@ -626,11 +825,14 @@ fn the_pet_lane_is_registered_under_the_action_bar_header() { let mut s = harness(); on_page(&mut s); + // The header row a command sits under: the last `HEADER_*` row above it. let category = |s: &mut UiScript, name: &str| { s.eval::(&format!( - r#"for i = 1, GetNumBindings() do - local n, c = GetBinding(i) - if n == "{name}" then return c end + r#"local header + for i = 1, GetNumBindings() do + local n = GetBinding(i) + if strsub(n, 1, 6) == "HEADER" then header = n end + if n == "{name}" then return header end end"# )) .unwrap() @@ -639,7 +841,7 @@ fn the_pet_lane_is_registered_under_the_action_bar_header() { let name = format!("BONUSACTIONBUTTON{i}"); assert_eq!( category(&mut s, &name), - "BINDING_HEADER_ACTIONBAR", + "HEADER_ACTIONBAR", "{name} files under the action bar, as 1.12 does" ); let key = s diff --git a/crates/benilla-app/src/ui_script/layer_tests.rs b/crates/benilla-app/src/ui_script/layer_tests.rs index f0576e65..f50e2be9 100644 --- a/crates/benilla-app/src/ui_script/layer_tests.rs +++ b/crates/benilla-app/src/ui_script/layer_tests.rs @@ -236,8 +236,8 @@ fn the_stock_ui_switch_is_a_dev_builds_alone() { assert!(layer_enabled_by(true, None) && layer_enabled_by(true, Some("0"))); } -/// Booted without the layer, as `WOW_STOCK_UI=1` does: no layer file loads, the stock files raise -/// nothing, and the host's calls into the layer are no-ops. +/// Booted without the layer, as `WOW_STOCK_UI=1` does: no layer file loads and the stock files +/// raise nothing. #[test] fn the_stock_ui_switch_boots_the_core_without_the_layer() { benilla_formats::wow_data_or_skip!(); @@ -250,7 +250,7 @@ fn the_stock_ui_switch_boots_the_core_without_the_layer() { // One global from each layer file that defines a new one. for name in [ "BenillaScrollBar_Step", - "KeyBindings_OnHostKey", + "BenillaKeyBindings_OnKeyDown", "BenillaOptionsFrame_SelectCategory", "BenillaGameMenuButtonEditMode", "BENILLA_BAG_WAS_OPEN", @@ -263,13 +263,233 @@ fn the_stock_ui_switch_boots_the_core_without_the_layer() { } assert!(!s.eval::("return BenillaOptionsFrame ~= nil").unwrap()); assert!(!s.eval::("return ZZORDER_SAW_LAYER").unwrap()); - // The host's calls into the layer. - for body in [ - super::ERRORS_TOGGLE.to_string(), - super::ERRORS_CLEAR.to_string(), - super::host_key_capture("CTRL-J"), - ] { - s.run(&body).unwrap_or_else(|e| panic!("{body}: {e}")); + assert!(s.errors().is_empty(), "script errors: {:?}", s.errors()); +} + +/// Types `line` into the chat box and sends it through the stock `ChatEdit_ParseText` +/// (`ChatFrame.lua:2087`), counting the `HELP_TEXT_SIMPLE` lines it prints. +fn type_line(s: &UiScript, line: &str) -> i64 { + s.run(&format!( + r#"HELPS = 0 + local box = ChatFrameEditBox + box.chatFrame = box.chatFrame or DEFAULT_CHAT_FRAME + local frame = box.chatFrame + local add = frame.AddMessage + frame.AddMessage = function(f, text, r, g, b, id) + if text == HELP_TEXT_SIMPLE then HELPS = HELPS + 1 end + return add(f, text, r, g, b, id) + end + box:SetText({line:?}) + ChatEdit_ParseText(box, 1) + frame.AddMessage = add"# + )) + .unwrap_or_else(|e| panic!("{line}: {e}")); + s.eval("return HELPS").unwrap() +} + +/// The layer's player commands are plain `SlashCmdList` rows: `/reload` runs `ReloadUI()`, +/// `/errors` (`/err`) toggles the log and `/errors clear` empties it. 1.12 has no `/convertraid` +/// (the Raid tab's button converts, `RaidFrame.lua:63-71`), so it answers `HELP_TEXT_SIMPLE`, and +/// the forwarding hook `SubmitChatInput` is gone. +#[test] +fn the_layers_commands_are_slash_rows_and_convertraid_is_unknown() { + benilla_formats::wow_data_or_skip!(); + let _l = ENV_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let (mut s, failures) = production_load("slash", false); + assert!(failures.is_empty(), "load failures: {failures:#?}"); + assert!(s.eval::("return SubmitChatInput == nil").unwrap()); + + s.take_session_requests(); + assert_eq!(type_line(&s, "/reload"), 0); + assert_eq!( + s.take_session_requests(), + [benilla_ui::script::SessionRequest::ReloadUi], + "/reload is ReloadUI()" + ); + assert!( + s.take_chat_input().is_empty(), + "nothing is handed to the host" + ); + + assert_eq!(type_line(&s, "/errors"), 0); + assert!(s + .eval::("return BenillaScriptLogFrame:IsVisible()") + .unwrap()); + assert_eq!(type_line(&s, "/err"), 0); + assert!(!s + .eval::("return BenillaScriptLogFrame:IsVisible()") + .unwrap()); + s.run("BenillaScriptLog_Record('kept')").unwrap(); + assert_eq!(type_line(&s, "/errors clear "), 0); + assert_eq!( + s.eval::("return table.getn(BenillaScriptLog.rows)") + .unwrap(), + 0, + "/errors clear empties the log" + ); + + assert_eq!(type_line(&s, "/convertraid"), 1, "the unknown-command line"); + assert!(s.take_chat_input().is_empty()); + assert!(s.errors().is_empty(), "script errors: {:?}", s.errors()); +} + +/// In a player build the dev instruments are no `SlashCmdList` row, so a typed one answers +/// `HELP_TEXT_SIMPLE` once and hands the host nothing to re-run; in a dev build each is a host row +/// that hands the host its line. +#[test] +fn the_dev_commands_are_host_rows_in_a_dev_build_alone() { + benilla_formats::wow_data_or_skip!(); + let _l = ENV_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let lines = [ + "/chattest", + "/shot", + "/liquid", + "/castvis", + "/partytest ping", + "/reaction", + "/react Probe", + ]; + + let (mut s, failures) = production_load("devcmds-player", false); + assert!(failures.is_empty(), "load failures: {failures:#?}"); + crate::ui_chat::commands::register_dev_commands(&s, false); + for line in lines { + assert_eq!( + type_line(&s, line), + 1, + "{line}: the unknown-command line, once" + ); + assert!( + s.take_chat_input().is_empty(), + "{line}: nothing queued to loop" + ); + let cmd = line[1..].split(' ').next().unwrap(); + assert!(!s.has_slash_command(cmd), "{cmd} is no SlashCmdList row"); + } + drop(s); + + let (mut s, failures) = production_load("devcmds-dev", false); + assert!(failures.is_empty(), "load failures: {failures:#?}"); + crate::ui_chat::commands::register_dev_commands(&s, true); + for line in lines { + assert_eq!(type_line(&s, line), 0, "{line}"); + let queued = s.take_chat_input(); + assert_eq!(queued.len(), 1, "{line}: {queued:?}"); + // An alias queues under the command's first one, as the row's handler knows only that. + let want = line.replacen("/react ", "/reaction ", 1); + assert_eq!(queued[0].trim(), want, "{line}"); } assert!(s.errors().is_empty(), "script errors: {:?}", s.errors()); } + +/// The `/errors` log collects off the Lua error handler, ahead of the stock `_ERRORMESSAGE` +/// (`BasicControls.xml:16`), which still shows the `ScriptErrors` dialog: an addon's runtime error, +/// an error in one of the layer's own handlers, and one raised in the stock load before the layer +/// loaded (the engine hands caught errors to the handler after the load). +#[test] +fn the_error_log_collects_off_the_error_handler() { + benilla_formats::wow_data_or_skip!(); + let _l = ENV_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let before = r#" + local pre = CreateFrame("Button", "ZZPreLayer") + pre:SetScript("OnClick", function() error("before the layer") end) + pre:Click() + "#; + let (mut s, failures) = production_load_with("errlog", false, before, |root| { + let dir = root.join("ZZErr"); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write(dir.join("ZZErr.toc"), "## Interface: 11200\nerr.lua\n").unwrap(); + std::fs::write( + dir.join("err.lua"), + "ZZErrButton = CreateFrame('Button', 'ZZErrButton')\n\ + ZZErrButton:SetScript('OnClick', function() error('an addon boom') end)\n", + ) + .unwrap(); + }); + assert!( + failures.iter().all(|f| f.contains("before the layer")), + "load failures: {failures:#?}" + ); + s.run("ZZErrButton:Click()").unwrap(); + // A layer handler raising: the window's OnShow calls this. + s.run("BenillaOptionsFrame_UpdateScale = function() error('a layer boom') end") + .unwrap(); + s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap(); + s.dispatch_script_errors_to_handler(); + + let rows: Vec = s + .eval( + "local out = {} \ + for _, row in ipairs(BenillaScriptLog.rows) do table.insert(out, row.message) end \ + return out", + ) + .unwrap(); + for want in ["before the layer", "an addon boom", "a layer boom"] { + assert!( + rows.iter().any(|r| r.contains(want)), + "{want:?} is in the log: {rows:#?}" + ); + } + assert!( + s.eval::("return ScriptErrors:IsVisible()").unwrap(), + "the stock handler still runs after the log" + ); + let shown: String = s.eval("return ScriptErrors_Message:GetText()").unwrap(); + assert!( + shown.contains("before the layer"), + "the first error shows: {shown}" + ); + // A repeat is one row with a count. + s.run("ZZErrButton:Click() ZZErrButton:Click()").unwrap(); + s.dispatch_script_errors_to_handler(); + assert_eq!( + s.eval::( + "for _, row in ipairs(BenillaScriptLog.rows) do \ + if strfind(row.message, 'an addon boom', 1, 1) then return row.count end \ + end" + ) + .unwrap(), + 3 + ); +} + +/// Every global the layer defines takes the `Benilla` prefix (a slash alias its `SLASH_BENILLA_` +/// form, as `ChatEdit_ParseText` reads it), so no layer name meets a stock or an addon one; a stock +/// global the layer redefines keeps its name and is not a new definition here. +#[test] +fn every_global_the_layer_defines_takes_the_benilla_prefix() { + benilla_formats::wow_data_or_skip!(); + let _l = ENV_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let (s, failures) = production_load("prefix", false); + assert!(failures.is_empty(), "load failures: {failures:#?}"); + let log: Vec = s.eval("return DEFINE_LOG").unwrap(); + let start = defined_at(&log, "BenillaScrollBar_Step"); + let end = defined_at(&log, "ZZORDER_SAW_LAYER"); + let dispatch = |n: &str| { + n == "this" + || n == "event" + || n.strip_prefix("arg") + .is_some_and(|d| !d.is_empty() && d.bytes().all(|b| b.is_ascii_digit())) + }; + let unprefixed: Vec<&String> = log[start..end] + .iter() + .filter(|n| { + !(n.starts_with("Benilla") + || n.starts_with("BENILLA_") + || n.starts_with("SLASH_BENILLA_")) + && !dispatch(n) + }) + .collect(); + assert!( + unprefixed.is_empty(), + "layer globals without the Benilla prefix: {unprefixed:?}" + ); +} diff --git a/crates/benilla-app/src/ui_script/lifecycle.rs b/crates/benilla-app/src/ui_script/lifecycle.rs index 31c48d8d..58e1841f 100644 --- a/crates/benilla-app/src/ui_script/lifecycle.rs +++ b/crates/benilla-app/src/ui_script/lifecycle.rs @@ -367,6 +367,7 @@ pub(crate) fn load_ingame_ui_on_world_entry(world: &mut World) { // No sound during the load edge, as in the reference's `UI_Init` (`0x48fbfa` → `0x49016d`). silenced_ui_load(&mut script, |script| { let _ = load_ingame_ui(script, identity.as_ref(), &roster, version_check); + crate::ui_chat::commands::register_dev_commands(script, crate::run_mode::dev_affordances()); // The new Minimap's zoom indices from the persisted CVars, as the reference's minimap reset // copies each CVar into its live index; `Minimap:SetZoom` owns them from here. script.set_minimap_zoom(zoom.0, zoom.1); @@ -385,25 +386,22 @@ pub(crate) fn load_ingame_ui_on_world_entry(world: &mut World) { }, ); }); - // Deviation: failed addons are reported in chat, because otherwise an addon that does not - // load says nothing on screen. Counted from the deduplicated diagnostics log as `/errors` - // lists them, after the whole load edge (handlers included); the VM is fresh, so every `Load` - // row is this load's. - let failed = script - .diagnostics() - .iter() - .filter(|d| d.kind == benilla_ui::script::diagnostics::DiagnosticKind::Load) - .count(); - if failed > 0 { - if let Some(mut chat) = world.get_resource_mut::() { + // Deviation: each failed addon file is reported in chat, one line apiece, because otherwise + // an addon that does not load says nothing on screen: a failure that never raised reaches no + // error handler, so the `/errors` log cannot hold it. Read from the engine's deduplicated + // record after the whole load edge (handlers included); the VM is fresh, so every `Load` row + // is this load's. + if let Some(mut chat) = world.get_resource_mut::() { + for row in script + .diagnostics() + .iter() + .filter(|d| d.kind == benilla_ui::script::diagnostics::DiagnosticKind::Load) + { + let head = row.message.lines().next().unwrap_or_default(); // Queued: the chat feed drains `ChatLog` once the UI is up. chat.push_event(crate::ui_chat::ChatEvent::text_only( crate::ui_chat::ChatEventKind::System, - format!( - "{failed} addon load {} — type /errors to see {}.", - if failed == 1 { "failure" } else { "failures" }, - if failed == 1 { "it" } else { "them" } - ), + format!("Addon load failure: {head}"), )); } } diff --git a/crates/benilla-app/src/ui_script/manifest.rs b/crates/benilla-app/src/ui_script/manifest.rs index f27aed0a..2913cb14 100644 --- a/crates/benilla-app/src/ui_script/manifest.rs +++ b/crates/benilla-app/src/ui_script/manifest.rs @@ -26,19 +26,6 @@ pub(super) fn layer_enabled_by(dev_build: bool, stock_ui: Option<&str>) -> bool !(dev_build && stock_ui == Some("1")) } -/// `/errors`: toggles the layer's script error log; a no-op when the run boots without the layer. -pub(crate) const ERRORS_TOGGLE: &str = - "if BenillaScriptLog_Toggle then BenillaScriptLog_Toggle() end"; - -/// `/errors clear`: empties the layer's script error log, or does nothing without the layer. -pub(crate) const ERRORS_CLEAR: &str = "if BenillaScriptLog_Clear then BenillaScriptLog_Clear() end"; - -/// Hands a chord the layer's Keybindings page is capturing (`BenillaBindCapture`), a bind string -/// such as `CTRL-J`, to its handler; a no-op without the layer. -pub(crate) fn host_key_capture(chord: &str) -> String { - format!("if KeyBindings_OnHostKey then KeyBindings_OnHostKey(\"{chord}\") end") -} - /// Every entry in load order: the core's rows, none without an install, then the layer's. #[cfg(test)] pub(super) fn manifest_files() -> Vec { diff --git a/crates/benilla-app/src/ui_script/mod.rs b/crates/benilla-app/src/ui_script/mod.rs index fcbfa2bb..7d9f5909 100644 --- a/crates/benilla-app/src/ui_script/mod.rs +++ b/crates/benilla-app/src/ui_script/mod.rs @@ -34,7 +34,6 @@ pub(crate) use session::VmMemo; // Not test-only: the addon harness loads the whole shipped interface under each addon. pub(crate) use manifest::load_default_ui; pub(crate) use manifest::load_ingame_ui; -pub(crate) use manifest::{host_key_capture, ERRORS_CLEAR, ERRORS_TOGGLE}; /// Whether the pointer is over any UI (the egui dev overlay or a player-UI frame), combined by /// [`arbitrate_pointer_over_ui`]; gameplay reads it, so it is not the dev plugin's. diff --git a/crates/benilla-app/src/ui_script/options_tests.rs b/crates/benilla-app/src/ui_script/options_tests.rs index c366fa35..29028eee 100644 --- a/crates/benilla-app/src/ui_script/options_tests.rs +++ b/crates/benilla-app/src/ui_script/options_tests.rs @@ -173,7 +173,7 @@ fn the_selected_row_wears_the_gold_wash_and_hover_runs_blue() { benilla_formats::wow_data_or_skip!(); let mut s = harness(); // Scale 1, so rects and the pointer share coordinates; at 1024x768 the fit clamp stays above 1. - s.run("ERA_WINDOW_SCALE = 1").unwrap(); + s.run("BENILLA_ERA_WINDOW_SCALE = 1").unwrap(); s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap(); assert_eq!( @@ -275,7 +275,7 @@ fn the_selected_row_wears_the_gold_wash_and_hover_runs_blue() { fn the_ground_dim_draws_over_the_tile_and_clear_of_the_rope() { benilla_formats::wow_data_or_skip!(); let mut s = harness(); - s.run("ERA_WINDOW_SCALE = 1").unwrap(); + s.run("BENILLA_ERA_WINDOW_SCALE = 1").unwrap(); s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap(); s.resolve(); @@ -514,7 +514,7 @@ fn a_track_press_seats_the_thumb_and_keeps_dragging() { benilla_formats::wow_data_or_skip!(); let mut s = harness_on(audio_harness()); s.set_cvar_host("MasterVolume", "0.1"); - s.run("ERA_WINDOW_SCALE = 1").unwrap(); // pointer and rects share coordinates + s.run("BENILLA_ERA_WINDOW_SCALE = 1").unwrap(); // pointer and rects share coordinates s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap(); s.run("BenillaOptionsFrameCategoryListRowAudio:Click()") .unwrap(); @@ -1839,7 +1839,7 @@ fn defaults_resets_the_controls_page_to_registered_defaults() { assert!(s.errors().is_empty(), "script errors: {:?}", s.errors()); } -/// Run a few frames so the page body's fit converges: `OptionsScroll_Fit` reads the previous +/// Run a few frames so the page body's fit converges: `BenillaOptionsScroll_Fit` reads the previous /// frame's rects. fn settle(s: &mut UiScript) { for _ in 0..4 { @@ -1942,11 +1942,14 @@ fn a_broad_search_scrolls_the_page_instead_of_overflowing_it() { clip.bottom ); - // Widget coordinates here, not the extract's pixels: the window carries `ERA_WINDOW_SCALE`. + // Widget coordinates here, not the extract's pixels: the window carries `BENILLA_ERA_WINDOW_SCALE`. let sf_bottom: f32 = s .eval("return BenillaOptionsFrameContainerScroll:GetBottom()") .unwrap(); - let tail = |s: &UiScript| -> f32 { s.eval("return OptionsScroll_ContentBottom()").unwrap() }; + let tail = |s: &UiScript| -> f32 { + s.eval("return BenillaOptionsScroll_ContentBottom()") + .unwrap() + }; assert!( tail(&s) < sf_bottom, "the tail starts below the fold ({} vs {sf_bottom})", @@ -2068,7 +2071,7 @@ fn the_page_scroll_bar_wears_the_trough_with_its_arrows_in_the_sockets() { // ── The row tooltips ───────────────────────────────────────────────────────────── -/// Hover a row's label half, 60 units in at mid-height. Callers pin `ERA_WINDOW_SCALE = 1` first, +/// Hover a row's label half, 60 units in at mid-height. Callers pin `BENILLA_ERA_WINDOW_SCALE = 1` first, /// so the row's rect and the pointer share coordinates. fn hover_label(s: &mut UiScript, frame: &str) { s.resolve(); @@ -2112,7 +2115,7 @@ fn a_hovered_row_raises_its_1_12_description_on_the_era_seat() { s.set_text_measurer(Box::new(super::FixedWidthFont(6.0))); s.run("OPTION_TOOLTIP_GAMEFIELD_DESELECT = \"Checking this will prevent the deselection.\"") .unwrap(); - s.run("ERA_WINDOW_SCALE = 1").unwrap(); + s.run("BENILLA_ERA_WINDOW_SCALE = 1").unwrap(); s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap(); let row = "BenillaOptionsFrameContainerBodyControlsRowStickyTarget"; @@ -2181,7 +2184,7 @@ fn a_row_with_no_1_12_string_raises_no_plate() { let mut s = harness_on(audio_harness()); s.run("OPTION_TOOLTIP_GAMEFIELD_DESELECT = \"Sticky's own description.\"") .unwrap(); - s.run("ERA_WINDOW_SCALE = 1").unwrap(); + s.run("BENILLA_ERA_WINDOW_SCALE = 1").unwrap(); s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap(); hover_label( &mut s, @@ -2201,10 +2204,10 @@ fn a_row_with_no_1_12_string_raises_no_plate() { ); // Sticky's plate up, then straight into the mute row with no OnLeave in between. - s.run("OptionsRow_Hover(BenillaOptionsFrameContainerBodyControlsRowStickyTarget, 1)") + s.run("BenillaOptionsRow_Hover(BenillaOptionsFrameContainerBodyControlsRowStickyTarget, 1)") .unwrap(); assert!(s.eval::("return GameTooltip:IsVisible()").unwrap()); - s.run("OptionsRow_Hover(BenillaOptionsFrameContainerBodyControlsRowAutoLoot, 1)") + s.run("BenillaOptionsRow_Hover(BenillaOptionsFrameContainerBodyControlsRowAutoLoot, 1)") .unwrap(); assert!( !s.eval::("return GameTooltip:IsVisible()").unwrap(), @@ -2232,7 +2235,7 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() { let listing: String = s .eval( "local out = {} \ - for page, rows in pairs(OPTIONS_PAGE_ROWS) do \ + for page, rows in pairs(BENILLA_OPTIONS_PAGE_ROWS) do \ for _, rkey in ipairs(rows) do \ local row = getglobal(\"BenillaOptionsFrameContainerBody\" .. page .. rkey) \ table.insert(out, page .. rkey .. \"=\" .. (row.tip or \"\")) \ @@ -2320,7 +2323,7 @@ fn every_flavor_of_row_raises_its_plate_from_the_page_it_lives_on() { let mut s = harness_on(audio_harness()); // Stand-in strings for every key the rows name. s.run( - "for page, rows in pairs(OPTIONS_PAGE_ROWS) do \ + "for page, rows in pairs(BENILLA_OPTIONS_PAGE_ROWS) do \ for _, rkey in ipairs(rows) do \ local row = getglobal(\"BenillaOptionsFrameContainerBody\" .. page .. rkey) \ if row.tip then setglobal(row.tip, \"described: \" .. rkey) end \ @@ -2328,7 +2331,7 @@ fn every_flavor_of_row_raises_its_plate_from_the_page_it_lives_on() { end", ) .unwrap(); - s.run("ERA_WINDOW_SCALE = 1").unwrap(); + s.run("BENILLA_ERA_WINDOW_SCALE = 1").unwrap(); s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap(); let mut raised = 0; @@ -2346,7 +2349,7 @@ fn every_flavor_of_row_raises_its_plate_from_the_page_it_lives_on() { .unwrap(); let rows: String = s .eval(&format!( - "return table.concat(OPTIONS_PAGE_ROWS.{page}, \",\")" + "return table.concat(BENILLA_OPTIONS_PAGE_ROWS.{page}, \",\")" )) .unwrap(); for rkey in rows.split(',') { @@ -2387,7 +2390,7 @@ fn every_flavor_of_row_raises_its_plate_from_the_page_it_lives_on() { } /// 1.12's AdvancedOptionsCombatText box as saved-global rows: a click writes the global, never a -/// CVar, and runs the family's apply, `CombatText_UpdateOrLoad`. +/// CVar, and runs the family's apply, `BenillaCombatText_UpdateOrLoad`. #[test] fn the_combat_page_writes_saved_variable_globals_and_applies_them() { benilla_formats::wow_data_or_skip!(); @@ -3236,7 +3239,7 @@ fn the_defaults_button_is_armed_by_rows_not_by_a_category() { s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap(); let keys: Vec = s - .eval::("return table.concat(OPTIONS_CATEGORY_KEYS, \",\")") + .eval::("return table.concat(BENILLA_OPTIONS_CATEGORY_KEYS, \",\")") .unwrap() .split(',') .map(str::to_string) @@ -3245,8 +3248,8 @@ fn the_defaults_button_is_armed_by_rows_not_by_a_category() { for key in &keys { let has_rows = s .eval::(&format!( - "return OPTIONS_PAGE_ROWS[\"{key}\"] ~= nil and \ - getn(OPTIONS_PAGE_ROWS[\"{key}\"]) > 0" + "return BENILLA_OPTIONS_PAGE_ROWS[\"{key}\"] ~= nil and \ + getn(BENILLA_OPTIONS_PAGE_ROWS[\"{key}\"]) > 0" )) .unwrap(); assert!( @@ -3966,7 +3969,7 @@ fn the_two_option_tabs_fit_their_labels_at_the_drawn_scale() { assert_eq!( num(&mut s, "BenillaOptionsFrame:GetScale()"), 0.78, - "ERA_WINDOW_SCALE" + "BENILLA_ERA_WINDOW_SCALE" ); for tab in ["BenillaOptionsFrameGameTab", "BenillaOptionsFrameAddOnsTab"] { @@ -4156,7 +4159,7 @@ fn every_registered_reference_cvar_has_a_row_on_our_own_window() { let ours: std::collections::HashSet = s .eval::>( "local out = {} \ - for page, rows in pairs(OPTIONS_PAGE_ROWS) do \ + for page, rows in pairs(BENILLA_OPTIONS_PAGE_ROWS) do \ for _, rkey in ipairs(rows) do \ local row = getglobal(\"BenillaOptionsFrameContainerBody\" .. page .. rkey) \ if row.cvar then table.insert(out, row.cvar) end \ @@ -4396,7 +4399,7 @@ fn the_guild_line_greys_with_player_names_and_the_follow_speed_with_the_style() .unwrap() }; assert!(speed_thumb(&mut s)); - s.run("SetCVar(\"cameraSmoothStyle\", \"0\") OptionsPage_Refresh(\"Controls\")") + s.run("SetCVar(\"cameraSmoothStyle\", \"0\") BenillaOptionsPage_Refresh(\"Controls\")") .unwrap(); assert!( !speed_thumb(&mut s), @@ -4410,7 +4413,7 @@ fn the_guild_line_greys_with_player_names_and_the_follow_speed_with_the_style() :IsMouseEnabled()" ) .unwrap()); - s.run("SetCVar(\"cameraSmoothStyle\", \"1\") OptionsPage_Refresh(\"Controls\")") + s.run("SetCVar(\"cameraSmoothStyle\", \"1\") BenillaOptionsPage_Refresh(\"Controls\")") .unwrap(); assert!(speed_thumb(&mut s)); assert!(s.errors().is_empty(), "script errors: {:?}", s.errors()); diff --git a/crates/benilla-app/src/ui_script/shipped_xml_tests.rs b/crates/benilla-app/src/ui_script/shipped_xml_tests.rs index ebd21a28..800d1b74 100644 --- a/crates/benilla-app/src/ui_script/shipped_xml_tests.rs +++ b/crates/benilla-app/src/ui_script/shipped_xml_tests.rs @@ -1964,3 +1964,75 @@ fn the_layer_does_not_grow() { "assets/ui holds the layer's manifest and its files, nothing else" ); } + +/// The layer speaks the 1.12 dialect: a handler body is the chunk itself (`0x704c70`), so it reads +/// its frame from `this`, never `self`; the loader reads no `function=`; the strata table +/// (`0x8119f8`) has no `BLIZZARD`; `SetDrawLayer` (`0x79a780`) takes a layer name alone; and a +/// button's state textures take a texture, never a colour (`0x781970`). +#[test] +fn the_layer_speaks_the_1_12_dialect() { + fn handlers(el: &benilla_ui::framexml::Element, out: &mut Vec<(String, String)>) { + if el.tag == "Scripts" { + for h in &el.children { + out.push((h.tag.clone(), h.body.clone())); + assert!( + h.attr("function").is_none(), + "<{} function=…> is no 1.12 form", + h.tag + ); + } + } + for c in &el.children { + handlers(c, out); + } + } + let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui"); + let files = super::addons::Addon::layer().toc.files; + assert!(files.len() >= 8, "the layer's toc lists {files:?}"); + let word = |text: &str, w: &str| { + text.match_indices(w).any(|(i, _)| { + let before = text[..i].chars().next_back(); + let after = text[i + w.len()..].chars().next(); + !before.is_some_and(|c| c.is_alphanumeric() || c == '_') + && !after.is_some_and(|c| c.is_alphanumeric() || c == '_') + }) + }; + for file in files { + let text = std::fs::read_to_string(dir.join(&file)).expect("read"); + let doc = benilla_ui::framexml::parse(&text).expect("parses"); + let mut found = Vec::new(); + for item in &doc.items { + use benilla_ui::framexml::TopLevel; + if let TopLevel::Template(el) | TopLevel::Instance(el) = item { + handlers(el, &mut found); + } + } + for (tag, body) in found { + assert!(!word(&body, "self"), "{file}: <{tag}> reads `self`: {body}"); + } + assert!(!text.contains("BLIZZARD"), "{file}: BLIZZARD strata"); + for line in text.lines().filter(|l| l.contains("SetDrawLayer(")) { + assert_eq!( + line.matches(',').count(), + 0, + "{file}: SetDrawLayer takes the layer alone: {line}" + ); + } + for verb in [ + "SetNormalTexture(", + "SetPushedTexture(", + "SetHighlightTexture(", + "SetDisabledTexture(", + "SetCheckedTexture(", + "SetDisabledCheckedTexture(", + ] { + for line in text.lines().filter(|l| l.contains(verb)) { + let args = line.split(verb).nth(1).unwrap_or_default(); + assert!( + !args.trim_start().starts_with(|c: char| c.is_ascii_digit()), + "{file}: {verb} takes a texture, never a colour: {line}" + ); + } + } + } +} diff --git a/crates/benilla-app/src/ui_script/world_entry_tests.rs b/crates/benilla-app/src/ui_script/world_entry_tests.rs index 418c0aca..a5761fad 100644 --- a/crates/benilla-app/src/ui_script/world_entry_tests.rs +++ b/crates/benilla-app/src/ui_script/world_entry_tests.rs @@ -865,8 +865,9 @@ fn the_login_one_shots_wait_for_the_in_game_ui() { let _ = std::fs::remove_dir_all(&tmp); } -/// An addon that fails to load without raising (a `.toc` naming a file the package lacks) is kept -/// in the log, readable from Lua by the error window, and announced in chat. +/// An addon that fails to load without raising (a `.toc` naming a file the package lacks) reaches +/// no Lua error handler, so the `/errors` log (which collects off the handler) does not hold it: +/// the engine's record keeps it, and a chat line names it. #[test] fn an_addon_that_fails_to_load_without_raising_is_readable_in_the_error_log() { benilla_formats::wow_data_or_skip!(); @@ -914,57 +915,20 @@ fn an_addon_that_fails_to_load_without_raising_is_readable_in_the_error_log() { row.message ); - // 2. Readable from Lua through the window's own reads. - let count: i64 = script - .eval("local shown = BenillaGetNumScriptErrors() return shown") - .expect("BenillaGetNumScriptErrors is installed"); - assert!(count >= 1, "the window's own read sees it"); - let seen: String = script - .eval( - "local text = '' \ - for i = 1, BenillaGetNumScriptErrors() do \ - local seq, kind, message = BenillaGetScriptErrorInfo(i) \ - if kind == 'load' then text = message end \ - end \ - return text", - ) - .expect("BenillaGetScriptErrorInfo is installed"); - assert!( - seen.contains("AaMissing"), - "the window walks the log and finds it: {seen:?}" - ); - - assert!( - script - .eval::("return BenillaScriptLogFrame ~= nil") - .expect("eval"), - "ScriptLogFrame.xml loaded and built the window" - ); - - // The repaint runs over a real row, so a nil global anywhere on its path raises here. + // 2. Not in the `/errors` log: it never reached the error handler. script .eval::<()>("BenillaScriptLog_Update() return nil") - .expect("the window repaints over a real log without raising"); - // Some row shows it, not necessarily row 1: the log also holds the warnings a world entry - // raises before any addon loads. - let row_labels: Vec = (1..=13) - .filter_map(|i| { - script - .eval::>(&format!("return BenillaScriptLogRow{i}Label:GetText()")) - .expect("eval") - }) - .collect(); + .expect("the window repaints without raising"); assert!( - row_labels.iter().any(|l| l.contains("AaMissing")), - "a row shows the failure, trimmed to the row's width: {row_labels:?}" - ); - let summary: String = script - .eval::>("return BenillaScriptLogSummary:GetText()") - .expect("eval") - .unwrap_or_default(); - assert!( - summary.contains("problem"), - "the summary line counted them: {summary:?}" + !script + .eval::( + "for _, row in ipairs(BenillaScriptLog.rows) do \ + if strfind(row.message, 'AaMissing', 1, 1) then return true end \ + end \ + return false" + ) + .expect("eval"), + "a failure no handler saw is not in the handler-fed log" ); // 3. No dialog, since nothing raised: the reference answers an absent or unparseable file @@ -977,12 +941,14 @@ fn an_addon_that_fails_to_load_without_raising_is_readable_in_the_error_log() { through `_ERRORMESSAGE` and through every addon handler that replaces it" ); - // 4. Deviation: a chat line tells the player to look, where the reference stays silent, - // because an unannounced log goes unread. - assert_eq!( - world.resource::().pending_len(), - 1, - "world entry queued the 'N addon load failures — type /errors' line" + // 4. Deviation: a chat line names the failure, where the reference stays silent, because + // otherwise it reaches no screen. + let lines = world.resource::().pending_lines(); + assert!( + lines + .iter() + .any(|l| l.starts_with("Addon load failure: ") && l.contains("AaMissing")), + "world entry queued the failure's line: {lines:?}" ); drop(world); diff --git a/crates/benilla-app/src/ui_text/engine/mod.rs b/crates/benilla-app/src/ui_text/engine/mod.rs index c32806b4..daa29078 100644 --- a/crates/benilla-app/src/ui_text/engine/mod.rs +++ b/crates/benilla-app/src/ui_text/engine/mod.rs @@ -816,7 +816,7 @@ mod ppem_tests { assert_eq!(e.ppem(12.0), 12, "an exact size is itself"); assert_eq!(e.ppem(12.48), 12, "…and a fractional one rounds, not snaps"); assert_eq!(e.ppem(12.5), 13); - // `ERA_WINDOW_SCALE` (0.78), which the Options window and the Game Menu wear. + // `BENILLA_ERA_WINDOW_SCALE` (0.78), which the Options window and the Game Menu wear. assert_eq!(e.ppem(16.0 * 0.78), 12); e.dpi = 2.0; assert_eq!(e.ppem(12.0), 24); diff --git a/crates/benilla-app/src/ui_text/layout/mod.rs b/crates/benilla-app/src/ui_text/layout/mod.rs index dd269f48..12684773 100644 --- a/crates/benilla-app/src/ui_text/layout/mod.rs +++ b/crates/benilla-app/src/ui_text/layout/mod.rs @@ -407,7 +407,7 @@ mod measure_fits_render { "Main Menu", ]; - /// `ERA_WINDOW_SCALE` of benilla's game menu and options window: fonts land on no whole size. + /// `BENILLA_ERA_WINDOW_SCALE` of benilla's game menu and options window: fonts land on no whole size. const ERA: f32 = 0.78; fn spec(h: f32) -> FontSpec<'static> { diff --git a/crates/benilla-ui/src/loader/scripts.rs b/crates/benilla-ui/src/loader/scripts.rs index 8ddabecc..4a4a5f45 100644 --- a/crates/benilla-ui/src/loader/scripts.rs +++ b/crates/benilla-ui/src/loader/scripts.rs @@ -1,4 +1,4 @@ -use mlua::{Function, ObjectLike, Table, Value}; +use mlua::{Function, ObjectLike, Table}; use crate::framexml::Element; @@ -28,7 +28,7 @@ impl Loader<'_> { // function: `SetScript` (`0x7025c0`) tests only NULL and the first byte // (`0x7025f8`), and nothing trims the body (`0x6f29d0`). The stock blanks // (`BuffFrame.xml:101`) are whitespace, so their `GetScript` answers a function. - let cleared = handler.body.is_empty() && handler.attr("function").is_none(); + let cleared = handler.body.is_empty(); let func = if cleared { None } else { @@ -77,11 +77,9 @@ impl Loader<'_> { onload } - /// Compile a handler body, or resolve its `function=` global: not 1.12, whose loader never - /// reads the attribute. A 1.12 body takes no arguments and reads its frame from `this`; the - /// `self` parameter is not 1.12 either (our `assets/ui` bodies use it) and falls back to `this` - /// for a hook that calls a captured handler bare. The prologue shares the body's first line, - /// so line numbers match the reference, which loads the raw body as the chunk (`0x704c70`). + /// Compile a handler body as the chunk itself, as the reference loads it (`0x704c70`): it takes + /// no arguments and reads its frame from `this`, so a `self` in it is an ordinary global. The + /// loader reads no other attribute of the element (no `function=`). pub(super) fn compile_handler( &mut self, handler: &Element, @@ -91,41 +89,24 @@ impl Loader<'_> { ) -> Option { // The raw body, never trimmed: a whitespace body is a real empty chunk (`apply_scripts`). let body = handler.body.as_str(); - if !body.is_empty() { - let src = format!( - "return function(self, ...) if self == nil then self = this end {body}\nend" - ); - match self - .lua() - .load(&src) - .set_name(format!("{owner}:{name}")) - .set_mode(mlua::ChunkMode::Text) - .eval::() - { - Ok(f) => return Some(f), - Err(e) => { - self.report - .errors - .push(format!("{dbg}: compiling <{name}>: {e}")); - return None; - } + if body.is_empty() { + return None; + } + match self + .lua() + .load(body) + .set_name(format!("{owner}:{name}")) + .set_mode(mlua::ChunkMode::Text) + .into_function() + { + Ok(f) => Some(f), + Err(e) => { + self.report + .errors + .push(format!("{dbg}: compiling <{name}>: {e}")); + None } } - if let Some(global) = handler.attr("function") { - match self.lua().globals().get::(global) { - Ok(Value::Function(f)) => return Some(f), - _ => { - self.warn_once( - &format!("fn:{global}"), - format!( - "{dbg}: <{name} function=\"{global}\">: no such global function (yet)" - ), - ); - return None; - } - } - } - None } /// Fire a captured `OnLoad` under the event path's convention, `this` set and restored diff --git a/crates/benilla-ui/src/loader/tests.rs b/crates/benilla-ui/src/loader/tests.rs index dd5bcd1c..a8923c88 100644 --- a/crates/benilla-ui/src/loader/tests.rs +++ b/crates/benilla-ui/src/loader/tests.rs @@ -311,7 +311,7 @@ mod loader_tests { - table.insert(loadorder, "child"); ChildLoaded = self:GetName() + table.insert(loadorder, "child"); ChildLoaded = this:GetName() @@ -330,7 +330,7 @@ mod loader_tests { assert!(s.eval::("return MyFrameChild ~= nil").unwrap()); assert!(s.eval::("return MyFrameTex ~= nil").unwrap()); - // The child's handler uses `self`, the parent's `this`; both work. + // Both handlers read their frame from `this`. assert_eq!( s.eval::("return ChildLoaded").unwrap(), "MyFrameChild" @@ -2409,16 +2409,17 @@ mod chunk_name_tests { ); } - /// The 1.12 hook idiom: an addon captures a handler with `GetScript` and calls it with no - /// arguments, the frame passed only as `this`; `Loader::compile_handler` falls back to `this`. + /// A handler body is the chunk itself (`0x704c70`): it takes no arguments and reads its frame + /// from `this`, so `self` in it is an ordinary global, and the 1.12 hook idiom (an addon + /// captures a handler with `GetScript` and calls it bare) reaches the same frame. #[test] - fn a_script_captured_and_called_with_no_arguments_still_sees_its_frame() { + fn a_handler_body_is_the_chunk_and_self_is_a_global() { let s = UiScript::new().unwrap(); let doc = parse( r#" - SEEN = self:GetName() + SEEN = this:GetName() SELF_SEEN = self "#, @@ -2426,11 +2427,15 @@ mod chunk_name_tests { let report = load_in(&s, &doc, "Test.xml", &no_files); assert!(report.errors.is_empty(), "{:?}", report.errors); - // The engine's call passes `self` and sets `this`. - s.run("SEEN = nil Hooked:Show()").unwrap(); + s.run("self = 'the global' SEEN = nil Hooked:Show()") + .unwrap(); assert_eq!(s.eval::("return SEEN").unwrap(), "Hooked"); + assert_eq!( + s.eval::("return SELF_SEEN").unwrap(), + "the global", + "`self` is no parameter of the body" + ); - // The addon's: the captured script called bare, with only `this` set. s.run( "SEEN = nil \ local original = Hooked:GetScript(\"OnShow\") \ @@ -2439,11 +2444,29 @@ mod chunk_name_tests { this = nil", ) .unwrap(); - assert_eq!( - s.eval::("return SEEN").unwrap(), - "Hooked", - "the reference's own no-argument contract must reach the same frame" + assert_eq!(s.eval::("return SEEN").unwrap(), "Hooked"); + } + + /// The loader reads no `function=` attribute: a handler element with no body clears the + /// handler, whatever global the attribute names. + #[test] + fn a_function_attribute_binds_nothing() { + let s = UiScript::new().unwrap(); + s.run("function NamedHandler() RAN = true end").unwrap(); + let doc = parse( + r#" + + + + "#, ); + let report = load_in(&s, &doc, "Test.xml", &no_files); + assert!(report.errors.is_empty(), "{:?}", report.errors); + s.run("Named:Show()").unwrap(); + assert!(s.eval::("return RAN == nil").unwrap()); + assert!(s + .eval::("return Named:GetScript(\"OnShow\") == nil") + .unwrap()); } } diff --git a/crates/benilla-ui/src/script/cursor/drag.rs b/crates/benilla-ui/src/script/cursor/drag.rs index de883c71..caf3cfc2 100644 --- a/crates/benilla-ui/src/script/cursor/drag.rs +++ b/crates/benilla-ui/src/script/cursor/drag.rs @@ -108,13 +108,13 @@ mod tests { local a = CreateFrame("Frame", "A") a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true) a:RegisterForDrag("LeftButton") - a:SetScript("OnDragStart", function(self, button) drag_starts = drag_starts + 1; drag_button = button end) - a:SetScript("OnDragStop", function(self) drag_stops = drag_stops + 1 end) - a:SetScript("OnClick", function(self) click_a = click_a + 1 end) + a:SetScript("OnDragStart", function() local self, button = this, arg1 drag_starts = drag_starts + 1; drag_button = button end) + a:SetScript("OnDragStop", function() local self = this drag_stops = drag_stops + 1 end) + a:SetScript("OnClick", function() local self = this click_a = click_a + 1 end) local b = CreateFrame("Frame", "B") b:SetPoint("BOTTOMLEFT", 400, 0); b:SetWidth(400); b:SetHeight(600); b:EnableMouse(true) - b:SetScript("OnReceiveDrag", function(self) receives = receives + 1 end) - b:SetScript("OnClick", function(self) click_b = click_b + 1 end) + b:SetScript("OnReceiveDrag", function() local self = this receives = receives + 1 end) + b:SetScript("OnClick", function() local self = this click_b = click_b + 1 end) "#, ) .unwrap(); @@ -170,7 +170,7 @@ mod tests { local a = CreateFrame("Frame", "A") a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true) a:RegisterForDrag("LeftButton") - a:SetScript("OnClick", function(self) clicks = clicks + 1 end) + a:SetScript("OnClick", function() local self = this clicks = clicks + 1 end) "#, ) .unwrap(); @@ -192,7 +192,7 @@ mod tests { local a = CreateFrame("Frame", "A") a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true) a:RegisterForDrag("LeftButton") - a:SetScript("OnClick", function(self) clicks = clicks + 1 end) + a:SetScript("OnClick", function() local self = this clicks = clicks + 1 end) "#, ) .unwrap(); @@ -271,7 +271,7 @@ mod tests { heard, name, quality = 0, nil, nil local f = CreateFrame("Frame", "Listener") f:RegisterEvent("DELETE_ITEM_CONFIRM") - f:SetScript("OnEvent", function(self, event, n, q) heard = heard + 1; name = n; quality = q end) + f:SetScript("OnEvent", function() local self, event, n, q = this, event, arg1, arg2 heard = heard + 1; name = n; quality = q end) "#, ) .unwrap(); diff --git a/crates/benilla-ui/src/script/diagnostics.rs b/crates/benilla-ui/src/script/diagnostics.rs index 5ce47a31..f89ab257 100644 --- a/crates/benilla-ui/src/script/diagnostics.rs +++ b/crates/benilla-ui/src/script/diagnostics.rs @@ -1,7 +1,7 @@ -//! The script error log: this session's script errors, addon load failures and warnings, kept -//! for the player to read. The reference has no such log: its `_ERRORMESSAGE` shows only the first -//! message of a burst, and a load failure that never raises reaches no screen. Dispatch and -//! `_ERRORMESSAGE` are unchanged by it. +//! The engine's record of this session's script errors, addon load failures and warnings, for the +//! host (the world-entry load-failure lines, the addon harness) and dev instruments; no Lua global +//! reads it. The layer's `/errors` window keeps its own log off the Lua error handler, as a 1.12 +//! addon does. Dispatch and `_ERRORMESSAGE` are unchanged by it. //! //! A repeat bumps its row's count and keeps its place, rows are oldest first, and past //! [`DIAGNOSTIC_LOG_CAP`] distinct messages the oldest is evicted; `seq` is never reused, so an @@ -10,7 +10,7 @@ use std::collections::VecDeque; -use mlua::{IntoLua, Lua, MultiValue, Value}; +use mlua::Lua; use super::Model; @@ -91,19 +91,16 @@ impl DiagnosticLog { } /// Retained rows. + #[cfg(test)] pub(crate) fn len(&self) -> usize { self.rows.len() } - /// Distinct failures recorded this session, evicted and cleared rows included. + /// Distinct failures recorded this session, evicted rows included. + #[cfg(test)] pub(crate) fn total(&self) -> u64 { self.seq } - - /// Forget the rows but not the numbering, so no `#N` ever names two failures. - pub(crate) fn clear(&mut self) { - self.rows.clear(); - } } impl super::UiScript { @@ -112,17 +109,6 @@ impl super::UiScript { self.model_ref().diagnostics.rows().cloned().collect() } - /// `(retained rows, distinct failures ever recorded)`. - pub fn diagnostic_counts(&self) -> (usize, u64) { - let m = self.model_ref(); - (m.diagnostics.len(), m.diagnostics.total()) - } - - /// Forget the retained rows; the numbering carries on. - pub fn clear_diagnostics(&self) { - self.model_mut().diagnostics.clear(); - } - /// Retain an addon load failure that never raised (a missing file, an unparseable document, a /// dependency cycle or missing dependency, a broken `Bindings.xml`). It is not dispatched to /// `geterrorhandler()`: the reference logs such a failure and shows nothing. The caller still @@ -159,50 +145,6 @@ pub(crate) fn record_warning(lua: &Lua, msg: &str) { .record(DiagnosticKind::Warning, msg); } -/// Register the error-log reads `BenillaScriptLogFrame` polls, `Benilla`-prefixed as they are not -/// 1.12 API (`tests::reference_surface` enforces the prefix). -pub(super) fn install(lua: &Lua) -> mlua::Result<()> { - // BenillaGetNumScriptErrors() → retained, totalEverRecorded - lua.globals().set( - "BenillaGetNumScriptErrors", - lua.create_function(|lua, ()| { - let model = lua.app_data_ref::().expect("model app_data"); - Ok((model.diagnostics.len(), model.diagnostics.total())) - })?, - )?; - // BenillaGetScriptErrorInfo(index) → seq, kind, message, count; 1-based, oldest first, and - // nothing for an out-of-range index, so a row gone mid-repaint reads as gone. - lua.globals().set( - "BenillaGetScriptErrorInfo", - lua.create_function(|lua, index: i64| { - let model = lua.app_data_ref::().expect("model app_data"); - let Some(row) = usize::try_from(index) - .ok() - .and_then(|i| i.checked_sub(1)) - .and_then(|i| model.diagnostics.rows().nth(i)) - else { - return Ok(MultiValue::new()); - }; - Ok(MultiValue::from_vec(vec![ - Value::Integer(row.seq as i64), - lua.create_string(row.kind.tag())?.into_lua(lua)?, - lua.create_string(&row.message)?.into_lua(lua)?, - Value::Integer(i64::from(row.count)), - ])) - })?, - )?; - lua.globals().set( - "BenillaClearScriptErrors", - lua.create_function(|lua, ()| { - lua.app_data_mut::() - .expect("model app_data") - .diagnostics - .clear(); - Ok(()) - })?, - ) -} - #[cfg(test)] mod tests { use super::*; @@ -308,19 +250,4 @@ mod tests { ); assert_eq!(log.total() as usize, DIAGNOSTIC_LOG_CAP + 10); } - - #[test] - fn clear_forgets_rows_but_not_the_numbering() { - let mut log = DiagnosticLog::default(); - log.record(DiagnosticKind::Error, "a"); - log.record(DiagnosticKind::Error, "b"); - log.clear(); - assert_eq!(log.len(), 0); - log.record(DiagnosticKind::Error, "c"); - assert_eq!( - log.rows().next().unwrap().seq, - 3, - "a number never names two different failures" - ); - } } diff --git a/crates/benilla-ui/src/script/event.rs b/crates/benilla-ui/src/script/event.rs index 854788fd..75bc1a47 100644 --- a/crates/benilla-ui/src/script/event.rs +++ b/crates/benilla-ui/src/script/event.rs @@ -6,7 +6,7 @@ use std::borrow::Cow; -use mlua::{Function, Lua, MultiValue, Table, Value}; +use mlua::{Function, Lua, Table, Value}; use super::{Model, ScriptValue, REG_SCRIPTS}; use crate::script::object::frame_wrapper; @@ -230,8 +230,8 @@ fn arg_name(i: usize) -> Cow<'static, str> { } /// The calling convention, shared by [`fire`] and the loader's `OnLoad`: sets `this`, `event` and -/// `arg1..argN`, passes `(self[, event], extra...)`, and restores the globals after the call, even -/// on error, so nested fires are safe. +/// `arg1..argN` and calls the handler with no arguments, as `0x705199` calls `lua_pcall` with +/// zero; the globals are restored after the call, even on error, so nested fires are safe. pub(crate) fn invoke_with_globals( lua: &Lua, wrapper: Table, @@ -257,15 +257,8 @@ pub(crate) fn invoke_with_globals( g.set(arg_name(i + 1).as_ref(), v.clone())?; } - let mut modern: Vec = Vec::with_capacity(2 + n); - modern.push(Value::Table(wrapper)); - if let Some(ev) = event_name { - modern.push(Value::String(lua.create_string(ev)?)); - } - modern.extend(extra.iter().cloned()); - // A protected call; the globals are restored before its outcome returns. - let outcome = func.call::<()>(MultiValue::from_vec(modern)); + let outcome = func.call::<()>(()); g.set("this", saved_this)?; g.set("event", saved_event)?; diff --git a/crates/benilla-ui/src/script/keybind.rs b/crates/benilla-ui/src/script/keybind.rs index d1c4b250..1795d3a6 100644 --- a/crates/benilla-ui/src/script/keybind.rs +++ b/crates/benilla-ui/src/script/keybind.rs @@ -1,7 +1,8 @@ //! The key-binding table behind `GetBinding`, `SetBinding`, `LoadBindings` and their kin: per -//! command, a category header and its chords, spelled `[ALT-][CTRL-][SHIFT-]` and matched by -//! string equality as the reference does. Host commands carry their 1.12 defaults; an addon's -//! `Bindings.xml` adds ordinary rows that also carry a Lua body. +//! command, its chords, spelled `[ALT-][CTRL-][SHIFT-]` and matched by string equality as +//! the reference does, and the one flat list `GetBinding` walks, whose section headers are rows of +//! their own. Host commands carry their 1.12 defaults; an addon's `Bindings.xml` adds ordinary rows +//! that also carry a Lua body. use std::collections::HashMap; @@ -11,8 +12,9 @@ use crate::bindings_xml::AddonBinding; use super::Model; -/// One host-registered command: the 1.12 name, its category header's global string -/// (`BINDING_HEADER_MOVEMENT`), whether it also runs on release (`runOnUp`, read only by +/// One host-registered command: the 1.12 name, the header global string of the section it is filed +/// under (`BINDING_HEADER_MOVEMENT`; a change from the previous command opens a `HEADER_*` row), +/// whether it also runs on release (`runOnUp`, read only by /// `RunCommand` at `0x4b7bf1`, never a bindability rule), and the 1.12 default chords. #[derive(Clone, Copy, Debug)] pub struct KeybindCommand { @@ -127,7 +129,6 @@ fn is_valid_binding_key(key: &str) -> bool { #[derive(Clone, Debug)] struct Entry { name: String, - category: String, run_on_up: bool, defaults: [Option; 2], /// The live chords in bind order; `GetBindingKey` and the window show the first two. @@ -138,10 +139,25 @@ struct Entry { body: Option, } +/// One row of the list `GetNumBindings`/`GetBinding` walk, in load order. +#[derive(Clone, Debug)] +enum Row { + /// A section header, `HEADER_
`: the reference stores `sprintf("HEADER_%s", header)` + /// (`0x846f58`) as a row of its own with a display ordinal and no script (`0x4b72f5`-`0x4b72fa`). + Header(String), + /// A command, an index into [`KeybindState::entries`]. + Command(usize), +} + /// The table, the stored sets and the queued host requests; lives in [`Model`]. #[derive(Default)] pub(crate) struct KeybindState { entries: Vec, + /// The display list, headers and commands in load order. + rows: Vec, + /// The section the last host command was filed under, so the next one opens a header only on + /// a change. + host_section: Option, by_name: HashMap, /// Stored sets in entry order: `[0]` account (set 1), `[1]` character (set 2, if any). stored: [Option>>; 2], @@ -152,15 +168,39 @@ pub(crate) struct KeybindState { /// Bumped by every change to the live table: the app's signal to re-derive dispatch. generation: u64, requests: Vec, - /// Set by `BenillaBindCapture`: the host swallows raw input and hands the page the chord. - capture_armed: bool, } impl KeybindState { - /// The rows `GetNumBindings`/`GetBinding` enumerate: all but `hidden="true"`, since the stock - /// window skips only `HEADER` rows (`Blizzard_BindingUI.lua:87`). - fn visible(&self) -> impl Iterator { - self.entries.iter().filter(|e| !e.hidden) + /// The rows `GetNumBindings`/`GetBinding` enumerate: every header and every command but a + /// `hidden="true"` one, which the reference numbers from a second counter, negated + /// (`0x4b73da`-`0x4b73e5`), so `GetBinding`'s ordinal walk (`0x4b7c80`) never reaches it and + /// `GetNumBindings` (`0x4b7f40`) counts only the first. + fn visible(&self) -> impl Iterator { + self.rows.iter().filter(|r| match r { + Row::Header(_) => true, + Row::Command(i) => !self.entries[*i].hidden, + }) + } + + /// Open a section: a `HEADER_
` row, unless one of that name exists, which the reference + /// refuses (`0x4b71c9`-`0x4b7258`) and loads the binding under the section already open. + fn open_header(&mut self, header: &str) { + let name = format!("HEADER_{header}"); + if !self + .rows + .iter() + .any(|r| matches!(r, Row::Header(h) if h.eq_ignore_ascii_case(&name))) + { + self.rows.push(Row::Header(name)); + } + } + + /// Append a command and its row. + fn push_entry(&mut self, entry: Entry) { + let idx = self.entries.len(); + self.by_name.insert(entry.name.to_ascii_uppercase(), idx); + self.entries.push(entry); + self.rows.push(Row::Command(idx)); } /// `SetBinding(key[, command])`: unbind `key` everywhere, then append it to the command's @@ -255,26 +295,29 @@ impl KeybindState { } /// Append one addon's parsed `Bindings.xml`, skipping a name already registered, so a host - /// command keeps it and no handed-out index moves. A `header` opens a section - /// (`BINDING_HEADER_
`) until the next one, as in the reference's flat list. - /// Deviation: rows before a file's first header take the addon's name as their section, not - /// the previous file's, because under a foreign header the player could not find them. + /// command keeps it and no handed-out index moves. A `header` adds its `HEADER_
` row + /// before the binding's own, as in the reference's flat list. + /// Deviation: a file whose first binding has no header opens a `HEADER_` row, where the + /// reference files those rows under the previous file's last header, because under a foreign + /// header the player could not find them. fn register_addon(&mut self, addon: &str, bindings: &[AddonBinding]) { - let mut section = addon.to_string(); + let mut opened = false; for b in bindings { // A foreign `platform` skips the whole node, header included (`0x4b70c3`-`0x4b70e5`). if b.platform.as_deref().is_some_and(|p| p != THIS_PLATFORM) { continue; } - // Before the duplicate skip: a duplicate's header still opens its section. - if let Some(h) = &b.header { - section = format!("BINDING_HEADER_{h}"); - } + // A name already defined skips the node before its header is read (`0x4b70f1`-`0x4b717e`). let key = b.name.to_ascii_uppercase(); if self.by_name.contains_key(&key) { continue; } - let idx = self.entries.len(); + match &b.header { + Some(h) => self.open_header(h), + None if !opened => self.open_header(addon), + None => {} + } + opened = true; // The stored chord: the current set first, then the account set, as `load` falls back. let stored = { let cur = if self.current_set == 2 { 1 } else { 0 }; @@ -283,10 +326,8 @@ impl KeybindState { .or_else(|| self.stored_by_name[0].get(&key)) .cloned() }; - self.by_name.insert(key, idx); - self.entries.push(Entry { + self.push_entry(Entry { name: b.name.clone(), - category: section.clone(), run_on_up: b.run_on_up, // 1.12's `` carries no default chord. defaults: [None, None], @@ -317,11 +358,17 @@ impl super::UiScript { continue; } let defaults = [c.default1.map(str::to_owned), c.default2.map(str::to_owned)]; - let idx = model.keybinds.entries.len(); - model.keybinds.by_name.insert(key, idx); - model.keybinds.entries.push(Entry { + let kb = &mut model.keybinds; + if kb.host_section.as_deref() != Some(c.category) { + kb.host_section = Some(c.category.to_owned()); + let header = c + .category + .strip_prefix("BINDING_HEADER_") + .unwrap_or(c.category); + kb.open_header(header); + } + kb.push_entry(Entry { name: c.name.to_owned(), - category: c.category.to_owned(), run_on_up: c.run_on_up, keys: defaults.iter().flatten().cloned().collect(), defaults, @@ -425,11 +472,6 @@ impl super::UiScript { pub fn character_bindings_exist(&self) -> bool { self.model_mut().keybinds.stored[1].is_some() } - - /// While true, the host swallows raw input and calls `KeyBindings_OnHostKey("")`. - pub fn bind_capture_armed(&self) -> bool { - self.model_mut().keybinds.capture_armed - } } /// Register one addon's `Bindings.xml` from a bare `&Lua`: `LoadAddOn` runs inside a Lua binding @@ -441,9 +483,10 @@ pub(crate) fn register_addon_bindings(lua: &Lua, addon: &str, bindings: &[AddonB .register_addon(addon, bindings); } -/// The binding globals. `GetBinding` returns command, category, key1, key2, which is not 1.12's -/// shape: 1.12 returns command, key1, key2, with each header a `HEADER_*` row -/// (`Blizzard_BindingUI.lua:84`). Only `GetNumBindings` and `GetBinding` skip hidden rows. +/// The binding globals. `GetBinding(i)` answers the row's name and then every key bound to it +/// (`0x4b7f60`: `1 + matches` values), so a `HEADER_*` row answers its name alone, which the stock +/// window reads as three names (`Blizzard_BindingUI.lua:84`, `:87`). Only `GetNumBindings` and +/// `GetBinding` skip hidden rows. pub(super) fn install(lua: &Lua) -> mlua::Result<()> { lua.globals().set( "GetNumBindings", @@ -456,17 +499,16 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { "GetBinding", lua.create_function(|lua, i: usize| { let model = lua.app_data_mut::().expect("model app_data"); - let Some(e) = i - .checked_sub(1) - .and_then(|i| model.keybinds.visible().nth(i)) - else { + let kb = &model.keybinds; + let Some(row) = i.checked_sub(1).and_then(|i| kb.visible().nth(i)) else { return Ok(MultiValue::new()); }; - let mut out = vec![ - Value::String(lua.create_string(&e.name)?), - Value::String(lua.create_string(&e.category)?), - ]; - for k in e.keys.iter().take(2) { + let (name, keys): (&str, &[String]) = match row { + Row::Header(h) => (h, &[]), + Row::Command(c) => (&kb.entries[*c].name, &kb.entries[*c].keys), + }; + let mut out = vec![Value::String(lua.create_string(name)?)]; + for k in keys { out.push(Value::String(lua.create_string(k)?)); } Ok(MultiValue::from_iter(out)) @@ -545,21 +587,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { Ok(model.keybinds.current_set) })?, )?; - lua.globals().set( - "BenillaCharacterBindingsExist", - lua.create_function(|lua, ()| { - let model = lua.app_data_mut::().expect("model app_data"); - Ok(model.keybinds.stored[1].is_some()) - })?, - )?; - lua.globals().set( - "BenillaBindCapture", - lua.create_function(|lua, armed: Option| { - let mut model = lua.app_data_mut::().expect("model app_data"); - model.keybinds.capture_armed = armed.unwrap_or(false); - Ok(()) - })?, - ) + Ok(()) } #[cfg(test)] @@ -596,17 +624,40 @@ mod tests { s } + /// Every row `GetBinding` answers, each as its values in order. + fn rows(s: &UiScript) -> Vec> { + s.eval( + "local out = {} \ + for i = 1, GetNumBindings() do out[i] = { GetBinding(i) } end \ + return out", + ) + .unwrap() + } + + fn row(values: &[&str]) -> Vec { + values.iter().map(|v| v.to_string()).collect() + } + #[test] - fn the_table_reads_like_the_reference() { + fn the_list_reads_like_the_reference() { let s = script(); - assert_eq!(s.eval::("return GetNumBindings()").unwrap(), 3); - // Command, category, key1, key2 (not 1.12's three values), the defaults seeded live. + // A header row answers its name alone; a command, its name and then every key, no + // category (`0x4b7f60`). + assert_eq!( + rows(&s), + [ + row(&["HEADER_MOVEMENT"]), + row(&["MOVEFORWARD", "W", "UP"]), + row(&["JUMP", "SPACE", "NUMPAD0"]), + row(&["HEADER_CAMERA"]), + row(&["CAMERAZOOMIN", "MOUSEWHEELUP"]), + ] + ); + // Every key, not two. + s.run(r#"SetBinding("F", "JUMP")"#).unwrap(); + assert_eq!(rows(&s)[2], row(&["JUMP", "SPACE", "NUMPAD0", "F"])); assert!(s - .eval::( - r#"local c, cat, k1, k2 = GetBinding(1) - return c == "MOVEFORWARD" and cat == "BINDING_HEADER_MOVEMENT" - and k1 == "W" and k2 == "UP""# - ) + .eval::("return GetBinding(0) == nil and GetBinding(6) == nil") .unwrap()); assert_eq!( s.eval::(r#"return GetBindingAction("NUMPAD0")"#) @@ -614,7 +665,7 @@ mod tests { "JUMP" ); assert_eq!( - s.eval::(r#"return GetBindingAction("F")"#).unwrap(), + s.eval::(r#"return GetBindingAction("G")"#).unwrap(), "", "an unbound key reads as the empty command, like the client" ); @@ -748,22 +799,16 @@ mod tests { "registration must move the generation — it is the app's re-derive-dispatch signal" ); - // Three host rows and two addon rows: the addon's `MOVEFORWARD` is skipped and + // Its header row, then two commands: the addon's `MOVEFORWARD` is skipped and // `PROBEHIDDEN` is not listed. - assert_eq!(s.eval::("return GetNumBindings()").unwrap(), 5); - assert!(s - .eval::( - r#"local c, cat, k1 = GetBinding(4) - return c == "PROBEHOLD" and cat == "BINDING_HEADER_PROBE" and k1 == nil"# - ) - .unwrap()); - assert!( - s.eval::( - r#"local c, cat = GetBinding(5) - return c == "PROBEEDGE" and cat == "BINDING_HEADER_PROBE""# - ) - .unwrap(), - "a row with no header of its own belongs to the section the last header opened" + assert_eq!(s.eval::("return GetNumBindings()").unwrap(), 8); + assert_eq!( + rows(&s)[5..], + [ + row(&["HEADER_PROBE"]), + row(&["PROBEHOLD"]), + row(&["PROBEEDGE"]) + ] ); assert!(s .eval::( @@ -795,22 +840,49 @@ mod tests { .eval::(r#"return SetBinding("MOUSEWHEELDOWN", "PROBEEDGE") == 1"#) .unwrap()); - // A file with no header: its rows' section is the addon's name. + // A duplicate's header is never read (`0x4b70f1`-`0x4b717e`); a header already in the + // list is refused and the row files under the open one (`0x4b71c9`-`0x4b7258`); a hidden + // row's header is a row, since the header is written before the hidden test (`0x4b73bc`). + let parsed = crate::bindings_xml::parse( + r#" + Dup(); + Again(); + + "#, + ) + .expect("well-formed"); + s.register_addon_bindings("ProbeMore", &parsed); + assert_eq!( + rows(&s)[8..], + [row(&["PROBEAGAIN"]), row(&["HEADER_QUIET"])] + ); + + // A file with no header opens a row of the addon's name (the deviation on + // `register_addon`). let parsed = crate::bindings_xml::parse( r#"Lib();"#, ) .expect("well-formed"); s.register_addon_bindings("ProbeLib", &parsed); - assert!(s - .eval::( - r#"local c, cat = GetBinding(6); return c == "LIBKEY" and cat == "ProbeLib""# - ) - .unwrap()); + assert_eq!( + rows(&s)[10..], + [row(&["HEADER_ProbeLib"]), row(&["LIBKEY"])] + ); // Addon rows only, in registration order, hidden included. let bodies = s.addon_binding_bodies(); let names: Vec<&str> = bodies.iter().map(|b| b.name.as_str()).collect(); - assert_eq!(names, ["PROBEHOLD", "PROBEEDGE", "PROBEHIDDEN", "LIBKEY"]); + assert_eq!( + names, + [ + "PROBEHOLD", + "PROBEEDGE", + "PROBEHIDDEN", + "PROBEAGAIN", + "PROBEHIDDENHEAD", + "LIBKEY" + ] + ); assert!(bodies[0].run_on_up && !bodies[1].run_on_up); assert!(bodies[0].body.contains(r#"keystate == "down""#)); } @@ -844,7 +916,7 @@ mod tests { } #[test] - fn host_seeding_feeds_load_and_the_capture_arm_reads_back() { + fn host_seeding_feeds_load() { let mut s = script(); s.seed_binding_set(1, Some(vec![("JUMP".into(), vec!["F".into()])])); s.load_binding_set(1); @@ -857,11 +929,6 @@ mod tests { s.eval::(r#"return GetBindingAction("W")"#).unwrap(), "MOVEFORWARD" ); - assert!(!s.bind_capture_armed()); - s.run("BenillaBindCapture(true)").unwrap(); - assert!(s.bind_capture_armed()); - s.run("BenillaBindCapture(false)").unwrap(); - assert!(!s.bind_capture_armed()); let snap = s.keybind_snapshot(); assert_eq!(snap[1].0, "JUMP"); assert_eq!(snap[1].1, vec!["F".to_string()]); diff --git a/crates/benilla-ui/src/script/messageframe/scrolling.rs b/crates/benilla-ui/src/script/messageframe/scrolling.rs index 6060f4c1..0207cfa9 100644 --- a/crates/benilla-ui/src/script/messageframe/scrolling.rs +++ b/crates/benilla-ui/src/script/messageframe/scrolling.rs @@ -247,26 +247,6 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { lua.set_named_registry_value(REG_SCROLLINGMESSAGEFRAME_METHODS, m)?; - // BenillaChatTabPressed(): the chat edit box's OnTabPressed sets a flag for the app's whisper - // cycle (`UiScript::take_chat_tab`). - lua.globals().set( - "BenillaChatTabPressed", - lua.create_function(|lua, ()| { - let mut model = lua.app_data_mut::().expect("model app_data"); - model.chat_tab = true; - Ok(()) - })?, - )?; - // SubmitChatInput(text): the chat edit box's OnEnterPressed queues the line for the app to - // parse as a chat command; an empty line is queued too, and the app takes it as a cancel. - lua.globals().set( - "SubmitChatInput", - lua.create_function(|lua, text: Option| { - let mut model = lua.app_data_mut::().expect("model app_data"); - model.chat_input.push(text.unwrap_or_default()); - Ok(()) - })?, - )?; Ok(()) } @@ -699,7 +679,8 @@ mod tests { f:SetPoint('BOTTOMLEFT', 0, 0)\n\ f:SetWidth(430)\n\ f:SetHeight(120)\n\ - f:SetScript('OnHyperlinkClick', function(self, link, text, button)\n\ + f:SetScript('OnHyperlinkClick', function()\n\ + local link, text, button = arg1, arg2, arg3\n\ CLICKED = link .. '#' .. text .. '#' .. button\n\ end)", ) diff --git a/crates/benilla-ui/src/script/mod.rs b/crates/benilla-ui/src/script/mod.rs index 2e957f15..8b3decb5 100644 --- a/crates/benilla-ui/src/script/mod.rs +++ b/crates/benilla-ui/src/script/mod.rs @@ -579,7 +579,6 @@ impl UiScript { worldmap::install(&lua)?; worldstate::install(&lua)?; net_stats::install(&lua)?; - diagnostics::install(&lua)?; let s = UiScript { lua, @@ -828,7 +827,8 @@ impl UiScript { true } - /// Drain the chat lines `SubmitChatInput` queued since the last call, for the app to parse. + /// Drain the chat lines queued since the last call (a probe's, a host `SlashCmdList` row's), for + /// the app to parse. pub fn take_chat_input(&mut self) -> Vec { std::mem::take(&mut self.model_mut().chat_input) } @@ -838,11 +838,6 @@ impl UiScript { self.model_mut().chat_input.push(line); } - /// Whether Tab was pressed in the chat edit box since the last call: the whisper cycle's cue. - pub fn take_chat_tab(&mut self) -> bool { - std::mem::take(&mut self.model_mut().chat_tab) - } - /// Replace the hyperlink spans `(frame, y-up rect, link, markup)` the app rasterized this /// frame; a release inside one fires `OnHyperlinkClick` on its frame ([`pointer`]). pub fn set_link_spans(&mut self, spans: Vec<(FrameHandle, Rect, String, String)>) { diff --git a/crates/benilla-ui/src/script/model.rs b/crates/benilla-ui/src/script/model.rs index 939ea0cd..6c442807 100644 --- a/crates/benilla-ui/src/script/model.rs +++ b/crates/benilla-ui/src/script/model.rs @@ -143,8 +143,6 @@ pub(crate) struct Model { pub(crate) layout_rounds: u64, /// Per frame, rasterized links `(y-up rect, link, full |H…|h markup)` for `OnHyperlinkClick`. pub(crate) link_spans: HashMap>, - /// Tab was pressed in the chat edit box (`BenillaChatTabPressed`), for the whisper cycle. - pub(crate) chat_tab: bool, /// Region visuals (texture, colour, text) and layout (anchors, size, justify). pub(crate) region_data: HashMap, /// Each frame's backdrop plate (`` or `SetBackdrop`, the client's `frame+0x1ac`). @@ -873,7 +871,7 @@ pub(crate) struct Model { /// Reputation calls queued, applied locally first because none of the three sends is acked. pub(crate) reputation_sends: Vec, - /// Lines the chat box submitted (`SubmitChatInput`), for the app's slash-command parser. + /// Lines for the app's slash-command parser: a probe's, or a host `SlashCmdList` row's. pub(crate) chat_input: Vec, /// The world map's pushed catalog and feed, and its selection. @@ -1019,7 +1017,6 @@ impl Model { layout_rounds: 0, resolved: HashMap::new(), link_spans: HashMap::new(), - chat_tab: false, region_data: HashMap::new(), backdrops: HashMap::new(), simple_html: simplehtml::SimpleHtmlStates::new(), diff --git a/crates/benilla-ui/src/script/slash.rs b/crates/benilla-ui/src/script/slash.rs index 46f8d6a0..a97e8265 100644 --- a/crates/benilla-ui/src/script/slash.rs +++ b/crates/benilla-ui/src/script/slash.rs @@ -1,6 +1,6 @@ //! `SlashCmdList` dispatch from the host, for a line that reaches it without passing the stock //! `ChatEdit_ParseText` (`ChatFrame.lua`): the same walk, over the table the stock `ChatFrame.lua` -//! declares, where the stock commands and benilla's own are entries. +//! declares, where the stock commands, benilla's layer's and an addon's are entries. use mlua::{Table, Value}; @@ -25,6 +25,33 @@ impl super::UiScript { self.find_slash_handler(cmd).is_some() } + /// Register a host command as a `SlashCmdList` row, as an addon registers one: `SLASH_` + /// for each alias (with its `/`) and a handler that queues `" "` on the chat + /// input the host drains ([`Self::take_chat_input`]). A no-op before `ChatFrame.lua` has + /// declared `SlashCmdList`. + pub fn register_host_slash_command(&self, key: &str, aliases: &[&str]) -> mlua::Result<()> { + let globals = self.lua.globals(); + let Ok(list) = globals.get::("SlashCmdList") else { + return Ok(()); + }; + let Some(first) = aliases.first() else { + return Ok(()); + }; + for (n, alias) in aliases.iter().enumerate() { + globals.set(format!("SLASH_{key}{}", n + 1), *alias)?; + } + let first = first.to_string(); + let handler = self.lua.create_function(move |lua, msg: Option| { + let line = format!("{first} {}", msg.unwrap_or_default()); + lua.app_data_mut::() + .expect("model app_data") + .chat_input + .push(line); + Ok(()) + })?; + list.set(key, handler) + } + /// The reference's walk: over `SlashCmdList`'s keys, then `SLASH_` to the first gap, /// case-insensitive. fn find_slash_handler(&self, cmd: &str) -> Option { @@ -122,6 +149,24 @@ mod tests { ); } + /// A host row is an ordinary `SlashCmdList` row that queues its line for the host. + #[test] + fn a_host_command_queues_its_line() { + let mut s = UiScript::new().unwrap(); + s.register_host_slash_command("HOSTPROBE", &["/hostprobe", "/hp"]) + .unwrap(); + assert!( + !s.has_slash_command("hostprobe"), + "no table yet: nothing registers" + ); + s.run("SlashCmdList = {}").unwrap(); + s.register_host_slash_command("HOSTPROBE", &["/hostprobe", "/hp"]) + .unwrap(); + assert_eq!(s.eval::("return SLASH_HOSTPROBE2").unwrap(), "/hp"); + assert!(s.run_slash_command("hp", "a b")); + assert_eq!(s.take_chat_input(), ["/hostprobe a b"]); + } + #[test] fn a_non_function_entry_is_not_a_command() { let s = UiScript::new().unwrap(); diff --git a/crates/benilla-ui/src/script/tests/button.rs b/crates/benilla-ui/src/script/tests/button.rs index 4ebf3132..80145c84 100644 --- a/crates/benilla-ui/src/script/tests/button.rs +++ b/crates/benilla-ui/src/script/tests/button.rs @@ -333,7 +333,7 @@ fn disabled_button_swallows_clicks_checkbutton_toggles_before_onclick() { clicks, seen_checked = 0, nil local cb = CreateFrame("CheckButton", "Toggler") cb:SetPoint("BOTTOMLEFT", 0, 0); cb:SetWidth(100); cb:SetHeight(100) - cb:SetScript("OnClick", function(self, button, down) + cb:SetScript("OnClick", function() local self, button, down = this, arg1, arg2 clicks = clicks + 1 seen_checked = self:GetChecked() end) @@ -376,7 +376,7 @@ fn default_registration_is_left_click_only_right_click_reaches_nothing() { clicks = 0 local btn = CreateFrame("Button", "Vendor") btn:SetPoint("BOTTOMLEFT", 0, 0); btn:SetWidth(100); btn:SetHeight(100) - btn:SetScript("OnClick", function(self, button, down) clicks = clicks + 1 end) + btn:SetScript("OnClick", function() local self, button, down = this, arg1, arg2 clicks = clicks + 1 end) "#, ) .unwrap(); @@ -399,7 +399,7 @@ fn register_for_clicks_grows_right_click_and_carries_the_button_name() { local btn = CreateFrame("Button", "Vendor") btn:SetPoint("BOTTOMLEFT", 0, 0); btn:SetWidth(100); btn:SetHeight(100) btn:RegisterForClicks("LeftButtonUp", "RightButtonUp") - btn:SetScript("OnClick", function(self, button, down) + btn:SetScript("OnClick", function() local self, button, down = this, arg1, arg2 clicks = clicks + 1 click_btn = button arg1_btn = arg1 -- the 1.12 legacy-global convention, same value @@ -431,7 +431,7 @@ fn down_registration_fires_on_press_and_toggles_checked_once() { local cb = CreateFrame("CheckButton", "QuickSell") cb:SetPoint("BOTTOMLEFT", 0, 0); cb:SetWidth(100); cb:SetHeight(100) cb:RegisterForClicks("LeftButtonDown") - cb:SetScript("OnClick", function(self, button, down) + cb:SetScript("OnClick", function() local self, button, down = this, arg1, arg2 clicks = clicks + 1 seen_down = down end) diff --git a/crates/benilla-ui/src/script/tests/create_frame_template.rs b/crates/benilla-ui/src/script/tests/create_frame_template.rs index 15b954e4..506a6d99 100644 --- a/crates/benilla-ui/src/script/tests/create_frame_template.rs +++ b/crates/benilla-ui/src/script/tests/create_frame_template.rs @@ -36,7 +36,7 @@ fn a_runtime_template_brings_size_anchors_regions_and_a_fired_onload() { - ProbeLoadedAs = self:GetName(); ProbeLoadWidth = self:GetWidth() + ProbeLoadedAs = this:GetName(); ProbeLoadWidth = this:GetWidth() "#, diff --git a/crates/benilla-ui/src/script/tests/events.rs b/crates/benilla-ui/src/script/tests/events.rs index 2a244eee..e0fa1247 100644 --- a/crates/benilla-ui/src/script/tests/events.rs +++ b/crates/benilla-ui/src/script/tests/events.rs @@ -4,20 +4,21 @@ use super::common::script; use crate::script::*; +/// A handler gets no arguments (`0x705199` calls `lua_pcall` with zero); its inputs are the +/// `this`, `event` and `argN` globals. #[test] -fn fire_event_both_conventions_in_one_handler() { +fn an_event_handler_gets_no_arguments_and_reads_the_globals() { let mut s = script(); s.run( r#" local f = CreateFrame("Frame", "EF") f:RegisterEvent("UNIT_HEALTH") - f:SetScript("OnEvent", function(self, event, ...) - r_this_eq_self = (this == self) -- legacy `this` global == modern `self` - r_event_global = event -- modern `event` arg - r_event_eq = (event == _G.event) -- == legacy `event` global - r_arg1_eq = (arg1 == arg[1]) -- legacy `arg1` global == the vararg table - r_arg1 = arg1 - r_arg2 = arg[2] + f:SetScript("OnEvent", function(a, b, c) + r_args = (a == nil and b == nil and c == nil) + r_this = (this == EF) + r_event = event + r_arg1 = arg1 + r_arg2 = arg2 end) "#, ) @@ -29,13 +30,12 @@ fn fire_event_both_conventions_in_one_handler() { ); assert!(s.errors().is_empty(), "{:?}", s.errors()); - assert!(s.eval::("return r_this_eq_self").unwrap()); - assert_eq!( - s.eval::("return r_event_global").unwrap(), - "UNIT_HEALTH" + assert!( + s.eval::("return r_args").unwrap(), + "a declared parameter is nil: no positional argument is passed" ); - assert!(s.eval::("return r_event_eq").unwrap()); - assert!(s.eval::("return r_arg1_eq").unwrap()); + assert!(s.eval::("return r_this").unwrap()); + assert_eq!(s.eval::("return r_event").unwrap(), "UNIT_HEALTH"); assert_eq!(s.eval::("return r_arg1").unwrap(), "player"); assert_eq!(s.eval::("return r_arg2").unwrap(), 42); } diff --git a/crates/benilla-ui/src/script/tests/input.rs b/crates/benilla-ui/src/script/tests/input.rs index ba8c1076..8eb98547 100644 --- a/crates/benilla-ui/src/script/tests/input.rs +++ b/crates/benilla-ui/src/script/tests/input.rs @@ -13,10 +13,10 @@ fn enable_mouse_gates_hit_testing() { -- transparent to hits; the enabled frame behind it (`a`) must capture. local a = CreateFrame("Frame", "A") a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(800); a:SetHeight(600); a:EnableMouse(true) - a:SetScript("OnEnter", function(self) who = self:GetName() end) + a:SetScript("OnEnter", function() local self = this who = self:GetName() end) local b = CreateFrame("Frame", "B") b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(800); b:SetHeight(600); b:EnableMouse(false) - b:SetScript("OnEnter", function(self) who = self:GetName() end) + b:SetScript("OnEnter", function() local self = this who = self:GetName() end) -- 1/nil, not a boolean: this is the exact comparison shape that inverts, so the -- test asserts it rather than leaning on truthiness. assert(a:IsMouseEnabled() == 1 and b:IsMouseEnabled() == nil) @@ -49,10 +49,10 @@ fn hit_order_is_strata_then_level_then_the_earlier_linked_frame() { who = nil local a = CreateFrame("Frame", "A") a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(800); a:SetHeight(600); a:EnableMouse(true) - a:SetScript("OnEnter", function(self) who = self:GetName() end) + a:SetScript("OnEnter", function() local self = this who = self:GetName() end) local b = CreateFrame("Frame", "B") b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(800); b:SetHeight(600); b:EnableMouse(true) - b:SetScript("OnEnter", function(self) who = self:GetName() end) + b:SetScript("OnEnter", function() local self = this who = self:GetName() end) "#, ) .unwrap(); @@ -91,8 +91,8 @@ fn mouse_move_fires_enter_then_leave_across_a_boundary_with_correct_self() { enter_self_ok, leave_self_ok = false, false local a = CreateFrame("Frame", "A") -- left half only a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true) - a:SetScript("OnEnter", function(self) enters = enters + 1; enter_self_ok = (self == a) end) - a:SetScript("OnLeave", function(self) leaves = leaves + 1; leave_self_ok = (self == a) end) + a:SetScript("OnEnter", function() local self = this enters = enters + 1; enter_self_ok = (self == a) end) + a:SetScript("OnLeave", function() local self = this leaves = leaves + 1; leave_self_ok = (self == a) end) "#, ) .unwrap(); @@ -122,10 +122,10 @@ fn onclick_fires_on_press_release_same_frame_not_when_release_lands_elsewhere() clicks_a, clicks_b, click_btn = 0, 0, nil local a = CreateFrame("Frame", "A") -- left half a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true) - a:SetScript("OnClick", function(self, button, down) clicks_a = clicks_a + 1; click_btn = button end) + a:SetScript("OnClick", function() local self, button, down = this, arg1, arg2 clicks_a = clicks_a + 1; click_btn = button end) local b = CreateFrame("Frame", "B") -- right half b:SetPoint("BOTTOMLEFT", 400, 0); b:SetWidth(400); b:SetHeight(600); b:EnableMouse(true) - b:SetScript("OnClick", function(self, button, down) clicks_b = clicks_b + 1 end) + b:SetScript("OnClick", function() local self, button, down = this, arg1, arg2 clicks_b = clicks_b + 1 end) "#, ) .unwrap(); @@ -200,7 +200,7 @@ fn mouse_wheel_passes_delta_to_the_captured_frame() { -- XML-load-time only), so a runtime-created frame needs this explicitly, exactly as it -- needs EnableMouse. Mouse-enablement alone puts nothing in the wheel index. a:EnableMouseWheel(true) - a:SetScript("OnMouseWheel", function(self, delta) wheel = delta end) + a:SetScript("OnMouseWheel", function() local self, delta = this, arg1 wheel = delta end) "#, ) .unwrap(); @@ -344,8 +344,8 @@ fn a_second_fast_click_fires_on_double_click_instead_of_the_second_on_click() { clicks, doubles, dblbtn = 0, 0, nil local b = CreateFrame("Button", "DblB") b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(200); b:SetHeight(200); b:EnableMouse(true) - b:SetScript("OnClick", function(self, button) clicks = clicks + 1 end) - b:SetScript("OnDoubleClick", function(self, button) doubles = doubles + 1 dblbtn = button end) + b:SetScript("OnClick", function() local self, button = this, arg1 clicks = clicks + 1 end) + b:SetScript("OnDoubleClick", function() local self, button = this, arg1 doubles = doubles + 1 dblbtn = button end) "#, ) .unwrap(); @@ -483,7 +483,7 @@ fn a_multi_registered_button_pairs_a_left_click_with_a_right_one() { local b = CreateFrame("Button", "MixDbl") b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(200); b:SetHeight(200); b:EnableMouse(true) b:RegisterForClicks("LeftButtonUp", "RightButtonUp") - b:SetScript("OnDoubleClick", function(self, button) doubles = doubles + 1 dblbtn = button end) + b:SetScript("OnDoubleClick", function() local self, button = this, arg1 doubles = doubles + 1 dblbtn = button end) "#, ) .unwrap(); @@ -716,8 +716,8 @@ fn two_frames() -> crate::script::UiScript { local function mk(name, x) local f = CreateFrame("Frame", name) f:SetPoint("BOTTOMLEFT", x, 200); f:SetWidth(200); f:SetHeight(200); f:EnableMouse(true) - f:SetScript("OnMouseDown", function(self) table.insert(log, self:GetName()..":down") end) - f:SetScript("OnMouseUp", function(self) table.insert(log, self:GetName()..":up") end) + f:SetScript("OnMouseDown", function() local self = this table.insert(log, self:GetName()..":down") end) + f:SetScript("OnMouseUp", function() local self = this table.insert(log, self:GetName()..":up") end) return f end mk("A", 100) @@ -799,8 +799,8 @@ fn a_disabled_button_takes_the_hover_but_fires_no_enter_or_leave() { local function mk(name, x) local b = CreateFrame("Button", name) b:SetPoint("BOTTOMLEFT", x, 0); b:SetWidth(100); b:SetHeight(100); b:EnableMouse(true) - b:SetScript("OnEnter", function(self) table.insert(log, self:GetName()..":enter") end) - b:SetScript("OnLeave", function(self) table.insert(log, self:GetName()..":leave") end) + b:SetScript("OnEnter", function() local self = this table.insert(log, self:GetName()..":enter") end) + b:SetScript("OnLeave", function() local self = this table.insert(log, self:GetName()..":leave") end) return b end Off = mk("Off", 0) diff --git a/crates/benilla-ui/src/script/tests/reference_surface.rs b/crates/benilla-ui/src/script/tests/reference_surface.rs index 1bf260e7..c3490a3a 100644 --- a/crates/benilla-ui/src/script/tests/reference_surface.rs +++ b/crates/benilla-ui/src/script/tests/reference_surface.rs @@ -33,15 +33,13 @@ fn allowed_beyond_1_12() -> HashSet<&'static str> { // `getfenv(0)`); ours does, as our `getglobal`/`setglobal` are written over it. "_G", // ── WoW API past 1.12 ── - // Only `SubmitChatInput` is called from our UI files (`ScriptLogFrame.xml`'s slash - // commands); each of the rest awaits its 1.12 equivalent, removal or a reason. + // Each awaits its 1.12 equivalent, removal or a reason; none is called from our UI files. "CancelUnitBuff", "GetCursorInfo", "GetInventoryItemID", "GetPlayerFacing", "GetTradePartnerName", "IsGossipOptionCoded", - "SubmitChatInput", "UnitAura", "UnitIsAFK", "UnitIsDND", @@ -1514,3 +1512,24 @@ fn the_nameplate_verbs_ignore_their_arguments_and_return_nothing() { "zero values — observably different from nil for a caller that counts" ); } + +/// The host seams benilla's own interface used are gone, now that it speaks 1.12 alone: the key +/// capture, the error-log reads, the chat forwarders and the uncalled binding-set probe. +#[test] +fn the_layers_host_seams_are_gone() { + let s = script(); + for name in [ + "BenillaBindCapture", + "BenillaCharacterBindingsExist", + "BenillaGetNumScriptErrors", + "BenillaGetScriptErrorInfo", + "BenillaClearScriptErrors", + "SubmitChatInput", + "BenillaChatTabPressed", + ] { + assert!( + s.eval::(&format!("return {name} == nil")).unwrap(), + "{name} is still a global" + ); + } +} diff --git a/crates/benilla-ui/src/script/tests/scrollframe.rs b/crates/benilla-ui/src/script/tests/scrollframe.rs index 1e4a912c..689a6cf9 100644 --- a/crates/benilla-ui/src/script/tests/scrollframe.rs +++ b/crates/benilla-ui/src/script/tests/scrollframe.rs @@ -288,12 +288,12 @@ fn vertical_scroll_fires_as_given_and_update_rect_fires_range_changed() { frame:SetScrollChild(child) seen_v = nil - frame:SetScript("OnVerticalScroll", function(self, offset) + frame:SetScript("OnVerticalScroll", function() local self, offset = this, arg1 seen_v = offset assert(self == frame and arg1 == offset, "handler-firing conventions carry the value") end) seen_lo, seen_hi = nil, nil - frame:SetScript("OnScrollRangeChanged", function(self, lo, hi) + frame:SetScript("OnScrollRangeChanged", function() local self, lo, hi = this, arg1, arg2 seen_lo, seen_hi = lo, hi end) "#, @@ -336,7 +336,7 @@ fn update_scroll_child_rect_sees_a_same_tick_resize_with_no_intervening_resolve( s.run( r#" seen_hi = nil - SF:SetScript("OnScrollRangeChanged", function(self, lo, hi) seen_hi = hi end) + SF:SetScript("OnScrollRangeChanged", function() local self, lo, hi = this, arg1, arg2 seen_hi = hi end) Child:SetWidth(300); Child:SetHeight(600) -- grows well past the frame — all in this ONE tick SF:UpdateScrollChildRect() "#, diff --git a/crates/benilla-ui/src/script/tests/size_changed.rs b/crates/benilla-ui/src/script/tests/size_changed.rs index cd950a35..58985bf9 100644 --- a/crates/benilla-ui/src/script/tests/size_changed.rs +++ b/crates/benilla-ui/src/script/tests/size_changed.rs @@ -16,7 +16,7 @@ fn a_resize_fires_on_size_changed_and_a_move_does_not() { Panel = CreateFrame("Frame", "SizedPanel") Panel:SetPoint("BOTTOMLEFT", 100, 100) Panel:SetWidth(200); Panel:SetHeight(50) - Panel:SetScript("OnSizeChanged", function(self, aw, ah) + Panel:SetScript("OnSizeChanged", function() local self, aw, ah = this, arg1, arg2 fires = fires + 1 w = aw h = ah end) "#, @@ -62,7 +62,7 @@ fn an_anchor_driven_resize_fires_it_too() { Child = CreateFrame("Frame", "SizeChild", Parent) Child:SetPoint("BOTTOMLEFT", Parent, "BOTTOMLEFT", 0, 0) Child:SetPoint("TOPRIGHT", Parent, "TOPRIGHT", 0, 0) - Child:SetScript("OnSizeChanged", function(self, aw, ah) fires = fires + 1 w = aw end) + Child:SetScript("OnSizeChanged", function() local self, aw, ah = this, arg1, arg2 fires = fires + 1 w = aw end) "#, ) .unwrap(); @@ -94,7 +94,7 @@ fn a_handler_that_resizes_its_own_frame_settles_instead_of_spinning() { Sq:SetPoint("BOTTOMLEFT", 0, 0) Sq:SetWidth(200); Sq:SetHeight(50) -- The idiom: "keep me square". It writes back into the very input that fired it. - Sq:SetScript("OnSizeChanged", function(self, aw, ah) + Sq:SetScript("OnSizeChanged", function() local self, aw, ah = this, arg1, arg2 fires = fires + 1 if ah ~= aw then self:SetHeight(aw) end end) diff --git a/crates/benilla-ui/src/script/tests/slider.rs b/crates/benilla-ui/src/script/tests/slider.rs index 5549710c..078cad81 100644 --- a/crates/benilla-ui/src/script/tests/slider.rs +++ b/crates/benilla-ui/src/script/tests/slider.rs @@ -215,7 +215,7 @@ fn slider_setvalue_fires_onvaluechanged_only_on_change() { local sl = CreateFrame("Slider", "SlEvt") sl:SetMinMaxValues(0, 10) seen = {} - sl:SetScript("OnValueChanged", function(self, value) + sl:SetScript("OnValueChanged", function() local self, value = this, arg1 table.insert(seen, value) assert(self == sl and arg1 == value, "handler-firing conventions carry the value") end) diff --git a/crates/benilla-ui/src/script/tests/statusbar.rs b/crates/benilla-ui/src/script/tests/statusbar.rs index 62eb5925..c5418580 100644 --- a/crates/benilla-ui/src/script/tests/statusbar.rs +++ b/crates/benilla-ui/src/script/tests/statusbar.rs @@ -50,7 +50,7 @@ fn statusbar_setvalue_fires_onvaluechanged() { local b = CreateFrame("StatusBar", "SbEvt") b:SetMinMaxValues(0, 10) seen = {} - b:SetScript("OnValueChanged", function(self, value) + b:SetScript("OnValueChanged", function() local self, value = this, arg1 table.insert(seen, value) assert(self == b and arg1 == value, "handler-firing conventions carry the value") end) diff --git a/crates/benilla-ui/src/script/tests/tooltip_item/mechanics.rs b/crates/benilla-ui/src/script/tests/tooltip_item/mechanics.rs index e07b04ef..2c4c17e7 100644 --- a/crates/benilla-ui/src/script/tests/tooltip_item/mechanics.rs +++ b/crates/benilla-ui/src/script/tests/tooltip_item/mechanics.rs @@ -55,7 +55,7 @@ fn bag_item_money_law_and_fallback() { money_fired = nil local a = CreateFrame("Button", "Slot3"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10) local tt = CreateFrame("GameTooltip", "TT") - tt:SetScript("OnTooltipAddMoney", function(self) money_fired = arg1 end) + tt:SetScript("OnTooltipAddMoney", function() local self = this money_fired = arg1 end) -- No merchant: no money handler fires. tt:SetOwner(a, "ANCHOR_RIGHT") tt:SetBagItem(0, 1) diff --git a/crates/benilla-ui/src/script/tests/toplevel.rs b/crates/benilla-ui/src/script/tests/toplevel.rs index c8c7159d..05b56653 100644 --- a/crates/benilla-ui/src/script/tests/toplevel.rs +++ b/crates/benilla-ui/src/script/tests/toplevel.rs @@ -649,13 +649,13 @@ fn a_raise_with_a_hidden_child_keeps_the_windows_own_siblings_level() { Close:SetPoint("BOTTOMRIGHT", Win, "BOTTOMRIGHT", -15, 15) Close:SetWidth(100); Close:SetHeight(21) Close:EnableMouse(true) - Close:SetScript("OnEnter", function(self) hovered = self:GetName() end) - Close:SetScript("OnClick", function(self) clicked = self:GetName() end) + Close:SetScript("OnEnter", function() local self = this hovered = self:GetName() end) + Close:SetScript("OnClick", function() local self = this clicked = self:GetName() end) Body = CreateFrame("Frame", "Body", Win) -- level 2, covers the whole window Body:SetAllPoints(Win) Body:EnableMouse(true) - Body:SetScript("OnEnter", function(self) hovered = self:GetName() end) + Body:SetScript("OnEnter", function() local self = this hovered = self:GetName() end) Body:Hide() Win:Hide() diff --git a/crates/benilla-ui/src/script/tests/visibility.rs b/crates/benilla-ui/src/script/tests/visibility.rs index fcb21929..78f97e9f 100644 --- a/crates/benilla-ui/src/script/tests/visibility.rs +++ b/crates/benilla-ui/src/script/tests/visibility.rs @@ -47,7 +47,7 @@ fn tick_runs_onupdate_only_when_effectively_visible() { r#" ticks, last = 0, 0 local f = CreateFrame("Frame", "UF") - f:SetScript("OnUpdate", function(self, elapsed) ticks = ticks + 1; last = elapsed end) + f:SetScript("OnUpdate", function() local self, elapsed = this, arg1 ticks = ticks + 1; last = elapsed end) "#, ) .unwrap();