diff --git a/modules/Element.lua b/modules/Element.lua index 7ee1c3f..e40d8e6 100644 --- a/modules/Element.lua +++ b/modules/Element.lua @@ -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) diff --git a/modules/Theme.lua b/modules/Theme.lua index 622b23c..752edcd 100644 --- a/modules/Theme.lua +++ b/modules/Theme.lua @@ -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 diff --git a/modules/ThemeManager.lua b/modules/ThemeManager.lua deleted file mode 100644 index 803ed86..0000000 --- a/modules/ThemeManager.lua +++ /dev/null @@ -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