bustle-0.4.4: Bustle/Diagram.hs
{-
Bustle.Diagram: shapes for sequence diagrams
Copyright (C) 2008–2009 Collabora Ltd.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-}
module Bustle.Diagram
( Diagram
-- Shapes, and smart constructors therefore
, Shape(..)
, memberLabel
, timestampLabel
, headers
, headerHeight
-- Attributes of shapes
, Arrowhead(..)
, Side(..)
, Colour(..)
, Rect
-- Annoying constants that users of this module need.
, columnWidth
, timestampAndMemberWidth
, firstColumnOffset
, eventHeight
-- Displaying diagrams
, diagramDimensions
, topLeftJustifyDiagram
, translateDiagram
, drawDiagram
, drawRegion
)
where
import Data.List (unzip4)
import Control.Arrow ((&&&))
import Control.Applicative ((<$>), (<*>))
import Control.Monad.Reader
import Graphics.Rendering.Cairo
import Graphics.UI.Gtk.Cairo (cairoCreateContext, showLayout)
import Graphics.Rendering.Pango.Layout
import Graphics.Rendering.Pango.Font
import qualified Bustle.Markup as Markup
import Bustle.Markup (Markup)
import Bustle.Util
import Bustle.Types (ObjectPath, InterfaceName, MemberName)
-- Sorry Mum
import System.IO.Unsafe (unsafePerformIO)
type Point = (Double, Double)
type Rect = (Double, Double, Double, Double)
data Arrowhead = Above | Below
deriving (Eq, Show, Read, Ord)
above, below :: Arrowhead -> Bool
above Above = True
above Below = False
below = not . above
voffset :: Num a => Arrowhead -> (a -> a -> a)
voffset Above = (-)
voffset Below = (+)
data Side = L | R
deriving (Eq, Show, Read, Ord)
offset :: Num a => Side -> (a -> a -> a)
offset L = (-)
offset R = (+)
data Colour = Colour Double Double Double
deriving (Eq, Show, Read, Ord)
data Shape = Header { strs :: [String]
, shapex, shapey :: Double
}
| MemberLabel { labelPath :: ObjectPath
, labelInterface :: Maybe InterfaceName
, labelMember :: MemberName
, shapeIsReturn :: Bool
, shapex :: Double -- The coordinates of the *centre*
, shapey :: Double -- of the label
}
| TimestampLabel { str :: String
, shapex :: Double -- The coordinates of the
, shapey :: Double -- *centre* of the timestamp
}
| ClientLines { shapexs :: NonEmpty Double -- The x-coordinates of the lines to draw
, shapey1, shapey2 :: Double
}
| Rule { shapex1, shapex2, shapey :: Double }
| Arrow { shapecolour :: Maybe Colour
, arrowhead :: Arrowhead
, shapex1, shapex2, shapey :: Double
}
| SignalArrow { shapex1, epicentre, shapex2, shapey :: Double }
| DirectedSignalArrow { epicentre, shapex, shapey :: Double }
| Arc { topx, topy, bottomx, bottomy :: Double
, arcside :: Side
, caption :: String
}
| Highlight { highlightRegion :: Rect
}
deriving (Show, Eq)
-- Smart constructors for TimestampLabel and MemberLabel that fill in the
-- hardcoded (spit) x coordinates.
memberLabel :: ObjectPath
-> Maybe InterfaceName
-> MemberName
-> Bool -- ^ True if this is a return; False if it's a call
-> Double -- ^ y-coordinate
-> Shape
memberLabel p i m isReturn y = MemberLabel p i m isReturn memberx y
timestampLabel :: String -> Double -> Shape
timestampLabel s y = TimestampLabel s timestampx y
type Diagram = [Shape]
arcControlPoints :: Shape -> (Point, Point)
arcControlPoints (Arc { topx=x1, topy=y1, bottomx=x2, bottomy=y2, arcside=s }) =
let (+-) = offset s
cp1 = (x1 +- 60, y1 + 10)
cp2 = (x2 +- 60, y2 - 10)
in (cp1, cp2)
arcControlPoints _ = error "i see you've played arcy-shapey before"
mapX, mapY :: (Double -> Double) -> (Shape -> Shape)
mapX f s = case s of
Rule {} -> s { shapex1 = f (shapex1 s)
, shapex2 = f (shapex2 s)
}
Arrow {} -> s { shapex1 = f (shapex1 s)
, shapex2 = f (shapex2 s)
}
SignalArrow {} -> s { shapex1 = f (shapex1 s)
, epicentre = f (epicentre s)
, shapex2 = f (shapex2 s)
}
Arc {} -> s { topx = f (topx s)
, bottomx = f (bottomx s)
}
ClientLines {} -> s { shapexs = mapNonEmpty f (shapexs s) }
_ -> s { shapex = f (shapex s) }
mapY f s = case s of
Arc {} -> s { topy = f (topy s)
, bottomy = f (bottomy s)
}
ClientLines {} -> s { shapey1 = f (shapey1 s)
, shapey2 = f (shapey2 s)
}
_ -> s { shapey = f (shapey s) }
--
-- Constants
--
eventHeight :: Double
eventHeight = 30
timestampx, timestampWidth :: Double
timestampx = 0 + timestampWidth / 2
timestampWidth = 60
memberx, memberWidth :: Double
memberx = timestampWidth + memberWidth / 2
memberWidth = 340
timestampAndMemberWidth :: Double
timestampAndMemberWidth = timestampWidth + memberWidth
columnWidth :: Double
columnWidth = 90
-- Method return arcs can go outside the first column. Empirically, 20 is
-- enough to stop the arc (or the duration text) overlapping the object path
-- etc.
firstColumnOffset :: Double
firstColumnOffset = 20 + columnWidth / 2
--
-- Calculating bounds of shapes
--
minMax :: Ord a => (a, a) -> (a, a)
minMax = uncurry min &&& uncurry max
xMinMax :: Shape -> (Double, Double)
xMinMax = minMax . (shapex1 &&& shapex2)
fromCentre :: Double -> Double -> Double -> Rect
fromCentre x y width =
(x - width / 2, y - height / 2,
x + width / 2, y + height / 2)
where height = eventHeight
headerHeight :: [String] -> Double
headerHeight = fromIntegral . (10 *) . length
bounds :: Shape -> Rect
bounds s = case s of
ClientLines {} ->
let xs = nonEmptyToList (shapexs s)
in (minimum xs, shapey1 s, maximum xs, shapey2 s)
Rule {} -> (shapex1 s, shapey s, shapex2 s, shapey s)
Arrow {} ->
let (x1, x2) = xMinMax s
y1 = shapey s - (if above (arrowhead s) then 5 else 0)
y2 = shapey s + (if below (arrowhead s) then 5 else 0)
in (x1, y1, x2, y2)
SignalArrow {} ->
let (x1, x2) = xMinMax s
(y1, y2) = (subtract 5) &&& (+5) $ shapey s
in (x1, y1, x2, y2)
DirectedSignalArrow {} ->
let (x1, x2) = minMax (epicentre s, shapex s)
(y1, y2) = (subtract 5) &&& (+5) $ shapey s
in (x1, y1, x2, y2)
Arc { topx=x1, bottomx=x2, topy=y1, bottomy=y2 } ->
let ((cx, _), (dx, _)) = arcControlPoints s
-- FIXME: magic 5 makes the bounding box include the text
in (min x1 cx, y1, max x2 dx, y2 + 5)
TimestampLabel { shapex=x, shapey=y } -> fromCentre x y timestampWidth
MemberLabel { shapex=x, shapey=y } -> fromCentre x y memberWidth
Header { strs = ss, shapex = x, shapey = y} ->
let width = columnWidth
height = headerHeight ss
in (x - width / 2, y,
x + width / 2, y + height)
Highlight r -> r
intersects :: Rect -> Rect -> Bool
intersects (x,y,w,z) (x', y', w', z') =
not $ or [x > w', w < x', y > z', z < y']
-- Constructs a series of headers of various-sized lists of names,
-- bottom-justified.
headers :: [(Double, [String])] -- list of (x-coordinate, names)
-> Double -- y-coordinate of top of headers
-> (Double, [Shape]) -- the headers' combined height, and shapes
headers [] _ = (0, [])
headers xss y = (height, shapes)
where heights = map (headerHeight . snd) xss
height = maximum heights
adjs = map (height -) heights
shapes = zipWith (\(x, ss) adj -> Header ss x (y + adj)) xss adjs
--
-- Drawing
--
diagramBounds :: Diagram -> ((Double, Double), (Double, Double))
diagramBounds shapes = ((minimum (0:x1s), minimum (0:y1s))
,(maximum (0:x2s), maximum (0:y2s))
)
where
(x1s, y1s, x2s, y2s) = unzip4 $ map bounds shapes
diagramDimensions :: Diagram -> (Double, Double)
diagramDimensions shapes = (x2 - x1, y2 - y1)
where
((x1, y1), (x2, y2)) = diagramBounds shapes
topLeftJustifyDiagram
:: Diagram -- ^ the original diagram
-> ((Double, Double), Diagram) -- ^ the diagram transformed to be in
-- positive space, and the (x, y)-axis
-- shifts necessary to do so
topLeftJustifyDiagram shapes =
(translation, shapes')
where
((x1, y1), _) = diagramBounds shapes
translation = (negate x1, negate y1)
shapes' = translateDiagram translation shapes
translateDiagram :: (Double, Double) -> (Diagram -> Diagram)
translateDiagram (x, y) = map (mapX (+ x) . mapY (+ y))
drawDiagramInternal :: (Shape -> Bool) -- ^ A filter for the shapes
-> Bool -- ^ True to draw canvas items' bounding boxes
-- (for debugging)
-> Diagram -- ^ A diagram to render
-> Render ()
drawDiagramInternal f drawBounds shapes = do
clearCanvas
forM_ (filter f shapes) $ \x -> do
when drawBounds (drawBoundingBox x)
draw x
drawDiagram :: Bool -- ^ True to draw canvas items' bounding boxes (for
-- debugging)
-> Diagram -- ^ A diagram to render
-> Render ()
drawDiagram = drawDiagramInternal (const True)
drawRegion :: Rect -> Bool -> Diagram -> Render ()
drawRegion r = drawDiagramInternal isVisible
where isVisible = intersects r . bounds
saved :: Render () -> Render ()
saved act = save >> act >> restore
clearCanvas :: Render ()
clearCanvas = saved $ do
setSourceRGB 1 1 1
setOperator OperatorSource
paint
drawBoundingBox :: Shape -> Render ()
drawBoundingBox s = saved $ do
let (x,y,w,z) = bounds s
setSourceRGB 0 0 1
rectangle x y (w - x) (z - y)
stroke
draw :: Shape -> Render ()
draw s = draw' s
where draw' = case s of
Arc {} -> let ((cx, cy), (dx, dy)) = arcControlPoints s
in drawArc cx cy dx dy <$>
topx <*> topy <*> bottomx <*> bottomy <*> caption
SignalArrow {} -> drawSignalArrow <$> epicentre
<*> (Just . shapex1)
<*> (Just . shapex2)
<*> shapey
DirectedSignalArrow { } -> drawDirectedSignalArrow <$> epicentre
<*> shapex
<*> shapey
Arrow {} -> drawArrow <$> shapecolour <*> arrowhead <*> shapex1 <*>
shapex2 <*> shapey
Header {} -> drawHeader <$> strs <*> shapex <*> shapey
MemberLabel {} -> drawMember <$> labelPath
<*> labelInterface
<*> labelMember
<*> shapeIsReturn
<*> shapex
<*> shapey
TimestampLabel {} -> drawTimestamp <$> str
<*> shapex
<*> shapey
ClientLines {} -> drawClientLines <$> shapexs <*> shapey1 <*> shapey2
Rule {} -> drawRule <$> shapex1
<*> shapex2
<*> shapey
Highlight {} -> drawHighlight <$> highlightRegion
halfArrowHead :: Arrowhead -> Bool -> Render ()
halfArrowHead a left = do
(x,y) <- getCurrentPoint
let x' = if left then x - 10 else x + 10
let y' = voffset a y 5
if left -- work around weird artifacts
then moveTo x' y' >> lineTo x y
else lineTo x' y' >> moveTo x y
arrowHead :: Bool -> Render ()
arrowHead left = halfArrowHead Above left >> halfArrowHead Below left
drawArrow :: Maybe Colour -> Arrowhead -> Double -> Double -> Double
-> Render ()
drawArrow c a from to y = saved $ do
maybe (return ()) (\(Colour r g b) -> setSourceRGB r g b) c
moveTo from y
lineTo to y
halfArrowHead a (from < to)
stroke
drawDirectedSignalArrow :: Double -- ^ the signal emission source
-> Double -- ^ signal target coordinate
-> Double -- ^ vertical coordinate
-> Render ()
drawDirectedSignalArrow e x y
| x < e = drawSignalArrow e (Just x) Nothing y
| otherwise = drawSignalArrow e Nothing (Just x) y
drawSignalArrow :: Double -- ^ the signal emission source
-> Maybe Double -- ^ left-pointing arrow coordinate
-> Maybe Double -- ^ right-pointing arrow coordinate
-> Double -- ^ vertical coordinate
-> Render ()
drawSignalArrow e mleft mright y = do
newPath
arc e y 5 0 (2 * pi)
stroke
maybeM mleft $ \left -> do
moveTo left y
arrowHead False
lineTo (e - 5) y
stroke
maybeM mright $ \right -> do
moveTo (e + 5) y
lineTo right y
arrowHead True
stroke
drawArc :: Double -> Double -> Double -> Double
-> Double -> Double -> Double -> Double
-> String
-> Render ()
drawArc cx cy dx dy x1 y1 x2 y2 cap = saved $ do
setSourceRGB 0.4 0.7 0.4
setDash [3, 3] 0
moveTo x1 y1
curveTo cx cy dx dy x2 y2
stroke
setSourceRGB 0 0 0
l <- mkLayout (Markup.escape cap) EllipsizeNone AlignLeft
(PangoRectangle _ _ textWidth _, _) <- liftIO $ layoutGetExtents l
let tx = min x2 dx + abs (x2 - dx) / 2
moveTo (if x1 > cx then tx - textWidth else tx) (y2 - 5)
showLayout l
font :: FontDescription
font = unsafePerformIO $ do
fd <- fontDescriptionNew
fontDescriptionSetSize fd 7
fontDescriptionSetFamily fd "Sans"
return fd
{-# NOINLINE font #-}
mkLayout :: (MonadIO m)
=> Markup -> EllipsizeMode -> LayoutAlignment
-> m PangoLayout
mkLayout s e a = liftIO $ do
ctx <- cairoCreateContext Nothing
layout <- layoutEmpty ctx
layoutSetMarkup layout (Markup.unMarkup s)
layoutSetFontDescription layout (Just font)
layoutSetEllipsize layout e
layoutSetAlignment layout a
return layout
withWidth :: MonadIO m => m PangoLayout -> Double -> m PangoLayout
withWidth m w = do
l <- m
liftIO $ layoutSetWidth l (Just w)
return l
drawHeader :: [String] -> Double -> Double -> Render ()
drawHeader names x y = forM_ (zip [0..] names) $ \(i, name) -> do
l <- mkLayout (Markup.escape name) EllipsizeEnd AlignCenter `withWidth` columnWidth
moveTo (x - (columnWidth / 2)) (y + i * h)
showLayout l
where h = 10
drawMember :: ObjectPath
-> Maybe InterfaceName
-> MemberName
-> Bool
-> Double
-> Double
-> Render ()
drawMember p i m isReturn x y = do
drawOne path (y - 10)
drawOne fullMethod y
where
drawOne markup y' = do
l <- mkLayout markup EllipsizeStart AlignLeft `withWidth` memberWidth
moveTo (x - memberWidth / 2) y'
showLayout l
path = (if isReturn then id else Markup.b) $ Markup.escape p
fullMethod =
(if isReturn then Markup.i else id) $ Markup.formatMember i m
drawTimestamp :: String -> Double -> Double -> Render ()
drawTimestamp ts x y = do
moveTo (x - timestampWidth / 2) (y - 10)
showLayout =<< mkLayout (Markup.escape ts) EllipsizeNone AlignLeft `withWidth` timestampWidth
drawClientLines :: NonEmpty Double -> Double -> Double -> Render ()
drawClientLines xs y1 y2 = saved $ do
setSourceRGB 0.7 0.7 0.7
forM_ (nonEmptyToList xs) $ \x -> do
moveTo x y1
lineTo x y2
stroke
drawRule :: Double -> Double -> Double -> Render ()
drawRule x1 x2 y = saved $ do
setSourceRGB 0.9 0.9 0.9
setLineWidth 0.5
moveTo x1 y
lineTo x2 y
stroke
drawHighlight :: Rect -> Render ()
drawHighlight (x1, y1, x2, y2) = saved $ do
setSourceRGB 0.8 0.9 1.0
rectangle x1 y1 (x2 - x1) (y2 - y1)
fill
-- vim: sw=2 sts=2