added theme state locks

This commit is contained in:
Michael Freno
2025-12-04 20:33:04 -05:00
parent 2554d76a84
commit 61500f9131
4 changed files with 150 additions and 2 deletions

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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)