cleanup
This commit is contained in:
@@ -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
|
--- Easing functions for animations
|
||||||
local Easing = {
|
local Easing = {
|
||||||
linear = function(t)
|
linear = function(t)
|
||||||
@@ -47,27 +40,15 @@ local Easing = {
|
|||||||
return t == 1 and 1 or 1 - math.pow(2, -10 * t)
|
return t == 1 and 1 or 1 - math.pow(2, -10 * t)
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
|
---@class Animation
|
||||||
local Animation = {}
|
|
||||||
Animation.__index = Animation
|
|
||||||
|
|
||||||
---@class AnimationProps
|
|
||||||
---@field duration number
|
---@field duration number
|
||||||
---@field start {width?:number, height?:number, opacity?:number}
|
---@field start {width?:number, height?:number, opacity?:number}
|
||||||
---@field final {width?:number, height?:number, opacity?:number}
|
---@field final {width?:number, height?:number, opacity?:number}
|
||||||
|
---@field elapsed number
|
||||||
---@field transform table?
|
---@field transform table?
|
||||||
---@field transition table?
|
---@field transition table?
|
||||||
local AnimationProps = {}
|
local Animation = {}
|
||||||
|
Animation.__index = Animation
|
||||||
---@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?
|
|
||||||
|
|
||||||
---@param props AnimationProps
|
---@param props AnimationProps
|
||||||
---@return Animation
|
---@return Animation
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
--[[
|
|
||||||
FlexLove Blur Module
|
|
||||||
Fast Gaussian blur implementation with canvas caching
|
|
||||||
]]
|
|
||||||
|
|
||||||
local Blur = {}
|
local Blur = {}
|
||||||
|
|
||||||
-- Canvas cache to avoid recreating canvases every frame
|
-- Canvas cache to avoid recreating canvases every frame
|
||||||
|
|||||||
@@ -1,11 +1,3 @@
|
|||||||
--[[
|
|
||||||
Provides color handling with RGB/RGBA support and hex string conversion
|
|
||||||
]]
|
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- Error Handling Utilities
|
|
||||||
-- ====================
|
|
||||||
|
|
||||||
--- Standardized error message formatter
|
--- Standardized error message formatter
|
||||||
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
||||||
---@param message string -- Error message
|
---@param message string -- Error message
|
||||||
|
|||||||
@@ -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()
|
local function getModifiers()
|
||||||
return {
|
return {
|
||||||
shift = love.keyboard.isDown("lshift") or love.keyboard.isDown("rshift"),
|
shift = love.keyboard.isDown("lshift") or love.keyboard.isDown("rshift"),
|
||||||
@@ -33,42 +17,42 @@ EventHandler.__index = EventHandler
|
|||||||
---@return EventHandler
|
---@return EventHandler
|
||||||
function EventHandler.new(config, deps)
|
function EventHandler.new(config, deps)
|
||||||
config = config or {}
|
config = config or {}
|
||||||
|
|
||||||
local self = setmetatable({}, EventHandler)
|
local self = setmetatable({}, EventHandler)
|
||||||
|
|
||||||
-- Store dependencies
|
-- Store dependencies
|
||||||
self._InputEvent = deps.InputEvent
|
self._InputEvent = deps.InputEvent
|
||||||
self._GuiState = deps.GuiState
|
self._GuiState = deps.GuiState
|
||||||
|
|
||||||
-- Event callback
|
-- Event callback
|
||||||
self.onEvent = config.onEvent
|
self.onEvent = config.onEvent
|
||||||
|
|
||||||
-- Mouse button state tracking {button -> boolean}
|
-- Mouse button state tracking {button -> boolean}
|
||||||
self._pressed = config._pressed or {}
|
self._pressed = config._pressed or {}
|
||||||
|
|
||||||
-- Click detection state
|
-- Click detection state
|
||||||
self._lastClickTime = config._lastClickTime
|
self._lastClickTime = config._lastClickTime
|
||||||
self._lastClickButton = config._lastClickButton
|
self._lastClickButton = config._lastClickButton
|
||||||
self._clickCount = config._clickCount or 0
|
self._clickCount = config._clickCount or 0
|
||||||
|
|
||||||
-- Drag tracking per button {button -> position}
|
-- Drag tracking per button {button -> position}
|
||||||
self._dragStartX = config._dragStartX or {}
|
self._dragStartX = config._dragStartX or {}
|
||||||
self._dragStartY = config._dragStartY or {}
|
self._dragStartY = config._dragStartY or {}
|
||||||
self._lastMouseX = config._lastMouseX or {}
|
self._lastMouseX = config._lastMouseX or {}
|
||||||
self._lastMouseY = config._lastMouseY or {}
|
self._lastMouseY = config._lastMouseY or {}
|
||||||
|
|
||||||
-- Touch state tracking {touchId -> boolean}
|
-- Touch state tracking {touchId -> boolean}
|
||||||
self._touchPressed = config._touchPressed or {}
|
self._touchPressed = config._touchPressed or {}
|
||||||
|
|
||||||
-- Hover state
|
-- Hover state
|
||||||
self._hovered = config._hovered or false
|
self._hovered = config._hovered or false
|
||||||
|
|
||||||
-- Reference to parent element (set via initialize)
|
-- Reference to parent element (set via initialize)
|
||||||
self._element = nil
|
self._element = nil
|
||||||
|
|
||||||
-- Scrollbar press tracking flag
|
-- Scrollbar press tracking flag
|
||||||
self._scrollbarPressHandled = false
|
self._scrollbarPressHandled = false
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -97,8 +81,10 @@ end
|
|||||||
--- Restore state from persistence (for immediate mode)
|
--- Restore state from persistence (for immediate mode)
|
||||||
---@param state table State data
|
---@param state table State data
|
||||||
function EventHandler:setState(state)
|
function EventHandler:setState(state)
|
||||||
if not state then return end
|
if not state then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
self._pressed = state._pressed or {}
|
self._pressed = state._pressed or {}
|
||||||
self._lastClickTime = state._lastClickTime
|
self._lastClickTime = state._lastClickTime
|
||||||
self._lastClickButton = state._lastClickButton
|
self._lastClickButton = state._lastClickButton
|
||||||
@@ -119,9 +105,9 @@ function EventHandler:processMouseEvents(mx, my, isHovering, isActiveElement)
|
|||||||
if not self._element then
|
if not self._element then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local element = self._element
|
local element = self._element
|
||||||
|
|
||||||
-- Check if currently dragging (allows drag continuation even if occluded)
|
-- Check if currently dragging (allows drag continuation even if occluded)
|
||||||
local isDragging = false
|
local isDragging = false
|
||||||
for _, button in ipairs({ 1, 2, 3 }) do
|
for _, button in ipairs({ 1, 2, 3 }) do
|
||||||
@@ -130,17 +116,17 @@ function EventHandler:processMouseEvents(mx, my, isHovering, isActiveElement)
|
|||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Can only process events if we have handler, element is enabled, and is active or dragging
|
-- 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)
|
local canProcessEvents = (self.onEvent or element.editable) and not element.disabled and (isActiveElement or isDragging)
|
||||||
|
|
||||||
if not canProcessEvents then
|
if not canProcessEvents then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Process all three mouse buttons
|
-- Process all three mouse buttons
|
||||||
local buttons = { 1, 2, 3 } -- left, right, middle
|
local buttons = { 1, 2, 3 } -- left, right, middle
|
||||||
|
|
||||||
for _, button in ipairs(buttons) do
|
for _, button in ipairs(buttons) do
|
||||||
if isHovering or isDragging then
|
if isHovering or isDragging then
|
||||||
if love.mouse.isDown(button) then
|
if love.mouse.isDown(button) then
|
||||||
@@ -172,10 +158,12 @@ end
|
|||||||
---@param my number Mouse Y position
|
---@param my number Mouse Y position
|
||||||
---@param button number Mouse button (1=left, 2=right, 3=middle)
|
---@param button number Mouse button (1=left, 2=right, 3=middle)
|
||||||
function EventHandler:_handleMousePress(mx, my, button)
|
function EventHandler:_handleMousePress(mx, my, button)
|
||||||
if not self._element then return end
|
if not self._element then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local element = self._element
|
local element = self._element
|
||||||
|
|
||||||
-- Check if press is on scrollbar first (skip if already handled)
|
-- Check if press is on scrollbar first (skip if already handled)
|
||||||
if button == 1 and not self._scrollbarPressHandled and element._handleScrollbarPress then
|
if button == 1 and not self._scrollbarPressHandled and element._handleScrollbarPress then
|
||||||
if element:_handleScrollbarPress(mx, my, button) then
|
if element:_handleScrollbarPress(mx, my, button) then
|
||||||
@@ -185,7 +173,7 @@ function EventHandler:_handleMousePress(mx, my, button)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Fire press event
|
-- Fire press event
|
||||||
if self.onEvent then
|
if self.onEvent then
|
||||||
local modifiers = getModifiers()
|
local modifiers = getModifiers()
|
||||||
@@ -199,15 +187,15 @@ function EventHandler:_handleMousePress(mx, my, button)
|
|||||||
})
|
})
|
||||||
self.onEvent(element, pressEvent)
|
self.onEvent(element, pressEvent)
|
||||||
end
|
end
|
||||||
|
|
||||||
self._pressed[button] = true
|
self._pressed[button] = true
|
||||||
|
|
||||||
-- Set mouse down position for text selection on left click
|
-- Set mouse down position for text selection on left click
|
||||||
if button == 1 and element._textEditor then
|
if button == 1 and element._textEditor then
|
||||||
element._mouseDownPosition = element._textEditor:mouseToTextPosition(mx, my)
|
element._mouseDownPosition = element._textEditor:mouseToTextPosition(mx, my)
|
||||||
element._textDragOccurred = false -- Reset drag flag on press
|
element._textDragOccurred = false -- Reset drag flag on press
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Record drag start position per button
|
-- Record drag start position per button
|
||||||
self._dragStartX[button] = mx
|
self._dragStartX[button] = mx
|
||||||
self._dragStartY[button] = my
|
self._dragStartY[button] = my
|
||||||
@@ -221,20 +209,22 @@ end
|
|||||||
---@param button number Mouse button
|
---@param button number Mouse button
|
||||||
---@param isHovering boolean Whether mouse is over element
|
---@param isHovering boolean Whether mouse is over element
|
||||||
function EventHandler:_handleMouseDrag(mx, my, button, isHovering)
|
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 element = self._element
|
||||||
|
|
||||||
local lastX = self._lastMouseX[button] or mx
|
local lastX = self._lastMouseX[button] or mx
|
||||||
local lastY = self._lastMouseY[button] or my
|
local lastY = self._lastMouseY[button] or my
|
||||||
|
|
||||||
if lastX ~= mx or lastY ~= my then
|
if lastX ~= mx or lastY ~= my then
|
||||||
-- Mouse has moved - fire drag event only if still hovering
|
-- Mouse has moved - fire drag event only if still hovering
|
||||||
if self.onEvent and isHovering then
|
if self.onEvent and isHovering then
|
||||||
local modifiers = getModifiers()
|
local modifiers = getModifiers()
|
||||||
local dx = mx - self._dragStartX[button]
|
local dx = mx - self._dragStartX[button]
|
||||||
local dy = my - self._dragStartY[button]
|
local dy = my - self._dragStartY[button]
|
||||||
|
|
||||||
local dragEvent = self._InputEvent.new({
|
local dragEvent = self._InputEvent.new({
|
||||||
type = "drag",
|
type = "drag",
|
||||||
button = button,
|
button = button,
|
||||||
@@ -247,12 +237,12 @@ function EventHandler:_handleMouseDrag(mx, my, button, isHovering)
|
|||||||
})
|
})
|
||||||
self.onEvent(element, dragEvent)
|
self.onEvent(element, dragEvent)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Handle text selection drag for editable elements
|
-- Handle text selection drag for editable elements
|
||||||
if button == 1 and element.editable and element._focused and element._handleTextDrag then
|
if button == 1 and element.editable and element._focused and element._handleTextDrag then
|
||||||
element:_handleTextDrag(mx, my)
|
element:_handleTextDrag(mx, my)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Update last known position for this button
|
-- Update last known position for this button
|
||||||
self._lastMouseX[button] = mx
|
self._lastMouseX[button] = mx
|
||||||
self._lastMouseY[button] = my
|
self._lastMouseY[button] = my
|
||||||
@@ -264,27 +254,29 @@ end
|
|||||||
---@param my number Mouse Y position
|
---@param my number Mouse Y position
|
||||||
---@param button number Mouse button
|
---@param button number Mouse button
|
||||||
function EventHandler:_handleMouseRelease(mx, my, 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 element = self._element
|
||||||
|
|
||||||
local currentTime = love.timer.getTime()
|
local currentTime = love.timer.getTime()
|
||||||
local modifiers = getModifiers()
|
local modifiers = getModifiers()
|
||||||
|
|
||||||
-- Determine click count (double-click detection)
|
-- Determine click count (double-click detection)
|
||||||
local clickCount = 1
|
local clickCount = 1
|
||||||
local doubleClickThreshold = 0.3 -- 300ms for double-click
|
local doubleClickThreshold = 0.3 -- 300ms for double-click
|
||||||
|
|
||||||
if self._lastClickTime and self._lastClickButton == button and (currentTime - self._lastClickTime) < doubleClickThreshold then
|
if self._lastClickTime and self._lastClickButton == button and (currentTime - self._lastClickTime) < doubleClickThreshold then
|
||||||
clickCount = self._clickCount + 1
|
clickCount = self._clickCount + 1
|
||||||
else
|
else
|
||||||
clickCount = 1
|
clickCount = 1
|
||||||
end
|
end
|
||||||
|
|
||||||
self._clickCount = clickCount
|
self._clickCount = clickCount
|
||||||
self._lastClickTime = currentTime
|
self._lastClickTime = currentTime
|
||||||
self._lastClickButton = button
|
self._lastClickButton = button
|
||||||
|
|
||||||
-- Determine event type based on button
|
-- Determine event type based on button
|
||||||
local eventType = "click"
|
local eventType = "click"
|
||||||
if button == 2 then
|
if button == 2 then
|
||||||
@@ -292,7 +284,7 @@ function EventHandler:_handleMouseRelease(mx, my, button)
|
|||||||
elseif button == 3 then
|
elseif button == 3 then
|
||||||
eventType = "middleclick"
|
eventType = "middleclick"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Fire click event
|
-- Fire click event
|
||||||
if self.onEvent then
|
if self.onEvent then
|
||||||
local clickEvent = self._InputEvent.new({
|
local clickEvent = self._InputEvent.new({
|
||||||
@@ -305,18 +297,18 @@ function EventHandler:_handleMouseRelease(mx, my, button)
|
|||||||
})
|
})
|
||||||
self.onEvent(element, clickEvent)
|
self.onEvent(element, clickEvent)
|
||||||
end
|
end
|
||||||
|
|
||||||
self._pressed[button] = false
|
self._pressed[button] = false
|
||||||
|
|
||||||
-- Clean up drag tracking
|
-- Clean up drag tracking
|
||||||
self._dragStartX[button] = nil
|
self._dragStartX[button] = nil
|
||||||
self._dragStartY[button] = nil
|
self._dragStartY[button] = nil
|
||||||
|
|
||||||
-- Clean up text selection drag tracking
|
-- Clean up text selection drag tracking
|
||||||
if button == 1 then
|
if button == 1 then
|
||||||
element._mouseDownPosition = nil
|
element._mouseDownPosition = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Focus editable elements on left click
|
-- Focus editable elements on left click
|
||||||
if button == 1 and element.editable then
|
if button == 1 and element.editable then
|
||||||
-- Only focus if not already focused (to avoid moving cursor to end)
|
-- 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
|
if not wasFocused then
|
||||||
element:focus()
|
element:focus()
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Handle text click for cursor positioning and word selection
|
-- Handle text click for cursor positioning and word selection
|
||||||
-- Only process click if no text drag occurred (to preserve drag selection)
|
-- Only process click if no text drag occurred (to preserve drag selection)
|
||||||
if element._handleTextClick and not element._textDragOccurred then
|
if element._handleTextClick and not element._textDragOccurred then
|
||||||
element:_handleTextClick(mx, my, clickCount)
|
element:_handleTextClick(mx, my, clickCount)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Reset drag flag after release
|
-- Reset drag flag after release
|
||||||
element._textDragOccurred = false
|
element._textDragOccurred = false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Fire release event
|
-- Fire release event
|
||||||
if self.onEvent then
|
if self.onEvent then
|
||||||
local releaseEvent = self._InputEvent.new({
|
local releaseEvent = self._InputEvent.new({
|
||||||
@@ -354,14 +346,14 @@ function EventHandler:processTouchEvents()
|
|||||||
if not self._element or not self.onEvent then
|
if not self._element or not self.onEvent then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local element = self._element
|
local element = self._element
|
||||||
|
|
||||||
local bx = element.x
|
local bx = element.x
|
||||||
local by = element.y
|
local by = element.y
|
||||||
local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
|
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 bh = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom)
|
||||||
|
|
||||||
local touches = love.touch.getTouches()
|
local touches = love.touch.getTouches()
|
||||||
for _, id in ipairs(touches) do
|
for _, id in ipairs(touches) do
|
||||||
local tx, ty = love.touch.getPosition(id)
|
local tx, ty = love.touch.getPosition(id)
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
-- ====================
|
|
||||||
-- Grid Layout System
|
|
||||||
-- ====================
|
|
||||||
|
|
||||||
local modulePath = (...):match("(.-)[^%.]+$")
|
local modulePath = (...):match("(.-)[^%.]+$")
|
||||||
local utils = require(modulePath .. "utils")
|
local utils = require(modulePath .. "utils")
|
||||||
local enums = utils.enums
|
local enums = utils.enums
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
-- ====================
|
|
||||||
-- GUI State Module
|
|
||||||
-- ====================
|
|
||||||
-- Shared state between Gui and Element to avoid circular dependencies
|
|
||||||
|
|
||||||
---@class GuiState
|
---@class GuiState
|
||||||
local GuiState = {
|
local GuiState = {
|
||||||
-- Top-level elements
|
-- Top-level elements
|
||||||
|
|||||||
@@ -1,12 +1,3 @@
|
|||||||
--[[
|
|
||||||
ImageCache.lua - Image caching system for FlexLove
|
|
||||||
Provides efficient image loading and caching with memory management
|
|
||||||
]]
|
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- ImageCache
|
|
||||||
-- ====================
|
|
||||||
|
|
||||||
---@class ImageCache
|
---@class ImageCache
|
||||||
---@field _cache table<string, {image: love.Image, imageData: love.ImageData?}>
|
---@field _cache table<string, {image: love.Image, imageData: love.ImageData?}>
|
||||||
local ImageCache = {}
|
local ImageCache = {}
|
||||||
|
|||||||
@@ -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
|
--- Standardized error message formatter
|
||||||
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
||||||
---@param message string -- Error message
|
---@param message string -- Error message
|
||||||
|
|||||||
@@ -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
|
--- Standardized error message formatter
|
||||||
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
||||||
---@param message string -- Error message
|
---@param message string -- Error message
|
||||||
|
|||||||
@@ -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
|
--- Standardized error message formatter
|
||||||
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
||||||
---@param message string -- Error message
|
---@param message string -- Error message
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
-- ====================
|
|
||||||
-- Input Event System
|
|
||||||
-- ====================
|
|
||||||
|
|
||||||
---@class InputEvent
|
---@class InputEvent
|
||||||
---@field type "click"|"press"|"release"|"rightclick"|"middleclick"|"drag"
|
---@field type "click"|"press"|"release"|"rightclick"|"middleclick"|"drag"
|
||||||
---@field button number -- Mouse button: 1 (left), 2 (right), 3 (middle)
|
---@field button number -- Mouse button: 1 (left), 2 (right), 3 (middle)
|
||||||
|
|||||||
@@ -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
|
---@class LayoutEngine
|
||||||
---@field element Element Reference to the parent element
|
---@field element Element Reference to the parent element
|
||||||
---@field positioning Positioning Layout positioning mode
|
---@field positioning Positioning Layout positioning mode
|
||||||
|
|||||||
@@ -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 modulePath = (...):match("(.-)[^%.]+$")
|
||||||
local ImageScaler = require(modulePath .. "ImageScaler")
|
local ImageScaler = require(modulePath .. "ImageScaler")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
--- Standardized error message formatter
|
||||||
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
||||||
---@param message string -- Error message
|
---@param message string -- Error message
|
||||||
|
|||||||
@@ -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 = {}
|
local Renderer = {}
|
||||||
Renderer.__index = Renderer
|
Renderer.__index = Renderer
|
||||||
|
|
||||||
@@ -25,9 +8,9 @@ Renderer.__index = Renderer
|
|||||||
function Renderer.new(config, deps)
|
function Renderer.new(config, deps)
|
||||||
local Color = deps.Color
|
local Color = deps.Color
|
||||||
local ImageCache = deps.ImageCache
|
local ImageCache = deps.ImageCache
|
||||||
|
|
||||||
local self = setmetatable({}, Renderer)
|
local self = setmetatable({}, Renderer)
|
||||||
|
|
||||||
-- Store dependencies for instance methods
|
-- Store dependencies for instance methods
|
||||||
self._Color = Color
|
self._Color = Color
|
||||||
self._RoundedRect = deps.RoundedRect
|
self._RoundedRect = deps.RoundedRect
|
||||||
@@ -39,36 +22,36 @@ function Renderer.new(config, deps)
|
|||||||
self._utils = deps.utils
|
self._utils = deps.utils
|
||||||
self._FONT_CACHE = deps.utils.FONT_CACHE
|
self._FONT_CACHE = deps.utils.FONT_CACHE
|
||||||
self._TextAlign = deps.utils.enums.TextAlign
|
self._TextAlign = deps.utils.enums.TextAlign
|
||||||
|
|
||||||
-- Store reference to parent element (will be set via initialize)
|
-- Store reference to parent element (will be set via initialize)
|
||||||
self._element = nil
|
self._element = nil
|
||||||
|
|
||||||
-- Visual properties
|
-- Visual properties
|
||||||
self.backgroundColor = config.backgroundColor or Color.new(0, 0, 0, 0)
|
self.backgroundColor = config.backgroundColor or Color.new(0, 0, 0, 0)
|
||||||
self.borderColor = config.borderColor or Color.new(0, 0, 0, 1)
|
self.borderColor = config.borderColor or Color.new(0, 0, 0, 1)
|
||||||
self.opacity = config.opacity or 1
|
self.opacity = config.opacity or 1
|
||||||
|
|
||||||
-- Border configuration
|
-- Border configuration
|
||||||
self.border = config.border or {
|
self.border = config.border or {
|
||||||
top = false,
|
top = false,
|
||||||
right = false,
|
right = false,
|
||||||
bottom = false,
|
bottom = false,
|
||||||
left = false
|
left = false,
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Corner radius
|
-- Corner radius
|
||||||
self.cornerRadius = config.cornerRadius or {
|
self.cornerRadius = config.cornerRadius or {
|
||||||
topLeft = 0,
|
topLeft = 0,
|
||||||
topRight = 0,
|
topRight = 0,
|
||||||
bottomLeft = 0,
|
bottomLeft = 0,
|
||||||
bottomRight = 0
|
bottomRight = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Theme properties
|
-- Theme properties
|
||||||
self.theme = config.theme
|
self.theme = config.theme
|
||||||
self.themeComponent = config.themeComponent
|
self.themeComponent = config.themeComponent
|
||||||
self._themeState = "normal"
|
self._themeState = "normal"
|
||||||
|
|
||||||
-- Image properties
|
-- Image properties
|
||||||
self.imagePath = config.imagePath
|
self.imagePath = config.imagePath
|
||||||
self.image = config.image
|
self.image = config.image
|
||||||
@@ -76,12 +59,12 @@ function Renderer.new(config, deps)
|
|||||||
self.objectFit = config.objectFit or "fill"
|
self.objectFit = config.objectFit or "fill"
|
||||||
self.objectPosition = config.objectPosition or "center center"
|
self.objectPosition = config.objectPosition or "center center"
|
||||||
self.imageOpacity = config.imageOpacity or 1
|
self.imageOpacity = config.imageOpacity or 1
|
||||||
|
|
||||||
-- Blur effects
|
-- Blur effects
|
||||||
self.contentBlur = config.contentBlur
|
self.contentBlur = config.contentBlur
|
||||||
self.backdropBlur = config.backdropBlur
|
self.backdropBlur = config.backdropBlur
|
||||||
self._blurInstance = nil
|
self._blurInstance = nil
|
||||||
|
|
||||||
-- Load image if path provided
|
-- Load image if path provided
|
||||||
if self.imagePath and not self.image then
|
if self.imagePath and not self.image then
|
||||||
local loadedImage, err = ImageCache.load(self.imagePath)
|
local loadedImage, err = ImageCache.load(self.imagePath)
|
||||||
@@ -95,7 +78,7 @@ function Renderer.new(config, deps)
|
|||||||
else
|
else
|
||||||
self._loadedImage = nil
|
self._loadedImage = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -115,12 +98,12 @@ function Renderer:getBlurInstance()
|
|||||||
elseif self.backdropBlur and self.backdropBlur.quality then
|
elseif self.backdropBlur and self.backdropBlur.quality then
|
||||||
quality = self.backdropBlur.quality
|
quality = self.backdropBlur.quality
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Create or reuse blur instance
|
-- Create or reuse blur instance
|
||||||
if not self._blurInstance or self._blurInstance.quality ~= quality then
|
if not self._blurInstance or self._blurInstance.quality ~= quality then
|
||||||
self._blurInstance = self._Blur.new(quality)
|
self._blurInstance = self._Blur.new(quality)
|
||||||
end
|
end
|
||||||
|
|
||||||
return self._blurInstance
|
return self._blurInstance
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -137,12 +120,7 @@ end
|
|||||||
---@param height number Height
|
---@param height number Height
|
||||||
---@param drawBackgroundColor table Background color (may have animation applied)
|
---@param drawBackgroundColor table Background color (may have animation applied)
|
||||||
function Renderer:_drawBackground(x, y, width, height, drawBackgroundColor)
|
function Renderer:_drawBackground(x, y, width, height, drawBackgroundColor)
|
||||||
local backgroundWithOpacity = self._Color.new(
|
local backgroundWithOpacity = self._Color.new(drawBackgroundColor.r, drawBackgroundColor.g, drawBackgroundColor.b, drawBackgroundColor.a * self.opacity)
|
||||||
drawBackgroundColor.r,
|
|
||||||
drawBackgroundColor.g,
|
|
||||||
drawBackgroundColor.b,
|
|
||||||
drawBackgroundColor.a * self.opacity
|
|
||||||
)
|
|
||||||
love.graphics.setColor(backgroundWithOpacity:toRGBA())
|
love.graphics.setColor(backgroundWithOpacity:toRGBA())
|
||||||
self._RoundedRect.draw("fill", x, y, width, height, self.cornerRadius)
|
self._RoundedRect.draw("fill", x, y, width, height, self.cornerRadius)
|
||||||
end
|
end
|
||||||
@@ -160,22 +138,22 @@ function Renderer:_drawImage(x, y, paddingLeft, paddingTop, contentWidth, conten
|
|||||||
if not self._loadedImage then
|
if not self._loadedImage then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Calculate image bounds (content area - respects padding)
|
-- Calculate image bounds (content area - respects padding)
|
||||||
local imageX = x + paddingLeft
|
local imageX = x + paddingLeft
|
||||||
local imageY = y + paddingTop
|
local imageY = y + paddingTop
|
||||||
local imageWidth = contentWidth
|
local imageWidth = contentWidth
|
||||||
local imageHeight = contentHeight
|
local imageHeight = contentHeight
|
||||||
|
|
||||||
-- Combine element opacity with imageOpacity
|
-- Combine element opacity with imageOpacity
|
||||||
local finalOpacity = self.opacity * self.imageOpacity
|
local finalOpacity = self.opacity * self.imageOpacity
|
||||||
|
|
||||||
-- Apply cornerRadius clipping if set
|
-- Apply cornerRadius clipping if set
|
||||||
local hasCornerRadius = self.cornerRadius.topLeft > 0
|
local hasCornerRadius = self.cornerRadius.topLeft > 0
|
||||||
or self.cornerRadius.topRight > 0
|
or self.cornerRadius.topRight > 0
|
||||||
or self.cornerRadius.bottomLeft > 0
|
or self.cornerRadius.bottomLeft > 0
|
||||||
or self.cornerRadius.bottomRight > 0
|
or self.cornerRadius.bottomRight > 0
|
||||||
|
|
||||||
if hasCornerRadius then
|
if hasCornerRadius then
|
||||||
-- Use stencil to clip image to rounded corners
|
-- Use stencil to clip image to rounded corners
|
||||||
love.graphics.stencil(function()
|
love.graphics.stencil(function()
|
||||||
@@ -183,10 +161,10 @@ function Renderer:_drawImage(x, y, paddingLeft, paddingTop, contentWidth, conten
|
|||||||
end, "replace", 1)
|
end, "replace", 1)
|
||||||
love.graphics.setStencilTest("greater", 0)
|
love.graphics.setStencilTest("greater", 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Draw the image
|
-- Draw the image
|
||||||
self._ImageRenderer.draw(self._loadedImage, imageX, imageY, imageWidth, imageHeight, self.objectFit, self.objectPosition, finalOpacity)
|
self._ImageRenderer.draw(self._loadedImage, imageX, imageY, imageWidth, imageHeight, self.objectFit, self.objectPosition, finalOpacity)
|
||||||
|
|
||||||
-- Clear stencil if it was used
|
-- Clear stencil if it was used
|
||||||
if hasCornerRadius then
|
if hasCornerRadius then
|
||||||
love.graphics.setStencilTest()
|
love.graphics.setStencilTest()
|
||||||
@@ -204,7 +182,7 @@ function Renderer:_drawTheme(x, y, borderBoxWidth, borderBoxHeight, scaleCorners
|
|||||||
if not self.themeComponent then
|
if not self.themeComponent then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Get the theme to use
|
-- Get the theme to use
|
||||||
local themeToUse = nil
|
local themeToUse = nil
|
||||||
if self.theme then
|
if self.theme then
|
||||||
@@ -222,26 +200,26 @@ function Renderer:_drawTheme(x, y, borderBoxWidth, borderBoxHeight, scaleCorners
|
|||||||
-- Use active theme
|
-- Use active theme
|
||||||
themeToUse = self._Theme.getActive()
|
themeToUse = self._Theme.getActive()
|
||||||
end
|
end
|
||||||
|
|
||||||
if not themeToUse then
|
if not themeToUse then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Get the component from the theme
|
-- Get the component from the theme
|
||||||
local component = themeToUse.components[self.themeComponent]
|
local component = themeToUse.components[self.themeComponent]
|
||||||
if not component then
|
if not component then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check for state-specific override
|
-- Check for state-specific override
|
||||||
local state = self._themeState
|
local state = self._themeState
|
||||||
if state and component.states and component.states[state] then
|
if state and component.states and component.states[state] then
|
||||||
component = component.states[state]
|
component = component.states[state]
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Use component-specific atlas if available, otherwise use theme atlas
|
-- Use component-specific atlas if available, otherwise use theme atlas
|
||||||
local atlasToUse = component._loadedAtlas or themeToUse.atlas
|
local atlasToUse = component._loadedAtlas or themeToUse.atlas
|
||||||
|
|
||||||
if atlasToUse and component.regions then
|
if atlasToUse and component.regions then
|
||||||
-- Validate component has required structure
|
-- Validate component has required structure
|
||||||
local hasAllRegions = component.regions.topLeft
|
local hasAllRegions = component.regions.topLeft
|
||||||
@@ -253,7 +231,7 @@ function Renderer:_drawTheme(x, y, borderBoxWidth, borderBoxHeight, scaleCorners
|
|||||||
and component.regions.bottomLeft
|
and component.regions.bottomLeft
|
||||||
and component.regions.bottomCenter
|
and component.regions.bottomCenter
|
||||||
and component.regions.bottomRight
|
and component.regions.bottomRight
|
||||||
|
|
||||||
if hasAllRegions then
|
if hasAllRegions then
|
||||||
-- Pass element-level overrides for scaleCorners and scalingAlgorithm
|
-- Pass element-level overrides for scaleCorners and scalingAlgorithm
|
||||||
self._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)
|
||||||
@@ -267,17 +245,12 @@ end
|
|||||||
---@param borderBoxWidth number Border box width
|
---@param borderBoxWidth number Border box width
|
||||||
---@param borderBoxHeight number Border box height
|
---@param borderBoxHeight number Border box height
|
||||||
function Renderer:_drawBorders(x, y, borderBoxWidth, borderBoxHeight)
|
function Renderer:_drawBorders(x, y, borderBoxWidth, borderBoxHeight)
|
||||||
local borderColorWithOpacity = self._Color.new(
|
local borderColorWithOpacity = self._Color.new(self.borderColor.r, self.borderColor.g, self.borderColor.b, self.borderColor.a * self.opacity)
|
||||||
self.borderColor.r,
|
|
||||||
self.borderColor.g,
|
|
||||||
self.borderColor.b,
|
|
||||||
self.borderColor.a * self.opacity
|
|
||||||
)
|
|
||||||
love.graphics.setColor(borderColorWithOpacity:toRGBA())
|
love.graphics.setColor(borderColorWithOpacity:toRGBA())
|
||||||
|
|
||||||
-- Check if all borders are enabled
|
-- Check if all borders are enabled
|
||||||
local allBorders = self.border.top and self.border.bottom and self.border.left and self.border.right
|
local allBorders = self.border.top and self.border.bottom and self.border.left and self.border.right
|
||||||
|
|
||||||
if allBorders then
|
if allBorders then
|
||||||
-- Draw complete rounded rectangle border
|
-- Draw complete rounded rectangle border
|
||||||
self._RoundedRect.draw("line", x, y, borderBoxWidth, borderBoxHeight, self.cornerRadius)
|
self._RoundedRect.draw("line", x, y, borderBoxWidth, borderBoxHeight, self.cornerRadius)
|
||||||
@@ -305,14 +278,14 @@ function Renderer:draw(backdropCanvas)
|
|||||||
if self.opacity <= 0 then
|
if self.opacity <= 0 then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Element must be initialized before drawing
|
-- Element must be initialized before drawing
|
||||||
if not self._element then
|
if not self._element then
|
||||||
error("Renderer:draw() called before initialize(). Call renderer:initialize(element) first.")
|
error("Renderer:draw() called before initialize(). Call renderer:initialize(element) first.")
|
||||||
end
|
end
|
||||||
|
|
||||||
local element = self._element
|
local element = self._element
|
||||||
|
|
||||||
-- Handle opacity during animation
|
-- Handle opacity during animation
|
||||||
local drawBackgroundColor = self.backgroundColor
|
local drawBackgroundColor = self.backgroundColor
|
||||||
if element.animation then
|
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)
|
drawBackgroundColor = self._Color.new(self.backgroundColor.r, self.backgroundColor.g, self.backgroundColor.b, anim.opacity)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Cache border box dimensions for this draw call (optimization)
|
-- Cache border box dimensions for this draw call (optimization)
|
||||||
local borderBoxWidth = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
|
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)
|
local borderBoxHeight = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom)
|
||||||
|
|
||||||
-- LAYER 0.5: Draw backdrop blur if configured (before background)
|
-- LAYER 0.5: Draw backdrop blur if configured (before background)
|
||||||
if self.backdropBlur and self.backdropBlur.intensity > 0 and backdropCanvas then
|
if self.backdropBlur and self.backdropBlur.intensity > 0 and backdropCanvas then
|
||||||
local blurInstance = self:getBlurInstance()
|
local blurInstance = self:getBlurInstance()
|
||||||
@@ -333,25 +306,16 @@ function Renderer:draw(backdropCanvas)
|
|||||||
self._Blur.applyBackdrop(blurInstance, self.backdropBlur.intensity, element.x, element.y, borderBoxWidth, borderBoxHeight, backdropCanvas)
|
self._Blur.applyBackdrop(blurInstance, self.backdropBlur.intensity, element.x, element.y, borderBoxWidth, borderBoxHeight, backdropCanvas)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- LAYER 1: Draw backgroundColor first (behind everything)
|
-- LAYER 1: Draw backgroundColor first (behind everything)
|
||||||
self:_drawBackground(element.x, element.y, borderBoxWidth, borderBoxHeight, drawBackgroundColor)
|
self:_drawBackground(element.x, element.y, borderBoxWidth, borderBoxHeight, drawBackgroundColor)
|
||||||
|
|
||||||
-- LAYER 1.5: Draw image on top of backgroundColor (if image exists)
|
-- LAYER 1.5: Draw image on top of backgroundColor (if image exists)
|
||||||
self:_drawImage(
|
self:_drawImage(element.x, element.y, element.padding.left, element.padding.top, element.width, element.height, borderBoxWidth, borderBoxHeight)
|
||||||
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)
|
-- LAYER 2: Draw theme on top of backgroundColor (if theme exists)
|
||||||
self:_drawTheme(element.x, element.y, borderBoxWidth, borderBoxHeight, element.scaleCorners, element.scalingAlgorithm)
|
self:_drawTheme(element.x, element.y, borderBoxWidth, borderBoxHeight, element.scaleCorners, element.scalingAlgorithm)
|
||||||
|
|
||||||
-- LAYER 3: Draw borders on top of theme
|
-- LAYER 3: Draw borders on top of theme
|
||||||
self:_drawBorders(element.x, element.y, borderBoxWidth, borderBoxHeight)
|
self:_drawBorders(element.x, element.y, borderBoxWidth, borderBoxHeight)
|
||||||
end
|
end
|
||||||
@@ -382,7 +346,7 @@ end
|
|||||||
function Renderer:wrapLine(element, line, maxWidth)
|
function Renderer:wrapLine(element, line, maxWidth)
|
||||||
-- UTF-8 support
|
-- UTF-8 support
|
||||||
local utf8 = utf8 or require("utf8")
|
local utf8 = utf8 or require("utf8")
|
||||||
|
|
||||||
if not element.editable then
|
if not element.editable then
|
||||||
return { { text = line, startIdx = 0, endIdx = utf8.len(line) } }
|
return { { text = line, startIdx = 0, endIdx = utf8.len(line) } }
|
||||||
end
|
end
|
||||||
@@ -596,7 +560,8 @@ function Renderer:drawText(element)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if displayText and displayText ~= "" then
|
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
|
or element.textColor
|
||||||
local textColorWithOpacity = self._Color.new(textColor.r, textColor.g, textColor.b, textColor.a * self.opacity)
|
local textColorWithOpacity = self._Color.new(textColor.r, textColor.g, textColor.b, textColor.a * self.opacity)
|
||||||
love.graphics.setColor(textColorWithOpacity:toRGBA())
|
love.graphics.setColor(textColorWithOpacity:toRGBA())
|
||||||
|
|||||||
@@ -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 = {}
|
local RoundedRect = {}
|
||||||
|
|
||||||
--- Generate points for a rounded rectangle
|
--- Generate points for a rounded rectangle
|
||||||
|
|||||||
@@ -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
|
---@class ScrollManager
|
||||||
---@field overflow string -- "visible"|"hidden"|"auto"|"scroll"
|
---@field overflow string -- "visible"|"hidden"|"auto"|"scroll"
|
||||||
---@field overflowX string? -- X-axis specific overflow (overrides overflow)
|
---@field overflowX string? -- X-axis specific overflow (overrides overflow)
|
||||||
@@ -41,15 +34,15 @@ ScrollManager.__index = ScrollManager
|
|||||||
function ScrollManager.new(config, deps)
|
function ScrollManager.new(config, deps)
|
||||||
local Color = deps.Color
|
local Color = deps.Color
|
||||||
local self = setmetatable({}, ScrollManager)
|
local self = setmetatable({}, ScrollManager)
|
||||||
|
|
||||||
-- Store dependency for instance methods
|
-- Store dependency for instance methods
|
||||||
self._Color = Color
|
self._Color = Color
|
||||||
|
|
||||||
-- Configuration
|
-- Configuration
|
||||||
self.overflow = config.overflow or "hidden"
|
self.overflow = config.overflow or "hidden"
|
||||||
self.overflowX = config.overflowX
|
self.overflowX = config.overflowX
|
||||||
self.overflowY = config.overflowY
|
self.overflowY = config.overflowY
|
||||||
|
|
||||||
-- Scrollbar appearance
|
-- Scrollbar appearance
|
||||||
self.scrollbarWidth = config.scrollbarWidth or 12
|
self.scrollbarWidth = config.scrollbarWidth or 12
|
||||||
self.scrollbarColor = config.scrollbarColor or Color.new(0.5, 0.5, 0.5, 0.8)
|
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.scrollbarRadius = config.scrollbarRadius or 6
|
||||||
self.scrollbarPadding = config.scrollbarPadding or 2
|
self.scrollbarPadding = config.scrollbarPadding or 2
|
||||||
self.scrollSpeed = config.scrollSpeed or 20
|
self.scrollSpeed = config.scrollSpeed or 20
|
||||||
|
|
||||||
-- hideScrollbars can be boolean or table {vertical: boolean, horizontal: boolean}
|
-- hideScrollbars can be boolean or table {vertical: boolean, horizontal: boolean}
|
||||||
if config.hideScrollbars ~= nil then
|
if config.hideScrollbars ~= nil then
|
||||||
if type(config.hideScrollbars) == "boolean" then
|
if type(config.hideScrollbars) == "boolean" then
|
||||||
@@ -73,19 +66,19 @@ function ScrollManager.new(config, deps)
|
|||||||
else
|
else
|
||||||
self.hideScrollbars = { vertical = false, horizontal = false }
|
self.hideScrollbars = { vertical = false, horizontal = false }
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Internal overflow state
|
-- Internal overflow state
|
||||||
self._overflowX = false
|
self._overflowX = false
|
||||||
self._overflowY = false
|
self._overflowY = false
|
||||||
self._contentWidth = 0
|
self._contentWidth = 0
|
||||||
self._contentHeight = 0
|
self._contentHeight = 0
|
||||||
|
|
||||||
-- Scroll state (can be restored from config in immediate mode)
|
-- Scroll state (can be restored from config in immediate mode)
|
||||||
self._scrollX = config._scrollX or 0
|
self._scrollX = config._scrollX or 0
|
||||||
self._scrollY = config._scrollY or 0
|
self._scrollY = config._scrollY or 0
|
||||||
self._maxScrollX = 0
|
self._maxScrollX = 0
|
||||||
self._maxScrollY = 0
|
self._maxScrollY = 0
|
||||||
|
|
||||||
-- Scrollbar interaction state
|
-- Scrollbar interaction state
|
||||||
self._scrollbarHoveredVertical = false
|
self._scrollbarHoveredVertical = false
|
||||||
self._scrollbarHoveredHorizontal = false
|
self._scrollbarHoveredHorizontal = false
|
||||||
@@ -93,10 +86,10 @@ function ScrollManager.new(config, deps)
|
|||||||
self._hoveredScrollbar = nil -- "vertical" or "horizontal"
|
self._hoveredScrollbar = nil -- "vertical" or "horizontal"
|
||||||
self._scrollbarDragOffset = 0
|
self._scrollbarDragOffset = 0
|
||||||
self._scrollbarPressHandled = false
|
self._scrollbarPressHandled = false
|
||||||
|
|
||||||
-- Element reference (set via initialize)
|
-- Element reference (set via initialize)
|
||||||
self._element = nil
|
self._element = nil
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -111,34 +104,34 @@ function ScrollManager:detectOverflow()
|
|||||||
if not self._element then
|
if not self._element then
|
||||||
error("ScrollManager:detectOverflow() called before initialize()")
|
error("ScrollManager:detectOverflow() called before initialize()")
|
||||||
end
|
end
|
||||||
|
|
||||||
local element = self._element
|
local element = self._element
|
||||||
|
|
||||||
-- Reset overflow state
|
-- Reset overflow state
|
||||||
self._overflowX = false
|
self._overflowX = false
|
||||||
self._overflowY = false
|
self._overflowY = false
|
||||||
self._contentWidth = element.width
|
self._contentWidth = element.width
|
||||||
self._contentHeight = element.height
|
self._contentHeight = element.height
|
||||||
|
|
||||||
-- Skip detection if overflow is visible (no clipping needed)
|
-- Skip detection if overflow is visible (no clipping needed)
|
||||||
local overflowX = self.overflowX or self.overflow
|
local overflowX = self.overflowX or self.overflow
|
||||||
local overflowY = self.overflowY or self.overflow
|
local overflowY = self.overflowY or self.overflow
|
||||||
if overflowX == "visible" and overflowY == "visible" then
|
if overflowX == "visible" and overflowY == "visible" then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Calculate content bounds based on children
|
-- Calculate content bounds based on children
|
||||||
if #element.children == 0 then
|
if #element.children == 0 then
|
||||||
return -- No children, no overflow
|
return -- No children, no overflow
|
||||||
end
|
end
|
||||||
|
|
||||||
local minX, minY = 0, 0
|
local minX, minY = 0, 0
|
||||||
local maxX, maxY = 0, 0
|
local maxX, maxY = 0, 0
|
||||||
|
|
||||||
-- Content area starts after padding
|
-- Content area starts after padding
|
||||||
local contentX = element.x + element.padding.left
|
local contentX = element.x + element.padding.left
|
||||||
local contentY = element.y + element.padding.top
|
local contentY = element.y + element.padding.top
|
||||||
|
|
||||||
for _, child in ipairs(element.children) do
|
for _, child in ipairs(element.children) do
|
||||||
-- Skip absolutely positioned children (they don't contribute to overflow)
|
-- Skip absolutely positioned children (they don't contribute to overflow)
|
||||||
if not child._explicitlyAbsolute then
|
if not child._explicitlyAbsolute then
|
||||||
@@ -147,27 +140,27 @@ function ScrollManager:detectOverflow()
|
|||||||
local childTop = child.y - contentY
|
local childTop = child.y - contentY
|
||||||
local childRight = childLeft + child:getBorderBoxWidth() + child.margin.right
|
local childRight = childLeft + child:getBorderBoxWidth() + child.margin.right
|
||||||
local childBottom = childTop + child:getBorderBoxHeight() + child.margin.bottom
|
local childBottom = childTop + child:getBorderBoxHeight() + child.margin.bottom
|
||||||
|
|
||||||
maxX = math.max(maxX, childRight)
|
maxX = math.max(maxX, childRight)
|
||||||
maxY = math.max(maxY, childBottom)
|
maxY = math.max(maxY, childBottom)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Calculate content dimensions
|
-- Calculate content dimensions
|
||||||
self._contentWidth = maxX
|
self._contentWidth = maxX
|
||||||
self._contentHeight = maxY
|
self._contentHeight = maxY
|
||||||
|
|
||||||
-- Detect overflow
|
-- Detect overflow
|
||||||
local containerWidth = element.width
|
local containerWidth = element.width
|
||||||
local containerHeight = element.height
|
local containerHeight = element.height
|
||||||
|
|
||||||
self._overflowX = self._contentWidth > containerWidth
|
self._overflowX = self._contentWidth > containerWidth
|
||||||
self._overflowY = self._contentHeight > containerHeight
|
self._overflowY = self._contentHeight > containerHeight
|
||||||
|
|
||||||
-- Calculate maximum scroll bounds
|
-- Calculate maximum scroll bounds
|
||||||
self._maxScrollX = math.max(0, self._contentWidth - containerWidth)
|
self._maxScrollX = math.max(0, self._contentWidth - containerWidth)
|
||||||
self._maxScrollY = math.max(0, self._contentHeight - containerHeight)
|
self._maxScrollY = math.max(0, self._contentHeight - containerHeight)
|
||||||
|
|
||||||
-- Clamp current scroll position to new bounds
|
-- Clamp current scroll position to new bounds
|
||||||
self._scrollX = math.max(0, math.min(self._scrollX, self._maxScrollX))
|
self._scrollX = math.max(0, math.min(self._scrollX, self._maxScrollX))
|
||||||
self._scrollY = math.max(0, math.min(self._scrollY, self._maxScrollY))
|
self._scrollY = math.max(0, math.min(self._scrollY, self._maxScrollY))
|
||||||
@@ -235,28 +228,28 @@ function ScrollManager:calculateScrollbarDimensions()
|
|||||||
if not self._element then
|
if not self._element then
|
||||||
error("ScrollManager:calculateScrollbarDimensions() called before initialize()")
|
error("ScrollManager:calculateScrollbarDimensions() called before initialize()")
|
||||||
end
|
end
|
||||||
|
|
||||||
local element = self._element
|
local element = self._element
|
||||||
local result = {
|
local result = {
|
||||||
vertical = { visible = false, trackHeight = 0, thumbHeight = 0, thumbY = 0 },
|
vertical = { visible = false, trackHeight = 0, thumbHeight = 0, thumbY = 0 },
|
||||||
horizontal = { visible = false, trackWidth = 0, thumbWidth = 0, thumbX = 0 },
|
horizontal = { visible = false, trackWidth = 0, thumbWidth = 0, thumbX = 0 },
|
||||||
}
|
}
|
||||||
|
|
||||||
local overflowX = self.overflowX or self.overflow
|
local overflowX = self.overflowX or self.overflow
|
||||||
local overflowY = self.overflowY or self.overflow
|
local overflowY = self.overflowY or self.overflow
|
||||||
|
|
||||||
-- Vertical scrollbar
|
-- Vertical scrollbar
|
||||||
-- Note: overflow="scroll" always shows scrollbar; overflow="auto" only when content overflows
|
-- Note: overflow="scroll" always shows scrollbar; overflow="auto" only when content overflows
|
||||||
if overflowY == "scroll" then
|
if overflowY == "scroll" then
|
||||||
-- Always show scrollbar for "scroll" mode
|
-- Always show scrollbar for "scroll" mode
|
||||||
result.vertical.visible = true
|
result.vertical.visible = true
|
||||||
result.vertical.trackHeight = element.height - (self.scrollbarPadding * 2)
|
result.vertical.trackHeight = element.height - (self.scrollbarPadding * 2)
|
||||||
|
|
||||||
if self._overflowY then
|
if self._overflowY then
|
||||||
-- Content overflows, calculate proper thumb size
|
-- Content overflows, calculate proper thumb size
|
||||||
local contentRatio = element.height / math.max(self._contentHeight, element.height)
|
local contentRatio = element.height / math.max(self._contentHeight, element.height)
|
||||||
result.vertical.thumbHeight = math.max(20, result.vertical.trackHeight * contentRatio)
|
result.vertical.thumbHeight = math.max(20, result.vertical.trackHeight * contentRatio)
|
||||||
|
|
||||||
-- Calculate thumb position based on scroll ratio
|
-- Calculate thumb position based on scroll ratio
|
||||||
local scrollRatio = self._maxScrollY > 0 and (self._scrollY / self._maxScrollY) or 0
|
local scrollRatio = self._maxScrollY > 0 and (self._scrollY / self._maxScrollY) or 0
|
||||||
local maxThumbY = result.vertical.trackHeight - result.vertical.thumbHeight
|
local maxThumbY = result.vertical.trackHeight - result.vertical.thumbHeight
|
||||||
@@ -270,29 +263,29 @@ function ScrollManager:calculateScrollbarDimensions()
|
|||||||
-- Only show scrollbar when content actually overflows
|
-- Only show scrollbar when content actually overflows
|
||||||
result.vertical.visible = true
|
result.vertical.visible = true
|
||||||
result.vertical.trackHeight = element.height - (self.scrollbarPadding * 2)
|
result.vertical.trackHeight = element.height - (self.scrollbarPadding * 2)
|
||||||
|
|
||||||
-- Calculate thumb height based on content ratio
|
-- Calculate thumb height based on content ratio
|
||||||
local contentRatio = element.height / math.max(self._contentHeight, element.height)
|
local contentRatio = element.height / math.max(self._contentHeight, element.height)
|
||||||
result.vertical.thumbHeight = math.max(20, result.vertical.trackHeight * contentRatio)
|
result.vertical.thumbHeight = math.max(20, result.vertical.trackHeight * contentRatio)
|
||||||
|
|
||||||
-- Calculate thumb position based on scroll ratio
|
-- Calculate thumb position based on scroll ratio
|
||||||
local scrollRatio = self._maxScrollY > 0 and (self._scrollY / self._maxScrollY) or 0
|
local scrollRatio = self._maxScrollY > 0 and (self._scrollY / self._maxScrollY) or 0
|
||||||
local maxThumbY = result.vertical.trackHeight - result.vertical.thumbHeight
|
local maxThumbY = result.vertical.trackHeight - result.vertical.thumbHeight
|
||||||
result.vertical.thumbY = maxThumbY * scrollRatio
|
result.vertical.thumbY = maxThumbY * scrollRatio
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Horizontal scrollbar
|
-- Horizontal scrollbar
|
||||||
-- Note: overflow="scroll" always shows scrollbar; overflow="auto" only when content overflows
|
-- Note: overflow="scroll" always shows scrollbar; overflow="auto" only when content overflows
|
||||||
if overflowX == "scroll" then
|
if overflowX == "scroll" then
|
||||||
-- Always show scrollbar for "scroll" mode
|
-- Always show scrollbar for "scroll" mode
|
||||||
result.horizontal.visible = true
|
result.horizontal.visible = true
|
||||||
result.horizontal.trackWidth = element.width - (self.scrollbarPadding * 2)
|
result.horizontal.trackWidth = element.width - (self.scrollbarPadding * 2)
|
||||||
|
|
||||||
if self._overflowX then
|
if self._overflowX then
|
||||||
-- Content overflows, calculate proper thumb size
|
-- Content overflows, calculate proper thumb size
|
||||||
local contentRatio = element.width / math.max(self._contentWidth, element.width)
|
local contentRatio = element.width / math.max(self._contentWidth, element.width)
|
||||||
result.horizontal.thumbWidth = math.max(20, result.horizontal.trackWidth * contentRatio)
|
result.horizontal.thumbWidth = math.max(20, result.horizontal.trackWidth * contentRatio)
|
||||||
|
|
||||||
-- Calculate thumb position based on scroll ratio
|
-- Calculate thumb position based on scroll ratio
|
||||||
local scrollRatio = self._maxScrollX > 0 and (self._scrollX / self._maxScrollX) or 0
|
local scrollRatio = self._maxScrollX > 0 and (self._scrollX / self._maxScrollX) or 0
|
||||||
local maxThumbX = result.horizontal.trackWidth - result.horizontal.thumbWidth
|
local maxThumbX = result.horizontal.trackWidth - result.horizontal.thumbWidth
|
||||||
@@ -306,17 +299,17 @@ function ScrollManager:calculateScrollbarDimensions()
|
|||||||
-- Only show scrollbar when content actually overflows
|
-- Only show scrollbar when content actually overflows
|
||||||
result.horizontal.visible = true
|
result.horizontal.visible = true
|
||||||
result.horizontal.trackWidth = element.width - (self.scrollbarPadding * 2)
|
result.horizontal.trackWidth = element.width - (self.scrollbarPadding * 2)
|
||||||
|
|
||||||
-- Calculate thumb width based on content ratio
|
-- Calculate thumb width based on content ratio
|
||||||
local contentRatio = element.width / math.max(self._contentWidth, element.width)
|
local contentRatio = element.width / math.max(self._contentWidth, element.width)
|
||||||
result.horizontal.thumbWidth = math.max(20, result.horizontal.trackWidth * contentRatio)
|
result.horizontal.thumbWidth = math.max(20, result.horizontal.trackWidth * contentRatio)
|
||||||
|
|
||||||
-- Calculate thumb position based on scroll ratio
|
-- Calculate thumb position based on scroll ratio
|
||||||
local scrollRatio = self._maxScrollX > 0 and (self._scrollX / self._maxScrollX) or 0
|
local scrollRatio = self._maxScrollX > 0 and (self._scrollX / self._maxScrollX) or 0
|
||||||
local maxThumbX = result.horizontal.trackWidth - result.horizontal.thumbWidth
|
local maxThumbX = result.horizontal.trackWidth - result.horizontal.thumbWidth
|
||||||
result.horizontal.thumbX = maxThumbX * scrollRatio
|
result.horizontal.thumbX = maxThumbX * scrollRatio
|
||||||
end
|
end
|
||||||
|
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -328,19 +321,19 @@ function ScrollManager:getScrollbarAtPosition(mouseX, mouseY)
|
|||||||
if not self._element then
|
if not self._element then
|
||||||
error("ScrollManager:getScrollbarAtPosition() called before initialize()")
|
error("ScrollManager:getScrollbarAtPosition() called before initialize()")
|
||||||
end
|
end
|
||||||
|
|
||||||
local element = self._element
|
local element = self._element
|
||||||
local overflowX = self.overflowX or self.overflow
|
local overflowX = self.overflowX or self.overflow
|
||||||
local overflowY = self.overflowY or self.overflow
|
local overflowY = self.overflowY or self.overflow
|
||||||
|
|
||||||
if not (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto") then
|
if not (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto") then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local dims = self:calculateScrollbarDimensions()
|
local dims = self:calculateScrollbarDimensions()
|
||||||
local x, y = element.x, element.y
|
local x, y = element.x, element.y
|
||||||
local w, h = element.width, element.height
|
local w, h = element.width, element.height
|
||||||
|
|
||||||
-- Check vertical scrollbar (only if not hidden)
|
-- Check vertical scrollbar (only if not hidden)
|
||||||
if dims.vertical.visible and not self.hideScrollbars.vertical then
|
if dims.vertical.visible and not self.hideScrollbars.vertical then
|
||||||
-- Position scrollbar within content area (x, y is border-box origin)
|
-- 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 trackY = contentY + self.scrollbarPadding
|
||||||
local trackW = self.scrollbarWidth
|
local trackW = self.scrollbarWidth
|
||||||
local trackH = dims.vertical.trackHeight
|
local trackH = dims.vertical.trackHeight
|
||||||
|
|
||||||
if mouseX >= trackX and mouseX <= trackX + trackW and mouseY >= trackY and mouseY <= trackY + trackH then
|
if mouseX >= trackX and mouseX <= trackX + trackW and mouseY >= trackY and mouseY <= trackY + trackH then
|
||||||
-- Check if over thumb
|
-- Check if over thumb
|
||||||
local thumbY = trackY + dims.vertical.thumbY
|
local thumbY = trackY + dims.vertical.thumbY
|
||||||
@@ -362,7 +355,7 @@ function ScrollManager:getScrollbarAtPosition(mouseX, mouseY)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check horizontal scrollbar (only if not hidden)
|
-- Check horizontal scrollbar (only if not hidden)
|
||||||
if dims.horizontal.visible and not self.hideScrollbars.horizontal then
|
if dims.horizontal.visible and not self.hideScrollbars.horizontal then
|
||||||
-- Position scrollbar within content area (x, y is border-box origin)
|
-- 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 trackY = contentY + h - self.scrollbarWidth - self.scrollbarPadding
|
||||||
local trackW = dims.horizontal.trackWidth
|
local trackW = dims.horizontal.trackWidth
|
||||||
local trackH = self.scrollbarWidth
|
local trackH = self.scrollbarWidth
|
||||||
|
|
||||||
if mouseX >= trackX and mouseX <= trackX + trackW and mouseY >= trackY and mouseY <= trackY + trackH then
|
if mouseX >= trackX and mouseX <= trackX + trackW and mouseY >= trackY and mouseY <= trackY + trackH then
|
||||||
-- Check if over thumb
|
-- Check if over thumb
|
||||||
local thumbX = trackX + dims.horizontal.thumbX
|
local thumbX = trackX + dims.horizontal.thumbX
|
||||||
@@ -384,7 +377,7 @@ function ScrollManager:getScrollbarAtPosition(mouseX, mouseY)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -397,23 +390,23 @@ function ScrollManager:handleMousePress(mouseX, mouseY, button)
|
|||||||
if not self._element then
|
if not self._element then
|
||||||
error("ScrollManager:handleMousePress() called before initialize()")
|
error("ScrollManager:handleMousePress() called before initialize()")
|
||||||
end
|
end
|
||||||
|
|
||||||
if button ~= 1 then
|
if button ~= 1 then
|
||||||
return false
|
return false
|
||||||
end -- Only left click
|
end -- Only left click
|
||||||
|
|
||||||
local scrollbar = self:getScrollbarAtPosition(mouseX, mouseY)
|
local scrollbar = self:getScrollbarAtPosition(mouseX, mouseY)
|
||||||
if not scrollbar then
|
if not scrollbar then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
if scrollbar.region == "thumb" then
|
if scrollbar.region == "thumb" then
|
||||||
-- Start dragging thumb
|
-- Start dragging thumb
|
||||||
self._scrollbarDragging = true
|
self._scrollbarDragging = true
|
||||||
self._hoveredScrollbar = scrollbar.component
|
self._hoveredScrollbar = scrollbar.component
|
||||||
local dims = self:calculateScrollbarDimensions()
|
local dims = self:calculateScrollbarDimensions()
|
||||||
local element = self._element
|
local element = self._element
|
||||||
|
|
||||||
if scrollbar.component == "vertical" then
|
if scrollbar.component == "vertical" then
|
||||||
local contentY = element.y + element.padding.top
|
local contentY = element.y + element.padding.top
|
||||||
local trackY = contentY + self.scrollbarPadding
|
local trackY = contentY + self.scrollbarPadding
|
||||||
@@ -425,14 +418,14 @@ function ScrollManager:handleMousePress(mouseX, mouseY, button)
|
|||||||
local thumbX = trackX + dims.horizontal.thumbX
|
local thumbX = trackX + dims.horizontal.thumbX
|
||||||
self._scrollbarDragOffset = mouseX - thumbX
|
self._scrollbarDragOffset = mouseX - thumbX
|
||||||
end
|
end
|
||||||
|
|
||||||
return true -- Event consumed
|
return true -- Event consumed
|
||||||
elseif scrollbar.region == "track" then
|
elseif scrollbar.region == "track" then
|
||||||
-- Click on track - jump to position
|
-- Click on track - jump to position
|
||||||
self:_scrollToTrackPosition(mouseX, mouseY, scrollbar.component)
|
self:_scrollToTrackPosition(mouseX, mouseY, scrollbar.component)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -444,28 +437,28 @@ function ScrollManager:handleMouseMove(mouseX, mouseY)
|
|||||||
if not self._element then
|
if not self._element then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
if not self._scrollbarDragging then
|
if not self._scrollbarDragging then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
local dims = self:calculateScrollbarDimensions()
|
local dims = self:calculateScrollbarDimensions()
|
||||||
local element = self._element
|
local element = self._element
|
||||||
|
|
||||||
if self._hoveredScrollbar == "vertical" then
|
if self._hoveredScrollbar == "vertical" then
|
||||||
local contentY = element.y + element.padding.top
|
local contentY = element.y + element.padding.top
|
||||||
local trackY = contentY + self.scrollbarPadding
|
local trackY = contentY + self.scrollbarPadding
|
||||||
local trackH = dims.vertical.trackHeight
|
local trackH = dims.vertical.trackHeight
|
||||||
local thumbH = dims.vertical.thumbHeight
|
local thumbH = dims.vertical.thumbHeight
|
||||||
|
|
||||||
-- Calculate new thumb position
|
-- Calculate new thumb position
|
||||||
local newThumbY = mouseY - self._scrollbarDragOffset - trackY
|
local newThumbY = mouseY - self._scrollbarDragOffset - trackY
|
||||||
newThumbY = math.max(0, math.min(newThumbY, trackH - thumbH))
|
newThumbY = math.max(0, math.min(newThumbY, trackH - thumbH))
|
||||||
|
|
||||||
-- Convert thumb position to scroll position
|
-- Convert thumb position to scroll position
|
||||||
local scrollRatio = (trackH - thumbH) > 0 and (newThumbY / (trackH - thumbH)) or 0
|
local scrollRatio = (trackH - thumbH) > 0 and (newThumbY / (trackH - thumbH)) or 0
|
||||||
local newScrollY = scrollRatio * self._maxScrollY
|
local newScrollY = scrollRatio * self._maxScrollY
|
||||||
|
|
||||||
self:setScroll(nil, newScrollY)
|
self:setScroll(nil, newScrollY)
|
||||||
return true
|
return true
|
||||||
elseif self._hoveredScrollbar == "horizontal" then
|
elseif self._hoveredScrollbar == "horizontal" then
|
||||||
@@ -473,19 +466,19 @@ function ScrollManager:handleMouseMove(mouseX, mouseY)
|
|||||||
local trackX = contentX + self.scrollbarPadding
|
local trackX = contentX + self.scrollbarPadding
|
||||||
local trackW = dims.horizontal.trackWidth
|
local trackW = dims.horizontal.trackWidth
|
||||||
local thumbW = dims.horizontal.thumbWidth
|
local thumbW = dims.horizontal.thumbWidth
|
||||||
|
|
||||||
-- Calculate new thumb position
|
-- Calculate new thumb position
|
||||||
local newThumbX = mouseX - self._scrollbarDragOffset - trackX
|
local newThumbX = mouseX - self._scrollbarDragOffset - trackX
|
||||||
newThumbX = math.max(0, math.min(newThumbX, trackW - thumbW))
|
newThumbX = math.max(0, math.min(newThumbX, trackW - thumbW))
|
||||||
|
|
||||||
-- Convert thumb position to scroll position
|
-- Convert thumb position to scroll position
|
||||||
local scrollRatio = (trackW - thumbW) > 0 and (newThumbX / (trackW - thumbW)) or 0
|
local scrollRatio = (trackW - thumbW) > 0 and (newThumbX / (trackW - thumbW)) or 0
|
||||||
local newScrollX = scrollRatio * self._maxScrollX
|
local newScrollX = scrollRatio * self._maxScrollX
|
||||||
|
|
||||||
self:setScroll(newScrollX, nil)
|
self:setScroll(newScrollX, nil)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -496,12 +489,12 @@ function ScrollManager:handleMouseRelease(button)
|
|||||||
if button ~= 1 then
|
if button ~= 1 then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
if self._scrollbarDragging then
|
if self._scrollbarDragging then
|
||||||
self._scrollbarDragging = false
|
self._scrollbarDragging = false
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -513,39 +506,39 @@ function ScrollManager:_scrollToTrackPosition(mouseX, mouseY, component)
|
|||||||
if not self._element then
|
if not self._element then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local dims = self:calculateScrollbarDimensions()
|
local dims = self:calculateScrollbarDimensions()
|
||||||
local element = self._element
|
local element = self._element
|
||||||
|
|
||||||
if component == "vertical" then
|
if component == "vertical" then
|
||||||
local contentY = element.y + element.padding.top
|
local contentY = element.y + element.padding.top
|
||||||
local trackY = contentY + self.scrollbarPadding
|
local trackY = contentY + self.scrollbarPadding
|
||||||
local trackH = dims.vertical.trackHeight
|
local trackH = dims.vertical.trackHeight
|
||||||
local thumbH = dims.vertical.thumbHeight
|
local thumbH = dims.vertical.thumbHeight
|
||||||
|
|
||||||
-- Calculate target thumb position (centered on click)
|
-- Calculate target thumb position (centered on click)
|
||||||
local targetThumbY = mouseY - trackY - (thumbH / 2)
|
local targetThumbY = mouseY - trackY - (thumbH / 2)
|
||||||
targetThumbY = math.max(0, math.min(targetThumbY, trackH - thumbH))
|
targetThumbY = math.max(0, math.min(targetThumbY, trackH - thumbH))
|
||||||
|
|
||||||
-- Convert to scroll position
|
-- Convert to scroll position
|
||||||
local scrollRatio = (trackH - thumbH) > 0 and (targetThumbY / (trackH - thumbH)) or 0
|
local scrollRatio = (trackH - thumbH) > 0 and (targetThumbY / (trackH - thumbH)) or 0
|
||||||
local newScrollY = scrollRatio * self._maxScrollY
|
local newScrollY = scrollRatio * self._maxScrollY
|
||||||
|
|
||||||
self:setScroll(nil, newScrollY)
|
self:setScroll(nil, newScrollY)
|
||||||
elseif component == "horizontal" then
|
elseif component == "horizontal" then
|
||||||
local contentX = element.x + element.padding.left
|
local contentX = element.x + element.padding.left
|
||||||
local trackX = contentX + self.scrollbarPadding
|
local trackX = contentX + self.scrollbarPadding
|
||||||
local trackW = dims.horizontal.trackWidth
|
local trackW = dims.horizontal.trackWidth
|
||||||
local thumbW = dims.horizontal.thumbWidth
|
local thumbW = dims.horizontal.thumbWidth
|
||||||
|
|
||||||
-- Calculate target thumb position (centered on click)
|
-- Calculate target thumb position (centered on click)
|
||||||
local targetThumbX = mouseX - trackX - (thumbW / 2)
|
local targetThumbX = mouseX - trackX - (thumbW / 2)
|
||||||
targetThumbX = math.max(0, math.min(targetThumbX, trackW - thumbW))
|
targetThumbX = math.max(0, math.min(targetThumbX, trackW - thumbW))
|
||||||
|
|
||||||
-- Convert to scroll position
|
-- Convert to scroll position
|
||||||
local scrollRatio = (trackW - thumbW) > 0 and (targetThumbX / (trackW - thumbW)) or 0
|
local scrollRatio = (trackW - thumbW) > 0 and (targetThumbX / (trackW - thumbW)) or 0
|
||||||
local newScrollX = scrollRatio * self._maxScrollX
|
local newScrollX = scrollRatio * self._maxScrollX
|
||||||
|
|
||||||
self:setScroll(newScrollX, nil)
|
self:setScroll(newScrollX, nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -557,16 +550,16 @@ end
|
|||||||
function ScrollManager:handleWheel(x, y)
|
function ScrollManager:handleWheel(x, y)
|
||||||
local overflowX = self.overflowX or self.overflow
|
local overflowX = self.overflowX or self.overflow
|
||||||
local overflowY = self.overflowY or self.overflow
|
local overflowY = self.overflowY or self.overflow
|
||||||
|
|
||||||
if not (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto") then
|
if not (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto") then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
local hasVerticalOverflow = self._overflowY and self._maxScrollY > 0
|
local hasVerticalOverflow = self._overflowY and self._maxScrollY > 0
|
||||||
local hasHorizontalOverflow = self._overflowX and self._maxScrollX > 0
|
local hasHorizontalOverflow = self._overflowX and self._maxScrollX > 0
|
||||||
|
|
||||||
local scrolled = false
|
local scrolled = false
|
||||||
|
|
||||||
-- Vertical scrolling
|
-- Vertical scrolling
|
||||||
if y ~= 0 and hasVerticalOverflow then
|
if y ~= 0 and hasVerticalOverflow then
|
||||||
local delta = -y * self.scrollSpeed -- Negative because wheel up = scroll up
|
local delta = -y * self.scrollSpeed -- Negative because wheel up = scroll up
|
||||||
@@ -574,7 +567,7 @@ function ScrollManager:handleWheel(x, y)
|
|||||||
self:setScroll(nil, newScrollY)
|
self:setScroll(nil, newScrollY)
|
||||||
scrolled = true
|
scrolled = true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Horizontal scrolling
|
-- Horizontal scrolling
|
||||||
if x ~= 0 and hasHorizontalOverflow then
|
if x ~= 0 and hasHorizontalOverflow then
|
||||||
local delta = -x * self.scrollSpeed
|
local delta = -x * self.scrollSpeed
|
||||||
@@ -582,7 +575,7 @@ function ScrollManager:handleWheel(x, y)
|
|||||||
self:setScroll(newScrollX, nil)
|
self:setScroll(newScrollX, nil)
|
||||||
scrolled = true
|
scrolled = true
|
||||||
end
|
end
|
||||||
|
|
||||||
return scrolled
|
return scrolled
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -591,7 +584,7 @@ end
|
|||||||
---@param mouseY number
|
---@param mouseY number
|
||||||
function ScrollManager:updateHoverState(mouseX, mouseY)
|
function ScrollManager:updateHoverState(mouseX, mouseY)
|
||||||
local scrollbar = self:getScrollbarAtPosition(mouseX, mouseY)
|
local scrollbar = self:getScrollbarAtPosition(mouseX, mouseY)
|
||||||
|
|
||||||
if scrollbar then
|
if scrollbar then
|
||||||
if scrollbar.component == "vertical" then
|
if scrollbar.component == "vertical" then
|
||||||
self._scrollbarHoveredVertical = true
|
self._scrollbarHoveredVertical = true
|
||||||
@@ -640,7 +633,7 @@ function ScrollManager:setState(state)
|
|||||||
if not state then
|
if not state then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if state.scrollX then
|
if state.scrollX then
|
||||||
self._scrollX = state.scrollX
|
self._scrollX = state.scrollX
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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
|
---@class StateManager
|
||||||
local StateManager = {}
|
local StateManager = {}
|
||||||
|
|
||||||
@@ -23,7 +16,7 @@ local callSiteCounters = {}
|
|||||||
-- Configuration
|
-- Configuration
|
||||||
local config = {
|
local config = {
|
||||||
stateRetentionFrames = 60, -- Keep unused state for 60 frames (~1 second at 60fps)
|
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
|
---@param depth number|nil Current recursion depth
|
||||||
---@return string
|
---@return string
|
||||||
local function hashProps(props, visited, depth)
|
local function hashProps(props, visited, depth)
|
||||||
if not props then return "" end
|
if not props then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
-- Initialize visited table on first call
|
-- Initialize visited table on first call
|
||||||
visited = visited or {}
|
visited = visited or {}
|
||||||
depth = depth or 0
|
depth = depth or 0
|
||||||
|
|
||||||
-- Limit recursion depth to prevent deep nesting issues
|
-- Limit recursion depth to prevent deep nesting issues
|
||||||
if depth > 3 then
|
if depth > 3 then
|
||||||
return "[deep]"
|
return "[deep]"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check if we've already visited this table (circular reference)
|
-- Check if we've already visited this table (circular reference)
|
||||||
if visited[props] then
|
if visited[props] then
|
||||||
return "[circular]"
|
return "[circular]"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Mark this table as visited
|
-- Mark this table as visited
|
||||||
visited[props] = true
|
visited[props] = true
|
||||||
|
|
||||||
local parts = {}
|
local parts = {}
|
||||||
local keys = {}
|
local keys = {}
|
||||||
|
|
||||||
-- Properties to skip (they cause issues or aren't relevant for ID generation)
|
-- Properties to skip (they cause issues or aren't relevant for ID generation)
|
||||||
local skipKeys = {
|
local skipKeys = {
|
||||||
onEvent = true,
|
onEvent = true,
|
||||||
@@ -70,12 +65,12 @@ local function hashProps(props, visited, depth)
|
|||||||
onEnter = true,
|
onEnter = true,
|
||||||
userdata = true,
|
userdata = true,
|
||||||
-- Dynamic input/state properties that should not affect ID stability
|
-- Dynamic input/state properties that should not affect ID stability
|
||||||
text = true, -- Text content changes as user types
|
text = true, -- Text content changes as user types
|
||||||
placeholder = true, -- Placeholder text is presentational
|
placeholder = true, -- Placeholder text is presentational
|
||||||
editable = true, -- Editable state can be toggled dynamically
|
editable = true, -- Editable state can be toggled dynamically
|
||||||
selectOnFocus = true, -- Input behavior flag
|
selectOnFocus = true, -- Input behavior flag
|
||||||
autoGrow = true, -- Auto-grow behavior flag
|
autoGrow = true, -- Auto-grow behavior flag
|
||||||
passwordMode = true, -- Password mode can be toggled
|
passwordMode = true, -- Password mode can be toggled
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Collect and sort keys for consistent ordering
|
-- Collect and sort keys for consistent ordering
|
||||||
@@ -121,17 +116,17 @@ function StateManager.generateID(props, parent)
|
|||||||
local filename = source:match("([^/\\]+)$") or source -- Get filename
|
local filename = source:match("([^/\\]+)$") or source -- Get filename
|
||||||
filename = filename:gsub("%.lua$", "") -- Remove .lua extension
|
filename = filename:gsub("%.lua$", "") -- Remove .lua extension
|
||||||
local locationKey = filename .. "_L" .. line
|
local locationKey = filename .. "_L" .. line
|
||||||
|
|
||||||
-- If we have a parent, use tree-based ID generation for stability
|
-- If we have a parent, use tree-based ID generation for stability
|
||||||
if parent and parent.id and parent.id ~= "" then
|
if parent and parent.id and parent.id ~= "" then
|
||||||
-- Count how many children the parent currently has
|
-- Count how many children the parent currently has
|
||||||
-- This gives us a stable sibling index
|
-- This gives us a stable sibling index
|
||||||
local siblingIndex = #(parent.children or {})
|
local siblingIndex = #(parent.children or {})
|
||||||
|
|
||||||
-- Generate ID based on parent ID + sibling position (NO line number for stability)
|
-- 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
|
-- This ensures the same position in the tree always gets the same ID
|
||||||
local baseID = parent.id .. "_child" .. siblingIndex
|
local baseID = parent.id .. "_child" .. siblingIndex
|
||||||
|
|
||||||
-- Add property hash if provided (for additional differentiation at same position)
|
-- Add property hash if provided (for additional differentiation at same position)
|
||||||
if props then
|
if props then
|
||||||
local propHash = hashProps(props)
|
local propHash = hashProps(props)
|
||||||
@@ -144,17 +139,17 @@ function StateManager.generateID(props, parent)
|
|||||||
baseID = baseID .. "_" .. hash
|
baseID = baseID .. "_" .. hash
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return baseID
|
return baseID
|
||||||
end
|
end
|
||||||
|
|
||||||
-- No parent (top-level element): use call-site counter approach
|
-- No parent (top-level element): use call-site counter approach
|
||||||
-- Track how many elements have been created at this location
|
-- Track how many elements have been created at this location
|
||||||
callSiteCounters[locationKey] = (callSiteCounters[locationKey] or 0) + 1
|
callSiteCounters[locationKey] = (callSiteCounters[locationKey] or 0) + 1
|
||||||
local instanceNum = callSiteCounters[locationKey]
|
local instanceNum = callSiteCounters[locationKey]
|
||||||
|
|
||||||
local baseID = locationKey
|
local baseID = locationKey
|
||||||
|
|
||||||
-- Add instance number if multiple elements created at same location (e.g., in loops)
|
-- Add instance number if multiple elements created at same location (e.g., in loops)
|
||||||
if instanceNum > 1 then
|
if instanceNum > 1 then
|
||||||
baseID = baseID .. "_" .. instanceNum
|
baseID = baseID .. "_" .. instanceNum
|
||||||
@@ -193,48 +188,100 @@ function StateManager.getState(id, defaultState)
|
|||||||
if not stateStore[id] then
|
if not stateStore[id] then
|
||||||
-- Merge default state with standard structure
|
-- Merge default state with standard structure
|
||||||
stateStore[id] = defaultState or {}
|
stateStore[id] = defaultState or {}
|
||||||
|
|
||||||
-- Ensure all standard properties exist with defaults
|
-- Ensure all standard properties exist with defaults
|
||||||
local state = stateStore[id]
|
local state = stateStore[id]
|
||||||
|
|
||||||
-- Interaction states
|
-- Interaction states
|
||||||
if state.hover == nil then state.hover = false end
|
if state.hover == nil then
|
||||||
if state.pressed == nil then state.pressed = false end
|
state.hover = false
|
||||||
if state.focused == nil then state.focused = false end
|
end
|
||||||
if state.disabled == nil then state.disabled = false end
|
if state.pressed == nil then
|
||||||
if state.active == nil then state.active = false end
|
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
|
-- Scrollbar states
|
||||||
if state.scrollbarHoveredVertical == nil then state.scrollbarHoveredVertical = false end
|
if state.scrollbarHoveredVertical == nil then
|
||||||
if state.scrollbarHoveredHorizontal == nil then state.scrollbarHoveredHorizontal = false end
|
state.scrollbarHoveredVertical = false
|
||||||
if state.scrollbarDragging == nil then state.scrollbarDragging = false end
|
end
|
||||||
if state.hoveredScrollbar == nil then state.hoveredScrollbar = nil end
|
if state.scrollbarHoveredHorizontal == nil then
|
||||||
if state.scrollbarDragOffset == nil then state.scrollbarDragOffset = 0 end
|
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
|
-- Scroll position
|
||||||
if state.scrollX == nil then state.scrollX = 0 end
|
if state.scrollX == nil then
|
||||||
if state.scrollY == nil then state.scrollY = 0 end
|
state.scrollX = 0
|
||||||
|
end
|
||||||
|
if state.scrollY == nil then
|
||||||
|
state.scrollY = 0
|
||||||
|
end
|
||||||
|
|
||||||
-- Click tracking
|
-- Click tracking
|
||||||
if state._pressed == nil then state._pressed = {} end
|
if state._pressed == nil then
|
||||||
if state._lastClickTime == nil then state._lastClickTime = nil end
|
state._pressed = {}
|
||||||
if state._lastClickButton == nil then state._lastClickButton = nil end
|
end
|
||||||
if state._clickCount == nil then state._clickCount = 0 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
|
-- Drag tracking
|
||||||
if state._dragStartX == nil then state._dragStartX = {} end
|
if state._dragStartX == nil then
|
||||||
if state._dragStartY == nil then state._dragStartY = {} end
|
state._dragStartX = {}
|
||||||
if state._lastMouseX == nil then state._lastMouseX = {} end
|
end
|
||||||
if state._lastMouseY == nil then state._lastMouseY = {} 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
|
-- Input/focus state
|
||||||
if state._hovered == nil then state._hovered = nil end
|
if state._hovered == nil then
|
||||||
if state._focused == nil then state._focused = nil end
|
state._hovered = nil
|
||||||
if state._cursorPosition == nil then state._cursorPosition = nil end
|
end
|
||||||
if state._selectionStart == nil then state._selectionStart = nil end
|
if state._focused == nil then
|
||||||
if state._selectionEnd == nil then state._selectionEnd = nil end
|
state._focused = nil
|
||||||
if state._textBuffer == nil then state._textBuffer = "" end
|
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
|
-- Create metadata
|
||||||
stateMetadata[id] = {
|
stateMetadata[id] = {
|
||||||
lastFrame = frameNumber,
|
lastFrame = frameNumber,
|
||||||
@@ -278,12 +325,12 @@ end
|
|||||||
---@param newState table New state values to merge
|
---@param newState table New state values to merge
|
||||||
function StateManager.updateState(id, newState)
|
function StateManager.updateState(id, newState)
|
||||||
local state = StateManager.getState(id)
|
local state = StateManager.getState(id)
|
||||||
|
|
||||||
-- Merge new state into existing state
|
-- Merge new state into existing state
|
||||||
for key, value in pairs(newState) do
|
for key, value in pairs(newState) do
|
||||||
state[key] = value
|
state[key] = value
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Update metadata
|
-- Update metadata
|
||||||
stateMetadata[id].lastFrame = frameNumber
|
stateMetadata[id].lastFrame = frameNumber
|
||||||
end
|
end
|
||||||
@@ -468,7 +515,7 @@ end
|
|||||||
---@return table state Active state values
|
---@return table state Active state values
|
||||||
function StateManager.getActiveState(id)
|
function StateManager.getActiveState(id)
|
||||||
local state = StateManager.getState(id)
|
local state = StateManager.getState(id)
|
||||||
|
|
||||||
-- Return only the active state properties (not tracking frames or internal state)
|
-- Return only the active state properties (not tracking frames or internal state)
|
||||||
return {
|
return {
|
||||||
hover = state.hover,
|
hover = state.hover,
|
||||||
|
|||||||
@@ -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 utf8 = utf8 or require("utf8")
|
||||||
|
|
||||||
local TextEditor = {}
|
local TextEditor = {}
|
||||||
|
|||||||
@@ -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 modulePath = (...):match("(.-)[^%.]+$")
|
||||||
local function req(name)
|
local function req(name)
|
||||||
return require(modulePath .. name)
|
return require(modulePath .. name)
|
||||||
end
|
end
|
||||||
|
|
||||||
local NinePatchParser = req("NinePatchParser")
|
local NinePatchParser = req("NinePatchParser")
|
||||||
local ImageScaler = req("ImageScaler")
|
|
||||||
|
|
||||||
--- Standardized error message formatter
|
--- Standardized error message formatter
|
||||||
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
||||||
|
|||||||
@@ -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
|
---@class ThemeManager
|
||||||
---@field theme string? -- Theme name to use
|
---@field theme string? -- Theme name to use
|
||||||
---@field themeComponent string? -- Component name from theme (e.g., "button", "panel")
|
---@field themeComponent string? -- Component name from theme (e.g., "button", "panel")
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
-- ====================
|
|
||||||
-- Units System
|
|
||||||
-- ====================
|
|
||||||
|
|
||||||
--- Unit parsing and viewport calculations
|
|
||||||
local Units = {}
|
local Units = {}
|
||||||
|
|
||||||
--- Parse a unit value (string or number) into value and unit type
|
--- Parse a unit value (string or number) into value and unit type
|
||||||
|
|||||||
127
modules/types.lua
Normal file
127
modules/types.lua
Normal file
@@ -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 = {}
|
||||||
@@ -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 = {
|
local enums = {
|
||||||
---@enum TextAlign
|
---@enum TextAlign
|
||||||
TextAlign = { START = "start", CENTER = "center", END = "end", JUSTIFY = "justify" },
|
TextAlign = { START = "start", CENTER = "center", END = "end", JUSTIFY = "justify" },
|
||||||
|
|||||||
Reference in New Issue
Block a user