{-# LANGUAGE ScopedTypeVariables #-}
module System.Taffybar.Widget.Generic.Graph (
GraphHandle
, GraphConfig(..)
, GraphDirection(..)
, GraphStyle(..)
, graphNew
, graphAddSample
, defaultGraphConfig
) where
import Control.Concurrent
import Control.Monad ( when )
import Control.Monad.IO.Class
import Data.Foldable ( mapM_ )
import Data.Sequence ( Seq, (<|), viewl, ViewL(..) )
import qualified Data.Sequence as S
import qualified Data.Text as T
import qualified GI.Cairo.Render as C
import GI.Cairo.Render.Connector
import qualified GI.Cairo.Render.Matrix as M
import qualified GI.Gtk as Gtk
import Prelude hiding ( mapM_ )
import System.Taffybar.Util
import System.Taffybar.Widget.Util
newtype GraphHandle = GH (MVar GraphState)
data GraphState =
GraphState { graphIsBootstrapped :: Bool
, graphHistory :: [Seq Double]
, graphCanvas :: Gtk.DrawingArea
, graphConfig :: GraphConfig
}
data GraphDirection = LEFT_TO_RIGHT | RIGHT_TO_LEFT deriving (Eq)
type RGBA = (Double, Double, Double, Double)
data GraphStyle
= Area
| Line
data GraphConfig = GraphConfig {
graphPadding :: Int
, graphBackgroundColor :: RGBA
, graphBorderColor :: RGBA
, graphBorderWidth :: Int
, graphDataColors :: [RGBA]
, graphDataStyles :: [GraphStyle]
, graphHistorySize :: Int
, graphLabel :: Maybe T.Text
, graphWidth :: Int
, graphDirection :: GraphDirection
}
defaultGraphConfig :: GraphConfig
defaultGraphConfig =
GraphConfig
{ graphPadding = 2
, graphBackgroundColor = (0.0, 0.0, 0.0, 1.0)
, graphBorderColor = (0.5, 0.5, 0.5, 1.0)
, graphBorderWidth = 1
, graphDataColors = cycle [(1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0)]
, graphDataStyles = repeat Area
, graphHistorySize = 20
, graphLabel = Nothing
, graphWidth = 50
, graphDirection = LEFT_TO_RIGHT
}
graphAddSample :: GraphHandle -> [Double] -> IO ()
graphAddSample (GH mv) rawData = do
s <- readMVar mv
let drawArea = graphCanvas s
histSize = graphHistorySize (graphConfig s)
histsAndNewVals = zip pcts (graphHistory s)
newHists = case graphHistory s of
[] -> map S.singleton pcts
_ -> map (\(p,h) -> S.take histSize $ p <| h) histsAndNewVals
when (graphIsBootstrapped s) $ do
modifyMVar_ mv (\s' -> return s' { graphHistory = newHists })
postGUIASync $ Gtk.widgetQueueDraw drawArea
where
pcts = map (clamp 0 1) rawData
clamp :: Double -> Double -> Double -> Double
clamp lo hi d = max lo $ min hi d
outlineData :: (Double -> Double) -> Double -> Double -> C.Render ()
outlineData pctToY xStep pct = do
(curX,_) <- C.getCurrentPoint
C.lineTo (curX + xStep) (pctToY pct)
renderFrameAndBackground :: GraphConfig -> Int -> Int -> C.Render ()
renderFrameAndBackground cfg w h = do
let (backR, backG, backB, backA) = graphBackgroundColor cfg
(frameR, frameG, frameB, frameA) = graphBorderColor cfg
pad = graphPadding cfg
fpad = fromIntegral pad
fw = fromIntegral w
fh = fromIntegral h
C.setSourceRGBA backR backG backB backA
C.rectangle fpad fpad (fw - 2 * fpad) (fh - 2 * fpad)
C.fill
when (graphBorderWidth cfg > 0) $ do
let p = fromIntegral (graphBorderWidth cfg)
C.setLineWidth p
C.setSourceRGBA frameR frameG frameB frameA
C.rectangle (fpad + (p / 2)) (fpad + (p / 2))
(fw - 2 * fpad - p) (fh - 2 * fpad - p)
C.stroke
renderGraph :: [Seq Double] -> GraphConfig -> Int -> Int -> Double -> C.Render ()
renderGraph hists cfg w h xStep = do
renderFrameAndBackground cfg w h
C.setLineWidth 0.1
let pad = fromIntegral $ graphPadding cfg
let framePad = fromIntegral $ graphBorderWidth cfg
C.translate (pad + framePad) (pad + framePad)
let xS = (fromIntegral w - 2 * pad - 2 * framePad) / fromIntegral w
yS = (fromIntegral h - 2 * pad - 2 * framePad) / fromIntegral h
C.scale xS yS
when (graphDirection cfg == RIGHT_TO_LEFT) $
C.transform $ M.Matrix (-1) 0 0 1 (fromIntegral w) 0
let pctToY pct = fromIntegral h * (1 - pct)
renderDataSet hist color style
| S.length hist <= 1 = return ()
| otherwise = do
let (r, g, b, a) = color
originY = pctToY newestSample
originX = 0
newestSample :< hist' = viewl hist
C.setSourceRGBA r g b a
C.moveTo originX originY
mapM_ (outlineData pctToY xStep) hist'
case style of
Area -> do
(endX, _) <- C.getCurrentPoint
C.lineTo endX (fromIntegral h)
C.lineTo 0 (fromIntegral h)
C.fill
Line -> do
C.setLineWidth 1.0
C.stroke
sequence_ $ zipWith3 renderDataSet hists (graphDataColors cfg)
(graphDataStyles cfg)
drawBorder :: MVar GraphState -> Gtk.DrawingArea -> C.Render ()
drawBorder mv drawArea = do
(w, h) <- widgetGetAllocatedSize drawArea
s <- liftIO $ readMVar mv
let cfg = graphConfig s
renderFrameAndBackground cfg w h
liftIO $ modifyMVar_ mv (\s' -> return s' { graphIsBootstrapped = True })
return ()
drawGraph :: MVar GraphState -> Gtk.DrawingArea -> C.Render ()
drawGraph mv drawArea = do
(w, h) <- widgetGetAllocatedSize drawArea
drawBorder mv drawArea
s <- liftIO $ readMVar mv
let hist = graphHistory s
cfg = graphConfig s
histSize = graphHistorySize cfg
xStep = fromIntegral w / fromIntegral (histSize - 1)
case hist of
[] -> renderFrameAndBackground cfg w h
_ -> renderGraph hist cfg w h xStep
graphNew :: MonadIO m => GraphConfig -> m (Gtk.Widget, GraphHandle)
graphNew cfg = liftIO $ do
drawArea <- Gtk.drawingAreaNew
mv <- newMVar GraphState { graphIsBootstrapped = False
, graphHistory = []
, graphCanvas = drawArea
, graphConfig = cfg
}
Gtk.widgetSetSizeRequest drawArea (fromIntegral $ graphWidth cfg) (-1)
_ <- Gtk.onWidgetDraw drawArea $ \ctx -> renderWithContext
(drawGraph mv drawArea) ctx >> return True
box <- Gtk.boxNew Gtk.OrientationHorizontal 1
case graphLabel cfg of
Nothing -> return ()
Just lbl -> do
l <- Gtk.labelNew (Nothing :: Maybe T.Text)
Gtk.labelSetMarkup l lbl
Gtk.boxPackStart box l False False 0
Gtk.widgetSetVexpand drawArea True
Gtk.widgetSetVexpand box True
Gtk.boxPackStart box drawArea True True 0
Gtk.widgetShowAll box
giBox <- Gtk.toWidget box
return (giBox, GH mv)