merge theme/thememanager
This commit is contained in:
@@ -22,7 +22,6 @@ local LayoutEngine = req("LayoutEngine")
|
|||||||
local Renderer = req("Renderer")
|
local Renderer = req("Renderer")
|
||||||
local EventHandler = req("EventHandler")
|
local EventHandler = req("EventHandler")
|
||||||
local ScrollManager = req("ScrollManager")
|
local ScrollManager = req("ScrollManager")
|
||||||
local ThemeManager = req("ThemeManager")
|
|
||||||
|
|
||||||
-- Extract utilities
|
-- Extract utilities
|
||||||
local enums = utils.enums
|
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)
|
-- Initialize state manager ID for immediate mode (use self.id which may be auto-generated)
|
||||||
self._stateId = self.id
|
self._stateId = self.id
|
||||||
|
|
||||||
self._themeManager = ThemeManager.new({
|
self._themeManager = Theme.Manager.new({
|
||||||
theme = props.theme or Gui.defaultTheme,
|
theme = props.theme or Gui.defaultTheme,
|
||||||
themeComponent = props.themeComponent or nil,
|
themeComponent = props.themeComponent or nil,
|
||||||
disabled = props.disabled or false,
|
disabled = props.disabled or false,
|
||||||
@@ -253,8 +252,6 @@ function Element.new(props)
|
|||||||
disableHighlight = props.disableHighlight,
|
disableHighlight = props.disableHighlight,
|
||||||
scaleCorners = props.scaleCorners,
|
scaleCorners = props.scaleCorners,
|
||||||
scalingAlgorithm = props.scalingAlgorithm,
|
scalingAlgorithm = props.scalingAlgorithm,
|
||||||
}, {
|
|
||||||
Theme = Theme,
|
|
||||||
})
|
})
|
||||||
self._themeManager:initialize(self)
|
self._themeManager:initialize(self)
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ local function getFlexLoveBasePath()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Fallback: try common paths
|
-- Fallback: try a common path
|
||||||
return "libs", "libs"
|
return "libs", "libs"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -335,22 +335,17 @@ end
|
|||||||
---@return Theme
|
---@return Theme
|
||||||
function Theme.load(path)
|
function Theme.load(path)
|
||||||
local definition
|
local definition
|
||||||
|
|
||||||
-- Build the theme module path relative to FlexLove
|
|
||||||
local themePath = FLEXLOVE_BASE_PATH .. ".themes." .. path
|
local themePath = FLEXLOVE_BASE_PATH .. ".themes." .. path
|
||||||
|
|
||||||
local success, result = pcall(function()
|
local success, result = pcall(function()
|
||||||
return require(themePath)
|
return require(themePath)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
if success then
|
if success then
|
||||||
definition = result
|
definition = result
|
||||||
else
|
else
|
||||||
-- Fallback: try as direct path
|
|
||||||
success, result = pcall(function()
|
success, result = pcall(function()
|
||||||
return require(path)
|
return require(path)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
if success then
|
if success then
|
||||||
definition = result
|
definition = result
|
||||||
else
|
else
|
||||||
@@ -359,14 +354,12 @@ function Theme.load(path)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local theme = Theme.new(definition)
|
local theme = Theme.new(definition)
|
||||||
-- Register theme by both its display name and load path
|
|
||||||
themes[theme.name] = theme
|
themes[theme.name] = theme
|
||||||
themes[path] = theme
|
themes[path] = theme
|
||||||
|
|
||||||
return theme
|
return theme
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Set the active theme
|
|
||||||
---@param themeOrName Theme|string
|
---@param themeOrName Theme|string
|
||||||
function Theme.setActive(themeOrName)
|
function Theme.setActive(themeOrName)
|
||||||
if type(themeOrName) == "string" then
|
if type(themeOrName) == "string" then
|
||||||
@@ -494,4 +487,218 @@ function Theme.get(themeName)
|
|||||||
return themes[themeName]
|
return themes[themeName]
|
||||||
end
|
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
|
return Theme
|
||||||
|
|||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user