packages feed

swarm-0.6.0.0: src/swarm-tui/Swarm/TUI/View/Structure.hs

{-# LANGUAGE OverloadedStrings #-}

-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Display logic for Structures.
module Swarm.TUI.View.Structure (
  renderStructuresDisplay,
  makeListWidget,
) where

import Brick hiding (Direction, Location, getName)
import Brick.Focus
import Brick.Widgets.Center (hCenter)
import Brick.Widgets.List qualified as BL
import Control.Lens hiding (Const, from)
import Data.Map.NonEmpty qualified as NEM
import Data.Map.Strict qualified as M
import Data.Set qualified as Set
import Data.Text qualified as T
import Data.Vector qualified as V
import Swarm.Game.Entity (Entity, entityDisplay)
import Swarm.Game.Scenario (StructureCells)
import Swarm.Game.Scenario.Topography.Area
import Swarm.Game.Scenario.Topography.Placement (getStructureName)
import Swarm.Game.Scenario.Topography.Structure qualified as Structure
import Swarm.Game.Scenario.Topography.Structure.Recognition (foundStructures)
import Swarm.Game.Scenario.Topography.Structure.Recognition.Precompute (getEntityGrid)
import Swarm.Game.Scenario.Topography.Structure.Recognition.Registry (foundByName)
import Swarm.Game.Scenario.Topography.Structure.Recognition.Type
import Swarm.Game.State
import Swarm.Game.State.Substate (structureRecognition)
import Swarm.Language.Syntax.Direction (directionJsonModifier)
import Swarm.TUI.Model.Name
import Swarm.TUI.Model.Structure
import Swarm.TUI.View.Attribute.Attr
import Swarm.TUI.View.CellDisplay
import Swarm.TUI.View.Util
import Swarm.Util (commaList)

-- | Render a two-pane widget with structure selection on the left
-- and single-structure details on the right.
structureWidget :: GameState -> StructureInfo StructureCells Entity -> Widget n
structureWidget gs s =
  vBox
    [ hBox
        [ headerItem "Name" theName
        , padLeft (Pad 2)
            . headerItem "Size"
            . T.pack
            . renderRectDimensions
            . getAreaDimensions
            $ entityProcessedGrid s
        , occurrenceCountSuffix
        ]
    , reorientabilityWidget
    , maybeDescriptionWidget
    , padTop (Pad 1) $
        hBox
          [ structureIllustration
          , padLeft (Pad 4) ingredientsBox
          ]
    ]
 where
  headerItem h content =
    hBox
      [ padRight (Pad 1) $ txt $ h <> ":"
      , withAttr boldAttr $ txt content
      ]

  annotatedStructureGrid = annotatedGrid s

  supportedOrientations = Set.toList . Structure.recognize . namedGrid $ annotatedStructureGrid

  renderSymmetry = \case
    NoSymmetry -> "no"
    TwoFold -> "2-fold"
    FourFold -> "4-fold"

  reorientabilityWidget =
    txt $
      T.unwords
        [ "Orientable:"
        , commaList $ map (T.pack . directionJsonModifier . show) supportedOrientations
        , "with"
        , renderSymmetry $ symmetry annotatedStructureGrid
        , "rotational symmetry."
        ]

  maybeDescriptionWidget =
    maybe emptyWidget (padTop (Pad 1) . withAttr italicAttr . txtWrap) $
      Structure.description . namedGrid . annotatedGrid $
        s

  registry = gs ^. discovery . structureRecognition . foundStructures
  occurrenceCountSuffix = case M.lookup theName $ foundByName registry of
    Nothing -> emptyWidget
    Just inner -> padLeft (Pad 2) . headerItem "Count" . T.pack . show $ NEM.size inner

  structureIllustration = vBox $ map (hBox . map renderOneCell) cells
  d = namedGrid $ annotatedGrid s

  ingredientsBox =
    vBox
      [ padBottom (Pad 1) $ withAttr boldAttr $ txt "Materials:"
      , ingredientLines
      ]
  ingredientLines = vBox . map showCount . M.toList $ entityCounts s

  showCount (e, c) =
    hBox
      [ drawLabelledEntityName e
      , txt $
          T.unwords
            [ ":"
            , T.pack $ show c
            ]
      ]

  theName = getStructureName $ Structure.name d
  cells = getEntityGrid d
  renderOneCell = maybe (txt " ") (renderDisplay . view entityDisplay)

makeListWidget :: [StructureInfo StructureCells Entity] -> BL.List Name (StructureInfo StructureCells Entity)
makeListWidget structureDefinitions =
  BL.listMoveTo 0 $ BL.list (StructureWidgets StructuresList) (V.fromList structureDefinitions) 1

renderStructuresDisplay :: GameState -> StructureDisplay -> Widget Name
renderStructuresDisplay gs structureDisplay =
  vBox
    [ hBox
        [ leftSide
        , padLeft (Pad 2) structureElaboration
        ]
    , footer
    ]
 where
  footer = hCenter $ withAttr italicAttr $ txt "NOTE: [Tab] toggles focus between panes"
  lw = _structurePanelListWidget structureDisplay
  fr = _structurePanelFocus structureDisplay
  leftSide =
    hLimitPercent 25 $
      padAll 1 $
        vBox
          [ hCenter $ withAttr boldAttr $ txt "Candidates"
          , padAll 1 $
              vLimit 10 $
                withFocusRing fr (BL.renderList drawSidebarListItem) lw
          ]

  -- Adds very subtle coloring to indicate focus switch
  highlightIfFocused = case focusGetCurrent fr of
    Just (StructureWidgets StructureSummary) -> withAttr lightCyanAttr
    _ -> id

  -- Note: An extra "padRight" is inserted to account for the vertical scrollbar,
  -- whether or not it appears.
  structureElaboration =
    clickable (StructureWidgets StructureSummary)
      . maybeScroll ModalViewport
      . maybe emptyWidget (padAll 1 . padRight (Pad 1) . highlightIfFocused . structureWidget gs . snd)
      $ BL.listSelectedElement lw

drawSidebarListItem ::
  Bool ->
  StructureInfo StructureCells Entity ->
  Widget Name
drawSidebarListItem _isSelected (StructureInfo annotated _ _) =
  txt . getStructureName . Structure.name $ namedGrid annotated