blank-canvas-0.6: Graphics/Blank/Types/Font.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
module Graphics.Blank.Types.Font where
import Control.Applicative
import Control.Monad
import Data.Char
import Data.Default.Class
import Data.Ix (Ix)
import Data.Maybe
import Data.Monoid
import Data.String
import qualified Data.Text as TS
import Data.Text (Text)
import qualified Data.Text.Lazy.Builder as B (singleton)
import Graphics.Blank.JavaScript
import Graphics.Blank.Parser
import Graphics.Blank.Types
import Graphics.Blank.Types.CSS
import qualified Text.ParserCombinators.ReadP as ReadP
import Text.ParserCombinators.ReadP hiding ((<++), choice, pfail)
import qualified Text.ParserCombinators.ReadPrec as ReadPrec
import Text.ParserCombinators.ReadPrec (ReadPrec, (<++), lift, pfail)
import Text.Read (Read(..), readListPrecDefault)
import TextShow (TextShow(..), Builder, FromTextShow(..), showbSpace)
-------------------------------------------------------------------------------
-- | A data type that can represent a browser font.
class CanvasFont a where
-- | Convert a value into a JavaScript string representing a font value.
jsCanvasFont :: a -> Builder
instance CanvasFont Text where
jsCanvasFont = jsText
instance CanvasFont Font where
jsCanvasFont = jsFont
-------------------------------------------------------------------------------
-- | A CSS-style font data type.
data Font = FontProperties
{ fontStyle :: FontStyle
, fontVariant :: FontVariant
, fontWeight :: FontWeight
, fontSize :: FontSize
, lineHeight :: LineHeight
, fontFamily :: [FontFamily]
} -- ^ A font specified by its individual longhand properties.
| CaptionFont -- ^ The font used for captioned controls (e.g., buttons, drop-downs, etc.)
| IconFont -- ^ The font used to label icons.
| MenuFont -- ^ The font used in menus (e.g., dropdown menus and menu lists).
| MessageBoxFont -- ^ The font used in dialog boxes.
| SmallCaptionFont -- ^ The font used for labeling small controls.
| StatusBarFont -- ^ The font used in window status bars.
deriving (Eq, Ord)
-- |
-- Creates a new font from the 'FontFamily' list, using the 'Default' instances
-- for the other five longhand properties. If you only wish to change certain
-- properties and leave the others alone, this provides a convenient mechanism
-- for doing so:
--
-- @
-- ('defFont' ["Gill Sans Extrabold", 'sansSerif']) {
-- 'fontStyle' = 'italic'
-- , 'fontSize' = 12 # 'px'
-- , 'lineHeight' = 14 # 'px'
-- }
-- @
defFont :: [FontFamily] -> Font
defFont = FontProperties def def def def def
-- | Shorthand for 'CaptionFont'.
caption :: Font
caption = CaptionFont
-- | Shorthand for 'IconFont'.
icon :: Font
icon = IconFont
-- | Shorthand for 'MenuFont'.
menu :: Font
menu = MenuFont
-- | Shorthand for 'MessageBoxFont'.
messageBox :: Font
messageBox = MessageBoxFont
-- | Shorthand for 'SmallCaptionFont'.
smallCaption :: Font
smallCaption = SmallCaptionFont
-- | Shorthand for 'StatusBarFont'.
statusBar :: Font
statusBar = StatusBarFont
instance IsString Font where
fromString = read
instance JSArg Font where
showbJS = jsFont
jsFont :: Font -> Builder
jsFont = jsLiteralBuilder . showb
instance Read Font where
readPrec = do
lift skipSpaces
ReadPrec.choice
[ CaptionFont <$ lift (stringCI "caption")
, IconFont <$ lift (stringCI "icon")
, MenuFont <$ lift (stringCI "menu")
, MessageBoxFont <$ lift (stringCI "message-box")
, SmallCaptionFont <$ lift (stringCI "small-caption")
, StatusBarFont <$ lift (stringCI "status-bar")
, readFontProperties Nothing Nothing Nothing
]
readListPrec = readListPrecDefault
-- | Like 'Either', but with three possibilities instead of two.
data OneOfThree a b c = One a | Two b | Three c
-- |
-- The formal syntax for the font CSS property
-- (https://developer.mozilla.org/en-US/docs/Web/CSS/font#Syntax) is surprisingly complex.
-- It requires that font-style, font-variant, and font-weight must be defined, if any,
-- before the font-size value. Furthermore, each of those three properties may only be defined at
-- most once, and the relative order of the three does not matter. This is a tall order for the
-- Text.ParserCombinators modules, so we use a heavily monadic utility function to detect
-- make it easier to catch bad input. The three Maybe arguments each represent whether its
-- respective property has not (Nothing) or has (Just) been read. If it has been read, then
-- readFontProperties will not attempt to parse it again.
--
-- readFontProperties will proceed to parse the remaining Font longhand properties once
-- either all three of the first properties have been parsed, or when it is unsuccessful at
-- parsing any of the first three properties.
readFontProperties :: Maybe FontStyle -> Maybe FontVariant -> Maybe FontWeight -> ReadPrec Font
readFontProperties style variant weight =
-- If all three properties have been parsed, proceed to the remaining three properties.
if isJust style && isJust variant && isJust weight
then readFontProperties' style variant weight
else do
-- If the property has already been parsed, do not parse it again.
let parseCheck :: Maybe a -> ReadPrec a -> ReadPrec a
parseCheck mb parser = if isJust mb then pfail else parser
readStyle, readVariant, readWeight :: ReadPrec (OneOfThree FontStyle FontVariant FontWeight)
readStyle = One <$> parseCheck style readPrec
readVariant = Two <$> parseCheck variant readPrec
readWeight = Three <$> parseCheck weight readPrec
-- First attempt to parse font-style, then font-variant, then font-weight (unless one
-- of them has already been parsed, in which case skip to the next property parser.
prop <- maybeReadPrec $ readStyle <++ readVariant <++ readWeight
-- Check to see which property, if any, was parsed.
case prop of
Just (One style') -> do
when (isJust style) pfail -- Safeguard to ensure a property is not parsed twice.
readFontProperties (Just style') variant weight
Just (Two variant') -> do
when (isJust variant) pfail
readFontProperties style (Just variant') weight
Just (Three weight') -> do
when (isJust weight) pfail
readFontProperties style variant (Just weight')
-- If no properties were parsed, proceed to the remaining three properties.
Nothing -> readFontProperties' style variant weight
-- |
-- Parses the remaining three Font longhand properties (font-size, line-height, and
-- font-family). Make sure to also parse the forward slash, if any, that separates
-- the font-size and line-height properties.
readFontProperties' :: Maybe FontStyle -> Maybe FontVariant -> Maybe FontWeight -> ReadPrec Font
readFontProperties' mbStyle mbVariant mbWeight =
FontProperties (fromMaybe def mbStyle) (fromMaybe def mbVariant) (fromMaybe def mbWeight)
<$> readPrec
<*> lift (option def $ skipSpaces *> char '/' *> unlift readPrec)
<*> (lift (munch1 isSpace) *> readPrec)
instance Show Font where
showsPrec p = showsPrec p . FromTextShow
instance TextShow Font where
showb (FontProperties style variant weight size height' family)
= showb style
<> showbSpace
<> showb variant
<> showbSpace
<> showb weight
<> showbSpace
<> showb size
<> B.singleton '/'
<> showb height'
<> showbSpace
<> showb family
showb CaptionFont = "caption"
showb IconFont = "icon"
showb MenuFont = "menu"
showb MessageBoxFont = "message-box"
showb SmallCaptionFont = "small-caption"
showb StatusBarFont = "status-bar"
-------------------------------------------------------------------------------
-- | Specifies if a 'Font' is italic or oblique.
data FontStyle = NormalStyle -- ^ Selects a font classified as normal (default).
| ItalicStyle -- ^ Selects a font that is labeled italic, or if one is not available,
-- one labeled oblique.
| ObliqueStyle -- ^ Selects a font that is labeled oblique.
deriving (Bounded, Enum, Eq, Ix, Ord)
-- | Shorthand for 'ItalicStyle'.
italic :: FontStyle
italic = ItalicStyle
-- | Shorthand for 'ObliqueStyle'.
oblique :: FontStyle
oblique = ObliqueStyle
instance Default FontStyle where
def = NormalStyle
instance IsString FontStyle where
fromString = read
instance NormalProperty FontStyle
instance Read FontStyle where
readPrec = lift $ do
skipSpaces
ReadP.choice
[ NormalStyle <$ stringCI "normal"
, ItalicStyle <$ stringCI "italic"
, ObliqueStyle <$ stringCI "oblique"
]
readListPrec = readListPrecDefault
instance Show FontStyle where
showsPrec p = showsPrec p . FromTextShow
instance TextShow FontStyle where
showb NormalStyle = "normal"
showb ItalicStyle = "italic"
showb ObliqueStyle = "oblique"
-------------------------------------------------------------------------------
-- | Specifies the face of a 'Font'.
data FontVariant = NormalVariant -- ^ A normal font face (default).
| SmallCapsVariant -- ^ A font face with small capital letters for lowercase characters.
deriving (Bounded, Enum, Eq, Ix, Ord)
-- | Shorthand for 'SmallCapsVariant'.
smallCaps :: FontVariant
smallCaps = SmallCapsVariant
instance Default FontVariant where
def = NormalVariant
instance IsString FontVariant where
fromString = read
instance NormalProperty FontVariant
instance Read FontVariant where
readPrec = lift $ do
skipSpaces
(NormalVariant <$ stringCI "normal") <|> (SmallCapsVariant <$ stringCI "small-caps")
readListPrec = readListPrecDefault
instance Show FontVariant where
showsPrec p = showsPrec p . FromTextShow
instance TextShow FontVariant where
showb NormalVariant = "normal"
showb SmallCapsVariant = "small-caps"
-------------------------------------------------------------------------------
-- |
-- Specifies the boldness of a 'Font'. Note that 'FontWeight' is an instance of
-- 'Num' so that the nine numeric weights can be used directly. For example:
--
-- @
-- ('defFont' ['sansSerif']) { 'fontWeight' = 900 }
-- @
--
-- Attempting to use a numeric weight other than the nine given will result in
-- a runtime error.
data FontWeight = NormalWeight -- ^ Default.
| BoldWeight
| BolderWeight
| LighterWeight
| Weight100
| Weight200
| Weight300
| Weight400
| Weight500
| Weight600
| Weight700
| Weight800
| Weight900
deriving (Bounded, Enum, Eq, Ix, Ord)
-- | Shorthand for 'BoldWeight'.
bold :: FontWeight
bold = BoldWeight
-- | Shorthand for 'BolderWeight'.
bolder :: FontWeight
bolder = BolderWeight
-- | Shorthand for 'LighterWeight'.
lighter :: FontWeight
lighter = LighterWeight
fontWeightError :: a
fontWeightError = error "invalid font-weight operation"
instance Default FontWeight where
def = NormalWeight
instance IsString FontWeight where
fromString = read
instance NormalProperty FontWeight
instance Num FontWeight where
(+) = fontWeightError
(-) = fontWeightError
(*) = fontWeightError
abs = fontWeightError
signum = fontWeightError
fromInteger 100 = Weight100
fromInteger 200 = Weight200
fromInteger 300 = Weight300
fromInteger 400 = Weight400
fromInteger 500 = Weight500
fromInteger 600 = Weight600
fromInteger 700 = Weight700
fromInteger 800 = Weight800
fromInteger 900 = Weight900
fromInteger _ = fontWeightError
instance Read FontWeight where
readPrec = lift $ do
skipSpaces
ReadP.choice
[ NormalWeight <$ stringCI "normal"
, BoldWeight <$ stringCI "bold"
, BolderWeight <$ stringCI "bolder"
, LighterWeight <$ stringCI "lighter"
, Weight100 <$ string "100"
, Weight200 <$ string "200"
, Weight300 <$ string "300"
, Weight400 <$ string "400"
, Weight500 <$ string "500"
, Weight600 <$ string "600"
, Weight700 <$ string "700"
, Weight800 <$ string "800"
, Weight900 <$ string "900"
]
readListPrec = readListPrecDefault
instance Show FontWeight where
showsPrec p = showsPrec p . FromTextShow
instance TextShow FontWeight where
showb NormalWeight = "normal"
showb BoldWeight = "bold"
showb BolderWeight = "bolder"
showb LighterWeight = "lighter"
showb Weight100 = "100"
showb Weight200 = "200"
showb Weight300 = "300"
showb Weight400 = "400"
showb Weight500 = "500"
showb Weight600 = "600"
showb Weight700 = "700"
showb Weight800 = "800"
showb Weight900 = "900"
-------------------------------------------------------------------------------
-- | The desired height of 'Font' glyphs.
--
-- ==== __Examples__
--
-- @
-- ('defFont' ['sansSerif']) { 'fontSize' = 'xxSmall' }
-- ('defFont' ['sansSerif']) { 'fontSize' = 30 # 'pt' }
-- ('defFont' ['sansSerif']) { 'fontSize' = 50 # 'percent' }
-- @
data FontSize = XXSmallSize
| XSmallSize
| SmallSize
| MediumSize -- ^ Default.
| LargeSize
| XLargeSize
| XXLargeSize
| LargerSize
| SmallerSize
| FontSizeLength Length
| FontSizePercentage Percentage
deriving (Eq, Ord)
-- | Shorthand for 'XXSmallSize'.
xxSmall :: FontSize
xxSmall = XXSmallSize
-- | Shorthand for 'XSmallSize'.
xSmall :: FontSize
xSmall = XSmallSize
-- | Shorthand for 'SmallSize'.
small :: FontSize
small = SmallSize
-- | Shorthand for 'MediumSize'.
medium :: FontSize
medium = MediumSize
-- | Shorthand for 'LargeSize'.
large :: FontSize
large = LargeSize
-- | Shorthand for 'XLargeSize'.
xLarge :: FontSize
xLarge = XLargeSize
-- | Shorthand for 'XXLargeSize'.
xxLarge :: FontSize
xxLarge = XXLargeSize
-- | Shorthand for 'LargerSize'.
larger :: FontSize
larger = LargerSize
-- | Shorthand for 'SmallerSize'.
smaller :: FontSize
smaller = SmallerSize
instance Default FontSize where
def = MediumSize
instance IsString FontSize where
fromString = read
instance LengthProperty FontSize where
fromLength = FontSizeLength
instance PercentageProperty FontSize where
percent = FontSizePercentage
instance Read FontSize where
readPrec = do
lift $ skipSpaces
ReadPrec.choice
[ XXSmallSize <$ lift (stringCI "xx-small")
, XSmallSize <$ lift (stringCI "x-small")
, SmallSize <$ lift (stringCI "small")
, MediumSize <$ lift (stringCI "medium")
, LargeSize <$ lift (stringCI "large")
, XLargeSize <$ lift (stringCI "x-large")
, XXLargeSize <$ lift (stringCI "xx-large")
, LargerSize <$ lift (stringCI "larger")
, SmallerSize <$ lift (stringCI "smaller")
, FontSizeLength <$> readPrec
, FontSizePercentage <$> readPrec <* lift (char '%')
]
readListPrec = readListPrecDefault
instance Show FontSize where
showsPrec p = showsPrec p . FromTextShow
instance TextShow FontSize where
showb XXSmallSize = "xx-small"
showb XSmallSize = "x-small"
showb SmallSize = "small"
showb MediumSize = "medium"
showb LargeSize = "large"
showb XLargeSize = "x-large"
showb XXLargeSize = "xx-large"
showb LargerSize = "larger"
showb SmallerSize = "smaller"
showb (FontSizeLength l) = showb l
showb (FontSizePercentage p) = jsDouble p <> B.singleton '%'
-------------------------------------------------------------------------------
-- | The height of the line boxes in a 'Font'.
--
-- ==== __Examples__
--
-- @
-- ('defFont' ['sansSerif']) { 'lineHeight' = 'normal' }
-- ('defFont' ['sansSerif']) { 'lineHeight' = 50 }
-- ('defFont' ['sansSerif']) { 'lineHeight' = 30 # 'em' }
-- ('defFont' ['sansSerif']) { 'lineHeight' = 70 # 'percent' }
-- @
data LineHeight = NormalLineHeight -- ^ Default.
| LineHeightNumber Double
| LineHeightLength Length
| LineHeightPercentage Percentage
deriving (Eq, Ord)
lineHeightError :: a
lineHeightError = error "no arithmetic for line-height"
instance Default LineHeight where
def = NormalLineHeight
instance Fractional LineHeight where
(/) = lineHeightError
recip = lineHeightError
fromRational = LineHeightNumber . fromRational
instance IsString LineHeight where
fromString = read
instance LengthProperty LineHeight where
fromLength = LineHeightLength
instance NormalProperty LineHeight
instance Num LineHeight where
(+) = lineHeightError
(-) = lineHeightError
(*) = lineHeightError
abs = lineHeightError
signum = lineHeightError
fromInteger = LineHeightNumber . fromInteger
instance PercentageProperty LineHeight where
percent = LineHeightPercentage
instance Read LineHeight where
readPrec = do
lift skipSpaces
ReadPrec.choice
[ NormalLineHeight <$ lift (stringCI "normal")
, LineHeightNumber <$> readPrec
, LineHeightLength <$> readPrec
, LineHeightPercentage <$> readPrec <* lift (char '%')
]
readListPrec = readListPrecDefault
instance Show LineHeight where
showsPrec p = showsPrec p . FromTextShow
instance TextShow LineHeight where
showb NormalLineHeight = "normal"
showb (LineHeightNumber n) = jsDouble n
showb (LineHeightLength l) = showb l
showb (LineHeightPercentage p) = jsDouble p <> B.singleton '%'
-------------------------------------------------------------------------------
-- |
-- The name of a 'Font' family. Note that both 'FontFamily' and @['FontFamily']@
-- are instances of 'IsString', so it is possible to produce 'FontFamily' values
-- in several different ways. For example, these are all of type 'FontFamily':
--
-- @
-- 'FontFamilyName' "Gill Sans Extrabold"
-- "Gill Sans Extrabold" :: 'FontFamily'
-- 'serif'
-- "serif" :: 'FontFamily'
-- @
--
-- These are all of type @['FontFamily']@:
--
-- @
-- ['FontFamilyName' \"Helvetica\", 'serif']
-- [\"Helvetica\", "serif"] :: ['FontFamily']
-- "Helvetica, serif" :: ['FontFamily']
-- @
data FontFamily = FontFamilyName Text -- ^ The name of a custom font family.
| SerifFamily -- ^ A generic font family where glyphs have
-- serifed endings.
| SansSerifFamily -- ^ A generic font family where glyphs do not
-- have serifed endings.
| MonospaceFamily -- ^ A generic font family where all glyphs have
-- the same fixed width.
| CursiveFamily -- ^ A generic font family with cursive glyphs.
| FantasyFamily -- ^ A generic font family where glyphs have
-- decorative, playful representations.
deriving (Eq, Ord)
-- | Shorthand for 'SerifFamily'.
serif :: FontFamily
serif = SerifFamily
-- | Shorthand for 'SansSerifFamily'.
sansSerif :: FontFamily
sansSerif = SansSerifFamily
-- | Shorthand for 'MonospaceFamily'.
monospace :: FontFamily
monospace = MonospaceFamily
-- | Shorthand for 'CursiveFamily'.
cursive :: FontFamily
cursive = CursiveFamily
-- | Shorthand for 'FantasyFamily'.
fantasy :: FontFamily
fantasy = FantasyFamily
instance IsString FontFamily where
fromString = read
-- |
-- There are two separate 'IsString' instances for 'FontFamily' so that single font
-- families and lists of font families alike can be converted from string literals.
instance IsString [FontFamily] where
fromString = read
instance Read FontFamily where
readPrec = lift $ do
skipSpaces
ReadP.choice
[ SerifFamily <$ stringCI "serif"
, SansSerifFamily <$ stringCI "sans-serif"
, MonospaceFamily <$ stringCI "monospace"
, CursiveFamily <$ stringCI "cursive"
, FantasyFamily <$ stringCI "fantasy"
, let quoted quote = between (char quote) (char quote)
in quoted '"' (readFontFamily $ Just '"')
<|> quoted '\'' (readFontFamily $ Just '\'')
<|> readFontFamily Nothing
]
-- readListPrec is overloaded so that it will read in a comma-separated list of
-- family names not delimited by square brackets, as per the CSS syntax.
readListPrec = lift . sepBy1 (unlift readPrec) $ skipSpaces *> char ','
readFontFamily :: Maybe Char -> ReadP FontFamily
readFontFamily mQuote = do
name <- case mQuote of
Just quote -> munch (/= quote)
Nothing -> unwords <$> sepBy1 cssIdent (munch1 isSpace)
return . FontFamilyName $ TS.pack name
instance Show FontFamily where
showsPrec p = showsPrec p . FromTextShow
showList = showsPrec 0 . FromTextShow
instance TextShow FontFamily where
showb (FontFamilyName name) = showb name
showb SerifFamily = "serif"
showb SansSerifFamily = "sans-serif"
showb MonospaceFamily = "monospace"
showb CursiveFamily = "cursive"
showb FantasyFamily = "fantasy"
-- Omit the square brackets when showing a list of font families so that
-- it matches the CSS syntax.
showbList = jsList showb
-------------------------------------------------------------------------------
-- | A convenient way to use the 'Default' normal value for several 'Font'
-- longhand properties.
class Default a => NormalProperty a where
-- | The default value for a CSS property. For example, it can be used
-- like this:
--
-- @
-- ('defFont' ['sansSerif']) { 'lineHeight' = 'normal' }
-- @
normal :: a
normal = def