File: Frame.hs

package info (click to toggle)
haskell-lambdahack 0.11.0.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,056 kB
  • sloc: haskell: 45,636; makefile: 219
file content (218 lines) | stat: -rw-r--r-- 9,798 bytes parent folder | download | duplicates (3)
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
{-# LANGUAGE RankNTypes #-}
-- | Screen frames.
--
-- Note that @PointArray.Array@ here represents a screen frame and so
-- screen positions are denoted by @Point@, contrary to the convention
-- that @Point@ refers to game map coordinates, as outlined
-- in description of 'PointSquare' that should normally be used in that role.
module Game.LambdaHack.Client.UI.Frame
  ( ColorMode(..)
  , FrameST, FrameForall(..), FrameBase(..), Frame
  , PreFrame3, PreFrames3, PreFrame, PreFrames
  , SingleFrame(..), OverlaySpace
  , blankSingleFrame, truncateOverlay, overlayFrame
#ifdef EXPOSE_INTERNAL
    -- * Internal operations
  , truncateAttrLine
#endif
  ) where

import Prelude ()

import Game.LambdaHack.Core.Prelude

import           Control.Monad.ST.Strict
import           Data.Function
import qualified Data.Vector.Generic as G
import qualified Data.Vector.Unboxed as U
import qualified Data.Vector.Unboxed.Mutable as VM
import           Data.Word

import           Game.LambdaHack.Client.UI.Content.Screen
import           Game.LambdaHack.Client.UI.Overlay
import           Game.LambdaHack.Client.UI.PointUI
import qualified Game.LambdaHack.Common.PointArray as PointArray
import qualified Game.LambdaHack.Definition.Color as Color

-- | Color mode for the display.
data ColorMode =
    ColorFull  -- ^ normal, with full colours
  | ColorBW    -- ^ black and white only
  deriving Eq

type FrameST s = G.Mutable U.Vector s Word32 -> ST s ()

-- | Efficiently composable representation of an operation
-- on a frame, that is, on a mutable vector. When the composite operation
-- is eventually performed, the vector is frozen to become a 'SingleFrame'.
newtype FrameForall = FrameForall {unFrameForall :: forall s. FrameST s}

-- | Action that results in a base frame, to be modified further.
newtype FrameBase = FrameBase
  {unFrameBase :: forall s. ST s (G.Mutable U.Vector s Word32)}

-- | A frame, that is, a base frame and all its modifications.
type Frame = ( (FrameBase, FrameForall)
             , (OverlaySpace, OverlaySpace, OverlaySpace) )

-- | Components of a frame, before it's decided if the first can be overwritten
-- in-place or needs to be copied.
type PreFrame3 = (PreFrame, (OverlaySpace, OverlaySpace, OverlaySpace))

-- | Sequence of screen frames, including delays. Potentially based on a single
-- base frame.
type PreFrames3 = [Maybe PreFrame3]

-- | A simpler variant of @PreFrame3@.
type PreFrame = (U.Vector Word32, FrameForall)

-- | A simpler variant of @PreFrames3@.
type PreFrames = [Maybe PreFrame]

-- | Representation of an operation of overwriting a frame with a single line
-- at the given row.
writeLine :: Int -> AttrString -> FrameForall
{-# INLINE writeLine #-}
writeLine offset al = FrameForall $ \v -> do
  let writeAt _ [] = return ()
      writeAt off (ac32 : rest) = do
        VM.write v off (Color.attrCharW32 ac32)
        writeAt (off + 1) rest
  writeAt offset al

-- | A frame that is padded to fill the whole screen with optional
-- overlays to display in proportional, square and monospace fonts.
--
-- Note that we don't provide a list of color-highlighed box positions
-- to be drawn separately, because overlays need to obscure not only map,
-- but the highlights as well, so highlights need to be included earlier.
--
-- See the description of 'PointSquare' for explanation of why screen
-- coordinates in @singleArray@ are @Point@ even though they should be
-- 'PointSquare'.
data SingleFrame = SingleFrame
  { singleArray         :: PointArray.Array Color.AttrCharW32
  , singlePropOverlay   :: OverlaySpace
  , singleSquareOverlay :: OverlaySpace
  , singleMonoOverlay   :: OverlaySpace }
  deriving (Show, Eq)

type OverlaySpace = [(PointUI, AttrString)]

blankSingleFrame :: ScreenContent -> SingleFrame
blankSingleFrame ScreenContent{rwidth, rheight} =
  SingleFrame (PointArray.replicateA rwidth rheight Color.spaceAttrW32)
              []
              []
              []

-- | Truncate the overlay: for each line, if it's too long, it's truncated
-- and if there are too many lines, excess is dropped and warning is appended.
-- The width, in the second argument, is calculated in characters,
-- not in UI (mono font) coordinates, so that taking and dropping characters
-- is performed correctly.
truncateOverlay :: Bool -> Int -> Int -> Bool -> Int -> Bool -> Overlay
                -> OverlaySpace
truncateOverlay halveXstart width rheight wipeAdjacentRaw fillLen onBlank ov =
  let wipeAdjacent = wipeAdjacentRaw && not onBlank
      canvasLength = if onBlank then rheight else rheight - 2
      supHeight = maxYofOverlay ov
      trimmedY = canvasLength - 1
      -- Sadly, this does not trim the other, concurrent, overlays that may
      -- obscure the last line and so contend with the "trimmed" message.
      -- Tough luck; just avoid overrunning overlays in higher level code.
      ovTopFiltered = filter (\(PointUI _ y, _) -> y < trimmedY) ov
      trimmedAlert = ( PointUI 0 trimmedY
                     , stringToAL "--a portion of the text trimmed--" )
      extraLine | supHeight < 3
                  || supHeight >= trimmedY
                  || not wipeAdjacent = []
                | otherwise =
        let supHs = filter (\(PointUI _ y, _) -> y == supHeight) ov
        in if null supHs
           then []
           else let (PointUI xLast yLast, _) =
                      minimumBy (comparing $ \(PointUI x _, _) -> x) supHs
                in [(PointUI xLast (yLast + 1), emptyAttrLine)]
      -- This is crude, because an al at lower x may be longer, but KISS.
      -- This also gives a solid rule which al overwrite others
      -- when merging overlays, independent of the order of merging
      -- (except for duplicate x, for which initial order is retained).
      -- The order functions is cheap, we use @sortBy@, not @sortOn@.
      ovTop = groupBy ((==) `on` \(PointUI _ y, _) -> y)
              $ sortBy (comparing $ \(PointUI x y, _) -> (y, x))
              $ if supHeight >= canvasLength
                then ovTopFiltered ++ [trimmedAlert]
                else ov ++ extraLine
      -- Unlike the trimming above, adding spaces around overlay depends
      -- on there being no gaps and a natural order.
      -- Probably also gives messy results when X offsets are not all the same.
      -- Below we at least mitigate the case of multiple lines per row.
      f _ _ [] = error "empty list of overlay lines at the given row"
      f (yPrev, lenPrev) (yNext, lenNext) (minAl@(PointUI _ yCur, _) : rest) =
        g (if yPrev == yCur - 1 then lenPrev else 0)
          (if yNext == yCur + 1 then lenNext else 0)
          fillLen
          minAl
        : map (g 0 0 0) rest
      g lenPrev lenNext fillL (p@(PointUI xstartRaw _), layerLine) =
        let xstart = if halveXstart then xstartRaw `div` 2 else xstartRaw
            -- TODO: lenPrev and lenNext is from the same kind of font;
            -- if fonts are mixed, too few spaces are added.
            -- We'd need to keep a global store of line lengths
            -- for every position on the screen, filled first going
            -- over all texts and only afterwards texts rendered.
            -- And prop font measure would still make this imprecise.
            -- TODO: rewrite ovBackdrop according to this idea,
            -- but then process square font only mode with the same mechanism.
            maxLen = if wipeAdjacent then max lenPrev lenNext else 0
            fillFromStart = max fillL (1 + maxLen) - xstart
            available = width - xstart
        in (p, truncateAttrLine wipeAdjacent available fillFromStart layerLine)
      rightExtentOfLine (PointUI xstartRaw _, al) =
        let xstart = if halveXstart then xstartRaw `div` 2 else xstartRaw
        in min (width - 1) (xstart + length (attrLine al))
      yAndLen [] = (-99, 0)
      yAndLen als@((PointUI _ y, _) : _) =
        (y, maximum $ map rightExtentOfLine als)
      lens = map yAndLen ovTop
      f2 = map g2
      g2 (p@(PointUI xstartRaw _), layerLine) =
        let xstart = if halveXstart then xstartRaw `div` 2 else xstartRaw
            available = width - xstart
        in (p, truncateAttrLine False available 0 layerLine)
  in concat $ if onBlank
              then map f2 ovTop
              else zipWith3 f ((-9, 0) : lens) (drop 1 lens ++ [(999, 0)]) ovTop

-- | Add a space at the message end, for display overlayed over the level map.
-- Also trim (do not wrap!) too long lines. Also add many spaces when under
-- longer lines.
truncateAttrLine :: Bool -> Int -> Int -> AttrLine -> AttrString
truncateAttrLine addSpaces available fillFromStart aLine =
  let al = attrLine aLine
      len = length al
  in if | null al -> if addSpaces
                     then replicate fillFromStart Color.spaceAttrW32
                     else al
        | len == available - 1 && addSpaces -> al ++ [Color.spaceAttrW32]
        | otherwise -> case compare available len of
            LT -> take (available - 1) al ++ [Color.trimmedLineAttrW32]
            GT | addSpaces ->
              let alSpace = al ++ [Color.spaceAttrW32, Color.spaceAttrW32]
                  whiteN = fillFromStart - len - 2
              in if whiteN <= 0
                 then alSpace  -- speedup (supposedly) for menus
                 else alSpace ++ replicate whiteN Color.spaceAttrW32
            _ -> al

-- | Overlays either the game map only or the whole empty screen frame.
-- We assume the lines of the overlay are not too long nor too many.
overlayFrame :: Int -> OverlaySpace -> PreFrame -> PreFrame
overlayFrame width ov (m, ff) =
  ( m
  , FrameForall $ \v -> do
      unFrameForall ff v
      mapM_ (\(PointUI px py, l) ->
               let offset = py * width + px `div` 2
               in unFrameForall (writeLine offset l) v) ov )