ghc-vis-0.5: src/GHC/Vis.hs
{-# LANGUAGE CPP, RankNTypes, ImpredicativeTypes #-}
{- |
Module : GHC.Vis
Copyright : (c) Dennis Felsing
License : 3-Clause BSD-style
Maintainer : dennis@felsin9.de
Although ghc-vis is meant to be used in GHCi it can also be used as a library
in regular Haskell programs which are run or compiled by GHC. You can run those
programs using \"runghc example.hs\" or \"ghc -threaded example.hs && ./example\".
Without the \"-threaded\"-Flag ghc-vis does not work correctly. This is an
example using ghc-vis outside of GHCi:
> import GHC.Vis
>
> main = do
> putStrLn "Start"
> let a = "teeest"
> let b = [1..3]
> let c = b ++ b
> let d = [1..]
> putStrLn $ show $ d !! 1
>
> visualization
> view a "a"
> view b "b"
> view c "c"
> view d "d"
>
> getChar
> switch
>
> getChar
> putStrLn "End"
-}
module GHC.Vis (
visualization, -- TODO: Maybe rename to vis
view,
eval,
switch,
update,
clear,
export
)
where
#if __GLASGOW_HASKELL__ < 706
import Prelude hiding (catch, error)
#else
import Prelude hiding (error)
#endif
import Graphics.UI.Gtk hiding (Box, Signal)
import qualified Graphics.UI.Gtk.Gdk.Events as E
import System.IO
import Control.Concurrent
import Control.Monad
import Control.Exception hiding (evaluate)
import Data.Char
import Data.IORef
import System.Timeout
import System.Mem
import GHC.HeapView hiding (name)
import GHC.Vis.Types hiding (view)
import qualified GHC.Vis.Types as T
import GHC.Vis.View.Common
import qualified GHC.Vis.View.List as List
#ifdef GRAPH_VIEW
import Data.GraphViz.Commands
import qualified GHC.Vis.View.Graph as Graph
#endif
import Graphics.Rendering.Cairo
views :: [View]
views =
View List.redraw List.click List.move List.updateObjects List.export :
#ifdef GRAPH_VIEW
View Graph.redraw Graph.click Graph.move Graph.updateObjects Graph.export :
#endif
[]
title :: String
title = "ghc-vis"
backgroundColor :: Color
backgroundColor = Color 0xffff 0xffff 0xffff
defaultSize :: (Int, Int)
defaultSize = (640, 480)
zoomIncrement :: Double
zoomIncrement = 1.25
positionIncrement :: Double
positionIncrement = 50
bigPositionIncrement :: Double
bigPositionIncrement = 200
signalTimeout :: Int
signalTimeout = 1000000
-- | This is the main function. It's to be called from GHCi and launches a
-- graphical window in a new thread.
visualization :: IO ()
visualization = do
vr <- swapMVar visRunning True
unless vr $ void $ forkIO visMainThread
-- | Add expressions with a name to the visualization window.
view :: a -> String -> IO ()
view a name = put $ NewSignal (asBox a) name
-- | Evaluate an object that is shown in the visualization. (Names start with 't')
eval :: String -> IO ()
eval t = evaluate t >> update
-- | Switch between the list view and the graph view
switch :: IO ()
switch = put SwitchSignal
-- | When an object is updated by accessing it, you have to call this to
-- refresh the visualization window. You can also click on an object to force
-- an update.
update :: IO ()
update = put UpdateSignal
-- | Clear the visualization window, removing all expressions from it.
clear :: IO ()
clear = put ClearSignal
-- | Export the current visualization view to a file, format depends on the
-- file ending. Currently supported: svg, png, pdf, ps
export :: String -> IO ()
export filename = case mbDrawFn of
Right error -> putStrLn error
Left _ -> put $ ExportSignal ((\(Left x) -> x) mbDrawFn) filename
where mbDrawFn = case map toLower (reverse . take 4 . reverse $ filename) of
".svg" -> Left withSVGSurface
".pdf" -> Left withPDFSurface
".png" -> Left withPNGSurface
_:".ps" -> Left withPSSurface
_ -> Right "Unknown file extension, try one of the following: .svg, .pdf, .ps, .png"
withPNGSurface filePath width height action =
withImageSurface FormatARGB32 (ceiling width) (ceiling height) $
\surface -> do
ret <- action surface
surfaceWriteToPNG surface filePath
return ret
put :: Signal -> IO ()
put s = void $ timeout signalTimeout $ putMVar visSignal s
visMainThread :: IO ()
visMainThread = do
initGUI
window <- windowNew
canvas <- drawingAreaNew
widgetModifyBg canvas StateNormal backgroundColor
set window [ windowTitle := title
, containerChild := canvas
]
(uncurry $ windowSetDefaultSize window) defaultSize
onExpose canvas $ const $ do
runCorrect redraw >>= \f -> f canvas
runCorrect move >>= \f -> f canvas
return True
onMotionNotify canvas False $ \e -> do
state <- readIORef visState
modifyIORef visState (\s -> s {mousePos = (E.eventX e, E.eventY e)})
if dragging state
then do
let (oldX, oldY) = mousePos state
(deltaX, deltaY) = (E.eventX e - oldX, E.eventY e - oldY)
(oldPosX, oldPosY) = position state
modifyIORef visState (\s -> s {position = (oldPosX + deltaX, oldPosY + deltaY)})
widgetQueueDraw canvas
else
runCorrect move >>= \f -> f canvas
return True
onButtonPress canvas $ \e -> do
when (E.eventButton e == LeftButton && E.eventClick e == SingleClick) $
join $ runCorrect click
when (E.eventButton e == RightButton && E.eventClick e == SingleClick) $
modifyIORef visState (\s -> s {dragging = True})
when (E.eventButton e == MiddleButton && E.eventClick e == SingleClick) $ do
modifyIORef visState (\s -> s {zoomRatio = 1, position = (0, 0)})
widgetQueueDraw canvas
return True
onButtonRelease canvas $ \e -> do
when (E.eventButton e == RightButton) $
modifyIORef visState (\s -> s {dragging = False})
return True
onScroll canvas $ \e -> do
state <- readIORef visState
when (E.eventDirection e == ScrollUp) $ do
let newZoomRatio = zoomRatio state * zoomIncrement
newPos <- zoomImage canvas state newZoomRatio (mousePos state)
modifyIORef visState (\s -> s {zoomRatio = newZoomRatio, position = newPos})
when (E.eventDirection e == ScrollDown) $ do
let newZoomRatio = zoomRatio state / zoomIncrement
newPos <- zoomImage canvas state newZoomRatio (mousePos state)
modifyIORef visState (\s -> s {zoomRatio = newZoomRatio, position = newPos})
widgetQueueDraw canvas
return True
onKeyPress window $ \e -> do
--putStrLn $ E.eventKeyName e
state <- readIORef visState
when (E.eventKeyName e `elem` ["plus", "Page_Up", "KP_Add"]) $ do
let newZoomRatio = zoomRatio state * zoomIncrement
(oldX, oldY) = position state
newPos = (oldX*zoomIncrement, oldY*zoomIncrement)
modifyIORef visState (\s -> s {zoomRatio = newZoomRatio, position = newPos})
when (E.eventKeyName e `elem` ["minus", "Page_Down", "KP_Subtract"]) $ do
let newZoomRatio = zoomRatio state / zoomIncrement
(oldX, oldY) = position state
newPos = (oldX/zoomIncrement, oldY/zoomIncrement)
modifyIORef visState (\s -> s {zoomRatio = newZoomRatio, position = newPos})
when (E.eventKeyName e `elem` ["0", "equal"]) $
modifyIORef visState (\s -> s {zoomRatio = 1, position = (0, 0)})
when (E.eventKeyName e `elem` ["Left", "h", "a"]) $
modifyIORef visState (\s ->
let (x,y) = position s
newX = x + positionIncrement
in s {position = (newX, y)})
when (E.eventKeyName e `elem` ["Right", "l", "d"]) $
modifyIORef visState (\s ->
let (x,y) = position s
newX = x - positionIncrement
in s {position = (newX, y)})
when (E.eventKeyName e `elem` ["Up", "k", "w"]) $
modifyIORef visState (\s ->
let (x,y) = position s
newY = y + positionIncrement
in s {position = (x, newY)})
when (E.eventKeyName e `elem` ["Down", "j", "s"]) $
modifyIORef visState (\s ->
let (x,y) = position s
newY = y - positionIncrement
in s {position = (x, newY)})
when (E.eventKeyName e `elem` ["H", "A"]) $
modifyIORef visState (\s ->
let (x,y) = position s
newX = x + bigPositionIncrement
in s {position = (newX, y)})
when (E.eventKeyName e `elem` ["L", "D"]) $
modifyIORef visState (\s ->
let (x,y) = position s
newX = x - bigPositionIncrement
in s {position = (newX, y)})
when (E.eventKeyName e `elem` ["K", "W"]) $
modifyIORef visState (\s ->
let (x,y) = position s
newY = y + bigPositionIncrement
in s {position = (x, newY)})
when (E.eventKeyName e `elem` ["J", "S"]) $
modifyIORef visState (\s ->
let (x,y) = position s
newY = y - bigPositionIncrement
in s {position = (x, newY)})
when (E.eventKeyName e `elem` ["space", "Return", "KP_Enter"]) $
join $ runCorrect click
when (E.eventKeyName e `elem` ["v"]) $
put SwitchSignal
when (E.eventKeyName e `elem` ["c"]) $
put ClearSignal
when (E.eventKeyName e `elem` ["u"]) $
put UpdateSignal
widgetQueueDraw canvas
return True
widgetShowAll window
reactThread <- forkIO $ react canvas window
--onDestroy window mainQuit -- Causes :r problems with multiple windows
onDestroy window (quit reactThread)
mainGUI
return ()
quit :: ThreadId -> IO ()
quit reactThread = do
swapMVar visRunning False
killThread reactThread
react :: (WidgetClass w1, WidgetClass w2) => w1 -> w2 -> IO b
react canvas window = do
-- Timeout used to handle ghci reloads (:r)
-- Reloads cause the visSignal to be reinitialized, but takeMVar is still
-- waiting for the old one. This solution is not perfect, but it works for
-- now.
mbSignal <- timeout signalTimeout (takeMVar visSignal)
case mbSignal of
Nothing -> do
running <- readMVar visRunning
if running then react canvas window else
-- :r caused visRunning to be reset
(do swapMVar visRunning True
timeout signalTimeout (putMVar visSignal UpdateSignal)
react canvas window)
Just signal -> do
case signal of
NewSignal x n -> modifyMVar_ visBoxes (
\y -> if (x,n) `elem` y then return y else return $ y ++ [(x,n)])
ClearSignal -> modifyMVar_ visBoxes (\_ -> return [])
UpdateSignal -> return ()
SwitchSignal -> doSwitch
ExportSignal d f -> catch (runCorrect exportView >>= \e -> e d f)
(\e -> do let err = show (e :: IOException)
hPutStrLn stderr $ "Couldn't export to file \"" ++ f ++ "\": " ++ err
return ())
boxes <- readMVar visBoxes
performGC -- TODO: Else Blackholes appear. Do we want this?
-- Blackholes stop our current thread and only resume after
-- they have been replaced with their result, thereby leading
-- to an additional element in the HeapMap we don't want.
-- Example for bad behaviour that would happen then:
-- λ> let xs = [1..42] :: [Int]
-- λ> let x = 17 :: Int
-- λ> let ys = [ y | y <- xs, y >= x ]
runCorrect updateObjects >>= \f -> f boxes
postGUISync $ widgetQueueDraw canvas
react canvas window
#ifdef GRAPH_VIEW
where doSwitch = isGraphvizInstalled >>= \gvi -> if gvi
then modifyIORef visState (\s -> s {T.view = succN (T.view s), zoomRatio = 1, position = (0, 0)})
else putStrLn "Cannot switch view: Graphviz not installed"
succN GraphView = ListView
succN ListView = GraphView
#else
where doSwitch = putStrLn "Cannot switch view: Graph view disabled at build"
#endif
runCorrect :: (View -> f) -> IO f
runCorrect f = do
s <- readIORef visState
return $ f $ views !! fromEnum (T.view s)
zoomImage :: WidgetClass w1 => w1 -> State -> Double -> T.Point -> IO T.Point
zoomImage _canvas s newZoomRatio _mousePos@(_x', _y') = do
-- TODO: Mouse must stay at same spot
--E.Rectangle _ _ rw' rh' <- widgetGetAllocation canvas
--let (rw, rh) = (fromIntegral rw', fromIntegral rh')
--let (x,y) = (x' - rw / 2, y' - rh / 2)
let (oldPosX, oldPosY) = position s
zoom = newZoomRatio / zoomRatio s
newPos = (oldPosX * zoom, oldPosY * zoom)
return newPos
-- Zoom into mouse, but only working from (0,0)
-- newPos = ( oldPosX + x * zoomRatio s - x * newZoomRatio
-- , oldPosY + y * zoomRatio s - y * newZoomRatio )
-- Zoom into center:
-- newPos = ( oldPosX * zoomIncrement
-- , oldPosY * zoomIncrement )