-- Modules might be imported solely so Haddock can hyperlink the -- identifiers {-# OPTIONS_GHC -fno-warn-unused-imports #-} {-# LANGUAGE OverloadedStrings #-} {-| The Rainbox tutorial Rainbox helps you create arrangements of (possibly) colorful text boxes. This module contains a tutorial. Typically the "Rainbox" module contains all you need. There is also a "Rainbox.Core" module which contains all the innards of Rainbox, but ordinarily you won't need it. The basic building block of Rainbox is known as a @core@. A core is either a single 'Chunk' or a blank box of arbitrary size. A core made of a single 'Chunk' should not contain any newlines. Leave newline handling up to Rainbox. However, Rainbox will not check to ensure that your 'Chunk' does not contain any newline characters. If it does contain newlines, your boxes will not look right. Also, Rainbox needs to know how wide your 'Chunk' are, in columns. To measure width, Rainbox simply counts the number of characters in the 'Chunk'. Therefore, if you need accented characters, be sure to use a single character, not composed characters. That is, to get รก, use U+00E1, not U+00B4 and U+0061. Many things in Rainbox have height and width. Both the height and width of an object can be zero, but never less than zero. A @core@ made from a 'Chunk' always has a height of 1, and its width is equal to the number of characters in the 'Text's that make up the 'Chunk'. A @core@ made from a blank box has the height and width that you give it, though neither its height nor its width is ever smaller than zero. The next biggest building block after the @core@ is @payload@. There are two different types of payloads: vertical payloads and horizontal ones. A vertical payload aligns itself next to other vertical payloads on a vertical axis, creating a chain of payloads. The vertical payload also has an alignment. The alignment determines whether the payload lines up along the axis on its left side, right side, or in the center of the payload. The vertical payload also has a background color, which has type 'Radiant'. Think of the background color as extending infinitely from both the left and right side of the vertical payload. When the vertical payload is combined with other vertical payloads into a 'Box' 'Vertical', this background color is used as necessary so that the 'Box' 'Vertical' forms a rectangle. The horizontal payload is similar to the vertical payload, but the axis is horizontal rather than vertical. The alignment determines whether the payload aligns the axis along the top, center, or bottom of the payload. A horizontal payload also contains a background color; it extends infinitely from both the top and bottom of the horizontal payload. Finally, the biggest building block of Rainbox is the box. There are two types of boxes: a 'Box' 'Horizontal', which holds zero or more horizontal payloads, and a 'Box' 'Vertical', which holds zero or more vertical payloads. Each kind of box is a 'Monoid', so you can combine it using the usual monoid functions. So, to give a visual, a 'Box' 'Vertical' with five payloads might look like this: @ -- function: 'box1' V Vertical axis +----+ | v1 | | | +----+--------+ | v2 | | | +--------+-------------+ | | | v3 | | | | | +--------+----------+ | v4 | +----+----+-----+ | v5 | | | +---------+ @ Each payload is marked in the middle with @v1@, @v2@, etc. Note how each payload has a different size. @v1@ and @v2@, and @v4@ have a 'left' alignment, as their left edge is lined up with the vertical axis. @v3@ has a 'right' alignment. @v5@ has a 'center' alignment. Think of each payload has having a background color extending infinitely off of its left and right sides. These five payloads put together make a 'Box' 'Vertical'. Since 'Box' 'Vertical' is a monoid, you can combine various 'Box' 'Vertical'. Indeed, the pictured 'Box' 'Vertical' can be built only by combining smaller 'Box' 'Vertical', as when you create payloads they are always given to you as a single payload wrapped in a 'Box' 'Vertical'. Now, you want to render your 'Box' 'Vertical'. You use the 'render' function, which makes a sequence of Rainbow 'Chunk'. This turns your 'Box' 'Vertical' into a nice rectangle for on-screen rendering: @ -- function: 'renderBox1' +--------+----+--------+ | | v1 | | | | | | +--------+----+--------+ | | v2 | | | | +--------+-------------+ | | | | v3 | | | | | | | | +--------+----------+--+ | | v4 | | +---+----+----+-----+--+ | | v5 | | | | | | +---+---------+--------+ @ The spaces to the left and right of each payload are filled in with the appropriate background color, which is the background color of the adjoining payload. What if you want to place the 'Box' 'Vertical' alongside another box? If you want to put it next to another 'Box' 'Vertical', just use 'mappend' or '<>'. But what if you want to put it next to a 'Box' 'Horizontal'? Let's suppose you have a 'Box' 'Horizontal' that looks like this: @ -- function: 'box2' +----+ | h1 | | | Horizontal Axis > +----+----------+ | | | h2 | | | +----------+ @ The @h1@ payload has alignment 'bottom', because its bottom edge is lined up along the horizontal axis. The @h2@ payload has alignment 'top'. You want to connect this 'Box' 'Horizontal' with the 'Box' 'Vertical' made above. You can't connect them directly because they are different types. You can, however, take a complete 'Box' and wrap it inside another 'Box'. This allows you to wrap a 'Box' 'Vertical' inside of a 'Box' 'Horizontal', and vice versa, or even wrap a 'Box' 'Horizontal' inside of another 'Box' 'Horizontal'. You do this with the 'wrap' function, which is applied to the alignment for the new box, its background color, and the box you want to wrap. ao, let's say you take the 'Box' 'Vertical' created above and wrap it inside a 'Box' 'Horizontal' with 'top' alignment and a background color, and then you combine it with the 'Box' 'Horizontal' created above. The result: @ -- function: 'box3' +----+ | h1 | | | +--------+----+--------+----+----------+ | | v1 | | | | | | | | | h2 | +--------+----+--------+ | | | | v2 | +----------+ | | | +--------+-------------+ | | | | v3 | | | | | | | | +--------+----------+--+ | | v4 | | +---+----+----+-----+--+ | | v5 | | | | | | +---+---------+--------+ @ The old 'Box' 'Vertical', which is now wrapped in a 'Box' 'Horizontal', now has a background color which extends infinitely from the top and bottom of the box. It is now just a payload inside of the 'Box' 'Horizontal'. The other two payloads in the 'Box' 'Horizontal', @h1@ and @h2@, also have background colors extending from their tops and bottoms. So, when you render this 'Box' 'Horizontal' with 'render', you get this: @ -- function: 'renderBox3' +----------------------+----+----------+ | | h1 | | | | | | +--------+----+--------+----+----------+ | | v1 | | | | | | | | | h2 | +--------+----+--------+ | | | | v2 | +----------+ | | | | | +--------+-------------+ | | | | | | | | v3 | | | | | | | | | | | | | | +--------+----------+--+ | | | | v4 | | | | +---+----+----+-----+--+ | | | | v5 | | | | | | | | | | +---+---------+--------+----+----------+ @ The area above the old 'Box' 'Vertical' has the background color that we used in the application of 'convert'. The area below the @h1@ payload has its background color, and the area above and below the @h2@ payload has its background color. What if you just want to create an empty space? You can create entire blank boxes with 'blank', but often it is enough to use 'spacer', which gives you a one-dimensional 'Box' 'Horizontal' or 'Box' 'Vertical'. If you are creating a 'Box' 'Horizontal', 'spacer' gives you a box with width, but no height; for a 'Box' 'Vertical', you get a box with height, but no width. So, to return to the example 'Box' 'Horizontal', let's say you want to add a blank space a few columns wide on the right side. You 'mappend' a 'Box' 'Horizontal' created with 'spacer' to get this: @ -- function: 'box4' +----+ | h1 | | | Horizontal Axis > +----+----------+--+ | | | h2 | | | +----------+ @ On the right side you now have a payload with width, but no height. But it does have a background color. So when you 'render' the box, you get this: @ -- function: 'renderBox4' +----+----------+--+ | h1 | | | | | | | +----+----------+ | | | | | | | h2 | | | | | | +----+----------+--+ @ You can also use 'spreader' to make a 'Box' 'Horizontal' taller or a 'Box' 'Vertical' wider. 'spreader' creates a one-dimensional 'Box' that is perpendicular to the axis. Pay attention to the alignment of the 'spreader' as this will determine how your box expands. Let's say I want to take the 'Box' that contains the @h1@ and @h2@ payloads as created above, but I also want to make sure the box is at least 12 rows tall. To do this, 'mappend' a 'spreader' that is 12 rows tall. The result upon 'render'ing is: @ -- functions: 'box5' 'renderBox5' +----+----------+--+ | | | | +----+ | | | h1 | | | | | | | +----+----------+ | | | | | | | h2 | | | | | | | +----------+ | | | | | +----+----------+--+ @ Those are the basics of the Rainbox model, which should be enough to get you started. Also helpful are the 'tableByRows' and 'tableByColumns' functions, which will help you build a simple grid that resembles a spreadsheet; see its documentation for hints to get started with that. You will also find an example using the 'tableByRows', as well as code to produce all of the examples shown above, in the source code below. == Why the use of 'Seq' everywhere, rather than lists? Rainbox uses 'Seq' from "Data.Sequence" because lists can be infinite. Practically every function in Rainbox will not accept infinite inputs, because Rainbox needs to know exactly how long and wide various payloads and boxes are in order to line them up correctly. Use of the 'Seq' most accurately reflects the fact that Rainbox does not work on infinite inputs. -} module Rainbox.Tutorial where import Data.Foldable (toList) import Data.Function ((&)) import Data.List (intersperse) import Data.Monoid ((<>)) import Data.Sequence (Seq) import qualified Data.Sequence as Seq import Data.Text (Text) import qualified Data.Text as X import qualified Rainbow import qualified Rainbox -- | Create a 'Box' for the given text. The default foreground and -- background colors of the terminal are used for the 'Text'; the -- given background is used as the background color for any added -- padding. textBox :: Rainbow.Radiant -> Text -> Rainbox.Box a textBox r = Rainbox.fromChunk Rainbox.center r . Rainbow.chunk -- | Centers the given 'Box' within a larger 'Box' that has the given -- height and width and background color. The larger 'Box' has the -- given 'Alignment'. within :: Rainbox.Orientation a => Rainbox.Alignment a -> Int -- ^ Number of rows -> Int -- ^ Number of columns -> Rainbow.Radiant -- ^ Background color -> Rainbox.Box a -> Rainbox.Box a within a r c b = Rainbox.wrap a b . mappend (Rainbox.spreader Rainbox.center r) . Rainbox.wrap Rainbox.centerH b . mappend (Rainbox.spreader Rainbox.center c) . Rainbox.wrap Rainbox.centerV b -- | Puts the given text in the center of a box. The resulting box is -- center aligned. textWithin :: Rainbox.Orientation a => Rainbox.Alignment a -> Int -- ^ Number of rows -> Int -- ^ Number of columns -> Rainbow.Radiant -- ^ Background color for smaller box -> Rainbow.Radiant -- ^ Background color for larger box -> Text -> Rainbox.Box a textWithin a r c bs bl = Rainbox.wrap a bl . within a r c bs . textBox bs box1 :: Rainbox.Box Rainbox.Vertical box1 = mconcat [ textWithin Rainbox.left 4 6 Rainbow.blue Rainbow.green "v1" , textWithin Rainbox.left 4 15 Rainbow.red Rainbow.magenta "v2" , textWithin Rainbox.right 6 10 Rainbow.yellow Rainbow.blue "v3" , textWithin Rainbox.left 3 12 Rainbow.green Rainbow.red "v4" , textWithin Rainbox.center 4 11 Rainbow.magenta Rainbow.blue "v5" ] renderBox1 :: IO () renderBox1 = mapM_ Rainbow.putChunk . toList . Rainbox.render $ box1 box2 :: Rainbox.Box Rainbox.Horizontal box2 = mconcat [ textWithin Rainbox.bottom 4 6 Rainbow.magenta Rainbow.green "h1" , textWithin Rainbox.top 5 12 Rainbow.blue Rainbow.yellow "h2" ] renderBox2 :: IO () renderBox2 = mapM_ Rainbow.putChunk . toList . Rainbox.render $ box2 box3 :: Rainbox.Box Rainbox.Horizontal box3 = mconcat [ Rainbox.wrap Rainbox.top Rainbow.yellow box1 , box2 ] renderBox3 :: IO () renderBox3 = mapM_ Rainbow.putChunk . toList . Rainbox.render $ box3 box4 :: Rainbox.Box Rainbox.Horizontal box4 = box2 <> Rainbox.spacer Rainbow.cyan 3 renderBox4 :: IO () renderBox4 = mapM_ Rainbow.putChunk . toList . Rainbox.render $ box4 box5 :: Rainbox.Box Rainbox.Horizontal box5 = box4 <> Rainbox.spreader Rainbox.center 12 renderBox5 :: IO () renderBox5 = mapM_ Rainbow.putChunk . toList . Rainbox.render $ box5 -- Sample code for 'tableByRows' -- -- Here is a simple data type representing stations in the -- Washington DC Metrorail system. data Line = Red | Blue | Orange | Green | Yellow | Silver deriving (Eq, Ord, Show, Enum) data Station = Station { name :: Text , metroLines :: [Line] , address :: [Text] , underground :: Bool } nameCell :: Rainbow.Radiant -> Text -> Rainbox.Cell nameCell bk nm = Rainbox.Cell cks Rainbox.top Rainbox.center bk where cks = Seq.singleton . Seq.singleton $ (Rainbow.chunk nm & Rainbow.back bk) linesCell :: Rainbow.Radiant -> [Line] -> Rainbox.Cell linesCell bk lns = Rainbox.Cell cks Rainbox.top Rainbox.center bk where cks = Seq.fromList . fmap (lineRow bk) $ lns lineRow :: Rainbow.Radiant -> Line -> Seq Rainbow.Chunk lineRow bk li = Seq.singleton ck where ck = Rainbow.chunk (X.pack . show $ li) & Rainbow.fore clr & Rainbow.back bk clr = case li of Red -> Rainbow.red Blue -> Rainbow.blue Orange -> Rainbow.yellow <> Rainbow.color256 220 Green -> Rainbow.green Yellow -> Rainbow.yellow Silver -> Rainbow.white <> Rainbow.grey addressCell :: Rainbow.Radiant -> [Text] -> Rainbox.Cell addressCell bk lns = Rainbox.Cell cks Rainbox.top Rainbox.center bk where cks = Seq.fromList . fmap addrRow $ lns addrRow txt = Seq.singleton $ Rainbow.chunk txt & Rainbow.back bk undergroundCell :: Rainbow.Radiant -> Bool -> Rainbox.Cell undergroundCell bk bl = Rainbox.Cell (Seq.singleton . Seq.singleton $ ck) Rainbox.top Rainbox.left bk where ck = (Rainbow.chunk $ if bl then "Yes" else "No") & Rainbow.back bk stations :: [Station] stations = [ Station "Metro Center" [Red, Orange, Silver, Blue] ["607 13th St NW", "Washington, DC 20005"] True , Station "L'Enfant Plaza" [Orange, Silver, Blue, Green, Yellow] ["600 Maryland Ave SW", "Washington, DC 20024"] True , Station "Silver Spring" [Red] ["8400 Colesville Rd", "Silver Spring, MD 20910"] False , Station "Court House" [Silver, Orange] ["2100 Wilson Blvd", "Arlington, VA 22201"] True , Station "Prince George's Plaza" [Green, Yellow] ["3575 East-West Hwy", "Hyattsville, MD 20782"] True ] coloredBack :: Rainbow.Radiant coloredBack = Rainbow.color256 195 -- | Converts a 'Station' to a list of 'Cell'. stationRow :: Rainbow.Radiant -> Station -> [Rainbox.Cell] stationRow bk st = [ nameCell bk . name $ st , linesCell bk . metroLines $ st , addressCell bk . address $ st , undergroundCell bk . underground $ st ] -- Building a table of stations on a row-by-row basis. allStationRows :: [[Rainbox.Cell]] allStationRows = zipWith stationRowWithSpacers (cycle [coloredBack, mempty]) stations where stationRowWithSpacers bk = intersperse (Rainbox.separator bk 1) . stationRow bk verticalStationTable :: Rainbox.Box Rainbox.Vertical verticalStationTable = Rainbox.tableByRows . Seq.fromList . fmap Seq.fromList $ allStationRows renderVerticalStationTable :: IO () renderVerticalStationTable = mapM_ Rainbow.putChunk . toList . Rainbox.render $ verticalStationTable -- Building a table of stations on a column-by-column basis. stationColumn :: Station -> [Rainbox.Cell] stationColumn st = [ nameCell coloredBack . name $ st , linesCell bland . metroLines $ st , addressCell coloredBack . address $ st , undergroundCell bland . underground $ st ] where bland = mempty allStationColumns :: [[Rainbox.Cell]] allStationColumns = intersperse spacerColumn . map stationColumn $ stations where spacerColumn = take 4 (cycle backgrounds) where backgrounds = [ Rainbox.separator coloredBack 1 , Rainbox.separator mempty 1 ] horizontalStationTable :: Rainbox.Box Rainbox.Horizontal horizontalStationTable = Rainbox.tableByColumns . Seq.fromList . fmap Seq.fromList $ allStationColumns renderHorizontalStationTable :: IO () renderHorizontalStationTable = mapM_ Rainbow.putChunk . toList . Rainbox.render $ horizontalStationTable