vty-5.15: src/Graphics/Vty/Span.hs
-- Copyright Corey O'Connor
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE GADTs #-}
-- | A picture is translated into a sequences of state changes and
-- character spans. The attribute is applied to all following spans,
-- including spans of the next row. The nth element of the sequence
-- represents the nth row (from top to bottom) of the picture to render.
--
-- A span op sequence will be defined for all rows and columns (and no
-- more) of the region provided with the picture to 'spansForPic'.
module Graphics.Vty.Span where
import Graphics.Vty.Attributes (Attr)
import Graphics.Vty.Image
import Graphics.Vty.Image.Internal ( clipText )
import qualified Data.Text.Lazy as TL
import Data.Vector (Vector)
import qualified Data.Vector as Vector
-- | This represents an operation on the terminal: either an attribute
-- change or the output of a text string.
data SpanOp =
-- | A span of UTF-8 text occupies a specific number of screen space
-- columns. A single UTF character does not necessarily represent 1
-- colunm. See Codec.Binary.UTF8.Width TextSpan [Attr] [output width
-- in columns] [number of characters] [data]
TextSpan
{ textSpanAttr :: !Attr
, textSpanOutputWidth :: !Int
, textSpanCharWidth :: !Int
, textSpanText :: DisplayText
}
-- | Skips the given number of columns.
| Skip !Int
-- | Marks the end of a row. Specifies how many columns are
-- remaining. These columns will not be explicitly overwritten with
-- the span ops. The terminal is require to assure the remaining
-- columns are clear.
| RowEnd !Int
deriving Eq
-- | A vector of span operations executed in succession. This represents
-- the operations required to render a row of the terminal. The
-- operations in one row may affect subsequent rows. For example,
-- setting the foreground color in one row will affect all subsequent
-- rows until the foreground color is changed.
type SpanOps = Vector SpanOp
dropOps :: Int -> SpanOps -> SpanOps
dropOps w = snd . splitOpsAt w
splitOpsAt :: Int -> SpanOps -> (SpanOps, SpanOps)
splitOpsAt inW inOps = splitOpsAt' inW inOps
where
splitOpsAt' 0 ops = (Vector.empty, ops)
splitOpsAt' remainingColumns ops = case Vector.head ops of
t@(TextSpan {}) -> if remainingColumns >= textSpanOutputWidth t
then let (pre,post) = splitOpsAt' (remainingColumns - textSpanOutputWidth t)
(Vector.tail ops)
in (Vector.cons t pre, post)
else let preTxt = clipText (textSpanText t) 0 remainingColumns
preOp = TextSpan { textSpanAttr = textSpanAttr t
, textSpanOutputWidth = remainingColumns
, textSpanCharWidth = fromIntegral $! TL.length preTxt
, textSpanText = preTxt
}
postWidth = textSpanOutputWidth t - remainingColumns
postTxt = clipText (textSpanText t) remainingColumns postWidth
postOp = TextSpan { textSpanAttr = textSpanAttr t
, textSpanOutputWidth = postWidth
, textSpanCharWidth = fromIntegral $! TL.length postTxt
, textSpanText = postTxt
}
in ( Vector.singleton preOp
, Vector.cons postOp (Vector.tail ops)
)
Skip w -> if remainingColumns >= w
then let (pre,post) = splitOpsAt' (remainingColumns - w) (Vector.tail ops)
in (Vector.cons (Skip w) pre, post)
else ( Vector.singleton $ Skip remainingColumns
, Vector.cons (Skip (w - remainingColumns)) (Vector.tail ops)
)
RowEnd _ -> error "cannot split ops containing a row end"
-- | A vector of span operation vectors for display, one per row of the
-- output region.
type DisplayOps = Vector SpanOps
instance Show SpanOp where
show (TextSpan attr ow cw _) = "TextSpan(" ++ show attr ++ ")(" ++ show ow ++ ", " ++ show cw ++ ")"
show (Skip ow) = "Skip(" ++ show ow ++ ")"
show (RowEnd ow) = "RowEnd(" ++ show ow ++ ")"
-- | The number of columns the DisplayOps are defined for.
--
-- All spans are verified to define same number of columns.
displayOpsColumns :: DisplayOps -> Int
displayOpsColumns ops
| Vector.length ops == 0 = 0
| otherwise = Vector.length $ Vector.head ops
-- | The number of rows the DisplayOps are defined for.
displayOpsRows :: DisplayOps -> Int
displayOpsRows ops = Vector.length ops
affectedRegion :: DisplayOps -> DisplayRegion
affectedRegion ops = (displayOpsColumns ops, displayOpsRows ops)
-- | The number of columns a SpanOps affects.
spanOpsEffectedColumns :: SpanOps -> Int
spanOpsEffectedColumns inOps = Vector.foldl' spanOpsEffectedColumns' 0 inOps
where
spanOpsEffectedColumns' t (TextSpan _ w _ _ ) = t + w
spanOpsEffectedColumns' t (Skip w) = t + w
spanOpsEffectedColumns' t (RowEnd w) = t + w
-- | The width of a single SpanOp in columns.
spanOpHasWidth :: SpanOp -> Maybe (Int, Int)
spanOpHasWidth (TextSpan _ ow cw _) = Just (cw, ow)
spanOpHasWidth (Skip ow) = Just (ow,ow)
spanOpHasWidth (RowEnd ow) = Just (ow,ow)
-- | The number of columns to the character at the given position in the
-- span op.
columnsToCharOffset :: Int -> SpanOp -> Int
columnsToCharOffset cx (TextSpan _ _ _ utf8Str) =
let str = TL.unpack utf8Str
in wcswidth (take cx str)
columnsToCharOffset cx (Skip _) = cx
columnsToCharOffset cx (RowEnd _) = cx