lipstick-0.1.0.0: internal/Lipstick/Internal/Mod.hs
-- | The representation of modifiers and the records they change, and the
-- modifiers themselves. Hidden: the public API is "Lipstick.Mod".
--
-- A @'Mod' f@ is two changes: one to the fields of the thing @f@, and one
-- to the 'Props' every thing has. "Lipstick.Mod" explains how they are used.
module Lipstick.Internal.Mod
( Mod (..)
, fieldMod
, propMod
, runMod
, appMod
-- * Properties of every thing
, Props (..)
, defProps
, propsOf
, withProps
, cropToProps
-- * Components
, HasBox (..)
, HasBorder (..)
, HasSize (..)
, HasAlign (..)
, HasWrap (..)
, HasCrop
, Sides (..)
, uniformSides
, Box (..)
, defBox
, BorderProps (..)
, defBorderProps
, Edges (..)
, borderEdges
, blendBorder
, Size (..)
, defSize
, Align (..)
, defAlign
-- * Text attributes
, bold
, faint
, italic
, blink
, reverseVideo
, strikethrough
, flag
, unsetFlag
, underline
, underlineStyle
, underlineColor
, fg
, bg
, unsetFg
, unsetBg
, unsetUnderline
, unsetUnderlineColor
, link
, unsetLink
-- * Padding and margins
, padding
, paddingAll
, paddingTop
, paddingRight
, paddingBottom
, paddingLeft
, margin
, marginAll
, marginTop
, marginRight
, marginBottom
, marginLeft
, marginBg
, paddingChar
, marginChar
, unsetPadding
, unsetMargins
, unsetMarginBg
-- * Borders
, border
, borderFg
, borderBg
, borderFgBlend
, borderFgBlendOffset
, borderTopFg
, borderRightFg
, borderBottomFg
, borderLeftFg
, borderTopBg
, borderRightBg
, borderBottomBg
, borderLeftBg
, unsetBorder
, borderTop
, borderRight
, borderBottom
, borderLeft
-- * Size
, width
, height
, maxWidth
, maxHeight
, grow
, fillWidth
, stretchHeight
, unsetWidth
, unsetHeight
, unsetMaxWidth
, unsetMaxHeight
-- * Alignment
, align
, valign
, unsetAlign
-- * Wrapping
, noWrap
, unsetNoWrap
) where
import Data.Maybe (fromMaybe, isJust)
import Data.Text (Text)
import Lipstick.Internal.Border (Border, normalBorder)
import Lipstick.Internal.Color (Color, blend)
import Lipstick.Internal.Grid
import Lipstick.Internal.Types
-- | A modification of a thing @f@: of its own fields, and of the
-- properties every thing has. '<>' applies the left modifier first, so
-- settings on the right win.
data Mod f = Mod (f -> f) (Props -> Props)
instance Semigroup (Mod f) where
Mod f1 g1 <> Mod f2 g2 = Mod (f2 . f1) (g2 . g1)
instance Monoid (Mod f) where
mempty = Mod id id
-- | A modifier of the thing's own fields.
fieldMod :: (f -> f) -> Mod f
fieldMod f = Mod f id
-- | A modifier of the properties every thing has.
propMod :: (Props -> Props) -> Mod f
propMod = Mod id
-- | Apply a modifier to a thing's defaults.
runMod :: Mod f -> f -> (f, Props)
runMod (Mod f g) x = (f x, g defProps)
-- | Apply only the field part of a modifier.
appMod :: Mod f -> f -> f
appMod (Mod f _) = f
-- | Properties every thing has.
data Props = Props
{ propAttrs :: !Attrs
-- ^ base text attributes for everything the thing draws
, propMaxWidth :: !(Maybe Int)
, propMaxHeight :: !(Maybe Int)
}
deriving (Eq, Show)
defProps :: Props
defProps = Props mempty Nothing Nothing
-- | The common properties a modifier sets, starting from the defaults.
propsOf :: Mod f -> Props
propsOf (Mod _ g) = g defProps
-- | What most builders do with the properties: lay the attributes under
-- everything, then crop.
withProps :: Props -> [[Cell]] -> Grid
withProps p = cropToProps p . underlay (propAttrs p) (fillAttrs (propAttrs p))
-- | Crop lines to 'maxWidth' and 'maxHeight'.
cropToProps :: Props -> [[Cell]] -> Grid
cropToProps p ls =
let croppedW = maybe ls (\n -> map (cropCells n) ls) (propMaxWidth p)
in fromCellLines (maybe croppedW (`take` croppedW) (propMaxHeight p))
-- | Things with padding and margins: 'Lipstick.Style.Style'. The
-- modifiers 'padding', 'margin', 'marginBg', 'paddingChar', 'marginChar'
-- and their variants apply to them.
class HasBox f where
overBox :: (Box -> Box) -> f -> f
-- | Things with a border: 'Lipstick.Style.Style' and
-- 'Lipstick.Table.Table'. The modifiers 'border', 'borderFg', 'borderTop'
-- and the other border modifiers apply to them.
class HasBorder f where
overBorder :: (BorderProps -> BorderProps) -> f -> f
-- | Things with a size: 'Lipstick.Style.Style', 'Lipstick.Table.Table',
-- 'Lipstick.Outline.Outline' and 'Lipstick.Place.Place'. The modifiers
-- 'width', 'height', 'grow', 'fillWidth', 'stretchHeight', 'unsetWidth' and
-- 'unsetHeight' apply to them. ('maxWidth' and 'maxHeight' work on
-- anything.)
class HasSize f where
overSize :: (Size -> Size) -> f -> f
-- | Things whose content can be aligned: 'Lipstick.Style.Style' and
-- 'Lipstick.Place.Place'. The modifiers 'align', 'valign' and 'unsetAlign'
-- apply to them.
class HasAlign f where
overAlign :: (Align -> Align) -> f -> f
-- | Things whose text can be wrapped: 'Lipstick.Style.Style' and
-- 'Lipstick.Table.Table'. The modifiers 'noWrap' and 'unsetNoWrap' apply
-- to them.
class HasWrap f where
overWrap :: (Bool -> Bool) -> f -> f
-- | Things whose result can be cropped: every thing but
-- 'Lipstick.Block.Inline', whose text is laid out with the text around it.
-- The modifiers 'maxWidth', 'maxHeight', 'unsetMaxWidth' and
-- 'unsetMaxHeight' apply to them.
class HasCrop f
-- | One value for each side: top, right, bottom and left, as in CSS.
data Sides a = Sides {sideTop, sideRight, sideBottom, sideLeft :: !a}
deriving (Eq, Show, Functor)
uniformSides :: a -> Sides a
uniformSides a = Sides a a a a
data Box = Box
{ boxPadding :: !(Sides Int)
, boxMargin :: !(Sides Int)
, boxMarginBg :: !(Maybe Color)
, boxPaddingChar :: !Char
, boxMarginChar :: !Char
}
deriving (Eq, Show)
defBox :: Box
defBox = Box (uniformSides 0) (uniformSides 0) Nothing ' ' ' '
data BorderProps = BorderProps
{ borderChars :: !(Maybe Border)
, borderSides :: !(Sides (Maybe Bool))
, borderFgColors :: !(Sides (Maybe Color))
, borderBgColors :: !(Sides (Maybe Color))
, borderFgStops :: ![Color]
-- ^ when not empty, a gradient that replaces 'borderFgColors'
, borderFgOffset :: !Int
-- ^ how many cells the gradient is rotated clockwise
}
deriving (Eq, Show)
defBorderProps :: BorderProps
defBorderProps = BorderProps Nothing (uniformSides Nothing) (uniformSides Nothing) (uniformSides Nothing) [] 0
-- | Which sides of a border are drawn.
data Edges = Edges {edgeTop, edgeRight, edgeBottom, edgeLeft :: !Bool}
deriving (Eq, Show)
-- | The characters to draw a border with, and the sides to draw. A side
-- left as 'Nothing' is drawn only when the characters are set; without
-- characters, sides switched on explicitly use 'normalBorder'.
borderEdges :: BorderProps -> (Border, Edges)
borderEdges p =
let on side = fromMaybe (isJust (borderChars p)) (side (borderSides p))
in ( fromMaybe normalBorder (borderChars p)
, Edges (on sideTop) (on sideRight) (on sideBottom) (on sideLeft)
)
-- | Recolour the border cells with a gradient, clockwise from the top-left
-- and rotated clockwise by an offset. The rows are the bordered box; @nl@
-- and @nr@ are the number of cells in the left and right border columns.
blendBorder :: [Color] -> Int -> Edges -> Int -> Int -> [[Cell]] -> [[Cell]]
blendBorder [] _ _ _ _ rows = rows
blendBorder stops offset edges nl nr rows =
let (topRows, rest) = splitAt (if edgeTop edges then 1 else 0) rows
(middleRows, bottomRows) = splitAt (length rest - (if edgeBottom edges then 1 else 0)) rest
n = sum (map length topRows) + length middleRows * (nr + nl) + sum (map length bottomRows)
colors = rotate offset (blend n stops)
(cTop, c1) = splitAt (sum (map length topRows)) colors
(cRight, c2) = splitAt (length middleRows * nr) c1
(cBottom, cLeft) = splitAt (sum (map length bottomRows)) c2
paint cs cells = zipWith (\c cell -> cell {cellAttrs = (cellAttrs cell) {attrFg = Just c}}) cs cells
rightCols = chunks nr cRight
leftCols = reverse (chunks nl cLeft)
middle' =
[ let (l, rest') = splitAt nl row
(inner, r) = splitAt (length rest' - nr) rest'
in paint (reverse lc) l ++ inner ++ paint rc r
| (row, lc, rc) <- zip3 middleRows (leftCols ++ repeat []) (rightCols ++ repeat [])
]
in [paint cTop row | row <- topRows]
++ middle'
++ [reverse (paint cBottom (reverse row)) | row <- bottomRows]
where
rotate k xs
| null xs = xs
| otherwise = let r = length xs - k `mod` length xs in drop r xs ++ take r xs
chunks k xs
| k <= 0 = []
| null xs = []
| otherwise = let (a, b) = splitAt k xs in a : chunks k b
-- | Width and height include padding and border, but not margins.
data Size = Size
{ sizeWidth :: !(Maybe Int)
, sizeHeight :: !(Maybe Int)
, sizeGrow :: !Int
-- ^ weight for spare width; 0 for none
, sizeStretch :: !Bool
-- ^ grow to the height of the row
}
deriving (Eq, Show)
defSize :: Size
defSize = Size Nothing Nothing 0 False
data Align = Align
{ alignH :: !Position
, alignV :: !Position
}
deriving (Eq, Show)
defAlign :: Align
defAlign = Align left top
-- Text attributes -------------------------------------------------------------
attrsMod :: (Attrs -> Attrs) -> Mod f
attrsMod g = propMod (\p -> p {propAttrs = g (propAttrs p)})
-- | Bold text.
bold :: Mod f
bold = flag Bold True
-- | Faint (dim) text.
faint :: Mod f
faint = flag Faint True
-- | Italic text.
italic :: Mod f
italic = flag Italic True
-- | Blinking text.
blink :: Mod f
blink = flag Blink True
-- | Swap the foreground and background colours.
reverseVideo :: Mod f
reverseVideo = flag Reverse True
-- | Text with a line through it.
strikethrough :: Mod f
strikethrough = flag Strikethrough True
-- | Set a flag explicitly; @flag Bold False@ turns off bold inherited from
-- an enclosing style.
flag :: Flag -> Bool -> Mod f
flag f v = attrsMod (setFlag f (Just v))
-- | Forget a flag, so an enclosing style's setting shows through.
unsetFlag :: Flag -> Mod f
unsetFlag f = attrsMod (setFlag f Nothing)
-- | A single underline: @underlineStyle SingleUnderline@.
underline :: Mod f
underline = underlineStyle SingleUnderline
-- | The kind of underline. Double, curly, dotted and dashed underlines are
-- sent only to true-colour terminals; on other profiles they are shown as
-- a single underline.
underlineStyle :: Underline -> Mod f
underlineStyle u = attrsMod (\a -> a {attrUnderline = Just u})
-- | The colour of the underline, when there is one. It is printed only
-- for the 256-colour and true-colour profiles.
underlineColor :: Color -> Mod f
underlineColor c = attrsMod (\a -> a {attrUnderlineColor = Just c})
-- | The foreground (text) colour.
fg :: Color -> Mod f
fg c = attrsMod (\a -> a {attrFg = Just c})
-- | The background colour.
bg :: Color -> Mod f
bg c = attrsMod (\a -> a {attrBg = Just c})
-- | Forget the foreground colour, so an enclosing style's shows through.
unsetFg :: Mod f
unsetFg = attrsMod (\a -> a {attrFg = Nothing})
-- | Forget the background colour, so an enclosing style's shows through.
unsetBg :: Mod f
unsetBg = attrsMod (\a -> a {attrBg = Nothing})
-- | Forget the underline, so an enclosing style's shows through.
unsetUnderline :: Mod f
unsetUnderline = attrsMod (\a -> a {attrUnderline = Nothing})
-- | Forget the underline colour, so an enclosing style's shows through.
unsetUnderlineColor :: Mod f
unsetUnderlineColor = attrsMod (\a -> a {attrUnderlineColor = Nothing})
-- | Make the text a hyperlink (OSC 8).
link :: Text -> Mod f
link url = attrsMod (\a -> a {attrLink = Just url})
-- | Forget the hyperlink, so an enclosing style's shows through.
unsetLink :: Mod f
unsetLink = attrsMod (\a -> a {attrLink = Nothing})
-- Padding and margins ---------------------------------------------------------
paddingMod :: HasBox f => (Sides Int -> Sides Int) -> Mod f
paddingMod g = fieldMod (overBox (\b -> b {boxPadding = fmap (max 0) (g (boxPadding b))}))
marginMod :: HasBox f => (Sides Int -> Sides Int) -> Mod f
marginMod g = fieldMod (overBox (\b -> b {boxMargin = fmap (max 0) (g (boxMargin b))}))
-- | @padding vertical horizontal@. Negative amounts count as 0, as for
-- all padding and margins.
padding :: HasBox f => Int -> Int -> Mod f
padding v h = paddingMod (const (Sides v h v h))
-- | The same padding on all four sides.
paddingAll :: HasBox f => Int -> Mod f
paddingAll n = paddingMod (const (uniformSides n))
-- | Padding above the content, in lines.
paddingTop :: HasBox f => Int -> Mod f
paddingTop n = paddingMod (\s -> s {sideTop = n})
-- | Padding to the right of the content, in columns.
paddingRight :: HasBox f => Int -> Mod f
paddingRight n = paddingMod (\s -> s {sideRight = n})
-- | Padding below the content, in lines.
paddingBottom :: HasBox f => Int -> Mod f
paddingBottom n = paddingMod (\s -> s {sideBottom = n})
-- | Padding to the left of the content, in columns.
paddingLeft :: HasBox f => Int -> Mod f
paddingLeft n = paddingMod (\s -> s {sideLeft = n})
-- | @margin vertical horizontal@
margin :: HasBox f => Int -> Int -> Mod f
margin v h = marginMod (const (Sides v h v h))
-- | The same margin on all four sides.
marginAll :: HasBox f => Int -> Mod f
marginAll n = marginMod (const (uniformSides n))
-- | The margin above the box, in lines.
marginTop :: HasBox f => Int -> Mod f
marginTop n = marginMod (\s -> s {sideTop = n})
-- | The margin to the right of the box, in columns.
marginRight :: HasBox f => Int -> Mod f
marginRight n = marginMod (\s -> s {sideRight = n})
-- | The margin below the box, in lines.
marginBottom :: HasBox f => Int -> Mod f
marginBottom n = marginMod (\s -> s {sideBottom = n})
-- | The margin to the left of the box, in columns.
marginLeft :: HasBox f => Int -> Mod f
marginLeft n = marginMod (\s -> s {sideLeft = n})
-- | Background colour of the margins.
marginBg :: HasBox f => Color -> Mod f
marginBg c = fieldMod (overBox (\b -> b {boxMarginBg = Just c}))
-- | No padding on any side.
unsetPadding :: HasBox f => Mod f
unsetPadding = paddingMod (const (uniformSides 0))
-- | No margins on any side.
unsetMargins :: HasBox f => Mod f
unsetMargins = marginMod (const (uniformSides 0))
-- | Forget the 'marginBg': the margins have no background colour of
-- their own.
unsetMarginBg :: HasBox f => Mod f
unsetMarginBg = fieldMod (overBox (\b -> b {boxMarginBg = Nothing}))
-- | The character padding is filled with (default a space).
paddingChar :: HasBox f => Char -> Mod f
paddingChar c = fieldMod (overBox (\b -> b {boxPaddingChar = c}))
-- | The character margins are filled with (default a space).
marginChar :: HasBox f => Char -> Mod f
marginChar c = fieldMod (overBox (\b -> b {boxMarginChar = c}))
-- Borders ---------------------------------------------------------------------
borderMod :: HasBorder f => (BorderProps -> BorderProps) -> Mod f
borderMod = fieldMod . overBorder
-- | Draw a border with these characters, on all sides not switched off.
border :: HasBorder f => Border -> Mod f
border b = borderMod (\p -> p {borderChars = Just b})
-- | The colour of the whole border. It replaces a 'borderFgBlend'
-- gradient.
borderFg :: HasBorder f => Color -> Mod f
borderFg c = borderMod (\p -> p {borderFgColors = uniformSides (Just c), borderFgStops = []})
-- | The background colour of the whole border.
borderBg :: HasBorder f => Color -> Mod f
borderBg c = borderMod (\p -> p {borderBgColors = uniformSides (Just c)})
-- | The colour of the top side of the border, corners included. Like
-- every side colour, it replaces a 'borderFgBlend' gradient.
borderTopFg :: HasBorder f => Color -> Mod f
borderTopFg c = borderFgSide (\s -> s {sideTop = Just c})
-- | The colour of the right side of the border, without the corners.
borderRightFg :: HasBorder f => Color -> Mod f
borderRightFg c = borderFgSide (\s -> s {sideRight = Just c})
-- | The colour of the bottom side of the border, corners included.
borderBottomFg :: HasBorder f => Color -> Mod f
borderBottomFg c = borderFgSide (\s -> s {sideBottom = Just c})
-- | The colour of the left side of the border, without the corners.
borderLeftFg :: HasBorder f => Color -> Mod f
borderLeftFg c = borderFgSide (\s -> s {sideLeft = Just c})
-- | The background colour of the top side of the border, corners
-- included.
borderTopBg :: HasBorder f => Color -> Mod f
borderTopBg c = borderBgSide (\s -> s {sideTop = Just c})
-- | The background colour of the right side of the border, without the
-- corners.
borderRightBg :: HasBorder f => Color -> Mod f
borderRightBg c = borderBgSide (\s -> s {sideRight = Just c})
-- | The background colour of the bottom side of the border, corners
-- included.
borderBottomBg :: HasBorder f => Color -> Mod f
borderBottomBg c = borderBgSide (\s -> s {sideBottom = Just c})
-- | The background colour of the left side of the border, without the
-- corners.
borderLeftBg :: HasBorder f => Color -> Mod f
borderLeftBg c = borderBgSide (\s -> s {sideLeft = Just c})
borderFgSide, borderBgSide :: HasBorder f => (Sides (Maybe Color) -> Sides (Maybe Color)) -> Mod f
borderFgSide g = borderMod (\p -> p {borderFgColors = g (borderFgColors p), borderFgStops = []})
borderBgSide g = borderMod (\p -> p {borderBgColors = g (borderBgColors p)})
-- | Rotate a 'borderFgBlend' gradient clockwise by this many cells
-- (anticlockwise if negative).
borderFgBlendOffset :: HasBorder f => Int -> Mod f
borderFgBlendOffset n = borderMod (\p -> p {borderFgOffset = n})
-- | No border, and no border colours.
unsetBorder :: HasBorder f => Mod f
unsetBorder = borderMod (const defBorderProps)
-- | Colour the border with a gradient through these colours, going
-- clockwise from the top-left corner.
borderFgBlend :: HasBorder f => [Color] -> Mod f
borderFgBlend cs = borderMod (\p -> p {borderFgStops = cs})
-- | Whether the top side of the border is drawn. By default a side is
-- drawn when a 'border' is set; switched on without one, it is drawn with
-- 'normalBorder'.
borderTop :: HasBorder f => Bool -> Mod f
borderTop v = borderSide (\s -> s {sideTop = Just v})
-- | Whether the right side of the border is drawn, as for 'borderTop'.
borderRight :: HasBorder f => Bool -> Mod f
borderRight v = borderSide (\s -> s {sideRight = Just v})
-- | Whether the bottom side of the border is drawn, as for 'borderTop'.
borderBottom :: HasBorder f => Bool -> Mod f
borderBottom v = borderSide (\s -> s {sideBottom = Just v})
-- | Whether the left side of the border is drawn, as for 'borderTop'.
borderLeft :: HasBorder f => Bool -> Mod f
borderLeft v = borderSide (\s -> s {sideLeft = Just v})
borderSide :: HasBorder f => (Sides (Maybe Bool) -> Sides (Maybe Bool)) -> Mod f
borderSide g = borderMod (\p -> p {borderSides = g (borderSides p)})
-- Size ------------------------------------------------------------------------
sizeMod :: HasSize f => (Size -> Size) -> Mod f
sizeMod = fieldMod . overSize
-- | @width n@: a total width of @n@ columns when there is room for it.
-- Laid out in less space (a narrow terminal, a full row), the thing
-- shrinks like any other rather than overflowing. For a style, the width
-- includes padding and border, but not margins. A negative width counts
-- as 0, as do negative heights and crops.
width :: HasSize f => Int -> Mod f
width n = sizeMod (\s -> s {sizeWidth = Just (max 0 n)})
-- | @height n@: the thing is at least @n@ lines tall. For a style, the
-- height includes padding and border, but not margins.
height :: HasSize f => Int -> Mod f
height n = sizeMod (\s -> s {sizeHeight = Just (max 0 n)})
-- | Take a share of the spare width, in proportion to the weight, when the
-- thing is laid out next to others or on its own in the output's width.
-- With a 'width', that width is the least it takes.
grow :: HasSize f => Int -> Mod f
grow k = sizeMod (\s -> s {sizeGrow = max 0 k})
-- | Fill the width available: @grow 1@.
fillWidth :: HasSize f => Mod f
fillWidth = grow 1
-- | Grow to the height of the row the thing is in (as the tallest thing
-- next to it).
stretchHeight :: HasSize f => Mod f
stretchHeight = sizeMod (\s -> s {sizeStretch = True})
-- | Crop the final result (for a style, margins included) to at most this
-- many columns.
maxWidth :: HasCrop f => Int -> Mod f
maxWidth n = propMod (\p -> p {propMaxWidth = Just (max 0 n)})
-- | Crop the final result (for a style, margins included) to at most this
-- many lines. A table leaves out the body rows that do not fit instead,
-- with a row of ellipses in their place where there is room for it (see
-- 'Lipstick.Table.yOffset').
maxHeight :: HasCrop f => Int -> Mod f
maxHeight n = propMod (\p -> p {propMaxHeight = Just (max 0 n)})
-- | Forget the 'width'.
unsetWidth :: HasSize f => Mod f
unsetWidth = sizeMod (\s -> s {sizeWidth = Nothing})
-- | Forget the 'height'.
unsetHeight :: HasSize f => Mod f
unsetHeight = sizeMod (\s -> s {sizeHeight = Nothing})
-- | Forget the 'maxWidth': no crop to a width.
unsetMaxWidth :: HasCrop f => Mod f
unsetMaxWidth = propMod (\p -> p {propMaxWidth = Nothing})
-- | Forget the 'maxHeight': no crop to a height.
unsetMaxHeight :: HasCrop f => Mod f
unsetMaxHeight = propMod (\p -> p {propMaxHeight = Nothing})
-- Alignment -------------------------------------------------------------------
-- | Horizontal alignment.
align :: HasAlign f => Position -> Mod f
align p = fieldMod (overAlign (\a -> a {alignH = p}))
-- | Vertical alignment.
valign :: HasAlign f => Position -> Mod f
valign p = fieldMod (overAlign (\a -> a {alignV = p}))
-- | Back to the default alignment, 'left' and 'top'.
unsetAlign :: HasAlign f => Mod f
unsetAlign = fieldMod (overAlign (const defAlign))
-- Wrapping --------------------------------------------------------------------
-- | Cut lines that are too long with an ellipsis instead of wrapping them.
noWrap :: HasWrap f => Mod f
noWrap = fieldMod (overWrap (const False))
-- | Wrap lines again, even inside a style that sets 'noWrap'.
unsetNoWrap :: HasWrap f => Mod f
unsetNoWrap = fieldMod (overWrap (const True))