happlets-lib-gtk-0.1.0.0: src/Happlets/Lib/Gtk.hs
-- | This is the Gtk+ version 2 back-end for Happlets, and will also serve as the Happlet referecne
-- implementation. Creating a window automatically calls the the Gtk+ initializer. Use
-- 'newGtkWindow' to create a new window, and any of the "Happlet.World" functinos to manipulate the
-- windows. This module re-exports the "Happlets" module so it is not necessary to import both.
module Happlets.Lib.Gtk
( gtkHapplet, GtkGUI, GtkRedraw,
GtkWindow, gtkLaunchEventLoop,
GtkImage, CairoRender(..), GtkCairoDiagram, gtkCairoDiagram,
cairoClearCanvas, cairoSetColorRGBA32,
gtkAnimationFrameRate,
module Happlets,
module Happlets.Draw
)
where
----------------------------------------------------------------------------------------------------
import Happlets
import Happlets.Draw
import Happlets.Provider
import Control.Arrow
import Control.Concurrent
--import Control.Lens
--import Control.Monad
--import Control.Monad.Reader
import Control.Monad.State
import Data.IORef
import Data.Maybe
import Data.Semigroup
import qualified Data.Text as Strict
import Data.Time.Clock
--import Data.Typeable
import Data.Word
import qualified Graphics.Rendering.Cairo as Cairo
--import qualified Graphics.UI.Gtk.Abstract.Container as Gtk
import qualified Graphics.UI.Gtk.Abstract.Widget as Gtk
import qualified Graphics.UI.Gtk.Cairo as Gtk
--import qualified Graphics.UI.Gtk.Display.Image as Gtk
--import qualified Graphics.UI.Gtk.Gdk.GtkWindowState as Gtk
import qualified Graphics.UI.Gtk.Gdk.Drawable as Gtk
import qualified Graphics.UI.Gtk.Gdk.DrawWindow as Gtk
import qualified Graphics.UI.Gtk.Gdk.EventM as Gtk
--import qualified Graphics.UI.Gtk.Gdk.Keys as Gtk
import qualified Graphics.UI.Gtk.Gdk.GC as Gtk
import qualified Graphics.UI.Gtk.Gdk.Pixmap as Gtk
import qualified Graphics.UI.Gtk.Gdk.Region as Gtk
import qualified Graphics.UI.Gtk.Gdk.Screen as Gtk
import qualified Graphics.UI.Gtk.General.General as Gtk
--import qualified Graphics.UI.Gtk.Layout.Alignment as Gtk
import qualified Graphics.UI.Gtk.Windows.Window as Gtk
import Diagrams.Backend.Cairo.Internal
import Diagrams.BoundingBox
import Diagrams.Core.Compile
import Diagrams.Core.Types
import Diagrams.Size (dims)
import Linear.Affine
import Linear.V2 (V2(..))
import System.IO
import System.IO.Unsafe
--import System.Glib.Attributes (AttrOp(..))
--import qualified System.Glib.Attributes as Glib
import qualified System.Glib.Signals as Glib
import qualified System.Glib.Utils as Glib
import qualified System.Glib.MainLoop as Glib
import Debug.Trace
----------------------------------------------------------------------------------------------------
debugThisModule :: Bool
debugThisModule = True
type LogGUI = String -> IO ()
mkLogger
:: (Monad m, MonadIO m)
=> String -> Bool -> m LogGUI
mkLogger func enable = return $ if not (debugThisModule && enable)
then const $ return ()
else \ msg -> do
tid <- myThreadId
traceIO $ '[' : show tid ++ "][Happlets.Lib.Gtk." ++ func ++ "] " ++ msg
----------------------------------------------------------------------------------------------------
dToW16 :: Double -> Word16
dToW16 = round . (*) (realToFrac (maxBound::Word16))
dToGrey :: Double -> Gtk.Color
dToGrey d = Gtk.Color (dToW16 d) (dToW16 d) (dToW16 d)
-- | This is the frame rate used by default when installing an animation event handler. This value
-- can be configured by modifying the 'Happlets.Config.animationFrameRate'.
gtkAnimationFrameRate :: Double
gtkAnimationFrameRate = 60.0
----------------------------------------------------------------------------------------------------
-- | This data type acts as a handle to a Gtk+ environment, which contains pointers to the window
-- and graphpics contexts which you can manipulate by inserting or removing your own Happlets to be
-- displayed within it.
data GtkWindow
= GtkUnlockedWin !(MVar GtkWindowState)
-- ^ This is the constructor used by public-facing APIs. The 'Happlets.Initialize.newWindow'
-- 'Happlets.Initialize.deleteWindow', and 'Happlets.Initialize.attachWindow' functions all
-- return or pass this constructor around.
| GtkLockedWin !(GtkWindowState)
-- ^ This is the constructor use internally, particularly when evaluating the 'Happlets.GUI.GUI'
-- monad. When evaluation completes, it is stored back into the MVar for 'GtkUnlockedWin' and the
-- 'GtkUnlockedWin' is returned instead.
-- | A monadic wrapper around a 'Cario.Render' monad. The only reason for this to exist is because
-- there needs to be an instance of 'Data.Semigroup.Semigroup'.
--
-- The 'Controller' type is defined to use the 'CairoRender' as it's @view@ type. You can also
-- convert a 'GtkCairoDiagram' to a 'CairoRender' using the 'gtkCairoDiagram' function.
newtype CairoRender a = CairoRender { runCairoRender :: Cairo.Render a }
deriving (Functor, Applicative, Monad, MonadIO)
instance Semigroup a => Semigroup (CairoRender a) where
(CairoRender a) <> (CairoRender b) = CairoRender $ (<>) <$> a <*> b
instance Monoid a => Monoid (CairoRender a) where
mappend (CairoRender a) (CairoRender b) = CairoRender $ mappend <$> a <*> b
mempty = return mempty
-- | Contains parameters that can exist /after/ the 'Gtk.Window' has been allocated. These
-- parameters are stored in an 'MVar' which is initialized in an empty state until the Gtk+ window
-- is actually made visible (when it recieves the first "configure" event). The reason for this is
-- that many of the 'Happlets.GUI.GUI' functions may try to evaluate a redraw before the window
-- exists and the DrawWIndow has been allocated.
data GtkWindowLive
= GtkWindowLive
{ gtkDrawWindow :: !Gtk.DrawWindow
, theGtkPixmap :: !Gtk.Pixmap
-- ^ This is the 'Gtk.Pixmap' that contains the buffer of the image that is displayed in the
-- Happlet window. Every time the window is resized, this object is re-allocated, so it is
-- important that this value be accessible in only one location, do not store a copy of it
-- into an MVar or IORef which other threads may write to arbitrarily, unless you like the
-- image to be garbled.
, theGtkGraphCtx :: !Gtk.GC
-- ^ This is the old-fashioned GUI graphics context, which keeps track of things like
-- foreground color, background color, clip region, and pen location. This is used for Gdk
-- (not Gtk+) drawing primitives. As far as I know, this data structure is deprecated and does
-- not even exist in Gtk+ version 3, but it is still the only way to perform certain
-- graphics operations in Gtk+ version 2.
}
data GtkWindowState
= GtkWindowState
{ currentConfig :: !Config
, thisWindow :: !(MVar GtkWindowState)
, gtkWindowLive :: !(MVar GtkWindowLive)
, gtkWindow :: !Gtk.Window
, theInitReaction :: !(ConnectReact PixSize)
, theResizeReaction :: !(ConnectReact PixCoord)
, theVisibilityReaction :: !(ConnectReact Bool)
, theFocusReaction :: !(ConnectReact Bool)
, theMouseHandler :: !(ConnectReact Mouse)
, theCursorHandler :: !(ConnectReact Mouse)
, theKeyHandler :: !(ConnectReact Keyboard)
, theAnimatorThread :: !(ConnectReact AnimationMoment)
}
data ConnectReact event
= Disconnected
| ConnectReact{ doDisconnect :: GtkState (), doReact :: event -> GtkState Bool }
evalConnectReact :: ConnectReact event -> event -> GtkState Bool
evalConnectReact = \ case
Disconnected -> const $ return False
ConnectReact{doReact=f} -> f
forceDisconnect :: Lens' GtkWindowState (ConnectReact event) -> GtkState ()
forceDisconnect connection = use connection >>= \ case
ConnectReact{doDisconnect=discon} -> discon >> connection .= Disconnected
Disconnected -> return ()
--gtkPixmap :: Lens' GtkWindowLive Gtk.Pixmap
--gtkPixmap = lens theGtkPixmap $ \ a b -> a{ theGtkPixmap = b }
--gtkGraphCtx :: Lens' GtkWindowLive Gtk.GC
--gtkGraphCtx = lens theGtkGraphCtx $ \ a b -> a{ theGtkGraphCtx = b }
initReaction :: Lens' GtkWindowState (ConnectReact PixSize)
initReaction = lens theInitReaction $ \ a b -> a{ theInitReaction = b }
visibilityReaction :: Lens' GtkWindowState (ConnectReact Bool)
visibilityReaction = lens theVisibilityReaction $ \ a b -> a{ theVisibilityReaction = b }
focusReaction :: Lens' GtkWindowState (ConnectReact Bool)
focusReaction = lens theFocusReaction $ \ a b -> a{ theFocusReaction = b }
resizeReaction :: Lens' GtkWindowState (ConnectReact PixCoord)
resizeReaction = lens theResizeReaction $ \ a b -> a{ theResizeReaction = b }
mouseHandler :: Lens' GtkWindowState (ConnectReact Mouse)
mouseHandler = lens theMouseHandler $ \ a b -> a{ theMouseHandler = b }
cursorHandler :: Lens' GtkWindowState (ConnectReact Mouse)
cursorHandler = lens theCursorHandler $ \ a b -> a{ theCursorHandler = b }
keyHandler :: Lens' GtkWindowState (ConnectReact Keyboard)
keyHandler = lens theKeyHandler $ \ a b -> a{ theKeyHandler = b }
animatorThread :: Lens' GtkWindowState (ConnectReact AnimationMoment)
animatorThread = lens theAnimatorThread $ \ a b -> a{ theAnimatorThread = b }
-- Create an image buffer with an alpha channel for a 'Graphics.UI.Gtk.Abstract.Widget.Widget' to
-- be rendered.
mkAlphaChannel :: LogGUI -> Gtk.Widget -> IO ()
mkAlphaChannel logGUI window = do
logGUI $ "Gtk.widgetGetScreen"
screen <- Gtk.widgetGetScreen window
logGUI $ "Gtk.screenGetRGBAColormap"
colormap <- Gtk.screenGetRGBAColormap screen
case colormap of
Nothing -> do
logGUI $ "-- Colormap dos NOT support alpha channels"
return ()
Just colormap -> do
logGUI $ "Gtk.widgetSetColorMap"
Gtk.widgetSetColormap window colormap
--gtkCanvas :: GtkState Gtk.DrawWindow
--gtkCanvas = gets gtkWindowLive >>= liftIO . liftM gtkDrawWindow . readMVar
----------------------------------------------------------------------------------------------------
newtype GtkState a
= GtkState { unwrapGtkState :: StateT GtkWindowState IO a }
deriving (Functor)
instance Applicative GtkState where
pure = return
(<*>) = ap
instance Monad GtkState where
return = GtkState . return
(GtkState a) >>= b = GtkState $ a >>= unwrapGtkState . b
instance MonadState GtkWindowState GtkState where
state = GtkState . state
instance MonadIO GtkState where
liftIO = GtkState . liftIO
runGtkState :: GtkState a -> GtkWindowState -> IO (a, GtkWindowState)
runGtkState (GtkState f) = runStateT f
-- | This function acquires a lock on the 'GtkState' and begins evaluating the 'GtkState'
-- function. This function should be evaluated at the top level of the procedure that is evaluated
-- by an event handler.
lockGtkWindow :: GtkWindow -> GtkState a -> IO a
lockGtkWindow win f = case win of
GtkLockedWin{} -> error "lockGtkWindow: evaluated on an already locked 'GtkLockedWin' window."
GtkUnlockedWin mvar -> modifyMVar mvar $ liftM (\ (a, b) -> (b, a)) . runGtkState f
-- | This function evaluates 'Happlets.GUI.GUI' functions within event handlers. It evaluates to a
-- 'GtkState' function, which means you are required to have first evaluated 'unlockGtkWindow'. This
-- function will then lock the 'Happlets.GUI.Happlet' and then evaluate the 'Happlets.GUI.GUI'
-- function.
gtkRunHapplet
:: Happlet model -> (GUIContinue -> GtkState ()) -> GtkGUI model void -> GtkState Bool
gtkRunHapplet happ cont f = do
logGUI <- mkLogger "gtkRunHapplet" True
env <- get
liftIO $ logGUI $ "locking Happlet, evaluating GUI function..."
((env, keep), _) <- liftIO $ onHapplet happ $ \ model -> do
logGUI "evalGUI"
gui <- evalGUI f happ (GtkLockedWin env) model
let model = theGUIModel gui
case theGUIWindow gui of
GtkUnlockedWin{} -> error
"gtkRunHapplet: evalGUI evaluated on GtkLockedWin but returned GtkUnlockedWin"
GtkLockedWin env -> do
logGUI "runGtkState -- evaluate continuation"
((), env) <- runGtkState (cont $ theGUIContinue gui) env
return ((env, guiIsLive gui), model)
state $ const (keep, env)
-- | This function is intended to be passed as a parameter to 'gtkRunHapplet' by event handling
-- functions which check if the 'Happlets.GUI.GUI' function evaluated to 'Happlets.GUI.disable'.
checkGUIContinue
:: LogGUI
-> Lens' GtkWindowState (ConnectReact event)
-> GUIContinue
-> GtkState ()
checkGUIContinue logGUI connection = \ case
GUIHalt -> forceDisconnect connection
GUIFail msg -> do
liftIO $ logGUI $ "GUI function evaluated to \"fail\" with message:\n " ++ show msg
forceDisconnect connection
GUIContinue -> return ()
-- | This function should be evaluated from within a 'GtkGUI' function when it is necessary to
-- update the 'GtkWindowState', usually this is for installing or removing event handlers.
runGtkStateGUI :: GtkState a -> GtkGUI model a
runGtkStateGUI f = getGUIState >>= \ gui -> case theGUIWindow gui of
GtkUnlockedWin{} -> error $ "runGtkStateGUI: " ++
"Evaluated a GtkState function within a GtkGUI function on a locked GtkWindow."
-- The 'window' value passed to the 'evalGUI' function must be a 'GtkLockedWin' constructor.
GtkLockedWin env -> do
(a, env) <- liftIO $ runGtkState f env
putGUIState $ gui{ theGUIWindow = GtkLockedWin env }
return a
-- | Create the window and install the permanent event handlers.
createWin :: Config -> IO GtkWindowState
createWin cfg = do
logGUI <- mkLogger "createWin" True
forM_ (configErrorsOnLoad cfg) (hPrint stderr)
logGUI $ "Gtk.windowNew"
window <- Gtk.windowNew
logGUI $ "Gtk.widgetSetHasWindow"
Gtk.widgetSetHasWindow window True
logGUI $ "Gtk.widgetSetAppPaintable"
Gtk.widgetSetAppPaintable window True
logGUI $ "Gtk.widgetSetDoubleBuffered window False"
Gtk.widgetSetDoubleBuffered window False
when (isJust $ cfg ^. backgroundTransparency) $ mkAlphaChannel logGUI $ Gtk.castToWidget window
logGUI $ "Gtk.windowSetTypeHint GtkWindowTtypeHintNormal"
Gtk.windowSetTypeHint window Gtk.WindowTypeHintNormal
logGUI $ "Gtk.windowSetDefaultSize " ++ show (cfg ^. recommendWindowSize)
uncurry (Gtk.windowSetDefaultSize window) $ cfg ^. recommendWindowSize
logGUI $ "Gtk.windowSetDecorated " ++ show (cfg ^. decorateWindow)
Gtk.windowSetDecorated window $ cfg ^. decorateWindow
this <- newEmptyMVar
live <- newEmptyMVar
let env = GtkWindowState
{ currentConfig = cfg
, thisWindow = this
, gtkWindowLive = live
, gtkWindow = window
, theInitReaction = Disconnected
, theResizeReaction = Disconnected
, theVisibilityReaction = Disconnected
, theFocusReaction = Disconnected
, theMouseHandler = Disconnected
, theCursorHandler = Disconnected
, theKeyHandler = Disconnected
, theAnimatorThread = Disconnected
}
((), env) <- flip runGtkState env $ do
installExposeEventHandler
installDeleteEventHandler
installInitEventHandler
putMVar this env
return env
-- | Expose event handlers do not evaluate any 'GUI' functions. The expose event handler's only task
-- is to blit the canvas pixmap to the window's image buffer.
--
-- The expose event handler installed here actually will remove itself and install another expose
-- event handler which will be used throughout the remainder of the windows lifetime. The reason
-- this is necessary due to some idiosyncratic behavior of Gtk+, namely that the first time the
-- expose event handler is called, the window's image buffer has not been properly allocated and the
-- event handler that does the blitting will be needing a pointer to an image buffer, so the
-- permanent event handler cannot be installed until the pointer to the image buffer becomes
-- available.
installExposeEventHandler :: GtkState ()
installExposeEventHandler = do
logGUI <- mkLogger "installExposeEventHandler" True
env <- get
liftIO $ do
logGUI $ "Gtk.widgetAddEvents env [Gtk.StructureMask]"
Gtk.widgetAddEvents (gtkWindow env) [Gtk.StructureMask, Gtk.ExposureMask]
--------------------------------------- Expose Event -----------------------------------
logGUI "Glib.on Gtk.exposeEvent -- for init handler"
initExposeEventMVar <- newEmptyMVar
(>>= (putMVar initExposeEventMVar)) $ Glib.on (gtkWindow env) Gtk.exposeEvent $ do
logGUI <- mkLogger "initExposeEventCallback" True
liftIO $ do
logGUI "Gtk.eventWindow >>= putMVar gtkDrawWindowMVar"
takeMVar initExposeEventMVar >>= Glib.signalDisconnect
logGUI "Glib.on window Gtk.exposeEvent"
Glib.on (gtkWindow env) Gtk.exposeEvent $ do
logGUI <- mkLogger "exposeEventCallback" False
canvas <- Gtk.eventWindow
region <- Gtk.eventRegion
liftIO $ withMVar (gtkWindowLive env) $ \ livest -> do
Gtk.gcSetClipRegion (theGtkGraphCtx livest) region
(w, h) <- Gtk.drawableGetSize (theGtkPixmap livest)
logGUI $ "Gtk.drawRectangle 0 0 " ++ show w ++ ' ' : show h
Gtk.drawRectangle canvas (theGtkGraphCtx livest) True 0 0 w h
--liftIO $ gdkBlit canvas (theGtkGraphCtx env) (V2 0 0) region >> return True
return True
------------------------------------------------------------------------------------
return True
-- | The init event handler was responsible for pulling the init GUI function from the
-- 'GtkWindowState' and evaluating it, then installing a configure event handler.
installInitEventHandler :: GtkState ()
installInitEventHandler = do
logGUI <- mkLogger "permanentHandlers" True
env <- get
liftIO $ do
------------------------------------ Initializing Event --------------------------------
logGUI "Glib.on Gtk.configureEvent -- for init handler"
initHandler <- newEmptyMVar
(>>= (putMVar initHandler)) $ Glib.on (gtkWindow env) Gtk.configureEvent $ do
logGUI <- mkLogger "initCallback" True
canvas <- Gtk.eventWindow
size <- Gtk.eventSize
let allocSize = dimsForAlloc size
let evt = sampCoord <$> uncurry V2 size
liftIO $ lockGtkWindow (GtkUnlockedWin $ thisWindow env) $ do
live <- gets gtkWindowLive
liftIO $ do
logGUI $ "newGtkWindowLive (dimsForAlloc " ++
show size ++ " --> " ++ show allocSize ++ ")"
livest <- newGtkWindowLive (currentConfig env) canvas allocSize
logGUI "putMVar gtkWindowLive"
putMVar live livest
logGUI $ "initReaction " ++ show size
use initReaction >>= flip evalConnectReact evt
forceDisconnect initReaction
liftIO $ do
logGUI "Glib.signalDisconnect initCallback"
takeMVar initHandler >>= Glib.signalDisconnect
logGUI "installResizeEventHandler -- a permanent handler"
installResizeEventHandler
return False
-- | This function installs the configure event handler, which is called whenever the window is
-- resized.
installResizeEventHandler :: GtkState ()
installResizeEventHandler = do
logGUI <- mkLogger "installResizeEventHandler" True
env <- get
liftIO $ do
logGUI "Glib.on window Gtk.configureEvent"
void $ Glib.on (gtkWindow env) Gtk.configureEvent $ do
logGUI <- mkLogger "configureEventCallback" True
canvas <- Gtk.eventWindow
size <- Gtk.eventSize
let evt = sampCoord <$> uncurry V2 size
liftIO $ lockGtkWindow (GtkUnlockedWin $ thisWindow env) $ do
liftIO $ logGUI $ "resizeGtkDrawContext " ++ show size
resizeGtkDrawContext canvas size
liftIO $ logGUI "resizeReaction"
use resizeReaction >>= flip evalConnectReact evt
liftIO $ do
logGUI "Gtk.widgetQueueDraw"
Gtk.widgetQueueDraw (gtkWindow env)
return False
-- | This event handler is responsible for cleaning up when a window close event occurs. Whether the
-- application quit or not when the window closes is configurable by parameters in the
-- 'Happlets.Config.Config' data structure.
installDeleteEventHandler :: GtkState ()
installDeleteEventHandler = do
logGUI <- mkLogger "installDeleteEventHandler" True
env <- get
liftIO $ do
logGUI "Glib.on Gtk.deleteEvent"
Glib.on (gtkWindow env) Gtk.deleteEvent $ liftIO $
lockGtkWindow (GtkUnlockedWin $ thisWindow env) $ do
logGUI <- mkLogger "deleteWindowCallback" True
let cfg = currentConfig env
if cfg ^. deleteWindowOnClose
then do
liftIO $ logGUI "Gtk.widgetDestroy -- willDeleteWindowOnClose is True"
disconnectAll
liftIO $ Gtk.widgetDestroy (gtkWindow env)
else liftIO $ do
logGUI $ "Gtk.widgetHideAll -- willDeleteWindowOnClose is False"
Gtk.widgetHideAll (gtkWindow env)
liftIO $ if not (cfg ^. quitOnWindowClose)
then logGUI "-- quitOnWindowClose is False"
else do
logGUI $ "Gtk.mainQuit -- quitOnWindowClose is True"
Gtk.mainQuit
return True
logGUI "-- done installing permanent event handlers"
deleteWin :: GtkWindowState -> IO ()
deleteWin env = do
logGUI <- mkLogger "deleteWin" True
logGUI "Gtk.widgetDestroy"
Gtk.widgetDestroy (gtkWindow env)
--evalGtkDraw :: GtkDraw -> GtkState ()
--evalGtkDraw (CairoRender draw) = get >>= \ env -> liftIO $ case theGtkPixmap env of
-- Just pixmap -> Gtk.renderWithDrawable pixmap draw
-- Nothing -> Gtk.widgetGetDrawWindow (gtkWindow env) >>= flip Gtk.renderWithDrawable draw
-- | Compute rectangular dimensions for a window buffer allocation that are large enough to contain
-- the given rectangluar dimensions, but are also an even multiple of some unit value. When resizing
-- windows with 'resizeGtkDrawContext' this function is used to decide whether it is necessary to
-- re-allocate the window buffer or if the old window buffer is still large enough to contain the
-- new window dimensions.
dimsForAlloc :: (Int, Int) -> (Int, Int)
dimsForAlloc (w, h) = (step w, step h) where
factor = 32
step a = (div (abs a) factor + 1) * factor
-- For 32-bit pixels, with a minimum alloctable rectangle size of 32*32 means the minimum buffer
-- allocation will be 32*32*32 = 32^3 = (2^5)^3 = 2^15 = 32768 bytes.
-- | This function obtains a widget drawing window object and it's size, and allocates a new pixmap
-- buffer and GDK drawing context for it. This function conditionally calls 'newGtkWindowLive' if
-- 'dimsForAlloc' returns a different value than what is already allocated. This function also locks
-- the 'gtkWindowLive' MVar, so it is assumed the MVar is not empty. So this function must not be
-- called until the "initCallback" has been evaluated.
resizeGtkDrawContext :: Gtk.DrawWindow -> (Int, Int) -> GtkState ()
resizeGtkDrawContext canvas size = do
logGUI <- mkLogger "resizeGtkDrawContext" True
env <- get
let cfg = currentConfig env
let newDims = dimsForAlloc size
liftIO $ modifyMVar_ (gtkWindowLive env) $ \ livest -> do
oldDims <- Gtk.drawableGetSize (theGtkPixmap livest)
if newDims == oldDims
then do
logGUI $ "-- will not re-allocate window buffer, old buffer size = " ++
show oldDims ++ ", new buffer size = " ++ show newDims
return livest
else do
logGUI $ "gtkAllocNewPixmap " ++ show newDims
newGtkWindowLive cfg canvas newDims
-- | Allocates a new 'GtkWindowLive', including the 'Gtk.Pixmap' buffer and 'Gtk.GC' graphics
-- context. The buffer allocated will be exactly the dimensions given without checking if the size
-- is valid, and invalid demensions will crash the thread.
newGtkWindowLive :: Config -> Gtk.DrawWindow -> (Int, Int) -> IO GtkWindowLive
newGtkWindowLive cfg canvas size@(w, h) = do
logGUI <- mkLogger "gtkAllocNewPixmap" True
let depth = Just $ if isJust $ cfg ^. backgroundTransparency then 32 else 24
let grey = cfg ^. backgroundGreyValue
logGUI $ "Gtk.pixmapNew " ++ show size ++ ' ' : show depth
pixmap <- Gtk.pixmapNew (Just canvas) w h depth
--logGUI $ "Gtk.gcNew -- should be Gtk.DrawWindow of 'window'"
--graphCtx <- Gtk.gcNew canvas
--gcVals <- Gtk.gcGetValues graphCtx
--logGUI $ "Gtk.gcSetValues"
--Gtk.gcSetValues graphCtx gcVals
-- { Gtk.background = dToGrey grey
-- , Gtk.tile = Just pixmap
-- , Gtk.fill = Gtk.Tiled
-- }
gc <- liftIO $ Gtk.gcNewWithValues canvas Gtk.newGCValues
{ Gtk.background = dToGrey grey
, Gtk.tile = Just pixmap
, Gtk.fill = Gtk.Tiled
}
return GtkWindowLive
{ gtkDrawWindow = canvas
, theGtkPixmap = pixmap
, theGtkGraphCtx = gc
}
disconnectAll :: GtkState ()
disconnectAll = do
forceDisconnect visibilityReaction
forceDisconnect focusReaction
forceDisconnect mouseHandler
forceDisconnect cursorHandler
forceDisconnect keyHandler
forceDisconnect animatorThread
forceDisconnect resizeReaction
----------------------------------------------------------------------------------------------------
-- | This is a type of 'Happlets.GUI.GUI' function where the @window@ type is 'GtkWindow' and the
-- @draw@ type is 'GtkDraw' (a Cairo 'Graphics.Rendering.Cairo.Render' function).
type GtkGUI model a = GUI GtkWindow model a
-- | Creates a happlet and associates it with a window.
gtkNewWindow :: Config -> IO GtkWindow
gtkNewWindow cfg = do
logGUI <- mkLogger "gtkWindowNew" True
logGUI "createWin"
GtkUnlockedWin . thisWindow <$> createWin cfg
gtkWindowVisible :: Bool -> GtkGUI model ()
gtkWindowVisible visible = do
logGUI <- mkLogger ("gtkWindowVisible "++show visible) True
runGtkStateGUI $ get >>= \ env -> liftIO $ do
if visible
then do
logGUI "Gtk.widgetShow"
Gtk.widgetShowAll (gtkWindow env)
logGUI "Gtk.widgetQueueDraw"
Gtk.widgetQueueDraw (gtkWindow env)
else do
logGUI "Gtk.widgetHideAll"
Gtk.widgetHideAll (gtkWindow env)
gtkAttachHapplet :: Bool -> GtkWindow -> Happlet model -> (PixSize -> GtkGUI model ()) -> IO ()
gtkAttachHapplet showWin win happ init = do
logGUI <- mkLogger "gtkAttachHapplet" True
logGUI "lockGtkWindow"
lockGtkWindow win $ do
disconnectAll
initReaction .= ConnectReact
{ doDisconnect = return () -- This function is disconnected automatically by the init callback.
, doReact = \ size -> do
logGUI <- mkLogger "initReaction" True
env <- get
liftIO $ logGUI $ "gtkRunHapplet " ++ show size
void $ gtkRunHapplet happ (const $ return ()) $ init size
when showWin $ liftIO $ do
logGUI "Gtk.widgetShowAll"
Gtk.widgetShowAll (gtkWindow env)
logGUI "Gtk.widgetQueueDraw"
Gtk.widgetQueueDraw (gtkWindow env)
return False
}
win <- gets gtkWindow
liftIO $ logGUI "Gtk.widgetShow" >> Gtk.widgetShow win
-- | Change the happlet displayed in the current window. Remove the current Happlet event handlers
-- and re-install the event handlers for the given Happlet.
gtkSetHapplet :: Happlet newmodel -> (PixSize -> GtkGUI newmodel ()) -> GtkGUI oldmodel ()
gtkSetHapplet newHapp init = do
logGUI <- mkLogger "windowSetHapplet" True
oldHapp <- askHapplet
if sameHapplet oldHapp newHapp
then liftIO $ logGUI "-- the Happlet given is the one that is already attached to this window"
else do
liftIO $ logGUI "disconnectAll -- on current window"
runGtkStateGUI disconnectAll
st <- getGUIState
(env, _) <- case theGUIWindow st of
GtkUnlockedWin{} -> error $ "gtkSetHapplet: " ++
"Evaluated a GtkState function within a GtkGUI function on a locked GtkWindow."
-- The 'window' value passed to the 'evalGUI' function must be a 'GtkLockedWin' constructor.
GtkLockedWin env -> liftIO $ do
size <- Gtk.widgetGetDrawWindow (gtkWindow env) >>= Gtk.drawableGetSize
let evt = sampCoord <$> uncurry V2 size
logGUI $ "onHapplet -- run initializer " ++ show size
onHapplet newHapp $
liftM (theGUIWindow &&& theGUIModel) . evalGUI (init evt) newHapp (GtkLockedWin env)
--logGUI "Gtk.widgetQueueDraw"
--Gtk.widgetQueueDraw $ gtkWindow win
--logGUI "Gtk.widgetShow"
--Gtk.widgetShow $ gtkWindow win
putGUIState $ st{ theGUIWindow = env }
void disable
-- Set which function redraws the window.
evalRedraw :: (PixSize -> CairoRender a) -> GtkState a
evalRedraw redraw = do
logGUI <- mkLogger "evalRedraw" True
env <- get
liftIO $ withMVar (gtkWindowLive env) $ \ livest -> do
-- Obtain the size from the draw window, but will not draw to the draw window.
size <- fmap sampCoord . uncurry V2 <$> Gtk.drawableGetSize (gtkDrawWindow livest)
logGUI "Gtk.renderWithDrawable -- to theGtkPixmap"
a <- Gtk.renderWithDrawable (theGtkPixmap livest) $ runCairoRender $ redraw size
(w, h) <- Gtk.drawableGetSize (gtkDrawWindow livest)
region <- Gtk.regionRectangle $ Gtk.Rectangle 0 0 w h
logGUI "Gtk.drawWindowInvalidateRegion"
Gtk.drawWindowInvalidateRegion (gtkDrawWindow livest) region True
return a
----------------------------------------------------------------------------------------------------
instance HappletWindow GtkWindow CairoRender where
windowChangeHapplet = gtkSetHapplet
onView = runGtkStateGUI . evalRedraw
instance Happlet2DGraphics CairoRender where
clearScreen = unpackRGBA32Color >>> \ (r,g,b,a) -> CairoRender $ cairoClearCanvas r g b a
drawLine = cairoDrawLine
drawPath = cairoDrawPath
drawRect = cairoDrawRect
setPoint = error "TODO: canvasSetPoint has not yet been implemented in this Happlets back-end"
getPoint = error "TODO: canvasGetPoint has not yet been implemented in this Happlets back-end"
-- | Push the graphics context by calling 'Cairo.save' before evaluating a given 'Cario.Render'
-- function. When evaluation completes, pop the graphics context by calling 'Cairo.restore'.
cairoPreserve :: CairoRender a -> CairoRender a
cairoPreserve f = CairoRender Cairo.save >> f <* CairoRender Cairo.restore
cairoMoveTo :: Point2D RealApprox -> CairoRender ()
cairoMoveTo = CairoRender . uncurry Cairo.moveTo . view pointXY . fmap unwrapRealApprox
cairoLineTo :: Point2D RealApprox -> CairoRender ()
cairoLineTo = CairoRender . uncurry Cairo.lineTo . view pointXY . fmap unwrapRealApprox
cairoDrawLine :: LineColor -> LineWidth -> Line2D RealApprox -> CairoRender ()
cairoDrawLine color width line = cairoPreserve $ do
CairoRender $ do
cairoSetColorRGBA32 color
Cairo.setLineCap Cairo.LineCapRound
Cairo.setLineWidth $ unwrapRealApprox width
cairoMoveTo $ line ^. line2DHead
cairoLineTo $ line ^. line2DTail
CairoRender $ Cairo.stroke
cairoDrawPath :: LineColor -> LineWidth -> [Point2D RealApprox] -> CairoRender ()
cairoDrawPath color width =
let run a ax = cairoPreserve $ do
CairoRender $ do
cairoSetColorRGBA32 color
Cairo.setLineCap Cairo.LineCapRound
Cairo.setLineWidth $ unwrapRealApprox width
Cairo.setLineJoin Cairo.LineJoinMiter
cairoMoveTo a
forM_ ax cairoLineTo
CairoRender Cairo.stroke
in \ case { [] -> return (); [a] -> run a [a]; a:ax -> run a ax; }
cairoDrawRect :: LineColor -> LineWidth -> FillColor -> Rect2D RealApprox -> CairoRender ()
cairoDrawRect lineColor width fillColor rect = cairoPreserve $ do
CairoRender $ cairoSetColorRGBA32 fillColor
cairoMoveTo $ rect ^. rect2DHead
let ((x0,y0),(x1,y1)) = view pointXY *** view pointXY $
(unwrapRealApprox <$> rect) ^. rect2DPoints
CairoRender $ do
Cairo.rectangle x0 y0 x1 y1
Cairo.fill
cairoSetColorRGBA32 lineColor
Cairo.setLineWidth $ unwrapRealApprox width
Cairo.setLineJoin Cairo.LineJoinMiter
Cairo.rectangle x0 y0 x1 y1
Cairo.stroke
--cairoSelectFont :: SelectFont -> GtkGUI model (Maybe FontExtents)
--cairoSelectFont font0 = runGtkStateGUI $ evalCairo $ do
-- let loop (bold,italic,oblique,syms) = \ case
-- [] -> (bold, italic, oblique, Strict.unwords $ syms [])
-- a:ax -> let w = Strict.toLower a in ax & case w of
-- w | w == Strict.pack "bold" -> loop (bold + 1, italic, oblique, syms)
-- w | w == Strict.pack "italic" -> loop (bold, italic + 1, oblique, syms)
-- w | w == Strict.pack "oblique" -> loop (bold, italic, oblique + 1, syms)
-- _ -> loop (bold, italic, oblique, syms . (a :))
-- let zero = 0 :: Int
-- let one = 1 :: Int
-- let (bold,italic,oblique,font) = loop (0, 0, 0, id) $ Strict.words font0
-- if italic > zero && oblique > zero || bold > one || italic > one || oblique > one
-- then return Nothing
-- else do
-- Cairo.selectFontFace font
-- (if italic > 0 then Cairo.FontSlantItalic else
-- if oblique > 0 then Cairo.FontSlantOblique else Cairo.FontSlantNormal)
-- (if bold > 0 then Cairo.FontWeightBold else Cairo.FontWeightNormal)
-- ext <- Cairo.fontExtents
-- return $ Just FontExtents
-- { fontExtentsAscent = RealApprox $ Cairo.fontExtentsAscent ext
-- , fontExtentsDescent = RealApprox $ Cairo.fontExtentsDescent ext
-- , fontExtentsHeight = RealApprox $ Cairo.fontExtentsHeight ext
-- , fontExtentsMaxXadvance = RealApprox $ Cairo.fontExtentsMaxXadvance ext
-- , fontExtentsMaxYadvance = RealApprox $ Cairo.fontExtentsMaxYadvance ext
-- }
----------------------------------------------------------------------------------------------------
instance Managed GtkWindow where
windowVisible = gtkWindowVisible
visibleEvents = installEventHandler "visibleEvents" visibilityReaction
[Gtk.VisibilityNotifyMask] $ \ logGUI env next -> do
let win = gtkWindow env
logGUI "Glib.on Gtk.visibilityNotifyEvent"
vis <- Glib.on win Gtk.visibilityNotifyEvent $
Gtk.eventVisibilityState >>=
liftIO . next . not . (== Gtk.VisibilityFullyObscured) >>
return False
return $ do
Glib.signalDisconnect vis
focusEvents = installEventHandler "focusEvents" focusReaction
[Gtk.FocusChangeMask] $ \ logGUI env next -> do
let win = gtkWindow env
logGUI "Glib.on Glib.focusInEvent"
focin <- Glib.on win Gtk.focusInEvent $ liftIO (next True) >> return False
logGUI "Glib.on Glib.focusOutEvent"
focout <- Glib.on win Gtk.focusOutEvent $ liftIO (next False) >> return False
return $ do
Glib.signalDisconnect focin
Glib.signalDisconnect focout
instance CanResize GtkWindow where
resizeEvents = installEventHandler "resizeEvents" resizeReaction [] (\ _ _ _ -> return $ pure ())
data AnimationThreadControl
= AnimationThreadControl
{ animationThreadAlive :: !Bool
, animationInitTime :: !UTCTime
}
instance CanAnimate GtkWindow where
animationIsRunning = runGtkStateGUI $
gets theAnimatorThread <&> \ case { Disconnected -> False; _ -> True; }
stepFrameEvents react = do
flip (installEventHandler "stepFrameEvents" animatorThread []) react $ \ logGUI env next -> do
--let rate = env & theAnimationFrameRate . currentConfig -- TODO: restore this!!!
let rate = 0.05 -- any slow rate
t0 <- getCurrentTime
t0ref <- newIORef AnimationThreadControl
{ animationThreadAlive = True
, animationInitTime = t0
}
logGUI "Glib.on Glib.timeoutAdd"
frame <- flip Glib.timeoutAdd (min 200 $ floor (1000.0 / rate)) $ do
AnimationThreadControl{animationThreadAlive=alive,animationInitTime=t0} <- readIORef t0ref
if not alive then return False else
diffUTCTime <$> getCurrentTime <*> pure t0 >>= next . realToFrac
--
-- TODO: the 'next' function needs to be evaluated here at time 0, becuase 'timeoutAdd' does
-- not call it until after the first time step interval has passed. For slow animations, this
-- will cause a noticable delay between the time the 'stepFrameEvents' function is evaluated
-- and the first frame callback is evaluated.
return $ do
logGUI "Glib.signalDisconnect"
modifyIORef t0ref $ \ ctrl -> ctrl{ animationThreadAlive = False }
Glib.timeoutRemove frame
instance CanKeyboard GtkWindow where
keyboardEvents = installEventHandler "keyboardEvents" keyHandler
[Gtk.KeyReleaseMask, Gtk.KeyPressMask] $ \ logGUI env next -> do
let win = gtkWindow env
logGUI "Glib.on Gtk.keyReleaseEvent"
press <- Glib.on win Gtk.keyReleaseEvent $
handleKey False >>= liftIO . next >> return False
logGUI "Glib.on Gtk.keyPressEvent"
release <- Glib.on win Gtk.keyPressEvent $
handleKey True >>= liftIO . next >> return False
return $ do
logGUI "Glib.signalDisconnect"
Glib.signalDisconnect press
Glib.signalDisconnect release
instance CanMouse GtkWindow where
providedMouseDevices = return []
mouseEvents = \ case
MouseButton -> installEventHandler "mouseButtonEvents" mouseHandler
[Gtk.ButtonPressMask] $ \ logGUI env next -> do
let win = gtkWindow env
logGUI "Glib.on Gtk.buttonPressEvent"
press <- Glib.on win Gtk.buttonPressEvent $
handleMouse True >>= liftIO . next >> return False
release <- Glib.on win Gtk.buttonReleaseEvent $
handleMouse False >>= liftIO . next >> return False
return $ do
logGUI "Glib.signalDisconnect"
Glib.signalDisconnect press
Glib.signalDisconnect release
MouseDrag -> installEventHandler "mouseDragEvents" mouseHandler
[Gtk.ButtonPressMask, Gtk.ButtonMotionMask] $ \ logGUI env next -> do
let win = gtkWindow env
logGUI "Glib.on Gtk.buttonPressEvent"
press <- Glib.on win Gtk.buttonPressEvent $
handleMouse True >>= liftIO . next >> return False
logGUI "Glib.on Gtk.buttonReleaseEvent"
release <- Glib.on win Gtk.buttonReleaseEvent $
handleMouse False >>= liftIO . next >> return False
logGUI "Glib.on Gtk.motionNotifyEvent"
button <- Glib.on win Gtk.motionNotifyEvent $
handleCursor True >>= liftIO . next >> return False
return $ do
logGUI "Glib.signalDisconnect"
Glib.signalDisconnect press
Glib.signalDisconnect release
Glib.signalDisconnect button
MouseAll -> installEventHandler "mouseAllEvents" mouseHandler
[Gtk.PointerMotionMask, Gtk.ButtonMotionMask] $ \ logGUI env next -> do
let win = gtkWindow env
logGUI "Glib.on Gtk.motionNotifyEvent"
button <- newIORef False
press <- Glib.on win Gtk.buttonPressEvent $
liftIO (writeIORef button False) >>
handleMouse True >>= liftIO . next >> return False
release <- Glib.on win Gtk.buttonReleaseEvent $
liftIO (writeIORef button True) >>
handleMouse False >>= liftIO . next >> return False
motion <- Glib.on win Gtk.motionNotifyEvent $
liftIO (readIORef button) >>=
handleCursor >>=liftIO . next >> return False
return $ do
logGUI "Glib.signalDisconnect"
Glib.signalDisconnect press
Glib.signalDisconnect release
Glib.signalDisconnect motion
-- | This function enables an event handler, and takes a continuation which will be evaluated by
-- this function call to do the low-level work of install the event handler into the Gtk
-- window. This function also does the work of taking a 'Happlets.GUI.GUI' function to be evaluated
-- on each event, converting this function to a 'ConnectReact' function, and installing it into the
-- 'GtkWindow's internal state so that it can be actually evaluated every time the low-level event
-- handler is evaluated. The result is a function that can be used to instantiate any of the
-- "Happlets.GUI" module's event handler classes.
installEventHandler
:: String
-> Lens' GtkWindowState (ConnectReact event)
-> [Gtk.EventMask]
-> (LogGUI -> GtkWindowState -> (event -> IO Bool) -> IO (IO ()))
-> (event -> GtkGUI model ())
-> GtkGUI model ()
installEventHandler logString connectReact masks install react = do
logGUI <- mkLogger logString True
happ <- askHapplet
liftIO $ logGUI "runGtkStateGUI"
runGtkStateGUI $ do
env <- get
callback <- use connectReact
disconnect <- case callback of
ConnectReact{doDisconnect=disconnect} -> do
liftIO $ logGUI "-- event handler already installed"
return disconnect
Disconnected -> liftIO $ do
logGUI "-- install event handler"
disconnect <- install logGUI env $ \ event -> do
logGUI "lockGtkWindow"
lockGtkWindow (GtkUnlockedWin $ thisWindow env) $ do
stillAlive <- use connectReact >>= flip evalConnectReact event
unless stillAlive $ forceDisconnect connectReact
return stillAlive
logGUI $ "Gtk.widgetAddEvents " ++ show masks
Gtk.widgetAddEvents (gtkWindow env) masks
return $ liftIO $ do
logGUI $ "Gtk.widgetDelEvents " ++ show masks
Gtk.widgetDelEvents (gtkWindow env) masks
disconnect
liftIO $ logGUI "resizeReaction -- now enabled"
connectReact .= ConnectReact
{ doReact = gtkRunHapplet happ (checkGUIContinue logGUI resizeReaction) . react
, doDisconnect = disconnect
}
handleMouse :: Pressed -> Gtk.EventM Gtk.EButton Mouse
handleMouse upDown = do
logGUI <- mkLogger "handleMouse" True
(x, y) <- Gtk.eventCoordinates
button <- getMouseButton
mods <- packGtkModifiers <$> Gtk.eventModifierAll
let evt = Mouse "" upDown mods button (V2 (round x) (round y))
liftIO $ logGUI $ "handleMouse " ++ show evt
return evt
handleCursor :: Pressed -> Gtk.EventM Gtk.EMotion Mouse
handleCursor upDown = do
logGUI <- mkLogger "handleCursor" True
(x, y) <- Gtk.eventCoordinates
mods <- packGtkModifiers <$> Gtk.eventModifierAll
let evt = Mouse "" upDown mods MotionOnly (V2 (round x) (round y))
liftIO $ logGUI $ "handleCursor " ++ show evt
return evt
handleKey :: Pressed -> Gtk.EventM Gtk.EKey Keyboard
handleKey upDown = do
logGUI <- mkLogger "handleKey" True
key <- Gtk.eventKeyVal
mods <- packGtkModifiers <$> Gtk.eventModifierAll
let evt = RawKey upDown mods key
liftIO $ logGUI $ "handleKey " ++ show evt
return evt
getMouseButton :: Gtk.EventM Gtk.EButton MouseButton
getMouseButton = Gtk.eventButton <&> \ case
Gtk.LeftButton -> LeftClick
Gtk.MiddleButton -> MiddleClick
Gtk.RightButton -> RightClick
Gtk.OtherButton{} -> SideClick
packGtkModifiers :: [Gtk.Modifier] -> ModifierBits
packGtkModifiers = packModifiers .
( (=<<) $ \ case
Gtk.Shift -> [Shift]
Gtk.Lock -> [CapsLock]
Gtk.Control -> [Ctrl]
Gtk.Alt -> [Alt1]
Gtk.Alt2 -> [Alt2]
Gtk.Super -> [Super1]
Gtk.Hyper -> [Super2]
_ -> []
)
--screenMasksToGdkRegion :: [ScreenMask] -> (Int, Int) -> IO Gtk.Region
--screenMasksToGdkRegion masks (w, h) = do
-- logGUI <- mkLogger "screenMasksToGdkRegion" True
-- let center x w = let (a, r) = divMod w 2 in (x - a, x + a + r)
-- let int (SampCoord i) = fromIntegral i :: Int
-- let rect xa' ya' xb' yb' =
-- let xa = int $ min xa' xb'
-- xb = int $ max xa' xb'
-- ya = int $ min ya' yb'
-- yb = int $ max ya' yb'
-- w = xb - xa
-- h = yb - ya
-- in Gtk.Rectangle xa ya w h
-- region <- Gtk.regionNew
-- let union = Gtk.regionUnion region
-- let loop = \ case
-- [] -> return ()
-- mask:masks -> case mask of
-- ScreenAll ->
-- Gtk.regionRectangle (Gtk.Rectangle 0 0 w h) >>= union
-- ScreenRect (V2 xa ya) (V2 xb yb) -> do
-- logGUI $ "Gtk.regionRectangle ScreenRect{ xa="++
-- show xa++", ya="++show ya++", xb="++show xb++", yb="++show yb++" }"
-- Gtk.regionRectangle (rect xa ya xb yb) >>= union
-- loop masks
-- ScreenOval (V2 x y) w h -> do
-- let (xa, xb) = center x w
-- (ya, yb) = center y h
-- -- TODO: Gtk.regionPolygon with approximate oval shape if greater than a certain size.
-- logGUI $ "Gtk.regionRectangle " ++ ("ScreenOval{ x="++
-- show xa++" y="++show ya++" xb="++show xb++" yb="++show yb++" }")
-- Gtk.regionRectangle (rect xa ya xb yb) >>= union
-- loop masks
-- ScreenTriangle (V2 xa ya) (V2 xb yb) (V2 xc yc) -> do
-- let points = [(int xa, int ya), (int xb, int yb), (int xc, int yc)]
-- logGUI $ "Gtk.regionPolygon "++show points
-- Gtk.regionPolygon points Gtk.WindingRule >>= union
-- loop masks
-- ScreenVertical x lo hi ->
-- Gtk.regionRectangle (rect x lo (x + 1) hi) >>= union >> loop masks
-- ScreenHorizontal y lo hi ->
-- Gtk.regionRectangle (rect lo y hi (y + 1)) >>= union >> loop masks
-- loop masks >> return region
---- | Queues a redraw on the area of the window covered by the given list of
---- 'Happlets.Redraw.ScreenMask's.
--updateRegions :: Gtk.DrawWindow -> [ScreenMask] -> IO ()
--updateRegions canvas masks = if null masks then return () else do
-- logGUI <- mkLogger "updateRegions" False
-- region <- Gtk.drawableGetSize canvas >>= screenMasksToGdkRegion masks
-- logGUI "Gtk.drawWindowInvalidateRegion"
-- Gtk.drawWindowInvalidateRegion canvas region True
-- | Blit a 'Gtk.Pixmap' to some drawable canvas.
gdkBlit :: Gtk.DrawableClass canvas => canvas -> Gtk.Pixmap -> PixCoord -> IO ()
--gdkBlit :: Gtk.DrawableClass canvas => canvas -> Gtk.Pixmap -> PixCoord -> [ScreenMask] -> IO ()
gdkBlit canvas pixmap (V2 dx dy) = do
logGUI <- mkLogger "gdkBlit" True
--region <- screenMasksToGdkRegion masks (fromIntegral dx, fromIntegral dy)
(w, h) <- Gtk.drawableGetSize canvas
-- TODO: This creates an update region for the whole window. This needs to be made more efficient,
-- such that only the minimal updated region is redrawn.
region <- Gtk.regionNew
Gtk.regionRectangle (Gtk.Rectangle 0 0 w h) >>= Gtk.regionUnion region
logGUI $ "Gtk.gcNewWithValues " ++ show dx ++ ' ' : show dy
gc <- Gtk.gcNewWithValues canvas $ Gtk.newGCValues
{ Gtk.tile = Just pixmap
, Gtk.fill = Gtk.Tiled
, Gtk.tsXOrigin = fromIntegral dx
, Gtk.tsYOrigin = fromIntegral dy
}
Gtk.gcSetClipRegion gc region
logGUI $ "Gtk.drawRectangle 0 0 " ++ show w ++ ' ' : show h
Gtk.drawRectangle canvas gc True 0 0 w h
--rects <- Gtk.regionGetRectangles region
--forM_ rects $ \ rect@(Gtk.Rectangle x y w h) -> do
-- logGUI $ "Gtk.drawRectangle canvas gc True (" ++ show rect ++ ")"
-- Gtk.drawRectangle canvas gc True (x + fromIntegral dx) (y + fromIntegral dy) w h
----------------------------------------------------------------------------------------------------
-- | A 'GtkDrawing' is a function that produces a 'Diagrams.Core.Types.Diagram' from a
-- 'Diagrams.BoundingBox.BoundingBox'. Use the 'Diagrams.BoundingBox.BoundingBox' information to
-- inform the placement and scale of your diagram.
type GtkCairoDiagram = BoundingBox V2 Double -> Diagram Cairo
-- | This is a 'Happlets.View.Redraw' type function specific to this Gtk+ back-end provider.
type GtkRedraw a = CairoRender a
-- | This data type contains a pointer to an image buffer in memory, and also a function used to
-- perform some drawing to the pixel values.
data GtkImage
= GtkImage
{ gtkPixmapMVar :: MVar Gtk.Pixmap
}
instance CanBufferImages GtkWindow GtkImage CairoRender where
newImageBuffer (V2 (SampCoord w) (SampCoord h)) (CairoRender draw) = liftIO $ do
pixmap <- Gtk.pixmapNew (Nothing :: Maybe Gtk.Pixmap)
(fromIntegral w) (fromIntegral h) (Just 32)
Gtk.renderWithDrawable pixmap draw
mvar <- newMVar pixmap
return GtkImage
{ gtkPixmapMVar = mvar
}
resizeImageBuffer (GtkImage{gtkPixmapMVar=mvar}) (V2 w h) (CairoRender draw) =
liftIO $ modifyMVar mvar $ \ pixmap -> do
pixmap <- Gtk.pixmapNew (Just pixmap) (fromIntegral w) (fromIntegral h) (Just 32)
a <- Gtk.renderWithDrawable pixmap draw
return (pixmap, a)
drawImage (GtkImage{gtkPixmapMVar=mvar}) (CairoRender draw) =
liftIO $ withMVar mvar $ flip Gtk.renderWithDrawable draw
blitImage (GtkImage{gtkPixmapMVar=mvar}) offset = runGtkStateGUI $ do
gtkwin <- gets gtkWindow
canvas <- liftIO $ Gtk.widgetGetDrawWindow gtkwin
liftIO $ withMVar mvar $ \ pixmap -> gdkBlit canvas pixmap offset
blitImageTo (GtkImage{gtkPixmapMVar=src}) (GtkImage{gtkPixmapMVar=targ}) offset =
liftIO $ withMVar src $ \ src -> withMVar targ $ \ targ -> gdkBlit targ src offset
-- | This is a helpful function you can use for your 'Happlet.Control.controlRedraw' function to clear
-- the window with a background color, given by the four 'Prelude.Double' parameters for Red, Green,
-- Blue, and Alpha (in that order).
cairoClearCanvas :: Double -> Double -> Double -> Double -> Cairo.Render ()
cairoClearCanvas r g b a = do
logGUI <- mkLogger "cairoClearCanvas" True
liftIO $ logGUI $ unwords $ show <$> [r,g,b,a]
op <- Cairo.getOperator
Cairo.setOperator Cairo.OperatorSource
Cairo.setSourceRGBA r g b a
Cairo.paint
Cairo.setOperator op
-- | Use the Happlets-native color data type 'Happlets.Draw.Color.PackedRGBA32' to set the Cairo
-- "source" color in the Cairo context.
cairoSetColorRGBA32 :: PackedRGBA32 -> Cairo.Render ()
cairoSetColorRGBA32 = unpackRGBA32Color >>> \ (r,g,b,a) -> Cairo.setSourceRGBA r g b a
-- | Convert a 'GtkCairoDiagram', which is a type of 'Diagrams.Core.Types.Diagram', and convert it
-- to a Cairo 'Cairo.Render'-ing computation which can be used to set the 'controlView' of the
-- 'Controller'.
gtkCairoDiagram :: GtkCairoDiagram -> V2 Double -> Cairo.Render ()
gtkCairoDiagram diagram size = snd $ renderDia Cairo
( CairoOptions
{ _cairoFileName = ""
, _cairoSizeSpec = dims size
, _cairoOutputType = RenderOnly
, _cairoBypassAdjust = True
}
) (diagram $ fromCorners origin (P size))
----------------------------------------------------------------------------------------------------
-- | This is the Happlet back-end 'Happlets.Provider.Provider' which you must pass to
-- 'Happlets.GUI.runGUI' in the @main@ function of your Happlet program.
gtkHapplet :: Provider GtkWindow
gtkHapplet = Provider
{ defaultConfig = Config
{ theConfigErrorsOnLoad = []
, theConfigFilePath = ""
, theRegisteredAppName = "Happlet"
, theWindowTitleBar = "Happlet"
, theBackgroundTransparency = Just 0.9
, theBackgroundGreyValue = 1.0
, theRecommendWindowPosition = (0, 30)
, theRecommendWindowSize = (800, 600)
, theAnimationFrameRate = 60.0
, willDecorateWindow = True
, willQuitOnWindowClose = False
, willDeleteWindowOnClose = False
}
, doInitializeGUI = gtkInit
, doGUIEventLoopLaunch = gtkLaunchEventLoop
, doWindowNew = gtkNewWindow
, doWindowDelete = flip lockGtkWindow $ get >>= liftIO . deleteWin
, doWindowAttach = gtkAttachHapplet
}
-- A static variable used to ensure the functions in this module only call 'gtkInit' once.
gtkInitCheck :: MVar Bool
gtkInitCheck = unsafePerformIO $ newMVar False
{-# NOINLINE gtkInitCheck #-}
-- You must call this function prior to all evaluation of any GUI programming. This will call the
-- stateful IO functions that initialize the Gtk+ library. Be sure to call this only once.
gtkInit :: IO ()
gtkInit = do
alreadyInitd <- readMVar gtkInitCheck
unless alreadyInitd $ do
logGUI <- mkLogger "gtkInit" True
logGUI "Gtk.initGUI"
Gtk.initGUI >>= mapM_ logGUI
modifyMVar_ gtkInitCheck $ return . const True
-- | Launch the Gtk+ main event loop. This function will never return. If you fail to call this
-- function, your GUI will never run.
gtkLaunchEventLoop :: Config -> IO ()
gtkLaunchEventLoop cfg = do
logGUI <- mkLogger "gtkEventLoop" True
let appName = cfg ^. registeredAppName
unless (Strict.null appName) $ do
logGUI $ "Glib.setProgramName " ++ show appName
Glib.setProgramName appName
logGUI $ "Glib.setApplicationName " ++ show appName
Glib.setApplicationName appName
logGUI "Gtk.mainGUI"
Gtk.mainGUI