{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE NoImplicitPrelude #-} {-# 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.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 Prelude.Compat 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