reflex-vty-1.2.0.0: src/Reflex/Vty/Style.hs
-- |
-- Module: Reflex.Vty.Style
-- Description: Lip Gloss-inspired declarative styling for vty images
--
-- A 'Style' is a record of presentation rules (foreground/background color, text
-- transforms and variants, padding, margin, borders, dimensions, and alignment)
-- that can be rendered to 'Graphics.Vty.Image'. Styles support inheritance:
-- 'inherit' fills only the fields that are unset in the child, and the 'with*'
-- setters replace fields.
--
-- Use 'render' or 'renderB' to produce an 'Image' from a 'Style' and some text.
module Reflex.Vty.Style
( -- * Types
Style (..)
, def
-- ** Sub-types
, Color
, UnderlineStyle (..)
, BorderStyle (..)
, HAlign (..)
, VAlign (..)
, Padding (..)
, Margin (..)
-- * Color constants
, black
, red
, green
, yellow
, blue
, magenta
, cyan
, white
, brightBlack
, brightRed
, brightGreen
, brightYellow
, brightBlue
, brightMagenta
, brightCyan
, brightWhite
, rgbColor
-- * Border style presets
, singleBorder
, roundedBorder
, thickBorder
, doubleBorder
, asciiBorder
, markdownBorder
, noBorder
, innerHalfBorder
, outerHalfBlockBorder
-- * Setters
-- ** Color
, withForeground
, withBackground
-- ** Text transforms
, withBold
, withItalic
, withFaint
, withBlink
, withStrikethrough
, withReverse
-- ** Underline
, withUnderline
, withUnderlineColor
-- ** Hyperlink
, withHyperlink
-- ** Padding and margin
, withPadding
, withPaddingTop
, withPaddingRight
, withPaddingBottom
, withPaddingLeft
, withMargin
, withMarginTop
, withMarginRight
, withMarginBottom
, withMarginLeft
-- ** Border
, withBorder
, withBorderForeground
, withBorderBackground
, withBorderTop
, withBorderBottom
, withBorderLeft
, withBorderRight
-- ** Dimensions
, withWidth
, withHeight
, withMaxWidth
, withMaxHeight
-- ** Alignment
, withAlignH
, withAlignV
-- ** Whitespace
, withWhitespace
-- ** Text transform
, withTransform
-- ** Tab width
, withTabWidth
-- ** Margin background
, withMarginBackground
-- ** Inline mode
, withInline
-- ** Color whitespace
, withColorWhitespace
-- ** Per-side border colors
, withBorderTopForeground
, withBorderTopBackground
, withBorderBottomForeground
, withBorderBottomBackground
, withBorderLeftForeground
, withBorderLeftBackground
, withBorderRightForeground
, withBorderRightBackground
-- * Composition
, inherit
-- * Rendering
, render
, renderB
, applyAttr
, mergeAttr
, measure
-- * Image composition
, joinHorizontal
, joinVertical
, placeHorizontal
, placeVertical
, place
-- * Text utilities
, truncateWith
, textHeight
, textSize
) where
import Control.Applicative ((<|>))
import Data.Default (Default (..))
import Data.Maybe (fromMaybe, isJust)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Graphics.Vty as V
import Reflex (Behavior, Reflex)
import Data.Text.Zipper (charWidth, textWidth)
-- | A terminal color. Currently an alias for vty's 'V.Color'; this keeps the
-- public API stable if vty's representation changes later.
type Color = V.Color
-- | Underline decoration variants. vty only supports a single
-- 'V.underline' style bit, so 'UnderlineSingle' is the only one rendered
-- distinctly; the others are accepted for API compatibility with Lip Gloss
-- and degrade to single underline (or none) at render time.
data UnderlineStyle
= UnderlineNone
| UnderlineSingle
| UnderlineDouble
| UnderlineCurly
| UnderlineDotted
| UnderlineDashed
deriving (Bounded, Enum, Eq, Ord, Read, Show)
-- | Horizontal alignment within the available width.
data HAlign = HAlignLeft | HAlignCenter | HAlignRight
deriving (Bounded, Enum, Eq, Ord, Read, Show)
-- | Vertical alignment within the available height.
data VAlign = VAlignTop | VAlignMiddle | VAlignBottom
deriving (Bounded, Enum, Eq, Ord, Read, Show)
-- | Padding on the four sides (top, right, bottom, left), in cells.
data Padding = Padding
{ _padding_top :: !Int
, _padding_right :: !Int
, _padding_bottom :: !Int
, _padding_left :: !Int
}
deriving (Eq, Ord, Show)
instance Default Padding where
def = Padding 0 0 0 0
-- | Margin on the four sides (top, right, bottom, left), in cells. Margin
-- is drawn outside the border using the same whitespace character as
-- padding.
data Margin = Margin
{ _margin_top :: !Int
, _margin_right :: !Int
, _margin_bottom :: !Int
, _margin_left :: !Int
}
deriving (Eq, Ord, Show)
instance Default Margin where
def = Margin 0 0 0 0
-- | The eight characters that make up a box border. 'Nothing' on a side
-- means the side is not drawn; 'Nothing' on a corner means the corner is
-- not drawn when both adjacent sides are present.
data BorderStyle = BorderStyle
{ _border_top :: !(Maybe Char)
, _border_bottom :: !(Maybe Char)
, _border_left :: !(Maybe Char)
, _border_right :: !(Maybe Char)
, _border_topLeft :: !(Maybe Char)
, _border_topRight :: !(Maybe Char)
, _border_bottomLeft :: !(Maybe Char)
, _border_bottomRight :: !(Maybe Char)
}
deriving (Eq, Ord, Show)
-- | A declarative presentational style, modelled on Lip Gloss's @Style@.
-- Every field is 'Maybe' so that 'inherit' can fill only the gaps; use the
-- 'with*' setters to populate fields and 'def' for an empty style.
data Style = Style
{ _style_foreground :: !(Maybe Color)
, _style_background :: !(Maybe Color)
, _style_bold :: !(Maybe Bool)
, _style_italic :: !(Maybe Bool)
, _style_faint :: !(Maybe Bool)
, _style_blink :: !(Maybe Bool)
, _style_strikethrough :: !(Maybe Bool)
, _style_reverse :: !(Maybe Bool)
, _style_underline :: !(Maybe UnderlineStyle)
, _style_underlineColor :: !(Maybe Color)
, _style_hyperlink :: !(Maybe Text)
, _style_padding :: !Padding
, _style_margin :: !Margin
, _style_border :: !(Maybe BorderStyle)
, _style_borderTop :: !(Maybe Bool)
, _style_borderBottom :: !(Maybe Bool)
, _style_borderLeft :: !(Maybe Bool)
, _style_borderRight :: !(Maybe Bool)
, _style_borderForeground :: !(Maybe Color)
, _style_borderBackground :: !(Maybe Color)
, _style_width :: !(Maybe Int)
, _style_height :: !(Maybe Int)
, _style_maxWidth :: !(Maybe Int)
, _style_maxHeight :: !(Maybe Int)
, _style_alignHorizontal :: !(Maybe HAlign)
, _style_alignVertical :: !(Maybe VAlign)
, _style_whitespaceChar :: !(Maybe Char)
, _style_transform :: !(Maybe (Text -> Text))
, _style_tabWidth :: !(Maybe Int)
, _style_marginBackground :: !(Maybe Color)
, _style_inline :: !(Maybe Bool)
, _style_colorWhitespace :: !(Maybe Bool)
, _style_borderTopForeground :: !(Maybe Color)
, _style_borderTopBackground :: !(Maybe Color)
, _style_borderBottomForeground :: !(Maybe Color)
, _style_borderBottomBackground :: !(Maybe Color)
, _style_borderLeftForeground :: !(Maybe Color)
, _style_borderLeftBackground :: !(Maybe Color)
, _style_borderRightForeground :: !(Maybe Color)
, _style_borderRightBackground :: !(Maybe Color)
}
instance Default Style where
def =
Style
{ _style_foreground = Nothing
, _style_background = Nothing
, _style_bold = Nothing
, _style_italic = Nothing
, _style_faint = Nothing
, _style_blink = Nothing
, _style_strikethrough = Nothing
, _style_reverse = Nothing
, _style_underline = Nothing
, _style_underlineColor = Nothing
, _style_hyperlink = Nothing
, _style_padding = def
, _style_margin = def
, _style_border = Nothing
, _style_borderTop = Nothing
, _style_borderBottom = Nothing
, _style_borderLeft = Nothing
, _style_borderRight = Nothing
, _style_borderForeground = Nothing
, _style_borderBackground = Nothing
, _style_width = Nothing
, _style_height = Nothing
, _style_maxWidth = Nothing
, _style_maxHeight = Nothing
, _style_alignHorizontal = Nothing
, _style_alignVertical = Nothing
, _style_whitespaceChar = Nothing
, _style_transform = Nothing
, _style_tabWidth = Nothing
, _style_marginBackground = Nothing
, _style_inline = Nothing
, _style_colorWhitespace = Nothing
, _style_borderTopForeground = Nothing
, _style_borderTopBackground = Nothing
, _style_borderBottomForeground = Nothing
, _style_borderBottomBackground = Nothing
, _style_borderLeftForeground = Nothing
, _style_borderLeftBackground = Nothing
, _style_borderRightForeground = Nothing
, _style_borderRightBackground = Nothing
}
-- | Fill the gaps in @child@ with values from @parent@. Only 'Nothing'
-- fields (and zero padding/margin) are inherited; non-'Nothing' fields in
-- @child@ are kept. This mirrors Lip Gloss's @Inherit@ semantics.
inherit :: Style -> Style -> Style
inherit parent child =
Style
{ _style_foreground = pick _style_foreground
, _style_background = pick _style_background
, _style_bold = pick _style_bold
, _style_italic = pick _style_italic
, _style_faint = pick _style_faint
, _style_blink = pick _style_blink
, _style_strikethrough = pick _style_strikethrough
, _style_reverse = pick _style_reverse
, _style_underline = pick _style_underline
, _style_underlineColor = pick _style_underlineColor
, _style_hyperlink = pick _style_hyperlink
, _style_padding = inheritPadding (_style_padding parent) (_style_padding child)
, _style_margin = inheritMargin (_style_margin parent) (_style_margin child)
, _style_border = pick _style_border
, _style_borderTop = pick _style_borderTop
, _style_borderBottom = pick _style_borderBottom
, _style_borderLeft = pick _style_borderLeft
, _style_borderRight = pick _style_borderRight
, _style_borderForeground = pick _style_borderForeground
, _style_borderBackground = pick _style_borderBackground
, _style_width = pick _style_width
, _style_height = pick _style_height
, _style_maxWidth = pick _style_maxWidth
, _style_maxHeight = pick _style_maxHeight
, _style_alignHorizontal = pick _style_alignHorizontal
, _style_alignVertical = pick _style_alignVertical
, _style_whitespaceChar = pick _style_whitespaceChar
, _style_transform = pick _style_transform
, _style_tabWidth = pick _style_tabWidth
, _style_marginBackground = pick _style_marginBackground
, _style_inline = pick _style_inline
, _style_colorWhitespace = pick _style_colorWhitespace
, _style_borderTopForeground = pick _style_borderTopForeground
, _style_borderTopBackground = pick _style_borderTopBackground
, _style_borderBottomForeground = pick _style_borderBottomForeground
, _style_borderBottomBackground = pick _style_borderBottomBackground
, _style_borderLeftForeground = pick _style_borderLeftForeground
, _style_borderLeftBackground = pick _style_borderLeftBackground
, _style_borderRightForeground = pick _style_borderRightForeground
, _style_borderRightBackground = pick _style_borderRightBackground
}
where
pick :: forall a. (Style -> Maybe a) -> Maybe a
pick f = case f child of
Just x -> Just x
Nothing -> f parent
inheritPadding :: Padding -> Padding -> Padding
inheritPadding parent child =
Padding
{ _padding_top = nz (_padding_top child) (_padding_top parent)
, _padding_right = nz (_padding_right child) (_padding_right parent)
, _padding_bottom = nz (_padding_bottom child) (_padding_bottom parent)
, _padding_left = nz (_padding_left child) (_padding_left parent)
}
where
nz c p = if c /= 0 then c else p
inheritMargin :: Margin -> Margin -> Margin
inheritMargin parent child =
Margin
{ _margin_top = nz (_margin_top child) (_margin_top parent)
, _margin_right = nz (_margin_right child) (_margin_right parent)
, _margin_bottom = nz (_margin_bottom child) (_margin_bottom parent)
, _margin_left = nz (_margin_left child) (_margin_left parent)
}
where
nz c p = if c /= 0 then c else p
----------------------------------------------------------------------------
-- Color constants (re-exported from vty for convenience)
----------------------------------------------------------------------------
black, red, green, yellow, blue, magenta, cyan, white :: Color
black = V.black
red = V.red
green = V.green
yellow = V.yellow
blue = V.blue
magenta = V.magenta
cyan = V.cyan
white = V.white
brightBlack, brightRed, brightGreen, brightYellow, brightBlue, brightMagenta, brightCyan, brightWhite :: Color
brightBlack = V.brightBlack
brightRed = V.brightRed
brightGreen = V.brightGreen
brightYellow = V.brightYellow
brightBlue = V.brightBlue
brightMagenta = V.brightMagenta
brightCyan = V.brightCyan
brightWhite = V.brightWhite
-- | Construct a true-color 'Color' from sRGB components. Unlike vty's
-- 'V.rgbColor' (which downsamples to the 256-color cube at construction), this
-- preserves the full 24-bit 'V.RGBColor' so the host can downsample once per
-- frame based on the detected 'Reflex.Vty.ColorProfile.ColorProfile'.
rgbColor :: Int -> Int -> Int -> Color
rgbColor = V.srgbColor
----------------------------------------------------------------------------
-- Border style presets
----------------------------------------------------------------------------
-- | Single-line box-drawing border: @┌─┐│┘─└│@.
singleBorder :: BorderStyle
singleBorder =
BorderStyle
(Just '─')
(Just '─')
(Just '│')
(Just '│')
(Just '┌')
(Just '┐')
(Just '└')
(Just '┘')
-- | Rounded-corner single-line border: @╭─╮│╯─╰│@.
roundedBorder :: BorderStyle
roundedBorder =
BorderStyle
(Just '─')
(Just '─')
(Just '│')
(Just '│')
(Just '╭')
(Just '╮')
(Just '╰')
(Just '╯')
-- | Thick single-line border: @┏━┓┃┛━┗┃@.
thickBorder :: BorderStyle
thickBorder =
BorderStyle
(Just '━')
(Just '━')
(Just '┃')
(Just '┃')
(Just '┏')
(Just '┓')
(Just '┗')
(Just '┛')
-- | Double-line border: @╔═╗║╝═╚║@.
doubleBorder :: BorderStyle
doubleBorder =
BorderStyle
(Just '═')
(Just '═')
(Just '║')
(Just '║')
(Just '╔')
(Just '╗')
(Just '╚')
(Just '╝')
-- | ASCII-only border using @-|+@.
asciiBorder :: BorderStyle
asciiBorder =
BorderStyle
(Just '-')
(Just '-')
(Just '|')
(Just '|')
(Just '+')
(Just '+')
(Just '+')
(Just '+')
-- | Markdown-table border using @-|@ with no corners.
markdownBorder :: BorderStyle
markdownBorder =
BorderStyle
(Just '-')
(Just '-')
(Just '|')
(Just '|')
Nothing
Nothing
Nothing
Nothing
-- | A border with all sides absent. Useful as a base to enable only
-- specific sides via 'withBorderTop' etc.
noBorder :: BorderStyle
noBorder = BorderStyle Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
-- | Inner half-block border using half-block characters: @▀▄▌▐@.
innerHalfBorder :: BorderStyle
innerHalfBorder =
BorderStyle
(Just '▀')
(Just '▄')
(Just '▌')
(Just '▐')
(Just '▀')
(Just '▀')
(Just '▄')
(Just '▄')
-- | Outer half-block border using lower-eighth block characters: @▔▁@.
outerHalfBlockBorder :: BorderStyle
outerHalfBlockBorder =
BorderStyle
(Just '▔')
(Just '▁')
(Just '▏')
(Just '▕')
(Just '▔')
(Just '▔')
(Just '▁')
(Just '▁')
----------------------------------------------------------------------------
-- Setters
----------------------------------------------------------------------------
withForeground :: Color -> Style -> Style
withForeground c s = s {_style_foreground = Just c}
withBackground :: Color -> Style -> Style
withBackground c s = s {_style_background = Just c}
withBold :: Style -> Style
withBold s = s {_style_bold = Just True}
withItalic :: Style -> Style
withItalic s = s {_style_italic = Just True}
withFaint :: Style -> Style
withFaint s = s {_style_faint = Just True}
withBlink :: Style -> Style
withBlink s = s {_style_blink = Just True}
withStrikethrough :: Style -> Style
withStrikethrough s = s {_style_strikethrough = Just True}
withReverse :: Style -> Style
withReverse s = s {_style_reverse = Just True}
withUnderline :: UnderlineStyle -> Style -> Style
withUnderline u s = s {_style_underline = Just u}
withUnderlineColor :: Color -> Style -> Style
withUnderlineColor c s = s {_style_underlineColor = Just c}
withHyperlink :: Text -> Style -> Style
withHyperlink url s = s {_style_hyperlink = Just url}
withPadding :: Int -> Int -> Int -> Int -> Style -> Style
withPadding t r b l s = s {_style_padding = Padding t r b l}
withPaddingTop :: Int -> Style -> Style
withPaddingTop n s = s {_style_padding = (_style_padding s) {_padding_top = n}}
withPaddingRight :: Int -> Style -> Style
withPaddingRight n s = s {_style_padding = (_style_padding s) {_padding_right = n}}
withPaddingBottom :: Int -> Style -> Style
withPaddingBottom n s = s {_style_padding = (_style_padding s) {_padding_bottom = n}}
withPaddingLeft :: Int -> Style -> Style
withPaddingLeft n s = s {_style_padding = (_style_padding s) {_padding_left = n}}
withMargin :: Int -> Int -> Int -> Int -> Style -> Style
withMargin t r b l s = s {_style_margin = Margin t r b l}
withMarginTop :: Int -> Style -> Style
withMarginTop n s = s {_style_margin = (_style_margin s) {_margin_top = n}}
withMarginRight :: Int -> Style -> Style
withMarginRight n s = s {_style_margin = (_style_margin s) {_margin_right = n}}
withMarginBottom :: Int -> Style -> Style
withMarginBottom n s = s {_style_margin = (_style_margin s) {_margin_bottom = n}}
withMarginLeft :: Int -> Style -> Style
withMarginLeft n s = s {_style_margin = (_style_margin s) {_margin_left = n}}
withBorder :: BorderStyle -> Style -> Style
withBorder b s = s {_style_border = Just b}
withBorderForeground :: Color -> Style -> Style
withBorderForeground c s = s {_style_borderForeground = Just c}
withBorderBackground :: Color -> Style -> Style
withBorderBackground c s = s {_style_borderBackground = Just c}
withBorderTop :: Bool -> Style -> Style
withBorderTop b s = s {_style_borderTop = Just b}
withBorderBottom :: Bool -> Style -> Style
withBorderBottom b s = s {_style_borderBottom = Just b}
withBorderLeft :: Bool -> Style -> Style
withBorderLeft b s = s {_style_borderLeft = Just b}
withBorderRight :: Bool -> Style -> Style
withBorderRight b s = s {_style_borderRight = Just b}
withWidth :: Int -> Style -> Style
withWidth n s = s {_style_width = Just n}
withHeight :: Int -> Style -> Style
withHeight n s = s {_style_height = Just n}
withMaxWidth :: Int -> Style -> Style
withMaxWidth n s = s {_style_maxWidth = Just n}
withMaxHeight :: Int -> Style -> Style
withMaxHeight n s = s {_style_maxHeight = Just n}
withAlignH :: HAlign -> Style -> Style
withAlignH a s = s {_style_alignHorizontal = Just a}
withAlignV :: VAlign -> Style -> Style
withAlignV a s = s {_style_alignVertical = Just a}
withWhitespace :: Char -> Style -> Style
withWhitespace c s = s {_style_whitespaceChar = Just c}
withTransform :: (Text -> Text) -> Style -> Style
withTransform fn s = s {_style_transform = Just fn}
withTabWidth :: Int -> Style -> Style
withTabWidth w s = s {_style_tabWidth = Just w}
withMarginBackground :: Color -> Style -> Style
withMarginBackground c s = s {_style_marginBackground = Just c}
withInline :: Bool -> Style -> Style
withInline b s = s {_style_inline = Just b}
withColorWhitespace :: Bool -> Style -> Style
withColorWhitespace b s = s {_style_colorWhitespace = Just b}
withBorderTopForeground :: Color -> Style -> Style
withBorderTopForeground c s = s {_style_borderTopForeground = Just c}
withBorderTopBackground :: Color -> Style -> Style
withBorderTopBackground c s = s {_style_borderTopBackground = Just c}
withBorderBottomForeground :: Color -> Style -> Style
withBorderBottomForeground c s = s {_style_borderBottomForeground = Just c}
withBorderBottomBackground :: Color -> Style -> Style
withBorderBottomBackground c s = s {_style_borderBottomBackground = Just c}
withBorderLeftForeground :: Color -> Style -> Style
withBorderLeftForeground c s = s {_style_borderLeftForeground = Just c}
withBorderLeftBackground :: Color -> Style -> Style
withBorderLeftBackground c s = s {_style_borderLeftBackground = Just c}
withBorderRightForeground :: Color -> Style -> Style
withBorderRightForeground c s = s {_style_borderRightForeground = Just c}
withBorderRightBackground :: Color -> Style -> Style
withBorderRightBackground c s = s {_style_borderRightBackground = Just c}
----------------------------------------------------------------------------
-- Rendering
----------------------------------------------------------------------------
-- | An attr where every field is 'V.KeepCurrent', so painted cells inherit
-- the underlying layer's value for any attribute the 'Style' doesn't
-- explicitly set. Used as the base for 'render' so themed backgrounds
-- show through where the 'Style' has no explicit color.
transparentAttr :: V.Attr
transparentAttr =
V.Attr
{ V.attrStyle = V.KeepCurrent
, V.attrForeColor = V.KeepCurrent
, V.attrBackColor = V.KeepCurrent
, V.attrURL = V.KeepCurrent
}
-- | Merge the color and text-transform fields of a 'Style' onto an
-- existing 'V.Attr'. Fields that are 'Nothing' in the 'Style' are left
-- unchanged (the existing 'V.Attr' value is kept). Padding, margin,
-- border, dimensions, and alignment are not handled here: those are
-- layout concerns handled by 'render'. This is used by widgets that want
-- to layer a 'Style' on top of the ambient 'HasTheme' attribute.
applyAttr :: Style -> V.Attr -> V.Attr
applyAttr s attr0 =
applyUnderline $
applyHyperlink $
applyReverse $
applyStrikethrough $
applyBlink $
applyFaint $
applyItalic $
applyBold $
applyBackground $
applyForeground attr0
where
applyForeground a = maybe a (V.withForeColor a) (_style_foreground s)
applyBackground a = maybe a (V.withBackColor a) (_style_background s)
applyFlag field st a = case field s of
Just True -> V.withStyle a st
_ -> a
applyBold = applyFlag _style_bold V.bold
applyItalic = applyFlag _style_italic V.italic
applyFaint = applyFlag _style_faint V.dim
applyBlink = applyFlag _style_blink V.blink
applyStrikethrough = applyFlag _style_strikethrough V.strikethrough
applyReverse = applyFlag _style_reverse V.reverseVideo
applyUnderline a = case _style_underline s of
Nothing -> a
Just UnderlineNone -> a
Just _ -> V.withStyle a V.underline
applyHyperlink a = maybe a (V.withURL a) (_style_hyperlink s)
-- | Overlay one 'V.Attr' on top of another. Fields in the overlay that
-- are 'V.SetTo' take precedence; 'V.Default' and 'V.KeepCurrent' fall
-- through to the base. Used by 'Reflex.Vty.Widget.Text.richText' to layer
-- custom attributes on top of the ambient theme attr, so that unset fields
-- inherit the theme rather than falling back to terminal defaults.
mergeAttr :: V.Attr -> V.Attr -> V.Attr
mergeAttr base overlay =
V.Attr
{ V.attrStyle = pick (V.attrStyle base) (V.attrStyle overlay)
, V.attrForeColor = pick (V.attrForeColor base) (V.attrForeColor overlay)
, V.attrBackColor = pick (V.attrBackColor base) (V.attrBackColor overlay)
, V.attrURL = pick (V.attrURL base) (V.attrURL overlay)
}
where
pick _ (V.SetTo v) = V.SetTo v
pick b _ = b
-- | Render some 'Text' with a 'Style' to a vty 'V.Image'. Applies text
-- transforms and colors (via 'applyAttr'), pads and aligns the content,
-- draws the border, applies margin, and finally enforces width/height
-- minimums and max-width/max-height clipping.
render :: Style -> Text -> V.Image
render s content =
if fromMaybe False (_style_inline s)
then applyMaxSize contentImage
else applyMaxSize $ applySize $ applyMargin $ applyBorder $ applyPadding $ contentImage
where
ws = fromMaybe ' ' (_style_whitespaceChar s)
-- Use KeepCurrent for all unset attrs so underlying layers (e.g. the
-- themed fill) show through where the Style doesn't explicitly set a
-- color or style.
baseAttr = applyAttr s transparentAttr
-- Attr for whitespace fills: use transparent when colorWhitespace is False.
fillAttr = if fromMaybe True (_style_colorWhitespace s) then baseAttr else transparentAttr
-- Per-side border attrs, falling back to the generic border fg/bg,
-- layered on top of the content attr.
borderAttrTop = applyAttr (borderSideStyle (_style_borderTopForeground s) (_style_borderTopBackground s)) baseAttr
borderAttrBottom = applyAttr (borderSideStyle (_style_borderBottomForeground s) (_style_borderBottomBackground s)) baseAttr
borderAttrLeft = applyAttr (borderSideStyle (_style_borderLeftForeground s) (_style_borderLeftBackground s)) baseAttr
borderAttrRight = applyAttr (borderSideStyle (_style_borderRightForeground s) (_style_borderRightBackground s)) baseAttr
borderSideStyle perSideFg perSideBg =
def
{ _style_foreground = perSideFg <|> _style_borderForeground s
, _style_background = perSideBg <|> _style_borderBackground s
}
-- Render text with newlines
contentImage = V.vertCat $ map (V.text' baseAttr) (T.split (== '\n') processedContent)
processedContent = expandTabs $ case _style_transform s of
Just fn -> fn content
Nothing -> content
expandTabs txt = case _style_tabWidth s of
Just w -> T.replace "\t" (T.replicate w " ") txt
Nothing -> txt
-- Whitespace fill of a given width/height using the whitespace char.
fillImage :: V.Attr -> Int -> Int -> V.Image
fillImage a w h
| w <= 0 || h <= 0 = V.emptyImage
| otherwise = V.charFill a ws w h
-- Horizontal alignment of an image within a width.
placeH :: V.Attr -> Int -> V.Image -> V.Image
placeH a w img
| imgW >= w = img
| otherwise = V.horizCat [leftPad, img, rightPad]
where
imgW = V.imageWidth img
imgH = V.imageHeight img
slack = w - imgW
leftPad = case fromMaybe HAlignLeft (_style_alignHorizontal s) of
HAlignLeft -> V.emptyImage
HAlignCenter -> fillImage a (slack `div` 2) imgH
HAlignRight -> fillImage a slack imgH
rightPad = case fromMaybe HAlignLeft (_style_alignHorizontal s) of
HAlignLeft -> fillImage a slack imgH
HAlignCenter -> fillImage a (slack - slack `div` 2) imgH
HAlignRight -> V.emptyImage
-- Vertical alignment of an image within a height.
placeV :: V.Attr -> Int -> V.Image -> V.Image
placeV a h img
| imgH >= h = img
| otherwise = V.vertCat [topPad, img, bottomPad]
where
imgH = V.imageHeight img
imgW = V.imageWidth img
slack = h - imgH
topPad = case fromMaybe VAlignTop (_style_alignVertical s) of
VAlignTop -> V.emptyImage
VAlignMiddle -> fillImage a imgW (slack `div` 2)
VAlignBottom -> fillImage a imgW slack
bottomPad = case fromMaybe VAlignTop (_style_alignVertical s) of
VAlignTop -> fillImage a imgW slack
VAlignMiddle -> fillImage a imgW (slack - slack `div` 2)
VAlignBottom -> V.emptyImage
-- Width/height minimums: pad the image out to the requested size.
applySize img =
let w = fromMaybe (V.imageWidth img) (_style_width s)
h = fromMaybe (V.imageHeight img) (_style_height s)
img' =
if V.imageWidth img < w
then placeH fillAttr w img
else img
img'' =
if V.imageHeight img' < h
then placeV fillAttr h img'
else img'
in img''
-- Padding.
applyPadding img =
let p = _style_padding s
top = fillImage fillAttr (innerW img) (_padding_top p)
bottom = fillImage fillAttr (innerW img) (_padding_bottom p)
left = fillImage fillAttr (_padding_left p) (innerH img)
right = fillImage fillAttr (_padding_right p) (innerH img)
innerW = V.imageWidth
innerH = V.imageHeight
in V.vertCat
[ top
, V.horizCat [left, img, right]
, bottom
]
-- Border.
applyBorder img =
case _style_border s of
Nothing -> img
Just b ->
drawBorder
borderAttrTop
borderAttrBottom
borderAttrLeft
borderAttrRight
b
(_style_borderTop s)
(_style_borderBottom s)
(_style_borderLeft s)
(_style_borderRight s)
img
-- Margin. Uses colored fill when _style_marginBackground is set,
-- otherwise transparent pad so underlying layers show through.
applyMargin img =
let m = _style_margin s
in case _style_marginBackground s of
Nothing -> V.pad (_margin_left m) (_margin_top m) (_margin_right m) (_margin_bottom m) img
Just _ ->
let marginAttr = applyAttr (borderStyleAttr s {_style_border = Nothing, _style_borderForeground = _style_marginBackground s}) baseAttr
w = V.imageWidth img
h = V.imageHeight img
leftPad = fillImage marginAttr (_margin_left m) h
rightPad = fillImage marginAttr (_margin_right m) h
topW = w + _margin_left m + _margin_right m
topPad = fillImage marginAttr topW (_margin_top m)
bottomPad = fillImage marginAttr topW (_margin_bottom m)
in V.vertCat [topPad, V.horizCat [leftPad, img, rightPad], bottomPad]
-- Max-width / max-height clipping.
applyMaxSize img =
let clipW = maybe img (\w -> V.crop w (V.imageHeight img) img) (_style_maxWidth s)
clipH = maybe clipW (\h -> V.crop (V.imageWidth clipW) h clipW) (_style_maxHeight s)
in clipH
-- | A 'Style' that carries only the border color fields, for use as the
-- 'V.Attr' when drawing border characters.
borderStyleAttr :: Style -> Style
borderStyleAttr s =
def
{ _style_foreground = _style_borderForeground s
, _style_background = _style_borderBackground s
}
-- | Draw a border around an image, respecting per-side toggles. A side is
-- drawn when the 'BorderStyle' has a character for it and the
-- per-side toggle (if set) is 'True'. When a side's toggle is 'Nothing',
-- the side is drawn iff the 'BorderStyle' defines it. Corners are drawn
-- when both adjacent sides are present and the 'BorderStyle' defines the
-- corner.
drawBorder
:: V.Attr
-- ^ top attr
-> V.Attr
-- ^ bottom attr
-> V.Attr
-- ^ left attr
-> V.Attr
-- ^ right attr
-> BorderStyle
-> Maybe Bool
-- ^ top toggle
-> Maybe Bool
-- ^ bottom toggle
-> Maybe Bool
-- ^ left toggle
-> Maybe Bool
-- ^ right toggle
-> V.Image
-> V.Image
drawBorder topA bottomA leftA rightA b mTop mBot mLeft mRight img =
V.vertCat [topRow, middleRow, bottomRow]
where
w = V.imageWidth img
topOn = sideOn _border_top mTop
bottomOn = sideOn _border_bottom mBot
leftOn = sideOn _border_left mLeft
rightOn = sideOn _border_right mRight
sideOn f mt = maybe (isJust (f b)) id mt
hFillTop c n = V.charFill topA c (max 0 n) 1
hFillBottom c n = V.charFill bottomA c (max 0 n) 1
vFillLeft c n = V.charFill leftA c 1 (max 0 n)
vFillRight c n = V.charFill rightA c 1 (max 0 n)
topChar = _border_top b >>= \c -> if topOn then Just c else Nothing
bottomChar = _border_bottom b >>= \c -> if bottomOn then Just c else Nothing
leftChar = _border_left b >>= \c -> if leftOn then Just c else Nothing
rightChar = _border_right b >>= \c -> if rightOn then Just c else Nothing
topLeftChar = _border_topLeft b >>= \c -> if topOn && leftOn then Just (V.char topA c) else Nothing
topRightChar = _border_topRight b >>= \c -> if topOn && rightOn then Just (V.char topA c) else Nothing
bottomLeftChar = _border_bottomLeft b >>= \c -> if bottomOn && leftOn then Just (V.char bottomA c) else Nothing
bottomRightChar = _border_bottomRight b >>= \c -> if bottomOn && rightOn then Just (V.char bottomA c) else Nothing
topRow =
V.horizCat $
maybe [] (: []) topLeftChar
++ maybe [] (\c -> [hFillTop c w]) topChar
++ maybe [] (: []) topRightChar
middleRow =
V.horizCat $
maybe [] (\c -> [vFillLeft c (V.imageHeight img)]) leftChar
++ [img]
++ maybe [] (\c -> [vFillRight c (V.imageHeight img)]) rightChar
bottomRow =
V.horizCat $
maybe [] (: []) bottomLeftChar
++ maybe [] (\c -> [hFillBottom c w]) bottomChar
++ maybe [] (: []) bottomRightChar
-- | Reactive variant of 'render' for the common widget case.
renderB :: Reflex t => Behavior t Style -> Behavior t Text -> Behavior t V.Image
renderB bs bt = render <$> bs <*> bt
-- | The width and height (in terminal cells) that 'render' will produce
-- for the given 'Style' and 'Text', /before/ max-width/max-height
-- clipping. Useful for layout-sensitive widgets that need to measure
-- before rendering.
measure :: Style -> Text -> (Int, Int)
measure s content =
(totalW, totalH)
where
contentLines = T.split (== '\n') processedContent
processedContent = case _style_tabWidth s of
Just w -> T.replace "\t" (T.replicate w " ") $ case _style_transform s of
Just fn -> fn content
Nothing -> content
Nothing -> case _style_transform s of
Just fn -> fn content
Nothing -> content
contentW = maximum (0 : map textWidth contentLines)
contentH = length contentLines
p = _style_padding s
m = _style_margin s
borderWidth = if hasBorder then 2 else 0
borderHeight = if hasBorder then 2 else 0
hasBorder =
maybe False (const True) (_style_border s)
|| any
id
[ fromMaybe False (_style_borderTop s)
, fromMaybe False (_style_borderBottom s)
, fromMaybe False (_style_borderLeft s)
, fromMaybe False (_style_borderRight s)
]
minW = fromMaybe 0 (_style_width s)
minH = fromMaybe 0 (_style_height s)
innerW = max minW contentW + _padding_left p + _padding_right p
innerH = max minH contentH + _padding_top p + _padding_bottom p
totalW = innerW + borderWidth + _margin_left m + _margin_right m
totalH = innerH + borderHeight + _margin_top m + _margin_bottom m
----------------------------------------------------------------------------
-- Image composition
----------------------------------------------------------------------------
-- | Join images horizontally, aligning them vertically by the given
-- 'VAlign'. Shorter images are padded with spaces to match the tallest.
joinHorizontal :: VAlign -> [V.Image] -> V.Image
joinHorizontal _ [] = V.emptyImage
joinHorizontal align imgs = V.horizCat (map pad imgs)
where
maxH = maximum (map V.imageHeight imgs)
pad img = placeVertical align maxH img
-- | Join images vertically, aligning them horizontally by the given
-- 'HAlign'. Narrower images are padded with spaces to match the widest.
joinVertical :: HAlign -> [V.Image] -> V.Image
joinVertical _ [] = V.emptyImage
joinVertical align imgs = V.vertCat (map pad imgs)
where
maxW = maximum (map V.imageWidth imgs)
pad img = placeHorizontal align maxW img
-- | Place an image within a target width, horizontally aligned.
-- Pads with spaces on the appropriate side(s).
placeHorizontal :: HAlign -> Int -> V.Image -> V.Image
placeHorizontal align targetW img
| imgW >= targetW = img
| otherwise = V.horizCat [leftPad, img, rightPad]
where
imgW = V.imageWidth img
slack = targetW - imgW
(leftW, rightW) = case align of
HAlignLeft -> (0, slack)
HAlignCenter -> (slack `div` 2, slack - slack `div` 2)
HAlignRight -> (slack, 0)
leftPad = padImg leftW (V.imageHeight img)
rightPad = padImg rightW (V.imageHeight img)
-- | Place an image within a target height, vertically aligned.
-- Pads with spaces above and/or below.
placeVertical :: VAlign -> Int -> V.Image -> V.Image
placeVertical align targetH img
| imgH >= targetH = img
| otherwise = V.vertCat [topPad, img, bottomPad]
where
imgH = V.imageHeight img
slack = targetH - imgH
(topH, bottomH) = case align of
VAlignTop -> (0, slack)
VAlignMiddle -> (slack `div` 2, slack - slack `div` 2)
VAlignBottom -> (slack, 0)
topPad = padImg (V.imageWidth img) topH
bottomPad = padImg (V.imageWidth img) bottomH
-- | Place an image within a target @(width, height)@ with both
-- horizontal and vertical alignment.
place :: HAlign -> VAlign -> Int -> Int -> V.Image -> V.Image
place hAlign vAlign targetW targetH =
placeHorizontal hAlign targetW . placeVertical vAlign targetH
padImg :: Int -> Int -> V.Image
padImg w h
| w <= 0 || h <= 0 = V.emptyImage
| otherwise = V.charFill V.defAttr ' ' w h
----------------------------------------------------------------------------
-- Text utilities
----------------------------------------------------------------------------
-- | Truncate text to fit within a display width, using the given
-- character as an ellipsis if truncation is needed. Wide characters
-- are handled correctly via 'textWidth'.
truncateWith :: Char -> Int -> Text -> Text
truncateWith ellipsis maxWidth txt
| textWidth txt <= maxWidth = txt
| maxWidth <= 0 = ""
| otherwise = takeToWidth (maxWidth - charWidth ellipsis) txt <> T.singleton ellipsis
-- | Number of lines in the text (counting newlines + 1).
textHeight :: Text -> Int
textHeight = length . T.split (== '\n')
-- | Display width and height of the text.
textSize :: Text -> (Int, Int)
textSize txt = (maximum (0 : map textWidth (T.split (== '\n') txt)), textHeight txt)
----------------------------------------------------------------------------
-- Internal helpers for truncateWith
----------------------------------------------------------------------------
takeToWidth :: Int -> Text -> Text
takeToWidth targetW txt = go 0 (T.unpack txt)
where
go _ [] = ""
go w (c : cs)
| w >= targetW = ""
| w + cw > targetW = ""
| otherwise = T.singleton c <> go (w + cw) cs
where
cw = max 1 (charWidth c)