working on better 9patch support
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
Cartographer.lua
|
Cartographer.lua
|
||||||
OverlayStats.lua
|
OverlayStats.lua
|
||||||
|
themes/metal/
|
||||||
|
themes/space/
|
||||||
.DS_STORE
|
.DS_STORE
|
||||||
|
|||||||
48
CHANGELOG.md
48
CHANGELOG.md
@@ -5,6 +5,17 @@ All notable changes to FlexLove will be documented in this file.
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### 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
|
- **Corner Radius Support**: Added `cornerRadius` property for rounded corners
|
||||||
- Supports uniform radius (single number) or individual corners (table)
|
- Supports uniform radius (single number) or individual corners (table)
|
||||||
- Automatically clips children to parent's rounded corners using stencil buffer
|
- 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
|
## 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`
|
### From `background` to `backgroundColor`
|
||||||
|
|
||||||
If you're updating existing code, replace all instances of `background` with `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
|
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
|
VERSION: 1.0.0
|
||||||
LICENSE: MIT
|
LICENSE: MIT
|
||||||
================================================================================
|
For full documentation, see README.md
|
||||||
]]
|
]]
|
||||||
|
|
||||||
-- ====================
|
-- ====================
|
||||||
@@ -314,6 +97,295 @@ function Color.fromHex(hexWithTag)
|
|||||||
end
|
end
|
||||||
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
|
-- Theme System
|
||||||
-- ====================
|
-- ====================
|
||||||
@@ -325,12 +397,17 @@ end
|
|||||||
---@field h number -- Height in atlas
|
---@field h number -- Height in atlas
|
||||||
|
|
||||||
---@class ThemeComponent
|
---@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 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 stretch {horizontal:table<integer, string>, vertical:table<integer, string>}
|
||||||
---@field states table<string, ThemeComponent>?
|
---@field states table<string, ThemeComponent>?
|
||||||
---@field contentAutoSizingMultiplier {width:number?, height:number?}? -- Optional: multiplier for auto-sized content dimensions
|
---@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 _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
|
---@class FontFamily
|
||||||
---@field path string -- Path to the font file (relative to FlexLove or absolute)
|
---@field path string -- Path to the font file (relative to FlexLove or absolute)
|
||||||
@@ -424,9 +501,6 @@ local function safeLoadImage(imagePath)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Create a new theme instance
|
|
||||||
---@param definition ThemeDefinition
|
|
||||||
---@return Theme
|
|
||||||
--- Validate theme definition structure
|
--- Validate theme definition structure
|
||||||
---@param definition ThemeDefinition
|
---@param definition ThemeDefinition
|
||||||
---@return boolean, string? -- Returns true if valid, or false with error message
|
---@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.fonts = definition.fonts or {}
|
||||||
self.contentAutoSizingMultiplier = definition.contentAutoSizingMultiplier or nil
|
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
|
for componentName, component in pairs(self.components) do
|
||||||
if component.atlas then
|
if component.atlas then
|
||||||
if type(component.atlas) == "string" then
|
if type(component.atlas) == "string" then
|
||||||
local resolvedPath = resolveImagePath(component.atlas)
|
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)
|
local image, err = safeLoadImage(resolvedPath)
|
||||||
if image then
|
if image then
|
||||||
component._loadedAtlas = image
|
component._loadedAtlas = image
|
||||||
@@ -504,17 +595,95 @@ function Theme.new(definition)
|
|||||||
end
|
end
|
||||||
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
|
if component.states then
|
||||||
for stateName, stateComponent in pairs(component.states) do
|
for stateName, stateComponent in pairs(component.states) do
|
||||||
if stateComponent.atlas then
|
if stateComponent.atlas then
|
||||||
if type(stateComponent.atlas) == "string" then
|
if type(stateComponent.atlas) == "string" then
|
||||||
local resolvedPath = resolveImagePath(stateComponent.atlas)
|
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
|
else
|
||||||
stateComponent._loadedAtlas = stateComponent.atlas
|
stateComponent._loadedAtlas = stateComponent.atlas
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
end
|
end
|
||||||
@@ -781,16 +950,16 @@ end
|
|||||||
|
|
||||||
local NineSlice = {}
|
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 component ThemeComponent
|
||||||
---@param atlas love.Image
|
---@param atlas love.Image
|
||||||
---@param x number -- X position of border box (top-left corner)
|
---@param x number -- X position (top-left corner)
|
||||||
---@param y number -- Y position of border box (top-left corner)
|
---@param y number -- Y position (top-left corner)
|
||||||
---@param contentWidth number -- Width of content area (excludes padding)
|
---@param width number -- Total width (border-box)
|
||||||
---@param contentHeight number -- Height of content area (excludes padding)
|
---@param height number -- Total height (border-box)
|
||||||
---@param padding {top:number, right:number, bottom:number, left:number} -- Padding defines border thickness
|
|
||||||
---@param opacity number?
|
---@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
|
if not component or not atlas then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -800,102 +969,54 @@ function NineSlice.draw(component, atlas, x, y, contentWidth, contentHeight, pad
|
|||||||
|
|
||||||
local regions = component.regions
|
local regions = component.regions
|
||||||
|
|
||||||
-- Calculate source image border dimensions from regions
|
-- Extract border dimensions from regions (in pixels)
|
||||||
local sourceBorderLeft = regions.topLeft.w
|
local left = regions.topLeft.w
|
||||||
local sourceBorderRight = regions.topRight.w
|
local right = regions.topRight.w
|
||||||
local sourceBorderTop = regions.topLeft.h
|
local top = regions.topLeft.h
|
||||||
local sourceBorderBottom = regions.bottomLeft.h
|
local bottom = regions.bottomLeft.h
|
||||||
local sourceCenterWidth = regions.middleCenter.w
|
local centerW = regions.middleCenter.w
|
||||||
local sourceCenterHeight = regions.middleCenter.h
|
local centerH = regions.middleCenter.h
|
||||||
|
|
||||||
-- Calculate scale factors to fit borders within padding
|
-- Calculate content area (space remaining after borders)
|
||||||
-- Borders scale to fit the padding dimensions
|
local contentWidth = width - left - right
|
||||||
local scaleLeft = padding.left / sourceBorderLeft
|
local contentHeight = height - top - bottom
|
||||||
local scaleRight = padding.right / sourceBorderRight
|
|
||||||
local scaleTop = padding.top / sourceBorderTop
|
-- Clamp to prevent negative dimensions
|
||||||
local scaleBottom = padding.bottom / sourceBorderBottom
|
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
|
-- Create quads for each region
|
||||||
local atlasWidth, atlasHeight = atlas:getDimensions()
|
local atlasWidth, atlasHeight = atlas:getDimensions()
|
||||||
|
|
||||||
-- Helper to create quad
|
|
||||||
local function makeQuad(region)
|
local function makeQuad(region)
|
||||||
return love.graphics.newQuad(region.x, region.y, region.w, region.h, atlasWidth, atlasHeight)
|
return love.graphics.newQuad(region.x, region.y, region.w, region.h, atlasWidth, atlasHeight)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Top-left corner (scales to fit top-left padding)
|
-- CORNERS (no scaling - 1:1 pixel perfect)
|
||||||
love.graphics.draw(atlas, makeQuad(regions.topLeft), x, y, 0, scaleLeft, scaleTop)
|
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)
|
-- TOP/BOTTOM EDGES (stretch horizontally only)
|
||||||
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)
|
|
||||||
if contentWidth > 0 then
|
if contentWidth > 0 then
|
||||||
local stretchScaleX = contentWidth / sourceCenterWidth
|
love.graphics.draw(atlas, makeQuad(regions.topCenter), x + left, y, 0, scaleX, 1)
|
||||||
love.graphics.draw(atlas, makeQuad(regions.topCenter), x + padding.left, y, 0, stretchScaleX, scaleTop)
|
love.graphics.draw(atlas, makeQuad(regions.bottomCenter), x + left, y + top + contentHeight, 0, scaleX, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Bottom edge (stretched to content width, scaled to padding.bottom height)
|
-- LEFT/RIGHT EDGES (stretch vertically only)
|
||||||
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)
|
|
||||||
if contentHeight > 0 then
|
if contentHeight > 0 then
|
||||||
local stretchScaleY = contentHeight / sourceCenterHeight
|
love.graphics.draw(atlas, makeQuad(regions.middleLeft), x, y + top, 0, 1, scaleY)
|
||||||
love.graphics.draw(atlas, makeQuad(regions.middleLeft), x, y + padding.top, 0, scaleLeft, stretchScaleY)
|
love.graphics.draw(atlas, makeQuad(regions.middleRight), x + left + contentWidth, y + top, 0, 1, scaleY)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Right edge (scaled to padding.right width, stretched to content height)
|
-- CENTER (stretch both dimensions)
|
||||||
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)
|
|
||||||
if contentWidth > 0 and contentHeight > 0 then
|
if contentWidth > 0 and contentHeight > 0 then
|
||||||
local stretchScaleX = contentWidth / sourceCenterWidth
|
love.graphics.draw(atlas, makeQuad(regions.middleCenter), x + left, y + top, 0, scaleX, scaleY)
|
||||||
local stretchScaleY = contentHeight / sourceCenterHeight
|
|
||||||
love.graphics.draw(
|
|
||||||
atlas,
|
|
||||||
makeQuad(regions.middleCenter),
|
|
||||||
x + padding.left,
|
|
||||||
y + padding.top,
|
|
||||||
0,
|
|
||||||
stretchScaleX,
|
|
||||||
stretchScaleY
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Reset color
|
-- Reset color
|
||||||
@@ -3308,8 +3429,10 @@ function Element:draw()
|
|||||||
and component.regions.bottomCenter
|
and component.regions.bottomCenter
|
||||||
and component.regions.bottomRight
|
and component.regions.bottomRight
|
||||||
if hasAllRegions then
|
if hasAllRegions then
|
||||||
-- NineSlice.draw expects content dimensions (without padding), not border-box
|
-- Calculate border-box dimensions (content + padding)
|
||||||
NineSlice.draw(component, atlasToUse, self.x, self.y, self.width, self.height, self.padding, self.opacity)
|
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
|
else
|
||||||
-- Silently skip drawing if component structure is invalid
|
-- Silently skip drawing if component structure is invalid
|
||||||
end
|
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 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)
|
-- - 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 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)
|
-- Handle shorthand properties first (horizontal/vertical)
|
||||||
local resolvedHorizontalPadding = nil
|
local resolvedHorizontalPadding = nil
|
||||||
@@ -4135,6 +4260,8 @@ Gui.new = Element.new
|
|||||||
Gui.Element = Element
|
Gui.Element = Element
|
||||||
Gui.Animation = Animation
|
Gui.Animation = Animation
|
||||||
Gui.Theme = Theme
|
Gui.Theme = Theme
|
||||||
|
Gui.ImageDataReader = ImageDataReader
|
||||||
|
Gui.NinePatchParser = NinePatchParser
|
||||||
|
|
||||||
-- Export individual enums for convenience
|
-- Export individual enums for convenience
|
||||||
return {
|
return {
|
||||||
@@ -4144,6 +4271,9 @@ return {
|
|||||||
Color = Color,
|
Color = Color,
|
||||||
Theme = Theme,
|
Theme = Theme,
|
||||||
Animation = Animation,
|
Animation = Animation,
|
||||||
|
ImageScaler = ImageScaler,
|
||||||
|
ImageDataReader = ImageDataReader,
|
||||||
|
NinePatchParser = NinePatchParser,
|
||||||
enums = enums,
|
enums = enums,
|
||||||
-- Export individual enums at top level
|
-- Export individual enums at top level
|
||||||
Positioning = Positioning,
|
Positioning = Positioning,
|
||||||
|
|||||||
166
README.md
166
README.md
@@ -1,9 +1,20 @@
|
|||||||
# FlexLöve
|
# 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.
|
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
|
## ⚠️ Development Status
|
||||||
|
|
||||||
This library is under active development. While many features are functional, some aspects may change or have incomplete/broken implementations.
|
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
|
- **Element Management**: Hierarchical element structures with automatic sizing
|
||||||
- **Interactive Elements**: Buttons with click detection, event system, and callbacks
|
- **Interactive Elements**: Buttons with click detection, event system, and callbacks
|
||||||
- **Theme System**: 9-slice/9-patch theming with state support (normal, hover, pressed, disabled)
|
- **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
|
- **Animations**: Built-in animation support for transitions and effects
|
||||||
- **Responsive Design**: Automatic resizing with viewport units (vw, vh, %)
|
- **Responsive Design**: Automatic resizing with viewport units (vw, vh, %)
|
||||||
- **Color Handling**: Utility classes for managing colors in various formats
|
- **Color Handling**: Utility classes for managing colors in various formats
|
||||||
@@ -36,62 +48,52 @@ local Color = FlexLove.Color
|
|||||||
|
|
||||||
```lua
|
```lua
|
||||||
local FlexLove = require("FlexLove")
|
local FlexLove = require("FlexLove")
|
||||||
local Gui = FlexLove.GUI
|
|
||||||
local Color = FlexLove.Color
|
|
||||||
|
|
||||||
function love.load()
|
-- Initialize with base scaling and theme
|
||||||
-- Initialize GUI system
|
FlexLove.Gui.init({
|
||||||
Gui.init({
|
baseScale = { width = 1920, height = 1080 },
|
||||||
baseScale = { width = 1920, height = 1080 }
|
theme = "space"
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Create a container
|
-- Create a button with flexbox layout
|
||||||
local container = Gui.new({
|
local button = FlexLove.Element.new({
|
||||||
x = 100,
|
width = "20vw",
|
||||||
y = 100,
|
height = "10vh",
|
||||||
width = 400,
|
backgroundColor = FlexLove.Color.new(0.2, 0.2, 0.8, 1),
|
||||||
height = 300,
|
text = "Click Me",
|
||||||
backgroundColor = Color.new(0.2, 0.2, 0.2, 1),
|
textSize = "md",
|
||||||
cornerRadius = 10,
|
themeComponent = "button",
|
||||||
border = { top = true, bottom = true, left = true, right = true },
|
callback = function(element, event)
|
||||||
borderColor = Color.new(0.8, 0.8, 0.8, 1),
|
print("Button clicked!")
|
||||||
positioning = "flex",
|
end
|
||||||
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,
|
|
||||||
text = "Click Me",
|
|
||||||
textAlign = "center",
|
|
||||||
textColor = Color.new(1, 1, 1, 1),
|
|
||||||
backgroundColor = Color.new(0.2, 0.6, 0.9, 1),
|
|
||||||
cornerRadius = 8,
|
|
||||||
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)
|
function love.update(dt)
|
||||||
Gui.update(dt)
|
FlexLove.Gui.update(dt)
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.draw()
|
function love.draw()
|
||||||
Gui.draw()
|
FlexLove.Gui.draw()
|
||||||
end
|
|
||||||
|
|
||||||
function love.resize(w, h)
|
|
||||||
Gui.resize()
|
|
||||||
end
|
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
|
## Core Concepts
|
||||||
|
|
||||||
### Element Properties
|
### 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:
|
Themes support state-based rendering:
|
||||||
- `normal` - Default state
|
- `normal` - Default state
|
||||||
- `hover` - Mouse over element
|
- `hover` - Mouse over element
|
||||||
@@ -273,18 +305,35 @@ Create smooth transitions:
|
|||||||
local Animation = FlexLove.Animation
|
local Animation = FlexLove.Animation
|
||||||
|
|
||||||
-- Fade 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
|
-- 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
|
-- Custom animation with easing
|
||||||
element.animation = Animation.new({
|
local customAnim = FlexLove.Animation.new({
|
||||||
duration = 1.0,
|
duration = 1.0,
|
||||||
from = { width = 100, height = 50 },
|
start = { opacity = 0, width = 100 },
|
||||||
to = { width = 200, height = 100 },
|
final = { opacity = 1, width = 200 },
|
||||||
easing = "easeInOut"
|
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
|
## API Reference
|
||||||
@@ -375,6 +424,15 @@ lua testing/runAll.lua
|
|||||||
lua testing/__tests__/<specific_test>
|
lua testing/__tests__/<specific_test>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Version & Compatibility
|
||||||
|
|
||||||
|
**Current Version**: 1.0.0
|
||||||
|
|
||||||
|
**Compatibility:**
|
||||||
|
- **Lua**: 5.1+
|
||||||
|
- **LÖVE**: 11.x (tested)
|
||||||
|
- **LuaJIT**: Compatible
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License - see LICENSE file for details.
|
MIT License - see LICENSE file for details.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
-- Test padding resize behavior with percentage units
|
-- Test padding resize behavior with percentage units
|
||||||
|
package.path = package.path .. ";?.lua"
|
||||||
local luaunit = require("testing.luaunit")
|
local luaunit = require("testing.luaunit")
|
||||||
local FlexLove = require("FlexLove")
|
local FlexLove = require("FlexLove")
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ function TestPaddingResize:setUp()
|
|||||||
|
|
||||||
-- Initialize with base scaling
|
-- Initialize with base scaling
|
||||||
FlexLove.Gui.init({
|
FlexLove.Gui.init({
|
||||||
baseScale = { width = 1920, height = 1080 }
|
baseScale = { width = 1920, height = 1080 },
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -237,4 +238,4 @@ function TestPaddingResize:testMixedPaddingUnits()
|
|||||||
luaunit.assertTrue(initialLeft < element.padding.left, "Left padding (vh) should increase")
|
luaunit.assertTrue(initialLeft < element.padding.left, "Left padding (vh) should increase")
|
||||||
end
|
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__/17_sibling_space_reservation_tests.lua",
|
||||||
"testing/__tests__/18_font_family_inheritance_tests.lua",
|
"testing/__tests__/18_font_family_inheritance_tests.lua",
|
||||||
"testing/__tests__/19_negative_margin_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
|
local success = true
|
||||||
print("========================================")
|
print("========================================")
|
||||||
print("Running ALL tests")
|
print("Running ALL tests")
|
||||||
@@ -45,6 +47,5 @@ print("========================================")
|
|||||||
print("All tests completed")
|
print("All tests completed")
|
||||||
print("========================================")
|
print("========================================")
|
||||||
|
|
||||||
-- Run the tests and exit with appropriate code
|
|
||||||
local result = luaunit.LuaUnit.run()
|
local result = luaunit.LuaUnit.run()
|
||||||
os.exit(success and result or 1)
|
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
|
## Tips
|
||||||
|
|
||||||
1. **Start Simple**: Begin with one component (button) before creating a full theme
|
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
|
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
|
5. **Atlas Packing**: Use tools like TexturePacker or Aseprite to create efficient atlases
|
||||||
6. **Transparency**: Use semi-transparent backgroundColor to tint themed elements
|
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
|
## 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
|
-- 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 = {}
|
local Color = {}
|
||||||
Color.__index = Color
|
Color.__index = Color
|
||||||
|
|
||||||
@@ -21,183 +18,43 @@ return {
|
|||||||
components = {
|
components = {
|
||||||
card = {
|
card = {
|
||||||
atlas = "themes/space/card.png",
|
atlas = "themes/space/card.png",
|
||||||
regions = {
|
insets = { left = 66, top = 66, right = 66, bottom = 66 },
|
||||||
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" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
cardv2 = {
|
cardv2 = {
|
||||||
atlas = "themes/space/card-v2.png",
|
atlas = "themes/space/card-v2.png",
|
||||||
regions = {
|
insets = { left = 66, top = 66, right = 66, bottom = 66 },
|
||||||
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" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
cardv3 = {
|
cardv3 = {
|
||||||
atlas = "themes/space/card-v3.png",
|
atlas = "themes/space/card-v3.png",
|
||||||
regions = {
|
insets = { left = 286, top = 100, right = 286, bottom = 100 },
|
||||||
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" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
panel = {
|
panel = {
|
||||||
atlas = "themes/space/panel.png",
|
atlas = "themes/space/panel.png",
|
||||||
regions = {
|
insets = { left = 38, top = 30, right = 22, bottom = 30 },
|
||||||
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" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
panelred = {
|
panelred = {
|
||||||
atlas = "themes/space/panel-red.png",
|
atlas = "themes/space/panel-red.png",
|
||||||
regions = {
|
insets = { left = 38, top = 30, right = 22, bottom = 30 },
|
||||||
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" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
panelgreen = {
|
panelgreen = {
|
||||||
atlas = "themes/space/panel-green.png",
|
atlas = "themes/space/panel-green.png",
|
||||||
regions = {
|
insets = { left = 38, top = 30, right = 22, bottom = 30 },
|
||||||
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" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
button = {
|
button = {
|
||||||
atlas = "themes/space/button.png",
|
atlas = "themes/space/button.png",
|
||||||
regions = {
|
insets = { left = 14, top = 14, right = 14, bottom = 14 },
|
||||||
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" },
|
|
||||||
},
|
|
||||||
states = {
|
states = {
|
||||||
hover = {
|
hover = {
|
||||||
atlas = "themes/space/button-hover.png",
|
atlas = "themes/space/button-hover.png",
|
||||||
regions = {
|
insets = { left = 14, top = 14, right = 14, bottom = 14 },
|
||||||
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" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
pressed = {
|
pressed = {
|
||||||
atlas = "themes/space/button-pressed.png",
|
atlas = "themes/space/button-pressed.png",
|
||||||
regions = {
|
insets = { left = 14, top = 14, right = 14, bottom = 14 },
|
||||||
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" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
disabled = {
|
disabled = {
|
||||||
atlas = "themes/space/button-disabled.png",
|
atlas = "themes/space/button-disabled.png",
|
||||||
regions = {
|
insets = { left = 14, top = 14, right = 14, bottom = 14 },
|
||||||
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" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user