{-# LANGUAGE TypeOperators #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE ViewPatterns #-} {-# OPTIONS_GHC -Wno-name-shadowing #-} module Graphics.UI.Threepenny.Editors.Layout ( -- * Renderableable widgets Renderable(..) -- * Layout engine , Layout(Grid, Single) , beside , above -- * Layout monoids -- ** Flat , Vertical(..) , vertical , Horizontal(..) , horizontal -- ** Columns , Columns (Break, Next) -- * Type level layouts , type (|*|)(..) , type (-*-)(..) ) where import Data.Biapplicative import Data.Bifoldable import Data.Foldable (length) import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map import Data.Maybe import Data.Monoid import Data.Sequence (Seq) import qualified Data.Sequence as Seq import GHC.Exts (IsList (..)) import Graphics.UI.Threepenny.Core as UI import Graphics.UI.Threepenny.Elements import Graphics.UI.Threepenny.Widgets -- | Closely related to 'Widget', this class represents types that can be rendered to an 'Element' class Renderable w where render :: w -> UI Element getLayout :: w -> Layout render = runLayout . getLayout getLayout = Cell . Just . render instance Renderable Element where render = return instance Renderable a => Renderable (UI a) where render = (>>= render) instance Renderable TextEntry where render = return . getElement instance Renderable (ListBox a) where render = return . getElement instance Renderable String where render = string data Layout = Grid (Seq (Seq Layout)) -- ^ A non empty list of rows, where all the rows are assumed to have the same length | Cell (Maybe (UI Element)) pattern Empty :: Layout pattern Empty = Cell Nothing pattern Single :: UI Element -> Layout pattern Single x = Cell (Just x) above, beside :: Layout -> Layout -> Layout above (Grid rows@(length.head.toList -> l1)) (Grid rows'@(length.head.toList -> l2)) = Grid $ fmap pad1 rows <> fmap pad2 rows' where pad l1 l2 | l1 >= l2 = id | otherwise = (<> Seq.replicate (l2-l1) (Cell Nothing)) pad1 = pad l1 l2 pad2 = pad l2 l1 above x (Cell Nothing) = x above (Cell Nothing) x = x above cell@(Cell Just{}) x = above (Grid [[cell]]) x above x cell@(Cell Just{}) = above x (Grid [[cell]]) beside (Grid rows@(length -> l1)) (Grid rows'@(length -> l2)) = Grid $ Seq.zipWith (<>) (pad1 rows) (pad2 rows') where pad l1 l2 | l1 >= l2 = id | otherwise = \x -> let padding = Seq.replicate (length $ head $ toList x) (Cell Nothing) in x <> Seq.replicate (l2 - l1) padding pad1 = pad l1 l2 pad2 = pad l2 l1 beside x (Cell Nothing) = x beside (Cell Nothing) x = x beside cell@(Cell Just{}) x = beside (Grid [[cell]]) x beside x cell@(Cell Just{}) = beside x (Grid [[cell]]) instance Renderable Layout where getLayout = id runLayout :: Layout -> UI Element runLayout (Grid rows) = grid (toList $ fmap (fmap runLayout . toList) rows) runLayout (Cell el) = fromMaybe new el -- | A layout monoid that places everything in a single column newtype Vertical = Vertical { getVertical :: Layout} vertical :: Renderable w => w -> Vertical vertical = Vertical . getLayout instance Monoid Vertical where mempty = Vertical Empty mappend (Vertical a) (Vertical b)= Vertical $ above a b instance Renderable Vertical where getLayout = getVertical -- | A layout monoid that places everything in a single row newtype Horizontal = Horizontal { getHorizontal :: Layout} horizontal :: Renderable w => w -> Horizontal horizontal = Horizontal . getLayout instance Monoid Horizontal where mempty = Horizontal Empty mappend (Horizontal a) (Horizontal b)= Horizontal $ beside a b instance Renderable Horizontal where getLayout = getHorizontal -- | A layout monoid that lays elements in columns data Columns = Next Layout -- ^ Continue in the same column | Break Layout -- ^ Continue in the next column | Columns { next :: (Int, Int) -- ^ (row, column) , acc :: Map (Int, Int) Layout } instance Renderable Columns where getLayout = layoutColumns instance Show Columns where show(Next _) = "Next" show(Break _) = "Break" show Columns{..} = unwords ["Columns", show next, show (Map.keys acc)] layoutColumns :: Columns -> Layout layoutColumns (Next l) = l layoutColumns (Break l) = l layoutColumns Columns{acc} | Map.null acc = Empty | otherwise = getLayout $ foldMap Vertical [ getLayout $ foldMap Horizontal $ catMaybes [Map.lookup (i, j) acc | j <- [0 .. c]] | i <- [0 .. r] ] where r = maximum $ fst <$> Map.keys acc c = maximum $ snd <$> Map.keys acc instance Monoid Columns where mempty = Columns (-1,-1) mempty mappend (Next a) (Columns (r,c) g) = let xy = (r+1, max 0 c) in Columns xy (Map.insert xy a g) mappend (Break a) (Columns (_,c) g) = let xy = (0, c + 1) in Columns xy (Map.insert xy a g) -- merging two columns should not ever happen, but if it does we will merge the columns and Break into a new one mappend (Columns (r,c) g) (Columns (r',_) g') = Columns (r+r'+1, -1) (Map.union g (Map.mapKeys (\(x,y) -> (x+r+1,y+c+1)) g')) mappend c@Columns{} other = mappend other c -- Next and Break merges should not arise in practice either mappend (Next a) (Next b) = Columns ( 1,0) (Map.fromList [((0,0),a), ((1,0),b)]) mappend (Next a) (Break b) = Columns ( 0,1) (Map.fromList [((0,0),a), ((0,1),b)]) mappend (Break a) (Break b) = mappend (Next a) (Break b) mappend (Break a) (Next b) = mappend (Next a) (Next b) -- | Type level Horizontal layouts data a |*| b = a :|*| b instance Bifunctor (|*|) where bimap f g (a :|*| b) = f a :|*| g b instance Bifoldable (|*|) where bifoldMap f g (a :|*| b) = f a `mappend` g b instance Biapplicative (|*|) where bipure a b = a :|*| b fa :|*| fb <<*>> a :|*| b = fa a :|*| fb b instance (Renderable a, Renderable b) => Renderable (a |*| b) where getLayout (a :|*| b) = getLayout a `beside` getLayout b -- | Type level Vertical layouts data a -*- b = a :-*- b instance Bifunctor (-*-) where bimap f g (a :-*- b) = f a :-*- g b instance Bifoldable (-*-) where bifoldMap f g (a :-*- b) = f a `mappend` g b instance Biapplicative (-*-) where bipure a b = a :-*- b fa :-*- fb <<*>> a :-*- b = fa a :-*- fb b instance (Renderable a, Renderable b) => Renderable (a -*- b) where getLayout (a :-*- b) = getLayout a `above` getLayout b