From 87526c34ecf89683cfc162142bbf4d281950026c Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Fri, 31 Oct 2025 21:01:39 -0400 Subject: [PATCH] module refactor completion --- FlexLove.lua | 341 +++++----------------------------------- flexlove/Element.lua | 75 +++++++-- flexlove/Grid.lua | 136 ++++++++++++++++ flexlove/GuiState.lua | 36 +++++ flexlove/InputEvent.lua | 46 ++++++ flexlove/NineSlice.lua | 3 +- flexlove/Theme.lua | 15 +- flexlove/constants.lua | 4 +- flexlove/utils.lua | 172 +++++++------------- 9 files changed, 393 insertions(+), 435 deletions(-) create mode 100644 flexlove/Grid.lua create mode 100644 flexlove/GuiState.lua create mode 100644 flexlove/InputEvent.lua diff --git a/FlexLove.lua b/FlexLove.lua index 7055051..c4e5c49 100644 --- a/FlexLove.lua +++ b/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 Color = require("flexlove.Color") -local ImageDataReader = require("flexlove.ImageDataReader") -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) +local modulePath = (...):match("(.-)[^%.]+$") -- Get the module path prefix (e.g., "libs." or "") +local function req(name) + return require(modulePath .. name) end --- ==================== --- Top level GUI manager --- ==================== +local Blur = req("flexlove.Blur") +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") ---- ----@class Gui ----@field topElements table ----@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 -} +-- Extract from utils +local enums = utils.enums +local getModifiers = utils.getModifiers local Positioning, FlexDirection, JustifyContent, AlignContent, AlignItems, TextAlign, AlignSelf, JustifySelf, FlexWrap = enums.Positioning, @@ -64,135 +48,14 @@ local Positioning, FlexDirection, JustifyContent, AlignContent, AlignItems, Text enums.FlexWrap -- ==================== --- Grid System +-- Top level GUI manager -- ==================== ---- 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 +---@class Gui +local Gui = GuiState --- 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) if config.baseScale then Gui.baseScale = { @@ -200,22 +63,18 @@ function Gui.init(config) height = config.baseScale.height or 1080, } - -- Calculate initial scale factors local currentWidth, currentHeight = Units.getViewport() Gui.scaleFactors.x = currentWidth / Gui.baseScale.width Gui.scaleFactors.y = currentHeight / Gui.baseScale.height end - -- Load and set theme if specified if config.theme then local success, err = pcall(function() if type(config.theme) == "string" then - -- Load theme by name Theme.load(config.theme) Theme.setActive(config.theme) Gui.defaultTheme = config.theme elseif type(config.theme) == "table" then - -- Load theme from definition local theme = Theme.new(config.theme) Theme.setActive(theme) 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 return true 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 if child.positioning == "absolute" then if child.z > elem.z and child:contains(clickX, clickY) then @@ -251,36 +108,16 @@ function Gui.isOccluded(elem, clickX, clickY) return false end ---- Get current scale factors ----@return number, number -- scaleX, scaleY -function Gui.getScaleFactors() - return Gui.scaleFactors.x, Gui.scaleFactors.y -end - function Gui.resize() local newWidth, newHeight = love.window.getMode() - -- Update scale factors if base scale is set if Gui.baseScale then Gui.scaleFactors.x = newWidth / Gui.baseScale.width Gui.scaleFactors.y = newHeight / Gui.baseScale.height 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() - -- Clear game/backdrop canvas cache on resize (will be recreated with new dimensions) Gui._gameCanvas = nil Gui._backdropCanvas = nil Gui._canvasDimensions = { width = 0, height = 0 } @@ -290,33 +127,20 @@ function Gui.resize() end end --- Canvas cache for game rendering (reused across frames) +-- Canvas cache for game rendering Gui._gameCanvas = nil Gui._backdropCanvas = nil Gui._canvasDimensions = { width = 0, height = 0 } ----@param gameDrawFunc function|nil -- Function to draw game content, needed for backdrop blur ----@param postDrawFunc function|nil -- Optional function to draw after GUI (for top-level shaders/effects) ----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 +---@param gameDrawFunc function|nil +---@param postDrawFunc function|nil function Gui.draw(gameDrawFunc, postDrawFunc) - -- Save the current canvas state to support nested rendering local outerCanvas = love.graphics.getCanvas() - local gameCanvas = nil - -- Render game content to a canvas if function provided if type(gameDrawFunc) == "function" then 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 Gui._gameCanvas = 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.clear() - gameDrawFunc() -- Call the drawing function + gameDrawFunc() love.graphics.setCanvas(outerCanvas) - -- Draw game canvas to the outer canvas (or screen if none) love.graphics.setColor(1, 1, 1, 1) love.graphics.draw(gameCanvas, 0, 0) end - -- Sort elements by z-index before drawing table.sort(Gui.topElements, function(a, b) return a.z < b.z end) - -- Check if any element (recursively) needs backdrop blur local function hasBackdropBlur(element) if element.backdropBlur and element.backdropBlur.intensity > 0 then return true @@ -362,109 +183,89 @@ function Gui.draw(gameDrawFunc, postDrawFunc) end end - -- If backdrop blur is needed, render to a progressive canvas if needsBackdropCanvas and gameCanvas then local backdropCanvas = Gui._backdropCanvas local prevColor = { love.graphics.getColor() } - -- Initialize backdrop canvas with game content love.graphics.setCanvas(backdropCanvas) love.graphics.clear() love.graphics.setColor(1, 1, 1, 1) love.graphics.draw(gameCanvas, 0, 0) - -- Reset to outer canvas (screen or parent canvas) love.graphics.setCanvas(outerCanvas) love.graphics.setColor(unpack(prevColor)) - -- Draw each element, updating backdrop canvas progressively for _, win in ipairs(Gui.topElements) do - -- Draw element with current backdrop state to outer canvas win:draw(backdropCanvas) - -- Update backdrop canvas to include this element (for next elements) love.graphics.setCanvas(backdropCanvas) love.graphics.setColor(1, 1, 1, 1) - win:draw(nil) -- Draw without backdrop blur to the backdrop canvas - love.graphics.setCanvas(outerCanvas) -- Reset to outer canvas + win:draw(nil) + love.graphics.setCanvas(outerCanvas) end else - -- No backdrop blur needed, draw normally for _, win in ipairs(Gui.topElements) do win:draw(nil) end end - -- Call post-draw function if provided (for top-level shaders/effects) if type(postDrawFunc) == "function" then postDrawFunc() end - -- Restore the original canvas state love.graphics.setCanvas(outerCanvas) end ---- Find the topmost element at given coordinates (considering z-index) +--- Find the topmost element at given coordinates ---@param x number ---@param y number ----@return Element? -- Returns the topmost element or nil +---@return Element? function Gui.getElementAtPosition(x, y) local candidates = {} - -- Recursively collect all elements that contain the point local function collectHits(element) - -- Check if point is within element bounds local bx = element.x local by = element.y 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) 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 table.insert(candidates, element) end - -- Check children for _, child in ipairs(element.children) do collectHits(child) end end end - -- Collect hits from all top-level elements for _, element in ipairs(Gui.topElements) do collectHits(element) end - -- Sort by z-index (highest first) table.sort(candidates, function(a, b) return a.z > b.z end) - -- Return the topmost element (highest z-index) return candidates[1] end function Gui.update(dt) - -- Reset event handling flags for new frame local mx, my = love.mouse.getPosition() local topElement = Gui.getElementAtPosition(mx, my) - -- Mark which element should handle events this frame Gui._activeEventElement = topElement - -- Update all elements for _, win in ipairs(Gui.topElements) do win:update(dt) end - -- Clear active element for next frame Gui._activeEventElement = nil end --- Forward text input to focused element ----@param text string -- Character input +---@param text string function Gui.textinput(text) if Gui._focusedElement then Gui._focusedElement:textinput(text) @@ -472,9 +273,9 @@ function Gui.textinput(text) end --- Forward key press to focused element ----@param key string -- Key name ----@param scancode string -- Scancode ----@param isrepeat boolean -- Whether this is a key repeat +---@param key string +---@param scancode string +---@param isrepeat boolean function Gui.keypressed(key, scancode, isrepeat) if Gui._focusedElement then Gui._focusedElement:keypressed(key, scancode, isrepeat) @@ -483,23 +284,18 @@ end --- Handle mouse wheel scrolling function Gui.wheelmoved(x, y) - -- Get mouse position local mx, my = love.mouse.getPosition() - -- Find the deepest scrollable element at mouse position local function findScrollableAtPosition(elements, mx, my) - -- Check in reverse z-order (top to bottom) for i = #elements, 1, -1 do local element = elements[i] - -- Check if mouse is over element local bx = element.x local by = element.y 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) 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 local childResult = findScrollableAtPosition(element.children, mx, my) if childResult then @@ -507,7 +303,6 @@ function Gui.wheelmoved(x, y) end end - -- Check if this element is scrollable local overflowX = element.overflowX 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 @@ -531,67 +326,15 @@ function Gui.destroy() win:destroy() end Gui.topElements = {} - -- Reset base scale and scale factors Gui.baseScale = nil Gui.scaleFactors = { x = 1.0, y = 1.0 } - -- Reset cached viewport Gui._cachedViewport = { width = 0, height = 0 } - -- Clear game/backdrop canvas cache Gui._gameCanvas = nil Gui._backdropCanvas = nil Gui._canvasDimensions = { width = 0, height = 0 } - -- Clear focused element Gui._focusedElement = nil 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.Element = Element Gui.Animation = Animation @@ -616,6 +359,8 @@ return { Theme = Theme, RoundedRect = RoundedRect, NineSlice = NineSlice, + Grid = Grid, + InputEvent = InputEvent, -- Enums (individual) Positioning = Positioning, diff --git a/flexlove/Element.lua b/flexlove/Element.lua index cfb8e9d..17fdfb4 100644 --- a/flexlove/Element.lua +++ b/flexlove/Element.lua @@ -2,6 +2,47 @@ -- 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: --------------------------------- @@ -191,7 +232,7 @@ function Element.new(props) self.contentAutoSizingMultiplier = props.contentAutoSizingMultiplier else -- 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 -- First check if themeComponent has a multiplier if self.themeComponent then @@ -412,7 +453,7 @@ function Element.new(props) self.fontFamily = self.parent.fontFamily elseif props.themeComponent then -- 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 self.fontFamily = "default" else @@ -572,7 +613,7 @@ function Element.new(props) local use9PatchPadding = false local ninePatchContentPadding = nil 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 local component = themeToUse.components[self.themeComponent] 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 -- The contentPadding values are in the original image's pixel coordinates, -- 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 local component = themeToUse.components[self.themeComponent] local atlasImage = component._loadedAtlas or themeToUse.atlas @@ -825,7 +866,7 @@ function Element.new(props) self.textColor = props.textColor else -- 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 self.textColor = themeToUse.colors.text else @@ -956,7 +997,7 @@ function Element.new(props) self.textColor = self.parent.textColor else -- 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 self.textColor = themeToUse.colors.text else @@ -1620,7 +1661,7 @@ function Element:getScaledContentPadding() return nil 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 return nil end @@ -2330,14 +2371,14 @@ function Element:draw(backdropCanvas) local themeToUse = nil if self.theme then -- Element specifies a specific theme - load it if needed - if themes[self.theme] then - themeToUse = themes[self.theme] + if Theme.get(self.theme) then + themeToUse = Theme.get(self.theme) else -- Try to load the theme pcall(function() Theme.load(self.theme) end) - themeToUse = themes[self.theme] + themeToUse = Theme.get(self.theme) end else -- Use active theme @@ -2423,7 +2464,7 @@ function Element:draw(backdropCanvas) local fontPath = nil if self.fontFamily then -- 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 fontPath = themeToUse.fonts[self.fontFamily] else @@ -2432,7 +2473,7 @@ function Element:draw(backdropCanvas) end elseif self.themeComponent then -- 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 fontPath = themeToUse.fonts.default end @@ -3221,14 +3262,14 @@ function Element:calculateTextWidth() -- Resolve font path from font family (same logic as in draw) local fontPath = nil 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 fontPath = themeToUse.fonts[self.fontFamily] else fontPath = self.fontFamily end 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 fontPath = themeToUse.fonts.default end @@ -3264,14 +3305,14 @@ function Element:calculateTextHeight() -- Resolve font path from font family (same logic as in draw) local fontPath = nil 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 fontPath = themeToUse.fonts[self.fontFamily] else fontPath = self.fontFamily end 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 fontPath = themeToUse.fonts.default end @@ -3917,7 +3958,7 @@ function Element:_getFont() -- Get font path from theme or element local fontPath = nil 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 fontPath = themeToUse.fonts[self.fontFamily] else diff --git a/flexlove/Grid.lua b/flexlove/Grid.lua new file mode 100644 index 0000000..991dea8 --- /dev/null +++ b/flexlove/Grid.lua @@ -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 diff --git a/flexlove/GuiState.lua b/flexlove/GuiState.lua new file mode 100644 index 0000000..2ad9a44 --- /dev/null +++ b/flexlove/GuiState.lua @@ -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 diff --git a/flexlove/InputEvent.lua b/flexlove/InputEvent.lua new file mode 100644 index 0000000..fa70817 --- /dev/null +++ b/flexlove/InputEvent.lua @@ -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 diff --git a/flexlove/NineSlice.lua b/flexlove/NineSlice.lua index b288494..25eab21 100644 --- a/flexlove/NineSlice.lua +++ b/flexlove/NineSlice.lua @@ -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. ]] -local ImageScaler = require("flexlove.ImageScaler") +local modulePath = (...):match("(.-)[^%.]+$") +local ImageScaler = require(modulePath .. "ImageScaler") --- Standardized error message formatter ---@param module string -- Module name (e.g., "Color", "Theme", "Units") diff --git a/flexlove/Theme.lua b/flexlove/Theme.lua index 439bc0a..b4f56cd 100644 --- a/flexlove/Theme.lua +++ b/flexlove/Theme.lua @@ -4,9 +4,11 @@ Manages theme loading, registration, and component/color/font access. Supports 9-patch images, component states, and dynamic theme switching. ]] -local Color = require("flexlove.Color") -local NinePatchParser = require("flexlove.NinePatchParser") -local ImageScaler = require("flexlove.ImageScaler") +local modulePath = (...):match("(.-)[^%.]+$") +local function req(name) return require(modulePath .. name) end + +local NinePatchParser = req("NinePatchParser") +local ImageScaler = req("ImageScaler") --- Standardized error message formatter ---@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) 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 diff --git a/flexlove/constants.lua b/flexlove/constants.lua index d8ad7d0..a134f26 100644 --- a/flexlove/constants.lua +++ b/flexlove/constants.lua @@ -13,4 +13,6 @@ local TEXT_SIZE_PRESETS = { ["4xl"] = 7.0, -- 7vh } -return { TEXT_SIZE_PRESETS } +return { + TEXT_SIZE_PRESETS = TEXT_SIZE_PRESETS +} diff --git a/flexlove/utils.lua b/flexlove/utils.lua index a9fa94d..c81640b 100644 --- a/flexlove/utils.lua +++ b/flexlove/utils.lua @@ -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 ---@return {shift:boolean, ctrl:boolean, alt:boolean, super:boolean} local function getModifiers() @@ -164,72 +79,94 @@ local function getModifiers() end local TEXT_SIZE_PRESETS = { - ["2xs"] = 0.75, -- 0.75vh - xxs = 0.75, -- 0.75vh - xs = 1.25, -- 1.25vh - sm = 1.75, -- 1.75vh - md = 2.25, -- 2.25vh (default) - lg = 2.75, -- 2.75vh - xl = 3.5, -- 3.5vh - xxl = 4.5, -- 4.5vh - ["2xl"] = 4.5, -- 4.5vh - ["3xl"] = 5.0, -- 5vh - ["4xl"] = 7.0, -- 7vh + ["2xs"] = 0.75, + xxs = 0.75, + xs = 1.25, + sm = 1.75, + md = 2.25, + lg = 2.75, + xl = 3.5, + xxl = 4.5, + ["2xl"] = 4.5, + ["3xl"] = 5.0, + ["4xl"] = 7.0, } --- ==================== --- Text Size Utilities --- ==================== - --- Resolve text size preset to viewport units ---@param sizeValue string|number ----@return number?, string? -- Returns value and unit ("vh" for presets, original unit otherwise) +---@return number?, string? local function resolveTextSizePreset(sizeValue) if type(sizeValue) == "string" then - -- Check if it's a preset local preset = TEXT_SIZE_PRESETS[sizeValue] if preset then return preset, "vh" end end - -- Not a preset, return nil to indicate normal parsing should occur return nil, nil 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_MAX_SIZE = 50 -- Limit cache size to prevent unbounded growth -local FONT_CACHE_ORDER = {} -- Track access order for LRU eviction +local FONT_CACHE_MAX_SIZE = 50 +local FONT_CACHE_ORDER = {} --- Create or get a font from cache ---@param size number ----@param fontPath string? -- Optional: path to font file +---@param fontPath string? ---@return love.Font function FONT_CACHE.get(size, fontPath) - -- Create cache key from size and font path local cacheKey = fontPath and (fontPath .. "_" .. tostring(size)) or tostring(size) if not FONT_CACHE[cacheKey] then if fontPath then - -- Load custom font local resolvedPath = resolveImagePath(fontPath) - -- Note: love.graphics.newFont signature is (path, size) for custom fonts local success, font = pcall(love.graphics.newFont, resolvedPath, size) if success then FONT_CACHE[cacheKey] = font else - -- Fallback to default font if custom font fails to load print("[FlexLove] Failed to load font: " .. fontPath .. " - using default font") FONT_CACHE[cacheKey] = love.graphics.newFont(size) end else - -- Load default font FONT_CACHE[cacheKey] = love.graphics.newFont(size) end - -- Add to access order for LRU tracking table.insert(FONT_CACHE_ORDER, cacheKey) - -- Evict oldest entry if cache is full (LRU eviction) if #FONT_CACHE_ORDER > FONT_CACHE_MAX_SIZE then local oldestKey = table.remove(FONT_CACHE_ORDER, 1) FONT_CACHE[oldestKey] = nil @@ -240,7 +177,7 @@ end --- Get font for text size (cached) ---@param textSize number? ----@param fontPath string? -- Optional: path to font file +---@param fontPath string? ---@return love.Font function FONT_CACHE.getFont(textSize, fontPath) if textSize then @@ -250,4 +187,9 @@ function FONT_CACHE.getFont(textSize, fontPath) end end -return { enums, FONT_CACHE, resolveTextSizePreset, getModifiers } +return { + enums = enums, + FONT_CACHE = FONT_CACHE, + resolveTextSizePreset = resolveTextSizePreset, + getModifiers = getModifiers, +}