diff --git a/src/Swarm/TUI/Controller.hs b/src/Swarm/TUI/Controller.hs index 509a34ebc8..be57a70f19 100644 --- a/src/Swarm/TUI/Controller.hs +++ b/src/Swarm/TUI/Controller.hs @@ -1,5 +1,4 @@ {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE PatternSynonyms #-} -- | -- Module : Swarm.TUI.Controller @@ -81,6 +80,7 @@ import Swarm.Language.Requirement qualified as R import Swarm.Language.Syntax import Swarm.Language.Typed (Typed (..)) import Swarm.Language.Types +import Swarm.TUI.Controller.ControllerUtils import Swarm.TUI.Inventory.Sorting (cycleSortDirection, cycleSortOrder) import Swarm.TUI.List import Swarm.TUI.Model @@ -90,24 +90,6 @@ import Swarm.Version (NewReleaseFailure (..)) import System.Clock import Witch (into) --- | Pattern synonyms to simplify brick event handler -pattern Key :: V.Key -> BrickEvent n e -pattern Key k = VtyEvent (V.EvKey k []) - -pattern CharKey, ControlChar, MetaChar :: Char -> BrickEvent n e -pattern CharKey c = VtyEvent (V.EvKey (V.KChar c) []) -pattern ControlChar c = VtyEvent (V.EvKey (V.KChar c) [V.MCtrl]) -pattern MetaChar c = VtyEvent (V.EvKey (V.KChar c) [V.MMeta]) - -pattern ShiftKey :: V.Key -> BrickEvent n e -pattern ShiftKey k = VtyEvent (V.EvKey k [V.MShift]) - -pattern EscapeKey :: BrickEvent n e -pattern EscapeKey = VtyEvent (V.EvKey V.KEsc []) - -pattern FKey :: Int -> BrickEvent n e -pattern FKey c = VtyEvent (V.EvKey (V.KFun c) []) - -- | The top-level event handler for the TUI. handleEvent :: BrickEvent Name AppEvent -> EventM Name AppState () handleEvent = \case diff --git a/src/Swarm/TUI/Controller/ControllerUtils.hs b/src/Swarm/TUI/Controller/ControllerUtils.hs new file mode 100644 index 0000000000..4df80592aa --- /dev/null +++ b/src/Swarm/TUI/Controller/ControllerUtils.hs @@ -0,0 +1,24 @@ +{-# LANGUAGE PatternSynonyms #-} + +module Swarm.TUI.Controller.ControllerUtils where + +import Brick hiding (Direction) +import Graphics.Vty qualified as V + +-- | Pattern synonyms to simplify brick event handler +pattern Key :: V.Key -> BrickEvent n e +pattern Key k = VtyEvent (V.EvKey k []) + +pattern CharKey, ControlChar, MetaChar :: Char -> BrickEvent n e +pattern CharKey c = VtyEvent (V.EvKey (V.KChar c) []) +pattern ControlChar c = VtyEvent (V.EvKey (V.KChar c) [V.MCtrl]) +pattern MetaChar c = VtyEvent (V.EvKey (V.KChar c) [V.MMeta]) + +pattern ShiftKey :: V.Key -> BrickEvent n e +pattern ShiftKey k = VtyEvent (V.EvKey k [V.MShift]) + +pattern EscapeKey :: BrickEvent n e +pattern EscapeKey = VtyEvent (V.EvKey V.KEsc []) + +pattern FKey :: Int -> BrickEvent n e +pattern FKey c = VtyEvent (V.EvKey (V.KFun c) []) diff --git a/src/Swarm/TUI/View.hs b/src/Swarm/TUI/View.hs index 6e8b2d4042..5ae415cad0 100644 --- a/src/Swarm/TUI/View.hs +++ b/src/Swarm/TUI/View.hs @@ -91,12 +91,12 @@ import Swarm.Game.World qualified as W import Swarm.Language.Pretty (prettyText) import Swarm.Language.Syntax import Swarm.Language.Typecheck (inferConst) -import Swarm.Language.Types (Polytype) import Swarm.TUI.Attr import Swarm.TUI.Border import Swarm.TUI.Inventory.Sorting (renderSortMethod) import Swarm.TUI.Model import Swarm.TUI.Panel +import Swarm.TUI.View.ViewUtils import Swarm.Util import Swarm.Version (NewReleaseFailure (..)) import System.Clock (TimeSpec (..)) @@ -390,10 +390,6 @@ drawTime t showTicks = maybeDrawTime :: Integer -> Bool -> GameState -> Maybe (Widget n) maybeDrawTime t showTicks gs = guard (clockInstalled gs) $> drawTime t showTicks --- | Render the type of the current REPL input to be shown to the user. -drawType :: Polytype -> Widget Name -drawType = withAttr infoAttr . padLeftRight 1 . txt . prettyText - -- | Draw info about the current number of ticks per second. drawTPS :: AppState -> Widget Name drawTPS s = hBox (tpsInfo : rateInfo) @@ -426,10 +422,6 @@ chooseCursor s locs = case s ^. uiState . uiModal of Nothing -> showFirstCursor s locs Just _ -> Nothing --- | Width cap for modal and error message windows -maxModalWindowWidth :: Int -maxModalWindowWidth = 500 - -- | Render the error dialog window with a given error message renderErrorDialog :: Text -> Widget Name renderErrorDialog err = renderDialog (dialog (Just "Error") Nothing (maxModalWindowWidth `min` requiredWidth)) errContent @@ -473,77 +465,6 @@ drawModal s = \case GoalModal g -> padLeftRight 1 (displayParagraphs g) KeepPlayingModal -> padLeftRight 1 (displayParagraphs ["Have fun! Hit Ctrl-Q whenever you're ready to proceed to the next challenge or return to the menu."]) -quitMsg :: Menu -> Text -quitMsg m = "Are you sure you want to " <> quitAction <> "? All progress on this scenario will be lost!" - where - quitAction = case m of - NoMenu -> "quit" - _ -> "return to the menu" - --- | Generate a fresh modal window of the requested type. -generateModal :: AppState -> ModalType -> Modal -generateModal s mt = Modal mt (dialog (Just title) buttons (maxModalWindowWidth `min` requiredWidth)) - where - currentScenario = s ^. uiState . scenarioRef - currentSeed = s ^. gameState . seed - haltingMessage = case s ^. uiState . uiMenu of - NoMenu -> Just "Quit" - _ -> Nothing - descriptionWidth = 100 - helpWidth = 80 - (title, buttons, requiredWidth) = - case mt of - HelpModal -> (" Help ", Nothing, helpWidth) - RobotsModal -> ("Robots", Nothing, descriptionWidth) - RecipesModal -> ("Available Recipes", Nothing, descriptionWidth) - CommandsModal -> ("Available Commands", Nothing, descriptionWidth) - MessagesModal -> ("Messages", Nothing, descriptionWidth) - WinModal -> - let nextMsg = "Next challenge!" - stopMsg = fromMaybe "Return to the menu" haltingMessage - continueMsg = "Keep playing" - in ( "" - , Just - ( 0 - , [ (nextMsg, NextButton scene) - | Just scene <- [nextScenario (s ^. uiState . uiMenu)] - ] - ++ [ (stopMsg, QuitButton) - , (continueMsg, KeepPlayingButton) - ] - ) - , sum (map length [nextMsg, stopMsg, continueMsg]) + 32 - ) - DescriptionModal e -> (descriptionTitle e, Nothing, descriptionWidth) - QuitModal -> - let stopMsg = fromMaybe ("Quit to" ++ maybe "" (" " ++) (into @String <$> curMenuName s) ++ " menu") haltingMessage - maybeStartOver = sequenceA ("Start over", StartOverButton currentSeed <$> currentScenario) - in ( "" - , Just - ( 0 - , catMaybes - [ Just ("Keep playing", CancelButton) - , maybeStartOver - , Just (stopMsg, QuitButton) - ] - ) - , T.length (quitMsg (s ^. uiState . uiMenu)) + 4 - ) - GoalModal _ -> - let goalModalTitle = case currentScenario of - Nothing -> "Goal" - Just (scenario, _) -> scenario ^. scenarioName - in (" " <> T.unpack goalModalTitle <> " ", Nothing, 80) - KeepPlayingModal -> ("", Just (0, [("OK", CancelButton)]), 80) - --- | Get the name of the current New Game menu. -curMenuName :: AppState -> Maybe Text -curMenuName s = case s ^. uiState . uiMenu of - NewGameMenu (_ :| (parentMenu : _)) -> - Just (parentMenu ^. BL.listSelectedElementL . to scenarioItemName) - NewGameMenu _ -> Just "Scenarios" - _ -> Nothing - robotsListWidget :: AppState -> Widget Name robotsListWidget s = hCenter table where @@ -713,9 +634,6 @@ drawConst c = hBox [padLeft (Pad $ 13 - T.length constName) (txt constName), txt constName = syntax . constInfo $ c constSig = " : " <> prettyText (inferConst c) -descriptionTitle :: Entity -> String -descriptionTitle e = " " ++ from @Text (e ^. entityName) ++ " " - -- | Generate a pop-up widget to display the description of an entity. descriptionWidget :: AppState -> Entity -> Widget Name descriptionWidget s e = padLeftRight 1 (explainEntry s e) diff --git a/src/Swarm/TUI/View/ViewUtils.hs b/src/Swarm/TUI/View/ViewUtils.hs new file mode 100644 index 0000000000..01a2070f2a --- /dev/null +++ b/src/Swarm/TUI/View/ViewUtils.hs @@ -0,0 +1,103 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Swarm.TUI.View.ViewUtils where + +import Brick hiding (Direction) +import Brick.Widgets.Dialog +import Brick.Widgets.List qualified as BL +import Control.Lens hiding (Const, from) +import Data.List.NonEmpty (NonEmpty (..)) +import Data.Maybe (catMaybes, fromMaybe) +import Data.Text (Text) +import Data.Text qualified as T +import Swarm.Game.Entity as E +import Swarm.Game.Scenario (scenarioName) +import Swarm.Game.ScenarioInfo (scenarioItemName) +import Swarm.Game.State +import Swarm.Language.Pretty (prettyText) +import Swarm.Language.Types (Polytype) +import Swarm.TUI.Attr +import Swarm.TUI.Model +import Witch (from, into) + +-- | Generate a fresh modal window of the requested type. +generateModal :: AppState -> ModalType -> Modal +generateModal s mt = Modal mt (dialog (Just title) buttons (maxModalWindowWidth `min` requiredWidth)) + where + currentScenario = s ^. uiState . scenarioRef + currentSeed = s ^. gameState . seed + haltingMessage = case s ^. uiState . uiMenu of + NoMenu -> Just "Quit" + _ -> Nothing + descriptionWidth = 100 + helpWidth = 80 + (title, buttons, requiredWidth) = + case mt of + HelpModal -> (" Help ", Nothing, helpWidth) + RobotsModal -> ("Robots", Nothing, descriptionWidth) + RecipesModal -> ("Available Recipes", Nothing, descriptionWidth) + CommandsModal -> ("Available Commands", Nothing, descriptionWidth) + MessagesModal -> ("Messages", Nothing, descriptionWidth) + WinModal -> + let nextMsg = "Next challenge!" + stopMsg = fromMaybe "Return to the menu" haltingMessage + continueMsg = "Keep playing" + in ( "" + , Just + ( 0 + , [ (nextMsg, NextButton scene) + | Just scene <- [nextScenario (s ^. uiState . uiMenu)] + ] + ++ [ (stopMsg, QuitButton) + , (continueMsg, KeepPlayingButton) + ] + ) + , sum (map length [nextMsg, stopMsg, continueMsg]) + 32 + ) + DescriptionModal e -> (descriptionTitle e, Nothing, descriptionWidth) + QuitModal -> + let stopMsg = fromMaybe ("Quit to" ++ maybe "" (" " ++) (into @String <$> curMenuName s) ++ " menu") haltingMessage + maybeStartOver = sequenceA ("Start over", StartOverButton currentSeed <$> currentScenario) + in ( "" + , Just + ( 0 + , catMaybes + [ Just ("Keep playing", CancelButton) + , maybeStartOver + , Just (stopMsg, QuitButton) + ] + ) + , T.length (quitMsg (s ^. uiState . uiMenu)) + 4 + ) + GoalModal _ -> + let goalModalTitle = case currentScenario of + Nothing -> "Goal" + Just (scenario, _) -> scenario ^. scenarioName + in (" " <> T.unpack goalModalTitle <> " ", Nothing, 80) + KeepPlayingModal -> ("", Just (0, [("OK", CancelButton)]), 80) + +-- | Render the type of the current REPL input to be shown to the user. +drawType :: Polytype -> Widget Name +drawType = withAttr infoAttr . padLeftRight 1 . txt . prettyText + +descriptionTitle :: Entity -> String +descriptionTitle e = " " ++ from @Text (e ^. entityName) ++ " " + +-- | Width cap for modal and error message windows +maxModalWindowWidth :: Int +maxModalWindowWidth = 500 + +-- | Get the name of the current New Game menu. +curMenuName :: AppState -> Maybe Text +curMenuName s = case s ^. uiState . uiMenu of + NewGameMenu (_ :| (parentMenu : _)) -> + Just (parentMenu ^. BL.listSelectedElementL . to scenarioItemName) + NewGameMenu _ -> Just "Scenarios" + _ -> Nothing + +quitMsg :: Menu -> Text +quitMsg m = "Are you sure you want to " <> quitAction <> "? All progress on this scenario will be lost!" + where + quitAction = case m of + NoMenu -> "quit" + _ -> "return to the menu" diff --git a/swarm.cabal b/swarm.cabal index 9a5695a52b..337f0cea86 100644 --- a/swarm.cabal +++ b/swarm.cabal @@ -115,7 +115,9 @@ library Swarm.TUI.Panel Swarm.TUI.Model Swarm.TUI.View + Swarm.TUI.View.ViewUtils Swarm.TUI.Controller + Swarm.TUI.Controller.ControllerUtils Swarm.TUI.Inventory.Sorting Swarm.App Swarm.Version