159 lines
5.0 KiB
Haskell
159 lines
5.0 KiB
Haskell
{-# LANGUAGE OverloadedStrings #-}
|
|
module UserInterface where
|
|
|
|
import qualified GI.Gtk as G
|
|
import qualified GI.Gdk as Gdk
|
|
import qualified GI.Cairo.Render as R
|
|
import qualified GI.Cairo.Render.Connector as C
|
|
import Data.GI.Base.ManagedPtr (unsafeCastTo)
|
|
import Data.Maybe (fromJust)
|
|
import Data.IORef
|
|
import Control.Monad (when)
|
|
|
|
import Queries
|
|
|
|
data DrawingContext = DC
|
|
{ path :: Path Double
|
|
, lastPos :: Maybe (Point Double)
|
|
, isErasing :: Bool
|
|
}
|
|
|
|
emptyDC :: DrawingContext
|
|
emptyDC = DC
|
|
{ path = []
|
|
, lastPos = Nothing
|
|
, isErasing = False
|
|
}
|
|
|
|
-- Radius of the circle representing the eraser
|
|
circleRadius :: Double
|
|
circleRadius = 50
|
|
|
|
setLastPos :: Maybe (Point Double) -> DrawingContext -> DrawingContext
|
|
setLastPos p (DC path _ e) = DC path p e
|
|
|
|
isMouseDown :: DrawingContext -> Bool
|
|
isMouseDown (DC _ Nothing _) = False
|
|
isMouseDown (DC _ _ _) = True
|
|
|
|
setErasing :: Bool -> DrawingContext -> DrawingContext
|
|
setErasing e (DC p l _) = DC p l e
|
|
|
|
addPoint :: Point Double -> DrawingContext -> DrawingContext
|
|
addPoint _ (DC path Nothing e) = DC path Nothing e
|
|
addPoint p (DC path (Just lastPos) e) =
|
|
let newLine = Line lastPos p
|
|
in DC (newLine : path) (Just p) e
|
|
|
|
erase :: OpenCircle Double -> DrawingContext -> DrawingContext
|
|
erase c (DC path _ e) = DC (removeCircleFromPath c path) (Just (center c)) e
|
|
-- ^ Change last mouse pos to the circle center
|
|
|
|
drawPath :: Path Double -> R.Render ()
|
|
drawPath [] = return ()
|
|
drawPath (line:lines) = do
|
|
R.moveTo (xCoord (p1 line)) (yCoord (p1 line))
|
|
R.lineTo (xCoord (p2 line)) (yCoord (p2 line))
|
|
drawPath lines
|
|
|
|
drawAll :: DrawingContext -> Gdk.Rectangle -> R.Render ()
|
|
drawAll (DC path lastPos isErasing) rect = do
|
|
x <- fromIntegral <$> Gdk.getRectangleX rect
|
|
y <- fromIntegral <$> Gdk.getRectangleY rect
|
|
width <- fromIntegral <$> Gdk.getRectangleWidth rect
|
|
height <- fromIntegral <$> Gdk.getRectangleHeight rect
|
|
|
|
R.setSourceRGB 0 0 0
|
|
R.rectangle x y width height
|
|
R.fill
|
|
-- Path color
|
|
R.setSourceRGB 255 255 255
|
|
drawPath (reverse path)
|
|
R.stroke
|
|
|
|
when isErasing $ case lastPos of
|
|
Nothing -> return ()
|
|
Just (Point mouseX mouseY) -> do
|
|
-- Eraser color
|
|
R.setSourceRGB 0 0 255
|
|
R.arc mouseX mouseY circleRadius 0 (2 * pi)
|
|
R.stroke
|
|
|
|
|
|
doDraw :: G.DrawingArea -> IORef DrawingContext -> Point Double -> IO ()
|
|
doDraw drawingArea ctx pos = do
|
|
mouseDown <- isMouseDown <$> readIORef ctx
|
|
-- Only add points if we are drawing
|
|
when mouseDown $ do
|
|
modifyIORef' ctx (addPoint pos)
|
|
G.widgetQueueDraw drawingArea
|
|
|
|
doErase :: G.DrawingArea -> IORef DrawingContext -> Point Double -> IO ()
|
|
doErase drawingArea ctx pos = do
|
|
mouseDown <- isMouseDown <$> readIORef ctx
|
|
-- Only erase when ouse down
|
|
when mouseDown $ do
|
|
let circle = OpenCircle pos circleRadius
|
|
modifyIORef' ctx (erase circle)
|
|
G.widgetQueueDraw drawingArea
|
|
|
|
mainGUI :: IORef DrawingContext -> IO ()
|
|
mainGUI ctx = do
|
|
G.init Nothing
|
|
|
|
builder <- G.builderNew
|
|
G.builderAddFromFile builder "geometry.ui"
|
|
|
|
window <- G.builderGetObject builder "window" >>= unsafeCastTo G.Window . fromJust
|
|
eraseSwitch <- G.builderGetObject builder "eraseSwitch" >>= unsafeCastTo G.Switch . fromJust
|
|
drawingArea <- G.builderGetObject builder "drawingArea" >>= unsafeCastTo G.DrawingArea . fromJust
|
|
|
|
-- receive mouse events
|
|
G.widgetAddEvents drawingArea [Gdk.EventMaskPointerMotionMask, Gdk.EventMaskButtonPressMask, Gdk.EventMaskButtonReleaseMask]
|
|
|
|
-- When we press on the button we calculate the convex hull
|
|
-- G.onButtonClicked button $ do
|
|
-- points <- paths <$> readIORef ctx
|
|
-- let convexHullPath = convexHull (concat points)
|
|
-- modifyIORef' ctx (setConvexHull convexHullPath)
|
|
-- G.widgetQueueDraw drawingArea
|
|
|
|
G.onSwitchStateSet eraseSwitch $ \newState -> do
|
|
modifyIORef' ctx (setErasing newState)
|
|
return True
|
|
|
|
G.onWidgetDraw drawingArea $ \cairoCtx -> do
|
|
rect <- G.widgetGetAllocation drawingArea
|
|
dc <- readIORef ctx
|
|
C.renderWithContext (drawAll dc rect) cairoCtx
|
|
return True
|
|
|
|
G.onWidgetMotionNotifyEvent drawingArea $ \e -> do
|
|
x <- Gdk.getEventMotionX e
|
|
y <- Gdk.getEventMotionY e
|
|
let pos = Point x y
|
|
isErasing <- isErasing <$> readIORef ctx
|
|
|
|
if isErasing
|
|
then doErase drawingArea ctx pos
|
|
else doDraw drawingArea ctx pos
|
|
|
|
return True
|
|
|
|
G.onWidgetButtonPressEvent drawingArea $ \e -> do
|
|
x <- Gdk.getEventButtonX e
|
|
y <- Gdk.getEventButtonY e
|
|
let pos = Point x y
|
|
modifyIORef' ctx (setLastPos (Just pos))
|
|
return True
|
|
|
|
G.onWidgetButtonReleaseEvent drawingArea $ \_ -> do
|
|
modifyIORef' ctx (setLastPos Nothing)
|
|
return False
|
|
|
|
G.onWidgetDestroy window G.mainQuit
|
|
|
|
G.widgetShowAll window
|
|
|
|
G.main
|