immediate mode scroll regression fixed

This commit is contained in:
Michael Freno
2025-11-13 22:33:53 -05:00
parent 7ae09ec690
commit 93af33825d
21 changed files with 192 additions and 3681 deletions

View File

@@ -85,7 +85,11 @@ local function isPointInElement(element, x, y)
local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
local bh = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom)
-- Walk up parent chain to check clipping and apply scroll offsets
-- Calculate scroll offset from parent chain
local scrollOffsetX = 0
local scrollOffsetY = 0
-- Walk up parent chain to check clipping and accumulate scroll offsets
local current = element.parent
while current do
local overflowX = current.overflowX or current.overflow
@@ -101,12 +105,20 @@ local function isPointInElement(element, x, y)
if x < parentX or x > parentX + parentW or y < parentY or y > parentY + parentH then
return false -- Point is clipped by parent
end
-- Accumulate scroll offset
scrollOffsetX = scrollOffsetX + (current._scrollX or 0)
scrollOffsetY = scrollOffsetY + (current._scrollY or 0)
end
current = current.parent
end
return x >= bx and x <= bx + bw and y >= by and y <= by + bh
-- Adjust mouse position by scroll offset for hit testing
local adjustedX = x + scrollOffsetX
local adjustedY = y + scrollOffsetY
return adjustedX >= bx and adjustedX <= bx + bw and adjustedY >= by and adjustedY <= by + bh
end
--- Get the topmost element at a screen position

View File

@@ -540,6 +540,7 @@ function Element.new(props)
Grid = Grid,
Units = Units,
Context = Context,
ErrorHandler = ErrorHandler,
})
self._layoutEngine:initialize(self)
@@ -633,7 +634,10 @@ function Element.new(props)
-- Pixel units
self.textSize = value
else
ErrorHandler.error("Element", "Unknown textSize unit: " .. unit)
ErrorHandler.error(
"Element",
string.format("Unknown textSize unit '%s'. Valid units: px, %%, vw, vh, ew, eh. Or use presets: xs, sm, md, lg, xl, xxl, 2xl, 3xl, 4xl", unit)
)
end
else
-- Validate pixel textSize value
@@ -835,13 +839,18 @@ function Element.new(props)
-- Re-resolve ew/eh textSize units now that width/height are set
if props.textSize and type(props.textSize) == "string" then
local value, unit = Units.parse(props.textSize)
if unit == "ew" then
-- Element width relative (now that width is set)
self.textSize = (value / 100) * self.width
elseif unit == "eh" then
-- Element height relative (now that height is set)
self.textSize = (value / 100) * self.height
-- Check if it's a preset first (presets don't need re-resolution)
local presetValue, presetUnit = resolveTextSizePreset(props.textSize)
if not presetValue then
-- Not a preset, parse and check for ew/eh units
local value, unit = Units.parse(props.textSize)
if unit == "ew" then
-- Element width relative (now that width is set)
self.textSize = (value / 100) * self.width
elseif unit == "eh" then
-- Element height relative (now that height is set)
self.textSize = (value / 100) * self.height
end
end
end
@@ -1026,7 +1035,7 @@ function Element.new(props)
-- Absolute positioning is relative to parent's content area (padding box)
local baseX = self.parent.x + self.parent.padding.left
local baseY = self.parent.y + self.parent.padding.top
-- Handle x position with units
if props.x then
if type(props.x) == "string" then
@@ -1253,16 +1262,36 @@ function Element.new(props)
-- Update the LayoutEngine with actual layout properties
-- (it was initialized early with defaults for auto-sizing calculations)
self._layoutEngine.positioning = self.positioning
if self.flexDirection then self._layoutEngine.flexDirection = self.flexDirection end
if self.flexWrap then self._layoutEngine.flexWrap = self.flexWrap end
if self.justifyContent then self._layoutEngine.justifyContent = self.justifyContent end
if self.alignItems then self._layoutEngine.alignItems = self.alignItems end
if self.alignContent then self._layoutEngine.alignContent = self.alignContent end
if self.gap then self._layoutEngine.gap = self.gap end
if self.gridRows then self._layoutEngine.gridRows = self.gridRows end
if self.gridColumns then self._layoutEngine.gridColumns = self.gridColumns end
if self.columnGap then self._layoutEngine.columnGap = self.columnGap end
if self.rowGap then self._layoutEngine.rowGap = self.rowGap end
if self.flexDirection then
self._layoutEngine.flexDirection = self.flexDirection
end
if self.flexWrap then
self._layoutEngine.flexWrap = self.flexWrap
end
if self.justifyContent then
self._layoutEngine.justifyContent = self.justifyContent
end
if self.alignItems then
self._layoutEngine.alignItems = self.alignItems
end
if self.alignContent then
self._layoutEngine.alignContent = self.alignContent
end
if self.gap then
self._layoutEngine.gap = self.gap
end
if self.gridRows then
self._layoutEngine.gridRows = self.gridRows
end
if self.gridColumns then
self._layoutEngine.gridColumns = self.gridColumns
end
if self.columnGap then
self._layoutEngine.columnGap = self.columnGap
end
if self.rowGap then
self._layoutEngine.rowGap = self.rowGap
end
---animation
self.transform = props.transform or {}
@@ -1285,6 +1314,7 @@ function Element.new(props)
_scrollY = props._scrollY,
}, {
utils = utils,
Color = Color,
})
self._scrollManager:initialize(self)
@@ -1464,15 +1494,6 @@ function Element:_handleScrollbarRelease(button)
return false
end
--- Scroll to track click position (internal method used by ScrollManager)
---@param mouseX number
---@param mouseY number
---@param component string -- "vertical" or "horizontal"
function Element:_scrollToTrackPosition(mouseX, mouseY, component)
-- This method is now handled internally by ScrollManager
-- Keeping empty stub for backward compatibility
end
--- Handle mouse wheel scrolling (delegates to ScrollManager)
---@param x number -- Horizontal scroll amount
---@param y number -- Vertical scroll amount
@@ -1794,10 +1815,11 @@ function Element:draw(backdropCanvas)
self._renderer:drawText(self)
-- 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 then
if self.onEvent and not self.disableHighlight and self._eventHandler then
-- Check if any button is pressed
local anyPressed = false
for _, pressed in pairs(self._pressed) do
local pressedState = self._eventHandler:getState()._pressed or {}
for _, pressed in pairs(pressedState) do
if pressed then
anyPressed = true
break

View File

@@ -57,6 +57,7 @@ function LayoutEngine.new(props, deps)
self._Grid = deps.Grid
self._Units = deps.Units
self._Context = deps.Context
self._ErrorHandler = deps.ErrorHandler
self._Positioning = Positioning
self._FlexDirection = FlexDirection
self._JustifyContent = JustifyContent
@@ -174,6 +175,22 @@ function LayoutEngine:layoutChildren()
local isFlexChild = not (child.positioning == self._Positioning.ABSOLUTE and child._explicitlyAbsolute)
if isFlexChild then
table.insert(flexChildren, child)
-- Warn if child uses percentage sizing but parent has autosizing
if self._ErrorHandler then
if child.units and child.units.width then
if child.units.width.unit == "%" and self.element.autosizing and self.element.autosizing.width then
self._ErrorHandler.warn("LayoutEngine",
string.format("Child '%s' uses percentage width but parent has auto-sizing enabled. This may cause unexpected results", child.id or "unnamed"))
end
end
if child.units and child.units.height then
if child.units.height.unit == "%" and self.element.autosizing and self.element.autosizing.height then
self._ErrorHandler.warn("LayoutEngine",
string.format("Child '%s' uses percentage height but parent has auto-sizing enabled. This may cause unexpected results", child.id or "unnamed"))
end
end
end
end
end

View File

@@ -1,6 +1,7 @@
local Units = {}
local Context = nil
local ErrorHandler = nil
--- Initialize Units module with Context dependency
---@param context table The Context module
@@ -8,6 +9,12 @@ function Units.initialize(context)
Context = context
end
--- Initialize ErrorHandler dependency
---@param errorHandler table The ErrorHandler module
function Units.initializeErrorHandler(errorHandler)
ErrorHandler = errorHandler
end
---@param value string|number
---@return number, string -- Returns numeric value and unit type ("px", "%", "vw", "vh")
function Units.parse(value)
@@ -16,20 +23,34 @@ function Units.parse(value)
end
if type(value) ~= "string" then
-- Fallback to 0px for invalid types
if ErrorHandler then
ErrorHandler.error("Units", string.format("Invalid unit value type. Expected string or number, got %s", type(value)))
end
return 0, "px"
end
-- Check for invalid format (space between number and unit)
if value:match("%d%s+%a") then
if ErrorHandler then
ErrorHandler.error("Units", string.format("Invalid unit string '%s' (contains space). Use format like '50px' or '50%%'", value))
end
return 0, "px"
end
-- Match number followed by optional unit
local numStr, unit = value:match("^([%-]?[%d%.]+)(.*)$")
if not numStr then
-- Fallback to 0px for invalid format
if ErrorHandler then
ErrorHandler.error("Units", string.format("Invalid unit format '%s'. Expected format: number + unit (e.g., '50px', '10%%', '2vw')", value))
end
return 0, "px"
end
local num = tonumber(numStr)
if not num then
-- Fallback to 0px for invalid numeric value
if ErrorHandler then
ErrorHandler.error("Units", string.format("Invalid numeric value in '%s'", value))
end
return 0, "px"
end
@@ -40,6 +61,9 @@ function Units.parse(value)
local validUnits = { px = true, ["%"] = true, vw = true, vh = true, ew = true, eh = true }
if not validUnits[unit] then
if ErrorHandler then
ErrorHandler.error("Units", string.format("Unknown unit '%s' in '%s'. Valid units: px, %%, vw, vh, ew, eh", unit, value))
end
return num, "px"
end
@@ -59,7 +83,11 @@ function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
return value
elseif unit == "%" then
if not parentSize then
error(formatError("Units", "Percentage units require parent dimension"))
if ErrorHandler then
ErrorHandler.error("Units", "Percentage units require parent dimension. Element has no parent or parent dimension not available")
else
error("Percentage units require parent dimension")
end
end
return (value / 100) * parentSize
elseif unit == "vw" then
@@ -67,7 +95,11 @@ function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
elseif unit == "vh" then
return (value / 100) * viewportHeight
else
error(formatError("Units", string.format("Unknown unit type: '%s'. Valid units: px, %%, vw, vh, ew, eh", unit)))
if ErrorHandler then
ErrorHandler.error("Units", string.format("Unknown unit '%s'. Valid units: px, %%, vw, vh, ew, eh", unit))
else
error(string.format("Unknown unit type: '%s'", unit))
end
end
end