From 61500f9131be357ada559cf7c6ba0573b216005e Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Thu, 4 Dec 2025 20:33:04 -0500 Subject: [PATCH] added theme state locks --- modules/Element.lua | 6 ++ modules/ErrorHandler.lua | 24 ++++++++ modules/Theme.lua | 121 ++++++++++++++++++++++++++++++++++++++- modules/types.lua | 1 + 4 files changed, 150 insertions(+), 2 deletions(-) diff --git a/modules/Element.lua b/modules/Element.lua index 57e8030..3f16df8 100644 --- a/modules/Element.lua +++ b/modules/Element.lua @@ -309,9 +309,15 @@ function Element.new(props) disabled = props.disabled or false, active = props.active or false, disableHighlight = props.disableHighlight, + themeStateLock = props.themeStateLock or false, scaleCorners = props.scaleCorners, scalingAlgorithm = props.scalingAlgorithm, }) + + -- Validate themeStateLock after ThemeManager is created + if props.themeStateLock and props.themeComponent then + self._themeManager:validateThemeStateLock() + end -- Expose theme properties for backward compatibility self.theme = self._themeManager.theme diff --git a/modules/ErrorHandler.lua b/modules/ErrorHandler.lua index e0ddf02..7961282 100644 --- a/modules/ErrorHandler.lua +++ b/modules/ErrorHandler.lua @@ -187,6 +187,30 @@ local ErrorCodes = { description = "Invalid theme color", suggestion = "Theme colors must be valid color values (hex, rgba, Color object)", }, + THM_007 = { + code = "FLEXLOVE_THM_007", + category = "THM", + description = "themeStateLock has no effect without a valid theme component", + suggestion = "Ensure themeComponent is set and valid when using themeStateLock", + }, + THM_008 = { + code = "FLEXLOVE_THM_008", + category = "THM", + description = "Theme component has no state variants", + suggestion = "themeStateLock has no effect on components without state variants", + }, + THM_009 = { + code = "FLEXLOVE_THM_009", + category = "THM", + description = "Requested theme state does not exist", + suggestion = "Use one of the available theme states or set themeStateLock to false", + }, + THM_010 = { + code = "FLEXLOVE_THM_010", + category = "THM", + description = "Invalid themeStateLock type", + suggestion = "themeStateLock must be boolean or string (state name)", + }, -- Event Errors (EVT_001 - EVT_099) EVT_001 = { diff --git a/modules/Theme.lua b/modules/Theme.lua index 1a3139d..0dd3020 100644 --- a/modules/Theme.lua +++ b/modules/Theme.lua @@ -742,6 +742,7 @@ end ---@field disabled boolean ---@field active boolean ---@field disableHighlight boolean -- If true, disable pressed highlight overlay +---@field themeStateLock boolean|string? -- Lock theme state: true/"default" = lock to base state, false = normal behavior, string = specific state ---@field scaleCorners number? -- Scale multiplier for 9-patch corners/edges ---@field scalingAlgorithm string? -- "nearest" or "bilinear" scaling for 9-patch ---@field _element Element? -- Reference to parent Element @@ -749,7 +750,7 @@ local ThemeManager = {} ThemeManager.__index = ThemeManager ---Create a new ThemeManager instance ----@param config table Configuration options {theme: string?, themeComponent: string?, disabled: boolean?, active: boolean?, disableHighlight: boolean?, scaleCorners: number?, scalingAlgorithm: string?} +---@param config table Configuration options {theme: string?, themeComponent: string?, disabled: boolean?, active: boolean?, disableHighlight: boolean?, themeStateLock: boolean|string?, scaleCorners: number?, scalingAlgorithm: string?} ---@return ThemeManager manager The new ThemeManager instance function ThemeManager.new(config) local self = setmetatable({}, ThemeManager) @@ -759,10 +760,18 @@ function ThemeManager.new(config) self.disabled = config.disabled or false self.active = config.active or false self.disableHighlight = config.disableHighlight + self.themeStateLock = config.themeStateLock or false self.scaleCorners = config.scaleCorners self.scalingAlgorithm = config.scalingAlgorithm - self._themeState = "normal" + -- Set initial state based on themeStateLock + if self.themeStateLock == true or self.themeStateLock == "default" then + self._themeState = "normal" + elseif type(self.themeStateLock) == "string" then + self._themeState = self.themeStateLock + else + self._themeState = "normal" + end return self end @@ -774,6 +783,31 @@ end ---@param isDisabled boolean Whether element is disabled ---@return string state The new theme state ("normal", "hover", "pressed", "active", "disabled") function ThemeManager:updateState(isHovered, isPressed, isFocused, isDisabled) + -- If themeStateLock is set (and not false), use the locked state + if self.themeStateLock ~= false and self.themeStateLock ~= nil then + local lockedState + + if self.themeStateLock == true or self.themeStateLock == "default" then + -- true or "default" means lock to "normal" (base state) + lockedState = "normal" + elseif type(self.themeStateLock) == "string" then + -- String means lock to specific state + lockedState = self.themeStateLock + + -- Validate the locked state exists in the theme component (will be done during initialization) + -- For now, just use the string value + else + -- Invalid themeStateLock value, fall back to normal behavior + lockedState = nil + end + + if lockedState then + self._themeState = lockedState + return lockedState + end + end + + -- Normal behavior: calculate state based on interaction local newState = "normal" if isDisabled or self.disabled then @@ -961,6 +995,89 @@ function ThemeManager:setTheme(themeName, componentName) self.themeComponent = componentName end +---Validate themeStateLock and warn if invalid +---@return boolean isValid True if themeStateLock is valid or false/nil +function ThemeManager:validateThemeStateLock() + -- false or nil is always valid (no lock) + if not self.themeStateLock or self.themeStateLock == false then + return true + end + + -- true is always valid (lock to normal) + if self.themeStateLock == true then + return true + end + + -- String value needs validation + if type(self.themeStateLock) == "string" then + -- "default" is always valid (lock to normal/base state) + if self.themeStateLock == "default" then + return true + end + + local component = self:getComponent() + + -- If no component, warn that themeStateLock has no effect + if not component then + if self.themeComponent then + Theme._ErrorHandler:warn("Theme", "THM_007", { + themeComponent = self.themeComponent, + reason = "themeStateLock has no effect without a valid theme component", + }) + end + self.themeStateLock = false + return false + end + + -- Check if component has any states at all + if not component.states or type(component.states) ~= "table" or next(component.states) == nil then + Theme._ErrorHandler:warn("Theme", "THM_008", { + themeComponent = self.themeComponent, + reason = "Theme component has no state variants, themeStateLock has no effect", + }) + self.themeStateLock = false + return false + end + + -- Check if the specified state exists + if not component.states[self.themeStateLock] then + -- Warn and fall back to false (no lock) + Theme._ErrorHandler:warn("Theme", "THM_009", { + themeComponent = self.themeComponent, + requestedState = self.themeStateLock, + availableStates = table.concat(self:_getAvailableStates(component), ", "), + fallback = "themeStateLock disabled (using dynamic state)", + }) + self.themeStateLock = false + return false + end + + return true + end + + -- Invalid type for themeStateLock + Theme._ErrorHandler:warn("Theme", "THM_010", { + themeStateLockType = type(self.themeStateLock), + reason = "themeStateLock must be boolean or string", + fallback = "themeStateLock disabled", + }) + self.themeStateLock = false + return false +end + +---Get available state names for a component +---@param component ThemeComponent The component to check +---@return table stateNames Array of state names +function ThemeManager:_getAvailableStates(component) + local states = {} + if component and component.states and type(component.states) == "table" then + for stateName, _ in pairs(component.states) do + table.insert(states, stateName) + end + end + return states +end + Theme.Manager = ThemeManager --- Check theme definitions for correctness before use to catch configuration errors early diff --git a/modules/types.lua b/modules/types.lua index dce8183..d758b53 100644 --- a/modules/types.lua +++ b/modules/types.lua @@ -91,6 +91,7 @@ local AnimationProps = {} ---@field disabled boolean? -- Whether the element is disabled (default: false) ---@field active boolean? -- Whether the element is active/focused (for inputs, default: false) ---@field disableHighlight boolean? -- Whether to disable the pressed state highlight overlay (default: false, or true when using themeComponent) +---@field themeStateLock boolean|string? -- Lock theme state: true/"default" = lock to base state, false = normal behavior, string = specific state ("hover", "pressed", "active", "disabled") (default: false) ---@field contentAutoSizingMultiplier {width:number?, height:number?}? -- Multiplier for auto-sized content dimensions (default: sourced from theme or {1, 1}) ---@field scaleCorners number? -- Scale multiplier for 9-patch corners/edges. E.g., 2 = 2x size (overrides theme setting) ---@field scalingAlgorithm "nearest"|"bilinear"? -- Scaling algorithm for 9-patch corners: "nearest" (sharp/pixelated) or "bilinear" (smooth) (overrides theme setting)