feat: add customDraw callback support to Element

- Add customDraw property to Element.new() for custom rendering callbacks
- Add getComputedBox() method to access element's content area position/size
- Call customDraw in Element:draw() between text and children (Layer 4.5)
- Graphics state isolated with push/pop and color reset
- Enables rendering custom content (e.g. game objects) within UI elements
This commit is contained in:
Michael Freno
2026-01-05 10:37:15 -05:00
parent cd99f15cec
commit 32cc418449

View File

@@ -355,6 +355,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
@@ -1313,10 +1315,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",
@@ -1500,7 +1510,7 @@ function Element.new(props)
-- Warn if explicit x/y is set on a child that will be positioned by flex layout -- Warn if explicit x/y is set on a child that will be positioned by flex layout
-- This position will be overridden unless the child has positioning="absolute" -- This position will be overridden unless the child has positioning="absolute"
local parentWillUseFlex = self.parent.positioning ~= "grid" local parentWillUseFlex = self.parent.positioning ~= "grid"
local childIsRelative = self.positioning ~= "absolute" or not self._explicitlyAbsolute local childIsRelative = self.positioning ~= "absolute" or not self._explicitlyAbsolute
if parentWillUseFlex and childIsRelative and (props.x or props.y) then if parentWillUseFlex and childIsRelative and (props.x or props.y) then
Element._ErrorHandler:warn("Element", "LAY_008", { Element._ErrorHandler:warn("Element", "LAY_008", {
@@ -1583,10 +1593,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",
@@ -1937,6 +1955,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()
@@ -2477,6 +2507,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