From 712b3c40e93b363f7acf86f9ac586d714e257702 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Thu, 13 Nov 2025 00:06:09 -0500 Subject: [PATCH] cleanup --- modules/Animation.lua | 27 +----- modules/Blur.lua | 5 -- modules/Color.lua | 8 -- modules/EventHandler.lua | 128 +++++++++++++------------- modules/Grid.lua | 4 - modules/GuiState.lua | 5 -- modules/ImageCache.lua | 9 -- modules/ImageDataReader.lua | 9 -- modules/ImageRenderer.lua | 9 -- modules/ImageScaler.lua | 9 -- modules/InputEvent.lua | 4 - modules/LayoutEngine.lua | 13 --- modules/NinePatch.lua | 6 -- modules/NinePatchParser.lua | 9 -- modules/Renderer.lua | 125 ++++++++++---------------- modules/RoundedRect.lua | 5 -- modules/ScrollManager.lua | 161 ++++++++++++++++----------------- modules/StateManager.lua | 173 +++++++++++++++++++++++------------- modules/TextEditor.lua | 25 ------ modules/Theme.lua | 7 -- modules/ThemeManager.lua | 7 -- modules/Units.lua | 5 -- modules/types.lua | 127 ++++++++++++++++++++++++++ modules/utils.lua | 100 --------------------- 24 files changed, 423 insertions(+), 557 deletions(-) create mode 100644 modules/types.lua diff --git a/modules/Animation.lua b/modules/Animation.lua index cd8c958..1015d15 100644 --- a/modules/Animation.lua +++ b/modules/Animation.lua @@ -1,10 +1,3 @@ ----@class Animation ----@field duration number ----@field start {width?:number, height?:number, opacity?:number} ----@field final {width?:number, height?:number, opacity?:number} ----@field elapsed number ----@field transform table? ----@field transition table? --- Easing functions for animations local Easing = { linear = function(t) @@ -47,27 +40,15 @@ local Easing = { return t == 1 and 1 or 1 - math.pow(2, -10 * t) end, } - -local Animation = {} -Animation.__index = Animation - ----@class AnimationProps +---@class Animation ---@field duration number ---@field start {width?:number, height?:number, opacity?:number} ---@field final {width?:number, height?:number, opacity?:number} +---@field elapsed number ---@field transform table? ---@field transition table? -local AnimationProps = {} - ----@class TransformProps ----@field scale {x?:number, y?:number}? ----@field rotate number? ----@field translate {x?:number, y?:number}? ----@field skew {x?:number, y?:number}? - ----@class TransitionProps ----@field duration number? ----@field easing string? +local Animation = {} +Animation.__index = Animation ---@param props AnimationProps ---@return Animation diff --git a/modules/Blur.lua b/modules/Blur.lua index ef17ab2..9ff6e9e 100644 --- a/modules/Blur.lua +++ b/modules/Blur.lua @@ -1,8 +1,3 @@ ---[[ -FlexLove Blur Module -Fast Gaussian blur implementation with canvas caching -]] - local Blur = {} -- Canvas cache to avoid recreating canvases every frame diff --git a/modules/Color.lua b/modules/Color.lua index 6ecc2ed..256ba13 100644 --- a/modules/Color.lua +++ b/modules/Color.lua @@ -1,11 +1,3 @@ ---[[ -Provides color handling with RGB/RGBA support and hex string conversion -]] - --- ==================== --- Error Handling Utilities --- ==================== - --- Standardized error message formatter ---@param module string -- Module name (e.g., "Color", "Theme", "Units") ---@param message string -- Error message diff --git a/modules/EventHandler.lua b/modules/EventHandler.lua index 07f2563..6030945 100644 --- a/modules/EventHandler.lua +++ b/modules/EventHandler.lua @@ -1,19 +1,3 @@ --- ==================== --- Event Handler Module --- ==================== --- 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 - --- Get keyboard modifiers helper local function getModifiers() return { shift = love.keyboard.isDown("lshift") or love.keyboard.isDown("rshift"), @@ -33,42 +17,42 @@ EventHandler.__index = EventHandler ---@return EventHandler function EventHandler.new(config, deps) config = config or {} - + local self = setmetatable({}, EventHandler) - + -- Store dependencies self._InputEvent = deps.InputEvent self._GuiState = deps.GuiState - + -- Event callback self.onEvent = config.onEvent - + -- Mouse button state tracking {button -> boolean} self._pressed = config._pressed or {} - + -- Click detection state self._lastClickTime = config._lastClickTime self._lastClickButton = config._lastClickButton self._clickCount = config._clickCount or 0 - + -- Drag tracking per button {button -> position} self._dragStartX = config._dragStartX or {} self._dragStartY = config._dragStartY or {} self._lastMouseX = config._lastMouseX or {} self._lastMouseY = config._lastMouseY or {} - + -- Touch state tracking {touchId -> boolean} self._touchPressed = config._touchPressed or {} - + -- Hover state self._hovered = config._hovered or false - + -- Reference to parent element (set via initialize) self._element = nil - + -- Scrollbar press tracking flag self._scrollbarPressHandled = false - + return self end @@ -97,8 +81,10 @@ end --- Restore state from persistence (for immediate mode) ---@param state table State data function EventHandler:setState(state) - if not state then return end - + if not state then + return + end + self._pressed = state._pressed or {} self._lastClickTime = state._lastClickTime self._lastClickButton = state._lastClickButton @@ -119,9 +105,9 @@ function EventHandler:processMouseEvents(mx, my, isHovering, isActiveElement) if not self._element then return end - + local element = self._element - + -- Check if currently dragging (allows drag continuation even if occluded) local isDragging = false for _, button in ipairs({ 1, 2, 3 }) do @@ -130,17 +116,17 @@ function EventHandler:processMouseEvents(mx, my, isHovering, isActiveElement) break end end - + -- Can only process events if we have handler, element is enabled, and is active or dragging local canProcessEvents = (self.onEvent or element.editable) and not element.disabled and (isActiveElement or isDragging) - + if not canProcessEvents then return end - + -- Process all three mouse buttons local buttons = { 1, 2, 3 } -- left, right, middle - + for _, button in ipairs(buttons) do if isHovering or isDragging then if love.mouse.isDown(button) then @@ -172,10 +158,12 @@ end ---@param my number Mouse Y position ---@param button number Mouse button (1=left, 2=right, 3=middle) function EventHandler:_handleMousePress(mx, my, button) - if not self._element then return end - + if not self._element then + return + end + local element = self._element - + -- Check if press is on scrollbar first (skip if already handled) if button == 1 and not self._scrollbarPressHandled and element._handleScrollbarPress then if element:_handleScrollbarPress(mx, my, button) then @@ -185,7 +173,7 @@ function EventHandler:_handleMousePress(mx, my, button) return end end - + -- Fire press event if self.onEvent then local modifiers = getModifiers() @@ -199,15 +187,15 @@ function EventHandler:_handleMousePress(mx, my, button) }) self.onEvent(element, pressEvent) end - + self._pressed[button] = true - + -- Set mouse down position for text selection on left click if button == 1 and element._textEditor then element._mouseDownPosition = element._textEditor:mouseToTextPosition(mx, my) element._textDragOccurred = false -- Reset drag flag on press end - + -- Record drag start position per button self._dragStartX[button] = mx self._dragStartY[button] = my @@ -221,20 +209,22 @@ end ---@param button number Mouse button ---@param isHovering boolean Whether mouse is over element function EventHandler:_handleMouseDrag(mx, my, button, isHovering) - if not self._element then return end - + if not self._element then + return + end + local element = self._element - + 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 = self._InputEvent.new({ type = "drag", button = button, @@ -247,12 +237,12 @@ function EventHandler:_handleMouseDrag(mx, my, button, isHovering) }) self.onEvent(element, dragEvent) end - + -- Handle text selection drag for editable elements if button == 1 and element.editable and element._focused and element._handleTextDrag then element:_handleTextDrag(mx, my) end - + -- Update last known position for this button self._lastMouseX[button] = mx self._lastMouseY[button] = my @@ -264,27 +254,29 @@ end ---@param my number Mouse Y position ---@param button number Mouse button function EventHandler:_handleMouseRelease(mx, my, button) - if not self._element then return end - + if not self._element then + return + end + local element = self._element - + local currentTime = love.timer.getTime() local modifiers = getModifiers() - + -- Determine click count (double-click detection) local clickCount = 1 local doubleClickThreshold = 0.3 -- 300ms for double-click - + if self._lastClickTime and self._lastClickButton == button and (currentTime - self._lastClickTime) < doubleClickThreshold then clickCount = self._clickCount + 1 else clickCount = 1 end - + self._clickCount = clickCount self._lastClickTime = currentTime self._lastClickButton = button - + -- Determine event type based on button local eventType = "click" if button == 2 then @@ -292,7 +284,7 @@ function EventHandler:_handleMouseRelease(mx, my, button) elseif button == 3 then eventType = "middleclick" end - + -- Fire click event if self.onEvent then local clickEvent = self._InputEvent.new({ @@ -305,18 +297,18 @@ function EventHandler:_handleMouseRelease(mx, my, button) }) self.onEvent(element, clickEvent) end - + self._pressed[button] = false - + -- Clean up drag tracking self._dragStartX[button] = nil self._dragStartY[button] = nil - + -- Clean up text selection drag tracking if button == 1 then element._mouseDownPosition = nil end - + -- Focus editable elements on left click if button == 1 and element.editable then -- Only focus if not already focused (to avoid moving cursor to end) @@ -324,17 +316,17 @@ function EventHandler:_handleMouseRelease(mx, my, button) if not wasFocused then element:focus() end - + -- Handle text click for cursor positioning and word selection -- Only process click if no text drag occurred (to preserve drag selection) if element._handleTextClick and not element._textDragOccurred then element:_handleTextClick(mx, my, clickCount) end - + -- Reset drag flag after release element._textDragOccurred = false end - + -- Fire release event if self.onEvent then local releaseEvent = self._InputEvent.new({ @@ -354,14 +346,14 @@ function EventHandler:processTouchEvents() if not self._element or not self.onEvent then return end - + local element = self._element - + local bx = element.x local by = element.y local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right) local bh = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom) - + local touches = love.touch.getTouches() for _, id in ipairs(touches) do local tx, ty = love.touch.getPosition(id) diff --git a/modules/Grid.lua b/modules/Grid.lua index 991dea8..982a9eb 100644 --- a/modules/Grid.lua +++ b/modules/Grid.lua @@ -1,7 +1,3 @@ --- ==================== --- Grid Layout System --- ==================== - local modulePath = (...):match("(.-)[^%.]+$") local utils = require(modulePath .. "utils") local enums = utils.enums diff --git a/modules/GuiState.lua b/modules/GuiState.lua index 9703df0..fc209ef 100644 --- a/modules/GuiState.lua +++ b/modules/GuiState.lua @@ -1,8 +1,3 @@ --- ==================== --- GUI State Module --- ==================== --- Shared state between Gui and Element to avoid circular dependencies - ---@class GuiState local GuiState = { -- Top-level elements diff --git a/modules/ImageCache.lua b/modules/ImageCache.lua index b7779be..e44f7cf 100644 --- a/modules/ImageCache.lua +++ b/modules/ImageCache.lua @@ -1,12 +1,3 @@ ---[[ -ImageCache.lua - Image caching system for FlexLove -Provides efficient image loading and caching with memory management -]] - --- ==================== --- ImageCache --- ==================== - ---@class ImageCache ---@field _cache table local ImageCache = {} diff --git a/modules/ImageDataReader.lua b/modules/ImageDataReader.lua index 8f9dff6..5df3ab2 100644 --- a/modules/ImageDataReader.lua +++ b/modules/ImageDataReader.lua @@ -1,12 +1,3 @@ ---[[ -ImageDataReader.lua - Image data reading utilities for FlexLove -Provides functions to load and read pixel data from images -]] - --- ==================== --- Error Handling Utilities --- ==================== - --- Standardized error message formatter ---@param module string -- Module name (e.g., "Color", "Theme", "Units") ---@param message string -- Error message diff --git a/modules/ImageRenderer.lua b/modules/ImageRenderer.lua index b5e7871..f1637a8 100644 --- a/modules/ImageRenderer.lua +++ b/modules/ImageRenderer.lua @@ -1,12 +1,3 @@ ---[[ -ImageRenderer.lua - Image rendering utilities for FlexLove -Provides object-fit modes and object-position support for image rendering -]] - --- ==================== --- Error Handling Utilities --- ==================== - --- Standardized error message formatter ---@param module string -- Module name (e.g., "Color", "Theme", "Units") ---@param message string -- Error message diff --git a/modules/ImageScaler.lua b/modules/ImageScaler.lua index c71d470..b0ae26f 100644 --- a/modules/ImageScaler.lua +++ b/modules/ImageScaler.lua @@ -1,12 +1,3 @@ ---[[ -ImageScaler.lua - Image scaling utilities for FlexLove -Provides nearest-neighbor and bilinear interpolation scaling algorithms -]] - --- ==================== --- Error Handling Utilities --- ==================== - --- Standardized error message formatter ---@param module string -- Module name (e.g., "Color", "Theme", "Units") ---@param message string -- Error message diff --git a/modules/InputEvent.lua b/modules/InputEvent.lua index fa70817..dedbdf6 100644 --- a/modules/InputEvent.lua +++ b/modules/InputEvent.lua @@ -1,7 +1,3 @@ --- ==================== --- Input Event System --- ==================== - ---@class InputEvent ---@field type "click"|"press"|"release"|"rightclick"|"middleclick"|"drag" ---@field button number -- Mouse button: 1 (left), 2 (right), 3 (middle) diff --git a/modules/LayoutEngine.lua b/modules/LayoutEngine.lua index b666448..1b0c49e 100644 --- a/modules/LayoutEngine.lua +++ b/modules/LayoutEngine.lua @@ -1,16 +1,3 @@ --- ==================== --- LayoutEngine Module --- ==================== --- Handles all layout calculations for Element including: --- - Flexbox layout algorithm --- - Grid layout delegation --- - Auto-sizing calculations --- - CSS positioning offsets ---- ---- 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 ---@field positioning Positioning Layout positioning mode diff --git a/modules/NinePatch.lua b/modules/NinePatch.lua index 136ea4a..655579a 100644 --- a/modules/NinePatch.lua +++ b/modules/NinePatch.lua @@ -1,9 +1,3 @@ ---[[ -NinePatch - 9-Patch Renderer for FlexLove -Handles rendering of 9-patch components with Android-style scaling. -Corners can be scaled independently while edges stretch in one dimension. -]] - local modulePath = (...):match("(.-)[^%.]+$") local ImageScaler = require(modulePath .. "ImageScaler") diff --git a/modules/NinePatchParser.lua b/modules/NinePatchParser.lua index d4657c5..31bf4db 100644 --- a/modules/NinePatchParser.lua +++ b/modules/NinePatchParser.lua @@ -1,12 +1,3 @@ ---[[ -NinePatchParser.lua - 9-patch PNG parser for FlexLove -Parses Android-style 9-patch images to extract stretch regions and content padding -]] - --- ==================== --- Error Handling Utilities --- ==================== - --- Standardized error message formatter ---@param module string -- Module name (e.g., "Color", "Theme", "Units") ---@param message string -- Error message diff --git a/modules/Renderer.lua b/modules/Renderer.lua index 3dcad72..24ca34c 100644 --- a/modules/Renderer.lua +++ b/modules/Renderer.lua @@ -1,20 +1,3 @@ ---- Renderer Module --- Handles all visual rendering for Elements including backgrounds, borders, --- images, themes, blur effects, and text rendering. --- --- This module is responsible for the visual presentation layer of Elements, --- delegating from Element's draw() method to keep rendering concerns separated. ---- ---- 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 @@ -25,9 +8,9 @@ Renderer.__index = Renderer function Renderer.new(config, deps) local Color = deps.Color local ImageCache = deps.ImageCache - + local self = setmetatable({}, Renderer) - + -- Store dependencies for instance methods self._Color = Color self._RoundedRect = deps.RoundedRect @@ -39,36 +22,36 @@ function Renderer.new(config, deps) 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 - + -- Visual properties self.backgroundColor = config.backgroundColor or Color.new(0, 0, 0, 0) self.borderColor = config.borderColor or Color.new(0, 0, 0, 1) self.opacity = config.opacity or 1 - + -- Border configuration self.border = config.border or { top = false, right = false, bottom = false, - left = false + left = false, } - + -- Corner radius self.cornerRadius = config.cornerRadius or { topLeft = 0, topRight = 0, bottomLeft = 0, - bottomRight = 0 + bottomRight = 0, } - + -- Theme properties self.theme = config.theme self.themeComponent = config.themeComponent self._themeState = "normal" - + -- Image properties self.imagePath = config.imagePath self.image = config.image @@ -76,12 +59,12 @@ function Renderer.new(config, deps) self.objectFit = config.objectFit or "fill" self.objectPosition = config.objectPosition or "center center" self.imageOpacity = config.imageOpacity or 1 - + -- Blur effects self.contentBlur = config.contentBlur self.backdropBlur = config.backdropBlur self._blurInstance = nil - + -- Load image if path provided if self.imagePath and not self.image then local loadedImage, err = ImageCache.load(self.imagePath) @@ -95,7 +78,7 @@ function Renderer.new(config, deps) else self._loadedImage = nil end - + return self end @@ -115,12 +98,12 @@ function Renderer:getBlurInstance() elseif self.backdropBlur and self.backdropBlur.quality then quality = self.backdropBlur.quality end - + -- Create or reuse blur instance if not self._blurInstance or self._blurInstance.quality ~= quality then self._blurInstance = self._Blur.new(quality) end - + return self._blurInstance end @@ -137,12 +120,7 @@ end ---@param height number Height ---@param drawBackgroundColor table Background color (may have animation applied) function Renderer:_drawBackground(x, y, width, height, drawBackgroundColor) - local backgroundWithOpacity = self._Color.new( - drawBackgroundColor.r, - drawBackgroundColor.g, - drawBackgroundColor.b, - drawBackgroundColor.a * self.opacity - ) + local backgroundWithOpacity = self._Color.new(drawBackgroundColor.r, drawBackgroundColor.g, drawBackgroundColor.b, drawBackgroundColor.a * self.opacity) love.graphics.setColor(backgroundWithOpacity:toRGBA()) self._RoundedRect.draw("fill", x, y, width, height, self.cornerRadius) end @@ -160,22 +138,22 @@ function Renderer:_drawImage(x, y, paddingLeft, paddingTop, contentWidth, conten if not self._loadedImage then return end - + -- Calculate image bounds (content area - respects padding) local imageX = x + paddingLeft local imageY = y + paddingTop local imageWidth = contentWidth local imageHeight = contentHeight - + -- Combine element opacity with imageOpacity local finalOpacity = self.opacity * self.imageOpacity - + -- Apply cornerRadius clipping if set local hasCornerRadius = self.cornerRadius.topLeft > 0 or self.cornerRadius.topRight > 0 or self.cornerRadius.bottomLeft > 0 or self.cornerRadius.bottomRight > 0 - + if hasCornerRadius then -- Use stencil to clip image to rounded corners love.graphics.stencil(function() @@ -183,10 +161,10 @@ function Renderer:_drawImage(x, y, paddingLeft, paddingTop, contentWidth, conten end, "replace", 1) love.graphics.setStencilTest("greater", 0) end - + -- Draw the image self._ImageRenderer.draw(self._loadedImage, imageX, imageY, imageWidth, imageHeight, self.objectFit, self.objectPosition, finalOpacity) - + -- Clear stencil if it was used if hasCornerRadius then love.graphics.setStencilTest() @@ -204,7 +182,7 @@ function Renderer:_drawTheme(x, y, borderBoxWidth, borderBoxHeight, scaleCorners if not self.themeComponent then return end - + -- Get the theme to use local themeToUse = nil if self.theme then @@ -222,26 +200,26 @@ function Renderer:_drawTheme(x, y, borderBoxWidth, borderBoxHeight, scaleCorners -- Use active theme themeToUse = self._Theme.getActive() end - + if not themeToUse then return end - + -- Get the component from the theme local component = themeToUse.components[self.themeComponent] if not component then return end - + -- Check for state-specific override local state = self._themeState if state and component.states and component.states[state] then component = component.states[state] end - + -- Use component-specific atlas if available, otherwise use theme atlas local atlasToUse = component._loadedAtlas or themeToUse.atlas - + if atlasToUse and component.regions then -- Validate component has required structure local hasAllRegions = component.regions.topLeft @@ -253,7 +231,7 @@ function Renderer:_drawTheme(x, y, borderBoxWidth, borderBoxHeight, scaleCorners and component.regions.bottomLeft and component.regions.bottomCenter and component.regions.bottomRight - + if hasAllRegions then -- Pass element-level overrides for scaleCorners and scalingAlgorithm self._NinePatch.draw(component, atlasToUse, x, y, borderBoxWidth, borderBoxHeight, self.opacity, scaleCorners, scalingAlgorithm) @@ -267,17 +245,12 @@ end ---@param borderBoxWidth number Border box width ---@param borderBoxHeight number Border box height function Renderer:_drawBorders(x, y, borderBoxWidth, borderBoxHeight) - local borderColorWithOpacity = self._Color.new( - self.borderColor.r, - self.borderColor.g, - self.borderColor.b, - self.borderColor.a * self.opacity - ) + local borderColorWithOpacity = self._Color.new(self.borderColor.r, self.borderColor.g, self.borderColor.b, self.borderColor.a * self.opacity) love.graphics.setColor(borderColorWithOpacity:toRGBA()) - + -- Check if all borders are enabled local allBorders = self.border.top and self.border.bottom and self.border.left and self.border.right - + if allBorders then -- Draw complete rounded rectangle border self._RoundedRect.draw("line", x, y, borderBoxWidth, borderBoxHeight, self.cornerRadius) @@ -305,14 +278,14 @@ function Renderer:draw(backdropCanvas) if self.opacity <= 0 then return end - + -- Element must be initialized before drawing if not self._element then error("Renderer:draw() called before initialize(). Call renderer:initialize(element) first.") end - + local element = self._element - + -- Handle opacity during animation local drawBackgroundColor = self.backgroundColor if element.animation then @@ -321,11 +294,11 @@ function Renderer:draw(backdropCanvas) drawBackgroundColor = self._Color.new(self.backgroundColor.r, self.backgroundColor.g, self.backgroundColor.b, anim.opacity) end end - + -- Cache border box dimensions for this draw call (optimization) local borderBoxWidth = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right) local borderBoxHeight = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom) - + -- LAYER 0.5: Draw backdrop blur if configured (before background) if self.backdropBlur and self.backdropBlur.intensity > 0 and backdropCanvas then local blurInstance = self:getBlurInstance() @@ -333,25 +306,16 @@ function Renderer:draw(backdropCanvas) self._Blur.applyBackdrop(blurInstance, self.backdropBlur.intensity, element.x, element.y, borderBoxWidth, borderBoxHeight, backdropCanvas) end end - + -- LAYER 1: Draw backgroundColor first (behind everything) self:_drawBackground(element.x, element.y, borderBoxWidth, borderBoxHeight, drawBackgroundColor) - + -- LAYER 1.5: Draw image on top of backgroundColor (if image exists) - self:_drawImage( - element.x, - element.y, - element.padding.left, - element.padding.top, - element.width, - element.height, - borderBoxWidth, - borderBoxHeight - ) - + self:_drawImage(element.x, element.y, element.padding.left, element.padding.top, element.width, element.height, borderBoxWidth, borderBoxHeight) + -- LAYER 2: Draw theme on top of backgroundColor (if theme exists) self:_drawTheme(element.x, element.y, borderBoxWidth, borderBoxHeight, element.scaleCorners, element.scalingAlgorithm) - + -- LAYER 3: Draw borders on top of theme self:_drawBorders(element.x, element.y, borderBoxWidth, borderBoxHeight) end @@ -382,7 +346,7 @@ end function Renderer:wrapLine(element, line, maxWidth) -- UTF-8 support local utf8 = utf8 or require("utf8") - + if not element.editable then return { { text = line, startIdx = 0, endIdx = utf8.len(line) } } end @@ -596,7 +560,8 @@ function Renderer:drawText(element) end if displayText and displayText ~= "" then - 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) + 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 = self._Color.new(textColor.r, textColor.g, textColor.b, textColor.a * self.opacity) love.graphics.setColor(textColorWithOpacity:toRGBA()) diff --git a/modules/RoundedRect.lua b/modules/RoundedRect.lua index 51c074b..114245e 100644 --- a/modules/RoundedRect.lua +++ b/modules/RoundedRect.lua @@ -1,8 +1,3 @@ ---[[ -RoundedRect - Rounded Rectangle Helper for FlexLove -Provides functions for generating and drawing rounded rectangles with per-corner radius control. -]] - local RoundedRect = {} --- Generate points for a rounded rectangle diff --git a/modules/ScrollManager.lua b/modules/ScrollManager.lua index 3d874a2..c35b704 100644 --- a/modules/ScrollManager.lua +++ b/modules/ScrollManager.lua @@ -1,10 +1,3 @@ ---- ScrollManager.lua ---- Handles scrolling, overflow detection, and scrollbar rendering/interaction for Elements ---- Extracted from Element.lua as part of element-refactor-modularization task 05 ---- ---- Dependencies (must be injected via deps parameter): ---- - Color: Color module for creating color instances - ---@class ScrollManager ---@field overflow string -- "visible"|"hidden"|"auto"|"scroll" ---@field overflowX string? -- X-axis specific overflow (overrides overflow) @@ -41,15 +34,15 @@ ScrollManager.__index = ScrollManager function ScrollManager.new(config, deps) 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 self.overflowY = config.overflowY - + -- Scrollbar appearance self.scrollbarWidth = config.scrollbarWidth or 12 self.scrollbarColor = config.scrollbarColor or Color.new(0.5, 0.5, 0.5, 0.8) @@ -57,7 +50,7 @@ function ScrollManager.new(config, deps) self.scrollbarRadius = config.scrollbarRadius or 6 self.scrollbarPadding = config.scrollbarPadding or 2 self.scrollSpeed = config.scrollSpeed or 20 - + -- hideScrollbars can be boolean or table {vertical: boolean, horizontal: boolean} if config.hideScrollbars ~= nil then if type(config.hideScrollbars) == "boolean" then @@ -73,19 +66,19 @@ function ScrollManager.new(config, deps) else self.hideScrollbars = { vertical = false, horizontal = false } end - + -- Internal overflow state self._overflowX = false self._overflowY = false self._contentWidth = 0 self._contentHeight = 0 - + -- Scroll state (can be restored from config in immediate mode) self._scrollX = config._scrollX or 0 self._scrollY = config._scrollY or 0 self._maxScrollX = 0 self._maxScrollY = 0 - + -- Scrollbar interaction state self._scrollbarHoveredVertical = false self._scrollbarHoveredHorizontal = false @@ -93,10 +86,10 @@ function ScrollManager.new(config, deps) self._hoveredScrollbar = nil -- "vertical" or "horizontal" self._scrollbarDragOffset = 0 self._scrollbarPressHandled = false - + -- Element reference (set via initialize) self._element = nil - + return self end @@ -111,34 +104,34 @@ function ScrollManager:detectOverflow() if not self._element then error("ScrollManager:detectOverflow() called before initialize()") end - + local element = self._element - + -- Reset overflow state self._overflowX = false self._overflowY = false self._contentWidth = element.width self._contentHeight = element.height - + -- Skip detection if overflow is visible (no clipping needed) local overflowX = self.overflowX or self.overflow local overflowY = self.overflowY or self.overflow if overflowX == "visible" and overflowY == "visible" then return end - + -- Calculate content bounds based on children if #element.children == 0 then return -- No children, no overflow end - + local minX, minY = 0, 0 local maxX, maxY = 0, 0 - + -- Content area starts after padding local contentX = element.x + element.padding.left local contentY = element.y + element.padding.top - + for _, child in ipairs(element.children) do -- Skip absolutely positioned children (they don't contribute to overflow) if not child._explicitlyAbsolute then @@ -147,27 +140,27 @@ function ScrollManager:detectOverflow() local childTop = child.y - contentY local childRight = childLeft + child:getBorderBoxWidth() + child.margin.right local childBottom = childTop + child:getBorderBoxHeight() + child.margin.bottom - + maxX = math.max(maxX, childRight) maxY = math.max(maxY, childBottom) end end - + -- Calculate content dimensions self._contentWidth = maxX self._contentHeight = maxY - + -- Detect overflow local containerWidth = element.width local containerHeight = element.height - + self._overflowX = self._contentWidth > containerWidth self._overflowY = self._contentHeight > containerHeight - + -- Calculate maximum scroll bounds self._maxScrollX = math.max(0, self._contentWidth - containerWidth) self._maxScrollY = math.max(0, self._contentHeight - containerHeight) - + -- Clamp current scroll position to new bounds self._scrollX = math.max(0, math.min(self._scrollX, self._maxScrollX)) self._scrollY = math.max(0, math.min(self._scrollY, self._maxScrollY)) @@ -235,28 +228,28 @@ function ScrollManager:calculateScrollbarDimensions() if not self._element then error("ScrollManager:calculateScrollbarDimensions() called before initialize()") end - + local element = self._element local result = { vertical = { visible = false, trackHeight = 0, thumbHeight = 0, thumbY = 0 }, horizontal = { visible = false, trackWidth = 0, thumbWidth = 0, thumbX = 0 }, } - + local overflowX = self.overflowX or self.overflow local overflowY = self.overflowY or self.overflow - + -- Vertical scrollbar -- Note: overflow="scroll" always shows scrollbar; overflow="auto" only when content overflows if overflowY == "scroll" then -- Always show scrollbar for "scroll" mode result.vertical.visible = true result.vertical.trackHeight = element.height - (self.scrollbarPadding * 2) - + if self._overflowY then -- Content overflows, calculate proper thumb size local contentRatio = element.height / math.max(self._contentHeight, element.height) result.vertical.thumbHeight = math.max(20, result.vertical.trackHeight * contentRatio) - + -- Calculate thumb position based on scroll ratio local scrollRatio = self._maxScrollY > 0 and (self._scrollY / self._maxScrollY) or 0 local maxThumbY = result.vertical.trackHeight - result.vertical.thumbHeight @@ -270,29 +263,29 @@ function ScrollManager:calculateScrollbarDimensions() -- Only show scrollbar when content actually overflows result.vertical.visible = true result.vertical.trackHeight = element.height - (self.scrollbarPadding * 2) - + -- Calculate thumb height based on content ratio local contentRatio = element.height / math.max(self._contentHeight, element.height) result.vertical.thumbHeight = math.max(20, result.vertical.trackHeight * contentRatio) - + -- Calculate thumb position based on scroll ratio local scrollRatio = self._maxScrollY > 0 and (self._scrollY / self._maxScrollY) or 0 local maxThumbY = result.vertical.trackHeight - result.vertical.thumbHeight result.vertical.thumbY = maxThumbY * scrollRatio end - + -- Horizontal scrollbar -- Note: overflow="scroll" always shows scrollbar; overflow="auto" only when content overflows if overflowX == "scroll" then -- Always show scrollbar for "scroll" mode result.horizontal.visible = true result.horizontal.trackWidth = element.width - (self.scrollbarPadding * 2) - + if self._overflowX then -- Content overflows, calculate proper thumb size local contentRatio = element.width / math.max(self._contentWidth, element.width) result.horizontal.thumbWidth = math.max(20, result.horizontal.trackWidth * contentRatio) - + -- Calculate thumb position based on scroll ratio local scrollRatio = self._maxScrollX > 0 and (self._scrollX / self._maxScrollX) or 0 local maxThumbX = result.horizontal.trackWidth - result.horizontal.thumbWidth @@ -306,17 +299,17 @@ function ScrollManager:calculateScrollbarDimensions() -- Only show scrollbar when content actually overflows result.horizontal.visible = true result.horizontal.trackWidth = element.width - (self.scrollbarPadding * 2) - + -- Calculate thumb width based on content ratio local contentRatio = element.width / math.max(self._contentWidth, element.width) result.horizontal.thumbWidth = math.max(20, result.horizontal.trackWidth * contentRatio) - + -- Calculate thumb position based on scroll ratio local scrollRatio = self._maxScrollX > 0 and (self._scrollX / self._maxScrollX) or 0 local maxThumbX = result.horizontal.trackWidth - result.horizontal.thumbWidth result.horizontal.thumbX = maxThumbX * scrollRatio end - + return result end @@ -328,19 +321,19 @@ function ScrollManager:getScrollbarAtPosition(mouseX, mouseY) if not self._element then error("ScrollManager:getScrollbarAtPosition() called before initialize()") end - + local element = self._element local overflowX = self.overflowX or self.overflow local overflowY = self.overflowY or self.overflow - + if not (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto") then return nil end - + local dims = self:calculateScrollbarDimensions() local x, y = element.x, element.y local w, h = element.width, element.height - + -- Check vertical scrollbar (only if not hidden) if dims.vertical.visible and not self.hideScrollbars.vertical then -- Position scrollbar within content area (x, y is border-box origin) @@ -350,7 +343,7 @@ function ScrollManager:getScrollbarAtPosition(mouseX, mouseY) local trackY = contentY + self.scrollbarPadding local trackW = self.scrollbarWidth local trackH = dims.vertical.trackHeight - + if mouseX >= trackX and mouseX <= trackX + trackW and mouseY >= trackY and mouseY <= trackY + trackH then -- Check if over thumb local thumbY = trackY + dims.vertical.thumbY @@ -362,7 +355,7 @@ function ScrollManager:getScrollbarAtPosition(mouseX, mouseY) end end end - + -- Check horizontal scrollbar (only if not hidden) if dims.horizontal.visible and not self.hideScrollbars.horizontal then -- Position scrollbar within content area (x, y is border-box origin) @@ -372,7 +365,7 @@ function ScrollManager:getScrollbarAtPosition(mouseX, mouseY) local trackY = contentY + h - self.scrollbarWidth - self.scrollbarPadding local trackW = dims.horizontal.trackWidth local trackH = self.scrollbarWidth - + if mouseX >= trackX and mouseX <= trackX + trackW and mouseY >= trackY and mouseY <= trackY + trackH then -- Check if over thumb local thumbX = trackX + dims.horizontal.thumbX @@ -384,7 +377,7 @@ function ScrollManager:getScrollbarAtPosition(mouseX, mouseY) end end end - + return nil end @@ -397,23 +390,23 @@ function ScrollManager:handleMousePress(mouseX, mouseY, button) if not self._element then error("ScrollManager:handleMousePress() called before initialize()") end - + if button ~= 1 then return false end -- Only left click - + local scrollbar = self:getScrollbarAtPosition(mouseX, mouseY) if not scrollbar then return false end - + if scrollbar.region == "thumb" then -- Start dragging thumb self._scrollbarDragging = true self._hoveredScrollbar = scrollbar.component local dims = self:calculateScrollbarDimensions() local element = self._element - + if scrollbar.component == "vertical" then local contentY = element.y + element.padding.top local trackY = contentY + self.scrollbarPadding @@ -425,14 +418,14 @@ function ScrollManager:handleMousePress(mouseX, mouseY, button) local thumbX = trackX + dims.horizontal.thumbX self._scrollbarDragOffset = mouseX - thumbX end - + return true -- Event consumed elseif scrollbar.region == "track" then -- Click on track - jump to position self:_scrollToTrackPosition(mouseX, mouseY, scrollbar.component) return true end - + return false end @@ -444,28 +437,28 @@ function ScrollManager:handleMouseMove(mouseX, mouseY) if not self._element then return false end - + if not self._scrollbarDragging then return false end - + local dims = self:calculateScrollbarDimensions() local element = self._element - + if self._hoveredScrollbar == "vertical" then local contentY = element.y + element.padding.top local trackY = contentY + self.scrollbarPadding local trackH = dims.vertical.trackHeight local thumbH = dims.vertical.thumbHeight - + -- Calculate new thumb position local newThumbY = mouseY - self._scrollbarDragOffset - trackY newThumbY = math.max(0, math.min(newThumbY, trackH - thumbH)) - + -- Convert thumb position to scroll position local scrollRatio = (trackH - thumbH) > 0 and (newThumbY / (trackH - thumbH)) or 0 local newScrollY = scrollRatio * self._maxScrollY - + self:setScroll(nil, newScrollY) return true elseif self._hoveredScrollbar == "horizontal" then @@ -473,19 +466,19 @@ function ScrollManager:handleMouseMove(mouseX, mouseY) local trackX = contentX + self.scrollbarPadding local trackW = dims.horizontal.trackWidth local thumbW = dims.horizontal.thumbWidth - + -- Calculate new thumb position local newThumbX = mouseX - self._scrollbarDragOffset - trackX newThumbX = math.max(0, math.min(newThumbX, trackW - thumbW)) - + -- Convert thumb position to scroll position local scrollRatio = (trackW - thumbW) > 0 and (newThumbX / (trackW - thumbW)) or 0 local newScrollX = scrollRatio * self._maxScrollX - + self:setScroll(newScrollX, nil) return true end - + return false end @@ -496,12 +489,12 @@ function ScrollManager:handleMouseRelease(button) if button ~= 1 then return false end - + if self._scrollbarDragging then self._scrollbarDragging = false return true end - + return false end @@ -513,39 +506,39 @@ function ScrollManager:_scrollToTrackPosition(mouseX, mouseY, component) if not self._element then return end - + local dims = self:calculateScrollbarDimensions() local element = self._element - + if component == "vertical" then local contentY = element.y + element.padding.top local trackY = contentY + self.scrollbarPadding local trackH = dims.vertical.trackHeight local thumbH = dims.vertical.thumbHeight - + -- Calculate target thumb position (centered on click) local targetThumbY = mouseY - trackY - (thumbH / 2) targetThumbY = math.max(0, math.min(targetThumbY, trackH - thumbH)) - + -- Convert to scroll position local scrollRatio = (trackH - thumbH) > 0 and (targetThumbY / (trackH - thumbH)) or 0 local newScrollY = scrollRatio * self._maxScrollY - + self:setScroll(nil, newScrollY) elseif component == "horizontal" then local contentX = element.x + element.padding.left local trackX = contentX + self.scrollbarPadding local trackW = dims.horizontal.trackWidth local thumbW = dims.horizontal.thumbWidth - + -- Calculate target thumb position (centered on click) local targetThumbX = mouseX - trackX - (thumbW / 2) targetThumbX = math.max(0, math.min(targetThumbX, trackW - thumbW)) - + -- Convert to scroll position local scrollRatio = (trackW - thumbW) > 0 and (targetThumbX / (trackW - thumbW)) or 0 local newScrollX = scrollRatio * self._maxScrollX - + self:setScroll(newScrollX, nil) end end @@ -557,16 +550,16 @@ end function ScrollManager:handleWheel(x, y) local overflowX = self.overflowX or self.overflow local overflowY = self.overflowY or self.overflow - + if not (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto") then return false end - + local hasVerticalOverflow = self._overflowY and self._maxScrollY > 0 local hasHorizontalOverflow = self._overflowX and self._maxScrollX > 0 - + local scrolled = false - + -- Vertical scrolling if y ~= 0 and hasVerticalOverflow then local delta = -y * self.scrollSpeed -- Negative because wheel up = scroll up @@ -574,7 +567,7 @@ function ScrollManager:handleWheel(x, y) self:setScroll(nil, newScrollY) scrolled = true end - + -- Horizontal scrolling if x ~= 0 and hasHorizontalOverflow then local delta = -x * self.scrollSpeed @@ -582,7 +575,7 @@ function ScrollManager:handleWheel(x, y) self:setScroll(newScrollX, nil) scrolled = true end - + return scrolled end @@ -591,7 +584,7 @@ end ---@param mouseY number function ScrollManager:updateHoverState(mouseX, mouseY) local scrollbar = self:getScrollbarAtPosition(mouseX, mouseY) - + if scrollbar then if scrollbar.component == "vertical" then self._scrollbarHoveredVertical = true @@ -640,7 +633,7 @@ function ScrollManager:setState(state) if not state then return end - + if state.scrollX then self._scrollX = state.scrollX end diff --git a/modules/StateManager.lua b/modules/StateManager.lua index ec908cb..8bbc025 100644 --- a/modules/StateManager.lua +++ b/modules/StateManager.lua @@ -1,10 +1,3 @@ --- ==================== --- State Manager Module --- ==================== --- Unified state management system for immediate mode GUI elements --- Combines ID-based state persistence with interactive state tracking --- Handles all element state: interaction, scroll, input, click tracking, etc. - ---@class StateManager local StateManager = {} @@ -23,7 +16,7 @@ local callSiteCounters = {} -- Configuration local config = { stateRetentionFrames = 60, -- Keep unused state for 60 frames (~1 second at 60fps) - maxStateEntries = 1000, -- Maximum state entries before forced GC + maxStateEntries = 1000, -- Maximum state entries before forced GC } -- ==================== @@ -36,28 +29,30 @@ local config = { ---@param depth number|nil Current recursion depth ---@return string local function hashProps(props, visited, depth) - if not props then return "" end + if not props then + return "" + end -- Initialize visited table on first call visited = visited or {} depth = depth or 0 - + -- Limit recursion depth to prevent deep nesting issues if depth > 3 then return "[deep]" end - + -- Check if we've already visited this table (circular reference) if visited[props] then return "[circular]" end - + -- Mark this table as visited visited[props] = true local parts = {} local keys = {} - + -- Properties to skip (they cause issues or aren't relevant for ID generation) local skipKeys = { onEvent = true, @@ -70,12 +65,12 @@ local function hashProps(props, visited, depth) onEnter = true, userdata = true, -- Dynamic input/state properties that should not affect ID stability - text = true, -- Text content changes as user types - placeholder = true, -- Placeholder text is presentational - editable = true, -- Editable state can be toggled dynamically - selectOnFocus = true, -- Input behavior flag - autoGrow = true, -- Auto-grow behavior flag - passwordMode = true, -- Password mode can be toggled + text = true, -- Text content changes as user types + placeholder = true, -- Placeholder text is presentational + editable = true, -- Editable state can be toggled dynamically + selectOnFocus = true, -- Input behavior flag + autoGrow = true, -- Auto-grow behavior flag + passwordMode = true, -- Password mode can be toggled } -- Collect and sort keys for consistent ordering @@ -121,17 +116,17 @@ function StateManager.generateID(props, parent) local filename = source:match("([^/\\]+)$") or source -- Get filename filename = filename:gsub("%.lua$", "") -- Remove .lua extension local locationKey = filename .. "_L" .. line - + -- If we have a parent, use tree-based ID generation for stability if parent and parent.id and parent.id ~= "" then -- Count how many children the parent currently has -- This gives us a stable sibling index local siblingIndex = #(parent.children or {}) - + -- Generate ID based on parent ID + sibling position (NO line number for stability) -- This ensures the same position in the tree always gets the same ID local baseID = parent.id .. "_child" .. siblingIndex - + -- Add property hash if provided (for additional differentiation at same position) if props then local propHash = hashProps(props) @@ -144,17 +139,17 @@ function StateManager.generateID(props, parent) baseID = baseID .. "_" .. hash end end - + return baseID end - + -- No parent (top-level element): use call-site counter approach -- Track how many elements have been created at this location callSiteCounters[locationKey] = (callSiteCounters[locationKey] or 0) + 1 local instanceNum = callSiteCounters[locationKey] - + local baseID = locationKey - + -- Add instance number if multiple elements created at same location (e.g., in loops) if instanceNum > 1 then baseID = baseID .. "_" .. instanceNum @@ -193,48 +188,100 @@ function StateManager.getState(id, defaultState) if not stateStore[id] then -- Merge default state with standard structure stateStore[id] = defaultState or {} - + -- Ensure all standard properties exist with defaults local state = stateStore[id] - + -- Interaction states - if state.hover == nil then state.hover = false end - if state.pressed == nil then state.pressed = false end - if state.focused == nil then state.focused = false end - if state.disabled == nil then state.disabled = false end - if state.active == nil then state.active = false end - + if state.hover == nil then + state.hover = false + end + if state.pressed == nil then + state.pressed = false + end + if state.focused == nil then + state.focused = false + end + if state.disabled == nil then + state.disabled = false + end + if state.active == nil then + state.active = false + end + -- Scrollbar states - if state.scrollbarHoveredVertical == nil then state.scrollbarHoveredVertical = false end - if state.scrollbarHoveredHorizontal == nil then state.scrollbarHoveredHorizontal = false end - if state.scrollbarDragging == nil then state.scrollbarDragging = false end - if state.hoveredScrollbar == nil then state.hoveredScrollbar = nil end - if state.scrollbarDragOffset == nil then state.scrollbarDragOffset = 0 end - + if state.scrollbarHoveredVertical == nil then + state.scrollbarHoveredVertical = false + end + if state.scrollbarHoveredHorizontal == nil then + state.scrollbarHoveredHorizontal = false + end + if state.scrollbarDragging == nil then + state.scrollbarDragging = false + end + if state.hoveredScrollbar == nil then + state.hoveredScrollbar = nil + end + if state.scrollbarDragOffset == nil then + state.scrollbarDragOffset = 0 + end + -- Scroll position - if state.scrollX == nil then state.scrollX = 0 end - if state.scrollY == nil then state.scrollY = 0 end - + if state.scrollX == nil then + state.scrollX = 0 + end + if state.scrollY == nil then + state.scrollY = 0 + end + -- Click tracking - if state._pressed == nil then state._pressed = {} end - if state._lastClickTime == nil then state._lastClickTime = nil end - if state._lastClickButton == nil then state._lastClickButton = nil end - if state._clickCount == nil then state._clickCount = 0 end - + if state._pressed == nil then + state._pressed = {} + end + if state._lastClickTime == nil then + state._lastClickTime = nil + end + if state._lastClickButton == nil then + state._lastClickButton = nil + end + if state._clickCount == nil then + state._clickCount = 0 + end + -- Drag tracking - if state._dragStartX == nil then state._dragStartX = {} end - if state._dragStartY == nil then state._dragStartY = {} end - if state._lastMouseX == nil then state._lastMouseX = {} end - if state._lastMouseY == nil then state._lastMouseY = {} end - + if state._dragStartX == nil then + state._dragStartX = {} + end + if state._dragStartY == nil then + state._dragStartY = {} + end + if state._lastMouseX == nil then + state._lastMouseX = {} + end + if state._lastMouseY == nil then + state._lastMouseY = {} + end + -- Input/focus state - if state._hovered == nil then state._hovered = nil end - if state._focused == nil then state._focused = nil end - if state._cursorPosition == nil then state._cursorPosition = nil end - if state._selectionStart == nil then state._selectionStart = nil end - if state._selectionEnd == nil then state._selectionEnd = nil end - if state._textBuffer == nil then state._textBuffer = "" end - + if state._hovered == nil then + state._hovered = nil + end + if state._focused == nil then + state._focused = nil + end + if state._cursorPosition == nil then + state._cursorPosition = nil + end + if state._selectionStart == nil then + state._selectionStart = nil + end + if state._selectionEnd == nil then + state._selectionEnd = nil + end + if state._textBuffer == nil then + state._textBuffer = "" + end + -- Create metadata stateMetadata[id] = { lastFrame = frameNumber, @@ -278,12 +325,12 @@ end ---@param newState table New state values to merge function StateManager.updateState(id, newState) local state = StateManager.getState(id) - + -- Merge new state into existing state for key, value in pairs(newState) do state[key] = value end - + -- Update metadata stateMetadata[id].lastFrame = frameNumber end @@ -468,7 +515,7 @@ end ---@return table state Active state values function StateManager.getActiveState(id) local state = StateManager.getState(id) - + -- Return only the active state properties (not tracking frames or internal state) return { hover = state.hover, diff --git a/modules/TextEditor.lua b/modules/TextEditor.lua index 137dfc9..396eea2 100644 --- a/modules/TextEditor.lua +++ b/modules/TextEditor.lua @@ -1,28 +1,3 @@ --- ==================== --- TextEditor Module --- ==================== --- Handles all text editing functionality including: --- - Text buffer management --- - Cursor positioning and navigation --- - Text selection --- - Multi-line text with wrapping --- - 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("(.-)[^%.]+$") -local function req(name) - return require(modulePath .. name) -end - --- UTF-8 support local utf8 = utf8 or require("utf8") local TextEditor = {} diff --git a/modules/Theme.lua b/modules/Theme.lua index 61d7d6e..622b23c 100644 --- a/modules/Theme.lua +++ b/modules/Theme.lua @@ -1,16 +1,9 @@ ---[[ -Theme - Theme System for FlexLove -Manages theme loading, registration, and component/color/font access. -Supports 9-patch images, component states, and dynamic theme switching. -]] - local modulePath = (...):match("(.-)[^%.]+$") local function req(name) return require(modulePath .. name) end local NinePatchParser = req("NinePatchParser") -local ImageScaler = req("ImageScaler") --- Standardized error message formatter ---@param module string -- Module name (e.g., "Color", "Theme", "Units") diff --git a/modules/ThemeManager.lua b/modules/ThemeManager.lua index db45d0e..7f946fd 100644 --- a/modules/ThemeManager.lua +++ b/modules/ThemeManager.lua @@ -1,10 +1,3 @@ ---- ThemeManager.lua ---- Manages theme application, state transitions, and property resolution for Elements ---- Extracted from Element.lua as part of element-refactor-modularization task 06 ---- ---- Dependencies (must be injected via deps parameter): ---- - Theme: Theme module for loading and accessing themes - ---@class ThemeManager ---@field theme string? -- Theme name to use ---@field themeComponent string? -- Component name from theme (e.g., "button", "panel") diff --git a/modules/Units.lua b/modules/Units.lua index 551d424..b9cd8b3 100644 --- a/modules/Units.lua +++ b/modules/Units.lua @@ -1,8 +1,3 @@ --- ==================== --- Units System --- ==================== - ---- Unit parsing and viewport calculations local Units = {} --- Parse a unit value (string or number) into value and unit type diff --git a/modules/types.lua b/modules/types.lua new file mode 100644 index 0000000..c36491e --- /dev/null +++ b/modules/types.lua @@ -0,0 +1,127 @@ +--=====================================-- +-- only used for types with lua_ls, if you're using a different LSP (or none), you can remove this file -- +--=====================================-- + +--=====================================-- +-- For Animation.lua +--=====================================-- +---@class AnimationProps +---@field duration number +---@field start {width?:number, height?:number, opacity?:number} +---@field final {width?:number, height?:number, opacity?:number} +---@field transform table? +---@field transition table? +local AnimationProps = {} + +---@class TransformProps +---@field scale {x?:number, y?:number}? +---@field rotate number? +---@field translate {x?:number, y?:number}? +---@field skew {x?:number, y?:number}? + +---@class TransitionProps +---@field duration number? +---@field easing string? + +--=====================================-- +-- For Element.lua +--=====================================-- +---@class ElementProps +---@field id string? -- Unique identifier for the element (auto-generated in immediate mode if not provided) +---@field parent Element? -- Parent element for hierarchical structure +---@field x number|string? -- X coordinate of the element (default: 0) +---@field y number|string? -- Y coordinate of the element (default: 0) +---@field z number? -- Z-index for layering (default: 0) +---@field width number|string? -- Width of the element (default: calculated automatically) +---@field height number|string? -- Height of the element (default: calculated automatically) +---@field top number|string? -- Offset from top edge (CSS-style positioning) +---@field right number|string? -- Offset from right edge (CSS-style positioning) +---@field bottom number|string? -- Offset from bottom edge (CSS-style positioning) +---@field left number|string? -- Offset from left edge (CSS-style positioning) +---@field border Border? -- Border configuration for the element +---@field borderColor Color? -- Color of the border (default: black) +---@field opacity number? -- Element opacity 0-1 (default: 1) +---@field backgroundColor Color? -- Background color (default: transparent) +---@field cornerRadius number|{topLeft:number?, topRight:number?, bottomLeft:number?, bottomRight:number?}? -- Corner radius: number (all corners) or table for individual corners (default: 0) +---@field gap number|string? -- Space between children elements (default: 0) +---@field padding {top:number|string?, right:number|string?, bottom:number|string?, left:number|string?, horizontal:number|string?, vertical:number|string?}? -- Padding around children (default: {top=0, right=0, bottom=0, left=0}) +---@field margin {top:number|string?, right:number|string?, bottom:number|string?, left:number|string?, horizontal:number|string?, vertical:number|string?}? -- Margin around element (default: {top=0, right=0, bottom=0, left=0}) +---@field text string? -- Text content to display (default: nil) +---@field textAlign TextAlign? -- Alignment of the text content (default: START) +---@field textColor Color? -- Color of the text content (default: black or theme text color) +---@field textSize number|string? -- Font size: number (px), string with units ("2vh", "10%"), or preset ("xxs"|"xs"|"sm"|"md"|"lg"|"xl"|"xxl"|"3xl"|"4xl") (default: "md" or 12px) +---@field minTextSize number? -- Minimum text size in pixels for auto-scaling +---@field maxTextSize number? -- Maximum text size in pixels for auto-scaling +---@field fontFamily string? -- Font family name from theme or path to font file (default: theme default or system default, inherits from parent) +---@field autoScaleText boolean? -- Whether text should auto-scale with window size (default: true) +---@field positioning Positioning? -- Layout positioning mode: "absolute"|"relative"|"flex"|"grid" (default: RELATIVE) +---@field flexDirection FlexDirection? -- Direction of flex layout: "horizontal"|"vertical" (default: HORIZONTAL) +---@field justifyContent JustifyContent? -- Alignment of items along main axis (default: FLEX_START) +---@field alignItems AlignItems? -- Alignment of items along cross axis (default: STRETCH) +---@field alignContent AlignContent? -- Alignment of lines in multi-line flex containers (default: STRETCH) +---@field flexWrap FlexWrap? -- Whether children wrap to multiple lines: "nowrap"|"wrap"|"wrap-reverse" (default: NOWRAP) +---@field justifySelf JustifySelf? -- Alignment of the item itself along main axis (default: AUTO) +---@field alignSelf AlignSelf? -- Alignment of the item itself along cross axis (default: AUTO) +---@field onEvent fun(element:Element, event:InputEvent)? -- Callback function for interaction events +---@field onFocus fun(element:Element, event:InputEvent)? -- Callback when element receives focus +---@field onBlur fun(element:Element, event:InputEvent)? -- Callback when element loses focus +---@field onTextInput fun(element:Element, text:string)? -- Callback when text is input +---@field onTextChange fun(element:Element, text:string)? -- Callback when text content changes +---@field onEnter fun(element:Element)? -- Callback when Enter key is pressed +---@field transform TransformProps? -- Transform properties for animations and styling +---@field transition TransitionProps? -- Transition settings for animations +---@field gridRows number? -- Number of rows in the grid (default: 1) +---@field gridColumns number? -- Number of columns in the grid (default: 1) +---@field columnGap number|string? -- Gap between grid columns (default: 0) +---@field rowGap number|string? -- Gap between grid rows (default: 0) +---@field theme string? -- Theme name to use (e.g., "space", "metal"). Defaults to theme from Gui.init() +---@field themeComponent string? -- Theme component to use (e.g., "panel", "button", "input"). If nil, no theme is applied +---@field disabled boolean? -- Whether the element is disabled (default: false) +---@field active boolean? -- Whether the element is active/focused (for inputs, default: false) +---@field disableHighlight boolean? -- Whether to disable the pressed state highlight overlay (default: false, or true when using themeComponent) +---@field contentAutoSizingMultiplier {width:number?, height:number?}? -- Multiplier for auto-sized content dimensions (default: sourced from theme or {1, 1}) +---@field scaleCorners number? -- Scale multiplier for 9-patch corners/edges. E.g., 2 = 2x size (overrides theme setting) +---@field scalingAlgorithm "nearest"|"bilinear"? -- Scaling algorithm for 9-patch corners: "nearest" (sharp/pixelated) or "bilinear" (smooth) (overrides theme setting) +---@field contentBlur {intensity:number, quality:number}? -- Blur the element's content including children (intensity: 0-100, quality: 1-10, default: nil) +---@field backdropBlur {intensity:number, quality:number}? -- Blur content behind the element (intensity: 0-100, quality: 1-10, default: nil) +---@field editable boolean? -- Whether the element is editable (default: false) +---@field multiline boolean? -- Whether the element supports multiple lines (default: false) +---@field textWrap boolean|"word"|"char"? -- Text wrapping mode (default: false for single-line, "word" for multi-line) +---@field maxLines number? -- Maximum number of lines (default: nil) +---@field maxLength number? -- Maximum text length in characters (default: nil) +---@field placeholder string? -- Placeholder text when empty (default: nil) +---@field passwordMode boolean? -- Whether to display text as password (default: false, disables multiline) +---@field inputType "text"|"number"|"email"|"url"? -- Input type for validation (default: "text") +---@field textOverflow "clip"|"ellipsis"|"scroll"? -- Text overflow behavior (default: "clip") +---@field scrollable boolean? -- Whether text is scrollable (default: false for single-line, true for multi-line) +---@field autoGrow boolean? -- Whether element auto-grows with text (default: false for single-line, true for multi-line) +---@field selectOnFocus boolean? -- Whether to select all text on focus (default: false) +---@field cursorColor Color? -- Cursor color (default: nil, uses textColor) +---@field selectionColor Color? -- Selection background color (default: nil, uses theme or default) +---@field cursorBlinkRate number? -- Cursor blink rate in seconds (default: 0.5) +---@field overflow "visible"|"hidden"|"scroll"|"auto"? -- Overflow behavior (default: "hidden") +---@field overflowX "visible"|"hidden"|"scroll"|"auto"? -- X-axis overflow (overrides overflow) +---@field overflowY "visible"|"hidden"|"scroll"|"auto"? -- Y-axis overflow (overrides overflow) +---@field scrollbarWidth number? -- Width of scrollbar track in pixels (default: 12) +---@field scrollbarColor Color? -- Scrollbar thumb color (default: Color.new(0.5, 0.5, 0.5, 0.8)) +---@field scrollbarTrackColor Color? -- Scrollbar track color (default: Color.new(0.2, 0.2, 0.2, 0.5)) +---@field scrollbarRadius number? -- Corner radius for scrollbar (default: 6) +---@field scrollbarPadding number? -- Padding between scrollbar and edge (default: 2) +---@field scrollSpeed number? -- Pixels per wheel notch (default: 20) +---@field hideScrollbars boolean|{vertical:boolean, horizontal:boolean}? -- Hide scrollbars (boolean for both, or table for individual control, default: false) +---@field imagePath string? -- Path to image file (auto-loads via ImageCache) +---@field image love.Image? -- Image object to display +---@field objectFit "fill"|"contain"|"cover"|"scale-down"|"none"? -- Image fit mode (default: "fill") +---@field objectPosition string? -- Image position like "center center", "top left", "50% 50%" (default: "center center") +---@field imageOpacity number? -- Image opacity 0-1 (default: 1, combines with element opacity) +---@field _scrollX number? -- Internal: scroll X position (restored in immediate mode) +---@field _scrollY number? -- Internal: scroll Y position (restored in immediate mode) +---@field userdata table? -- User-defined data storage for custom properties +local ElementProps = {} + +---@class Border +---@field top boolean +---@field right boolean +---@field bottom boolean +---@field left boolean +local Border = {} diff --git a/modules/utils.lua b/modules/utils.lua index 3c2bf09..7f7502d 100644 --- a/modules/utils.lua +++ b/modules/utils.lua @@ -1,103 +1,3 @@ ----@class ElementProps ----@field id string? -- Unique identifier for the element (auto-generated in immediate mode if not provided) ----@field parent Element? -- Parent element for hierarchical structure ----@field x number|string? -- X coordinate of the element (default: 0) ----@field y number|string? -- Y coordinate of the element (default: 0) ----@field z number? -- Z-index for layering (default: 0) ----@field width number|string? -- Width of the element (default: calculated automatically) ----@field height number|string? -- Height of the element (default: calculated automatically) ----@field top number|string? -- Offset from top edge (CSS-style positioning) ----@field right number|string? -- Offset from right edge (CSS-style positioning) ----@field bottom number|string? -- Offset from bottom edge (CSS-style positioning) ----@field left number|string? -- Offset from left edge (CSS-style positioning) ----@field border Border? -- Border configuration for the element ----@field borderColor Color? -- Color of the border (default: black) ----@field opacity number? -- Element opacity 0-1 (default: 1) ----@field backgroundColor Color? -- Background color (default: transparent) ----@field cornerRadius number|{topLeft:number?, topRight:number?, bottomLeft:number?, bottomRight:number?}? -- Corner radius: number (all corners) or table for individual corners (default: 0) ----@field gap number|string? -- Space between children elements (default: 0) ----@field padding {top:number|string?, right:number|string?, bottom:number|string?, left:number|string?, horizontal:number|string?, vertical:number|string?}? -- Padding around children (default: {top=0, right=0, bottom=0, left=0}) ----@field margin {top:number|string?, right:number|string?, bottom:number|string?, left:number|string?, horizontal:number|string?, vertical:number|string?}? -- Margin around element (default: {top=0, right=0, bottom=0, left=0}) ----@field text string? -- Text content to display (default: nil) ----@field textAlign TextAlign? -- Alignment of the text content (default: START) ----@field textColor Color? -- Color of the text content (default: black or theme text color) ----@field textSize number|string? -- Font size: number (px), string with units ("2vh", "10%"), or preset ("xxs"|"xs"|"sm"|"md"|"lg"|"xl"|"xxl"|"3xl"|"4xl") (default: "md" or 12px) ----@field minTextSize number? -- Minimum text size in pixels for auto-scaling ----@field maxTextSize number? -- Maximum text size in pixels for auto-scaling ----@field fontFamily string? -- Font family name from theme or path to font file (default: theme default or system default, inherits from parent) ----@field autoScaleText boolean? -- Whether text should auto-scale with window size (default: true) ----@field positioning Positioning? -- Layout positioning mode: "absolute"|"relative"|"flex"|"grid" (default: RELATIVE) ----@field flexDirection FlexDirection? -- Direction of flex layout: "horizontal"|"vertical" (default: HORIZONTAL) ----@field justifyContent JustifyContent? -- Alignment of items along main axis (default: FLEX_START) ----@field alignItems AlignItems? -- Alignment of items along cross axis (default: STRETCH) ----@field alignContent AlignContent? -- Alignment of lines in multi-line flex containers (default: STRETCH) ----@field flexWrap FlexWrap? -- Whether children wrap to multiple lines: "nowrap"|"wrap"|"wrap-reverse" (default: NOWRAP) ----@field justifySelf JustifySelf? -- Alignment of the item itself along main axis (default: AUTO) ----@field alignSelf AlignSelf? -- Alignment of the item itself along cross axis (default: AUTO) ----@field onEvent fun(element:Element, event:InputEvent)? -- Callback function for interaction events ----@field onFocus fun(element:Element, event:InputEvent)? -- Callback when element receives focus ----@field onBlur fun(element:Element, event:InputEvent)? -- Callback when element loses focus ----@field onTextInput fun(element:Element, text:string)? -- Callback when text is input ----@field onTextChange fun(element:Element, text:string)? -- Callback when text content changes ----@field onEnter fun(element:Element)? -- Callback when Enter key is pressed ----@field transform TransformProps? -- Transform properties for animations and styling ----@field transition TransitionProps? -- Transition settings for animations ----@field gridRows number? -- Number of rows in the grid (default: 1) ----@field gridColumns number? -- Number of columns in the grid (default: 1) ----@field columnGap number|string? -- Gap between grid columns (default: 0) ----@field rowGap number|string? -- Gap between grid rows (default: 0) ----@field theme string? -- Theme name to use (e.g., "space", "metal"). Defaults to theme from Gui.init() ----@field themeComponent string? -- Theme component to use (e.g., "panel", "button", "input"). If nil, no theme is applied ----@field disabled boolean? -- Whether the element is disabled (default: false) ----@field active boolean? -- Whether the element is active/focused (for inputs, default: false) ----@field disableHighlight boolean? -- Whether to disable the pressed state highlight overlay (default: false, or true when using themeComponent) ----@field contentAutoSizingMultiplier {width:number?, height:number?}? -- Multiplier for auto-sized content dimensions (default: sourced from theme or {1, 1}) ----@field scaleCorners number? -- Scale multiplier for 9-patch corners/edges. E.g., 2 = 2x size (overrides theme setting) ----@field scalingAlgorithm "nearest"|"bilinear"? -- Scaling algorithm for 9-patch corners: "nearest" (sharp/pixelated) or "bilinear" (smooth) (overrides theme setting) ----@field contentBlur {intensity:number, quality:number}? -- Blur the element's content including children (intensity: 0-100, quality: 1-10, default: nil) ----@field backdropBlur {intensity:number, quality:number}? -- Blur content behind the element (intensity: 0-100, quality: 1-10, default: nil) ----@field editable boolean? -- Whether the element is editable (default: false) ----@field multiline boolean? -- Whether the element supports multiple lines (default: false) ----@field textWrap boolean|"word"|"char"? -- Text wrapping mode (default: false for single-line, "word" for multi-line) ----@field maxLines number? -- Maximum number of lines (default: nil) ----@field maxLength number? -- Maximum text length in characters (default: nil) ----@field placeholder string? -- Placeholder text when empty (default: nil) ----@field passwordMode boolean? -- Whether to display text as password (default: false, disables multiline) ----@field inputType "text"|"number"|"email"|"url"? -- Input type for validation (default: "text") ----@field textOverflow "clip"|"ellipsis"|"scroll"? -- Text overflow behavior (default: "clip") ----@field scrollable boolean? -- Whether text is scrollable (default: false for single-line, true for multi-line) ----@field autoGrow boolean? -- Whether element auto-grows with text (default: false for single-line, true for multi-line) ----@field selectOnFocus boolean? -- Whether to select all text on focus (default: false) ----@field cursorColor Color? -- Cursor color (default: nil, uses textColor) ----@field selectionColor Color? -- Selection background color (default: nil, uses theme or default) ----@field cursorBlinkRate number? -- Cursor blink rate in seconds (default: 0.5) ----@field overflow "visible"|"hidden"|"scroll"|"auto"? -- Overflow behavior (default: "hidden") ----@field overflowX "visible"|"hidden"|"scroll"|"auto"? -- X-axis overflow (overrides overflow) ----@field overflowY "visible"|"hidden"|"scroll"|"auto"? -- Y-axis overflow (overrides overflow) ----@field scrollbarWidth number? -- Width of scrollbar track in pixels (default: 12) ----@field scrollbarColor Color? -- Scrollbar thumb color (default: Color.new(0.5, 0.5, 0.5, 0.8)) ----@field scrollbarTrackColor Color? -- Scrollbar track color (default: Color.new(0.2, 0.2, 0.2, 0.5)) ----@field scrollbarRadius number? -- Corner radius for scrollbar (default: 6) ----@field scrollbarPadding number? -- Padding between scrollbar and edge (default: 2) ----@field scrollSpeed number? -- Pixels per wheel notch (default: 20) ----@field hideScrollbars boolean|{vertical:boolean, horizontal:boolean}? -- Hide scrollbars (boolean for both, or table for individual control, default: false) ----@field imagePath string? -- Path to image file (auto-loads via ImageCache) ----@field image love.Image? -- Image object to display ----@field objectFit "fill"|"contain"|"cover"|"scale-down"|"none"? -- Image fit mode (default: "fill") ----@field objectPosition string? -- Image position like "center center", "top left", "50% 50%" (default: "center center") ----@field imageOpacity number? -- Image opacity 0-1 (default: 1, combines with element opacity) ----@field _scrollX number? -- Internal: scroll X position (restored in immediate mode) ----@field _scrollY number? -- Internal: scroll Y position (restored in immediate mode) ----@field userdata table? -- User-defined data storage for custom properties -local ElementProps = {} - ----@class Border ----@field top boolean ----@field right boolean ----@field bottom boolean ----@field left boolean -local Border = {} - local enums = { ---@enum TextAlign TextAlign = { START = "start", CENTER = "center", END = "end", JUSTIFY = "justify" },