change to DI

This commit is contained in:
Michael Freno
2025-11-12 23:30:29 -05:00
parent 84f45a019b
commit b886085d3e
8 changed files with 258 additions and 192 deletions

View File

@@ -1,5 +1,4 @@
--[[ --[[
Color.lua - Color utility class for FlexLove
Provides color handling with RGB/RGBA support and hex string conversion Provides color handling with RGB/RGBA support and hex string conversion
]] ]]

View File

@@ -1,8 +1,3 @@
-- ====================
-- Element Object
-- ====================
-- Setup module path for relative requires
local modulePath = (...):match("(.-)[^%.]+$") local modulePath = (...):match("(.-)[^%.]+$")
local function req(name) local function req(name)
return require(modulePath .. name) return require(modulePath .. name)
@@ -204,6 +199,9 @@ function Element.new(props)
-- Initialize EventHandler for event processing -- Initialize EventHandler for event processing
self._eventHandler = EventHandler.new({ self._eventHandler = EventHandler.new({
onEvent = self.onEvent, onEvent = self.onEvent,
}, {
InputEvent = InputEvent,
GuiState = GuiState,
}) })
self._eventHandler:initialize(self) self._eventHandler:initialize(self)
@@ -219,6 +217,8 @@ function Element.new(props)
disableHighlight = props.disableHighlight, disableHighlight = props.disableHighlight,
scaleCorners = props.scaleCorners, scaleCorners = props.scaleCorners,
scalingAlgorithm = props.scalingAlgorithm, scalingAlgorithm = props.scalingAlgorithm,
}, {
Theme = Theme,
}) })
self._themeManager:initialize(self) self._themeManager:initialize(self)
@@ -320,6 +320,11 @@ function Element.new(props)
onTextInput = props.onTextInput, onTextInput = props.onTextInput,
onTextChange = props.onTextChange, onTextChange = props.onTextChange,
onEnter = props.onEnter, onEnter = props.onEnter,
}, {
GuiState = GuiState,
StateManager = StateManager,
Color = Color,
utils = utils,
}) })
-- Initialize will be called after self is fully constructed -- Initialize will be called after self is fully constructed
end end
@@ -425,9 +430,15 @@ function Element.new(props)
objectFit = self.objectFit, objectFit = self.objectFit,
objectPosition = self.objectPosition, objectPosition = self.objectPosition,
imageOpacity = self.imageOpacity, imageOpacity = self.imageOpacity,
contentBlur = self.contentBlur, }, {
backdropBlur = self.backdropBlur, Color = Color,
_themeState = self._themeState, RoundedRect = RoundedRect,
NinePatch = NinePatch,
ImageRenderer = ImageRenderer,
ImageCache = ImageCache,
Theme = Theme,
Blur = Blur,
utils = utils,
}) })
self._renderer:initialize(self) self._renderer:initialize(self)
@@ -1135,6 +1146,9 @@ function Element.new(props)
gridColumns = self.gridColumns, gridColumns = self.gridColumns,
columnGap = self.columnGap, columnGap = self.columnGap,
rowGap = self.rowGap, rowGap = self.rowGap,
}, {
utils = utils,
Grid = Grid,
}) })
-- Initialize immediately so it can be used for auto-sizing calculations -- Initialize immediately so it can be used for auto-sizing calculations
self._layoutEngine:initialize(self) self._layoutEngine:initialize(self)
@@ -1158,6 +1172,8 @@ function Element.new(props)
hideScrollbars = props.hideScrollbars, hideScrollbars = props.hideScrollbars,
_scrollX = props._scrollX, _scrollX = props._scrollX,
_scrollY = props._scrollY, _scrollY = props._scrollY,
}, {
utils = utils,
}) })
self._scrollManager:initialize(self) self._scrollManager:initialize(self)
@@ -1958,14 +1974,9 @@ function Element:update(dt)
if self.themeComponent then if self.themeComponent then
-- Check if any button is pressed via EventHandler -- Check if any button is pressed via EventHandler
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( local newThemeState = self._themeManager:updateState(isHovering and isActiveElement, anyPressed, self._focused, self.disabled)
isHovering and isActiveElement,
anyPressed,
self._focused,
self.disabled
)
-- Update state (in StateManager if in immediate mode, otherwise locally) -- Update state (in StateManager if in immediate mode, otherwise locally)
if self._stateId and Gui._immediateMode then if self._stateId and Gui._immediateMode then

View File

@@ -3,15 +3,16 @@
-- ==================== -- ====================
-- Handles all user input events (mouse, keyboard, touch) for UI elements -- Handles all user input events (mouse, keyboard, touch) for UI elements
-- Manages event state, click detection, drag tracking, hover, and focus -- Manages event state, click detection, drag tracking, hover, and focus
---
--- Dependencies (must be injected via deps parameter):
--- - InputEvent: Input event class for creating event objects
--- - GuiState: GUI state manager (unused currently, reserved for future use)
local modulePath = (...):match("(.-)[^%.]+$") local modulePath = (...):match("(.-)[^%.]+$")
local function req(name) local function req(name)
return require(modulePath .. name) return require(modulePath .. name)
end end
local InputEvent = req("InputEvent")
local GuiState = req("GuiState")
-- Get keyboard modifiers helper -- Get keyboard modifiers helper
local function getModifiers() local function getModifiers()
return { return {
@@ -28,12 +29,22 @@ EventHandler.__index = EventHandler
--- Create a new EventHandler instance --- Create a new EventHandler instance
---@param config table Configuration options ---@param config table Configuration options
---@param deps table Dependencies {InputEvent, GuiState}
---@return EventHandler ---@return EventHandler
function EventHandler.new(config) function EventHandler.new(config, deps)
-- Pure DI: Dependencies must be injected
assert(deps, "EventHandler.new: deps parameter is required")
assert(deps.InputEvent, "EventHandler.new: deps.InputEvent is required")
assert(deps.GuiState, "EventHandler.new: deps.GuiState is required")
config = config or {} config = config or {}
local self = setmetatable({}, EventHandler) local self = setmetatable({}, EventHandler)
-- Store dependencies
self._InputEvent = deps.InputEvent
self._GuiState = deps.GuiState
-- Event callback -- Event callback
self.onEvent = config.onEvent self.onEvent = config.onEvent
@@ -183,7 +194,7 @@ function EventHandler:_handleMousePress(mx, my, button)
-- Fire press event -- Fire press event
if self.onEvent then if self.onEvent then
local modifiers = getModifiers() local modifiers = getModifiers()
local pressEvent = InputEvent.new({ local pressEvent = self._InputEvent.new({
type = "press", type = "press",
button = button, button = button,
x = mx, x = mx,
@@ -222,14 +233,14 @@ function EventHandler:_handleMouseDrag(mx, my, button, isHovering)
local lastX = self._lastMouseX[button] or mx local lastX = self._lastMouseX[button] or mx
local lastY = self._lastMouseY[button] or my local lastY = self._lastMouseY[button] or my
if lastX ~= mx or lastY ~= my then if lastX ~= mx or lastY ~= my then
-- Mouse has moved - fire drag event only if still hovering -- Mouse has moved - fire drag event only if still hovering
if self.onEvent and isHovering then if self.onEvent and isHovering then
local modifiers = getModifiers() local modifiers = getModifiers()
local dx = mx - self._dragStartX[button] local dx = mx - self._dragStartX[button]
local dy = my - self._dragStartY[button] local dy = my - self._dragStartY[button]
local dragEvent = InputEvent.new({ local dragEvent = self._InputEvent.new({
type = "drag", type = "drag",
button = button, button = button,
x = mx, x = mx,
@@ -289,7 +300,7 @@ function EventHandler:_handleMouseRelease(mx, my, button)
-- Fire click event -- Fire click event
if self.onEvent then if self.onEvent then
local clickEvent = InputEvent.new({ local clickEvent = self._InputEvent.new({
type = eventType, type = eventType,
button = button, button = button,
x = mx, x = mx,
@@ -331,7 +342,7 @@ function EventHandler:_handleMouseRelease(mx, my, button)
-- Fire release event -- Fire release event
if self.onEvent then if self.onEvent then
local releaseEvent = InputEvent.new({ local releaseEvent = self._InputEvent.new({
type = "release", type = "release",
button = button, button = button,
x = mx, x = mx,
@@ -363,7 +374,7 @@ function EventHandler:processTouchEvents()
self._touchPressed[id] = true self._touchPressed[id] = true
elseif self._touchPressed[id] then elseif self._touchPressed[id] then
-- Create touch event (treat as left click) -- Create touch event (treat as left click)
local touchEvent = InputEvent.new({ local touchEvent = self._InputEvent.new({
type = "click", type = "click",
button = 1, button = 1,
x = tx, x = tx,

View File

@@ -6,26 +6,10 @@
-- - Grid layout delegation -- - Grid layout delegation
-- - Auto-sizing calculations -- - Auto-sizing calculations
-- - CSS positioning offsets -- - CSS positioning offsets
---
-- Setup module path for relative requires --- Dependencies (must be injected via deps parameter):
local modulePath = (...):match("(.-)[^%.]+$") --- - utils: Utility functions and enums
local function req(name) --- - Grid: Grid layout module
return require(modulePath .. name)
end
-- Module dependencies
local utils = req("utils")
local Grid = req("Grid")
-- Extract enum values
local enums = utils.enums
local Positioning = enums.Positioning
local FlexDirection = enums.FlexDirection
local JustifyContent = enums.JustifyContent
local AlignContent = enums.AlignContent
local AlignItems = enums.AlignItems
local AlignSelf = enums.AlignSelf
local FlexWrap = enums.FlexWrap
---@class LayoutEngine ---@class LayoutEngine
---@field element Element Reference to the parent element ---@field element Element Reference to the parent element
@@ -58,10 +42,36 @@ LayoutEngine.__index = LayoutEngine
--- Create a new LayoutEngine instance --- Create a new LayoutEngine instance
---@param props LayoutEngineProps ---@param props LayoutEngineProps
---@param deps table Dependencies {utils, Grid}
---@return LayoutEngine ---@return LayoutEngine
function LayoutEngine.new(props) function LayoutEngine.new(props, deps)
-- Pure DI: Dependencies must be injected
assert(deps, "LayoutEngine.new: deps parameter is required")
assert(deps.utils, "LayoutEngine.new: deps.utils is required")
assert(deps.Grid, "LayoutEngine.new: deps.Grid is required")
-- Extract enums from utils
local enums = deps.utils.enums
local Positioning = enums.Positioning
local FlexDirection = enums.FlexDirection
local JustifyContent = enums.JustifyContent
local AlignContent = enums.AlignContent
local AlignItems = enums.AlignItems
local AlignSelf = enums.AlignSelf
local FlexWrap = enums.FlexWrap
local self = setmetatable({}, LayoutEngine) local self = setmetatable({}, LayoutEngine)
-- Store dependencies for instance methods
self._Grid = deps.Grid
self._Positioning = Positioning
self._FlexDirection = FlexDirection
self._JustifyContent = JustifyContent
self._AlignContent = AlignContent
self._AlignItems = AlignItems
self._AlignSelf = AlignSelf
self._FlexWrap = FlexWrap
-- Layout configuration -- Layout configuration
self.positioning = props.positioning or Positioning.FLEX self.positioning = props.positioning or Positioning.FLEX
self.flexDirection = props.flexDirection or FlexDirection.HORIZONTAL self.flexDirection = props.flexDirection or FlexDirection.HORIZONTAL
@@ -104,9 +114,9 @@ function LayoutEngine:applyPositioningOffsets(child)
-- Only apply offsets to explicitly absolute children or children in relative/absolute containers -- Only apply offsets to explicitly absolute children or children in relative/absolute containers
-- Flex/grid children ignore positioning offsets as they participate in layout -- Flex/grid children ignore positioning offsets as they participate in layout
local isFlexChild = child.positioning == Positioning.FLEX local isFlexChild = child.positioning == self._Positioning.FLEX
or child.positioning == Positioning.GRID or child.positioning == self._Positioning.GRID
or (child.positioning == Positioning.ABSOLUTE and not child._explicitlyAbsolute) or (child.positioning == self._Positioning.ABSOLUTE and not child._explicitlyAbsolute)
if not isFlexChild then if not isFlexChild then
-- Apply absolute positioning for explicitly absolute children -- Apply absolute positioning for explicitly absolute children
@@ -140,7 +150,7 @@ end
function LayoutEngine:layoutChildren() function LayoutEngine:layoutChildren()
local element = self.element local element = self.element
if self.positioning == Positioning.ABSOLUTE or self.positioning == Positioning.RELATIVE then if self.positioning == self._Positioning.ABSOLUTE or self.positioning == self._Positioning.RELATIVE then
-- Absolute/Relative positioned containers don't layout their children according to flex rules, -- Absolute/Relative positioned containers don't layout their children according to flex rules,
-- but they should still apply CSS positioning offsets to their children -- but they should still apply CSS positioning offsets to their children
for _, child in ipairs(element.children) do for _, child in ipairs(element.children) do
@@ -152,8 +162,8 @@ function LayoutEngine:layoutChildren()
end end
-- Handle grid layout -- Handle grid layout
if self.positioning == Positioning.GRID then if self.positioning == self._Positioning.GRID then
Grid.layoutGridItems(element) self._Grid.layoutGridItems(element)
return return
end end
@@ -166,7 +176,7 @@ function LayoutEngine:layoutChildren()
-- Get flex children (children that participate in flex layout) -- Get flex children (children that participate in flex layout)
local flexChildren = {} local flexChildren = {}
for _, child in ipairs(element.children) do for _, child in ipairs(element.children) do
local isFlexChild = not (child.positioning == Positioning.ABSOLUTE and child._explicitlyAbsolute) local isFlexChild = not (child.positioning == self._Positioning.ABSOLUTE and child._explicitlyAbsolute)
if isFlexChild then if isFlexChild then
table.insert(flexChildren, child) table.insert(flexChildren, child)
end end
@@ -184,12 +194,12 @@ function LayoutEngine:layoutChildren()
for _, child in ipairs(element.children) do for _, child in ipairs(element.children) do
-- Only consider absolutely positioned children with explicit positioning -- Only consider absolutely positioned children with explicit positioning
if child.positioning == Positioning.ABSOLUTE and child._explicitlyAbsolute then if child.positioning == self._Positioning.ABSOLUTE and child._explicitlyAbsolute then
-- BORDER-BOX MODEL: Use border-box dimensions for space calculations -- BORDER-BOX MODEL: Use border-box dimensions for space calculations
local childBorderBoxWidth = child:getBorderBoxWidth() local childBorderBoxWidth = child:getBorderBoxWidth()
local childBorderBoxHeight = child:getBorderBoxHeight() local childBorderBoxHeight = child:getBorderBoxHeight()
if self.flexDirection == FlexDirection.HORIZONTAL then if self.flexDirection == self._FlexDirection.HORIZONTAL then
-- Horizontal layout: main axis is X, cross axis is Y -- Horizontal layout: main axis is X, cross axis is Y
-- Check for left positioning (reserves space at main axis start) -- Check for left positioning (reserves space at main axis start)
if child.left then if child.left then
@@ -241,7 +251,7 @@ 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
if self.flexDirection == FlexDirection.HORIZONTAL then if self.flexDirection == self._FlexDirection.HORIZONTAL then
availableMainSize = element.width - reservedMainStart - reservedMainEnd availableMainSize = element.width - reservedMainStart - reservedMainEnd
availableCrossSize = element.height - reservedCrossStart - reservedCrossEnd availableCrossSize = element.height - reservedCrossStart - reservedCrossEnd
else else
@@ -252,7 +262,7 @@ function LayoutEngine:layoutChildren()
-- Handle flex wrap: create lines of children -- Handle flex wrap: create lines of children
local lines = {} local lines = {}
if self.flexWrap == FlexWrap.NOWRAP then if self.flexWrap == self._FlexWrap.NOWRAP then
-- All children go on one line -- All children go on one line
lines[1] = flexChildren lines[1] = flexChildren
else else
@@ -265,7 +275,7 @@ function LayoutEngine:layoutChildren()
-- Include margins in size calculations -- Include margins in size calculations
local childMainSize = 0 local childMainSize = 0
local childMainMargin = 0 local childMainMargin = 0
if self.flexDirection == FlexDirection.HORIZONTAL then if self.flexDirection == self._FlexDirection.HORIZONTAL then
childMainSize = child:getBorderBoxWidth() childMainSize = child:getBorderBoxWidth()
childMainMargin = child.margin.left + child.margin.right childMainMargin = child.margin.left + child.margin.right
else else
@@ -296,7 +306,7 @@ function LayoutEngine:layoutChildren()
end end
-- Handle wrap-reverse: reverse the order of lines -- Handle wrap-reverse: reverse the order of lines
if self.flexWrap == FlexWrap.WRAP_REVERSE then if self.flexWrap == self._FlexWrap.WRAP_REVERSE then
local reversedLines = {} local reversedLines = {}
for i = #lines, 1, -1 do for i = #lines, 1, -1 do
table.insert(reversedLines, lines[i]) table.insert(reversedLines, lines[i])
@@ -316,7 +326,7 @@ function LayoutEngine:layoutChildren()
-- Include margins in cross-axis size calculations -- Include margins in cross-axis size calculations
local childCrossSize = 0 local childCrossSize = 0
local childCrossMargin = 0 local childCrossMargin = 0
if self.flexDirection == FlexDirection.HORIZONTAL then if self.flexDirection == self._FlexDirection.HORIZONTAL then
childCrossSize = child:getBorderBoxHeight() childCrossSize = child:getBorderBoxHeight()
childCrossMargin = child.margin.top + child.margin.bottom childCrossMargin = child.margin.top + child.margin.bottom
else else
@@ -336,7 +346,7 @@ 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 == AlignItems.STRETCH or self.alignItems == AlignItems.CENTER or self.alignItems == 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
@@ -351,22 +361,22 @@ function LayoutEngine:layoutChildren()
local freeLineSpace = availableCrossSize - totalLinesHeight local freeLineSpace = availableCrossSize - totalLinesHeight
-- Apply AlignContent logic for both single and multiple lines -- Apply AlignContent logic for both single and multiple lines
if self.alignContent == AlignContent.FLEX_START then if self.alignContent == self._AlignContent.FLEX_START then
lineStartPos = 0 lineStartPos = 0
elseif self.alignContent == AlignContent.CENTER then elseif self.alignContent == self._AlignContent.CENTER then
lineStartPos = freeLineSpace / 2 lineStartPos = freeLineSpace / 2
elseif self.alignContent == AlignContent.FLEX_END then elseif self.alignContent == self._AlignContent.FLEX_END then
lineStartPos = freeLineSpace lineStartPos = freeLineSpace
elseif self.alignContent == AlignContent.SPACE_BETWEEN then elseif self.alignContent == self._AlignContent.SPACE_BETWEEN then
lineStartPos = 0 lineStartPos = 0
if #lines > 1 then if #lines > 1 then
lineSpacing = self.gap + (freeLineSpace / (#lines - 1)) lineSpacing = self.gap + (freeLineSpace / (#lines - 1))
end end
elseif self.alignContent == AlignContent.SPACE_AROUND then elseif self.alignContent == self._AlignContent.SPACE_AROUND then
local spaceAroundEach = freeLineSpace / #lines local spaceAroundEach = freeLineSpace / #lines
lineStartPos = spaceAroundEach / 2 lineStartPos = spaceAroundEach / 2
lineSpacing = self.gap + spaceAroundEach lineSpacing = self.gap + spaceAroundEach
elseif self.alignContent == AlignContent.STRETCH then elseif self.alignContent == self._AlignContent.STRETCH then
lineStartPos = 0 lineStartPos = 0
if #lines > 1 and freeLineSpace > 0 then if #lines > 1 and freeLineSpace > 0 then
lineSpacing = self.gap + (freeLineSpace / #lines) lineSpacing = self.gap + (freeLineSpace / #lines)
@@ -388,7 +398,7 @@ function LayoutEngine:layoutChildren()
-- BORDER-BOX MODEL: Use border-box dimensions for layout calculations -- BORDER-BOX MODEL: Use border-box dimensions for layout calculations
local totalChildrenSize = 0 local totalChildrenSize = 0
for _, child in ipairs(line) do for _, child in ipairs(line) do
if self.flexDirection == FlexDirection.HORIZONTAL then if self.flexDirection == self._FlexDirection.HORIZONTAL then
totalChildrenSize = totalChildrenSize + child:getBorderBoxWidth() + child.margin.left + child.margin.right totalChildrenSize = totalChildrenSize + child:getBorderBoxWidth() + child.margin.left + child.margin.right
else else
totalChildrenSize = totalChildrenSize + child:getBorderBoxHeight() + child.margin.top + child.margin.bottom totalChildrenSize = totalChildrenSize + child:getBorderBoxHeight() + child.margin.top + child.margin.bottom
@@ -403,22 +413,22 @@ function LayoutEngine:layoutChildren()
local startPos = 0 local startPos = 0
local itemSpacing = self.gap local itemSpacing = self.gap
if self.justifyContent == JustifyContent.FLEX_START then if self.justifyContent == self._JustifyContent.FLEX_START then
startPos = 0 startPos = 0
elseif self.justifyContent == JustifyContent.CENTER then elseif self.justifyContent == self._JustifyContent.CENTER then
startPos = freeSpace / 2 startPos = freeSpace / 2
elseif self.justifyContent == JustifyContent.FLEX_END then elseif self.justifyContent == self._JustifyContent.FLEX_END then
startPos = freeSpace startPos = freeSpace
elseif self.justifyContent == JustifyContent.SPACE_BETWEEN then elseif self.justifyContent == self._JustifyContent.SPACE_BETWEEN then
startPos = 0 startPos = 0
if #line > 1 then if #line > 1 then
itemSpacing = self.gap + (freeSpace / (#line - 1)) itemSpacing = self.gap + (freeSpace / (#line - 1))
end end
elseif self.justifyContent == JustifyContent.SPACE_AROUND then elseif self.justifyContent == self._JustifyContent.SPACE_AROUND then
local spaceAroundEach = freeSpace / #line local spaceAroundEach = freeSpace / #line
startPos = spaceAroundEach / 2 startPos = spaceAroundEach / 2
itemSpacing = self.gap + spaceAroundEach itemSpacing = self.gap + spaceAroundEach
elseif self.justifyContent == JustifyContent.SPACE_EVENLY then elseif self.justifyContent == self._JustifyContent.SPACE_EVENLY then
local spaceBetween = freeSpace / (#line + 1) local spaceBetween = freeSpace / (#line + 1)
startPos = spaceBetween startPos = spaceBetween
itemSpacing = self.gap + spaceBetween itemSpacing = self.gap + spaceBetween
@@ -430,11 +440,11 @@ function LayoutEngine:layoutChildren()
for _, child in ipairs(line) do for _, child in ipairs(line) do
-- Determine effective cross-axis alignment -- Determine effective cross-axis alignment
local effectiveAlign = child.alignSelf local effectiveAlign = child.alignSelf
if effectiveAlign == nil or effectiveAlign == AlignSelf.AUTO then if effectiveAlign == nil or effectiveAlign == self._AlignSelf.AUTO then
effectiveAlign = self.alignItems effectiveAlign = self.alignItems
end end
if self.flexDirection == FlexDirection.HORIZONTAL then if self.flexDirection == self._FlexDirection.HORIZONTAL then
-- Horizontal layout: main axis is X, cross axis is Y -- Horizontal layout: main axis is X, cross axis is Y
-- Position child at border box (x, y represents top-left including padding) -- Position child at border box (x, y represents top-left including padding)
-- Add reservedMainStart and left margin to account for absolutely positioned siblings and margins -- Add reservedMainStart and left margin to account for absolutely positioned siblings and margins
@@ -444,13 +454,13 @@ function LayoutEngine:layoutChildren()
local childBorderBoxHeight = child:getBorderBoxHeight() local childBorderBoxHeight = child:getBorderBoxHeight()
local childTotalCrossSize = childBorderBoxHeight + child.margin.top + child.margin.bottom local childTotalCrossSize = childBorderBoxHeight + child.margin.top + child.margin.bottom
if effectiveAlign == AlignItems.FLEX_START then if effectiveAlign == self._AlignItems.FLEX_START then
child.y = element.y + element.padding.top + reservedCrossStart + currentCrossPos + child.margin.top child.y = element.y + element.padding.top + reservedCrossStart + currentCrossPos + child.margin.top
elseif effectiveAlign == AlignItems.CENTER then elseif effectiveAlign == self._AlignItems.CENTER then
child.y = element.y + element.padding.top + reservedCrossStart + currentCrossPos + ((lineHeight - childTotalCrossSize) / 2) + child.margin.top child.y = element.y + element.padding.top + reservedCrossStart + currentCrossPos + ((lineHeight - childTotalCrossSize) / 2) + child.margin.top
elseif effectiveAlign == AlignItems.FLEX_END then elseif effectiveAlign == self._AlignItems.FLEX_END then
child.y = element.y + element.padding.top + reservedCrossStart + currentCrossPos + lineHeight - childTotalCrossSize + child.margin.top child.y = element.y + element.padding.top + reservedCrossStart + currentCrossPos + lineHeight - childTotalCrossSize + child.margin.top
elseif effectiveAlign == AlignItems.STRETCH then elseif effectiveAlign == self._AlignItems.STRETCH then
-- STRETCH: Only apply if height was not explicitly set -- STRETCH: Only apply if height was not explicitly set
if child.autosizing and child.autosizing.height then if child.autosizing and child.autosizing.height then
-- STRETCH: Set border-box height to lineHeight minus margins, content area shrinks to fit -- STRETCH: Set border-box height to lineHeight minus margins, content area shrinks to fit
@@ -481,13 +491,13 @@ function LayoutEngine:layoutChildren()
local childBorderBoxWidth = child:getBorderBoxWidth() local childBorderBoxWidth = child:getBorderBoxWidth()
local childTotalCrossSize = childBorderBoxWidth + child.margin.left + child.margin.right local childTotalCrossSize = childBorderBoxWidth + child.margin.left + child.margin.right
if effectiveAlign == AlignItems.FLEX_START then if effectiveAlign == self._AlignItems.FLEX_START then
child.x = element.x + element.padding.left + reservedCrossStart + currentCrossPos + child.margin.left child.x = element.x + element.padding.left + reservedCrossStart + currentCrossPos + child.margin.left
elseif effectiveAlign == AlignItems.CENTER then elseif effectiveAlign == self._AlignItems.CENTER then
child.x = element.x + element.padding.left + reservedCrossStart + currentCrossPos + ((lineHeight - childTotalCrossSize) / 2) + child.margin.left child.x = element.x + element.padding.left + reservedCrossStart + currentCrossPos + ((lineHeight - childTotalCrossSize) / 2) + child.margin.left
elseif effectiveAlign == AlignItems.FLEX_END then elseif effectiveAlign == self._AlignItems.FLEX_END then
child.x = element.x + element.padding.left + reservedCrossStart + currentCrossPos + lineHeight - childTotalCrossSize + child.margin.left child.x = element.x + element.padding.left + reservedCrossStart + currentCrossPos + lineHeight - childTotalCrossSize + child.margin.left
elseif effectiveAlign == AlignItems.STRETCH then elseif effectiveAlign == self._AlignItems.STRETCH then
-- STRETCH: Only apply if width was not explicitly set -- STRETCH: Only apply if width was not explicitly set
if child.autosizing and child.autosizing.width then if child.autosizing and child.autosizing.width then
-- STRETCH: Set border-box width to lineHeight minus margins, content area shrinks to fit -- STRETCH: Set border-box width to lineHeight minus margins, content area shrinks to fit
@@ -517,7 +527,7 @@ function LayoutEngine:layoutChildren()
-- Position explicitly absolute children after flex layout -- Position explicitly absolute children after flex layout
for _, child in ipairs(element.children) do for _, child in ipairs(element.children) do
if child.positioning == Positioning.ABSOLUTE and child._explicitlyAbsolute then if child.positioning == self._Positioning.ABSOLUTE and child._explicitlyAbsolute then
-- Apply positioning offsets (top, right, bottom, left) -- Apply positioning offsets (top, right, bottom, left)
self:applyPositioningOffsets(child) self:applyPositioningOffsets(child)
@@ -547,7 +557,7 @@ function LayoutEngine:calculateAutoWidth()
-- For HORIZONTAL flex: sum children widths + gaps -- For HORIZONTAL flex: sum children widths + gaps
-- For VERTICAL flex: max of children widths -- For VERTICAL flex: max of children widths
local isHorizontal = self.flexDirection == FlexDirection.HORIZONTAL local isHorizontal = self.flexDirection == self._FlexDirection.HORIZONTAL
local totalWidth = contentWidth local totalWidth = contentWidth
local maxWidth = contentWidth local maxWidth = contentWidth
local participatingChildren = 0 local participatingChildren = 0
@@ -587,7 +597,7 @@ function LayoutEngine:calculateAutoHeight()
-- For VERTICAL flex: sum children heights + gaps -- For VERTICAL flex: sum children heights + gaps
-- For HORIZONTAL flex: max of children heights -- For HORIZONTAL flex: max of children heights
local isVertical = self.flexDirection == FlexDirection.VERTICAL local isVertical = self.flexDirection == self._FlexDirection.VERTICAL
local totalHeight = height local totalHeight = height
local maxHeight = height local maxHeight = height
local participatingChildren = 0 local participatingChildren = 0

View File

@@ -4,37 +4,53 @@
-- --
-- This module is responsible for the visual presentation layer of Elements, -- This module is responsible for the visual presentation layer of Elements,
-- delegating from Element's draw() method to keep rendering concerns separated. -- delegating from Element's draw() method to keep rendering concerns separated.
---
-- Setup module path for relative requires --- Dependencies (must be injected via deps parameter):
local modulePath = (...):match("(.-)[^%.]+$") --- - Color: Color module for color manipulation
local function req(name) --- - RoundedRect: Rounded rectangle drawing module
return require(modulePath .. name) --- - NinePatch: 9-patch rendering module
end --- - ImageRenderer: Image rendering module
--- - ImageCache: Image caching module
--- - Theme: Theme management module
--- - Blur: Blur effects module
--- - utils: Utility functions (FONT_CACHE, enums)
local Renderer = {} local Renderer = {}
Renderer.__index = Renderer Renderer.__index = Renderer
-- Dependencies
local Color = req("Color")
local RoundedRect = req("RoundedRect")
local NinePatch = req("NinePatch")
local ImageRenderer = req("ImageRenderer")
local ImageCache = req("ImageCache")
local Theme = req("Theme")
local Blur = req("Blur")
local utils = req("utils")
-- Font cache and enums (shared with Element for now - could be refactored later)
local FONT_CACHE = utils.FONT_CACHE
local enums = utils.enums
local TextAlign = enums.TextAlign
--- Create a new Renderer instance --- Create a new Renderer instance
---@param config table Configuration table with rendering properties ---@param config table Configuration table with rendering properties
---@param deps table Dependencies {Color, RoundedRect, NinePatch, ImageRenderer, ImageCache, Theme, Blur, utils}
---@return table Renderer instance ---@return table Renderer instance
function Renderer.new(config) function Renderer.new(config, deps)
-- Pure DI: Dependencies must be injected
assert(deps, "Renderer.new: deps parameter is required")
assert(deps.Color, "Renderer.new: deps.Color is required")
assert(deps.RoundedRect, "Renderer.new: deps.RoundedRect is required")
assert(deps.NinePatch, "Renderer.new: deps.NinePatch is required")
assert(deps.ImageRenderer, "Renderer.new: deps.ImageRenderer is required")
assert(deps.ImageCache, "Renderer.new: deps.ImageCache is required")
assert(deps.Theme, "Renderer.new: deps.Theme is required")
assert(deps.Blur, "Renderer.new: deps.Blur is required")
assert(deps.utils, "Renderer.new: deps.utils is required")
local Color = deps.Color
local ImageCache = deps.ImageCache
local self = setmetatable({}, Renderer) local self = setmetatable({}, Renderer)
-- Store dependencies for instance methods
self._Color = Color
self._RoundedRect = deps.RoundedRect
self._NinePatch = deps.NinePatch
self._ImageRenderer = deps.ImageRenderer
self._ImageCache = ImageCache
self._Theme = deps.Theme
self._Blur = deps.Blur
self._utils = deps.utils
self._FONT_CACHE = deps.utils.FONT_CACHE
self._TextAlign = deps.utils.enums.TextAlign
-- Store reference to parent element (will be set via initialize) -- Store reference to parent element (will be set via initialize)
self._element = nil self._element = nil
@@ -113,7 +129,7 @@ function Renderer:getBlurInstance()
-- Create or reuse blur instance -- Create or reuse blur instance
if not self._blurInstance or self._blurInstance.quality ~= quality then if not self._blurInstance or self._blurInstance.quality ~= quality then
self._blurInstance = Blur.new(quality) self._blurInstance = self._Blur.new(quality)
end end
return self._blurInstance return self._blurInstance
@@ -132,14 +148,14 @@ end
---@param height number Height ---@param height number Height
---@param drawBackgroundColor table Background color (may have animation applied) ---@param drawBackgroundColor table Background color (may have animation applied)
function Renderer:_drawBackground(x, y, width, height, drawBackgroundColor) function Renderer:_drawBackground(x, y, width, height, drawBackgroundColor)
local backgroundWithOpacity = Color.new( local backgroundWithOpacity = self._Color.new(
drawBackgroundColor.r, drawBackgroundColor.r,
drawBackgroundColor.g, drawBackgroundColor.g,
drawBackgroundColor.b, drawBackgroundColor.b,
drawBackgroundColor.a * self.opacity drawBackgroundColor.a * self.opacity
) )
love.graphics.setColor(backgroundWithOpacity:toRGBA()) love.graphics.setColor(backgroundWithOpacity:toRGBA())
RoundedRect.draw("fill", x, y, width, height, self.cornerRadius) self._RoundedRect.draw("fill", x, y, width, height, self.cornerRadius)
end end
--- Draw image layer --- Draw image layer
@@ -174,13 +190,13 @@ function Renderer:_drawImage(x, y, paddingLeft, paddingTop, contentWidth, conten
if hasCornerRadius then if hasCornerRadius then
-- Use stencil to clip image to rounded corners -- Use stencil to clip image to rounded corners
love.graphics.stencil(function() love.graphics.stencil(function()
RoundedRect.draw("fill", x, y, borderBoxWidth, borderBoxHeight, self.cornerRadius) self._RoundedRect.draw("fill", x, y, borderBoxWidth, borderBoxHeight, self.cornerRadius)
end, "replace", 1) end, "replace", 1)
love.graphics.setStencilTest("greater", 0) love.graphics.setStencilTest("greater", 0)
end end
-- Draw the image -- Draw the image
ImageRenderer.draw(self._loadedImage, imageX, imageY, imageWidth, imageHeight, self.objectFit, self.objectPosition, finalOpacity) self._ImageRenderer.draw(self._loadedImage, imageX, imageY, imageWidth, imageHeight, self.objectFit, self.objectPosition, finalOpacity)
-- Clear stencil if it was used -- Clear stencil if it was used
if hasCornerRadius then if hasCornerRadius then
@@ -204,18 +220,18 @@ function Renderer:_drawTheme(x, y, borderBoxWidth, borderBoxHeight, scaleCorners
local themeToUse = nil local themeToUse = nil
if self.theme then if self.theme then
-- Element specifies a specific theme - load it if needed -- Element specifies a specific theme - load it if needed
if Theme.get(self.theme) then if self._Theme.get(self.theme) then
themeToUse = Theme.get(self.theme) themeToUse = self._Theme.get(self.theme)
else else
-- Try to load the theme -- Try to load the theme
pcall(function() pcall(function()
Theme.load(self.theme) self._Theme.load(self.theme)
end) end)
themeToUse = Theme.get(self.theme) themeToUse = self._Theme.get(self.theme)
end end
else else
-- Use active theme -- Use active theme
themeToUse = Theme.getActive() themeToUse = self._Theme.getActive()
end end
if not themeToUse then if not themeToUse then
@@ -251,7 +267,7 @@ function Renderer:_drawTheme(x, y, borderBoxWidth, borderBoxHeight, scaleCorners
if hasAllRegions then if hasAllRegions then
-- Pass element-level overrides for scaleCorners and scalingAlgorithm -- Pass element-level overrides for scaleCorners and scalingAlgorithm
NinePatch.draw(component, atlasToUse, x, y, borderBoxWidth, borderBoxHeight, self.opacity, scaleCorners, scalingAlgorithm) self._NinePatch.draw(component, atlasToUse, x, y, borderBoxWidth, borderBoxHeight, self.opacity, scaleCorners, scalingAlgorithm)
end end
end end
end end
@@ -262,7 +278,7 @@ end
---@param borderBoxWidth number Border box width ---@param borderBoxWidth number Border box width
---@param borderBoxHeight number Border box height ---@param borderBoxHeight number Border box height
function Renderer:_drawBorders(x, y, borderBoxWidth, borderBoxHeight) function Renderer:_drawBorders(x, y, borderBoxWidth, borderBoxHeight)
local borderColorWithOpacity = Color.new( local borderColorWithOpacity = self._Color.new(
self.borderColor.r, self.borderColor.r,
self.borderColor.g, self.borderColor.g,
self.borderColor.b, self.borderColor.b,
@@ -275,7 +291,7 @@ function Renderer:_drawBorders(x, y, borderBoxWidth, borderBoxHeight)
if allBorders then if allBorders then
-- Draw complete rounded rectangle border -- Draw complete rounded rectangle border
RoundedRect.draw("line", x, y, borderBoxWidth, borderBoxHeight, self.cornerRadius) self._RoundedRect.draw("line", x, y, borderBoxWidth, borderBoxHeight, self.cornerRadius)
else else
-- Draw individual borders (without rounded corners for partial borders) -- Draw individual borders (without rounded corners for partial borders)
if self.border.top then if self.border.top then
@@ -313,7 +329,7 @@ function Renderer:draw(backdropCanvas)
if element.animation then if element.animation then
local anim = element.animation:interpolate() local anim = element.animation:interpolate()
if anim.opacity then if anim.opacity then
drawBackgroundColor = Color.new(self.backgroundColor.r, self.backgroundColor.g, self.backgroundColor.b, anim.opacity) drawBackgroundColor = self._Color.new(self.backgroundColor.r, self.backgroundColor.g, self.backgroundColor.b, anim.opacity)
end end
end end
@@ -325,7 +341,7 @@ function Renderer:draw(backdropCanvas)
if self.backdropBlur and self.backdropBlur.intensity > 0 and backdropCanvas then if self.backdropBlur and self.backdropBlur.intensity > 0 and backdropCanvas then
local blurInstance = self:getBlurInstance() local blurInstance = self:getBlurInstance()
if blurInstance then if blurInstance then
Blur.applyBackdrop(blurInstance, self.backdropBlur.intensity, element.x, element.y, borderBoxWidth, borderBoxHeight, backdropCanvas) self._Blur.applyBackdrop(blurInstance, self.backdropBlur.intensity, element.x, element.y, borderBoxWidth, borderBoxHeight, backdropCanvas)
end end
end end
@@ -366,7 +382,7 @@ function Renderer:getFont(element)
end end
end end
return FONT_CACHE.getFont(element.textSize, fontPath) return self._FONT_CACHE.getFont(element.textSize, fontPath)
end end
--- Wrap a line of text based on element's textWrap mode --- Wrap a line of text based on element's textWrap mode
@@ -591,9 +607,9 @@ function Renderer:drawText(element)
end end
if displayText and displayText ~= "" then if displayText and displayText ~= "" then
local textColor = isPlaceholder and Color.new(element.textColor.r * 0.5, element.textColor.g * 0.5, element.textColor.b * 0.5, element.textColor.a * 0.5) local textColor = isPlaceholder and self._Color.new(element.textColor.r * 0.5, element.textColor.g * 0.5, element.textColor.b * 0.5, element.textColor.a * 0.5)
or element.textColor or element.textColor
local textColorWithOpacity = Color.new(textColor.r, textColor.g, textColor.b, textColor.a * self.opacity) local textColorWithOpacity = self._Color.new(textColor.r, textColor.g, textColor.b, textColor.a * self.opacity)
love.graphics.setColor(textColorWithOpacity:toRGBA()) love.graphics.setColor(textColorWithOpacity:toRGBA())
local origFont = love.graphics.getFont() local origFont = love.graphics.getFont()
@@ -602,7 +618,7 @@ function Renderer:drawText(element)
local fontPath = nil local fontPath = nil
if element.fontFamily then if element.fontFamily then
-- Check if fontFamily is a theme font name -- Check if fontFamily is a theme font name
local themeToUse = element.theme and Theme.get(element.theme) or Theme.getActive() local themeToUse = element.theme and self._Theme.get(element.theme) or self._Theme.getActive()
if themeToUse and themeToUse.fonts and themeToUse.fonts[element.fontFamily] then if themeToUse and themeToUse.fonts and themeToUse.fonts[element.fontFamily] then
fontPath = themeToUse.fonts[element.fontFamily] fontPath = themeToUse.fonts[element.fontFamily]
else else
@@ -611,14 +627,14 @@ function Renderer:drawText(element)
end end
elseif element.themeComponent then elseif element.themeComponent then
-- If using themeComponent but no fontFamily specified, check for default font in theme -- If using themeComponent but no fontFamily specified, check for default font in theme
local themeToUse = element.theme and Theme.get(element.theme) or Theme.getActive() local themeToUse = element.theme and self._Theme.get(element.theme) or self._Theme.getActive()
if themeToUse and themeToUse.fonts and themeToUse.fonts.default then if themeToUse and themeToUse.fonts and themeToUse.fonts.default then
fontPath = themeToUse.fonts.default fontPath = themeToUse.fonts.default
end end
end end
-- Use cached font instead of creating new one every frame -- Use cached font instead of creating new one every frame
local font = FONT_CACHE.get(element.textSize, fontPath) local font = self._FONT_CACHE.get(element.textSize, fontPath)
love.graphics.setFont(font) love.graphics.setFont(font)
end end
local font = love.graphics.getFont() local font = love.graphics.getFont()
@@ -652,11 +668,11 @@ function Renderer:drawText(element)
if element.textWrap and (element.textWrap == "word" or element.textWrap == "char" or element.textWrap == true) then if element.textWrap and (element.textWrap == "word" or element.textWrap == "char" or element.textWrap == true) then
-- Use printf for wrapped text -- Use printf for wrapped text
local align = "left" local align = "left"
if element.textAlign == TextAlign.CENTER then if element.textAlign == self._TextAlign.CENTER then
align = "center" align = "center"
elseif element.textAlign == TextAlign.END then elseif element.textAlign == self._TextAlign.END then
align = "right" align = "right"
elseif element.textAlign == TextAlign.JUSTIFY then elseif element.textAlign == self._TextAlign.JUSTIFY then
align = "justify" align = "justify"
end end
@@ -667,16 +683,16 @@ function Renderer:drawText(element)
love.graphics.printf(displayText, tx, ty, textAreaWidth, align) love.graphics.printf(displayText, tx, ty, textAreaWidth, align)
else else
-- Use regular print for non-wrapped text -- Use regular print for non-wrapped text
if element.textAlign == TextAlign.START then if element.textAlign == self._TextAlign.START then
tx = contentX tx = contentX
ty = contentY ty = contentY
elseif element.textAlign == TextAlign.CENTER then elseif element.textAlign == self._TextAlign.CENTER then
tx = contentX + (textAreaWidth - textWidth) / 2 tx = contentX + (textAreaWidth - textWidth) / 2
ty = contentY + (textAreaHeight - textHeight) / 2 ty = contentY + (textAreaHeight - textHeight) / 2
elseif element.textAlign == TextAlign.END then elseif element.textAlign == self._TextAlign.END then
tx = contentX + textAreaWidth - textWidth - 10 tx = contentX + textAreaWidth - textWidth - 10
ty = contentY + textAreaHeight - textHeight - 10 ty = contentY + textAreaHeight - textHeight - 10
elseif element.textAlign == TextAlign.JUSTIFY then elseif element.textAlign == self._TextAlign.JUSTIFY then
--- need to figure out spreading --- need to figure out spreading
tx = contentX tx = contentX
ty = contentY ty = contentY
@@ -703,7 +719,7 @@ function Renderer:drawText(element)
-- Draw cursor for focused editable elements (even if text is empty) -- Draw cursor for focused editable elements (even if text is empty)
if element._textEditor and element._textEditor:isFocused() and element._textEditor._cursorVisible then if element._textEditor and element._textEditor:isFocused() and element._textEditor._cursorVisible then
local cursorColor = element.cursorColor or element.textColor local cursorColor = element.cursorColor or element.textColor
local cursorWithOpacity = Color.new(cursorColor.r, cursorColor.g, cursorColor.b, cursorColor.a * self.opacity) local cursorWithOpacity = self._Color.new(cursorColor.r, cursorColor.g, cursorColor.b, cursorColor.a * self.opacity)
love.graphics.setColor(cursorWithOpacity:toRGBA()) love.graphics.setColor(cursorWithOpacity:toRGBA())
-- Calculate cursor position using TextEditor method -- Calculate cursor position using TextEditor method
@@ -734,8 +750,8 @@ function Renderer:drawText(element)
-- Draw selection highlight for editable elements -- Draw selection highlight for editable elements
if element._textEditor and element._textEditor:isFocused() and element._textEditor:hasSelection() and element.text and element.text ~= "" then if element._textEditor and element._textEditor:isFocused() and element._textEditor:hasSelection() and element.text and element.text ~= "" then
local selStart, selEnd = element._textEditor:getSelection() local selStart, selEnd = element._textEditor:getSelection()
local selectionColor = element.selectionColor or Color.new(0.3, 0.5, 0.8, 0.5) local selectionColor = element.selectionColor or self._Color.new(0.3, 0.5, 0.8, 0.5)
local selectionWithOpacity = Color.new(selectionColor.r, selectionColor.g, selectionColor.b, selectionColor.a * self.opacity) local selectionWithOpacity = self._Color.new(selectionColor.r, selectionColor.g, selectionColor.b, selectionColor.a * self.opacity)
-- Get selection rectangles from TextEditor -- Get selection rectangles from TextEditor
local selectionRects = element._textEditor:_getSelectionRects(selStart, selEnd) local selectionRects = element._textEditor:_getSelectionRects(selStart, selEnd)
@@ -774,14 +790,14 @@ function Renderer:drawText(element)
if element.textSize then if element.textSize then
local fontPath = nil local fontPath = nil
if element.fontFamily then if element.fontFamily then
local themeToUse = element.theme and Theme.get(element.theme) or Theme.getActive() local themeToUse = element.theme and self._Theme.get(element.theme) or self._Theme.getActive()
if themeToUse and themeToUse.fonts and themeToUse.fonts[element.fontFamily] then if themeToUse and themeToUse.fonts and themeToUse.fonts[element.fontFamily] then
fontPath = themeToUse.fonts[element.fontFamily] fontPath = themeToUse.fonts[element.fontFamily]
else else
fontPath = element.fontFamily fontPath = element.fontFamily
end end
end end
local font = FONT_CACHE.get(element.textSize, fontPath) local font = self._FONT_CACHE.get(element.textSize, fontPath)
love.graphics.setFont(font) love.graphics.setFont(font)
end end
@@ -802,7 +818,7 @@ function Renderer:drawText(element)
-- Draw cursor -- Draw cursor
local cursorColor = element.cursorColor or element.textColor local cursorColor = element.cursorColor or element.textColor
local cursorWithOpacity = Color.new(cursorColor.r, cursorColor.g, cursorColor.b, cursorColor.a * self.opacity) local cursorWithOpacity = self._Color.new(cursorColor.r, cursorColor.g, cursorColor.b, cursorColor.a * self.opacity)
love.graphics.setColor(cursorWithOpacity:toRGBA()) love.graphics.setColor(cursorWithOpacity:toRGBA())
love.graphics.rectangle("fill", contentX, contentY, 2, textHeight) love.graphics.rectangle("fill", contentX, contentY, 2, textHeight)
@@ -832,10 +848,10 @@ function Renderer:drawScrollbars(element, x, y, w, h, dims)
local thumbColor = element.scrollbarColor local thumbColor = element.scrollbarColor
if element._scrollbarDragging and element._hoveredScrollbar == "vertical" then if element._scrollbarDragging and element._hoveredScrollbar == "vertical" then
-- Active state: brighter -- Active state: brighter
thumbColor = Color.new(math.min(1, thumbColor.r * 1.4), math.min(1, thumbColor.g * 1.4), math.min(1, thumbColor.b * 1.4), thumbColor.a) thumbColor = self._Color.new(math.min(1, thumbColor.r * 1.4), math.min(1, thumbColor.g * 1.4), math.min(1, thumbColor.b * 1.4), thumbColor.a)
elseif element._scrollbarHoveredVertical then elseif element._scrollbarHoveredVertical then
-- Hover state: slightly brighter -- Hover state: slightly brighter
thumbColor = Color.new(math.min(1, thumbColor.r * 1.2), math.min(1, thumbColor.g * 1.2), math.min(1, thumbColor.b * 1.2), thumbColor.a) thumbColor = self._Color.new(math.min(1, thumbColor.r * 1.2), math.min(1, thumbColor.g * 1.2), math.min(1, thumbColor.b * 1.2), thumbColor.a)
end end
-- Draw track -- Draw track
@@ -859,10 +875,10 @@ function Renderer:drawScrollbars(element, x, y, w, h, dims)
local thumbColor = element.scrollbarColor local thumbColor = element.scrollbarColor
if element._scrollbarDragging and element._hoveredScrollbar == "horizontal" then if element._scrollbarDragging and element._hoveredScrollbar == "horizontal" then
-- Active state: brighter -- Active state: brighter
thumbColor = Color.new(math.min(1, thumbColor.r * 1.4), math.min(1, thumbColor.g * 1.4), math.min(1, thumbColor.b * 1.4), thumbColor.a) thumbColor = self._Color.new(math.min(1, thumbColor.r * 1.4), math.min(1, thumbColor.g * 1.4), math.min(1, thumbColor.b * 1.4), thumbColor.a)
elseif element._scrollbarHoveredHorizontal then elseif element._scrollbarHoveredHorizontal then
-- Hover state: slightly brighter -- Hover state: slightly brighter
thumbColor = Color.new(math.min(1, thumbColor.r * 1.2), math.min(1, thumbColor.g * 1.2), math.min(1, thumbColor.b * 1.2), thumbColor.a) thumbColor = self._Color.new(math.min(1, thumbColor.r * 1.2), math.min(1, thumbColor.g * 1.2), math.min(1, thumbColor.b * 1.2), thumbColor.a)
end end
-- Draw track -- Draw track
@@ -885,7 +901,7 @@ end
---@param borderBoxHeight number Border box height ---@param borderBoxHeight number Border box height
function Renderer:drawPressedState(x, y, borderBoxWidth, borderBoxHeight) function Renderer:drawPressedState(x, y, borderBoxWidth, borderBoxHeight)
love.graphics.setColor(0.5, 0.5, 0.5, 0.3 * self.opacity) -- Semi-transparent gray for pressed state with opacity love.graphics.setColor(0.5, 0.5, 0.5, 0.3 * self.opacity) -- Semi-transparent gray for pressed state with opacity
RoundedRect.draw("fill", x, y, borderBoxWidth, borderBoxHeight, self.cornerRadius) self._RoundedRect.draw("fill", x, y, borderBoxWidth, borderBoxHeight, self.cornerRadius)
end end
--- Cleanup renderer resources --- Cleanup renderer resources

View File

@@ -1,14 +1,9 @@
--- ScrollManager.lua --- ScrollManager.lua
--- Handles scrolling, overflow detection, and scrollbar rendering/interaction for Elements --- Handles scrolling, overflow detection, and scrollbar rendering/interaction for Elements
--- Extracted from Element.lua as part of element-refactor-modularization task 05 --- Extracted from Element.lua as part of element-refactor-modularization task 05
---
-- Setup module path for relative requires --- Dependencies (must be injected via deps parameter):
local modulePath = (...):match("(.-)[^%.]+$") --- - Color: Color module for creating color instances
local function req(name)
return require(modulePath .. name)
end
local Color = req("Color")
---@class ScrollManager ---@class ScrollManager
---@field overflow string -- "visible"|"hidden"|"auto"|"scroll" ---@field overflow string -- "visible"|"hidden"|"auto"|"scroll"
@@ -41,10 +36,19 @@ ScrollManager.__index = ScrollManager
--- Create a new ScrollManager instance --- Create a new ScrollManager instance
---@param config table Configuration options ---@param config table Configuration options
---@param deps table Dependencies {Color: Color module}
---@return ScrollManager ---@return ScrollManager
function ScrollManager.new(config) function ScrollManager.new(config, deps)
-- Pure DI: Dependencies must be injected
assert(deps, "ScrollManager.new: deps parameter is required")
assert(deps.Color, "ScrollManager.new: deps.Color is required")
local Color = deps.Color
local self = setmetatable({}, ScrollManager) local self = setmetatable({}, ScrollManager)
-- Store dependency for instance methods
self._Color = Color
-- Configuration -- Configuration
self.overflow = config.overflow or "hidden" self.overflow = config.overflow or "hidden"
self.overflowX = config.overflowX self.overflowX = config.overflowX

View File

@@ -9,6 +9,12 @@
-- - Focus management -- - Focus management
-- - Keyboard input handling -- - Keyboard input handling
-- - Text rendering (cursor, selection highlights) -- - Text rendering (cursor, selection highlights)
---
--- Dependencies (must be injected via deps parameter):
--- - GuiState: GUI state manager
--- - StateManager: State persistence for immediate mode
--- - Color: Color utility class (reserved for future use)
--- - utils: Utility functions (FONT_CACHE, getModifiers)
-- Setup module path for relative requires -- Setup module path for relative requires
local modulePath = (...):match("(.-)[^%.]+$") local modulePath = (...):match("(.-)[^%.]+$")
@@ -16,16 +22,6 @@ local function req(name)
return require(modulePath .. name) return require(modulePath .. name)
end end
-- Module dependencies
local GuiState = req("GuiState")
local StateManager = req("StateManager")
local Color = req("Color")
local utils = req("utils")
-- Extract utilities
local FONT_CACHE = utils.FONT_CACHE
local getModifiers = utils.getModifiers
-- UTF-8 support -- UTF-8 support
local utf8 = utf8 or require("utf8") local utf8 = utf8 or require("utf8")
@@ -51,10 +47,25 @@ TextEditor.__index = TextEditor
---Create a new TextEditor instance ---Create a new TextEditor instance
---@param config TextEditorConfig ---@param config TextEditorConfig
---@param deps table Dependencies {GuiState, StateManager, Color, utils}
---@return table TextEditor instance ---@return table TextEditor instance
function TextEditor.new(config) function TextEditor.new(config, deps)
-- Pure DI: Dependencies must be injected
assert(deps, "TextEditor.new: deps parameter is required")
assert(deps.GuiState, "TextEditor.new: deps.GuiState is required")
assert(deps.StateManager, "TextEditor.new: deps.StateManager is required")
assert(deps.Color, "TextEditor.new: deps.Color is required")
assert(deps.utils, "TextEditor.new: deps.utils is required")
local self = setmetatable({}, TextEditor) local self = setmetatable({}, TextEditor)
-- Store dependencies
self._GuiState = deps.GuiState
self._StateManager = deps.StateManager
self._Color = deps.Color
self._FONT_CACHE = deps.utils.FONT_CACHE
self._getModifiers = deps.utils.getModifiers
-- Store configuration -- Store configuration
self.editable = config.editable or false self.editable = config.editable or false
self.multiline = config.multiline or false self.multiline = config.multiline or false
@@ -117,12 +128,12 @@ function TextEditor:initialize(element)
self._element = element self._element = element
-- Restore state from StateManager if in immediate mode -- Restore state from StateManager if in immediate mode
if element._stateId and GuiState._immediateMode then if element._stateId and self._GuiState._immediateMode then
local state = StateManager.getState(element._stateId) local state = self._StateManager.getState(element._stateId)
if state then if state then
if state._focused then if state._focused then
self._focused = true self._focused = true
GuiState._focusedElement = element self._GuiState._focusedElement = element
end end
if state._textBuffer and state._textBuffer ~= "" then if state._textBuffer and state._textBuffer ~= "" then
self._textBuffer = state._textBuffer self._textBuffer = state._textBuffer
@@ -912,16 +923,16 @@ end
---Focus this element for keyboard input ---Focus this element for keyboard input
function TextEditor:focus() function TextEditor:focus()
if GuiState._focusedElement and GuiState._focusedElement ~= self._element then if self._GuiState._focusedElement and self._GuiState._focusedElement ~= self._element then
-- Blur the previously focused element's text editor if it has one -- Blur the previously focused element's text editor if it has one
if GuiState._focusedElement._textEditor then if self._GuiState._focusedElement._textEditor then
GuiState._focusedElement._textEditor:blur() self._GuiState._focusedElement._textEditor:blur()
end end
end end
self._focused = true self._focused = true
if self._element then if self._element then
GuiState._focusedElement = self._element self._GuiState._focusedElement = self._element
end end
self:_resetCursorBlink() self:_resetCursorBlink()
@@ -943,8 +954,8 @@ end
function TextEditor:blur() function TextEditor:blur()
self._focused = false self._focused = false
if self._element and GuiState._focusedElement == self._element then if self._element and self._GuiState._focusedElement == self._element then
GuiState._focusedElement = nil self._GuiState._focusedElement = nil
end end
if self.onBlur and self._element then if self.onBlur and self._element then
@@ -1006,7 +1017,7 @@ function TextEditor:handleKeyPress(key, scancode, isrepeat)
return return
end end
local modifiers = getModifiers() local modifiers = self._getModifiers()
local ctrl = modifiers.ctrl or modifiers.super local ctrl = modifiers.ctrl or modifiers.super
-- Handle cursor movement with selection -- Handle cursor movement with selection
@@ -1538,11 +1549,11 @@ end
---Save state to StateManager (for immediate mode) ---Save state to StateManager (for immediate mode)
function TextEditor:_saveState() function TextEditor:_saveState()
if not self._element or not self._element._stateId or not GuiState._immediateMode then if not self._element or not self._element._stateId or not self._GuiState._immediateMode then
return return
end end
StateManager.updateState(self._element._stateId, { self._StateManager.updateState(self._element._stateId, {
_focused = self._focused, _focused = self._focused,
_textBuffer = self._textBuffer, _textBuffer = self._textBuffer,
_cursorPosition = self._cursorPosition, _cursorPosition = self._cursorPosition,

View File

@@ -1,14 +1,9 @@
--- ThemeManager.lua --- ThemeManager.lua
--- Manages theme application, state transitions, and property resolution for Elements --- Manages theme application, state transitions, and property resolution for Elements
--- Extracted from Element.lua as part of element-refactor-modularization task 06 --- Extracted from Element.lua as part of element-refactor-modularization task 06
---
-- Setup module path for relative requires --- Dependencies (must be injected via deps parameter):
local modulePath = (...):match("(.-)[^%.]+$") --- - Theme: Theme module for loading and accessing themes
local function req(name)
return require(modulePath .. name)
end
local Theme = req("Theme")
---@class ThemeManager ---@class ThemeManager
---@field theme string? -- Theme name to use ---@field theme string? -- Theme name to use
@@ -25,10 +20,19 @@ ThemeManager.__index = ThemeManager
--- Create new ThemeManager instance --- Create new ThemeManager instance
---@param config table Configuration options ---@param config table Configuration options
---@param deps table Dependencies {Theme: Theme module}
---@return ThemeManager ---@return ThemeManager
function ThemeManager.new(config) function ThemeManager.new(config, deps)
-- Pure DI: Dependencies must be injected
assert(deps, "ThemeManager.new: deps parameter is required")
assert(deps.Theme, "ThemeManager.new: deps.Theme is required")
local Theme = deps.Theme
local self = setmetatable({}, ThemeManager) local self = setmetatable({}, ThemeManager)
-- Store dependency for instance methods
self._Theme = Theme
-- Theme configuration -- Theme configuration
self.theme = config.theme self.theme = config.theme
self.themeComponent = config.themeComponent self.themeComponent = config.themeComponent
@@ -103,9 +107,9 @@ end
---@return table? The theme object or nil ---@return table? The theme object or nil
function ThemeManager:getTheme() function ThemeManager:getTheme()
if self.theme then if self.theme then
return Theme.get(self.theme) return self._Theme.get(self.theme)
end end
return Theme.getActive() return self._Theme.getActive()
end end
--- Get the component definition from the theme --- Get the component definition from the theme