restart
This commit is contained in:
@@ -1,436 +0,0 @@
|
||||
--[[
|
||||
ThemeManager - Theme and State Management for FlexLove Elements
|
||||
Extracts all theme-related functionality from Element.lua into a dedicated module.
|
||||
Handles theme state management, component loading, 9-patch rendering, and property resolution.
|
||||
]]
|
||||
|
||||
-- Setup module path for relative requires
|
||||
local modulePath = (...):match("(.-)[^%.]+$")
|
||||
local function req(name)
|
||||
return require(modulePath .. name)
|
||||
end
|
||||
|
||||
-- Module dependencies
|
||||
local Theme = req("Theme")
|
||||
local NinePatch = req("NinePatch")
|
||||
local StateManager = req("StateManager")
|
||||
|
||||
--- Standardized error message formatter
|
||||
---@param module string -- Module name
|
||||
---@param message string -- Error message
|
||||
---@return string -- Formatted error message
|
||||
local function formatError(module, message)
|
||||
return string.format("[FlexLove.%s] %s", module, message)
|
||||
end
|
||||
|
||||
---@class ThemeManager
|
||||
---@field theme string? -- Theme name to use
|
||||
---@field themeComponent string? -- Component name from theme
|
||||
---@field _themeState string -- Current theme state (normal, hover, pressed, active, disabled)
|
||||
---@field disabled boolean -- Whether element is disabled
|
||||
---@field active boolean -- Whether element is active/focused
|
||||
---@field disableHighlight boolean -- Whether to disable pressed state highlight
|
||||
---@field scaleCorners number? -- Scale multiplier for 9-patch corners
|
||||
---@field scalingAlgorithm "nearest"|"bilinear"? -- Scaling algorithm for 9-patch
|
||||
---@field contentAutoSizingMultiplier table? -- Multiplier for auto-sized content
|
||||
---@field _element Element? -- Reference to parent element (set via initialize)
|
||||
---@field _stateId string? -- State manager ID for immediate mode
|
||||
local ThemeManager = {}
|
||||
ThemeManager.__index = ThemeManager
|
||||
|
||||
--- Create a new ThemeManager instance
|
||||
---@param config table -- Configuration options
|
||||
---@return ThemeManager
|
||||
function ThemeManager.new(config)
|
||||
local self = setmetatable({}, ThemeManager)
|
||||
|
||||
-- Theme configuration
|
||||
self.theme = config.theme
|
||||
self.themeComponent = config.themeComponent
|
||||
|
||||
-- State properties
|
||||
self._themeState = "normal"
|
||||
self.disabled = config.disabled or false
|
||||
self.active = config.active or false
|
||||
self.disableHighlight = config.disableHighlight
|
||||
|
||||
-- 9-patch rendering properties
|
||||
self.scaleCorners = config.scaleCorners
|
||||
self.scalingAlgorithm = config.scalingAlgorithm
|
||||
|
||||
-- Content sizing properties
|
||||
self.contentAutoSizingMultiplier = config.contentAutoSizingMultiplier
|
||||
|
||||
-- Element reference (set via initialize)
|
||||
self._element = nil
|
||||
self._stateId = config.stateId
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Initialize ThemeManager with parent element reference
|
||||
--- This links the ThemeManager to its parent element for accessing dimensions and state
|
||||
---@param element Element -- Parent element
|
||||
function ThemeManager:initialize(element)
|
||||
self._element = element
|
||||
self._stateId = element._stateId or element.id
|
||||
end
|
||||
|
||||
--- Update theme state based on interaction
|
||||
--- State priority: disabled > pressed > active > hover > normal
|
||||
---@param isHovered boolean -- Whether element is hovered
|
||||
---@param isPressed boolean -- Whether element is pressed (any button)
|
||||
---@param isFocused boolean -- Whether element is focused
|
||||
---@param isDisabled boolean -- Whether element is disabled
|
||||
function ThemeManager:updateState(isHovered, isPressed, isFocused, isDisabled)
|
||||
if not self.themeComponent then
|
||||
return
|
||||
end
|
||||
|
||||
local newThemeState = "normal"
|
||||
|
||||
-- State priority: disabled > active > pressed > hover > normal
|
||||
if isDisabled or self.disabled then
|
||||
newThemeState = "disabled"
|
||||
elseif self.active or isFocused then
|
||||
newThemeState = "active"
|
||||
elseif isPressed then
|
||||
newThemeState = "pressed"
|
||||
elseif isHovered then
|
||||
newThemeState = "hover"
|
||||
end
|
||||
|
||||
-- Update local state
|
||||
self._themeState = newThemeState
|
||||
|
||||
-- Update StateManager if in immediate mode
|
||||
if self._stateId then
|
||||
local GuiState = req("GuiState")
|
||||
if GuiState._immediateMode then
|
||||
StateManager.updateState(self._stateId, {
|
||||
hover = (newThemeState == "hover"),
|
||||
pressed = (newThemeState == "pressed"),
|
||||
focused = (newThemeState == "active" or isFocused),
|
||||
disabled = isDisabled or self.disabled,
|
||||
active = self.active,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Get current theme state
|
||||
---@return string -- Current state (normal, hover, pressed, active, disabled)
|
||||
function ThemeManager:getState()
|
||||
return self._themeState
|
||||
end
|
||||
|
||||
--- Get theme component for current state
|
||||
--- Returns the component data with state-specific overrides applied
|
||||
---@return table|nil -- Component data or nil if not found
|
||||
function ThemeManager:getThemeComponent()
|
||||
if not self.themeComponent then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Get the theme to use
|
||||
local themeToUse = self:_getTheme()
|
||||
if not themeToUse then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Get the component from the theme
|
||||
local component = themeToUse.components[self.themeComponent]
|
||||
if not component then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Check for state-specific override
|
||||
local state = self._themeState
|
||||
if state and state ~= "normal" and component.states and component.states[state] then
|
||||
component = component.states[state]
|
||||
end
|
||||
|
||||
return component
|
||||
end
|
||||
|
||||
--- Check if theme component exists
|
||||
---@return boolean
|
||||
function ThemeManager:hasThemeComponent()
|
||||
return self.themeComponent ~= nil and self:getThemeComponent() ~= nil
|
||||
end
|
||||
|
||||
--- Get the theme to use (element theme or active theme)
|
||||
---@return table|nil -- Theme data or nil if not found
|
||||
function ThemeManager:_getTheme()
|
||||
local themeToUse = nil
|
||||
|
||||
if self.theme then
|
||||
-- Element specifies a specific theme - load it if needed
|
||||
if Theme.get(self.theme) then
|
||||
themeToUse = Theme.get(self.theme)
|
||||
else
|
||||
-- Try to load the theme
|
||||
pcall(function()
|
||||
Theme.load(self.theme)
|
||||
end)
|
||||
themeToUse = Theme.get(self.theme)
|
||||
end
|
||||
else
|
||||
-- Use active theme
|
||||
themeToUse = Theme.getActive()
|
||||
end
|
||||
|
||||
return themeToUse
|
||||
end
|
||||
|
||||
--- Get atlas image for current component
|
||||
---@return love.Image|nil -- Atlas image or nil
|
||||
function ThemeManager:_getAtlas()
|
||||
local component = self:getThemeComponent()
|
||||
if not component then
|
||||
return nil
|
||||
end
|
||||
|
||||
local themeToUse = self:_getTheme()
|
||||
if not themeToUse then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Use component-specific atlas if available, otherwise use theme atlas
|
||||
return component._loadedAtlas or themeToUse.atlas
|
||||
end
|
||||
|
||||
--- Render theme component (9-patch or other)
|
||||
---@param x number -- X position
|
||||
---@param y number -- Y position
|
||||
---@param width number -- Width (border-box)
|
||||
---@param height number -- Height (border-box)
|
||||
---@param opacity number? -- Opacity (0-1)
|
||||
function ThemeManager:render(x, y, width, height, opacity)
|
||||
if not self.themeComponent then
|
||||
return
|
||||
end
|
||||
|
||||
opacity = opacity or 1
|
||||
|
||||
-- Get the theme to use
|
||||
local themeToUse = self:_getTheme()
|
||||
if not themeToUse then
|
||||
return
|
||||
end
|
||||
|
||||
-- Get the component from the theme
|
||||
local component = themeToUse.components[self.themeComponent]
|
||||
if not component then
|
||||
return
|
||||
end
|
||||
|
||||
-- Check for state-specific override
|
||||
local state = self._themeState
|
||||
if state and component.states and component.states[state] then
|
||||
component = component.states[state]
|
||||
end
|
||||
|
||||
-- Use component-specific atlas if available, otherwise use theme atlas
|
||||
local atlasToUse = component._loadedAtlas or themeToUse.atlas
|
||||
|
||||
if atlasToUse and component.regions then
|
||||
-- Validate component has required structure for 9-patch
|
||||
local hasAllRegions = component.regions.topLeft
|
||||
and component.regions.topCenter
|
||||
and component.regions.topRight
|
||||
and component.regions.middleLeft
|
||||
and component.regions.middleCenter
|
||||
and component.regions.middleRight
|
||||
and component.regions.bottomLeft
|
||||
and component.regions.bottomCenter
|
||||
and component.regions.bottomRight
|
||||
|
||||
if hasAllRegions then
|
||||
-- Render 9-patch with element-level overrides
|
||||
NinePatch.draw(
|
||||
component,
|
||||
atlasToUse,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
opacity,
|
||||
self.scaleCorners,
|
||||
self.scalingAlgorithm
|
||||
)
|
||||
else
|
||||
-- Silently skip drawing if component structure is invalid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Get styled property value from theme for current state
|
||||
--- This allows theme components to provide default values for properties
|
||||
---@param property string -- Property name (e.g., "backgroundColor", "textColor")
|
||||
---@return any|nil -- Property value or nil if not found
|
||||
function ThemeManager:getStyle(property)
|
||||
local component = self:getThemeComponent()
|
||||
if not component then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Check if component has style properties
|
||||
if component.style and component.style[property] then
|
||||
return component.style[property]
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Set theme and component
|
||||
---@param themeName string? -- Theme name
|
||||
---@param componentName string? -- Component name
|
||||
function ThemeManager:setTheme(themeName, componentName)
|
||||
self.theme = themeName
|
||||
self.themeComponent = componentName
|
||||
end
|
||||
|
||||
--- Get scale corners multiplier
|
||||
---@return number|nil
|
||||
function ThemeManager:getScaleCorners()
|
||||
-- Element-level override takes priority
|
||||
if self.scaleCorners ~= nil then
|
||||
return self.scaleCorners
|
||||
end
|
||||
|
||||
-- Fall back to component setting
|
||||
local component = self:getThemeComponent()
|
||||
if component and component.scaleCorners then
|
||||
return component.scaleCorners
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Get scaling algorithm
|
||||
---@return "nearest"|"bilinear"
|
||||
function ThemeManager:getScalingAlgorithm()
|
||||
-- Element-level override takes priority
|
||||
if self.scalingAlgorithm ~= nil then
|
||||
return self.scalingAlgorithm
|
||||
end
|
||||
|
||||
-- Fall back to component setting
|
||||
local component = self:getThemeComponent()
|
||||
if component and component.scalingAlgorithm then
|
||||
return component.scalingAlgorithm
|
||||
end
|
||||
|
||||
-- Default to bilinear
|
||||
return "bilinear"
|
||||
end
|
||||
|
||||
--- Get the current state's scaled content padding
|
||||
--- Returns the contentPadding for the current theme state, scaled to the element's size
|
||||
---@param borderBoxWidth number -- Border-box width
|
||||
---@param borderBoxHeight number -- Border-box height
|
||||
---@return table|nil -- {left, top, right, bottom} or nil if no contentPadding
|
||||
function ThemeManager:getScaledContentPadding(borderBoxWidth, borderBoxHeight)
|
||||
if not self.themeComponent then
|
||||
return nil
|
||||
end
|
||||
|
||||
local themeToUse = self:_getTheme()
|
||||
if not themeToUse or not themeToUse.components[self.themeComponent] then
|
||||
return nil
|
||||
end
|
||||
|
||||
local component = themeToUse.components[self.themeComponent]
|
||||
|
||||
-- Check for state-specific override
|
||||
local state = self._themeState or "normal"
|
||||
if state and state ~= "normal" and component.states and component.states[state] then
|
||||
component = component.states[state]
|
||||
end
|
||||
|
||||
if not component._ninePatchData or not component._ninePatchData.contentPadding then
|
||||
return nil
|
||||
end
|
||||
|
||||
local contentPadding = component._ninePatchData.contentPadding
|
||||
|
||||
-- Scale contentPadding to match the actual rendered size
|
||||
local atlasImage = component._loadedAtlas or themeToUse.atlas
|
||||
if atlasImage and type(atlasImage) ~= "string" then
|
||||
local originalWidth, originalHeight = atlasImage:getDimensions()
|
||||
local scaleX = borderBoxWidth / originalWidth
|
||||
local scaleY = borderBoxHeight / originalHeight
|
||||
|
||||
return {
|
||||
left = contentPadding.left * scaleX,
|
||||
top = contentPadding.top * scaleY,
|
||||
right = contentPadding.right * scaleX,
|
||||
bottom = contentPadding.bottom * scaleY,
|
||||
}
|
||||
else
|
||||
-- Return unscaled values as fallback
|
||||
return {
|
||||
left = contentPadding.left,
|
||||
top = contentPadding.top,
|
||||
right = contentPadding.right,
|
||||
bottom = contentPadding.bottom,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
--- Get content auto-sizing multiplier from theme
|
||||
--- Priority: element config > theme component > theme default
|
||||
---@return table -- {width, height} multipliers
|
||||
function ThemeManager:getContentAutoSizingMultiplier()
|
||||
-- If explicitly set in config, use that
|
||||
if self.contentAutoSizingMultiplier then
|
||||
return self.contentAutoSizingMultiplier
|
||||
end
|
||||
|
||||
-- Try to source from theme
|
||||
local themeToUse = self:_getTheme()
|
||||
if themeToUse then
|
||||
-- First check if themeComponent has a multiplier
|
||||
if self.themeComponent then
|
||||
local component = themeToUse.components[self.themeComponent]
|
||||
if component and component.contentAutoSizingMultiplier then
|
||||
return component.contentAutoSizingMultiplier
|
||||
elseif themeToUse.contentAutoSizingMultiplier then
|
||||
-- Fall back to theme default
|
||||
return themeToUse.contentAutoSizingMultiplier
|
||||
end
|
||||
elseif themeToUse.contentAutoSizingMultiplier then
|
||||
return themeToUse.contentAutoSizingMultiplier
|
||||
end
|
||||
end
|
||||
|
||||
-- Default multiplier
|
||||
return { 1, 1 }
|
||||
end
|
||||
|
||||
--- Update disabled state
|
||||
---@param disabled boolean
|
||||
function ThemeManager:setDisabled(disabled)
|
||||
self.disabled = disabled
|
||||
end
|
||||
|
||||
--- Update active state
|
||||
---@param active boolean
|
||||
function ThemeManager:setActive(active)
|
||||
self.active = active
|
||||
end
|
||||
|
||||
--- Get disabled state
|
||||
---@return boolean
|
||||
function ThemeManager:isDisabled()
|
||||
return self.disabled
|
||||
end
|
||||
|
||||
--- Get active state
|
||||
---@return boolean
|
||||
function ThemeManager:isActive()
|
||||
return self.active
|
||||
end
|
||||
|
||||
return ThemeManager
|
||||
Reference in New Issue
Block a user