From 76277e2bcd6ca650b1310993ab265bf8416b976d Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Thu, 5 Sep 2024 22:13:10 -0700 Subject: [PATCH] navigable table --- .hlint.yaml | 2 + app/game/Swarm/App.hs | 1 + src/swarm-doc/Swarm/Doc/Wiki/Cheatsheet.hs | 2 +- src/swarm-topography/Swarm/Game/Universe.hs | 13 + src/swarm-tui/Swarm/TUI/Controller.hs | 7 + .../Swarm/TUI/Controller/EventHandlers.hs | 1 + .../TUI/Controller/EventHandlers/Frame.hs | 1 + .../TUI/Controller/EventHandlers/Main.hs | 1 + .../TUI/Controller/EventHandlers/REPL.hs | 1 + .../TUI/Controller/EventHandlers/Robot.hs | 1 + .../TUI/Controller/EventHandlers/World.hs | 1 + .../Swarm/TUI/Controller/UpdateUI.hs | 28 ++ src/swarm-tui/Swarm/TUI/Controller/Util.hs | 3 +- src/swarm-tui/Swarm/TUI/Editor/Masking.hs | 2 +- src/swarm-tui/Swarm/TUI/Editor/View.hs | 2 +- src/swarm-tui/Swarm/TUI/Model.hs | 1 - src/swarm-tui/Swarm/TUI/Model/KeyBindings.hs | 1 + src/swarm-tui/Swarm/TUI/Model/Name.hs | 2 + src/swarm-tui/Swarm/TUI/Model/UI.hs | 209 +----------- src/swarm-tui/Swarm/TUI/Model/UI/Gameplay.hs | 224 +++++++++++++ src/swarm-tui/Swarm/TUI/View.hs | 17 +- src/swarm-tui/Swarm/TUI/View/Achievement.hs | 1 + src/swarm-tui/Swarm/TUI/View/CellDisplay.hs | 2 +- src/swarm-tui/Swarm/TUI/View/Popup.hs | 3 +- src/swarm-tui/Swarm/TUI/View/Robot.hs | 314 +++++++++++++----- src/swarm-tui/Swarm/TUI/View/RobotDisplay.hs | 69 ++++ src/swarm-tui/Swarm/TUI/View/Util.hs | 18 +- swarm.cabal | 6 + 28 files changed, 626 insertions(+), 307 deletions(-) create mode 100644 src/swarm-tui/Swarm/TUI/Model/UI/Gameplay.hs create mode 100644 src/swarm-tui/Swarm/TUI/View/RobotDisplay.hs diff --git a/.hlint.yaml b/.hlint.yaml index 048ca5232..7f8d06b48 100644 --- a/.hlint.yaml +++ b/.hlint.yaml @@ -28,6 +28,8 @@ - {name: Data.List.head, within: []} - {name: Prelude.head, within: [Swarm.Web.Tournament.Database.Query]} - {name: Prelude.tail, within: []} + - {name: Prelude.maximum, within: [Swarm.Util]} + - {name: Prelude.minimum, within: []} - {name: Prelude.!!, within: [Swarm.Util.indexWrapNonEmpty, TestEval]} - {name: undefined, within: [Swarm.Language.Key, TestUtil]} - {name: fromJust, within: []} diff --git a/app/game/Swarm/App.hs b/app/game/Swarm/App.hs index ad4b4974c..c7edf9a6f 100644 --- a/app/game/Swarm/App.hs +++ b/app/game/Swarm/App.hs @@ -38,6 +38,7 @@ import Swarm.Language.Pretty (prettyText) import Swarm.Log (LogSource (SystemLog), Severity (..)) import Swarm.TUI.Controller import Swarm.TUI.Model +import Swarm.TUI.Model.Name import Swarm.TUI.Model.StateUpdate import Swarm.TUI.Model.UI (uiAttrMap) import Swarm.TUI.View diff --git a/src/swarm-doc/Swarm/Doc/Wiki/Cheatsheet.hs b/src/swarm-doc/Swarm/Doc/Wiki/Cheatsheet.hs index 17d7a58fb..6b3323ecc 100644 --- a/src/swarm-doc/Swarm/Doc/Wiki/Cheatsheet.hs +++ b/src/swarm-doc/Swarm/Doc/Wiki/Cheatsheet.hs @@ -99,7 +99,7 @@ listToRow mw xs = wrap '|' . T.intercalate "|" $ zipWith format mw xs format w x = wrap ' ' x <> T.replicate (w - T.length x) " " maxWidths :: [[Text]] -> [Int] -maxWidths = map (maximum . map T.length) . transpose +maxWidths = map (maximum0 . map T.length) . transpose -- ** COMMANDS diff --git a/src/swarm-topography/Swarm/Game/Universe.hs b/src/swarm-topography/Swarm/Game/Universe.hs index 10781c586..b3388ca67 100644 --- a/src/swarm-topography/Swarm/Game/Universe.hs +++ b/src/swarm-topography/Swarm/Game/Universe.hs @@ -13,6 +13,7 @@ import Control.Lens (makeLenses, view) import Data.Function (on) import Data.Int (Int32) import Data.Text (Text) +import Data.Text qualified as T import Data.Yaml (FromJSON, ToJSON, Value (Object), parseJSON, withText, (.:)) import GHC.Generics (Generic) import Linear (V2 (..)) @@ -82,3 +83,15 @@ defaultCosmicLocation = Cosmic DefaultRootSubworld origin offsetBy :: Cosmic Location -> V2 Int32 -> Cosmic Location offsetBy loc v = fmap (.+^ v) loc + +locationToString :: Location -> String +locationToString (Location x y) = + unwords $ map show [x, y] + +renderCoordsString :: Cosmic Location -> String +renderCoordsString (Cosmic sw coords) = + unwords $ locationToString coords : suffix + where + suffix = case sw of + DefaultRootSubworld -> [] + SubworldName swName -> ["in", T.unpack swName] diff --git a/src/swarm-tui/Swarm/TUI/Controller.hs b/src/swarm-tui/Swarm/TUI/Controller.hs index 4410422c8..5e0ae2a82 100644 --- a/src/swarm-tui/Swarm/TUI/Controller.hs +++ b/src/swarm-tui/Swarm/TUI/Controller.hs @@ -36,6 +36,7 @@ import Brick.Widgets.Dialog import Brick.Widgets.Edit (Editor, applyEdit, handleEditorEvent) import Brick.Widgets.List (handleListEvent) import Brick.Widgets.List qualified as BL +import Brick.Widgets.TabularList.Mixed import Control.Applicative (pure) import Control.Category ((>>>)) import Control.Lens as Lens @@ -101,6 +102,7 @@ import Swarm.TUI.Model.Name import Swarm.TUI.Model.Repl import Swarm.TUI.Model.StateUpdate import Swarm.TUI.Model.UI +import Swarm.TUI.View.RobotDisplay import Swarm.Util hiding (both, (<<.=)) -- ~~~~ Note [liftA2 re-export from Prelude] @@ -418,6 +420,11 @@ handleModalEvent = \case refreshList $ uiState . uiGameplay . uiDialogs . uiStructure . structurePanelListWidget StructureSummary -> handleInfoPanelEvent modalScroll (VtyEvent ev) _ -> handleInfoPanelEvent modalScroll (VtyEvent ev) + Just RobotsModal -> case ev of + V.EvKey (V.KChar '\t') [] -> uiState . uiGameplay . uiDialogs . uiRobot . robotsDisplayMode %= cycleEnum + _ -> + Brick.zoom (uiState . uiGameplay . uiDialogs . uiRobot . robotListContent . libList) $ + handleMixedListEvent ev _ -> handleInfoPanelEvent modalScroll (VtyEvent ev) where refreshGoalList lw = nestEventM' lw $ handleListEventWithSeparators ev shouldSkipSelection diff --git a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers.hs b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers.hs index f35e99aeb..8d875c02d 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers.hs @@ -46,6 +46,7 @@ import Swarm.TUI.Controller.EventHandlers.Robot (handleRobotPanelEvent, robotEve import Swarm.TUI.Controller.EventHandlers.World (worldEventHandlers) import Swarm.TUI.Model import Swarm.TUI.Model.Event (SwarmEvent, swarmEvents) +import Swarm.TUI.Model.Name import Swarm.Util (parens, squote) -- ~~~~ Note [how Swarm event handlers work] diff --git a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Frame.hs b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Frame.hs index 73cc9b86e..e52d2a6ac 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Frame.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Frame.hs @@ -26,6 +26,7 @@ import Swarm.TUI.Controller.UpdateUI import Swarm.TUI.Controller.Util import Swarm.TUI.Model import Swarm.TUI.Model.Achievements (popupAchievement) +import Swarm.TUI.Model.Name import Swarm.TUI.Model.UI import System.Clock diff --git a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Main.hs b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Main.hs index 7bf59d637..ada85975b 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Main.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Main.hs @@ -26,6 +26,7 @@ import Swarm.TUI.Model import Swarm.TUI.Model.DebugOption (DebugOption (ToggleCreative, ToggleWorldEditor)) import Swarm.TUI.Model.Dialog.Goal import Swarm.TUI.Model.Event (MainEvent (..), SwarmEvent (..)) +import Swarm.TUI.Model.Name import Swarm.TUI.Model.UI import System.Clock (Clock (..), TimeSpec (..), getTime) diff --git a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/REPL.hs b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/REPL.hs index b6b28c5b9..275d920c0 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/REPL.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/REPL.hs @@ -21,6 +21,7 @@ import Swarm.Game.State.Substate import Swarm.TUI.Controller.Util import Swarm.TUI.Model import Swarm.TUI.Model.Event +import Swarm.TUI.Model.Name import Swarm.TUI.Model.Repl import Swarm.TUI.Model.UI diff --git a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Robot.hs b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Robot.hs index b2a0bcefb..c6c456518 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Robot.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Robot.hs @@ -31,6 +31,7 @@ import Swarm.TUI.Inventory.Sorting (cycleSortDirection, cycleSortOrder) import Swarm.TUI.List import Swarm.TUI.Model import Swarm.TUI.Model.Event +import Swarm.TUI.Model.Name import Swarm.TUI.Model.UI import Swarm.TUI.View.Util (generateModal) diff --git a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/World.hs b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/World.hs index b4dbe921a..aa4968aaf 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/World.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/World.hs @@ -22,6 +22,7 @@ import Swarm.Language.Syntax.Direction (Direction (..), directionSyntax) import Swarm.TUI.Controller.Util import Swarm.TUI.Model import Swarm.TUI.Model.Event +import Swarm.TUI.Model.Name import Swarm.TUI.Model.UI -- | Handle a user input event in the world view panel. diff --git a/src/swarm-tui/Swarm/TUI/Controller/UpdateUI.hs b/src/swarm-tui/Swarm/TUI/Controller/UpdateUI.hs index d9a22e9b6..94aa0cd24 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/UpdateUI.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/UpdateUI.hs @@ -13,7 +13,9 @@ import Brick hiding (Direction, Location) import Brick.Focus -- See Note [liftA2 re-export from Prelude] + import Brick.Widgets.List qualified as BL +import Brick.Widgets.TabularList.Mixed (MixedTabularList (..)) import Control.Applicative (liftA2, pure) import Control.Lens as Lens import Control.Monad (unless, when) @@ -43,6 +45,8 @@ import Swarm.TUI.Model.Name import Swarm.TUI.Model.Repl import Swarm.TUI.Model.UI import Swarm.TUI.View.Objective qualified as GR +import Swarm.TUI.View.Robot +import Swarm.TUI.View.RobotDisplay (libList, robID, robotListContent) import Witch (into) import Prelude hiding (Applicative (..)) @@ -165,6 +169,8 @@ updateUI = do newPopups <- generateNotificationPopups + doRobotListUpdate g + let redraw = g ^. needsRedraw || inventoryUpdated @@ -174,6 +180,28 @@ updateUI = do || newPopups pure redraw +doRobotListUpdate :: GameState -> EventM Name AppState () +doRobotListUpdate g = do + gp <- use $ uiState . uiGameplay + dOps <- use $ uiState . uiDebugOptions + + let rd = + mkRobotDisplay + ( RobotRenderingContext + { _mygs = g + , _gameplay = gp + , _timing = gp ^. uiTiming + , _uiDbg = dOps + } + ) + + let MixedTabularList oldList _ _ = gp ^. uiDialogs . uiRobot . robotListContent . libList + maybeOldSelectedRID = robID . snd <$> BL.listSelectedElement oldList + rd' = case maybeOldSelectedRID of + Nothing -> rd + Just oldSelectedRID -> rd & libList %~ (\(MixedTabularList ls a b) -> MixedTabularList (BL.listFindBy ((== oldSelectedRID) . robID) ls) a b) + uiState . uiGameplay . uiDialogs . uiRobot . robotListContent .= rd' + -- | Either pops up the updated Goals modal -- or pops up the Congratulations (Win) modal, or pops -- up the Condolences (Lose) modal. diff --git a/src/swarm-tui/Swarm/TUI/Controller/Util.hs b/src/swarm-tui/Swarm/TUI/Controller/Util.hs index dff5581f9..2f257d705 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/Util.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/Util.hs @@ -35,13 +35,12 @@ import Swarm.Language.Capability (Capability (CDebug)) import Swarm.Language.Syntax hiding (Key) import Swarm.TUI.Model ( AppState, - FocusablePanel, ModalType (..), - Name (..), gameState, modalScroll, uiState, ) +import Swarm.TUI.Model.Name import Swarm.TUI.Model.Repl (REPLHistItem, REPLPrompt, REPLState, addREPLItem, replHistory, replPromptText, replPromptType) import Swarm.TUI.Model.UI import Swarm.TUI.View.Util (generateModal) diff --git a/src/swarm-tui/Swarm/TUI/Editor/Masking.hs b/src/swarm-tui/Swarm/TUI/Editor/Masking.hs index 547640abd..8f43b9488 100644 --- a/src/swarm-tui/Swarm/TUI/Editor/Masking.hs +++ b/src/swarm-tui/Swarm/TUI/Editor/Masking.hs @@ -8,7 +8,7 @@ import Swarm.Game.Universe import Swarm.Game.World.Coords import Swarm.TUI.Editor.Model import Swarm.TUI.Editor.Util qualified as EU -import Swarm.TUI.Model.UI +import Swarm.TUI.Model.UI.Gameplay shouldHideWorldCell :: UIGameplay -> Coords -> Bool shouldHideWorldCell ui coords = diff --git a/src/swarm-tui/Swarm/TUI/Editor/View.hs b/src/swarm-tui/Swarm/TUI/Editor/View.hs index 4c73991e4..0a9e5b986 100644 --- a/src/swarm-tui/Swarm/TUI/Editor/View.hs +++ b/src/swarm-tui/Swarm/TUI/Editor/View.hs @@ -118,7 +118,7 @@ drawWorldEditor toplevelFocusRing uis = L.intersperse "@" [ EA.renderRectDimensions rectArea - , VU.locationToString upperLeftLoc + , locationToString upperLeftLoc ] where upperLeftLoc = coordsToLoc upperLeftCoord diff --git a/src/swarm-tui/Swarm/TUI/Model.hs b/src/swarm-tui/Swarm/TUI/Model.hs index 821932c67..40bc5a388 100644 --- a/src/swarm-tui/Swarm/TUI/Model.hs +++ b/src/swarm-tui/Swarm/TUI/Model.hs @@ -13,7 +13,6 @@ module Swarm.TUI.Model ( -- $uilabel AppEvent (..), FocusablePanel (..), - Name (..), -- ** Web command WebCommand (..), diff --git a/src/swarm-tui/Swarm/TUI/Model/KeyBindings.hs b/src/swarm-tui/Swarm/TUI/Model/KeyBindings.hs index dc47c69c5..8c48523a4 100644 --- a/src/swarm-tui/Swarm/TUI/Model/KeyBindings.hs +++ b/src/swarm-tui/Swarm/TUI/Model/KeyBindings.hs @@ -28,6 +28,7 @@ import Swarm.Language.Pretty (prettyText) import Swarm.TUI.Controller.EventHandlers import Swarm.TUI.Model import Swarm.TUI.Model.Event (SwarmEvent, defaultSwarmBindings, swarmEvents) +import Swarm.TUI.Model.Name -- See Note [how Swarm event handlers work] diff --git a/src/swarm-tui/Swarm/TUI/Model/Name.hs b/src/swarm-tui/Swarm/TUI/Model/Name.hs index 035ac846e..a27f76d26 100644 --- a/src/swarm-tui/Swarm/TUI/Model/Name.hs +++ b/src/swarm-tui/Swarm/TUI/Model/Name.hs @@ -104,6 +104,8 @@ data Name StructureWidgets StructureWidget | -- | The list of scenario choices. ScenarioList + | -- | The robots list + RobotsList | -- | The scrollable viewport for the info panel. InfoViewport | -- | The scrollable viewport for any modal dialog. diff --git a/src/swarm-tui/Swarm/TUI/Model/UI.hs b/src/swarm-tui/Swarm/TUI/Model/UI.hs index 4fcddcc8b..c60eec722 100644 --- a/src/swarm-tui/Swarm/TUI/Model/UI.hs +++ b/src/swarm-tui/Swarm/TUI/Model/UI.hs @@ -31,6 +31,7 @@ module Swarm.TUI.Model.UI ( uiModal, uiGoal, uiStructure, + uiRobot, uiDialogs, uiIsAutoPlay, uiAchievements, @@ -61,29 +62,21 @@ module Swarm.TUI.Model.UI ( import Brick (AttrMap) import Brick.Focus -import Brick.Widgets.List qualified as BL import Control.Arrow ((&&&)) import Control.Effect.Accum import Control.Effect.Lift import Control.Lens hiding (from, (<.>)) -import Data.Bits (FiniteBits (finiteBitSize)) import Data.List.Extra (enumerate) import Data.Map (Map) import Data.Map qualified as M import Data.Sequence (Seq) import Data.Set (Set) -import Data.Text (Text) import Data.Text qualified as T import Swarm.Game.Achievement.Attainment import Swarm.Game.Achievement.Definitions import Swarm.Game.Achievement.Persistence import Swarm.Game.Failure (SystemFailure) import Swarm.Game.ResourceLoading (getSwarmHistoryPath) -import Swarm.Game.ScenarioInfo ( - ScenarioInfoPair, - ) -import Swarm.Game.Universe -import Swarm.Game.World.Coords import Swarm.TUI.Editor.Model import Swarm.TUI.Inventory.Sorting import Swarm.TUI.Launch.Model @@ -93,201 +86,14 @@ import Swarm.TUI.Model.Dialog import Swarm.TUI.Model.Menu import Swarm.TUI.Model.Name import Swarm.TUI.Model.Repl +import Swarm.TUI.Model.UI.Gameplay import Swarm.TUI.View.Attribute.Attr (swarmAttrMap) +import Swarm.TUI.View.Robot +import Swarm.TUI.View.RobotDisplay import Swarm.Util -import Swarm.Util.Lens (makeLensesExcluding, makeLensesNoSigs) +import Swarm.Util.Lens (makeLensesNoSigs) import System.Clock -data UITiming = UITiming - { _uiShowFPS :: Bool - , _uiTPF :: Double - , _uiFPS :: Double - , _lgTicksPerSecond :: Int - , _tickCount :: Int - , _frameCount :: Int - , _frameTickCount :: Int - , _lastFrameTime :: TimeSpec - , _accumulatedTime :: TimeSpec - , _lastInfoTime :: TimeSpec - } - --- * Lenses for UITiming - -makeLensesExcluding ['_lgTicksPerSecond] ''UITiming - --- | A toggle to show the FPS by pressing @f@ -uiShowFPS :: Lens' UITiming Bool - --- | Computed ticks per milliseconds -uiTPF :: Lens' UITiming Double - --- | Computed frames per milliseconds -uiFPS :: Lens' UITiming Double - --- | The base-2 logarithm of the current game speed in ticks/second. --- Note that we cap this value to the range of +/- log2 INTMAX. -lgTicksPerSecond :: Lens' UITiming Int -lgTicksPerSecond = lens _lgTicksPerSecond safeSetLgTicks - where - maxLog = finiteBitSize (maxBound :: Int) - maxTicks = maxLog - 2 - minTicks = 2 - maxLog - safeSetLgTicks ui lTicks - | lTicks < minTicks = setLgTicks ui minTicks - | lTicks > maxTicks = setLgTicks ui maxTicks - | otherwise = setLgTicks ui lTicks - setLgTicks ui lTicks = ui {_lgTicksPerSecond = lTicks} - --- | A counter used to track how many ticks have happened since the --- last time we updated the ticks/frame statistics. -tickCount :: Lens' UITiming Int - --- | A counter used to track how many frames have been rendered since the --- last time we updated the ticks/frame statistics. -frameCount :: Lens' UITiming Int - --- | A counter used to track how many ticks have happened in the --- current frame, so we can stop when we get to the tick cap. -frameTickCount :: Lens' UITiming Int - --- | The time of the last info widget update -lastInfoTime :: Lens' UITiming TimeSpec - --- | The time of the last 'Swarm.TUI.Model.Frame' event. -lastFrameTime :: Lens' UITiming TimeSpec - --- | The amount of accumulated real time. Every time we get a 'Swarm.TUI.Model.Frame' --- event, we accumulate the amount of real time that happened since --- the last frame, then attempt to take an appropriate number of --- ticks to "catch up", based on the target tick rate. --- --- See https://gafferongames.com/post/fix_your_timestep/ . -accumulatedTime :: Lens' UITiming TimeSpec - -data UIInventory = UIInventory - { _uiInventoryList :: Maybe (Int, BL.List Name InventoryListEntry) - , _uiInventorySort :: InventorySortOptions - , _uiInventorySearch :: Maybe Text - , _uiShowZero :: Bool - , _uiInventoryShouldUpdate :: Bool - } - --- * Lenses for UIInventory - -makeLensesNoSigs ''UIInventory - --- | The order and direction of sorting inventory list. -uiInventorySort :: Lens' UIInventory InventorySortOptions - --- | The current search string used to narrow the inventory view. -uiInventorySearch :: Lens' UIInventory (Maybe Text) - --- | The hash value of the focused robot entity (so we can tell if its --- inventory changed) along with a list of the items in the --- focused robot's inventory. -uiInventoryList :: Lens' UIInventory (Maybe (Int, BL.List Name InventoryListEntry)) - --- | A toggle to show or hide inventory items with count 0 by pressing @0@ -uiShowZero :: Lens' UIInventory Bool - --- | Whether the Inventory ui panel should update -uiInventoryShouldUpdate :: Lens' UIInventory Bool - --- | State that backs various modal dialogs -data UIDialogs = UIDialogs - { _uiModal :: Maybe Modal - , _uiGoal :: GoalDisplay - , _uiStructure :: StructureDisplay - } - --- * Lenses for UIDialogs - -makeLensesNoSigs ''UIDialogs - --- | When this is 'Just', it represents a modal to be displayed on --- top of the UI, e.g. for the Help screen. -uiModal :: Lens' UIDialogs (Maybe Modal) - --- | Status of the scenario goal: whether there is one, and whether it --- has been displayed to the user initially. -uiGoal :: Lens' UIDialogs GoalDisplay - --- | Definition and status of a recognizable structure -uiStructure :: Lens' UIDialogs StructureDisplay - --- | The main record holding the gameplay UI state. For access to the fields, --- see the lenses below. -data UIGameplay = UIGameplay - { _uiFocusRing :: FocusRing Name - , _uiWorldCursor :: Maybe (Cosmic Coords) - , _uiWorldEditor :: WorldEditor Name - , _uiREPL :: REPLState - , _uiInventory :: UIInventory - , _uiScrollToEnd :: Bool - , _uiDialogs :: UIDialogs - , _uiIsAutoPlay :: Bool - , _uiShowREPL :: Bool - , _uiShowDebug :: Bool - , _uiHideRobotsUntil :: TimeSpec - , _uiTiming :: UITiming - , _scenarioRef :: Maybe ScenarioInfoPair - } - --- * Lenses for UIGameplay - -makeLensesNoSigs ''UIGameplay - --- | Temporal information for gameplay UI -uiTiming :: Lens' UIGameplay UITiming - --- | Inventory information for gameplay UI -uiInventory :: Lens' UIGameplay UIInventory - --- | The focus ring is the set of UI panels we can cycle among using --- the @Tab@ key. -uiFocusRing :: Lens' UIGameplay (FocusRing Name) - --- | The last clicked position on the world view. -uiWorldCursor :: Lens' UIGameplay (Maybe (Cosmic Coords)) - --- | State of all World Editor widgets -uiWorldEditor :: Lens' UIGameplay (WorldEditor Name) - --- | The state of REPL panel. -uiREPL :: Lens' UIGameplay REPLState - --- | A flag telling the UI to scroll the info panel to the very end --- (used when a new log message is appended). -uiScrollToEnd :: Lens' UIGameplay Bool - --- | State that backs various modal dialogs -uiDialogs :: Lens' UIGameplay UIDialogs - --- | When running with @--autoplay@, suppress the goal dialogs. --- --- For development, the @--cheat@ flag shows goals again. -uiIsAutoPlay :: Lens' UIGameplay Bool - --- | A toggle to expand or collapse the REPL by pressing @Ctrl-k@ -uiShowREPL :: Lens' UIGameplay Bool - --- | A toggle to show CESK machine debug view and step through it. --- --- Note that the ability to use it can be enabled by player robot --- gaining the capability, or being in creative mode or with --- the debug option 'Swarm.TUI.Model.DebugOption.DebugCESK'. -uiShowDebug :: Lens' UIGameplay Bool - --- | Hide robots on the world map. -uiHideRobotsUntil :: Lens' UIGameplay TimeSpec - --- | Whether to show or hide robots on the world map. -uiShowRobots :: Getter UIGameplay Bool -uiShowRobots = to (\ui -> ui ^. uiTiming . lastFrameTime > ui ^. uiHideRobotsUntil) - --- | The currently active Scenario description, useful for starting over. -scenarioRef :: Lens' UIGameplay (Maybe ScenarioInfoPair) - -- * Toplevel UIState definition data UIState = UIState @@ -395,6 +201,11 @@ initUIState speedFactor showMainMenu debug = do { _uiModal = Nothing , _uiGoal = emptyGoalDisplay , _uiStructure = emptyStructureDisplay + , _uiRobot = + RobotDisplay + { _robotsDisplayMode = RobotList + , _robotListContent = emptyRobotDisplay debug + } } , _uiIsAutoPlay = False , _uiTiming = diff --git a/src/swarm-tui/Swarm/TUI/Model/UI/Gameplay.hs b/src/swarm-tui/Swarm/TUI/Model/UI/Gameplay.hs new file mode 100644 index 000000000..4c990168b --- /dev/null +++ b/src/swarm-tui/Swarm/TUI/Model/UI/Gameplay.hs @@ -0,0 +1,224 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE ViewPatterns #-} + +-- | +-- SPDX-License-Identifier: BSD-3-Clause +module Swarm.TUI.Model.UI.Gameplay where + +import Brick.Focus +import Brick.Widgets.List qualified as BL +import Control.Lens hiding (from, (<.>)) +import Data.Bits (FiniteBits (finiteBitSize)) +import Data.Text (Text) +import Swarm.Game.ScenarioInfo ( + ScenarioInfoPair, + ) +import Swarm.Game.Universe +import Swarm.Game.World.Coords +import Swarm.TUI.Editor.Model +import Swarm.TUI.Inventory.Sorting +import Swarm.TUI.Model.Dialog.Goal +import Swarm.TUI.Model.Dialog.Structure +import Swarm.TUI.Model.Menu +import Swarm.TUI.Model.Name +import Swarm.TUI.Model.Repl +import Swarm.TUI.View.RobotDisplay +import Swarm.Util.Lens (makeLensesExcluding, makeLensesNoSigs) +import System.Clock + +data UITiming = UITiming + { _uiShowFPS :: Bool + , _uiTPF :: Double + , _uiFPS :: Double + , _lgTicksPerSecond :: Int + , _tickCount :: Int + , _frameCount :: Int + , _frameTickCount :: Int + , _lastFrameTime :: TimeSpec + , _accumulatedTime :: TimeSpec + , _lastInfoTime :: TimeSpec + } + +-- * Lenses for UITiming + +makeLensesExcluding ['_lgTicksPerSecond] ''UITiming + +-- | A toggle to show the FPS by pressing @f@ +uiShowFPS :: Lens' UITiming Bool + +-- | Computed ticks per milliseconds +uiTPF :: Lens' UITiming Double + +-- | Computed frames per milliseconds +uiFPS :: Lens' UITiming Double + +-- | The base-2 logarithm of the current game speed in ticks/second. +-- Note that we cap this value to the range of +/- log2 INTMAX. +lgTicksPerSecond :: Lens' UITiming Int +lgTicksPerSecond = lens _lgTicksPerSecond safeSetLgTicks + where + maxLog = finiteBitSize (maxBound :: Int) + maxTicks = maxLog - 2 + minTicks = 2 - maxLog + safeSetLgTicks ui lTicks + | lTicks < minTicks = setLgTicks ui minTicks + | lTicks > maxTicks = setLgTicks ui maxTicks + | otherwise = setLgTicks ui lTicks + setLgTicks ui lTicks = ui {_lgTicksPerSecond = lTicks} + +-- | A counter used to track how many ticks have happened since the +-- last time we updated the ticks/frame statistics. +tickCount :: Lens' UITiming Int + +-- | A counter used to track how many frames have been rendered since the +-- last time we updated the ticks/frame statistics. +frameCount :: Lens' UITiming Int + +-- | A counter used to track how many ticks have happened in the +-- current frame, so we can stop when we get to the tick cap. +frameTickCount :: Lens' UITiming Int + +-- | The time of the last info widget update +lastInfoTime :: Lens' UITiming TimeSpec + +-- | The time of the last 'Swarm.TUI.Model.Frame' event. +lastFrameTime :: Lens' UITiming TimeSpec + +-- | The amount of accumulated real time. Every time we get a 'Swarm.TUI.Model.Frame' +-- event, we accumulate the amount of real time that happened since +-- the last frame, then attempt to take an appropriate number of +-- ticks to "catch up", based on the target tick rate. +-- +-- See https://gafferongames.com/post/fix_your_timestep/ . +accumulatedTime :: Lens' UITiming TimeSpec + +data UIInventory = UIInventory + { _uiInventoryList :: Maybe (Int, BL.List Name InventoryListEntry) + , _uiInventorySort :: InventorySortOptions + , _uiInventorySearch :: Maybe Text + , _uiShowZero :: Bool + , _uiInventoryShouldUpdate :: Bool + } + +-- * Lenses for UIInventory + +makeLensesNoSigs ''UIInventory + +-- | The order and direction of sorting inventory list. +uiInventorySort :: Lens' UIInventory InventorySortOptions + +-- | The current search string used to narrow the inventory view. +uiInventorySearch :: Lens' UIInventory (Maybe Text) + +-- | The hash value of the focused robot entity (so we can tell if its +-- inventory changed) along with a list of the items in the +-- focused robot's inventory. +uiInventoryList :: Lens' UIInventory (Maybe (Int, BL.List Name InventoryListEntry)) + +-- | A toggle to show or hide inventory items with count 0 by pressing @0@ +uiShowZero :: Lens' UIInventory Bool + +-- | Whether the Inventory ui panel should update +uiInventoryShouldUpdate :: Lens' UIInventory Bool + +-- | State that backs various modal dialogs +data UIDialogs = UIDialogs + { _uiModal :: Maybe Modal + , _uiGoal :: GoalDisplay + , _uiStructure :: StructureDisplay + , _uiRobot :: RobotDisplay + } + +-- * Lenses for UIDialogs + +makeLensesNoSigs ''UIDialogs + +-- | When this is 'Just', it represents a modal to be displayed on +-- top of the UI, e.g. for the Help screen. +uiModal :: Lens' UIDialogs (Maybe Modal) + +-- | Status of the scenario goal: whether there is one, and whether it +-- has been displayed to the user initially. +uiGoal :: Lens' UIDialogs GoalDisplay + +-- | Definition and status of a recognizable structure +uiStructure :: Lens' UIDialogs StructureDisplay + +-- | Definition and status of a recognizable structure +uiRobot :: Lens' UIDialogs RobotDisplay + +-- | The main record holding the gameplay UI state. For access to the fields, +-- see the lenses below. +data UIGameplay = UIGameplay + { _uiFocusRing :: FocusRing Name + , _uiWorldCursor :: Maybe (Cosmic Coords) + , _uiWorldEditor :: WorldEditor Name + , _uiREPL :: REPLState + , _uiInventory :: UIInventory + , _uiScrollToEnd :: Bool + , _uiDialogs :: UIDialogs + , _uiIsAutoPlay :: Bool + , _uiShowREPL :: Bool + , _uiShowDebug :: Bool + , _uiHideRobotsUntil :: TimeSpec + , _uiTiming :: UITiming + , _scenarioRef :: Maybe ScenarioInfoPair + } + +-- * Lenses for UIGameplay + +makeLensesNoSigs ''UIGameplay + +-- | Temporal information for gameplay UI +uiTiming :: Lens' UIGameplay UITiming + +-- | Inventory information for gameplay UI +uiInventory :: Lens' UIGameplay UIInventory + +-- | The focus ring is the set of UI panels we can cycle among using +-- the @Tab@ key. +uiFocusRing :: Lens' UIGameplay (FocusRing Name) + +-- | The last clicked position on the world view. +uiWorldCursor :: Lens' UIGameplay (Maybe (Cosmic Coords)) + +-- | State of all World Editor widgets +uiWorldEditor :: Lens' UIGameplay (WorldEditor Name) + +-- | The state of REPL panel. +uiREPL :: Lens' UIGameplay REPLState + +-- | A flag telling the UI to scroll the info panel to the very end +-- (used when a new log message is appended). +uiScrollToEnd :: Lens' UIGameplay Bool + +-- | State that backs various modal dialogs +uiDialogs :: Lens' UIGameplay UIDialogs + +-- | When running with @--autoplay@, suppress the goal dialogs. +-- +-- For development, the @--cheat@ flag shows goals again. +uiIsAutoPlay :: Lens' UIGameplay Bool + +-- | A toggle to expand or collapse the REPL by pressing @Ctrl-k@ +uiShowREPL :: Lens' UIGameplay Bool + +-- | A toggle to show CESK machine debug view and step through it. +-- +-- Note that the ability to use it can be enabled by player robot +-- gaining the capability, or being in creative mode or with +-- the debug option 'Swarm.TUI.Model.DebugOption.DebugCESK'. +uiShowDebug :: Lens' UIGameplay Bool + +-- | Hide robots on the world map. +uiHideRobotsUntil :: Lens' UIGameplay TimeSpec + +-- | Whether to show or hide robots on the world map. +uiShowRobots :: Getter UIGameplay Bool +uiShowRobots = to (\ui -> ui ^. uiTiming . lastFrameTime > ui ^. uiHideRobotsUntil) + +-- | The currently active Scenario description, useful for starting over. +scenarioRef :: Lens' UIGameplay (Maybe ScenarioInfoPair) diff --git a/src/swarm-tui/Swarm/TUI/View.hs b/src/swarm-tui/Swarm/TUI/View.hs index 9d2215615..6462b161c 100644 --- a/src/swarm-tui/Swarm/TUI/View.hs +++ b/src/swarm-tui/Swarm/TUI/View.hs @@ -47,6 +47,7 @@ import Brick.Widgets.Dialog import Brick.Widgets.Edit (getEditContents, renderEditor) import Brick.Widgets.List qualified as BL import Brick.Widgets.Table qualified as BT +import Brick.Widgets.TabularList.Mixed (MixedTabularList (..)) import Control.Lens as Lens hiding (Const, from) import Control.Monad (guard) import Data.Array (range) @@ -132,6 +133,7 @@ import Swarm.TUI.Model.DebugOption (DebugOption (..)) import Swarm.TUI.Model.Dialog.Goal (goalsContent, hasAnythingToShow) import Swarm.TUI.Model.Event qualified as SE import Swarm.TUI.Model.KeyBindings (handlerNameKeysDescription) +import Swarm.TUI.Model.Name import Swarm.TUI.Model.Repl import Swarm.TUI.Model.UI import Swarm.TUI.Panel @@ -142,6 +144,7 @@ import Swarm.TUI.View.Logo import Swarm.TUI.View.Objective qualified as GR import Swarm.TUI.View.Popup import Swarm.TUI.View.Robot +import Swarm.TUI.View.RobotDisplay import Swarm.TUI.View.Structure qualified as SR import Swarm.TUI.View.Util as VU import Swarm.Util @@ -378,7 +381,7 @@ makeBestScoreRows scenarioStat = , Just $ describeProgress b ) where - maxLeftColumnWidth = maximum (map (T.length . describeCriteria) enumerate) + maxLeftColumnWidth = maximum0 (map (T.length . describeCriteria) enumerate) mkCriteriaRow = withAttr dimAttr . padLeft Max @@ -607,6 +610,7 @@ drawDialog :: AppState -> Widget Name drawDialog s = case s ^. uiState . uiGameplay . uiDialogs . uiModal of Just (Modal mt d) -> renderDialog d $ case mt of GoalModal -> drawModal s mt + RobotsModal -> drawModal s mt _ -> maybeScroll ModalViewport $ drawModal s mt Nothing -> emptyWidget @@ -614,7 +618,14 @@ drawDialog s = case s ^. uiState . uiGameplay . uiDialogs . uiModal of drawModal :: AppState -> ModalType -> Widget Name drawModal s = \case HelpModal -> helpWidget (s ^. gameState . randomness . seed) (s ^. runtimeState . webPort) (s ^. keyEventHandling) - RobotsModal -> robotsListWidget s + RobotsModal -> case s ^. uiState . uiGameplay . uiDialogs . uiRobot . robotsDisplayMode of + RobotList -> renderTheRobots $ s ^. uiState . uiGameplay . uiDialogs . uiRobot . robotListContent + SingleRobotDetails -> case maybeSelectedRID of + Nothing -> str "No selection" + Just selectedRID -> str $ unwords ["Selected robot", show selectedRID] + where + MixedTabularList oldList _ _ = s ^. uiState . uiGameplay . uiDialogs . uiRobot . robotListContent . libList + maybeSelectedRID = robID . snd <$> BL.listSelectedElement oldList RecipesModal -> availableListWidget (s ^. gameState) RecipeList CommandsModal -> commandsListWidget (s ^. gameState) MessagesModal -> availableListWidget (s ^. gameState) MessageList @@ -690,7 +701,7 @@ helpWidget theSeed mport keyState = keyHandlerToText = handlerNameKeysDescription (keyState ^. keyConfig) -- Get maximum width of the table columns so it all neatly aligns txtFilled n t = padRight (Pad $ max 0 (n - textWidth t)) $ txt t - (maxN, maxK, maxD) = map3 (maximum . map textWidth) . unzip3 $ keyHandlerToText <$> allEventHandlers + (maxN, maxK, maxD) = map3 (maximum0 . map textWidth) . unzip3 $ keyHandlerToText <$> allEventHandlers map3 f (n, k, d) = (f n, f k, f d) data NotificationList = RecipeList | MessageList diff --git a/src/swarm-tui/Swarm/TUI/View/Achievement.hs b/src/swarm-tui/Swarm/TUI/View/Achievement.hs index 47f4a9b6a..cd1765516 100644 --- a/src/swarm-tui/Swarm/TUI/View/Achievement.hs +++ b/src/swarm-tui/Swarm/TUI/View/Achievement.hs @@ -16,6 +16,7 @@ import Swarm.Game.Achievement.Attainment import Swarm.Game.Achievement.Definitions import Swarm.Game.Achievement.Description import Swarm.TUI.Model +import Swarm.TUI.Model.Name import Swarm.TUI.Model.UI import Swarm.TUI.View.Attribute.Attr import Swarm.TUI.View.Util (drawMarkdown) diff --git a/src/swarm-tui/Swarm/TUI/View/CellDisplay.hs b/src/swarm-tui/Swarm/TUI/View/CellDisplay.hs index 8d9320c0e..99c1883c6 100644 --- a/src/swarm-tui/Swarm/TUI/View/CellDisplay.hs +++ b/src/swarm-tui/Swarm/TUI/View/CellDisplay.hs @@ -48,7 +48,7 @@ import Swarm.TUI.Editor.Masking import Swarm.TUI.Editor.Model import Swarm.TUI.Editor.Util qualified as EU import Swarm.TUI.Model.Name -import Swarm.TUI.Model.UI +import Swarm.TUI.Model.UI.Gameplay import Swarm.TUI.View.Attribute.Attr import Swarm.Util (applyWhen) import Witch (from) diff --git a/src/swarm-tui/Swarm/TUI/View/Popup.hs b/src/swarm-tui/Swarm/TUI/View/Popup.hs index 96b30e62f..dfd1c8e71 100644 --- a/src/swarm-tui/Swarm/TUI/View/Popup.hs +++ b/src/swarm-tui/Swarm/TUI/View/Popup.hs @@ -14,9 +14,10 @@ import Control.Lens ((^.)) import Swarm.Game.Achievement.Definitions (title) import Swarm.Game.Achievement.Description (describe) import Swarm.Language.Syntax (constInfo, syntax) -import Swarm.TUI.Model (AppState, Name, uiState) +import Swarm.TUI.Model (AppState, uiState) import Swarm.TUI.Model.Dialog.Popup (Popup (..), currentPopup, popupFrames) import Swarm.TUI.Model.Event qualified as SE +import Swarm.TUI.Model.Name import Swarm.TUI.Model.UI (uiPopups) import Swarm.TUI.View.Attribute.Attr (notifAttr) import Swarm.TUI.View.Util (bindingText) diff --git a/src/swarm-tui/Swarm/TUI/View/Robot.hs b/src/swarm-tui/Swarm/TUI/View/Robot.hs index 93ff2415b..4a461ba50 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot.hs @@ -1,20 +1,29 @@ +{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE NoGeneralizedNewtypeDeriving #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} -- | -- SPDX-License-Identifier: BSD-3-Clause --- --- A UI-centric model for presentation of Robot details. module Swarm.TUI.View.Robot where -import Brick hiding (Direction, Location) -import Brick.Widgets.Center (hCenter) -import Brick.Widgets.Table qualified as BT +import Brick +import Brick.Widgets.Border +import Brick.Widgets.TabularList.Mixed +import Control.Lens hiding (from, (<.>)) import Control.Lens as Lens hiding (Const, from) import Data.IntMap qualified as IM +import Data.List (transpose) import Data.Map qualified as M import Data.Maybe (fromMaybe) -import Linear +import Data.Sequence (Seq) +import Data.Sequence qualified as S +import Data.Set (Set) +import Data.Text qualified as T +import Data.Vector (Vector) +import Data.Vector qualified as V +import Linear (V2 (..), distance) import Numeric (showFFloat) import Swarm.Game.CESK (CESK (..)) import Swarm.Game.Entity as E @@ -28,17 +37,159 @@ import Swarm.Game.State.Substate import Swarm.Game.Tick (addTicks) import Swarm.Game.Universe import Swarm.Game.World.Coords -import Swarm.TUI.Model -import Swarm.TUI.Model.DebugOption (DebugOption (..)) -import Swarm.TUI.Model.UI +import Swarm.TUI.Model.DebugOption +import Swarm.TUI.Model.Name +import Swarm.TUI.Model.UI.Gameplay import Swarm.TUI.View.Attribute.Attr import Swarm.TUI.View.CellDisplay -import Swarm.TUI.View.Util as VU -import Swarm.Util +import Swarm.TUI.View.RobotDisplay +import Swarm.Util (applyWhen, maximum0) import Swarm.Util.UnitInterval import Swarm.Util.WindowedCounter qualified as WC import System.Clock (TimeSpec (..)) +instance Semigroup ColWidth where + ColW w1 <> ColW w2 = ColW $ max w1 w2 + +data RobotRenderingContext = RobotRenderingContext + { _mygs :: GameState + , _gameplay :: UIGameplay + , _timing :: UITiming + , _uiDbg :: Set DebugOption + } + +makeLenses ''RobotRenderingContext + +mkRobotDisplay :: RobotRenderingContext -> RobotListContent +mkRobotDisplay c = + RobotListContent + { _libList = mixedTabularList RobotsList (mkLibraryEntries c) (LstItmH 1) (wprk uiDebug) wpr + , _libRenderers = + MixedRenderers + { cell = dc uiDebug + , rowHdr = Just rowHdr + , colHdr = Just $ colHdr uiDebug + , colHdrRowHdr = Just $ ColHdrRowHdr $ \_ _ -> vLimit 1 (fill ' ') <=> hBorder + } + } + where + uiDebug = c ^. uiDbg + +emptyRobotDisplay :: Set DebugOption -> RobotListContent +emptyRobotDisplay uiDebug = + RobotListContent + { _libList = mixedTabularList RobotsList mempty (LstItmH 1) (wprk uiDebug) wpr + , _libRenderers = + MixedRenderers + { cell = dc uiDebug + , rowHdr = Just rowHdr + , colHdr = Just $ colHdr uiDebug + , colHdrRowHdr = Just $ ColHdrRowHdr $ \_ _ -> vLimit 1 (fill ' ') <=> hBorder + } + } + +renderTheRobots :: RobotListContent -> Widget Name +renderTheRobots rd = + renderMixedTabularList (rd ^. libRenderers) (LstFcs True) (rd ^. libList) + +columnHdrAttr :: AttrName +columnHdrAttr = attrName "columnHeader" + +rowHdrAttr :: AttrName +rowHdrAttr = attrName "rowHeader" + +colHdr :: Set DebugOption -> MixedColHdr Name Widths +colHdr uiDebug = + MixedColHdr + { draw = \_ (MColC (Ix ci)) -> case hdrs V.!? ci of + Just ch -> withAttr columnHdrAttr (padRight Max $ str ch) <=> hBorder + Nothing -> emptyWidget + , widths = \(Widths ws) -> zipWith (<>) ws (map (ColW . length) $ V.toList hdrs) + , height = ColHdrH 2 + } + where + hdrs = colHdrs uiDebug + +-- | Enumerates the rows by position (not 'RID'). +rowHdr :: RowHdr Name RobotWidgetRow +rowHdr = + RowHdr + { draw = \_ (WdthD wd) (RowHdrCtxt (Sel s)) rh -> + let attrFn = + if s + then id + else withAttr rowHdrAttr + in attrFn $ padRight (Pad $ if wd > 0 then 0 else 1) $ padLeft Max (str $ show rh) + , width = \_ rh -> RowHdrW . (+ 2) . maximum0 $ map (length . show) rh + , toRH = \_ (Ix i) -> i + 1 + } + +headingStringList :: [String] +headingStringList = + map ($ headingStrings) accessorList + where + headingStrings = + LibRobotRow + { _fID = "ID" + , _fName = "Name" + , _fAge = "Age" + , _fPos = "Pos" + , _fItems = "Items" + , _fStatus = "Status" + , _fActns = "Actns" + , _fCmds = "Cmds" + , _fCycles = "Cycles" + , _fActivity = "Activity" + , _fLog = "Log" + } + +dropFirstColumn :: Set DebugOption -> [a] -> [a] +dropFirstColumn uiDebug = + applyWhen (not debugRID) (drop 1) + where + debugRID = uiDebug ^. Lens.contains ListRobotIDs + +colHdrs :: Set DebugOption -> Vector String +colHdrs uiDebug = V.fromList $ dropFirstColumn uiDebug headingStringList + +accessorList :: [LibRobotRow a -> a] +accessorList = + [ _fID + , _fName + , _fAge + , _fPos + , _fItems + , _fStatus + , _fActns + , _fCmds + , _fCycles + , _fActivity + , _fLog + ] + +dc :: Set DebugOption -> ListFocused -> MixedCtxt -> RobotWidgetRow -> Widget Name +dc uiDebug _ (MxdCtxt _ (MColC (Ix ci))) r = + maybe emptyWidget (renderPlainCell . wWidget . ($ rPayload r)) (indexedAccessors V.!? ci) + where + indexedAccessors = V.fromList accessors + accessors = dropFirstColumn uiDebug accessorList + renderPlainCell = padRight Max + +-- | For a single-constructor datatype like 'RobotWidgetRow', +-- this implementation is trivial. +wpr :: WidthsPerRow RobotWidgetRow Widths +wpr = WsPerR $ \(Widths x) _ -> x + +wprk :: Set DebugOption -> WidthsPerRowKind RobotWidgetRow Widths +wprk uiDebug = WsPerRK $ \(AvlW _) allRows -> + Widths {robotRowWidths = mkWidths allRows} + where + colHeaderRowLengths = map length $ dropFirstColumn uiDebug headingStringList + mkWidths = map (ColW . (+ 1) . maximum0) . transpose . (colHeaderRowLengths :) . map getColWidthsForRow + where + getColWidthsForRow :: RobotWidgetRow -> [Int] + getColWidthsForRow r = map (wWidth . ($ rPayload r)) $ dropFirstColumn uiDebug accessorList + -- | Render the percentage of ticks that this robot was active. -- This indicator can take some time to "warm up" and stabilize -- due to the sliding window. @@ -50,11 +201,13 @@ import System.Clock (TimeSpec (..)) -- hence 'WC.getOccupancy' will never be @1@ if we use the current tick directly as -- obtained from the 'ticks' function. -- So we "rewind" it to the previous tick for the purpose of this display. -renderDutyCycle :: GameState -> Robot -> Widget Name -renderDutyCycle gs robot = - withAttr dutyCycleAttr . str . flip (showFFloat (Just 1)) "%" $ dutyCyclePercentage +renderDutyCycle :: TemporalState -> Robot -> WidthWidget +renderDutyCycle temporalState robot = + WidthWidget (length tx) (withAttr dutyCycleAttr $ str tx) where - curTicks = gs ^. temporal . ticks + tx = showFFloat (Just 1) dutyCyclePercentage "%" + + curTicks = temporalState ^. ticks window = robot ^. activityCounts . activityWindow -- Rewind to previous tick @@ -66,55 +219,60 @@ renderDutyCycle gs robot = dutyCyclePercentage :: Double dutyCyclePercentage = 100 * getValue dutyCycleRatio -robotsListWidget :: AppState -> Widget Name -robotsListWidget s = hCenter table +mkLibraryEntries :: RobotRenderingContext -> Seq RobotWidgetRow +mkLibraryEntries c = + mkRobotRow <$> S.fromList robots where - table = - BT.renderTable - . BT.columnBorders False - . BT.setDefaultColAlignment BT.AlignCenter - -- Inventory count is right aligned - . BT.alignRight 4 - . BT.table - $ map (padLeftRight 1) <$> (headers : robotsTable) - headings = - [ "Name" - , "Age" - , "Pos" - , "Items" - , "Status" - , "Actns" - , "Cmds" - , "Cycles" - , "Activity" - , "Log" - ] - headers = withAttr robotAttr . txt <$> applyWhen debugRID ("ID" :) headings - robotsTable = mkRobotRow <$> robots + basePos :: Point V2 Double + basePos = realToFrac <$> fromMaybe origin (g ^? baseRobot . robotLocation . planar) + -- Keep the base and non system robot (e.g. no seed) + isRelevant robot = robot ^. robotID == 0 || not (robot ^. systemRobot) + -- Keep the robot that are less than 32 unit away from the base + isNear robot = creative || distance (realToFrac <$> robot ^. robotLocation . planar) basePos < 32 + robots :: [Robot] + robots = + filter (\robot -> debugAllRobots || (isRelevant robot && isNear robot)) + . IM.elems + $ g ^. robotInfo . robotMap + creative = g ^. creativeMode + debugAllRobots = c ^. uiDbg . Lens.contains ListAllRobots + g = c ^. mygs + mkRobotRow robot = - applyWhen debugRID (idWidget :) cells + RobotRowPayload (robot ^. robotID) $ + LibRobotRow + { _fID = + let tx = show $ robot ^. robotID + in WidthWidget (length tx) (str tx) + , _fName = nameWidget + , _fAge = WidthWidget (length ageStr) (str ageStr) + , _fPos = locWidget + , _fItems = + let tx = show rInvCount + in WidthWidget (length tx) (padRight (Pad 1) (str tx)) + , _fStatus = statusWidget + , _fActns = + let tx = show $ robot ^. activityCounts . tangibleCommandCount + in WidthWidget (length tx) (str tx) + , -- TODO(#1341): May want to expose the details of this histogram in + -- a per-robot pop-up + _fCmds = strWidget $ show . sum . M.elems $ robot ^. activityCounts . commandsHistogram + , _fCycles = strWidget $ show $ robot ^. activityCounts . lifetimeStepCount + , _fActivity = renderDutyCycle (c ^. mygs . temporal) robot + , _fLog = WidthWidget (T.length rLog) (txt rLog) + } where - cells = - [ nameWidget - , str ageStr - , locWidget - , padRight (Pad 1) (str $ show rInvCount) - , statusWidget - , str $ show $ robot ^. activityCounts . tangibleCommandCount - , -- TODO(#1341): May want to expose the details of this histogram in - -- a per-robot pop-up - str . show . sum . M.elems $ robot ^. activityCounts . commandsHistogram - , str $ show $ robot ^. activityCounts . lifetimeStepCount - , renderDutyCycle (s ^. gameState) robot - , txt rLog - ] - - idWidget = str $ show $ robot ^. robotID + strWidget tx = WidthWidget (length tx) (str tx) + nameWidget = - hBox - [ renderDisplay (robot ^. robotDisplay) - , highlightSystem . txt $ " " <> robot ^. robotName - ] + WidthWidget (2 + T.length nameTxt) w + where + w = + hBox + [ renderDisplay (robot ^. robotDisplay) + , highlightSystem . txt $ " " <> nameTxt + ] + nameTxt = robot ^. robotName highlightSystem = if robot ^. systemRobot then withAttr highlightAttr else id @@ -125,7 +283,7 @@ robotsListWidget s = hCenter table | otherwise = show (age `div` 3600 * 24) <> "day" where TimeSpec createdAtSec _ = robot ^. robotCreatedAt - TimeSpec nowSec _ = s ^. uiState . uiGameplay . uiTiming . lastFrameTime + TimeSpec nowSec _ = c ^. timing . lastFrameTime age = nowSec - createdAtSec rInvCount = sum $ map fst . E.elems $ robot ^. robotEntity . entityInventory @@ -133,35 +291,27 @@ robotsListWidget s = hCenter table | robot ^. robotLogUpdated = "x" | otherwise = " " - locWidget = hBox [worldCell, str $ " " <> locStr] + locWidget = + WidthWidget (2 + length locStr) w where + w = hBox [worldCell, str $ " " <> locStr] rCoords = fmap locToCoords rLoc rLoc = robot ^. robotLocation worldCell = drawLoc - (s ^. uiState . uiGameplay) + (c ^. gameplay) g rCoords locStr = renderCoordsString rLoc statusWidget = case robot ^. machine of - Waiting {} -> txt "waiting" + Waiting {} -> + let tx = "waiting" + in WidthWidget (length tx) (str tx) _ - | isActive robot -> withAttr notifAttr $ txt "busy" - | otherwise -> withAttr greenAttr $ txt "idle" - - basePos :: Point V2 Double - basePos = realToFrac <$> fromMaybe origin (g ^? baseRobot . robotLocation . planar) - -- Keep the base and non system robot (e.g. no seed) - isRelevant robot = robot ^. robotID == 0 || not (robot ^. systemRobot) - -- Keep the robot that are less than 32 unit away from the base - isNear robot = creative || distance (realToFrac <$> robot ^. robotLocation . planar) basePos < 32 - robots :: [Robot] - robots = - filter (\robot -> debugAllRobots || (isRelevant robot && isNear robot)) - . IM.elems - $ g ^. robotInfo . robotMap - creative = g ^. creativeMode - debugRID = s ^. uiState . uiDebugOptions . Lens.contains ListRobotIDs - debugAllRobots = s ^. uiState . uiDebugOptions . Lens.contains ListAllRobots - g = s ^. gameState + | isActive robot -> + let tx = "busy" + in WidthWidget (length tx) (withAttr notifAttr $ str tx) + | otherwise -> + let tx = "idle" + in WidthWidget (length tx) (withAttr greenAttr $ str tx) diff --git a/src/swarm-tui/Swarm/TUI/View/RobotDisplay.hs b/src/swarm-tui/Swarm/TUI/View/RobotDisplay.hs new file mode 100644 index 000000000..0e5df06a7 --- /dev/null +++ b/src/swarm-tui/Swarm/TUI/View/RobotDisplay.hs @@ -0,0 +1,69 @@ +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE NoGeneralizedNewtypeDeriving #-} + +-- | +-- SPDX-License-Identifier: BSD-3-Clause +module Swarm.TUI.View.RobotDisplay where + +import Brick +import Brick.Widgets.TabularList.Mixed +import Control.Lens hiding (from, (<.>)) +import GHC.Generics (Generic) +import Swarm.Game.Robot +import Swarm.TUI.Model.Name + +data RobotRowPayload a = RobotRowPayload + { robID :: RID + , rPayload :: LibRobotRow a + } + deriving (Functor) + +data WidthWidget = WidthWidget + { wWidth :: Int + , wWidget :: Widget Name + } + +newtype Widths = Widths + { robotRowWidths :: [ColWidth] + } + deriving (Generic) + +type RobotWidgetRow = RobotRowPayload WidthWidget +type RobotHeaderRow = LibRobotRow String + +data LibRobotRow a = LibRobotRow + { _fID :: a + , _fName :: a + , _fAge :: a + , _fPos :: a + , _fItems :: a + , _fStatus :: a + , _fActns :: a + , _fCmds :: a + , _fCycles :: a + , _fActivity :: a + , _fLog :: a + } + deriving (Functor) + +data RobotsDisplayMode + = RobotList + | SingleRobotDetails + deriving (Eq, Show, Enum, Bounded) + +type LibraryList = MixedTabularList Name RobotWidgetRow Widths +type LibraryRenderers = MixedRenderers Name RobotWidgetRow Widths + +data RobotListContent = RobotListContent + { _libList :: LibraryList + , _libRenderers :: LibraryRenderers + } + +makeLenses ''RobotListContent + +data RobotDisplay = RobotDisplay + { _robotsDisplayMode :: RobotsDisplayMode + , _robotListContent :: RobotListContent + } + +makeLenses ''RobotDisplay diff --git a/src/swarm-tui/Swarm/TUI/View/Util.hs b/src/swarm-tui/Swarm/TUI/View/Util.hs index 63c3fe39e..87bdc4a9e 100644 --- a/src/swarm-tui/Swarm/TUI/View/Util.hs +++ b/src/swarm-tui/Swarm/TUI/View/Util.hs @@ -18,23 +18,23 @@ import Data.Text qualified as T import Graphics.Vty qualified as V import Swarm.Game.Entity as E import Swarm.Game.Land -import Swarm.Game.Location import Swarm.Game.Scenario (scenarioMetadata, scenarioName) import Swarm.Game.ScenarioInfo (scenarioItemName) import Swarm.Game.State import Swarm.Game.State.Landscape import Swarm.Game.State.Substate import Swarm.Game.Terrain -import Swarm.Game.Universe import Swarm.Language.Pretty (prettyTextLine) import Swarm.Language.Syntax (Syntax) import Swarm.Language.Text.Markdown qualified as Markdown import Swarm.Language.Types (Polytype) import Swarm.TUI.Model import Swarm.TUI.Model.Event (SwarmEvent) +import Swarm.TUI.Model.Name import Swarm.TUI.Model.UI import Swarm.TUI.View.Attribute.Attr import Swarm.TUI.View.CellDisplay +import Swarm.Util (maximum0) import Witch (from, into) -- | Generate a fresh modal window of the requested type. @@ -114,7 +114,7 @@ generateModal s mt = Modal mt (dialog (Just $ str title) buttons (maxModalWindow TerrainPaletteModal -> ("Terrain", Nothing, w) where tm = s ^. gameState . landscape . terrainAndEntities . terrainMap - wordLength = maximum $ map (T.length . getTerrainWord) (M.keys $ terrainByName tm) + wordLength = maximum0 $ map (T.length . getTerrainWord) (M.keys $ terrainByName tm) w = wordLength + 6 EntityPaletteModal -> ("Entity", Nothing, 30) @@ -188,10 +188,6 @@ quitMsg m = "Are you sure you want to " <> quitAction <> "? All progress on this NoMenu -> "quit" _ -> "return to the menu" -locationToString :: Location -> String -locationToString (Location x y) = - unwords $ map show [x, y] - -- | Display a list of text-wrapped paragraphs with one blank line after each. displayParagraphs :: [Text] -> Widget Name displayParagraphs = layoutParagraphs . map txtWrap @@ -256,11 +252,3 @@ bindingText s e = maybe "" ppBindingShort b Binding V.KLeft m | null m -> "←" Binding V.KRight m | null m -> "→" bi -> ppBinding bi - -renderCoordsString :: Cosmic Location -> String -renderCoordsString (Cosmic sw coords) = - unwords $ locationToString coords : suffix - where - suffix = case sw of - DefaultRootSubworld -> [] - SubworldName swName -> ["in", T.unpack swName] diff --git a/swarm.cabal b/swarm.cabal index 08967a931..4049f577b 100644 --- a/swarm.cabal +++ b/swarm.cabal @@ -152,6 +152,9 @@ common brick common brick-list-skip build-depends: brick-list-skip >=0.1.1.2 && <0.2 +common brick-tabular-list + build-depends: brick-tabular-list >=2.2.0 && <2.2.1 + common bytestring build-depends: bytestring >=0.10 && <0.13 @@ -955,6 +958,7 @@ library swarm-tui base, brick, brick-list-skip, + brick-tabular-list, bytestring, clock, colour, @@ -1026,6 +1030,7 @@ library swarm-tui Swarm.TUI.Model.Repl Swarm.TUI.Model.StateUpdate Swarm.TUI.Model.UI + Swarm.TUI.Model.UI.Gameplay Swarm.TUI.Model.WebCommand Swarm.TUI.Panel Swarm.TUI.View @@ -1038,6 +1043,7 @@ library swarm-tui Swarm.TUI.View.Objective Swarm.TUI.View.Popup Swarm.TUI.View.Robot + Swarm.TUI.View.RobotDisplay Swarm.TUI.View.Structure Swarm.TUI.View.Util