module refactor completion
This commit is contained in:
341
FlexLove.lua
341
FlexLove.lua
@@ -6,51 +6,35 @@ For full documentation, see README.md
|
|||||||
]]
|
]]
|
||||||
|
|
||||||
-- ====================
|
-- ====================
|
||||||
-- Module Imports
|
-- Module Imports (using relative paths)
|
||||||
-- ====================
|
-- ====================
|
||||||
local Blur = require("flexlove.Blur")
|
local modulePath = (...):match("(.-)[^%.]+$") -- Get the module path prefix (e.g., "libs." or "")
|
||||||
local Color = require("flexlove.Color")
|
local function req(name)
|
||||||
local ImageDataReader = require("flexlove.ImageDataReader")
|
return require(modulePath .. name)
|
||||||
local NinePatchParser = require("flexlove.NinePatchParser")
|
|
||||||
local ImageScaler = require("flexlove.ImageScaler")
|
|
||||||
local ImageCache = require("flexlove.ImageCache")
|
|
||||||
local ImageRenderer = require("flexlove.ImageRenderer")
|
|
||||||
local Theme = require("flexlove.Theme")
|
|
||||||
local RoundedRect = require("flexlove.RoundedRect")
|
|
||||||
local NineSlice = require("flexlove.NineSlice")
|
|
||||||
local enums = require("flexlove.types")
|
|
||||||
local constants = require("flexlove.constants")
|
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- Error Handling Utilities
|
|
||||||
-- ====================
|
|
||||||
|
|
||||||
--- 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
|
end
|
||||||
|
|
||||||
-- ====================
|
local Blur = req("flexlove.Blur")
|
||||||
-- Top level GUI manager
|
local Color = req("flexlove.Color")
|
||||||
-- ====================
|
local ImageDataReader = req("flexlove.ImageDataReader")
|
||||||
|
local NinePatchParser = req("flexlove.NinePatchParser")
|
||||||
|
local ImageScaler = req("flexlove.ImageScaler")
|
||||||
|
local ImageCache = req("flexlove.ImageCache")
|
||||||
|
local ImageRenderer = req("flexlove.ImageRenderer")
|
||||||
|
local Theme = req("flexlove.Theme")
|
||||||
|
local RoundedRect = req("flexlove.RoundedRect")
|
||||||
|
local NineSlice = req("flexlove.NineSlice")
|
||||||
|
local utils = req("flexlove.utils")
|
||||||
|
local constants = req("flexlove.constants")
|
||||||
|
local Units = req("flexlove.Units")
|
||||||
|
local Animation = req("flexlove.Animation")
|
||||||
|
local GuiState = req("flexlove.GuiState")
|
||||||
|
local Grid = req("flexlove.Grid")
|
||||||
|
local InputEvent = req("flexlove.InputEvent")
|
||||||
|
local Element = req("flexlove.Element")
|
||||||
|
|
||||||
---
|
-- Extract from utils
|
||||||
---@class Gui
|
local enums = utils.enums
|
||||||
---@field topElements table<integer, Element>
|
local getModifiers = utils.getModifiers
|
||||||
---@field baseScale {width:number, height:number}?
|
|
||||||
---@field scaleFactors {x:number, y:number}
|
|
||||||
---@field defaultTheme string? -- Default theme name to use for elements
|
|
||||||
local Gui = {
|
|
||||||
topElements = {},
|
|
||||||
baseScale = nil,
|
|
||||||
scaleFactors = { x = 1.0, y = 1.0 },
|
|
||||||
defaultTheme = nil,
|
|
||||||
_cachedViewport = { width = 0, height = 0 }, -- Cached viewport dimensions
|
|
||||||
_focusedElement = nil, -- Currently focused element for keyboard input
|
|
||||||
}
|
|
||||||
|
|
||||||
local Positioning, FlexDirection, JustifyContent, AlignContent, AlignItems, TextAlign, AlignSelf, JustifySelf, FlexWrap =
|
local Positioning, FlexDirection, JustifyContent, AlignContent, AlignItems, TextAlign, AlignSelf, JustifySelf, FlexWrap =
|
||||||
enums.Positioning,
|
enums.Positioning,
|
||||||
@@ -64,135 +48,14 @@ local Positioning, FlexDirection, JustifyContent, AlignContent, AlignItems, Text
|
|||||||
enums.FlexWrap
|
enums.FlexWrap
|
||||||
|
|
||||||
-- ====================
|
-- ====================
|
||||||
-- Grid System
|
-- Top level GUI manager
|
||||||
-- ====================
|
-- ====================
|
||||||
|
|
||||||
--- Simple grid layout calculations
|
---@class Gui
|
||||||
local Grid = {}
|
local Gui = GuiState
|
||||||
|
|
||||||
--- Layout grid items within a grid container using simple row/column counts
|
|
||||||
---@param element Element -- Grid container element
|
|
||||||
function Grid.layoutGridItems(element)
|
|
||||||
local rows = element.gridRows or 1
|
|
||||||
local columns = element.gridColumns or 1
|
|
||||||
|
|
||||||
-- Calculate space reserved by absolutely positioned siblings
|
|
||||||
local reservedLeft = 0
|
|
||||||
local reservedRight = 0
|
|
||||||
local reservedTop = 0
|
|
||||||
local reservedBottom = 0
|
|
||||||
|
|
||||||
for _, child in ipairs(element.children) do
|
|
||||||
-- Only consider absolutely positioned children with explicit positioning
|
|
||||||
if child.positioning == Positioning.ABSOLUTE and child._explicitlyAbsolute then
|
|
||||||
-- BORDER-BOX MODEL: Use border-box dimensions for space calculations
|
|
||||||
local childBorderBoxWidth = child:getBorderBoxWidth()
|
|
||||||
local childBorderBoxHeight = child:getBorderBoxHeight()
|
|
||||||
|
|
||||||
if child.left then
|
|
||||||
reservedLeft = math.max(reservedLeft, child.left + childBorderBoxWidth)
|
|
||||||
end
|
|
||||||
if child.right then
|
|
||||||
reservedRight = math.max(reservedRight, child.right + childBorderBoxWidth)
|
|
||||||
end
|
|
||||||
if child.top then
|
|
||||||
reservedTop = math.max(reservedTop, child.top + childBorderBoxHeight)
|
|
||||||
end
|
|
||||||
if child.bottom then
|
|
||||||
reservedBottom = math.max(reservedBottom, child.bottom + childBorderBoxHeight)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Calculate available space (accounting for padding and reserved space)
|
|
||||||
-- BORDER-BOX MODEL: element.width and element.height are already content dimensions
|
|
||||||
local availableWidth = element.width - reservedLeft - reservedRight
|
|
||||||
local availableHeight = element.height - reservedTop - reservedBottom
|
|
||||||
|
|
||||||
-- Get gaps
|
|
||||||
local columnGap = element.columnGap or 0
|
|
||||||
local rowGap = element.rowGap or 0
|
|
||||||
|
|
||||||
-- Calculate cell sizes (equal distribution)
|
|
||||||
local totalColumnGaps = (columns - 1) * columnGap
|
|
||||||
local totalRowGaps = (rows - 1) * rowGap
|
|
||||||
local cellWidth = (availableWidth - totalColumnGaps) / columns
|
|
||||||
local cellHeight = (availableHeight - totalRowGaps) / rows
|
|
||||||
|
|
||||||
-- Get children that participate in grid layout
|
|
||||||
local gridChildren = {}
|
|
||||||
for _, child in ipairs(element.children) do
|
|
||||||
if not (child.positioning == Positioning.ABSOLUTE and child._explicitlyAbsolute) then
|
|
||||||
table.insert(gridChildren, child)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Place children in grid cells
|
|
||||||
for i, child in ipairs(gridChildren) do
|
|
||||||
-- Calculate row and column (0-indexed for calculation)
|
|
||||||
local index = i - 1
|
|
||||||
local col = index % columns
|
|
||||||
local row = math.floor(index / columns)
|
|
||||||
|
|
||||||
-- Skip if we've exceeded the grid
|
|
||||||
if row >= rows then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Calculate cell position (accounting for reserved space)
|
|
||||||
local cellX = element.x + element.padding.left + reservedLeft + (col * (cellWidth + columnGap))
|
|
||||||
local cellY = element.y + element.padding.top + reservedTop + (row * (cellHeight + rowGap))
|
|
||||||
|
|
||||||
-- Apply alignment within grid cell (default to stretch)
|
|
||||||
local effectiveAlignItems = element.alignItems or AlignItems.STRETCH
|
|
||||||
|
|
||||||
-- Stretch child to fill cell by default
|
|
||||||
-- BORDER-BOX MODEL: Set border-box dimensions, content area adjusts automatically
|
|
||||||
if effectiveAlignItems == AlignItems.STRETCH or effectiveAlignItems == "stretch" then
|
|
||||||
child.x = cellX
|
|
||||||
child.y = cellY
|
|
||||||
child._borderBoxWidth = cellWidth
|
|
||||||
child._borderBoxHeight = cellHeight
|
|
||||||
child.width = math.max(0, cellWidth - child.padding.left - child.padding.right)
|
|
||||||
child.height = math.max(0, cellHeight - child.padding.top - child.padding.bottom)
|
|
||||||
-- Disable auto-sizing when stretched by grid
|
|
||||||
child.autosizing.width = false
|
|
||||||
child.autosizing.height = false
|
|
||||||
elseif effectiveAlignItems == AlignItems.CENTER or effectiveAlignItems == "center" then
|
|
||||||
local childBorderBoxWidth = child:getBorderBoxWidth()
|
|
||||||
local childBorderBoxHeight = child:getBorderBoxHeight()
|
|
||||||
child.x = cellX + (cellWidth - childBorderBoxWidth) / 2
|
|
||||||
child.y = cellY + (cellHeight - childBorderBoxHeight) / 2
|
|
||||||
elseif effectiveAlignItems == AlignItems.FLEX_START or effectiveAlignItems == "flex-start" or effectiveAlignItems == "start" then
|
|
||||||
child.x = cellX
|
|
||||||
child.y = cellY
|
|
||||||
elseif effectiveAlignItems == AlignItems.FLEX_END or effectiveAlignItems == "flex-end" or effectiveAlignItems == "end" then
|
|
||||||
local childBorderBoxWidth = child:getBorderBoxWidth()
|
|
||||||
local childBorderBoxHeight = child:getBorderBoxHeight()
|
|
||||||
child.x = cellX + cellWidth - childBorderBoxWidth
|
|
||||||
child.y = cellY + cellHeight - childBorderBoxHeight
|
|
||||||
else
|
|
||||||
-- Default to stretch
|
|
||||||
child.x = cellX
|
|
||||||
child.y = cellY
|
|
||||||
child._borderBoxWidth = cellWidth
|
|
||||||
child._borderBoxHeight = cellHeight
|
|
||||||
child.width = math.max(0, cellWidth - child.padding.left - child.padding.right)
|
|
||||||
child.height = math.max(0, cellHeight - child.padding.top - child.padding.bottom)
|
|
||||||
-- Disable auto-sizing when stretched by grid
|
|
||||||
child.autosizing.width = false
|
|
||||||
child.autosizing.height = false
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Layout child's children if it has any
|
|
||||||
if #child.children > 0 then
|
|
||||||
child:layoutChildren()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Initialize FlexLove with configuration
|
--- Initialize FlexLove with configuration
|
||||||
---@param config {baseScale?: {width?:number, height?:number}, theme?: string|ThemeDefinition} --Default: {width: 1920, height: 1080}
|
---@param config {baseScale?: {width?:number, height?:number}, theme?: string|ThemeDefinition}
|
||||||
function Gui.init(config)
|
function Gui.init(config)
|
||||||
if config.baseScale then
|
if config.baseScale then
|
||||||
Gui.baseScale = {
|
Gui.baseScale = {
|
||||||
@@ -200,22 +63,18 @@ function Gui.init(config)
|
|||||||
height = config.baseScale.height or 1080,
|
height = config.baseScale.height or 1080,
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Calculate initial scale factors
|
|
||||||
local currentWidth, currentHeight = Units.getViewport()
|
local currentWidth, currentHeight = Units.getViewport()
|
||||||
Gui.scaleFactors.x = currentWidth / Gui.baseScale.width
|
Gui.scaleFactors.x = currentWidth / Gui.baseScale.width
|
||||||
Gui.scaleFactors.y = currentHeight / Gui.baseScale.height
|
Gui.scaleFactors.y = currentHeight / Gui.baseScale.height
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Load and set theme if specified
|
|
||||||
if config.theme then
|
if config.theme then
|
||||||
local success, err = pcall(function()
|
local success, err = pcall(function()
|
||||||
if type(config.theme) == "string" then
|
if type(config.theme) == "string" then
|
||||||
-- Load theme by name
|
|
||||||
Theme.load(config.theme)
|
Theme.load(config.theme)
|
||||||
Theme.setActive(config.theme)
|
Theme.setActive(config.theme)
|
||||||
Gui.defaultTheme = config.theme
|
Gui.defaultTheme = config.theme
|
||||||
elseif type(config.theme) == "table" then
|
elseif type(config.theme) == "table" then
|
||||||
-- Load theme from definition
|
|
||||||
local theme = Theme.new(config.theme)
|
local theme = Theme.new(config.theme)
|
||||||
Theme.setActive(theme)
|
Theme.setActive(theme)
|
||||||
Gui.defaultTheme = theme.name
|
Gui.defaultTheme = theme.name
|
||||||
@@ -238,8 +97,6 @@ function Gui.isOccluded(elem, clickX, clickY)
|
|||||||
if element.z > elem.z and element:contains(clickX, clickY) then
|
if element.z > elem.z and element:contains(clickX, clickY) then
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
--TODO: check if walking the children tree is necessary here - might only need to check for absolute positioned
|
|
||||||
--children
|
|
||||||
for _, child in ipairs(element.children) do
|
for _, child in ipairs(element.children) do
|
||||||
if child.positioning == "absolute" then
|
if child.positioning == "absolute" then
|
||||||
if child.z > elem.z and child:contains(clickX, clickY) then
|
if child.z > elem.z and child:contains(clickX, clickY) then
|
||||||
@@ -251,36 +108,16 @@ function Gui.isOccluded(elem, clickX, clickY)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Get current scale factors
|
|
||||||
---@return number, number -- scaleX, scaleY
|
|
||||||
function Gui.getScaleFactors()
|
|
||||||
return Gui.scaleFactors.x, Gui.scaleFactors.y
|
|
||||||
end
|
|
||||||
|
|
||||||
function Gui.resize()
|
function Gui.resize()
|
||||||
local newWidth, newHeight = love.window.getMode()
|
local newWidth, newHeight = love.window.getMode()
|
||||||
|
|
||||||
-- Update scale factors if base scale is set
|
|
||||||
if Gui.baseScale then
|
if Gui.baseScale then
|
||||||
Gui.scaleFactors.x = newWidth / Gui.baseScale.width
|
Gui.scaleFactors.x = newWidth / Gui.baseScale.width
|
||||||
Gui.scaleFactors.y = newHeight / Gui.baseScale.height
|
Gui.scaleFactors.y = newHeight / Gui.baseScale.height
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Clear scaled region caches for all themes
|
|
||||||
for _, theme in pairs(themes) do
|
|
||||||
if theme.components then
|
|
||||||
for _, component in pairs(theme.components) do
|
|
||||||
if component._scaledRegionCache then
|
|
||||||
component._scaledRegionCache = {}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Clear blur canvas cache on resize
|
|
||||||
Blur.clearCache()
|
Blur.clearCache()
|
||||||
|
|
||||||
-- Clear game/backdrop canvas cache on resize (will be recreated with new dimensions)
|
|
||||||
Gui._gameCanvas = nil
|
Gui._gameCanvas = nil
|
||||||
Gui._backdropCanvas = nil
|
Gui._backdropCanvas = nil
|
||||||
Gui._canvasDimensions = { width = 0, height = 0 }
|
Gui._canvasDimensions = { width = 0, height = 0 }
|
||||||
@@ -290,33 +127,20 @@ function Gui.resize()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Canvas cache for game rendering (reused across frames)
|
-- Canvas cache for game rendering
|
||||||
Gui._gameCanvas = nil
|
Gui._gameCanvas = nil
|
||||||
Gui._backdropCanvas = nil
|
Gui._backdropCanvas = nil
|
||||||
Gui._canvasDimensions = { width = 0, height = 0 }
|
Gui._canvasDimensions = { width = 0, height = 0 }
|
||||||
|
|
||||||
---@param gameDrawFunc function|nil -- Function to draw game content, needed for backdrop blur
|
---@param gameDrawFunc function|nil
|
||||||
---@param postDrawFunc function|nil -- Optional function to draw after GUI (for top-level shaders/effects)
|
---@param postDrawFunc function|nil
|
||||||
---function love.draw()
|
|
||||||
--- FlexLove.Gui.draw(function()
|
|
||||||
--- --Game rendering logic
|
|
||||||
--- RenderSystem:update()
|
|
||||||
--- end, function()
|
|
||||||
--- -- Layers on top of GUI - blurs will not extend to this
|
|
||||||
--- overlayStats.draw()
|
|
||||||
--- end)
|
|
||||||
---end
|
|
||||||
function Gui.draw(gameDrawFunc, postDrawFunc)
|
function Gui.draw(gameDrawFunc, postDrawFunc)
|
||||||
-- Save the current canvas state to support nested rendering
|
|
||||||
local outerCanvas = love.graphics.getCanvas()
|
local outerCanvas = love.graphics.getCanvas()
|
||||||
|
|
||||||
local gameCanvas = nil
|
local gameCanvas = nil
|
||||||
|
|
||||||
-- Render game content to a canvas if function provided
|
|
||||||
if type(gameDrawFunc) == "function" then
|
if type(gameDrawFunc) == "function" then
|
||||||
local width, height = love.graphics.getDimensions()
|
local width, height = love.graphics.getDimensions()
|
||||||
|
|
||||||
-- Recreate canvases only if dimensions changed or canvas doesn't exist
|
|
||||||
if not Gui._gameCanvas or Gui._canvasDimensions.width ~= width or Gui._canvasDimensions.height ~= height then
|
if not Gui._gameCanvas or Gui._canvasDimensions.width ~= width or Gui._canvasDimensions.height ~= height then
|
||||||
Gui._gameCanvas = love.graphics.newCanvas(width, height)
|
Gui._gameCanvas = love.graphics.newCanvas(width, height)
|
||||||
Gui._backdropCanvas = love.graphics.newCanvas(width, height)
|
Gui._backdropCanvas = love.graphics.newCanvas(width, height)
|
||||||
@@ -328,20 +152,17 @@ function Gui.draw(gameDrawFunc, postDrawFunc)
|
|||||||
|
|
||||||
love.graphics.setCanvas(gameCanvas)
|
love.graphics.setCanvas(gameCanvas)
|
||||||
love.graphics.clear()
|
love.graphics.clear()
|
||||||
gameDrawFunc() -- Call the drawing function
|
gameDrawFunc()
|
||||||
love.graphics.setCanvas(outerCanvas)
|
love.graphics.setCanvas(outerCanvas)
|
||||||
|
|
||||||
-- Draw game canvas to the outer canvas (or screen if none)
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
love.graphics.draw(gameCanvas, 0, 0)
|
love.graphics.draw(gameCanvas, 0, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Sort elements by z-index before drawing
|
|
||||||
table.sort(Gui.topElements, function(a, b)
|
table.sort(Gui.topElements, function(a, b)
|
||||||
return a.z < b.z
|
return a.z < b.z
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- Check if any element (recursively) needs backdrop blur
|
|
||||||
local function hasBackdropBlur(element)
|
local function hasBackdropBlur(element)
|
||||||
if element.backdropBlur and element.backdropBlur.intensity > 0 then
|
if element.backdropBlur and element.backdropBlur.intensity > 0 then
|
||||||
return true
|
return true
|
||||||
@@ -362,109 +183,89 @@ function Gui.draw(gameDrawFunc, postDrawFunc)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- If backdrop blur is needed, render to a progressive canvas
|
|
||||||
if needsBackdropCanvas and gameCanvas then
|
if needsBackdropCanvas and gameCanvas then
|
||||||
local backdropCanvas = Gui._backdropCanvas
|
local backdropCanvas = Gui._backdropCanvas
|
||||||
local prevColor = { love.graphics.getColor() }
|
local prevColor = { love.graphics.getColor() }
|
||||||
|
|
||||||
-- Initialize backdrop canvas with game content
|
|
||||||
love.graphics.setCanvas(backdropCanvas)
|
love.graphics.setCanvas(backdropCanvas)
|
||||||
love.graphics.clear()
|
love.graphics.clear()
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
love.graphics.draw(gameCanvas, 0, 0)
|
love.graphics.draw(gameCanvas, 0, 0)
|
||||||
|
|
||||||
-- Reset to outer canvas (screen or parent canvas)
|
|
||||||
love.graphics.setCanvas(outerCanvas)
|
love.graphics.setCanvas(outerCanvas)
|
||||||
love.graphics.setColor(unpack(prevColor))
|
love.graphics.setColor(unpack(prevColor))
|
||||||
|
|
||||||
-- Draw each element, updating backdrop canvas progressively
|
|
||||||
for _, win in ipairs(Gui.topElements) do
|
for _, win in ipairs(Gui.topElements) do
|
||||||
-- Draw element with current backdrop state to outer canvas
|
|
||||||
win:draw(backdropCanvas)
|
win:draw(backdropCanvas)
|
||||||
|
|
||||||
-- Update backdrop canvas to include this element (for next elements)
|
|
||||||
love.graphics.setCanvas(backdropCanvas)
|
love.graphics.setCanvas(backdropCanvas)
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
win:draw(nil) -- Draw without backdrop blur to the backdrop canvas
|
win:draw(nil)
|
||||||
love.graphics.setCanvas(outerCanvas) -- Reset to outer canvas
|
love.graphics.setCanvas(outerCanvas)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
-- No backdrop blur needed, draw normally
|
|
||||||
for _, win in ipairs(Gui.topElements) do
|
for _, win in ipairs(Gui.topElements) do
|
||||||
win:draw(nil)
|
win:draw(nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Call post-draw function if provided (for top-level shaders/effects)
|
|
||||||
if type(postDrawFunc) == "function" then
|
if type(postDrawFunc) == "function" then
|
||||||
postDrawFunc()
|
postDrawFunc()
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Restore the original canvas state
|
|
||||||
love.graphics.setCanvas(outerCanvas)
|
love.graphics.setCanvas(outerCanvas)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Find the topmost element at given coordinates (considering z-index)
|
--- Find the topmost element at given coordinates
|
||||||
---@param x number
|
---@param x number
|
||||||
---@param y number
|
---@param y number
|
||||||
---@return Element? -- Returns the topmost element or nil
|
---@return Element?
|
||||||
function Gui.getElementAtPosition(x, y)
|
function Gui.getElementAtPosition(x, y)
|
||||||
local candidates = {}
|
local candidates = {}
|
||||||
|
|
||||||
-- Recursively collect all elements that contain the point
|
|
||||||
local function collectHits(element)
|
local function collectHits(element)
|
||||||
-- Check if point is within element bounds
|
|
||||||
local bx = element.x
|
local bx = element.x
|
||||||
local by = element.y
|
local by = element.y
|
||||||
local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
|
local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
|
||||||
local bh = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom)
|
local bh = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom)
|
||||||
|
|
||||||
if x >= bx and x <= bx + bw and y >= by and y <= by + bh then
|
if x >= bx and x <= bx + bw and y >= by and y <= by + bh then
|
||||||
-- Only consider elements with callbacks (interactive elements)
|
|
||||||
if element.callback and not element.disabled then
|
if element.callback and not element.disabled then
|
||||||
table.insert(candidates, element)
|
table.insert(candidates, element)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check children
|
|
||||||
for _, child in ipairs(element.children) do
|
for _, child in ipairs(element.children) do
|
||||||
collectHits(child)
|
collectHits(child)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Collect hits from all top-level elements
|
|
||||||
for _, element in ipairs(Gui.topElements) do
|
for _, element in ipairs(Gui.topElements) do
|
||||||
collectHits(element)
|
collectHits(element)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Sort by z-index (highest first)
|
|
||||||
table.sort(candidates, function(a, b)
|
table.sort(candidates, function(a, b)
|
||||||
return a.z > b.z
|
return a.z > b.z
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- Return the topmost element (highest z-index)
|
|
||||||
return candidates[1]
|
return candidates[1]
|
||||||
end
|
end
|
||||||
|
|
||||||
function Gui.update(dt)
|
function Gui.update(dt)
|
||||||
-- Reset event handling flags for new frame
|
|
||||||
local mx, my = love.mouse.getPosition()
|
local mx, my = love.mouse.getPosition()
|
||||||
local topElement = Gui.getElementAtPosition(mx, my)
|
local topElement = Gui.getElementAtPosition(mx, my)
|
||||||
|
|
||||||
-- Mark which element should handle events this frame
|
|
||||||
Gui._activeEventElement = topElement
|
Gui._activeEventElement = topElement
|
||||||
|
|
||||||
-- Update all elements
|
|
||||||
for _, win in ipairs(Gui.topElements) do
|
for _, win in ipairs(Gui.topElements) do
|
||||||
win:update(dt)
|
win:update(dt)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Clear active element for next frame
|
|
||||||
Gui._activeEventElement = nil
|
Gui._activeEventElement = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Forward text input to focused element
|
--- Forward text input to focused element
|
||||||
---@param text string -- Character input
|
---@param text string
|
||||||
function Gui.textinput(text)
|
function Gui.textinput(text)
|
||||||
if Gui._focusedElement then
|
if Gui._focusedElement then
|
||||||
Gui._focusedElement:textinput(text)
|
Gui._focusedElement:textinput(text)
|
||||||
@@ -472,9 +273,9 @@ function Gui.textinput(text)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Forward key press to focused element
|
--- Forward key press to focused element
|
||||||
---@param key string -- Key name
|
---@param key string
|
||||||
---@param scancode string -- Scancode
|
---@param scancode string
|
||||||
---@param isrepeat boolean -- Whether this is a key repeat
|
---@param isrepeat boolean
|
||||||
function Gui.keypressed(key, scancode, isrepeat)
|
function Gui.keypressed(key, scancode, isrepeat)
|
||||||
if Gui._focusedElement then
|
if Gui._focusedElement then
|
||||||
Gui._focusedElement:keypressed(key, scancode, isrepeat)
|
Gui._focusedElement:keypressed(key, scancode, isrepeat)
|
||||||
@@ -483,23 +284,18 @@ end
|
|||||||
|
|
||||||
--- Handle mouse wheel scrolling
|
--- Handle mouse wheel scrolling
|
||||||
function Gui.wheelmoved(x, y)
|
function Gui.wheelmoved(x, y)
|
||||||
-- Get mouse position
|
|
||||||
local mx, my = love.mouse.getPosition()
|
local mx, my = love.mouse.getPosition()
|
||||||
|
|
||||||
-- Find the deepest scrollable element at mouse position
|
|
||||||
local function findScrollableAtPosition(elements, mx, my)
|
local function findScrollableAtPosition(elements, mx, my)
|
||||||
-- Check in reverse z-order (top to bottom)
|
|
||||||
for i = #elements, 1, -1 do
|
for i = #elements, 1, -1 do
|
||||||
local element = elements[i]
|
local element = elements[i]
|
||||||
|
|
||||||
-- Check if mouse is over element
|
|
||||||
local bx = element.x
|
local bx = element.x
|
||||||
local by = element.y
|
local by = element.y
|
||||||
local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
|
local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
|
||||||
local bh = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom)
|
local bh = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom)
|
||||||
|
|
||||||
if mx >= bx and mx <= bx + bw and my >= by and my <= by + bh then
|
if mx >= bx and mx <= bx + bw and my >= by and my <= by + bh then
|
||||||
-- Check children first (depth-first)
|
|
||||||
if #element.children > 0 then
|
if #element.children > 0 then
|
||||||
local childResult = findScrollableAtPosition(element.children, mx, my)
|
local childResult = findScrollableAtPosition(element.children, mx, my)
|
||||||
if childResult then
|
if childResult then
|
||||||
@@ -507,7 +303,6 @@ function Gui.wheelmoved(x, y)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check if this element is scrollable
|
|
||||||
local overflowX = element.overflowX or element.overflow
|
local overflowX = element.overflowX or element.overflow
|
||||||
local overflowY = element.overflowY or element.overflow
|
local overflowY = element.overflowY or element.overflow
|
||||||
if (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto") and (element._overflowX or element._overflowY) then
|
if (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto") and (element._overflowX or element._overflowY) then
|
||||||
@@ -531,67 +326,15 @@ function Gui.destroy()
|
|||||||
win:destroy()
|
win:destroy()
|
||||||
end
|
end
|
||||||
Gui.topElements = {}
|
Gui.topElements = {}
|
||||||
-- Reset base scale and scale factors
|
|
||||||
Gui.baseScale = nil
|
Gui.baseScale = nil
|
||||||
Gui.scaleFactors = { x = 1.0, y = 1.0 }
|
Gui.scaleFactors = { x = 1.0, y = 1.0 }
|
||||||
-- Reset cached viewport
|
|
||||||
Gui._cachedViewport = { width = 0, height = 0 }
|
Gui._cachedViewport = { width = 0, height = 0 }
|
||||||
-- Clear game/backdrop canvas cache
|
|
||||||
Gui._gameCanvas = nil
|
Gui._gameCanvas = nil
|
||||||
Gui._backdropCanvas = nil
|
Gui._backdropCanvas = nil
|
||||||
Gui._canvasDimensions = { width = 0, height = 0 }
|
Gui._canvasDimensions = { width = 0, height = 0 }
|
||||||
-- Clear focused element
|
|
||||||
Gui._focusedElement = nil
|
Gui._focusedElement = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Simple GUI library for LOVE2D
|
|
||||||
-- Provides element and button creation, drawing, and click handling.
|
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- Event System
|
|
||||||
-- ====================
|
|
||||||
|
|
||||||
---@class InputEvent
|
|
||||||
---@field type "click"|"press"|"release"|"rightclick"|"middleclick"|"drag"
|
|
||||||
---@field button number -- Mouse button: 1 (left), 2 (right), 3 (middle)
|
|
||||||
---@field x number -- Mouse X position
|
|
||||||
---@field y number -- Mouse Y position
|
|
||||||
---@field dx number? -- Delta X from drag start (only for drag events)
|
|
||||||
---@field dy number? -- Delta Y from drag start (only for drag events)
|
|
||||||
---@field modifiers {shift:boolean, ctrl:boolean, alt:boolean, super:boolean}
|
|
||||||
---@field clickCount number -- Number of clicks (for double/triple click detection)
|
|
||||||
---@field timestamp number -- Time when event occurred
|
|
||||||
local InputEvent = {}
|
|
||||||
InputEvent.__index = InputEvent
|
|
||||||
|
|
||||||
---@class InputEventProps
|
|
||||||
---@field type "click"|"press"|"release"|"rightclick"|"middleclick"|"drag"
|
|
||||||
---@field button number
|
|
||||||
---@field x number
|
|
||||||
---@field y number
|
|
||||||
---@field dx number?
|
|
||||||
---@field dy number?
|
|
||||||
---@field modifiers {shift:boolean, ctrl:boolean, alt:boolean, super:boolean}
|
|
||||||
---@field clickCount number?
|
|
||||||
---@field timestamp number?
|
|
||||||
|
|
||||||
--- Create a new input event
|
|
||||||
---@param props InputEventProps
|
|
||||||
---@return InputEvent
|
|
||||||
function InputEvent.new(props)
|
|
||||||
local self = setmetatable({}, InputEvent)
|
|
||||||
self.type = props.type
|
|
||||||
self.button = props.button
|
|
||||||
self.x = props.x
|
|
||||||
self.y = props.y
|
|
||||||
self.dx = props.dx
|
|
||||||
self.dy = props.dy
|
|
||||||
self.modifiers = props.modifiers
|
|
||||||
self.clickCount = props.clickCount or 1
|
|
||||||
self.timestamp = props.timestamp or love.timer.getTime()
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
Gui.new = Element.new
|
Gui.new = Element.new
|
||||||
Gui.Element = Element
|
Gui.Element = Element
|
||||||
Gui.Animation = Animation
|
Gui.Animation = Animation
|
||||||
@@ -616,6 +359,8 @@ return {
|
|||||||
Theme = Theme,
|
Theme = Theme,
|
||||||
RoundedRect = RoundedRect,
|
RoundedRect = RoundedRect,
|
||||||
NineSlice = NineSlice,
|
NineSlice = NineSlice,
|
||||||
|
Grid = Grid,
|
||||||
|
InputEvent = InputEvent,
|
||||||
|
|
||||||
-- Enums (individual)
|
-- Enums (individual)
|
||||||
Positioning = Positioning,
|
Positioning = Positioning,
|
||||||
|
|||||||
@@ -2,6 +2,47 @@
|
|||||||
-- Element Object
|
-- Element Object
|
||||||
-- ====================
|
-- ====================
|
||||||
|
|
||||||
|
-- Module dependencies (using relative paths)
|
||||||
|
local modulePath = (...):match("(.-)[^%.]+$")
|
||||||
|
local function req(name)
|
||||||
|
return require(modulePath .. name)
|
||||||
|
end
|
||||||
|
|
||||||
|
local GuiState = req("GuiState")
|
||||||
|
local Theme = req("Theme")
|
||||||
|
local Color = req("Color")
|
||||||
|
local Units = req("Units")
|
||||||
|
local Blur = req("Blur")
|
||||||
|
local ImageRenderer = req("ImageRenderer")
|
||||||
|
local NineSlice = req("NineSlice")
|
||||||
|
local RoundedRect = req("RoundedRect")
|
||||||
|
local Animation = req("Animation")
|
||||||
|
local ImageCache = req("ImageCache")
|
||||||
|
local utils = req("utils")
|
||||||
|
local constants = req("constants")
|
||||||
|
local Grid = req("Grid")
|
||||||
|
local InputEvent = req("InputEvent")
|
||||||
|
|
||||||
|
-- Extract utilities
|
||||||
|
local enums = utils.enums
|
||||||
|
local FONT_CACHE = utils.FONT_CACHE
|
||||||
|
local resolveTextSizePreset = utils.resolveTextSizePreset
|
||||||
|
local getModifiers = utils.getModifiers
|
||||||
|
|
||||||
|
-- Extract enum values
|
||||||
|
local Positioning = enums.Positioning
|
||||||
|
local FlexDirection = enums.FlexDirection
|
||||||
|
local JustifyContent = enums.JustifyContent
|
||||||
|
local AlignContent = enums.AlignContent
|
||||||
|
local AlignItems = enums.AlignItems
|
||||||
|
local TextAlign = enums.TextAlign
|
||||||
|
local AlignSelf = enums.AlignSelf
|
||||||
|
local JustifySelf = enums.JustifySelf
|
||||||
|
local FlexWrap = enums.FlexWrap
|
||||||
|
|
||||||
|
-- Reference to Gui (via GuiState)
|
||||||
|
local Gui = GuiState
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
INTERNAL FIELD NAMING CONVENTIONS:
|
INTERNAL FIELD NAMING CONVENTIONS:
|
||||||
---------------------------------
|
---------------------------------
|
||||||
@@ -191,7 +232,7 @@ function Element.new(props)
|
|||||||
self.contentAutoSizingMultiplier = props.contentAutoSizingMultiplier
|
self.contentAutoSizingMultiplier = props.contentAutoSizingMultiplier
|
||||||
else
|
else
|
||||||
-- Try to source from theme
|
-- Try to source from theme
|
||||||
local themeToUse = self.theme and themes[self.theme] or Theme.getActive()
|
local themeToUse = self.theme and Theme.get(self.theme) or Theme.getActive()
|
||||||
if themeToUse then
|
if themeToUse then
|
||||||
-- First check if themeComponent has a multiplier
|
-- First check if themeComponent has a multiplier
|
||||||
if self.themeComponent then
|
if self.themeComponent then
|
||||||
@@ -412,7 +453,7 @@ function Element.new(props)
|
|||||||
self.fontFamily = self.parent.fontFamily
|
self.fontFamily = self.parent.fontFamily
|
||||||
elseif props.themeComponent then
|
elseif props.themeComponent then
|
||||||
-- If using themeComponent, try to get default from theme
|
-- If using themeComponent, try to get default from theme
|
||||||
local themeToUse = self.theme and themes[self.theme] or Theme.getActive()
|
local themeToUse = self.theme and Theme.get(self.theme) or Theme.getActive()
|
||||||
if themeToUse and themeToUse.fonts and themeToUse.fonts["default"] then
|
if themeToUse and themeToUse.fonts and themeToUse.fonts["default"] then
|
||||||
self.fontFamily = "default"
|
self.fontFamily = "default"
|
||||||
else
|
else
|
||||||
@@ -572,7 +613,7 @@ function Element.new(props)
|
|||||||
local use9PatchPadding = false
|
local use9PatchPadding = false
|
||||||
local ninePatchContentPadding = nil
|
local ninePatchContentPadding = nil
|
||||||
if self.themeComponent then
|
if self.themeComponent then
|
||||||
local themeToUse = self.theme and themes[self.theme] or Theme.getActive()
|
local themeToUse = self.theme and Theme.get(self.theme) or Theme.getActive()
|
||||||
if themeToUse and themeToUse.components[self.themeComponent] then
|
if themeToUse and themeToUse.components[self.themeComponent] then
|
||||||
local component = themeToUse.components[self.themeComponent]
|
local component = themeToUse.components[self.themeComponent]
|
||||||
if component._ninePatchData and component._ninePatchData.contentPadding then
|
if component._ninePatchData and component._ninePatchData.contentPadding then
|
||||||
@@ -602,7 +643,7 @@ function Element.new(props)
|
|||||||
-- Scale 9-patch content padding to match the actual rendered size
|
-- Scale 9-patch content padding to match the actual rendered size
|
||||||
-- The contentPadding values are in the original image's pixel coordinates,
|
-- The contentPadding values are in the original image's pixel coordinates,
|
||||||
-- but we need to scale them proportionally to the element's actual size
|
-- but we need to scale them proportionally to the element's actual size
|
||||||
local themeToUse = self.theme and themes[self.theme] or Theme.getActive()
|
local themeToUse = self.theme and Theme.get(self.theme) or Theme.getActive()
|
||||||
if themeToUse and themeToUse.components[self.themeComponent] then
|
if themeToUse and themeToUse.components[self.themeComponent] then
|
||||||
local component = themeToUse.components[self.themeComponent]
|
local component = themeToUse.components[self.themeComponent]
|
||||||
local atlasImage = component._loadedAtlas or themeToUse.atlas
|
local atlasImage = component._loadedAtlas or themeToUse.atlas
|
||||||
@@ -825,7 +866,7 @@ function Element.new(props)
|
|||||||
self.textColor = props.textColor
|
self.textColor = props.textColor
|
||||||
else
|
else
|
||||||
-- Try to get text color from theme
|
-- Try to get text color from theme
|
||||||
local themeToUse = self.theme and themes[self.theme] or Theme.getActive()
|
local themeToUse = self.theme and Theme.get(self.theme) or Theme.getActive()
|
||||||
if themeToUse and themeToUse.colors and themeToUse.colors.text then
|
if themeToUse and themeToUse.colors and themeToUse.colors.text then
|
||||||
self.textColor = themeToUse.colors.text
|
self.textColor = themeToUse.colors.text
|
||||||
else
|
else
|
||||||
@@ -956,7 +997,7 @@ function Element.new(props)
|
|||||||
self.textColor = self.parent.textColor
|
self.textColor = self.parent.textColor
|
||||||
else
|
else
|
||||||
-- Try to get text color from theme
|
-- Try to get text color from theme
|
||||||
local themeToUse = self.theme and themes[self.theme] or Theme.getActive()
|
local themeToUse = self.theme and Theme.get(self.theme) or Theme.getActive()
|
||||||
if themeToUse and themeToUse.colors and themeToUse.colors.text then
|
if themeToUse and themeToUse.colors and themeToUse.colors.text then
|
||||||
self.textColor = themeToUse.colors.text
|
self.textColor = themeToUse.colors.text
|
||||||
else
|
else
|
||||||
@@ -1620,7 +1661,7 @@ function Element:getScaledContentPadding()
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local themeToUse = self.theme and themes[self.theme] or Theme.getActive()
|
local themeToUse = self.theme and Theme.get(self.theme) or Theme.getActive()
|
||||||
if not themeToUse or not themeToUse.components[self.themeComponent] then
|
if not themeToUse or not themeToUse.components[self.themeComponent] then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
@@ -2330,14 +2371,14 @@ function Element:draw(backdropCanvas)
|
|||||||
local themeToUse = nil
|
local themeToUse = nil
|
||||||
if self.theme then
|
if self.theme then
|
||||||
-- Element specifies a specific theme - load it if needed
|
-- Element specifies a specific theme - load it if needed
|
||||||
if themes[self.theme] then
|
if Theme.get(self.theme) then
|
||||||
themeToUse = themes[self.theme]
|
themeToUse = Theme.get(self.theme)
|
||||||
else
|
else
|
||||||
-- Try to load the theme
|
-- Try to load the theme
|
||||||
pcall(function()
|
pcall(function()
|
||||||
Theme.load(self.theme)
|
Theme.load(self.theme)
|
||||||
end)
|
end)
|
||||||
themeToUse = themes[self.theme]
|
themeToUse = Theme.get(self.theme)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
-- Use active theme
|
-- Use active theme
|
||||||
@@ -2423,7 +2464,7 @@ function Element:draw(backdropCanvas)
|
|||||||
local fontPath = nil
|
local fontPath = nil
|
||||||
if self.fontFamily then
|
if self.fontFamily then
|
||||||
-- Check if fontFamily is a theme font name
|
-- Check if fontFamily is a theme font name
|
||||||
local themeToUse = self.theme and themes[self.theme] or Theme.getActive()
|
local themeToUse = self.theme and Theme.get(self.theme) or Theme.getActive()
|
||||||
if themeToUse and themeToUse.fonts and themeToUse.fonts[self.fontFamily] then
|
if themeToUse and themeToUse.fonts and themeToUse.fonts[self.fontFamily] then
|
||||||
fontPath = themeToUse.fonts[self.fontFamily]
|
fontPath = themeToUse.fonts[self.fontFamily]
|
||||||
else
|
else
|
||||||
@@ -2432,7 +2473,7 @@ function Element:draw(backdropCanvas)
|
|||||||
end
|
end
|
||||||
elseif self.themeComponent then
|
elseif self.themeComponent then
|
||||||
-- If using themeComponent but no fontFamily specified, check for default font in theme
|
-- If using themeComponent but no fontFamily specified, check for default font in theme
|
||||||
local themeToUse = self.theme and themes[self.theme] or Theme.getActive()
|
local themeToUse = self.theme and Theme.get(self.theme) or Theme.getActive()
|
||||||
if themeToUse and themeToUse.fonts and themeToUse.fonts.default then
|
if themeToUse and themeToUse.fonts and themeToUse.fonts.default then
|
||||||
fontPath = themeToUse.fonts.default
|
fontPath = themeToUse.fonts.default
|
||||||
end
|
end
|
||||||
@@ -3221,14 +3262,14 @@ function Element:calculateTextWidth()
|
|||||||
-- Resolve font path from font family (same logic as in draw)
|
-- Resolve font path from font family (same logic as in draw)
|
||||||
local fontPath = nil
|
local fontPath = nil
|
||||||
if self.fontFamily then
|
if self.fontFamily then
|
||||||
local themeToUse = self.theme and themes[self.theme] or Theme.getActive()
|
local themeToUse = self.theme and Theme.get(self.theme) or Theme.getActive()
|
||||||
if themeToUse and themeToUse.fonts and themeToUse.fonts[self.fontFamily] then
|
if themeToUse and themeToUse.fonts and themeToUse.fonts[self.fontFamily] then
|
||||||
fontPath = themeToUse.fonts[self.fontFamily]
|
fontPath = themeToUse.fonts[self.fontFamily]
|
||||||
else
|
else
|
||||||
fontPath = self.fontFamily
|
fontPath = self.fontFamily
|
||||||
end
|
end
|
||||||
elseif self.themeComponent then
|
elseif self.themeComponent then
|
||||||
local themeToUse = self.theme and themes[self.theme] or Theme.getActive()
|
local themeToUse = self.theme and Theme.get(self.theme) or Theme.getActive()
|
||||||
if themeToUse and themeToUse.fonts and themeToUse.fonts.default then
|
if themeToUse and themeToUse.fonts and themeToUse.fonts.default then
|
||||||
fontPath = themeToUse.fonts.default
|
fontPath = themeToUse.fonts.default
|
||||||
end
|
end
|
||||||
@@ -3264,14 +3305,14 @@ function Element:calculateTextHeight()
|
|||||||
-- Resolve font path from font family (same logic as in draw)
|
-- Resolve font path from font family (same logic as in draw)
|
||||||
local fontPath = nil
|
local fontPath = nil
|
||||||
if self.fontFamily then
|
if self.fontFamily then
|
||||||
local themeToUse = self.theme and themes[self.theme] or Theme.getActive()
|
local themeToUse = self.theme and Theme.get(self.theme) or Theme.getActive()
|
||||||
if themeToUse and themeToUse.fonts and themeToUse.fonts[self.fontFamily] then
|
if themeToUse and themeToUse.fonts and themeToUse.fonts[self.fontFamily] then
|
||||||
fontPath = themeToUse.fonts[self.fontFamily]
|
fontPath = themeToUse.fonts[self.fontFamily]
|
||||||
else
|
else
|
||||||
fontPath = self.fontFamily
|
fontPath = self.fontFamily
|
||||||
end
|
end
|
||||||
elseif self.themeComponent then
|
elseif self.themeComponent then
|
||||||
local themeToUse = self.theme and themes[self.theme] or Theme.getActive()
|
local themeToUse = self.theme and Theme.get(self.theme) or Theme.getActive()
|
||||||
if themeToUse and themeToUse.fonts and themeToUse.fonts.default then
|
if themeToUse and themeToUse.fonts and themeToUse.fonts.default then
|
||||||
fontPath = themeToUse.fonts.default
|
fontPath = themeToUse.fonts.default
|
||||||
end
|
end
|
||||||
@@ -3917,7 +3958,7 @@ function Element:_getFont()
|
|||||||
-- Get font path from theme or element
|
-- Get font path from theme or element
|
||||||
local fontPath = nil
|
local fontPath = nil
|
||||||
if self.fontFamily then
|
if self.fontFamily then
|
||||||
local themeToUse = self.theme and themes[self.theme] or Theme.getActive()
|
local themeToUse = self.theme and Theme.get(self.theme) or Theme.getActive()
|
||||||
if themeToUse and themeToUse.fonts and themeToUse.fonts[self.fontFamily] then
|
if themeToUse and themeToUse.fonts and themeToUse.fonts[self.fontFamily] then
|
||||||
fontPath = themeToUse.fonts[self.fontFamily]
|
fontPath = themeToUse.fonts[self.fontFamily]
|
||||||
else
|
else
|
||||||
|
|||||||
136
flexlove/Grid.lua
Normal file
136
flexlove/Grid.lua
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
-- ====================
|
||||||
|
-- Grid Layout System
|
||||||
|
-- ====================
|
||||||
|
|
||||||
|
local modulePath = (...):match("(.-)[^%.]+$")
|
||||||
|
local utils = require(modulePath .. "utils")
|
||||||
|
local enums = utils.enums
|
||||||
|
|
||||||
|
local Positioning = enums.Positioning
|
||||||
|
local AlignItems = enums.AlignItems
|
||||||
|
|
||||||
|
--- Simple grid layout calculations
|
||||||
|
local Grid = {}
|
||||||
|
|
||||||
|
--- Layout grid items within a grid container using simple row/column counts
|
||||||
|
---@param element Element -- Grid container element
|
||||||
|
function Grid.layoutGridItems(element)
|
||||||
|
local rows = element.gridRows or 1
|
||||||
|
local columns = element.gridColumns or 1
|
||||||
|
|
||||||
|
-- Calculate space reserved by absolutely positioned siblings
|
||||||
|
local reservedLeft = 0
|
||||||
|
local reservedRight = 0
|
||||||
|
local reservedTop = 0
|
||||||
|
local reservedBottom = 0
|
||||||
|
|
||||||
|
for _, child in ipairs(element.children) do
|
||||||
|
-- Only consider absolutely positioned children with explicit positioning
|
||||||
|
if child.positioning == Positioning.ABSOLUTE and child._explicitlyAbsolute then
|
||||||
|
-- BORDER-BOX MODEL: Use border-box dimensions for space calculations
|
||||||
|
local childBorderBoxWidth = child:getBorderBoxWidth()
|
||||||
|
local childBorderBoxHeight = child:getBorderBoxHeight()
|
||||||
|
|
||||||
|
if child.left then
|
||||||
|
reservedLeft = math.max(reservedLeft, child.left + childBorderBoxWidth)
|
||||||
|
end
|
||||||
|
if child.right then
|
||||||
|
reservedRight = math.max(reservedRight, child.right + childBorderBoxWidth)
|
||||||
|
end
|
||||||
|
if child.top then
|
||||||
|
reservedTop = math.max(reservedTop, child.top + childBorderBoxHeight)
|
||||||
|
end
|
||||||
|
if child.bottom then
|
||||||
|
reservedBottom = math.max(reservedBottom, child.bottom + childBorderBoxHeight)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Calculate available space (accounting for padding and reserved space)
|
||||||
|
-- BORDER-BOX MODEL: element.width and element.height are already content dimensions
|
||||||
|
local availableWidth = element.width - reservedLeft - reservedRight
|
||||||
|
local availableHeight = element.height - reservedTop - reservedBottom
|
||||||
|
|
||||||
|
-- Get gaps
|
||||||
|
local columnGap = element.columnGap or 0
|
||||||
|
local rowGap = element.rowGap or 0
|
||||||
|
|
||||||
|
-- Calculate cell sizes (equal distribution)
|
||||||
|
local totalColumnGaps = (columns - 1) * columnGap
|
||||||
|
local totalRowGaps = (rows - 1) * rowGap
|
||||||
|
local cellWidth = (availableWidth - totalColumnGaps) / columns
|
||||||
|
local cellHeight = (availableHeight - totalRowGaps) / rows
|
||||||
|
|
||||||
|
-- Get children that participate in grid layout
|
||||||
|
local gridChildren = {}
|
||||||
|
for _, child in ipairs(element.children) do
|
||||||
|
if not (child.positioning == Positioning.ABSOLUTE and child._explicitlyAbsolute) then
|
||||||
|
table.insert(gridChildren, child)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Place children in grid cells
|
||||||
|
for i, child in ipairs(gridChildren) do
|
||||||
|
-- Calculate row and column (0-indexed for calculation)
|
||||||
|
local index = i - 1
|
||||||
|
local col = index % columns
|
||||||
|
local row = math.floor(index / columns)
|
||||||
|
|
||||||
|
-- Skip if we've exceeded the grid
|
||||||
|
if row >= rows then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Calculate cell position (accounting for reserved space)
|
||||||
|
local cellX = element.x + element.padding.left + reservedLeft + (col * (cellWidth + columnGap))
|
||||||
|
local cellY = element.y + element.padding.top + reservedTop + (row * (cellHeight + rowGap))
|
||||||
|
|
||||||
|
-- Apply alignment within grid cell (default to stretch)
|
||||||
|
local effectiveAlignItems = element.alignItems or AlignItems.STRETCH
|
||||||
|
|
||||||
|
-- Stretch child to fill cell by default
|
||||||
|
-- BORDER-BOX MODEL: Set border-box dimensions, content area adjusts automatically
|
||||||
|
if effectiveAlignItems == AlignItems.STRETCH or effectiveAlignItems == "stretch" then
|
||||||
|
child.x = cellX
|
||||||
|
child.y = cellY
|
||||||
|
child._borderBoxWidth = cellWidth
|
||||||
|
child._borderBoxHeight = cellHeight
|
||||||
|
child.width = math.max(0, cellWidth - child.padding.left - child.padding.right)
|
||||||
|
child.height = math.max(0, cellHeight - child.padding.top - child.padding.bottom)
|
||||||
|
-- Disable auto-sizing when stretched by grid
|
||||||
|
child.autosizing.width = false
|
||||||
|
child.autosizing.height = false
|
||||||
|
elseif effectiveAlignItems == AlignItems.CENTER or effectiveAlignItems == "center" then
|
||||||
|
local childBorderBoxWidth = child:getBorderBoxWidth()
|
||||||
|
local childBorderBoxHeight = child:getBorderBoxHeight()
|
||||||
|
child.x = cellX + (cellWidth - childBorderBoxWidth) / 2
|
||||||
|
child.y = cellY + (cellHeight - childBorderBoxHeight) / 2
|
||||||
|
elseif effectiveAlignItems == AlignItems.FLEX_START or effectiveAlignItems == "flex-start" or effectiveAlignItems == "start" then
|
||||||
|
child.x = cellX
|
||||||
|
child.y = cellY
|
||||||
|
elseif effectiveAlignItems == AlignItems.FLEX_END or effectiveAlignItems == "flex-end" or effectiveAlignItems == "end" then
|
||||||
|
local childBorderBoxWidth = child:getBorderBoxWidth()
|
||||||
|
local childBorderBoxHeight = child:getBorderBoxHeight()
|
||||||
|
child.x = cellX + cellWidth - childBorderBoxWidth
|
||||||
|
child.y = cellY + cellHeight - childBorderBoxHeight
|
||||||
|
else
|
||||||
|
-- Default to stretch
|
||||||
|
child.x = cellX
|
||||||
|
child.y = cellY
|
||||||
|
child._borderBoxWidth = cellWidth
|
||||||
|
child._borderBoxHeight = cellHeight
|
||||||
|
child.width = math.max(0, cellWidth - child.padding.left - child.padding.right)
|
||||||
|
child.height = math.max(0, cellHeight - child.padding.top - child.padding.bottom)
|
||||||
|
-- Disable auto-sizing when stretched by grid
|
||||||
|
child.autosizing.width = false
|
||||||
|
child.autosizing.height = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Layout child's children if it has any
|
||||||
|
if #child.children > 0 then
|
||||||
|
child:layoutChildren()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return Grid
|
||||||
36
flexlove/GuiState.lua
Normal file
36
flexlove/GuiState.lua
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
-- ====================
|
||||||
|
-- GUI State Module
|
||||||
|
-- ====================
|
||||||
|
-- Shared state between Gui and Element to avoid circular dependencies
|
||||||
|
|
||||||
|
---@class GuiState
|
||||||
|
local GuiState = {
|
||||||
|
-- Top-level elements
|
||||||
|
topElements = {},
|
||||||
|
|
||||||
|
-- Base scale configuration
|
||||||
|
baseScale = nil, -- {width: number, height: number}
|
||||||
|
|
||||||
|
-- Current scale factors
|
||||||
|
scaleFactors = { x = 1.0, y = 1.0 },
|
||||||
|
|
||||||
|
-- Default theme name
|
||||||
|
defaultTheme = nil,
|
||||||
|
|
||||||
|
-- Currently focused element (for keyboard input)
|
||||||
|
_focusedElement = nil,
|
||||||
|
|
||||||
|
-- Active event element (for current frame)
|
||||||
|
_activeEventElement = nil,
|
||||||
|
|
||||||
|
-- Cached viewport dimensions
|
||||||
|
_cachedViewport = { width = 0, height = 0 },
|
||||||
|
}
|
||||||
|
|
||||||
|
--- Get current scale factors
|
||||||
|
---@return number, number -- scaleX, scaleY
|
||||||
|
function GuiState.getScaleFactors()
|
||||||
|
return GuiState.scaleFactors.x, GuiState.scaleFactors.y
|
||||||
|
end
|
||||||
|
|
||||||
|
return GuiState
|
||||||
46
flexlove/InputEvent.lua
Normal file
46
flexlove/InputEvent.lua
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
-- ====================
|
||||||
|
-- Input Event System
|
||||||
|
-- ====================
|
||||||
|
|
||||||
|
---@class InputEvent
|
||||||
|
---@field type "click"|"press"|"release"|"rightclick"|"middleclick"|"drag"
|
||||||
|
---@field button number -- Mouse button: 1 (left), 2 (right), 3 (middle)
|
||||||
|
---@field x number -- Mouse X position
|
||||||
|
---@field y number -- Mouse Y position
|
||||||
|
---@field dx number? -- Delta X from drag start (only for drag events)
|
||||||
|
---@field dy number? -- Delta Y from drag start (only for drag events)
|
||||||
|
---@field modifiers {shift:boolean, ctrl:boolean, alt:boolean, super:boolean}
|
||||||
|
---@field clickCount number -- Number of clicks (for double/triple click detection)
|
||||||
|
---@field timestamp number -- Time when event occurred
|
||||||
|
local InputEvent = {}
|
||||||
|
InputEvent.__index = InputEvent
|
||||||
|
|
||||||
|
---@class InputEventProps
|
||||||
|
---@field type "click"|"press"|"release"|"rightclick"|"middleclick"|"drag"
|
||||||
|
---@field button number
|
||||||
|
---@field x number
|
||||||
|
---@field y number
|
||||||
|
---@field dx number?
|
||||||
|
---@field dy number?
|
||||||
|
---@field modifiers {shift:boolean, ctrl:boolean, alt:boolean, super:boolean}
|
||||||
|
---@field clickCount number?
|
||||||
|
---@field timestamp number?
|
||||||
|
|
||||||
|
--- Create a new input event
|
||||||
|
---@param props InputEventProps
|
||||||
|
---@return InputEvent
|
||||||
|
function InputEvent.new(props)
|
||||||
|
local self = setmetatable({}, InputEvent)
|
||||||
|
self.type = props.type
|
||||||
|
self.button = props.button
|
||||||
|
self.x = props.x
|
||||||
|
self.y = props.y
|
||||||
|
self.dx = props.dx
|
||||||
|
self.dy = props.dy
|
||||||
|
self.modifiers = props.modifiers
|
||||||
|
self.clickCount = props.clickCount or 1
|
||||||
|
self.timestamp = props.timestamp or love.timer.getTime()
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
return InputEvent
|
||||||
@@ -4,7 +4,8 @@ Handles rendering of 9-patch components with Android-style scaling.
|
|||||||
Corners can be scaled independently while edges stretch in one dimension.
|
Corners can be scaled independently while edges stretch in one dimension.
|
||||||
]]
|
]]
|
||||||
|
|
||||||
local ImageScaler = require("flexlove.ImageScaler")
|
local modulePath = (...):match("(.-)[^%.]+$")
|
||||||
|
local ImageScaler = require(modulePath .. "ImageScaler")
|
||||||
|
|
||||||
--- 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")
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ Manages theme loading, registration, and component/color/font access.
|
|||||||
Supports 9-patch images, component states, and dynamic theme switching.
|
Supports 9-patch images, component states, and dynamic theme switching.
|
||||||
]]
|
]]
|
||||||
|
|
||||||
local Color = require("flexlove.Color")
|
local modulePath = (...):match("(.-)[^%.]+$")
|
||||||
local NinePatchParser = require("flexlove.NinePatchParser")
|
local function req(name) return require(modulePath .. name) end
|
||||||
local ImageScaler = require("flexlove.ImageScaler")
|
|
||||||
|
local NinePatchParser = req("NinePatchParser")
|
||||||
|
local ImageScaler = req("ImageScaler")
|
||||||
|
|
||||||
--- 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")
|
||||||
@@ -490,4 +492,11 @@ function Theme.getColorOrDefault(colorName, fallback)
|
|||||||
return fallback or Color.new(1, 1, 1, 1)
|
return fallback or Color.new(1, 1, 1, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Get a theme by name
|
||||||
|
---@param themeName string -- Name of the theme
|
||||||
|
---@return Theme|nil -- Returns theme or nil if not found
|
||||||
|
function Theme.get(themeName)
|
||||||
|
return themes[themeName]
|
||||||
|
end
|
||||||
|
|
||||||
return Theme
|
return Theme
|
||||||
|
|||||||
@@ -13,4 +13,6 @@ local TEXT_SIZE_PRESETS = {
|
|||||||
["4xl"] = 7.0, -- 7vh
|
["4xl"] = 7.0, -- 7vh
|
||||||
}
|
}
|
||||||
|
|
||||||
return { TEXT_SIZE_PRESETS }
|
return {
|
||||||
|
TEXT_SIZE_PRESETS = TEXT_SIZE_PRESETS
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,91 +66,6 @@ local enums = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
---@class ElementProps
|
|
||||||
---@field id string?
|
|
||||||
---@field parent Element? -- Parent element for hierarchical structure
|
|
||||||
---@field x number|string? -- X coordinate of the element (default: 0)
|
|
||||||
---@field y number|string? -- Y coordinate of the element (default: 0)
|
|
||||||
---@field z number? -- Z-index for layering (default: 0)
|
|
||||||
---@field width number|string? -- Width of the element (default: calculated automatically)
|
|
||||||
---@field height number|string? -- Height of the element (default: calculated automatically)
|
|
||||||
---@field top number|string? -- Offset from top edge (CSS-style positioning)
|
|
||||||
---@field right number|string? -- Offset from right edge (CSS-style positioning)
|
|
||||||
---@field bottom number|string? -- Offset from bottom edge (CSS-style positioning)
|
|
||||||
---@field left number|string? -- Offset from left edge (CSS-style positioning)
|
|
||||||
---@field border Border? -- Border configuration for the element
|
|
||||||
---@field borderColor Color? -- Color of the border (default: black)
|
|
||||||
---@field opacity number?
|
|
||||||
---@field backgroundColor Color? -- Background color (default: transparent)
|
|
||||||
---@field cornerRadius number|{topLeft:number?, topRight:number?, bottomLeft:number?, bottomRight:number?}? -- Corner radius: number (all corners) or table for individual corners (default: 0)
|
|
||||||
---@field gap number|string? -- Space between children elements (default: 10)
|
|
||||||
---@field padding {top:number|string?, right:number|string?, bottom:number|string?, left:number|string?, horizontal: number|string?, vertical:number|string?}? -- Padding around children (default: {top=0, right=0, bottom=0, left=0})
|
|
||||||
---@field margin {top:number|string?, right:number|string?, bottom:number|string?, left:number|string?, horizontal: number|string?, vertical:number|string?}? -- Margin around children (default: {top=0, right=0, bottom=0, left=0})
|
|
||||||
---@field text string? -- Text content to display (default: nil)
|
|
||||||
---@field titleColor Color? -- Color of the text content (default: black)
|
|
||||||
---@field textAlign TextAlign? -- Alignment of the text content (default: START)
|
|
||||||
---@field textColor Color? -- Color of the text content (default: black)
|
|
||||||
---@field textSize number|string? -- Font size: number (px), string with units ("2vh", "10%"), or preset ("xxs"|"xs"|"sm"|"md"|"lg"|"xl"|"xxl"|"3xl"|"4xl") (default: "md")
|
|
||||||
---@field minTextSize number?
|
|
||||||
---@field maxTextSize number?
|
|
||||||
---@field fontFamily string? -- Font family name from theme or path to font file (default: theme default or system default)
|
|
||||||
---@field autoScaleText boolean? -- Whether text should auto-scale with window size (default: true)
|
|
||||||
---@field positioning Positioning? -- Layout positioning mode (default: RELATIVE)
|
|
||||||
---@field flexDirection FlexDirection? -- Direction of flex layout (default: HORIZONTAL)
|
|
||||||
---@field justifyContent JustifyContent? -- Alignment of items along main axis (default: FLEX_START)
|
|
||||||
---@field alignItems AlignItems? -- Alignment of items along cross axis (default: STRETCH)
|
|
||||||
---@field alignContent AlignContent? -- Alignment of lines in multi-line flex containers (default: STRETCH)
|
|
||||||
---@field flexWrap FlexWrap? -- Whether children wrap to multiple lines (default: NOWRAP)
|
|
||||||
---@field justifySelf JustifySelf? -- Alignment of the item itself along main axis (default: AUTO)
|
|
||||||
---@field alignSelf AlignSelf? -- Alignment of the item itself along cross axis (default: AUTO)
|
|
||||||
---@field callback fun(element:Element, event:InputEvent)? -- Callback function for interaction events
|
|
||||||
---@field transform table? -- Transform properties for animations and styling
|
|
||||||
---@field transition table? -- Transition settings for animations
|
|
||||||
---@field gridRows number? -- Number of rows in the grid (default: 1)
|
|
||||||
---@field gridColumns number? -- Number of columns in the grid (default: 1)
|
|
||||||
---@field columnGap number|string? -- Gap between grid columns
|
|
||||||
---@field rowGap number|string? -- Gap between grid rows
|
|
||||||
---@field theme string? -- Theme name to use (e.g., "space", "dark"). Defaults to theme from Gui.init()
|
|
||||||
---@field themeComponent string? -- Theme component to use (e.g., "panel", "button", "input"). If nil, no theme is applied
|
|
||||||
---@field disabled boolean? -- Whether the element is disabled (default: false)
|
|
||||||
---@field active boolean? -- Whether the element is active/focused (for inputs, default: false)
|
|
||||||
---@field disableHighlight boolean? -- Whether to disable the pressed state highlight overlay (default: false)
|
|
||||||
---@field contentAutoSizingMultiplier {width:number?, height:number?}? -- Multiplier for auto-sized content dimensions (default: sourced from theme)
|
|
||||||
---@field scaleCorners number? -- Scale multiplier for 9-slice corners/edges. E.g., 2 = 2x size (overrides theme setting)
|
|
||||||
---@field scalingAlgorithm "nearest"|"bilinear"? -- Scaling algorithm for 9-slice corners: "nearest" (sharp/pixelated) or "bilinear" (smooth) (overrides theme setting)
|
|
||||||
---@field contentBlur {intensity:number, quality:number}? -- Blur the element's content including children (intensity: 0-100, quality: 1-10, default: nil)
|
|
||||||
---@field backdropBlur {intensity:number, quality:number}? -- Blur content behind the element (intensity: 0-100, quality: 1-10, default: nil)
|
|
||||||
---@field editable boolean? -- Whether the element is editable (default: false)
|
|
||||||
---@field multiline boolean? -- Whether the element supports multiple lines (default: false)
|
|
||||||
---@field textWrap boolean|"word"|"char"? -- Text wrapping mode (default: false for single-line, "word" for multi-line)
|
|
||||||
---@field maxLines number? -- Maximum number of lines (default: nil)
|
|
||||||
---@field maxLength number? -- Maximum text length in characters (default: nil)
|
|
||||||
---@field placeholder string? -- Placeholder text when empty (default: nil)
|
|
||||||
---@field passwordMode boolean? -- Whether to display text as password (default: false)
|
|
||||||
---@field inputType "text"|"number"|"email"|"url"? -- Input type for validation (default: "text")
|
|
||||||
---@field textOverflow "clip"|"ellipsis"|"scroll"? -- Text overflow behavior (default: "clip")
|
|
||||||
---@field scrollable boolean? -- Whether text is scrollable (default: false for single-line, true for multi-line)
|
|
||||||
---@field autoGrow boolean? -- Whether element auto-grows with text (default: false)
|
|
||||||
---@field selectOnFocus boolean? -- Whether to select all text on focus (default: false)
|
|
||||||
---@field cursorColor Color? -- Cursor color (default: nil, uses textColor)
|
|
||||||
---@field selectionColor Color? -- Selection background color (default: nil, uses theme or default)
|
|
||||||
---@field cursorBlinkRate number? -- Cursor blink rate in seconds (default: 0.5)
|
|
||||||
---@field overflow "visible"|"hidden"|"scroll"|"auto"? -- Overflow behavior (default: "visible")
|
|
||||||
---@field overflowX "visible"|"hidden"|"scroll"|"auto"? -- X-axis overflow (overrides overflow)
|
|
||||||
---@field overflowY "visible"|"hidden"|"scroll"|"auto"? -- Y-axis overflow (overrides overflow)
|
|
||||||
---@field scrollbarWidth number? -- Width of scrollbar track in pixels (default: 12)
|
|
||||||
---@field scrollbarColor Color? -- Scrollbar thumb color
|
|
||||||
---@field scrollbarTrackColor Color? -- Scrollbar track color
|
|
||||||
---@field scrollbarRadius number? -- Corner radius for scrollbar (default: 6)
|
|
||||||
---@field scrollbarPadding number? -- Padding between scrollbar and edge (default: 2)
|
|
||||||
---@field scrollSpeed number? -- Pixels per wheel notch (default: 20)
|
|
||||||
|
|
||||||
---@class Border
|
|
||||||
---@field top boolean?
|
|
||||||
---@field right boolean?
|
|
||||||
---@field bottom boolean?
|
|
||||||
---@field left boolean?
|
|
||||||
|
|
||||||
--- Get current keyboard modifiers state
|
--- Get current keyboard modifiers state
|
||||||
---@return {shift:boolean, ctrl:boolean, alt:boolean, super:boolean}
|
---@return {shift:boolean, ctrl:boolean, alt:boolean, super:boolean}
|
||||||
local function getModifiers()
|
local function getModifiers()
|
||||||
@@ -164,72 +79,94 @@ local function getModifiers()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local TEXT_SIZE_PRESETS = {
|
local TEXT_SIZE_PRESETS = {
|
||||||
["2xs"] = 0.75, -- 0.75vh
|
["2xs"] = 0.75,
|
||||||
xxs = 0.75, -- 0.75vh
|
xxs = 0.75,
|
||||||
xs = 1.25, -- 1.25vh
|
xs = 1.25,
|
||||||
sm = 1.75, -- 1.75vh
|
sm = 1.75,
|
||||||
md = 2.25, -- 2.25vh (default)
|
md = 2.25,
|
||||||
lg = 2.75, -- 2.75vh
|
lg = 2.75,
|
||||||
xl = 3.5, -- 3.5vh
|
xl = 3.5,
|
||||||
xxl = 4.5, -- 4.5vh
|
xxl = 4.5,
|
||||||
["2xl"] = 4.5, -- 4.5vh
|
["2xl"] = 4.5,
|
||||||
["3xl"] = 5.0, -- 5vh
|
["3xl"] = 5.0,
|
||||||
["4xl"] = 7.0, -- 7vh
|
["4xl"] = 7.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- Text Size Utilities
|
|
||||||
-- ====================
|
|
||||||
|
|
||||||
--- Resolve text size preset to viewport units
|
--- Resolve text size preset to viewport units
|
||||||
---@param sizeValue string|number
|
---@param sizeValue string|number
|
||||||
---@return number?, string? -- Returns value and unit ("vh" for presets, original unit otherwise)
|
---@return number?, string?
|
||||||
local function resolveTextSizePreset(sizeValue)
|
local function resolveTextSizePreset(sizeValue)
|
||||||
if type(sizeValue) == "string" then
|
if type(sizeValue) == "string" then
|
||||||
-- Check if it's a preset
|
|
||||||
local preset = TEXT_SIZE_PRESETS[sizeValue]
|
local preset = TEXT_SIZE_PRESETS[sizeValue]
|
||||||
if preset then
|
if preset then
|
||||||
return preset, "vh"
|
return preset, "vh"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- Not a preset, return nil to indicate normal parsing should occur
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--- Auto-detect the base path where FlexLove is located
|
||||||
|
---@return string filesystemPath
|
||||||
|
local function getFlexLoveBasePath()
|
||||||
|
local info = debug.getinfo(1, "S")
|
||||||
|
if info and info.source then
|
||||||
|
local source = info.source
|
||||||
|
if source:sub(1, 1) == "@" then
|
||||||
|
source = source:sub(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local filesystemPath = source:match("(.*/)")
|
||||||
|
if filesystemPath then
|
||||||
|
local fsPath = filesystemPath
|
||||||
|
fsPath = fsPath:gsub("^%./", "")
|
||||||
|
fsPath = fsPath:gsub("/$", "")
|
||||||
|
fsPath = fsPath:gsub("/flexlove$", "")
|
||||||
|
return fsPath
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return "libs"
|
||||||
|
end
|
||||||
|
|
||||||
|
local FLEXLOVE_FILESYSTEM_PATH = getFlexLoveBasePath()
|
||||||
|
|
||||||
|
--- Helper function to resolve paths relative to FlexLove
|
||||||
|
---@param path string
|
||||||
|
---@return string
|
||||||
|
local function resolveImagePath(path)
|
||||||
|
if path:match("^/") or path:match("^[A-Z]:") then
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
return FLEXLOVE_FILESYSTEM_PATH .. "/" .. path
|
||||||
|
end
|
||||||
|
|
||||||
local FONT_CACHE = {}
|
local FONT_CACHE = {}
|
||||||
local FONT_CACHE_MAX_SIZE = 50 -- Limit cache size to prevent unbounded growth
|
local FONT_CACHE_MAX_SIZE = 50
|
||||||
local FONT_CACHE_ORDER = {} -- Track access order for LRU eviction
|
local FONT_CACHE_ORDER = {}
|
||||||
|
|
||||||
--- Create or get a font from cache
|
--- Create or get a font from cache
|
||||||
---@param size number
|
---@param size number
|
||||||
---@param fontPath string? -- Optional: path to font file
|
---@param fontPath string?
|
||||||
---@return love.Font
|
---@return love.Font
|
||||||
function FONT_CACHE.get(size, fontPath)
|
function FONT_CACHE.get(size, fontPath)
|
||||||
-- Create cache key from size and font path
|
|
||||||
local cacheKey = fontPath and (fontPath .. "_" .. tostring(size)) or tostring(size)
|
local cacheKey = fontPath and (fontPath .. "_" .. tostring(size)) or tostring(size)
|
||||||
|
|
||||||
if not FONT_CACHE[cacheKey] then
|
if not FONT_CACHE[cacheKey] then
|
||||||
if fontPath then
|
if fontPath then
|
||||||
-- Load custom font
|
|
||||||
local resolvedPath = resolveImagePath(fontPath)
|
local resolvedPath = resolveImagePath(fontPath)
|
||||||
-- Note: love.graphics.newFont signature is (path, size) for custom fonts
|
|
||||||
local success, font = pcall(love.graphics.newFont, resolvedPath, size)
|
local success, font = pcall(love.graphics.newFont, resolvedPath, size)
|
||||||
if success then
|
if success then
|
||||||
FONT_CACHE[cacheKey] = font
|
FONT_CACHE[cacheKey] = font
|
||||||
else
|
else
|
||||||
-- Fallback to default font if custom font fails to load
|
|
||||||
print("[FlexLove] Failed to load font: " .. fontPath .. " - using default font")
|
print("[FlexLove] Failed to load font: " .. fontPath .. " - using default font")
|
||||||
FONT_CACHE[cacheKey] = love.graphics.newFont(size)
|
FONT_CACHE[cacheKey] = love.graphics.newFont(size)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
-- Load default font
|
|
||||||
FONT_CACHE[cacheKey] = love.graphics.newFont(size)
|
FONT_CACHE[cacheKey] = love.graphics.newFont(size)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Add to access order for LRU tracking
|
|
||||||
table.insert(FONT_CACHE_ORDER, cacheKey)
|
table.insert(FONT_CACHE_ORDER, cacheKey)
|
||||||
|
|
||||||
-- Evict oldest entry if cache is full (LRU eviction)
|
|
||||||
if #FONT_CACHE_ORDER > FONT_CACHE_MAX_SIZE then
|
if #FONT_CACHE_ORDER > FONT_CACHE_MAX_SIZE then
|
||||||
local oldestKey = table.remove(FONT_CACHE_ORDER, 1)
|
local oldestKey = table.remove(FONT_CACHE_ORDER, 1)
|
||||||
FONT_CACHE[oldestKey] = nil
|
FONT_CACHE[oldestKey] = nil
|
||||||
@@ -240,7 +177,7 @@ end
|
|||||||
|
|
||||||
--- Get font for text size (cached)
|
--- Get font for text size (cached)
|
||||||
---@param textSize number?
|
---@param textSize number?
|
||||||
---@param fontPath string? -- Optional: path to font file
|
---@param fontPath string?
|
||||||
---@return love.Font
|
---@return love.Font
|
||||||
function FONT_CACHE.getFont(textSize, fontPath)
|
function FONT_CACHE.getFont(textSize, fontPath)
|
||||||
if textSize then
|
if textSize then
|
||||||
@@ -250,4 +187,9 @@ function FONT_CACHE.getFont(textSize, fontPath)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return { enums, FONT_CACHE, resolveTextSizePreset, getModifiers }
|
return {
|
||||||
|
enums = enums,
|
||||||
|
FONT_CACHE = FONT_CACHE,
|
||||||
|
resolveTextSizePreset = resolveTextSizePreset,
|
||||||
|
getModifiers = getModifiers,
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user