packages feed

nano-ui-0.1.0.0: lib/NanoUI/Widgets/Node.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

-- | Widget node construction and interaction responses.
module NanoUI.Widgets.Node
  ( Response (..)
  , HasResponse (..)
  , respId
  , respRect
  , respHovered
  , respPressed
  , respClicked
  , respChanged
  , respSubmitted
  , respRightPressed
  , respRightClicked
  , mkResponse
  , emptyModalResp
  , setClicked
  , setChanged
  , setSubmitted
  , parentIdx
  , container
  , containerResponse
  , withContainerNode
  , floatingPanel
  , addWidget
  , addWidgetStyled
  , addWidgetWithOptions
  , addSizingLeafNode
  , resolveInteraction
  , tagContainer
  )
where

import Control.Monad (when)
import Data.IORef (readIORef, writeIORef)
import Data.Text (Text)
import Effectful (Eff, type (:>))
import NanoUI.Context
  ( Context (..)
  , isDisabled
  , pointerBlockedByOverlay
  , OverlayState (..)
  , getsOverlay
  , modifyOverlay
  )
import NanoUI.Id (WidgetId (..), enterScope, hashWidgetId, scopeTag)
import NanoUI.Input
  ( Input (..)
  , inputMouseDown
  , inputMousePos
  , inputMouseReleased
  , inputMouseRightDown
  , inputMouseRightReleased
  )
import NanoUI.Layout.Arena
  ( NodeIdx
  , NodeType (..)
  , addNode
  , addNodeFromLayout
  , rootAttachParent
  , setNodeText
  , setOptions
  , setNodeValue
  , setStyleIdx
  , setWidgetId
  )
import NanoUI.Monad (Ui, askContext, askInput, nextId, uiIO)
import NanoUI.WidgetText (packTextNodeStyleFull)
import NanoUI.Style
  ( AlignX (..)
  , AlignY (..)
  , Direction (..)
  , Layout (..)
  , Padding (..)
  , Sizing (..)
  )
import NanoUI.Types (Rect (..), rectContains, rectH, rectHit, rectUnion, rectW)
import NanoUI.Frame.Hit (findNodeByWidgetId, nodeInteractionHit, scrollHitRect)

parentIdx :: [Int] -> Int
parentIdx = \case
  [] -> -1
  (p : _) -> p

-- | Anything that carries a widget 'Response' (composite widget results such
-- as 'NanoUI.Widgets.Tabs.TabResponse'). The @resp*@ accessors work on all of them.
class HasResponse r where
  toResponse :: r -> Response

instance HasResponse Response where
  {-# INLINE toResponse #-}
  toResponse = id

{-# INLINE respId #-}
respId :: HasResponse r => r -> WidgetId
respId = rawRespId . toResponse

{-# INLINE respRect #-}
respRect :: HasResponse r => r -> Rect
respRect = rawRespRect . toResponse

{-# INLINE respHovered #-}
respHovered :: HasResponse r => r -> Bool
respHovered = rawRespHovered . toResponse

{-# INLINE respPressed #-}
respPressed :: HasResponse r => r -> Bool
respPressed = rawRespPressed . toResponse

{-# INLINE respClicked #-}
respClicked :: HasResponse r => r -> Bool
respClicked = rawRespClicked . toResponse

{-# INLINE respChanged #-}
respChanged :: HasResponse r => r -> Bool
respChanged = rawRespChanged . toResponse

{-# INLINE respSubmitted #-}
respSubmitted :: HasResponse r => r -> Bool
respSubmitted = rawRespSubmitted . toResponse

{-# INLINE respRightPressed #-}
respRightPressed :: HasResponse r => r -> Bool
respRightPressed = rawRespRightPressed . toResponse

{-# INLINE respRightClicked #-}
respRightClicked :: HasResponse r => r -> Bool
respRightClicked = rawRespRightClicked . toResponse

data Response = Response
  { rawRespId :: !WidgetId
  , rawRespRect :: !Rect
  , rawRespHovered :: !Bool
  , rawRespPressed :: !Bool
  , rawRespClicked :: !Bool
  , rawRespChanged :: !Bool
  , rawRespSubmitted :: !Bool
  , rawRespRightPressed :: !Bool
  , rawRespRightClicked :: !Bool
  }
  deriving (Eq, Show)

instance Semigroup Response where
  a <> b =
    Response
      { rawRespId = if rawRespId b == WidgetId 0 then rawRespId a else rawRespId b
      , rawRespRect = unionRespRect (rawRespRect a) (rawRespRect b)
      , rawRespHovered = rawRespHovered a || rawRespHovered b
      , rawRespPressed = rawRespPressed a || rawRespPressed b
      , rawRespClicked = rawRespClicked a || rawRespClicked b
      , rawRespChanged = rawRespChanged a || rawRespChanged b
      , rawRespSubmitted = rawRespSubmitted a || rawRespSubmitted b
      , rawRespRightPressed = rawRespRightPressed a || rawRespRightPressed b
      , rawRespRightClicked = rawRespRightClicked a || rawRespRightClicked b
      }

instance Monoid Response where
  mempty = mkResponse (WidgetId 0) (Rect 0 0 0 0) False False False False

unionRespRect :: Rect -> Rect -> Rect
unionRespRect a b
  | rectW a <= 0 || rectH a <= 0 = b
  | rectW b <= 0 || rectH b <= 0 = a
  | otherwise = rectUnion a b

setClicked :: Bool -> Response -> Response
setClicked c r = r {rawRespClicked = c}

setChanged :: Bool -> Response -> Response
setChanged c r = r {rawRespChanged = c}

setSubmitted :: Bool -> Response -> Response
setSubmitted s r = r {rawRespSubmitted = s}

mkResponse :: WidgetId -> Rect -> Bool -> Bool -> Bool -> Bool -> Response
mkResponse wid rect hovered pressed clicked changed =
  Response
    { rawRespId = wid
    , rawRespRect = rect
    , rawRespHovered = hovered
    , rawRespPressed = pressed
    , rawRespClicked = clicked
    , rawRespChanged = changed
    , rawRespSubmitted = False
    , rawRespRightPressed = False
    , rawRespRightClicked = False
    }

emptyModalResp :: WidgetId -> Response
emptyModalResp wid = mempty {rawRespId = wid}

container :: Ui :> es => NodeType -> Layout -> Eff es a -> Eff es a
container nt layout child = runContainer nt layout Nothing child

containerResponse :: Ui :> es => NodeType -> Layout -> Eff es a -> Eff es (a, Response)
containerResponse nt layout child = do
  wid <- nextId
  ctx <- askContext
  inp <- askInput
  r <- runContainer nt layout (Just wid) child
  resp <- uiIO (resolveInteraction ctx inp wid)
  pure (r, resp)

runContainer :: Ui :> es => NodeType -> Layout -> Maybe WidgetId -> Eff es a -> Eff es a
runContainer nt layout mWid child = do
  ctx <- askContext
  idx <- uiIO $ do
    stack <- readIORef (ctxContainerStack ctx)
    idx <- addNodeFromLayout (ctxNodeArena ctx) nt (parentIdx stack) layout
    mapM_ (setWidgetId (ctxNodeArena ctx) idx) mWid
    pure idx
  withContainerNode True idx child

-- | Push container node @idx@ (already added under the current parent), run
-- @child@ inside it, then pop. @scoped@ also runs the children in a fresh id
-- scope; it changes the children's widget ids (and so their store keys), so
-- callers pick it explicitly: plain containers scope, scroll containers do not.
withContainerNode :: Ui :> es => Bool -> NodeIdx -> Eff es a -> Eff es a
withContainerNode scoped idx child = do
  ctx <- askContext
  (stack, parentIds) <- uiIO $ do
    stack0 <- readIORef (ctxContainerStack ctx)
    writeIORef (ctxContainerStack ctx) (idx : stack0)
    ids0 <- readIORef (ctxIdContext ctx)
    if scoped
      then do
        let (parentIds, childIds) = enterScope scopeTag ids0
        writeIORef (ctxIdContext ctx) childIds
        pure (stack0, parentIds)
      else pure (stack0, ids0)
  r <- child
  uiIO $ do
    writeIORef (ctxContainerStack ctx) stack
    when scoped $ writeIORef (ctxIdContext ctx) parentIds
  pure r

-- | A floating panel (popup, modal, window): its node attaches to the root
-- layer and it is the current floating panel while @body@ runs. @addPanel@
-- adds the node under the given parent; @enter@ runs once the node is pushed
-- (seeding its rect, opening a modal).
floatingPanel ::
  Ui :> es => Bool -> WidgetId -> (Int -> IO NodeIdx) -> IO () -> Eff es a -> Eff es a
floatingPanel scoped wid addPanel enter body = do
  ctx <- askContext
  let arena = ctxNodeArena ctx
  prevFloat <- uiIO (getsOverlay ctx osCurrentFloatingId)
  idx <- uiIO $ do
    stack <- readIORef (ctxContainerStack ctx)
    idx <- addPanel =<< rootAttachParent arena (parentIdx stack)
    setWidgetId arena idx wid
    pure idx
  r <- withContainerNode scoped idx (uiIO (enter >> modifyOverlay ctx (\os -> os {osCurrentFloatingId = Just wid})) >> body)
  uiIO (modifyOverlay ctx (\os -> os {osCurrentFloatingId = prevFloat}))
  pure r

addSizingLeafNode ::
  Context
  -> Input
  -> WidgetId
  -> NodeType
  -> Direction
  -> Sizing
  -> Sizing
  -> IO Response
addSizingLeafNode ctx inp wid nt dir wSiz hSiz = do
  stack <- readIORef (ctxContainerStack ctx)
  let
    parent = parentIdx stack
  idx <-
    addNode
      (ctxNodeArena ctx)
      nt
      parent
      dir
      wSiz
      hSiz
      (Padding 0 0 0 0)
      0
      0
      0
      1e9
      1e9
      0
      AlignStart
      AlignTop
  setWidgetId (ctxNodeArena ctx) idx wid
  resolveInteraction ctx inp wid

{-# INLINE addWidget #-}
addWidget ::
  Ui :> es =>
  WidgetId
  -> NodeType
  -> Text
  -> Float
  -> Layout
  -> Eff es Response
addWidget wid nt txt value layout = addWidgetStyled wid nt txt value layout 0

{-# INLINE addWidgetStyled #-}
addWidgetStyled ::
  Ui :> es =>
  WidgetId
  -> NodeType
  -> Text
  -> Float
  -> Layout
  -> Int
  -> Eff es Response
addWidgetStyled wid nt txt value layout styleIdx = do
  ctx <- askContext
  inp <- askInput
  uiIO $ do
    stack <- readIORef (ctxContainerStack ctx)
    let
      parent = parentIdx stack
    idx <- addNodeFromLayout (ctxNodeArena ctx) nt parent layout
    setNodeText (ctxNodeArena ctx) idx txt
    setNodeValue (ctxNodeArena ctx) idx value
    let effectiveStyle
          | nt == NodeText = packTextNodeStyleFull (layoutFontVariant layout) (layoutFontWeight layout) (layoutFontStyle layout) (layoutTextDecoration layout) styleIdx
          | otherwise = styleIdx
    setStyleIdx (ctxNodeArena ctx) idx effectiveStyle
    setWidgetId (ctxNodeArena ctx) idx wid
    resolveInteraction ctx inp wid

addWidgetWithOptions ::
  Ui :> es =>
  WidgetId
  -> NodeType
  -> Text
  -> [Text]
  -> Float
  -> Layout
  -> Eff es Response
addWidgetWithOptions wid nt txt opts value layout = do
  ctx <- askContext
  inp <- askInput
  uiIO $ do
    stack <- readIORef (ctxContainerStack ctx)
    let parent = parentIdx stack
    idx <- addNodeFromLayout (ctxNodeArena ctx) nt parent layout
    setNodeText (ctxNodeArena ctx) idx txt
    setOptions (ctxNodeArena ctx) idx opts
    setNodeValue (ctxNodeArena ctx) idx value
    setStyleIdx (ctxNodeArena ctx) idx 0
    setWidgetId (ctxNodeArena ctx) idx wid
    resolveInteraction ctx inp wid

resolveInteraction :: Context -> Input -> WidgetId -> IO Response
resolveInteraction ctx inp wid = do
  mrect <- scrollHitRect ctx wid
  active <- readIORef (ctxActiveId ctx)
  pending <- readIORef (ctxClickedId ctx)
  let
    mouse = inputMousePos inp
    rect = case mrect of
      Just r -> r
      Nothing -> Rect 0 0 0 0
    canHit = rectHit rect mouse || pending == wid
  if not canHit
    then pure $! mkResponse wid rect False False False False
    else do
      disabled <- isDisabled ctx wid
      blocked <- pointerBlockedByOverlay ctx mouse
      mIdx <- findNodeByWidgetId ctx wid
      let
        hitAt p = case mIdx of
          Nothing -> pure (rectContains rect p)
          Just idx -> nodeInteractionHit ctx idx rect p
        -- Whether the button held in @ref@ went down on this widget. A press
        -- the frame never saw (synthesized input, or one swallowed before it
        -- arrived) leaves the gesture unowned, so nobody is ruled out.
        startedHere ref = readIORef ref >>= maybe (pure True) hitAt
      -- A held button belongs to whatever it went down on. Another widget the
      -- drag passes over is not hovered, so it neither lights up nor reports a
      -- press of its own.
      captured <-
        if not (inputMouseDown inp)
          then pure False
          else
            if hashWidgetId active /= 0 && active /= wid
              then pure True
              else not <$> startedHere (ctxPressPos ctx)
      hovered <-
        if disabled || blocked || captured
          then pure False
          else hitAt mouse
      let
        pressed = hovered && inputMouseDown inp
        rightPressed = hovered && inputMouseRightDown inp
      -- The click belongs to whatever the press went down on: a release that
      -- drifted here from a neighbouring widget is not this widget's click.
      released <-
        if hovered && inputMouseReleased inp
          then startedHere (ctxPressPos ctx)
          else pure False
      rightReleased <-
        if hovered && inputMouseRightReleased inp
          then startedHere (ctxRightPressPos ctx)
          else pure False
      when (released && wid == active) $
        writeIORef (ctxReleaseClickedId ctx) wid
      let
        clicked = released || pending == wid
        rightClicked = rightReleased
      pure $!
        Response
          { rawRespId = wid
          , rawRespRect = rect
          , rawRespHovered = hovered
          , rawRespPressed = pressed
          , rawRespClicked = clicked
          , rawRespChanged = False
          , rawRespSubmitted = False
          , rawRespRightPressed = rightPressed
          , rawRespRightClicked = rightClicked
          }

-- | Stamp the current container with a widget id (radio/tree group key).
tagContainer :: Ui :> es => WidgetId -> Eff es ()
tagContainer wid = do
  ctx <- askContext
  uiIO $ do
    stack <- readIORef (ctxContainerStack ctx)
    case stack of
      (idx : _) -> setWidgetId (ctxNodeArena ctx) idx wid
      [] -> pure ()