theme loading fixed, need to fix application

This commit is contained in:
Michael Freno
2025-10-13 09:03:23 -04:00
parent f9da77401a
commit 4ecfb7f354
4 changed files with 193 additions and 66 deletions

View File

@@ -83,6 +83,55 @@ Theme.__index = Theme
local themes = {} local themes = {}
local activeTheme = nil local activeTheme = nil
--- Auto-detect the base path where FlexLove is located
---@return string modulePath, string filesystemPath
local function getFlexLoveBasePath()
-- Get debug info to find where this file is loaded from
local info = debug.getinfo(1, "S")
if info and info.source then
local source = info.source
-- Remove leading @ if present
if source:sub(1, 1) == "@" then
source = source:sub(2)
end
-- Extract the directory path (remove FlexLove.lua)
local filesystemPath = source:match("(.*/)")
if filesystemPath then
-- Store the original filesystem path for loading assets
local fsPath = filesystemPath
-- Remove leading ./ if present
fsPath = fsPath:gsub("^%./", "")
-- Remove trailing /
fsPath = fsPath:gsub("/$", "")
-- Convert filesystem path to Lua module path
local modulePath = fsPath:gsub("/", ".")
return modulePath, fsPath
end
end
-- Fallback: try common paths
return "libs", "libs"
end
-- Store the base paths when module loads
local FLEXLOVE_BASE_PATH, FLEXLOVE_FILESYSTEM_PATH = getFlexLoveBasePath()
--- Helper function to resolve image paths relative to FlexLove
---@param imagePath string
---@return string
local function resolveImagePath(imagePath)
-- If path is already absolute or starts with known LÖVE paths, use as-is
if imagePath:match("^/") or imagePath:match("^[A-Z]:") then
return imagePath
end
-- Otherwise, make it relative to FlexLove's location
return FLEXLOVE_FILESYSTEM_PATH .. "/" .. imagePath
end
--- Create a new theme instance --- Create a new theme instance
---@param definition ThemeDefinition ---@param definition ThemeDefinition
---@return Theme ---@return Theme
@@ -93,7 +142,8 @@ function Theme.new(definition)
-- Load global atlas if it's a string path -- Load global atlas if it's a string path
if definition.atlas then if definition.atlas then
if type(definition.atlas) == "string" then if type(definition.atlas) == "string" then
self.atlas = love.graphics.newImage(definition.atlas) local resolvedPath = resolveImagePath(definition.atlas)
self.atlas = love.graphics.newImage(resolvedPath)
else else
self.atlas = definition.atlas self.atlas = definition.atlas
end end
@@ -106,7 +156,8 @@ function Theme.new(definition)
for componentName, component in pairs(self.components) do for componentName, component in pairs(self.components) do
if component.atlas then if component.atlas then
if type(component.atlas) == "string" then if type(component.atlas) == "string" then
component._loadedAtlas = love.graphics.newImage(component.atlas) local resolvedPath = resolveImagePath(component.atlas)
component._loadedAtlas = love.graphics.newImage(resolvedPath)
else else
component._loadedAtlas = component.atlas component._loadedAtlas = component.atlas
end end
@@ -117,7 +168,8 @@ function Theme.new(definition)
for stateName, stateComponent in pairs(component.states) do for stateName, stateComponent in pairs(component.states) do
if stateComponent.atlas then if stateComponent.atlas then
if type(stateComponent.atlas) == "string" then if type(stateComponent.atlas) == "string" then
stateComponent._loadedAtlas = love.graphics.newImage(stateComponent.atlas) local resolvedPath = resolveImagePath(stateComponent.atlas)
stateComponent._loadedAtlas = love.graphics.newImage(resolvedPath)
else else
stateComponent._loadedAtlas = stateComponent.atlas stateComponent._loadedAtlas = stateComponent.atlas
end end
@@ -130,35 +182,37 @@ function Theme.new(definition)
end end
--- Load a theme from a Lua file --- Load a theme from a Lua file
---@param path string -- Path to theme definition file ---@param path string -- Path to theme definition file (e.g., "space" or "mytheme")
---@return Theme ---@return Theme
function Theme.load(path) function Theme.load(path)
-- Check if it's a built-in theme
local builtInPath = "themes/" .. path .. ".lua"
local definition local definition
-- Try to load as built-in first -- Build the theme module path relative to FlexLove
local themePath = FLEXLOVE_BASE_PATH .. ".themes." .. path
local success, result = pcall(function() local success, result = pcall(function()
return love.filesystem.load(builtInPath)() return require(themePath)
end) end)
if success then if success then
definition = result definition = result
else else
-- Try to load as custom path -- Fallback: try as direct path
success, result = pcall(function() success, result = pcall(function()
return love.filesystem.load(path)() return require(path)
end) end)
if success then if success then
definition = result definition = result
else else
error("Failed to load theme from: " .. path .. "\nError: " .. tostring(result)) error("Failed to load theme '" .. path .. "'\nTried: " .. themePath .. "\nError: " .. tostring(result))
end end
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
return theme return theme
end end
@@ -665,15 +719,12 @@ end
---@field topElements table<integer, Element> ---@field topElements table<integer, Element>
---@field baseScale {width:number, height:number}? ---@field baseScale {width:number, height:number}?
---@field scaleFactors {x:number, y:number} ---@field scaleFactors {x:number, y:number}
---@field init fun(config: {baseScale: {width:number, height:number}}): nil ---@field defaultTheme string? -- Default theme name to use for elements
---@field resize fun(): nil
---@field draw fun(): nil
---@field update fun(dt:number): nil
---@field destroy fun(): nil
local Gui = { local Gui = {
topElements = {}, topElements = {},
baseScale = nil, baseScale = nil,
scaleFactors = { x = 1.0, y = 1.0 }, scaleFactors = { x = 1.0, y = 1.0 },
defaultTheme = nil,
} }
--- Initialize FlexLove with configuration --- Initialize FlexLove with configuration
@@ -693,14 +744,24 @@ function Gui.init(config)
-- Load and set theme if specified -- Load and set theme if specified
if config.theme then if config.theme then
if type(config.theme) == "string" then local success, err = pcall(function()
-- Load theme by name if type(config.theme) == "string" then
Theme.load(config.theme) -- Load theme by name
Theme.setActive(config.theme) Theme.load(config.theme)
elseif type(config.theme) == "table" then Theme.setActive(config.theme)
-- Load theme from definition Gui.defaultTheme = config.theme
local theme = Theme.new(config.theme) print("[FlexLove] Theme loaded: " .. config.theme)
Theme.setActive(theme) elseif type(config.theme) == "table" then
-- Load theme from definition
local theme = Theme.new(config.theme)
Theme.setActive(theme)
Gui.defaultTheme = theme.name
print("[FlexLove] Theme loaded: " .. theme.name)
end
end)
if not success then
print("[FlexLove] Failed to load theme: " .. tostring(err))
end end
end end
end end
@@ -1074,7 +1135,8 @@ Element.__index = Element
---@field gridColumns number? -- Number of columns in the grid (default: 1) ---@field gridColumns number? -- Number of columns in the grid (default: 1)
---@field columnGap number|string? -- Gap between grid columns ---@field columnGap number|string? -- Gap between grid columns
---@field rowGap number|string? -- Gap between grid rows ---@field rowGap number|string? -- Gap between grid rows
---@field theme string|{component:string, state:string?}? -- Theme component to use for rendering ---@field theme string? -- Theme name to use (e.g., "space", "dark"). Defaults to theme from Gui.init()
---@field themeComponent string? -- Theme component to use (e.g., "panel", "button", "input"). If nil, no theme is applied
---@field disabled boolean? -- Whether the element is disabled (default: false) ---@field disabled boolean? -- Whether the element is disabled (default: false)
---@field active boolean? -- Whether the element is active/focused (for inputs, default: false) ---@field active boolean? -- Whether the element is active/focused (for inputs, default: false)
local ElementProps = {} local ElementProps = {}
@@ -1095,9 +1157,15 @@ function Element.new(props)
self._touchPressed = {} self._touchPressed = {}
-- Initialize theme -- Initialize theme
self.theme = props.theme
self._themeState = "normal" self._themeState = "normal"
-- Handle theme property:
-- - theme: which theme to use (defaults to Gui.defaultTheme if not specified)
-- - themeComponent: which component from the theme (e.g., "panel", "button", "input")
-- If themeComponent is nil, no theme is applied (manual styling)
self.theme = props.theme or Gui.defaultTheme
self.themeComponent = props.themeComponent or nil
-- Initialize state properties -- Initialize state properties
self.disabled = props.disabled or false self.disabled = props.disabled or false
self.active = props.active or false self.active = props.active or false
@@ -2067,25 +2135,39 @@ function Element:draw()
end end
end end
-- Check if element has a theme -- Check if element has a theme component
local hasTheme = false local hasTheme = false
if self.theme then if self.themeComponent then
local componentName, state -- Get the theme to use
local themeToUse = nil
if type(self.theme) == "string" then if self.theme then
componentName = self.theme -- Element specifies a specific theme - load it if needed
state = self._themeState if themes[self.theme] then
themeToUse = themes[self.theme]
else
-- Try to load the theme
pcall(function()
Theme.load(self.theme)
end)
themeToUse = themes[self.theme]
end
else else
componentName = self.theme.component -- Use active theme
state = self.theme.state or self._themeState themeToUse = Theme.getActive()
end end
local component = Theme.getComponent(componentName, state) if themeToUse then
if component then -- Get the component from the theme
local activeTheme = Theme.getActive() local component = themeToUse.components[self.themeComponent]
if activeTheme then if component then
-- 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 -- Use component-specific atlas if available, otherwise use theme atlas
local atlasToUse = component._loadedAtlas or activeTheme.atlas local atlasToUse = component._loadedAtlas or themeToUse.atlas
if atlasToUse then if atlasToUse then
NineSlice.draw( NineSlice.draw(
@@ -2098,8 +2180,14 @@ function Element:draw()
self.opacity self.opacity
) )
hasTheme = true hasTheme = true
else
print("[FlexLove] No atlas for component: " .. self.themeComponent)
end end
else
print("[FlexLove] Component not found: " .. self.themeComponent .. " in theme: " .. themeToUse.name)
end end
else
print("[FlexLove] No theme available for themeComponent: " .. self.themeComponent)
end end
end end
@@ -2249,7 +2337,7 @@ function Element:update(dt)
end end
-- Handle click detection for element with enhanced event system -- Handle click detection for element with enhanced event system
if self.callback or self.theme then if self.callback or self.themeComponent then
local mx, my = love.mouse.getPosition() local mx, my = love.mouse.getPosition()
-- Clickable area is the border box (x, y already includes padding) -- Clickable area is the border box (x, y already includes padding)
local bx = self.x local bx = self.x
@@ -2259,7 +2347,7 @@ function Element:update(dt)
local isHovering = mx >= bx and mx <= bx + bw and my >= by and my <= by + bh local isHovering = mx >= bx and mx <= bx + bw and my >= by and my <= by + bh
-- Update theme state based on interaction -- Update theme state based on interaction
if self.theme then if self.themeComponent then
-- Disabled state takes priority -- Disabled state takes priority
if self.disabled then if self.disabled then
self._themeState = "disabled" self._themeState = "disabled"

View File

@@ -10,7 +10,7 @@ print("=== Base Scaling Demo ===\n")
-- Initialize with base scale (call this in love.load()) -- Initialize with base scale (call this in love.load())
Gui.init({ Gui.init({
baseScale = { width = 800, height = 600 } baseScale = { width = 800, height = 600 },
}) })
print("Designing UI for 800x600 base resolution\n") print("Designing UI for 800x600 base resolution\n")
@@ -32,22 +32,36 @@ local button = Gui.new({
}) })
print("At 800x600 (base resolution):") print("At 800x600 (base resolution):")
print(string.format(" Button: x=%d, y=%d, width=%d, height=%d, textSize=%d", print(
button.x, button.y, button.width, button.height, button.textSize)) string.format(
print(string.format(" Padding: left=%d, top=%d (NOT scaled)", " Button: x=%d, y=%d, width=%d, height=%d, textSize=%d",
button.padding.left, button.padding.top)) button.x,
button.y,
button.width,
button.height,
button.textSize
)
)
print(string.format(" Padding: left=%d, top=%d (NOT scaled)", button.padding.left, button.padding.top))
-- Simulate window resize to 1600x1200 (2x scale) -- Simulate window resize to 1600x1200 (2x scale)
print("\nResizing window to 1600x1200...") print("\nResizing window to 1600x1200...")
love.window.setMode(1600, 1200) love.window.setMode(1600, 1200)
Gui.resize() -- This updates all elements Gui.resize() -- This updates all elements
local sx, sy = Gui.getScaleFactors() local sx, sy = Gui.getScaleFactors()
print(string.format("Scale factors: x=%.1f, y=%.1f", sx, sy)) print(string.format("Scale factors: x=%.1f, y=%.1f", sx, sy))
print(string.format(" Button: x=%d, y=%d, width=%d, height=%d, textSize=%d", print(
button.x, button.y, button.width, button.height, button.textSize)) string.format(
print(string.format(" Padding: left=%d, top=%d (NOT scaled)", " Button: x=%d, y=%d, width=%d, height=%d, textSize=%d",
button.padding.left, button.padding.top)) button.x,
button.y,
button.width,
button.height,
button.textSize
)
)
print(string.format(" Padding: left=%d, top=%d (NOT scaled)", button.padding.left, button.padding.top))
-- Simulate window resize to 400x300 (0.5x scale) -- Simulate window resize to 400x300 (0.5x scale)
print("\nResizing window to 400x300...") print("\nResizing window to 400x300...")
@@ -56,16 +70,23 @@ Gui.resize()
sx, sy = Gui.getScaleFactors() sx, sy = Gui.getScaleFactors()
print(string.format("Scale factors: x=%.1f, y=%.1f", sx, sy)) print(string.format("Scale factors: x=%.1f, y=%.1f", sx, sy))
print(string.format(" Button: x=%d, y=%d, width=%d, height=%d, textSize=%d", print(
button.x, button.y, button.width, button.height, button.textSize)) string.format(
print(string.format(" Padding: left=%d, top=%d (NOT scaled)", " Button: x=%d, y=%d, width=%d, height=%d, textSize=%d",
button.padding.left, button.padding.top)) button.x,
button.y,
button.width,
button.height,
button.textSize
)
)
print(string.format(" Padding: left=%d, top=%d (NOT scaled)", button.padding.left, button.padding.top))
print("\n=== Usage ===") print("\n=== Usage ===")
print("In your main.lua:") print("In your main.lua:")
print([[ print([[
function love.load() function love.load()
local FlexLove = require("game.libs.FlexLove") local FlexLove = require("libs.FlexLove")
local Gui = FlexLove.GUI local Gui = FlexLove.GUI
-- Initialize with your design resolution -- Initialize with your design resolution

View File

@@ -1,21 +1,28 @@
-- Example: Setting theme in Gui.init() -- Example: Setting theme in Gui.init()
-- NOTE: This should be called in love.load() after LÖVE graphics is initialized
local FlexLove = require("FlexLove") local FlexLove = require("FlexLove")
local Gui = FlexLove.GUI local Gui = FlexLove.GUI
local Color = FlexLove.Color local Color = FlexLove.Color
local Theme = FlexLove.Theme
-- In love.load():
-- Initialize GUI with theme -- Initialize GUI with theme
Gui.init({ Gui.init({
baseScale = { width = 1920, height = 1080 }, baseScale = { width = 1920, height = 1080 },
theme = "space" -- Load and activate the space theme theme = "space" -- Load and activate the space theme
}) })
-- Alternative: Load theme manually if Gui.init() is called before love.load()
-- Theme.load("space")
-- Theme.setActive("space")
-- Now all elements can use the theme -- Now all elements can use the theme
local panel = Gui.new({ local panel = Gui.new({
x = 100, x = 100,
y = 100, y = 100,
width = 400, width = 400,
height = 300, height = 300,
theme = "panel", themeComponent = "panel",
padding = { top = 20, right = 20, bottom = 20, left = 20 }, padding = { top = 20, right = 20, bottom = 20, left = 20 },
}) })
@@ -28,7 +35,7 @@ local button1 = Gui.new({
text = "Normal Button", text = "Normal Button",
textAlign = "center", textAlign = "center",
textColor = Color.new(1, 1, 1, 1), textColor = Color.new(1, 1, 1, 1),
theme = "button", themeComponent = "button",
callback = function(element, event) callback = function(element, event)
if event.type == "click" then if event.type == "click" then
print("Button clicked!") print("Button clicked!")
@@ -45,7 +52,7 @@ local button2 = Gui.new({
text = "Disabled", text = "Disabled",
textAlign = "center", textAlign = "center",
textColor = Color.new(0.6, 0.6, 0.6, 1), textColor = Color.new(0.6, 0.6, 0.6, 1),
theme = "button", themeComponent = "button",
disabled = true, -- Shows disabled state disabled = true, -- Shows disabled state
callback = function(element, event) callback = function(element, event)
print("This won't fire!") print("This won't fire!")
@@ -60,7 +67,7 @@ local input1 = Gui.new({
height = 40, height = 40,
text = "Type here...", text = "Type here...",
textColor = Color.new(1, 1, 1, 1), textColor = Color.new(1, 1, 1, 1),
theme = "input", themeComponent = "input",
}) })
local input2 = Gui.new({ local input2 = Gui.new({
@@ -71,7 +78,7 @@ local input2 = Gui.new({
height = 40, height = 40,
text = "Active input", text = "Active input",
textColor = Color.new(1, 1, 1, 1), textColor = Color.new(1, 1, 1, 1),
theme = "input", themeComponent = "input",
active = true, -- Shows active/focused state active = true, -- Shows active/focused state
}) })
@@ -83,7 +90,7 @@ local input3 = Gui.new({
height = 40, height = 40,
text = "Disabled input", text = "Disabled input",
textColor = Color.new(0.6, 0.6, 0.6, 1), textColor = Color.new(0.6, 0.6, 0.6, 1),
theme = "input", themeComponent = "input",
disabled = true, -- Shows disabled state disabled = true, -- Shows disabled state
}) })

View File

@@ -1,7 +1,18 @@
-- Space Theme -- Space Theme
-- All images are 256x256 with perfectly centered 9-slice regions -- All images are 256x256 with perfectly centered 9-slice regions
local Color = require("FlexLove").Color -- Define Color inline to avoid circular dependency
local Color = {}
Color.__index = Color
function Color.new(r, g, b, a)
local self = setmetatable({}, Color)
self.r = r or 0
self.g = g or 0
self.b = b or 0
self.a = a or 1
return self
end
return { return {
name = "Space Theme", name = "Space Theme",