restart
This commit is contained in:
@@ -1,478 +0,0 @@
|
||||
--[[
|
||||
Renderer.lua - Rendering module for FlexLove Element
|
||||
Handles all visual rendering including backgrounds, borders, images, themes, and effects
|
||||
]]
|
||||
|
||||
-- Setup module path for relative requires
|
||||
local modulePath = (...):match("(.-)[^%.]+$")
|
||||
local function req(name)
|
||||
return require(modulePath .. name)
|
||||
end
|
||||
|
||||
-- Module dependencies
|
||||
local Color = req("Color")
|
||||
local RoundedRect = req("RoundedRect")
|
||||
local NinePatch = req("NinePatch")
|
||||
local ImageRenderer = req("ImageRenderer")
|
||||
local Blur = req("Blur")
|
||||
local Theme = req("Theme")
|
||||
local utils = req("utils")
|
||||
|
||||
-- Extract utilities
|
||||
local FONT_CACHE = utils.FONT_CACHE
|
||||
|
||||
-- ====================
|
||||
-- Renderer Class
|
||||
-- ====================
|
||||
|
||||
---@class Renderer
|
||||
---@field element Element -- Reference to parent element
|
||||
---@field backgroundColor Color -- Background color
|
||||
---@field borderColor Color -- Border color
|
||||
---@field opacity number -- Opacity (0-1)
|
||||
---@field border {top:boolean, right:boolean, bottom:boolean, left:boolean} -- Border sides
|
||||
---@field cornerRadius {topLeft:number, topRight:number, bottomLeft:number, bottomRight:number} -- Corner radii
|
||||
---@field theme string? -- Theme name
|
||||
---@field themeComponent string? -- Theme component name
|
||||
---@field _themeState string -- Current theme state (normal, hover, pressed, active, disabled)
|
||||
---@field imagePath string? -- Path to image file
|
||||
---@field image love.Image? -- Image object
|
||||
---@field _loadedImage love.Image? -- Cached loaded image
|
||||
---@field objectFit string -- Image fit mode
|
||||
---@field objectPosition string -- Image position
|
||||
---@field imageOpacity number -- Image opacity
|
||||
---@field contentBlur table? -- Content blur settings
|
||||
---@field backdropBlur table? -- Backdrop blur settings
|
||||
---@field _blurInstance table? -- Cached blur instance
|
||||
---@field scaleCorners number? -- 9-patch corner scale multiplier
|
||||
---@field scalingAlgorithm string? -- 9-patch scaling algorithm
|
||||
---@field disableHighlight boolean -- Disable pressed state highlight
|
||||
local Renderer = {}
|
||||
Renderer.__index = Renderer
|
||||
|
||||
--- Create a new Renderer instance
|
||||
---@param config table -- Configuration options
|
||||
---@return Renderer
|
||||
function Renderer.new(config)
|
||||
local self = setmetatable({}, Renderer)
|
||||
|
||||
-- Initialize rendering state
|
||||
self.backgroundColor = config.backgroundColor or Color.new(0, 0, 0, 0)
|
||||
self.borderColor = config.borderColor or Color.new(0, 0, 0, 1)
|
||||
self.opacity = config.opacity or 1
|
||||
|
||||
-- Border configuration
|
||||
self.border = config.border or {
|
||||
top = false,
|
||||
right = false,
|
||||
bottom = false,
|
||||
left = false,
|
||||
}
|
||||
|
||||
-- Corner radius configuration
|
||||
self.cornerRadius = config.cornerRadius or {
|
||||
topLeft = 0,
|
||||
topRight = 0,
|
||||
bottomLeft = 0,
|
||||
bottomRight = 0,
|
||||
}
|
||||
|
||||
-- Theme configuration
|
||||
self.theme = config.theme
|
||||
self.themeComponent = config.themeComponent
|
||||
self._themeState = config._themeState or "normal"
|
||||
|
||||
-- Image configuration
|
||||
self.imagePath = config.imagePath
|
||||
self.image = config.image
|
||||
self._loadedImage = config._loadedImage
|
||||
self.objectFit = config.objectFit or "fill"
|
||||
self.objectPosition = config.objectPosition or "center center"
|
||||
self.imageOpacity = config.imageOpacity or 1
|
||||
|
||||
-- Blur configuration
|
||||
self.contentBlur = config.contentBlur
|
||||
self.backdropBlur = config.backdropBlur
|
||||
self._blurInstance = config._blurInstance
|
||||
|
||||
-- 9-patch configuration
|
||||
self.scaleCorners = config.scaleCorners
|
||||
self.scalingAlgorithm = config.scalingAlgorithm
|
||||
|
||||
-- Visual feedback configuration
|
||||
self.disableHighlight = config.disableHighlight or false
|
||||
|
||||
-- Element reference (set via initialize)
|
||||
self.element = nil
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Initialize renderer with parent element reference
|
||||
---@param element Element
|
||||
function Renderer:initialize(element)
|
||||
self.element = element
|
||||
end
|
||||
|
||||
--- Main draw method - orchestrates all rendering
|
||||
---@param backdropCanvas love.Canvas? -- Canvas for backdrop blur
|
||||
function Renderer:draw(backdropCanvas)
|
||||
-- Early exit if element is invisible (optimization)
|
||||
if self.opacity <= 0 then
|
||||
return
|
||||
end
|
||||
|
||||
-- Get element reference for convenience
|
||||
local element = self.element
|
||||
if not element then
|
||||
return
|
||||
end
|
||||
|
||||
-- Handle opacity during animation
|
||||
local drawBackgroundColor = self.backgroundColor
|
||||
if element.animation then
|
||||
local anim = element.animation:interpolate()
|
||||
if anim.opacity then
|
||||
drawBackgroundColor = Color.new(self.backgroundColor.r, self.backgroundColor.g, self.backgroundColor.b, anim.opacity)
|
||||
end
|
||||
end
|
||||
|
||||
-- Cache border box dimensions for this draw call (optimization)
|
||||
local borderBoxWidth = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
|
||||
local borderBoxHeight = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom)
|
||||
|
||||
-- LAYER 0.5: Draw backdrop blur if configured (before background)
|
||||
if self.backdropBlur and self.backdropBlur.intensity > 0 and backdropCanvas then
|
||||
local blurInstance = element:getBlurInstance()
|
||||
if blurInstance then
|
||||
Blur.applyBackdrop(blurInstance, self.backdropBlur.intensity, element.x, element.y, borderBoxWidth, borderBoxHeight, backdropCanvas)
|
||||
end
|
||||
end
|
||||
|
||||
-- LAYER 1: Draw backgroundColor first (behind everything)
|
||||
self:drawBackground(element.x, element.y, borderBoxWidth, borderBoxHeight, drawBackgroundColor)
|
||||
|
||||
-- LAYER 1.5: Draw image on top of backgroundColor (if image exists)
|
||||
if self._loadedImage then
|
||||
self:drawImage(element.x, element.y, borderBoxWidth, borderBoxHeight)
|
||||
end
|
||||
|
||||
-- LAYER 2: Draw theme on top of backgroundColor (if theme exists)
|
||||
if self.themeComponent then
|
||||
self:drawTheme(element.x, element.y, borderBoxWidth, borderBoxHeight)
|
||||
end
|
||||
|
||||
-- LAYER 3: Draw borders on top of theme (always render if specified)
|
||||
self:drawBorder(element.x, element.y, borderBoxWidth, borderBoxHeight)
|
||||
end
|
||||
|
||||
--- Draw background with corner radius
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@param width number
|
||||
---@param height number
|
||||
---@param drawBackgroundColor Color? -- Optional override for background color
|
||||
function Renderer:drawBackground(x, y, width, height, drawBackgroundColor)
|
||||
drawBackgroundColor = drawBackgroundColor or self.backgroundColor
|
||||
|
||||
-- Apply opacity to background color
|
||||
local backgroundWithOpacity = Color.new(
|
||||
drawBackgroundColor.r,
|
||||
drawBackgroundColor.g,
|
||||
drawBackgroundColor.b,
|
||||
drawBackgroundColor.a * self.opacity
|
||||
)
|
||||
|
||||
love.graphics.setColor(backgroundWithOpacity:toRGBA())
|
||||
RoundedRect.draw("fill", x, y, width, height, self.cornerRadius)
|
||||
end
|
||||
|
||||
--- Draw image with object-fit modes
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@param borderBoxWidth number
|
||||
---@param borderBoxHeight number
|
||||
function Renderer:drawImage(x, y, borderBoxWidth, borderBoxHeight)
|
||||
if not self._loadedImage or not self.element then
|
||||
return
|
||||
end
|
||||
|
||||
local element = self.element
|
||||
|
||||
-- Calculate image bounds (content area - respects padding)
|
||||
local imageX = x + element.padding.left
|
||||
local imageY = y + element.padding.top
|
||||
local imageWidth = element.width
|
||||
local imageHeight = element.height
|
||||
|
||||
-- Combine element opacity with imageOpacity
|
||||
local finalOpacity = self.opacity * self.imageOpacity
|
||||
|
||||
-- Apply cornerRadius clipping if set
|
||||
local hasCornerRadius = self.cornerRadius.topLeft > 0
|
||||
or self.cornerRadius.topRight > 0
|
||||
or self.cornerRadius.bottomLeft > 0
|
||||
or self.cornerRadius.bottomRight > 0
|
||||
|
||||
if hasCornerRadius then
|
||||
-- Use stencil to clip image to rounded corners
|
||||
love.graphics.stencil(function()
|
||||
RoundedRect.draw("fill", x, y, borderBoxWidth, borderBoxHeight, self.cornerRadius)
|
||||
end, "replace", 1)
|
||||
love.graphics.setStencilTest("greater", 0)
|
||||
end
|
||||
|
||||
-- Draw the image
|
||||
ImageRenderer.draw(self._loadedImage, imageX, imageY, imageWidth, imageHeight, self.objectFit, self.objectPosition, finalOpacity)
|
||||
|
||||
-- Clear stencil if it was used
|
||||
if hasCornerRadius then
|
||||
love.graphics.setStencilTest()
|
||||
end
|
||||
end
|
||||
|
||||
--- Draw theme component using 9-patch rendering
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@param borderBoxWidth number
|
||||
---@param borderBoxHeight number
|
||||
function Renderer:drawTheme(x, y, borderBoxWidth, borderBoxHeight)
|
||||
if not self.themeComponent or not self.element then
|
||||
return
|
||||
end
|
||||
|
||||
-- Get the theme to use
|
||||
local themeToUse = nil
|
||||
if self.theme then
|
||||
-- Element specifies a specific theme - load it if needed
|
||||
if Theme.get(self.theme) then
|
||||
themeToUse = Theme.get(self.theme)
|
||||
else
|
||||
-- Try to load the theme
|
||||
pcall(function()
|
||||
Theme.load(self.theme)
|
||||
end)
|
||||
themeToUse = Theme.get(self.theme)
|
||||
end
|
||||
else
|
||||
-- Use active theme
|
||||
themeToUse = Theme.getActive()
|
||||
end
|
||||
|
||||
if not themeToUse then
|
||||
return
|
||||
end
|
||||
|
||||
-- Get the component from the theme
|
||||
local component = themeToUse.components[self.themeComponent]
|
||||
if not component then
|
||||
return
|
||||
end
|
||||
|
||||
-- Check for state-specific override
|
||||
local state = self._themeState
|
||||
if state and component.states and component.states[state] then
|
||||
component = component.states[state]
|
||||
end
|
||||
|
||||
-- Use component-specific atlas if available, otherwise use theme atlas
|
||||
local atlasToUse = component._loadedAtlas or themeToUse.atlas
|
||||
|
||||
if not atlasToUse or not component.regions then
|
||||
return
|
||||
end
|
||||
|
||||
-- Validate component has required structure
|
||||
local hasAllRegions = component.regions.topLeft
|
||||
and component.regions.topCenter
|
||||
and component.regions.topRight
|
||||
and component.regions.middleLeft
|
||||
and component.regions.middleCenter
|
||||
and component.regions.middleRight
|
||||
and component.regions.bottomLeft
|
||||
and component.regions.bottomCenter
|
||||
and component.regions.bottomRight
|
||||
|
||||
if not hasAllRegions then
|
||||
return
|
||||
end
|
||||
|
||||
-- Pass element-level overrides for scaleCorners and scalingAlgorithm
|
||||
NinePatch.draw(component, atlasToUse, x, y, borderBoxWidth, borderBoxHeight, self.opacity, self.scaleCorners, self.scalingAlgorithm)
|
||||
end
|
||||
|
||||
--- Draw borders on specified sides
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@param width number
|
||||
---@param height number
|
||||
function Renderer:drawBorder(x, y, width, height)
|
||||
-- Apply opacity to border color
|
||||
local borderColorWithOpacity = Color.new(
|
||||
self.borderColor.r,
|
||||
self.borderColor.g,
|
||||
self.borderColor.b,
|
||||
self.borderColor.a * self.opacity
|
||||
)
|
||||
|
||||
love.graphics.setColor(borderColorWithOpacity:toRGBA())
|
||||
|
||||
-- Check if all borders are enabled
|
||||
local allBorders = self.border.top and self.border.bottom and self.border.left and self.border.right
|
||||
|
||||
if allBorders then
|
||||
-- Draw complete rounded rectangle border
|
||||
RoundedRect.draw("line", x, y, width, height, self.cornerRadius)
|
||||
else
|
||||
-- Draw individual borders (without rounded corners for partial borders)
|
||||
if self.border.top then
|
||||
love.graphics.line(x, y, x + width, y)
|
||||
end
|
||||
if self.border.bottom then
|
||||
love.graphics.line(x, y + height, x + width, y + height)
|
||||
end
|
||||
if self.border.left then
|
||||
love.graphics.line(x, y, x, y + height)
|
||||
end
|
||||
if self.border.right then
|
||||
love.graphics.line(x + width, y, x + width, y + height)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Draw pressed state highlight overlay
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@param width number
|
||||
---@param height number
|
||||
function Renderer:drawPressedHighlight(x, y, width, height)
|
||||
if self.disableHighlight or not self.element then
|
||||
return
|
||||
end
|
||||
|
||||
local element = self.element
|
||||
|
||||
-- Check if element has onEvent handler
|
||||
if not element.onEvent then
|
||||
return
|
||||
end
|
||||
|
||||
-- Check if any button is pressed
|
||||
local anyPressed = false
|
||||
for _, pressed in pairs(element._pressed) do
|
||||
if pressed then
|
||||
anyPressed = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if anyPressed then
|
||||
love.graphics.setColor(0.5, 0.5, 0.5, 0.3 * self.opacity) -- Semi-transparent gray for pressed state with opacity
|
||||
RoundedRect.draw("fill", x, y, width, height, self.cornerRadius)
|
||||
end
|
||||
end
|
||||
|
||||
--- Set background color
|
||||
---@param color Color
|
||||
function Renderer:setBackgroundColor(color)
|
||||
self.backgroundColor = color
|
||||
end
|
||||
|
||||
--- Set border color
|
||||
---@param color Color
|
||||
function Renderer:setBorderColor(color)
|
||||
self.borderColor = color
|
||||
end
|
||||
|
||||
--- Set opacity
|
||||
---@param opacity number
|
||||
function Renderer:setOpacity(opacity)
|
||||
self.opacity = opacity
|
||||
end
|
||||
|
||||
--- Set theme state
|
||||
---@param state string
|
||||
function Renderer:setThemeState(state)
|
||||
self._themeState = state
|
||||
end
|
||||
|
||||
--- Set loaded image
|
||||
---@param image love.Image?
|
||||
function Renderer:setLoadedImage(image)
|
||||
self._loadedImage = image
|
||||
end
|
||||
|
||||
--- Get blur instance (delegates to element)
|
||||
---@return table?
|
||||
function Renderer:getBlurInstance()
|
||||
if not self.element then
|
||||
return nil
|
||||
end
|
||||
return self.element:getBlurInstance()
|
||||
end
|
||||
|
||||
--- Update renderer state from element
|
||||
--- Call this when element properties change
|
||||
function Renderer:syncFromElement()
|
||||
if not self.element then
|
||||
return
|
||||
end
|
||||
|
||||
local element = self.element
|
||||
|
||||
-- Sync rendering properties
|
||||
self.backgroundColor = element.backgroundColor
|
||||
self.borderColor = element.borderColor
|
||||
self.opacity = element.opacity
|
||||
self.border = element.border
|
||||
self.cornerRadius = element.cornerRadius
|
||||
self.theme = element.theme
|
||||
self.themeComponent = element.themeComponent
|
||||
self._themeState = element._themeState
|
||||
self.imagePath = element.imagePath
|
||||
self.image = element.image
|
||||
self._loadedImage = element._loadedImage
|
||||
self.objectFit = element.objectFit
|
||||
self.objectPosition = element.objectPosition
|
||||
self.imageOpacity = element.imageOpacity
|
||||
self.contentBlur = element.contentBlur
|
||||
self.backdropBlur = element.backdropBlur
|
||||
self._blurInstance = element._blurInstance
|
||||
self.scaleCorners = element.scaleCorners
|
||||
self.scalingAlgorithm = element.scalingAlgorithm
|
||||
self.disableHighlight = element.disableHighlight
|
||||
end
|
||||
|
||||
--- Update element state from renderer
|
||||
--- Call this when renderer properties change
|
||||
function Renderer:syncToElement()
|
||||
if not self.element then
|
||||
return
|
||||
end
|
||||
|
||||
local element = self.element
|
||||
|
||||
-- Sync rendering properties back to element
|
||||
element.backgroundColor = self.backgroundColor
|
||||
element.borderColor = self.borderColor
|
||||
element.opacity = self.opacity
|
||||
element.border = self.border
|
||||
element.cornerRadius = self.cornerRadius
|
||||
element.theme = self.theme
|
||||
element.themeComponent = self.themeComponent
|
||||
element._themeState = self._themeState
|
||||
element.imagePath = self.imagePath
|
||||
element.image = self.image
|
||||
element._loadedImage = self._loadedImage
|
||||
element.objectFit = self.objectFit
|
||||
element.objectPosition = self.objectPosition
|
||||
element.imageOpacity = self.imageOpacity
|
||||
element.contentBlur = self.contentBlur
|
||||
element.backdropBlur = self.backdropBlur
|
||||
element._blurInstance = self._blurInstance
|
||||
element.scaleCorners = self.scaleCorners
|
||||
element.scalingAlgorithm = self.scalingAlgorithm
|
||||
element.disableHighlight = self.disableHighlight
|
||||
end
|
||||
|
||||
return Renderer
|
||||
Reference in New Issue
Block a user