From 3373d43b1bbdd130339b2c58198a7252f0a52061 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Thu, 13 Nov 2025 20:42:47 -0500 Subject: [PATCH] fix Gui references --- FlexLove.lua | 3 + README.md | 2 +- modules/Element.lua | 184 +++++++--------------- modules/LayoutEngine.lua | 324 ++++++++++++++++++++++----------------- modules/Units.lua | 12 +- modules/types.lua | 2 +- themes/README.md | 2 +- 7 files changed, 256 insertions(+), 273 deletions(-) diff --git a/FlexLove.lua b/FlexLove.lua index f9725df..e66254a 100644 --- a/FlexLove.lua +++ b/FlexLove.lua @@ -24,6 +24,9 @@ local enums = utils.enums ---@class FlexLove local flexlove = Context +-- Initialize Units module with Context dependency +Units.initialize(Context) + -- Add version and metadata flexlove._VERSION = "0.1.0" flexlove._DESCRIPTION = "UI Library for LÖVE Framework based on flexbox" diff --git a/README.md b/README.md index 7429502..01ed4b5 100644 --- a/README.md +++ b/README.md @@ -441,7 +441,7 @@ end Support for viewport-relative units: ```lua -local element = Gui.new({ +local element = FlexLove.new({ width = "50vw", -- 50% of viewport width height = "30vh", -- 30% of viewport height x = "25%", -- 25% of parent width diff --git a/modules/Element.lua b/modules/Element.lua index be100f9..c323b9d 100644 --- a/modules/Element.lua +++ b/modules/Element.lua @@ -31,43 +31,16 @@ 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 Context) -local Gui = Context - --- UTF-8 support (available in LÖVE/Lua 5.3+) -local utf8 = utf8 or require("utf8") - ---[[ -INTERNAL FIELD NAMING CONVENTIONS: ---------------------------------- -Fields prefixed with underscore (_) are internal/private and should not be accessed directly: - -- _eventHandler: Internal EventHandler instance for input event processing -- _themeState: Internal current theme state (managed automatically) -- _borderBoxWidth: Internal cached border-box width (optimization) -- _borderBoxHeight: Internal cached border-box height (optimization) -- _explicitlyAbsolute: Internal flag for positioning logic -- _originalPositioning: Internal original positioning value -- _cachedResult: Internal animation cache (Animation class) -- _resultDirty: Internal animation dirty flag (Animation class) -- _loadedAtlas: Internal cached atlas image (ThemeComponent) -- _cachedViewport: Internal viewport cache (Gui class) - -Public API methods to access internal state: -- Element:getBorderBoxWidth() - Get border-box width -- Element:getBorderBoxHeight() - Get border-box height -- Element:getBounds() - Get element bounds -]] +local Positioning, FlexDirection, JustifyContent, AlignContent, AlignItems, TextAlign, AlignSelf, JustifySelf, FlexWrap = + enums.Positioning, + enums.FlexDirection, + enums.JustifyContent, + enums.AlignContent, + enums.AlignItems, + enums.TextAlign, + enums.AlignSelf, + enums.JustifySelf, + enums.FlexWrap ---@class Element ---@field id string @@ -266,7 +239,7 @@ function Element.new(props) self.onEvent = props.onEvent -- Auto-generate ID in immediate mode if not provided - if Gui._immediateMode and (not props.id or props.id == "") then + if Context._immediateMode and (not props.id or props.id == "") then self.id = StateManager.generateID(props, props.parent) else self.id = props.id or "" @@ -292,7 +265,7 @@ function Element.new(props) self._stateId = self.id self._themeManager = Theme.Manager.new({ - theme = props.theme or Gui.defaultTheme, + theme = props.theme or Context.defaultTheme, themeComponent = props.themeComponent or nil, disabled = props.disabled or false, active = props.active or false, @@ -466,7 +439,7 @@ function Element.new(props) end -- Sync self.text with restored _textBuffer for editable elements in immediate mode - if self.editable and Gui._immediateMode and self._textBuffer then + if self.editable and Context._immediateMode and self._textBuffer then self.text = self._textBuffer end @@ -574,10 +547,8 @@ function Element.new(props) }, } - -- Get scale factors from Gui (will be used later) - local scaleX, scaleY = Gui.getScaleFactors() + local scaleX, scaleY = Context.getScaleFactors() - -- Store original textSize units and constraints self.minTextSize = props.minTextSize self.maxTextSize = props.maxTextSize @@ -649,7 +620,7 @@ function Element.new(props) end -- Pixel textSize value - if self.autoScaleText and Gui.baseScale then + if self.autoScaleText and Context.baseScale then -- With base scaling: store original pixel value and scale relative to base resolution self.units.textSize = { value = props.textSize, unit = "px" } self.textSize = props.textSize * scaleY @@ -661,13 +632,13 @@ function Element.new(props) self.textSize = props.textSize -- Initial size is the specified pixel value else -- No auto-scaling: apply base scaling if set, otherwise use raw value - self.textSize = Gui.baseScale and (props.textSize * scaleY) or props.textSize + self.textSize = Context.baseScale and (props.textSize * scaleY) or props.textSize self.units.textSize = { value = props.textSize, unit = "px" } end end else -- No textSize specified - use auto-scaling default - if self.autoScaleText and Gui.baseScale then + if self.autoScaleText and Context.baseScale then -- With base scaling: use 12px as default and scale self.units.textSize = { value = 12, unit = "px" } self.textSize = 12 * scaleY @@ -677,7 +648,7 @@ function Element.new(props) self.textSize = (1.5 / 100) * viewportHeight else -- No auto-scaling: use 12px with optional base scaling - self.textSize = Gui.baseScale and (12 * scaleY) or 12 + self.textSize = Context.baseScale and (12 * scaleY) or 12 self.units.textSize = { value = nil, unit = "px" } end end @@ -692,7 +663,7 @@ function Element.new(props) local parentWidth = self.parent and self.parent.width or viewportWidth tempWidth = Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth) else - tempWidth = Gui.baseScale and (widthProp * scaleX) or widthProp + tempWidth = Context.baseScale and (widthProp * scaleX) or widthProp self.units.width = { value = widthProp, unit = "px" } end self.width = tempWidth @@ -723,7 +694,7 @@ function Element.new(props) tempHeight = Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight) else -- Apply base scaling to pixel values - tempHeight = Gui.baseScale and (heightProp * scaleY) or heightProp + tempHeight = Context.baseScale and (heightProp * scaleY) or heightProp self.units.height = { value = heightProp, unit = "px" } end self.height = tempHeight @@ -760,6 +731,7 @@ function Element.new(props) -- Check if we should use 9-patch content padding for auto-sizing local use9PatchPadding = false local ninePatchContentPadding = nil + local tempPadding = nil if self._themeManager:hasThemeComponent() then local component = self._themeManager:getComponent() if component and component._ninePatchData and component._ninePatchData.contentPadding then @@ -777,31 +749,22 @@ function Element.new(props) then use9PatchPadding = true ninePatchContentPadding = component._ninePatchData.contentPadding + local scaledPadding = self._themeManager:getScaledContentPadding(tempWidth, tempHeight) + if scaledPadding then + tempPadding = scaledPadding + else + tempPadding = { + left = ninePatchContentPadding.left, + top = ninePatchContentPadding.top, + right = ninePatchContentPadding.right, + bottom = ninePatchContentPadding.bottom, + } + end end + tempPadding = Units.resolveSpacing(props.padding, self.width, self.height) end end - -- First, resolve padding using temporary dimensions - -- For auto-sized elements, this is content width; for explicit sizing, this is border-box width - local tempPadding - if use9PatchPadding then - -- Get scaled 9-patch content padding from ThemeManager - local scaledPadding = self._themeManager:getScaledContentPadding(tempWidth, tempHeight) - if scaledPadding then - tempPadding = scaledPadding - else - -- Fallback if scaling fails - tempPadding = { - left = ninePatchContentPadding.left, - top = ninePatchContentPadding.top, - right = ninePatchContentPadding.right, - bottom = ninePatchContentPadding.bottom, - } - end - else - tempPadding = Units.resolveSpacing(props.padding, self.width, self.height) - end - -- Margin percentages are relative to parent's dimensions (CSS spec) local parentWidth = self.parent and self.parent.width or viewportWidth local parentHeight = self.parent and self.parent.height or viewportHeight @@ -853,8 +816,8 @@ function Element.new(props) end -- Apply min/max constraints (also scaled) - local minSize = self.minTextSize and (Gui.baseScale and (self.minTextSize * scaleY) or self.minTextSize) - local maxSize = self.maxTextSize and (Gui.baseScale and (self.maxTextSize * scaleY) or self.maxTextSize) + local minSize = self.minTextSize and (Context.baseScale and (self.minTextSize * scaleY) or self.minTextSize) + local maxSize = self.maxTextSize and (Context.baseScale and (self.maxTextSize * scaleY) or self.maxTextSize) if minSize and self.textSize < minSize then self.textSize = minSize @@ -943,7 +906,7 @@ function Element.new(props) ------ add hereditary ------ if props.parent == nil then - table.insert(Gui.topElements, self) + table.insert(Context.topElements, self) -- Handle x position with units if props.x then @@ -953,7 +916,7 @@ function Element.new(props) self.x = Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth) else -- Apply base scaling to pixel positions - self.x = Gui.baseScale and (props.x * scaleX) or props.x + self.x = Context.baseScale and (props.x * scaleX) or props.x self.units.x = { value = props.x, unit = "px" } end else @@ -969,7 +932,7 @@ function Element.new(props) self.y = Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight) else -- Apply base scaling to pixel positions - self.y = Gui.baseScale and (props.y * scaleY) or props.y + self.y = Context.baseScale and (props.y * scaleY) or props.y self.units.y = { value = props.y, unit = "px" } end else @@ -1039,7 +1002,7 @@ function Element.new(props) self.x = Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth) else -- Apply base scaling to pixel positions - self.x = Gui.baseScale and (props.x * scaleX) or props.x + self.x = Context.baseScale and (props.x * scaleX) or props.x self.units.x = { value = props.x, unit = "px" } end else @@ -1056,7 +1019,7 @@ function Element.new(props) self.y = Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight) else -- Apply base scaling to pixel positions - self.y = Gui.baseScale and (props.y * scaleY) or props.y + self.y = Context.baseScale and (props.y * scaleY) or props.y self.units.y = { value = props.y, unit = "px" } end else @@ -1079,7 +1042,7 @@ function Element.new(props) self.x = baseX + offsetX else -- Apply base scaling to pixel offsets - local scaledOffset = Gui.baseScale and (props.x * scaleX) or props.x + local scaledOffset = Context.baseScale and (props.x * scaleX) or props.x self.x = baseX + scaledOffset self.units.x = { value = props.x, unit = "px" } end @@ -1092,12 +1055,12 @@ function Element.new(props) if type(props.y) == "string" then local value, unit = Units.parse(props.y) self.units.y = { value = value, unit = unit } - local parentHeight = self.parent.height + parentHeight = self.parent.height local offsetY = Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight) self.y = baseY + offsetY else -- Apply base scaling to pixel offsets - local scaledOffset = Gui.baseScale and (props.y * scaleY) or props.y + local scaledOffset = Context.baseScale and (props.y * scaleY) or props.y self.y = baseY + scaledOffset self.units.y = { value = props.y, unit = "px" } end @@ -1109,13 +1072,11 @@ function Element.new(props) self.z = props.z or self.parent.z or 0 end - -- Set textColor with priority: props > parent > theme text color > black if props.textColor then self.textColor = props.textColor elseif self.parent.textColor then self.textColor = self.parent.textColor else - -- Try to get text color from theme via ThemeManager local themeToUse = self._themeManager:getTheme() if themeToUse and themeToUse.colors and themeToUse.colors.text then self.textColor = themeToUse.colors.text @@ -1267,7 +1228,7 @@ function Element.new(props) utils = utils, Grid = Grid, Units = Units, - Gui = Gui, + Context = Context, }) -- Initialize immediately so it can be used for auto-sizing calculations self._layoutEngine:initialize(self) @@ -1327,7 +1288,7 @@ function Element.new(props) end -- Register element in z-index tracking for immediate mode - if Gui._immediateMode then + if Context._immediateMode then Context.registerElement(self) end @@ -1718,7 +1679,7 @@ function Element:addChild(child) -- In immediate mode, defer layout until endFrame() when all elements are created -- This prevents premature overflow detection with incomplete children - if not Gui._immediateMode then + if not Context._immediateMode then self:layoutChildren() end end @@ -1738,9 +1699,9 @@ end --- Destroy element and its children function Element:destroy() -- Remove from global elements list - for i, win in ipairs(Gui.topElements) do + for i, win in ipairs(Context.topElements) do if win == self then - table.remove(Gui.topElements, i) + table.remove(Context.topElements, i) break end end @@ -1938,17 +1899,15 @@ end ---@param dt number function Element:update(dt) -- Restore scrollbar state from StateManager in immediate mode - if self._stateId and Gui._immediateMode then + if self._stateId and Context._immediateMode then local state = StateManager.getState(self._stateId) if state then - -- Restore to Element properties (for backward compatibility) self._scrollbarHoveredVertical = state.scrollbarHoveredVertical or false self._scrollbarHoveredHorizontal = state.scrollbarHoveredHorizontal or false self._scrollbarDragging = state.scrollbarDragging or false self._hoveredScrollbar = state.hoveredScrollbar self._scrollbarDragOffset = state.scrollbarDragOffset or 0 - -- Also restore to ScrollManager if it exists if self._scrollManager then self._scrollManager._scrollbarHoveredVertical = self._scrollbarHoveredVertical self._scrollManager._scrollbarHoveredHorizontal = self._scrollbarHoveredHorizontal @@ -1988,14 +1947,12 @@ function Element:update(dt) local mx, my = love.mouse.getPosition() - -- Update scrollbar hover state via ScrollManager if self._scrollManager then self._scrollManager:updateHoverState(mx, my) self:_syncScrollManagerState() end - -- Update scrollbar state in StateManager if in immediate mode - if self._stateId and Gui._immediateMode then + if self._stateId and Context._immediateMode then StateManager.updateState(self._stateId, { scrollbarHoveredVertical = self._scrollbarHoveredVertical, scrollbarHoveredHorizontal = self._scrollbarHoveredHorizontal, @@ -2004,18 +1961,15 @@ function Element:update(dt) }) end - -- Handle scrollbar dragging if self._scrollbarDragging and love.mouse.isDown(1) then self:_handleScrollbarDrag(mx, my) elseif self._scrollbarDragging then - -- Mouse button released - delegate to ScrollManager if self._scrollManager then self._scrollManager:handleMouseRelease(1) self:_syncScrollManagerState() end - -- Update StateManager if in immediate mode - if self._stateId and Gui._immediateMode then + if self._stateId and Context._immediateMode then StateManager.updateState(self._stateId, { scrollbarDragging = false, }) @@ -2080,13 +2034,13 @@ function Element:update(dt) -- Check if this is the topmost element at the mouse position (z-index ordering) -- This prevents blocked elements from receiving interactions or visual feedback local isActiveElement - if Gui._immediateMode then + if Context._immediateMode then -- In immediate mode, use z-index occlusion detection local topElement = Context.getTopElementAt(mx, my) isActiveElement = (topElement == self or topElement == nil) else -- In retained mode, use the old _activeEventElement mechanism - isActiveElement = (Gui._activeEventElement == nil or Gui._activeEventElement == self) + isActiveElement = (Context._activeEventElement == nil or Context._activeEventElement == self) end -- Update theme state based on interaction @@ -2098,7 +2052,7 @@ function Element:update(dt) local newThemeState = self._themeManager:updateState(isHovering and isActiveElement, anyPressed, self._focused, self.disabled) -- Update state (in StateManager if in immediate mode, otherwise locally) - if self._stateId and Gui._immediateMode then + if self._stateId and Context._immediateMode then -- Update in StateManager for immediate mode local hover = newThemeState == "hover" local pressed = newThemeState == "pressed" @@ -2180,15 +2134,15 @@ function Element:resize(newGameWidth, newGameHeight) if self.units.textSize.value then local unit = self.units.textSize.unit local value = self.units.textSize.value - local _, scaleY = Gui.getScaleFactors() + local _, scaleY = Context.getScaleFactors() if unit == "ew" then -- Element width relative (use current width) self.textSize = (value / 100) * self.width -- Apply min/max constraints - local minSize = self.minTextSize and (Gui.baseScale and (self.minTextSize * scaleY) or self.minTextSize) - local maxSize = self.maxTextSize and (Gui.baseScale and (self.maxTextSize * scaleY) or self.maxTextSize) + local minSize = self.minTextSize and (Context.baseScale and (self.minTextSize * scaleY) or self.minTextSize) + local maxSize = self.maxTextSize and (Context.baseScale and (self.maxTextSize * scaleY) or self.maxTextSize) if minSize and self.textSize < minSize then self.textSize = minSize end @@ -2203,8 +2157,8 @@ function Element:resize(newGameWidth, newGameHeight) self.textSize = (value / 100) * self.height -- Apply min/max constraints - local minSize = self.minTextSize and (Gui.baseScale and (self.minTextSize * scaleY) or self.minTextSize) - local maxSize = self.maxTextSize and (Gui.baseScale and (self.maxTextSize * scaleY) or self.maxTextSize) + local minSize = self.minTextSize and (Context.baseScale and (self.minTextSize * scaleY) or self.minTextSize) + local maxSize = self.maxTextSize and (Context.baseScale and (self.maxTextSize * scaleY) or self.maxTextSize) if minSize and self.textSize < minSize then self.textSize = minSize end @@ -2230,7 +2184,6 @@ function Element:calculateTextWidth() end if self.textSize then - -- Resolve font path from font family (same logic as in draw) local fontPath = nil if self.fontFamily then local themeToUse = self._themeManager:getTheme() @@ -2245,7 +2198,6 @@ function Element:calculateTextWidth() local tempFont = FONT_CACHE.get(self.textSize, fontPath) local width = tempFont:getWidth(self.text) - -- Apply contentAutoSizingMultiplier if set if self.contentAutoSizingMultiplier and self.contentAutoSizingMultiplier.width then width = width * self.contentAutoSizingMultiplier.width end @@ -2254,7 +2206,6 @@ function Element:calculateTextWidth() local font = love.graphics.getFont() local width = font:getWidth(self.text) - -- Apply contentAutoSizingMultiplier if set if self.contentAutoSizingMultiplier and self.contentAutoSizingMultiplier.width then width = width * self.contentAutoSizingMultiplier.width end @@ -2267,10 +2218,8 @@ function Element:calculateTextHeight() return 0 end - -- Get the font local font if self.textSize then - -- Resolve font path from font family (same logic as in draw) local fontPath = nil if self.fontFamily then local themeToUse = self._themeManager:getTheme() @@ -2289,26 +2238,19 @@ function Element:calculateTextHeight() local height = font:getHeight() - -- If text wrapping is enabled, calculate height based on wrapped lines if self.textWrap and (self.textWrap == "word" or self.textWrap == "char" or self.textWrap == true) then - -- Calculate available width for wrapping local availableWidth = self.width - -- If width is not set or is 0, try to use parent's content width if (not availableWidth or availableWidth <= 0) and self.parent then - -- Use parent's content width (excluding padding) availableWidth = self.parent.width end if availableWidth and availableWidth > 0 then - -- Get the wrapped text lines using getWrap (returns width and table of lines) local wrappedWidth, wrappedLines = font:getWrap(self.text, availableWidth) - -- Height is line height * number of lines height = height * #wrappedLines end end - -- Apply contentAutoSizingMultiplier if set if self.contentAutoSizingMultiplier and self.contentAutoSizingMultiplier.height then height = height * self.contentAutoSizingMultiplier.height end @@ -2317,19 +2259,11 @@ function Element:calculateTextHeight() end function Element:calculateAutoWidth() - -- During construction, LayoutEngine might not be initialized yet - -- Fall back to text width calculation - if not self._layoutEngine then - return self:calculateTextWidth() - end - -- Delegate to LayoutEngine return self._layoutEngine:calculateAutoWidth() end --- Calculate auto height based on children function Element:calculateAutoHeight() - -- During construction, LayoutEngine might not be initialized yet - -- Fall back to text height calculation if not self._layoutEngine then return self:calculateTextHeight() end diff --git a/modules/LayoutEngine.lua b/modules/LayoutEngine.lua index 6936f79..b98f234 100644 --- a/modules/LayoutEngine.lua +++ b/modules/LayoutEngine.lua @@ -13,7 +13,7 @@ ---@field rowGap number? Gap between grid rows ---@field _Grid table ---@field _Units table ----@field _Gui table +---@field _Context table ---@field _Positioning table ---@field _FlexDirection table ---@field _JustifyContent table @@ -39,7 +39,7 @@ LayoutEngine.__index = LayoutEngine --- Create a new LayoutEngine instance ---@param props LayoutEngineProps ----@param deps table Dependencies {utils, Grid, Units, Gui} +---@param deps table Dependencies {utils, Grid, Units, Context} ---@return LayoutEngine function LayoutEngine.new(props, deps) local enums = deps.utils.enums @@ -56,7 +56,7 @@ function LayoutEngine.new(props, deps) -- Store dependencies for instance methods self._Grid = deps.Grid self._Units = deps.Units - self._Gui = deps.Gui + self._Context = deps.Context self._Positioning = Positioning self._FlexDirection = FlexDirection self._JustifyContent = JustifyContent @@ -141,12 +141,14 @@ end --- Layout children within this element according to positioning mode function LayoutEngine:layoutChildren() - local element = self.element + if self.element == nil then + return + end if self.positioning == self._Positioning.ABSOLUTE or self.positioning == self._Positioning.RELATIVE then -- Absolute/Relative positioned containers don't layout their children according to flex rules, -- but they should still apply CSS positioning offsets to their children - for _, child in ipairs(element.children) do + for _, child in ipairs(self.element.children) do if child.top or child.right or child.bottom or child.left then self:applyPositioningOffsets(child) end @@ -156,11 +158,11 @@ function LayoutEngine:layoutChildren() -- Handle grid layout if self.positioning == self._Positioning.GRID then - self._Grid.layoutGridItems(element) + self._Grid.layoutGridItems(self.element) return end - local childCount = #element.children + local childCount = #self.element.children if childCount == 0 then return @@ -168,7 +170,7 @@ function LayoutEngine:layoutChildren() -- Get flex children (children that participate in flex layout) local flexChildren = {} - for _, child in ipairs(element.children) do + for _, child in ipairs(self.element.children) do local isFlexChild = not (child.positioning == self._Positioning.ABSOLUTE and child._explicitlyAbsolute) if isFlexChild then table.insert(flexChildren, child) @@ -185,7 +187,7 @@ function LayoutEngine:layoutChildren() local reservedCrossStart = 0 -- Space reserved at the start of cross axis (top for horizontal, left for vertical) local reservedCrossEnd = 0 -- Space reserved at the end of cross axis (bottom for horizontal, right for vertical) - for _, child in ipairs(element.children) do + for _, child in ipairs(self.element.children) do -- Only consider absolutely positioned children with explicit positioning if child.positioning == self._Positioning.ABSOLUTE and child._explicitlyAbsolute then -- BORDER-BOX MODEL: Use border-box dimensions for space calculations @@ -245,11 +247,11 @@ function LayoutEngine:layoutChildren() local availableMainSize = 0 local availableCrossSize = 0 if self.flexDirection == self._FlexDirection.HORIZONTAL then - availableMainSize = element.width - reservedMainStart - reservedMainEnd - availableCrossSize = element.height - reservedCrossStart - reservedCrossEnd + availableMainSize = self.element.width - reservedMainStart - reservedMainEnd + availableCrossSize = self.element.height - reservedCrossStart - reservedCrossEnd else - availableMainSize = element.height - reservedMainStart - reservedMainEnd - availableCrossSize = element.width - reservedCrossStart - reservedCrossEnd + availableMainSize = self.element.height - reservedMainStart - reservedMainEnd + availableCrossSize = self.element.width - reservedCrossStart - reservedCrossEnd end -- Handle flex wrap: create lines of children @@ -448,11 +450,16 @@ function LayoutEngine:layoutChildren() local childTotalCrossSize = childBorderBoxHeight + child.margin.top + child.margin.bottom if effectiveAlign == self._AlignItems.FLEX_START then - child.y = element.y + element.padding.top + reservedCrossStart + currentCrossPos + child.margin.top + child.y = self.element.y + self.element.padding.top + reservedCrossStart + currentCrossPos + child.margin.top elseif effectiveAlign == self._AlignItems.CENTER then - child.y = element.y + element.padding.top + reservedCrossStart + currentCrossPos + ((lineHeight - childTotalCrossSize) / 2) + child.margin.top + child.y = self.element.y + + self.element.padding.top + + reservedCrossStart + + currentCrossPos + + ((lineHeight - childTotalCrossSize) / 2) + + child.margin.top elseif effectiveAlign == self._AlignItems.FLEX_END then - child.y = element.y + element.padding.top + reservedCrossStart + currentCrossPos + lineHeight - childTotalCrossSize + child.margin.top + child.y = self.element.y + self.element.padding.top + reservedCrossStart + currentCrossPos + lineHeight - childTotalCrossSize + child.margin.top elseif effectiveAlign == self._AlignItems.STRETCH then -- STRETCH: Only apply if height was not explicitly set if child.autosizing and child.autosizing.height then @@ -461,7 +468,7 @@ function LayoutEngine:layoutChildren() child._borderBoxHeight = availableHeight child.height = math.max(0, availableHeight - child.padding.top - child.padding.bottom) end - child.y = element.y + element.padding.top + reservedCrossStart + currentCrossPos + child.margin.top + child.y = self.element.y + self.element.padding.top + reservedCrossStart + currentCrossPos + child.margin.top end -- Apply positioning offsets (top, right, bottom, left) @@ -485,11 +492,16 @@ function LayoutEngine:layoutChildren() local childTotalCrossSize = childBorderBoxWidth + child.margin.left + child.margin.right if effectiveAlign == self._AlignItems.FLEX_START then - child.x = element.x + element.padding.left + reservedCrossStart + currentCrossPos + child.margin.left + child.x = self.element.x + self.element.padding.left + reservedCrossStart + currentCrossPos + child.margin.left elseif effectiveAlign == self._AlignItems.CENTER then - child.x = element.x + element.padding.left + reservedCrossStart + currentCrossPos + ((lineHeight - childTotalCrossSize) / 2) + child.margin.left + child.x = self.element.x + + self.element.padding.left + + reservedCrossStart + + currentCrossPos + + ((lineHeight - childTotalCrossSize) / 2) + + child.margin.left elseif effectiveAlign == self._AlignItems.FLEX_END then - child.x = element.x + element.padding.left + reservedCrossStart + currentCrossPos + lineHeight - childTotalCrossSize + child.margin.left + child.x = self.element.x + self.element.padding.left + reservedCrossStart + currentCrossPos + lineHeight - childTotalCrossSize + child.margin.left elseif effectiveAlign == self._AlignItems.STRETCH then -- STRETCH: Only apply if width was not explicitly set if child.autosizing and child.autosizing.width then @@ -498,7 +510,7 @@ function LayoutEngine:layoutChildren() child._borderBoxWidth = availableWidth child.width = math.max(0, availableWidth - child.padding.left - child.padding.right) end - child.x = element.x + element.padding.left + reservedCrossStart + currentCrossPos + child.margin.left + child.x = self.element.x + self.element.padding.left + reservedCrossStart + currentCrossPos + child.margin.left end -- Apply positioning offsets (top, right, bottom, left) @@ -519,7 +531,7 @@ function LayoutEngine:layoutChildren() end -- Position explicitly absolute children after flex layout - for _, child in ipairs(element.children) do + for _, child in ipairs(self.element.children) do if child.positioning == self._Positioning.ABSOLUTE and child._explicitlyAbsolute then -- Apply positioning offsets (top, right, bottom, left) self:applyPositioningOffsets(child) @@ -532,19 +544,21 @@ function LayoutEngine:layoutChildren() end -- Detect overflow after children are laid out - if element._detectOverflow then - element:_detectOverflow() + if self.element._detectOverflow then + self.element:_detectOverflow() end end --- Calculate auto width based on children ----@return number The calculated width +---@return number function LayoutEngine:calculateAutoWidth() - local element = self.element + if self.element == nil then + return 0 + end -- BORDER-BOX MODEL: Calculate content width, caller will add padding to get border-box - local contentWidth = element:calculateTextWidth() - if not element.children or #element.children == 0 then + local contentWidth = self.element:calculateTextWidth() + if not self.element.children or #self.element.children == 0 then return contentWidth end @@ -555,7 +569,7 @@ function LayoutEngine:calculateAutoWidth() local maxWidth = contentWidth local participatingChildren = 0 - for _, child in ipairs(element.children) do + for _, child in ipairs(self.element.children) do -- Skip explicitly absolute positioned children as they don't affect parent auto-sizing if not child._explicitlyAbsolute then -- BORDER-BOX MODEL: Use border-box width for auto-sizing calculations @@ -578,13 +592,14 @@ function LayoutEngine:calculateAutoWidth() end end ---- Calculate auto height based on children ----@return number The calculated height +---@return number function LayoutEngine:calculateAutoHeight() - local element = self.element + if self.element == nil then + return 0 + end - local height = element:calculateTextHeight() - if not element.children or #element.children == 0 then + local height = self.element:calculateTextHeight() + if not self.element.children or #self.element.children == 0 then return height end @@ -595,7 +610,7 @@ function LayoutEngine:calculateAutoHeight() local maxHeight = height local participatingChildren = 0 - for _, child in ipairs(element.children) do + for _, child in ipairs(self.element.children) do -- Skip explicitly absolute positioned children as they don't affect parent auto-sizing if not child._explicitlyAbsolute then -- BORDER-BOX MODEL: Use border-box height for auto-sizing calculations @@ -622,162 +637,174 @@ end ---@param newViewportWidth number ---@param newViewportHeight number function LayoutEngine:recalculateUnits(newViewportWidth, newViewportHeight) - local element = self.element + if self.element == nil then + return + end local Units = self._Units - local Gui = self._Gui -- Get updated scale factors - local scaleX, scaleY = Gui.getScaleFactors() + local scaleX, scaleY = self._Context.getScaleFactors() -- Recalculate border-box width if using viewport or percentage units (skip auto-sized) -- Store in _borderBoxWidth temporarily, will calculate content width after padding is resolved - if element.units.width.unit ~= "px" and element.units.width.unit ~= "auto" then - local parentWidth = element.parent and element.parent.width or newViewportWidth - element._borderBoxWidth = Units.resolve(element.units.width.value, element.units.width.unit, newViewportWidth, newViewportHeight, parentWidth) - elseif element.units.width.unit == "px" and element.units.width.value and Gui.baseScale then + if self.element.units.width.unit ~= "px" and self.element.units.width.unit ~= "auto" then + local parentWidth = self.element.parent and self.element.parent.width or newViewportWidth + self.element._borderBoxWidth = + Units.resolve(self.element.units.width.value, self.element.units.width.unit, newViewportWidth, newViewportHeight, parentWidth) + elseif self.element.units.width.unit == "px" and self.element.units.width.value and self._Context.baseScale then -- Reapply base scaling to pixel widths (border-box) - element._borderBoxWidth = element.units.width.value * scaleX + self.element._borderBoxWidth = self.element.units.width.value * scaleX end -- Recalculate border-box height if using viewport or percentage units (skip auto-sized) -- Store in _borderBoxHeight temporarily, will calculate content height after padding is resolved - if element.units.height.unit ~= "px" and element.units.height.unit ~= "auto" then - local parentHeight = element.parent and element.parent.height or newViewportHeight - element._borderBoxHeight = Units.resolve(element.units.height.value, element.units.height.unit, newViewportWidth, newViewportHeight, parentHeight) - elseif element.units.height.unit == "px" and element.units.height.value and Gui.baseScale then + if self.element.units.height.unit ~= "px" and self.element.units.height.unit ~= "auto" then + local parentHeight = self.element.parent and self.element.parent.height or newViewportHeight + self.element._borderBoxHeight = + Units.resolve(self.element.units.height.value, self.element.units.height.unit, newViewportWidth, newViewportHeight, parentHeight) + elseif self.element.units.height.unit == "px" and self.element.units.height.value and self._Context.baseScale then -- Reapply base scaling to pixel heights (border-box) - element._borderBoxHeight = element.units.height.value * scaleY + self.element._borderBoxHeight = self.element.units.height.value * scaleY end -- Recalculate position if using viewport or percentage units - if element.units.x.unit ~= "px" then - local parentWidth = element.parent and element.parent.width or newViewportWidth - local baseX = element.parent and element.parent.x or 0 - local offsetX = Units.resolve(element.units.x.value, element.units.x.unit, newViewportWidth, newViewportHeight, parentWidth) - element.x = baseX + offsetX + if self.element.units.x.unit ~= "px" then + local parentWidth = self.element.parent and self.element.parent.width or newViewportWidth + local baseX = self.element.parent and self.element.parent.x or 0 + local offsetX = Units.resolve(self.element.units.x.value, self.element.units.x.unit, newViewportWidth, newViewportHeight, parentWidth) + self.element.x = baseX + offsetX else -- For pixel units, update position relative to parent's new position (with base scaling) - if element.parent then - local baseX = element.parent.x - local scaledOffset = Gui.baseScale and (element.units.x.value * scaleX) or element.units.x.value - element.x = baseX + scaledOffset - elseif Gui.baseScale then + if self.element.parent then + local baseX = self.element.parent.x + local scaledOffset = self._Context.baseScale and (self.element.units.x.value * scaleX) or self.element.units.x.value + self.element.x = baseX + scaledOffset + elseif self._Context.baseScale then -- Top-level element with pixel position - apply base scaling - element.x = element.units.x.value * scaleX + self.element.x = self.element.units.x.value * scaleX end end - if element.units.y.unit ~= "px" then - local parentHeight = element.parent and element.parent.height or newViewportHeight - local baseY = element.parent and element.parent.y or 0 - local offsetY = Units.resolve(element.units.y.value, element.units.y.unit, newViewportWidth, newViewportHeight, parentHeight) - element.y = baseY + offsetY + if self.element.units.y.unit ~= "px" then + local parentHeight = self.element.parent and self.element.parent.height or newViewportHeight + local baseY = self.element.parent and self.element.parent.y or 0 + local offsetY = Units.resolve(self.element.units.y.value, self.element.units.y.unit, newViewportWidth, newViewportHeight, parentHeight) + self.element.y = baseY + offsetY else -- For pixel units, update position relative to parent's new position (with base scaling) - if element.parent then - local baseY = element.parent.y - local scaledOffset = Gui.baseScale and (element.units.y.value * scaleY) or element.units.y.value - element.y = baseY + scaledOffset - elseif Gui.baseScale then + if self.element.parent then + local baseY = self.element.parent.y + local scaledOffset = self._Context.baseScale and (self.element.units.y.value * scaleY) or self.element.units.y.value + self.element.y = baseY + scaledOffset + elseif self._Context.baseScale then -- Top-level element with pixel position - apply base scaling - element.y = element.units.y.value * scaleY + self.element.y = self.element.units.y.value * scaleY end end -- Recalculate textSize if auto-scaling is enabled or using viewport/element-relative units - if element.autoScaleText and element.units.textSize.value then - local unit = element.units.textSize.unit - local value = element.units.textSize.value + if self.element.autoScaleText and self.element.units.textSize.value then + local unit = self.element.units.textSize.unit + local value = self.element.units.textSize.value - if unit == "px" and Gui.baseScale then + if unit == "px" and self._Context.baseScale then -- With base scaling: scale pixel values relative to base resolution - element.textSize = value * scaleY + self.element.textSize = value * scaleY elseif unit == "px" then -- Without base scaling but auto-scaling enabled: text doesn't scale - element.textSize = value + self.element.textSize = value elseif unit == "%" or unit == "vh" then -- Percentage and vh are relative to viewport height - element.textSize = Units.resolve(value, unit, newViewportWidth, newViewportHeight, newViewportHeight) + self.element.textSize = Units.resolve(value, unit, newViewportWidth, newViewportHeight, newViewportHeight) elseif unit == "vw" then -- vw is relative to viewport width - element.textSize = Units.resolve(value, unit, newViewportWidth, newViewportHeight, newViewportWidth) + self.element.textSize = Units.resolve(value, unit, newViewportWidth, newViewportHeight, newViewportWidth) elseif unit == "ew" then -- Element width relative - element.textSize = (value / 100) * element.width + self.element.textSize = (value / 100) * self.element.width elseif unit == "eh" then -- Element height relative - element.textSize = (value / 100) * element.height + self.element.textSize = (value / 100) * self.element.height else - element.textSize = Units.resolve(value, unit, newViewportWidth, newViewportHeight, nil) + self.element.textSize = Units.resolve(value, unit, newViewportWidth, newViewportHeight, nil) end -- Apply min/max constraints (with base scaling) - local minSize = element.minTextSize and (Gui.baseScale and (element.minTextSize * scaleY) or element.minTextSize) - local maxSize = element.maxTextSize and (Gui.baseScale and (element.maxTextSize * scaleY) or element.maxTextSize) + local minSize = self.element.minTextSize and (self._Context.baseScale and (self.element.minTextSize * scaleY) or self.element.minTextSize) + local maxSize = self.element.maxTextSize and (self._Context.baseScale and (self.element.maxTextSize * scaleY) or self.element.maxTextSize) - if minSize and element.textSize < minSize then - element.textSize = minSize + if minSize and self.element.textSize < minSize then + self.element.textSize = minSize end - if maxSize and element.textSize > maxSize then - element.textSize = maxSize + if maxSize and self.element.textSize > maxSize then + self.element.textSize = maxSize end -- Protect against too-small text sizes (minimum 1px) - if element.textSize < 1 then - element.textSize = 1 -- Minimum 1px + if self.element.textSize < 1 then + self.element.textSize = 1 -- Minimum 1px end - elseif element.units.textSize.unit == "px" and element.units.textSize.value and Gui.baseScale then + elseif self.element.units.textSize.unit == "px" and self.element.units.textSize.value and self._Context.baseScale then -- No auto-scaling but base scaling is set: reapply base scaling to pixel text sizes - element.textSize = element.units.textSize.value * scaleY + self.element.textSize = self.element.units.textSize.value * scaleY -- Protect against too-small text sizes (minimum 1px) - if element.textSize < 1 then - element.textSize = 1 -- Minimum 1px + if self.element.textSize < 1 then + self.element.textSize = 1 -- Minimum 1px end end -- Final protection: ensure textSize is always at least 1px (catches all edge cases) - if element.text and element.textSize and element.textSize < 1 then - element.textSize = 1 -- Minimum 1px + if self.element.text and self.element.textSize and self.element.textSize < 1 then + self.element.textSize = 1 -- Minimum 1px end -- Recalculate gap if using viewport or percentage units - if element.units.gap.unit ~= "px" then - local containerSize = (self.flexDirection == self._FlexDirection.HORIZONTAL) and (element.parent and element.parent.width or newViewportWidth) - or (element.parent and element.parent.height or newViewportHeight) - element.gap = Units.resolve(element.units.gap.value, element.units.gap.unit, newViewportWidth, newViewportHeight, containerSize) + if self.element.units.gap.unit ~= "px" then + local containerSize = (self.flexDirection == self._FlexDirection.HORIZONTAL) and (self.element.parent and self.element.parent.width or newViewportWidth) + or (self.element.parent and self.element.parent.height or newViewportHeight) + self.element.gap = Units.resolve(self.element.units.gap.value, self.element.units.gap.unit, newViewportWidth, newViewportHeight, containerSize) end -- Recalculate spacing (padding/margin) if using viewport or percentage units -- For percentage-based padding: -- - If element has a parent: use parent's border-box dimensions (CSS spec for child elements) -- - If element has no parent: use element's own border-box dimensions (CSS spec for root elements) - local parentBorderBoxWidth = element.parent and element.parent._borderBoxWidth or element._borderBoxWidth or newViewportWidth - local parentBorderBoxHeight = element.parent and element.parent._borderBoxHeight or element._borderBoxHeight or newViewportHeight + local parentBorderBoxWidth = self.element.parent and self.element.parent._borderBoxWidth or self.element._borderBoxWidth or newViewportWidth + local parentBorderBoxHeight = self.element.parent and self.element.parent._borderBoxHeight or self.element._borderBoxHeight or newViewportHeight -- Handle shorthand properties first (horizontal/vertical) local resolvedHorizontalPadding = nil local resolvedVerticalPadding = nil - if element.units.padding.horizontal and element.units.padding.horizontal.unit ~= "px" then - resolvedHorizontalPadding = - Units.resolve(element.units.padding.horizontal.value, element.units.padding.horizontal.unit, newViewportWidth, newViewportHeight, parentBorderBoxWidth) - elseif element.units.padding.horizontal and element.units.padding.horizontal.value then - resolvedHorizontalPadding = element.units.padding.horizontal.value + if self.element.units.padding.horizontal and self.element.units.padding.horizontal.unit ~= "px" then + resolvedHorizontalPadding = Units.resolve( + self.element.units.padding.horizontal.value, + self.element.units.padding.horizontal.unit, + newViewportWidth, + newViewportHeight, + parentBorderBoxWidth + ) + elseif self.element.units.padding.horizontal and self.element.units.padding.horizontal.value then + resolvedHorizontalPadding = self.element.units.padding.horizontal.value end - if element.units.padding.vertical and element.units.padding.vertical.unit ~= "px" then - resolvedVerticalPadding = - Units.resolve(element.units.padding.vertical.value, element.units.padding.vertical.unit, newViewportWidth, newViewportHeight, parentBorderBoxHeight) - elseif element.units.padding.vertical and element.units.padding.vertical.value then - resolvedVerticalPadding = element.units.padding.vertical.value + if self.element.units.padding.vertical and self.element.units.padding.vertical.unit ~= "px" then + resolvedVerticalPadding = Units.resolve( + self.element.units.padding.vertical.value, + self.element.units.padding.vertical.unit, + newViewportWidth, + newViewportHeight, + parentBorderBoxHeight + ) + elseif self.element.units.padding.vertical and self.element.units.padding.vertical.value then + resolvedVerticalPadding = self.element.units.padding.vertical.value end - -- Resolve individual padding sides (with fallback to shorthand) for _, side in ipairs({ "top", "right", "bottom", "left" }) do -- Check if this side was explicitly set or if we should use shorthand local useShorthand = false - if not element.units.padding[side].explicit then + if not self.element.units.padding[side].explicit then -- Not explicitly set, check if we have shorthand if side == "left" or side == "right" then useShorthand = resolvedHorizontalPadding ~= nil @@ -789,15 +816,15 @@ function LayoutEngine:recalculateUnits(newViewportWidth, newViewportHeight) if useShorthand then -- Use shorthand value if side == "left" or side == "right" then - element.padding[side] = resolvedHorizontalPadding + self.element.padding[side] = resolvedHorizontalPadding else - element.padding[side] = resolvedVerticalPadding + self.element.padding[side] = resolvedVerticalPadding end - elseif element.units.padding[side].unit ~= "px" then + elseif self.element.units.padding[side].unit ~= "px" then -- Recalculate non-pixel units local parentSize = (side == "top" or side == "bottom") and parentBorderBoxHeight or parentBorderBoxWidth - element.padding[side] = - Units.resolve(element.units.padding[side].value, element.units.padding[side].unit, newViewportWidth, newViewportHeight, parentSize) + self.element.padding[side] = + Units.resolve(self.element.units.padding[side].value, self.element.units.padding[side].unit, newViewportWidth, newViewportHeight, parentSize) end -- If unit is "px" and not using shorthand, value stays the same end @@ -806,25 +833,35 @@ function LayoutEngine:recalculateUnits(newViewportWidth, newViewportHeight) local resolvedHorizontalMargin = nil local resolvedVerticalMargin = nil - if element.units.margin.horizontal and element.units.margin.horizontal.unit ~= "px" then - resolvedHorizontalMargin = - Units.resolve(element.units.margin.horizontal.value, element.units.margin.horizontal.unit, newViewportWidth, newViewportHeight, parentBorderBoxWidth) - elseif element.units.margin.horizontal and element.units.margin.horizontal.value then - resolvedHorizontalMargin = element.units.margin.horizontal.value + if self.element.units.margin.horizontal and self.element.units.margin.horizontal.unit ~= "px" then + resolvedHorizontalMargin = Units.resolve( + self.element.units.margin.horizontal.value, + self.element.units.margin.horizontal.unit, + newViewportWidth, + newViewportHeight, + parentBorderBoxWidth + ) + elseif self.element.units.margin.horizontal and self.element.units.margin.horizontal.value then + resolvedHorizontalMargin = self.element.units.margin.horizontal.value end - if element.units.margin.vertical and element.units.margin.vertical.unit ~= "px" then - resolvedVerticalMargin = - Units.resolve(element.units.margin.vertical.value, element.units.margin.vertical.unit, newViewportWidth, newViewportHeight, parentBorderBoxHeight) - elseif element.units.margin.vertical and element.units.margin.vertical.value then - resolvedVerticalMargin = element.units.margin.vertical.value + if self.element.units.margin.vertical and self.element.units.margin.vertical.unit ~= "px" then + resolvedVerticalMargin = Units.resolve( + self.element.units.margin.vertical.value, + self.element.units.margin.vertical.unit, + newViewportWidth, + newViewportHeight, + parentBorderBoxHeight + ) + elseif self.element.units.margin.vertical and self.element.units.margin.vertical.value then + resolvedVerticalMargin = self.element.units.margin.vertical.value end -- Resolve individual margin sides (with fallback to shorthand) for _, side in ipairs({ "top", "right", "bottom", "left" }) do -- Check if this side was explicitly set or if we should use shorthand local useShorthand = false - if not element.units.margin[side].explicit then + if not self.element.units.margin[side].explicit then -- Not explicitly set, check if we have shorthand if side == "left" or side == "right" then useShorthand = resolvedHorizontalMargin ~= nil @@ -836,14 +873,15 @@ function LayoutEngine:recalculateUnits(newViewportWidth, newViewportHeight) if useShorthand then -- Use shorthand value if side == "left" or side == "right" then - element.margin[side] = resolvedHorizontalMargin + self.element.margin[side] = resolvedHorizontalMargin else - element.margin[side] = resolvedVerticalMargin + self.element.margin[side] = resolvedVerticalMargin end - elseif element.units.margin[side].unit ~= "px" then + elseif self.element.units.margin[side].unit ~= "px" then -- Recalculate non-pixel units local parentSize = (side == "top" or side == "bottom") and parentBorderBoxHeight or parentBorderBoxWidth - element.margin[side] = Units.resolve(element.units.margin[side].value, element.units.margin[side].unit, newViewportWidth, newViewportHeight, parentSize) + self.element.margin[side] = + Units.resolve(self.element.units.margin[side].value, self.element.units.margin[side].unit, newViewportWidth, newViewportHeight, parentSize) end -- If unit is "px" and not using shorthand, value stays the same end @@ -852,31 +890,31 @@ function LayoutEngine:recalculateUnits(newViewportWidth, newViewportHeight) -- For explicitly-sized elements (non-auto), _borderBoxWidth/_borderBoxHeight were set earlier -- Now we calculate content width/height by subtracting padding -- Only recalculate if using viewport/percentage units (where _borderBoxWidth actually changed) - if element.units.width.unit ~= "auto" and element.units.width.unit ~= "px" then + if self.element.units.width.unit ~= "auto" and self.element.units.width.unit ~= "px" then -- _borderBoxWidth was recalculated for viewport/percentage units -- Calculate content width by subtracting padding - element.width = math.max(0, element._borderBoxWidth - element.padding.left - element.padding.right) - elseif element.units.width.unit == "auto" then + self.element.width = math.max(0, self.element._borderBoxWidth - self.element.padding.left - self.element.padding.right) + elseif self.element.units.width.unit == "auto" then -- For auto-sized elements, width is content width (calculated in resize method) -- Update border-box to include padding - element._borderBoxWidth = element.width + element.padding.left + element.padding.right + self.element._borderBoxWidth = self.element.width + self.element.padding.left + self.element.padding.right end -- For pixel units, width stays as-is (may have been manually modified) - if element.units.height.unit ~= "auto" and element.units.height.unit ~= "px" then + if self.element.units.height.unit ~= "auto" and self.element.units.height.unit ~= "px" then -- _borderBoxHeight was recalculated for viewport/percentage units -- Calculate content height by subtracting padding - element.height = math.max(0, element._borderBoxHeight - element.padding.top - element.padding.bottom) - elseif element.units.height.unit == "auto" then + self.element.height = math.max(0, self.element._borderBoxHeight - self.element.padding.top - self.element.padding.bottom) + elseif self.element.units.height.unit == "auto" then -- For auto-sized elements, height is content height (calculated in resize method) -- Update border-box to include padding - element._borderBoxHeight = element.height + element.padding.top + element.padding.bottom + self.element._borderBoxHeight = self.element.height + self.element.padding.top + self.element.padding.bottom end -- For pixel units, height stays as-is (may have been manually modified) -- Detect overflow after layout calculations - if element._detectOverflow then - element:_detectOverflow() + if self.element._detectOverflow then + self.element:_detectOverflow() end end diff --git a/modules/Units.lua b/modules/Units.lua index a64b719..220f676 100644 --- a/modules/Units.lua +++ b/modules/Units.lua @@ -1,5 +1,13 @@ local Units = {} +local Context = nil + +--- Initialize Units module with Context dependency +---@param context table The Context module +function Units.initialize(context) + Context = context +end + ---@param value string|number ---@return number, string -- Returns numeric value and unit type ("px", "%", "vw", "vh") function Units.parse(value) @@ -66,8 +74,8 @@ end ---@return number, number -- width, height function Units.getViewport() -- Return cached viewport if available (only during resize operations) - if Gui and Gui._cachedViewport and Gui._cachedViewport.width > 0 then - return Gui._cachedViewport.width, Gui._cachedViewport.height + if Context and Context._cachedViewport and Context._cachedViewport.width > 0 then + return Context._cachedViewport.width, Context._cachedViewport.height end if love.graphics and love.graphics.getDimensions then diff --git a/modules/types.lua b/modules/types.lua index c36491e..765f56b 100644 --- a/modules/types.lua +++ b/modules/types.lua @@ -74,7 +74,7 @@ local AnimationProps = {} ---@field gridColumns number? -- Number of columns in the grid (default: 1) ---@field columnGap number|string? -- Gap between grid columns (default: 0) ---@field rowGap number|string? -- Gap between grid rows (default: 0) ----@field theme string? -- Theme name to use (e.g., "space", "metal"). Defaults to theme from Gui.init() +---@field theme string? -- Theme name to use (e.g., "space", "metal"). Defaults to theme from flexlove.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) diff --git a/themes/README.md b/themes/README.md index 1c18fbe..2c8b087 100644 --- a/themes/README.md +++ b/themes/README.md @@ -327,7 +327,7 @@ return { Elements with `themeComponent` automatically set `disableHighlight = true` to prevent the default gray pressed overlay from interfering with theme visuals. You can override this: ```lua -Gui.new({ +FlexLove.new({ themeComponent = "button", disableHighlight = false, -- Force enable highlight overlay -- ...