getting ready for first release
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,4 +6,5 @@ themes/metal/
|
|||||||
themes/space/
|
themes/space/
|
||||||
.DS_STORE
|
.DS_STORE
|
||||||
tasks
|
tasks
|
||||||
testoutput.txt
|
testoutput
|
||||||
|
release
|
||||||
|
|||||||
323
FlexLove.lua
323
FlexLove.lua
@@ -1,10 +1,3 @@
|
|||||||
--[[
|
|
||||||
FlexLove - UI Library for LÖVE Framework 'based' on flexbox
|
|
||||||
VERSION: 0.1.0
|
|
||||||
LICENSE: MIT
|
|
||||||
For full documentation, see README.md
|
|
||||||
]]
|
|
||||||
|
|
||||||
local modulePath = (...):match("(.-)[^%.]+$") -- Get the module path prefix (e.g., "libs." or "")
|
local modulePath = (...):match("(.-)[^%.]+$") -- Get the module path prefix (e.g., "libs." or "")
|
||||||
local function req(name)
|
local function req(name)
|
||||||
return require(modulePath .. "modules." .. name)
|
return require(modulePath .. "modules." .. name)
|
||||||
@@ -12,60 +5,75 @@ end
|
|||||||
|
|
||||||
-- internals
|
-- internals
|
||||||
local Blur = req("Blur")
|
local Blur = req("Blur")
|
||||||
local ImageCache = req("ImageCache")
|
|
||||||
local ImageDataReader = req("ImageDataReader")
|
|
||||||
local ImageRenderer = req("ImageRenderer")
|
|
||||||
local ImageScaler = req("ImageScaler")
|
|
||||||
local NinePatchParser = req("NinePatchParser")
|
|
||||||
local utils = req("utils")
|
local utils = req("utils")
|
||||||
local Units = req("Units")
|
local Units = req("Units")
|
||||||
local GuiState = req("GuiState")
|
local GuiState = req("GuiState")
|
||||||
local StateManager = req("StateManager")
|
local StateManager = req("StateManager")
|
||||||
|
---@type Element
|
||||||
-- externals
|
local Element = req("Element")
|
||||||
---@type Theme
|
---@type Theme
|
||||||
local Theme = req("Theme")
|
local Theme = req("Theme")
|
||||||
|
|
||||||
|
-- externals
|
||||||
---@type Animation
|
---@type Animation
|
||||||
local Animation = req("Animation")
|
local Animation = req("Animation")
|
||||||
---@type Color
|
---@type Color
|
||||||
local Color = req("Color")
|
local Color = req("Color")
|
||||||
---@type Element
|
|
||||||
local Element = req("Element")
|
|
||||||
|
|
||||||
local enums = utils.enums
|
local enums = utils.enums
|
||||||
|
|
||||||
local Positioning, FlexDirection, JustifyContent, AlignContent, AlignItems, TextAlign, AlignSelf, JustifySelf, FlexWrap =
|
|
||||||
enums.Positioning,
|
|
||||||
enums.FlexDirection,
|
|
||||||
enums.JustifyContent,
|
|
||||||
enums.AlignContent,
|
|
||||||
enums.AlignItems,
|
|
||||||
enums.TextAlign,
|
|
||||||
enums.AlignSelf,
|
|
||||||
enums.JustifySelf,
|
|
||||||
enums.FlexWrap
|
|
||||||
|
|
||||||
-- ====================
|
-- ====================
|
||||||
-- Top level GUI manager
|
-- FlexLove - UI Library
|
||||||
-- ====================
|
-- ====================
|
||||||
|
|
||||||
---@class Gui
|
---@class FlexLove
|
||||||
local Gui = GuiState
|
local flexlove = {
|
||||||
|
_VERSION = "FlexLove v0.1.0",
|
||||||
|
_DESCRIPTION = "UI Library for LÖVE Framework based on flexbox",
|
||||||
|
_URL = "https://github.com/Station-Alpha/FlexLove",
|
||||||
|
_LICENSE = [[
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Mike Freno
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
]],
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Copy GuiState properties into flexlove
|
||||||
|
for k, v in pairs(GuiState) do
|
||||||
|
flexlove[k] = v
|
||||||
|
end
|
||||||
|
|
||||||
--- Initialize FlexLove with configuration
|
--- Initialize FlexLove with configuration
|
||||||
---@param config {baseScale?: {width?:number, height?:number}, theme?: string|ThemeDefinition, immediateMode?: boolean, stateRetentionFrames?: number, maxStateEntries?: number, autoFrameManagement?: boolean}
|
---@param config {baseScale?: {width?:number, height?:number}, theme?: string|ThemeDefinition, immediateMode?: boolean, stateRetentionFrames?: number, maxStateEntries?: number, autoFrameManagement?: boolean}
|
||||||
function Gui.init(config)
|
function flexlove.init(config)
|
||||||
config = config or {}
|
config = config or {}
|
||||||
|
|
||||||
if config.baseScale then
|
if config.baseScale then
|
||||||
Gui.baseScale = {
|
flexlove.baseScale = {
|
||||||
width = config.baseScale.width or 1920,
|
width = config.baseScale.width or 1920,
|
||||||
height = config.baseScale.height or 1080,
|
height = config.baseScale.height or 1080,
|
||||||
}
|
}
|
||||||
|
|
||||||
local currentWidth, currentHeight = Units.getViewport()
|
local currentWidth, currentHeight = Units.getViewport()
|
||||||
Gui.scaleFactors.x = currentWidth / Gui.baseScale.width
|
flexlove.scaleFactors.x = currentWidth / flexlove.baseScale.width
|
||||||
Gui.scaleFactors.y = currentHeight / Gui.baseScale.height
|
flexlove.scaleFactors.y = currentHeight / flexlove.baseScale.height
|
||||||
end
|
end
|
||||||
|
|
||||||
if config.theme then
|
if config.theme then
|
||||||
@@ -73,11 +81,11 @@ function Gui.init(config)
|
|||||||
if type(config.theme) == "string" then
|
if type(config.theme) == "string" then
|
||||||
Theme.load(config.theme)
|
Theme.load(config.theme)
|
||||||
Theme.setActive(config.theme)
|
Theme.setActive(config.theme)
|
||||||
Gui.defaultTheme = config.theme
|
flexlove.defaultTheme = config.theme
|
||||||
elseif type(config.theme) == "table" then
|
elseif type(config.theme) == "table" then
|
||||||
local theme = Theme.new(config.theme)
|
local theme = Theme.new(config.theme)
|
||||||
Theme.setActive(theme)
|
Theme.setActive(theme)
|
||||||
Gui.defaultTheme = theme.name
|
flexlove.defaultTheme = theme.name
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -87,10 +95,10 @@ function Gui.init(config)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local immediateMode = config.immediateMode or false
|
local immediateMode = config.immediateMode or false
|
||||||
Gui.setMode(immediateMode and "immediate" or "retained")
|
flexlove.setMode(immediateMode and "immediate" or "retained")
|
||||||
|
|
||||||
-- Configure auto frame management (defaults to false for manual control)
|
-- Configure auto frame management (defaults to false for manual control)
|
||||||
Gui._autoFrameManagement = config.autoFrameManagement or false
|
flexlove._autoFrameManagement = config.autoFrameManagement or false
|
||||||
|
|
||||||
-- Configure state management
|
-- Configure state management
|
||||||
if config.stateRetentionFrames or config.maxStateEntries then
|
if config.stateRetentionFrames or config.maxStateEntries then
|
||||||
@@ -101,42 +109,42 @@ function Gui.init(config)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Gui.resize()
|
function flexlove.resize()
|
||||||
local newWidth, newHeight = love.window.getMode()
|
local newWidth, newHeight = love.window.getMode()
|
||||||
|
|
||||||
if Gui.baseScale then
|
if flexlove.baseScale then
|
||||||
Gui.scaleFactors.x = newWidth / Gui.baseScale.width
|
flexlove.scaleFactors.x = newWidth / flexlove.baseScale.width
|
||||||
Gui.scaleFactors.y = newHeight / Gui.baseScale.height
|
flexlove.scaleFactors.y = newHeight / flexlove.baseScale.height
|
||||||
end
|
end
|
||||||
|
|
||||||
Blur.clearCache()
|
Blur.clearCache()
|
||||||
|
|
||||||
Gui._gameCanvas = nil
|
flexlove._gameCanvas = nil
|
||||||
Gui._backdropCanvas = nil
|
flexlove._backdropCanvas = nil
|
||||||
Gui._canvasDimensions = { width = 0, height = 0 }
|
flexlove._canvasDimensions = { width = 0, height = 0 }
|
||||||
|
|
||||||
for _, win in ipairs(Gui.topElements) do
|
for _, win in ipairs(flexlove.topElements) do
|
||||||
win:resize(newWidth, newHeight)
|
win:resize(newWidth, newHeight)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Set the rendering mode (immediate or retained)
|
--- Set the rendering mode (immediate or retained)
|
||||||
---@param mode "immediate"|"retained" The rendering mode to use
|
---@param mode "immediate"|"retained" The rendering mode to use
|
||||||
function Gui.setMode(mode)
|
function flexlove.setMode(mode)
|
||||||
if mode == "immediate" then
|
if mode == "immediate" then
|
||||||
Gui._immediateMode = true
|
flexlove._immediateMode = true
|
||||||
Gui._immediateModeState = StateManager
|
flexlove._immediateModeState = StateManager
|
||||||
-- Reset frame state
|
-- Reset frame state
|
||||||
Gui._frameStarted = false
|
flexlove._frameStarted = false
|
||||||
Gui._autoBeganFrame = false
|
flexlove._autoBeganFrame = false
|
||||||
elseif mode == "retained" then
|
elseif mode == "retained" then
|
||||||
Gui._immediateMode = false
|
flexlove._immediateMode = false
|
||||||
Gui._immediateModeState = nil
|
flexlove._immediateModeState = nil
|
||||||
-- Clear immediate mode state
|
-- Clear immediate mode state
|
||||||
Gui._frameStarted = false
|
flexlove._frameStarted = false
|
||||||
Gui._autoBeganFrame = false
|
flexlove._autoBeganFrame = false
|
||||||
Gui._currentFrameElements = {}
|
flexlove._currentFrameElements = {}
|
||||||
Gui._frameNumber = 0
|
flexlove._frameNumber = 0
|
||||||
else
|
else
|
||||||
error("[FlexLove] Invalid mode: " .. tostring(mode) .. ". Expected 'immediate' or 'retained'")
|
error("[FlexLove] Invalid mode: " .. tostring(mode) .. ". Expected 'immediate' or 'retained'")
|
||||||
end
|
end
|
||||||
@@ -144,34 +152,34 @@ end
|
|||||||
|
|
||||||
--- Get the current rendering mode
|
--- Get the current rendering mode
|
||||||
---@return "immediate"|"retained"
|
---@return "immediate"|"retained"
|
||||||
function Gui.getMode()
|
function flexlove.getMode()
|
||||||
return Gui._immediateMode and "immediate" or "retained"
|
return flexlove._immediateMode and "immediate" or "retained"
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Begin a new immediate mode frame
|
--- Begin a new immediate mode frame
|
||||||
function Gui.beginFrame()
|
function flexlove.beginFrame()
|
||||||
if not Gui._immediateMode then
|
if not flexlove._immediateMode then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Increment frame counter
|
-- Increment frame counter
|
||||||
Gui._frameNumber = Gui._frameNumber + 1
|
flexlove._frameNumber = flexlove._frameNumber + 1
|
||||||
StateManager.incrementFrame()
|
StateManager.incrementFrame()
|
||||||
|
|
||||||
-- Clear current frame elements
|
-- Clear current frame elements
|
||||||
Gui._currentFrameElements = {}
|
flexlove._currentFrameElements = {}
|
||||||
Gui._frameStarted = true
|
flexlove._frameStarted = true
|
||||||
|
|
||||||
-- Clear top elements (they will be recreated this frame)
|
-- Clear top elements (they will be recreated this frame)
|
||||||
Gui.topElements = {}
|
flexlove.topElements = {}
|
||||||
|
|
||||||
-- Clear z-index ordered elements from previous frame
|
-- Clear z-index ordered elements from previous frame
|
||||||
GuiState.clearFrameElements()
|
GuiState.clearFrameElements()
|
||||||
end
|
end
|
||||||
|
|
||||||
--- End the current immediate mode frame
|
--- End the current immediate mode frame
|
||||||
function Gui.endFrame()
|
function flexlove.endFrame()
|
||||||
if not Gui._immediateMode then
|
if not flexlove._immediateMode then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -180,7 +188,7 @@ function Gui.endFrame()
|
|||||||
|
|
||||||
-- Layout all top-level elements now that all children have been added
|
-- Layout all top-level elements now that all children have been added
|
||||||
-- This ensures overflow detection happens with complete child lists
|
-- This ensures overflow detection happens with complete child lists
|
||||||
for _, element in ipairs(Gui._currentFrameElements) do
|
for _, element in ipairs(flexlove._currentFrameElements) do
|
||||||
if not element.parent then
|
if not element.parent then
|
||||||
element:layoutChildren() -- Layout with all children present
|
element:layoutChildren() -- Layout with all children present
|
||||||
end
|
end
|
||||||
@@ -188,7 +196,7 @@ function Gui.endFrame()
|
|||||||
|
|
||||||
-- Auto-update all top-level elements (triggers additional state updates)
|
-- Auto-update all top-level elements (triggers additional state updates)
|
||||||
-- This must happen BEFORE saving state so that scroll positions and overflow are calculated
|
-- This must happen BEFORE saving state so that scroll positions and overflow are calculated
|
||||||
for _, element in ipairs(Gui._currentFrameElements) do
|
for _, element in ipairs(flexlove._currentFrameElements) do
|
||||||
-- Only update top-level elements (those without parents in the current frame)
|
-- Only update top-level elements (those without parents in the current frame)
|
||||||
-- Element:update() will recursively update children
|
-- Element:update() will recursively update children
|
||||||
if not element.parent then
|
if not element.parent then
|
||||||
@@ -197,7 +205,7 @@ function Gui.endFrame()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Save state back for all elements created this frame
|
-- Save state back for all elements created this frame
|
||||||
for _, element in ipairs(Gui._currentFrameElements) do
|
for _, element in ipairs(flexlove._currentFrameElements) do
|
||||||
if element.id and element.id ~= "" then
|
if element.id and element.id ~= "" then
|
||||||
local state = StateManager.getState(element.id, {})
|
local state = StateManager.getState(element.id, {})
|
||||||
|
|
||||||
@@ -237,21 +245,21 @@ function Gui.endFrame()
|
|||||||
StateManager.forceCleanupIfNeeded()
|
StateManager.forceCleanupIfNeeded()
|
||||||
|
|
||||||
-- Clear frame started flag
|
-- Clear frame started flag
|
||||||
Gui._frameStarted = false
|
flexlove._frameStarted = false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Canvas cache for game rendering
|
-- Canvas cache for game rendering
|
||||||
Gui._gameCanvas = nil
|
flexlove._gameCanvas = nil
|
||||||
Gui._backdropCanvas = nil
|
flexlove._backdropCanvas = nil
|
||||||
Gui._canvasDimensions = { width = 0, height = 0 }
|
flexlove._canvasDimensions = { width = 0, height = 0 }
|
||||||
|
|
||||||
---@param gameDrawFunc function|nil
|
---@param gameDrawFunc function|nil
|
||||||
---@param postDrawFunc function|nil
|
---@param postDrawFunc function|nil
|
||||||
function Gui.draw(gameDrawFunc, postDrawFunc)
|
function flexlove.draw(gameDrawFunc, postDrawFunc)
|
||||||
-- Auto-end frame if it was auto-started in immediate mode
|
-- Auto-end frame if it was auto-started in immediate mode
|
||||||
if Gui._immediateMode and Gui._autoBeganFrame then
|
if flexlove._immediateMode and flexlove._autoBeganFrame then
|
||||||
Gui.endFrame()
|
flexlove.endFrame()
|
||||||
Gui._autoBeganFrame = false
|
flexlove._autoBeganFrame = false
|
||||||
end
|
end
|
||||||
|
|
||||||
local outerCanvas = love.graphics.getCanvas()
|
local outerCanvas = love.graphics.getCanvas()
|
||||||
@@ -260,14 +268,14 @@ function Gui.draw(gameDrawFunc, postDrawFunc)
|
|||||||
if type(gameDrawFunc) == "function" then
|
if type(gameDrawFunc) == "function" then
|
||||||
local width, height = love.graphics.getDimensions()
|
local width, height = love.graphics.getDimensions()
|
||||||
|
|
||||||
if not Gui._gameCanvas or Gui._canvasDimensions.width ~= width or Gui._canvasDimensions.height ~= height then
|
if not flexlove._gameCanvas or flexlove._canvasDimensions.width ~= width or flexlove._canvasDimensions.height ~= height then
|
||||||
Gui._gameCanvas = love.graphics.newCanvas(width, height)
|
flexlove._gameCanvas = love.graphics.newCanvas(width, height)
|
||||||
Gui._backdropCanvas = love.graphics.newCanvas(width, height)
|
flexlove._backdropCanvas = love.graphics.newCanvas(width, height)
|
||||||
Gui._canvasDimensions.width = width
|
flexlove._canvasDimensions.width = width
|
||||||
Gui._canvasDimensions.height = height
|
flexlove._canvasDimensions.height = height
|
||||||
end
|
end
|
||||||
|
|
||||||
gameCanvas = Gui._gameCanvas
|
gameCanvas = flexlove._gameCanvas
|
||||||
|
|
||||||
love.graphics.setCanvas(gameCanvas)
|
love.graphics.setCanvas(gameCanvas)
|
||||||
love.graphics.clear()
|
love.graphics.clear()
|
||||||
@@ -278,7 +286,7 @@ function Gui.draw(gameDrawFunc, postDrawFunc)
|
|||||||
love.graphics.draw(gameCanvas, 0, 0)
|
love.graphics.draw(gameCanvas, 0, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
table.sort(Gui.topElements, function(a, b)
|
table.sort(flexlove.topElements, function(a, b)
|
||||||
return a.z < b.z
|
return a.z < b.z
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -295,7 +303,7 @@ function Gui.draw(gameDrawFunc, postDrawFunc)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local needsBackdropCanvas = false
|
local needsBackdropCanvas = false
|
||||||
for _, win in ipairs(Gui.topElements) do
|
for _, win in ipairs(flexlove.topElements) do
|
||||||
if hasBackdropBlur(win) then
|
if hasBackdropBlur(win) then
|
||||||
needsBackdropCanvas = true
|
needsBackdropCanvas = true
|
||||||
break
|
break
|
||||||
@@ -303,7 +311,7 @@ function Gui.draw(gameDrawFunc, postDrawFunc)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if needsBackdropCanvas and gameCanvas then
|
if needsBackdropCanvas and gameCanvas then
|
||||||
local backdropCanvas = Gui._backdropCanvas
|
local backdropCanvas = flexlove._backdropCanvas
|
||||||
local prevColor = { love.graphics.getColor() }
|
local prevColor = { love.graphics.getColor() }
|
||||||
|
|
||||||
love.graphics.setCanvas(backdropCanvas)
|
love.graphics.setCanvas(backdropCanvas)
|
||||||
@@ -314,7 +322,7 @@ function Gui.draw(gameDrawFunc, postDrawFunc)
|
|||||||
love.graphics.setCanvas(outerCanvas)
|
love.graphics.setCanvas(outerCanvas)
|
||||||
love.graphics.setColor(unpack(prevColor))
|
love.graphics.setColor(unpack(prevColor))
|
||||||
|
|
||||||
for _, win in ipairs(Gui.topElements) do
|
for _, win in ipairs(flexlove.topElements) do
|
||||||
-- Check if this element tree has backdrop blur
|
-- Check if this element tree has backdrop blur
|
||||||
local needsBackdrop = hasBackdropBlur(win)
|
local needsBackdrop = hasBackdropBlur(win)
|
||||||
|
|
||||||
@@ -334,7 +342,7 @@ function Gui.draw(gameDrawFunc, postDrawFunc)
|
|||||||
love.graphics.setCanvas(outerCanvas)
|
love.graphics.setCanvas(outerCanvas)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
for _, win in ipairs(Gui.topElements) do
|
for _, win in ipairs(flexlove.topElements) do
|
||||||
win:draw(nil)
|
win:draw(nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -365,7 +373,7 @@ end
|
|||||||
---@param x number
|
---@param x number
|
||||||
---@param y number
|
---@param y number
|
||||||
---@return Element?
|
---@return Element?
|
||||||
function Gui.getElementAtPosition(x, y)
|
function flexlove.getElementAtPosition(x, y)
|
||||||
local candidates = {}
|
local candidates = {}
|
||||||
local blockingElements = {}
|
local blockingElements = {}
|
||||||
|
|
||||||
@@ -420,7 +428,7 @@ function Gui.getElementAtPosition(x, y)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, element in ipairs(Gui.topElements) do
|
for _, element in ipairs(flexlove.topElements) do
|
||||||
collectHits(element)
|
collectHits(element)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -457,21 +465,21 @@ function Gui.getElementAtPosition(x, y)
|
|||||||
return blockingElements[1]
|
return blockingElements[1]
|
||||||
end
|
end
|
||||||
|
|
||||||
function Gui.update(dt)
|
function flexlove.update(dt)
|
||||||
local mx, my = love.mouse.getPosition()
|
local mx, my = love.mouse.getPosition()
|
||||||
local topElement = Gui.getElementAtPosition(mx, my)
|
local topElement = flexlove.getElementAtPosition(mx, my)
|
||||||
|
|
||||||
Gui._activeEventElement = topElement
|
flexlove._activeEventElement = topElement
|
||||||
|
|
||||||
for _, win in ipairs(Gui.topElements) do
|
for _, win in ipairs(flexlove.topElements) do
|
||||||
win:update(dt)
|
win:update(dt)
|
||||||
end
|
end
|
||||||
|
|
||||||
Gui._activeEventElement = nil
|
flexlove._activeEventElement = nil
|
||||||
|
|
||||||
-- In immediate mode, save state after update so that cursor blink timer changes persist
|
-- In immediate mode, save state after update so that cursor blink timer changes persist
|
||||||
if Gui._immediateMode and Gui._currentFrameElements then
|
if flexlove._immediateMode and flexlove._currentFrameElements then
|
||||||
for _, element in ipairs(Gui._currentFrameElements) do
|
for _, element in ipairs(flexlove._currentFrameElements) do
|
||||||
if element.id and element.id ~= "" and element.editable and element._focused then
|
if element.id and element.id ~= "" and element.editable and element._focused then
|
||||||
local state = StateManager.getState(element.id, {})
|
local state = StateManager.getState(element.id, {})
|
||||||
|
|
||||||
@@ -489,9 +497,9 @@ end
|
|||||||
|
|
||||||
--- Forward text input to focused element
|
--- Forward text input to focused element
|
||||||
---@param text string
|
---@param text string
|
||||||
function Gui.textinput(text)
|
function flexlove.textinput(text)
|
||||||
if Gui._focusedElement then
|
if flexlove._focusedElement then
|
||||||
Gui._focusedElement:textinput(text)
|
flexlove._focusedElement:textinput(text)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -499,14 +507,14 @@ end
|
|||||||
---@param key string
|
---@param key string
|
||||||
---@param scancode string
|
---@param scancode string
|
||||||
---@param isrepeat boolean
|
---@param isrepeat boolean
|
||||||
function Gui.keypressed(key, scancode, isrepeat)
|
function flexlove.keypressed(key, scancode, isrepeat)
|
||||||
if Gui._focusedElement then
|
if flexlove._focusedElement then
|
||||||
Gui._focusedElement:keypressed(key, scancode, isrepeat)
|
flexlove._focusedElement:keypressed(key, scancode, isrepeat)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Handle mouse wheel scrolling
|
--- Handle mouse wheel scrolling
|
||||||
function Gui.wheelmoved(x, y)
|
function flexlove.wheelmoved(x, y)
|
||||||
local mx, my = love.mouse.getPosition()
|
local mx, my = love.mouse.getPosition()
|
||||||
|
|
||||||
local function findScrollableAtPosition(elements, mx, my)
|
local function findScrollableAtPosition(elements, mx, my)
|
||||||
@@ -538,7 +546,7 @@ function Gui.wheelmoved(x, y)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- In immediate mode, use z-index ordered elements and respect occlusion
|
-- In immediate mode, use z-index ordered elements and respect occlusion
|
||||||
if Gui._immediateMode then
|
if flexlove._immediateMode then
|
||||||
-- Find topmost scrollable element at mouse position using z-index ordering
|
-- Find topmost scrollable element at mouse position using z-index ordering
|
||||||
for i = #GuiState._zIndexOrderedElements, 1, -1 do
|
for i = #GuiState._zIndexOrderedElements, 1, -1 do
|
||||||
local element = GuiState._zIndexOrderedElements[i]
|
local element = GuiState._zIndexOrderedElements[i]
|
||||||
@@ -559,7 +567,7 @@ function Gui.wheelmoved(x, y)
|
|||||||
end
|
end
|
||||||
else
|
else
|
||||||
-- In retained mode, use the old tree traversal method
|
-- In retained mode, use the old tree traversal method
|
||||||
local scrollableElement = findScrollableAtPosition(Gui.topElements, mx, my)
|
local scrollableElement = findScrollableAtPosition(flexlove.topElements, mx, my)
|
||||||
if scrollableElement then
|
if scrollableElement then
|
||||||
scrollableElement:_handleWheelScroll(x, y)
|
scrollableElement:_handleWheelScroll(x, y)
|
||||||
end
|
end
|
||||||
@@ -567,35 +575,35 @@ function Gui.wheelmoved(x, y)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Destroy all elements and their children
|
--- Destroy all elements and their children
|
||||||
function Gui.destroy()
|
function flexlove.destroy()
|
||||||
for _, win in ipairs(Gui.topElements) do
|
for _, win in ipairs(flexlove.topElements) do
|
||||||
win:destroy()
|
win:destroy()
|
||||||
end
|
end
|
||||||
Gui.topElements = {}
|
flexlove.topElements = {}
|
||||||
Gui.baseScale = nil
|
flexlove.baseScale = nil
|
||||||
Gui.scaleFactors = { x = 1.0, y = 1.0 }
|
flexlove.scaleFactors = { x = 1.0, y = 1.0 }
|
||||||
Gui._cachedViewport = { width = 0, height = 0 }
|
flexlove._cachedViewport = { width = 0, height = 0 }
|
||||||
Gui._gameCanvas = nil
|
flexlove._gameCanvas = nil
|
||||||
Gui._backdropCanvas = nil
|
flexlove._backdropCanvas = nil
|
||||||
Gui._canvasDimensions = { width = 0, height = 0 }
|
flexlove._canvasDimensions = { width = 0, height = 0 }
|
||||||
Gui._focusedElement = nil
|
flexlove._focusedElement = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Create a new element (supports both immediate and retained mode)
|
--- Create a new element (supports both immediate and retained mode)
|
||||||
---@param props ElementProps
|
---@param props ElementProps
|
||||||
---@return Element
|
---@return Element
|
||||||
function Gui.new(props)
|
function flexlove.new(props)
|
||||||
props = props or {}
|
props = props or {}
|
||||||
|
|
||||||
-- If not in immediate mode, use standard Element.new
|
-- If not in immediate mode, use standard Element.new
|
||||||
if not Gui._immediateMode then
|
if not flexlove._immediateMode then
|
||||||
return Element.new(props)
|
return Element.new(props)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Auto-begin frame if not manually started (convenience feature)
|
-- Auto-begin frame if not manually started (convenience feature)
|
||||||
if not Gui._frameStarted then
|
if not flexlove._frameStarted then
|
||||||
Gui.beginFrame()
|
flexlove.beginFrame()
|
||||||
Gui._autoBeganFrame = true
|
flexlove._autoBeganFrame = true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Immediate mode: generate ID if not provided
|
-- Immediate mode: generate ID if not provided
|
||||||
@@ -686,7 +694,7 @@ function Gui.new(props)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Store element in current frame tracking
|
-- Store element in current frame tracking
|
||||||
table.insert(Gui._currentFrameElements, element)
|
table.insert(flexlove._currentFrameElements, element)
|
||||||
|
|
||||||
-- Save state back at end of frame (we'll do this in endFrame)
|
-- Save state back at end of frame (we'll do this in endFrame)
|
||||||
-- For now, we need to update the state when properties change
|
-- For now, we need to update the state when properties change
|
||||||
@@ -698,8 +706,8 @@ end
|
|||||||
|
|
||||||
--- Get state count (for debugging)
|
--- Get state count (for debugging)
|
||||||
---@return number
|
---@return number
|
||||||
function Gui.getStateCount()
|
function flexlove.getStateCount()
|
||||||
if not Gui._immediateMode then
|
if not flexlove._immediateMode then
|
||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
return StateManager.getStateCount()
|
return StateManager.getStateCount()
|
||||||
@@ -707,16 +715,16 @@ end
|
|||||||
|
|
||||||
--- Clear state for a specific element ID
|
--- Clear state for a specific element ID
|
||||||
---@param id string
|
---@param id string
|
||||||
function Gui.clearState(id)
|
function flexlove.clearState(id)
|
||||||
if not Gui._immediateMode then
|
if not flexlove._immediateMode then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
StateManager.clearState(id)
|
StateManager.clearState(id)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Clear all immediate mode states
|
--- Clear all immediate mode states
|
||||||
function Gui.clearAllStates()
|
function flexlove.clearAllStates()
|
||||||
if not Gui._immediateMode then
|
if not flexlove._immediateMode then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
StateManager.clearAllStates()
|
StateManager.clearAllStates()
|
||||||
@@ -724,8 +732,8 @@ end
|
|||||||
|
|
||||||
--- Get state statistics (for debugging)
|
--- Get state statistics (for debugging)
|
||||||
---@return table
|
---@return table
|
||||||
function Gui.getStateStats()
|
function flexlove.getStateStats()
|
||||||
if not Gui._immediateMode then
|
if not flexlove._immediateMode then
|
||||||
return { stateCount = 0, frameNumber = 0 }
|
return { stateCount = 0, frameNumber = 0 }
|
||||||
end
|
end
|
||||||
return StateManager.getStats()
|
return StateManager.getStats()
|
||||||
@@ -734,65 +742,40 @@ end
|
|||||||
--- Helper function: Create a button with default styling
|
--- Helper function: Create a button with default styling
|
||||||
---@param props table
|
---@param props table
|
||||||
---@return Element
|
---@return Element
|
||||||
function Gui.button(props)
|
function flexlove.button(props)
|
||||||
props = props or {}
|
props = props or {}
|
||||||
props.themeComponent = props.themeComponent or "button"
|
props.themeComponent = props.themeComponent or "button"
|
||||||
return Gui.new(props)
|
return flexlove.new(props)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Helper function: Create a panel/container
|
--- Helper function: Create a panel/container
|
||||||
---@param props table
|
---@param props table
|
||||||
---@return Element
|
---@return Element
|
||||||
function Gui.panel(props)
|
function flexlove.panel(props)
|
||||||
props = props or {}
|
props = props or {}
|
||||||
return Gui.new(props)
|
return flexlove.new(props)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Helper function: Create a text label
|
--- Helper function: Create a text label
|
||||||
---@param props table
|
---@param props table
|
||||||
---@return Element
|
---@return Element
|
||||||
function Gui.text(props)
|
function flexlove.text(props)
|
||||||
props = props or {}
|
props = props or {}
|
||||||
return Gui.new(props)
|
return flexlove.new(props)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Helper function: Create an input field
|
--- Helper function: Create an input field
|
||||||
---@param props table
|
---@param props table
|
||||||
---@return Element
|
---@return Element
|
||||||
function Gui.input(props)
|
function flexlove.input(props)
|
||||||
props = props or {}
|
props = props or {}
|
||||||
props.editable = true
|
props.editable = true
|
||||||
return Gui.new(props)
|
return flexlove.new(props)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Export original Element.new for direct access if needed
|
-- only export what should be used externally
|
||||||
Gui.Element = Element
|
flexlove.Animation = Animation
|
||||||
Gui.Animation = Animation
|
flexlove.Color = Color
|
||||||
Gui.Theme = Theme
|
flexlove.enums = enums
|
||||||
Gui.ImageCache = ImageCache
|
|
||||||
Gui.ImageDataReader = ImageDataReader
|
|
||||||
Gui.ImageRenderer = ImageRenderer
|
|
||||||
Gui.ImageScaler = ImageScaler
|
|
||||||
Gui.NinePatchParser = NinePatchParser
|
|
||||||
Gui.StateManager = StateManager
|
|
||||||
|
|
||||||
return {
|
return flexlove
|
||||||
Gui = Gui,
|
|
||||||
Element = Element,
|
|
||||||
Color = Color,
|
|
||||||
Theme = Theme,
|
|
||||||
Positioning = Positioning,
|
|
||||||
FlexDirection = FlexDirection,
|
|
||||||
JustifyContent = JustifyContent,
|
|
||||||
AlignContent = AlignContent,
|
|
||||||
AlignItems = AlignItems,
|
|
||||||
TextAlign = TextAlign,
|
|
||||||
AlignSelf = AlignSelf,
|
|
||||||
JustifySelf = JustifySelf,
|
|
||||||
FlexWrap = FlexWrap,
|
|
||||||
enums = enums,
|
|
||||||
-- generally should not be used directly, exported for testing, mainly
|
|
||||||
ImageCache = ImageCache,
|
|
||||||
ImageRenderer = ImageRenderer,
|
|
||||||
ImageScaler = ImageScaler,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# FlexLöve
|
# FlexLöve v0.1.0
|
||||||
|
|
||||||
**A comprehensive UI library providing flexbox/grid layouts, theming, animations, and event handling for LÖVE2D games.**
|
**A comprehensive UI library providing flexbox/grid layouts, theming, animations, and event handling for LÖVE2D games.**
|
||||||
|
|
||||||
|
|||||||
98
modules/ErrorHandler.lua
Normal file
98
modules/ErrorHandler.lua
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
-- modules/ErrorHandler.lua
|
||||||
|
local ErrorHandler = {}
|
||||||
|
|
||||||
|
--- Format an error or warning message
|
||||||
|
---@param module string The module name (e.g., "Element", "Units", "Theme")
|
||||||
|
---@param level string "Error" or "Warning"
|
||||||
|
---@param message string The error/warning message
|
||||||
|
---@return string Formatted message
|
||||||
|
local function formatMessage(module, level, message)
|
||||||
|
return string.format("[FlexLove - %s] %s: %s", module, level, message)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Throw a critical error (stops execution)
|
||||||
|
---@param module string The module name
|
||||||
|
---@param message string The error message
|
||||||
|
function ErrorHandler.error(module, message)
|
||||||
|
error(formatMessage(module, "Error", message), 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Print a warning (non-critical, continues execution)
|
||||||
|
---@param module string The module name
|
||||||
|
---@param message string The warning message
|
||||||
|
function ErrorHandler.warn(module, message)
|
||||||
|
print(formatMessage(module, "Warning", message))
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Validate that a value is not nil
|
||||||
|
---@param module string The module name
|
||||||
|
---@param value any The value to check
|
||||||
|
---@param paramName string The parameter name
|
||||||
|
---@return boolean True if valid
|
||||||
|
function ErrorHandler.assertNotNil(module, value, paramName)
|
||||||
|
if value == nil then
|
||||||
|
ErrorHandler.error(module, string.format("Parameter '%s' cannot be nil", paramName))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Validate that a value is of the expected type
|
||||||
|
---@param module string The module name
|
||||||
|
---@param value any The value to check
|
||||||
|
---@param expectedType string The expected type name
|
||||||
|
---@param paramName string The parameter name
|
||||||
|
---@return boolean True if valid
|
||||||
|
function ErrorHandler.assertType(module, value, expectedType, paramName)
|
||||||
|
local actualType = type(value)
|
||||||
|
if actualType ~= expectedType then
|
||||||
|
ErrorHandler.error(module, string.format(
|
||||||
|
"Parameter '%s' must be %s, got %s",
|
||||||
|
paramName, expectedType, actualType
|
||||||
|
))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Validate that a number is within a range
|
||||||
|
---@param module string The module name
|
||||||
|
---@param value number The value to check
|
||||||
|
---@param min number Minimum value (inclusive)
|
||||||
|
---@param max number Maximum value (inclusive)
|
||||||
|
---@param paramName string The parameter name
|
||||||
|
---@return boolean True if valid
|
||||||
|
function ErrorHandler.assertRange(module, value, min, max, paramName)
|
||||||
|
if value < min or value > max then
|
||||||
|
ErrorHandler.error(module, string.format(
|
||||||
|
"Parameter '%s' must be between %s and %s, got %s",
|
||||||
|
paramName, tostring(min), tostring(max), tostring(value)
|
||||||
|
))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Warn if a value is deprecated
|
||||||
|
---@param module string The module name
|
||||||
|
---@param oldName string The deprecated name
|
||||||
|
---@param newName string The new name to use
|
||||||
|
function ErrorHandler.warnDeprecated(module, oldName, newName)
|
||||||
|
ErrorHandler.warn(module, string.format(
|
||||||
|
"'%s' is deprecated. Use '%s' instead",
|
||||||
|
oldName, newName
|
||||||
|
))
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Warn about a common mistake
|
||||||
|
---@param module string The module name
|
||||||
|
---@param issue string Description of the issue
|
||||||
|
---@param suggestion string Suggested fix
|
||||||
|
function ErrorHandler.warnCommonMistake(module, issue, suggestion)
|
||||||
|
ErrorHandler.warn(module, string.format(
|
||||||
|
"%s. Suggestion: %s",
|
||||||
|
issue, suggestion
|
||||||
|
))
|
||||||
|
end
|
||||||
|
|
||||||
|
return ErrorHandler
|
||||||
@@ -4,6 +4,7 @@ local function req(name)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local NinePatchParser = req("NinePatchParser")
|
local NinePatchParser = req("NinePatchParser")
|
||||||
|
local Color = req("Color")
|
||||||
|
|
||||||
--- Standardized error message formatter
|
--- Standardized error message formatter
|
||||||
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
||||||
@@ -699,6 +700,7 @@ function ThemeManager:setTheme(themeName, componentName)
|
|||||||
self.themeComponent = componentName
|
self.themeComponent = componentName
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Export both Theme and ThemeManager
|
||||||
Theme.Manager = ThemeManager
|
Theme.Manager = ThemeManager
|
||||||
|
|
||||||
return Theme
|
return Theme
|
||||||
|
|||||||
Reference in New Issue
Block a user