---@class Renderer ---@field backgroundColor Color ---@field borderColor Color ---@field opacity number ---@field border {top:boolean, right:boolean, bottom:boolean, left:boolean} ---@field cornerRadius {topLeft:number, topRight:number, bottomLeft:number, bottomRight:number} ---@field theme string? ---@field themeComponent string? ---@field _themeState string ---@field imagePath string? ---@field image love.Image? ---@field _loadedImage love.Image? ---@field objectFit string ---@field objectPosition string ---@field imageOpacity number ---@field contentBlur {intensity:number, quality:number}? ---@field backdropBlur {intensity:number, quality:number}? ---@field _blurInstance table? ---@field _element Element? ---@field _Color Color ---@field _RoundedRect table ---@field _NinePatch table ---@field _ImageRenderer table ---@field _ImageCache table ---@field _Theme table ---@field _Transform Transform ---@field _Blur Blur ---@field _utils table ---@field _FONT_CACHE table ---@field _TextAlign table ---@field _ErrorHandler ErrorHandler ---@field _Performance Performance? Performance module dependency local Renderer = {} Renderer.__index = Renderer --- Initialize module with shared dependencies ---@param deps table Dependencies {ErrorHandler, Performance} function Renderer.init(deps) Renderer._ErrorHandler = deps.ErrorHandler Renderer._Performance = deps.Performance end --- Create a new Renderer instance ---@param config table Configuration table with rendering properties ---@param deps table Dependencies {Color, RoundedRect, NinePatch, ImageRenderer, ImageCache, Theme, Blur, Transform, utils} function Renderer.new(config, deps) local Color = deps.Color local ImageCache = deps.ImageCache local self = setmetatable({}, Renderer) -- Store dependencies for instance methods self._Color = Color self._RoundedRect = deps.RoundedRect self._NinePatch = deps.NinePatch self._ImageRenderer = deps.ImageRenderer self._ImageCache = ImageCache self._Theme = deps.Theme self._Blur = deps.Blur self._Transform = deps.Transform self._utils = deps.utils self._FONT_CACHE = deps.utils.FONT_CACHE self._TextAlign = deps.utils.enums.TextAlign -- Visual properties self.backgroundColor = config.backgroundColor or Color.new(0, 0, 0, 0) self.borderColor = config.borderColor or Color.new(0, 0, 0, 1) self.opacity = config.opacity or 1 -- Border configuration self.border = config.border or { top = false, right = false, bottom = false, left = false, } -- Corner radius self.cornerRadius = config.cornerRadius or { topLeft = 0, topRight = 0, bottomLeft = 0, bottomRight = 0, } -- Theme properties self.theme = config.theme self.themeComponent = config.themeComponent self._themeState = "normal" -- Image properties self.imagePath = config.imagePath self.image = config.image self._loadedImage = nil self.objectFit = config.objectFit or "fill" self.objectPosition = config.objectPosition or "center center" self.imageOpacity = config.imageOpacity or 1 self.imageRepeat = config.imageRepeat or "no-repeat" self.imageTint = config.imageTint -- Blur effects self.contentBlur = config.contentBlur self.backdropBlur = config.backdropBlur self._blurInstance = nil -- Load image if path provided if self.imagePath and not self.image then local loadedImage, err = ImageCache.load(self.imagePath) if loadedImage then self._loadedImage = loadedImage else self._loadedImage = nil end elseif self.image then self._loadedImage = self.image else self._loadedImage = nil end return self end --- Get or create blur instance for this element ---@return table|nil Blur instance or nil function Renderer:getBlurInstance() -- Determine quality from blur settings local quality = "medium" if self.contentBlur and self.contentBlur.quality then quality = self.contentBlur.quality elseif self.backdropBlur and self.backdropBlur.quality then quality = self.backdropBlur.quality end -- Map string quality to numeric quality (1-10) local numericQuality = 5 -- default medium if type(quality) == "string" then if quality == "low" then numericQuality = 3 elseif quality == "medium" then numericQuality = 5 elseif quality == "high" then numericQuality = 8 end elseif type(quality) == "number" then numericQuality = quality end -- Create or reuse blur instance if not self._blurInstance or self._blurInstance.quality ~= numericQuality then self._blurInstance = self._Blur.new({ quality = numericQuality }) end return self._blurInstance end --- Set theme state (normal, hover, pressed, disabled, active) ---@param state string The theme state function Renderer:setThemeState(state) self._themeState = state end --- Draw background layer ---@param x number X position ---@param y number Y position ---@param width number Width ---@param height number Height ---@param drawBackgroundColor table Background color (may have animation applied) function Renderer:_drawBackground(x, y, width, height, drawBackgroundColor) local backgroundWithOpacity = self._Color.new(drawBackgroundColor.r, drawBackgroundColor.g, drawBackgroundColor.b, drawBackgroundColor.a * self.opacity) love.graphics.setColor(backgroundWithOpacity:toRGBA()) self._RoundedRect.draw("fill", x, y, width, height, self.cornerRadius) end --- Draw image layer ---@param x number X position (border box) ---@param y number Y position (border box) ---@param paddingLeft number Left padding ---@param paddingTop number Top padding ---@param contentWidth number Content width ---@param contentHeight number Content height ---@param borderBoxWidth number Border box width ---@param borderBoxHeight number Border box height function Renderer:_drawImage(x, y, paddingLeft, paddingTop, contentWidth, contentHeight, borderBoxWidth, borderBoxHeight) if not self._loadedImage then return end -- Calculate image bounds (content area - respects padding) local imageX = x + paddingLeft local imageY = y + paddingTop local imageWidth = contentWidth local imageHeight = contentHeight -- Combine element opacity with imageOpacity local finalOpacity = self.opacity * self.imageOpacity -- Apply cornerRadius clipping if set local hasCornerRadius = false if self.cornerRadius then if type(self.cornerRadius) == "number" then hasCornerRadius = self.cornerRadius > 0 else hasCornerRadius = self.cornerRadius.topLeft > 0 or self.cornerRadius.topRight > 0 or self.cornerRadius.bottomLeft > 0 or self.cornerRadius.bottomRight > 0 end end if hasCornerRadius then -- Use stencil to clip image to rounded corners local success, err = pcall(function() love.graphics.stencil(function() self._RoundedRect.draw("fill", x, y, borderBoxWidth, borderBoxHeight, self.cornerRadius) end, "replace", 1) love.graphics.setStencilTest("greater", 0) end) if not success then -- Check if it's a stencil buffer error if err and err:match("stencil") then local cornerRadiusStr if type(self.cornerRadius) == "number" then cornerRadiusStr = tostring(self.cornerRadius) else cornerRadiusStr = string.format( "TL:%d TR:%d BL:%d BR:%d", self.cornerRadius.topLeft, self.cornerRadius.topRight, self.cornerRadius.bottomLeft, self.cornerRadius.bottomRight ) end Renderer._ErrorHandler:warn("Renderer", "IMG_001", "Cannot apply corner radius to image: stencil buffer not available", { imagePath = self.imagePath or "unknown", cornerRadius = cornerRadiusStr, error = tostring(err), }, "Ensure the active canvas has stencil=true enabled, or remove cornerRadius from images") -- Continue without corner radius hasCornerRadius = false else -- Re-throw if it's a different error error(err, 2) end end end -- Draw the image based on repeat mode if self.imageRepeat and self.imageRepeat ~= "no-repeat" then -- Use tiled rendering self._ImageRenderer.drawTiled(self._loadedImage, imageX, imageY, imageWidth, imageHeight, self.imageRepeat, finalOpacity, self.imageTint) else -- Use standard fit-based rendering self._ImageRenderer.draw(self._loadedImage, imageX, imageY, imageWidth, imageHeight, self.objectFit, self.objectPosition, finalOpacity, self.imageTint) end -- Clear stencil if it was used if hasCornerRadius then love.graphics.setStencilTest() end end --- Draw theme layer (9-patch) ---@param x number X position ---@param y number Y position ---@param borderBoxWidth number Border box width ---@param borderBoxHeight number Border box height ---@param scaleCorners boolean Whether to scale corners (from element) ---@param scalingAlgorithm string Scaling algorithm (from element) function Renderer:_drawTheme(x, y, borderBoxWidth, borderBoxHeight, scaleCorners, scalingAlgorithm) if not self.themeComponent 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 self._Theme.get(self.theme) then themeToUse = self._Theme.get(self.theme) else -- Try to load the theme pcall(function() self._Theme.load(self.theme) end) themeToUse = self._Theme.get(self.theme) end else -- Use active theme themeToUse = self._Theme.getActive() end if not themeToUse then return end -- Get the component from the theme local component = themeToUse.components[self.themeComponent] if not component then return end -- Check for state-specific override local state = self._themeState if state and component.states and component.states[state] then component = component.states[state] end -- Use component-specific atlas if available, otherwise use theme atlas local atlasToUse = component._loadedAtlas or themeToUse.atlas if atlasToUse and component.regions then -- Validate component has required structure local hasAllRegions = component.regions.topLeft 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 hasAllRegions then -- Pass element-level overrides for scaleCorners and scalingAlgorithm self._NinePatch.draw(component, atlasToUse, x, y, borderBoxWidth, borderBoxHeight, self.opacity, scaleCorners, scalingAlgorithm) end end end --- Draw borders ---@param x number X position ---@param y number Y position ---@param borderBoxWidth number Border box width ---@param borderBoxHeight number Border box height function Renderer:_drawBorders(x, y, borderBoxWidth, borderBoxHeight) -- OPTIMIZATION: Early exit if no border (nil or all false) if not self.border then return end -- Handle border as number (uniform border width) if type(self.border) == "number" then local borderColorWithOpacity = self._Color.new(self.borderColor.r, self.borderColor.g, self.borderColor.b, self.borderColor.a * self.opacity) love.graphics.setColor(borderColorWithOpacity:toRGBA()) self._RoundedRect.draw("line", x, y, borderBoxWidth, borderBoxHeight, self.cornerRadius) return end local borderColorWithOpacity = self._Color.new(self.borderColor.r, self.borderColor.g, self.borderColor.b, self.borderColor.a * self.opacity) love.graphics.setColor(borderColorWithOpacity:toRGBA()) -- Check if all borders are enabled local allBorders = self.border.top and self.border.bottom and self.border.left and self.border.right if allBorders then -- Draw complete rounded rectangle border self._RoundedRect.draw("line", x, y, borderBoxWidth, borderBoxHeight, self.cornerRadius) else -- Draw individual borders (without rounded corners for partial borders) if self.border.top then love.graphics.line(x, y, x + borderBoxWidth, y) end if self.border.bottom then love.graphics.line(x, y + borderBoxHeight, x + borderBoxWidth, y + borderBoxHeight) end if self.border.left then love.graphics.line(x, y, x, y + borderBoxHeight) end if self.border.right then love.graphics.line(x + borderBoxWidth, y, x + borderBoxWidth, y + borderBoxHeight) end end end --- Main draw method - renders all visual layers ---@param element Element The parent Element instance ---@param backdropCanvas table|nil Backdrop canvas for backdrop blur function Renderer:draw(element, backdropCanvas) if not element then Renderer._ErrorHandler:warn("Renderer", "SYS_002", "Element parameter required", { method = "draw", }, "Pass element as first parameter to draw()") return end -- Start performance timing local elementId if Renderer._Performance and Renderer._Performance.enabled and element then elementId = element.id or "unnamed" Renderer._Performance:startTimer("render_" .. elementId) Renderer._Performance:incrementCounter("draw_calls", 1) end -- Early exit if element is invisible (optimization) if self.opacity <= 0 then if Renderer._Performance and Renderer._Performance.enabled and elementId then Renderer._Performance:stopTimer("render_" .. elementId) end return end -- Handle opacity during animation local drawBackgroundColor = self.backgroundColor if element.animation then local anim = element.animation:interpolate() if anim.opacity then drawBackgroundColor = self._Color.new(self.backgroundColor.r, self.backgroundColor.g, self.backgroundColor.b, anim.opacity) end end -- Cache border box dimensions for this draw call (optimization) local borderBoxWidth = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right) local borderBoxHeight = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom) -- Apply transform if exists local hasTransform = element.transform and self._Transform and not self._Transform.isIdentity(element.transform) if hasTransform then self._Transform.apply(element.transform, element.x, element.y, element.width, element.height) end -- LAYER 0.5: Draw backdrop blur if configured (before background) if self.backdropBlur and self.backdropBlur.intensity > 0 and backdropCanvas then local blurInstance = self:getBlurInstance() if blurInstance then self._Blur.applyBackdrop(blurInstance, self.backdropBlur.intensity, element.x, element.y, borderBoxWidth, borderBoxHeight, backdropCanvas) end end -- LAYER 1: Draw backgroundColor first (behind everything) self:_drawBackground(element.x, element.y, borderBoxWidth, borderBoxHeight, drawBackgroundColor) -- LAYER 1.5: Draw image on top of backgroundColor (if image exists) self:_drawImage(element.x, element.y, element.padding.left, element.padding.top, element.width, element.height, borderBoxWidth, borderBoxHeight) -- LAYER 2: Draw theme on top of backgroundColor (if theme exists) self:_drawTheme(element.x, element.y, borderBoxWidth, borderBoxHeight, element.scaleCorners, element.scalingAlgorithm) -- LAYER 3: Draw borders on top of theme self:_drawBorders(element.x, element.y, borderBoxWidth, borderBoxHeight) -- Unapply transform if it was applied if hasTransform then self._Transform.unapply() end -- Stop performance timing if Renderer._Performance and Renderer._Performance.enabled and elementId then Renderer._Performance:stopTimer("render_" .. elementId) end end --- Get font for element (resolves from theme or fontFamily) ---@param element table Reference to the parent Element instance ---@return love.Font function Renderer:getFont(element) return self._utils.getFont(element.textSize, element.fontFamily, element.themeComponent, element._themeManager) end --- Wrap a line of text based on element's textWrap mode ---@param element table Reference to the parent Element instance ---@param line string The line of text to wrap ---@param maxWidth number Maximum width for wrapping ---@return table Array of {text, startIdx, endIdx} function Renderer:wrapLine(element, line, maxWidth) -- UTF-8 support local utf8 = utf8 or require("utf8") if not element.editable then return { { text = line, startIdx = 0, endIdx = utf8.len(line) } } end local font = self:getFont(element) local wrappedParts = {} local currentLine = "" local startIdx = 0 -- Helper function to extract a UTF-8 character by character index local function getUtf8Char(str, charIndex) local byteStart = utf8.offset(str, charIndex) if not byteStart then return "" end local byteEnd = utf8.offset(str, charIndex + 1) if byteEnd then return str:sub(byteStart, byteEnd - 1) else return str:sub(byteStart) end end if element.textWrap == "word" then -- Tokenize into words and whitespace, preserving exact spacing local tokens = {} local pos = 1 local lineLen = utf8.len(line) while pos <= lineLen do -- Check if current position is whitespace local char = getUtf8Char(line, pos) if char:match("%s") then -- Collect whitespace sequence local wsStart = pos while pos <= lineLen and getUtf8Char(line, pos):match("%s") do pos = pos + 1 end table.insert(tokens, { type = "space", text = line:sub(utf8.offset(line, wsStart), utf8.offset(line, pos) and utf8.offset(line, pos) - 1 or #line), startPos = wsStart - 1, length = pos - wsStart, }) else -- Collect word (non-whitespace sequence) local wordStart = pos while pos <= lineLen and not getUtf8Char(line, pos):match("%s") do pos = pos + 1 end table.insert(tokens, { type = "word", text = line:sub(utf8.offset(line, wordStart), utf8.offset(line, pos) and utf8.offset(line, pos) - 1 or #line), startPos = wordStart - 1, length = pos - wordStart, }) end end -- Process tokens and wrap local charPos = 0 -- Track our position in the original line for i, token in ipairs(tokens) do if token.type == "word" then local testLine = currentLine .. token.text local width = font:getWidth(testLine) if width > maxWidth and currentLine ~= "" then -- Current line is full, wrap before this word local currentLineLen = utf8.len(currentLine) table.insert(wrappedParts, { text = currentLine, startIdx = startIdx, endIdx = startIdx + currentLineLen, }) startIdx = charPos currentLine = token.text charPos = charPos + token.length -- Check if the word itself is too long - if so, break it with character wrapping if font:getWidth(token.text) > maxWidth then local wordLen = utf8.len(token.text) local charLine = "" local charStartIdx = startIdx for j = 1, wordLen do local char = getUtf8Char(token.text, j) local testCharLine = charLine .. char local charWidth = font:getWidth(testCharLine) if charWidth > maxWidth and charLine ~= "" then table.insert(wrappedParts, { text = charLine, startIdx = charStartIdx, endIdx = charStartIdx + utf8.len(charLine), }) charStartIdx = charStartIdx + utf8.len(charLine) charLine = char else charLine = testCharLine end end currentLine = charLine startIdx = charStartIdx end elseif width > maxWidth and currentLine == "" then -- Word is too long to fit on a line by itself - use character wrapping local wordLen = utf8.len(token.text) local charLine = "" local charStartIdx = startIdx for j = 1, wordLen do local char = getUtf8Char(token.text, j) local testCharLine = charLine .. char local charWidth = font:getWidth(testCharLine) if charWidth > maxWidth and charLine ~= "" then table.insert(wrappedParts, { text = charLine, startIdx = charStartIdx, endIdx = charStartIdx + utf8.len(charLine), }) charStartIdx = charStartIdx + utf8.len(charLine) charLine = char else charLine = testCharLine end end currentLine = charLine startIdx = charStartIdx charPos = charPos + token.length else currentLine = testLine charPos = charPos + token.length end else -- It's whitespace - add to current line currentLine = currentLine .. token.text charPos = charPos + token.length end end else -- Character wrapping local lineLength = utf8.len(line) for i = 1, lineLength do local char = getUtf8Char(line, i) local testLine = currentLine .. char local width = font:getWidth(testLine) if width > maxWidth and currentLine ~= "" then table.insert(wrappedParts, { text = currentLine, startIdx = startIdx, endIdx = startIdx + utf8.len(currentLine), }) currentLine = char startIdx = i - 1 else currentLine = testLine end end end -- Add remaining text if currentLine ~= "" then table.insert(wrappedParts, { text = currentLine, startIdx = startIdx, endIdx = startIdx + utf8.len(currentLine), }) end -- Ensure at least one part if #wrappedParts == 0 then table.insert(wrappedParts, { text = "", startIdx = 0, endIdx = 0, }) end return wrappedParts end --- Draw text content (includes text, cursor, selection, placeholder, password masking) ---@param element table Reference to the parent Element instance function Renderer:drawText(element) -- Update text layout if dirty (for multiline auto-grow) if element._textEditor then element._textEditor:_updateTextIfDirty(element) element._textEditor:updateAutoGrowHeight(element) end -- For editable elements, use TextEditor buffer; for non-editable, use text local displayText = element._textEditor and element._textEditor:getText() or element.text local isPlaceholder = false local isPasswordMasked = false -- Show placeholder if editable and empty if element.editable and (not displayText or displayText == "") and element.placeholder then displayText = element.placeholder isPlaceholder = true end -- Apply password masking if enabled if element.passwordMode and displayText and displayText ~= "" and not isPlaceholder then local maskedText = string.rep("•", utf8.len(displayText)) displayText = maskedText isPasswordMasked = true end if displayText and displayText ~= "" then local textColor = isPlaceholder and self._Color.new(element.textColor.r * 0.5, element.textColor.g * 0.5, element.textColor.b * 0.5, element.textColor.a * 0.5) or element.textColor local textColorWithOpacity = self._Color.new(textColor.r, textColor.g, textColor.b, textColor.a * self.opacity) love.graphics.setColor(textColorWithOpacity:toRGBA()) local origFont = love.graphics.getFont() if element.textSize then -- Use cached font instead of creating new one every frame local font = self._utils.getFont(element.textSize, element.fontFamily, element.themeComponent, element._themeManager) love.graphics.setFont(font) end local font = love.graphics.getFont() local textWidth = font:getWidth(displayText) local textHeight = font:getHeight() local tx, ty -- Text is drawn in the content box (inside padding) -- For 9-patch components, use contentPadding if available local textPaddingLeft = element.padding.left local textPaddingTop = element.padding.top local textAreaWidth = element.width local textAreaHeight = element.height -- Check if we should use 9-patch contentPadding for text positioning local scaledContentPadding = element:getScaledContentPadding() if scaledContentPadding then 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) textPaddingLeft = scaledContentPadding.left textPaddingTop = scaledContentPadding.top textAreaWidth = borderBoxWidth - scaledContentPadding.left - scaledContentPadding.right textAreaHeight = borderBoxHeight - scaledContentPadding.top - scaledContentPadding.bottom end local contentX = element.x + textPaddingLeft local contentY = element.y + textPaddingTop -- Check if text wrapping is enabled if element.textWrap and (element.textWrap == "word" or element.textWrap == "char" or element.textWrap == true) then -- Use printf for wrapped text local align = "left" if element.textAlign == self._TextAlign.CENTER then align = "center" elseif element.textAlign == self._TextAlign.END then align = "right" elseif element.textAlign == self._TextAlign.JUSTIFY then align = "justify" end tx = contentX ty = contentY -- Use printf with the available width for wrapping love.graphics.printf(displayText, tx, ty, textAreaWidth, align) else -- Use regular print for non-wrapped text if element.textAlign == self._TextAlign.START then tx = contentX ty = contentY elseif element.textAlign == self._TextAlign.CENTER then tx = contentX + (textAreaWidth - textWidth) / 2 ty = contentY + (textAreaHeight - textHeight) / 2 elseif element.textAlign == self._TextAlign.END then tx = contentX + textAreaWidth - textWidth - 10 ty = contentY + textAreaHeight - textHeight - 10 elseif element.textAlign == self._TextAlign.JUSTIFY then --- need to figure out spreading tx = contentX ty = contentY end -- Apply scroll offset for editable single-line inputs if element.editable and not element.multiline and element._textScrollX then tx = tx - element._textScrollX end -- Use scissor to clip text to content area for editable inputs if element.editable and not element.multiline then love.graphics.setScissor(contentX, contentY, textAreaWidth, textAreaHeight) end love.graphics.print(displayText, tx, ty) -- Reset scissor if element.editable and not element.multiline then love.graphics.setScissor() end end -- Draw cursor for focused editable elements (even if text is empty) if element._textEditor and element._textEditor:isFocused() and element._textEditor._cursorVisible then local cursorColor = element.cursorColor or element.textColor local cursorWithOpacity = self._Color.new(cursorColor.r, cursorColor.g, cursorColor.b, cursorColor.a * self.opacity) love.graphics.setColor(cursorWithOpacity:toRGBA()) -- Calculate cursor position using TextEditor method local cursorRelX, cursorRelY = element._textEditor:_getCursorScreenPosition(element) local cursorX = contentX + cursorRelX local cursorY = contentY + cursorRelY local cursorHeight = textHeight -- Apply scroll offset for single-line inputs if not element.multiline and element._textEditor._textScrollX then cursorX = cursorX - element._textEditor._textScrollX end -- Apply scissor for single-line editable inputs if not element.multiline then love.graphics.setScissor(contentX, contentY, textAreaWidth, textAreaHeight) end -- Draw cursor line love.graphics.rectangle("fill", cursorX, cursorY, 2, cursorHeight) -- Reset scissor if not element.multiline then love.graphics.setScissor() end end -- Draw selection highlight for editable elements if element._textEditor and element._textEditor:isFocused() and element._textEditor:hasSelection() and element.text and element.text ~= "" then local selStart, selEnd = element._textEditor:getSelection() local selectionColor = element.selectionColor or self._Color.new(0.3, 0.5, 0.8, 0.5) local selectionWithOpacity = self._Color.new(selectionColor.r, selectionColor.g, selectionColor.b, selectionColor.a * self.opacity) -- Get selection rectangles from TextEditor local selectionRects = element._textEditor:_getSelectionRects(element, selStart, selEnd) -- Apply scissor for single-line editable inputs if not element.multiline then love.graphics.setScissor(contentX, contentY, textAreaWidth, textAreaHeight) end -- Draw selection background rectangles love.graphics.setColor(selectionWithOpacity:toRGBA()) for _, rect in ipairs(selectionRects) do local rectX = contentX + rect.x local rectY = contentY + rect.y if not element.multiline and element._textEditor._textScrollX then rectX = rectX - element._textEditor._textScrollX end love.graphics.rectangle("fill", rectX, rectY, rect.width, rect.height) end -- Reset scissor if not element.multiline then love.graphics.setScissor() end end if element.textSize then love.graphics.setFont(origFont) end end -- Draw cursor for focused editable elements even when empty if element._textEditor and element._textEditor:isFocused() and element._textEditor._cursorVisible and (not displayText or displayText == "") then -- Set up font for cursor rendering local origFont = love.graphics.getFont() if element.textSize then local font = self._utils.getFont(element.textSize, element.fontFamily, element.themeComponent, element._themeManager) love.graphics.setFont(font) end local font = love.graphics.getFont() local textHeight = font:getHeight() -- Calculate text area position local textPaddingLeft = element.padding.left local textPaddingTop = element.padding.top local scaledContentPadding = element:getScaledContentPadding() if scaledContentPadding then textPaddingLeft = scaledContentPadding.left textPaddingTop = scaledContentPadding.top end local contentX = element.x + textPaddingLeft local contentY = element.y + textPaddingTop -- Draw cursor local cursorColor = element.cursorColor or element.textColor local cursorWithOpacity = self._Color.new(cursorColor.r, cursorColor.g, cursorColor.b, cursorColor.a * self.opacity) love.graphics.setColor(cursorWithOpacity:toRGBA()) love.graphics.rectangle("fill", contentX, contentY, 2, textHeight) if element.textSize then love.graphics.setFont(origFont) end end end --- Draw scrollbars (both vertical and horizontal) ---@param element table Reference to the parent Element instance ---@param x number X position ---@param y number Y position ---@param w number Width ---@param h number Height ---@param dims table Scrollbar dimensions from _calculateScrollbarDimensions function Renderer:drawScrollbars(element, x, y, w, h, dims) -- Vertical scrollbar if dims.vertical.visible and not element.hideScrollbars.vertical then -- Position scrollbar within content area (x, y is border-box origin) local contentX = x + element.padding.left local contentY = y + element.padding.top local trackX = contentX + w - element.scrollbarWidth - element.scrollbarPadding local trackY = contentY + element.scrollbarPadding -- Determine thumb color based on state (independent for vertical) local thumbColor = element.scrollbarColor if element._scrollbarDragging and element._hoveredScrollbar == "vertical" then -- Active state: brighter 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 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 love.graphics.setColor(element.scrollbarTrackColor:toRGBA()) love.graphics.rectangle("fill", trackX, trackY, element.scrollbarWidth, dims.vertical.trackHeight, element.scrollbarRadius) -- Draw thumb with state-based color love.graphics.setColor(thumbColor:toRGBA()) love.graphics.rectangle("fill", trackX, trackY + dims.vertical.thumbY, element.scrollbarWidth, dims.vertical.thumbHeight, element.scrollbarRadius) end -- Horizontal scrollbar if dims.horizontal.visible and not element.hideScrollbars.horizontal then -- Position scrollbar within content area (x, y is border-box origin) local contentX = x + element.padding.left local contentY = y + element.padding.top local trackX = contentX + element.scrollbarPadding local trackY = contentY + h - element.scrollbarWidth - element.scrollbarPadding -- Determine thumb color based on state (independent for horizontal) local thumbColor = element.scrollbarColor if element._scrollbarDragging and element._hoveredScrollbar == "horizontal" then -- Active state: brighter 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 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 love.graphics.setColor(element.scrollbarTrackColor:toRGBA()) love.graphics.rectangle("fill", trackX, trackY, dims.horizontal.trackWidth, element.scrollbarWidth, element.scrollbarRadius) -- Draw thumb with state-based color love.graphics.setColor(thumbColor:toRGBA()) love.graphics.rectangle("fill", trackX + dims.horizontal.thumbX, trackY, dims.horizontal.thumbWidth, element.scrollbarWidth, element.scrollbarRadius) end -- Reset color love.graphics.setColor(1, 1, 1, 1) end --- Draw visual feedback when element is pressed ---@param x number X position ---@param y number Y position ---@param borderBoxWidth number Border box width ---@param borderBoxHeight number Border box height function Renderer:drawPressedState(x, y, borderBoxWidth, borderBoxHeight) love.graphics.setColor(0.5, 0.5, 0.5, 0.3 * self.opacity) -- Semi-transparent gray for pressed state with opacity self._RoundedRect.draw("fill", x, y, borderBoxWidth, borderBoxHeight, self.cornerRadius) end --- Cleanup renderer resources function Renderer:destroy() self._loadedImage = nil self._blurInstance = nil end --- Cleanup method to break circular references (for immediate mode) function Renderer:_cleanup() -- Renderer doesn't create circular references (no element back-reference) -- Module refs are singletons -- In immediate mode, full element cleanup happens via array clearing end return Renderer