module Data.Sifflet.TreeLayout
(VColor(..), white, cream, black, lightBlue, lightBlueGreen
, lightGray, mediumGray, darkGray
, yellow, darkBlueGreen, blueGreen
, Style(..), VFont(..), defaultStyle, style0, style1, style2, style3
, wstyle
, styleIncreasePadding
, FontTextExtents(..), setFont, measureText, styleTextExtents, ftExtents
, TextBox(..), makeTextBox, tbWidth, tbHeight, tbBottom
, tbCenter, tbTextCenter, tbBoxCenter, offsetTextBoxCenters
, tbSetWidth
, treeSizes
, GNode(..), treeGNodes, gnodeText, gnodeTextBB
, Iolet(..), ioletCenter, makeIolets, makeIoletsRow
, pointInIolet
, IoletCounter, zeroIoletCounter
, makeGNode
, TreeLayout, LayoutNode(..)
, layoutNodeSource, layoutRootBB, layoutTreeBB
, treeLayout
, treeLayoutPaddedSize, treeLayoutSize, treeLayoutWidth
, layoutTreeMoveCenterTo
, pointInLayoutNode, pointInGNode, findRect
, treeLayoutWiden
)
where
import System.IO.Unsafe
import Graphics.Rendering.Cairo (FontExtents(..),
FontSlant(..), FontWeight(..),
Format(..), Render,
TextExtents(..),
fontExtents,
renderWith,
textExtents,
selectFontFace, setFontSize,
withImageSurface)
import Graphics.UI.Gtk (Rectangle)
import Data.Traversable ()
import Data.Sifflet.Geometry
import Data.Sifflet.Tree as T hiding (tree)
import Text.Sifflet.Repr ()
import Language.Sifflet.Util
data VColor = ColorRGB Double Double Double |
ColorRGBA Double Double Double Double
black, white, cream, lightBlue, lightBlueGreen :: VColor
black = ColorRGB 0 0 0
white = ColorRGB 1 1 1
cream = ColorRGB 0.94902 0.94902 0.82745
lightBlue = ColorRGB 0.43529 0.43922 0.95686
lightBlueGreen = ColorRGB 0 1 1
lightGray, mediumGray, darkGray :: VColor
lightGray = ColorRGBA 0.75 0.75 0.75 0.5
mediumGray = ColorRGB 0.5 0.5 0.5
darkGray = ColorRGB 0.25 0.25 0.25
yellow, darkBlueGreen, blueGreen :: VColor
yellow = ColorRGB 0.9 0.9 0
darkBlueGreen = ColorRGB 0 0.3 0.3
blueGreen = ColorRGB 0 0.4 0.4
data VFont = VFont {vfontFace :: String, vfontSlant :: FontSlant,
vfontWeight :: FontWeight,
vfontSize :: Double}
data Style =
Style {styleFont :: VFont,
lineWidth :: Double,
textMargin :: Double,
hpad, vpad :: Double,
exomargin :: Double,
vtinypad :: (Double, Double),
styleFramePad :: Double,
styleNormalTextColor, styleNormalFillColor,
styleNormalEdgeColor,
styleActiveTextColor, styleActiveFillColor,
styleActiveEdgeColor,
styleSelectedTextColor, styleSelectedFillColor,
styleSelectedEdgeColor,
styleTetherColor :: VColor,
styleAuxOffset :: Position,
styleAuxColor :: VColor,
styleAuxFont :: VFont,
styleIoletRadius :: Double,
styleShowNodeBoxes :: Bool,
styleShowNodePorts :: Bool
}
styleIncreasePadding :: Style -> Double -> Style
styleIncreasePadding style factor =
style {hpad = factor * hpad style,
vpad = factor * vpad style}
style0, style1, style2, style3, defaultStyle :: Style
style0 =
let green = ColorRGB 0.1 0.9 0.1
veryDarkGray = ColorRGB 0.1 0.1 0.1
brighterGreen = ColorRGB 0.5 0.95 0.5
in Style {styleFont = VFont "serif" FontSlantNormal FontWeightNormal 18,
lineWidth = 2,
textMargin = 18.0,
hpad = 27, vpad = 36,
exomargin = 0,
vtinypad = (4.5, 4.5),
styleFramePad = 35,
styleNormalTextColor = green,
styleNormalEdgeColor = green,
styleNormalFillColor = veryDarkGray,
styleActiveTextColor = brighterGreen,
styleActiveEdgeColor = brighterGreen,
styleActiveFillColor = mediumGray,
styleSelectedTextColor = blueGreen,
styleSelectedEdgeColor = blueGreen,
styleSelectedFillColor = darkGray,
styleTetherColor = white,
styleAuxOffset = Position 0.0 (20.0),
styleAuxColor = lightGray,
styleAuxFont =
VFont "serif" FontSlantItalic FontWeightNormal 12,
styleIoletRadius = 5,
styleShowNodeBoxes = True,
styleShowNodePorts = True
}
style1 =
let veryDarkBlue = ColorRGB 0 0 0.1
pinkIHope = ColorRGB 1 0.9 0.9
in style0 {styleNormalTextColor = veryDarkBlue,
styleNormalEdgeColor = veryDarkBlue,
styleNormalFillColor = lightGray,
styleActiveTextColor = pinkIHope,
styleActiveEdgeColor = pinkIHope
}
style2 =
let darkPink = ColorRGB 1 0.5 0.5
in style1 {styleNormalFillColor = white,
styleActiveTextColor = darkPink,
styleActiveEdgeColor = darkPink}
style3 =
let semiTranspBlue = ColorRGBA 0 0.2 0.9 0.5
in style0 {exomargin = 10,
styleNormalTextColor = yellow,
styleNormalEdgeColor = yellow,
styleNormalFillColor = darkBlueGreen,
styleActiveTextColor = yellow,
styleActiveEdgeColor = yellow,
styleActiveFillColor = blueGreen,
styleAuxColor = semiTranspBlue
}
defaultStyle = style3
wstyle :: Style
wstyle =
style3 {
styleFont = VFont "serif" FontSlantNormal FontWeightNormal 14,
textMargin = 8.0,
hpad = 10.0, vpad = 24.0,
vtinypad = (0.0, 0.0),
exomargin = 10.0,
styleFramePad = 15,
styleNormalTextColor = yellow,
styleNormalEdgeColor = mediumGray,
styleNormalFillColor = darkBlueGreen,
styleActiveTextColor = yellow,
styleActiveEdgeColor = mediumGray,
styleActiveFillColor = blueGreen,
styleSelectedTextColor = black,
styleSelectedEdgeColor = lightBlueGreen,
styleSelectedFillColor = lightBlueGreen,
styleTetherColor =
ColorRGBA 0.7 0.7 0.7 0.3,
styleAuxOffset = Position 0 (16),
styleAuxFont = VFont "serif" FontSlantItalic FontWeightNormal 12,
styleAuxColor = ColorRGBA 0.9 0.5 0.5 1.0,
styleIoletRadius = 5
}
pointInLayoutNode :: Position -> LayoutNode e -> Bool
pointInLayoutNode point = pointInGNode point . nodeGNode
pointInGNode :: Position -> GNode e -> Bool
pointInGNode point = pointInBB point . gnodeNodeBB
findNode :: Position -> TreeLayout e -> Maybe (LayoutNode e)
findNode point (T.Node node@(LayoutNode _rootGNode treeBB) sublayouts) =
if pointInBB point treeBB
then if pointInLayoutNode point node
then Just node
else findInSubs point sublayouts
else Nothing
where findInSubs :: Position -> [TreeLayout e] -> Maybe (LayoutNode e)
findInSubs _p [] = Nothing
findInSubs p (l:ls) =
let found = findNode p l in
case found of
(Just _) -> found
Nothing -> findInSubs p ls
findRect :: Position -> TreeLayout e -> Maybe Rectangle
findRect point tlo =
case findNode point tlo of
Nothing -> Nothing
Just node -> Just (bbToRect (gnodeNodeBB (nodeGNode node)))
setFont :: VFont -> Render ()
setFont (VFont face slant weight size) = do
selectFontFace face slant weight
setFontSize size
data FontTextExtents = FontTextExtents {
fontAscent::Double, fontDescent::Double, fontHeight::Double,
fontMaxXadvance::Double, fontMaxYadvance::Double,
textXbearing::Double, textYbearing::Double,
extTextWidth::Double, extTextHeight::Double,
textXadvance::Double, textYadvance::Double}
deriving (Eq, Read, Show)
measureText :: Style -> String -> Size
measureText style str =
let extents = styleTextExtents style str
in Size (textXadvance extents)
(fontHeight extents + fontDescent extents)
styleTextExtents :: Style -> String -> FontTextExtents
styleTextExtents style str =
unsafePerformIO $
withImageSurface FormatARGB32 0 0 $ \ surface ->
renderWith surface $
setFont (styleFont style) >>
ftExtents str
ftExtents :: String -> Render FontTextExtents
ftExtents text = do
FontExtents asc desc fheight maxxadv maxyadv <- fontExtents
TextExtents xbear ybear twidth theight xadv yadv <- textExtents text
return (FontTextExtents asc desc fheight maxxadv maxyadv
xbear ybear twidth theight xadv yadv)
data TextBox = TextBox {tbText :: String,
tbTextBB :: BBox,
tbBoxBB :: BBox
}
deriving (Eq, Read, Show)
makeTextBox :: Style -> String -> TextBox
makeTextBox style text =
let extents = styleTextExtents style text
Size textW textH = measureText style text
margin = textMargin style
boxW = textW + 2.0 * margin
boxH = textH + 2.0 * margin
boxX = 0
boxY = 0
textX = (boxW textW) / 2.0
raise = 0.5 * fontDescent extents
textY = fontHeight extents + (boxH textH) / 2.0 raise
textBB = BBox textX textY textW textH
boxBB = BBox boxX boxY boxW boxH
in TextBox text textBB boxBB
instance Widen TextBox where
widen tb@(TextBox _text textBB boxBB) minWidth =
let w = bbWidth boxBB
in if w >= minWidth
then tb
else let dw = minWidth w
in tb {tbTextBB = translate (dw / 2) 0 textBB,
tbBoxBB = widen boxBB minWidth}
instance Translate TextBox where
translate dx dy (TextBox text textBB boxBB) =
TextBox text (translate dx dy textBB) (translate dx dy boxBB)
tbWidth :: TextBox -> Double
tbWidth = bbWidth .tbBoxBB
tbSetWidth :: TextBox -> Double -> TextBox
tbSetWidth tbox w =
let TextBox text textBB boxBB = tbox
boxBB' = bbSetWidth boxBB w
(dx, dy) = positionDelta (bbCenter boxBB) (bbCenter boxBB')
in TextBox text (translate dx dy textBB) boxBB'
tbHeight :: TextBox -> Double
tbHeight = bbHeight . tbBoxBB
tbBottom :: TextBox -> Double
tbBottom = bbBottom . tbBoxBB
tbCenter :: TextBox -> Position
tbCenter = tbBoxCenter
tbBoxCenter :: TextBox -> Position
tbBoxCenter = bbCenter . tbBoxBB
tbTextCenter :: TextBox -> Position
tbTextCenter = bbCenter . tbTextBB
offsetTextBoxCenters :: Position -> TextBox -> TextBox -> TextBox
offsetTextBoxCenters offset anchor floater =
let Position ax ay = tbBoxCenter anchor
Position fx fy = tbBoxCenter floater
Position ox oy = offset
dx = ax + ox fx
dy = ay + oy fy
in translate dx dy floater
data GNode e = GNode {gnodeValue :: e,
gnodeTextBoxes :: [TextBox],
gnodeNodeBB :: BBox,
gnodeInlets :: [Iolet],
gnodeOutlets :: [Iolet]
}
deriving (Eq)
gnodeText :: GNode e -> String
gnodeText = tbText . head . gnodeTextBoxes
gnodeTextBB :: GNode e -> BBox
gnodeTextBB = tbTextBB . head . gnodeTextBoxes
instance (Show e) => Show (GNode e) where
show (GNode v tbs nodeBB inlets outlets) =
par "GNode" [show v, show tbs, show nodeBB, show inlets, show outlets]
instance (Repr e) => Repr (GNode e) where
repr (GNode v tbs nodeBB inlets outlets) =
par "GNode" [repr v, show tbs, show nodeBB, show inlets, show outlets]
instance Translate (GNode e) where
translate dx dy (GNode value textboxes nodeBB inlets outlets) =
GNode value
(map (translate dx dy) textboxes)
(translate dx dy nodeBB)
(translate dx dy inlets)
(translate dx dy outlets)
type IoletCounter e = e -> (Int, Int)
zeroIoletCounter :: IoletCounter e
zeroIoletCounter _node = (0, 0)
makeGNode :: (Repr e) => Style -> IoletCounter e -> e -> GNode e
makeGNode style countIolets value =
let textboxes1 = map (makeTextBox style) (reprl value)
textboxes2 = case textboxes1 of
[_tb] -> textboxes1
[tb1, tb2] ->
[tb1,
offsetTextBoxCenters (styleAuxOffset style) tb1 tb2]
_ -> wrong textboxes1
nodeBB = case textboxes2 of
[tb] -> tbBoxBB tb
[tb1, tb2] -> bbMerge (tbBoxBB tb1) (tbBoxBB tb2)
_ -> wrong textboxes2
wrong tbs =
errcats ["makeGNode: wrong no. of text boxes;",
"expected 1 or 2, but got", show (length tbs)]
(inlets, outlets) = makeIolets style nodeBB (countIolets value)
in GNode value textboxes2 nodeBB inlets outlets
makeIolets :: Style -> BBox -> (Int, Int) -> ([Iolet], [Iolet])
makeIolets style bbox (nin, nout) =
(makeIoletsRow style (bbXCenter bbox) (bbBottom bbox) nin,
makeIoletsRow style (bbXCenter bbox) (bbTop bbox) nout)
makeIoletsRow :: Style -> Double -> Double -> Int -> [Iolet]
makeIoletsRow style cx cy n =
let radius = styleIoletRadius style
diam = 2 * radius
w = fromIntegral n * diam
x1 = cx w / 2 + radius
x i = x1 + fromIntegral (i 1) * diam
make i = Iolet (Circle (Position (x i) cy) radius)
in map make [1..n]
newtype Iolet = Iolet Circle
deriving (Eq, Read, Show)
instance Translate Iolet where
translate dx dy (Iolet circle) = Iolet (translate dx dy circle)
ioletCenter :: Iolet -> Position
ioletCenter (Iolet circle) = circleCenter circle
pointInIolet :: Position -> Iolet -> Bool
pointInIolet point (Iolet circle) = pointInCircle point circle
treeSizes :: Style -> Tree (GNode e) -> Tree Size
treeSizes style gTree =
let subtreeSizes = map (treeSizes style) (subForest gTree)
(BBox _ _ rootWidth rootHeight) = gnodeNodeBB (rootLabel gTree)
treeWidth = max rootWidth (paddedWidth style subtreeSizes)
treeHeight = rootHeight + paddedHeight style subtreeSizes
in T.Node (Size treeWidth treeHeight) subtreeSizes
paddedWidth :: Style -> [Tree Size] -> Double
paddedWidth _style [] = 0
paddedWidth style subtrees =
sum [w | (T.Node (Size w _h) _) <- subtrees] +
hpad style * fromIntegral (length subtrees 1)
paddedHeight :: Style -> [Tree Size] -> Double
paddedHeight _style [] = 0
paddedHeight style subtrees =
vpad style + maximum [h | (T.Node (Size _w h) _) <- subtrees]
type TreeLayout e = Tree (LayoutNode e)
data LayoutNode e = LayoutNode {nodeGNode :: GNode e,
nodeTreeBB :: BBox}
deriving (Eq)
layoutNodeSource :: LayoutNode e -> e
layoutNodeSource = gnodeValue . nodeGNode
instance (Show e) => Show (LayoutNode e) where
show (LayoutNode gnode treebb) = par "LayoutNode" [show gnode, show treebb]
instance (Repr e) => Repr (LayoutNode e) where
repr (LayoutNode gnode treebb) = par "LayoutNode" [repr gnode, show treebb]
instance Translate (LayoutNode e) where
translate dx dy (LayoutNode gnode treeBB) =
LayoutNode (translate dx dy gnode)
(translate dx dy treeBB)
instance Widen (LayoutNode e) where
widen node@(LayoutNode gNode treeBB) minWidth =
let dw = bbWidth treeBB minWidth
in if dw <= 0
then node
else LayoutNode (translate (dw / 2) 0 gNode)
(widen treeBB minWidth)
layoutRootBB :: TreeLayout e -> BBox
layoutRootBB = gnodeNodeBB . nodeGNode . rootLabel
layoutTreeBB :: TreeLayout e -> BBox
layoutTreeBB = nodeTreeBB . rootLabel
treeLayout :: (Repr e) => Style -> IoletCounter e -> Tree e -> TreeLayout e
treeLayout style counter tree =
let t1 = treeGNodes style counter tree
t2 = treeSizes style t1
start = Position (hpad style) (vpad style)
t3 = treeLayout2 style start tree t1 t2
in treeLayoutAddMargin t3 (exomargin style)
treeGNodes :: Repr e =>
Style -> IoletCounter e -> Tree e -> Tree (GNode e)
treeGNodes style counter tree = fmap (makeGNode style counter) tree
treeLayout2 :: (Repr e) =>
Style
-> Position
-> Tree e
-> Tree (GNode e)
-> Tree Size
-> TreeLayout e
treeLayout2 style
(Position startX startY)
(T.Node _root subtrees)
(T.Node gnode subGNodes)
(T.Node (Size treeWidth treeHeight) subTreeSizes)
=
let
nodeHeight = bbHeight (gnodeNodeBB gnode)
subtreesTotalWidth = paddedWidth style subTreeSizes
subX = startX + (treeWidth subtreesTotalWidth) / 2
subY = startY + nodeHeight + vpad style
sublayouts :: (Repr e) =>
Double -> [Tree e] -> [Tree (GNode e)] -> [Tree Size] ->
[TreeLayout e]
sublayouts _ [] [] [] = []
sublayouts x (t:ts) (g:gs) (s:ss) =
treeLayout2 style (Position x subY) t g s :
sublayouts (x + sizeW (rootLabel s) + hpad style) ts gs ss
sublayouts _ _ _ _ = error "treeLayout2: mismatched list lengths"
in T.Node (LayoutNode
(centerGNode gnode startX startY treeWidth nodeHeight)
(BBox startX startY treeWidth treeHeight))
(sublayouts subX subtrees subGNodes subTreeSizes)
centerGNode :: GNode e -> Double -> Double -> Double -> Double -> GNode e
centerGNode gnode startx starty awidth aheight =
let cx = startx + awidth / 2
cy = starty + aheight / 2
Position cgx cgy = bbCenter (gnodeNodeBB gnode)
in translate (cx cgx) (cy cgy) gnode
treeLayoutPaddedSize :: Style -> TreeLayout e -> Size
treeLayoutPaddedSize style tlo =
let Size w h = treeLayoutSize tlo
in Size (w + 2.0 * hpad style) (h + 2.0 * vpad style)
treeLayoutSize :: TreeLayout e -> Size
treeLayoutSize tlo =
let BBox _x _y w h = layoutTreeBB tlo in Size w h
layoutTreeMoveCenterTo ::
Double -> Double -> TreeLayout e -> TreeLayout e
layoutTreeMoveCenterTo newX newY layoutTree =
let Position oldX oldY = bbCenter (nodeTreeBB (rootLabel layoutTree))
in translate (newX oldX) (newY oldY) layoutTree
treeLayoutAddMargin :: TreeLayout e -> Double -> TreeLayout e
treeLayoutAddMargin tree margin =
let LayoutNode {nodeGNode = rootGNode, nodeTreeBB = treeBB} =
rootLabel tree
subtrees = subForest tree
BBox x y w h = treeBB
treeBB' = BBox (x margin) (y margin)
(w + 2 * margin) (h + 2 * margin)
root' = translate margin margin (LayoutNode rootGNode treeBB')
subtrees' = translate margin margin subtrees
in T.Node root' subtrees'
treeLayoutWidth :: TreeLayout e -> Double
treeLayoutWidth = bbWidth . layoutTreeBB
treeLayoutWiden :: TreeLayout e -> Double -> TreeLayout e
treeLayoutWiden tlo minWidth =
let w = treeLayoutWidth tlo
in
if w >= minWidth
then tlo
else let dw = minWidth w
T.Node root subs = translate (dw / 2) 0 tlo
LayoutNode rootGNode treeBB = root
root' = LayoutNode rootGNode
(translate (dw / 2)
0
(widen treeBB minWidth))
in T.Node root' subs