packages feed

hgeometry-0.7.0.0: interactive/Viewer.hs

{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Main where

import           Control.Exception (catch)
import           Control.Lens
import           Control.Monad (forM_)
import           Data.Ext
import           Data.GI.Base
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Text as T
import qualified GI.Gdk as Gdk
import qualified GI.Gtk as Gtk
import           Graphics.Rendering.Cairo.Canvas (Canvas)
import qualified Graphics.Rendering.Cairo.Canvas as Canvas
import           Linear.Affine ((.-.),(.+^))
import           Linear.V2
import           Linear.Vector ((*^))
import           Reactive.Banana
import           Reactive.Banana.Frameworks
import           Reactive.Banana.GI.Gtk
import qualified RenderCanvas as Render
import           RenderUtil

main :: IO ()
main = runGtk `catch` (\(e::Gtk.GError) -> Gtk.gerrorMessage e >>= putStrLn . T.unpack)
  where
    runGtk = do
      _ <- Gtk.init Nothing
      compile networkDescription >>= actuate
      Gtk.main

data ArrowKey = UpKey | DownKey | LeftKey | RightKey deriving (Show,Read,Eq,Bounded,Enum)

toArrowKey         :: T.Text -> Maybe ArrowKey
toArrowKey "Up"    = Just UpKey
toArrowKey "Down"  = Just DownKey
toArrowKey "Left"  = Just LeftKey
toArrowKey "Right" = Just RightKey
toArrowKey _       = Nothing




toDirection          :: Num a => ArrowKey -> V2 a
toDirection UpKey    = V2 0    1
toDirection DownKey  = V2 0    (-1)
toDirection LeftKey  = V2 (-1) 0
toDirection RightKey = V2 1    0


networkDescription :: MomentIO ()
networkDescription = do
    b <- Gtk.builderNew
    _ <- Gtk.builderAddFromFile b "interactive/viewport.glade"

    window   <- castB b "window" Gtk.Window
    destroyE <- signalE0 window #destroy
    reactimate $ Gtk.mainQuit <$ destroyE

    -- mouseLabel <- castB b "mouseLabel" Gtk.Label

    drawingArea  <- castB b "canvas" Gtk.DrawingArea
    drawingAreaH <- realToFrac . fromIntegral . snd <$> #getPreferredHeight drawingArea
    drawingAreaW <- realToFrac . fromIntegral . snd <$> #getPreferredWidth  drawingArea

    Gtk.widgetAddEvents drawingArea (gflagsToWord [ Gdk.EventMaskPointerMotionMask
                                                  , Gdk.EventMaskButtonPressMask
                                                  , Gdk.EventMaskSmoothScrollMask
                                                  , Gdk.EventMaskKeyPressMask
                                                  ])

    -- scroll Events
    scrollE <- signalE1' drawingArea #scrollEvent $ \e ->
                     Gdk.getEventScrollDeltaY e

    -- events when we press a key
    keyPressedE <- signalE1' drawingArea #keyPressEvent $ \e -> do
                     v  <- Gdk.getEventKeyKeyval e
                     Gdk.keyvalName v
    -- events where we press an arrow key
    let arrowKeyE = filterJust . fmap (>>= toArrowKey) $ keyPressedE

    -- handle mouse clicks
    mousePressedE <- signalE1' drawingArea #buttonPressEvent $ \e -> do
                      x <- Gdk.getEventButtonX e
                      y <- Gdk.getEventButtonY e
                      return $! V2 x ((-1*y) + drawingAreaH)

    lastMousePressB <- stepper undefined mousePressedE

    -- mouse release
    -- mouseReleasedE <- signalE1' drawingArea #buttonReleaseEvent $ \e -> do
    --                     x <- Gdk.getEventButtonX e
    --                     y <- Gdk.getEventButtonY e
    --                     return $! V2 x ((-1*y) + drawingAreaH)

    -- mouse coordinates
    mouseMotionE <- signalE1' drawingArea #motionNotifyEvent $ \e -> do
                      x  <- Gdk.getEventMotionX e
                      y  <- Gdk.getEventMotionY e
                      st <- Gdk.getEventMotionState e
                      let !p = V2 x ((-1*y) + drawingAreaH)
                      return (p,st)
    mouseMotionB  <- stepper undefined mouseMotionE

    -- difference between the current mouse position and where we clicked last
    let dragOffsetB = (\p (q,_) -> p .-. q) <$> lastMousePressB <*> mouseMotionB

        -- sample the displacement vector whenever we are have a move event
        -- and the moude button is still on
        dragOffsetE = dragOffsetB
                   <@ filterE ((Gdk.ModifierTypeButton1Mask `elem`) . snd) mouseMotionE


    zoomLevelB <- accumB 1 $ (\dy -> (+0.1*dy)) <$> scrollE


    let lastPosE = unions [ (\k -> (.+^ 2 *^ toDirection k)) <$> arrowKeyE   -- key event
                          , (\v -> (.+^ v))                  <$> dragOffsetE -- drag event
                          ]
    viewPortPosB <- accumB (V2 (drawingAreaW/2) (drawingAreaH/2)) $ lastPosE

    let viewPortB = ViewPort <$> pure drawWorld'
                             <*> pure (V2 drawingAreaW drawingAreaH)
                             <*> viewPortPosB
                             <*> zoomLevelB
                             <*> pure 0

    -- draw everything
    draw drawingArea (mirrored drawingAreaH render <$> viewPortB)
    #showAll window

data ViewPort a = ViewPort { drawWorld             :: Canvas a
                           , screenSize            :: V2 Double
                           , clippwingWindowCenter :: V2 Double
                           , zoomLevel             :: Double
                           , rotation              :: Double
                           }

clippingWindow :: ViewPort a -> Canvas.Dim
clippingWindow (ViewPort _ (V2 w h) (V2 cx cy) z _) = let x = cx - z*w/2
                                                          y = cy - z*h/2
                                                      in Canvas.D x y (z*w) (z*h)

-- some drawing
drawWorld' :: Canvas ()
drawWorld' = do
    Canvas.background $ Canvas.gray 255
    Canvas.stroke $ Canvas.gray 0
    forM_ [1..20] $ \i ->
      forM_ [1..16] $ \j -> do
        Canvas.rect (Canvas.D (100*i) (100*j) 20 20)

-- | Mirror the canvas s.t. the bottom-left corner is the origin
mirrored       :: Double -> (a -> Canvas ()) -> a -> Canvas ()
mirrored h d x = do Canvas.scale     $ V2 1 (-1)
                    Canvas.translate $ V2 0 (-1*h)
                    d x

-- | Render the view
render    :: ViewPort a -> Canvas a
render vp = do
    let (Canvas.D x y _ _) = clippingWindow vp

    Canvas.scale $ V2 (1/zoomLevel vp) (1/zoomLevel vp) -- scale everything s.t. the
                                                        -- cippingWindow equals
                                                        -- the window size
    Canvas.translate $ V2 (-1*x) (-1*y) -- move screen to the origin
    drawWorld vp