some consolidation
This commit is contained in:
@@ -24,11 +24,17 @@ local EventHandler = req("EventHandler")
|
||||
local ScrollManager = req("ScrollManager")
|
||||
local ErrorHandler = req("ErrorHandler")
|
||||
|
||||
-- Initialize ErrorHandler for validation utilities
|
||||
utils.initializeErrorHandler(ErrorHandler)
|
||||
|
||||
-- Extract utilities
|
||||
local enums = utils.enums
|
||||
local FONT_CACHE = utils.FONT_CACHE
|
||||
local resolveTextSizePreset = utils.resolveTextSizePreset
|
||||
local getModifiers = utils.getModifiers
|
||||
local validateEnum = utils.validateEnum
|
||||
local validateRange = utils.validateRange
|
||||
local validateType = utils.validateType
|
||||
|
||||
-- Extract enum values
|
||||
local Positioning, FlexDirection, JustifyContent, AlignContent, AlignItems, TextAlign, AlignSelf, JustifySelf, FlexWrap =
|
||||
@@ -185,52 +191,6 @@ local Positioning, FlexDirection, JustifyContent, AlignContent, AlignItems, Text
|
||||
local Element = {}
|
||||
Element.__index = Element
|
||||
|
||||
-- Validation helper functions
|
||||
local function validateEnum(value, enumTable, propName, moduleName)
|
||||
if value == nil then
|
||||
return true
|
||||
end
|
||||
|
||||
for _, validValue in pairs(enumTable) do
|
||||
if value == validValue then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- Build list of valid options
|
||||
local validOptions = {}
|
||||
for _, v in pairs(enumTable) do
|
||||
table.insert(validOptions, "'" .. v .. "'")
|
||||
end
|
||||
table.sort(validOptions)
|
||||
|
||||
ErrorHandler.error(moduleName or "Element", string.format("%s must be one of: %s. Got: '%s'", propName, table.concat(validOptions, ", "), tostring(value)))
|
||||
end
|
||||
|
||||
local function validateRange(value, min, max, propName, moduleName)
|
||||
if value == nil then
|
||||
return true
|
||||
end
|
||||
if type(value) ~= "number" then
|
||||
ErrorHandler.error(moduleName or "Element", string.format("%s must be a number, got %s", propName, type(value)))
|
||||
end
|
||||
if value < min or value > max then
|
||||
ErrorHandler.error(moduleName or "Element", string.format("%s must be between %s and %s, got %s", propName, tostring(min), tostring(max), tostring(value)))
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function validateType(value, expectedType, propName, moduleName)
|
||||
if value == nil then
|
||||
return true
|
||||
end
|
||||
local actualType = type(value)
|
||||
if actualType ~= expectedType then
|
||||
ErrorHandler.error(moduleName or "Element", string.format("%s must be %s, got %s", propName, expectedType, actualType))
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---@param props ElementProps
|
||||
---@return Element
|
||||
function Element.new(props)
|
||||
@@ -277,6 +237,7 @@ function Element.new(props)
|
||||
self._eventHandler = EventHandler.new(eventHandlerConfig, {
|
||||
InputEvent = InputEvent,
|
||||
Context = Context,
|
||||
utils = utils,
|
||||
})
|
||||
self._eventHandler:initialize(self)
|
||||
|
||||
@@ -2265,33 +2226,9 @@ function Element:calculateTextWidth()
|
||||
return 0
|
||||
end
|
||||
|
||||
if self.textSize then
|
||||
local fontPath = nil
|
||||
if self.fontFamily then
|
||||
local themeToUse = self._themeManager:getTheme()
|
||||
if themeToUse and themeToUse.fonts and themeToUse.fonts[self.fontFamily] then
|
||||
fontPath = themeToUse.fonts[self.fontFamily]
|
||||
else
|
||||
fontPath = self.fontFamily
|
||||
end
|
||||
elseif self.themeComponent then
|
||||
fontPath = self._themeManager:getDefaultFontFamily()
|
||||
end
|
||||
|
||||
local tempFont = FONT_CACHE.get(self.textSize, fontPath)
|
||||
local width = tempFont:getWidth(self.text)
|
||||
if self.contentAutoSizingMultiplier and self.contentAutoSizingMultiplier.width then
|
||||
width = width * self.contentAutoSizingMultiplier.width
|
||||
end
|
||||
return width
|
||||
end
|
||||
|
||||
local font = love.graphics.getFont()
|
||||
local font = utils.getFont(self.textSize, self.fontFamily, self.themeComponent, self._themeManager)
|
||||
local width = font:getWidth(self.text)
|
||||
if self.contentAutoSizingMultiplier and self.contentAutoSizingMultiplier.width then
|
||||
width = width * self.contentAutoSizingMultiplier.width
|
||||
end
|
||||
return width
|
||||
return utils.applyContentMultiplier(width, self.contentAutoSizingMultiplier, "width")
|
||||
end
|
||||
|
||||
---@return number
|
||||
@@ -2300,24 +2237,7 @@ function Element:calculateTextHeight()
|
||||
return 0
|
||||
end
|
||||
|
||||
local font
|
||||
if self.textSize then
|
||||
local fontPath = nil
|
||||
if self.fontFamily then
|
||||
local themeToUse = self._themeManager:getTheme()
|
||||
if themeToUse and themeToUse.fonts and themeToUse.fonts[self.fontFamily] then
|
||||
fontPath = themeToUse.fonts[self.fontFamily]
|
||||
else
|
||||
fontPath = self.fontFamily
|
||||
end
|
||||
elseif self.themeComponent then
|
||||
fontPath = self._themeManager:getDefaultFontFamily()
|
||||
end
|
||||
font = FONT_CACHE.get(self.textSize, fontPath)
|
||||
else
|
||||
font = love.graphics.getFont()
|
||||
end
|
||||
|
||||
local font = utils.getFont(self.textSize, self.fontFamily, self.themeComponent, self._themeManager)
|
||||
local height = font:getHeight()
|
||||
|
||||
if self.textWrap and (self.textWrap == "word" or self.textWrap == "char" or self.textWrap == true) then
|
||||
@@ -2333,11 +2253,7 @@ function Element:calculateTextHeight()
|
||||
end
|
||||
end
|
||||
|
||||
if self.contentAutoSizingMultiplier and self.contentAutoSizingMultiplier.height then
|
||||
height = height * self.contentAutoSizingMultiplier.height
|
||||
end
|
||||
|
||||
return height
|
||||
return utils.applyContentMultiplier(height, self.contentAutoSizingMultiplier, "height")
|
||||
end
|
||||
|
||||
function Element:calculateAutoWidth()
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
local function getModifiers()
|
||||
return {
|
||||
shift = love.keyboard.isDown("lshift") or love.keyboard.isDown("rshift"),
|
||||
ctrl = love.keyboard.isDown("lctrl") or love.keyboard.isDown("rctrl"),
|
||||
alt = love.keyboard.isDown("lalt") or love.keyboard.isDown("ralt"),
|
||||
meta = love.keyboard.isDown("lgui") or love.keyboard.isDown("rgui"),
|
||||
}
|
||||
end
|
||||
|
||||
---@class EventHandler
|
||||
---@field onEvent fun(element:Element, event:InputEvent)?
|
||||
---@field _pressed table<number, boolean>
|
||||
@@ -23,12 +14,13 @@ end
|
||||
---@field _scrollbarPressHandled boolean
|
||||
---@field _InputEvent table
|
||||
---@field _Context table
|
||||
---@field _utils table
|
||||
local EventHandler = {}
|
||||
EventHandler.__index = EventHandler
|
||||
|
||||
--- Create a new EventHandler instance
|
||||
---@param config table Configuration options
|
||||
---@param deps table Dependencies {InputEvent, Context}
|
||||
---@param deps table Dependencies {InputEvent, Context, utils}
|
||||
---@return EventHandler
|
||||
function EventHandler.new(config, deps)
|
||||
config = config or {}
|
||||
@@ -37,6 +29,7 @@ function EventHandler.new(config, deps)
|
||||
|
||||
self._InputEvent = deps.InputEvent
|
||||
self._Context = deps.Context
|
||||
self._utils = deps.utils
|
||||
|
||||
self.onEvent = config.onEvent
|
||||
|
||||
@@ -222,7 +215,7 @@ function EventHandler:_handleMousePress(mx, my, button)
|
||||
|
||||
-- Fire press event
|
||||
if self.onEvent then
|
||||
local modifiers = getModifiers()
|
||||
local modifiers = self._utils.getModifiers()
|
||||
local pressEvent = self._InputEvent.new({
|
||||
type = "press",
|
||||
button = button,
|
||||
@@ -267,7 +260,7 @@ function EventHandler:_handleMouseDrag(mx, my, button, isHovering)
|
||||
if lastX ~= mx or lastY ~= my then
|
||||
-- Mouse has moved - fire drag event only if still hovering
|
||||
if self.onEvent and isHovering then
|
||||
local modifiers = getModifiers()
|
||||
local modifiers = self._utils.getModifiers()
|
||||
local dx = mx - self._dragStartX[button]
|
||||
local dy = my - self._dragStartY[button]
|
||||
|
||||
@@ -307,7 +300,7 @@ function EventHandler:_handleMouseRelease(mx, my, button)
|
||||
local element = self._element
|
||||
|
||||
local currentTime = love.timer.getTime()
|
||||
local modifiers = getModifiers()
|
||||
local modifiers = self._utils.getModifiers()
|
||||
|
||||
-- Determine click count (double-click detection)
|
||||
local clickCount = 1
|
||||
@@ -412,7 +405,7 @@ function EventHandler:processTouchEvents()
|
||||
button = 1,
|
||||
x = tx,
|
||||
y = ty,
|
||||
modifiers = getModifiers(),
|
||||
modifiers = self._utils.getModifiers(),
|
||||
clickCount = 1,
|
||||
})
|
||||
self.onEvent(element, touchEvent)
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
local modulePath = (...):match("(.-)[^%.]+$")
|
||||
local function req(name)
|
||||
return require(modulePath .. name)
|
||||
end
|
||||
|
||||
local utils = req("utils")
|
||||
|
||||
---@class ImageCache
|
||||
---@field _cache table<string, {image: love.Image, imageData: love.ImageData?}>
|
||||
local ImageCache = {}
|
||||
ImageCache._cache = {}
|
||||
|
||||
--- Normalize a file path for consistent cache keys
|
||||
---@param path string -- File path to normalize
|
||||
---@return string -- Normalized path
|
||||
local function normalizePath(path)
|
||||
path = path:match("^%s*(.-)%s*$")
|
||||
path = path:gsub("\\", "/")
|
||||
path = path:gsub("/+", "/")
|
||||
return path
|
||||
end
|
||||
|
||||
--- Load an image from file path with caching
|
||||
--- Returns cached image if already loaded, otherwise loads and caches it
|
||||
---@param imagePath string -- Path to image file
|
||||
@@ -24,7 +21,7 @@ function ImageCache.load(imagePath, loadImageData)
|
||||
return nil, "Invalid image path: path must be a non-empty string"
|
||||
end
|
||||
|
||||
local normalizedPath = normalizePath(imagePath)
|
||||
local normalizedPath = utils.normalizePath(imagePath)
|
||||
|
||||
if ImageCache._cache[normalizedPath] then
|
||||
return ImageCache._cache[normalizedPath].image, nil
|
||||
@@ -61,7 +58,7 @@ function ImageCache.get(imagePath)
|
||||
return nil
|
||||
end
|
||||
|
||||
local normalizedPath = normalizePath(imagePath)
|
||||
local normalizedPath = utils.normalizePath(imagePath)
|
||||
local cached = ImageCache._cache[normalizedPath]
|
||||
return cached and cached.image or nil
|
||||
end
|
||||
@@ -74,7 +71,7 @@ function ImageCache.getImageData(imagePath)
|
||||
return nil
|
||||
end
|
||||
|
||||
local normalizedPath = normalizePath(imagePath)
|
||||
local normalizedPath = utils.normalizePath(imagePath)
|
||||
local cached = ImageCache._cache[normalizedPath]
|
||||
return cached and cached.imageData or nil
|
||||
end
|
||||
@@ -87,7 +84,7 @@ function ImageCache.remove(imagePath)
|
||||
return false
|
||||
end
|
||||
|
||||
local normalizedPath = normalizePath(imagePath)
|
||||
local normalizedPath = utils.normalizePath(imagePath)
|
||||
if ImageCache._cache[normalizedPath] then
|
||||
local cached = ImageCache._cache[normalizedPath]
|
||||
if cached.image then
|
||||
|
||||
@@ -352,18 +352,7 @@ end
|
||||
---@param element table Reference to the parent Element instance
|
||||
---@return love.Font
|
||||
function Renderer:getFont(element)
|
||||
-- Get font path from theme or element
|
||||
local fontPath = nil
|
||||
if element.fontFamily then
|
||||
local themeToUse = element._themeManager:getTheme()
|
||||
if themeToUse and themeToUse.fonts and themeToUse.fonts[element.fontFamily] then
|
||||
fontPath = themeToUse.fonts[element.fontFamily]
|
||||
else
|
||||
fontPath = element.fontFamily
|
||||
end
|
||||
end
|
||||
|
||||
return self._FONT_CACHE.getFont(element.textSize, fontPath)
|
||||
return self._utils.getFont(element.textSize, element.fontFamily, element.themeComponent, element._themeManager)
|
||||
end
|
||||
|
||||
--- Wrap a line of text based on element's textWrap mode
|
||||
@@ -596,27 +585,8 @@ function Renderer:drawText(element)
|
||||
|
||||
local origFont = love.graphics.getFont()
|
||||
if element.textSize then
|
||||
-- Resolve font path from font family
|
||||
local fontPath = nil
|
||||
if element.fontFamily then
|
||||
-- Check if fontFamily is a theme font name
|
||||
local themeToUse = element.theme and self._Theme.get(element.theme) or self._Theme.getActive()
|
||||
if themeToUse and themeToUse.fonts and themeToUse.fonts[element.fontFamily] then
|
||||
fontPath = themeToUse.fonts[element.fontFamily]
|
||||
else
|
||||
-- Treat as direct path to font file
|
||||
fontPath = element.fontFamily
|
||||
end
|
||||
elseif element.themeComponent then
|
||||
-- If using themeComponent but no fontFamily specified, check for default font in theme
|
||||
local themeToUse = element.theme and self._Theme.get(element.theme) or self._Theme.getActive()
|
||||
if themeToUse and themeToUse.fonts and themeToUse.fonts.default then
|
||||
fontPath = themeToUse.fonts.default
|
||||
end
|
||||
end
|
||||
|
||||
-- Use cached font instead of creating new one every frame
|
||||
local font = self._FONT_CACHE.get(element.textSize, fontPath)
|
||||
local font = self._utils.getFont(element.textSize, element.fontFamily, element.themeComponent, element._themeManager)
|
||||
love.graphics.setFont(font)
|
||||
end
|
||||
local font = love.graphics.getFont()
|
||||
@@ -770,16 +740,7 @@ function Renderer:drawText(element)
|
||||
-- Set up font for cursor rendering
|
||||
local origFont = love.graphics.getFont()
|
||||
if element.textSize then
|
||||
local fontPath = nil
|
||||
if element.fontFamily then
|
||||
local themeToUse = element.theme and self._Theme.get(element.theme) or self._Theme.getActive()
|
||||
if themeToUse and themeToUse.fonts and themeToUse.fonts[element.fontFamily] then
|
||||
fontPath = themeToUse.fonts[element.fontFamily]
|
||||
else
|
||||
fontPath = element.fontFamily
|
||||
end
|
||||
end
|
||||
local font = self._FONT_CACHE.get(element.textSize, fontPath)
|
||||
local font = self._utils.getFont(element.textSize, element.fontFamily, element.themeComponent, element._themeManager)
|
||||
love.graphics.setFont(font)
|
||||
end
|
||||
|
||||
@@ -830,10 +791,12 @@ function Renderer:drawScrollbars(element, x, y, w, h, dims)
|
||||
local thumbColor = element.scrollbarColor
|
||||
if element._scrollbarDragging and element._hoveredScrollbar == "vertical" then
|
||||
-- Active state: brighter
|
||||
thumbColor = self._Color.new(math.min(1, thumbColor.r * 1.4), math.min(1, thumbColor.g * 1.4), math.min(1, thumbColor.b * 1.4), thumbColor.a)
|
||||
local r, g, b, a = self._utils.brightenColor(thumbColor.r, thumbColor.g, thumbColor.b, thumbColor.a, 1.4)
|
||||
thumbColor = self._Color.new(r, g, b, a)
|
||||
elseif element._scrollbarHoveredVertical then
|
||||
-- Hover state: slightly brighter
|
||||
thumbColor = self._Color.new(math.min(1, thumbColor.r * 1.2), math.min(1, thumbColor.g * 1.2), math.min(1, thumbColor.b * 1.2), thumbColor.a)
|
||||
local r, g, b, a = self._utils.brightenColor(thumbColor.r, thumbColor.g, thumbColor.b, thumbColor.a, 1.2)
|
||||
thumbColor = self._Color.new(r, g, b, a)
|
||||
end
|
||||
|
||||
-- Draw track
|
||||
@@ -857,10 +820,12 @@ function Renderer:drawScrollbars(element, x, y, w, h, dims)
|
||||
local thumbColor = element.scrollbarColor
|
||||
if element._scrollbarDragging and element._hoveredScrollbar == "horizontal" then
|
||||
-- Active state: brighter
|
||||
thumbColor = self._Color.new(math.min(1, thumbColor.r * 1.4), math.min(1, thumbColor.g * 1.4), math.min(1, thumbColor.b * 1.4), thumbColor.a)
|
||||
local r, g, b, a = self._utils.brightenColor(thumbColor.r, thumbColor.g, thumbColor.b, thumbColor.a, 1.4)
|
||||
thumbColor = self._Color.new(r, g, b, a)
|
||||
elseif element._scrollbarHoveredHorizontal then
|
||||
-- Hover state: slightly brighter
|
||||
thumbColor = self._Color.new(math.min(1, thumbColor.r * 1.2), math.min(1, thumbColor.g * 1.2), math.min(1, thumbColor.b * 1.2), thumbColor.a)
|
||||
local r, g, b, a = self._utils.brightenColor(thumbColor.r, thumbColor.g, thumbColor.b, thumbColor.a, 1.2)
|
||||
thumbColor = self._Color.new(r, g, b, a)
|
||||
end
|
||||
|
||||
-- Draw track
|
||||
|
||||
@@ -25,19 +25,21 @@
|
||||
---@field _scrollbarDragOffset number -- Offset from thumb top when drag started
|
||||
---@field _scrollbarPressHandled boolean -- Track if scrollbar press was handled this frame
|
||||
---@field _Color table
|
||||
---@field _utils table
|
||||
local ScrollManager = {}
|
||||
ScrollManager.__index = ScrollManager
|
||||
|
||||
--- Create a new ScrollManager instance
|
||||
---@param config table Configuration options
|
||||
---@param deps table Dependencies {Color: Color module}
|
||||
---@param deps table Dependencies {Color: Color module, utils: utils module}
|
||||
---@return ScrollManager
|
||||
function ScrollManager.new(config, deps)
|
||||
local Color = deps.Color
|
||||
local self = setmetatable({}, ScrollManager)
|
||||
|
||||
-- Store dependency for instance methods
|
||||
-- Store dependencies for instance methods
|
||||
self._Color = Color
|
||||
self._utils = deps.utils
|
||||
|
||||
-- Configuration
|
||||
self.overflow = config.overflow or "hidden"
|
||||
@@ -53,20 +55,7 @@ function ScrollManager.new(config, deps)
|
||||
self.scrollSpeed = config.scrollSpeed or 20
|
||||
|
||||
-- hideScrollbars can be boolean or table {vertical: boolean, horizontal: boolean}
|
||||
if config.hideScrollbars ~= nil then
|
||||
if type(config.hideScrollbars) == "boolean" then
|
||||
self.hideScrollbars = { vertical = config.hideScrollbars, horizontal = config.hideScrollbars }
|
||||
elseif type(config.hideScrollbars) == "table" then
|
||||
self.hideScrollbars = {
|
||||
vertical = config.hideScrollbars.vertical ~= nil and config.hideScrollbars.vertical or false,
|
||||
horizontal = config.hideScrollbars.horizontal ~= nil and config.hideScrollbars.horizontal or false,
|
||||
}
|
||||
else
|
||||
self.hideScrollbars = { vertical = false, horizontal = false }
|
||||
end
|
||||
else
|
||||
self.hideScrollbars = { vertical = false, horizontal = false }
|
||||
end
|
||||
self.hideScrollbars = self._utils.normalizeBooleanTable(config.hideScrollbars, false)
|
||||
|
||||
-- Internal overflow state
|
||||
self._overflowX = false
|
||||
@@ -163,8 +152,8 @@ function ScrollManager:detectOverflow()
|
||||
self._maxScrollY = math.max(0, self._contentHeight - containerHeight)
|
||||
|
||||
-- Clamp current scroll position to new bounds
|
||||
self._scrollX = math.max(0, math.min(self._scrollX, self._maxScrollX))
|
||||
self._scrollY = math.max(0, math.min(self._scrollY, self._maxScrollY))
|
||||
self._scrollX = self._utils.clamp(self._scrollX, 0, self._maxScrollX)
|
||||
self._scrollY = self._utils.clamp(self._scrollY, 0, self._maxScrollY)
|
||||
end
|
||||
|
||||
--- Set scroll position with bounds clamping
|
||||
@@ -172,10 +161,10 @@ end
|
||||
---@param y number? -- Y scroll position (nil to keep current)
|
||||
function ScrollManager:setScroll(x, y)
|
||||
if x ~= nil then
|
||||
self._scrollX = math.max(0, math.min(x, self._maxScrollX))
|
||||
self._scrollX = self._utils.clamp(x, 0, self._maxScrollX)
|
||||
end
|
||||
if y ~= nil then
|
||||
self._scrollY = math.max(0, math.min(y, self._maxScrollY))
|
||||
self._scrollY = self._utils.clamp(y, 0, self._maxScrollY)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -190,10 +179,10 @@ end
|
||||
---@param dy number? -- Y delta (nil for no change)
|
||||
function ScrollManager:scrollBy(dx, dy)
|
||||
if dx then
|
||||
self._scrollX = math.max(0, math.min(self._scrollX + dx, self._maxScrollX))
|
||||
self._scrollX = self._utils.clamp(self._scrollX + dx, 0, self._maxScrollX)
|
||||
end
|
||||
if dy then
|
||||
self._scrollY = math.max(0, math.min(self._scrollY + dy, self._maxScrollY))
|
||||
self._scrollY = self._utils.clamp(self._scrollY + dy, 0, self._maxScrollY)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -454,7 +443,7 @@ function ScrollManager:handleMouseMove(mouseX, mouseY)
|
||||
|
||||
-- Calculate new thumb position
|
||||
local newThumbY = mouseY - self._scrollbarDragOffset - trackY
|
||||
newThumbY = math.max(0, math.min(newThumbY, trackH - thumbH))
|
||||
newThumbY = self._utils.clamp(newThumbY, 0, trackH - thumbH)
|
||||
|
||||
-- Convert thumb position to scroll position
|
||||
local scrollRatio = (trackH - thumbH) > 0 and (newThumbY / (trackH - thumbH)) or 0
|
||||
@@ -470,7 +459,7 @@ function ScrollManager:handleMouseMove(mouseX, mouseY)
|
||||
|
||||
-- Calculate new thumb position
|
||||
local newThumbX = mouseX - self._scrollbarDragOffset - trackX
|
||||
newThumbX = math.max(0, math.min(newThumbX, trackW - thumbW))
|
||||
newThumbX = self._utils.clamp(newThumbX, 0, trackW - thumbW)
|
||||
|
||||
-- Convert thumb position to scroll position
|
||||
local scrollRatio = (trackW - thumbW) > 0 and (newThumbX / (trackW - thumbW)) or 0
|
||||
@@ -519,7 +508,7 @@ function ScrollManager:_scrollToTrackPosition(mouseX, mouseY, component)
|
||||
|
||||
-- Calculate target thumb position (centered on click)
|
||||
local targetThumbY = mouseY - trackY - (thumbH / 2)
|
||||
targetThumbY = math.max(0, math.min(targetThumbY, trackH - thumbH))
|
||||
targetThumbY = self._utils.clamp(targetThumbY, 0, trackH - thumbH)
|
||||
|
||||
-- Convert to scroll position
|
||||
local scrollRatio = (trackH - thumbH) > 0 and (targetThumbY / (trackH - thumbH)) or 0
|
||||
@@ -534,7 +523,7 @@ function ScrollManager:_scrollToTrackPosition(mouseX, mouseY, component)
|
||||
|
||||
-- Calculate target thumb position (centered on click)
|
||||
local targetThumbX = mouseX - trackX - (thumbW / 2)
|
||||
targetThumbX = math.max(0, math.min(targetThumbX, trackW - thumbW))
|
||||
targetThumbX = self._utils.clamp(targetThumbX, 0, trackW - thumbW)
|
||||
|
||||
-- Convert to scroll position
|
||||
local scrollRatio = (trackW - thumbW) > 0 and (targetThumbX / (trackW - thumbW)) or 0
|
||||
|
||||
@@ -5,6 +5,7 @@ end
|
||||
|
||||
local NinePatchParser = req("NinePatchParser")
|
||||
local Color = req("Color")
|
||||
local utils = req("utils")
|
||||
|
||||
--- Standardized error message formatter
|
||||
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
||||
@@ -52,47 +53,6 @@ end
|
||||
-- Store the base paths when module loads
|
||||
local FLEXLOVE_BASE_PATH, FLEXLOVE_FILESYSTEM_PATH = getFlexLoveBasePath()
|
||||
|
||||
--- Helper function to resolve image paths relative to FlexLove
|
||||
---@param imagePath string
|
||||
---@return string
|
||||
local function resolveImagePath(imagePath)
|
||||
-- If path is already absolute or starts with known LÖVE paths, use as-is
|
||||
if imagePath:match("^/") or imagePath:match("^[A-Z]:") then
|
||||
return imagePath
|
||||
end
|
||||
|
||||
-- Otherwise, make it relative to FlexLove's location
|
||||
return FLEXLOVE_FILESYSTEM_PATH .. "/" .. imagePath
|
||||
end
|
||||
|
||||
--- Safely load an image with error handling
|
||||
--- Returns both Image and ImageData to avoid deprecated getData() API
|
||||
---@param imagePath string
|
||||
---@return love.Image?, love.ImageData?, string? -- Returns image, imageData, or nil with error message
|
||||
local function safeLoadImage(imagePath)
|
||||
local success, imageData = pcall(function()
|
||||
return love.image.newImageData(imagePath)
|
||||
end)
|
||||
|
||||
if not success then
|
||||
local errorMsg = string.format("[FlexLove] Failed to load image data: %s - %s", imagePath, tostring(imageData))
|
||||
print(errorMsg)
|
||||
return nil, nil, errorMsg
|
||||
end
|
||||
|
||||
local imageSuccess, image = pcall(function()
|
||||
return love.graphics.newImage(imageData)
|
||||
end)
|
||||
|
||||
if imageSuccess then
|
||||
return image, imageData, nil
|
||||
else
|
||||
local errorMsg = string.format("[FlexLove] Failed to create image: %s - %s", imagePath, tostring(image))
|
||||
print(errorMsg)
|
||||
return nil, nil, errorMsg
|
||||
end
|
||||
end
|
||||
|
||||
--- Validate theme definition structure
|
||||
---@param definition ThemeDefinition
|
||||
---@return boolean, string? -- Returns true if valid, or false with error message
|
||||
@@ -184,8 +144,8 @@ function Theme.new(definition)
|
||||
-- Load global atlas if it's a string path
|
||||
if definition.atlas then
|
||||
if type(definition.atlas) == "string" then
|
||||
local resolvedPath = resolveImagePath(definition.atlas)
|
||||
local image, imageData, loaderr = safeLoadImage(resolvedPath)
|
||||
local resolvedPath = utils.resolveImagePath(definition.atlas)
|
||||
local image, imageData, loaderr = utils.safeLoadImage(resolvedPath)
|
||||
if image then
|
||||
self.atlas = image
|
||||
self.atlasData = imageData
|
||||
@@ -234,7 +194,7 @@ function Theme.new(definition)
|
||||
-- Helper function to load atlas with 9-patch support
|
||||
local function loadAtlasWithNinePatch(comp, atlasPath, errorContext)
|
||||
---@diagnostic disable-next-line
|
||||
local resolvedPath = resolveImagePath(atlasPath)
|
||||
local resolvedPath = utils.resolveImagePath(atlasPath)
|
||||
---@diagnostic disable-next-line
|
||||
local is9Patch = not comp.insets and atlasPath:match("%.9%.png$")
|
||||
|
||||
@@ -248,7 +208,7 @@ function Theme.new(definition)
|
||||
end
|
||||
end
|
||||
|
||||
local image, imageData, loaderr = safeLoadImage(resolvedPath)
|
||||
local image, imageData, loaderr = utils.safeLoadImage(resolvedPath)
|
||||
if image then
|
||||
-- Strip guide border for 9-patch images
|
||||
if is9Patch and imageData then
|
||||
|
||||
@@ -186,10 +186,268 @@ function FONT_CACHE.getFont(textSize, fontPath)
|
||||
end
|
||||
end
|
||||
|
||||
-- Font resolution utilities
|
||||
|
||||
--- Resolve font path from fontFamily and theme
|
||||
---@param fontFamily string? Font family name or direct path
|
||||
---@param themeComponent string? Theme component name
|
||||
---@param themeManager table? ThemeManager instance
|
||||
---@return string? Resolved font path or nil
|
||||
local function resolveFontPath(fontFamily, themeComponent, themeManager)
|
||||
if fontFamily then
|
||||
-- Check if fontFamily is a theme font name
|
||||
local themeToUse = themeManager and themeManager:getTheme()
|
||||
if themeToUse and themeToUse.fonts and themeToUse.fonts[fontFamily] then
|
||||
return themeToUse.fonts[fontFamily]
|
||||
else
|
||||
-- Treat as direct path to font file
|
||||
return fontFamily
|
||||
end
|
||||
elseif themeComponent and themeManager then
|
||||
-- If using themeComponent but no fontFamily specified, check for default font in theme
|
||||
return themeManager:getDefaultFontFamily()
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Get font for element (resolves from theme or fontFamily)
|
||||
---@param textSize number? Text size in pixels
|
||||
---@param fontFamily string? Font family name or direct path
|
||||
---@param themeComponent string? Theme component name
|
||||
---@param themeManager table? ThemeManager instance
|
||||
---@return love.Font
|
||||
local function getFont(textSize, fontFamily, themeComponent, themeManager)
|
||||
local fontPath = resolveFontPath(fontFamily, themeComponent, themeManager)
|
||||
return FONT_CACHE.getFont(textSize, fontPath)
|
||||
end
|
||||
|
||||
--- Apply content auto-sizing multiplier to a dimension
|
||||
---@param value number The dimension value
|
||||
---@param multiplier table? The contentAutoSizingMultiplier table {width:number?, height:number?}
|
||||
---@param axis "width"|"height" Which axis to apply
|
||||
---@return number The multiplied value
|
||||
local function applyContentMultiplier(value, multiplier, axis)
|
||||
if multiplier and multiplier[axis] then
|
||||
return value * multiplier[axis]
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
-- Validation utilities
|
||||
local ErrorHandler = nil
|
||||
|
||||
--- Initialize ErrorHandler dependency for validation utilities
|
||||
---@param errorHandler table The ErrorHandler module
|
||||
local function initializeErrorHandler(errorHandler)
|
||||
ErrorHandler = errorHandler
|
||||
end
|
||||
|
||||
--- Validate that a value is in an enum table
|
||||
---@param value any Value to validate
|
||||
---@param enumTable table Enum table with valid values
|
||||
---@param propName string Property name for error messages
|
||||
---@param moduleName string? Module name for error messages (default: "Element")
|
||||
---@return boolean True if valid
|
||||
local function validateEnum(value, enumTable, propName, moduleName)
|
||||
if value == nil then
|
||||
return true
|
||||
end
|
||||
|
||||
for _, validValue in pairs(enumTable) do
|
||||
if value == validValue then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- Build list of valid options
|
||||
local validOptions = {}
|
||||
for _, v in pairs(enumTable) do
|
||||
table.insert(validOptions, "'" .. v .. "'")
|
||||
end
|
||||
table.sort(validOptions)
|
||||
|
||||
if ErrorHandler then
|
||||
ErrorHandler.error(moduleName or "Element", string.format("%s must be one of: %s. Got: '%s'", propName, table.concat(validOptions, ", "), tostring(value)))
|
||||
else
|
||||
error(string.format("%s must be one of: %s. Got: '%s'", propName, table.concat(validOptions, ", "), tostring(value)))
|
||||
end
|
||||
end
|
||||
|
||||
--- Validate that a numeric value is within a range
|
||||
---@param value any Value to validate
|
||||
---@param min number Minimum allowed value
|
||||
---@param max number Maximum allowed value
|
||||
---@param propName string Property name for error messages
|
||||
---@param moduleName string? Module name for error messages (default: "Element")
|
||||
---@return boolean True if valid
|
||||
local function validateRange(value, min, max, propName, moduleName)
|
||||
if value == nil then
|
||||
return true
|
||||
end
|
||||
if type(value) ~= "number" then
|
||||
if ErrorHandler then
|
||||
ErrorHandler.error(moduleName or "Element", string.format("%s must be a number, got %s", propName, type(value)))
|
||||
else
|
||||
error(string.format("%s must be a number, got %s", propName, type(value)))
|
||||
end
|
||||
end
|
||||
if value < min or value > max then
|
||||
if ErrorHandler then
|
||||
ErrorHandler.error(moduleName or "Element", string.format("%s must be between %s and %s, got %s", propName, tostring(min), tostring(max), tostring(value)))
|
||||
else
|
||||
error(string.format("%s must be between %s and %s, got %s", propName, tostring(min), tostring(max), tostring(value)))
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Validate that a value is of the expected type
|
||||
---@param value any Value to validate
|
||||
---@param expectedType string Expected type name
|
||||
---@param propName string Property name for error messages
|
||||
---@param moduleName string? Module name for error messages (default: "Element")
|
||||
---@return boolean True if valid
|
||||
local function validateType(value, expectedType, propName, moduleName)
|
||||
if value == nil then
|
||||
return true
|
||||
end
|
||||
local actualType = type(value)
|
||||
if actualType ~= expectedType then
|
||||
if ErrorHandler then
|
||||
ErrorHandler.error(moduleName or "Element", string.format("%s must be %s, got %s", propName, expectedType, actualType))
|
||||
else
|
||||
error(string.format("%s must be %s, got %s", propName, expectedType, actualType))
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Math utilities
|
||||
|
||||
--- Clamp a value between min and max
|
||||
---@param value number Value to clamp
|
||||
---@param min number Minimum value
|
||||
---@param max number Maximum value
|
||||
---@return number Clamped value
|
||||
local function clamp(value, min, max)
|
||||
return math.max(min, math.min(value, max))
|
||||
end
|
||||
|
||||
--- Linear interpolation between two values
|
||||
---@param a number Start value
|
||||
---@param b number End value
|
||||
---@param t number Interpolation factor (0-1)
|
||||
---@return number Interpolated value
|
||||
local function lerp(a, b, t)
|
||||
return a + (b - a) * t
|
||||
end
|
||||
|
||||
--- Round a number to the nearest integer
|
||||
---@param value number Value to round
|
||||
---@return number Rounded value
|
||||
local function round(value)
|
||||
return math.floor(value + 0.5)
|
||||
end
|
||||
|
||||
-- Path and Image utilities
|
||||
|
||||
--- Normalize a file path for consistent cache keys
|
||||
---@param path string File path to normalize
|
||||
---@return string Normalized path
|
||||
local function normalizePath(path)
|
||||
path = path:match("^%s*(.-)%s*$")
|
||||
path = path:gsub("\\", "/")
|
||||
path = path:gsub("/+", "/")
|
||||
return path
|
||||
end
|
||||
|
||||
--- Safely load an image with error handling
|
||||
--- Returns both Image and ImageData to avoid deprecated getData() API
|
||||
---@param imagePath string Path to image file
|
||||
---@return love.Image?, love.ImageData?, string? Returns image, imageData, or nil with error message
|
||||
local function safeLoadImage(imagePath)
|
||||
local success, imageData = pcall(function()
|
||||
return love.image.newImageData(imagePath)
|
||||
end)
|
||||
|
||||
if not success then
|
||||
local errorMsg = string.format("[FlexLove] Failed to load image data: %s - %s", imagePath, tostring(imageData))
|
||||
print(errorMsg)
|
||||
return nil, nil, errorMsg
|
||||
end
|
||||
|
||||
local imageSuccess, image = pcall(function()
|
||||
return love.graphics.newImage(imageData)
|
||||
end)
|
||||
|
||||
if imageSuccess then
|
||||
return image, imageData, nil
|
||||
else
|
||||
local errorMsg = string.format("[FlexLove] Failed to create image: %s - %s", imagePath, tostring(image))
|
||||
print(errorMsg)
|
||||
return nil, nil, errorMsg
|
||||
end
|
||||
end
|
||||
|
||||
-- Color manipulation utilities
|
||||
|
||||
--- Brighten a color by a factor
|
||||
---@param r number Red component (0-1)
|
||||
---@param g number Green component (0-1)
|
||||
---@param b number Blue component (0-1)
|
||||
---@param a number Alpha component (0-1)
|
||||
---@param factor number Brightness factor (e.g., 1.2 for 20% brighter)
|
||||
---@return number, number, number, number Brightened color components
|
||||
local function brightenColor(r, g, b, a, factor)
|
||||
return math.min(1, r * factor), math.min(1, g * factor), math.min(1, b * factor), a
|
||||
end
|
||||
|
||||
-- Property normalization utilities
|
||||
|
||||
--- Normalize a boolean or table property with vertical/horizontal fields
|
||||
---@param value boolean|table|nil Input value (boolean applies to both, table for individual control)
|
||||
---@param defaultValue boolean Default value if nil (default: false)
|
||||
---@return table Normalized table with vertical and horizontal fields
|
||||
local function normalizeBooleanTable(value, defaultValue)
|
||||
defaultValue = defaultValue or false
|
||||
|
||||
if value == nil then
|
||||
return { vertical = defaultValue, horizontal = defaultValue }
|
||||
end
|
||||
|
||||
if type(value) == "boolean" then
|
||||
return { vertical = value, horizontal = value }
|
||||
end
|
||||
|
||||
if type(value) == "table" then
|
||||
return {
|
||||
vertical = value.vertical ~= nil and value.vertical or defaultValue,
|
||||
horizontal = value.horizontal ~= nil and value.horizontal or defaultValue,
|
||||
}
|
||||
end
|
||||
|
||||
return { vertical = defaultValue, horizontal = defaultValue }
|
||||
end
|
||||
|
||||
return {
|
||||
enums = enums,
|
||||
FONT_CACHE = FONT_CACHE,
|
||||
resolveTextSizePreset = resolveTextSizePreset,
|
||||
getModifiers = getModifiers,
|
||||
TEXT_SIZE_PRESETS = TEXT_SIZE_PRESETS,
|
||||
initializeErrorHandler = initializeErrorHandler,
|
||||
validateEnum = validateEnum,
|
||||
validateRange = validateRange,
|
||||
validateType = validateType,
|
||||
clamp = clamp,
|
||||
lerp = lerp,
|
||||
round = round,
|
||||
normalizePath = normalizePath,
|
||||
safeLoadImage = safeLoadImage,
|
||||
brightenColor = brightenColor,
|
||||
resolveImagePath = resolveImagePath,
|
||||
normalizeBooleanTable = normalizeBooleanTable,
|
||||
resolveFontPath = resolveFontPath,
|
||||
getFont = getFont,
|
||||
applyContentMultiplier = applyContentMultiplier,
|
||||
}
|
||||
|
||||
75
release
75
release
@@ -1,75 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Release script for FlexLove
|
||||
# Usage: ./release <version>
|
||||
# Example: ./release 0.2.0
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Error: Version number required"
|
||||
echo "Usage: ./release <version>"
|
||||
echo "Example: ./release 0.2.0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="$1"
|
||||
|
||||
# Validate version format (basic semver check)
|
||||
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Error: Invalid version format. Use semantic versioning (e.g., 0.2.0)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Preparing release v$VERSION..."
|
||||
|
||||
# Update version in README.md
|
||||
echo "Updating README.md..."
|
||||
sed -i.bak "s/^# FlexLöve v[0-9]\+\.[0-9]\+\.[0-9]\+/# FlexLöve v$VERSION/" README.md
|
||||
rm README.md.bak
|
||||
|
||||
# Update version in FlexLove.lua
|
||||
echo "Updating FlexLove.lua..."
|
||||
sed -i.bak "s/FlexLove\._VERSION = \"[0-9]\+\.[0-9]\+\.[0-9]\+\"/FlexLove._VERSION = \"$VERSION\"/" FlexLove.lua
|
||||
rm FlexLove.lua.bak
|
||||
|
||||
# Show the changes
|
||||
echo ""
|
||||
echo "Changes made:"
|
||||
git diff README.md FlexLove.lua
|
||||
|
||||
# Ask for confirmation
|
||||
echo ""
|
||||
read -p "Do you want to commit these changes and create a release? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Release cancelled. Restoring original files..."
|
||||
git checkout README.md FlexLove.lua
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Commit the changes
|
||||
echo "Committing version bump..."
|
||||
git add README.md FlexLove.lua
|
||||
git commit -m "Bump version to v$VERSION"
|
||||
|
||||
# Create git tag
|
||||
echo "Creating git tag v$VERSION..."
|
||||
git tag -a "v$VERSION" -m "Release v$VERSION"
|
||||
|
||||
# Push changes and tags
|
||||
echo "Pushing to remote..."
|
||||
git push origin main
|
||||
git push origin "v$VERSION"
|
||||
|
||||
# Create GitHub release
|
||||
echo "Creating GitHub release..."
|
||||
gh release create "v$VERSION" \
|
||||
--title "v$VERSION" \
|
||||
--generate-notes
|
||||
|
||||
echo ""
|
||||
echo "✅ Release v$VERSION completed successfully!"
|
||||
echo " - README.md and FlexLove.lua updated"
|
||||
echo " - Git tag v$VERSION created"
|
||||
echo " - GitHub release created"
|
||||
Reference in New Issue
Block a user