{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TemplateHaskell #-}
module Graphics.Vty.PictureToSpans where
import Graphics.Vty.Attributes (Attr, currentAttr)
import Graphics.Vty.Image
import Graphics.Vty.Image.Internal
import Graphics.Vty.Picture
import Graphics.Vty.Span
import Lens.Micro
import Lens.Micro.Mtl
import Lens.Micro.TH
import Control.Monad.Reader
import Control.Monad.State.Strict hiding ( state )
import Control.Monad.ST.Strict
import qualified Data.Vector as Vector hiding ( take, replicate )
import Data.Vector.Mutable ( MVector(..))
import qualified Data.Vector.Mutable as MVector
import qualified Data.Text.Lazy as TL
type MRowOps s = MVector s SpanOps
type MSpanOps s = MVector s SpanOp
data BlitState = BlitState
{ _columnOffset :: Int
, _rowOffset :: Int
, _skipColumns :: Int
, _skipRows :: Int
, _remainingColumns :: Int
, _remainingRows :: Int
}
makeLenses ''BlitState
data BlitEnv s = BlitEnv
{ _region :: DisplayRegion
, _mrowOps :: MRowOps s
}
makeLenses ''BlitEnv
type BlitM s a = ReaderT (BlitEnv s) (StateT BlitState (ST s)) a
displayOpsForPic :: Picture -> DisplayRegion -> DisplayOps
displayOpsForPic pic r = Vector.create (combinedOpsForLayers pic r)
displayOpsForImage :: Image -> DisplayOps
displayOpsForImage i = displayOpsForPic (picForImage i) (imageWidth i, imageHeight i)
combinedOpsForLayers :: Picture -> DisplayRegion -> ST s (MRowOps s)
combinedOpsForLayers pic r
| regionWidth r == 0 || regionHeight r == 0 = MVector.new 0
| otherwise = do
layerOps <- mapM (\layer -> buildSpans layer r) (picLayers pic)
case layerOps of
[] -> fail "empty picture"
[ops] -> substituteSkips (picBackground pic) ops
topOps : lowerOps -> do
ops <- foldM mergeUnder topOps lowerOps
substituteSkips (picBackground pic) ops
substituteSkips :: Background -> MRowOps s -> ST s (MRowOps s)
substituteSkips ClearBackground ops = do
forM_ [0 .. MVector.length ops - 1] $ \row -> do
rowOps <- MVector.read ops row
let rowOps' = case Vector.last rowOps of
Skip w -> Vector.init rowOps `Vector.snoc` RowEnd w
_ -> rowOps
let rowOps'' = swapSkipsForSingleColumnCharSpan ' ' currentAttr rowOps'
MVector.write ops row rowOps''
return ops
substituteSkips (Background {backgroundChar, backgroundAttr}) ops = do
case safeWcwidth backgroundChar of
w | w == 0 -> fail $ "invalid background character " ++ show backgroundChar
| w == 1 -> do
forM_ [0 .. MVector.length ops - 1] $ \row -> do
rowOps <- MVector.read ops row
let rowOps' = swapSkipsForSingleColumnCharSpan backgroundChar backgroundAttr rowOps
MVector.write ops row rowOps'
| otherwise -> do
forM_ [0 .. MVector.length ops - 1] $ \row -> do
rowOps <- MVector.read ops row
let rowOps' = swapSkipsForCharSpan w backgroundChar backgroundAttr rowOps
MVector.write ops row rowOps'
return ops
mergeUnder :: MRowOps s -> MRowOps s -> ST s (MRowOps s)
mergeUnder upper lower = do
forM_ [0 .. MVector.length upper - 1] $ \row -> do
upperRowOps <- MVector.read upper row
lowerRowOps <- MVector.read lower row
let rowOps = mergeRowUnder upperRowOps lowerRowOps
MVector.write upper row rowOps
return upper
mergeRowUnder :: SpanOps -> SpanOps -> SpanOps
mergeRowUnder upperRowOps lowerRowOps =
onUpperOp Vector.empty (Vector.head upperRowOps) (Vector.tail upperRowOps) lowerRowOps
where
onUpperOp :: SpanOps -> SpanOp -> SpanOps -> SpanOps -> SpanOps
onUpperOp outOps op@(TextSpan _ w _ _) upperOps lowerOps =
let lowerOps' = dropOps w lowerOps
outOps' = Vector.snoc outOps op
in if Vector.null lowerOps'
then outOps'
else onUpperOp outOps' (Vector.head upperOps) (Vector.tail upperOps) lowerOps'
onUpperOp outOps (Skip w) upperOps lowerOps =
let (ops', lowerOps') = splitOpsAt w lowerOps
outOps' = outOps `mappend` ops'
in if Vector.null lowerOps'
then outOps'
else onUpperOp outOps' (Vector.head upperOps) (Vector.tail upperOps) lowerOps'
onUpperOp _ (RowEnd _) _ _ = error "cannot merge rows containing RowEnd ops"
swapSkipsForSingleColumnCharSpan :: Char -> Attr -> SpanOps -> SpanOps
swapSkipsForSingleColumnCharSpan c a = Vector.map f
where f (Skip ow) = let txt = TL.pack $ replicate ow c
in TextSpan a ow ow txt
f v = v
swapSkipsForCharSpan :: Int -> Char -> Attr -> SpanOps -> SpanOps
swapSkipsForCharSpan w c a = Vector.map f
where
f (Skip ow) = let txt0Cw = ow `div` w
txt0 = TL.pack $ replicate txt0Cw c
txt1Cw = ow `mod` w
txt1 = TL.pack $ replicate txt1Cw '…'
cw = txt0Cw + txt1Cw
txt = txt0 `TL.append` txt1
in TextSpan a ow cw txt
f v = v
buildSpans :: Image -> DisplayRegion -> ST s (MRowOps s)
buildSpans image outRegion = do
outOps <- MVector.replicate (regionHeight outRegion) Vector.empty
when (regionHeight outRegion > 0 && regionWidth outRegion > 0) $ do
let fullBuild = do
startImageBuild image
forM_ [0 .. (regionHeight outRegion - 1)] (addRowCompletion outRegion)
initEnv = BlitEnv outRegion outOps
initState = BlitState 0 0 0 0 (regionWidth outRegion) (regionHeight outRegion)
_ <- runStateT (runReaderT fullBuild initEnv) initState
return ()
return outOps
startImageBuild :: Image -> BlitM s ()
startImageBuild image = do
outOfBounds <- isOutOfBounds image <$> get
when (not outOfBounds) $ addMaybeClipped image
isOutOfBounds :: Image -> BlitState -> Bool
isOutOfBounds i s
| s ^. remainingColumns <= 0 = True
| s ^. remainingRows <= 0 = True
| s ^. skipColumns >= imageWidth i = True
| s ^. skipRows >= imageHeight i = True
| otherwise = False
addMaybeClipped :: forall s . Image -> BlitM s ()
addMaybeClipped EmptyImage = return ()
addMaybeClipped (HorizText a textStr ow _cw) = do
s <- use skipRows
when (s < 1) $ do
leftClip <- use skipColumns
rightClip <- use remainingColumns
let leftClipped = leftClip > 0
rightClipped = (ow - leftClip) > rightClip
if leftClipped || rightClipped
then let textStr' = clipText textStr leftClip rightClip
in addUnclippedText a textStr'
else addUnclippedText a textStr
addMaybeClipped (VertJoin topImage bottomImage _ow oh) = do
when (imageHeight topImage + imageHeight bottomImage > 0) $
addMaybeClippedJoin "vert_join" skipRows remainingRows rowOffset
(imageHeight topImage)
topImage
bottomImage
oh
addMaybeClipped (HorizJoin leftImage rightImage ow _oh) = do
when (imageWidth leftImage + imageWidth rightImage > 0) $
addMaybeClippedJoin "horiz_join" skipColumns remainingColumns columnOffset
(imageWidth leftImage)
leftImage
rightImage
ow
addMaybeClipped BGFill {outputWidth, outputHeight} = do
s <- get
let outputWidth' = min (outputWidth - s^.skipColumns) (s^.remainingColumns)
outputHeight' = min (outputHeight - s^.skipRows ) (s^.remainingRows)
y <- use rowOffset
forM_ [y..y+outputHeight'-1] $ snocOp (Skip outputWidth')
addMaybeClipped CropRight {croppedImage, outputWidth} = do
s <- use skipColumns
r <- use remainingColumns
let x = outputWidth - s
when (x < r) $ remainingColumns .= x
addMaybeClipped croppedImage
addMaybeClipped CropLeft {croppedImage, leftSkip} = do
skipColumns += leftSkip
addMaybeClipped croppedImage
addMaybeClipped CropBottom {croppedImage, outputHeight} = do
s <- use skipRows
r <- use remainingRows
let x = outputHeight - s
when (x < r) $ remainingRows .= x
addMaybeClipped croppedImage
addMaybeClipped CropTop {croppedImage, topSkip} = do
skipRows += topSkip
addMaybeClipped croppedImage
addMaybeClippedJoin :: forall s . String
-> Lens BlitState BlitState Int Int
-> Lens BlitState BlitState Int Int
-> Lens BlitState BlitState Int Int
-> Int
-> Image
-> Image
-> Int
-> BlitM s ()
addMaybeClippedJoin name skip remaining offset i0Dim i0 i1 size = do
state <- get
when (state^.remaining <= 0) $ fail $ name ++ " with remaining <= 0"
case state^.skip of
s | s > size -> put $ state & skip %~ subtract size
| s == 0 -> if state^.remaining > i0Dim
then do
addMaybeClipped i0
put $ state & offset %~ (+ i0Dim) & remaining %~ subtract i0Dim
addMaybeClipped i1
else addMaybeClipped i0
| s < i0Dim ->
let i0Dim' = i0Dim - s
in if state^.remaining <= i0Dim'
then addMaybeClipped i0
else do
addMaybeClipped i0
put $ state & offset %~ (+ i0Dim') & remaining %~ subtract i0Dim' & skip .~ 0
addMaybeClipped i1
| s >= i0Dim -> do
put $ state & skip %~ subtract i0Dim
addMaybeClipped i1
_ -> fail $ name ++ " has unhandled skip class"
addUnclippedText :: Attr -> DisplayText -> BlitM s ()
addUnclippedText a txt = do
let op = TextSpan a usedDisplayColumns
(fromIntegral $ TL.length txt)
txt
usedDisplayColumns = wctlwidth txt
use rowOffset >>= snocOp op
addRowCompletion :: DisplayRegion -> Int -> BlitM s ()
addRowCompletion displayRegion row = do
allRowOps <- view mrowOps
rowOps <- lift $ lift $ MVector.read allRowOps row
let endX = spanOpsAffectedColumns rowOps
when (endX < regionWidth displayRegion) $ do
let ow = regionWidth displayRegion - endX
snocOp (Skip ow) row
snocOp :: SpanOp -> Int -> BlitM s ()
snocOp !op !row = do
theMrowOps <- view mrowOps
theRegion <- view region
lift $ lift $ do
ops <- MVector.read theMrowOps row
let ops' = Vector.snoc ops op
when (spanOpsAffectedColumns ops' > regionWidth theRegion)
$ fail $ "row " ++ show row ++ " now exceeds region width"
MVector.write theMrowOps row ops'