getting ready for first release

This commit is contained in:
Michael Freno
2025-11-13 09:11:44 -05:00
parent 64aef0daf1
commit b173ab7354
5 changed files with 259 additions and 175 deletions

3
.gitignore vendored
View File

@@ -6,4 +6,5 @@ themes/metal/
themes/space/ themes/space/
.DS_STORE .DS_STORE
tasks tasks
testoutput.txt testoutput
release

View File

@@ -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
@@ -633,14 +641,14 @@ function Gui.new(props)
element._scrollbarDragging = state._scrollbarDragging ~= nil and state._scrollbarDragging or false element._scrollbarDragging = state._scrollbarDragging ~= nil and state._scrollbarDragging or false
element._hoveredScrollbar = state._hoveredScrollbar element._hoveredScrollbar = state._hoveredScrollbar
element._scrollbarDragOffset = state._scrollbarDragOffset ~= nil and state._scrollbarDragOffset or 0 element._scrollbarDragOffset = state._scrollbarDragOffset ~= nil and state._scrollbarDragOffset or 0
-- Sync scrollbar drag state to ScrollManager if it exists -- Sync scrollbar drag state to ScrollManager if it exists
if element._scrollManager then if element._scrollManager then
element._scrollManager._scrollbarDragging = element._scrollbarDragging element._scrollManager._scrollbarDragging = element._scrollbarDragging
element._scrollManager._hoveredScrollbar = element._hoveredScrollbar element._scrollManager._hoveredScrollbar = element._hoveredScrollbar
element._scrollManager._scrollbarDragOffset = element._scrollbarDragOffset element._scrollManager._scrollbarDragOffset = element._scrollbarDragOffset
end end
-- Restore cursor blink state -- Restore cursor blink state
element._cursorBlinkTimer = state._cursorBlinkTimer or element._cursorBlinkTimer or 0 element._cursorBlinkTimer = state._cursorBlinkTimer or element._cursorBlinkTimer or 0
if state._cursorVisible ~= nil then if state._cursorVisible ~= nil then
@@ -661,7 +669,7 @@ function Gui.new(props)
element._scrollbarDragging = state.scrollbarDragging element._scrollbarDragging = state.scrollbarDragging
element._hoveredScrollbar = state.hoveredScrollbar element._hoveredScrollbar = state.hoveredScrollbar
element._scrollbarDragOffset = state.scrollbarDragOffset or 0 element._scrollbarDragOffset = state.scrollbarDragOffset or 0
-- Sync interactive scroll state to ScrollManager if it exists -- Sync interactive scroll state to ScrollManager if it exists
if element._scrollManager then if element._scrollManager then
element._scrollManager._scrollbarHoveredVertical = element._scrollbarHoveredVertical or false element._scrollManager._scrollbarHoveredVertical = element._scrollbarHoveredVertical or false
@@ -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,
}

View File

@@ -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
View 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

View File

@@ -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