packages feed

swarm-0.7.0.0: src/swarm-tui/Swarm/TUI/View/Popup.hs

{-# LANGUAGE OverloadedStrings #-}

-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Rendering (& animating) notification popups.
module Swarm.TUI.View.Popup (
  drawPopups,
  startPopupAnimation,
  popupFrameDuration,
  popupFrames,
) where

import Brick (Widget (..), cropTopTo, padLeftRight, txt, vBox)
import Brick.Animation (Animation, AnimationManager, Clip, RunMode (..), newClip, renderAnimation, startAnimation)
import Brick.Widgets.Border (border)
import Brick.Widgets.Center (hCenterLayer)
import Brick.Widgets.Core (emptyWidget, hBox, withAttr)
import Control.Lens (Traversal', (^.), (^?))
import Control.Monad.IO.Class (MonadIO)
import Swarm.Game.Achievement.Definitions (title)
import Swarm.Game.Achievement.Description (describe)
import Swarm.Game.Popup (Popup (..))
import Swarm.Language.Syntax (constInfo, syntax)
import Swarm.TUI.Model (AppEvent, AppState, animTraversal, keyConfig, keyEventHandling, playState, progression, uiPopupAnimationState, _AnimActive)
import Swarm.TUI.Model.Event qualified as SE
import Swarm.TUI.Model.Name
import Swarm.TUI.View.Attribute.Attr (notifAttr)
import Swarm.TUI.View.Util (bindingText)
import Swarm.Util (commaList, squote)

-- | The number of milliseconds taken by each animation frame of the notification popup
popupFrameDuration :: Integer
popupFrameDuration = 50

-- | The number of animation frames for which to display a popup.
popupFrames :: Int
popupFrames = 125

-- | Draw the current notification popup (if any).
drawPopups :: AppState -> Widget Name
drawPopups s = renderAnimation (const emptyWidget) s mAnim
 where
  mAnim = s ^? playState . progression . uiPopupAnimationState . _AnimActive

-- | Signal the animation manager to start the popup animation.
startPopupAnimation :: MonadIO m => AnimationManager AppState AppEvent Name -> Popup -> m ()
startPopupAnimation mgr p = startAnimation mgr (makePopupClip p) popupFrameDuration Once trav
 where
  trav :: Traversal' AppState (Maybe (Animation AppState Name))
  trav = playState . progression . uiPopupAnimationState . animTraversal

makePopupClip :: Popup -> Clip AppState Name
makePopupClip p = newClip $ map drawPopupFrame [0 .. popupFrames]
 where
  drawPopupFrame f s = hCenterLayer $ cropTopTo (popupRows f) . border . padLeftRight 2 $ drawPopup s p

drawPopup :: AppState -> Popup -> Widget Name
drawPopup s = \case
  AchievementPopup ach ->
    hBox
      [ withAttr notifAttr (txt "Achievement unlocked: ")
      , txt (title (describe ach))
      ]
  RecipesPopup ->
    hBox
      [ withAttr notifAttr (txt "New recipes unlocked! ")
      , txt $ bindingText keyConf (SE.Main SE.ViewRecipesEvent) <> " to view."
      ]
  CommandsPopup cmds ->
    vBox
      [ hBox
          [ withAttr notifAttr (txt "New commands unlocked: ")
          , txt . commaList $ map (squote . syntax . constInfo) cmds
          ]
      , txt $ "Hit " <> bindingText keyConf (SE.Main SE.ViewCommandsEvent) <> " to view all available commands."
      ]
  DebugWarningPopup ->
    hBox
      [ withAttr notifAttr (txt "Warning: ")
      , txt "No progress will be saved, since debugging flags are in use."
      ]
 where
  keyConf = s ^. keyEventHandling . keyConfig

-- | Compute the number of rows of the notification popup we should be
--   showing, based on the number of animation frames the popup has existed.
--   This is what causes the popup to animate in and out of existence.
popupRows :: Int -> Int
popupRows f
  -- If we're less than halfway through the lifetime of the popup, use
  -- the number of animation frames elapsed since the beginning of the popup animation.
  -- This will become much larger than the actual number of rows in the
  -- popup, but the 'cropTopTo' function simply has no effect when given any value
  -- equal to or larger than the number of rows of a widget. This way the animation
  -- will continue to work for popups with any (reasonable) number of rows.
  | f <= popupFrames `div` 2 = f
  -- Otherwise, use the number of frames remaining.
  | otherwise = popupFrames - f