From b75438eef42e7091c527811026614edb0d0ee050 Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Thu, 5 Sep 2024 22:13:10 -0700 Subject: [PATCH 01/17] navigable table --- .hlint.yaml | 2 + app/game/Swarm/App.hs | 1 + src/swarm-doc/Swarm/Doc/Wiki/Cheatsheet.hs | 4 +- src/swarm-topography/Swarm/Game/Universe.hs | 15 + src/swarm-tui/Swarm/TUI/Controller.hs | 23 +- .../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 | 47 ++- 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 | 13 + src/swarm-tui/Swarm/TUI/Model/UI.hs | 211 +---------- src/swarm-tui/Swarm/TUI/Model/UI/Gameplay.hs | 226 ++++++++++++ src/swarm-tui/Swarm/TUI/View.hs | 20 +- 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 | 342 ++++++++++++++---- src/swarm-tui/Swarm/TUI/View/Robot/Details.hs | 74 ++++ src/swarm-tui/Swarm/TUI/View/Robot/Type.hs | 90 +++++ src/swarm-tui/Swarm/TUI/View/Util.hs | 18 +- src/swarm-util/Swarm/Util.hs | 11 + swarm.cabal | 8 + 30 files changed, 821 insertions(+), 305 deletions(-) create mode 100644 src/swarm-tui/Swarm/TUI/Model/UI/Gameplay.hs create mode 100644 src/swarm-tui/Swarm/TUI/View/Robot/Details.hs create mode 100644 src/swarm-tui/Swarm/TUI/View/Robot/Type.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 9db17ed77..477b21519 100644 --- a/src/swarm-doc/Swarm/Doc/Wiki/Cheatsheet.hs +++ b/src/swarm-doc/Swarm/Doc/Wiki/Cheatsheet.hs @@ -42,7 +42,7 @@ import Swarm.Language.Syntax (Const (..)) import Swarm.Language.Syntax qualified as Syntax import Swarm.Language.Text.Markdown as Markdown (docToMark) import Swarm.Language.Typecheck (inferConst) -import Swarm.Util (showT) +import Swarm.Util (maximum0, showT) -- * Types @@ -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..0ebd702ed 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,17 @@ defaultCosmicLocation = Cosmic DefaultRootSubworld origin offsetBy :: Cosmic Location -> V2 Int32 -> Cosmic Location offsetBy loc v = fmap (.+^ v) loc + +-- ** Rendering + +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..cb6a1c5f6 100644 --- a/src/swarm-tui/Swarm/TUI/Controller.hs +++ b/src/swarm-tui/Swarm/TUI/Controller.hs @@ -27,7 +27,6 @@ module Swarm.TUI.Controller ( ) where -- See Note [liftA2 re-export from Prelude] -import Prelude hiding (Applicative (..)) import Brick hiding (Direction, Location) import Brick.Focus @@ -36,10 +35,11 @@ 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 -import Control.Monad (unless, void, when) +import Control.Monad (forM_, unless, void, when) import Control.Monad.Extra (whenJust) import Control.Monad.IO.Class (MonadIO (liftIO)) import Control.Monad.State (MonadState, execState) @@ -87,7 +87,7 @@ import Swarm.Language.Value (Value (VKey), envTypes) import Swarm.Log import Swarm.TUI.Controller.EventHandlers import Swarm.TUI.Controller.SaveScenario (saveScenarioInfoOnQuit) -import Swarm.TUI.Controller.UpdateUI (updateAndRedrawUI) +import Swarm.TUI.Controller.UpdateUI import Swarm.TUI.Controller.Util import Swarm.TUI.Editor.Controller qualified as EC import Swarm.TUI.Editor.Model @@ -101,7 +101,10 @@ import Swarm.TUI.Model.Name import Swarm.TUI.Model.Repl import Swarm.TUI.Model.StateUpdate import Swarm.TUI.Model.UI +import Swarm.TUI.View.Robot (getList) +import Swarm.TUI.View.Robot.Type import Swarm.Util hiding (both, (<<.=)) +import Prelude hiding (Applicative (..)) -- ~~~~ Note [liftA2 re-export from Prelude] -- @@ -418,6 +421,20 @@ handleModalEvent = \case refreshList $ uiState . uiGameplay . uiDialogs . uiStructure . structurePanelListWidget StructureSummary -> handleInfoPanelEvent modalScroll (VtyEvent ev) _ -> handleInfoPanelEvent modalScroll (VtyEvent ev) + Just RobotsModal -> Brick.zoom (uiState . uiGameplay . uiDialogs . uiRobot) $ case ev of + V.EvKey (V.KChar '\t') [] -> robotDetailsFocus %= focusNext + _ -> do + foc <- use robotDetailsFocus + case focusGetCurrent foc of + (Just (RobotsListDialog (SingleRobotDetails RobotLogPane))) -> + Brick.zoom (robotListContent . robotDetailsPaneState . logsList) $ handleListEvent ev + _ -> do + Brick.zoom (robotListContent . robotsListWidget) $ + handleMixedListEvent ev + + -- Ensure list widget content is updated immediately + widget <- use $ robotListContent . robotsListWidget + forM_ (BL.listSelectedElement $ getList widget) $ updateRobotDetailsPane . snd _ -> 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 a2e30d42b..452079a01 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/UpdateUI.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/UpdateUI.hs @@ -7,24 +7,28 @@ module Swarm.TUI.Controller.UpdateUI ( updateUI, updateAndRedrawUI, + updateRobotDetailsPane, ) where -import Brick hiding (Direction, Location) -import Brick.Focus - -- See Note [liftA2 re-export from Prelude] +import Brick hiding (Direction, Location, on) +import Brick.Focus import Brick.Widgets.List qualified as BL import Control.Applicative (liftA2, pure) import Control.Lens as Lens -import Control.Monad (unless, when) +import Control.Monad (forM_, unless, when) import Control.Monad.IO.Class (liftIO) import Data.Foldable (toList) +import Data.Function (on) import Data.List.Extra (enumerate) +import Data.Map qualified as M import Data.Maybe (isNothing) import Data.String (fromString) import Data.Text qualified as T +import Data.Vector qualified as V import Swarm.Game.Entity hiding (empty) import Swarm.Game.Robot +import Swarm.Game.Robot.Activity import Swarm.Game.Robot.Concrete import Swarm.Game.State import Swarm.Game.State.Landscape @@ -43,6 +47,9 @@ 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.Robot.Type +import Swarm.Util (applyJust) import Witch (into) import Prelude hiding (Applicative (..)) @@ -165,6 +172,8 @@ updateUI = do newPopups <- generateNotificationPopups + doRobotListUpdate g + let redraw = g ^. needsRedraw || inventoryUpdated @@ -174,6 +183,36 @@ 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 + } + oldList = getList $ gp ^. uiDialogs . uiRobot . robotListContent . robotsListWidget + maybeOldSelected = snd <$> BL.listSelectedElement oldList + + maybeModificationFunc = + updateList . BL.listFindBy . ((==) `on` view (rob . robotID)) <$> maybeOldSelected + + uiState . uiGameplay . uiDialogs . uiRobot . robotListContent . robotsListWidget .= applyJust maybeModificationFunc rd + + Brick.zoom (uiState . uiGameplay . uiDialogs . uiRobot) $ + forM_ maybeOldSelected updateRobotDetailsPane + +updateRobotDetailsPane :: RobotWidgetRow -> EventM Name RobotDisplay () +updateRobotDetailsPane robotPayload = + Brick.zoom robotListContent $ do + robotDetailsPaneState . cmdHistogramList . BL.listElementsL .= V.fromList (M.toList (robotPayload ^. rob . activityCounts . commandsHistogram)) + robotDetailsPaneState . logsList . BL.listElementsL .= robotPayload ^. rob . robotLog + -- | 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 ce15747a6..dddceaaf4 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 ecb455ffb..c584fa26d 100644 --- a/src/swarm-tui/Swarm/TUI/Model/Name.hs +++ b/src/swarm-tui/Swarm/TUI/Model/Name.hs @@ -63,6 +63,17 @@ data Button | NextButton deriving (Eq, Ord, Show, Read, Bounded, Enum) +-- | Robot details +data RobotDetailSubpane + = RobotLogPane + | RobotCommandHistogramPane + deriving (Eq, Ord, Show, Read, Bounded, Enum) + +data RobotsDisplayMode + = RobotList + | SingleRobotDetails RobotDetailSubpane + deriving (Eq, Ord, Show, Read) + -- | 'Name' represents names to uniquely identify various components -- of the UI, such as forms, panels, caches, extents, lists, and buttons. data Name @@ -106,6 +117,8 @@ data Name StructureWidgets StructureWidget | -- | The list of scenario choices. ScenarioList + | -- | The robots list + RobotsListDialog RobotsDisplayMode | -- | 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 72ef5d12f..47ff02492 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, uiAutoShowObjectives, @@ -63,29 +64,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 @@ -95,203 +88,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.Robot.Type 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 - , _uiAutoShowObjectives :: 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@ the progress will not be saved. -uiIsAutoPlay :: Lens' UIGameplay Bool - --- | Do not open objectives modals on objective completion. -uiAutoShowObjectives :: 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 @@ -405,6 +209,11 @@ initUIState UIInitOptions {..} = do { _uiModal = Nothing , _uiGoal = emptyGoalDisplay , _uiStructure = emptyStructureDisplay + , _uiRobot = + RobotDisplay + { _robotDetailsFocus = focusRing $ map RobotsListDialog $ RobotList : map SingleRobotDetails enumerate + , _robotListContent = emptyRobotDisplay debugOptions + } } , _uiIsAutoPlay = False , _uiAutoShowObjectives = autoShowObjectives 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..820199d00 --- /dev/null +++ b/src/swarm-tui/Swarm/TUI/Model/UI/Gameplay.hs @@ -0,0 +1,226 @@ +{-# 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.Robot.Type +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 + , _uiAutoShowObjectives :: 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@ the progress will not be saved. +uiIsAutoPlay :: Lens' UIGameplay Bool + +-- | Do not open objectives modals on objective completion. +uiAutoShowObjectives :: 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 3201fc56c..ebe87c853 100644 --- a/src/swarm-tui/Swarm/TUI/View.hs +++ b/src/swarm-tui/Swarm/TUI/View.hs @@ -129,6 +129,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 @@ -139,6 +140,8 @@ 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.Robot.Details +import Swarm.TUI.View.Robot.Type import Swarm.TUI.View.Structure qualified as SR import Swarm.TUI.View.Util as VU import Swarm.Util @@ -377,7 +380,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 @@ -606,6 +609,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 @@ -613,7 +617,17 @@ 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 focusGetCurrent rFocusRing of + Just (RobotsListDialog (SingleRobotDetails _)) -> case maybeSelectedRobot of + Nothing -> str "No selection" + Just r -> renderRobotDetails rFocusRing r $ robotDialog ^. robotListContent . robotDetailsPaneState + where + oldList = getList $ robotDialog ^. robotListContent . robotsListWidget + maybeSelectedRobot = view rob . snd <$> BL.listSelectedElement oldList + _ -> renderRobotsList $ robotDialog ^. robotListContent + where + robotDialog = s ^. uiState . uiGameplay . uiDialogs . uiRobot + rFocusRing = robotDialog ^. robotDetailsFocus RecipesModal -> availableListWidget (s ^. gameState) RecipeList CommandsModal -> commandsListWidget (s ^. gameState) MessagesModal -> availableListWidget (s ^. gameState) MessageList @@ -689,7 +703,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..bc1d4d3e4 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot.hs @@ -1,5 +1,8 @@ +{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE NoGeneralizedNewtypeDeriving #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} -- | -- SPDX-License-Identifier: BSD-3-Clause @@ -7,14 +10,25 @@ -- 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.List qualified as BL +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 (mapAccumL) +import Data.List.NonEmpty (NonEmpty ((:|))) +import Data.List.NonEmpty qualified as NE 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 +42,223 @@ 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.Robot.Type +import Swarm.Util (applyWhen, maximum0, maximumNE) import Swarm.Util.UnitInterval import Swarm.Util.WindowedCounter qualified as WC import System.Clock (TimeSpec (..)) +extractColWidth :: ColWidth -> Int +extractColWidth (ColW x) = x + +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 -> MixedTabularList Name RobotWidgetRow Widths +mkRobotDisplay c = + mixedTabularList + (RobotsListDialog RobotList) + (mkLibraryEntries c) + (LstItmH 1) + (computeColumnWidths $ c ^. uiDbg) + assignRowWidthForConstructor + +emptyRobotDisplay :: Set DebugOption -> RobotListContent +emptyRobotDisplay uiDebug = + RobotListContent + { _robotsListWidget = mixedTabularList (RobotsListDialog RobotList) mempty (LstItmH 1) (computeColumnWidths uiDebug) assignRowWidthForConstructor + , _robotsListRenderers = + MixedRenderers + { cell = drawCell uiDebug + , rowHdr = Just rowHdr + , colHdr = Just $ colHdr uiDebug + , colHdrRowHdr = Just $ ColHdrRowHdr $ \_ _ -> vLimit 1 (fill ' ') <=> hBorder + } + , _robotDetailsPaneState = + RobotDetailsPaneState + { _logsList = BL.list (RobotsListDialog $ SingleRobotDetails RobotLogPane) mempty 1 + , _cmdHistogramList = BL.list (RobotsListDialog $ SingleRobotDetails RobotCommandHistogramPane) mempty 1 + } + } + +renderRobotsList :: RobotListContent -> Widget Name +renderRobotsList rd = + vLimit 30 $ + renderMixedTabularList (rd ^. robotsListRenderers) (LstFcs True) (rd ^. robotsListWidget) + +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 (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 + } + +data ColumnExpansion + = Grow + | Minimal + deriving (Eq) + +data ColumnAttributes = Col + { headingString :: String + , expansionPolicy :: ColumnExpansion + } + +getColumnAttrList :: Set DebugOption -> NonEmpty ColumnAttributes +getColumnAttrList dbgOptions = + NE.map ($ headingStrings) $ getAccessorList dbgOptions + where + headingStrings = + LibRobotRow + { _fID = Col "ID" Minimal + , _fName = Col "Name" Grow + , _fAge = Col "Age" Minimal + , _fPos = Col "Pos" Minimal + , _fItems = Col "Items" Minimal + , _fStatus = Col "Status" Minimal + , _fActns = Col "Actns" Minimal + , _fCmds = Col "Cmds" Minimal + , _fCycles = Col "Cycles" Minimal + , _fActivity = Col "Activity" Grow + , _fLog = Col "Log" Minimal + } + +colHdrs :: Set DebugOption -> Vector String +colHdrs = + V.fromList + . NE.toList + . NE.map headingString + . getColumnAttrList + +getAccessorList :: Set DebugOption -> NonEmpty (LibRobotRow a -> a) +getAccessorList dbgOptions = + applyWhen debugRID (NE.cons _fID) mainListSuffix + where + debugRID = dbgOptions ^. Lens.contains ListRobotIDs + + mainListSuffix = + _fName + :| [ _fAge + , _fPos + , _fItems + , _fStatus + , _fActns + , _fCmds + , _fCycles + , _fActivity + , _fLog + ] + +drawCell :: Set DebugOption -> ListFocused -> MixedCtxt -> RobotWidgetRow -> Widget Name +drawCell uiDebug _ (MxdCtxt _ (MColC (Ix ci))) r = + maybe emptyWidget (renderPlainCell . wWidget . ($ view rPayload r)) (indexedAccessors V.!? ci) + where + indexedAccessors = V.fromList $ NE.toList accessors + accessors = getAccessorList uiDebug + renderPlainCell = padRight Max + +-- | For a single-constructor datatype like 'RobotWidgetRow', +-- this implementation is trivial. +assignRowWidthForConstructor :: WidthsPerRow RobotWidgetRow Widths +assignRowWidthForConstructor = WsPerR $ \(Widths x) _ -> x + +-- | +-- First, computes the minimum width for each column, using +-- both the header string width, the widest visible cell content, +-- and adding 1 to the result of each column for padding. +-- +-- Second, to utilize the full available width for the table, distributes +-- the extra space equally among columns marked as 'Grow'. +computeColumnWidths :: Set DebugOption -> WidthsPerRowKind RobotWidgetRow Widths +computeColumnWidths uiDebug = WsPerRK $ \availableWidth allRows -> + let output = maybe [] (NE.toList . distributeWidths availableWidth) $ NE.nonEmpty allRows + in Widths {robotRowWidths = output} + where + distributeWidths (AvlW availableWidth) allRows = + NE.zipWith (\(ColW w) extra -> ColW $ w + extra) minWidthsPerColum distributedRemainderSpace + where + minWidthsPerColum = mkWidths allRows + totalRequiredWidth = sum $ NE.map extractColWidth minWidthsPerColum + spareWidth = availableWidth - totalRequiredWidth + + growPolicies = NE.map expansionPolicy colAttrList + growableColumnCount = length $ filter (== Grow) $ NE.toList growPolicies + (spacePerGrowable, remainingSpace) = spareWidth `divMod` growableColumnCount + + distributedRemainderSpace = snd $ mapAccumL addedWidth remainingSpace growPolicies + addedWidth remainder policy = case policy of + Grow -> (remainder - 1, spacePerGrowable + extra) + where + extra = fromEnum $ remainder > 0 + Minimal -> (remainder, 0) + + colAttrList = getColumnAttrList uiDebug + colHeaderRowLengths = NE.map (length . headingString) colAttrList + + -- We take the maximum of all cell widths, including the headers, and + -- add 1 for "padding". + -- NOTE: We don't necessarily need to pad the last column, but it's + -- simpler this way and it looks fine. + mkWidths :: NonEmpty RobotWidgetRow -> NonEmpty ColWidth + mkWidths = + NE.map (ColW . (+ 1) . maximumNE) + . NE.transpose + . (colHeaderRowLengths `NE.cons`) + . NE.map getColWidthsForRow + where + getColWidthsForRow :: RobotWidgetRow -> NonEmpty Int + getColWidthsForRow r = NE.map (wWidth . ($ view rPayload r)) $ getAccessorList uiDebug + +getList :: MixedTabularList n e w -> BL.GenericList n Seq e +getList (MixedTabularList oldList _ _) = oldList + +updateList :: + (BL.GenericList n1 Seq e -> BL.GenericList n2 Seq e) -> + MixedTabularList n1 e w -> + MixedTabularList n2 e w +updateList f (MixedTabularList ls a b) = MixedTabularList (f ls) a b + +strWidget :: String -> WidthWidget +strWidget tx = WidthPrecompute (length tx) (str tx) + -- | 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 +270,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 = + withAttr dutyCycleAttr <$> strWidget tx where - curTicks = gs ^. temporal . ticks + tx = showFFloat (Just 1) dutyCyclePercentage "%" + + curTicks = temporalState ^. ticks window = robot ^. activityCounts . activityWindow -- Rewind to previous tick @@ -66,55 +288,34 @@ 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 mkRobotRow robot = - applyWhen debugRID (idWidget :) cells + RobotRowPayload robot $ + LibRobotRow + { _fID = strWidget $ show $ robot ^. robotID + , _fName = nameWidget + , _fAge = strWidget ageStr + , _fPos = locWidget + , _fItems = padWidth 1 $ strWidget $ show rInvCount + , _fStatus = statusWidget + , _fActns = strWidget $ show $ robot ^. activityCounts . tangibleCommandCount + , _fCmds = strWidget $ show . sum . M.elems $ robot ^. activityCounts . commandsHistogram + , _fCycles = strWidget $ show $ robot ^. activityCounts . lifetimeStepCount + , _fActivity = renderDutyCycle (c ^. mygs . temporal) robot + , _fLog = strWidget $ pure 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 - nameWidget = - hBox - [ renderDisplay (robot ^. robotDisplay) - , highlightSystem . txt $ " " <> robot ^. robotName - ] + nameWidget = WidthPrecompute (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,30 +326,32 @@ 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 rLog - | robot ^. robotLogUpdated = "x" - | otherwise = " " + | robot ^. robotLogUpdated = 'x' + | otherwise = ' ' - locWidget = hBox [worldCell, str $ " " <> locStr] + locWidget = + WidthPrecompute (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 {} -> strWidget "waiting" _ - | isActive robot -> withAttr notifAttr $ txt "busy" - | otherwise -> withAttr greenAttr $ txt "idle" + | isActive robot -> withAttr notifAttr <$> strWidget "busy" + | otherwise -> withAttr greenAttr <$> strWidget "idle" basePos :: Point V2 Double basePos = realToFrac <$> fromMaybe origin (g ^? baseRobot . robotLocation . planar) @@ -162,6 +365,5 @@ robotsListWidget s = hCenter table . 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 + debugAllRobots = c ^. uiDbg . Lens.contains ListAllRobots + g = c ^. mygs diff --git a/src/swarm-tui/Swarm/TUI/View/Robot/Details.hs b/src/swarm-tui/Swarm/TUI/View/Robot/Details.hs new file mode 100644 index 000000000..88f75e697 --- /dev/null +++ b/src/swarm-tui/Swarm/TUI/View/Robot/Details.hs @@ -0,0 +1,74 @@ +-- | +-- SPDX-License-Identifier: BSD-3-Clause +module Swarm.TUI.View.Robot.Details (renderRobotDetails) where + +import Brick +import Brick.Widgets.Border +import Brick.Widgets.Center (hCenter) +import Brick.Widgets.List qualified as BL +import Brick.Widgets.Table qualified as BT + +import Brick.Focus +import Control.Lens hiding (from, (<.>)) +import Data.Map qualified as M +import Prettyprinter (pretty) +import Swarm.Game.Robot +import Swarm.Game.Robot.Activity (commandsHistogram) +import Swarm.Game.Robot.Concrete +import Swarm.Language.Pretty (prettyText) +import Swarm.Log +import Swarm.TUI.Model.Name +import Swarm.TUI.View.Attribute.Attr (boldAttr, cyanAttr) +import Swarm.TUI.View.Robot.Type + +renderRobotDetails :: FocusRing Name -> Robot -> RobotDetailsPaneState -> Widget Name +renderRobotDetails ring robot paneState = + vBox + [ str $ + unwords + [ "Selected robot" + , show $ view robotName robot + ] + , hBorder + , str " " + , hBox $ + map + hCenter + [ hLimitPercent 70 $ highlightBorderFor RobotLogPane $ borderWithLabel (str "Logs") logsTable + , hLimitPercent 30 $ highlightBorderFor RobotCommandHistogramPane $ borderWithLabel (str "Commands") commandsTable + ] + ] + where + highlightBorderFor n = + if isFocused then overrideAttr borderAttr cyanAttr else id + where + isFocused = focusGetCurrent ring == Just (RobotsListDialog $ SingleRobotDetails n) + + logsTable = withFocusRing ring (BL.renderList mkLogTableEntry) $ paneState ^. logsList + + mkLogTableEntry _isSelected x = + hBox + [ withAttr cyanAttr . str . show . pretty . view leTime $ x + , str ": " + , txt . view leText $ x + ] + + commandsTable = + BT.renderTable + . BT.columnBorders True + . BT.rowBorders False + . BT.surroundingBorder False + . BT.setDefaultColAlignment BT.AlignLeft + . BT.setColAlignment BT.AlignRight 0 + . BT.table + $ map (withAttr boldAttr . str) ["Command", "Count"] : commandHistogramEntries + + mkHistogramEntry (k, v) = + [ txt $ prettyText k + , str $ show v + ] + + commandHistogramEntries = + map mkHistogramEntry $ + M.toList $ + robot ^. activityCounts . commandsHistogram diff --git a/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs b/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs new file mode 100644 index 000000000..3aa51480d --- /dev/null +++ b/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs @@ -0,0 +1,90 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- | +-- SPDX-License-Identifier: BSD-3-Clause +module Swarm.TUI.View.Robot.Type where + +import Brick (Widget) +import Brick.Focus (FocusRing) +import Brick.Widgets.List qualified as BL +import Brick.Widgets.TabularList.Mixed +import Control.Lens hiding (Const, from, (<.>)) +import Data.Sequence (Seq) +import GHC.Generics (Generic) +import Swarm.Game.Robot +import Swarm.Language.Syntax (Const) +import Swarm.Log +import Swarm.TUI.Model.Name + +-- | It is desirable to store the a priori known width of widgets +-- based on their text length or other fixed properties, and +-- use this as the type of the Brick list. +-- +-- We can't defer this width computation to the "draw" function of a cell, +-- because we need access to all cell widths independently +-- outside of the cell draw function. +data WidthPrecompute a = WidthPrecompute + { wWidth :: Int + , wWidget :: a + } + deriving (Functor) + +type WidthWidget = WidthPrecompute (Widget Name) + +padWidth :: Int -> WidthPrecompute a -> WidthPrecompute a +padWidth extra (WidthPrecompute w x) = WidthPrecompute (w + extra) x + +newtype Widths = Widths + { robotRowWidths :: [ColWidth] + } + deriving (Generic) + +type RobotWidgetRow = RobotRowPayload WidthWidget + +-- | This type is parameterized such that the same +-- collection of fields can specify both +-- cell widgets and column headings +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 RobotRowPayload a = RobotRowPayload + { _rob :: Robot + , _rPayload :: LibRobotRow a + } + deriving (Functor) + +makeLenses ''RobotRowPayload + +data RobotDetailsPaneState = RobotDetailsPaneState + { _logsList :: BL.GenericList Name Seq LogEntry + , _cmdHistogramList :: BL.List Name (Const, Int) + } + +makeLenses ''RobotDetailsPaneState + +data RobotListContent = RobotListContent + { _robotsListWidget :: MixedTabularList Name RobotWidgetRow Widths + , _robotsListRenderers :: MixedRenderers Name RobotWidgetRow Widths + , _robotDetailsPaneState :: RobotDetailsPaneState + } + +makeLenses ''RobotListContent + +data RobotDisplay = RobotDisplay + { _robotDetailsFocus :: FocusRing Name + , _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/src/swarm-util/Swarm/Util.hs b/src/swarm-util/Swarm/Util.hs index 6f4ae556f..e3d928708 100644 --- a/src/swarm-util/Swarm/Util.hs +++ b/src/swarm-util/Swarm/Util.hs @@ -13,6 +13,7 @@ module Swarm.Util ( sortPair, maxOn, maximum0, + maximumNE, enumeratedMap, cycleEnum, enumerateNonEmpty, @@ -29,6 +30,7 @@ module Swarm.Util ( prependList, deleteKeys, applyWhen, + applyJust, hoistMaybe, unsnocNE, @@ -146,6 +148,11 @@ maximum0 :: (Num a, Ord a) => [a] -> a maximum0 [] = 0 maximum0 xs = maximum xs +-- | NOTE: We should be able to just use 'maximum' from "Data.Foldable1" +-- but it is not available for ghc 9.2 and 9.4. +maximumNE :: (Num a, Ord a) => NonEmpty a -> a +maximumNE = maximum + enumeratedMap :: Int -> [a] -> IntMap a enumeratedMap startIdx = IM.fromList . zip [startIdx ..] @@ -273,6 +280,10 @@ applyWhen :: Bool -> (a -> a) -> a -> a applyWhen True f x = f x applyWhen False _ x = x +applyJust :: Maybe (a -> a) -> a -> a +applyJust Nothing x = x +applyJust (Just f) x = f x + -- | Convert a 'Maybe' computation to 'MaybeT'. -- -- TODO (#1151): Use implementation from "transformers" package v0.6.0.0 diff --git a/swarm.cabal b/swarm.cabal index ba9d40f38..fab381f95 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 @@ -959,8 +962,10 @@ library swarm-tui base, brick, brick-list-skip, + brick-tabular-list, bytestring, clock, + prettyprinter, colour, containers, extra, @@ -1031,6 +1036,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 @@ -1043,6 +1049,8 @@ library swarm-tui Swarm.TUI.View.Objective Swarm.TUI.View.Popup Swarm.TUI.View.Robot + Swarm.TUI.View.Robot.Details + Swarm.TUI.View.Robot.Type Swarm.TUI.View.Structure Swarm.TUI.View.Util From db135d88804b75f79339cd89fe4682a30208560e Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Fri, 13 Sep 2024 22:33:15 -0700 Subject: [PATCH 02/17] cleanups --- src/swarm-tui/Swarm/TUI/Controller/UpdateUI.hs | 2 ++ src/swarm-tui/Swarm/TUI/View/Robot.hs | 10 +++++----- src/swarm-tui/Swarm/TUI/View/Robot/Type.hs | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/swarm-tui/Swarm/TUI/Controller/UpdateUI.hs b/src/swarm-tui/Swarm/TUI/Controller/UpdateUI.hs index 452079a01..fc40c7c25 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/UpdateUI.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/UpdateUI.hs @@ -199,6 +199,8 @@ doRobotListUpdate g = do oldList = getList $ gp ^. uiDialogs . uiRobot . robotListContent . robotsListWidget maybeOldSelected = snd <$> BL.listSelectedElement oldList + -- Since we're replacing the entire contents of the list, we need to preserve the + -- selected row here. maybeModificationFunc = updateList . BL.listFindBy . ((==) `on` view (rob . robotID)) <$> maybeOldSelected diff --git a/src/swarm-tui/Swarm/TUI/View/Robot.hs b/src/swarm-tui/Swarm/TUI/View/Robot.hs index bc1d4d3e4..ffc491018 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot.hs @@ -147,7 +147,7 @@ getColumnAttrList dbgOptions = NE.map ($ headingStrings) $ getAccessorList dbgOptions where headingStrings = - LibRobotRow + RobotRow { _fID = Col "ID" Minimal , _fName = Col "Name" Grow , _fAge = Col "Age" Minimal @@ -168,7 +168,7 @@ colHdrs = . NE.map headingString . getColumnAttrList -getAccessorList :: Set DebugOption -> NonEmpty (LibRobotRow a -> a) +getAccessorList :: Set DebugOption -> NonEmpty (RobotRow a -> a) getAccessorList dbgOptions = applyWhen debugRID (NE.cons _fID) mainListSuffix where @@ -202,8 +202,8 @@ assignRowWidthForConstructor = WsPerR $ \(Widths x) _ -> x -- | -- First, computes the minimum width for each column, using --- both the header string width, the widest visible cell content, --- and adding 1 to the result of each column for padding. +-- both the header string width and the widest visible cell content, +-- then adding 1 to the result of each column for padding. -- -- Second, to utilize the full available width for the table, distributes -- the extra space equally among columns marked as 'Grow'. @@ -294,7 +294,7 @@ mkLibraryEntries c = where mkRobotRow robot = RobotRowPayload robot $ - LibRobotRow + RobotRow { _fID = strWidget $ show $ robot ^. robotID , _fName = nameWidget , _fAge = strWidget ageStr diff --git a/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs b/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs index 3aa51480d..0714982b9 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs @@ -44,7 +44,7 @@ type RobotWidgetRow = RobotRowPayload WidthWidget -- | This type is parameterized such that the same -- collection of fields can specify both -- cell widgets and column headings -data LibRobotRow a = LibRobotRow +data RobotRow a = RobotRow { _fID :: a , _fName :: a , _fAge :: a @@ -61,7 +61,7 @@ data LibRobotRow a = LibRobotRow data RobotRowPayload a = RobotRowPayload { _rob :: Robot - , _rPayload :: LibRobotRow a + , _rPayload :: RobotRow a } deriving (Functor) From 93fd1a58c6ba107fa565a15a0386ddfda7cf3e68 Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Fri, 13 Sep 2024 22:37:00 -0700 Subject: [PATCH 03/17] use applyJust/applyWhen more --- .../Swarm/Game/State/Initialize.hs | 18 ++++++++---------- src/swarm-web/Swarm/Web.hs | 5 ++--- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/swarm-engine/Swarm/Game/State/Initialize.hs b/src/swarm-engine/Swarm/Game/State/Initialize.hs index ff0816e03..f1274185c 100644 --- a/src/swarm-engine/Swarm/Game/State/Initialize.hs +++ b/src/swarm-engine/Swarm/Game/State/Initialize.hs @@ -55,7 +55,7 @@ import Swarm.Game.World.Gen (Seed) import Swarm.Language.Capability (constCaps) import Swarm.Language.Syntax (allConst, erase) import Swarm.Language.Types -import Swarm.Util (binTuples, (?)) +import Swarm.Util (applyJust, applyWhen, binTuples, (?)) import System.Clock qualified as Clock import System.Random (mkStdGen) @@ -137,20 +137,18 @@ pureScenarioToGameState scenario theSeed now toRun gsc = -- of the scenario description). & ix baseID . machine - %~ case initialCodeToRun of - Nothing -> id - Just t -> const $ initMachine t + %~ applyJust (const . initMachine <$> initialCodeToRun) -- If we are in creative mode, give base all the things & ix baseID . robotInventory - %~ case scenario ^. scenarioOperation . scenarioCreative of - False -> id - True -> union (fromElems (map (0,) things)) + %~ applyWhen + (scenario ^. scenarioOperation . scenarioCreative) + (union (fromElems (map (0,) things))) & ix baseID . equippedDevices - %~ case scenario ^. scenarioOperation . scenarioCreative of - False -> id - True -> const (fromList devices) + %~ applyWhen + (scenario ^. scenarioOperation . scenarioCreative) + (const (fromList devices)) running = case robotList of [] -> False diff --git a/src/swarm-web/Swarm/Web.hs b/src/swarm-web/Swarm/Web.hs index 9150939f5..3ae52b377 100644 --- a/src/swarm-web/Swarm/Web.hs +++ b/src/swarm-web/Swarm/Web.hs @@ -81,6 +81,7 @@ import Swarm.TUI.Model hiding (SwarmKeyDispatchers (..)) import Swarm.TUI.Model.Dialog.Goal import Swarm.TUI.Model.Repl (REPLHistItem, replHistory, replSeq) import Swarm.TUI.Model.UI +import Swarm.Util (applyJust) import Swarm.Util.RingBuffer import Swarm.Web.Worldview import System.Timeout (timeout) @@ -312,9 +313,7 @@ webMain :: webMain baton port appStateRef chan = catch (Warp.runSettings settings app) handleErr where settings = Warp.setPort port $ onReady Warp.defaultSettings - onReady = case baton of - Just mv -> Warp.setBeforeMainLoop $ putMVar mv WebStarted - Nothing -> id + onReady = applyJust $ Warp.setBeforeMainLoop . flip putMVar WebStarted <$> baton server :: Server ToplevelAPI server = From 8f8f87af1406f807de55c9e6da85d6b865d7bfcb Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Sat, 14 Sep 2024 12:54:55 -0700 Subject: [PATCH 04/17] preserve re-export of Name --- app/game/Swarm/App.hs | 1 - src/swarm-tui/Swarm/TUI/Controller/EventHandlers.hs | 1 - src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Frame.hs | 1 - src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Main.hs | 1 - src/swarm-tui/Swarm/TUI/Controller/EventHandlers/REPL.hs | 1 - src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Robot.hs | 1 - src/swarm-tui/Swarm/TUI/Controller/EventHandlers/World.hs | 1 - src/swarm-tui/Swarm/TUI/Model.hs | 1 + src/swarm-tui/Swarm/TUI/Model/KeyBindings.hs | 1 - src/swarm-tui/Swarm/TUI/View/Achievement.hs | 1 - src/swarm-tui/Swarm/TUI/View/Robot.hs | 6 +++--- src/swarm-tui/Swarm/TUI/View/Robot/Type.hs | 8 ++++---- src/swarm-tui/Swarm/TUI/View/Util.hs | 1 - 13 files changed, 8 insertions(+), 17 deletions(-) diff --git a/app/game/Swarm/App.hs b/app/game/Swarm/App.hs index c7edf9a6f..ad4b4974c 100644 --- a/app/game/Swarm/App.hs +++ b/app/game/Swarm/App.hs @@ -38,7 +38,6 @@ 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-tui/Swarm/TUI/Controller/EventHandlers.hs b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers.hs index 8d875c02d..f35e99aeb 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers.hs @@ -46,7 +46,6 @@ 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 e52d2a6ac..73cc9b86e 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Frame.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Frame.hs @@ -26,7 +26,6 @@ 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 ada85975b..7bf59d637 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Main.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Main.hs @@ -26,7 +26,6 @@ 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 275d920c0..b6b28c5b9 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/REPL.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/REPL.hs @@ -21,7 +21,6 @@ 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 c6c456518..b2a0bcefb 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Robot.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Robot.hs @@ -31,7 +31,6 @@ 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 aa4968aaf..b4dbe921a 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/World.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/World.hs @@ -22,7 +22,6 @@ 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/Model.hs b/src/swarm-tui/Swarm/TUI/Model.hs index dddceaaf4..245892ff4 100644 --- a/src/swarm-tui/Swarm/TUI/Model.hs +++ b/src/swarm-tui/Swarm/TUI/Model.hs @@ -13,6 +13,7 @@ module Swarm.TUI.Model ( -- $uilabel AppEvent (..), FocusablePanel (..), + Name (..), -- helps to minimize import lines -- ** Web command WebCommand (..), diff --git a/src/swarm-tui/Swarm/TUI/Model/KeyBindings.hs b/src/swarm-tui/Swarm/TUI/Model/KeyBindings.hs index 8c48523a4..dc47c69c5 100644 --- a/src/swarm-tui/Swarm/TUI/Model/KeyBindings.hs +++ b/src/swarm-tui/Swarm/TUI/Model/KeyBindings.hs @@ -28,7 +28,6 @@ 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/View/Achievement.hs b/src/swarm-tui/Swarm/TUI/View/Achievement.hs index cd1765516..47f4a9b6a 100644 --- a/src/swarm-tui/Swarm/TUI/View/Achievement.hs +++ b/src/swarm-tui/Swarm/TUI/View/Achievement.hs @@ -16,7 +16,6 @@ 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/Robot.hs b/src/swarm-tui/Swarm/TUI/View/Robot.hs index ffc491018..0b50ed3cc 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot.hs @@ -257,7 +257,7 @@ updateList :: updateList f (MixedTabularList ls a b) = MixedTabularList (f ls) a b strWidget :: String -> WidthWidget -strWidget tx = WidthPrecompute (length tx) (str tx) +strWidget tx = WithWidth (length tx) (str tx) -- | Render the percentage of ticks that this robot was active. -- This indicator can take some time to "warm up" and stabilize @@ -308,7 +308,7 @@ mkLibraryEntries c = , _fLog = strWidget $ pure rLog } where - nameWidget = WidthPrecompute (2 + T.length nameTxt) w + nameWidget = WithWidth (2 + T.length nameTxt) w where w = hBox @@ -335,7 +335,7 @@ mkLibraryEntries c = | otherwise = ' ' locWidget = - WidthPrecompute (2 + length locStr) w + WithWidth (2 + length locStr) w where w = hBox [worldCell, str $ " " <> locStr] rCoords = fmap locToCoords rLoc diff --git a/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs b/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs index 0714982b9..2efebf243 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs @@ -23,16 +23,16 @@ import Swarm.TUI.Model.Name -- We can't defer this width computation to the "draw" function of a cell, -- because we need access to all cell widths independently -- outside of the cell draw function. -data WidthPrecompute a = WidthPrecompute +data WithWidth a = WithWidth { wWidth :: Int , wWidget :: a } deriving (Functor) -type WidthWidget = WidthPrecompute (Widget Name) +type WidthWidget = WithWidth (Widget Name) -padWidth :: Int -> WidthPrecompute a -> WidthPrecompute a -padWidth extra (WidthPrecompute w x) = WidthPrecompute (w + extra) x +padWidth :: Int -> WithWidth a -> WithWidth a +padWidth extra (WithWidth w x) = WithWidth (w + extra) x newtype Widths = Widths { robotRowWidths :: [ColWidth] diff --git a/src/swarm-tui/Swarm/TUI/View/Util.hs b/src/swarm-tui/Swarm/TUI/View/Util.hs index 87bdc4a9e..b4d2e63cb 100644 --- a/src/swarm-tui/Swarm/TUI/View/Util.hs +++ b/src/swarm-tui/Swarm/TUI/View/Util.hs @@ -30,7 +30,6 @@ 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 From ba1d997ab72659b7d413c38abfa76e92ff15ff6c Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Sat, 14 Sep 2024 12:58:15 -0700 Subject: [PATCH 05/17] explicit export list for Swarm.TUI.View.Robot --- src/swarm-tui/Swarm/TUI/View/Robot.hs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/swarm-tui/Swarm/TUI/View/Robot.hs b/src/swarm-tui/Swarm/TUI/View/Robot.hs index 0b50ed3cc..8eca80887 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot.hs @@ -8,7 +8,15 @@ -- SPDX-License-Identifier: BSD-3-Clause -- -- A UI-centric model for presentation of Robot details. -module Swarm.TUI.View.Robot where +module Swarm.TUI.View.Robot ( + emptyRobotDisplay, + RobotRenderingContext (..), + mkRobotDisplay, + getList, + updateList, + renderRobotsList, + renderDutyCycle, +) where import Brick import Brick.Widgets.Border From 9a300a8d9ebd1e75191bae4e373ab25d8ea3f2d3 Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Sat, 14 Sep 2024 13:00:09 -0700 Subject: [PATCH 06/17] add module docstring --- src/swarm-tui/Swarm/TUI/View/Robot/Details.hs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/swarm-tui/Swarm/TUI/View/Robot/Details.hs b/src/swarm-tui/Swarm/TUI/View/Robot/Details.hs index 88f75e697..44b119aad 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot/Details.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot/Details.hs @@ -1,5 +1,7 @@ -- | -- SPDX-License-Identifier: BSD-3-Clause +-- +-- Rendering of the "details" pane of the F2 robots dialog module Swarm.TUI.View.Robot.Details (renderRobotDetails) where import Brick From 19469cf23a9282ba02a7e5f5f0a20167fe227880 Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Sat, 14 Sep 2024 13:02:29 -0700 Subject: [PATCH 07/17] rename _rob lens to _robot --- .../Swarm/TUI/Controller/UpdateUI.hs | 6 +-- src/swarm-tui/Swarm/TUI/View.hs | 6 +-- src/swarm-tui/Swarm/TUI/View/Robot.hs | 42 +++++++++---------- src/swarm-tui/Swarm/TUI/View/Robot/Details.hs | 6 +-- src/swarm-tui/Swarm/TUI/View/Robot/Type.hs | 2 +- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/swarm-tui/Swarm/TUI/Controller/UpdateUI.hs b/src/swarm-tui/Swarm/TUI/Controller/UpdateUI.hs index fc40c7c25..59c034dc6 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/UpdateUI.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/UpdateUI.hs @@ -202,7 +202,7 @@ doRobotListUpdate g = do -- Since we're replacing the entire contents of the list, we need to preserve the -- selected row here. maybeModificationFunc = - updateList . BL.listFindBy . ((==) `on` view (rob . robotID)) <$> maybeOldSelected + updateList . BL.listFindBy . ((==) `on` view (robot . robotID)) <$> maybeOldSelected uiState . uiGameplay . uiDialogs . uiRobot . robotListContent . robotsListWidget .= applyJust maybeModificationFunc rd @@ -212,8 +212,8 @@ doRobotListUpdate g = do updateRobotDetailsPane :: RobotWidgetRow -> EventM Name RobotDisplay () updateRobotDetailsPane robotPayload = Brick.zoom robotListContent $ do - robotDetailsPaneState . cmdHistogramList . BL.listElementsL .= V.fromList (M.toList (robotPayload ^. rob . activityCounts . commandsHistogram)) - robotDetailsPaneState . logsList . BL.listElementsL .= robotPayload ^. rob . robotLog + robotDetailsPaneState . cmdHistogramList . BL.listElementsL .= V.fromList (M.toList (robotPayload ^. robot . activityCounts . commandsHistogram)) + robotDetailsPaneState . logsList . BL.listElementsL .= robotPayload ^. robot . robotLog -- | Either pops up the updated Goals modal -- or pops up the Congratulations (Win) modal, or pops diff --git a/src/swarm-tui/Swarm/TUI/View.hs b/src/swarm-tui/Swarm/TUI/View.hs index ebe87c853..0ae972edd 100644 --- a/src/swarm-tui/Swarm/TUI/View.hs +++ b/src/swarm-tui/Swarm/TUI/View.hs @@ -511,7 +511,7 @@ drawWorldCursorInfo worldEditor g cCoords = Cosmic _ coords = cCoords coordsWidget = str $ renderCoordsString $ fmap coordsToLoc cCoords - tileMembers = terrain : mapMaybe merge [entity, robot] + tileMembers = terrain : mapMaybe merge [entity, r] tileMemberWidgets = map (padRight $ Pad 1) . concat @@ -529,7 +529,7 @@ drawWorldCursorInfo worldEditor g cCoords = terrain = displayTerrainCell worldEditor ri cCoords entity = displayEntityCell worldEditor ri cCoords - robot = displayRobotCell g cCoords + r = displayRobotCell g cCoords merge = fmap sconcat . NE.nonEmpty . filter (not . (^. invisible)) @@ -623,7 +623,7 @@ drawModal s = \case Just r -> renderRobotDetails rFocusRing r $ robotDialog ^. robotListContent . robotDetailsPaneState where oldList = getList $ robotDialog ^. robotListContent . robotsListWidget - maybeSelectedRobot = view rob . snd <$> BL.listSelectedElement oldList + maybeSelectedRobot = view robot . snd <$> BL.listSelectedElement oldList _ -> renderRobotsList $ robotDialog ^. robotListContent where robotDialog = s ^. uiState . uiGameplay . uiDialogs . uiRobot diff --git a/src/swarm-tui/Swarm/TUI/View/Robot.hs b/src/swarm-tui/Swarm/TUI/View/Robot.hs index 8eca80887..be26ecd6d 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot.hs @@ -279,13 +279,13 @@ strWidget tx = WithWidth (length tx) (str tx) -- obtained from the 'ticks' function. -- So we "rewind" it to the previous tick for the purpose of this display. renderDutyCycle :: TemporalState -> Robot -> WidthWidget -renderDutyCycle temporalState robot = +renderDutyCycle temporalState r = withAttr dutyCycleAttr <$> strWidget tx where tx = showFFloat (Just 1) dutyCyclePercentage "%" curTicks = temporalState ^. ticks - window = robot ^. activityCounts . activityWindow + window = r ^. activityCounts . activityWindow -- Rewind to previous tick latestRobotTick = addTicks (-1) curTicks @@ -300,19 +300,19 @@ mkLibraryEntries :: RobotRenderingContext -> Seq RobotWidgetRow mkLibraryEntries c = mkRobotRow <$> S.fromList robots where - mkRobotRow robot = - RobotRowPayload robot $ + mkRobotRow r = + RobotRowPayload r $ RobotRow - { _fID = strWidget $ show $ robot ^. robotID + { _fID = strWidget $ show $ r ^. robotID , _fName = nameWidget , _fAge = strWidget ageStr , _fPos = locWidget , _fItems = padWidth 1 $ strWidget $ show rInvCount , _fStatus = statusWidget - , _fActns = strWidget $ show $ robot ^. activityCounts . tangibleCommandCount - , _fCmds = strWidget $ show . sum . M.elems $ robot ^. activityCounts . commandsHistogram - , _fCycles = strWidget $ show $ robot ^. activityCounts . lifetimeStepCount - , _fActivity = renderDutyCycle (c ^. mygs . temporal) robot + , _fActns = strWidget $ show $ r ^. activityCounts . tangibleCommandCount + , _fCmds = strWidget $ show . sum . M.elems $ r ^. activityCounts . commandsHistogram + , _fCycles = strWidget $ show $ r ^. activityCounts . lifetimeStepCount + , _fActivity = renderDutyCycle (c ^. mygs . temporal) r , _fLog = strWidget $ pure rLog } where @@ -320,12 +320,12 @@ mkLibraryEntries c = where w = hBox - [ renderDisplay (robot ^. robotDisplay) + [ renderDisplay (r ^. robotDisplay) , highlightSystem . txt $ " " <> nameTxt ] - nameTxt = robot ^. robotName + nameTxt = r ^. robotName - highlightSystem = if robot ^. systemRobot then withAttr highlightAttr else id + highlightSystem = if r ^. systemRobot then withAttr highlightAttr else id ageStr | age < 60 = show age <> "sec" @@ -333,13 +333,13 @@ mkLibraryEntries c = | age < 3600 * 24 = show (age `div` 3600) <> "hour" | otherwise = show (age `div` 3600 * 24) <> "day" where - TimeSpec createdAtSec _ = robot ^. robotCreatedAt + TimeSpec createdAtSec _ = r ^. robotCreatedAt TimeSpec nowSec _ = c ^. timing . lastFrameTime age = nowSec - createdAtSec - rInvCount = sum $ map fst . E.elems $ robot ^. robotEntity . entityInventory + rInvCount = sum $ map fst . E.elems $ r ^. robotEntity . entityInventory rLog - | robot ^. robotLogUpdated = 'x' + | r ^. robotLogUpdated = 'x' | otherwise = ' ' locWidget = @@ -347,7 +347,7 @@ mkLibraryEntries c = where w = hBox [worldCell, str $ " " <> locStr] rCoords = fmap locToCoords rLoc - rLoc = robot ^. robotLocation + rLoc = r ^. robotLocation worldCell = drawLoc (c ^. gameplay) @@ -355,21 +355,21 @@ mkLibraryEntries c = rCoords locStr = renderCoordsString rLoc - statusWidget = case robot ^. machine of + statusWidget = case r ^. machine of Waiting {} -> strWidget "waiting" _ - | isActive robot -> withAttr notifAttr <$> strWidget "busy" + | isActive r -> withAttr notifAttr <$> strWidget "busy" | otherwise -> withAttr greenAttr <$> strWidget "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) + isRelevant r = r ^. robotID == 0 || not (r ^. systemRobot) -- Keep the robot that are less than 32 unit away from the base - isNear robot = creative || distance (realToFrac <$> robot ^. robotLocation . planar) basePos < 32 + isNear r = creative || distance (realToFrac <$> r ^. robotLocation . planar) basePos < 32 robots :: [Robot] robots = - filter (\robot -> debugAllRobots || (isRelevant robot && isNear robot)) + filter (\r -> debugAllRobots || (isRelevant r && isNear r)) . IM.elems $ g ^. robotInfo . robotMap creative = g ^. creativeMode diff --git a/src/swarm-tui/Swarm/TUI/View/Robot/Details.hs b/src/swarm-tui/Swarm/TUI/View/Robot/Details.hs index 44b119aad..77c03028a 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot/Details.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot/Details.hs @@ -24,12 +24,12 @@ import Swarm.TUI.View.Attribute.Attr (boldAttr, cyanAttr) import Swarm.TUI.View.Robot.Type renderRobotDetails :: FocusRing Name -> Robot -> RobotDetailsPaneState -> Widget Name -renderRobotDetails ring robot paneState = +renderRobotDetails ring r paneState = vBox [ str $ unwords [ "Selected robot" - , show $ view robotName robot + , show $ view robotName r ] , hBorder , str " " @@ -73,4 +73,4 @@ renderRobotDetails ring robot paneState = commandHistogramEntries = map mkHistogramEntry $ M.toList $ - robot ^. activityCounts . commandsHistogram + r ^. activityCounts . commandsHistogram diff --git a/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs b/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs index 2efebf243..12f38b7df 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs @@ -60,7 +60,7 @@ data RobotRow a = RobotRow deriving (Functor) data RobotRowPayload a = RobotRowPayload - { _rob :: Robot + { _robot :: Robot , _rPayload :: RobotRow a } deriving (Functor) From a37214a7415c9b33a5df6b6126f36513bb3cd68a Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Sat, 14 Sep 2024 13:25:45 -0700 Subject: [PATCH 08/17] rename _rPayload lens to _row --- src/swarm-tui/Swarm/TUI/View.hs | 4 ++-- src/swarm-tui/Swarm/TUI/View/Robot.hs | 4 ++-- src/swarm-tui/Swarm/TUI/View/Robot/Type.hs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/swarm-tui/Swarm/TUI/View.hs b/src/swarm-tui/Swarm/TUI/View.hs index 0ae972edd..975c7db59 100644 --- a/src/swarm-tui/Swarm/TUI/View.hs +++ b/src/swarm-tui/Swarm/TUI/View.hs @@ -1034,14 +1034,14 @@ drawRobotPanel s | Just r <- s ^. gameState . to focusedRobot , Just (_, lst) <- s ^. uiState . uiGameplay . uiInventory . uiInventoryList = let drawClickableItem pos selb = clickable (InventoryListItem pos) . drawItem (lst ^. BL.listSelectedL) pos selb - row = + details = [ txt (r ^. robotName) , padLeft (Pad 2) . str . renderCoordsString $ r ^. robotLocation , padLeft (Pad 2) $ renderDisplay (r ^. robotDisplay) ] in padBottom Max $ vBox - [ hCenter $ hBox row + [ hCenter $ hBox details , withLeftPaddedVScrollBars . padLeft (Pad 1) . padTop (Pad 1) $ BL.renderListWithIndex drawClickableItem True lst ] diff --git a/src/swarm-tui/Swarm/TUI/View/Robot.hs b/src/swarm-tui/Swarm/TUI/View/Robot.hs index be26ecd6d..a775e1562 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot.hs @@ -197,7 +197,7 @@ getAccessorList dbgOptions = drawCell :: Set DebugOption -> ListFocused -> MixedCtxt -> RobotWidgetRow -> Widget Name drawCell uiDebug _ (MxdCtxt _ (MColC (Ix ci))) r = - maybe emptyWidget (renderPlainCell . wWidget . ($ view rPayload r)) (indexedAccessors V.!? ci) + maybe emptyWidget (renderPlainCell . wWidget . ($ view row r)) (indexedAccessors V.!? ci) where indexedAccessors = V.fromList $ NE.toList accessors accessors = getAccessorList uiDebug @@ -253,7 +253,7 @@ computeColumnWidths uiDebug = WsPerRK $ \availableWidth allRows -> . NE.map getColWidthsForRow where getColWidthsForRow :: RobotWidgetRow -> NonEmpty Int - getColWidthsForRow r = NE.map (wWidth . ($ view rPayload r)) $ getAccessorList uiDebug + getColWidthsForRow r = NE.map (wWidth . ($ view row r)) $ getAccessorList uiDebug getList :: MixedTabularList n e w -> BL.GenericList n Seq e getList (MixedTabularList oldList _ _) = oldList diff --git a/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs b/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs index 12f38b7df..44712b312 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs @@ -61,7 +61,7 @@ data RobotRow a = RobotRow data RobotRowPayload a = RobotRowPayload { _robot :: Robot - , _rPayload :: RobotRow a + , _row :: RobotRow a } deriving (Functor) From f771ade1fa0713f87d177fdbb5c760a89f80d884 Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Sat, 14 Sep 2024 13:47:22 -0700 Subject: [PATCH 09/17] comment on and rename padWidth --- src/swarm-tui/Swarm/TUI/View/Robot.hs | 2 +- src/swarm-tui/Swarm/TUI/View/Robot/Type.hs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/swarm-tui/Swarm/TUI/View/Robot.hs b/src/swarm-tui/Swarm/TUI/View/Robot.hs index a775e1562..8ddc69d47 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot.hs @@ -307,7 +307,7 @@ mkLibraryEntries c = , _fName = nameWidget , _fAge = strWidget ageStr , _fPos = locWidget - , _fItems = padWidth 1 $ strWidget $ show rInvCount + , _fItems = increaseWidth 1 $ strWidget $ show rInvCount , _fStatus = statusWidget , _fActns = strWidget $ show $ r ^. activityCounts . tangibleCommandCount , _fCmds = strWidget $ show . sum . M.elems $ r ^. activityCounts . commandsHistogram diff --git a/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs b/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs index 44712b312..f618999ce 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs @@ -31,8 +31,10 @@ data WithWidth a = WithWidth type WidthWidget = WithWidth (Widget Name) -padWidth :: Int -> WithWidth a -> WithWidth a -padWidth extra (WithWidth w x) = WithWidth (w + extra) x +-- | For left-aligned cell content, this has the effect +-- of right-padding +increaseWidth :: Int -> WithWidth a -> WithWidth a +increaseWidth extra (WithWidth w x) = WithWidth (w + extra) x newtype Widths = Widths { robotRowWidths :: [ColWidth] From 1df475b61b7b8047faa5bd7cbe73d322127c70bd Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Sat, 14 Sep 2024 13:50:50 -0700 Subject: [PATCH 10/17] remove orphan Semigroup instance --- src/swarm-tui/Swarm/TUI/View/Robot.hs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/swarm-tui/Swarm/TUI/View/Robot.hs b/src/swarm-tui/Swarm/TUI/View/Robot.hs index 8ddc69d47..80ce19cc3 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot.hs @@ -2,7 +2,6 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE NoGeneralizedNewtypeDeriving #-} -{-# OPTIONS_GHC -fno-warn-orphans #-} -- | -- SPDX-License-Identifier: BSD-3-Clause @@ -64,8 +63,8 @@ import System.Clock (TimeSpec (..)) extractColWidth :: ColWidth -> Int extractColWidth (ColW x) = x -instance Semigroup ColWidth where - ColW w1 <> ColW w2 = ColW $ max w1 w2 +getMaxWidth :: ColWidth -> ColWidth -> ColWidth +getMaxWidth (ColW w1) (ColW w2) = ColW $ max w1 w2 data RobotRenderingContext = RobotRenderingContext { _mygs :: GameState @@ -120,7 +119,7 @@ colHdr uiDebug = { draw = \_ (MColC (Ix ci)) -> case hdrs V.!? ci of Just ch -> withAttr columnHdrAttr (str ch) <=> hBorder Nothing -> emptyWidget - , widths = \(Widths ws) -> zipWith (<>) ws (map (ColW . length) $ V.toList hdrs) + , widths = \(Widths ws) -> zipWith getMaxWidth ws (map (ColW . length) $ V.toList hdrs) , height = ColHdrH 2 } where From a263b112ce14988f308485c48287e27b174c0e15 Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Sat, 14 Sep 2024 14:16:19 -0700 Subject: [PATCH 11/17] Add instructions for Tab control --- src/swarm-tui/Swarm/TUI/View.hs | 15 +------------- src/swarm-tui/Swarm/TUI/View/Robot.hs | 24 ++++++++++++++++++++++- src/swarm-tui/Swarm/TUI/View/Shared.hs | 15 ++++++++++++++ src/swarm-tui/Swarm/TUI/View/Structure.hs | 4 ++-- swarm.cabal | 1 + 5 files changed, 42 insertions(+), 17 deletions(-) create mode 100644 src/swarm-tui/Swarm/TUI/View/Shared.hs diff --git a/src/swarm-tui/Swarm/TUI/View.hs b/src/swarm-tui/Swarm/TUI/View.hs index 975c7db59..e9fd068be 100644 --- a/src/swarm-tui/Swarm/TUI/View.hs +++ b/src/swarm-tui/Swarm/TUI/View.hs @@ -129,7 +129,6 @@ 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 @@ -140,8 +139,6 @@ 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.Robot.Details -import Swarm.TUI.View.Robot.Type import Swarm.TUI.View.Structure qualified as SR import Swarm.TUI.View.Util as VU import Swarm.Util @@ -617,17 +614,7 @@ 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 -> case focusGetCurrent rFocusRing of - Just (RobotsListDialog (SingleRobotDetails _)) -> case maybeSelectedRobot of - Nothing -> str "No selection" - Just r -> renderRobotDetails rFocusRing r $ robotDialog ^. robotListContent . robotDetailsPaneState - where - oldList = getList $ robotDialog ^. robotListContent . robotsListWidget - maybeSelectedRobot = view robot . snd <$> BL.listSelectedElement oldList - _ -> renderRobotsList $ robotDialog ^. robotListContent - where - robotDialog = s ^. uiState . uiGameplay . uiDialogs . uiRobot - rFocusRing = robotDialog ^. robotDetailsFocus + RobotsModal -> drawRobotsModal $ s ^. uiState . uiGameplay . uiDialogs . uiRobot RecipesModal -> availableListWidget (s ^. gameState) RecipeList CommandsModal -> commandsListWidget (s ^. gameState) MessagesModal -> availableListWidget (s ^. gameState) MessageList diff --git a/src/swarm-tui/Swarm/TUI/View/Robot.hs b/src/swarm-tui/Swarm/TUI/View/Robot.hs index 80ce19cc3..43919dd29 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot.hs @@ -13,11 +13,13 @@ module Swarm.TUI.View.Robot ( mkRobotDisplay, getList, updateList, - renderRobotsList, + -- renderRobotsList, renderDutyCycle, + drawRobotsModal, ) where import Brick +import Brick.Focus import Brick.Widgets.Border import Brick.Widgets.List qualified as BL import Brick.Widgets.TabularList.Mixed @@ -54,7 +56,9 @@ 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.Robot.Details import Swarm.TUI.View.Robot.Type +import Swarm.TUI.View.Shared (tabControlFooter) import Swarm.Util (applyWhen, maximum0, maximumNE) import Swarm.Util.UnitInterval import Swarm.Util.WindowedCounter qualified as WC @@ -374,3 +378,21 @@ mkLibraryEntries c = creative = g ^. creativeMode debugAllRobots = c ^. uiDbg . Lens.contains ListAllRobots g = c ^. mygs + +drawRobotsModal :: RobotDisplay -> Widget Name +drawRobotsModal robotDialog = + vBox + [ mainContent + , tabControlFooter + ] + where + rFocusRing = robotDialog ^. robotDetailsFocus + + mainContent = case focusGetCurrent rFocusRing of + Just (RobotsListDialog (SingleRobotDetails _)) -> case maybeSelectedRobot of + Nothing -> str "No selection" + Just r -> renderRobotDetails rFocusRing r $ robotDialog ^. robotListContent . robotDetailsPaneState + where + oldList = getList $ robotDialog ^. robotListContent . robotsListWidget + maybeSelectedRobot = view robot . snd <$> BL.listSelectedElement oldList + _ -> renderRobotsList $ robotDialog ^. robotListContent diff --git a/src/swarm-tui/Swarm/TUI/View/Shared.hs b/src/swarm-tui/Swarm/TUI/View/Shared.hs new file mode 100644 index 000000000..69af0d026 --- /dev/null +++ b/src/swarm-tui/Swarm/TUI/View/Shared.hs @@ -0,0 +1,15 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- | +-- SPDX-License-Identifier: BSD-3-Clause +-- +-- UI view components shared across dialogs +module Swarm.TUI.View.Shared where + +import Brick +import Brick.Widgets.Center (hCenter) +import Swarm.TUI.Model.Name +import Swarm.TUI.View.Attribute.Attr (italicAttr) + +tabControlFooter :: Widget Name +tabControlFooter = hCenter $ withAttr italicAttr $ txt "NOTE: [Tab] toggles focus between panes" diff --git a/src/swarm-tui/Swarm/TUI/View/Structure.hs b/src/swarm-tui/Swarm/TUI/View/Structure.hs index 11a903ee3..d93d5d680 100644 --- a/src/swarm-tui/Swarm/TUI/View/Structure.hs +++ b/src/swarm-tui/Swarm/TUI/View/Structure.hs @@ -36,6 +36,7 @@ import Swarm.TUI.Model.Dialog.Structure import Swarm.TUI.Model.Name import Swarm.TUI.View.Attribute.Attr import Swarm.TUI.View.CellDisplay +import Swarm.TUI.View.Shared (tabControlFooter) import Swarm.TUI.View.Util import Swarm.Util (commaList) @@ -133,10 +134,9 @@ renderStructuresDisplay gs structureDisplay = [ leftSide , padLeft (Pad 2) structureElaboration ] - , footer + , tabControlFooter ] where - footer = hCenter $ withAttr italicAttr $ txt "NOTE: [Tab] toggles focus between panes" lw = _structurePanelListWidget structureDisplay fr = _structurePanelFocus structureDisplay leftSide = diff --git a/swarm.cabal b/swarm.cabal index fab381f95..313440bca 100644 --- a/swarm.cabal +++ b/swarm.cabal @@ -1051,6 +1051,7 @@ library swarm-tui Swarm.TUI.View.Robot Swarm.TUI.View.Robot.Details Swarm.TUI.View.Robot.Type + Swarm.TUI.View.Shared Swarm.TUI.View.Structure Swarm.TUI.View.Util From e7101b5c56ea7c4dd7fdfb9f3677123da0c0688a Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Sat, 14 Sep 2024 15:51:30 -0700 Subject: [PATCH 12/17] rename 'RobotRow' fields --- src/swarm-tui/Swarm/TUI/View/Robot.hs | 66 +++++++++++----------- src/swarm-tui/Swarm/TUI/View/Robot/Type.hs | 22 ++++---- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/swarm-tui/Swarm/TUI/View/Robot.hs b/src/swarm-tui/Swarm/TUI/View/Robot.hs index 43919dd29..16fc576b1 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot.hs @@ -159,17 +159,17 @@ getColumnAttrList dbgOptions = where headingStrings = RobotRow - { _fID = Col "ID" Minimal - , _fName = Col "Name" Grow - , _fAge = Col "Age" Minimal - , _fPos = Col "Pos" Minimal - , _fItems = Col "Items" Minimal - , _fStatus = Col "Status" Minimal - , _fActns = Col "Actns" Minimal - , _fCmds = Col "Cmds" Minimal - , _fCycles = Col "Cycles" Minimal - , _fActivity = Col "Activity" Grow - , _fLog = Col "Log" Minimal + { rowID = Col "ID" Minimal + , rowName = Col "Name" Grow + , rowAge = Col "Age" Minimal + , rowPos = Col "Pos" Minimal + , rowItems = Col "Items" Minimal + , rowStatus = Col "Status" Minimal + , rowActns = Col "Actns" Minimal + , rowCmds = Col "Cmds" Minimal + , rowCycles = Col "Cycles" Minimal + , rowActivity = Col "Activity" Grow + , rowLog = Col "Log" Minimal } colHdrs :: Set DebugOption -> Vector String @@ -181,21 +181,21 @@ colHdrs = getAccessorList :: Set DebugOption -> NonEmpty (RobotRow a -> a) getAccessorList dbgOptions = - applyWhen debugRID (NE.cons _fID) mainListSuffix + applyWhen debugRID (NE.cons rowID) mainListSuffix where debugRID = dbgOptions ^. Lens.contains ListRobotIDs mainListSuffix = - _fName - :| [ _fAge - , _fPos - , _fItems - , _fStatus - , _fActns - , _fCmds - , _fCycles - , _fActivity - , _fLog + rowName + :| [ rowAge + , rowPos + , rowItems + , rowStatus + , rowActns + , rowCmds + , rowCycles + , rowActivity + , rowLog ] drawCell :: Set DebugOption -> ListFocused -> MixedCtxt -> RobotWidgetRow -> Widget Name @@ -306,17 +306,17 @@ mkLibraryEntries c = mkRobotRow r = RobotRowPayload r $ RobotRow - { _fID = strWidget $ show $ r ^. robotID - , _fName = nameWidget - , _fAge = strWidget ageStr - , _fPos = locWidget - , _fItems = increaseWidth 1 $ strWidget $ show rInvCount - , _fStatus = statusWidget - , _fActns = strWidget $ show $ r ^. activityCounts . tangibleCommandCount - , _fCmds = strWidget $ show . sum . M.elems $ r ^. activityCounts . commandsHistogram - , _fCycles = strWidget $ show $ r ^. activityCounts . lifetimeStepCount - , _fActivity = renderDutyCycle (c ^. mygs . temporal) r - , _fLog = strWidget $ pure rLog + { rowID = strWidget $ show $ r ^. robotID + , rowName = nameWidget + , rowAge = strWidget ageStr + , rowPos = locWidget + , rowItems = increaseWidth 1 $ strWidget $ show rInvCount + , rowStatus = statusWidget + , rowActns = strWidget $ show $ r ^. activityCounts . tangibleCommandCount + , rowCmds = strWidget $ show . sum . M.elems $ r ^. activityCounts . commandsHistogram + , rowCycles = strWidget $ show $ r ^. activityCounts . lifetimeStepCount + , rowActivity = renderDutyCycle (c ^. mygs . temporal) r + , rowLog = strWidget $ pure rLog } where nameWidget = WithWidth (2 + T.length nameTxt) w diff --git a/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs b/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs index f618999ce..d3e7ed443 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs @@ -47,17 +47,17 @@ type RobotWidgetRow = RobotRowPayload WidthWidget -- collection of fields can specify both -- cell widgets and column headings data RobotRow a = RobotRow - { _fID :: a - , _fName :: a - , _fAge :: a - , _fPos :: a - , _fItems :: a - , _fStatus :: a - , _fActns :: a - , _fCmds :: a - , _fCycles :: a - , _fActivity :: a - , _fLog :: a + { rowID :: a + , rowName :: a + , rowAge :: a + , rowPos :: a + , rowItems :: a + , rowStatus :: a + , rowActns :: a + , rowCmds :: a + , rowCycles :: a + , rowActivity :: a + , rowLog :: a } deriving (Functor) From 04cacae259e3f9990fed36117a5fd59f77e593d4 Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Sat, 14 Sep 2024 15:58:07 -0700 Subject: [PATCH 13/17] add comment to ColumnExpansion --- src/swarm-tui/Swarm/TUI/View/Robot.hs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/swarm-tui/Swarm/TUI/View/Robot.hs b/src/swarm-tui/Swarm/TUI/View/Robot.hs index 16fc576b1..4d2d1f97c 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot.hs @@ -143,6 +143,10 @@ rowHdr = , toRH = \_ (Ix i) -> i + 1 } +-- | Note that with the current two constructors, this happens to be analogous +-- to 'Brick.Types.Size'. However, we'd like to reserve the right to add +-- more constructors/growth modes and not be bound by the Brick semantics, so +-- we have our own definition here. data ColumnExpansion = Grow | Minimal From 2c063cf4ba917bd0fd45d2ff8d4f833c773c229d Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Sat, 14 Sep 2024 16:14:30 -0700 Subject: [PATCH 14/17] explicit export list and docstring for Swarm.TUI.Model.UI.Gameplay --- src/swarm-tui/Swarm/TUI/Controller.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/SaveScenario.hs | 1 + .../Swarm/TUI/Controller/UpdateUI.hs | 1 + src/swarm-tui/Swarm/TUI/Controller/Util.hs | 3 ++ src/swarm-tui/Swarm/TUI/Editor/Controller.hs | 1 + src/swarm-tui/Swarm/TUI/Editor/View.hs | 1 + src/swarm-tui/Swarm/TUI/Model.hs | 1 + src/swarm-tui/Swarm/TUI/Model/StateUpdate.hs | 1 + src/swarm-tui/Swarm/TUI/Model/UI.hs | 42 +---------------- src/swarm-tui/Swarm/TUI/Model/UI/Gameplay.hs | 45 ++++++++++++++++++- src/swarm-tui/Swarm/TUI/View.hs | 1 + src/swarm-tui/Swarm/TUI/View/Util.hs | 1 + src/swarm-web/Swarm/Web.hs | 1 + 18 files changed, 64 insertions(+), 41 deletions(-) diff --git a/src/swarm-tui/Swarm/TUI/Controller.hs b/src/swarm-tui/Swarm/TUI/Controller.hs index cb6a1c5f6..a2dfec1af 100644 --- a/src/swarm-tui/Swarm/TUI/Controller.hs +++ b/src/swarm-tui/Swarm/TUI/Controller.hs @@ -101,6 +101,7 @@ import Swarm.TUI.Model.Name import Swarm.TUI.Model.Repl import Swarm.TUI.Model.StateUpdate import Swarm.TUI.Model.UI +import Swarm.TUI.Model.UI.Gameplay import Swarm.TUI.View.Robot (getList) import Swarm.TUI.View.Robot.Type import Swarm.Util hiding (both, (<<.=)) diff --git a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Frame.hs b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Frame.hs index 73cc9b86e..1e161e57d 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Frame.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Frame.hs @@ -27,6 +27,7 @@ import Swarm.TUI.Controller.Util import Swarm.TUI.Model import Swarm.TUI.Model.Achievements (popupAchievement) import Swarm.TUI.Model.UI +import Swarm.TUI.Model.UI.Gameplay import System.Clock ticksPerFrameCap :: Int diff --git a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Main.hs b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Main.hs index 7bf59d637..8f958d443 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Main.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Main.hs @@ -27,6 +27,7 @@ import Swarm.TUI.Model.DebugOption (DebugOption (ToggleCreative, ToggleWorldEdit import Swarm.TUI.Model.Dialog.Goal import Swarm.TUI.Model.Event (MainEvent (..), SwarmEvent (..)) import Swarm.TUI.Model.UI +import Swarm.TUI.Model.UI.Gameplay import System.Clock (Clock (..), TimeSpec (..), getTime) -- | Main keybindings event handler while running the game itself. diff --git a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/REPL.hs b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/REPL.hs index b6b28c5b9..c83b3e7df 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/REPL.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/REPL.hs @@ -23,6 +23,7 @@ import Swarm.TUI.Model import Swarm.TUI.Model.Event import Swarm.TUI.Model.Repl import Swarm.TUI.Model.UI +import Swarm.TUI.Model.UI.Gameplay -- | Handle a user input key event for the REPL. -- diff --git a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Robot.hs b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Robot.hs index b2a0bcefb..116018dc4 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Robot.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Robot.hs @@ -32,6 +32,7 @@ import Swarm.TUI.List import Swarm.TUI.Model import Swarm.TUI.Model.Event import Swarm.TUI.Model.UI +import Swarm.TUI.Model.UI.Gameplay import Swarm.TUI.View.Util (generateModal) -- | Handle user input events in the robot panel. diff --git a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/World.hs b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/World.hs index b4dbe921a..65cf9800a 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/World.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/EventHandlers/World.hs @@ -23,6 +23,7 @@ import Swarm.TUI.Controller.Util import Swarm.TUI.Model import Swarm.TUI.Model.Event import Swarm.TUI.Model.UI +import Swarm.TUI.Model.UI.Gameplay -- | Handle a user input event in the world view panel. worldEventHandlers :: [KeyEventHandler SwarmEvent (EventM Name AppState)] diff --git a/src/swarm-tui/Swarm/TUI/Controller/SaveScenario.hs b/src/swarm-tui/Swarm/TUI/Controller/SaveScenario.hs index 26f6b451e..2ba201111 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/SaveScenario.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/SaveScenario.hs @@ -26,6 +26,7 @@ import Swarm.TUI.Model import Swarm.TUI.Model.Achievements (attainAchievement') import Swarm.TUI.Model.Repl import Swarm.TUI.Model.UI +import Swarm.TUI.Model.UI.Gameplay import System.FilePath (splitDirectories) import Prelude hiding (Applicative (..)) diff --git a/src/swarm-tui/Swarm/TUI/Controller/UpdateUI.hs b/src/swarm-tui/Swarm/TUI/Controller/UpdateUI.hs index 59c034dc6..9c6f4645f 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/UpdateUI.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/UpdateUI.hs @@ -46,6 +46,7 @@ import Swarm.TUI.Model.Dialog.Popup (Popup (..), addPopup) import Swarm.TUI.Model.Name import Swarm.TUI.Model.Repl import Swarm.TUI.Model.UI +import Swarm.TUI.Model.UI.Gameplay import Swarm.TUI.View.Objective qualified as GR import Swarm.TUI.View.Robot import Swarm.TUI.View.Robot.Type diff --git a/src/swarm-tui/Swarm/TUI/Controller/Util.hs b/src/swarm-tui/Swarm/TUI/Controller/Util.hs index 2f257d705..0ad84b013 100644 --- a/src/swarm-tui/Swarm/TUI/Controller/Util.hs +++ b/src/swarm-tui/Swarm/TUI/Controller/Util.hs @@ -2,6 +2,8 @@ -- | -- SPDX-License-Identifier: BSD-3-Clause +-- +-- Keyboard key event patterns and drawing utilities module Swarm.TUI.Controller.Util where import Brick hiding (Direction) @@ -43,6 +45,7 @@ import Swarm.TUI.Model ( 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.Model.UI.Gameplay import Swarm.TUI.View.Util (generateModal) import System.Clock (Clock (..), getTime) diff --git a/src/swarm-tui/Swarm/TUI/Editor/Controller.hs b/src/swarm-tui/Swarm/TUI/Editor/Controller.hs index e52e4b666..46b2cd537 100644 --- a/src/swarm-tui/Swarm/TUI/Editor/Controller.hs +++ b/src/swarm-tui/Swarm/TUI/Editor/Controller.hs @@ -29,6 +29,7 @@ import Swarm.TUI.Editor.Util qualified as EU import Swarm.TUI.Model import Swarm.TUI.Model.Name import Swarm.TUI.Model.UI +import Swarm.TUI.Model.UI.Gameplay import Swarm.Util (hoistMaybe) import Swarm.Util.Erasable (maybeToErasable) import System.Clock diff --git a/src/swarm-tui/Swarm/TUI/Editor/View.hs b/src/swarm-tui/Swarm/TUI/Editor/View.hs index 0a9e5b986..057db5183 100644 --- a/src/swarm-tui/Swarm/TUI/Editor/View.hs +++ b/src/swarm-tui/Swarm/TUI/Editor/View.hs @@ -20,6 +20,7 @@ import Swarm.TUI.Editor.Model import Swarm.TUI.Model import Swarm.TUI.Model.Name import Swarm.TUI.Model.UI +import Swarm.TUI.Model.UI.Gameplay import Swarm.TUI.Panel import Swarm.TUI.View.Attribute.Attr import Swarm.TUI.View.CellDisplay (renderDisplay) diff --git a/src/swarm-tui/Swarm/TUI/Model.hs b/src/swarm-tui/Swarm/TUI/Model.hs index 245892ff4..4a6caa675 100644 --- a/src/swarm-tui/Swarm/TUI/Model.hs +++ b/src/swarm-tui/Swarm/TUI/Model.hs @@ -109,6 +109,7 @@ import Swarm.TUI.Model.Event (SwarmEvent) import Swarm.TUI.Model.Menu import Swarm.TUI.Model.Name import Swarm.TUI.Model.UI +import Swarm.TUI.Model.UI.Gameplay import Swarm.TUI.Model.WebCommand (RejectionReason (..), WebCommand (..), WebInvocationState (..)) import Swarm.Util.Lens (makeLensesNoSigs) import Text.Fuzzy qualified as Fuzzy diff --git a/src/swarm-tui/Swarm/TUI/Model/StateUpdate.hs b/src/swarm-tui/Swarm/TUI/Model/StateUpdate.hs index 6b8361d73..6d728bd1f 100644 --- a/src/swarm-tui/Swarm/TUI/Model/StateUpdate.hs +++ b/src/swarm-tui/Swarm/TUI/Model/StateUpdate.hs @@ -87,6 +87,7 @@ import Swarm.TUI.Model.KeyBindings import Swarm.TUI.Model.Name import Swarm.TUI.Model.Repl import Swarm.TUI.Model.UI +import Swarm.TUI.Model.UI.Gameplay import Swarm.TUI.View.Attribute.Attr (getWorldAttrName, swarmAttrMap) import Swarm.TUI.View.Attribute.CustomStyling (toAttrPair) import Swarm.TUI.View.Structure qualified as SR diff --git a/src/swarm-tui/Swarm/TUI/Model/UI.hs b/src/swarm-tui/Swarm/TUI/Model/UI.hs index 47ff02492..f3e11fd9e 100644 --- a/src/swarm-tui/Swarm/TUI/Model/UI.hs +++ b/src/swarm-tui/Swarm/TUI/Model/UI.hs @@ -8,52 +8,14 @@ -- SPDX-License-Identifier: BSD-3-Clause module Swarm.TUI.Model.UI ( UIState (..), - UIGameplay (..), - UITiming (..), - UIInventory (..), - GoalDisplay (..), - uiGameplay, - uiPopups, - uiTiming, - uiInventory, uiMenu, uiPlaying, uiDebugOptions, - uiFocusRing, uiLaunchConfig, - uiWorldCursor, - uiWorldEditor, - uiREPL, - uiInventoryList, - uiInventorySort, - uiInventorySearch, - uiScrollToEnd, - uiModal, - uiGoal, - uiStructure, - uiRobot, - uiDialogs, - uiIsAutoPlay, - uiAutoShowObjectives, + uiGameplay, uiAchievements, - lgTicksPerSecond, - lastFrameTime, - accumulatedTime, - tickCount, - frameCount, - frameTickCount, - lastInfoTime, - uiShowFPS, - uiShowREPL, - uiShowZero, - uiShowDebug, - uiShowRobots, - uiHideRobotsUntil, - uiInventoryShouldUpdate, - uiTPF, - uiFPS, uiAttrMap, - scenarioRef, + uiPopups, -- ** Initialization initFocusRing, diff --git a/src/swarm-tui/Swarm/TUI/Model/UI/Gameplay.hs b/src/swarm-tui/Swarm/TUI/Model/UI/Gameplay.hs index 820199d00..f8f99bed6 100644 --- a/src/swarm-tui/Swarm/TUI/Model/UI/Gameplay.hs +++ b/src/swarm-tui/Swarm/TUI/Model/UI/Gameplay.hs @@ -6,7 +6,50 @@ -- | -- SPDX-License-Identifier: BSD-3-Clause -module Swarm.TUI.Model.UI.Gameplay where +-- +-- Sub-records utilized by 'Swarm.TUI.Model.UI' +-- This exists as a separate module to avoid import cycles. +module Swarm.TUI.Model.UI.Gameplay ( + UIGameplay (..), + UITiming (..), + UIInventory (..), + GoalDisplay (..), + UIDialogs (..), + uiTiming, + uiInventory, + uiFocusRing, + uiWorldCursor, + uiWorldEditor, + uiREPL, + uiInventoryList, + uiInventorySort, + uiInventorySearch, + uiScrollToEnd, + uiModal, + uiGoal, + uiStructure, + uiRobot, + uiDialogs, + uiIsAutoPlay, + uiAutoShowObjectives, + lgTicksPerSecond, + lastFrameTime, + accumulatedTime, + tickCount, + frameCount, + frameTickCount, + lastInfoTime, + uiShowFPS, + uiShowREPL, + uiShowZero, + uiShowDebug, + uiShowRobots, + uiHideRobotsUntil, + uiInventoryShouldUpdate, + uiTPF, + uiFPS, + scenarioRef, +) where import Brick.Focus import Brick.Widgets.List qualified as BL diff --git a/src/swarm-tui/Swarm/TUI/View.hs b/src/swarm-tui/Swarm/TUI/View.hs index e9fd068be..4a9cae96a 100644 --- a/src/swarm-tui/Swarm/TUI/View.hs +++ b/src/swarm-tui/Swarm/TUI/View.hs @@ -131,6 +131,7 @@ import Swarm.TUI.Model.Event qualified as SE import Swarm.TUI.Model.KeyBindings (handlerNameKeysDescription) import Swarm.TUI.Model.Repl import Swarm.TUI.Model.UI +import Swarm.TUI.Model.UI.Gameplay import Swarm.TUI.Panel import Swarm.TUI.View.Achievement import Swarm.TUI.View.Attribute.Attr diff --git a/src/swarm-tui/Swarm/TUI/View/Util.hs b/src/swarm-tui/Swarm/TUI/View/Util.hs index b4d2e63cb..14bdfbaf0 100644 --- a/src/swarm-tui/Swarm/TUI/View/Util.hs +++ b/src/swarm-tui/Swarm/TUI/View/Util.hs @@ -31,6 +31,7 @@ import Swarm.Language.Types (Polytype) import Swarm.TUI.Model import Swarm.TUI.Model.Event (SwarmEvent) import Swarm.TUI.Model.UI +import Swarm.TUI.Model.UI.Gameplay import Swarm.TUI.View.Attribute.Attr import Swarm.TUI.View.CellDisplay import Swarm.Util (maximum0) diff --git a/src/swarm-web/Swarm/Web.hs b/src/swarm-web/Swarm/Web.hs index 3ae52b377..c2a6d4d53 100644 --- a/src/swarm-web/Swarm/Web.hs +++ b/src/swarm-web/Swarm/Web.hs @@ -81,6 +81,7 @@ import Swarm.TUI.Model hiding (SwarmKeyDispatchers (..)) import Swarm.TUI.Model.Dialog.Goal import Swarm.TUI.Model.Repl (REPLHistItem, replHistory, replSeq) import Swarm.TUI.Model.UI +import Swarm.TUI.Model.UI.Gameplay import Swarm.Util (applyJust) import Swarm.Util.RingBuffer import Swarm.Web.Worldview From 495f9686faef761aa13340e03d46f424b65e6f4b Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Sun, 15 Sep 2024 12:45:58 -0700 Subject: [PATCH 15/17] make polymorphic in name --- src/swarm-tui/Swarm/TUI/View/Shared.hs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/swarm-tui/Swarm/TUI/View/Shared.hs b/src/swarm-tui/Swarm/TUI/View/Shared.hs index 69af0d026..d61d87454 100644 --- a/src/swarm-tui/Swarm/TUI/View/Shared.hs +++ b/src/swarm-tui/Swarm/TUI/View/Shared.hs @@ -8,8 +8,7 @@ module Swarm.TUI.View.Shared where import Brick import Brick.Widgets.Center (hCenter) -import Swarm.TUI.Model.Name import Swarm.TUI.View.Attribute.Attr (italicAttr) -tabControlFooter :: Widget Name +tabControlFooter :: Widget n tabControlFooter = hCenter $ withAttr italicAttr $ txt "NOTE: [Tab] toggles focus between panes" From f70666bb9689f69108430473048872079e69dcdd Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Sun, 15 Sep 2024 12:49:21 -0700 Subject: [PATCH 16/17] revert a usage site of applyJust --- src/swarm-engine/Swarm/Game/State/Initialize.hs | 6 ++++-- src/swarm-tournament/Swarm/Web/Tournament.hs | 1 - src/swarm-util/Swarm/Util.hs | 2 ++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/swarm-engine/Swarm/Game/State/Initialize.hs b/src/swarm-engine/Swarm/Game/State/Initialize.hs index f1274185c..f670d1524 100644 --- a/src/swarm-engine/Swarm/Game/State/Initialize.hs +++ b/src/swarm-engine/Swarm/Game/State/Initialize.hs @@ -55,7 +55,7 @@ import Swarm.Game.World.Gen (Seed) import Swarm.Language.Capability (constCaps) import Swarm.Language.Syntax (allConst, erase) import Swarm.Language.Types -import Swarm.Util (applyJust, applyWhen, binTuples, (?)) +import Swarm.Util (applyWhen, binTuples, (?)) import System.Clock qualified as Clock import System.Random (mkStdGen) @@ -137,7 +137,9 @@ pureScenarioToGameState scenario theSeed now toRun gsc = -- of the scenario description). & ix baseID . machine - %~ applyJust (const . initMachine <$> initialCodeToRun) + %~ case initialCodeToRun of + Nothing -> id + Just t -> const $ initMachine t -- If we are in creative mode, give base all the things & ix baseID . robotInventory diff --git a/src/swarm-tournament/Swarm/Web/Tournament.hs b/src/swarm-tournament/Swarm/Web/Tournament.hs index 68c2b9bad..22a44c8d0 100644 --- a/src/swarm-tournament/Swarm/Web/Tournament.hs +++ b/src/swarm-tournament/Swarm/Web/Tournament.hs @@ -368,7 +368,6 @@ app unitTestFileserver appData = -- files there. -- Instead, we manually stub the paths that are used as redirects -- so that the web API invocation does not 404 when looking for them. - serveDirectoryEmbedded [ (TL.unpack defaultRedirectPage, "Hello World!") , (TL.unpack defaultSolutionSubmissionRedirectPage, "Hello World!") diff --git a/src/swarm-util/Swarm/Util.hs b/src/swarm-util/Swarm/Util.hs index e3d928708..dbd4b53c2 100644 --- a/src/swarm-util/Swarm/Util.hs +++ b/src/swarm-util/Swarm/Util.hs @@ -280,6 +280,8 @@ applyWhen :: Bool -> (a -> a) -> a -> a applyWhen True f x = f x applyWhen False _ x = x +-- | +-- Equivalent to `fromMaybe id`. applyJust :: Maybe (a -> a) -> a -> a applyJust Nothing x = x applyJust (Just f) x = f x From cbaaf22f37339dde89cb8e7b32a3e489ee9f196b Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Sun, 15 Sep 2024 15:39:28 -0700 Subject: [PATCH 17/17] Fix dialog controls --- src/swarm-tournament/Swarm/Web/Tournament.hs | 1 + src/swarm-tui/Swarm/TUI/Controller.hs | 52 +++++++++++++------- src/swarm-tui/Swarm/TUI/Model/UI.hs | 3 +- src/swarm-tui/Swarm/TUI/View/Robot.hs | 27 +++++----- src/swarm-tui/Swarm/TUI/View/Robot/Type.hs | 1 + 5 files changed, 51 insertions(+), 33 deletions(-) diff --git a/src/swarm-tournament/Swarm/Web/Tournament.hs b/src/swarm-tournament/Swarm/Web/Tournament.hs index 22a44c8d0..68c2b9bad 100644 --- a/src/swarm-tournament/Swarm/Web/Tournament.hs +++ b/src/swarm-tournament/Swarm/Web/Tournament.hs @@ -368,6 +368,7 @@ app unitTestFileserver appData = -- files there. -- Instead, we manually stub the paths that are used as redirects -- so that the web API invocation does not 404 when looking for them. + serveDirectoryEmbedded [ (TL.unpack defaultRedirectPage, "Hello World!") , (TL.unpack defaultSolutionSubmissionRedirectPage, "Hello World!") diff --git a/src/swarm-tui/Swarm/TUI/Controller.hs b/src/swarm-tui/Swarm/TUI/Controller.hs index a2dfec1af..1b9e01bae 100644 --- a/src/swarm-tui/Swarm/TUI/Controller.hs +++ b/src/swarm-tui/Swarm/TUI/Controller.hs @@ -296,7 +296,11 @@ handleMainEvent forceRedraw ev = do Web (RunWebCode e r) -> runBaseWebCode e r UpstreamVersion _ -> error "version event should be handled by top-level handler" VtyEvent (V.EvResize _ _) -> invalidateCache - EscapeKey | Just m <- s ^. uiState . uiGameplay . uiDialogs . uiModal -> closeModal m + EscapeKey + | Just m <- s ^. uiState . uiGameplay . uiDialogs . uiModal -> + if s ^. uiState . uiGameplay . uiDialogs . uiRobot . isDetailsOpened + then uiState . uiGameplay . uiDialogs . uiRobot . isDetailsOpened .= False + else closeModal m -- Pass to key handler (allows users to configure bindings) -- See Note [how Swarm event handlers work] VtyEvent (V.EvKey k m) @@ -379,19 +383,30 @@ closeModal m = do handleModalEvent :: V.Event -> EventM Name AppState () handleModalEvent = \case V.EvKey V.KEnter [] -> do - mdialog <- preuse $ uiState . uiGameplay . uiDialogs . uiModal . _Just . modalDialog - toggleModal QuitModal - case dialogSelection =<< mdialog of - Just (Button QuitButton, _) -> quitGame - Just (Button KeepPlayingButton, _) -> toggleModal KeepPlayingModal - Just (Button StartOverButton, StartOver currentSeed siPair) -> do - invalidateCache - restartGame currentSeed siPair - Just (Button NextButton, Next siPair) -> do - quitGame - invalidateCache - startGame siPair Nothing - _ -> return () + modal <- preuse $ uiState . uiGameplay . uiDialogs . uiModal . _Just . modalType + case modal of + Just RobotsModal -> do + robotDialog <- use $ uiState . uiGameplay . uiDialogs . uiRobot + unless (robotDialog ^. isDetailsOpened) $ do + let widget = robotDialog ^. robotListContent . robotsListWidget + forM_ (BL.listSelectedElement $ getList widget) $ \x -> do + Brick.zoom (uiState . uiGameplay . uiDialogs . uiRobot) $ do + isDetailsOpened .= True + updateRobotDetailsPane $ snd x + _ -> do + mdialog <- preuse $ uiState . uiGameplay . uiDialogs . uiModal . _Just . modalDialog + toggleModal QuitModal + case dialogSelection =<< mdialog of + Just (Button QuitButton, _) -> quitGame + Just (Button KeepPlayingButton, _) -> toggleModal KeepPlayingModal + Just (Button StartOverButton, StartOver currentSeed siPair) -> do + invalidateCache + restartGame currentSeed siPair + Just (Button NextButton, Next siPair) -> do + quitGame + invalidateCache + startGame siPair Nothing + _ -> return () ev -> do Brick.zoom (uiState . uiGameplay . uiDialogs . uiModal . _Just . modalDialog) (handleDialogEvent ev) modal <- preuse $ uiState . uiGameplay . uiDialogs . uiModal . _Just . modalType @@ -425,11 +440,10 @@ handleModalEvent = \case Just RobotsModal -> Brick.zoom (uiState . uiGameplay . uiDialogs . uiRobot) $ case ev of V.EvKey (V.KChar '\t') [] -> robotDetailsFocus %= focusNext _ -> do - foc <- use robotDetailsFocus - case focusGetCurrent foc of - (Just (RobotsListDialog (SingleRobotDetails RobotLogPane))) -> - Brick.zoom (robotListContent . robotDetailsPaneState . logsList) $ handleListEvent ev - _ -> do + isInDetailsMode <- use isDetailsOpened + if isInDetailsMode + then Brick.zoom (robotListContent . robotDetailsPaneState . logsList) $ handleListEvent ev + else do Brick.zoom (robotListContent . robotsListWidget) $ handleMixedListEvent ev diff --git a/src/swarm-tui/Swarm/TUI/Model/UI.hs b/src/swarm-tui/Swarm/TUI/Model/UI.hs index f3e11fd9e..cbfff8726 100644 --- a/src/swarm-tui/Swarm/TUI/Model/UI.hs +++ b/src/swarm-tui/Swarm/TUI/Model/UI.hs @@ -173,7 +173,8 @@ initUIState UIInitOptions {..} = do , _uiStructure = emptyStructureDisplay , _uiRobot = RobotDisplay - { _robotDetailsFocus = focusRing $ map RobotsListDialog $ RobotList : map SingleRobotDetails enumerate + { _robotDetailsFocus = focusRing $ map (RobotsListDialog . SingleRobotDetails) enumerate + , _isDetailsOpened = False , _robotListContent = emptyRobotDisplay debugOptions } } diff --git a/src/swarm-tui/Swarm/TUI/View/Robot.hs b/src/swarm-tui/Swarm/TUI/View/Robot.hs index 4d2d1f97c..18e30c5cc 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot.hs @@ -19,7 +19,6 @@ module Swarm.TUI.View.Robot ( ) where import Brick -import Brick.Focus import Brick.Widgets.Border import Brick.Widgets.List qualified as BL import Brick.Widgets.TabularList.Mixed @@ -385,18 +384,20 @@ mkLibraryEntries c = drawRobotsModal :: RobotDisplay -> Widget Name drawRobotsModal robotDialog = - vBox - [ mainContent - , tabControlFooter - ] + mainContent where rFocusRing = robotDialog ^. robotDetailsFocus - mainContent = case focusGetCurrent rFocusRing of - Just (RobotsListDialog (SingleRobotDetails _)) -> case maybeSelectedRobot of - Nothing -> str "No selection" - Just r -> renderRobotDetails rFocusRing r $ robotDialog ^. robotListContent . robotDetailsPaneState - where - oldList = getList $ robotDialog ^. robotListContent . robotsListWidget - maybeSelectedRobot = view robot . snd <$> BL.listSelectedElement oldList - _ -> renderRobotsList $ robotDialog ^. robotListContent + mainContent = + if robotDialog ^. isDetailsOpened + then + let oldList = getList $ robotDialog ^. robotListContent . robotsListWidget + maybeSelectedRobot = view robot . snd <$> BL.listSelectedElement oldList + detailsContent = case maybeSelectedRobot of + Nothing -> str "No selection" + Just r -> renderRobotDetails rFocusRing r $ robotDialog ^. robotListContent . robotDetailsPaneState + in vBox + [ detailsContent + , tabControlFooter + ] + else renderRobotsList $ robotDialog ^. robotListContent diff --git a/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs b/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs index d3e7ed443..9bb2dd8c7 100644 --- a/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs +++ b/src/swarm-tui/Swarm/TUI/View/Robot/Type.hs @@ -86,6 +86,7 @@ makeLenses ''RobotListContent data RobotDisplay = RobotDisplay { _robotDetailsFocus :: FocusRing Name + , _isDetailsOpened :: Bool , _robotListContent :: RobotListContent }