Skip to content

Commit

Permalink
Improvements to scrolling (#1481)
Browse files Browse the repository at this point in the history
- Add scrollbars on both the inventory and info panels
- Get rid of `. . .` at top and bottom of info panel, since we now have scrollbar as a visual indicator when there is more content
- Allow scrolling the REPL history (closes #60)
    - PgUp/PgDown can be used to scroll (Shift+PgUp/Dn were not recognized on my system)
    - Hitting any other key causes the view to jump back to the very bottom
    - A computation finishing + printing an output also causes the view to jump to the bottom
    - The REPL history is cached so that it only gets re-rendered whenever a new history entry (i.e. input or output) is added; this is needed since the history could get quite large.
- Also, fix the height of the key hint menus to 2 lines, even when the panel-specific menu (second line) is blank, so the world panel does not keep resizing as we move the focus between panels.

Thanks to @jtdaugherty for releasing `brick-1.10` with a new ability to specify blank space to the side of scrollbars; see jtdaugherty/brick#484 .

Also towards #1461 .
  • Loading branch information
byorgey authored Sep 3, 2023
1 parent 56febd9 commit 15dd824
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 107 deletions.
131 changes: 62 additions & 69 deletions src/Swarm/TUI/Controller.hs
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,8 @@ updateUI = do
let itName = fromString $ "it" ++ show itIx
let out = T.intercalate " " [itName, ":", prettyText finalType, "=", into (prettyValue v)]
uiState . uiREPL . replHistory %= addREPLItem (REPLOutput out)
invalidateCacheEntry REPLHistoryCache
vScrollToEnd replScroll
gameState . replStatus .= REPLDone (Just val)
gameState . baseRobot . robotContext . at itName .= Just val
gameState . replNextValueIndex %= (+ 1)
Expand Down Expand Up @@ -856,31 +858,13 @@ updateUI = do
uiState . uiScrollToEnd .= True
pure True

-- Decide whether the info panel has more content scrolled off the
-- top and/or bottom, so we can draw some indicators to show it if
-- so. Note, because we only know the update size and position of
-- the viewport *after* it has been rendered, this means the top and
-- bottom indicators will only be updated one frame *after* the info
-- panel updates, but this isn't really that big of deal.
infoPanelUpdated <- do
mvp <- lookupViewport InfoViewport
case mvp of
Nothing -> return False
Just vp -> do
let topMore = (vp ^. vpTop) > 0
botMore = (vp ^. vpTop + snd (vp ^. vpSize)) < snd (vp ^. vpContentSize)
oldTopMore <- uiState . uiMoreInfoTop <<.= topMore
oldBotMore <- uiState . uiMoreInfoBot <<.= botMore
return $ oldTopMore /= topMore || oldBotMore /= botMore

goalOrWinUpdated <- doGoalUpdates

let redraw =
g ^. needsRedraw
|| inventoryUpdated
|| replUpdated
|| logUpdated
|| infoPanelUpdated
|| goalOrWinUpdated
pure redraw

Expand Down Expand Up @@ -1131,57 +1115,66 @@ runBaseTerm topCtx =
-- | Handle a user input event for the REPL.
handleREPLEventTyping :: BrickEvent Name AppEvent -> EventM Name AppState ()
handleREPLEventTyping = \case
Key V.KEnter -> do
s <- get
let topCtx = topContext s
repl = s ^. uiState . uiREPL
uinput = repl ^. replPromptText

if not $ s ^. gameState . replWorking
then case repl ^. replPromptType of
CmdPrompt _ -> runBaseCode topCtx uinput
SearchPrompt hist ->
case lastEntry uinput hist of
Nothing -> uiState %= resetREPL "" (CmdPrompt [])
Just found
| T.null uinput -> uiState %= resetREPL "" (CmdPrompt [])
| otherwise -> do
uiState %= resetREPL found (CmdPrompt [])
modify validateREPLForm
else continueWithoutRedraw
Key V.KUp -> modify $ adjReplHistIndex Older
Key V.KDown -> modify $ adjReplHistIndex Newer
ControlChar 'r' -> do
s <- get
let uinput = s ^. uiState . uiREPL . replPromptText
case s ^. uiState . uiREPL . replPromptType of
CmdPrompt _ -> uiState . uiREPL . replPromptType .= SearchPrompt (s ^. uiState . uiREPL . replHistory)
SearchPrompt rh -> case lastEntry uinput rh of
Nothing -> pure ()
Just found -> uiState . uiREPL . replPromptType .= SearchPrompt (removeEntry found rh)
CharKey '\t' -> do
s <- get
let names = s ^.. gameState . baseRobot . robotContext . defTypes . to assocs . traverse . _1
uiState . uiREPL %= tabComplete names (s ^. gameState . entityMap)
modify validateREPLForm
EscapeKey -> do
formSt <- use $ uiState . uiREPL . replPromptType
case formSt of
CmdPrompt {} -> continueWithoutRedraw
SearchPrompt _ ->
uiState %= resetREPL "" (CmdPrompt [])
ControlChar 'd' -> do
text <- use $ uiState . uiREPL . replPromptText
if text == T.empty
then toggleModal QuitModal
else continueWithoutRedraw
-- finally if none match pass the event to the editor
ev -> do
Brick.zoom (uiState . uiREPL . replPromptEditor) (handleEditorEvent ev)
uiState . uiREPL . replPromptType %= \case
CmdPrompt _ -> CmdPrompt [] -- reset completions on any event passed to editor
SearchPrompt a -> SearchPrompt a
modify validateREPLForm
-- Scroll the REPL on PageUp or PageDown
Key V.KPageUp -> vScrollPage replScroll Brick.Up
Key V.KPageDown -> vScrollPage replScroll Brick.Down
k -> do
-- On any other key event, jump to the bottom of the REPL then handle the event
vScrollToEnd replScroll
case k of
Key V.KEnter -> do
s <- get
let topCtx = topContext s
repl = s ^. uiState . uiREPL
uinput = repl ^. replPromptText

if not $ s ^. gameState . replWorking
then case repl ^. replPromptType of
CmdPrompt _ -> do
runBaseCode topCtx uinput
invalidateCacheEntry REPLHistoryCache
SearchPrompt hist ->
case lastEntry uinput hist of
Nothing -> uiState %= resetREPL "" (CmdPrompt [])
Just found
| T.null uinput -> uiState %= resetREPL "" (CmdPrompt [])
| otherwise -> do
uiState %= resetREPL found (CmdPrompt [])
modify validateREPLForm
else continueWithoutRedraw
Key V.KUp -> modify $ adjReplHistIndex Older
Key V.KDown -> modify $ adjReplHistIndex Newer
ControlChar 'r' -> do
s <- get
let uinput = s ^. uiState . uiREPL . replPromptText
case s ^. uiState . uiREPL . replPromptType of
CmdPrompt _ -> uiState . uiREPL . replPromptType .= SearchPrompt (s ^. uiState . uiREPL . replHistory)
SearchPrompt rh -> case lastEntry uinput rh of
Nothing -> pure ()
Just found -> uiState . uiREPL . replPromptType .= SearchPrompt (removeEntry found rh)
CharKey '\t' -> do
s <- get
let names = s ^.. gameState . baseRobot . robotContext . defTypes . to assocs . traverse . _1
uiState . uiREPL %= tabComplete names (s ^. gameState . entityMap)
modify validateREPLForm
EscapeKey -> do
formSt <- use $ uiState . uiREPL . replPromptType
case formSt of
CmdPrompt {} -> continueWithoutRedraw
SearchPrompt _ ->
uiState %= resetREPL "" (CmdPrompt [])
ControlChar 'd' -> do
text <- use $ uiState . uiREPL . replPromptText
if text == T.empty
then toggleModal QuitModal
else continueWithoutRedraw
-- finally if none match pass the event to the editor
ev -> do
Brick.zoom (uiState . uiREPL . replPromptEditor) (handleEditorEvent ev)
uiState . uiREPL . replPromptType %= \case
CmdPrompt _ -> CmdPrompt [] -- reset completions on any event passed to editor
SearchPrompt a -> SearchPrompt a
modify validateREPLForm

data CompletionType
= FunctionName
Expand Down
4 changes: 4 additions & 0 deletions src/Swarm/TUI/Model.hs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ module Swarm.TUI.Model (
populateInventoryList,
infoScroll,
modalScroll,
replScroll,

-- * Runtime state
RuntimeState,
Expand Down Expand Up @@ -186,6 +187,9 @@ infoScroll = viewportScroll InfoViewport
modalScroll :: ViewportScroll Name
modalScroll = viewportScroll ModalViewport

replScroll :: ViewportScroll Name
replScroll = viewportScroll REPLViewport

-- ----------------------------------------------------------------------------
-- Runtime state --
-- ----------------------------------------------------------------------------
Expand Down
4 changes: 4 additions & 0 deletions src/Swarm/TUI/Model/Name.hs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ data Name
WorldEditorPanelControl WorldEditorFocusable
| -- | The REPL input form.
REPLInput
| -- | The REPL history cache.
REPLHistoryCache
| -- | The render cache for the world view.
WorldCache
| -- | The cached extent for the world view.
Expand Down Expand Up @@ -97,6 +99,8 @@ data Name
InfoViewport
| -- | The scrollable viewport for any modal dialog.
ModalViewport
| -- | The scrollable viewport for the REPL.
REPLViewport
| -- | A clickable button in a modal dialog.
Button Button
deriving (Eq, Ord, Show, Read)
6 changes: 6 additions & 0 deletions src/Swarm/TUI/Model/Repl.hs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module Swarm.TUI.Model.Repl (
addREPLItem,
restartREPLHistory,
getLatestREPLHistoryItems,
getSessionREPLHistoryItems,
moveReplHistIndex,
getCurrentItemText,
replIndexIsAtInput,
Expand Down Expand Up @@ -185,6 +186,11 @@ getLatestREPLHistoryItems n h = toList latestN
latestN = Seq.drop oldestIndex $ h ^. replSeq
oldestIndex = max (h ^. replStart) $ length (h ^. replSeq) - n

-- | Get only the items from the REPL history that were entered during
-- the current session.
getSessionREPLHistoryItems :: REPLHistory -> Seq REPLHistItem
getSessionREPLHistoryItems h = Seq.drop (h ^. replStart) (h ^. replSeq)

data TimeDir = Newer | Older deriving (Eq, Ord, Show)

moveReplHistIndex :: TimeDir -> Text -> REPLHistory -> REPLHistory
Expand Down
12 changes: 0 additions & 12 deletions src/Swarm/TUI/Model/UI.hs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ module Swarm.TUI.Model.UI (
uiInventory,
uiInventorySort,
uiInventorySearch,
uiMoreInfoTop,
uiMoreInfoBot,
uiScrollToEnd,
uiError,
uiModal,
Expand Down Expand Up @@ -107,8 +105,6 @@ data UIState = UIState
, _uiInventory :: Maybe (Int, BL.List Name InventoryListEntry)
, _uiInventorySort :: InventorySortOptions
, _uiInventorySearch :: Maybe Text
, _uiMoreInfoTop :: Bool
, _uiMoreInfoBot :: Bool
, _uiScrollToEnd :: Bool
, _uiError :: Maybe Text
, _uiModal :: Maybe Modal
Expand Down Expand Up @@ -177,12 +173,6 @@ uiInventorySearch :: Lens' UIState (Maybe Text)
-- focused robot's inventory.
uiInventory :: Lens' UIState (Maybe (Int, BL.List Name InventoryListEntry))

-- | Does the info panel contain more content past the top of the panel?
uiMoreInfoTop :: Lens' UIState Bool

-- | Does the info panel contain more content past the bottom of the panel?
uiMoreInfoBot :: Lens' UIState Bool

-- | A flag telling the UI to scroll the info panel to the very end
-- (used when a new log message is appended).
uiScrollToEnd :: Lens' UIState Bool
Expand Down Expand Up @@ -329,8 +319,6 @@ initUIState speedFactor showMainMenu cheatMode = do
, _uiInventory = Nothing
, _uiInventorySort = defaultSortOptions
, _uiInventorySearch = Nothing
, _uiMoreInfoTop = False
, _uiMoreInfoBot = False
, _uiScrollToEnd = False
, _uiError = Nothing
, _uiModal = Nothing
Expand Down
Loading

0 comments on commit 15dd824

Please sign in to comment.