{-# 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