{-# LANGUAGE RankNTypes, TupleSections #-} -- | Screen overlays. module Game.LambdaHack.Client.UI.Overlay ( -- * DisplayFont DisplayFont, isPropFont, isSquareFont, isMonoFont, textSize , -- * FontSetup FontSetup(..), multiFontSetup, singleFontSetup , -- * AttrString AttrString, blankAttrString, textToAS, textFgToAS, stringToAS , attrStringToString , (<+:>), (<\:>) -- * AttrLine , AttrLine, attrLine, emptyAttrLine, attrStringToAL, firstParagraph , textToAL, textFgToAL, stringToAL, linesAttr , splitAttrString, indentSplitAttrString -- * Overlay , Overlay, xytranslateOverlay, xtranslateOverlay, ytranslateOverlay , offsetOverlay, offsetOverlayX, typesetXY , updateLine, rectangleOfSpaces, maxYofOverlay, labDescOverlay #ifdef EXPOSE_INTERNAL -- * Internal operations , nonbreakableRev, isPrefixOfNonbreakable, breakAtSpace, splitAttrPhrase #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Char (isSpace) import qualified Data.Text as T import Game.LambdaHack.Client.UI.PointUI import qualified Game.LambdaHack.Definition.Color as Color -- * DisplayFont -- | Three types of fonts used in the UI. Overlays (layers, more or less) -- in proportional font are overwritten by layers in square font, -- which are overwritten by layers in mono font. -- All overlays overwrite the rendering of the game map, which is -- the underlying basic UI frame, comprised of square font glyps. -- -- This type needs to be kept abstract to ensure that frontend-enforced -- or user config-enforced font assignments in 'FontSetup' -- (e.g., stating that the supposedly proportional font is overriden -- to be the square font) can't be ignored. Otherwise a programmer -- could use arbirary @DisplayFont@, instead of the one taken from 'FontSetup', -- and so, e.g., calculating the width of an overlay so constructed -- in order to decide where another overlay can start would be inconsistent -- what what font is really eventually used when rendering. -- -- Note that the order of constructors has limited effect, -- but it illustrates how overwriting is explicitly implemented -- in frontends that support all fonts. data DisplayFont = PropFont | SquareFont | MonoFont deriving (Show, Eq, Enum) isPropFont, isSquareFont, isMonoFont :: DisplayFont -> Bool isPropFont = (== PropFont) isSquareFont = (== SquareFont) isMonoFont = (== MonoFont) textSize :: DisplayFont -> [a] -> Int textSize SquareFont l = 2 * length l textSize MonoFont l = length l textSize PropFont _ = error "size of proportional font texts is not defined" -- * FontSetup data FontSetup = FontSetup { squareFont :: DisplayFont , monoFont :: DisplayFont , propFont :: DisplayFont } deriving (Eq, Show) -- for unit tests multiFontSetup :: FontSetup multiFontSetup = FontSetup SquareFont MonoFont PropFont singleFontSetup :: FontSetup singleFontSetup = FontSetup SquareFont SquareFont SquareFont -- * AttrString -- | String of colourful text. End of line characters permitted. type AttrString = [Color.AttrCharW32] blankAttrString :: Int -> AttrString blankAttrString w = replicate w Color.spaceAttrW32 textToAS :: Text -> AttrString textToAS !t = let f c l = let !ac = Color.attrChar1ToW32 c in ac : l in T.foldr f [] t textFgToAS :: Color.Color -> Text -> AttrString textFgToAS !fg !t = let f ' ' l = Color.spaceAttrW32 : l -- for speed and simplicity (testing if char is a space) -- we always keep the space @White@ f c l = let !ac = Color.attrChar2ToW32 fg c in ac : l in T.foldr f [] t stringToAS :: String -> AttrString stringToAS = map Color.attrChar1ToW32 -- | Transform 'AttrString' type to 'String'. attrStringToString :: AttrString -> String attrStringToString = map Color.charFromW32 -- Follows minimorph.<+>. infixr 6 <+:> -- matches Monoid.<> (<+:>) :: AttrString -> AttrString -> AttrString (<+:>) [] l2 = l2 (<+:>) l1 [] = l1 (<+:>) l1 l2@(c2 : _) = if isSpace (Color.charFromW32 c2) || isSpace (Color.charFromW32 (last l1)) then l1 ++ l2 else l1 ++ [Color.spaceAttrW32] ++ l2 infixr 6 <\:> -- matches Monoid.<> (<\:>) :: AttrString -> AttrString -> AttrString (<\:>) [] l2 = l2 (<\:>) l1 [] = l1 (<\:>) l1 l2@(c2 : _) = if Color.charFromW32 c2 == '\n' || Color.charFromW32 (last l1) == '\n' then l1 ++ l2 else l1 ++ stringToAS "\n" ++ l2 -- We consider only these, because they are short and form a closed category. nonbreakableRev :: [String] nonbreakableRev = ["eht", "a", "na", "ehT", "A", "nA", "I"] isPrefixOfNonbreakable :: AttrString -> Bool isPrefixOfNonbreakable s = let isPrefixOfNb sRev nbRev = case stripPrefix nbRev sRev of Nothing -> False Just [] -> True Just (c : _) -> isSpace c in any (isPrefixOfNb $ attrStringToString s) nonbreakableRev breakAtSpace :: AttrString -> (AttrString, AttrString) breakAtSpace lRev = let (pre, post) = break (== Color.spaceAttrW32) lRev in case post of c : rest | c == Color.spaceAttrW32 -> if isPrefixOfNonbreakable rest then let (pre2, post2) = breakAtSpace rest in (pre ++ c : pre2, post2) else (pre, post) _ -> (pre, post) -- no space found, give up -- * AttrLine -- | Line of colourful text. End of line characters forbidden. Trailing -- @White@ space forbidden. newtype AttrLine = AttrLine {attrLine :: AttrString} deriving (Show, Eq) emptyAttrLine :: AttrLine emptyAttrLine = AttrLine [] attrStringToAL :: AttrString -> AttrLine attrStringToAL s = #ifdef WITH_EXPENSIVE_ASSERTIONS assert (allB (\ac -> Color.charFromW32 ac /= '\n') s) $ -- expensive in menus assert (null s || last s /= Color.spaceAttrW32 `blame` attrStringToString s) $ -- only expensive for menus, but often violated by code changes, so disabled -- outside test runs #endif AttrLine s firstParagraph :: AttrString -> AttrLine firstParagraph s = case linesAttr s of [] -> emptyAttrLine l : _ -> l textToAL :: Text -> AttrLine textToAL !t = let f '\n' _ = error $ "illegal end of line in: " ++ T.unpack t f c l = let !ac = Color.attrChar1ToW32 c in ac : l s = T.foldr f [] t in AttrLine $ #ifdef WITH_EXPENSIVE_ASSERTIONS assert (null s || last s /= Color.spaceAttrW32 `blame` t) #endif s textFgToAL :: Color.Color -> Text -> AttrLine textFgToAL !fg !t = let f '\n' _ = error $ "illegal end of line in: " ++ T.unpack t f ' ' l = Color.spaceAttrW32 : l -- for speed and simplicity (testing if char is a space) -- we always keep the space @White@ f c l = let !ac = Color.attrChar2ToW32 fg c in ac : l s = T.foldr f [] t in AttrLine $ #ifdef WITH_EXPENSIVE_ASSERTIONS assert (null s || last s /= Color.spaceAttrW32 `blame` t) #endif s stringToAL :: String -> AttrLine stringToAL s = attrStringToAL $ map Color.attrChar1ToW32 s -- Mimics @lines@. linesAttr :: AttrString -> [AttrLine] linesAttr [] = [] linesAttr l = cons (case break (\ac -> Color.charFromW32 ac == '\n') l of (h, t) -> (attrStringToAL h, case t of [] -> [] _ : tt -> linesAttr tt)) where cons ~(h, t) = h : t -- | Split a string into lines. Avoid breaking the line at a character -- other than space. Remove the spaces on which lines are broken, -- keep other spaces. In expensive assertions mode (dev debug mode) -- fail at trailing spaces, but keep leading spaces, e.g., to make -- distance from a text in another font. Newlines are respected. -- -- Note that we only split wrt @White@ space, nothing else, -- and the width, in the first argument, is calculated in characters, -- not in UI (mono font) coordinates, so that taking and dropping characters -- is performed correctly. splitAttrString :: Int -> Int -> AttrString -> [AttrLine] splitAttrString w0 w1 l = case linesAttr l of [] -> [] x : xs -> splitAttrPhrase w0 w1 x ++ concatMap (splitAttrPhrase w1 w1) xs indentSplitAttrString :: DisplayFont -> Int -> AttrString -> [AttrLine] indentSplitAttrString font w l = assert (w > 4) $ -- Sadly this depends on how wide the space is in propotional font, -- which varies wildly, so we err on the side of larger indent. let nspaces = case font of SquareFont -> 1 MonoFont -> 2 PropFont -> 4 ts = splitAttrString w (w - nspaces) l -- Proportional spaces are very narrow. spaces = replicate nspaces Color.spaceAttrW32 in case ts of [] -> [] hd : tl -> hd : map (AttrLine . (spaces ++) . attrLine) tl -- We pass empty line along for the case of appended buttons, which need -- either space or new lines before them. splitAttrPhrase :: Int -> Int -> AttrLine -> [AttrLine] splitAttrPhrase w0 w1 (AttrLine xs) | w0 >= length xs = [AttrLine xs] -- no problem, everything fits | otherwise = let (pre, postRaw) = splitAt w0 xs preRev = reverse pre ((ppre, ppost), post) = case postRaw of c : rest | c == Color.spaceAttrW32 && not (isPrefixOfNonbreakable preRev) -> (([], preRev), rest) _ -> (breakAtSpace preRev, postRaw) in if all (== Color.spaceAttrW32) ppost then AttrLine (reverse $ dropWhile (== Color.spaceAttrW32) preRev) : splitAttrPhrase w1 w1 (AttrLine post) else AttrLine (reverse $ dropWhile (== Color.spaceAttrW32) ppost) : splitAttrPhrase w1 w1 (AttrLine $ reverse ppre ++ post) -- * Overlay -- | A series of screen lines with start positions at which they should -- be overlayed over the base frame or a blank screen, depending on context. -- The position point is represented as in integer that is an index into the -- frame character array. -- The lines either fit the width of the screen or are intended -- for truncation when displayed. The start positions of lines may fall outside -- the length of the screen, too, unlike in @SingleFrame@. Then they are -- simply not shown. type Overlay = [(PointUI, AttrLine)] xytranslateOverlay :: Int -> Int -> Overlay -> Overlay xytranslateOverlay dx dy = map (\(PointUI x y, al) -> (PointUI (x + dx) (y + dy), al)) xtranslateOverlay :: Int -> Overlay -> Overlay xtranslateOverlay dx = xytranslateOverlay dx 0 ytranslateOverlay :: Int -> Overlay -> Overlay ytranslateOverlay = xytranslateOverlay 0 offsetOverlay :: [AttrLine] -> Overlay offsetOverlay = zipWith (curry (first $ PointUI 0)) [0..] offsetOverlayX :: [(Int, AttrLine)] -> Overlay offsetOverlayX = zipWith (\y (x, al) -> (PointUI x y, al)) [0..] typesetXY :: (Int, Int) -> [AttrLine] -> Overlay typesetXY (xoffset, yoffset) = zipWith (\y al -> (PointUI xoffset (y + yoffset), al)) [0..] -- @f@ should not enlarge the line beyond screen width nor introduce linebreaks. updateLine :: Int -> (Int -> AttrString -> AttrString) -> Overlay -> Overlay updateLine y f ov = let upd (p@(PointUI px py), AttrLine l) = if py == y then (p, AttrLine $ f px l) else (p, AttrLine l) in map upd ov rectangleOfSpaces :: Int -> Int -> Overlay rectangleOfSpaces x y = let blankAttrLine = AttrLine $ replicate x Color.nbspAttrW32 in offsetOverlay $ replicate y blankAttrLine maxYofOverlay :: Overlay -> Int maxYofOverlay ov = let yOfOverlay (PointUI _ y, _) = y in maximum $ 0 : map yOfOverlay ov labDescOverlay :: DisplayFont -> Int -> AttrString -> (Overlay, Overlay) labDescOverlay labFont width as = let (tLab, tDesc) = span (/= Color.spaceAttrW32) as labLen = textSize labFont tLab len = max 0 $ width - length tLab -- not labLen; TODO: type more strictly ovLab = offsetOverlay [attrStringToAL tLab] ovDesc = offsetOverlayX $ case splitAttrString len width tDesc of [] -> [] l : ls -> (labLen, l) : map (0,) ls in (ovLab, ovDesc)