packages feed

nano-ui-0.1.0.0: lib/NanoUI/Context/Overlay.hs

-- | Modal, floating-panel and menu pointer-capture state.
module NanoUI.Context.Overlay
  ( textInputEditActive
  , modalActive
  , overlayConsumesQuit
  , markEscapeConsumed
  , pointerBlockedByModal
  , pointerBlockedByOverlay
  , armMenuPointerCapture
  , seedFloatingPanel
  , beginModal
  , endModal
  , beginFrameModal
  , modalDamageFlip
  ) where

import Control.Monad (when)
import Data.IORef (readIORef)
import Data.IntMap.Strict qualified as IM

import NanoUI.Context.Core
  ( getMenuPointerGesture
  , getTextInputMenu
  , getsOverlay
  , modifyOverlay
  , setMenuPointerGesture
  , getsInteraction
  )
import NanoUI.Context.Types (Context (..), OverlayState (..), TextInputMenu (..), intKey, InteractionState (..))
import NanoUI.Id (WidgetId (..), hashWidgetId)
import NanoUI.Input (Input, Key (KeyEscape), inputKeys, inputKeysElem, inputMousePos, inputMousePressed)
import NanoUI.Types (Rect, V2, rectContains, rectHit, rectNonEmpty)

textInputEditActive :: Context -> IO Bool
textInputEditActive ctx = do
  focus <- readIORef (ctxFocusId ctx)
  menu <- getTextInputMenu ctx
  pure (hashWidgetId focus /= 0 || menu /= Nothing)

modalActive :: Context -> IO Bool
modalActive ctx = getsOverlay ctx (\os -> osModalWasActive os || osModalActive os)

overlayConsumesQuit :: Context -> Input -> IO Bool
overlayConsumesQuit ctx inp = do
  consumed <- getsOverlay ctx osEscapeConsumed
  pure (inputKeysElem KeyEscape (inputKeys inp) && consumed)

markEscapeConsumed :: Context -> IO ()
markEscapeConsumed ctx = modifyOverlay ctx (\os -> os {osEscapeConsumed = True})

pointerBlockedByModal :: Context -> IO Bool
pointerBlockedByModal ctx =
  getsOverlay ctx (\os -> osModalDepth os <= 0 && (osModalWasActive os || osModalActive os))

pointerBlockedByOverlay :: Context -> V2 -> IO Bool
pointerBlockedByOverlay ctx mouse = do
  gesture <- getMenuPointerGesture ctx
  blocked <-
    if gesture
      then pure True
      else do
        menuBlocked <- overlayMenuBlocksPointer ctx mouse
        if menuBlocked
          then pure True
          else do
            modalBlocked <- pointerBlockedByModal ctx
            if modalBlocked
              then pure True
              else do
                mTop <- cachedTopmost ctx mouse
                case mTop of
                  Nothing -> pure False
                  Just top -> do
                    mCur <- getsOverlay ctx osCurrentFloatingId
                    pure (mCur /= Just top)
  modifyOverlay ctx (\os -> os {osLastPointerBlocked = blocked})
  pure blocked

armMenuPointerCapture :: Context -> Input -> IO ()
armMenuPointerCapture ctx inp =
  when (inputMousePressed inp) $ do
    blocked <- overlayMenuBlocksPointer ctx (inputMousePos inp)
    setMenuPointerGesture ctx blocked

overlayMenuBlocksPointer :: Context -> V2 -> IO Bool
overlayMenuBlocksPointer ctx mouse = do
  mMenu <- getTextInputMenu ctx
  let textMenu =
        case mMenu of
          Just m | rectContains (textInputMenuRect m) mouse -> True
          _ -> False
  if textMenu
    then pure True
    else do
      mDrop <- getsInteraction ctx isOpenSelectDrop
      pure
        ( case mDrop of
            Just (_, r) -> rectContains r mouse
            Nothing -> False
        )

cachedTopmost :: Context -> V2 -> IO (Maybe WidgetId)
cachedTopmost ctx mouse = do
  cache <- getsOverlay ctx osTopmostCache
  case cache of
    Just (p, t) | p == mouse -> pure t
    _ -> do
      t <- topmostFloatingAtMouse ctx mouse
      modifyOverlay ctx (\os -> os {osTopmostCache = Just (mouse, t)})
      pure t

topmostFloatingAtMouse :: Context -> V2 -> IO (Maybe WidgetId)
topmostFloatingAtMouse ctx mouse = do
  os <- readIORef (ctxOverlayState ctx)
  let rects = osPrevFloatingRects os
      order = osPrevFloatingOrder os
      hit k = maybe False (`rectHit` mouse) (IM.lookup k rects)
      picked = foldl' (\acc k -> if hit k then Just k else acc) Nothing order
  pure (WidgetId . fromIntegral <$> picked)

seedFloatingPanel :: Context -> WidgetId -> Rect -> IO ()
seedFloatingPanel ctx wid rect
  | not (rectNonEmpty rect) = pure ()
  | otherwise = do
      let k = intKey wid
      modifyOverlay ctx $ \os ->
        let rects = IM.insert k rect (osPrevFloatingRects os)
            order = filter (/= k) (osPrevFloatingOrder os) ++ [k]
         in os
              { osPrevFloatingRects = rects
              , osPrevFloatingOrder = order
              , osTopmostCache = Nothing
              }

beginModal :: Context -> IO ()
beginModal ctx =
  modifyOverlay ctx (\os -> os {osModalActive = True, osModalDepth = osModalDepth os + 1})

endModal :: Context -> IO ()
endModal ctx =
  modifyOverlay ctx (\os -> os {osModalDepth = max 0 (osModalDepth os - 1)})

beginFrameModal :: Context -> IO ()
beginFrameModal ctx =
  modifyOverlay ctx $ \os ->
    os
      { osModalWasActive = osModalActive os
      , osModalActive = False
      , osModalDepth = 0
      , osTopmostCache = Nothing
      , osCurrentFloatingId = Nothing
      , osLastPointerBlocked = False
      , osEscapeConsumed = False
      }

modalDamageFlip :: Context -> IO Bool
modalDamageFlip ctx = getsOverlay ctx (\os -> osModalWasActive os /= osModalActive os)