From b886085d3e2c5a685509c45828369507c3dd3ce3 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Wed, 12 Nov 2025 23:30:29 -0500 Subject: [PATCH] change to DI --- modules/Color.lua | 1 - modules/Element.lua | 41 ++++++----- modules/EventHandler.lua | 31 ++++++--- modules/LayoutEngine.lua | 134 +++++++++++++++++++----------------- modules/Renderer.lua | 138 +++++++++++++++++++++----------------- modules/ScrollManager.lua | 22 +++--- modules/TextEditor.lua | 57 +++++++++------- modules/ThemeManager.lua | 26 ++++--- 8 files changed, 258 insertions(+), 192 deletions(-) diff --git a/modules/Color.lua b/modules/Color.lua index 0bfb141..6ecc2ed 100644 --- a/modules/Color.lua +++ b/modules/Color.lua @@ -1,5 +1,4 @@ --[[ -Color.lua - Color utility class for FlexLove Provides color handling with RGB/RGBA support and hex string conversion ]] diff --git a/modules/Element.lua b/modules/Element.lua index 2c7fa03..956c038 100644 --- a/modules/Element.lua +++ b/modules/Element.lua @@ -1,8 +1,3 @@ --- ==================== --- Element Object --- ==================== - --- Setup module path for relative requires local modulePath = (...):match("(.-)[^%.]+$") local function req(name) return require(modulePath .. name) @@ -204,6 +199,9 @@ function Element.new(props) -- Initialize EventHandler for event processing self._eventHandler = EventHandler.new({ onEvent = self.onEvent, + }, { + InputEvent = InputEvent, + GuiState = GuiState, }) self._eventHandler:initialize(self) @@ -219,6 +217,8 @@ function Element.new(props) disableHighlight = props.disableHighlight, scaleCorners = props.scaleCorners, scalingAlgorithm = props.scalingAlgorithm, + }, { + Theme = Theme, }) self._themeManager:initialize(self) @@ -320,6 +320,11 @@ function Element.new(props) onTextInput = props.onTextInput, onTextChange = props.onTextChange, onEnter = props.onEnter, + }, { + GuiState = GuiState, + StateManager = StateManager, + Color = Color, + utils = utils, }) -- Initialize will be called after self is fully constructed end @@ -425,9 +430,15 @@ function Element.new(props) objectFit = self.objectFit, objectPosition = self.objectPosition, imageOpacity = self.imageOpacity, - contentBlur = self.contentBlur, - backdropBlur = self.backdropBlur, - _themeState = self._themeState, + }, { + Color = Color, + RoundedRect = RoundedRect, + NinePatch = NinePatch, + ImageRenderer = ImageRenderer, + ImageCache = ImageCache, + Theme = Theme, + Blur = Blur, + utils = utils, }) self._renderer:initialize(self) @@ -1135,6 +1146,9 @@ function Element.new(props) gridColumns = self.gridColumns, columnGap = self.columnGap, rowGap = self.rowGap, + }, { + utils = utils, + Grid = Grid, }) -- Initialize immediately so it can be used for auto-sizing calculations self._layoutEngine:initialize(self) @@ -1158,6 +1172,8 @@ function Element.new(props) hideScrollbars = props.hideScrollbars, _scrollX = props._scrollX, _scrollY = props._scrollY, + }, { + utils = utils, }) self._scrollManager:initialize(self) @@ -1958,14 +1974,9 @@ function Element:update(dt) if self.themeComponent then -- Check if any button is pressed via EventHandler local anyPressed = self._eventHandler:isAnyButtonPressed() - + -- 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) -- Update state (in StateManager if in immediate mode, otherwise locally) if self._stateId and Gui._immediateMode then diff --git a/modules/EventHandler.lua b/modules/EventHandler.lua index b16b9d7..8b1cb4f 100644 --- a/modules/EventHandler.lua +++ b/modules/EventHandler.lua @@ -3,15 +3,16 @@ -- ==================== -- Handles all user input events (mouse, keyboard, touch) for UI elements -- 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 function req(name) return require(modulePath .. name) end -local InputEvent = req("InputEvent") -local GuiState = req("GuiState") - -- Get keyboard modifiers helper local function getModifiers() return { @@ -28,12 +29,22 @@ EventHandler.__index = EventHandler --- Create a new EventHandler instance ---@param config table Configuration options +---@param deps table Dependencies {InputEvent, GuiState} ---@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 {} local self = setmetatable({}, EventHandler) + -- Store dependencies + self._InputEvent = deps.InputEvent + self._GuiState = deps.GuiState + -- Event callback self.onEvent = config.onEvent @@ -183,7 +194,7 @@ function EventHandler:_handleMousePress(mx, my, button) -- Fire press event if self.onEvent then local modifiers = getModifiers() - local pressEvent = InputEvent.new({ + local pressEvent = self._InputEvent.new({ type = "press", button = button, x = mx, @@ -222,14 +233,14 @@ function EventHandler:_handleMouseDrag(mx, my, button, isHovering) local lastX = self._lastMouseX[button] or mx 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 if self.onEvent and isHovering then local modifiers = getModifiers() local dx = mx - self._dragStartX[button] local dy = my - self._dragStartY[button] - local dragEvent = InputEvent.new({ + local dragEvent = self._InputEvent.new({ type = "drag", button = button, x = mx, @@ -289,7 +300,7 @@ function EventHandler:_handleMouseRelease(mx, my, button) -- Fire click event if self.onEvent then - local clickEvent = InputEvent.new({ + local clickEvent = self._InputEvent.new({ type = eventType, button = button, x = mx, @@ -331,7 +342,7 @@ function EventHandler:_handleMouseRelease(mx, my, button) -- Fire release event if self.onEvent then - local releaseEvent = InputEvent.new({ + local releaseEvent = self._InputEvent.new({ type = "release", button = button, x = mx, @@ -363,7 +374,7 @@ function EventHandler:processTouchEvents() self._touchPressed[id] = true elseif self._touchPressed[id] then -- Create touch event (treat as left click) - local touchEvent = InputEvent.new({ + local touchEvent = self._InputEvent.new({ type = "click", button = 1, x = tx, diff --git a/modules/LayoutEngine.lua b/modules/LayoutEngine.lua index 146cc18..4b3d5ce 100644 --- a/modules/LayoutEngine.lua +++ b/modules/LayoutEngine.lua @@ -6,26 +6,10 @@ -- - Grid layout delegation -- - Auto-sizing calculations -- - CSS positioning offsets - --- Setup module path for relative requires -local modulePath = (...):match("(.-)[^%.]+$") -local function req(name) - 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 +--- +--- Dependencies (must be injected via deps parameter): +--- - utils: Utility functions and enums +--- - Grid: Grid layout module ---@class LayoutEngine ---@field element Element Reference to the parent element @@ -58,10 +42,36 @@ LayoutEngine.__index = LayoutEngine --- Create a new LayoutEngine instance ---@param props LayoutEngineProps +---@param deps table Dependencies {utils, Grid} ---@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) + -- 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 self.positioning = props.positioning or Positioning.FLEX 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 -- Flex/grid children ignore positioning offsets as they participate in layout - local isFlexChild = child.positioning == Positioning.FLEX - or child.positioning == Positioning.GRID - or (child.positioning == Positioning.ABSOLUTE and not child._explicitlyAbsolute) + local isFlexChild = child.positioning == self._Positioning.FLEX + or child.positioning == self._Positioning.GRID + or (child.positioning == self._Positioning.ABSOLUTE and not child._explicitlyAbsolute) if not isFlexChild then -- Apply absolute positioning for explicitly absolute children @@ -140,7 +150,7 @@ end function LayoutEngine:layoutChildren() 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, -- but they should still apply CSS positioning offsets to their children for _, child in ipairs(element.children) do @@ -152,8 +162,8 @@ function LayoutEngine:layoutChildren() end -- Handle grid layout - if self.positioning == Positioning.GRID then - Grid.layoutGridItems(element) + if self.positioning == self._Positioning.GRID then + self._Grid.layoutGridItems(element) return end @@ -166,7 +176,7 @@ function LayoutEngine:layoutChildren() -- Get flex children (children that participate in flex layout) local flexChildren = {} 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 table.insert(flexChildren, child) end @@ -184,12 +194,12 @@ function LayoutEngine:layoutChildren() for _, child in ipairs(element.children) do -- 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 local childBorderBoxWidth = child:getBorderBoxWidth() 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 -- Check for left positioning (reserves space at main axis start) 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) local availableMainSize = 0 local availableCrossSize = 0 - if self.flexDirection == FlexDirection.HORIZONTAL then + if self.flexDirection == self._FlexDirection.HORIZONTAL then availableMainSize = element.width - reservedMainStart - reservedMainEnd availableCrossSize = element.height - reservedCrossStart - reservedCrossEnd else @@ -252,7 +262,7 @@ function LayoutEngine:layoutChildren() -- Handle flex wrap: create lines of children local lines = {} - if self.flexWrap == FlexWrap.NOWRAP then + if self.flexWrap == self._FlexWrap.NOWRAP then -- All children go on one line lines[1] = flexChildren else @@ -265,7 +275,7 @@ function LayoutEngine:layoutChildren() -- Include margins in size calculations local childMainSize = 0 local childMainMargin = 0 - if self.flexDirection == FlexDirection.HORIZONTAL then + if self.flexDirection == self._FlexDirection.HORIZONTAL then childMainSize = child:getBorderBoxWidth() childMainMargin = child.margin.left + child.margin.right else @@ -296,7 +306,7 @@ function LayoutEngine:layoutChildren() end -- 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 = {} for i = #lines, 1, -1 do table.insert(reversedLines, lines[i]) @@ -316,7 +326,7 @@ function LayoutEngine:layoutChildren() -- Include margins in cross-axis size calculations local childCrossSize = 0 local childCrossMargin = 0 - if self.flexDirection == FlexDirection.HORIZONTAL then + if self.flexDirection == self._FlexDirection.HORIZONTAL then childCrossSize = child:getBorderBoxHeight() childCrossMargin = child.margin.top + child.margin.bottom else @@ -336,7 +346,7 @@ function LayoutEngine:layoutChildren() -- For single line layouts, CENTER, FLEX_END and STRETCH should use full cross size 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 lineHeights[1] = availableCrossSize totalLinesHeight = availableCrossSize @@ -351,22 +361,22 @@ function LayoutEngine:layoutChildren() local freeLineSpace = availableCrossSize - totalLinesHeight -- 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 - elseif self.alignContent == AlignContent.CENTER then + elseif self.alignContent == self._AlignContent.CENTER then lineStartPos = freeLineSpace / 2 - elseif self.alignContent == AlignContent.FLEX_END then + elseif self.alignContent == self._AlignContent.FLEX_END then lineStartPos = freeLineSpace - elseif self.alignContent == AlignContent.SPACE_BETWEEN then + elseif self.alignContent == self._AlignContent.SPACE_BETWEEN then lineStartPos = 0 if #lines > 1 then lineSpacing = self.gap + (freeLineSpace / (#lines - 1)) end - elseif self.alignContent == AlignContent.SPACE_AROUND then + elseif self.alignContent == self._AlignContent.SPACE_AROUND then local spaceAroundEach = freeLineSpace / #lines lineStartPos = spaceAroundEach / 2 lineSpacing = self.gap + spaceAroundEach - elseif self.alignContent == AlignContent.STRETCH then + elseif self.alignContent == self._AlignContent.STRETCH then lineStartPos = 0 if #lines > 1 and freeLineSpace > 0 then lineSpacing = self.gap + (freeLineSpace / #lines) @@ -388,7 +398,7 @@ function LayoutEngine:layoutChildren() -- BORDER-BOX MODEL: Use border-box dimensions for layout calculations local totalChildrenSize = 0 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 else totalChildrenSize = totalChildrenSize + child:getBorderBoxHeight() + child.margin.top + child.margin.bottom @@ -403,22 +413,22 @@ function LayoutEngine:layoutChildren() local startPos = 0 local itemSpacing = self.gap - if self.justifyContent == JustifyContent.FLEX_START then + if self.justifyContent == self._JustifyContent.FLEX_START then startPos = 0 - elseif self.justifyContent == JustifyContent.CENTER then + elseif self.justifyContent == self._JustifyContent.CENTER then startPos = freeSpace / 2 - elseif self.justifyContent == JustifyContent.FLEX_END then + elseif self.justifyContent == self._JustifyContent.FLEX_END then startPos = freeSpace - elseif self.justifyContent == JustifyContent.SPACE_BETWEEN then + elseif self.justifyContent == self._JustifyContent.SPACE_BETWEEN then startPos = 0 if #line > 1 then itemSpacing = self.gap + (freeSpace / (#line - 1)) end - elseif self.justifyContent == JustifyContent.SPACE_AROUND then + elseif self.justifyContent == self._JustifyContent.SPACE_AROUND then local spaceAroundEach = freeSpace / #line startPos = spaceAroundEach / 2 itemSpacing = self.gap + spaceAroundEach - elseif self.justifyContent == JustifyContent.SPACE_EVENLY then + elseif self.justifyContent == self._JustifyContent.SPACE_EVENLY then local spaceBetween = freeSpace / (#line + 1) startPos = spaceBetween itemSpacing = self.gap + spaceBetween @@ -430,11 +440,11 @@ function LayoutEngine:layoutChildren() for _, child in ipairs(line) do -- Determine effective cross-axis alignment local effectiveAlign = child.alignSelf - if effectiveAlign == nil or effectiveAlign == AlignSelf.AUTO then + if effectiveAlign == nil or effectiveAlign == self._AlignSelf.AUTO then effectiveAlign = self.alignItems end - if self.flexDirection == FlexDirection.HORIZONTAL then + if self.flexDirection == self._FlexDirection.HORIZONTAL then -- Horizontal layout: main axis is X, cross axis is Y -- 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 @@ -444,13 +454,13 @@ function LayoutEngine:layoutChildren() local childBorderBoxHeight = child:getBorderBoxHeight() 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 - 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 - 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 - elseif effectiveAlign == AlignItems.STRETCH then + elseif effectiveAlign == self._AlignItems.STRETCH then -- STRETCH: Only apply if height was not explicitly set if child.autosizing and child.autosizing.height then -- 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 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 - 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 - 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 - elseif effectiveAlign == AlignItems.STRETCH then + elseif effectiveAlign == self._AlignItems.STRETCH then -- STRETCH: Only apply if width was not explicitly set if child.autosizing and child.autosizing.width then -- 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 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) self:applyPositioningOffsets(child) @@ -547,7 +557,7 @@ function LayoutEngine:calculateAutoWidth() -- For HORIZONTAL flex: sum children widths + gaps -- For VERTICAL flex: max of children widths - local isHorizontal = self.flexDirection == FlexDirection.HORIZONTAL + local isHorizontal = self.flexDirection == self._FlexDirection.HORIZONTAL local totalWidth = contentWidth local maxWidth = contentWidth local participatingChildren = 0 @@ -587,7 +597,7 @@ function LayoutEngine:calculateAutoHeight() -- For VERTICAL flex: sum children heights + gaps -- For HORIZONTAL flex: max of children heights - local isVertical = self.flexDirection == FlexDirection.VERTICAL + local isVertical = self.flexDirection == self._FlexDirection.VERTICAL local totalHeight = height local maxHeight = height local participatingChildren = 0 diff --git a/modules/Renderer.lua b/modules/Renderer.lua index 0117405..0972cab 100644 --- a/modules/Renderer.lua +++ b/modules/Renderer.lua @@ -4,37 +4,53 @@ -- -- This module is responsible for the visual presentation layer of Elements, -- delegating from Element's draw() method to keep rendering concerns separated. - --- Setup module path for relative requires -local modulePath = (...):match("(.-)[^%.]+$") -local function req(name) - return require(modulePath .. name) -end +--- +--- Dependencies (must be injected via deps parameter): +--- - Color: Color module for color manipulation +--- - RoundedRect: Rounded rectangle drawing module +--- - NinePatch: 9-patch rendering module +--- - ImageRenderer: Image rendering module +--- - ImageCache: Image caching module +--- - Theme: Theme management module +--- - Blur: Blur effects module +--- - utils: Utility functions (FONT_CACHE, enums) local 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 ---@param config table Configuration table with rendering properties +---@param deps table Dependencies {Color, RoundedRect, NinePatch, ImageRenderer, ImageCache, Theme, Blur, utils} ---@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) + -- 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) self._element = nil @@ -113,7 +129,7 @@ function Renderer:getBlurInstance() -- Create or reuse blur instance if not self._blurInstance or self._blurInstance.quality ~= quality then - self._blurInstance = Blur.new(quality) + self._blurInstance = self._Blur.new(quality) end return self._blurInstance @@ -132,14 +148,14 @@ end ---@param height number Height ---@param drawBackgroundColor table Background color (may have animation applied) function Renderer:_drawBackground(x, y, width, height, drawBackgroundColor) - local backgroundWithOpacity = Color.new( + local backgroundWithOpacity = self._Color.new( drawBackgroundColor.r, drawBackgroundColor.g, drawBackgroundColor.b, drawBackgroundColor.a * self.opacity ) 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 --- Draw image layer @@ -174,13 +190,13 @@ function Renderer:_drawImage(x, y, paddingLeft, paddingTop, contentWidth, conten if hasCornerRadius then -- Use stencil to clip image to rounded corners 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) love.graphics.setStencilTest("greater", 0) end -- 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 if hasCornerRadius then @@ -204,18 +220,18 @@ function Renderer:_drawTheme(x, y, borderBoxWidth, borderBoxHeight, scaleCorners local themeToUse = nil if self.theme then -- Element specifies a specific theme - load it if needed - if Theme.get(self.theme) then - themeToUse = Theme.get(self.theme) + if self._Theme.get(self.theme) then + themeToUse = self._Theme.get(self.theme) else -- Try to load the theme pcall(function() - Theme.load(self.theme) + self._Theme.load(self.theme) end) - themeToUse = Theme.get(self.theme) + themeToUse = self._Theme.get(self.theme) end else -- Use active theme - themeToUse = Theme.getActive() + themeToUse = self._Theme.getActive() end if not themeToUse then @@ -251,7 +267,7 @@ function Renderer:_drawTheme(x, y, borderBoxWidth, borderBoxHeight, scaleCorners if hasAllRegions then -- 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 @@ -262,7 +278,7 @@ end ---@param borderBoxWidth number Border box width ---@param borderBoxHeight number Border box height function Renderer:_drawBorders(x, y, borderBoxWidth, borderBoxHeight) - local borderColorWithOpacity = Color.new( + local borderColorWithOpacity = self._Color.new( self.borderColor.r, self.borderColor.g, self.borderColor.b, @@ -275,7 +291,7 @@ function Renderer:_drawBorders(x, y, borderBoxWidth, borderBoxHeight) if allBorders then -- 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 -- Draw individual borders (without rounded corners for partial borders) if self.border.top then @@ -313,7 +329,7 @@ function Renderer:draw(backdropCanvas) if element.animation then local anim = element.animation:interpolate() 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 @@ -325,7 +341,7 @@ function Renderer:draw(backdropCanvas) if self.backdropBlur and self.backdropBlur.intensity > 0 and backdropCanvas then local blurInstance = self:getBlurInstance() 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 @@ -366,7 +382,7 @@ function Renderer:getFont(element) end end - return FONT_CACHE.getFont(element.textSize, fontPath) + return self._FONT_CACHE.getFont(element.textSize, fontPath) end --- Wrap a line of text based on element's textWrap mode @@ -591,9 +607,9 @@ function Renderer:drawText(element) end 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 - 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()) local origFont = love.graphics.getFont() @@ -602,7 +618,7 @@ function Renderer:drawText(element) local fontPath = nil if element.fontFamily then -- 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 fontPath = themeToUse.fonts[element.fontFamily] else @@ -611,14 +627,14 @@ function Renderer:drawText(element) end elseif element.themeComponent then -- 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 fontPath = themeToUse.fonts.default end end -- 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) end 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 -- Use printf for wrapped text local align = "left" - if element.textAlign == TextAlign.CENTER then + if element.textAlign == self._TextAlign.CENTER then align = "center" - elseif element.textAlign == TextAlign.END then + elseif element.textAlign == self._TextAlign.END then align = "right" - elseif element.textAlign == TextAlign.JUSTIFY then + elseif element.textAlign == self._TextAlign.JUSTIFY then align = "justify" end @@ -667,16 +683,16 @@ function Renderer:drawText(element) love.graphics.printf(displayText, tx, ty, textAreaWidth, align) else -- Use regular print for non-wrapped text - if element.textAlign == TextAlign.START then + if element.textAlign == self._TextAlign.START then tx = contentX ty = contentY - elseif element.textAlign == TextAlign.CENTER then + elseif element.textAlign == self._TextAlign.CENTER then tx = contentX + (textAreaWidth - textWidth) / 2 ty = contentY + (textAreaHeight - textHeight) / 2 - elseif element.textAlign == TextAlign.END then + elseif element.textAlign == self._TextAlign.END then tx = contentX + textAreaWidth - textWidth - 10 ty = contentY + textAreaHeight - textHeight - 10 - elseif element.textAlign == TextAlign.JUSTIFY then + elseif element.textAlign == self._TextAlign.JUSTIFY then --- need to figure out spreading tx = contentX ty = contentY @@ -703,7 +719,7 @@ function Renderer:drawText(element) -- Draw cursor for focused editable elements (even if text is empty) if element._textEditor and element._textEditor:isFocused() and element._textEditor._cursorVisible then 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()) -- Calculate cursor position using TextEditor method @@ -734,8 +750,8 @@ function Renderer:drawText(element) -- Draw selection highlight for editable elements 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 selectionColor = element.selectionColor or 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 selectionColor = element.selectionColor or self._Color.new(0.3, 0.5, 0.8, 0.5) + local selectionWithOpacity = self._Color.new(selectionColor.r, selectionColor.g, selectionColor.b, selectionColor.a * self.opacity) -- Get selection rectangles from TextEditor local selectionRects = element._textEditor:_getSelectionRects(selStart, selEnd) @@ -774,14 +790,14 @@ function Renderer:drawText(element) if element.textSize then local fontPath = nil 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 fontPath = themeToUse.fonts[element.fontFamily] else fontPath = element.fontFamily end end - local font = FONT_CACHE.get(element.textSize, fontPath) + local font = self._FONT_CACHE.get(element.textSize, fontPath) love.graphics.setFont(font) end @@ -802,7 +818,7 @@ function Renderer:drawText(element) -- Draw cursor 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.rectangle("fill", contentX, contentY, 2, textHeight) @@ -832,10 +848,10 @@ function Renderer:drawScrollbars(element, x, y, w, h, dims) local thumbColor = element.scrollbarColor if element._scrollbarDragging and element._hoveredScrollbar == "vertical" then -- 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 -- 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 -- Draw track @@ -859,10 +875,10 @@ function Renderer:drawScrollbars(element, x, y, w, h, dims) local thumbColor = element.scrollbarColor if element._scrollbarDragging and element._hoveredScrollbar == "horizontal" then -- 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 -- 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 -- Draw track @@ -885,7 +901,7 @@ end ---@param borderBoxHeight number Border box height 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 - RoundedRect.draw("fill", x, y, borderBoxWidth, borderBoxHeight, self.cornerRadius) + self._RoundedRect.draw("fill", x, y, borderBoxWidth, borderBoxHeight, self.cornerRadius) end --- Cleanup renderer resources diff --git a/modules/ScrollManager.lua b/modules/ScrollManager.lua index 12941c8..d0070b3 100644 --- a/modules/ScrollManager.lua +++ b/modules/ScrollManager.lua @@ -1,14 +1,9 @@ --- ScrollManager.lua --- Handles scrolling, overflow detection, and scrollbar rendering/interaction for Elements --- Extracted from Element.lua as part of element-refactor-modularization task 05 - --- Setup module path for relative requires -local modulePath = (...):match("(.-)[^%.]+$") -local function req(name) - return require(modulePath .. name) -end - -local Color = req("Color") +--- +--- Dependencies (must be injected via deps parameter): +--- - Color: Color module for creating color instances ---@class ScrollManager ---@field overflow string -- "visible"|"hidden"|"auto"|"scroll" @@ -41,10 +36,19 @@ ScrollManager.__index = ScrollManager --- Create a new ScrollManager instance ---@param config table Configuration options +---@param deps table Dependencies {Color: Color module} ---@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) + -- Store dependency for instance methods + self._Color = Color + -- Configuration self.overflow = config.overflow or "hidden" self.overflowX = config.overflowX diff --git a/modules/TextEditor.lua b/modules/TextEditor.lua index 4cb44e7..e2d8716 100644 --- a/modules/TextEditor.lua +++ b/modules/TextEditor.lua @@ -9,6 +9,12 @@ -- - Focus management -- - Keyboard input handling -- - 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 local modulePath = (...):match("(.-)[^%.]+$") @@ -16,16 +22,6 @@ local function req(name) return require(modulePath .. name) 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 local utf8 = utf8 or require("utf8") @@ -51,10 +47,25 @@ TextEditor.__index = TextEditor ---Create a new TextEditor instance ---@param config TextEditorConfig +---@param deps table Dependencies {GuiState, StateManager, Color, utils} ---@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) + -- 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 self.editable = config.editable or false self.multiline = config.multiline or false @@ -117,12 +128,12 @@ function TextEditor:initialize(element) self._element = element -- Restore state from StateManager if in immediate mode - if element._stateId and GuiState._immediateMode then - local state = StateManager.getState(element._stateId) + if element._stateId and self._GuiState._immediateMode then + local state = self._StateManager.getState(element._stateId) if state then if state._focused then self._focused = true - GuiState._focusedElement = element + self._GuiState._focusedElement = element end if state._textBuffer and state._textBuffer ~= "" then self._textBuffer = state._textBuffer @@ -912,16 +923,16 @@ end ---Focus this element for keyboard input 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 - if GuiState._focusedElement._textEditor then - GuiState._focusedElement._textEditor:blur() + if self._GuiState._focusedElement._textEditor then + self._GuiState._focusedElement._textEditor:blur() end end self._focused = true if self._element then - GuiState._focusedElement = self._element + self._GuiState._focusedElement = self._element end self:_resetCursorBlink() @@ -943,8 +954,8 @@ end function TextEditor:blur() self._focused = false - if self._element and GuiState._focusedElement == self._element then - GuiState._focusedElement = nil + if self._element and self._GuiState._focusedElement == self._element then + self._GuiState._focusedElement = nil end if self.onBlur and self._element then @@ -1006,7 +1017,7 @@ function TextEditor:handleKeyPress(key, scancode, isrepeat) return end - local modifiers = getModifiers() + local modifiers = self._getModifiers() local ctrl = modifiers.ctrl or modifiers.super -- Handle cursor movement with selection @@ -1538,11 +1549,11 @@ end ---Save state to StateManager (for immediate mode) 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 end - StateManager.updateState(self._element._stateId, { + self._StateManager.updateState(self._element._stateId, { _focused = self._focused, _textBuffer = self._textBuffer, _cursorPosition = self._cursorPosition, diff --git a/modules/ThemeManager.lua b/modules/ThemeManager.lua index d362e09..520941f 100644 --- a/modules/ThemeManager.lua +++ b/modules/ThemeManager.lua @@ -1,14 +1,9 @@ --- ThemeManager.lua --- Manages theme application, state transitions, and property resolution for Elements --- Extracted from Element.lua as part of element-refactor-modularization task 06 - --- Setup module path for relative requires -local modulePath = (...):match("(.-)[^%.]+$") -local function req(name) - return require(modulePath .. name) -end - -local Theme = req("Theme") +--- +--- Dependencies (must be injected via deps parameter): +--- - Theme: Theme module for loading and accessing themes ---@class ThemeManager ---@field theme string? -- Theme name to use @@ -25,10 +20,19 @@ ThemeManager.__index = ThemeManager --- Create new ThemeManager instance ---@param config table Configuration options +---@param deps table Dependencies {Theme: Theme module} ---@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) + -- Store dependency for instance methods + self._Theme = Theme + -- Theme configuration self.theme = config.theme self.themeComponent = config.themeComponent @@ -103,9 +107,9 @@ end ---@return table? The theme object or nil function ThemeManager:getTheme() if self.theme then - return Theme.get(self.theme) + return self._Theme.get(self.theme) end - return Theme.getActive() + return self._Theme.getActive() end --- Get the component definition from the theme