nano-ui-0.1.0.0: lib/NanoUI/Frame/Hit.hs
{-# LANGUAGE DataKinds #-}
-- | Layout hit testing for modals, windows, and overlay stacking.
module NanoUI.Frame.Hit
( findNodeByWidgetId
, findNodeByKey
, modalTreeOpen
, nodeInSubtree
, widgetIdInSubtree
, overlayHitAllowed
, topmostOverlayAtMouse
, topmostModalAtMouse
, widgetOverlayAllowed
, scrollHitRect
, nodePointVisible
, nodeClippedHit
, nodeInteractionHit
) where
import Data.Maybe (isJust)
import NanoUI.Context (Context (..), getPrevRect, getPrevClipRect)
import NanoUI.Id (WidgetId)
import NanoUI.Layout.Arena
( NodeIdx
, NodeType (NodeModal, NodePopup, NodeScrollContainer, NodeWindow)
, findNodeRevM
, getClipRect
, getNodeType
, getParent
, getRect
, getWidgetId
, lookupNodeByKey
, lookupNodeByWidgetId
, topModalNode
)
import NanoUI.Types (Rect (..), V2 (..), rectContains, rectH, rectW)
findNodeByWidgetId :: Context -> WidgetId -> IO (Maybe NodeIdx)
findNodeByWidgetId ctx wid = lookupNodeByWidgetId (ctxNodeArena ctx) wid
findNodeByKey :: Context -> Int -> IO (Maybe NodeIdx)
findNodeByKey ctx k = lookupNodeByKey (ctxNodeArena ctx) k
modalTreeOpen :: Context -> IO Bool
modalTreeOpen ctx = do
top <- topModalNode (ctxNodeArena ctx)
pure (isJust top)
nodeInSubtree :: Context -> NodeIdx -> NodeIdx -> IO Bool
nodeInSubtree ctx idx top = go idx
where
go i
| i < 0 = pure False
| i == top = pure True
| otherwise = do
parent <- getParent (ctxNodeArena ctx) i
go parent
-- | Membership predicate for an already-resolved subtree root. Callers
-- filtering many widgets can resolve the root once for the whole operation.
widgetIdInSubtree :: Context -> NodeIdx -> WidgetId -> IO Bool
widgetIdInSubtree ctx root wid = do
node <- findNodeByWidgetId ctx wid
maybe (pure False) (\idx -> nodeInSubtree ctx idx root) node
overlayHitAllowed :: Context -> NodeIdx -> V2 -> IO Bool
overlayHitAllowed ctx idx mouse = do
mModal <- topModalNode (ctxNodeArena ctx)
case mModal of
Just top -> nodeInSubtree ctx idx top
Nothing -> do
mTop <- topmostOverlayAtMouse ctx mouse
case mTop of
Nothing -> pure True
Just tidx -> nodeInSubtree ctx idx tidx
topmostOverlayAtMouse :: Context -> V2 -> IO (Maybe NodeIdx)
topmostOverlayAtMouse ctx mouse =
topmostFloatingAtMouse ctx mouse (\nt -> nt == NodeWindow || nt == NodePopup)
topmostModalAtMouse :: Context -> V2 -> IO (Maybe NodeIdx)
topmostModalAtMouse ctx mouse =
topmostFloatingAtMouse ctx mouse (== NodeModal)
topmostFloatingAtMouse :: Context -> V2 -> (NodeType -> Bool) -> IO (Maybe NodeIdx)
topmostFloatingAtMouse ctx mouse wanted =
findNodeRevM (ctxNodeArena ctx) $ \idx -> do
nt <- getNodeType (ctxNodeArena ctx) idx
if not (wanted nt)
then pure False
else do
(x, y, w, h) <- getRect (ctxNodeArena ctx) idx
pure (w > 0 && h > 0 && rectContains (Rect x y w h) mouse)
widgetOverlayAllowed :: Context -> WidgetId -> IO Bool
widgetOverlayAllowed ctx wid = do
top <- topModalNode (ctxNodeArena ctx)
case top of
Nothing -> pure True
Just modal -> widgetIdInSubtree ctx modal wid
-- Prev rects are visual space (snapshot after applyScrollOffsets).
scrollHitRect :: Context -> WidgetId -> IO (Maybe Rect)
scrollHitRect = getPrevRect
{-# INLINE nodePointVisible #-}
nodePointVisible :: Context -> NodeIdx -> V2 -> IO Bool
nodePointVisible ctx idx mouse = do
(x, y, w, h) <- getRect (ctxNodeArena ctx) idx
let vis = Rect x y w h
if not (w > 0 && h > 0 && rectContains vis mouse)
then pure False
else do
mClip <- getClipRect (ctxNodeArena ctx) idx
pure (maybe True (`rectContains` mouse) mClip)
{-# INLINE nodeClippedHit #-}
nodeClippedHit :: Context -> NodeIdx -> Rect -> V2 -> IO Bool
nodeClippedHit ctx idx rect mouse = do
if not (rectW rect > 0 && rectH rect > 0 && rectContains rect mouse)
then pure False
else do
na <- pure (ctxNodeArena ctx)
mLive <- getClipRect na idx
mClip <-
case mLive of
Just r -> pure (Just r)
Nothing -> do
wid <- getWidgetId na idx
getPrevClipRect ctx wid
pure (maybe True (`rectContains` mouse) mClip)
-- | Hit test during UI build (before applyScrollOffsets). Uses prev rects and
-- scroll viewport clips only, not per-node live clips.
{-# INLINE nodeInteractionHit #-}
nodeInteractionHit :: Context -> NodeIdx -> Rect -> V2 -> IO Bool
nodeInteractionHit ctx idx rect mouse = do
if not (rectW rect > 0 && rectH rect > 0 && rectContains rect mouse)
then pure False
else scrollViewportHit ctx idx mouse
scrollViewportHit :: Context -> NodeIdx -> V2 -> IO Bool
scrollViewportHit ctx idx mouse = go idx
where
go i
| i <= 0 = pure True
| otherwise = do
p <- getParent (ctxNodeArena ctx) i
if p < 0
then pure True
else do
nt <- getNodeType (ctxNodeArena ctx) p
if nt == NodeScrollContainer
then do
wid <- getWidgetId (ctxNodeArena ctx) p
mClip <- getPrevClipRect ctx wid
case mClip of
Nothing -> go p
Just clip ->
if rectContains clip mouse then go p else pure False
else go p