1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581
|
-- | Monadic operations on slideshows and related data.
module Game.LambdaHack.Client.UI.SlideshowM
( overlayToSlideshow, reportToSlideshow, reportToSlideshowKeepHalt
, displaySpaceEsc, displayMore, displayMoreKeep, displayYesNo, getConfirms
, displayChoiceScreen
, displayChoiceScreenWithRightPane
, displayChoiceScreenWithDefItemKey
, displayChoiceScreenWithRightPaneKMKM
, pushFrame, pushReportFrame
#ifdef EXPOSE_INTERNAL
-- * Internal operations
, getMenuIx, saveMenuIx, stepChoiceScreen, navigationKeys, findKYX
, drawHighlight, basicFrameWithoutReport
#endif
) where
import Prelude ()
import Game.LambdaHack.Core.Prelude
import qualified Data.Char as Char
import Data.Either
import qualified Data.EnumMap.Strict as EM
import qualified Data.Map.Strict as M
import qualified Data.Text as T
import Game.LambdaHack.Client.MonadClient
import Game.LambdaHack.Client.State
import Game.LambdaHack.Client.UI.Content.Screen
import Game.LambdaHack.Client.UI.ContentClientUI
import Game.LambdaHack.Client.UI.Frame
import Game.LambdaHack.Client.UI.FrameM
import qualified Game.LambdaHack.Client.UI.Key as K
import Game.LambdaHack.Client.UI.MonadClientUI
import Game.LambdaHack.Client.UI.Msg
import Game.LambdaHack.Client.UI.MsgM
import Game.LambdaHack.Client.UI.Overlay
import Game.LambdaHack.Client.UI.PointUI
import Game.LambdaHack.Client.UI.SessionUI
import Game.LambdaHack.Client.UI.Slideshow
import Game.LambdaHack.Client.UI.UIOptions
import Game.LambdaHack.Common.ClientOptions
import Game.LambdaHack.Common.Faction
import Game.LambdaHack.Common.MonadStateRead
import Game.LambdaHack.Common.State
import Game.LambdaHack.Common.Types
import Game.LambdaHack.Common.Vector
import qualified Game.LambdaHack.Definition.Color as Color
-- | Add current report to the overlay, split the result and produce,
-- possibly, many slides.
overlayToSlideshow :: MonadClientUI m
=> Int -> [K.KM] -> OKX -> m Slideshow
overlayToSlideshow y keys okx = do
CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui
UIOptions{uMsgWrapColumn} <- getsSession sUIOptions
report <- getReportUI True
recordHistory -- report will be shown soon, remove it to history
fontSetup <- getFontSetup
curTutorial <- getsSession scurTutorial
overrideTut <- getsSession soverrideTut
let displayTutorialHints = fromMaybe curTutorial overrideTut
return $! splitOverlay fontSetup displayTutorialHints
rwidth y uMsgWrapColumn report keys okx
-- | Split current report into a slideshow.
reportToSlideshow :: MonadClientUI m => [K.KM] -> m Slideshow
reportToSlideshow keys = do
CCUI{coscreen=ScreenContent{rheight}} <- getsSession sccui
overlayToSlideshow (rheight - 2) keys emptyOKX
-- | Split current report into a slideshow. Keep report unchanged.
-- Assume the game either halts waiting for a key after this is shown,
-- or many slides are produced, all but the last are displayed
-- with player promts between and the last is either shown
-- in full or ignored if inside macro (can be recovered from history,
-- if important). Unless the prompts interrupt the macro, which is as well.
reportToSlideshowKeepHalt :: MonadClientUI m => Bool -> [K.KM] -> m Slideshow
reportToSlideshowKeepHalt insideMenu keys = do
CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui
UIOptions{uMsgWrapColumn} <- getsSession sUIOptions
report <- getReportUI insideMenu
-- Don't do @recordHistory@; the message is important, but related
-- to the messages that come after, so should be shown together.
fontSetup <- getFontSetup
curTutorial <- getsSession scurTutorial
overrideTut <- getsSession soverrideTut
let displayTutorialHints = fromMaybe curTutorial overrideTut
return $! splitOverlay fontSetup displayTutorialHints
rwidth (rheight - 2) uMsgWrapColumn
report keys emptyOKX
-- | Display a message. Return value indicates if the player wants to continue.
-- Feature: if many pages, only the last SPACE exits (but first ESC).
displaySpaceEsc :: MonadClientUI m => ColorMode -> Text -> m Bool
displaySpaceEsc dm prompt = do
unless (T.null prompt) $ msgLnAdd MsgPromptGeneric prompt
-- Two frames drawn total (unless @prompt@ very long).
slides <- reportToSlideshow [K.spaceKM, K.escKM]
km <- getConfirms dm [K.spaceKM, K.escKM] slides
return $! km == K.spaceKM
-- | Display a message. Ignore keypresses.
-- Feature: if many pages, only the last SPACE exits (but first ESC).
displayMore :: MonadClientUI m => ColorMode -> Text -> m ()
displayMore dm prompt = do
unless (T.null prompt) $ msgLnAdd MsgPromptGeneric prompt
slides <- reportToSlideshow [K.spaceKM]
void $ getConfirms dm [K.spaceKM, K.escKM] slides
displayMoreKeep :: MonadClientUI m => ColorMode -> Text -> m ()
displayMoreKeep dm prompt = do
unless (T.null prompt) $ msgLnAdd MsgPromptGeneric prompt
slides <- reportToSlideshowKeepHalt True [K.spaceKM]
void $ getConfirms dm [K.spaceKM, K.escKM] slides
-- | Print a yes/no question and return the player's answer. Use black
-- and white colours to turn player's attention to the choice.
displayYesNo :: MonadClientUI m => ColorMode -> Text -> m Bool
displayYesNo dm prompt = do
unless (T.null prompt) $ msgLnAdd MsgPromptGeneric prompt
let yn = map K.mkChar ['y', 'n']
slides <- reportToSlideshow yn
km <- getConfirms dm (K.escKM : yn) slides
return $! km == K.mkChar 'y'
getConfirms :: MonadClientUI m
=> ColorMode -> [K.KM] -> Slideshow -> m K.KM
getConfirms dm extraKeys slides = do
ekm <- displayChoiceScreen "" dm False slides extraKeys
return $! either id (error $ "" `showFailure` ekm) ekm
-- | Display a, potentially, multi-screen menu and return the chosen
-- key or menu slot (and save the index in the whole menu so that the cursor
-- can again be placed at that spot next time menu is displayed).
--
-- This function is one of only two sources of menus and so,
-- effectively, UI modes.
displayChoiceScreen :: forall m . MonadClientUI m
=> String -> ColorMode -> Bool -> Slideshow -> [K.KM]
-> m KeyOrSlot
displayChoiceScreen = do
displayChoiceScreenWithRightPane (const $ return emptyOKX) False
-- | Display a, potentially, multi-screen menu and return the chosen
-- key or menu slot (and save the index in the whole menu so that the cursor
-- can again be placed at that spot next time menu is displayed).
-- Additionally, display something on the right half of the screen,
-- depending on which menu item is currently highlighted
--
-- This function is one of only two sources of menus and so,
-- effectively, UI modes.
displayChoiceScreenWithRightPane
:: forall m . MonadClientUI m
=> (KeyOrSlot -> m OKX)
-> Bool -> String -> ColorMode -> Bool -> Slideshow -> [K.KM]
-> m KeyOrSlot
displayChoiceScreenWithRightPane displayInRightPane
highlightBullet menuName dm sfBlank
frsX extraKeys = do
kmkm <- displayChoiceScreenWithRightPaneKMKM
displayInRightPane
highlightBullet menuName dm sfBlank
frsX extraKeys
return $! case kmkm of
Left (km, _) -> Left km
Right slot -> Right slot
-- | A specialized variant of 'displayChoiceScreenWithRightPane'.
displayChoiceScreenWithDefItemKey :: MonadClientUI m
=> (Int -> MenuSlot -> m OKX)
-> Slideshow
-> [K.KM]
-> String
-> m KeyOrSlot
displayChoiceScreenWithDefItemKey f sli itemKeys menuName = do
CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui
FontSetup{propFont} <- getFontSetup
let g ekm = case ekm of
Left{} -> return emptyOKX
Right slot -> do
if isSquareFont propFont
then return emptyOKX
else f (rwidth - 2) slot
displayChoiceScreenWithRightPane
g True menuName ColorFull False sli itemKeys
-- | A variant providing for a keypress the information about the label
-- of the menu slot which was selected during the keypress.
displayChoiceScreenWithRightPaneKMKM
:: forall m . MonadClientUI m
=> (KeyOrSlot -> m OKX)
-> Bool -> String -> ColorMode -> Bool -> Slideshow -> [K.KM]
-> m (Either (K.KM, KeyOrSlot) MenuSlot)
displayChoiceScreenWithRightPaneKMKM displayInRightPane
highlightBullet menuName dm sfBlank
frsX extraKeys = do
(maxIx, initIx, clearIx, m)
<- stepChoiceScreen highlightBullet dm sfBlank frsX extraKeys
let loop :: Int -> KeyOrSlot -> m (Either (K.KM, KeyOrSlot) MenuSlot, Int)
loop pointer km = do
okxRight <- displayInRightPane km
(final, kmkm1, pointer1) <- m pointer okxRight
if final
then return (kmkm1, pointer1)
else loop pointer1 $ case kmkm1 of
Left (km1, _) -> Left km1
Right slot -> Right slot
pointer0 <- getMenuIx menuName maxIx initIx clearIx
let km0 = case findKYX pointer0 $ slideshow frsX of
Nothing -> error $ "no menu keys" `showFailure` frsX
Just (_, (ekm, _), _) -> ekm
(km, pointer) <- loop pointer0 km0
saveMenuIx menuName initIx pointer
return km
getMenuIx :: MonadClientUI m => String -> Int -> Int -> Int -> m Int
getMenuIx menuName maxIx initIx clearIx = do
menuIxMap <- getsSession smenuIxMap
-- Beware, values in @menuIxMap@ may be negative (meaning: a key, not slot).
let menuIx = if menuName == ""
then clearIx
else maybe clearIx (+ initIx) (M.lookup menuName menuIxMap)
-- this may still be negative, from different context
return $! max clearIx $ min maxIx menuIx -- so clamp to point at item, not key
saveMenuIx :: MonadClientUI m => String -> Int -> Int -> m ()
saveMenuIx menuName initIx pointer =
unless (menuName == "") $
modifySession $ \sess ->
sess {smenuIxMap = M.insert menuName (pointer - initIx) $ smenuIxMap sess}
-- | This is one step of UI menu management user session.
--
-- There is limited looping involved to return a changed position
-- in the menu each time so that the surrounding code has anything
-- interesting to do. The exception is when finally confirming a selection,
-- in which case it's usually not changed compared to last step,
-- but it's presented differently to indicate it was confirmed.
--
-- Any extra keys in the `OKX` argument on top of the those in @Slideshow@
-- argument need to be contained in the @[K.KM]@ argument. Otherwise
-- they are not accepted.
stepChoiceScreen :: forall m . MonadClientUI m
=> Bool -> ColorMode -> Bool -> Slideshow -> [K.KM]
-> m ( Int, Int, Int
, Int -> OKX
-> m (Bool, Either (K.KM, KeyOrSlot) MenuSlot, Int) )
stepChoiceScreen highlightBullet dm sfBlank frsX extraKeys = do
CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui
FontSetup{..} <- getFontSetup
UIOptions{uVi, uLeftHand} <- getsSession sUIOptions
let !_A = assert (K.escKM `elem` extraKeys) ()
frs = slideshow frsX
keys = concatMap (lefts . map fst . snd) frs ++ extraKeys
cardinalKeys = K.cardinalAllKM uVi uLeftHand
handleDir = K.handleCardinal cardinalKeys
legalKeys = keys ++ navigationKeys ++ cardinalKeys
allOKX = concatMap snd frs
maxIx = length allOKX - 1
initIx = case findIndex (isRight . fst) allOKX of
Just p -> p
_ -> 0 -- can't be @length allOKX@ or a multi-page item menu
-- mangles saved index of other item munus
clearIx = if initIx > maxIx then 0 else initIx
canvasLength = if sfBlank then rheight else rheight - 2
trimmedY = canvasLength - 1 - 2 -- will be translated down 2 lines
trimmedAlert = ( PointUI 0 trimmedY
, stringToAL "--a portion of the text trimmed--" )
page :: Int -> OKX -> m (Bool, Either (K.KM, KeyOrSlot) MenuSlot, Int)
page pointer (ovsRight0, kyxsRight) = assert (pointer >= 0)
$ case findKYX pointer frs of
Nothing -> error $ "no menu keys" `showFailure` frs
Just ( (ovs0, kyxs2)
, (ekm, (PointUI x1 y, buttonWidth))
, ixOnPage ) -> do
let ovs1 = EM.map (updateLine y $ drawHighlight x1 buttonWidth) ovs0
ovs2 = if highlightBullet
then EM.map (highBullet kyxs2) ovs1
else ovs1
-- We add spaces in proportional font under the report rendered
-- in mono font and the right pane text in prop font,
-- but over menu lines in proportional font that can be
-- very long an should not peek from under the right pane text.
--
-- We translate the pane by two characters right, because it looks
-- better when a couple last characters of a line vanish
-- off-screen than when characters touch in the middle
-- of the screen. The code producing right panes should take care
-- to generate lines two shorter than usually.
--
-- We move the pane two characters down, because normally
-- reports should not be longer than three lines
-- and the third no longer than half width.
-- We also add two to three lines of backdrop at the bottom.
ymax = maxYofFontOverlayMap ovsRight0
-- Apparently prop spaces can be really narrow, hence so many.
-- With square font, this obscures the link in main menu,
-- so would need to complicated.
spaceRectangle | isSquareFont propFont = []
| otherwise =
rectangleOfSpaces (rwidth * 4)
(min canvasLength $ ymax + 5)
trim = filter (\(PointUI _ yRight, _) -> yRight < trimmedY)
-- The alert not clickable, because the player can enter
-- the menu entry and scroll through the unabridged blurb.
ovsRight1 = if ymax <= trimmedY
then ovsRight0
else EM.unionWith (++)
(EM.map trim ovsRight0)
(EM.singleton monoFont [trimmedAlert])
ovsRight = EM.unionWith (++)
(EM.singleton propFont spaceRectangle)
(EM.map (xytranslateOverlay 2 2) ovsRight1)
(ovs, kyxs) =
if EM.null ovsRight0
then (ovs2, kyxs2)
else sideBySideOKX rwidth 0 (ovs2, kyxs2) (ovsRight, kyxsRight)
kmkm ekm2 = case ekm2 of
Left km -> Left (km, ekm2)
Right slot -> Right slot
tmpResult pointer1 = case findKYX pointer1 frs of
Nothing -> error $ "no menu keys" `showFailure` frs
Just (_, (ekm1, _), _) -> return (False, kmkm ekm1, pointer1)
ignoreKey = return (False, kmkm ekm, pointer)
pageLen = length kyxs
xix :: KYX -> Bool
xix (_, (PointUI x1' _, _)) = x1' <= x1 + 2 && x1' >= x1 - 2
firstRowOfNextPage = pointer + pageLen - ixOnPage
restOKX = drop firstRowOfNextPage allOKX
-- This does not take into account the right pane, which is fine.
firstItemOfNextPage = case findIndex (isRight . fst) restOKX of
Just p -> p + firstRowOfNextPage
_ -> firstRowOfNextPage
interpretKey :: K.KM
-> m (Bool, Either (K.KM, KeyOrSlot) MenuSlot, Int)
interpretKey ikm =
case K.key ikm of
_ | ikm == K.controlP -> do
-- Silent, because any prompt would be shown too late.
printScreen
ignoreKey
K.Return -> case ekm of
Left km ->
if K.key km == K.Return
then return (True, Left (km, ekm), pointer)
else interpretKey km
Right c -> return (True, Right c, pointer)
K.LeftButtonRelease -> do
PointUI mx my <- getsSession spointer
let onChoice (_, (PointUI cx cy, ButtonWidth font clen)) =
let blen | isSquareFont font = 2 * clen
| otherwise = clen
in my == cy && mx >= cx && mx < cx + blen
case find onChoice kyxs of
Nothing | ikm `elem` keys ->
return (True, Left (ikm, ekm), pointer)
Nothing ->
if K.spaceKM `elem` keys
then return (True, Left (K.spaceKM, ekm), pointer)
else ignoreKey
Just (ckm, _) -> case ckm of
Left km ->
if K.key km == K.Return && km `elem` keys
then return (True, Left (km, ekm), pointer)
else interpretKey km
Right c -> return (True, Right c, pointer)
K.RightButtonRelease ->
if ikm `elem` keys
then return (True, Left (ikm, ekm), pointer)
else return (True, Left (K.escKM, ekm), pointer)
K.Space | firstItemOfNextPage <= maxIx ->
tmpResult firstItemOfNextPage
K.Unknown "SAFE_SPACE" ->
if firstItemOfNextPage <= maxIx
then tmpResult firstItemOfNextPage
else tmpResult clearIx
_ | ikm `elem` keys ->
return (True, Left (ikm, ekm), pointer)
_ | K.key ikm == K.WheelNorth
|| handleDir ikm == Just (Vector 0 (-1)) ->
case findIndex xix $ reverse $ take ixOnPage kyxs of
Nothing -> if pointer == 0 then tmpResult maxIx
else tmpResult (max 0 (pointer - 1))
Just ix -> tmpResult (max 0 (pointer - ix - 1))
_ | K.key ikm == K.WheelSouth
|| handleDir ikm == Just (Vector 0 1) ->
case findIndex xix $ drop (ixOnPage + 1) kyxs of
Nothing -> if pointer == maxIx then tmpResult 0
else tmpResult (min maxIx (pointer + 1))
Just ix -> tmpResult (pointer + ix + 1)
_ | handleDir ikm == Just (Vector (-1) 0) ->
case findKYX (max 0 (pointer - 1)) frs of
Just (_, (_, (PointUI _ y2, _)), _) | y2 == y ->
tmpResult (max 0 (pointer - 1))
_ -> ignoreKey
_ | handleDir ikm == Just (Vector 1 0) ->
case findKYX (min maxIx (pointer + 1)) frs of
Just (_, (_, (PointUI _ y2, _)), _) | y2 == y ->
tmpResult (min maxIx (pointer + 1))
_ -> ignoreKey
K.Home -> tmpResult clearIx
K.End -> tmpResult maxIx
K.PgUp ->
tmpResult (max 0 (pointer - ixOnPage - 1))
K.PgDn ->
-- This doesn't scroll by screenful when header very long
-- and menu non-empty, but that scenario is rare, so OK,
-- arrow keys may be used instead.
tmpResult (min maxIx firstItemOfNextPage)
K.Space -> ignoreKey
_ | K.key ikm `elem` [K.Char '?', K.Fun 1] -> do
-- Clear macros and invoke the help macro.
modifySession $ \sess ->
sess { smacroFrame =
emptyMacroFrame {keyPending =
KeyMacro [K.mkKM "F1"]}
, smacroStack = [] }
return (True, Left (K.escKM, ekm), pointer)
_ -> error $ "unknown key" `showFailure` ikm
pkm <- promptGetKey dm ovs sfBlank legalKeys
interpretKey pkm
m pointer okxRight =
if null frs
then return (True, Left (K.escKM, Left K.escKM), pointer)
else do
(final, km, pointer1) <- page pointer okxRight
let !_A1 = assert (either ((`elem` keys) . fst) (const True) km) ()
-- Pointer at a button included, hence greater than 0, not @clearIx@.
let !_A2 = assert (0 <= pointer1 && pointer1 <= maxIx
`blame` (pointer1, maxIx)) ()
return (final, km, pointer1)
return (maxIx, initIx, clearIx, m)
navigationKeys :: [K.KM]
navigationKeys = [ K.leftButtonReleaseKM, K.rightButtonReleaseKM
, K.returnKM, K.spaceKM, K.wheelNorthKM, K.wheelSouthKM
, K.pgupKM, K.pgdnKM, K.homeKM, K.endKM, K.controlP
, K.mkChar '?', K.mkKM "F1" ]
-- | Find a position in a menu.
-- The arguments go from first menu line and menu page to the last,
-- in order. Their indexing is from 0. We select the nearest item
-- with the index equal or less to the pointer.
findKYX :: Int -> [OKX] -> Maybe (OKX, KYX, Int)
findKYX _ [] = Nothing
findKYX pointer (okx@(_, kyxs) : frs2) =
case drop pointer kyxs of
[] -> -- not enough menu items on this page
case findKYX (pointer - length kyxs) frs2 of
Nothing -> -- no more menu items in later pages
case reverse kyxs of
[] -> Nothing
kyx : _ -> Just (okx, kyx, length kyxs - 1)
res -> res
kyx : _ -> Just (okx, kyx, pointer)
drawHighlight :: Int -> ButtonWidth -> Int -> AttrString -> AttrString
drawHighlight x1 (ButtonWidth font len) xstart as =
let highableAttrs = [Color.defAttr, Color.defAttr {Color.fg = Color.BrBlack}]
highAttr c | Color.acAttr c `notElem` highableAttrs
|| Color.acChar c == ' ' = c
highAttr c = c {Color.acAttr =
(Color.acAttr c) {Color.fg = Color.BrWhite}}
cursorAttr c = c {Color.acAttr =
(Color.acAttr c)
{Color.bg = Color.HighlightNoneCursor}}
noCursorAttr c = c {Color.acAttr =
(Color.acAttr c)
{Color.bg = Color.HighlightNone}}
-- This also highlights dull white item symbols, but who cares.
lenUI = if isSquareFont font then len * 2 else len
x1MinusXStartChars = if isSquareFont font
then (x1 - xstart) `div` 2
else x1 - xstart
(as1, asRest) = splitAt x1MinusXStartChars as
(as2, as3) = splitAt len asRest
highW32 = Color.attrCharToW32 . highAttr . Color.attrCharFromW32
as2High = map highW32 as2
cursorW32 = Color.attrCharToW32 . cursorAttr . Color.attrCharFromW32
(nonAlpha, alpha) = break (Char.isAlphaNum . Color.charFromW32) as2High
as2Cursor = case alpha of
[] -> []
ch : chrest -> cursorW32 ch : chrest
noCursorW32 = Color.attrCharToW32 . noCursorAttr . Color.attrCharFromW32
in if x1 + lenUI < xstart
then as
else as1 ++ map noCursorW32 nonAlpha ++ as2Cursor ++ as3
drawBullet :: Int -> ButtonWidth -> Int -> AttrString -> AttrString
drawBullet x1 (ButtonWidth font len) xstart as0 =
let diminishChar '-' = ' '
diminishChar '^' = '^'
diminishChar '"' = '"'
diminishChar _ = 'ยท'
highableAttr = Color.defAttr {Color.bg = Color.HighlightNoneCursor}
highW32 ac32 =
let ac = Color.attrCharFromW32 ac32
ch = diminishChar $ Color.acChar ac
in if | Color.acAttr ac /= highableAttr -> ac32
| Color.acChar ac == ' ' ->
error $ "drawBullet: HighlightNoneCursor space forbidden"
`showFailure` (ac, map Color.charFromW32 as0)
| ch == ' ' -> Color.spaceAttrW32
| otherwise ->
Color.attrCharToW32
$ ac { Color.acAttr = Color.defAttr {Color.fg = Color.BrBlack}
, Color.acChar = ch }
lenUI = if isSquareFont font then len * 2 else len
x1MinusXStartChars = if isSquareFont font
then (x1 - xstart) `div` 2
else x1 - xstart
(as1, asRest) = splitAt x1MinusXStartChars as0
(as2, as3) = splitAt len asRest
highAs = \case
toHighlight : rest -> highW32 toHighlight : rest
[] -> []
in if x1 + lenUI < xstart
then as0
else as1 ++ highAs as2 ++ as3
highBullet :: [KYX] -> Overlay -> Overlay
highBullet kyxs ov0 =
let f (_, (PointUI x1 y, buttonWidth)) =
updateLine y $ drawBullet x1 buttonWidth
in foldr f ov0 kyxs
-- This is not our turn, so we can't obstruct screen with messages
-- and message reformatting causes distraction, so there's no point
-- trying to squeeze the report into the single available line,
-- except when it's not our turn permanently, because AI runs UI.
--
-- The only real drawback of this is that when resting for longer time
-- I can't see the boring messages accumulate until a non-boring interrupts me.
basicFrameWithoutReport :: MonadClientUI m
=> LevelId -> Maybe Bool -> m PreFrame3
basicFrameWithoutReport arena forceReport = do
FontSetup{propFont} <- getFontSetup
sbenchMessages <- getsClient $ sbenchMessages . soptions
side <- getsClient sside
fact <- getsState $ (EM.! side) . sfactionD
truncRep <-
if | sbenchMessages -> do
slides <- reportToSlideshowKeepHalt False []
case slideshow slides of
[] -> return EM.empty
(ov, _) : _ -> do
-- See @stepQueryUI@. This strips either "--end-" or "--more-".
let ovProp = ov EM.! propFont
return $!
EM.singleton propFont
$ if EM.size ov > 1 then ovProp else init ovProp
| fromMaybe (gunderAI fact) forceReport -> do
report <- getReportUI False
let par1 = firstParagraph $ foldr (<+:>) [] $ renderReport True report
return $! EM.fromList [(propFont, [(PointUI 0 0, par1)])]
| otherwise -> return EM.empty
drawOverlay ColorFull False truncRep arena
-- | Push the frame depicting the current level to the frame queue.
-- Only one line of the report is shown, as in animations,
-- because it may not be our turn, so we can't clear the message
-- to see what is underneath.
pushFrame :: MonadClientUI m => Bool -> m ()
pushFrame delay = do
-- The delay before reaction to keypress was too long in case of many
-- projectiles flying and ending flight, so frames need to be skipped.
keyPressed <- anyKeyPressed
unless keyPressed $ do
lidV <- viewedLevelUI
frame <- basicFrameWithoutReport lidV Nothing
-- Pad with delay before and after to let player see, e.g., door being
-- opened a few ticks after it came into vision, the same turn.
displayFrames lidV $
if delay then [Nothing, Just frame, Nothing] else [Just frame]
pushReportFrame :: MonadClientUI m => m ()
pushReportFrame = do
lidV <- viewedLevelUI
frame <- basicFrameWithoutReport lidV (Just True)
displayFrames lidV [Just frame]
|