----------------------------------------------------------------------------- -- | -- Module : Graphics.Rendering.Chart.Renderable -- Copyright : (c) Tim Docker 2006, 2014 -- License : BSD-style (see chart/COPYRIGHT) -- -- This module contains the definition of the 'Renderable' type, which -- is a composable drawing element, along with assorted functions to -- them. -- {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE TemplateHaskell #-} module Graphics.Rendering.Chart.Renderable( Renderable(..), ToRenderable(..), PickFn, Rectangle(..), RectCornerStyle(..), rectangleToRenderable, drawRectangle, fillBackground, addMargins, emptyRenderable, embedRenderable, label, rlabel, spacer, spacer1, setPickFn, mapMaybePickFn, mapPickFn, nullPickFn, rect_minsize, rect_fillStyle, rect_lineStyle, rect_cornerStyle, ) where import Control.Monad import Control.Lens import Data.Monoid import Data.Default.Class import Graphics.Rendering.Chart.Geometry import Graphics.Rendering.Chart.Drawing import Graphics.Rendering.Chart.Utils -- | A function that maps a point in device coordinates to some value. -- -- Perhaps it might be generalised from Maybe a to -- (MonadPlus m ) => m a in the future. type PickFn a = Point -> Maybe a nullPickFn :: PickFn a nullPickFn = const Nothing -- | A Renderable is a record of functions required to layout a -- graphic element. data Renderable a = Renderable { -- | Calculate the minimum size of the renderable. minsize :: BackendProgram RectSize, -- | Draw the renderable with a rectangle, which covers -- the origin to a given point. -- -- The resulting "pick" function maps a point in the image to a value. render :: RectSize -> BackendProgram (PickFn a) } deriving (Functor) -- | A type class abtracting the conversion of a value to a Renderable. class ToRenderable a where toRenderable :: a -> Renderable () instance ToRenderable (Renderable a) where toRenderable = void emptyRenderable :: Renderable a emptyRenderable = spacer (0,0) -- | Create a blank renderable with a specified minimum size. spacer :: RectSize -> Renderable a spacer sz = Renderable { minsize = return sz, render = \_ -> return nullPickFn } -- | Create a blank renderable with a minimum size the same as -- some other renderable. spacer1 :: Renderable a -> Renderable b spacer1 r = r{ render = \_ -> return nullPickFn } -- | Replace the pick function of a renderable with another. setPickFn :: PickFn b -> Renderable a -> Renderable b setPickFn pickfn r = r{ render = \sz -> render r sz >> return pickfn } -- | Map a function over the result of a renderable's pickfunction, keeping only 'Just' results. mapMaybePickFn :: (a -> Maybe b) -> Renderable a -> Renderable b mapMaybePickFn f r = r{ render = \sz -> do pf <- render r sz return (join . fmap f . pf) } -- | Map a function over result of a renderable's pickfunction. mapPickFn :: (a -> b) -> Renderable a -> Renderable b mapPickFn f = mapMaybePickFn (Just . f) -- | Add some spacing at the edges of a renderable. addMargins :: (Double,Double,Double,Double) -- ^ The spacing to be added. -> Renderable a -- ^ The source renderable. -> Renderable a addMargins (t,b,l,r) rd = Renderable { minsize = mf, render = rf } where mf = do (w,h) <- minsize rd return (w+l+r,h+t+b) rf (w,h) = withTranslation (Point l t) $ do pickf <- render rd (w-l-r,h-t-b) return (mkpickf pickf (t,b,l,r) (w,h)) mkpickf pickf (t',b',l',r') (w,h) (Point x y) | x >= l' && x <= w-r' && y >= t' && t' <= h-b' = pickf (Point (x-l') (y-t')) | otherwise = Nothing -- | Overlay a renderable over a solid background fill. fillBackground :: FillStyle -> Renderable a -> Renderable a fillBackground fs r = r{ render = rf } where rf rsize@(w,h) = do withFillStyle fs $ do p <- alignFillPath $ rectPath (Rect (Point 0 0) (Point w h)) fillPath p render r rsize -- | Helper function for using a renderable, when we generate it -- in the BackendProgram monad. embedRenderable :: BackendProgram (Renderable a) -> Renderable a embedRenderable ca = Renderable { minsize = do { a <- ca; minsize a }, render = \ r -> do { a <- ca; render a r } } ---------------------------------------------------------------------- -- Labels -- | Construct a renderable from a text string, aligned with the axes. label :: FontStyle -> HTextAnchor -> VTextAnchor -> String -> Renderable String label fs hta vta = rlabel fs hta vta 0 -- | Construct a renderable from a text string, rotated wrt to axes. The angle -- of rotation is in degrees, measured clockwise from the horizontal. rlabel :: FontStyle -> HTextAnchor -> VTextAnchor -> Double -> String -> Renderable String rlabel fs hta vta rot s = Renderable { minsize = mf, render = rf } where mf = withFontStyle fs $ do ts <- textSize s let sz = (textSizeWidth ts, textSizeHeight ts) return (xwid sz, ywid sz) rf (w0,h0) = withFontStyle fs $ do ts <- textSize s let sz@(w,h) = (textSizeWidth ts, textSizeHeight ts) descent = textSizeDescent ts xadj HTA_Left = xwid sz/2 xadj HTA_Centre = w0/2 xadj HTA_Right = w0 - xwid sz/2 yadj VTA_Top = ywid sz/2 yadj VTA_Centre = h0/2 yadj VTA_Bottom = h0 - ywid sz/2 yadj VTA_BaseLine = h0 - ywid sz/2 + descent*acr withTranslation (Point 0 (-descent)) $ withTranslation (Point (xadj hta) (yadj vta)) $ withRotation rot' $ do drawText (Point (-w/2) (h/2)) s return (\_-> Just s) -- PickFn String rot' = rot / 180 * pi (cr,sr) = (cos rot', sin rot') (acr,asr) = (abs cr, abs sr) xwid (w,h) = w*acr + h*asr ywid (w,h) = w*asr + h*acr ---------------------------------------------------------------------- -- Rectangles data RectCornerStyle = RCornerSquare | RCornerBevel Double | RCornerRounded Double data Rectangle = Rectangle { _rect_minsize :: RectSize, _rect_fillStyle :: Maybe FillStyle, _rect_lineStyle :: Maybe LineStyle, _rect_cornerStyle :: RectCornerStyle } instance Default Rectangle where def = Rectangle { _rect_minsize = (0,0) , _rect_fillStyle = Nothing , _rect_lineStyle = Nothing , _rect_cornerStyle = RCornerSquare } instance ToRenderable Rectangle where toRenderable = rectangleToRenderable rectangleToRenderable :: Rectangle -> Renderable a rectangleToRenderable rectangle = Renderable mf rf where mf = return (_rect_minsize rectangle) rf = \rectSize -> drawRectangle (Point 0 0) rectangle{ _rect_minsize = rectSize } -- | Draw the specified rectangle such that its top-left vertex is placed at -- the given position drawRectangle :: Point -> Rectangle -> BackendProgram (PickFn a) drawRectangle point rectangle = do maybeM () (fill point size) (_rect_fillStyle rectangle) maybeM () (stroke point size) (_rect_lineStyle rectangle) return nullPickFn where size = _rect_minsize rectangle fill p sz fs = withFillStyle fs $ fillPath $ strokeRectangleP p sz (_rect_cornerStyle rectangle) stroke p sz ls = withLineStyle ls $ strokePath $ strokeRectangleP p sz (_rect_cornerStyle rectangle) strokeRectangleP (Point x1 y1) (x2,y2) RCornerSquare = let (x3,y3) = (x1+x2,y1+y2) in moveTo' x1 y1 <> lineTo' x1 y3 <> lineTo' x3 y3 <> lineTo' x3 y1 <> lineTo' x1 y1 strokeRectangleP (Point x1 y1) (x2,y2) (RCornerBevel s) = let (x3,y3) = (x1+x2,y1+y2) in moveTo' x1 (y1+s) <> lineTo' x1 (y3-s) <> lineTo' (x1+s) y3 <> lineTo' (x3-s) y3 <> lineTo' x3 (y3-s) <> lineTo' x3 (y1+s) <> lineTo' (x3-s) y1 <> lineTo' (x1+s) y1 <> lineTo' x1 (y1+s) strokeRectangleP (Point x1 y1) (x2,y2) (RCornerRounded s) = let (x3,y3) = (x1+x2,y1+y2) in arcNeg (Point (x1+s) (y3-s)) s (pi2*2) pi2 <> arcNeg (Point (x3-s) (y3-s)) s pi2 0 <> arcNeg (Point (x3-s) (y1+s)) s 0 (pi2*3) <> arcNeg (Point (x1+s) (y1+s)) s (pi2*3) (pi2*2) <> lineTo' x1 (y3-s) pi2 = pi / 2 $( makeLenses ''Rectangle )