{-# LANGUAGE DataKinds #-} {-# LANGUAGE NoStarIsType #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE CPP #-} {-# LANGUAGE AllowAmbiguousTypes #-} -- We need this to have correct links inside haddocks {-# OPTIONS_GHC -Wno-unused-imports #-} -- Required to use `showType` inside haddock coments {-# OPTIONS_GHC -Wno-unused-top-binds #-} {- | = Introduction and intended usage This library contains a small number of helpers that primarily aim to help users construct `IsLabel` instances to use @-XOverloadedLabels@ syntax to construct their color types using widely known web color syntax. There are two ways to use this API: 1. Simple type-class-based API 2. Advanced manual type-family-based API You should always prefer the simple API. == Simple API You should use the simple API to write an `IsLabel` instance for your own type representing a color. Depending on whether your type supports representing the alpha channel, you should choose between `IsWebColor` and `IsWebColorAlpha` type classes. Therefore, it's likely that you want to write an instance that looks like this: @ data RGBA = MkRGBA { red :: `Word8`, green :: `Word8`, blue :: `Word8`, alpha :: `Word8` } instance `IsWebColorAlpha` s => `IsLabel` s RGBA where `fromLabel` = `webColorAlpha` @s \r g b a -> MkRGBA { red = r, green = g, blue = b, alpha = a } @ Or this in case your type doesn't have an alpha channel: @ data RGB = MkRGB { red :: `Word8`, green :: `Word8`, blue :: `Word8` } instance `IsWebColor` s => `IsLabel` s RGB where `fromLabel` = `webColor` @s \r g b -> MkRGB { red = r, green = g, blue = b } @ Please do not try to use `IsWebColor` if your type can represent either RGB or RGBA colors; use `IsWebColorAlpha` and alternate data construction depending on the value of the alpha channel: @ data Color = MkRGB { red :: `Word8`, green :: `Word8`, blue :: `Word8` } | MkRGBA { red :: `Word8`, green :: `Word8`, blue :: `Word8`, alpha :: `Word8` } instance `IsWebColorAlpha` s => `IsLabel` s RGBA where `fromLabel` = `webColorAlpha` @s \r g b a -> if a `==` 255 then MkRGB { red = r, green = g, blue = b } else MkRGBA { red = r, green = g, blue = b, alpha = a } @ == Advanced API It's always recommended to use the simple API unless you want to drop named colors for some reason or want to work with raw `KnownNat`s. There are four main type families: * `ParseWebColor` * `ParseWebColorAlpha` * `ParseWebColorMaybeAlpha` * `ParseHexadecimalColor` The first two correspond to the output of `IsWebColor` and `IsWebColorAlpha` type classes listed in the previous section. `ParseWebColorMaybeAlpha` allows you to alternate behavior between presence and absence of the alpha channel in the input. Please let me know if you have a use case for this and why you can't simply consider 255 alpha as an RGB color. `ParseHexadecimalColor` converts hexadecimal strings of length 3, 4, 6, and 8 into correct color channels but doesn't accept named colors. In general, you give a parsing function a symbol argument and match results using the equality operator: @ f :: `ParseWebColorAlpha` s ~ '(r,g,b,a) => ... @ After that, you can set various constraints on the results. For example, this is how you can convert given values into a 32-bit number without any runtime computations: @ {-# LANGUAGE DataKinds, NoStarIsType, AllowAmbiguousTypes #-} f :: forall s r g b a w32. `ParseWebColorAlpha` s ~ '(r,g,b,a) => ((r * 0x100 + g) * 0x100 + b) * 0x100 + a ~ w32 => `KnownNat` w32 => `Word32` f = `fromInteger` `$` `natVal` (`Proxy` @w32) @ == To advanced end users If you're an advanced Haskell user. If you think that rules are just "recommendations." If you're ready to face possible breakages just for slight usability improvements. Then I present you a footgun. It is possible to write orphan instances for `IsWebColor` and `IsWebColorAlpha` type classes to extend #-syntax with custom named colors, just like this: @ instance `IsWebColor` "gold" where `webColor` k = k 255 215 0 instance `IsWebColorAlpha` "gold" where `webColorAlpha` k = k 255 215 0 255 @ I'm not going to guarantee you anything. Enjoy! -} module WebColor.Labels ( -- * Primary API IsWebColor(..), IsWebColorAlpha(..), -- * Advanced type-level API WebColor, WebColorAlpha, ParseWebColor, ParseWebColorAlpha, WebColorParsed, ParseWebColorMaybeAlpha, ParseHexadecimalColor, -- * Utility type families -- -- | These type families aren't required to use -- the library, but may be useful for defining your -- own parse routing on top of web colors ParseHexadecimalChar, ) where import Data.Proxy (Proxy (..)) #if __GLASGOW_HASKELL__ >= 906 import Data.Type.Equality (type (~)) #endif import Data.Word (Word8, Word32) import GHC.TypeLits (ErrorMessage (..), KnownNat, Nat, Symbol, TypeError, UnconsSymbol, natVal, type (*), type (+)) import Prelude (Char, Maybe (..), fromInteger, ($!), ($), (==)) import Data.Kind (Constraint) -- Imports for haddocks import GHC.OverloadedLabels (IsLabel(..)) #if __GLASGOW_HASKELL__ >= 908 import GHC.TypeError (Unsatisfiable) showType :: forall a. (Unsatisfiable (ShowType a)) => () showType :: forall {t} (a :: t). Unsatisfiable ('ShowType a) => () showType = () #endif -- | Red, green, blue, and optional alpha color channels in range from 0 to 255 type WebColorParsed = (Nat, Nat, Nat, Maybe Nat) -- | Parse a string containing a named color from a basic -- palette or hexadecimal representation with an optional -- alpha channel. -- -- * Named colors are always considered RGB-only: -- -- >>> showType @(`ParseWebColorMaybeAlpha` "red") -- '(255, 0, 0, 'Nothing) -- -- >>> showType @(`ParseWebColorMaybeAlpha` "silver") -- '(192, 192, 192, 'Nothing) -- -- `ParseWebColorMaybeAlpha` inherits many properties from `ParseHexadecimalColor`: -- -- * The difference between a `Nothing` alpha channel and @`Just` 255@ -- is purely syntactical, and you have to tackle the difference yourself -- * This family automatically handles shortened syntax -- * This family throws a compile-time error when the wrong number -- of hex characters is supplied -- -- Please refer to `ParseHexadecimalColor` for the examples. type ParseWebColorMaybeAlpha :: Symbol -> WebColorParsed type family ParseWebColorMaybeAlpha s where ParseWebColorMaybeAlpha "white" = '(255, 255, 255, Nothing) ParseWebColorMaybeAlpha "silver" = '(192, 192, 192, Nothing) ParseWebColorMaybeAlpha "gray" = '(128, 128, 128, Nothing) ParseWebColorMaybeAlpha "black" = '(0, 0, 0, Nothing) ParseWebColorMaybeAlpha "red" = '(255, 0, 0, Nothing) ParseWebColorMaybeAlpha "maroon" = '(128, 0, 0, Nothing) ParseWebColorMaybeAlpha "yellow" = '(255, 255, 0, Nothing) ParseWebColorMaybeAlpha "olive" = '(128, 128, 0, Nothing) ParseWebColorMaybeAlpha "lime" = '(0, 255, 0, Nothing) ParseWebColorMaybeAlpha "green" = '(0, 128, 0, Nothing) ParseWebColorMaybeAlpha "aqua" = '(0, 255, 255, Nothing) ParseWebColorMaybeAlpha "teal" = '(0, 128, 128, Nothing) ParseWebColorMaybeAlpha "blue" = '(0, 0, 255, Nothing) ParseWebColorMaybeAlpha "navy" = '(0, 0, 128, Nothing) ParseWebColorMaybeAlpha "fuchsia" = '(255, 0, 255, Nothing) ParseWebColorMaybeAlpha "purple" = '(128, 0, 128, Nothing) ParseWebColorMaybeAlpha s = ParseHexadecimalColor s -- | Parse a string containing hexadecimal representation of a -- color with an optional alpha channel. -- -- * This family doesn't handle named colors. -- * The difference between a `Nothing` alpha channel and @`Just` 255@ -- is purely syntactical, and you have to tackle the difference yourself: -- -- >>> showType @(`ParseHexadecimalColor` "fafefb") -- '(250, 254, 251, 'Nothing) -- -- >>> showType @(`ParseHexadecimalColor` "fafefbff") -- '(250, 254, 251, 'Just 255) -- -- * This family automatically handles shortened syntax: -- -- E.g. #123 is the same as #112233: -- -- >>> showType @(`ParseHexadecimalColor` "123") -- '(17, 34, 51, 'Nothing) -- -- >>> showType @(`ParseHexadecimalColor` "112233") -- '(17, 34, 51, 'Nothing) -- -- The same applies to #1234 and #11223344: -- -- >>> showType @(`ParseHexadecimalColor` "1234") -- '(17, 34, 51, 'Just 68) -- -- >>> showType @(`ParseHexadecimalColor` "11223344") -- '(17, 34, 51, 'Just 68) -- -- * This family throws a compile-time error when the wrong number -- of hex characters is supplied: -- -- >>> showType @(`ParseHexadecimalColor` "1") -- Unexpected number of hex codes -- expected 3, 4, 6 or 8 -- -- >>> showType @(`ParseHexadecimalColor` "1122334455") -- Unexpected number of hex codes -- expected 3, 4, 6 or 8 type ParseHexadecimalColor :: Symbol -> WebColorParsed type family ParseHexadecimalColor s where ParseHexadecimalColor s = ParseColorRec '[] (UnconsSymbol s) type ParseColorRec :: [Nat] -> Maybe (Char, Symbol) -> WebColorParsed type family ParseColorRec color str where ParseColorRec colors Nothing = UpgradeColor colors ParseColorRec colors (Just '(ch, t)) = ParseColorRec (ParseHexadecimalChar ch : colors) (UnconsSymbol t) -- | Red, green, and blue color channels in range from 0 to 255 type WebColor = (Nat, Nat, Nat) -- | Parse a string containing a named color from a basic -- palette or hexadecimal representation without an alpha channel. -- -- * For convenience, a 255 alpha channel is interpreted -- as a solid color without an alpha channel: -- -- >>> showType @(`ParseWebColor` "123") -- '(17, 34, 51) -- -- >>> showType @(`ParseWebColor` "123f") -- '(17, 34, 51) -- -- * Other alpha channel values result in a compile-time error: -- -- >>> showType @(`ParseWebColor` "123d") -- Unexpected alpha channel! RGB color expected -- -- This family inherits many properties from `ParseWebColorMaybeAlpha`: -- -- * Named colors are always considered RGB-only: -- -- >>> showType @(`ParseWebColor` "red") -- '(255, 0, 0) -- -- * This family automatically handles shortened syntax. -- * This family throws a compile-time error when the wrong number -- of hex characters is supplied type ParseWebColor :: Symbol -> WebColor type family ParseWebColor s where ParseWebColor s = ParseRGBColorWorker (ParseWebColorMaybeAlpha s) type ParseRGBColorWorker :: WebColorParsed -> WebColor type family ParseRGBColorWorker color where ParseRGBColorWorker '(r, g, b, Nothing) = '(r, g, b) ParseRGBColorWorker '(r, g, b, Just 255) = '(r, g, b) ParseRGBColorWorker '(_, _, _, Just _) = TypeError (Text "Unexpected alpha channel! RGB color expected") -- | Red, green, blue, and alpha color channels in range from 0 to 255 type WebColorAlpha = (Nat, Nat, Nat, Nat) -- | Parse a string containing a named color from a basic -- palette or a hexadecimal representation with an optional -- alpha channel. -- -- * This family converts an absence of an alpha channel into -- the 255 value. -- -- >>> showType @(`ParseWebColorAlpha` "123") -- '(17, 34, 51, 255) -- -- This family inherits many properties from `ParseWebColorMaybeAlpha`: -- -- * Named colors are always considered RGB-only and are handled the same way: -- -- >>> showType @(`ParseWebColorAlpha` "red") -- '(255, 0, 0, 255) -- -- * This family automatically handles shortened syntax. -- * This family throws a compile-time error when the wrong number -- of hex characters is supplied type ParseWebColorAlpha :: Symbol -> WebColorAlpha type family ParseWebColorAlpha s where ParseWebColorAlpha s = ParseRGBAColorWorker (ParseWebColorMaybeAlpha s) type ParseRGBAColorWorker :: WebColorParsed -> WebColorAlpha type family ParseRGBAColorWorker color where ParseRGBAColorWorker '(r, g, b, Nothing) = '(r, g, b, 0xFF) ParseRGBAColorWorker '(r, g, b, Just a) = '(r, g, b, a) type a & b = a -- | Parse a type-level string containing a named color from a basic -- palette or a hexadecimal representation and convert it into three -- bytes representing red, green, and blue channels -- -- -- * For convenience, a 255 alpha channel is interpreted -- as a solid color without an alpha channel: -- -- >>> webColor @"123" (,,) -- (17,34,51) -- -- >>> webColor @"123f" (,,) -- (17,34,51) -- -- * Other alpha channel values result in a compile-time error: -- -- >>> webColor @"123d" (,,) -- Unexpected alpha channel! RGB color expected -- -- * Named colors are always considered RGB-only: -- -- >>> webColor @"red" (,,) -- (255,0,0) type IsWebColor :: Symbol -> Constraint class IsWebColor s where -- | Parse a type-level string and give you bytes representing -- red, green, and blue colors. -- -- NB: @&@ is just a type alias that should help to keep with order. webColor :: (Word8 & "red" -> Word8 & "green" -> Word8 & "blue" -> r) -> r -- | Hacky instance to avoid a warning from GHC instance {-# OVERLAPPING #-} IsWebColor "red" where webColor :: forall r. ((Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> r) -> r webColor (Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> r k = (Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> r k Word8 & "red" 255 Word8 & "red" 0 Word8 & "red" 0 -- | Main instance where all the magic happens instance {-# OVERLAPPABLE #-} ( ParseWebColor s ~ '(r, g, b), KnownNat r, KnownNat g, KnownNat b ) => IsWebColor s where webColor :: forall r. ((Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> r) -> r webColor (Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> r k = (Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> r k ((Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> r) -> Proxy r -> (Word8 & "red") -> (Word8 & "red") -> r forall (n :: Nat) r. KnownNat n => ((Word8 & "red") -> r) -> Proxy n -> r `color` forall (t :: Nat). Proxy t forall {k} (t :: k). Proxy t Proxy @r ((Word8 & "red") -> (Word8 & "red") -> r) -> Proxy g -> (Word8 & "red") -> r forall (n :: Nat) r. KnownNat n => ((Word8 & "red") -> r) -> Proxy n -> r `color` forall (t :: Nat). Proxy t forall {k} (t :: k). Proxy t Proxy @g ((Word8 & "red") -> r) -> Proxy b -> r forall (n :: Nat) r. KnownNat n => ((Word8 & "red") -> r) -> Proxy n -> r `color` forall (t :: Nat). Proxy t forall {k} (t :: k). Proxy t Proxy @b -- | Parse a type-level string containing a named color from a basic -- palette or a hexadecimal representation and convert it into four -- bytes representing red, green, blue, and alpha channels -- -- -- * An absent alpha channel get converted into the 255 value: -- -- >>> webColorAlpha @"123" (,,,) -- (17,34,51,255) -- -- >>> webColorAlpha @"123f" (,,,) -- (17,34,51,255) -- -- * Named colors are always considered RGB-only: -- -- >>> webColorAlpha @"red" (,,,) -- (255,0,0,255) type IsWebColorAlpha :: Symbol -> Constraint class IsWebColorAlpha s where -- | Parse a type-level string and give you bytes representing -- red, green, blue, and alpha colors. -- -- NB: @&@ is just a type alias that should help to keep with order. webColorAlpha :: (Word8 & "red" -> Word8 & "green" -> Word8 & "blue" -> Word8 & "alpha" -> r) -> r -- | Hacky instance to avoid a warning from GHC instance {-# OVERLAPPING #-} IsWebColorAlpha "red" where webColorAlpha :: forall r. ((Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> r) -> r webColorAlpha (Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> r k = (Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> r k Word8 & "red" 255 Word8 & "red" 0 Word8 & "red" 0 Word8 & "red" 255 -- | Main instance where all the magic happens instance {-# OVERLAPPABLE #-} ( ParseWebColorAlpha s ~ '(r, g, b, a), KnownNat r, KnownNat g, KnownNat b, KnownNat a ) => IsWebColorAlpha s where webColorAlpha :: forall r. ((Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> r) -> r webColorAlpha (Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> r k = (Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> r k ((Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> r) -> Proxy r -> (Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> r forall (n :: Nat) r. KnownNat n => ((Word8 & "red") -> r) -> Proxy n -> r `color` forall (t :: Nat). Proxy t forall {k} (t :: k). Proxy t Proxy @r ((Word8 & "red") -> (Word8 & "red") -> (Word8 & "red") -> r) -> Proxy g -> (Word8 & "red") -> (Word8 & "red") -> r forall (n :: Nat) r. KnownNat n => ((Word8 & "red") -> r) -> Proxy n -> r `color` forall (t :: Nat). Proxy t forall {k} (t :: k). Proxy t Proxy @g ((Word8 & "red") -> (Word8 & "red") -> r) -> Proxy b -> (Word8 & "red") -> r forall (n :: Nat) r. KnownNat n => ((Word8 & "red") -> r) -> Proxy n -> r `color` forall (t :: Nat). Proxy t forall {k} (t :: k). Proxy t Proxy @b ((Word8 & "red") -> r) -> Proxy a -> r forall (n :: Nat) r. KnownNat n => ((Word8 & "red") -> r) -> Proxy n -> r `color` forall (t :: Nat). Proxy t forall {k} (t :: k). Proxy t Proxy @a color :: KnownNat n => (Word8 -> r) -> Proxy n -> r color :: forall (n :: Nat) r. KnownNat n => ((Word8 & "red") -> r) -> Proxy n -> r color (Word8 & "red") -> r k Proxy n p = (Word8 & "red") -> r k ((Word8 & "red") -> r) -> (Word8 & "red") -> r forall a b. (a -> b) -> a -> b $! Integer -> Word8 & "red" forall a. Num a => Integer -> a fromInteger (Proxy n -> Integer forall (n :: Nat) (proxy :: Nat -> Type). KnownNat n => proxy n -> Integer natVal Proxy n p) type UpgradeColor :: [Nat] -> WebColorParsed type family UpgradeColor nats where UpgradeColor [ b, g, r ] = UpgradeColor [ b, b, g, g, r, r] UpgradeColor [a, b, g, r ] = UpgradeColor [a, a, b, b, g, g, r, r] UpgradeColor [ b1, b2, g1, g2, r1, r2] = '(MakeByte r2 r1, MakeByte g2 g1, MakeByte b2 b1, Nothing) UpgradeColor [a1, a2, b1, b2, g1, g2, r1, r2] = '(MakeByte r2 r1, MakeByte g2 g1, MakeByte b2 b1, Just (MakeByte a2 a1)) UpgradeColor _ = TypeError (Text "Unexpected number of hex codes" :$$: Text "expected 3, 4, 6 or 8") -- | Convert a hexadecimal character (0 .. F) into a corresponging -- number. Case insensitive. type ParseHexadecimalChar :: Char -> Nat type family ParseHexadecimalChar ch where ParseHexadecimalChar '0' = 0x0 ParseHexadecimalChar '1' = 0x1 ParseHexadecimalChar '2' = 0x2 ParseHexadecimalChar '3' = 0x3 ParseHexadecimalChar '4' = 0x4 ParseHexadecimalChar '5' = 0x5 ParseHexadecimalChar '6' = 0x6 ParseHexadecimalChar '7' = 0x7 ParseHexadecimalChar '8' = 0x8 ParseHexadecimalChar '9' = 0x9 ParseHexadecimalChar 'a' = 0xa ParseHexadecimalChar 'b' = 0xb ParseHexadecimalChar 'c' = 0xc ParseHexadecimalChar 'd' = 0xd ParseHexadecimalChar 'e' = 0xe ParseHexadecimalChar 'f' = 0xf ParseHexadecimalChar 'A' = 0xA ParseHexadecimalChar 'B' = 0xB ParseHexadecimalChar 'C' = 0xC ParseHexadecimalChar 'D' = 0xD ParseHexadecimalChar 'E' = 0xE ParseHexadecimalChar 'F' = 0xF ParseHexadecimalChar ch = TypeError (Text "Unable to recognize a character: " :$$: ShowType ch) type MakeByte :: Nat -> Nat -> Nat type family MakeByte x y where MakeByte x y = x * 0x10 + y