working on better 9patch support
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
Cartographer.lua
|
||||
OverlayStats.lua
|
||||
themes/metal/
|
||||
themes/space/
|
||||
.DS_STORE
|
||||
|
||||
48
CHANGELOG.md
48
CHANGELOG.md
@@ -5,6 +5,17 @@ All notable changes to FlexLove will be documented in this file.
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Android 9-Patch Auto-Parsing**: Automatic parsing of *.9.png files
|
||||
- Automatically detects and parses Android 9-patch files based on .9.png extension
|
||||
- Extracts stretch regions and content padding from 1-pixel border markers
|
||||
- Supports multiple non-contiguous stretch regions on both axes
|
||||
- Manual insets override auto-parsing for backward compatibility
|
||||
- Console logging for transparency ("[FlexLove] Auto-parsed 9-patch: ...")
|
||||
- Added `ImageDataReader` module for pixel data extraction
|
||||
- Added `NinePatchParser` module with `parse()` function
|
||||
- Theme system automatically processes 9-patch files during theme loading
|
||||
- Space theme updated to use button.9.png with auto-parsing
|
||||
|
||||
- **Corner Radius Support**: Added `cornerRadius` property for rounded corners
|
||||
- Supports uniform radius (single number) or individual corners (table)
|
||||
- Automatically clips children to parent's rounded corners using stencil buffer
|
||||
@@ -83,6 +94,43 @@ All notable changes to FlexLove will be documented in this file.
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Using Android 9-Patch Files
|
||||
|
||||
FlexLove now automatically parses Android 9-patch (*.9.png) files. Simply rename your files to include the `.9.png` extension and remove manual insets:
|
||||
|
||||
```lua
|
||||
-- Old (manual insets)
|
||||
components = {
|
||||
button = {
|
||||
atlas = "themes/space/button.png",
|
||||
insets = { left = 14, top = 14, right = 14, bottom = 14 }
|
||||
}
|
||||
}
|
||||
|
||||
-- New (auto-parsed from .9.png file)
|
||||
components = {
|
||||
button = {
|
||||
atlas = "themes/space/button.9.png"
|
||||
-- insets automatically extracted from 9-patch borders
|
||||
}
|
||||
}
|
||||
|
||||
-- Manual override still works
|
||||
components = {
|
||||
button = {
|
||||
atlas = "themes/space/button.9.png",
|
||||
insets = { left = 20, top = 20, right = 20, bottom = 20 } -- Overrides auto-parsing
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**9-Patch Format:**
|
||||
- Top border (row 0): Horizontal stretch regions (black pixels = stretchable)
|
||||
- Left border (column 0): Vertical stretch regions (black pixels = stretchable)
|
||||
- Bottom border: Horizontal content padding (optional)
|
||||
- Right border: Vertical content padding (optional)
|
||||
- Supports multiple non-contiguous stretch regions for complex scaling patterns
|
||||
|
||||
### From `background` to `backgroundColor`
|
||||
|
||||
If you're updating existing code, replace all instances of `background` with `backgroundColor`:
|
||||
|
||||
758
FlexLove.lua
758
FlexLove.lua
@@ -1,225 +1,8 @@
|
||||
--[[
|
||||
================================================================================
|
||||
FlexLove - Flexible UI Library for LÖVE Framework
|
||||
================================================================================
|
||||
|
||||
A comprehensive UI library providing flexbox/grid layouts, theming, animations,
|
||||
and event handling for LÖVE2D games.
|
||||
|
||||
ARCHITECTURE OVERVIEW:
|
||||
---------------------
|
||||
1. Color System - RGBA color utilities with hex conversion
|
||||
2. Theme System - 9-slice theming with state support (normal/hover/pressed/disabled)
|
||||
3. Units System - Responsive units (px, %, vw, vh, ew, eh) with viewport scaling
|
||||
4. Layout System - Flexbox, Grid, Absolute, and Relative positioning
|
||||
5. Event System - Mouse/touch events with z-index ordering
|
||||
6. Animation System - Interpolation with easing functions
|
||||
7. GUI Manager - Top-level manager for elements and global state
|
||||
|
||||
API CONVENTIONS:
|
||||
---------------
|
||||
- Constructors: ClassName.new(props) -> instance
|
||||
- Static Methods: ClassName.methodName(args) -> result
|
||||
- Instance Methods: instance:methodName(args) -> result
|
||||
- Getters: instance:getPropertyName() -> value
|
||||
- Internal Fields: _fieldName (private, do not access directly)
|
||||
- Error Handling: Constructors throw errors, utility functions return nil + error string
|
||||
|
||||
NAMING PATTERNS:
|
||||
---------------
|
||||
- Classes: PascalCase (Element, Theme, Color)
|
||||
- Functions: camelCase (resolveImagePath, getViewport)
|
||||
- Properties: camelCase (backgroundColor, textColor, cornerRadius)
|
||||
- Constants: UPPER_SNAKE_CASE (TEXT_SIZE_PRESETS, FONT_CACHE_MAX_SIZE)
|
||||
- Private: _prefixedCamelCase (_pressed, _themeState, _borderBoxWidth)
|
||||
|
||||
PARAMETER ORDERING:
|
||||
------------------
|
||||
- Position: (x, y, width, height) - standard order
|
||||
- Units: (value, unit, viewportW, viewportH, parentSize) - value first
|
||||
- Drawing: (element, position, dimensions, styling, opacity) - element first
|
||||
|
||||
RETURN VALUE PATTERNS:
|
||||
---------------------
|
||||
- Single Success: return value
|
||||
- Success/Failure: return result, errorMessage (nil on success for error)
|
||||
- Multiple Values: return value1, value2 (documented in @return)
|
||||
- Constructors: Always return instance (never nil)
|
||||
|
||||
USAGE EXAMPLE:
|
||||
-------------
|
||||
```lua
|
||||
local FlexLove = require("libs.FlexLove")
|
||||
|
||||
-- Initialize with base scaling and theme
|
||||
FlexLove.Gui.init({
|
||||
baseScale = { width = 1920, height = 1080 },
|
||||
theme = "space"
|
||||
})
|
||||
|
||||
-- Create a button with flexbox layout
|
||||
local button = FlexLove.Element.new({
|
||||
width = "20vw",
|
||||
height = "10vh",
|
||||
backgroundColor = FlexLove.Color.new(0.2, 0.2, 0.8, 1),
|
||||
text = "Click Me",
|
||||
textSize = "md",
|
||||
themeComponent = "button",
|
||||
callback = function(element, event)
|
||||
print("Button clicked!")
|
||||
end
|
||||
})
|
||||
|
||||
-- In your love.update and love.draw:
|
||||
function love.update(dt)
|
||||
FlexLove.Gui.update(dt)
|
||||
end
|
||||
|
||||
function love.draw()
|
||||
FlexLove.Gui.draw()
|
||||
end
|
||||
```
|
||||
|
||||
ADDITIONAL EXAMPLES:
|
||||
-------------------
|
||||
|
||||
1. Creating Colors:
|
||||
```lua
|
||||
-- From RGB values (0-1 range)
|
||||
local red = FlexLove.Color.new(1, 0, 0, 1)
|
||||
|
||||
-- From hex string
|
||||
local blue = FlexLove.Color.fromHex("#0000FF")
|
||||
local semiTransparent = FlexLove.Color.fromHex("#FF000080")
|
||||
```
|
||||
|
||||
2. Responsive Units:
|
||||
```lua
|
||||
-- Viewport-relative units
|
||||
local container = FlexLove.Element.new({
|
||||
width = "50vw", -- 50% of viewport width
|
||||
height = "30vh", -- 30% of viewport height
|
||||
padding = { horizontal = "2vw", vertical = "1vh" }
|
||||
})
|
||||
|
||||
-- Percentage units (relative to parent)
|
||||
local child = FlexLove.Element.new({
|
||||
parent = container,
|
||||
width = "80%", -- 80% of parent width
|
||||
height = "50%" -- 50% of parent height
|
||||
})
|
||||
```
|
||||
|
||||
3. Flexbox Layout:
|
||||
```lua
|
||||
-- Horizontal flex container
|
||||
local row = FlexLove.Element.new({
|
||||
positioning = FlexLove.Positioning.FLEX,
|
||||
flexDirection = FlexLove.FlexDirection.HORIZONTAL,
|
||||
justifyContent = FlexLove.JustifyContent.SPACE_BETWEEN,
|
||||
alignItems = FlexLove.AlignItems.CENTER,
|
||||
gap = 10,
|
||||
width = "80vw",
|
||||
height = "10vh"
|
||||
})
|
||||
|
||||
-- Add children
|
||||
for i = 1, 3 do
|
||||
FlexLove.Element.new({
|
||||
parent = row,
|
||||
width = "20vw",
|
||||
height = "8vh",
|
||||
text = "Item " .. i
|
||||
})
|
||||
end
|
||||
```
|
||||
|
||||
4. Grid Layout:
|
||||
```lua
|
||||
-- 3x3 grid
|
||||
local grid = FlexLove.Element.new({
|
||||
positioning = FlexLove.Positioning.GRID,
|
||||
gridRows = 3,
|
||||
gridColumns = 3,
|
||||
columnGap = 10,
|
||||
rowGap = 10,
|
||||
width = "60vw",
|
||||
height = "60vh"
|
||||
})
|
||||
|
||||
-- Add 9 children (auto-placed in grid)
|
||||
for i = 1, 9 do
|
||||
FlexLove.Element.new({
|
||||
parent = grid,
|
||||
text = "Cell " .. i
|
||||
})
|
||||
end
|
||||
```
|
||||
|
||||
5. Theming:
|
||||
```lua
|
||||
-- Load and activate a theme
|
||||
FlexLove.Theme.load("space")
|
||||
FlexLove.Theme.setActive("space")
|
||||
|
||||
-- Use theme component
|
||||
local button = FlexLove.Element.new({
|
||||
themeComponent = "button",
|
||||
text = "Themed Button",
|
||||
callback = function(element, event)
|
||||
print("Clicked!")
|
||||
end
|
||||
})
|
||||
|
||||
-- Access theme resources
|
||||
local primaryColor = FlexLove.Theme.getColor("primary")
|
||||
local headingFont = FlexLove.Theme.getFont("heading")
|
||||
```
|
||||
|
||||
6. Animations:
|
||||
```lua
|
||||
-- Fade animation
|
||||
local fadeIn = FlexLove.Animation.fade(1.0, 0, 1)
|
||||
fadeIn:apply(element)
|
||||
|
||||
-- Scale animation
|
||||
local scaleUp = FlexLove.Animation.scale(0.5,
|
||||
{ width = 100, height = 50 },
|
||||
{ width = 200, height = 100 }
|
||||
)
|
||||
scaleUp:apply(element)
|
||||
|
||||
-- Custom animation with easing
|
||||
local customAnim = FlexLove.Animation.new({
|
||||
duration = 1.0,
|
||||
start = { opacity = 0, width = 100 },
|
||||
final = { opacity = 1, width = 200 },
|
||||
easing = "easeInOutCubic"
|
||||
})
|
||||
customAnim:apply(element)
|
||||
```
|
||||
|
||||
7. Event Handling:
|
||||
```lua
|
||||
local button = FlexLove.Element.new({
|
||||
text = "Interactive",
|
||||
callback = function(element, event)
|
||||
if event.type == "click" then
|
||||
print("Clicked with button:", event.button)
|
||||
print("Position:", event.x, event.y)
|
||||
print("Modifiers:", event.modifiers.shift, event.modifiers.ctrl)
|
||||
elseif event.type == "press" then
|
||||
print("Button pressed")
|
||||
elseif event.type == "release" then
|
||||
print("Button released")
|
||||
end
|
||||
end
|
||||
})
|
||||
```
|
||||
|
||||
VERSION: 1.0.0
|
||||
LICENSE: MIT
|
||||
================================================================================
|
||||
For full documentation, see README.md
|
||||
]]
|
||||
|
||||
-- ====================
|
||||
@@ -314,6 +97,295 @@ function Color.fromHex(hexWithTag)
|
||||
end
|
||||
end
|
||||
|
||||
-- ====================
|
||||
-- ImageDataReader
|
||||
-- ====================
|
||||
|
||||
local ImageDataReader = {}
|
||||
|
||||
--- Load ImageData from a file path
|
||||
---@param imagePath string
|
||||
---@return love.ImageData
|
||||
function ImageDataReader.loadImageData(imagePath)
|
||||
if not imagePath then
|
||||
error(formatError("ImageDataReader", "Image path cannot be nil"))
|
||||
end
|
||||
|
||||
local success, result = pcall(function()
|
||||
return love.image.newImageData(imagePath)
|
||||
end)
|
||||
|
||||
if not success then
|
||||
error(formatError("ImageDataReader", "Failed to load image data from '" .. imagePath .. "': " .. tostring(result)))
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--- Extract all pixels from a specific row
|
||||
---@param imageData love.ImageData
|
||||
---@param rowIndex number -- 0-based row index
|
||||
---@return table -- Array of {r, g, b, a} values (0-255 range)
|
||||
function ImageDataReader.getRow(imageData, rowIndex)
|
||||
if not imageData then
|
||||
error(formatError("ImageDataReader", "ImageData cannot be nil"))
|
||||
end
|
||||
|
||||
local width = imageData:getWidth()
|
||||
local height = imageData:getHeight()
|
||||
|
||||
if rowIndex < 0 or rowIndex >= height then
|
||||
error(formatError("ImageDataReader", string.format("Row index %d out of bounds (height: %d)", rowIndex, height)))
|
||||
end
|
||||
|
||||
local pixels = {}
|
||||
for x = 0, width - 1 do
|
||||
local r, g, b, a = imageData:getPixel(x, rowIndex)
|
||||
table.insert(pixels, {
|
||||
r = math.floor(r * 255 + 0.5),
|
||||
g = math.floor(g * 255 + 0.5),
|
||||
b = math.floor(b * 255 + 0.5),
|
||||
a = math.floor(a * 255 + 0.5),
|
||||
})
|
||||
end
|
||||
|
||||
return pixels
|
||||
end
|
||||
|
||||
--- Extract all pixels from a specific column
|
||||
---@param imageData love.ImageData
|
||||
---@param colIndex number -- 0-based column index
|
||||
---@return table -- Array of {r, g, b, a} values (0-255 range)
|
||||
function ImageDataReader.getColumn(imageData, colIndex)
|
||||
if not imageData then
|
||||
error(formatError("ImageDataReader", "ImageData cannot be nil"))
|
||||
end
|
||||
|
||||
local width = imageData:getWidth()
|
||||
local height = imageData:getHeight()
|
||||
|
||||
if colIndex < 0 or colIndex >= width then
|
||||
error(formatError("ImageDataReader", string.format("Column index %d out of bounds (width: %d)", colIndex, width)))
|
||||
end
|
||||
|
||||
local pixels = {}
|
||||
for y = 0, height - 1 do
|
||||
local r, g, b, a = imageData:getPixel(colIndex, y)
|
||||
table.insert(pixels, {
|
||||
r = math.floor(r * 255 + 0.5),
|
||||
g = math.floor(g * 255 + 0.5),
|
||||
b = math.floor(b * 255 + 0.5),
|
||||
a = math.floor(a * 255 + 0.5),
|
||||
})
|
||||
end
|
||||
|
||||
return pixels
|
||||
end
|
||||
|
||||
--- Check if a pixel is black with full alpha (9-patch marker)
|
||||
---@param r number -- Red (0-255)
|
||||
---@param g number -- Green (0-255)
|
||||
---@param b number -- Blue (0-255)
|
||||
---@param a number -- Alpha (0-255)
|
||||
---@return boolean
|
||||
function ImageDataReader.isBlackPixel(r, g, b, a)
|
||||
return r == 0 and g == 0 and b == 0 and a == 255
|
||||
end
|
||||
|
||||
-- ====================
|
||||
-- NinePatchParser
|
||||
-- ====================
|
||||
|
||||
local NinePatchParser = {}
|
||||
|
||||
--- Find all continuous runs of black pixels in a pixel array
|
||||
---@param pixels table -- Array of {r, g, b, a} pixel values
|
||||
---@return table -- Array of {start, end} pairs (1-based indices, inclusive)
|
||||
local function findBlackPixelRuns(pixels)
|
||||
local runs = {}
|
||||
local inRun = false
|
||||
local runStart = nil
|
||||
|
||||
for i = 1, #pixels do
|
||||
local pixel = pixels[i]
|
||||
local isBlack = ImageDataReader.isBlackPixel(pixel.r, pixel.g, pixel.b, pixel.a)
|
||||
|
||||
if isBlack and not inRun then
|
||||
-- Start of a new run
|
||||
inRun = true
|
||||
runStart = i
|
||||
elseif not isBlack and inRun then
|
||||
-- End of current run
|
||||
table.insert(runs, { start = runStart, ["end"] = i - 1 })
|
||||
inRun = false
|
||||
runStart = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle case where run extends to end of array
|
||||
if inRun then
|
||||
table.insert(runs, { start = runStart, ["end"] = #pixels })
|
||||
end
|
||||
|
||||
return runs
|
||||
end
|
||||
|
||||
--- Parse a 9-patch PNG image to extract stretch regions and content padding
|
||||
---@param imagePath string -- Path to the 9-patch image file
|
||||
---@return table|nil, string|nil -- Returns {insets, stretchX, stretchY} or nil, error message
|
||||
function NinePatchParser.parse(imagePath)
|
||||
if not imagePath then
|
||||
return nil, "Image path cannot be nil"
|
||||
end
|
||||
|
||||
local success, imageData = pcall(function()
|
||||
return ImageDataReader.loadImageData(imagePath)
|
||||
end)
|
||||
|
||||
if not success then
|
||||
return nil, "Failed to load image data: " .. tostring(imageData)
|
||||
end
|
||||
|
||||
local width = imageData:getWidth()
|
||||
local height = imageData:getHeight()
|
||||
|
||||
-- Validate minimum size (must be at least 3x3 with 1px border)
|
||||
if width < 3 or height < 3 then
|
||||
return nil, string.format("Invalid 9-patch dimensions: %dx%d (minimum 3x3)", width, height)
|
||||
end
|
||||
|
||||
-- Extract border pixels (0-based indexing, but we convert to 1-based for processing)
|
||||
local topBorder = ImageDataReader.getRow(imageData, 0)
|
||||
local leftBorder = ImageDataReader.getColumn(imageData, 0)
|
||||
local bottomBorder = ImageDataReader.getRow(imageData, height - 1)
|
||||
local rightBorder = ImageDataReader.getColumn(imageData, width - 1)
|
||||
|
||||
-- Remove corner pixels from borders (they're not part of the stretch/content markers)
|
||||
-- Top and bottom borders: remove first and last pixel
|
||||
local topStretchPixels = {}
|
||||
local bottomContentPixels = {}
|
||||
for i = 2, #topBorder - 1 do
|
||||
table.insert(topStretchPixels, topBorder[i])
|
||||
end
|
||||
for i = 2, #bottomBorder - 1 do
|
||||
table.insert(bottomContentPixels, bottomBorder[i])
|
||||
end
|
||||
|
||||
-- Left and right borders: remove first and last pixel
|
||||
local leftStretchPixels = {}
|
||||
local rightContentPixels = {}
|
||||
for i = 2, #leftBorder - 1 do
|
||||
table.insert(leftStretchPixels, leftBorder[i])
|
||||
end
|
||||
for i = 2, #rightBorder - 1 do
|
||||
table.insert(rightContentPixels, rightBorder[i])
|
||||
end
|
||||
|
||||
-- Find stretch regions (top and left borders)
|
||||
local stretchX = findBlackPixelRuns(topStretchPixels)
|
||||
local stretchY = findBlackPixelRuns(leftStretchPixels)
|
||||
|
||||
-- Find content padding regions (bottom and right borders)
|
||||
local contentX = findBlackPixelRuns(bottomContentPixels)
|
||||
local contentY = findBlackPixelRuns(rightContentPixels)
|
||||
|
||||
-- Validate that we have at least one stretch region
|
||||
if #stretchX == 0 or #stretchY == 0 then
|
||||
return nil, "No stretch regions found (top or left border has no black pixels)"
|
||||
end
|
||||
|
||||
-- Calculate insets from stretch regions
|
||||
-- Use the first stretch region's start and last stretch region's end
|
||||
local firstStretchX = stretchX[1]
|
||||
local lastStretchX = stretchX[#stretchX]
|
||||
local firstStretchY = stretchY[1]
|
||||
local lastStretchY = stretchY[#stretchY]
|
||||
|
||||
-- If content padding is defined, use it; otherwise use stretch regions
|
||||
local contentLeft, contentRight, contentTop, contentBottom
|
||||
|
||||
if #contentX > 0 then
|
||||
contentLeft = contentX[1].start
|
||||
contentRight = #topStretchPixels - contentX[#contentX]["end"]
|
||||
else
|
||||
contentLeft = firstStretchX.start
|
||||
contentRight = #topStretchPixels - lastStretchX["end"]
|
||||
end
|
||||
|
||||
if #contentY > 0 then
|
||||
contentTop = contentY[1].start
|
||||
contentBottom = #leftStretchPixels - contentY[#contentY]["end"]
|
||||
else
|
||||
contentTop = firstStretchY.start
|
||||
contentBottom = #leftStretchPixels - lastStretchY["end"]
|
||||
end
|
||||
|
||||
return {
|
||||
insets = {
|
||||
left = contentLeft,
|
||||
top = contentTop,
|
||||
right = contentRight,
|
||||
bottom = contentBottom,
|
||||
},
|
||||
stretchX = stretchX,
|
||||
stretchY = stretchY,
|
||||
}
|
||||
end
|
||||
|
||||
-- ====================
|
||||
-- ImageScaler
|
||||
-- ====================
|
||||
|
||||
local ImageScaler = {}
|
||||
|
||||
--- Scale an ImageData region using nearest-neighbor sampling
|
||||
--- Produces sharp, pixelated scaling - ideal for pixel art
|
||||
---@param sourceImageData love.ImageData -- Source image data
|
||||
---@param srcX number -- Source region X (0-based)
|
||||
---@param srcY number -- Source region Y (0-based)
|
||||
---@param srcW number -- Source region width
|
||||
---@param srcH number -- Source region height
|
||||
---@param destW number -- Destination width
|
||||
---@param destH number -- Destination height
|
||||
---@return love.ImageData -- Scaled image data
|
||||
function ImageScaler.scaleNearest(sourceImageData, srcX, srcY, srcW, srcH, destW, destH)
|
||||
if not sourceImageData then
|
||||
error(formatError("ImageScaler", "Source ImageData cannot be nil"))
|
||||
end
|
||||
|
||||
if srcW <= 0 or srcH <= 0 or destW <= 0 or destH <= 0 then
|
||||
error(formatError("ImageScaler", "Dimensions must be positive"))
|
||||
end
|
||||
|
||||
-- Create destination ImageData
|
||||
local destImageData = love.image.newImageData(destW, destH)
|
||||
|
||||
-- Calculate scale ratios (cached outside loops for performance)
|
||||
local scaleX = srcW / destW
|
||||
local scaleY = srcH / destH
|
||||
|
||||
-- Nearest-neighbor sampling
|
||||
for destY = 0, destH - 1 do
|
||||
for destX = 0, destW - 1 do
|
||||
-- Calculate source pixel coordinates using floor (nearest-neighbor)
|
||||
local srcPixelX = math.floor(destX * scaleX) + srcX
|
||||
local srcPixelY = math.floor(destY * scaleY) + srcY
|
||||
|
||||
-- Clamp to source bounds (safety check)
|
||||
srcPixelX = math.min(srcPixelX, srcX + srcW - 1)
|
||||
srcPixelY = math.min(srcPixelY, srcY + srcH - 1)
|
||||
|
||||
-- Sample source pixel
|
||||
local r, g, b, a = sourceImageData:getPixel(srcPixelX, srcPixelY)
|
||||
|
||||
-- Write to destination
|
||||
destImageData:setPixel(destX, destY, r, g, b, a)
|
||||
end
|
||||
end
|
||||
|
||||
return destImageData
|
||||
end
|
||||
|
||||
-- ====================
|
||||
-- Theme System
|
||||
-- ====================
|
||||
@@ -325,12 +397,17 @@ end
|
||||
---@field h number -- Height in atlas
|
||||
|
||||
---@class ThemeComponent
|
||||
---@field atlas string|love.Image? -- Optional: component-specific atlas (overrides theme atlas)
|
||||
---@field atlas string|love.Image? -- Optional: component-specific atlas (overrides theme atlas). Files ending in .9.png are auto-parsed
|
||||
---@field insets {left:number, top:number, right:number, bottom:number}? -- Optional: 9-patch insets (auto-extracted from .9.png files or manually defined)
|
||||
---@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 contentAutoSizingMultiplier {width:number?, height:number?}? -- Optional: multiplier for auto-sized content dimensions
|
||||
---@field scaleCorners boolean? -- Optional: scale non-stretched regions (corners/edges) with window size. Default: false
|
||||
---@field scalingAlgorithm "nearest"|"bilinear"? -- Optional: scaling algorithm for non-stretched regions. Default: "bilinear"
|
||||
---@field _loadedAtlas love.Image? -- Internal: cached loaded atlas image
|
||||
---@field _ninePatchData {insets:table, stretchX:table, stretchY:table}? -- Internal: parsed 9-patch data with multiple stretch regions
|
||||
---@field _scaledRegionCache table<string, love.Image>? -- Internal: cache for scaled corner/edge images
|
||||
|
||||
---@class FontFamily
|
||||
---@field path string -- Path to the font file (relative to FlexLove or absolute)
|
||||
@@ -424,9 +501,6 @@ local function safeLoadImage(imagePath)
|
||||
end
|
||||
end
|
||||
|
||||
--- Create a new theme instance
|
||||
---@param definition ThemeDefinition
|
||||
---@return Theme
|
||||
--- Validate theme definition structure
|
||||
---@param definition ThemeDefinition
|
||||
---@return boolean, string? -- Returns true if valid, or false with error message
|
||||
@@ -488,11 +562,28 @@ function Theme.new(definition)
|
||||
self.fonts = definition.fonts or {}
|
||||
self.contentAutoSizingMultiplier = definition.contentAutoSizingMultiplier or nil
|
||||
|
||||
-- Load component-specific atlases
|
||||
-- Load component-specific atlases and process 9-patch definitions
|
||||
for componentName, component in pairs(self.components) do
|
||||
if component.atlas then
|
||||
if type(component.atlas) == "string" then
|
||||
local resolvedPath = resolveImagePath(component.atlas)
|
||||
|
||||
-- Check if this is a 9-patch file that needs parsing
|
||||
local is9Patch = not component.insets and component.atlas:match("%.9%.png$")
|
||||
|
||||
-- Parse 9-patch BEFORE loading the image
|
||||
if is9Patch then
|
||||
local parseResult, parseErr = NinePatchParser.parse(resolvedPath)
|
||||
if parseResult then
|
||||
component.insets = parseResult.insets
|
||||
component._ninePatchData = parseResult -- Store full data including stretch regions
|
||||
print("[FlexLove] Auto-parsed 9-patch: " .. component.atlas)
|
||||
else
|
||||
print("[FlexLove] Warning: Failed to parse 9-patch '" .. component.atlas .. "': " .. tostring(parseErr))
|
||||
end
|
||||
end
|
||||
|
||||
-- Now load the image normally
|
||||
local image, err = safeLoadImage(resolvedPath)
|
||||
if image then
|
||||
component._loadedAtlas = image
|
||||
@@ -504,17 +595,95 @@ function Theme.new(definition)
|
||||
end
|
||||
end
|
||||
|
||||
-- Also load atlases for component states
|
||||
-- Process 9-patch insets into regions (new format)
|
||||
if component.insets then
|
||||
local atlasImage = component._loadedAtlas or self.atlas
|
||||
if atlasImage then
|
||||
local imgWidth, imgHeight = atlasImage:getDimensions()
|
||||
local left = component.insets.left or 0
|
||||
local top = component.insets.top or 0
|
||||
local right = component.insets.right or 0
|
||||
local bottom = component.insets.bottom or 0
|
||||
|
||||
-- Calculate center dimensions
|
||||
local centerWidth = imgWidth - left - right
|
||||
local centerHeight = imgHeight - top - bottom
|
||||
|
||||
-- Generate regions from insets
|
||||
component.regions = {
|
||||
topLeft = { x = 0, y = 0, w = left, h = top },
|
||||
topCenter = { x = left, y = 0, w = centerWidth, h = top },
|
||||
topRight = { x = left + centerWidth, y = 0, w = right, h = top },
|
||||
middleLeft = { x = 0, y = top, w = left, h = centerHeight },
|
||||
middleCenter = { x = left, y = top, w = centerWidth, h = centerHeight },
|
||||
middleRight = { x = left + centerWidth, y = top, w = right, h = centerHeight },
|
||||
bottomLeft = { x = 0, y = top + centerHeight, w = left, h = bottom },
|
||||
bottomCenter = { x = left, y = top + centerHeight, w = centerWidth, h = bottom },
|
||||
bottomRight = { x = left + centerWidth, y = top + centerHeight, w = right, h = bottom },
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- Also load atlases for component states and process their 9-patch definitions
|
||||
if component.states then
|
||||
for stateName, stateComponent in pairs(component.states) do
|
||||
if stateComponent.atlas then
|
||||
if type(stateComponent.atlas) == "string" then
|
||||
local resolvedPath = resolveImagePath(stateComponent.atlas)
|
||||
stateComponent._loadedAtlas = love.graphics.newImage(resolvedPath)
|
||||
|
||||
-- Check if this is a 9-patch file that needs parsing
|
||||
local is9Patch = not stateComponent.insets and stateComponent.atlas:match("%.9%.png$")
|
||||
|
||||
-- Parse 9-patch BEFORE loading the image
|
||||
if is9Patch then
|
||||
local parseResult, parseErr = NinePatchParser.parse(resolvedPath)
|
||||
if parseResult then
|
||||
stateComponent.insets = parseResult.insets
|
||||
stateComponent._ninePatchData = parseResult
|
||||
print("[FlexLove] Auto-parsed 9-patch state '" .. stateName .. "': " .. stateComponent.atlas)
|
||||
else
|
||||
print("[FlexLove] Warning: Failed to parse 9-patch state '" .. stateName .. "': " .. tostring(parseErr))
|
||||
end
|
||||
end
|
||||
|
||||
-- Now load the image normally
|
||||
local image, imgErr = safeLoadImage(resolvedPath)
|
||||
if image then
|
||||
stateComponent._loadedAtlas = image
|
||||
else
|
||||
print("[FlexLove] Warning: Failed to load state atlas '" .. stateName .. "': " .. tostring(imgErr))
|
||||
end
|
||||
else
|
||||
stateComponent._loadedAtlas = stateComponent.atlas
|
||||
end
|
||||
end
|
||||
|
||||
-- Process 9-patch insets for state components
|
||||
if stateComponent.insets then
|
||||
local atlasImage = stateComponent._loadedAtlas or component._loadedAtlas or self.atlas
|
||||
if atlasImage then
|
||||
local imgWidth, imgHeight = atlasImage:getDimensions()
|
||||
local left = stateComponent.insets.left or 0
|
||||
local top = stateComponent.insets.top or 0
|
||||
local right = stateComponent.insets.right or 0
|
||||
local bottom = stateComponent.insets.bottom or 0
|
||||
|
||||
local centerWidth = imgWidth - left - right
|
||||
local centerHeight = imgHeight - top - bottom
|
||||
|
||||
stateComponent.regions = {
|
||||
topLeft = { x = 0, y = 0, w = left, h = top },
|
||||
topCenter = { x = left, y = 0, w = centerWidth, h = top },
|
||||
topRight = { x = left + centerWidth, y = 0, w = right, h = top },
|
||||
middleLeft = { x = 0, y = top, w = left, h = centerHeight },
|
||||
middleCenter = { x = left, y = top, w = centerWidth, h = centerHeight },
|
||||
middleRight = { x = left + centerWidth, y = top, w = right, h = centerHeight },
|
||||
bottomLeft = { x = 0, y = top + centerHeight, w = left, h = bottom },
|
||||
bottomCenter = { x = left, y = top + centerHeight, w = centerWidth, h = bottom },
|
||||
bottomRight = { x = left + centerWidth, y = top + centerHeight, w = right, h = bottom },
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -781,16 +950,16 @@ end
|
||||
|
||||
local NineSlice = {}
|
||||
|
||||
--- Draw a 9-slice component with borders in padding area
|
||||
--- Draw a 9-patch component using Android-style rendering
|
||||
--- Corners are never scaled (1:1 pixels), edges stretch in one dimension only
|
||||
---@param component ThemeComponent
|
||||
---@param atlas love.Image
|
||||
---@param x number -- X position of border box (top-left corner)
|
||||
---@param y number -- Y position of border box (top-left corner)
|
||||
---@param contentWidth number -- Width of content area (excludes padding)
|
||||
---@param contentHeight number -- Height of content area (excludes padding)
|
||||
---@param padding {top:number, right:number, bottom:number, left:number} -- Padding defines border thickness
|
||||
---@param x number -- X position (top-left corner)
|
||||
---@param y number -- Y position (top-left corner)
|
||||
---@param width number -- Total width (border-box)
|
||||
---@param height number -- Total height (border-box)
|
||||
---@param opacity number?
|
||||
function NineSlice.draw(component, atlas, x, y, contentWidth, contentHeight, padding, opacity)
|
||||
function NineSlice.draw(component, atlas, x, y, width, height, opacity)
|
||||
if not component or not atlas then
|
||||
return
|
||||
end
|
||||
@@ -800,102 +969,54 @@ function NineSlice.draw(component, atlas, x, y, contentWidth, contentHeight, pad
|
||||
|
||||
local regions = component.regions
|
||||
|
||||
-- Calculate source image border dimensions from regions
|
||||
local sourceBorderLeft = regions.topLeft.w
|
||||
local sourceBorderRight = regions.topRight.w
|
||||
local sourceBorderTop = regions.topLeft.h
|
||||
local sourceBorderBottom = regions.bottomLeft.h
|
||||
local sourceCenterWidth = regions.middleCenter.w
|
||||
local sourceCenterHeight = regions.middleCenter.h
|
||||
-- Extract border dimensions from regions (in pixels)
|
||||
local left = regions.topLeft.w
|
||||
local right = regions.topRight.w
|
||||
local top = regions.topLeft.h
|
||||
local bottom = regions.bottomLeft.h
|
||||
local centerW = regions.middleCenter.w
|
||||
local centerH = regions.middleCenter.h
|
||||
|
||||
-- Calculate scale factors to fit borders within padding
|
||||
-- Borders scale to fit the padding dimensions
|
||||
local scaleLeft = padding.left / sourceBorderLeft
|
||||
local scaleRight = padding.right / sourceBorderRight
|
||||
local scaleTop = padding.top / sourceBorderTop
|
||||
local scaleBottom = padding.bottom / sourceBorderBottom
|
||||
-- Calculate content area (space remaining after borders)
|
||||
local contentWidth = width - left - right
|
||||
local contentHeight = height - top - bottom
|
||||
|
||||
-- Clamp to prevent negative dimensions
|
||||
contentWidth = math.max(0, contentWidth)
|
||||
contentHeight = math.max(0, contentHeight)
|
||||
|
||||
-- Calculate stretch scales for edges and center
|
||||
local scaleX = contentWidth / centerW
|
||||
local scaleY = contentHeight / centerH
|
||||
|
||||
-- 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 (scales to fit top-left padding)
|
||||
love.graphics.draw(atlas, makeQuad(regions.topLeft), x, y, 0, scaleLeft, scaleTop)
|
||||
-- CORNERS (no scaling - 1:1 pixel perfect)
|
||||
love.graphics.draw(atlas, makeQuad(regions.topLeft), x, y)
|
||||
love.graphics.draw(atlas, makeQuad(regions.topRight), x + left + contentWidth, y)
|
||||
love.graphics.draw(atlas, makeQuad(regions.bottomLeft), x, y + top + contentHeight)
|
||||
love.graphics.draw(atlas, makeQuad(regions.bottomRight), x + left + contentWidth, y + top + contentHeight)
|
||||
|
||||
-- Top-right corner (scales to fit top-right padding)
|
||||
love.graphics.draw(atlas, makeQuad(regions.topRight), x + padding.left + contentWidth, y, 0, scaleRight, scaleTop)
|
||||
|
||||
-- Bottom-left corner (scales to fit bottom-left padding)
|
||||
love.graphics.draw(atlas, makeQuad(regions.bottomLeft), x, y + padding.top + contentHeight, 0, scaleLeft, scaleBottom)
|
||||
|
||||
-- Bottom-right corner (scales to fit bottom-right padding)
|
||||
love.graphics.draw(
|
||||
atlas,
|
||||
makeQuad(regions.bottomRight),
|
||||
x + padding.left + contentWidth,
|
||||
y + padding.top + contentHeight,
|
||||
0,
|
||||
scaleRight,
|
||||
scaleBottom
|
||||
)
|
||||
|
||||
-- Top edge (stretched to content width, scaled to padding.top height)
|
||||
-- TOP/BOTTOM EDGES (stretch horizontally only)
|
||||
if contentWidth > 0 then
|
||||
local stretchScaleX = contentWidth / sourceCenterWidth
|
||||
love.graphics.draw(atlas, makeQuad(regions.topCenter), x + padding.left, y, 0, stretchScaleX, scaleTop)
|
||||
love.graphics.draw(atlas, makeQuad(regions.topCenter), x + left, y, 0, scaleX, 1)
|
||||
love.graphics.draw(atlas, makeQuad(regions.bottomCenter), x + left, y + top + contentHeight, 0, scaleX, 1)
|
||||
end
|
||||
|
||||
-- Bottom edge (stretched to content width, scaled to padding.bottom height)
|
||||
if contentWidth > 0 then
|
||||
local stretchScaleX = contentWidth / sourceCenterWidth
|
||||
love.graphics.draw(
|
||||
atlas,
|
||||
makeQuad(regions.bottomCenter),
|
||||
x + padding.left,
|
||||
y + padding.top + contentHeight,
|
||||
0,
|
||||
stretchScaleX,
|
||||
scaleBottom
|
||||
)
|
||||
end
|
||||
|
||||
-- Left edge (scaled to padding.left width, stretched to content height)
|
||||
-- LEFT/RIGHT EDGES (stretch vertically only)
|
||||
if contentHeight > 0 then
|
||||
local stretchScaleY = contentHeight / sourceCenterHeight
|
||||
love.graphics.draw(atlas, makeQuad(regions.middleLeft), x, y + padding.top, 0, scaleLeft, stretchScaleY)
|
||||
love.graphics.draw(atlas, makeQuad(regions.middleLeft), x, y + top, 0, 1, scaleY)
|
||||
love.graphics.draw(atlas, makeQuad(regions.middleRight), x + left + contentWidth, y + top, 0, 1, scaleY)
|
||||
end
|
||||
|
||||
-- Right edge (scaled to padding.right width, stretched to content height)
|
||||
if contentHeight > 0 then
|
||||
local stretchScaleY = contentHeight / sourceCenterHeight
|
||||
love.graphics.draw(
|
||||
atlas,
|
||||
makeQuad(regions.middleRight),
|
||||
x + padding.left + contentWidth,
|
||||
y + padding.top,
|
||||
0,
|
||||
scaleRight,
|
||||
stretchScaleY
|
||||
)
|
||||
end
|
||||
|
||||
-- Center (stretched to fill content area)
|
||||
-- CENTER (stretch both dimensions)
|
||||
if contentWidth > 0 and contentHeight > 0 then
|
||||
local stretchScaleX = contentWidth / sourceCenterWidth
|
||||
local stretchScaleY = contentHeight / sourceCenterHeight
|
||||
love.graphics.draw(
|
||||
atlas,
|
||||
makeQuad(regions.middleCenter),
|
||||
x + padding.left,
|
||||
y + padding.top,
|
||||
0,
|
||||
stretchScaleX,
|
||||
stretchScaleY
|
||||
)
|
||||
love.graphics.draw(atlas, makeQuad(regions.middleCenter), x + left, y + top, 0, scaleX, scaleY)
|
||||
end
|
||||
|
||||
-- Reset color
|
||||
@@ -3308,8 +3429,10 @@ function Element:draw()
|
||||
and component.regions.bottomCenter
|
||||
and component.regions.bottomRight
|
||||
if hasAllRegions then
|
||||
-- NineSlice.draw expects content dimensions (without padding), not border-box
|
||||
NineSlice.draw(component, atlasToUse, self.x, self.y, self.width, self.height, self.padding, self.opacity)
|
||||
-- Calculate border-box dimensions (content + padding)
|
||||
local borderBoxWidth = self.width + self.padding.left + self.padding.right
|
||||
local borderBoxHeight = self.height + self.padding.top + self.padding.bottom
|
||||
NineSlice.draw(component, atlasToUse, self.x, self.y, borderBoxWidth, borderBoxHeight, self.opacity)
|
||||
else
|
||||
-- Silently skip drawing if component structure is invalid
|
||||
end
|
||||
@@ -3776,7 +3899,9 @@ function Element:recalculateUnits(newViewportWidth, newViewportHeight)
|
||||
-- - If element has a parent: use parent's border-box dimensions (CSS spec for child elements)
|
||||
-- - If element has no parent: use element's own border-box dimensions (CSS spec for root elements)
|
||||
local parentBorderBoxWidth = self.parent and self.parent._borderBoxWidth or self._borderBoxWidth or newViewportWidth
|
||||
local parentBorderBoxHeight = self.parent and self.parent._borderBoxHeight or self._borderBoxHeight or newViewportHeight
|
||||
local parentBorderBoxHeight = self.parent and self.parent._borderBoxHeight
|
||||
or self._borderBoxHeight
|
||||
or newViewportHeight
|
||||
|
||||
-- Handle shorthand properties first (horizontal/vertical)
|
||||
local resolvedHorizontalPadding = nil
|
||||
@@ -4135,6 +4260,8 @@ Gui.new = Element.new
|
||||
Gui.Element = Element
|
||||
Gui.Animation = Animation
|
||||
Gui.Theme = Theme
|
||||
Gui.ImageDataReader = ImageDataReader
|
||||
Gui.NinePatchParser = NinePatchParser
|
||||
|
||||
-- Export individual enums for convenience
|
||||
return {
|
||||
@@ -4144,6 +4271,9 @@ return {
|
||||
Color = Color,
|
||||
Theme = Theme,
|
||||
Animation = Animation,
|
||||
ImageScaler = ImageScaler,
|
||||
ImageDataReader = ImageDataReader,
|
||||
NinePatchParser = NinePatchParser,
|
||||
enums = enums,
|
||||
-- Export individual enums at top level
|
||||
Positioning = Positioning,
|
||||
|
||||
154
README.md
154
README.md
@@ -1,9 +1,20 @@
|
||||
# FlexLöve
|
||||
|
||||
A Löve GUI library based on Flexbox with theming and animation support
|
||||
**A comprehensive UI library providing flexbox/grid layouts, theming, animations, and event handling for LÖVE2D games.**
|
||||
|
||||
FlexLöve is a lightweight, flexible GUI library for Löve2D that implements a flexbox-based layout system. It provides a simple way to create and manage UI elements with automatic layout calculations, animations, theming, and responsive design.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
1. **Color System** - RGBA color utilities with hex conversion
|
||||
2. **Theme System** - 9-slice theming with state support (normal/hover/pressed/disabled)
|
||||
- Automatic Android 9-patch (*.9.png) parsing with multi-region support
|
||||
3. **Units System** - Responsive units (px, %, vw, vh, ew, eh) with viewport scaling
|
||||
4. **Layout System** - Flexbox, Grid, Absolute, and Relative positioning
|
||||
5. **Event System** - Mouse/touch events with z-index ordering
|
||||
6. **Animation System** - Interpolation with easing functions
|
||||
7. **GUI Manager** - Top-level manager for elements and global state
|
||||
|
||||
## ⚠️ Development Status
|
||||
|
||||
This library is under active development. While many features are functional, some aspects may change or have incomplete/broken implementations.
|
||||
@@ -15,6 +26,7 @@ This library is under active development. While many features are functional, so
|
||||
- **Element Management**: Hierarchical element structures with automatic sizing
|
||||
- **Interactive Elements**: Buttons with click detection, event system, and callbacks
|
||||
- **Theme System**: 9-slice/9-patch theming with state support (normal, hover, pressed, disabled)
|
||||
- **Android 9-Patch Auto-Parsing**: Automatic parsing of *.9.png files with multi-region support
|
||||
- **Animations**: Built-in animation support for transitions and effects
|
||||
- **Responsive Design**: Automatic resizing with viewport units (vw, vh, %)
|
||||
- **Color Handling**: Utility classes for managing colors in various formats
|
||||
@@ -36,62 +48,52 @@ local Color = FlexLove.Color
|
||||
|
||||
```lua
|
||||
local FlexLove = require("FlexLove")
|
||||
local Gui = FlexLove.GUI
|
||||
local Color = FlexLove.Color
|
||||
|
||||
function love.load()
|
||||
-- Initialize GUI system
|
||||
Gui.init({
|
||||
baseScale = { width = 1920, height = 1080 }
|
||||
-- Initialize with base scaling and theme
|
||||
FlexLove.Gui.init({
|
||||
baseScale = { width = 1920, height = 1080 },
|
||||
theme = "space"
|
||||
})
|
||||
|
||||
-- Create a container
|
||||
local container = Gui.new({
|
||||
x = 100,
|
||||
y = 100,
|
||||
width = 400,
|
||||
height = 300,
|
||||
backgroundColor = Color.new(0.2, 0.2, 0.2, 1),
|
||||
cornerRadius = 10,
|
||||
border = { top = true, bottom = true, left = true, right = true },
|
||||
borderColor = Color.new(0.8, 0.8, 0.8, 1),
|
||||
positioning = "flex",
|
||||
flexDirection = "vertical",
|
||||
gap = 10,
|
||||
padding = { top = 20, right = 20, bottom = 20, left = 20 }
|
||||
})
|
||||
|
||||
-- Create a button
|
||||
local button = Gui.new({
|
||||
parent = container,
|
||||
width = 200,
|
||||
height = 50,
|
||||
-- Create a button with flexbox layout
|
||||
local button = FlexLove.Element.new({
|
||||
width = "20vw",
|
||||
height = "10vh",
|
||||
backgroundColor = FlexLove.Color.new(0.2, 0.2, 0.8, 1),
|
||||
text = "Click Me",
|
||||
textAlign = "center",
|
||||
textColor = Color.new(1, 1, 1, 1),
|
||||
backgroundColor = Color.new(0.2, 0.6, 0.9, 1),
|
||||
cornerRadius = 8,
|
||||
textSize = "md",
|
||||
themeComponent = "button",
|
||||
callback = function(element, event)
|
||||
if event.type == "click" then
|
||||
print("Button clicked!")
|
||||
end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
-- In your love.update and love.draw:
|
||||
function love.update(dt)
|
||||
Gui.update(dt)
|
||||
FlexLove.Gui.update(dt)
|
||||
end
|
||||
|
||||
function love.draw()
|
||||
Gui.draw()
|
||||
end
|
||||
|
||||
function love.resize(w, h)
|
||||
Gui.resize()
|
||||
FlexLove.Gui.draw()
|
||||
end
|
||||
```
|
||||
|
||||
## API Conventions
|
||||
|
||||
### Method Patterns
|
||||
- **Constructors**: `ClassName.new(props)` → instance
|
||||
- **Static Methods**: `ClassName.methodName(args)` → result
|
||||
- **Instance Methods**: `instance:methodName(args)` → result
|
||||
- **Getters**: `instance:getPropertyName()` → value
|
||||
- **Internal Fields**: `_fieldName` (private, do not access directly)
|
||||
- **Error Handling**: Constructors throw errors, utility functions return nil + error string
|
||||
|
||||
### Return Value Patterns
|
||||
- **Single Success**: return value
|
||||
- **Success/Failure**: return result, errorMessage (nil on success for error)
|
||||
- **Multiple Values**: return value1, value2 (documented in @return)
|
||||
- **Constructors**: Always return instance (never nil)
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Element Properties
|
||||
@@ -227,6 +229,36 @@ local button = Gui.new({
|
||||
})
|
||||
```
|
||||
|
||||
#### Android 9-Patch Support
|
||||
|
||||
FlexLove automatically parses Android 9-patch (*.9.png) files:
|
||||
|
||||
```lua
|
||||
-- Theme definition with auto-parsed 9-patch
|
||||
{
|
||||
name = "My Theme",
|
||||
components = {
|
||||
button = {
|
||||
atlas = "themes/mytheme/button.9.png"
|
||||
-- insets automatically extracted from 9-patch borders
|
||||
-- supports multiple stretch regions for complex scaling
|
||||
},
|
||||
panel = {
|
||||
atlas = "themes/mytheme/panel.png",
|
||||
insets = { left = 20, top = 20, right = 20, bottom = 20 }
|
||||
-- manual insets still supported (overrides auto-parsing)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**9-Patch Format:**
|
||||
- Files ending in `.9.png` are automatically detected and parsed
|
||||
- Top/left borders define stretchable regions (black pixels)
|
||||
- Bottom/right borders define content padding (optional)
|
||||
- Supports multiple non-contiguous stretch regions
|
||||
- Manual insets override auto-parsing when specified
|
||||
|
||||
Themes support state-based rendering:
|
||||
- `normal` - Default state
|
||||
- `hover` - Mouse over element
|
||||
@@ -273,18 +305,35 @@ Create smooth transitions:
|
||||
local Animation = FlexLove.Animation
|
||||
|
||||
-- Fade animation
|
||||
element.animation = Animation.fade(1.0, 0, 1)
|
||||
local fadeIn = FlexLove.Animation.fade(1.0, 0, 1)
|
||||
fadeIn:apply(element)
|
||||
|
||||
-- Scale animation
|
||||
element.animation = Animation.scale(0.5, 1, 1.2)
|
||||
local scaleUp = FlexLove.Animation.scale(0.5,
|
||||
{ width = 100, height = 50 },
|
||||
{ width = 200, height = 100 }
|
||||
)
|
||||
scaleUp:apply(element)
|
||||
|
||||
-- Custom animation
|
||||
element.animation = Animation.new({
|
||||
-- Custom animation with easing
|
||||
local customAnim = FlexLove.Animation.new({
|
||||
duration = 1.0,
|
||||
from = { width = 100, height = 50 },
|
||||
to = { width = 200, height = 100 },
|
||||
easing = "easeInOut"
|
||||
start = { opacity = 0, width = 100 },
|
||||
final = { opacity = 1, width = 200 },
|
||||
easing = "easeInOutCubic"
|
||||
})
|
||||
customAnim:apply(element)
|
||||
```
|
||||
|
||||
### Creating Colors
|
||||
|
||||
```lua
|
||||
-- From RGB values (0-1 range)
|
||||
local red = FlexLove.Color.new(1, 0, 0, 1)
|
||||
|
||||
-- From hex string
|
||||
local blue = FlexLove.Color.fromHex("#0000FF")
|
||||
local semiTransparent = FlexLove.Color.fromHex("#FF000080")
|
||||
```
|
||||
|
||||
## API Reference
|
||||
@@ -375,6 +424,15 @@ lua testing/runAll.lua
|
||||
lua testing/__tests__/<specific_test>
|
||||
```
|
||||
|
||||
## Version & Compatibility
|
||||
|
||||
**Current Version**: 1.0.0
|
||||
|
||||
**Compatibility:**
|
||||
- **Lua**: 5.1+
|
||||
- **LÖVE**: 11.x (tested)
|
||||
- **LuaJIT**: Compatible
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
-- Test padding resize behavior with percentage units
|
||||
package.path = package.path .. ";?.lua"
|
||||
local luaunit = require("testing.luaunit")
|
||||
local FlexLove = require("FlexLove")
|
||||
|
||||
@@ -13,7 +14,7 @@ function TestPaddingResize:setUp()
|
||||
|
||||
-- Initialize with base scaling
|
||||
FlexLove.Gui.init({
|
||||
baseScale = { width = 1920, height = 1080 }
|
||||
baseScale = { width = 1920, height = 1080 },
|
||||
})
|
||||
end
|
||||
|
||||
@@ -237,4 +238,4 @@ function TestPaddingResize:testMixedPaddingUnits()
|
||||
luaunit.assertTrue(initialLeft < element.padding.left, "Left padding (vh) should increase")
|
||||
end
|
||||
|
||||
return TestPaddingResize
|
||||
luaunit.LuaUnit.run()
|
||||
|
||||
99
testing/__tests__/21_ninepatch_parser_tests.lua
Normal file
99
testing/__tests__/21_ninepatch_parser_tests.lua
Normal file
@@ -0,0 +1,99 @@
|
||||
-- Test Suite for NinePatch Parser
|
||||
-- Tests ImageDataReader and NinePatchParser modules
|
||||
|
||||
package.path = package.path .. ";?.lua"
|
||||
local lu = require("testing.luaunit")
|
||||
|
||||
-- Mock love.graphics for testing without LÖVE runtime
|
||||
local love = love
|
||||
if not love then
|
||||
love = {}
|
||||
love.graphics = {}
|
||||
love.timer = {}
|
||||
love.window = {}
|
||||
|
||||
-- Mock functions
|
||||
function love.timer.getTime()
|
||||
return 0
|
||||
end
|
||||
|
||||
function love.window.getMode()
|
||||
return 800, 600
|
||||
end
|
||||
end
|
||||
|
||||
-- Load FlexLove
|
||||
local FlexLove = require("FlexLove")
|
||||
|
||||
-- ====================
|
||||
-- Test ImageDataReader
|
||||
-- ====================
|
||||
|
||||
TestImageDataReader = {}
|
||||
|
||||
function TestImageDataReader:test_isBlackPixel_identifiesBlackCorrectly()
|
||||
-- Black pixel with full alpha should return true
|
||||
lu.assertTrue(FlexLove.ImageDataReader.isBlackPixel(0, 0, 0, 255))
|
||||
end
|
||||
|
||||
function TestImageDataReader:test_isBlackPixel_rejectsNonBlack()
|
||||
-- Non-black colors should return false
|
||||
lu.assertFalse(FlexLove.ImageDataReader.isBlackPixel(255, 0, 0, 255)) -- Red
|
||||
lu.assertFalse(FlexLove.ImageDataReader.isBlackPixel(0, 255, 0, 255)) -- Green
|
||||
lu.assertFalse(FlexLove.ImageDataReader.isBlackPixel(0, 0, 255, 255)) -- Blue
|
||||
lu.assertFalse(FlexLove.ImageDataReader.isBlackPixel(128, 128, 128, 255)) -- Gray
|
||||
end
|
||||
|
||||
function TestImageDataReader:test_isBlackPixel_rejectsTransparent()
|
||||
-- Black with no alpha should return false
|
||||
lu.assertFalse(FlexLove.ImageDataReader.isBlackPixel(0, 0, 0, 0))
|
||||
lu.assertFalse(FlexLove.ImageDataReader.isBlackPixel(0, 0, 0, 128))
|
||||
end
|
||||
|
||||
-- ====================
|
||||
-- Test NinePatchParser Helper Functions
|
||||
-- ====================
|
||||
|
||||
TestNinePatchParserHelpers = {}
|
||||
|
||||
-- Note: findBlackPixelRuns is a local function, so we test it indirectly through parse()
|
||||
-- We'll create mock image data to test the full parsing pipeline
|
||||
|
||||
-- ====================
|
||||
-- Integration Tests
|
||||
-- ====================
|
||||
|
||||
TestNinePatchIntegration = {}
|
||||
|
||||
function TestNinePatchIntegration:test_themeLoadsWithNinePatch()
|
||||
-- This test verifies that the space theme can load with 9-patch button
|
||||
local success, err = pcall(function()
|
||||
FlexLove.Theme.load("space")
|
||||
end)
|
||||
|
||||
if not success then
|
||||
print("Theme load error: " .. tostring(err))
|
||||
end
|
||||
|
||||
lu.assertTrue(success, "Space theme should load successfully")
|
||||
end
|
||||
|
||||
function TestNinePatchIntegration:test_ninePatchButtonHasInsets()
|
||||
-- Load theme and verify button component has insets
|
||||
FlexLove.Theme.load("space")
|
||||
local theme = FlexLove.Theme.getActive()
|
||||
|
||||
lu.assertNotNil(theme, "Theme should be active")
|
||||
lu.assertNotNil(theme.components, "Theme should have components")
|
||||
|
||||
if theme.components and theme.components.button then
|
||||
-- Check if insets were auto-parsed or manually defined
|
||||
local hasInsets = theme.components.button.insets ~= nil or theme.components.button.regions ~= nil
|
||||
lu.assertTrue(hasInsets, "Button should have insets or regions defined")
|
||||
else
|
||||
lu.fail("Button component not found in theme")
|
||||
end
|
||||
end
|
||||
|
||||
-- Run tests
|
||||
lu.LuaUnit.run()
|
||||
202
testing/__tests__/22_image_scaler_nearest_tests.lua
Normal file
202
testing/__tests__/22_image_scaler_nearest_tests.lua
Normal file
@@ -0,0 +1,202 @@
|
||||
package.path = package.path .. ";?.lua"
|
||||
local luaunit = require("testing.luaunit")
|
||||
local loveStub = require("testing.loveStub")
|
||||
_G.love = loveStub
|
||||
|
||||
local FlexLove = require("FlexLove")
|
||||
|
||||
TestImageScalerNearest = {}
|
||||
|
||||
function TestImageScalerNearest:setUp()
|
||||
-- Create a simple test image (2x2 with distinct colors)
|
||||
self.testImage2x2 = love.image.newImageData(2, 2)
|
||||
-- Top-left: red
|
||||
self.testImage2x2:setPixel(0, 0, 1, 0, 0, 1)
|
||||
-- Top-right: green
|
||||
self.testImage2x2:setPixel(1, 0, 0, 1, 0, 1)
|
||||
-- Bottom-left: blue
|
||||
self.testImage2x2:setPixel(0, 1, 0, 0, 1, 1)
|
||||
-- Bottom-right: white
|
||||
self.testImage2x2:setPixel(1, 1, 1, 1, 1, 1)
|
||||
end
|
||||
|
||||
function TestImageScalerNearest:test2xScaling()
|
||||
-- Scale 2x2 to 4x4 (2x factor)
|
||||
local scaled = FlexLove.ImageScaler.scaleNearest(self.testImage2x2, 0, 0, 2, 2, 4, 4)
|
||||
|
||||
luaunit.assertEquals(scaled:getWidth(), 4)
|
||||
luaunit.assertEquals(scaled:getHeight(), 4)
|
||||
|
||||
-- Top-left quadrant should be red (0,0 -> 1,1)
|
||||
local r, g, b, a = scaled:getPixel(0, 0)
|
||||
luaunit.assertAlmostEquals(r, 1, 0.01)
|
||||
luaunit.assertAlmostEquals(g, 0, 0.01)
|
||||
luaunit.assertAlmostEquals(b, 0, 0.01)
|
||||
|
||||
r, g, b, a = scaled:getPixel(1, 1)
|
||||
luaunit.assertAlmostEquals(r, 1, 0.01)
|
||||
luaunit.assertAlmostEquals(g, 0, 0.01)
|
||||
luaunit.assertAlmostEquals(b, 0, 0.01)
|
||||
|
||||
-- Top-right quadrant should be green (2,0 -> 3,1)
|
||||
r, g, b, a = scaled:getPixel(2, 0)
|
||||
luaunit.assertAlmostEquals(r, 0, 0.01)
|
||||
luaunit.assertAlmostEquals(g, 1, 0.01)
|
||||
luaunit.assertAlmostEquals(b, 0, 0.01)
|
||||
|
||||
r, g, b, a = scaled:getPixel(3, 1)
|
||||
luaunit.assertAlmostEquals(r, 0, 0.01)
|
||||
luaunit.assertAlmostEquals(g, 1, 0.01)
|
||||
luaunit.assertAlmostEquals(b, 0, 0.01)
|
||||
|
||||
-- Bottom-left quadrant should be blue (0,2 -> 1,3)
|
||||
r, g, b, a = scaled:getPixel(0, 2)
|
||||
luaunit.assertAlmostEquals(r, 0, 0.01)
|
||||
luaunit.assertAlmostEquals(g, 0, 0.01)
|
||||
luaunit.assertAlmostEquals(b, 1, 0.01)
|
||||
|
||||
-- Bottom-right quadrant should be white (2,2 -> 3,3)
|
||||
r, g, b, a = scaled:getPixel(3, 3)
|
||||
luaunit.assertAlmostEquals(r, 1, 0.01)
|
||||
luaunit.assertAlmostEquals(g, 1, 0.01)
|
||||
luaunit.assertAlmostEquals(b, 1, 0.01)
|
||||
end
|
||||
|
||||
function TestImageScalerNearest:test3xScaling()
|
||||
-- Scale 2x2 to 6x6 (3x factor)
|
||||
local scaled = FlexLove.ImageScaler.scaleNearest(self.testImage2x2, 0, 0, 2, 2, 6, 6)
|
||||
|
||||
luaunit.assertEquals(scaled:getWidth(), 6)
|
||||
luaunit.assertEquals(scaled:getHeight(), 6)
|
||||
|
||||
-- Verify nearest-neighbor: each source pixel should map to 3x3 block
|
||||
-- Top-left (red) should cover 0-2, 0-2
|
||||
local r, g, b = scaled:getPixel(0, 0)
|
||||
luaunit.assertAlmostEquals(r, 1, 0.01)
|
||||
r, g, b = scaled:getPixel(2, 2)
|
||||
luaunit.assertAlmostEquals(r, 1, 0.01)
|
||||
|
||||
-- Top-right (green) should cover 3-5, 0-2
|
||||
r, g, b = scaled:getPixel(3, 0)
|
||||
luaunit.assertAlmostEquals(g, 1, 0.01)
|
||||
r, g, b = scaled:getPixel(5, 2)
|
||||
luaunit.assertAlmostEquals(g, 1, 0.01)
|
||||
end
|
||||
|
||||
function TestImageScalerNearest:testNonUniformScaling()
|
||||
-- Scale 2x2 to 6x4 (3x horizontal, 2x vertical)
|
||||
local scaled = FlexLove.ImageScaler.scaleNearest(self.testImage2x2, 0, 0, 2, 2, 6, 4)
|
||||
|
||||
luaunit.assertEquals(scaled:getWidth(), 6)
|
||||
luaunit.assertEquals(scaled:getHeight(), 4)
|
||||
|
||||
-- Top-left red should cover 0-2 horizontally, 0-1 vertically
|
||||
local r, g, b = scaled:getPixel(0, 0)
|
||||
luaunit.assertAlmostEquals(r, 1, 0.01)
|
||||
r, g, b = scaled:getPixel(2, 1)
|
||||
luaunit.assertAlmostEquals(r, 1, 0.01)
|
||||
|
||||
-- Top-right green should cover 3-5 horizontally, 0-1 vertically
|
||||
r, g, b = scaled:getPixel(3, 0)
|
||||
luaunit.assertAlmostEquals(g, 1, 0.01)
|
||||
end
|
||||
|
||||
function TestImageScalerNearest:testSameSizeScaling()
|
||||
-- Scale 2x2 to 2x2 (should be identical)
|
||||
local scaled = FlexLove.ImageScaler.scaleNearest(self.testImage2x2, 0, 0, 2, 2, 2, 2)
|
||||
|
||||
luaunit.assertEquals(scaled:getWidth(), 2)
|
||||
luaunit.assertEquals(scaled:getHeight(), 2)
|
||||
|
||||
-- Verify all pixels match original
|
||||
for y = 0, 1 do
|
||||
for x = 0, 1 do
|
||||
local r1, g1, b1, a1 = self.testImage2x2:getPixel(x, y)
|
||||
local r2, g2, b2, a2 = scaled:getPixel(x, y)
|
||||
luaunit.assertAlmostEquals(r1, r2, 0.01)
|
||||
luaunit.assertAlmostEquals(g1, g2, 0.01)
|
||||
luaunit.assertAlmostEquals(b1, b2, 0.01)
|
||||
luaunit.assertAlmostEquals(a1, a2, 0.01)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function TestImageScalerNearest:test1x1Scaling()
|
||||
-- Create 1x1 image
|
||||
local img1x1 = love.image.newImageData(1, 1)
|
||||
img1x1:setPixel(0, 0, 0.5, 0.5, 0.5, 1)
|
||||
|
||||
-- Scale to 4x4
|
||||
local scaled = FlexLove.ImageScaler.scaleNearest(img1x1, 0, 0, 1, 1, 4, 4)
|
||||
|
||||
luaunit.assertEquals(scaled:getWidth(), 4)
|
||||
luaunit.assertEquals(scaled:getHeight(), 4)
|
||||
|
||||
-- All pixels should be the same color
|
||||
for y = 0, 3 do
|
||||
for x = 0, 3 do
|
||||
local r, g, b = scaled:getPixel(x, y)
|
||||
luaunit.assertAlmostEquals(r, 0.5, 0.01)
|
||||
luaunit.assertAlmostEquals(g, 0.5, 0.01)
|
||||
luaunit.assertAlmostEquals(b, 0.5, 0.01)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function TestImageScalerNearest:testSubregionScaling()
|
||||
-- Create 4x4 image with different quadrants
|
||||
local img4x4 = love.image.newImageData(4, 4)
|
||||
|
||||
-- Fill with pattern: top-left red, rest black
|
||||
for y = 0, 3 do
|
||||
for x = 0, 3 do
|
||||
if x < 2 and y < 2 then
|
||||
img4x4:setPixel(x, y, 1, 0, 0, 1) -- red
|
||||
else
|
||||
img4x4:setPixel(x, y, 0, 0, 0, 1) -- black
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Scale only the top-left 2x2 red quadrant to 4x4
|
||||
local scaled = FlexLove.ImageScaler.scaleNearest(img4x4, 0, 0, 2, 2, 4, 4)
|
||||
|
||||
luaunit.assertEquals(scaled:getWidth(), 4)
|
||||
luaunit.assertEquals(scaled:getHeight(), 4)
|
||||
|
||||
-- All pixels should be red (from source quadrant)
|
||||
for y = 0, 3 do
|
||||
for x = 0, 3 do
|
||||
local r, g, b = scaled:getPixel(x, y)
|
||||
luaunit.assertAlmostEquals(r, 1, 0.01)
|
||||
luaunit.assertAlmostEquals(g, 0, 0.01)
|
||||
luaunit.assertAlmostEquals(b, 0, 0.01)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function TestImageScalerNearest:testAlphaChannel()
|
||||
-- Create image with varying alpha
|
||||
local img = love.image.newImageData(2, 2)
|
||||
img:setPixel(0, 0, 1, 0, 0, 1.0) -- Opaque red
|
||||
img:setPixel(1, 0, 0, 1, 0, 0.5) -- Semi-transparent green
|
||||
img:setPixel(0, 1, 0, 0, 1, 0.25) -- More transparent blue
|
||||
img:setPixel(1, 1, 1, 1, 1, 0.0) -- Fully transparent white
|
||||
|
||||
local scaled = FlexLove.ImageScaler.scaleNearest(img, 0, 0, 2, 2, 4, 4)
|
||||
|
||||
-- Check alpha values are preserved
|
||||
local r, g, b, a = scaled:getPixel(0, 0)
|
||||
luaunit.assertAlmostEquals(a, 1.0, 0.01)
|
||||
|
||||
r, g, b, a = scaled:getPixel(2, 0)
|
||||
luaunit.assertAlmostEquals(a, 0.5, 0.01)
|
||||
|
||||
r, g, b, a = scaled:getPixel(0, 2)
|
||||
luaunit.assertAlmostEquals(a, 0.25, 0.01)
|
||||
|
||||
r, g, b, a = scaled:getPixel(3, 3)
|
||||
luaunit.assertAlmostEquals(a, 0.0, 0.01)
|
||||
end
|
||||
|
||||
luaunit.LuaUnit.run()
|
||||
@@ -23,9 +23,11 @@ local testFiles = {
|
||||
"testing/__tests__/17_sibling_space_reservation_tests.lua",
|
||||
"testing/__tests__/18_font_family_inheritance_tests.lua",
|
||||
"testing/__tests__/19_negative_margin_tests.lua",
|
||||
"testing/__tests__/20_padding_resize_tests.lua",
|
||||
"testing/__tests__/21_ninepatch_parser_tests.lua",
|
||||
"testing/__tests__/22_image_scaler_nearest_tests.lua",
|
||||
}
|
||||
|
||||
-- testingun all tests, but don't exit on error
|
||||
local success = true
|
||||
print("========================================")
|
||||
print("Running ALL tests")
|
||||
@@ -45,6 +47,5 @@ print("========================================")
|
||||
print("All tests completed")
|
||||
print("========================================")
|
||||
|
||||
-- Run the tests and exit with appropriate code
|
||||
local result = luaunit.LuaUnit.run()
|
||||
os.exit(success and result or 1)
|
||||
|
||||
@@ -347,6 +347,57 @@ Gui.new({
|
||||
})
|
||||
```
|
||||
|
||||
## Corner Scaling
|
||||
|
||||
By default, 9-slice corners and non-stretched edges are rendered at their original pixel size (1:1). For pixel-art themes or when you want corners to scale with the window, you can enable corner scaling:
|
||||
|
||||
### Enabling Corner Scaling
|
||||
|
||||
```lua
|
||||
-- themes/my_theme.lua
|
||||
return {
|
||||
name = "My Theme",
|
||||
components = {
|
||||
button = {
|
||||
atlas = "themes/button.png",
|
||||
insets = { left = 8, top = 8, right = 8, bottom = 8 },
|
||||
scaleCorners = true, -- Enable corner scaling
|
||||
scalingAlgorithm = "bilinear" -- "bilinear" (smooth) or "nearest" (sharp/pixelated)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scaling Algorithms
|
||||
|
||||
- **`bilinear`** (default): Smooth interpolation between pixels. Best for most use cases.
|
||||
- **`nearest`**: Nearest-neighbor sampling. Best for pixel art that should maintain sharp edges.
|
||||
|
||||
### When to Use Corner Scaling
|
||||
|
||||
- **Pixel art themes**: Use `scaleCorners = true` with `scalingAlgorithm = "nearest"` to maintain crisp pixel boundaries
|
||||
- **High DPI displays**: Use `scaleCorners = true` with `scalingAlgorithm = "bilinear"` for smooth scaling
|
||||
- **Fixed-size UI**: Keep `scaleCorners = false` (default) for pixel-perfect rendering at original size
|
||||
|
||||
### Per-State Scaling
|
||||
|
||||
You can also set scaling per-state:
|
||||
|
||||
```lua
|
||||
button = {
|
||||
atlas = "themes/button_normal.png",
|
||||
scaleCorners = true,
|
||||
scalingAlgorithm = "bilinear",
|
||||
states = {
|
||||
hover = {
|
||||
atlas = "themes/button_hover.png",
|
||||
scaleCorners = true, -- Can override per state
|
||||
scalingAlgorithm = "nearest" -- Different algorithm for this state
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Start Simple**: Begin with one component (button) before creating a full theme
|
||||
@@ -355,6 +406,7 @@ Gui.new({
|
||||
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
|
||||
6. **Transparency**: Use semi-transparent backgroundColor to tint themed elements
|
||||
7. **Corner Scaling**: Enable for pixel art or responsive UIs; disable for pixel-perfect rendering
|
||||
|
||||
## Tools for Creating Atlases
|
||||
|
||||
|
||||
0
themes/metal.lua
Normal file
0
themes/metal.lua
Normal file
163
themes/space.lua
163
themes/space.lua
@@ -1,8 +1,5 @@
|
||||
-- Space Theme
|
||||
-- Panel is 882x687 with 110px border
|
||||
-- All other components are 189x189 with 31px/127px regions
|
||||
|
||||
-- Define Color inline to avoid circular dependency
|
||||
local Color = {}
|
||||
Color.__index = Color
|
||||
|
||||
@@ -21,183 +18,43 @@ return {
|
||||
components = {
|
||||
card = {
|
||||
atlas = "themes/space/card.png",
|
||||
regions = {
|
||||
topLeft = { x = 0, y = 0, w = 100, h = 100 },
|
||||
topCenter = { x = 100, y = 0, w = 205, h = 100 },
|
||||
topRight = { x = 305, y = 0, w = 100, h = 100 },
|
||||
middleLeft = { x = 0, y = 100, w = 100, h = 178 },
|
||||
middleCenter = { x = 100, y = 100, w = 205, h = 178 },
|
||||
middleRight = { x = 305, y = 100, w = 100, h = 178 },
|
||||
bottomLeft = { x = 0, y = 278, w = 100, h = 100 },
|
||||
bottomCenter = { x = 100, y = 278, w = 205, h = 100 },
|
||||
bottomRight = { x = 305, y = 278, w = 100, h = 100 },
|
||||
},
|
||||
stretch = {
|
||||
horizontal = { "topCenter", "middleCenter", "bottomCenter" },
|
||||
vertical = { "middleLeft", "middleCenter", "middleRight" },
|
||||
},
|
||||
insets = { left = 66, top = 66, right = 66, bottom = 66 },
|
||||
},
|
||||
cardv2 = {
|
||||
atlas = "themes/space/card-v2.png",
|
||||
regions = {
|
||||
topLeft = { x = 0, y = 0, w = 100, h = 100 },
|
||||
topCenter = { x = 100, y = 0, w = 205, h = 100 },
|
||||
topRight = { x = 305, y = 0, w = 100, h = 100 },
|
||||
middleLeft = { x = 0, y = 100, w = 100, h = 178 },
|
||||
middleCenter = { x = 100, y = 100, w = 205, h = 178 },
|
||||
middleRight = { x = 305, y = 100, w = 100, h = 178 },
|
||||
bottomLeft = { x = 0, y = 278, w = 100, h = 100 },
|
||||
bottomCenter = { x = 100, y = 278, w = 205, h = 100 },
|
||||
bottomRight = { x = 305, y = 278, w = 100, h = 100 },
|
||||
},
|
||||
stretch = {
|
||||
horizontal = { "topCenter", "middleCenter", "bottomCenter" },
|
||||
vertical = { "middleLeft", "middleCenter", "middleRight" },
|
||||
},
|
||||
insets = { left = 66, top = 66, right = 66, bottom = 66 },
|
||||
},
|
||||
cardv3 = {
|
||||
atlas = "themes/space/card-v3.png",
|
||||
regions = {
|
||||
topLeft = { x = 0, y = 0, w = 286, h = 100 },
|
||||
topCenter = { x = 286, y = 0, w = 74, h = 100 },
|
||||
topRight = { x = 360, y = 0, w = 286, h = 100 },
|
||||
middleLeft = { x = 0, y = 100, w = 286, h = 101 },
|
||||
middleCenter = { x = 286, y = 100, w = 74, h = 101 },
|
||||
middleRight = { x = 360, y = 100, w = 286, h = 101 },
|
||||
bottomLeft = { x = 0, y = 201, w = 286, h = 100 },
|
||||
bottomCenter = { x = 286, y = 201, w = 74, h = 100 },
|
||||
bottomRight = { x = 360, y = 201, w = 286, h = 100 },
|
||||
},
|
||||
stretch = {
|
||||
horizontal = { "topCenter", "middleCenter", "bottomCenter" },
|
||||
vertical = { "middleLeft", "middleCenter", "middleRight" },
|
||||
},
|
||||
insets = { left = 286, top = 100, right = 286, bottom = 100 },
|
||||
},
|
||||
panel = {
|
||||
atlas = "themes/space/panel.png",
|
||||
regions = {
|
||||
topLeft = { x = 0, y = 0, w = 38, h = 30 },
|
||||
topCenter = { x = 38, y = 0, w = 53, h = 30 },
|
||||
topRight = { x = 91, y = 0, w = 22, h = 30 },
|
||||
middleLeft = { x = 0, y = 30, w = 38, h = 5 },
|
||||
middleCenter = { x = 38, y = 30, w = 53, h = 5 },
|
||||
middleRight = { x = 91, y = 30, w = 22, h = 5 },
|
||||
bottomLeft = { x = 0, y = 35, w = 38, h = 30 },
|
||||
bottomCenter = { x = 38, y = 35, w = 53, h = 30 },
|
||||
bottomRight = { x = 91, y = 35, w = 22, h = 30 },
|
||||
},
|
||||
stretch = {
|
||||
horizontal = { "topCenter", "middleCenter", "bottomCenter" },
|
||||
vertical = { "middleLeft", "middleCenter", "middleRight" },
|
||||
},
|
||||
insets = { left = 38, top = 30, right = 22, bottom = 30 },
|
||||
},
|
||||
panelred = {
|
||||
atlas = "themes/space/panel-red.png",
|
||||
regions = {
|
||||
topLeft = { x = 0, y = 0, w = 38, h = 30 },
|
||||
topCenter = { x = 38, y = 0, w = 53, h = 30 },
|
||||
topRight = { x = 91, y = 0, w = 22, h = 30 },
|
||||
middleLeft = { x = 0, y = 30, w = 38, h = 5 },
|
||||
middleCenter = { x = 38, y = 30, w = 53, h = 5 },
|
||||
middleRight = { x = 91, y = 30, w = 22, h = 5 },
|
||||
bottomLeft = { x = 0, y = 35, w = 38, h = 30 },
|
||||
bottomCenter = { x = 38, y = 35, w = 53, h = 30 },
|
||||
bottomRight = { x = 91, y = 35, w = 22, h = 30 },
|
||||
},
|
||||
stretch = {
|
||||
horizontal = { "topCenter", "middleCenter", "bottomCenter" },
|
||||
vertical = { "middleLeft", "middleCenter", "middleRight" },
|
||||
},
|
||||
insets = { left = 38, top = 30, right = 22, bottom = 30 },
|
||||
},
|
||||
panelgreen = {
|
||||
atlas = "themes/space/panel-green.png",
|
||||
regions = {
|
||||
topLeft = { x = 0, y = 0, w = 38, h = 30 },
|
||||
topCenter = { x = 38, y = 0, w = 53, h = 30 },
|
||||
topRight = { x = 91, y = 0, w = 22, h = 30 },
|
||||
middleLeft = { x = 0, y = 30, w = 38, h = 5 },
|
||||
middleCenter = { x = 38, y = 30, w = 53, h = 5 },
|
||||
middleRight = { x = 91, y = 30, w = 22, h = 5 },
|
||||
bottomLeft = { x = 0, y = 35, w = 38, h = 30 },
|
||||
bottomCenter = { x = 38, y = 35, w = 53, h = 30 },
|
||||
bottomRight = { x = 91, y = 35, w = 22, h = 30 },
|
||||
},
|
||||
stretch = {
|
||||
horizontal = { "topCenter", "middleCenter", "bottomCenter" },
|
||||
vertical = { "middleLeft", "middleCenter", "middleRight" },
|
||||
},
|
||||
insets = { left = 38, top = 30, right = 22, bottom = 30 },
|
||||
},
|
||||
button = {
|
||||
atlas = "themes/space/button.png",
|
||||
regions = {
|
||||
topLeft = { x = 0, y = 0, w = 14, h = 14 },
|
||||
topCenter = { x = 14, y = 0, w = 86, h = 14 },
|
||||
topRight = { x = 100, y = 0, w = 14, h = 14 },
|
||||
middleLeft = { x = 0, y = 14, w = 14, h = 10 },
|
||||
middleCenter = { x = 14, y = 14, w = 86, h = 10 },
|
||||
middleRight = { x = 100, y = 14, w = 14, h = 10 },
|
||||
bottomLeft = { x = 0, y = 24, w = 14, h = 14 },
|
||||
bottomCenter = { x = 14, y = 24, w = 86, h = 14 },
|
||||
bottomRight = { x = 100, y = 24, w = 14, h = 14 },
|
||||
},
|
||||
stretch = {
|
||||
horizontal = { "topCenter", "middleCenter", "bottomCenter" },
|
||||
vertical = { "middleLeft", "middleCenter", "middleRight" },
|
||||
},
|
||||
insets = { left = 14, top = 14, right = 14, bottom = 14 },
|
||||
states = {
|
||||
hover = {
|
||||
atlas = "themes/space/button-hover.png",
|
||||
regions = {
|
||||
topLeft = { x = 0, y = 0, w = 14, h = 14 },
|
||||
topCenter = { x = 14, y = 0, w = 86, h = 14 },
|
||||
topRight = { x = 100, y = 0, w = 14, h = 14 },
|
||||
middleLeft = { x = 0, y = 14, w = 14, h = 10 },
|
||||
middleCenter = { x = 14, y = 14, w = 86, h = 10 },
|
||||
middleRight = { x = 100, y = 14, w = 14, h = 10 },
|
||||
bottomLeft = { x = 0, y = 24, w = 14, h = 14 },
|
||||
bottomCenter = { x = 14, y = 24, w = 86, h = 14 },
|
||||
bottomRight = { x = 100, y = 24, w = 14, h = 14 },
|
||||
},
|
||||
stretch = {
|
||||
horizontal = { "topCenter", "middleCenter", "bottomCenter" },
|
||||
vertical = { "middleLeft", "middleCenter", "middleRight" },
|
||||
},
|
||||
insets = { left = 14, top = 14, right = 14, bottom = 14 },
|
||||
},
|
||||
pressed = {
|
||||
atlas = "themes/space/button-pressed.png",
|
||||
regions = {
|
||||
topLeft = { x = 0, y = 0, w = 14, h = 14 },
|
||||
topCenter = { x = 14, y = 0, w = 86, h = 14 },
|
||||
topRight = { x = 100, y = 0, w = 14, h = 14 },
|
||||
middleLeft = { x = 0, y = 14, w = 14, h = 10 },
|
||||
middleCenter = { x = 14, y = 14, w = 86, h = 10 },
|
||||
middleRight = { x = 100, y = 14, w = 14, h = 10 },
|
||||
bottomLeft = { x = 0, y = 24, w = 14, h = 14 },
|
||||
bottomCenter = { x = 14, y = 24, w = 86, h = 14 },
|
||||
bottomRight = { x = 100, y = 24, w = 14, h = 14 },
|
||||
},
|
||||
stretch = {
|
||||
horizontal = { "topCenter", "middleCenter", "bottomCenter" },
|
||||
vertical = { "middleLeft", "middleCenter", "middleRight" },
|
||||
},
|
||||
insets = { left = 14, top = 14, right = 14, bottom = 14 },
|
||||
},
|
||||
disabled = {
|
||||
atlas = "themes/space/button-disabled.png",
|
||||
regions = {
|
||||
topLeft = { x = 0, y = 0, w = 14, h = 14 },
|
||||
topCenter = { x = 14, y = 0, w = 86, h = 14 },
|
||||
topRight = { x = 100, y = 0, w = 14, h = 14 },
|
||||
middleLeft = { x = 0, y = 14, w = 14, h = 10 },
|
||||
middleCenter = { x = 14, y = 14, w = 86, h = 10 },
|
||||
middleRight = { x = 100, y = 14, w = 14, h = 10 },
|
||||
bottomLeft = { x = 0, y = 24, w = 14, h = 14 },
|
||||
bottomCenter = { x = 14, y = 24, w = 86, h = 14 },
|
||||
bottomRight = { x = 100, y = 24, w = 14, h = 14 },
|
||||
},
|
||||
stretch = {
|
||||
horizontal = { "topCenter", "middleCenter", "bottomCenter" },
|
||||
vertical = { "middleLeft", "middleCenter", "middleRight" },
|
||||
},
|
||||
insets = { left = 14, top = 14, right = 14, bottom = 14 },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user