merge theme/thememanager

This commit is contained in:
Michael Freno
2025-11-13 08:51:53 -05:00
parent 45f40c4757
commit 64aef0daf1
3 changed files with 216 additions and 229 deletions

View File

@@ -22,7 +22,6 @@ local LayoutEngine = req("LayoutEngine")
local Renderer = req("Renderer")
local EventHandler = req("EventHandler")
local ScrollManager = req("ScrollManager")
local ThemeManager = req("ThemeManager")
-- Extract utilities
local enums = utils.enums
@@ -245,7 +244,7 @@ function Element.new(props)
-- Initialize state manager ID for immediate mode (use self.id which may be auto-generated)
self._stateId = self.id
self._themeManager = ThemeManager.new({
self._themeManager = Theme.Manager.new({
theme = props.theme or Gui.defaultTheme,
themeComponent = props.themeComponent or nil,
disabled = props.disabled or false,
@@ -253,8 +252,6 @@ function Element.new(props)
disableHighlight = props.disableHighlight,
scaleCorners = props.scaleCorners,
scalingAlgorithm = props.scalingAlgorithm,
}, {
Theme = Theme,
})
self._themeManager:initialize(self)

View File

@@ -44,7 +44,7 @@ local function getFlexLoveBasePath()
end
end
-- Fallback: try common paths
-- Fallback: try a common path
return "libs", "libs"
end
@@ -335,22 +335,17 @@ end
---@return Theme
function Theme.load(path)
local definition
-- Build the theme module path relative to FlexLove
local themePath = FLEXLOVE_BASE_PATH .. ".themes." .. path
local success, result = pcall(function()
return require(themePath)
end)
if success then
definition = result
else
-- Fallback: try as direct path
success, result = pcall(function()
return require(path)
end)
if success then
definition = result
else
@@ -359,14 +354,12 @@ function Theme.load(path)
end
local theme = Theme.new(definition)
-- Register theme by both its display name and load path
themes[theme.name] = theme
themes[path] = theme
return theme
end
--- Set the active theme
---@param themeOrName Theme|string
function Theme.setActive(themeOrName)
if type(themeOrName) == "string" then
@@ -494,4 +487,218 @@ function Theme.get(themeName)
return themes[themeName]
end
--------------------------------------------------------------------------------
-- ThemeManager: Instance-level theme state management
--------------------------------------------------------------------------------
---@class ThemeManager
---@field theme string? -- Override theme name
---@field themeComponent string? -- Component to use from theme
---@field _themeState string -- Current theme state (normal, hover, pressed, active, disabled)
---@field disabled boolean
---@field active boolean
---@field disableHighlight boolean -- If true, disable pressed highlight overlay
---@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
local ThemeManager = {}
ThemeManager.__index = ThemeManager
---@param config table Configuration options
---@return ThemeManager
function ThemeManager.new(config)
local self = setmetatable({}, ThemeManager)
self.theme = config.theme
self.themeComponent = config.themeComponent
self.disabled = config.disabled or false
self.active = config.active or false
self.disableHighlight = config.disableHighlight
self.scaleCorners = config.scaleCorners
self.scalingAlgorithm = config.scalingAlgorithm
self._themeState = "normal"
self._element = nil
return self
end
---@param element table The parent Element
function ThemeManager:initialize(element)
self._element = element
end
---@param isHovered boolean Whether element is hovered
---@param isPressed boolean Whether element is pressed
---@param isFocused boolean Whether element is focused
---@param isDisabled boolean Whether element is disabled
---@return string The new theme state
function ThemeManager:updateState(isHovered, isPressed, isFocused, isDisabled)
local newState = "normal"
if isDisabled or self.disabled then
newState = "disabled"
elseif self.active then
newState = "active"
elseif isPressed then
newState = "pressed"
elseif isHovered then
newState = "hover"
end
self._themeState = newState
return newState
end
---@return string The current theme state
function ThemeManager:getState()
return self._themeState
end
---@param state string The theme state to set
function ThemeManager:setState(state)
self._themeState = state
end
---@return boolean
function ThemeManager:hasThemeComponent()
return self.themeComponent ~= nil
end
---@return table?
function ThemeManager:getTheme()
if self.theme then
return Theme.get(self.theme)
end
return Theme.getActive()
end
---@return table?
function ThemeManager:getComponent()
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
return themeToUse.components[self.themeComponent]
end
---@return table?
function ThemeManager:getStateComponent()
local component = self:getComponent()
if not component then
return nil
end
local state = self._themeState
if state and state ~= "normal" and component.states and component.states[state] then
return component.states[state]
end
return component
end
---@param property string
---@return any?
function ThemeManager:getStyle(property)
local stateComponent = self:getStateComponent()
if not stateComponent then
return nil
end
return stateComponent[property]
end
---@param borderBoxWidth number
---@param borderBoxHeight number
---@return table? {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]
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
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,
}
end
return nil
end
---@return number?
function ThemeManager:getContentAutoSizingMultiplier()
if not self.themeComponent then
return nil
end
local themeToUse = self:getTheme()
if not themeToUse then
return nil
end
if self.themeComponent then
local component = themeToUse.components[self.themeComponent]
if component and component.contentAutoSizingMultiplier then
return component.contentAutoSizingMultiplier
elseif themeToUse.contentAutoSizingMultiplier then
return themeToUse.contentAutoSizingMultiplier
end
end
if themeToUse.contentAutoSizingMultiplier then
return themeToUse.contentAutoSizingMultiplier
end
return nil
end
---@return string?
function ThemeManager:getDefaultFontFamily()
local themeToUse = self:getTheme()
if themeToUse and themeToUse.fonts and themeToUse.fonts["default"] then
return themeToUse.fonts["default"]
end
return nil
end
---@param themeName string? The theme name
---@param componentName string? The component name
function ThemeManager:setTheme(themeName, componentName)
self.theme = themeName
self.themeComponent = componentName
end
Theme.Manager = ThemeManager
return Theme

View File

@@ -1,217 +0,0 @@
---@class ThemeManager
---@field theme string?
---@field themeComponent string?
---@field _themeState string -- Current theme state (normal, hover, pressed, active, disabled)
---@field disabled boolean
---@field active boolean
---@field disableHighlight boolean -- If true, disable pressed highlight overlay
---@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
---@field _Theme table
local ThemeManager = {}
ThemeManager.__index = ThemeManager
---@param config table Configuration options
---@param deps table Dependencies {Theme: Theme module}
---@return ThemeManager
function ThemeManager.new(config, deps)
local Theme = deps.Theme
local self = setmetatable({}, ThemeManager)
self._Theme = Theme
self.theme = config.theme
self.themeComponent = config.themeComponent
self.disabled = config.disabled or false
self.active = config.active or false
self.disableHighlight = config.disableHighlight
self.scaleCorners = config.scaleCorners
self.scalingAlgorithm = config.scalingAlgorithm
self._themeState = "normal"
self._element = nil
return self
end
---@param element table The parent Element
function ThemeManager:initialize(element)
self._element = element
end
---@param isHovered boolean Whether element is hovered
---@param isPressed boolean Whether element is pressed
---@param isFocused boolean Whether element is focused
---@param isDisabled boolean Whether element is disabled
---@return string The new theme state
function ThemeManager:updateState(isHovered, isPressed, isFocused, isDisabled)
local newState = "normal"
if isDisabled or self.disabled then
newState = "disabled"
elseif self.active then
newState = "active"
elseif isPressed then
newState = "pressed"
elseif isHovered then
newState = "hover"
end
self._themeState = newState
return newState
end
---@return string The current theme state
function ThemeManager:getState()
return self._themeState
end
---@param state string The theme state to set
function ThemeManager:setState(state)
self._themeState = state
end
---@return boolean
function ThemeManager:hasThemeComponent()
return self.themeComponent ~= nil
end
---@return table? The theme object or nil
function ThemeManager:getTheme()
if self.theme then
return self._Theme.get(self.theme)
end
return self._Theme.getActive()
end
---@return table? The component definition or nil
function ThemeManager:getComponent()
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
return themeToUse.components[self.themeComponent]
end
---@return table? The component definition for current state or nil
function ThemeManager:getStateComponent()
local component = self:getComponent()
if not component then
return nil
end
local state = self._themeState
if state and state ~= "normal" and component.states and component.states[state] then
return component.states[state]
end
return component
end
---@param property string The property name
---@return any? The property value or nil
function ThemeManager:getStyle(property)
local stateComponent = self:getStateComponent()
if not stateComponent then
return nil
end
return stateComponent[property]
end
---@param borderBoxWidth number
---@param borderBoxHeight number
---@return table? {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]
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
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,
}
end
return nil
end
---@return number? The multiplier or nil
function ThemeManager:getContentAutoSizingMultiplier()
if not self.themeComponent then
return nil
end
local themeToUse = self:getTheme()
if not themeToUse then
return nil
end
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
end
-- Fall back to theme default
if themeToUse.contentAutoSizingMultiplier then
return themeToUse.contentAutoSizingMultiplier
end
return nil
end
--- Get default font family from theme
---@return string? The font name or path, or nil
function ThemeManager:getDefaultFontFamily()
local themeToUse = self:getTheme()
if themeToUse and themeToUse.fonts and themeToUse.fonts["default"] then
return themeToUse.fonts["default"]
end
return nil
end
---@param themeName string? The theme name
---@param componentName string? The component name
function ThemeManager:setTheme(themeName, componentName)
self.theme = themeName
self.themeComponent = componentName
end
return ThemeManager