diff --git a/FlexLove.lua b/FlexLove.lua index 043e994..a904621 100644 --- a/FlexLove.lua +++ b/FlexLove.lua @@ -85,27 +85,6 @@ function Gui.init(config) end end ---- Check for Z-index coverage (occlusion) ----@param elem Element ----@param clickX number ----@param clickY number ----@return boolean -function Gui.isOccluded(elem, clickX, clickY) - for _, element in ipairs(Gui.topElements) do - if element.z > elem.z and element:contains(clickX, clickY) then - return true - end - for _, child in ipairs(element.children) do - if child.positioning == "absolute" then - if child.z > elem.z and child:contains(clickX, clickY) then - return true - end - end - end - end - return false -end - function Gui.resize() local newWidth, newHeight = love.window.getMode() @@ -214,12 +193,28 @@ function Gui.draw(gameDrawFunc, postDrawFunc) love.graphics.setCanvas(outerCanvas) end +--- Check if element is an ancestor of target +---@param element Element +---@param target Element +---@return boolean +local function isAncestor(element, target) + local current = target.parent + while current do + if current == element then + return true + end + current = current.parent + end + return false +end + --- Find the topmost element at given coordinates ---@param x number ---@param y number ---@return Element? function Gui.getElementAtPosition(x, y) local candidates = {} + local blockingElements = {} local function collectHits(element) local bx = element.x @@ -228,10 +223,17 @@ function Gui.getElementAtPosition(x, y) local bh = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom) if x >= bx and x <= bx + bw and y >= by and y <= by + bh then + -- Collect interactive elements (those with callbacks) if element.callback and not element.disabled then table.insert(candidates, element) end + -- Collect all visible elements for input blocking + -- Elements with opacity > 0 block input to elements below them + if element.opacity > 0 then + table.insert(blockingElements, element) + end + for _, child in ipairs(element.children) do collectHits(child) end @@ -242,11 +244,37 @@ function Gui.getElementAtPosition(x, y) collectHits(element) end + -- Sort both lists by z-index (highest first) table.sort(candidates, function(a, b) return a.z > b.z end) - return candidates[1] + table.sort(blockingElements, function(a, b) + return a.z > b.z + end) + + -- If we have interactive elements, return the topmost one + -- But only if there's no blocking element with higher z-index (that isn't an ancestor) + if #candidates > 0 then + local topCandidate = candidates[1] + + -- Check if any blocking element would prevent this interaction + if #blockingElements > 0 then + local topBlocker = blockingElements[1] + -- If the top blocker has higher z-index than the top candidate, + -- and the blocker is NOT an ancestor of the candidate, + -- return the blocker (even though it has no callback, it blocks input) + if topBlocker.z > topCandidate.z and not isAncestor(topBlocker, topCandidate) then + return topBlocker + end + end + + return topCandidate + end + + -- No interactive elements, but return topmost blocking element if any + -- This prevents clicks from passing through non-interactive overlays + return blockingElements[1] end function Gui.update(dt) diff --git a/modules/Element.lua b/modules/Element.lua index 3548f38..0755aed 100644 --- a/modules/Element.lua +++ b/modules/Element.lua @@ -2786,8 +2786,8 @@ function Element:update(dt) end end - -- Handle scrollbar hover detection local mx, my = love.mouse.getPosition() + local scrollbar = self:_getScrollbarAtPosition(mx, my) -- Update independent hover states for vertical and horizontal scrollbars @@ -2841,7 +2841,6 @@ function Element:update(dt) end end - -- Handle click detection for element with enhanced event system if self.callback or self.themeComponent then -- Clickable area is the border box (x, y already includes padding) -- BORDER-BOX MODEL: Use stored border-box dimensions for hit detection @@ -2851,6 +2850,10 @@ function Element:update(dt) local bh = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom) local isHovering = mx >= bx and mx <= bx + bw and my >= by and my <= by + bh + -- Check if this is the topmost element at the mouse position (z-index ordering) + -- This prevents blocked elements from receiving interactions or visual feedback + local isActiveElement = (Gui._activeEventElement == nil or Gui._activeEventElement == self) + -- Update theme state based on interaction if self.themeComponent then -- Disabled state takes priority @@ -2859,7 +2862,8 @@ function Element:update(dt) -- Active state (for inputs when focused/typing) elseif self.active then self._themeState = "active" - elseif isHovering then + -- Only show hover/pressed states if this element is active (not blocked) + elseif isHovering and isActiveElement then -- Check if any button is pressed local anyPressed = false for _, pressed in pairs(self._pressed) do @@ -2881,7 +2885,6 @@ function Element:update(dt) -- Only process button events if callback exists, element is not disabled, -- and this is the topmost element at the mouse position (z-index ordering) - local isActiveElement = (Gui._activeEventElement == nil or Gui._activeEventElement == self) if self.callback and not self.disabled and isActiveElement then -- Check all three mouse buttons local buttons = { 1, 2, 3 } -- left, right, middle diff --git a/modules/GuiState.lua b/modules/GuiState.lua index 2ad9a44..9153ac3 100644 --- a/modules/GuiState.lua +++ b/modules/GuiState.lua @@ -7,22 +7,22 @@ local GuiState = { -- Top-level elements topElements = {}, - + -- Base scale configuration baseScale = nil, -- {width: number, height: number} - + -- Current scale factors scaleFactors = { x = 1.0, y = 1.0 }, - + -- Default theme name defaultTheme = nil, - + -- Currently focused element (for keyboard input) _focusedElement = nil, - + -- Active event element (for current frame) _activeEventElement = nil, - + -- Cached viewport dimensions _cachedViewport = { width = 0, height = 0 }, }