Compare commits
11 Commits
cd99f15cec
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b671f501ec | ||
|
|
3a14a2939d | ||
|
|
1024fd81de | ||
|
|
ce690aa5dc | ||
|
|
49f37a1bb0 | ||
|
|
ac3517067b | ||
|
|
6cd1c80df9 | ||
|
|
157b932e80 | ||
|
|
121d787a0c | ||
|
|
8c43b45344 | ||
|
|
32cc418449 |
@@ -63,7 +63,7 @@ local enums = utils.enums
|
|||||||
|
|
||||||
---@class FlexLove
|
---@class FlexLove
|
||||||
local flexlove = Context
|
local flexlove = Context
|
||||||
flexlove._VERSION = "0.7.3"
|
flexlove._VERSION = "0.8.0"
|
||||||
flexlove._DESCRIPTION = "UI Library for LÖVE Framework based on flexbox"
|
flexlove._DESCRIPTION = "UI Library for LÖVE Framework based on flexbox"
|
||||||
flexlove._URL = "https://github.com/mikefreno/FlexLove"
|
flexlove._URL = "https://github.com/mikefreno/FlexLove"
|
||||||
flexlove._LICENSE = [[
|
flexlove._LICENSE = [[
|
||||||
|
|||||||
8506
docs/api.html
8506
docs/api.html
File diff suppressed because it is too large
Load Diff
@@ -285,7 +285,7 @@ cp FlexLove/FlexLove.lua your-project/</code></pre>
|
|||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<p>
|
<p>
|
||||||
FlexLöve v0.7.3 | MIT License |
|
FlexLöve v0.8.0 | MIT License |
|
||||||
<a href="https://github.com/mikefreno/FlexLove" style="color: #58a6ff"
|
<a href="https://github.com/mikefreno/FlexLove" style="color: #58a6ff"
|
||||||
>GitHub Repository</a
|
>GitHub Repository</a
|
||||||
>
|
>
|
||||||
|
|||||||
5920
docs/versions/v0.7.3/api.html
Normal file
5920
docs/versions/v0.7.3/api.html
Normal file
File diff suppressed because it is too large
Load Diff
85
flexlove-0.8.0-1.rockspec
Normal file
85
flexlove-0.8.0-1.rockspec
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package = "flexlove"
|
||||||
|
version = "0.8.0-1"
|
||||||
|
|
||||||
|
source = {
|
||||||
|
url = "git+https://github.com/mikefreno/FlexLove.git",
|
||||||
|
tag = "v0.8.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
description = {
|
||||||
|
summary = "A comprehensive UI library providing flexbox/grid layouts, theming, animations, and event handling for LÖVE2D games",
|
||||||
|
detailed = [[
|
||||||
|
FlexLöve is a lightweight, flexible GUI library for LÖVE2D that implements a
|
||||||
|
flexbox-based layout system. The goals of this project are two-fold: first,
|
||||||
|
anyone with basic CSS knowledge should be able to use this library with minimal
|
||||||
|
learning curve. Second, this library should take you from early prototyping to
|
||||||
|
production.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Flexbox and Grid Layout systems
|
||||||
|
- Modern theming with 9-patch support
|
||||||
|
- Animations and transitions
|
||||||
|
- Image rendering with CSS-like object-fit
|
||||||
|
- Touch events and gesture recognition
|
||||||
|
- Text input with rich editing features
|
||||||
|
- Responsive design with viewport units
|
||||||
|
- Both immediate and retained rendering modes
|
||||||
|
|
||||||
|
Going this route, you will need to link the luarocks path to your project:
|
||||||
|
(for mac/linux)
|
||||||
|
```lua
|
||||||
|
package.path = package.path .. ";/Users/<username>/.luarocks/share/lua/<version>/?.lua"
|
||||||
|
package.path = package.path .. ";/Users/<username>/.luarocks/share/lua/<version>/?/init.lua"
|
||||||
|
package.cpath = package.cpath .. ";/Users/<username>/.luarocks/lib/lua/<version>/?.so"
|
||||||
|
```
|
||||||
|
]],
|
||||||
|
homepage = "https://mikefreno.github.io/FlexLove/",
|
||||||
|
license = "MIT",
|
||||||
|
maintainer = "Mike Freno",
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies = {
|
||||||
|
"lua >= 5.1",
|
||||||
|
"luautf8 >= 0.1.3",
|
||||||
|
}
|
||||||
|
|
||||||
|
build = {
|
||||||
|
type = "builtin",
|
||||||
|
modules = {
|
||||||
|
["FlexLove"] = "FlexLove.lua",
|
||||||
|
["FlexLove.modules.Animation"] = "modules/Animation.lua",
|
||||||
|
["FlexLove.modules.Blur"] = "modules/Blur.lua",
|
||||||
|
["FlexLove.modules.Calc"] = "modules/Calc.lua",
|
||||||
|
["FlexLove.modules.Color"] = "modules/Color.lua",
|
||||||
|
["FlexLove.modules.Context"] = "modules/Context.lua",
|
||||||
|
["FlexLove.modules.Element"] = "modules/Element.lua",
|
||||||
|
["FlexLove.modules.ErrorHandler"] = "modules/ErrorHandler.lua",
|
||||||
|
["FlexLove.modules.EventHandler"] = "modules/EventHandler.lua",
|
||||||
|
["FlexLove.modules.FFI"] = "modules/FFI.lua",
|
||||||
|
["FlexLove.modules.GestureRecognizer"] = "modules/GestureRecognizer.lua",
|
||||||
|
["FlexLove.modules.Grid"] = "modules/Grid.lua",
|
||||||
|
["FlexLove.modules.ImageCache"] = "modules/ImageCache.lua",
|
||||||
|
["FlexLove.modules.ImageRenderer"] = "modules/ImageRenderer.lua",
|
||||||
|
["FlexLove.modules.ImageScaler"] = "modules/ImageScaler.lua",
|
||||||
|
["FlexLove.modules.InputEvent"] = "modules/InputEvent.lua",
|
||||||
|
["FlexLove.modules.LayoutEngine"] = "modules/LayoutEngine.lua",
|
||||||
|
["FlexLove.modules.MemoryScanner"] = "modules/MemoryScanner.lua",
|
||||||
|
["FlexLove.modules.ModuleLoader"] = "modules/ModuleLoader.lua",
|
||||||
|
["FlexLove.modules.NinePatch"] = "modules/NinePatch.lua",
|
||||||
|
["FlexLove.modules.Performance"] = "modules/Performance.lua",
|
||||||
|
["FlexLove.modules.Renderer"] = "modules/Renderer.lua",
|
||||||
|
["FlexLove.modules.RoundedRect"] = "modules/RoundedRect.lua",
|
||||||
|
["FlexLove.modules.ScrollManager"] = "modules/ScrollManager.lua",
|
||||||
|
["FlexLove.modules.StateManager"] = "modules/StateManager.lua",
|
||||||
|
["FlexLove.modules.TextEditor"] = "modules/TextEditor.lua",
|
||||||
|
["FlexLove.modules.Theme"] = "modules/Theme.lua",
|
||||||
|
["FlexLove.modules.types"] = "modules/types.lua",
|
||||||
|
["FlexLove.modules.Units"] = "modules/Units.lua",
|
||||||
|
["FlexLove.modules.UTF8"] = "modules/UTF8.lua",
|
||||||
|
["FlexLove.modules.utils"] = "modules/utils.lua",
|
||||||
|
},
|
||||||
|
--copy_directories = {
|
||||||
|
--"docs",
|
||||||
|
--"examples",
|
||||||
|
--},
|
||||||
|
}
|
||||||
@@ -32,6 +32,10 @@
|
|||||||
---@field flexWrap FlexWrap -- Whether children wrap to multiple lines (default: NOWRAP)
|
---@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 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 alignSelf AlignSelf -- Alignment of the item itself along cross axis (default: AUTO)
|
||||||
|
---@field flex number|string? -- Shorthand for flexGrow, flexShrink, flexBasis (e.g., 1, "0 1 auto", "none")
|
||||||
|
---@field flexGrow number -- How much the item will grow relative to siblings (default: 0)
|
||||||
|
---@field flexShrink number -- How much the item will shrink relative to siblings (default: 1)
|
||||||
|
---@field flexBasis string|number -- Initial main size before growing/shrinking (default: "auto")
|
||||||
---@field textSize number? -- Resolved font size for text content in pixels
|
---@field textSize number? -- Resolved font size for text content in pixels
|
||||||
---@field minTextSize number?
|
---@field minTextSize number?
|
||||||
---@field maxTextSize number?
|
---@field maxTextSize number?
|
||||||
@@ -133,8 +137,11 @@
|
|||||||
---@field scrollbarRadius number? -- Scrollbar corner radius
|
---@field scrollbarRadius number? -- Scrollbar corner radius
|
||||||
---@field scrollbarPadding number? -- Scrollbar padding from edges
|
---@field scrollbarPadding number? -- Scrollbar padding from edges
|
||||||
---@field scrollSpeed number? -- Scroll speed multiplier
|
---@field scrollSpeed number? -- Scroll speed multiplier
|
||||||
|
---@field invertScroll boolean? -- Invert mouse wheel scroll direction (default: false)
|
||||||
---@field scrollBarStyle string? -- Scrollbar style name from theme (selects from theme.scrollbars)
|
---@field scrollBarStyle string? -- Scrollbar style name from theme (selects from theme.scrollbars)
|
||||||
---@field scrollbarKnobOffset number|table? -- Scrollbar knob/handle offset (number or {x, y} or {horizontal, vertical})
|
---@field scrollbarKnobOffset number|table? -- Scrollbar knob/handle offset (number or {x, y} or {horizontal, vertical})
|
||||||
|
---@field scrollbarPlacement string? -- "reserve-space"|"overlay" -- Whether scrollbar reserves space or overlays content (default: "reserve-space")
|
||||||
|
---@field scrollbarBalance boolean? -- When true, reserve space on both sides of content for visual balance (default: false)
|
||||||
---@field _overflowX boolean? -- Internal: whether content overflows horizontally
|
---@field _overflowX boolean? -- Internal: whether content overflows horizontally
|
||||||
---@field _overflowY boolean? -- Internal: whether content overflows vertically
|
---@field _overflowY boolean? -- Internal: whether content overflows vertically
|
||||||
---@field _contentWidth number? -- Internal: total content width
|
---@field _contentWidth number? -- Internal: total content width
|
||||||
@@ -355,6 +362,8 @@ function Element.new(props)
|
|||||||
self.onEnter = props.onEnter
|
self.onEnter = props.onEnter
|
||||||
self.onEnterDeferred = props.onEnterDeferred or false
|
self.onEnterDeferred = props.onEnterDeferred or false
|
||||||
|
|
||||||
|
self.customDraw = props.customDraw -- Custom rendering callback
|
||||||
|
|
||||||
-- Initialize state manager ID for immediate mode (use self.id which may be auto-generated)
|
-- Initialize state manager ID for immediate mode (use self.id which may be auto-generated)
|
||||||
self._stateId = self.id
|
self._stateId = self.id
|
||||||
|
|
||||||
@@ -568,7 +577,10 @@ function Element.new(props)
|
|||||||
end
|
end
|
||||||
else
|
else
|
||||||
-- Store as table only if non-zero values exist
|
-- Store as table only if non-zero values exist
|
||||||
local hasNonZero = props.cornerRadius.topLeft or props.cornerRadius.topRight or props.cornerRadius.bottomLeft or props.cornerRadius.bottomRight
|
local hasNonZero = props.cornerRadius.topLeft
|
||||||
|
or props.cornerRadius.topRight
|
||||||
|
or props.cornerRadius.bottomLeft
|
||||||
|
or props.cornerRadius.bottomRight
|
||||||
if hasNonZero then
|
if hasNonZero then
|
||||||
self.cornerRadius = {
|
self.cornerRadius = {
|
||||||
topLeft = props.cornerRadius.topLeft or 0,
|
topLeft = props.cornerRadius.topLeft or 0,
|
||||||
@@ -609,7 +621,8 @@ function Element.new(props)
|
|||||||
|
|
||||||
-- Validate objectFit
|
-- Validate objectFit
|
||||||
if props.objectFit then
|
if props.objectFit then
|
||||||
local validObjectFit = { fill = "fill", contain = "contain", cover = "cover", ["scale-down"] = "scale-down", none = "none" }
|
local validObjectFit =
|
||||||
|
{ fill = "fill", contain = "contain", cover = "cover", ["scale-down"] = "scale-down", none = "none" }
|
||||||
Element._utils.validateEnum(props.objectFit, validObjectFit, "objectFit")
|
Element._utils.validateEnum(props.objectFit, validObjectFit, "objectFit")
|
||||||
end
|
end
|
||||||
self.objectFit = props.objectFit or "fill"
|
self.objectFit = props.objectFit or "fill"
|
||||||
@@ -781,6 +794,7 @@ function Element.new(props)
|
|||||||
y = { value = nil, unit = "px" },
|
y = { value = nil, unit = "px" },
|
||||||
textSize = { value = nil, unit = "px" },
|
textSize = { value = nil, unit = "px" },
|
||||||
gap = { value = nil, unit = "px" },
|
gap = { value = nil, unit = "px" },
|
||||||
|
flexBasis = { value = nil, unit = "auto" },
|
||||||
padding = {
|
padding = {
|
||||||
top = { value = nil, unit = "px" },
|
top = { value = nil, unit = "px" },
|
||||||
right = { value = nil, unit = "px" },
|
right = { value = nil, unit = "px" },
|
||||||
@@ -1014,6 +1028,82 @@ function Element.new(props)
|
|||||||
self.units.gap = { value = 0, unit = "px" }
|
self.units.gap = { value = 0, unit = "px" }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Handle flex shorthand property (sets flexGrow, flexShrink, flexBasis)
|
||||||
|
if props.flex ~= nil then
|
||||||
|
local grow, shrink, basis = Element._Units.parseFlexShorthand(props.flex)
|
||||||
|
|
||||||
|
-- Only set individual properties if they weren't explicitly provided
|
||||||
|
if props.flexGrow == nil then
|
||||||
|
props.flexGrow = grow
|
||||||
|
end
|
||||||
|
if props.flexShrink == nil then
|
||||||
|
props.flexShrink = shrink
|
||||||
|
end
|
||||||
|
if props.flexBasis == nil then
|
||||||
|
props.flexBasis = basis
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle flexGrow property
|
||||||
|
if props.flexGrow ~= nil then
|
||||||
|
if type(props.flexGrow) == "number" and props.flexGrow >= 0 then
|
||||||
|
self.flexGrow = props.flexGrow
|
||||||
|
else
|
||||||
|
Element._ErrorHandler:warn("Element", "FLEX_001", {
|
||||||
|
element = self.id or "unnamed",
|
||||||
|
issue = "flexGrow must be a non-negative number",
|
||||||
|
value = tostring(props.flexGrow),
|
||||||
|
})
|
||||||
|
self.flexGrow = 0
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.flexGrow = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle flexShrink property
|
||||||
|
if props.flexShrink ~= nil then
|
||||||
|
if type(props.flexShrink) == "number" and props.flexShrink >= 0 then
|
||||||
|
self.flexShrink = props.flexShrink
|
||||||
|
else
|
||||||
|
Element._ErrorHandler:warn("Element", "FLEX_002", {
|
||||||
|
element = self.id or "unnamed",
|
||||||
|
issue = "flexShrink must be a non-negative number",
|
||||||
|
value = tostring(props.flexShrink),
|
||||||
|
})
|
||||||
|
self.flexShrink = 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.flexShrink = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle flexBasis property
|
||||||
|
if props.flexBasis ~= nil then
|
||||||
|
local isCalc = Element._Calc and Element._Calc.isCalc(props.flexBasis)
|
||||||
|
if props.flexBasis == "auto" then
|
||||||
|
self.flexBasis = "auto"
|
||||||
|
self.units.flexBasis = { value = nil, unit = "auto" }
|
||||||
|
elseif type(props.flexBasis) == "string" or isCalc then
|
||||||
|
local value, unit = Element._Units.parse(props.flexBasis)
|
||||||
|
self.units.flexBasis = { value = value, unit = unit }
|
||||||
|
-- Don't resolve yet - LayoutEngine will handle this during layout
|
||||||
|
self.flexBasis = props.flexBasis
|
||||||
|
elseif type(props.flexBasis) == "number" then
|
||||||
|
self.flexBasis = props.flexBasis
|
||||||
|
self.units.flexBasis = { value = props.flexBasis, unit = "px" }
|
||||||
|
else
|
||||||
|
Element._ErrorHandler:warn("Element", "FLEX_003", {
|
||||||
|
element = self.id or "unnamed",
|
||||||
|
issue = "flexBasis must be a number, string, or 'auto'",
|
||||||
|
value = tostring(props.flexBasis),
|
||||||
|
})
|
||||||
|
self.flexBasis = "auto"
|
||||||
|
self.units.flexBasis = { value = nil, unit = "auto" }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.flexBasis = "auto"
|
||||||
|
self.units.flexBasis = { value = nil, unit = "auto" }
|
||||||
|
end
|
||||||
|
|
||||||
-- BORDER-BOX MODEL: For auto-sizing, we need to add padding to content dimensions
|
-- BORDER-BOX MODEL: For auto-sizing, we need to add padding to content dimensions
|
||||||
-- For explicit sizing, width/height already include padding (border-box)
|
-- For explicit sizing, width/height already include padding (border-box)
|
||||||
|
|
||||||
@@ -1313,10 +1403,18 @@ function Element.new(props)
|
|||||||
-- Warn if CSS positioning properties are used without absolute positioning
|
-- Warn if CSS positioning properties are used without absolute positioning
|
||||||
if (props.top or props.bottom or props.left or props.right) and not self._explicitlyAbsolute then
|
if (props.top or props.bottom or props.left or props.right) and not self._explicitlyAbsolute then
|
||||||
local properties = {}
|
local properties = {}
|
||||||
if props.top then table.insert(properties, "top") end
|
if props.top then
|
||||||
if props.bottom then table.insert(properties, "bottom") end
|
table.insert(properties, "top")
|
||||||
if props.left then table.insert(properties, "left") end
|
end
|
||||||
if props.right then table.insert(properties, "right") end
|
if props.bottom then
|
||||||
|
table.insert(properties, "bottom")
|
||||||
|
end
|
||||||
|
if props.left then
|
||||||
|
table.insert(properties, "left")
|
||||||
|
end
|
||||||
|
if props.right then
|
||||||
|
table.insert(properties, "right")
|
||||||
|
end
|
||||||
Element._ErrorHandler:warn("Element", "LAY_011", {
|
Element._ErrorHandler:warn("Element", "LAY_011", {
|
||||||
element = self.id or "unnamed",
|
element = self.id or "unnamed",
|
||||||
positioning = self._originalPositioning or "relative",
|
positioning = self._originalPositioning or "relative",
|
||||||
@@ -1422,7 +1520,10 @@ function Element.new(props)
|
|||||||
else
|
else
|
||||||
-- Default: children in flex/grid containers participate in parent's layout
|
-- Default: children in flex/grid containers participate in parent's layout
|
||||||
-- children in relative/absolute containers default to relative
|
-- children in relative/absolute containers default to relative
|
||||||
if self.parent.positioning == Element._utils.enums.Positioning.FLEX or self.parent.positioning == Element._utils.enums.Positioning.GRID then
|
if
|
||||||
|
self.parent.positioning == Element._utils.enums.Positioning.FLEX
|
||||||
|
or self.parent.positioning == Element._utils.enums.Positioning.GRID
|
||||||
|
then
|
||||||
self.positioning = Element._utils.enums.Positioning.ABSOLUTE -- They are positioned BY flex/grid, not AS flex/grid
|
self.positioning = Element._utils.enums.Positioning.ABSOLUTE -- They are positioned BY flex/grid, not AS flex/grid
|
||||||
self._explicitlyAbsolute = false -- Participate in parent's layout
|
self._explicitlyAbsolute = false -- Participate in parent's layout
|
||||||
else
|
else
|
||||||
@@ -1583,10 +1684,18 @@ function Element.new(props)
|
|||||||
-- Warn if CSS positioning properties are used without absolute positioning
|
-- Warn if CSS positioning properties are used without absolute positioning
|
||||||
if (props.top or props.bottom or props.left or props.right) and not self._explicitlyAbsolute then
|
if (props.top or props.bottom or props.left or props.right) and not self._explicitlyAbsolute then
|
||||||
local properties = {}
|
local properties = {}
|
||||||
if props.top then table.insert(properties, "top") end
|
if props.top then
|
||||||
if props.bottom then table.insert(properties, "bottom") end
|
table.insert(properties, "top")
|
||||||
if props.left then table.insert(properties, "left") end
|
end
|
||||||
if props.right then table.insert(properties, "right") end
|
if props.bottom then
|
||||||
|
table.insert(properties, "bottom")
|
||||||
|
end
|
||||||
|
if props.left then
|
||||||
|
table.insert(properties, "left")
|
||||||
|
end
|
||||||
|
if props.right then
|
||||||
|
table.insert(properties, "right")
|
||||||
|
end
|
||||||
Element._ErrorHandler:warn("Element", "LAY_011", {
|
Element._ErrorHandler:warn("Element", "LAY_011", {
|
||||||
element = self.id or "unnamed",
|
element = self.id or "unnamed",
|
||||||
positioning = self._originalPositioning or "relative",
|
positioning = self._originalPositioning or "relative",
|
||||||
@@ -1827,10 +1936,13 @@ function Element.new(props)
|
|||||||
scrollbarRadius = props.scrollbarRadius,
|
scrollbarRadius = props.scrollbarRadius,
|
||||||
scrollbarPadding = props.scrollbarPadding,
|
scrollbarPadding = props.scrollbarPadding,
|
||||||
scrollSpeed = props.scrollSpeed,
|
scrollSpeed = props.scrollSpeed,
|
||||||
|
invertScroll = props.invertScroll,
|
||||||
smoothScrollEnabled = props.smoothScrollEnabled,
|
smoothScrollEnabled = props.smoothScrollEnabled,
|
||||||
scrollBarStyle = props.scrollBarStyle,
|
scrollBarStyle = props.scrollBarStyle,
|
||||||
scrollbarKnobOffset = props.scrollbarKnobOffset,
|
scrollbarKnobOffset = props.scrollbarKnobOffset,
|
||||||
hideScrollbars = props.hideScrollbars,
|
hideScrollbars = props.hideScrollbars,
|
||||||
|
scrollbarPlacement = props.scrollbarPlacement,
|
||||||
|
scrollbarBalance = props.scrollbarBalance,
|
||||||
_scrollX = props._scrollX,
|
_scrollX = props._scrollX,
|
||||||
_scrollY = props._scrollY,
|
_scrollY = props._scrollY,
|
||||||
}, scrollManagerDeps)
|
}, scrollManagerDeps)
|
||||||
@@ -1845,9 +1957,12 @@ function Element.new(props)
|
|||||||
self.scrollbarRadius = self._scrollManager.scrollbarRadius
|
self.scrollbarRadius = self._scrollManager.scrollbarRadius
|
||||||
self.scrollbarPadding = self._scrollManager.scrollbarPadding
|
self.scrollbarPadding = self._scrollManager.scrollbarPadding
|
||||||
self.scrollSpeed = self._scrollManager.scrollSpeed
|
self.scrollSpeed = self._scrollManager.scrollSpeed
|
||||||
|
self.invertScroll = self._scrollManager.invertScroll
|
||||||
self.scrollBarStyle = self._scrollManager.scrollBarStyle
|
self.scrollBarStyle = self._scrollManager.scrollBarStyle
|
||||||
self.scrollbarKnobOffset = self._scrollManager.scrollbarKnobOffset
|
self.scrollbarKnobOffset = self._scrollManager.scrollbarKnobOffset
|
||||||
self.hideScrollbars = self._scrollManager.hideScrollbars
|
self.hideScrollbars = self._scrollManager.hideScrollbars
|
||||||
|
self.scrollbarPlacement = self._scrollManager.scrollbarPlacement
|
||||||
|
self.scrollbarBalance = self._scrollManager.scrollbarBalance
|
||||||
|
|
||||||
-- Initialize state properties (will be synced from ScrollManager)
|
-- Initialize state properties (will be synced from ScrollManager)
|
||||||
self._overflowX = false
|
self._overflowX = false
|
||||||
@@ -1937,6 +2052,18 @@ function Element:getBorderBoxHeight()
|
|||||||
return self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
|
return self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Get computed box dimensions (content area position and size)
|
||||||
|
--- Returns the position and size of the content area (inside padding)
|
||||||
|
---@return {x: number, y: number, width: number, height: number}
|
||||||
|
function Element:getComputedBox()
|
||||||
|
return {
|
||||||
|
x = self.x + self.padding.left,
|
||||||
|
y = self.y + self.padding.top,
|
||||||
|
width = self.width,
|
||||||
|
height = self.height,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
--- Mark this element and its ancestors as dirty, requiring layout recalculation
|
--- Mark this element and its ancestors as dirty, requiring layout recalculation
|
||||||
--- Call this when element properties change that affect layout
|
--- Call this when element properties change that affect layout
|
||||||
function Element:invalidateLayout()
|
function Element:invalidateLayout()
|
||||||
@@ -2206,7 +2333,8 @@ function Element:getAvailableContentWidth()
|
|||||||
-- Check if the element is using the scaled 9-patch contentPadding as its padding
|
-- Check if the element is using the scaled 9-patch contentPadding as its padding
|
||||||
-- Allow small floating point differences (within 0.1 pixels)
|
-- Allow small floating point differences (within 0.1 pixels)
|
||||||
local usingContentPaddingAsPadding = (
|
local usingContentPaddingAsPadding = (
|
||||||
math.abs(self.padding.left - scaledContentPadding.left) < 0.1 and math.abs(self.padding.right - scaledContentPadding.right) < 0.1
|
math.abs(self.padding.left - scaledContentPadding.left) < 0.1
|
||||||
|
and math.abs(self.padding.right - scaledContentPadding.right) < 0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
if not usingContentPaddingAsPadding then
|
if not usingContentPaddingAsPadding then
|
||||||
@@ -2230,7 +2358,8 @@ function Element:getAvailableContentHeight()
|
|||||||
-- Check if the element is using the scaled 9-patch contentPadding as its padding
|
-- Check if the element is using the scaled 9-patch contentPadding as its padding
|
||||||
-- Allow small floating point differences (within 0.1 pixels)
|
-- Allow small floating point differences (within 0.1 pixels)
|
||||||
local usingContentPaddingAsPadding = (
|
local usingContentPaddingAsPadding = (
|
||||||
math.abs(self.padding.top - scaledContentPadding.top) < 0.1 and math.abs(self.padding.bottom - scaledContentPadding.bottom) < 0.1
|
math.abs(self.padding.top - scaledContentPadding.top) < 0.1
|
||||||
|
and math.abs(self.padding.bottom - scaledContentPadding.bottom) < 0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
if not usingContentPaddingAsPadding then
|
if not usingContentPaddingAsPadding then
|
||||||
@@ -2253,7 +2382,10 @@ function Element:addChild(child)
|
|||||||
-- If child was created without explicit positioning, inherit from parent
|
-- If child was created without explicit positioning, inherit from parent
|
||||||
if child._originalPositioning == nil then
|
if child._originalPositioning == nil then
|
||||||
-- No explicit positioning was set during construction
|
-- No explicit positioning was set during construction
|
||||||
if self.positioning == Element._utils.enums.Positioning.FLEX or self.positioning == Element._utils.enums.Positioning.GRID then
|
if
|
||||||
|
self.positioning == Element._utils.enums.Positioning.FLEX
|
||||||
|
or self.positioning == Element._utils.enums.Positioning.GRID
|
||||||
|
then
|
||||||
child.positioning = Element._utils.enums.Positioning.ABSOLUTE -- They are positioned BY flex/grid, not AS flex/grid
|
child.positioning = Element._utils.enums.Positioning.ABSOLUTE -- They are positioned BY flex/grid, not AS flex/grid
|
||||||
child._explicitlyAbsolute = false -- Participate in parent's layout
|
child._explicitlyAbsolute = false -- Participate in parent's layout
|
||||||
else
|
else
|
||||||
@@ -2463,7 +2595,8 @@ function Element:draw(backdropCanvas)
|
|||||||
if self.animation then
|
if self.animation then
|
||||||
local anim = self.animation:interpolate()
|
local anim = self.animation:interpolate()
|
||||||
if anim.opacity then
|
if anim.opacity then
|
||||||
drawBackgroundColor = Element._Color.new(self.backgroundColor.r, self.backgroundColor.g, self.backgroundColor.b, anim.opacity)
|
drawBackgroundColor =
|
||||||
|
Element._Color.new(self.backgroundColor.r, self.backgroundColor.g, self.backgroundColor.b, anim.opacity)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2477,6 +2610,14 @@ function Element:draw(backdropCanvas)
|
|||||||
-- LAYER 4: Delegate text rendering (text, cursor, selection, placeholder, password masking) to Renderer module
|
-- LAYER 4: Delegate text rendering (text, cursor, selection, placeholder, password masking) to Renderer module
|
||||||
self._renderer:drawText(self)
|
self._renderer:drawText(self)
|
||||||
|
|
||||||
|
-- LAYER 4.5: Custom draw callback (if provided)
|
||||||
|
if self.customDraw then
|
||||||
|
love.graphics.push()
|
||||||
|
love.graphics.setColor(1, 1, 1, 1) -- Reset color to white
|
||||||
|
self.customDraw(self)
|
||||||
|
love.graphics.pop()
|
||||||
|
end
|
||||||
|
|
||||||
-- Draw visual feedback when element is pressed (if it has an onEvent handler and highlight is not disabled)
|
-- Draw visual feedback when element is pressed (if it has an onEvent handler and highlight is not disabled)
|
||||||
if self.onEvent and not self.disableHighlight and self._eventHandler then
|
if self.onEvent and not self.disableHighlight and self._eventHandler then
|
||||||
-- Check if any button is pressed
|
-- Check if any button is pressed
|
||||||
@@ -2524,7 +2665,8 @@ function Element:draw(backdropCanvas)
|
|||||||
-- Priority: axis-specific (overflowX/Y) > general (overflow) > default (hidden)
|
-- Priority: axis-specific (overflowX/Y) > general (overflow) > default (hidden)
|
||||||
local overflowX = self.overflowX or self.overflow
|
local overflowX = self.overflowX or self.overflow
|
||||||
local overflowY = self.overflowY or self.overflow
|
local overflowY = self.overflowY or self.overflow
|
||||||
local needsOverflowClipping = (overflowX ~= "visible" or overflowY ~= "visible") and (overflowX ~= nil or overflowY ~= nil)
|
local needsOverflowClipping = (overflowX ~= "visible" or overflowY ~= "visible")
|
||||||
|
and (overflowX ~= nil or overflowY ~= nil)
|
||||||
|
|
||||||
-- Apply scroll offset if overflow is not visible
|
-- Apply scroll offset if overflow is not visible
|
||||||
local hasScrollOffset = needsOverflowClipping and (self._scrollX ~= 0 or self._scrollY ~= 0)
|
local hasScrollOffset = needsOverflowClipping and (self._scrollX ~= 0 or self._scrollY ~= 0)
|
||||||
@@ -2534,7 +2676,8 @@ function Element:draw(backdropCanvas)
|
|||||||
-- BORDER-BOX MODEL: Use stored border-box dimensions for clipping
|
-- BORDER-BOX MODEL: Use stored border-box dimensions for clipping
|
||||||
local borderBoxWidth = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
|
local borderBoxWidth = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
|
||||||
local borderBoxHeight = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
|
local borderBoxHeight = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
|
||||||
local stencilFunc = Element._RoundedRect.stencilFunction(self.x, self.y, borderBoxWidth, borderBoxHeight, self.cornerRadius)
|
local stencilFunc =
|
||||||
|
Element._RoundedRect.stencilFunction(self.x, self.y, borderBoxWidth, borderBoxHeight, self.cornerRadius)
|
||||||
|
|
||||||
-- Temporarily disable canvas for stencil operation (LÖVE 11.5 workaround)
|
-- Temporarily disable canvas for stencil operation (LÖVE 11.5 workaround)
|
||||||
local currentCanvas = love.graphics.getCanvas()
|
local currentCanvas = love.graphics.getCanvas()
|
||||||
@@ -2595,7 +2738,15 @@ function Element:draw(backdropCanvas)
|
|||||||
if self.contentBlur and self.contentBlur.radius > 0 and #sortedChildren > 0 then
|
if self.contentBlur and self.contentBlur.radius > 0 and #sortedChildren > 0 then
|
||||||
local blurInstance = self:getBlurInstance()
|
local blurInstance = self:getBlurInstance()
|
||||||
if blurInstance then
|
if blurInstance then
|
||||||
Element._Blur.applyToRegion(blurInstance, self.contentBlur.radius, self.x, self.y, borderBoxWidth, borderBoxHeight, drawChildren)
|
Element._Blur.applyToRegion(
|
||||||
|
blurInstance,
|
||||||
|
self.contentBlur.radius,
|
||||||
|
self.x,
|
||||||
|
self.y,
|
||||||
|
borderBoxWidth,
|
||||||
|
borderBoxHeight,
|
||||||
|
drawChildren
|
||||||
|
)
|
||||||
else
|
else
|
||||||
drawChildren()
|
drawChildren()
|
||||||
end
|
end
|
||||||
@@ -2785,7 +2936,12 @@ function Element:update(dt)
|
|||||||
-- Check if we should handle scrollbar press for elements with overflow
|
-- Check if we should handle scrollbar press for elements with overflow
|
||||||
local overflowX = self.overflowX or self.overflow
|
local overflowX = self.overflowX or self.overflow
|
||||||
local overflowY = self.overflowY or self.overflow
|
local overflowY = self.overflowY or self.overflow
|
||||||
local hasScrollableOverflow = (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto")
|
local hasScrollableOverflow = (
|
||||||
|
overflowX == "scroll"
|
||||||
|
or overflowX == "auto"
|
||||||
|
or overflowY == "scroll"
|
||||||
|
or overflowY == "auto"
|
||||||
|
)
|
||||||
|
|
||||||
if hasScrollableOverflow and not self._scrollbarDragging then
|
if hasScrollableOverflow and not self._scrollbarDragging then
|
||||||
-- Check for scrollbar press on left mouse button
|
-- Check for scrollbar press on left mouse button
|
||||||
@@ -2877,7 +3033,8 @@ function Element:update(dt)
|
|||||||
local anyPressed = self._eventHandler:isAnyButtonPressed()
|
local anyPressed = self._eventHandler:isAnyButtonPressed()
|
||||||
|
|
||||||
-- Update theme state via ThemeManager
|
-- Update theme state via ThemeManager
|
||||||
local newThemeState = self._themeManager:updateState(isHovering and isActiveElement, anyPressed, self._focused, self.disabled)
|
local newThemeState =
|
||||||
|
self._themeManager:updateState(isHovering and isActiveElement, anyPressed, self._focused, self.disabled)
|
||||||
|
|
||||||
if self._stateId and self._elementMode == "immediate" then
|
if self._stateId and self._elementMode == "immediate" then
|
||||||
local hover = newThemeState == "hover"
|
local hover = newThemeState == "hover"
|
||||||
@@ -2954,8 +3111,10 @@ function Element:resize(newGameWidth, newGameHeight)
|
|||||||
self.textSize = (value / 100) * self.width
|
self.textSize = (value / 100) * self.width
|
||||||
|
|
||||||
-- Apply min/max constraints
|
-- Apply min/max constraints
|
||||||
local minSize = self.minTextSize and (Element._Context.baseScale and (self.minTextSize * scaleY) or self.minTextSize)
|
local minSize = self.minTextSize
|
||||||
local maxSize = self.maxTextSize and (Element._Context.baseScale and (self.maxTextSize * scaleY) or self.maxTextSize)
|
and (Element._Context.baseScale and (self.minTextSize * scaleY) or self.minTextSize)
|
||||||
|
local maxSize = self.maxTextSize
|
||||||
|
and (Element._Context.baseScale and (self.maxTextSize * scaleY) or self.maxTextSize)
|
||||||
if minSize and self.textSize < minSize then
|
if minSize and self.textSize < minSize then
|
||||||
self.textSize = minSize
|
self.textSize = minSize
|
||||||
end
|
end
|
||||||
@@ -2970,8 +3129,10 @@ function Element:resize(newGameWidth, newGameHeight)
|
|||||||
self.textSize = (value / 100) * self.height
|
self.textSize = (value / 100) * self.height
|
||||||
|
|
||||||
-- Apply min/max constraints
|
-- Apply min/max constraints
|
||||||
local minSize = self.minTextSize and (Element._Context.baseScale and (self.minTextSize * scaleY) or self.minTextSize)
|
local minSize = self.minTextSize
|
||||||
local maxSize = self.maxTextSize and (Element._Context.baseScale and (self.maxTextSize * scaleY) or self.maxTextSize)
|
and (Element._Context.baseScale and (self.minTextSize * scaleY) or self.minTextSize)
|
||||||
|
local maxSize = self.maxTextSize
|
||||||
|
and (Element._Context.baseScale and (self.maxTextSize * scaleY) or self.maxTextSize)
|
||||||
if minSize and self.textSize < minSize then
|
if minSize and self.textSize < minSize then
|
||||||
self.textSize = minSize
|
self.textSize = minSize
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -222,6 +222,123 @@ function LayoutEngine:_batchCalculatePositions(children, startX, startY, spacing
|
|||||||
return positions
|
return positions
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Calculate flex item sizes based on flexGrow, flexShrink, flexBasis
|
||||||
|
--- Implements CSS flexbox sizing algorithm
|
||||||
|
---@param children table Array of child elements in the flex line
|
||||||
|
---@param availableMainSize number Available space in main axis
|
||||||
|
---@param gap number Gap between items
|
||||||
|
---@param isHorizontal boolean Whether main axis is horizontal
|
||||||
|
---@return table mainSizes Array of calculated main sizes for each child
|
||||||
|
function LayoutEngine:_calculateFlexSizes(children, availableMainSize, gap, isHorizontal)
|
||||||
|
local childCount = #children
|
||||||
|
local totalGaps = math.max(0, childCount - 1) * gap
|
||||||
|
local availableForContent = availableMainSize - totalGaps
|
||||||
|
|
||||||
|
-- Step 1: Calculate hypothetical main sizes (flex basis resolution)
|
||||||
|
local hypotheticalSizes = {}
|
||||||
|
local flexBases = {}
|
||||||
|
local totalFlexBasis = 0
|
||||||
|
|
||||||
|
for i, child in ipairs(children) do
|
||||||
|
local flexBasis = child.flexBasis
|
||||||
|
local hypotheticalSize
|
||||||
|
|
||||||
|
-- Resolve flex-basis
|
||||||
|
if flexBasis == "auto" then
|
||||||
|
-- Use element's main size (width for horizontal, height for vertical)
|
||||||
|
if isHorizontal then
|
||||||
|
hypotheticalSize = child:getBorderBoxWidth()
|
||||||
|
else
|
||||||
|
hypotheticalSize = child:getBorderBoxHeight()
|
||||||
|
end
|
||||||
|
elseif type(flexBasis) == "number" then
|
||||||
|
hypotheticalSize = flexBasis
|
||||||
|
elseif type(flexBasis) == "string" and child.units.flexBasis then
|
||||||
|
-- Parse and resolve flex-basis with units
|
||||||
|
local value, unit = child.units.flexBasis.value, child.units.flexBasis.unit
|
||||||
|
hypotheticalSize =
|
||||||
|
self._Units.resolve(value, unit, self._Context.viewportWidth, self._Context.viewportHeight, availableMainSize)
|
||||||
|
else
|
||||||
|
-- Fallback to element's natural size
|
||||||
|
if isHorizontal then
|
||||||
|
hypotheticalSize = child:getBorderBoxWidth()
|
||||||
|
else
|
||||||
|
hypotheticalSize = child:getBorderBoxHeight()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add margins to hypothetical size
|
||||||
|
local childMargin = child.margin
|
||||||
|
if isHorizontal then
|
||||||
|
hypotheticalSize = hypotheticalSize + childMargin.left + childMargin.right
|
||||||
|
else
|
||||||
|
hypotheticalSize = hypotheticalSize + childMargin.top + childMargin.bottom
|
||||||
|
end
|
||||||
|
|
||||||
|
flexBases[i] = hypotheticalSize
|
||||||
|
hypotheticalSizes[i] = hypotheticalSize
|
||||||
|
totalFlexBasis = totalFlexBasis + hypotheticalSize
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Step 2: Determine if we need to grow or shrink
|
||||||
|
local freeSpace = availableForContent - totalFlexBasis
|
||||||
|
|
||||||
|
-- Step 3a: Handle positive free space (GROW)
|
||||||
|
if freeSpace > 0 then
|
||||||
|
local totalFlexGrow = 0
|
||||||
|
for _, child in ipairs(children) do
|
||||||
|
totalFlexGrow = totalFlexGrow + (child.flexGrow or 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
if totalFlexGrow > 0 then
|
||||||
|
-- Distribute free space proportionally to flex-grow values
|
||||||
|
for i, child in ipairs(children) do
|
||||||
|
local flexGrow = child.flexGrow or 0
|
||||||
|
if flexGrow > 0 then
|
||||||
|
local growAmount = (flexGrow / totalFlexGrow) * freeSpace
|
||||||
|
hypotheticalSizes[i] = hypotheticalSizes[i] + growAmount
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Step 3b: Handle negative free space (SHRINK)
|
||||||
|
elseif freeSpace < 0 then
|
||||||
|
local totalFlexShrink = 0
|
||||||
|
local totalScaledShrinkFactor = 0
|
||||||
|
|
||||||
|
for i, child in ipairs(children) do
|
||||||
|
local flexShrink = child.flexShrink or 1
|
||||||
|
totalFlexShrink = totalFlexShrink + flexShrink
|
||||||
|
-- Scaled shrink factor = flex-shrink × flex-basis
|
||||||
|
totalScaledShrinkFactor = totalScaledShrinkFactor + (flexShrink * flexBases[i])
|
||||||
|
end
|
||||||
|
|
||||||
|
if totalScaledShrinkFactor > 0 then
|
||||||
|
-- Distribute shrinkage proportionally to (flex-shrink × flex-basis)
|
||||||
|
for i, child in ipairs(children) do
|
||||||
|
local flexShrink = child.flexShrink or 1
|
||||||
|
if flexShrink > 0 then
|
||||||
|
local scaledShrinkFactor = flexShrink * flexBases[i]
|
||||||
|
local shrinkAmount = (scaledShrinkFactor / totalScaledShrinkFactor) * math.abs(freeSpace)
|
||||||
|
hypotheticalSizes[i] = math.max(0, hypotheticalSizes[i] - shrinkAmount)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Step 4: Return final main sizes (excluding margins)
|
||||||
|
local mainSizes = {}
|
||||||
|
for i, child in ipairs(children) do
|
||||||
|
local childMargin = child.margin
|
||||||
|
if isHorizontal then
|
||||||
|
mainSizes[i] = math.max(0, hypotheticalSizes[i] - childMargin.left - childMargin.right)
|
||||||
|
else
|
||||||
|
mainSizes[i] = math.max(0, hypotheticalSizes[i] - childMargin.top - childMargin.bottom)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return mainSizes
|
||||||
|
end
|
||||||
|
|
||||||
--- Layout children within this element according to positioning mode
|
--- Layout children within this element according to positioning mode
|
||||||
function LayoutEngine:layoutChildren()
|
function LayoutEngine:layoutChildren()
|
||||||
-- Start performance timing first (before any early returns)
|
-- Start performance timing first (before any early returns)
|
||||||
@@ -328,12 +445,60 @@ function LayoutEngine:layoutChildren()
|
|||||||
-- BORDER-BOX MODEL: element.width and element.height are already content dimensions (padding subtracted)
|
-- BORDER-BOX MODEL: element.width and element.height are already content dimensions (padding subtracted)
|
||||||
local availableMainSize = 0
|
local availableMainSize = 0
|
||||||
local availableCrossSize = 0
|
local availableCrossSize = 0
|
||||||
|
|
||||||
|
-- Reserve space for scrollbars if needed (reserve-space mode)
|
||||||
|
local scrollbarReservedWidth = 0
|
||||||
|
local scrollbarReservedHeight = 0
|
||||||
|
if self.element._scrollManager and self.element._scrollManager.scrollbarPlacement == "reserve-space" then
|
||||||
|
scrollbarReservedWidth, scrollbarReservedHeight = self.element._scrollManager:getReservedSpace(self.element)
|
||||||
|
end
|
||||||
|
|
||||||
if self.flexDirection == self._FlexDirection.HORIZONTAL then
|
if self.flexDirection == self._FlexDirection.HORIZONTAL then
|
||||||
availableMainSize = self.element.width
|
availableMainSize = self.element.width - scrollbarReservedWidth
|
||||||
availableCrossSize = self.element.height
|
availableCrossSize = self.element.height - scrollbarReservedHeight
|
||||||
else
|
else
|
||||||
availableMainSize = self.element.height
|
availableMainSize = self.element.height - scrollbarReservedHeight
|
||||||
availableCrossSize = self.element.width
|
availableCrossSize = self.element.width - scrollbarReservedWidth
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Adjust children with percentage-based cross-axis dimensions when scrollbar space is reserved
|
||||||
|
if (scrollbarReservedWidth > 0 or scrollbarReservedHeight > 0) then
|
||||||
|
local isHorizontal = self.flexDirection == self._FlexDirection.HORIZONTAL
|
||||||
|
for _, child in ipairs(flexChildren) do
|
||||||
|
if isHorizontal then
|
||||||
|
-- Horizontal flex: main-axis is width, cross-axis is height
|
||||||
|
-- Adjust main-axis width if percentage-based
|
||||||
|
if child.units and child.units.width and child.units.width.unit == "%" then
|
||||||
|
local newBorderBoxWidth = (child.units.width.value / 100) * availableMainSize
|
||||||
|
local newWidth = math.max(0, newBorderBoxWidth - child.padding.left - child.padding.right)
|
||||||
|
child.width = newWidth
|
||||||
|
child._borderBoxWidth = newBorderBoxWidth
|
||||||
|
end
|
||||||
|
-- Adjust cross-axis height if percentage-based
|
||||||
|
if child.units and child.units.height and child.units.height.unit == "%" then
|
||||||
|
local newBorderBoxHeight = (child.units.height.value / 100) * availableCrossSize
|
||||||
|
local newHeight = math.max(0, newBorderBoxHeight - child.padding.top - child.padding.bottom)
|
||||||
|
child.height = newHeight
|
||||||
|
child._borderBoxHeight = newBorderBoxHeight
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Vertical flex: main-axis is height, cross-axis is width
|
||||||
|
-- Adjust main-axis height if percentage-based
|
||||||
|
if child.units and child.units.height and child.units.height.unit == "%" then
|
||||||
|
local newBorderBoxHeight = (child.units.height.value / 100) * availableMainSize
|
||||||
|
local newHeight = math.max(0, newBorderBoxHeight - child.padding.top - child.padding.bottom)
|
||||||
|
child.height = newHeight
|
||||||
|
child._borderBoxHeight = newBorderBoxHeight
|
||||||
|
end
|
||||||
|
-- Adjust cross-axis width if percentage-based
|
||||||
|
if child.units and child.units.width and child.units.width.unit == "%" then
|
||||||
|
local newBorderBoxWidth = (child.units.width.value / 100) * availableCrossSize
|
||||||
|
local newWidth = math.max(0, newBorderBoxWidth - child.padding.left - child.padding.right)
|
||||||
|
child.width = newWidth
|
||||||
|
child._borderBoxWidth = newBorderBoxWidth
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Handle flex wrap: create lines of children
|
-- Handle flex wrap: create lines of children
|
||||||
@@ -398,13 +563,58 @@ function LayoutEngine:layoutChildren()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Apply flex sizing to each line BEFORE calculating line heights
|
||||||
|
-- Performance optimization: hoist enum comparison outside loop
|
||||||
|
local isHorizontal = self.flexDirection == self._FlexDirection.HORIZONTAL
|
||||||
|
|
||||||
|
for lineIndex, line in ipairs(lines) do
|
||||||
|
-- Check if any child in this line needs flex sizing
|
||||||
|
local needsFlexSizing = false
|
||||||
|
for _, child in ipairs(line) do
|
||||||
|
if (child.flexGrow and child.flexGrow > 0) or (child.flexBasis and child.flexBasis ~= "auto") then
|
||||||
|
needsFlexSizing = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Only apply flex sizing if needed
|
||||||
|
if needsFlexSizing then
|
||||||
|
-- Calculate flex sizes for this line
|
||||||
|
local mainSizes = self:_calculateFlexSizes(line, availableMainSize, self.gap, isHorizontal)
|
||||||
|
|
||||||
|
-- Apply calculated sizes to children
|
||||||
|
for i, child in ipairs(line) do
|
||||||
|
local mainSize = mainSizes[i]
|
||||||
|
|
||||||
|
if isHorizontal then
|
||||||
|
-- Update width for horizontal flex
|
||||||
|
child.width = mainSize
|
||||||
|
child._borderBoxWidth = mainSize
|
||||||
|
-- Invalidate width cache
|
||||||
|
child._borderBoxWidthCache = nil
|
||||||
|
else
|
||||||
|
-- Update height for vertical flex
|
||||||
|
child.height = mainSize
|
||||||
|
child._borderBoxHeight = mainSize
|
||||||
|
-- Invalidate height cache
|
||||||
|
child._borderBoxHeightCache = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Trigger layout for child's children if any
|
||||||
|
if #child.children > 0 then
|
||||||
|
child:layoutChildren()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Calculate line positions and heights (including child padding)
|
-- Calculate line positions and heights (including child padding)
|
||||||
-- Performance optimization: preallocate array if possible
|
-- Performance optimization: preallocate array if possible
|
||||||
local lineHeights = table.create and table.create(#lines) or {}
|
local lineHeights = table.create and table.create(#lines) or {}
|
||||||
local totalLinesHeight = 0
|
local totalLinesHeight = 0
|
||||||
|
|
||||||
-- Performance optimization: hoist enum comparison outside loop
|
-- Performance optimization: hoist enum comparison outside loop (already hoisted above)
|
||||||
local isHorizontal = self.flexDirection == self._FlexDirection.HORIZONTAL
|
-- local isHorizontal = self.flexDirection == self._FlexDirection.HORIZONTAL
|
||||||
|
|
||||||
for lineIndex, line in ipairs(lines) do
|
for lineIndex, line in ipairs(lines) do
|
||||||
local maxCrossSize = 0
|
local maxCrossSize = 0
|
||||||
@@ -435,7 +645,11 @@ function LayoutEngine:layoutChildren()
|
|||||||
|
|
||||||
-- For single line layouts, CENTER, FLEX_END and STRETCH should use full cross size
|
-- For single line layouts, CENTER, FLEX_END and STRETCH should use full cross size
|
||||||
if #lines == 1 then
|
if #lines == 1 then
|
||||||
if self.alignItems == self._AlignItems.STRETCH or self.alignItems == self._AlignItems.CENTER or self.alignItems == self._AlignItems.FLEX_END then
|
if
|
||||||
|
self.alignItems == self._AlignItems.STRETCH
|
||||||
|
or self.alignItems == self._AlignItems.CENTER
|
||||||
|
or self.alignItems == self._AlignItems.FLEX_END
|
||||||
|
then
|
||||||
-- STRETCH, CENTER, and FLEX_END should use full available cross size
|
-- STRETCH, CENTER, and FLEX_END should use full available cross size
|
||||||
lineHeights[1] = availableCrossSize
|
lineHeights[1] = availableCrossSize
|
||||||
totalLinesHeight = availableCrossSize
|
totalLinesHeight = availableCrossSize
|
||||||
@@ -508,22 +722,26 @@ function LayoutEngine:layoutChildren()
|
|||||||
if self.justifyContent == self._JustifyContent.FLEX_START then
|
if self.justifyContent == self._JustifyContent.FLEX_START then
|
||||||
startPos = 0
|
startPos = 0
|
||||||
elseif self.justifyContent == self._JustifyContent.CENTER then
|
elseif self.justifyContent == self._JustifyContent.CENTER then
|
||||||
startPos = freeSpace / 2
|
startPos = math.max(0, freeSpace / 2)
|
||||||
elseif self.justifyContent == self._JustifyContent.FLEX_END then
|
elseif self.justifyContent == self._JustifyContent.FLEX_END then
|
||||||
startPos = freeSpace
|
startPos = math.max(0, freeSpace)
|
||||||
elseif self.justifyContent == self._JustifyContent.SPACE_BETWEEN then
|
elseif self.justifyContent == self._JustifyContent.SPACE_BETWEEN then
|
||||||
startPos = 0
|
startPos = 0
|
||||||
if #line > 1 then
|
if #line > 1 and freeSpace > 0 then
|
||||||
itemSpacing = self.gap + (freeSpace / (#line - 1))
|
itemSpacing = self.gap + (freeSpace / (#line - 1))
|
||||||
end
|
end
|
||||||
elseif self.justifyContent == self._JustifyContent.SPACE_AROUND then
|
elseif self.justifyContent == self._JustifyContent.SPACE_AROUND then
|
||||||
local spaceAroundEach = freeSpace / #line
|
if freeSpace > 0 then
|
||||||
startPos = spaceAroundEach / 2
|
local spaceAroundEach = freeSpace / #line
|
||||||
itemSpacing = self.gap + spaceAroundEach
|
startPos = spaceAroundEach / 2
|
||||||
|
itemSpacing = self.gap + spaceAroundEach
|
||||||
|
end
|
||||||
elseif self.justifyContent == self._JustifyContent.SPACE_EVENLY then
|
elseif self.justifyContent == self._JustifyContent.SPACE_EVENLY then
|
||||||
local spaceBetween = freeSpace / (#line + 1)
|
if freeSpace > 0 then
|
||||||
startPos = spaceBetween
|
local spaceBetween = freeSpace / (#line + 1)
|
||||||
itemSpacing = self.gap + spaceBetween
|
startPos = spaceBetween
|
||||||
|
itemSpacing = self.gap + spaceBetween
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Position children in this line
|
-- Position children in this line
|
||||||
@@ -570,7 +788,11 @@ function LayoutEngine:layoutChildren()
|
|||||||
if effectiveAlign == alignItems_FLEX_START then
|
if effectiveAlign == alignItems_FLEX_START then
|
||||||
child.y = elementY + elementPaddingTop + currentCrossPos + childMarginTop
|
child.y = elementY + elementPaddingTop + currentCrossPos + childMarginTop
|
||||||
elseif effectiveAlign == alignItems_CENTER then
|
elseif effectiveAlign == alignItems_CENTER then
|
||||||
child.y = elementY + elementPaddingTop + currentCrossPos + ((lineHeight - childTotalCrossSize) / 2) + childMarginTop
|
child.y = elementY
|
||||||
|
+ elementPaddingTop
|
||||||
|
+ currentCrossPos
|
||||||
|
+ ((lineHeight - childTotalCrossSize) / 2)
|
||||||
|
+ childMarginTop
|
||||||
elseif effectiveAlign == alignItems_FLEX_END then
|
elseif effectiveAlign == alignItems_FLEX_END then
|
||||||
child.y = elementY + elementPaddingTop + currentCrossPos + lineHeight - childTotalCrossSize + childMarginTop
|
child.y = elementY + elementPaddingTop + currentCrossPos + lineHeight - childTotalCrossSize + childMarginTop
|
||||||
elseif effectiveAlign == alignItems_STRETCH then
|
elseif effectiveAlign == alignItems_STRETCH then
|
||||||
@@ -611,7 +833,11 @@ function LayoutEngine:layoutChildren()
|
|||||||
if effectiveAlign == alignItems_FLEX_START then
|
if effectiveAlign == alignItems_FLEX_START then
|
||||||
child.x = elementX + elementPaddingLeft + currentCrossPos + childMarginLeft
|
child.x = elementX + elementPaddingLeft + currentCrossPos + childMarginLeft
|
||||||
elseif effectiveAlign == alignItems_CENTER then
|
elseif effectiveAlign == alignItems_CENTER then
|
||||||
child.x = elementX + elementPaddingLeft + currentCrossPos + ((lineHeight - childTotalCrossSize) / 2) + childMarginLeft
|
child.x = elementX
|
||||||
|
+ elementPaddingLeft
|
||||||
|
+ currentCrossPos
|
||||||
|
+ ((lineHeight - childTotalCrossSize) / 2)
|
||||||
|
+ childMarginLeft
|
||||||
elseif effectiveAlign == alignItems_FLEX_END then
|
elseif effectiveAlign == alignItems_FLEX_END then
|
||||||
child.x = elementX + elementPaddingLeft + currentCrossPos + lineHeight - childTotalCrossSize + childMarginLeft
|
child.x = elementX + elementPaddingLeft + currentCrossPos + lineHeight - childTotalCrossSize + childMarginLeft
|
||||||
elseif effectiveAlign == alignItems_STRETCH then
|
elseif effectiveAlign == alignItems_STRETCH then
|
||||||
@@ -634,7 +860,11 @@ function LayoutEngine:layoutChildren()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Advance position by child's border-box height plus margins
|
-- Advance position by child's border-box height plus margins
|
||||||
currentMainPos = currentMainPos + child:getBorderBoxHeight() + child.margin.top + child.margin.bottom + itemSpacing
|
currentMainPos = currentMainPos
|
||||||
|
+ child:getBorderBoxHeight()
|
||||||
|
+ child.margin.top
|
||||||
|
+ child.margin.bottom
|
||||||
|
+ itemSpacing
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -921,8 +1151,13 @@ function LayoutEngine:recalculateUnits(newViewportWidth, newViewportHeight)
|
|||||||
-- Store in _borderBoxWidth temporarily, will calculate content width after padding is resolved
|
-- Store in _borderBoxWidth temporarily, will calculate content width after padding is resolved
|
||||||
if self.element.units.width.unit ~= "px" and self.element.units.width.unit ~= "auto" 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
|
local parentWidth = self.element.parent and self.element.parent.width or newViewportWidth
|
||||||
self.element._borderBoxWidth =
|
self.element._borderBoxWidth = Units.resolve(
|
||||||
Units.resolve(self.element.units.width.value, self.element.units.width.unit, newViewportWidth, newViewportHeight, parentWidth)
|
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
|
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)
|
-- Reapply base scaling to pixel widths (border-box)
|
||||||
self.element._borderBoxWidth = self.element.units.width.value * scaleX
|
self.element._borderBoxWidth = self.element.units.width.value * scaleX
|
||||||
@@ -932,8 +1167,13 @@ function LayoutEngine:recalculateUnits(newViewportWidth, newViewportHeight)
|
|||||||
-- Store in _borderBoxHeight temporarily, will calculate content height after padding is resolved
|
-- Store in _borderBoxHeight temporarily, will calculate content height after padding is resolved
|
||||||
if self.element.units.height.unit ~= "px" and self.element.units.height.unit ~= "auto" 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
|
local parentHeight = self.element.parent and self.element.parent.height or newViewportHeight
|
||||||
self.element._borderBoxHeight =
|
self.element._borderBoxHeight = Units.resolve(
|
||||||
Units.resolve(self.element.units.height.value, self.element.units.height.unit, newViewportWidth, newViewportHeight, parentHeight)
|
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
|
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)
|
-- Reapply base scaling to pixel heights (border-box)
|
||||||
self.element._borderBoxHeight = self.element.units.height.value * scaleY
|
self.element._borderBoxHeight = self.element.units.height.value * scaleY
|
||||||
@@ -943,13 +1183,20 @@ function LayoutEngine:recalculateUnits(newViewportWidth, newViewportHeight)
|
|||||||
if self.element.units.x.unit ~= "px" then
|
if self.element.units.x.unit ~= "px" then
|
||||||
local parentWidth = self.element.parent and self.element.parent.width or newViewportWidth
|
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 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)
|
local offsetX = Units.resolve(
|
||||||
|
self.element.units.x.value,
|
||||||
|
self.element.units.x.unit,
|
||||||
|
newViewportWidth,
|
||||||
|
newViewportHeight,
|
||||||
|
parentWidth
|
||||||
|
)
|
||||||
self.element.x = baseX + offsetX
|
self.element.x = baseX + offsetX
|
||||||
else
|
else
|
||||||
-- For pixel units, update position relative to parent's new position (with base scaling)
|
-- For pixel units, update position relative to parent's new position (with base scaling)
|
||||||
if self.element.parent then
|
if self.element.parent then
|
||||||
local baseX = self.element.parent.x
|
local baseX = self.element.parent.x
|
||||||
local scaledOffset = self._Context.baseScale and (self.element.units.x.value * scaleX) or self.element.units.x.value
|
local scaledOffset = self._Context.baseScale and (self.element.units.x.value * scaleX)
|
||||||
|
or self.element.units.x.value
|
||||||
self.element.x = baseX + scaledOffset
|
self.element.x = baseX + scaledOffset
|
||||||
elseif self._Context.baseScale then
|
elseif self._Context.baseScale then
|
||||||
-- Top-level element with pixel position - apply base scaling
|
-- Top-level element with pixel position - apply base scaling
|
||||||
@@ -960,13 +1207,20 @@ function LayoutEngine:recalculateUnits(newViewportWidth, newViewportHeight)
|
|||||||
if self.element.units.y.unit ~= "px" then
|
if self.element.units.y.unit ~= "px" then
|
||||||
local parentHeight = self.element.parent and self.element.parent.height or newViewportHeight
|
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 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)
|
local offsetY = Units.resolve(
|
||||||
|
self.element.units.y.value,
|
||||||
|
self.element.units.y.unit,
|
||||||
|
newViewportWidth,
|
||||||
|
newViewportHeight,
|
||||||
|
parentHeight
|
||||||
|
)
|
||||||
self.element.y = baseY + offsetY
|
self.element.y = baseY + offsetY
|
||||||
else
|
else
|
||||||
-- For pixel units, update position relative to parent's new position (with base scaling)
|
-- For pixel units, update position relative to parent's new position (with base scaling)
|
||||||
if self.element.parent then
|
if self.element.parent then
|
||||||
local baseY = self.element.parent.y
|
local baseY = self.element.parent.y
|
||||||
local scaledOffset = self._Context.baseScale and (self.element.units.y.value * scaleY) or self.element.units.y.value
|
local scaledOffset = self._Context.baseScale and (self.element.units.y.value * scaleY)
|
||||||
|
or self.element.units.y.value
|
||||||
self.element.y = baseY + scaledOffset
|
self.element.y = baseY + scaledOffset
|
||||||
elseif self._Context.baseScale then
|
elseif self._Context.baseScale then
|
||||||
-- Top-level element with pixel position - apply base scaling
|
-- Top-level element with pixel position - apply base scaling
|
||||||
@@ -1002,8 +1256,10 @@ function LayoutEngine:recalculateUnits(newViewportWidth, newViewportHeight)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Apply min/max constraints (with base scaling)
|
-- Apply min/max constraints (with base scaling)
|
||||||
local minSize = self.element.minTextSize and (self._Context.baseScale and (self.element.minTextSize * scaleY) or self.element.minTextSize)
|
local minSize = self.element.minTextSize
|
||||||
local maxSize = self.element.maxTextSize and (self._Context.baseScale and (self.element.maxTextSize * scaleY) or self.element.maxTextSize)
|
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 self.element.textSize < minSize then
|
if minSize and self.element.textSize < minSize then
|
||||||
self.element.textSize = minSize
|
self.element.textSize = minSize
|
||||||
@@ -1033,17 +1289,43 @@ function LayoutEngine:recalculateUnits(newViewportWidth, newViewportHeight)
|
|||||||
|
|
||||||
-- Recalculate gap if using viewport or percentage units
|
-- Recalculate gap if using viewport or percentage units
|
||||||
if self.element.units.gap.unit ~= "px" then
|
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)
|
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)
|
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)
|
self.element.gap = Units.resolve(
|
||||||
|
self.element.units.gap.value,
|
||||||
|
self.element.units.gap.unit,
|
||||||
|
newViewportWidth,
|
||||||
|
newViewportHeight,
|
||||||
|
containerSize
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Recalculate flexBasis if using viewport or percentage units
|
||||||
|
if
|
||||||
|
self.element.units.flexBasis
|
||||||
|
and self.element.units.flexBasis.unit ~= "auto"
|
||||||
|
and self.element.units.flexBasis.unit ~= "px"
|
||||||
|
then
|
||||||
|
local value, unit = self.element.units.flexBasis.value, self.element.units.flexBasis.unit
|
||||||
|
-- flexBasis uses parent dimensions for % (main axis determines which dimension)
|
||||||
|
local parentSize = self.element.parent and self.element.parent.width or newViewportWidth
|
||||||
|
local resolvedBasis = Units.resolve(value, unit, newViewportWidth, newViewportHeight, parentSize)
|
||||||
|
if type(resolvedBasis) == "number" then
|
||||||
|
self.element.flexBasis = resolvedBasis
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Recalculate spacing (padding/margin) if using viewport or percentage units
|
-- Recalculate spacing (padding/margin) if using viewport or percentage units
|
||||||
-- For percentage-based padding:
|
-- For percentage-based padding:
|
||||||
-- - If element has a parent: use parent's border-box dimensions (CSS spec for child elements)
|
-- - 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)
|
-- - If element has no parent: use element's own border-box dimensions (CSS spec for root elements)
|
||||||
local parentBorderBoxWidth = self.element.parent and self.element.parent._borderBoxWidth or self.element._borderBoxWidth or newViewportWidth
|
local parentBorderBoxWidth = self.element.parent and self.element.parent._borderBoxWidth
|
||||||
local parentBorderBoxHeight = self.element.parent and self.element.parent._borderBoxHeight or self.element._borderBoxHeight or newViewportHeight
|
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)
|
-- Handle shorthand properties first (horizontal/vertical)
|
||||||
local resolvedHorizontalPadding = nil
|
local resolvedHorizontalPadding = nil
|
||||||
@@ -1095,8 +1377,13 @@ function LayoutEngine:recalculateUnits(newViewportWidth, newViewportHeight)
|
|||||||
elseif self.element.units.padding[side].unit ~= "px" then
|
elseif self.element.units.padding[side].unit ~= "px" then
|
||||||
-- Recalculate non-pixel units
|
-- Recalculate non-pixel units
|
||||||
local parentSize = (side == "top" or side == "bottom") and parentBorderBoxHeight or parentBorderBoxWidth
|
local parentSize = (side == "top" or side == "bottom") and parentBorderBoxHeight or parentBorderBoxWidth
|
||||||
self.element.padding[side] =
|
self.element.padding[side] = Units.resolve(
|
||||||
Units.resolve(self.element.units.padding[side].value, self.element.units.padding[side].unit, newViewportWidth, newViewportHeight, parentSize)
|
self.element.units.padding[side].value,
|
||||||
|
self.element.units.padding[side].unit,
|
||||||
|
newViewportWidth,
|
||||||
|
newViewportHeight,
|
||||||
|
parentSize
|
||||||
|
)
|
||||||
end
|
end
|
||||||
-- If unit is "px" and not using shorthand, value stays the same
|
-- If unit is "px" and not using shorthand, value stays the same
|
||||||
end
|
end
|
||||||
@@ -1152,8 +1439,13 @@ function LayoutEngine:recalculateUnits(newViewportWidth, newViewportHeight)
|
|||||||
elseif self.element.units.margin[side].unit ~= "px" then
|
elseif self.element.units.margin[side].unit ~= "px" then
|
||||||
-- Recalculate non-pixel units
|
-- Recalculate non-pixel units
|
||||||
local parentSize = (side == "top" or side == "bottom") and parentBorderBoxHeight or parentBorderBoxWidth
|
local parentSize = (side == "top" or side == "bottom") and parentBorderBoxHeight or parentBorderBoxWidth
|
||||||
self.element.margin[side] =
|
self.element.margin[side] = Units.resolve(
|
||||||
Units.resolve(self.element.units.margin[side].value, self.element.units.margin[side].unit, newViewportWidth, newViewportHeight, parentSize)
|
self.element.units.margin[side].value,
|
||||||
|
self.element.units.margin[side].unit,
|
||||||
|
newViewportWidth,
|
||||||
|
newViewportHeight,
|
||||||
|
parentSize
|
||||||
|
)
|
||||||
end
|
end
|
||||||
-- If unit is "px" and not using shorthand, value stays the same
|
-- If unit is "px" and not using shorthand, value stays the same
|
||||||
end
|
end
|
||||||
@@ -1165,7 +1457,8 @@ function LayoutEngine:recalculateUnits(newViewportWidth, newViewportHeight)
|
|||||||
if self.element.units.width.unit ~= "auto" and self.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
|
-- _borderBoxWidth was recalculated for viewport/percentage units
|
||||||
-- Calculate content width by subtracting padding
|
-- Calculate content width by subtracting padding
|
||||||
self.element.width = math.max(0, self.element._borderBoxWidth - self.element.padding.left - self.element.padding.right)
|
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
|
elseif self.element.units.width.unit == "auto" then
|
||||||
-- For auto-sized elements, width is content width (calculated in resize method)
|
-- For auto-sized elements, width is content width (calculated in resize method)
|
||||||
-- Update border-box to include padding
|
-- Update border-box to include padding
|
||||||
@@ -1176,7 +1469,8 @@ function LayoutEngine:recalculateUnits(newViewportWidth, newViewportHeight)
|
|||||||
if self.element.units.height.unit ~= "auto" and self.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
|
-- _borderBoxHeight was recalculated for viewport/percentage units
|
||||||
-- Calculate content height by subtracting padding
|
-- Calculate content height by subtracting padding
|
||||||
self.element.height = math.max(0, self.element._borderBoxHeight - self.element.padding.top - self.element.padding.bottom)
|
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
|
elseif self.element.units.height.unit == "auto" then
|
||||||
-- For auto-sized elements, height is content height (calculated in resize method)
|
-- For auto-sized elements, height is content height (calculated in resize method)
|
||||||
-- Update border-box to include padding
|
-- Update border-box to include padding
|
||||||
|
|||||||
@@ -353,11 +353,11 @@ function Renderer:_drawBorders(x, y, borderBoxWidth, borderBoxHeight)
|
|||||||
|
|
||||||
-- Check if all borders are enabled with same width
|
-- Check if all borders are enabled with same width
|
||||||
local allBorders = self.border.top and self.border.bottom and self.border.left and self.border.right
|
local allBorders = self.border.top and self.border.bottom and self.border.left and self.border.right
|
||||||
local uniformWidth = allBorders and
|
local uniformWidth = allBorders
|
||||||
type(self.border.top) == "number" and
|
and type(self.border.top) == "number"
|
||||||
self.border.top == self.border.right and
|
and self.border.top == self.border.right
|
||||||
self.border.top == self.border.bottom and
|
and self.border.top == self.border.bottom
|
||||||
self.border.top == self.border.left
|
and self.border.top == self.border.left
|
||||||
|
|
||||||
if uniformWidth then
|
if uniformWidth then
|
||||||
-- Draw complete rounded rectangle border with uniform width
|
-- Draw complete rounded rectangle border with uniform width
|
||||||
@@ -938,28 +938,30 @@ function Renderer:drawScrollbars(element, x, y, w, h, dims)
|
|||||||
knobOffsetY = themeOffset.vertical
|
knobOffsetY = themeOffset.vertical
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Extract contentPadding from frame for knob sizing
|
||||||
|
local framePaddingLeft = 0
|
||||||
|
local framePaddingTop = 0
|
||||||
|
local framePaddingRight = 0
|
||||||
|
local framePaddingBottom = 0
|
||||||
|
if frameComponent and frameComponent._ninePatchData and frameComponent._ninePatchData.contentPadding then
|
||||||
|
framePaddingLeft = frameComponent._ninePatchData.contentPadding.left or 0
|
||||||
|
framePaddingTop = frameComponent._ninePatchData.contentPadding.top or 0
|
||||||
|
framePaddingRight = frameComponent._ninePatchData.contentPadding.right or 0
|
||||||
|
framePaddingBottom = frameComponent._ninePatchData.contentPadding.bottom or 0
|
||||||
|
end
|
||||||
|
|
||||||
-- Draw track (frame) if component exists
|
-- Draw track (frame) if component exists
|
||||||
if frameComponent and frameComponent._loadedAtlas and frameComponent.regions then
|
if frameComponent and frameComponent._loadedAtlas and frameComponent.regions then
|
||||||
self._NinePatch.draw(
|
self._NinePatch.draw(frameComponent, frameComponent._loadedAtlas, trackX, trackY, element.scrollbarWidth, dims.vertical.trackHeight)
|
||||||
frameComponent,
|
|
||||||
frameComponent._loadedAtlas,
|
|
||||||
trackX,
|
|
||||||
trackY,
|
|
||||||
element.scrollbarWidth,
|
|
||||||
dims.vertical.trackHeight
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Draw thumb (bar) if component exists
|
-- Draw thumb (bar) if component exists
|
||||||
if barComponent and barComponent._loadedAtlas and barComponent.regions then
|
if barComponent and barComponent._loadedAtlas and barComponent.regions then
|
||||||
self._NinePatch.draw(
|
-- Adjust knob dimensions to account for frame's contentPadding
|
||||||
barComponent,
|
-- Vertical scrollbar: width affected by left+right, height affected by top+bottom
|
||||||
barComponent._loadedAtlas,
|
local knobWidth = element.scrollbarWidth
|
||||||
trackX + knobOffsetX,
|
local knobHeight = dims.vertical.thumbHeight - framePaddingTop / 2
|
||||||
trackY + dims.vertical.thumbY + knobOffsetY,
|
self._NinePatch.draw(barComponent, barComponent._loadedAtlas, trackX + knobOffsetX, trackY + dims.vertical.thumbY + knobOffsetY, knobWidth, knobHeight)
|
||||||
element.scrollbarWidth,
|
|
||||||
dims.vertical.thumbHeight
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
-- Fallback to color-based rendering
|
-- Fallback to color-based rendering
|
||||||
@@ -1013,27 +1015,36 @@ function Renderer:drawScrollbars(element, x, y, w, h, dims)
|
|||||||
knobOffsetY = themeOffset.y
|
knobOffsetY = themeOffset.y
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Extract contentPadding from frame for knob sizing
|
||||||
|
local framePaddingLeft = 0
|
||||||
|
local framePaddingTop = 0
|
||||||
|
local framePaddingRight = 0
|
||||||
|
local framePaddingBottom = 0
|
||||||
|
if frameComponent and frameComponent._ninePatchData and frameComponent._ninePatchData.contentPadding then
|
||||||
|
framePaddingLeft = frameComponent._ninePatchData.contentPadding.left or 0
|
||||||
|
framePaddingTop = frameComponent._ninePatchData.contentPadding.top or 0
|
||||||
|
framePaddingRight = frameComponent._ninePatchData.contentPadding.right or 0
|
||||||
|
framePaddingBottom = frameComponent._ninePatchData.contentPadding.bottom or 0
|
||||||
|
end
|
||||||
|
|
||||||
-- Draw track (frame) if component exists
|
-- Draw track (frame) if component exists
|
||||||
if frameComponent and frameComponent._loadedAtlas and frameComponent.regions then
|
if frameComponent and frameComponent._loadedAtlas and frameComponent.regions then
|
||||||
self._NinePatch.draw(
|
self._NinePatch.draw(frameComponent, frameComponent._loadedAtlas, trackX, trackY, dims.horizontal.trackWidth, element.scrollbarWidth)
|
||||||
frameComponent,
|
|
||||||
frameComponent._loadedAtlas,
|
|
||||||
trackX,
|
|
||||||
trackY,
|
|
||||||
dims.horizontal.trackWidth,
|
|
||||||
element.scrollbarWidth
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Draw thumb (bar) if component exists
|
-- Draw thumb (bar) if component exists
|
||||||
if barComponent and barComponent._loadedAtlas and barComponent.regions then
|
if barComponent and barComponent._loadedAtlas and barComponent.regions then
|
||||||
|
-- Adjust knob dimensions to account for frame's contentPadding
|
||||||
|
-- Horizontal scrollbar: width affected by left+right, height affected by top+bottom
|
||||||
|
local knobWidth = dims.horizontal.thumbWidth - framePaddingLeft / 2
|
||||||
|
local knobHeight = element.scrollbarWidth - framePaddingTop - framePaddingBottom
|
||||||
self._NinePatch.draw(
|
self._NinePatch.draw(
|
||||||
barComponent,
|
barComponent,
|
||||||
barComponent._loadedAtlas,
|
barComponent._loadedAtlas,
|
||||||
trackX + dims.horizontal.thumbX + knobOffsetX,
|
trackX + dims.horizontal.thumbX + knobOffsetX,
|
||||||
trackY + knobOffsetY,
|
trackY + knobOffsetY,
|
||||||
dims.horizontal.thumbWidth,
|
knobWidth,
|
||||||
element.scrollbarWidth
|
knobHeight
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -8,9 +8,12 @@
|
|||||||
---@field scrollbarRadius number -- Border radius for scrollbars
|
---@field scrollbarRadius number -- Border radius for scrollbars
|
||||||
---@field scrollbarPadding number -- Padding around scrollbar
|
---@field scrollbarPadding number -- Padding around scrollbar
|
||||||
---@field scrollSpeed number -- Scroll speed for wheel events (pixels per wheel unit)
|
---@field scrollSpeed number -- Scroll speed for wheel events (pixels per wheel unit)
|
||||||
|
---@field invertScroll boolean -- Invert mouse wheel scroll direction (default: false)
|
||||||
---@field scrollBarStyle string? -- Scrollbar style name from theme (selects from theme.scrollbars)
|
---@field scrollBarStyle string? -- Scrollbar style name from theme (selects from theme.scrollbars)
|
||||||
---@field scrollbarKnobOffset table -- {x: number, y: number, horizontal: number, vertical: number} -- Offset for scrollbar knob/handle position
|
---@field scrollbarKnobOffset table -- {x: number, y: number, horizontal: number, vertical: number} -- Offset for scrollbar knob/handle position
|
||||||
---@field hideScrollbars table -- {vertical: boolean, horizontal: boolean}
|
---@field hideScrollbars table -- {vertical: boolean, horizontal: boolean}
|
||||||
|
---@field scrollbarPlacement string -- "reserve-space"|"overlay" -- Whether scrollbar reserves space or overlays content (default: "reserve-space")
|
||||||
|
---@field scrollbarBalance boolean -- When true, reserve space on both sides of content for visual balance (default: false)
|
||||||
---@field touchScrollEnabled boolean -- Enable touch scrolling
|
---@field touchScrollEnabled boolean -- Enable touch scrolling
|
||||||
---@field momentumScrollEnabled boolean -- Enable momentum scrolling
|
---@field momentumScrollEnabled boolean -- Enable momentum scrolling
|
||||||
---@field bounceEnabled boolean -- Enable bounce effects at boundaries
|
---@field bounceEnabled boolean -- Enable bounce effects at boundaries
|
||||||
@@ -83,6 +86,7 @@ function ScrollManager.new(config, deps)
|
|||||||
self.scrollbarRadius = config.scrollbarRadius or 6
|
self.scrollbarRadius = config.scrollbarRadius or 6
|
||||||
self.scrollbarPadding = config.scrollbarPadding or 2
|
self.scrollbarPadding = config.scrollbarPadding or 2
|
||||||
self.scrollSpeed = config.scrollSpeed or 20
|
self.scrollSpeed = config.scrollSpeed or 20
|
||||||
|
self.invertScroll = config.invertScroll or false
|
||||||
self.scrollBarStyle = config.scrollBarStyle -- Theme scrollbar style name (nil = use default)
|
self.scrollBarStyle = config.scrollBarStyle -- Theme scrollbar style name (nil = use default)
|
||||||
|
|
||||||
-- scrollbarKnobOffset can be number or table {x, y} or {horizontal, vertical}
|
-- scrollbarKnobOffset can be number or table {x, y} or {horizontal, vertical}
|
||||||
@@ -96,6 +100,12 @@ function ScrollManager.new(config, deps)
|
|||||||
-- hideScrollbars can be boolean or table {vertical: boolean, horizontal: boolean}
|
-- hideScrollbars can be boolean or table {vertical: boolean, horizontal: boolean}
|
||||||
self.hideScrollbars = self._utils.normalizeBooleanTable(config.hideScrollbars, false)
|
self.hideScrollbars = self._utils.normalizeBooleanTable(config.hideScrollbars, false)
|
||||||
|
|
||||||
|
-- Scrollbar placement: "reserve-space" (default) or "overlay"
|
||||||
|
self.scrollbarPlacement = config.scrollbarPlacement or "reserve-space"
|
||||||
|
|
||||||
|
-- Scrollbar balance: when true, reserve space on both sides for visual balance
|
||||||
|
self.scrollbarBalance = config.scrollbarBalance or false
|
||||||
|
|
||||||
-- Touch scrolling configuration
|
-- Touch scrolling configuration
|
||||||
self.touchScrollEnabled = config.touchScrollEnabled ~= false -- Default true
|
self.touchScrollEnabled = config.touchScrollEnabled ~= false -- Default true
|
||||||
self.momentumScrollEnabled = config.momentumScrollEnabled ~= false -- Default true
|
self.momentumScrollEnabled = config.momentumScrollEnabled ~= false -- Default true
|
||||||
@@ -144,6 +154,36 @@ function ScrollManager.new(config, deps)
|
|||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Get the space reserved for scrollbars (width and height reduction)
|
||||||
|
--- This is called BEFORE layout to reduce available space for children
|
||||||
|
---@param element Element The parent Element instance
|
||||||
|
---@return number reservedWidth, number reservedHeight
|
||||||
|
function ScrollManager:getReservedSpace(element)
|
||||||
|
if self.scrollbarPlacement ~= "reserve-space" then
|
||||||
|
return 0, 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local overflowX = self.overflowX or self.overflow
|
||||||
|
local overflowY = self.overflowY or self.overflow
|
||||||
|
|
||||||
|
local reservedWidth = 0
|
||||||
|
local reservedHeight = 0
|
||||||
|
|
||||||
|
-- Reserve space for vertical scrollbar if overflow mode requires it
|
||||||
|
if (overflowY == "scroll" or overflowY == "auto") and not self.hideScrollbars.vertical then
|
||||||
|
local scrollbarSpace = self.scrollbarWidth + (self.scrollbarPadding * 2)
|
||||||
|
reservedWidth = self.scrollbarBalance and (scrollbarSpace * 2) or scrollbarSpace
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Reserve space for horizontal scrollbar if overflow mode requires it
|
||||||
|
if (overflowX == "scroll" or overflowX == "auto") and not self.hideScrollbars.horizontal then
|
||||||
|
local scrollbarSpace = self.scrollbarWidth + (self.scrollbarPadding * 2)
|
||||||
|
reservedHeight = self.scrollbarBalance and (scrollbarSpace * 2) or scrollbarSpace
|
||||||
|
end
|
||||||
|
|
||||||
|
return reservedWidth, reservedHeight
|
||||||
|
end
|
||||||
|
|
||||||
--- Detect if content overflows container bounds
|
--- Detect if content overflows container bounds
|
||||||
---@param element Element The parent Element instance
|
---@param element Element The parent Element instance
|
||||||
function ScrollManager:detectOverflow(element)
|
function ScrollManager:detectOverflow(element)
|
||||||
@@ -197,6 +237,14 @@ function ScrollManager:detectOverflow(element)
|
|||||||
local containerWidth = element.width - element.padding.left - element.padding.right
|
local containerWidth = element.width - element.padding.left - element.padding.right
|
||||||
local containerHeight = element.height - element.padding.top - element.padding.bottom
|
local containerHeight = element.height - element.padding.top - element.padding.bottom
|
||||||
|
|
||||||
|
-- If scrollbarPlacement is "reserve-space", we need to subtract the reserved space
|
||||||
|
-- because the layout already accounted for it, but element.width/height are still full size
|
||||||
|
if self.scrollbarPlacement == "reserve-space" then
|
||||||
|
local reservedWidth, reservedHeight = self:getReservedSpace()
|
||||||
|
containerWidth = containerWidth - reservedWidth
|
||||||
|
containerHeight = containerHeight - reservedHeight
|
||||||
|
end
|
||||||
|
|
||||||
self._overflowX = self._contentWidth > containerWidth
|
self._overflowX = self._contentWidth > containerWidth
|
||||||
self._overflowY = self._contentHeight > containerHeight
|
self._overflowY = self._contentHeight > containerHeight
|
||||||
|
|
||||||
@@ -582,6 +630,9 @@ function ScrollManager:handleWheel(x, y)
|
|||||||
-- Vertical scrolling
|
-- Vertical scrolling
|
||||||
if y ~= 0 and hasVerticalOverflow then
|
if y ~= 0 and hasVerticalOverflow then
|
||||||
local delta = -y * self.scrollSpeed -- Negative because wheel up = scroll up
|
local delta = -y * self.scrollSpeed -- Negative because wheel up = scroll up
|
||||||
|
if self.invertScroll then
|
||||||
|
delta = -delta -- Invert scroll direction if enabled
|
||||||
|
end
|
||||||
if self.smoothScrollEnabled then
|
if self.smoothScrollEnabled then
|
||||||
-- Set target for smooth scrolling instead of instant jump
|
-- Set target for smooth scrolling instead of instant jump
|
||||||
self._targetScrollY = self._utils.clamp((self._targetScrollY or self._scrollY) + delta, 0, self._maxScrollY)
|
self._targetScrollY = self._utils.clamp((self._targetScrollY or self._scrollY) + delta, 0, self._maxScrollY)
|
||||||
@@ -596,6 +647,9 @@ function ScrollManager:handleWheel(x, y)
|
|||||||
-- Horizontal scrolling
|
-- Horizontal scrolling
|
||||||
if x ~= 0 and hasHorizontalOverflow then
|
if x ~= 0 and hasHorizontalOverflow then
|
||||||
local delta = -x * self.scrollSpeed
|
local delta = -x * self.scrollSpeed
|
||||||
|
if self.invertScroll then
|
||||||
|
delta = -delta -- Invert scroll direction if enabled
|
||||||
|
end
|
||||||
if self.smoothScrollEnabled then
|
if self.smoothScrollEnabled then
|
||||||
-- Set target for smooth scrolling instead of instant jump
|
-- Set target for smooth scrolling instead of instant jump
|
||||||
self._targetScrollX = self._utils.clamp((self._targetScrollX or self._scrollX) + delta, 0, self._maxScrollX)
|
self._targetScrollX = self._utils.clamp((self._targetScrollX or self._scrollX) + delta, 0, self._maxScrollX)
|
||||||
@@ -666,6 +720,8 @@ function ScrollManager:getState()
|
|||||||
_scrollbarHoveredHorizontal = self._scrollbarHoveredHorizontal or false,
|
_scrollbarHoveredHorizontal = self._scrollbarHoveredHorizontal or false,
|
||||||
scrollBarStyle = self.scrollBarStyle,
|
scrollBarStyle = self.scrollBarStyle,
|
||||||
scrollbarKnobOffset = self.scrollbarKnobOffset,
|
scrollbarKnobOffset = self.scrollbarKnobOffset,
|
||||||
|
scrollbarPlacement = self.scrollbarPlacement,
|
||||||
|
scrollbarBalance = self.scrollbarBalance,
|
||||||
_overflowX = self._overflowX,
|
_overflowX = self._overflowX,
|
||||||
_overflowY = self._overflowY,
|
_overflowY = self._overflowY,
|
||||||
_contentWidth = self._contentWidth,
|
_contentWidth = self._contentWidth,
|
||||||
@@ -744,6 +800,14 @@ function ScrollManager:setState(state)
|
|||||||
self.scrollbarKnobOffset = self._utils.normalizeOffsetTable(state.scrollbarKnobOffset, 0)
|
self.scrollbarKnobOffset = self._utils.normalizeOffsetTable(state.scrollbarKnobOffset, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if state.scrollbarPlacement ~= nil then
|
||||||
|
self.scrollbarPlacement = state.scrollbarPlacement
|
||||||
|
end
|
||||||
|
|
||||||
|
if state.scrollbarBalance ~= nil then
|
||||||
|
self.scrollbarBalance = state.scrollbarBalance
|
||||||
|
end
|
||||||
|
|
||||||
if state._overflowX ~= nil then
|
if state._overflowX ~= nil then
|
||||||
self._overflowX = state._overflowX
|
self._overflowX = state._overflowX
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -265,4 +265,73 @@ function Units.isValid(unitStr)
|
|||||||
return validUnits[unit] == true
|
return validUnits[unit] == true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Parse CSS flex shorthand into flexGrow, flexShrink, flexBasis
|
||||||
|
--- Supports: number, "auto", "none", "grow shrink basis"
|
||||||
|
---@param flexValue number|string The flex shorthand value
|
||||||
|
---@return number flexGrow
|
||||||
|
---@return number flexShrink
|
||||||
|
---@return string|number flexBasis
|
||||||
|
function Units.parseFlexShorthand(flexValue)
|
||||||
|
-- Single number: flex-grow
|
||||||
|
if type(flexValue) == "number" then
|
||||||
|
return flexValue, 1, 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- String values
|
||||||
|
if type(flexValue) == "string" then
|
||||||
|
-- "auto" = 1 1 auto
|
||||||
|
if flexValue == "auto" then
|
||||||
|
return 1, 1, "auto"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- "none" = 0 0 auto
|
||||||
|
if flexValue == "none" then
|
||||||
|
return 0, 0, "auto"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Parse "grow shrink basis" format
|
||||||
|
local parts = {}
|
||||||
|
for part in flexValue:gmatch("%S+") do
|
||||||
|
table.insert(parts, part)
|
||||||
|
end
|
||||||
|
|
||||||
|
local grow = 0
|
||||||
|
local shrink = 1
|
||||||
|
local basis = "auto"
|
||||||
|
|
||||||
|
if #parts == 1 then
|
||||||
|
-- Single value: could be grow (number) or basis (with unit)
|
||||||
|
local num = tonumber(parts[1])
|
||||||
|
if num then
|
||||||
|
grow = num
|
||||||
|
basis = 0
|
||||||
|
else
|
||||||
|
basis = parts[1]
|
||||||
|
end
|
||||||
|
elseif #parts == 2 then
|
||||||
|
-- Two values: grow shrink (both numbers) or grow basis
|
||||||
|
local num1 = tonumber(parts[1])
|
||||||
|
local num2 = tonumber(parts[2])
|
||||||
|
if num1 and num2 then
|
||||||
|
grow = num1
|
||||||
|
shrink = num2
|
||||||
|
basis = 0
|
||||||
|
elseif num1 then
|
||||||
|
grow = num1
|
||||||
|
basis = parts[2]
|
||||||
|
end
|
||||||
|
elseif #parts >= 3 then
|
||||||
|
-- Three values: grow shrink basis
|
||||||
|
grow = tonumber(parts[1]) or 0
|
||||||
|
shrink = tonumber(parts[2]) or 1
|
||||||
|
basis = parts[3]
|
||||||
|
end
|
||||||
|
|
||||||
|
return grow, shrink, basis
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Default fallback
|
||||||
|
return 0, 1, "auto"
|
||||||
|
end
|
||||||
|
|
||||||
return Units
|
return Units
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ local AnimationProps = {}
|
|||||||
---@field alignItems AlignItems? -- Alignment of items along cross axis (default: STRETCH)
|
---@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 alignContent AlignContent? -- Alignment of lines in multi-line flex containers (default: STRETCH)
|
||||||
---@field flexWrap FlexWrap? -- Whether children wrap to multiple lines: "nowrap"|"wrap"|"wrap-reverse" (default: NOWRAP)
|
---@field flexWrap FlexWrap? -- Whether children wrap to multiple lines: "nowrap"|"wrap"|"wrap-reverse" (default: NOWRAP)
|
||||||
|
---@field flex number|string? -- Shorthand for flexGrow, flexShrink, flexBasis: number (flex-grow only), string ("1 0 auto"), or nil (default: nil)
|
||||||
|
---@field flexGrow number? -- How much the element should grow relative to siblings (default: 0)
|
||||||
|
---@field flexShrink number? -- How much the element should shrink relative to siblings (default: 1)
|
||||||
|
---@field flexBasis number|string|CalcObject? -- Initial size before growing/shrinking: number (px), string ("50%", "10vw", "auto"), or CalcObject (default: "auto")
|
||||||
---@field justifySelf JustifySelf? -- Alignment of the item itself along main axis (default: AUTO)
|
---@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 alignSelf AlignSelf? -- Alignment of the item itself along cross axis (default: AUTO)
|
||||||
---@field onEvent fun(element:Element, event:InputEvent)? -- Callback function for interaction events
|
---@field onEvent fun(element:Element, event:InputEvent)? -- Callback function for interaction events
|
||||||
@@ -122,9 +126,12 @@ local AnimationProps = {}
|
|||||||
---@field scrollbarRadius number? -- Corner radius for scrollbar (default: 6)
|
---@field scrollbarRadius number? -- Corner radius for scrollbar (default: 6)
|
||||||
---@field scrollbarPadding number? -- Padding between scrollbar and edge (default: 2)
|
---@field scrollbarPadding number? -- Padding between scrollbar and edge (default: 2)
|
||||||
---@field scrollSpeed number? -- Pixels per wheel notch (default: 20)
|
---@field scrollSpeed number? -- Pixels per wheel notch (default: 20)
|
||||||
|
---@field invertScroll boolean? -- Invert mouse wheel scroll direction (default: false)
|
||||||
---@field smoothScrollEnabled boolean? -- Enable smooth scrolling animation for wheel events (default: false)
|
---@field smoothScrollEnabled boolean? -- Enable smooth scrolling animation for wheel events (default: false)
|
||||||
---@field scrollBarStyle string? -- Scrollbar style name from theme (selects from theme.scrollbars, default: uses first scrollbar or fallback rendering)
|
---@field scrollBarStyle string? -- Scrollbar style name from theme (selects from theme.scrollbars, default: uses first scrollbar or fallback rendering)
|
||||||
---@field scrollbarKnobOffset number|{x:number, y:number}|{horizontal:number, vertical:number}? -- Offset for scrollbar knob/handle position in pixels (number for both axes, or table for per-axis control, default: 0, adds to theme offset)
|
---@field scrollbarKnobOffset number|{x:number, y:number}|{horizontal:number, vertical:number}? -- Offset for scrollbar knob/handle position in pixels (number for both axes, or table for per-axis control, default: 0, adds to theme offset)
|
||||||
|
---@field scrollbarPlacement "reserve-space"|"overlay"? -- Scrollbar rendering mode: "reserve-space" (reduces content area, default) or "overlay" (renders over content)
|
||||||
|
---@field scrollbarBalance boolean? -- When true, reserve scrollbar space on both sides of content for visual balance (default: false)
|
||||||
---@field hideScrollbars boolean|{vertical:boolean, horizontal:boolean}? -- Hide scrollbars (boolean for both, or table for individual control, default: false)
|
---@field hideScrollbars boolean|{vertical:boolean, horizontal:boolean}? -- Hide scrollbars (boolean for both, or table for individual control, default: false)
|
||||||
---@field imagePath string? -- Path to image file (auto-loads via ImageCache)
|
---@field imagePath string? -- Path to image file (auto-loads via ImageCache)
|
||||||
---@field image love.Image? -- Image object to display
|
---@field image love.Image? -- Image object to display
|
||||||
|
|||||||
1007
testing/__tests__/flex_grow_shrink_test.lua
Normal file
1007
testing/__tests__/flex_grow_shrink_test.lua
Normal file
File diff suppressed because it is too large
Load Diff
264
testing/__tests__/scrollbar_placement_test.lua
Normal file
264
testing/__tests__/scrollbar_placement_test.lua
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
package.path = package.path .. ";./?.lua;./modules/?.lua"
|
||||||
|
local originalSearchers = package.searchers or package.loaders
|
||||||
|
table.insert(originalSearchers, 2, function(modname)
|
||||||
|
if modname:match("^FlexLove%.modules%.") then
|
||||||
|
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
|
||||||
|
return function()
|
||||||
|
return require("modules." .. moduleName)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
require("testing.loveStub")
|
||||||
|
local luaunit = require("testing.luaunit")
|
||||||
|
local FlexLove = require("FlexLove")
|
||||||
|
|
||||||
|
FlexLove.init()
|
||||||
|
|
||||||
|
TestScrollbarPlacement = {}
|
||||||
|
|
||||||
|
function TestScrollbarPlacement:setUp()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestScrollbarPlacement:test_reserve_space_with_percentage_height_children()
|
||||||
|
-- Test case from user: horizontal scroll container with 100% height children
|
||||||
|
-- Should NOT cause vertical overflow
|
||||||
|
local container = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
overflow = "scroll", -- Always shows scrollbars
|
||||||
|
scrollbarPlacement = "reserve-space",
|
||||||
|
})
|
||||||
|
|
||||||
|
local child1 = FlexLove.new({
|
||||||
|
width = 100,
|
||||||
|
height = "100%",
|
||||||
|
parent = container,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Trigger layout
|
||||||
|
container:layoutChildren()
|
||||||
|
container._scrollManager:detectOverflow(container)
|
||||||
|
|
||||||
|
local overflowX, overflowY = container._scrollManager:hasOverflow()
|
||||||
|
|
||||||
|
-- Child height should be reduced to account for horizontal scrollbar
|
||||||
|
-- Default scrollbar is 12px + 2px padding on each side = 16px
|
||||||
|
-- So child height should be 200 - 16 = 184px
|
||||||
|
luaunit.assertEquals(child1.height, 184)
|
||||||
|
|
||||||
|
-- Should have horizontal overflow (scroll mode), but NOT vertical
|
||||||
|
luaunit.assertFalse(overflowY, "Should not have vertical overflow with 100% height child")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestScrollbarPlacement:test_reserve_space_with_percentage_width_children()
|
||||||
|
-- Vertical scroll container with 100% width children
|
||||||
|
local container = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "column",
|
||||||
|
overflow = "scroll", -- Always shows scrollbars
|
||||||
|
scrollbarPlacement = "reserve-space",
|
||||||
|
})
|
||||||
|
|
||||||
|
local child1 = FlexLove.new({
|
||||||
|
width = "100%",
|
||||||
|
height = 100,
|
||||||
|
parent = container,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Trigger layout
|
||||||
|
container:layoutChildren()
|
||||||
|
container._scrollManager:detectOverflow(container)
|
||||||
|
|
||||||
|
local overflowX, overflowY = container._scrollManager:hasOverflow()
|
||||||
|
|
||||||
|
-- Child width should be reduced to account for vertical scrollbar
|
||||||
|
-- Default scrollbar is 12px + 2px padding on each side = 16px
|
||||||
|
-- So child width should be 200 - 16 = 184px
|
||||||
|
luaunit.assertEquals(child1.width, 184)
|
||||||
|
|
||||||
|
-- Should have vertical overflow (scroll mode), but NOT horizontal
|
||||||
|
luaunit.assertFalse(overflowX, "Should not have horizontal overflow with 100% width child")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestScrollbarPlacement:test_overlay_mode_no_size_adjustment()
|
||||||
|
-- Overlay mode should NOT adjust child sizes
|
||||||
|
local container = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
overflow = "scroll",
|
||||||
|
scrollbarPlacement = "overlay",
|
||||||
|
})
|
||||||
|
|
||||||
|
local child1 = FlexLove.new({
|
||||||
|
width = 100,
|
||||||
|
height = "100%",
|
||||||
|
parent = container,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Trigger layout
|
||||||
|
container:layoutChildren()
|
||||||
|
|
||||||
|
-- Child height should be full 200px (no reduction for scrollbar)
|
||||||
|
luaunit.assertEquals(child1.height, 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestScrollbarPlacement:test_auto_overflow_reserves_space_only_when_needed()
|
||||||
|
-- With overflow="auto" and reserve-space mode, space is reserved preemptively
|
||||||
|
-- But scrollbars should only be visible when content actually overflows
|
||||||
|
local container = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "column",
|
||||||
|
overflow = "auto",
|
||||||
|
scrollbarPlacement = "reserve-space",
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Child that doesn't cause overflow
|
||||||
|
local child1 = FlexLove.new({
|
||||||
|
width = "100%",
|
||||||
|
height = 100,
|
||||||
|
parent = container,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Trigger layout and overflow detection
|
||||||
|
container:layoutChildren()
|
||||||
|
container._scrollManager:detectOverflow(container)
|
||||||
|
|
||||||
|
local overflowX, overflowY = container._scrollManager:hasOverflow()
|
||||||
|
|
||||||
|
-- No overflow detected since content (100px) < available height (184px after scrollbar reservation)
|
||||||
|
luaunit.assertFalse(overflowY, "Should not have overflow")
|
||||||
|
-- Space is reserved preemptively with overflow="auto" to avoid layout shifts
|
||||||
|
luaunit.assertEquals(child1.width, 184, "Space should be reserved preemptively with auto mode")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestScrollbarPlacement:test_vertical_overflow_detected_with_reserved_space()
|
||||||
|
-- Test that overflow is properly detected when using reserve-space
|
||||||
|
local container = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "column",
|
||||||
|
overflow = "auto",
|
||||||
|
scrollbarPlacement = "reserve-space",
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Child that WILL cause overflow
|
||||||
|
local child1 = FlexLove.new({
|
||||||
|
width = "100%",
|
||||||
|
height = 300,
|
||||||
|
parent = container,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Trigger layout and overflow detection
|
||||||
|
container:layoutChildren()
|
||||||
|
container._scrollManager:detectOverflow(container)
|
||||||
|
|
||||||
|
local overflowX, overflowY = container._scrollManager:hasOverflow()
|
||||||
|
|
||||||
|
-- Should detect vertical overflow
|
||||||
|
luaunit.assertTrue(overflowY, "Should detect vertical overflow")
|
||||||
|
|
||||||
|
-- Child width should be reduced for vertical scrollbar
|
||||||
|
luaunit.assertEquals(child1.width, 184, "Child width should be reduced for scrollbar")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestScrollbarPlacement:test_scrollbar_balance_vertical()
|
||||||
|
-- Test scrollbarBalance with vertical scrollbar
|
||||||
|
local container = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "column",
|
||||||
|
overflow = "scroll",
|
||||||
|
scrollbarPlacement = "reserve-space",
|
||||||
|
scrollbarBalance = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
local child1 = FlexLove.new({
|
||||||
|
width = "100%",
|
||||||
|
height = 100,
|
||||||
|
parent = container
|
||||||
|
})
|
||||||
|
|
||||||
|
container:layoutChildren()
|
||||||
|
|
||||||
|
local reservedW, reservedH = container._scrollManager:getReservedSpace(container)
|
||||||
|
|
||||||
|
-- Should reserve double the space (16 * 2 = 32)
|
||||||
|
luaunit.assertEquals(reservedW, 32, "Should reserve doubled width for balance")
|
||||||
|
|
||||||
|
-- Child width should account for balanced space
|
||||||
|
luaunit.assertEquals(child1.width, 168, "Child width should be 200 - 32")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestScrollbarPlacement:test_scrollbar_balance_horizontal()
|
||||||
|
-- Test scrollbarBalance with horizontal scrollbar
|
||||||
|
local container = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
overflow = "scroll",
|
||||||
|
scrollbarPlacement = "reserve-space",
|
||||||
|
scrollbarBalance = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
local child1 = FlexLove.new({
|
||||||
|
width = 100,
|
||||||
|
height = "100%",
|
||||||
|
parent = container
|
||||||
|
})
|
||||||
|
|
||||||
|
container:layoutChildren()
|
||||||
|
|
||||||
|
local reservedW, reservedH = container._scrollManager:getReservedSpace(container)
|
||||||
|
|
||||||
|
-- Should reserve double the space (16 * 2 = 32)
|
||||||
|
luaunit.assertEquals(reservedH, 32, "Should reserve doubled height for balance")
|
||||||
|
|
||||||
|
-- Child height should account for balanced space
|
||||||
|
luaunit.assertEquals(child1.height, 168, "Child height should be 200 - 32")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestScrollbarPlacement:test_scrollbar_balance_both()
|
||||||
|
-- Test scrollbarBalance with both scrollbars
|
||||||
|
local container = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
positioning = "flex",
|
||||||
|
overflow = "scroll",
|
||||||
|
scrollbarPlacement = "reserve-space",
|
||||||
|
scrollbarBalance = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
local child1 = FlexLove.new({
|
||||||
|
width = "100%",
|
||||||
|
height = "100%",
|
||||||
|
parent = container
|
||||||
|
})
|
||||||
|
|
||||||
|
container:layoutChildren()
|
||||||
|
|
||||||
|
local reservedW, reservedH = container._scrollManager:getReservedSpace(container)
|
||||||
|
|
||||||
|
-- Both should reserve double the space
|
||||||
|
luaunit.assertEquals(reservedW, 32, "Should reserve doubled width for balance")
|
||||||
|
luaunit.assertEquals(reservedH, 32, "Should reserve doubled height for balance")
|
||||||
|
|
||||||
|
-- Child should be sized to balanced available space
|
||||||
|
luaunit.assertEquals(child1.width, 168, "Child width should be 200 - 32")
|
||||||
|
luaunit.assertEquals(child1.height, 168, "Child height should be 200 - 32")
|
||||||
|
end
|
||||||
|
|
||||||
|
if not _G.RUNNING_ALL_TESTS then
|
||||||
|
os.exit(luaunit.LuaUnit.run())
|
||||||
|
end
|
||||||
@@ -44,6 +44,7 @@ local testFiles = {
|
|||||||
"testing/__tests__/element_test.lua",
|
"testing/__tests__/element_test.lua",
|
||||||
"testing/__tests__/element_mode_override_test.lua",
|
"testing/__tests__/element_mode_override_test.lua",
|
||||||
"testing/__tests__/event_handler_test.lua",
|
"testing/__tests__/event_handler_test.lua",
|
||||||
|
"testing/__tests__/flex_grow_shrink_test.lua",
|
||||||
"testing/__tests__/flexlove_test.lua",
|
"testing/__tests__/flexlove_test.lua",
|
||||||
"testing/__tests__/grid_test.lua",
|
"testing/__tests__/grid_test.lua",
|
||||||
"testing/__tests__/image_cache_test.lua",
|
"testing/__tests__/image_cache_test.lua",
|
||||||
@@ -62,6 +63,7 @@ local testFiles = {
|
|||||||
"testing/__tests__/retained_prop_stability_test.lua",
|
"testing/__tests__/retained_prop_stability_test.lua",
|
||||||
"testing/__tests__/roundedrect_test.lua",
|
"testing/__tests__/roundedrect_test.lua",
|
||||||
"testing/__tests__/scroll_manager_test.lua",
|
"testing/__tests__/scroll_manager_test.lua",
|
||||||
|
"testing/__tests__/scrollbar_placement_test.lua",
|
||||||
"testing/__tests__/text_editor_test.lua",
|
"testing/__tests__/text_editor_test.lua",
|
||||||
"testing/__tests__/theme_test.lua",
|
"testing/__tests__/theme_test.lua",
|
||||||
"testing/__tests__/touch_events_test.lua",
|
"testing/__tests__/touch_events_test.lua",
|
||||||
|
|||||||
Reference in New Issue
Block a user