starting theme system

This commit is contained in:
Michael Freno
2025-10-12 18:38:16 -04:00
parent 18ff2c8223
commit 7306f036e0
5 changed files with 983 additions and 122 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
Cartographer.lua Cartographer.lua
OverlayStats.lua OverlayStats.lua
.DS_STORE

View File

@@ -48,6 +48,274 @@ function Color.fromHex(hexWithTag)
end end
end end
-- ====================
-- Theme System
-- ====================
---@class ThemeRegion
---@field x number -- X position in atlas
---@field y number -- Y position in atlas
---@field w number -- Width in atlas
---@field h number -- Height in atlas
---@class ThemeComponent
---@field atlas string|love.Image? -- Optional: component-specific atlas (overrides theme atlas)
---@field regions {topLeft:ThemeRegion, topCenter:ThemeRegion, topRight:ThemeRegion, middleLeft:ThemeRegion, middleCenter:ThemeRegion, middleRight:ThemeRegion, bottomLeft:ThemeRegion, bottomCenter:ThemeRegion, bottomRight:ThemeRegion}
---@field stretch {horizontal:table<integer, string>, vertical:table<integer, string>}
---@field states table<string, ThemeComponent>?
---@field _loadedAtlas love.Image? -- Internal: cached loaded atlas image
---@class ThemeDefinition
---@field name string
---@field atlas string|love.Image? -- Optional: global atlas (can be overridden per component)
---@field components table<string, ThemeComponent>
---@field colors table<string, Color>?
---@class Theme
---@field name string
---@field atlas love.Image? -- Optional: global atlas
---@field components table<string, ThemeComponent>
---@field colors table<string, Color>
local Theme = {}
Theme.__index = Theme
-- Global theme registry
local themes = {}
local activeTheme = nil
--- Create a new theme instance
---@param definition ThemeDefinition
---@return Theme
function Theme.new(definition)
local self = setmetatable({}, Theme)
self.name = definition.name
-- Load global atlas if it's a string path
if definition.atlas then
if type(definition.atlas) == "string" then
self.atlas = love.graphics.newImage(definition.atlas)
else
self.atlas = definition.atlas
end
end
self.components = definition.components or {}
self.colors = definition.colors or {}
-- Load component-specific atlases
for componentName, component in pairs(self.components) do
if component.atlas then
if type(component.atlas) == "string" then
component._loadedAtlas = love.graphics.newImage(component.atlas)
else
component._loadedAtlas = component.atlas
end
end
-- Also load atlases for component states
if component.states then
for stateName, stateComponent in pairs(component.states) do
if stateComponent.atlas then
if type(stateComponent.atlas) == "string" then
stateComponent._loadedAtlas = love.graphics.newImage(stateComponent.atlas)
else
stateComponent._loadedAtlas = stateComponent.atlas
end
end
end
end
end
return self
end
--- Load a theme from a Lua file
---@param path string -- Path to theme definition file
---@return Theme
function Theme.load(path)
-- Check if it's a built-in theme
local builtInPath = "themes/" .. path .. ".lua"
local definition
-- Try to load as built-in first
local success, result = pcall(function()
return love.filesystem.load(builtInPath)()
end)
if success then
definition = result
else
-- Try to load as custom path
success, result = pcall(function()
return love.filesystem.load(path)()
end)
if success then
definition = result
else
error("Failed to load theme from: " .. path .. "\nError: " .. tostring(result))
end
end
local theme = Theme.new(definition)
themes[theme.name] = theme
return theme
end
--- Set the active theme
---@param themeOrName Theme|string
function Theme.setActive(themeOrName)
if type(themeOrName) == "string" then
-- Try to load if not already loaded
if not themes[themeOrName] then
Theme.load(themeOrName)
end
activeTheme = themes[themeOrName]
else
activeTheme = themeOrName
end
if not activeTheme then
error("Failed to set active theme: " .. tostring(themeOrName))
end
end
--- Get the active theme
---@return Theme?
function Theme.getActive()
return activeTheme
end
--- Get a component from the active theme
---@param componentName string
---@param state string?
---@return ThemeComponent?
function Theme.getComponent(componentName, state)
if not activeTheme then
return nil
end
local component = activeTheme.components[componentName]
if not component then
return nil
end
-- Check for state-specific override
if state and component.states and component.states[state] then
return component.states[state]
end
return component
end
-- ====================
-- NineSlice Renderer
-- ====================
local NineSlice = {}
--- Draw a 9-slice component
---@param component ThemeComponent
---@param atlas love.Image
---@param x number
---@param y number
---@param width number
---@param height number
---@param opacity number?
function NineSlice.draw(component, atlas, x, y, width, height, opacity)
if not component or not atlas then
return
end
opacity = opacity or 1
love.graphics.setColor(1, 1, 1, opacity)
local regions = component.regions
-- Calculate dimensions
local cornerWidth = regions.topLeft.w
local cornerHeight = regions.topLeft.h
local rightCornerWidth = regions.topRight.w
local rightCornerHeight = regions.topRight.h
local bottomLeftHeight = regions.bottomLeft.h
local bottomRightHeight = regions.bottomRight.h
-- Center dimensions (stretchable area)
local centerWidth = width - cornerWidth - rightCornerWidth
local centerHeight = height - cornerHeight - bottomLeftHeight
-- Create quads for each region
local atlasWidth, atlasHeight = atlas:getDimensions()
-- Helper to create quad
local function makeQuad(region)
return love.graphics.newQuad(region.x, region.y, region.w, region.h, atlasWidth, atlasHeight)
end
-- Top-left corner
love.graphics.draw(atlas, makeQuad(regions.topLeft), x, y)
-- Top-right corner
love.graphics.draw(atlas, makeQuad(regions.topRight), x + width - rightCornerWidth, y)
-- Bottom-left corner
love.graphics.draw(atlas, makeQuad(regions.bottomLeft), x, y + height - bottomLeftHeight)
-- Bottom-right corner
love.graphics.draw(atlas, makeQuad(regions.bottomRight), x + width - rightCornerWidth, y + height - bottomRightHeight)
-- Top edge (stretched)
if centerWidth > 0 then
local scaleX = centerWidth / regions.topCenter.w
love.graphics.draw(atlas, makeQuad(regions.topCenter), x + cornerWidth, y, 0, scaleX, 1)
end
-- Bottom edge (stretched)
if centerWidth > 0 then
local scaleX = centerWidth / regions.bottomCenter.w
love.graphics.draw(
atlas,
makeQuad(regions.bottomCenter),
x + cornerWidth,
y + height - bottomLeftHeight,
0,
scaleX,
1
)
end
-- Left edge (stretched)
if centerHeight > 0 then
local scaleY = centerHeight / regions.middleLeft.h
love.graphics.draw(atlas, makeQuad(regions.middleLeft), x, y + cornerHeight, 0, 1, scaleY)
end
-- Right edge (stretched)
if centerHeight > 0 then
local scaleY = centerHeight / regions.middleRight.h
love.graphics.draw(
atlas,
makeQuad(regions.middleRight),
x + width - rightCornerWidth,
y + cornerHeight,
0,
1,
scaleY
)
end
-- Center (stretched both ways)
if centerWidth > 0 and centerHeight > 0 then
local scaleX = centerWidth / regions.middleCenter.w
local scaleY = centerHeight / regions.middleCenter.h
love.graphics.draw(atlas, makeQuad(regions.middleCenter), x + cornerWidth, y + cornerHeight, 0, scaleX, scaleY)
end
-- Reset color
love.graphics.setColor(1, 1, 1, 1)
end
local enums = { local enums = {
---@enum TextAlign ---@enum TextAlign
TextAlign = { START = "start", CENTER = "center", END = "end", JUSTIFY = "justify" }, TextAlign = { START = "start", CENTER = "center", END = "end", JUSTIFY = "justify" },
@@ -521,7 +789,7 @@ local function getModifiers()
shift = love.keyboard.isDown("lshift", "rshift"), shift = love.keyboard.isDown("lshift", "rshift"),
ctrl = love.keyboard.isDown("lctrl", "rctrl"), ctrl = love.keyboard.isDown("lctrl", "rctrl"),
alt = love.keyboard.isDown("lalt", "ralt"), alt = love.keyboard.isDown("lalt", "ralt"),
cmd = love.keyboard.isDown("lgui", "rgui") -- Mac Command key cmd = love.keyboard.isDown("lgui", "rgui"), -- Mac Command key
} }
end end
@@ -746,6 +1014,10 @@ end
---@field gridColumns number? -- Number of columns in the grid ---@field gridColumns number? -- Number of columns in the grid
---@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 _themeState string? -- Current theme state (normal, hover, pressed, active, disabled)
---@field disabled boolean? -- Whether the element is disabled (default: false)
---@field active boolean? -- Whether the element is active/focused (for inputs, default: false)
local Element = {} local Element = {}
Element.__index = Element Element.__index = Element
@@ -789,6 +1061,9 @@ 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 disabled boolean? -- Whether the element is disabled (default: false)
---@field active boolean? -- Whether the element is active/focused (for inputs, default: false)
local ElementProps = {} local ElementProps = {}
---@param props ElementProps ---@param props ElementProps
@@ -806,6 +1081,14 @@ function Element.new(props)
self._clickCount = 0 self._clickCount = 0
self._touchPressed = {} self._touchPressed = {}
-- Initialize theme
self.theme = props.theme
self._themeState = "normal"
-- Initialize state properties
self.disabled = props.disabled or false
self.active = props.active or false
-- Set parent first so it's available for size calculations -- Set parent first so it's available for size calculations
self.parent = props.parent self.parent = props.parent
@@ -1771,6 +2054,44 @@ function Element:draw()
end end
end end
-- Check if element has a theme
local hasTheme = false
if self.theme then
local componentName, state
if type(self.theme) == "string" then
componentName = self.theme
state = self._themeState
else
componentName = self.theme.component
state = self.theme.state or self._themeState
end
local component = Theme.getComponent(componentName, state)
if component then
local activeTheme = Theme.getActive()
if activeTheme then
-- Use component-specific atlas if available, otherwise use theme atlas
local atlasToUse = component._loadedAtlas or activeTheme.atlas
if atlasToUse then
NineSlice.draw(
component,
atlasToUse,
self.x,
self.y,
self.width + self.padding.left + self.padding.right,
self.height + self.padding.top + self.padding.bottom,
self.opacity
)
hasTheme = true
end
end
end
end
-- Draw background if no theme is used
if not hasTheme then
-- Apply opacity to all drawing operations -- Apply opacity to all drawing operations
-- (x, y) represents border box, so draw background from (x, y) -- (x, y) represents border box, so draw background from (x, y)
local backgroundWithOpacity = local backgroundWithOpacity =
@@ -1783,7 +2104,10 @@ function Element:draw()
self.width + self.padding.left + self.padding.right, self.width + self.padding.left + self.padding.right,
self.height + self.padding.top + self.padding.bottom self.height + self.padding.top + self.padding.bottom
) )
-- Draw borders based on border property end
-- Draw borders based on border property (skip if using theme)
if not hasTheme then
local borderColorWithOpacity = local borderColorWithOpacity =
Color.new(self.borderColor.r, self.borderColor.g, self.borderColor.b, self.borderColor.a * self.opacity) Color.new(self.borderColor.r, self.borderColor.g, self.borderColor.b, self.borderColor.a * self.opacity)
love.graphics.setColor(borderColorWithOpacity:toRGBA()) love.graphics.setColor(borderColorWithOpacity:toRGBA())
@@ -1809,6 +2133,7 @@ function Element:draw()
self.y + self.height + self.padding.top + self.padding.bottom self.y + self.height + self.padding.top + self.padding.bottom
) )
end end
end
-- Draw element text if present -- Draw element text if present
if self.text then if self.text then
@@ -1911,7 +2236,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 then if self.callback or self.theme 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
@@ -1920,8 +2245,38 @@ function Element:update(dt)
local bh = self.height + self.padding.top + self.padding.bottom local bh = self.height + self.padding.top + self.padding.bottom
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
if self.theme then
-- Disabled state takes priority
if self.disabled then
self._themeState = "disabled"
-- Active state (for inputs when focused/typing)
elseif self.active then
self._themeState = "active"
elseif isHovering then
-- Check if any button is pressed
local anyPressed = false
for _, pressed in pairs(self._pressed) do
if pressed then
anyPressed = true
break
end
end
if anyPressed then
self._themeState = "pressed"
else
self._themeState = "hover"
end
else
self._themeState = "normal"
end
end
-- Only process button events if callback exists and element is not disabled
if self.callback and not self.disabled then
-- Check all three mouse buttons -- Check all three mouse buttons
local buttons = {1, 2, 3} -- left, right, middle local buttons = { 1, 2, 3 } -- left, right, middle
for _, button in ipairs(buttons) do for _, button in ipairs(buttons) do
if isHovering then if isHovering then
@@ -1950,9 +2305,11 @@ function Element:update(dt)
local clickCount = 1 local clickCount = 1
local doubleClickThreshold = 0.3 -- 300ms for double-click local doubleClickThreshold = 0.3 -- 300ms for double-click
if self._lastClickTime if
self._lastClickTime
and self._lastClickButton == button and self._lastClickButton == button
and (currentTime - self._lastClickTime) < doubleClickThreshold then and (currentTime - self._lastClickTime) < doubleClickThreshold
then
clickCount = self._clickCount + 1 clickCount = self._clickCount + 1
else else
clickCount = 1 clickCount = 1
@@ -1998,8 +2355,10 @@ function Element:update(dt)
self._pressed[button] = false self._pressed[button] = false
end end
end end
end -- end if self.callback
-- Handle touch events (maintain backward compatibility) -- Handle touch events (maintain backward compatibility)
if self.callback then
local touches = love.touch.getTouches() local touches = love.touch.getTouches()
for _, id in ipairs(touches) do for _, id in ipairs(touches) do
local tx, ty = love.touch.getPosition(id) local tx, ty = love.touch.getPosition(id)
@@ -2020,6 +2379,7 @@ function Element:update(dt)
end end
end end
end end
end
end end
--- Recalculate units based on new viewport dimensions (for vw, vh, % units) --- Recalculate units based on new viewport dimensions (for vw, vh, % units)
@@ -2308,4 +2668,5 @@ end
Gui.new = Element.new Gui.new = Element.new
Gui.Element = Element Gui.Element = Element
Gui.Animation = Animation Gui.Animation = Animation
return { GUI = Gui, Color = Color, enums = enums } Gui.Theme = Theme
return { GUI = Gui, Color = Color, Theme = Theme, enums = enums }

View File

@@ -0,0 +1,216 @@
local FlexLove = require("FlexLove")
local Gui = FlexLove.GUI
local Theme = FlexLove.Theme
local Color = FlexLove.Color
---@class ThemeDemo
---@field window Element
---@field statusText Element
local ThemeDemo = {}
ThemeDemo.__index = ThemeDemo
function ThemeDemo.init()
local self = setmetatable({}, ThemeDemo)
-- Try to load and set the default theme
-- Note: This will fail if the atlas image doesn't exist yet
-- For now, we'll demonstrate the API without actually loading a theme
local themeLoaded = false
local themeError = nil
pcall(function()
Theme.load("default")
Theme.setActive("default")
themeLoaded = true
end)
-- Create main demo window (without theme for now)
self.window = Gui.new({
x = 50,
y = 50,
width = 700,
height = 550,
background = Color.new(0.15, 0.15, 0.2, 0.95),
border = { top = true, bottom = true, left = true, right = true },
borderColor = Color.new(0.8, 0.8, 0.8, 1),
positioning = "flex",
flexDirection = "vertical",
gap = 20,
padding = { top = 20, right = 20, bottom = 20, left = 20 },
})
-- Title
local title = Gui.new({
parent = self.window,
height = 40,
text = "Theme System Demo",
textSize = 20,
textAlign = "center",
textColor = Color.new(1, 1, 1, 1),
background = Color.new(0.2, 0.2, 0.3, 1),
})
-- Status message
self.statusText = Gui.new({
parent = self.window,
height = 60,
text = themeLoaded and "✓ Theme loaded successfully!\nTheme system is ready to use."
or "⚠ Theme not loaded (atlas image missing)\nShowing API demonstration without actual theme rendering.",
textSize = 14,
textAlign = "center",
textColor = themeLoaded and Color.new(0.3, 0.9, 0.3, 1) or Color.new(0.9, 0.7, 0.3, 1),
background = Color.new(0.1, 0.1, 0.15, 0.8),
padding = { top = 10, right = 10, bottom = 10, left = 10 },
})
-- Info section
local infoSection = Gui.new({
parent = self.window,
height = 350,
positioning = "flex",
flexDirection = "vertical",
gap = 15,
background = Color.new(0.1, 0.1, 0.15, 0.5),
padding = { top = 15, right = 15, bottom = 15, left = 15 },
})
-- Example 1: Basic themed button
local example1 = Gui.new({
parent = infoSection,
height = 80,
positioning = "flex",
flexDirection = "vertical",
gap = 10,
background = Color.new(0.12, 0.12, 0.17, 1),
padding = { top = 10, right = 10, bottom = 10, left = 10 },
})
Gui.new({
parent = example1,
height = 20,
text = "Example 1: Basic Themed Button",
textSize = 14,
textColor = Color.new(0.8, 0.9, 1, 1),
background = Color.new(0, 0, 0, 0),
})
-- This button would use theme if loaded
local themedButton = Gui.new({
parent = example1,
width = 150,
height = 40,
text = "Themed Button",
textAlign = "center",
textColor = Color.new(1, 1, 1, 1),
background = Color.new(0.2, 0.6, 0.9, 0.8),
-- theme = "button", -- Uncomment when theme atlas exists
callback = function(element, event)
if event.type == "click" then
print("Themed button clicked!")
end
end,
})
-- Example 2: Button with states
local example2 = Gui.new({
parent = infoSection,
height = 100,
positioning = "flex",
flexDirection = "vertical",
gap = 10,
background = Color.new(0.12, 0.12, 0.17, 1),
padding = { top = 10, right = 10, bottom = 10, left = 10 },
})
Gui.new({
parent = example2,
height = 20,
text = "Example 2: Button with Hover/Pressed States",
textSize = 14,
textColor = Color.new(0.8, 0.9, 1, 1),
background = Color.new(0, 0, 0, 0),
})
Gui.new({
parent = example2,
height = 15,
text = "Hover over or click the button to see state changes (when theme is loaded)",
textSize = 11,
textColor = Color.new(0.6, 0.7, 0.8, 1),
background = Color.new(0, 0, 0, 0),
})
local stateButton = Gui.new({
parent = example2,
width = 200,
height = 40,
text = "Interactive Button",
textAlign = "center",
textColor = Color.new(1, 1, 1, 1),
background = Color.new(0.3, 0.7, 0.4, 0.8),
-- theme = "button", -- Will automatically handle hover/pressed states
callback = function(element, event)
if event.type == "click" then
print("State button clicked! State was:", element._themeState)
end
end,
})
-- Example 3: Themed panel
local example3 = Gui.new({
parent = infoSection,
height = 120,
positioning = "flex",
flexDirection = "vertical",
gap = 10,
background = Color.new(0.12, 0.12, 0.17, 1),
padding = { top = 10, right = 10, bottom = 10, left = 10 },
})
Gui.new({
parent = example3,
height = 20,
text = "Example 3: Themed Panel/Container",
textSize = 14,
textColor = Color.new(0.8, 0.9, 1, 1),
background = Color.new(0, 0, 0, 0),
})
local themedPanel = Gui.new({
parent = example3,
width = 300,
height = 80,
background = Color.new(0.25, 0.25, 0.35, 0.9),
-- theme = "panel", -- Would use panel theme component
padding = { top = 10, right = 10, bottom = 10, left = 10 },
})
Gui.new({
parent = themedPanel,
text = "This is a themed panel container.\nIt would have a 9-slice border when theme is loaded.",
textSize = 12,
textColor = Color.new(0.9, 0.9, 1, 1),
textAlign = "center",
background = Color.new(0, 0, 0, 0),
})
-- Code example section
local codeSection = Gui.new({
parent = self.window,
height = 40,
background = Color.new(0.08, 0.08, 0.12, 1),
padding = { top = 10, right = 10, bottom = 10, left = 10 },
})
Gui.new({
parent = codeSection,
text = 'Usage: element = Gui.new({ theme = "button", ... })',
textSize = 12,
textColor = Color.new(0.5, 0.9, 0.5, 1),
background = Color.new(0, 0, 0, 0),
})
return self
end
return ThemeDemo.init()

238
themes/README.md Normal file
View File

@@ -0,0 +1,238 @@
# FlexLove Theme System
## Overview
FlexLove supports a flexible 9-slice/9-patch theming system that allows you to create scalable UI components using texture atlases.
## Image Organization Options
You have **three ways** to organize your theme images:
### Option 1: Separate Images Per Component (Recommended for Beginners)
Each component gets its own image file:
```
themes/
panel.png (24x24 pixels - 9-slice for panels)
button_normal.png (24x24 pixels - 9-slice for buttons)
button_hover.png (24x24 pixels - hover state)
button_pressed.png (24x24 pixels - pressed state)
input.png (24x24 pixels - 9-slice for inputs)
```
**Theme definition:**
```lua
return {
name = "My Theme",
components = {
panel = {
atlas = "themes/panel.png",
regions = { ... }
},
button = {
atlas = "themes/button_normal.png",
regions = { ... },
states = {
hover = {
atlas = "themes/button_hover.png",
regions = { ... }
},
pressed = {
atlas = "themes/button_pressed.png",
regions = { ... }
}
}
}
}
}
```
### Option 2: Single Atlas (Recommended for Performance)
All components in one texture atlas:
```
themes/
default_atlas.png (96x48 pixels containing all components)
```
**Theme definition:**
```lua
return {
name = "My Theme",
atlas = "themes/default_atlas.png", -- Global atlas
components = {
panel = {
regions = {
topLeft = {x=0, y=0, w=8, h=8},
-- ... regions reference positions in atlas
}
},
button = {
regions = {
topLeft = {x=24, y=0, w=8, h=8}, -- Different position in same atlas
-- ...
}
}
}
}
```
### Option 3: Hybrid (Best of Both Worlds)
Mix global atlas with component-specific images:
```lua
return {
name = "My Theme",
atlas = "themes/global_atlas.png", -- Fallback atlas
components = {
panel = {
-- Uses global atlas
regions = {x=0, y=0, ...}
},
button = {
atlas = "themes/button.png", -- Override with specific image
regions = {x=0, y=0, ...}
}
}
}
```
## 9-Slice Structure
Each component image is divided into 9 regions:
```
┌─────┬──────────┬─────┐
│ TL │ TC │ TR │ (Top: fixed height)
├─────┼──────────┼─────┤
│ ML │ MC │ MR │ (Middle: stretches)
├─────┼──────────┼─────┤
│ BL │ BC │ BR │ (Bottom: fixed height)
└─────┴──────────┴─────┘
Fixed Stretch Fixed
```
- **Corners** (TL, TR, BL, BR): Fixed size, never stretched
- **Edges** (TC, BC, ML, MR): Stretched in one direction
- **Center** (MC): Stretched in both directions
## Creating Theme Images
### Minimum Image Size
For a 9-slice image, you need at least **24x24 pixels**:
- 8px for each corner
- 8px for stretchable middle section
### Image Requirements
1. **Format**: PNG with transparency
2. **Color Mode**: RGBA
3. **Border Style**: Draw borders in the corner/edge regions
4. **Center**: Can be solid color or transparent
## Example: Creating a Button Image
For a button with rounded corners and a border:
```
button_normal.png (24x24 pixels)
Pixel layout:
┌────────┬────────────┬────────┐
│ ●●●●●● │ ██████████ │ ●●●●●● │ 8px
│ ●●●●●● │ ██████████ │ ●●●●●● │
├────────┼────────────┼────────┤
│ ██████ │ ░░░░░░░░░░ │ ██████ │ 8px (stretch)
│ ██████ │ ░░░░░░░░░░ │ ██████ │
├────────┼────────────┼────────┤
│ ●●●●●● │ ██████████ │ ●●●●●● │ 8px
│ ●●●●●● │ ██████████ │ ●●●●●● │
└────────┴────────────┴────────┘
8px 8px(stretch) 8px
Legend:
● = Corner (fixed)
█ = Border edge (stretched)
░ = Fill/background (stretched both ways)
```
## Usage in Code
```lua
local FlexLove = require("FlexLove")
local Theme = FlexLove.Theme
local Gui = FlexLove.GUI
-- Load theme
Theme.load("my_theme")
Theme.setActive("my_theme")
-- Create themed button
local button = Gui.new({
width = 150,
height = 40,
text = "Click Me",
theme = "button", -- Uses button component from active theme
callback = function(element, event)
print("Clicked!")
end
})
-- Create themed panel
local panel = Gui.new({
width = 300,
height = 200,
theme = "panel"
})
```
## Component States
Buttons automatically handle three states:
- **normal**: Default appearance
- **hover**: When mouse is over the button
- **pressed**: When button is being clicked
Define state-specific images in your theme:
```lua
button = {
atlas = "themes/button_normal.png",
regions = { ... },
states = {
hover = {
atlas = "themes/button_hover.png",
regions = { ... }
},
pressed = {
atlas = "themes/button_pressed.png",
regions = { ... }
}
}
}
```
## Tips
1. **Start Simple**: Begin with one component (button) before creating a full theme
2. **Test Scaling**: Make sure your 9-slice regions stretch properly at different sizes
3. **Consistent Style**: Keep corner sizes consistent across components
4. **State Variations**: For button states, change colors/brightness rather than structure
5. **Atlas Packing**: Use tools like TexturePacker or Aseprite to create efficient atlases
## Tools for Creating Atlases
- **TexturePacker**: Professional sprite sheet tool
- **Aseprite**: Pixel art editor with export options
- **Shoebox**: Free sprite sheet packer
- **GIMP/Photoshop**: Manual layout with guides
## See Also
- `default.lua` - Example theme with single atlas
- `separate_images_example.lua` - Example with separate images per component
- `ThemeSystemDemo.lua` - Interactive demo of theme system

45
themes/space.lua Normal file
View File

@@ -0,0 +1,45 @@
return {
name = "Space Theme",
components = {
panel = {
atlas = "themes/space/panel-compressed.png",
regions = {},
},
button = {
atlas = "themes/space/interactive-compressed.png",
regions = {},
states = {
hover = {
atlas = "themes/interactive_hover-compressed.png",
regions = { ... },
},
pressed = {
atlas = "themes/interactive_pressed-compressed.png",
regions = { ... },
},
disabled = {
atlas = "themes/interactive_disabled-compressed.png",
regions = { ... },
},
},
},
input = {
atlas = "themes/space/interactive-compressed.png",
regions = {},
states = {
hover = {
atlas = "themes/interactive_hover-compressed.png",
regions = { ... },
},
pressed = {
atlas = "themes/interactive_pressed-compressed.png",
regions = { ... },
},
disabled = {
atlas = "themes/interactive_disabled-compressed.png",
regions = { ... },
},
},
},
},
}