nano-ui-0.1.0.0: lib/NanoUI/Frame/Redraw.hs
{-# LANGUAGE DataKinds #-}
module NanoUI.Frame.Redraw
( needsRedraw
, pointerDragActive
, textFieldActive
, floatingPanelActive
, debugPanelOpen
, overlayMenuOpen
, probeHotId
) where
import Data.IORef (IORef, readIORef)
import Data.Maybe (isJust)
import NanoUI.Context
( Context (..)
, anyAnimating
, anySelectOpen
, getMenuPointerGesture
, getScrollDrag
, getStore
, getTextInputMenu
, getWindowDrag
, getWindowResize
, isDirty
, modalActive
)
import NanoUI.Frame.Hit (findNodeByWidgetId, nodePointVisible, overlayHitAllowed)
import NanoUI.Frame.Select (overlayMenuOwnerAt)
import NanoUI.Id (WidgetId (..), hashWidgetId)
import NanoUI.Input (Input (..), inputInteracted, inputMousePos, inputPointerHeld)
import NanoUI.Layout.Arena
( NodeType (..)
, findNodeM
, getNodeType
, getOptions
, getWidgetId
, isFloatingNode
, isWidgetNode
)
import NanoUI.Types (V2 (..))
needsRedraw :: Context -> Input -> Input -> IO Bool
needsRedraw ctx prev inp = do
dirty <- isDirty ctx
anim <- anyAnimating ctx
mDrag <- getScrollDrag ctx
mWinDrag <- getWindowDrag ctx
overlay <- overlayMenuOpen ctx
edit <- textFieldActive ctx
let moved = inputMousePos prev /= inputMousePos inp
if dirty
|| anim
|| inputInteracted prev inp
|| inputWindowRedraw inp
|| inputPointerHeld inp
|| isJust mDrag
|| isJust mWinDrag
|| (overlay && moved)
|| edit
then pure True
else
-- Idle: hover can only change when the pointer moved since the frame
-- whose hover state we still hold. Skip the O(n) hot probe otherwise.
if not moved
then pure False
else do
lastHot <- readIORef (ctxLastHotId ctx)
(/= lastHot) <$> probeHotId ctx (inputMousePos inp)
-- Window/scroll/resize drag marks dirty every frame, so input must still be
-- polled on those frames.
-- Color picker and slider hold ctxActiveId without extra window/scroll refs.
pointerDragActive :: Context -> IO Bool
pointerDragActive ctx = do
winDrag <- isJust <$> getWindowDrag ctx
scrollDrag <- isJust <$> getScrollDrag ctx
winResize <- isJust <$> getWindowResize ctx
sliderOrPicker <- focusedNodeIs ctx ctxActiveId (\nt -> nt == NodeSlider || nt == NodeColorPicker)
pure (winDrag || scrollDrag || winResize || sliderOrPicker)
-- | Whether the node of the widget id held in @ref@ satisfies @p@.
focusedNodeIs :: Context -> (Context -> IORef WidgetId) -> (NodeType -> Bool) -> IO Bool
focusedNodeIs ctx ref p = do
wid <- readIORef (ref ctx)
if hashWidgetId wid == 0
then pure False
else do
mIdx <- findNodeByWidgetId ctx wid
case mIdx of
Nothing -> pure False
Just idx -> p <$> getNodeType (ctxNodeArena ctx) idx
-- Select dropdown or text-input menu is open. Overlay hover is not a widget id.
-- A focused combo (a search-style field carrying options) also owns an open
-- dropdown: report it so every frame while it is up redraws with full damage:
-- the floating list is painted by an overlay, so clip-damage frames would
-- leave stale rows in the retained texture.
overlayMenuOpen :: Context -> IO Bool
overlayMenuOpen ctx = do
store <- getStore ctx
menu <- getTextInputMenu ctx
if anySelectOpen store || isJust menu
then pure True
else do
focus <- readIORef (ctxFocusId ctx)
if hashWidgetId focus == 0
then pure False
else do
mIdx <- findNodeByWidgetId ctx focus
case mIdx of
Nothing -> pure False
Just idx -> do
nt <- getNodeType (ctxNodeArena ctx) idx
if nt /= NodeTextInput
then pure False
else not . null <$> getOptions (ctxNodeArena ctx) idx
-- Focused text field or its context menu. Keep the loop live so typed bytes
-- are not stuck behind SDL_WaitEvent.
textFieldActive :: Context -> IO Bool
textFieldActive ctx = do
menu <- getTextInputMenu ctx
if isJust menu
then pure True
else focusedNodeIs ctx ctxFocusId (\nt -> nt == NodeTextInput || nt == NodeTextArea)
-- Last frame still has a floating node (modal or window). Used by backends to
-- decide whether overlay content might need periodic refresh (debug HUD).
floatingPanelActive :: Context -> IO Bool
floatingPanelActive ctx = do
modal <- modalActive ctx
if modal
then pure True
else isJust <$> findNodeM (ctxNodeArena ctx) (fmap isFloatingNode . getNodeType (ctxNodeArena ctx))
-- Floating window overlay (debug HUD). Prev floating rects persist across idle frames.
debugPanelOpen :: Context -> IO Bool
debugPanelOpen ctx =
isJust <$> findNodeM (ctxNodeArena ctx) (fmap (== NodeWindow) . getNodeType (ctxNodeArena ctx))
probeHotId :: Context -> V2 -> IO WidgetId
probeHotId ctx mouse = do
gesture <- getMenuPointerGesture ctx
if gesture
then pure (WidgetId 0)
else do
mOverlay <- overlayMenuOwnerAt ctx mouse
case mOverlay of
Just wid -> pure wid
-- Earlier siblings paint over later ones, so the first hit wins.
Nothing -> maybe (pure (WidgetId 0)) (getWidgetId na) =<< findNodeM na hits
where
na = ctxNodeArena ctx
hits idx = do
nt <- getNodeType na idx
if not (isWidgetNode nt)
then pure False
else do
visible <- nodePointVisible ctx idx mouse
if visible then overlayHitAllowed ctx idx mouse else pure False