begin major Element refactor
This commit is contained in:
@@ -24,7 +24,6 @@ local LayoutEngine = req("LayoutEngine")
|
|||||||
local Renderer = req("Renderer")
|
local Renderer = req("Renderer")
|
||||||
local EventHandler = req("EventHandler")
|
local EventHandler = req("EventHandler")
|
||||||
local ScrollManager = req("ScrollManager")
|
local ScrollManager = req("ScrollManager")
|
||||||
local ImageDataReader = req("ImageDataReader")
|
|
||||||
---@type ErrorHandler
|
---@type ErrorHandler
|
||||||
local ErrorHandler = req("ErrorHandler")
|
local ErrorHandler = req("ErrorHandler")
|
||||||
---@type Element
|
---@type Element
|
||||||
@@ -123,12 +122,12 @@ function flexlove.init(config)
|
|||||||
ImageScaler.init({ ErrorHandler = flexlove._ErrorHandler })
|
ImageScaler.init({ ErrorHandler = flexlove._ErrorHandler })
|
||||||
|
|
||||||
NinePatch.init({ ErrorHandler = flexlove._ErrorHandler })
|
NinePatch.init({ ErrorHandler = flexlove._ErrorHandler })
|
||||||
ImageDataReader.init({ ErrorHandler = flexlove._ErrorHandler })
|
|
||||||
|
|
||||||
Units.init({ Context = Context, ErrorHandler = flexlove._ErrorHandler })
|
Units.init({ Context = Context, ErrorHandler = flexlove._ErrorHandler })
|
||||||
Color.init({ ErrorHandler = flexlove._ErrorHandler })
|
Color.init({ ErrorHandler = flexlove._ErrorHandler })
|
||||||
utils.init({ ErrorHandler = flexlove._ErrorHandler })
|
utils.init({ ErrorHandler = flexlove._ErrorHandler })
|
||||||
Animation.init({ ErrorHandler = flexlove._ErrorHandler, Color = Color })
|
Animation.init({ ErrorHandler = flexlove._ErrorHandler, Color = Color })
|
||||||
|
Theme.init({ ErrorHandler = flexlove._ErrorHandler, Color = Color, utils = utils })
|
||||||
|
|
||||||
flexlove._defaultDependencies = {
|
flexlove._defaultDependencies = {
|
||||||
Context = Context,
|
Context = Context,
|
||||||
@@ -152,6 +151,7 @@ function flexlove.init(config)
|
|||||||
EventHandler = EventHandler,
|
EventHandler = EventHandler,
|
||||||
ScrollManager = ScrollManager,
|
ScrollManager = ScrollManager,
|
||||||
ErrorHandler = flexlove._ErrorHandler,
|
ErrorHandler = flexlove._ErrorHandler,
|
||||||
|
Performance = flexlove._Performance,
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.baseScale then
|
if config.baseScale then
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
---@class Color
|
---@class Color
|
||||||
---@field r number -- Red component (0-1)
|
---@field r number Red component (0-1)
|
||||||
---@field g number -- Green component (0-1)
|
---@field g number Green component (0-1)
|
||||||
---@field b number -- Blue component (0-1)
|
---@field b number Blue component (0-1)
|
||||||
---@field a number -- Alpha component (0-1)
|
---@field a number Alpha component (0-1)
|
||||||
|
---@field _ErrorHandler table? ErrorHandler module dependency
|
||||||
local Color = {}
|
local Color = {}
|
||||||
Color.__index = Color
|
Color.__index = Color
|
||||||
|
|
||||||
|
--- Initialize module with shared dependencies
|
||||||
|
---@param deps table Dependencies {ErrorHandler}
|
||||||
|
function Color.init(deps)
|
||||||
|
if type(deps) == "table" then
|
||||||
|
Color._ErrorHandler = deps.ErrorHandler
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
--- Build type-safe color objects with automatic validation and clamping
|
--- Build type-safe color objects with automatic validation and clamping
|
||||||
--- Use this to avoid invalid color values and ensure consistent LÖVE-compatible colors (0-1 range)
|
--- Use this to avoid invalid color values and ensure consistent LÖVE-compatible colors (0-1 range)
|
||||||
---@param r number? Red component (0-1), defaults to 0
|
---@param r number? Red component (0-1), defaults to 0
|
||||||
@@ -46,7 +55,7 @@ end
|
|||||||
function Color.fromHex(hexWithTag)
|
function Color.fromHex(hexWithTag)
|
||||||
-- Validate input type
|
-- Validate input type
|
||||||
if type(hexWithTag) ~= "string" then
|
if type(hexWithTag) ~= "string" then
|
||||||
Color._ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
|
Color._ErrorHandler:warn("Color", "VAL_004", "Invalid color format", {
|
||||||
input = tostring(hexWithTag),
|
input = tostring(hexWithTag),
|
||||||
issue = "not a string",
|
issue = "not a string",
|
||||||
fallback = "white (#FFFFFF)",
|
fallback = "white (#FFFFFF)",
|
||||||
@@ -60,7 +69,7 @@ function Color.fromHex(hexWithTag)
|
|||||||
local g = tonumber("0x" .. hex:sub(3, 4))
|
local g = tonumber("0x" .. hex:sub(3, 4))
|
||||||
local b = tonumber("0x" .. hex:sub(5, 6))
|
local b = tonumber("0x" .. hex:sub(5, 6))
|
||||||
if not r or not g or not b then
|
if not r or not g or not b then
|
||||||
Color._ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
|
Color._ErrorHandler:warn("Color", "VAL_004", "Invalid color format", {
|
||||||
input = hexWithTag,
|
input = hexWithTag,
|
||||||
issue = "invalid hex digits",
|
issue = "invalid hex digits",
|
||||||
fallback = "white (#FFFFFF)",
|
fallback = "white (#FFFFFF)",
|
||||||
@@ -74,7 +83,7 @@ function Color.fromHex(hexWithTag)
|
|||||||
local b = tonumber("0x" .. hex:sub(5, 6))
|
local b = tonumber("0x" .. hex:sub(5, 6))
|
||||||
local a = tonumber("0x" .. hex:sub(7, 8))
|
local a = tonumber("0x" .. hex:sub(7, 8))
|
||||||
if not r or not g or not b or not a then
|
if not r or not g or not b or not a then
|
||||||
Color._ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
|
Color._ErrorHandler:warn("Color", "VAL_004", "Invalid color format", {
|
||||||
input = hexWithTag,
|
input = hexWithTag,
|
||||||
issue = "invalid hex digits",
|
issue = "invalid hex digits",
|
||||||
fallback = "white (#FFFFFFFF)",
|
fallback = "white (#FFFFFFFF)",
|
||||||
@@ -83,7 +92,7 @@ function Color.fromHex(hexWithTag)
|
|||||||
end
|
end
|
||||||
return Color.new(r / 255, g / 255, b / 255, a / 255)
|
return Color.new(r / 255, g / 255, b / 255, a / 255)
|
||||||
else
|
else
|
||||||
Color._ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
|
Color._ErrorHandler:warn("Color", "VAL_004", "Invalid color format", {
|
||||||
input = hexWithTag,
|
input = hexWithTag,
|
||||||
expected = "#RRGGBB or #RRGGBBAA",
|
expected = "#RRGGBB or #RRGGBBAA",
|
||||||
hexLength = #hex,
|
hexLength = #hex,
|
||||||
@@ -337,10 +346,4 @@ function Color.lerp(colorA, colorB, t)
|
|||||||
return Color.new(r, g, b, a)
|
return Color.new(r, g, b, a)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Initialize dependencies
|
|
||||||
---@param deps table Dependencies: { ErrorHandler = ErrorHandler }
|
|
||||||
function Color.init(deps)
|
|
||||||
Color._ErrorHandler = deps.ErrorHandler
|
|
||||||
end
|
|
||||||
|
|
||||||
return Color
|
return Color
|
||||||
|
|||||||
@@ -157,11 +157,6 @@ Element.__index = Element
|
|||||||
---@param deps table Required dependency table (provided by FlexLove)
|
---@param deps table Required dependency table (provided by FlexLove)
|
||||||
---@return Element
|
---@return Element
|
||||||
function Element.new(props, deps)
|
function Element.new(props, deps)
|
||||||
if not deps then
|
|
||||||
-- Can't use ErrorHandler yet since deps contains it
|
|
||||||
error("[FlexLove - Element] Error: deps parameter is required. Pass Element.defaultDependencies from FlexLove.")
|
|
||||||
end
|
|
||||||
|
|
||||||
local self = setmetatable({}, Element)
|
local self = setmetatable({}, Element)
|
||||||
self._deps = deps
|
self._deps = deps
|
||||||
|
|
||||||
@@ -2855,18 +2850,16 @@ function Element:countElements()
|
|||||||
return count
|
return count
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Check and warn about performance issues in element hierarchy
|
|
||||||
function Element:_checkPerformanceWarnings()
|
function Element:_checkPerformanceWarnings()
|
||||||
-- Check if performance warnings are enabled
|
local Performance = self._deps and self._deps.Performance
|
||||||
local Performance = self._deps and (package.loaded["modules.Performance"] or package.loaded["libs.modules.Performance"])
|
if not Performance or not Performance.warningsEnabled then
|
||||||
if not Performance or not Performance.areWarningsEnabled() then
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check hierarchy depth
|
-- Check hierarchy depth
|
||||||
local depth = self:getHierarchyDepth()
|
local depth = self:getHierarchyDepth()
|
||||||
if depth >= 15 then
|
if depth >= 15 then
|
||||||
Performance.logWarning(
|
Performance:logWarning(
|
||||||
string.format("hierarchy_depth_%s", self.id),
|
string.format("hierarchy_depth_%s", self.id),
|
||||||
"Element",
|
"Element",
|
||||||
string.format("Element hierarchy depth is %d levels for element '%s'", depth, self.id or "unnamed"),
|
string.format("Element hierarchy depth is %d levels for element '%s'", depth, self.id or "unnamed"),
|
||||||
@@ -2879,7 +2872,7 @@ function Element:_checkPerformanceWarnings()
|
|||||||
if not self.parent then
|
if not self.parent then
|
||||||
local totalElements = self:countElements()
|
local totalElements = self:countElements()
|
||||||
if totalElements >= 1000 then
|
if totalElements >= 1000 then
|
||||||
Performance.logWarning(
|
Performance:logWarning(
|
||||||
"element_count_high",
|
"element_count_high",
|
||||||
"Element",
|
"Element",
|
||||||
string.format("UI contains %d+ elements", totalElements),
|
string.format("UI contains %d+ elements", totalElements),
|
||||||
@@ -2902,14 +2895,15 @@ end
|
|||||||
|
|
||||||
--- Track active animations and warn if too many
|
--- Track active animations and warn if too many
|
||||||
function Element:_trackActiveAnimations()
|
function Element:_trackActiveAnimations()
|
||||||
local Performance = self._deps and (package.loaded["modules.Performance"] or package.loaded["libs.modules.Performance"])
|
-- Get Performance instance from deps if available
|
||||||
if not Performance or not Performance.areWarningsEnabled() then
|
local Performance = self._deps and self._deps.Performance
|
||||||
|
if not Performance or not Performance.warningsEnabled then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local animCount = self:_countActiveAnimations()
|
local animCount = self:_countActiveAnimations()
|
||||||
if animCount >= 50 then
|
if animCount >= 50 then
|
||||||
Performance.logWarning(
|
Performance:logWarning(
|
||||||
"animation_count_high",
|
"animation_count_high",
|
||||||
"Element",
|
"Element",
|
||||||
string.format("%d+ animations running simultaneously", animCount),
|
string.format("%d+ animations running simultaneously", animCount),
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
local ImageDataReader = {}
|
|
||||||
|
|
||||||
-- ErrorHandler will be injected via init
|
|
||||||
local ErrorHandler = nil
|
|
||||||
|
|
||||||
--- Initialize ImageDataReader with dependencies
|
|
||||||
---@param deps table Dependencies table with ErrorHandler
|
|
||||||
function ImageDataReader.init(deps)
|
|
||||||
if deps and deps.ErrorHandler then
|
|
||||||
ErrorHandler = deps.ErrorHandler
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param imagePath string
|
|
||||||
---@return love.ImageData
|
|
||||||
function ImageDataReader.loadImageData(imagePath)
|
|
||||||
if not imagePath then
|
|
||||||
ErrorHandler.error("ImageDataReader", "VAL_001", "Image path cannot be nil")
|
|
||||||
end
|
|
||||||
|
|
||||||
local success, result = pcall(function()
|
|
||||||
return love.image.newImageData(imagePath)
|
|
||||||
end)
|
|
||||||
|
|
||||||
if not success then
|
|
||||||
ErrorHandler.error("ImageDataReader", "RES_001", "Failed to load image data from '" .. imagePath .. "': " .. tostring(result), {
|
|
||||||
imagePath = imagePath,
|
|
||||||
error = 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
|
|
||||||
ErrorHandler.error("ImageDataReader", "VAL_001", "ImageData cannot be nil")
|
|
||||||
end
|
|
||||||
|
|
||||||
local width = imageData:getWidth()
|
|
||||||
local height = imageData:getHeight()
|
|
||||||
|
|
||||||
if rowIndex < 0 or rowIndex >= height then
|
|
||||||
ErrorHandler.error("ImageDataReader", "VAL_002", string.format("Row index %d out of bounds (height: %d)", rowIndex, height), {
|
|
||||||
rowIndex = rowIndex,
|
|
||||||
height = 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
|
|
||||||
ErrorHandler.error("ImageDataReader", "VAL_001", "ImageData cannot be nil")
|
|
||||||
end
|
|
||||||
|
|
||||||
local width = imageData:getWidth()
|
|
||||||
local height = imageData:getHeight()
|
|
||||||
|
|
||||||
if colIndex < 0 or colIndex >= width then
|
|
||||||
ErrorHandler.error("ImageDataReader", "VAL_002", string.format("Column index %d out of bounds (width: %d)", colIndex, width), {
|
|
||||||
colIndex = colIndex,
|
|
||||||
width = 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
|
|
||||||
|
|
||||||
return ImageDataReader
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
--- Standardized error message formatter
|
|
||||||
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
|
||||||
---@param message string -- Error message
|
|
||||||
---@return string -- Formatted error message
|
|
||||||
local function formatError(module, message)
|
|
||||||
return string.format("[FlexLove.%s] %s", module, message)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- Dependencies
|
|
||||||
-- ====================
|
|
||||||
|
|
||||||
local ImageDataReader = require((...):match("(.-)[^%.]+$") .. "ImageDataReader")
|
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- 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 stretch insets from stretch regions (top/left guides)
|
|
||||||
-- 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]
|
|
||||||
|
|
||||||
-- Stretch insets define the 9-patch regions
|
|
||||||
local stretchLeft = firstStretchX.start
|
|
||||||
local stretchRight = #topStretchPixels - lastStretchX["end"]
|
|
||||||
local stretchTop = firstStretchY.start
|
|
||||||
local stretchBottom = #leftStretchPixels - lastStretchY["end"]
|
|
||||||
|
|
||||||
-- Calculate content padding from content guides (bottom/right guides)
|
|
||||||
-- 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 = stretchLeft
|
|
||||||
contentRight = stretchRight
|
|
||||||
end
|
|
||||||
|
|
||||||
if #contentY > 0 then
|
|
||||||
contentTop = contentY[1].start
|
|
||||||
contentBottom = #leftStretchPixels - contentY[#contentY]["end"]
|
|
||||||
else
|
|
||||||
contentTop = stretchTop
|
|
||||||
contentBottom = stretchBottom
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
insets = {
|
|
||||||
left = stretchLeft,
|
|
||||||
top = stretchTop,
|
|
||||||
right = stretchRight,
|
|
||||||
bottom = stretchBottom,
|
|
||||||
},
|
|
||||||
contentPadding = {
|
|
||||||
left = contentLeft,
|
|
||||||
top = contentTop,
|
|
||||||
right = contentRight,
|
|
||||||
bottom = contentBottom,
|
|
||||||
},
|
|
||||||
stretchX = stretchX,
|
|
||||||
stretchY = stretchY,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
return NinePatchParser
|
|
||||||
@@ -17,21 +17,26 @@
|
|||||||
---@field backdropBlur {intensity:number, quality:number}?
|
---@field backdropBlur {intensity:number, quality:number}?
|
||||||
---@field _blurInstance table?
|
---@field _blurInstance table?
|
||||||
---@field _element Element?
|
---@field _element Element?
|
||||||
---@field _Color table
|
---@field _Color Color
|
||||||
---@field _RoundedRect table
|
---@field _RoundedRect table
|
||||||
---@field _NinePatch table
|
---@field _NinePatch table
|
||||||
---@field _ImageRenderer table
|
---@field _ImageRenderer table
|
||||||
---@field _ImageCache table
|
---@field _ImageCache table
|
||||||
---@field _Theme table
|
---@field _Theme table
|
||||||
---@field _Blur table
|
---@field _Transform Transform
|
||||||
|
---@field _Blur Blur
|
||||||
---@field _utils table
|
---@field _utils table
|
||||||
---@field _FONT_CACHE table
|
---@field _FONT_CACHE table
|
||||||
---@field _TextAlign table
|
---@field _TextAlign table
|
||||||
|
---@field _ErrorHandler ErrorHandler
|
||||||
local Renderer = {}
|
local Renderer = {}
|
||||||
Renderer.__index = Renderer
|
Renderer.__index = Renderer
|
||||||
|
|
||||||
-- Lazy-loaded ErrorHandler
|
--- Initialize module with shared dependencies
|
||||||
local ErrorHandler
|
---@param deps table Dependencies {ErrorHandler}
|
||||||
|
function Renderer.init(deps)
|
||||||
|
Renderer._ErrorHandler = deps.ErrorHandler
|
||||||
|
end
|
||||||
|
|
||||||
--- Create a new Renderer instance
|
--- Create a new Renderer instance
|
||||||
---@param config table Configuration table with rendering properties
|
---@param config table Configuration table with rendering properties
|
||||||
@@ -198,18 +203,9 @@ function Renderer:_drawImage(x, y, paddingLeft, paddingTop, contentWidth, conten
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
if not success then
|
if not success then
|
||||||
-- Lazy-load ErrorHandler if needed
|
|
||||||
if not ErrorHandler then
|
|
||||||
ErrorHandler = require("modules.ErrorHandler")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Check if it's a stencil buffer error
|
-- Check if it's a stencil buffer error
|
||||||
if err and err:match("stencil") then
|
if err and err:match("stencil") then
|
||||||
ErrorHandler.warn(
|
Renderer._ErrorHandler:warn("Renderer", "IMG_001", "Cannot apply corner radius to image: stencil buffer not available", {
|
||||||
"Renderer",
|
|
||||||
"IMG_001",
|
|
||||||
"Cannot apply corner radius to image: stencil buffer not available",
|
|
||||||
{
|
|
||||||
imagePath = self.imagePath or "unknown",
|
imagePath = self.imagePath or "unknown",
|
||||||
cornerRadius = string.format(
|
cornerRadius = string.format(
|
||||||
"TL:%d TR:%d BL:%d BR:%d",
|
"TL:%d TR:%d BL:%d BR:%d",
|
||||||
@@ -219,9 +215,7 @@ function Renderer:_drawImage(x, y, paddingLeft, paddingTop, contentWidth, conten
|
|||||||
self.cornerRadius.bottomRight
|
self.cornerRadius.bottomRight
|
||||||
),
|
),
|
||||||
error = tostring(err),
|
error = tostring(err),
|
||||||
},
|
}, "Ensure the active canvas has stencil=true enabled, or remove cornerRadius from images")
|
||||||
"Ensure the active canvas has stencil=true enabled, or remove cornerRadius from images"
|
|
||||||
)
|
|
||||||
-- Continue without corner radius
|
-- Continue without corner radius
|
||||||
hasCornerRadius = false
|
hasCornerRadius = false
|
||||||
else
|
else
|
||||||
@@ -368,12 +362,10 @@ function Renderer:draw(backdropCanvas)
|
|||||||
|
|
||||||
-- Element must be initialized before drawing
|
-- Element must be initialized before drawing
|
||||||
if not self._element then
|
if not self._element then
|
||||||
if not ErrorHandler then
|
Renderer._ErrorHandler:warn("Renderer", "SYS_002", "Method called before initialization", {
|
||||||
ErrorHandler = require("modules.ErrorHandler")
|
method = "draw",
|
||||||
end
|
|
||||||
ErrorHandler.error("Renderer", "SYS_002", "Method called before initialization", {
|
|
||||||
method = "draw"
|
|
||||||
}, "Call renderer:initialize(element) before rendering")
|
}, "Call renderer:initialize(element) before rendering")
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local element = self._element
|
local element = self._element
|
||||||
|
|||||||
@@ -1,13 +1,3 @@
|
|||||||
local modulePath = (...):match("(.-)[^%.]+$")
|
|
||||||
local function req(name)
|
|
||||||
return require(modulePath .. name)
|
|
||||||
end
|
|
||||||
|
|
||||||
local NinePatchParser = req("NinePatchParser")
|
|
||||||
local Color = req("Color")
|
|
||||||
local utils = req("utils")
|
|
||||||
local ErrorHandler = req("ErrorHandler")
|
|
||||||
|
|
||||||
--- Auto-detect the base path where FlexLove is located
|
--- Auto-detect the base path where FlexLove is located
|
||||||
---@return string modulePath, string filesystemPath
|
---@return string modulePath, string filesystemPath
|
||||||
local function getFlexLoveBasePath()
|
local function getFlexLoveBasePath()
|
||||||
@@ -77,6 +67,242 @@ local function validateThemeDefinition(definition)
|
|||||||
return true, nil
|
return true, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Load image data from a file path
|
||||||
|
---@param imagePath string
|
||||||
|
---@return love.ImageData
|
||||||
|
local function loadImageData(imagePath)
|
||||||
|
if not imagePath then
|
||||||
|
error("Image path cannot be nil")
|
||||||
|
end
|
||||||
|
|
||||||
|
local success, result = pcall(function()
|
||||||
|
return love.image.newImageData(imagePath)
|
||||||
|
end)
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
error("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)
|
||||||
|
local function getRow(imageData, rowIndex)
|
||||||
|
if not imageData then
|
||||||
|
error("ImageData cannot be nil")
|
||||||
|
end
|
||||||
|
|
||||||
|
local width = imageData:getWidth()
|
||||||
|
local height = imageData:getHeight()
|
||||||
|
|
||||||
|
if rowIndex < 0 or rowIndex >= height then
|
||||||
|
error(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)
|
||||||
|
local function getColumn(imageData, colIndex)
|
||||||
|
if not imageData then
|
||||||
|
error("ImageData cannot be nil")
|
||||||
|
end
|
||||||
|
|
||||||
|
local width = imageData:getWidth()
|
||||||
|
local height = imageData:getHeight()
|
||||||
|
|
||||||
|
if colIndex < 0 or colIndex >= width then
|
||||||
|
error(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
|
||||||
|
local function isBlackPixel(r, g, b, a)
|
||||||
|
return r == 0 and g == 0 and b == 0 and a == 255
|
||||||
|
end
|
||||||
|
|
||||||
|
--- 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 = 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
|
||||||
|
local function parseNinePatch(imagePath)
|
||||||
|
if not imagePath then
|
||||||
|
return nil, "Image path cannot be nil"
|
||||||
|
end
|
||||||
|
|
||||||
|
local success, imageData = pcall(function()
|
||||||
|
return 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 = getRow(imageData, 0)
|
||||||
|
local leftBorder = getColumn(imageData, 0)
|
||||||
|
local bottomBorder = getRow(imageData, height - 1)
|
||||||
|
local rightBorder = 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 stretch insets from stretch regions (top/left guides)
|
||||||
|
-- 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]
|
||||||
|
|
||||||
|
-- Stretch insets define the 9-patch regions
|
||||||
|
local stretchLeft = firstStretchX.start
|
||||||
|
local stretchRight = #topStretchPixels - lastStretchX["end"]
|
||||||
|
local stretchTop = firstStretchY.start
|
||||||
|
local stretchBottom = #leftStretchPixels - lastStretchY["end"]
|
||||||
|
|
||||||
|
-- Calculate content padding from content guides (bottom/right guides)
|
||||||
|
-- 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 = stretchLeft
|
||||||
|
contentRight = stretchRight
|
||||||
|
end
|
||||||
|
|
||||||
|
if #contentY > 0 then
|
||||||
|
contentTop = contentY[1].start
|
||||||
|
contentBottom = #leftStretchPixels - contentY[#contentY]["end"]
|
||||||
|
else
|
||||||
|
contentTop = stretchTop
|
||||||
|
contentBottom = stretchBottom
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
insets = {
|
||||||
|
left = stretchLeft,
|
||||||
|
top = stretchTop,
|
||||||
|
right = stretchRight,
|
||||||
|
bottom = stretchBottom,
|
||||||
|
},
|
||||||
|
contentPadding = {
|
||||||
|
left = contentLeft,
|
||||||
|
top = contentTop,
|
||||||
|
right = contentRight,
|
||||||
|
bottom = contentBottom,
|
||||||
|
},
|
||||||
|
stretchX = stretchX,
|
||||||
|
stretchY = stretchY,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
---@class ThemeRegion
|
---@class ThemeRegion
|
||||||
---@field x number -- X position in atlas
|
---@field x number -- X position in atlas
|
||||||
---@field y number -- Y position in atlas
|
---@field y number -- Y position in atlas
|
||||||
@@ -117,9 +343,22 @@ end
|
|||||||
---@field colors table<string, Color>
|
---@field colors table<string, Color>
|
||||||
---@field fonts table<string, string> -- Font family definitions
|
---@field fonts table<string, string> -- Font family definitions
|
||||||
---@field contentAutoSizingMultiplier {width:number?, height:number?}? -- Optional: default multiplier for auto-sized content dimensions
|
---@field contentAutoSizingMultiplier {width:number?, height:number?}? -- Optional: default multiplier for auto-sized content dimensions
|
||||||
|
---@field _ErrorHandler table? ErrorHandler module dependency
|
||||||
|
---@field _Color table? Color module dependency
|
||||||
|
---@field _utils table? utils module dependency
|
||||||
local Theme = {}
|
local Theme = {}
|
||||||
Theme.__index = Theme
|
Theme.__index = Theme
|
||||||
|
|
||||||
|
--- Initialize module with shared dependencies
|
||||||
|
---@param deps table Dependencies {ErrorHandler, Color, utils}
|
||||||
|
function Theme.init(deps)
|
||||||
|
if type(deps) == "table" then
|
||||||
|
Theme._ErrorHandler = deps.ErrorHandler
|
||||||
|
Theme._Color = deps.Color
|
||||||
|
Theme._utils = deps.utils
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Global theme registry
|
-- Global theme registry
|
||||||
local themes = {}
|
local themes = {}
|
||||||
local activeTheme = nil
|
local activeTheme = nil
|
||||||
@@ -131,17 +370,19 @@ local activeTheme = nil
|
|||||||
function Theme.new(definition)
|
function Theme.new(definition)
|
||||||
-- Validate input type first
|
-- Validate input type first
|
||||||
if type(definition) ~= "table" then
|
if type(definition) ~= "table" then
|
||||||
ErrorHandler.error("Theme", "THM_001", "Invalid theme definition", {
|
Theme._ErrorHandler:warn("Theme", "THM_001", "Invalid theme definition", {
|
||||||
error = "Theme definition must be a table, got " .. type(definition)
|
error = "Theme definition must be a table, got " .. type(definition),
|
||||||
})
|
})
|
||||||
|
return Theme.new({ name = "fallback", components = {}, colors = {}, fonts = {} })
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Validate theme definition
|
-- Validate theme definition
|
||||||
local valid, err = validateThemeDefinition(definition)
|
local valid, err = validateThemeDefinition(definition)
|
||||||
if not valid then
|
if not valid then
|
||||||
ErrorHandler.error("Theme", "THM_001", "Invalid theme definition", {
|
Theme._ErrorHandler:warn("Theme", "THM_001", "Invalid theme definition", {
|
||||||
error = tostring(err)
|
error = tostring(err),
|
||||||
})
|
})
|
||||||
|
return Theme.new({ name = "fallback", components = {}, colors = {}, fonts = {} })
|
||||||
end
|
end
|
||||||
|
|
||||||
local self = setmetatable({}, Theme)
|
local self = setmetatable({}, Theme)
|
||||||
@@ -150,16 +391,16 @@ function Theme.new(definition)
|
|||||||
-- Load global atlas if it's a string path
|
-- Load global atlas if it's a string path
|
||||||
if definition.atlas then
|
if definition.atlas then
|
||||||
if type(definition.atlas) == "string" then
|
if type(definition.atlas) == "string" then
|
||||||
local resolvedPath = utils.resolveImagePath(definition.atlas)
|
local resolvedPath = Theme._utils.resolveImagePath(definition.atlas)
|
||||||
local image, imageData, loaderr = utils.safeLoadImage(resolvedPath)
|
local image, imageData, loaderr = Theme._utils.safeLoadImage(resolvedPath)
|
||||||
if image then
|
if image then
|
||||||
self.atlas = image
|
self.atlas = image
|
||||||
self.atlasData = imageData
|
self.atlasData = imageData
|
||||||
else
|
else
|
||||||
ErrorHandler.warn("Theme", "RES_001", "Failed to load global atlas", {
|
Theme._ErrorHandler:warn("Theme", "RES_001", "Failed to load global atlas", {
|
||||||
theme = definition.name,
|
theme = definition.name,
|
||||||
path = resolvedPath,
|
path = resolvedPath,
|
||||||
error = loaderr
|
error = loaderr,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@@ -184,11 +425,12 @@ function Theme.new(definition)
|
|||||||
local contentHeight = srcHeight - 2
|
local contentHeight = srcHeight - 2
|
||||||
|
|
||||||
if contentWidth <= 0 or contentHeight <= 0 then
|
if contentWidth <= 0 or contentHeight <= 0 then
|
||||||
ErrorHandler.error("Theme", "RES_002", "Nine-patch image too small", {
|
Theme._ErrorHandler:warn("Theme", "RES_002", "Nine-patch image too small", {
|
||||||
width = srcWidth,
|
width = srcWidth,
|
||||||
height = srcHeight,
|
height = srcHeight,
|
||||||
reason = "Image must be larger than 2x2 pixels to have content after stripping 1px border"
|
reason = "Image must be larger than 2x2 pixels to have content after stripping 1px border",
|
||||||
})
|
})
|
||||||
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Create new ImageData for content only
|
-- Create new ImageData for content only
|
||||||
@@ -208,25 +450,25 @@ function Theme.new(definition)
|
|||||||
-- Helper function to load atlas with 9-patch support
|
-- Helper function to load atlas with 9-patch support
|
||||||
local function loadAtlasWithNinePatch(comp, atlasPath, errorContext)
|
local function loadAtlasWithNinePatch(comp, atlasPath, errorContext)
|
||||||
---@diagnostic disable-next-line
|
---@diagnostic disable-next-line
|
||||||
local resolvedPath = utils.resolveImagePath(atlasPath)
|
local resolvedPath = Theme._utils.resolveImagePath(atlasPath)
|
||||||
---@diagnostic disable-next-line
|
---@diagnostic disable-next-line
|
||||||
local is9Patch = not comp.insets and atlasPath:match("%.9%.png$")
|
local is9Patch = not comp.insets and atlasPath:match("%.9%.png$")
|
||||||
|
|
||||||
if is9Patch then
|
if is9Patch then
|
||||||
local parseResult, parseErr = NinePatchParser.parse(resolvedPath)
|
local parseResult, parseErr = parseNinePatch(resolvedPath)
|
||||||
if parseResult then
|
if parseResult then
|
||||||
comp.insets = parseResult.insets
|
comp.insets = parseResult.insets
|
||||||
comp._ninePatchData = parseResult
|
comp._ninePatchData = parseResult
|
||||||
else
|
else
|
||||||
ErrorHandler.warn("Theme", "RES_003", "Failed to parse nine-patch image", {
|
Theme._ErrorHandler:warn("Theme", "RES_003", "Failed to parse nine-patch image", {
|
||||||
context = errorContext,
|
context = errorContext,
|
||||||
path = resolvedPath,
|
path = resolvedPath,
|
||||||
error = tostring(parseErr)
|
error = tostring(parseErr),
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local image, imageData, loaderr = utils.safeLoadImage(resolvedPath)
|
local image, imageData, loaderr = Theme._utils.safeLoadImage(resolvedPath)
|
||||||
if image then
|
if image then
|
||||||
-- Strip guide border for 9-patch images
|
-- Strip guide border for 9-patch images
|
||||||
if is9Patch and imageData then
|
if is9Patch and imageData then
|
||||||
@@ -239,10 +481,10 @@ function Theme.new(definition)
|
|||||||
comp._loadedAtlasData = imageData
|
comp._loadedAtlasData = imageData
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
ErrorHandler.warn("Theme", "RES_001", "Failed to load atlas", {
|
Theme._ErrorHandler:warn("Theme", "RES_001", "Failed to load atlas", {
|
||||||
context = errorContext,
|
context = errorContext,
|
||||||
path = resolvedPath,
|
path = resolvedPath,
|
||||||
error = tostring(loaderr)
|
error = tostring(loaderr),
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -333,11 +575,11 @@ function Theme.load(path)
|
|||||||
if success then
|
if success then
|
||||||
definition = result
|
definition = result
|
||||||
else
|
else
|
||||||
ErrorHandler.warn("Theme", "RES_004", "Failed to load theme file", {
|
Theme._ErrorHandler:warn("Theme", "RES_004", "Failed to load theme file", {
|
||||||
theme = path,
|
theme = path,
|
||||||
tried = themePath,
|
tried = themePath,
|
||||||
error = tostring(result),
|
error = tostring(result),
|
||||||
fallback = "nil (no theme loaded)"
|
fallback = "nil (no theme loaded)",
|
||||||
}, "Check that the theme file exists in the themes/ directory or provide a valid module path")
|
}, "Check that the theme file exists in the themes/ directory or provide a valid module path")
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
@@ -365,10 +607,10 @@ function Theme.setActive(themeOrName)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if not activeTheme then
|
if not activeTheme then
|
||||||
ErrorHandler.warn("Theme", "THM_002", "Failed to set active theme", {
|
Theme._ErrorHandler:warn("Theme", "THM_002", "Failed to set active theme", {
|
||||||
theme = tostring(themeOrName),
|
theme = tostring(themeOrName),
|
||||||
reason = "Theme not found or not loaded",
|
reason = "Theme not found or not loaded",
|
||||||
fallback = "current theme unchanged"
|
fallback = "current theme unchanged",
|
||||||
}, "Ensure the theme is loaded with Theme.load() before setting it active")
|
}, "Ensure the theme is loaded with Theme.load() before setting it active")
|
||||||
-- Keep current activeTheme unchanged (fallback behavior)
|
-- Keep current activeTheme unchanged (fallback behavior)
|
||||||
end
|
end
|
||||||
@@ -479,7 +721,7 @@ function Theme.getColorOrDefault(colorName, fallback)
|
|||||||
return color
|
return color
|
||||||
end
|
end
|
||||||
|
|
||||||
return fallback or Color.new(1, 1, 1, 1)
|
return fallback or Theme._Color.new(1, 1, 1, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Get a theme by name
|
--- Get a theme by name
|
||||||
@@ -776,7 +1018,7 @@ function Theme.validateTheme(theme, options)
|
|||||||
end
|
end
|
||||||
elseif colorType == "string" then
|
elseif colorType == "string" then
|
||||||
-- Validate color string
|
-- Validate color string
|
||||||
local isValid, err = Color.validateColor(colorValue)
|
local isValid, err = Theme._Color.validateColor(colorValue)
|
||||||
if not isValid then
|
if not isValid then
|
||||||
table.insert(errors, "Color '" .. colorName .. "': " .. err)
|
table.insert(errors, "Color '" .. colorName .. "': " .. err)
|
||||||
end
|
end
|
||||||
@@ -952,12 +1194,12 @@ function Theme.sanitizeTheme(theme)
|
|||||||
sanitized.colors[colorName] = colorValue
|
sanitized.colors[colorName] = colorValue
|
||||||
elseif colorType == "string" then
|
elseif colorType == "string" then
|
||||||
-- Try to validate color string
|
-- Try to validate color string
|
||||||
local isValid = Color.validateColor(colorValue)
|
local isValid = Theme._Color.validateColor(colorValue)
|
||||||
if isValid then
|
if isValid then
|
||||||
sanitized.colors[colorName] = colorValue
|
sanitized.colors[colorName] = colorValue
|
||||||
else
|
else
|
||||||
-- Provide fallback color
|
-- Provide fallback color
|
||||||
sanitized.colors[colorName] = Color.new(0, 0, 0, 1)
|
sanitized.colors[colorName] = Theme._Color.new(0, 0, 0, 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,93 +1,70 @@
|
|||||||
|
--- Utility module for parsing and resolving CSS-like units (px, %, vw, vh, ew, eh)
|
||||||
|
--- Provides unit parsing, validation, and conversion to pixel values
|
||||||
|
---@class Units
|
||||||
|
---@field _Context table? Context module dependency
|
||||||
|
---@field _ErrorHandler table? ErrorHandler module dependency
|
||||||
local Units = {}
|
local Units = {}
|
||||||
|
|
||||||
local Context = nil
|
--- Initialize Units module with dependencies
|
||||||
local ErrorHandler = nil
|
---@param deps table Dependencies: { Context = table?, ErrorHandler = table? }
|
||||||
|
|
||||||
--- Initialize Units module with Context dependency
|
|
||||||
---@param context table The Context module
|
|
||||||
--- Initialize dependencies
|
|
||||||
---@param deps table Dependencies: { Context = Context?, ErrorHandler = ErrorHandler? }
|
|
||||||
function Units.init(deps)
|
function Units.init(deps)
|
||||||
if type(deps) == "table" then
|
Units._Context = deps.Context
|
||||||
if deps.Context then
|
Units._ErrorHandler = deps.ErrorHandler
|
||||||
Context = deps.Context
|
|
||||||
end
|
|
||||||
if deps.ErrorHandler then
|
|
||||||
ErrorHandler = deps.ErrorHandler
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param value string|number
|
--- Parse a unit value into numeric value and unit type
|
||||||
---@return number, string -- Returns numeric value and unit type ("px", "%", "vw", "vh")
|
--- Supports: px (pixels), % (percentage), vw/vh (viewport), ew/eh (element)
|
||||||
|
---@param value string|number The value to parse (e.g., "50px", "10%", "2vw", 100)
|
||||||
|
---@return number numericValue The numeric portion of the value
|
||||||
|
---@return string unitType The unit type ("px", "%", "vw", "vh", "ew", "eh")
|
||||||
function Units.parse(value)
|
function Units.parse(value)
|
||||||
if type(value) == "number" then
|
if type(value) == "number" then
|
||||||
return value, "px"
|
return value, "px"
|
||||||
end
|
end
|
||||||
|
|
||||||
if type(value) ~= "string" then
|
if type(value) ~= "string" then
|
||||||
if ErrorHandler then
|
Units._ErrorHandler:warn("Units", "VAL_001", "Invalid property type", {
|
||||||
ErrorHandler.warn("Units", "VAL_001", "Invalid property type", {
|
|
||||||
property = "unit value",
|
property = "unit value",
|
||||||
expected = "string or number",
|
expected = "string or number",
|
||||||
got = type(value)
|
got = type(value),
|
||||||
}, "Using fallback: 0px")
|
}, "Using fallback: 0px")
|
||||||
else
|
|
||||||
print(string.format("[FlexLove - Units] Warning: Invalid unit value type. Expected string or number, got %s. Using fallback: 0px", type(value)))
|
|
||||||
end
|
|
||||||
return 0, "px"
|
return 0, "px"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check for unit-only input (e.g., "px", "%", "vw" without a number)
|
-- Check for unit-only input (e.g., "px", "%", "vw" without a number)
|
||||||
local validUnits = { px = true, ["%"] = true, vw = true, vh = true, ew = true, eh = true }
|
local validUnits = { px = true, ["%"] = true, vw = true, vh = true, ew = true, eh = true }
|
||||||
if validUnits[value] then
|
if validUnits[value] then
|
||||||
if ErrorHandler then
|
Units._ErrorHandler:warn("Units", "VAL_005", "Invalid unit format", {
|
||||||
ErrorHandler.warn("Units", "VAL_005", "Invalid unit format", {
|
|
||||||
input = value,
|
input = value,
|
||||||
expected = "number + unit (e.g., '50" .. value .. "')"
|
expected = "number + unit (e.g., '50" .. value .. "')",
|
||||||
}, string.format("Add a numeric value before '%s', like '50%s'. Using fallback: 0px", value, value))
|
}, string.format("Add a numeric value before '%s', like '50%s'. Using fallback: 0px", value, value))
|
||||||
else
|
|
||||||
print(string.format("[FlexLove - Units] Warning: Missing numeric value before unit '%s'. Use format like '50%s'. Using fallback: 0px", value, value))
|
|
||||||
end
|
|
||||||
return 0, "px"
|
return 0, "px"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check for invalid format (space between number and unit)
|
-- Check for invalid format (space between number and unit)
|
||||||
if value:match("%d%s+%a") then
|
if value:match("%d%s+%a") then
|
||||||
if ErrorHandler then
|
Units._ErrorHandler:warn("Units", "VAL_005", "Invalid unit format", {
|
||||||
ErrorHandler.warn("Units", "VAL_005", "Invalid unit format", {
|
|
||||||
input = value,
|
input = value,
|
||||||
issue = "contains space between number and unit"
|
issue = "contains space between number and unit",
|
||||||
}, "Remove spaces: use '50px' not '50 px'. Using fallback: 0px")
|
}, "Remove spaces: use '50px' not '50 px'. Using fallback: 0px")
|
||||||
else
|
|
||||||
print(string.format("[FlexLove - Units] Warning: Invalid unit string '%s' (contains space). Use format like '50px' or '50%%'. Using fallback: 0px", value))
|
|
||||||
end
|
|
||||||
return 0, "px"
|
return 0, "px"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Match number followed by optional unit
|
-- Match number followed by optional unit
|
||||||
local numStr, unit = value:match("^([%-]?[%d%.]+)(.*)$")
|
local numStr, unit = value:match("^([%-]?[%d%.]+)(.*)$")
|
||||||
if not numStr then
|
if not numStr then
|
||||||
if ErrorHandler then
|
Units._ErrorHandler:warn("Units", "VAL_005", "Invalid unit format", {
|
||||||
ErrorHandler.warn("Units", "VAL_005", "Invalid unit format", {
|
input = value,
|
||||||
input = value
|
|
||||||
}, "Expected format: number + unit (e.g., '50px', '10%', '2vw'). Using fallback: 0px")
|
}, "Expected format: number + unit (e.g., '50px', '10%', '2vw'). Using fallback: 0px")
|
||||||
else
|
|
||||||
print(string.format("[FlexLove - Units] Warning: Invalid unit format '%s'. Expected format: number + unit (e.g., '50px', '10%%', '2vw'). Using fallback: 0px", value))
|
|
||||||
end
|
|
||||||
return 0, "px"
|
return 0, "px"
|
||||||
end
|
end
|
||||||
|
|
||||||
local num = tonumber(numStr)
|
local num = tonumber(numStr)
|
||||||
if not num then
|
if not num then
|
||||||
if ErrorHandler then
|
Units._ErrorHandler:warn("Units", "VAL_005", "Invalid unit format", {
|
||||||
ErrorHandler.warn("Units", "VAL_005", "Invalid unit format", {
|
|
||||||
input = value,
|
input = value,
|
||||||
issue = "numeric value cannot be parsed"
|
issue = "numeric value cannot be parsed",
|
||||||
}, "Using fallback: 0px")
|
}, "Using fallback: 0px")
|
||||||
else
|
|
||||||
print(string.format("[FlexLove - Units] Warning: Invalid numeric value in '%s'. Using fallback: 0px", value))
|
|
||||||
end
|
|
||||||
return 0, "px"
|
return 0, "px"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -98,42 +75,35 @@ function Units.parse(value)
|
|||||||
|
|
||||||
-- validUnits is already defined at the top of the function
|
-- validUnits is already defined at the top of the function
|
||||||
if not validUnits[unit] then
|
if not validUnits[unit] then
|
||||||
if ErrorHandler then
|
Units._ErrorHandler:warn("Units", "VAL_005", "Invalid unit format", {
|
||||||
ErrorHandler.warn("Units", "VAL_005", "Invalid unit format", {
|
|
||||||
input = value,
|
input = value,
|
||||||
unit = unit,
|
unit = unit,
|
||||||
validUnits = "px, %, vw, vh, ew, eh"
|
validUnits = "px, %, vw, vh, ew, eh",
|
||||||
}, string.format("Treating '%s' as pixels", value))
|
}, string.format("Treating '%s' as pixels", value))
|
||||||
else
|
|
||||||
print(string.format("[FlexLove - Units] Warning: Unknown unit '%s' in '%s'. Valid units: px, %%, vw, vh, ew, eh. Treating as pixels", unit, value))
|
|
||||||
end
|
|
||||||
return num, "px"
|
return num, "px"
|
||||||
end
|
end
|
||||||
|
|
||||||
return num, unit
|
return num, unit
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Convert relative units to pixels based on viewport and parent dimensions
|
--- Convert relative units to absolute pixel values
|
||||||
---@param value number -- Numeric value to convert
|
--- Resolves %, vw, vh units based on viewport and parent dimensions
|
||||||
---@param unit string -- Unit type ("px", "%", "vw", "vh", "ew", "eh")
|
---@param value number Numeric value to convert
|
||||||
---@param viewportWidth number -- Current viewport width in pixels
|
---@param unit string Unit type ("px", "%", "vw", "vh", "ew", "eh")
|
||||||
---@param viewportHeight number -- Current viewport height in pixels
|
---@param viewportWidth number Current viewport width in pixels
|
||||||
---@param parentSize number? -- Required for percentage units (parent dimension)
|
---@param viewportHeight number Current viewport height in pixels
|
||||||
---@return number -- Resolved pixel value
|
---@param parentSize number? Required for percentage units (parent dimension in pixels)
|
||||||
---@throws Error if unit type is unknown or percentage used without parent size
|
---@return number resolvedValue Resolved pixel value
|
||||||
function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
|
function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
|
||||||
if unit == "px" then
|
if unit == "px" then
|
||||||
return value
|
return value
|
||||||
elseif unit == "%" then
|
elseif unit == "%" then
|
||||||
if not parentSize then
|
if not parentSize then
|
||||||
if ErrorHandler then
|
Units._ErrorHandler:warn("Units", "LAY_003", "Invalid dimensions", {
|
||||||
ErrorHandler.error("Units", "LAY_003", "Invalid dimensions", {
|
|
||||||
unit = "%",
|
unit = "%",
|
||||||
issue = "parent dimension not available"
|
issue = "parent dimension not available",
|
||||||
}, "Percentage units require a parent element with explicit dimensions")
|
}, "Percentage units require a parent element with explicit dimensions. Using fallback: 0px")
|
||||||
else
|
return 0
|
||||||
error("Percentage units require parent dimension")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
return (value / 100) * parentSize
|
return (value / 100) * parentSize
|
||||||
elseif unit == "vw" then
|
elseif unit == "vw" then
|
||||||
@@ -141,22 +111,22 @@ function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
|
|||||||
elseif unit == "vh" then
|
elseif unit == "vh" then
|
||||||
return (value / 100) * viewportHeight
|
return (value / 100) * viewportHeight
|
||||||
else
|
else
|
||||||
if ErrorHandler then
|
Units._ErrorHandler:warn("Units", "VAL_005", "Invalid unit format", {
|
||||||
ErrorHandler.error("Units", "VAL_005", "Invalid unit format", {
|
|
||||||
unit = unit,
|
unit = unit,
|
||||||
validUnits = "px, %, vw, vh, ew, eh"
|
validUnits = "px, %, vw, vh, ew, eh",
|
||||||
})
|
}, string.format("Unknown unit type: '%s'. Using fallback: 0px", unit))
|
||||||
else
|
return 0
|
||||||
error(string.format("Unknown unit type: '%s'", unit))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return number, number -- width, height
|
--- Get current viewport dimensions
|
||||||
|
--- Uses cached viewport during resize operations, otherwise queries LÖVE graphics
|
||||||
|
---@return number width Viewport width in pixels
|
||||||
|
---@return number height Viewport height in pixels
|
||||||
function Units.getViewport()
|
function Units.getViewport()
|
||||||
-- Return cached viewport if available (only during resize operations)
|
-- Return cached viewport if available (only during resize operations)
|
||||||
if Context and Context._cachedViewport and Context._cachedViewport.width > 0 then
|
if Units._Context._cachedViewport and Units._Context._cachedViewport.width > 0 then
|
||||||
return Context._cachedViewport.width, Context._cachedViewport.height
|
return Units._Context._cachedViewport.width, Units._Context._cachedViewport.height
|
||||||
end
|
end
|
||||||
|
|
||||||
if love.graphics and love.graphics.getDimensions then
|
if love.graphics and love.graphics.getDimensions then
|
||||||
@@ -167,10 +137,12 @@ function Units.getViewport()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param value number
|
--- Apply base scale factor to a value based on axis
|
||||||
---@param axis "x"|"y"
|
--- Used for responsive scaling of UI elements
|
||||||
---@param scaleFactors {x:number, y:number}
|
---@param value number The value to scale
|
||||||
---@return number
|
---@param axis "x"|"y" The axis to scale on
|
||||||
|
---@param scaleFactors {x:number, y:number} Scale factors for each axis
|
||||||
|
---@return number scaledValue The scaled value
|
||||||
function Units.applyBaseScale(value, axis, scaleFactors)
|
function Units.applyBaseScale(value, axis, scaleFactors)
|
||||||
if axis == "x" then
|
if axis == "x" then
|
||||||
return value * scaleFactors.x
|
return value * scaleFactors.x
|
||||||
@@ -179,10 +151,12 @@ function Units.applyBaseScale(value, axis, scaleFactors)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param spacingProps table?
|
--- Resolve spacing properties (margin, padding) to pixel values
|
||||||
---@param parentWidth number
|
--- Supports individual sides (top, right, bottom, left) and shortcuts (vertical, horizontal)
|
||||||
---@param parentHeight number
|
---@param spacingProps table? Spacing properties with top/right/bottom/left/vertical/horizontal
|
||||||
---@return table -- Resolved spacing with top, right, bottom, left in pixels
|
---@param parentWidth number Parent element width in pixels
|
||||||
|
---@param parentHeight number Parent element height in pixels
|
||||||
|
---@return table resolvedSpacing Table with top, right, bottom, left in pixels
|
||||||
function Units.resolveSpacing(spacingProps, parentWidth, parentHeight)
|
function Units.resolveSpacing(spacingProps, parentWidth, parentHeight)
|
||||||
if not spacingProps then
|
if not spacingProps then
|
||||||
return { top = 0, right = 0, bottom = 0, left = 0 }
|
return { top = 0, right = 0, bottom = 0, left = 0 }
|
||||||
@@ -230,8 +204,10 @@ function Units.resolveSpacing(spacingProps, parentWidth, parentHeight)
|
|||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param unitStr string
|
--- Validate a unit string format
|
||||||
---@return boolean
|
--- Checks if the string can be successfully parsed as a valid unit
|
||||||
|
---@param unitStr string The unit string to validate (e.g., "50px", "10%")
|
||||||
|
---@return boolean isValid True if the unit string is valid, false otherwise
|
||||||
function Units.isValid(unitStr)
|
function Units.isValid(unitStr)
|
||||||
if type(unitStr) ~= "string" then
|
if type(unitStr) ~= "string" then
|
||||||
return false
|
return false
|
||||||
@@ -264,14 +240,4 @@ function Units.isValid(unitStr)
|
|||||||
return validUnits[unit] == true
|
return validUnits[unit] == true
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param value string|number -- Value to parse and resolve
|
|
||||||
---@param viewportWidth number -- Current viewport width
|
|
||||||
---@param viewportHeight number -- Current viewport height
|
|
||||||
---@param parentSize number? -- Parent dimension for percentage units
|
|
||||||
---@return number -- Resolved pixel value
|
|
||||||
function Units.parseAndResolve(value, viewportWidth, viewportHeight, parentSize)
|
|
||||||
local numValue, unit = Units.parse(value)
|
|
||||||
return Units.resolve(numValue, unit, viewportWidth, viewportHeight, parentSize)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Units
|
return Units
|
||||||
|
|||||||
@@ -161,31 +161,36 @@ function TestUnitsResolve:testResolveDecimalPercentage()
|
|||||||
luaunit.assertAlmostEquals(result, 99.99, 0.01)
|
luaunit.assertAlmostEquals(result, 99.99, 0.01)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Test suite for Units.parseAndResolve()
|
-- Test suite for Units.parse() + Units.resolve() combination
|
||||||
TestUnitsParseAndResolve = {}
|
TestUnitsParseAndResolve = {}
|
||||||
|
|
||||||
function TestUnitsParseAndResolve:testParseAndResolvePixels()
|
function TestUnitsParseAndResolve:testParseAndResolvePixels()
|
||||||
local result = Units.parseAndResolve("100px", MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT)
|
local numValue, unit = Units.parse("100px")
|
||||||
|
local result = Units.resolve(numValue, unit, MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT)
|
||||||
luaunit.assertEquals(result, 100)
|
luaunit.assertEquals(result, 100)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestUnitsParseAndResolve:testParseAndResolveNumber()
|
function TestUnitsParseAndResolve:testParseAndResolveNumber()
|
||||||
local result = Units.parseAndResolve(100, MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT)
|
local numValue, unit = Units.parse(100)
|
||||||
|
local result = Units.resolve(numValue, unit, MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT)
|
||||||
luaunit.assertEquals(result, 100)
|
luaunit.assertEquals(result, 100)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestUnitsParseAndResolve:testParseAndResolvePercentage()
|
function TestUnitsParseAndResolve:testParseAndResolvePercentage()
|
||||||
local result = Units.parseAndResolve("50%", MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT, 400)
|
local numValue, unit = Units.parse("50%")
|
||||||
|
local result = Units.resolve(numValue, unit, MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT, 400)
|
||||||
luaunit.assertEquals(result, 200)
|
luaunit.assertEquals(result, 200)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestUnitsParseAndResolve:testParseAndResolveViewportWidth()
|
function TestUnitsParseAndResolve:testParseAndResolveViewportWidth()
|
||||||
local result = Units.parseAndResolve("10vw", MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT)
|
local numValue, unit = Units.parse("10vw")
|
||||||
|
local result = Units.resolve(numValue, unit, MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT)
|
||||||
luaunit.assertEquals(result, 192)
|
luaunit.assertEquals(result, 192)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestUnitsParseAndResolve:testParseAndResolveViewportHeight()
|
function TestUnitsParseAndResolve:testParseAndResolveViewportHeight()
|
||||||
local result = Units.parseAndResolve("50vh", MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT)
|
local numValue, unit = Units.parse("50vh")
|
||||||
|
local result = Units.resolve(numValue, unit, MOCK_VIEWPORT_WIDTH, MOCK_VIEWPORT_HEIGHT)
|
||||||
luaunit.assertEquals(result, 540)
|
luaunit.assertEquals(result, 540)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user