As part of my further experiments with GTK4 and Haskell, I wanted to vary gtk-picture by creating the Picture programatically. I named the alternative gtk-dynamic-picture.
Diagrams
I created the picture using the Diagrams project. I looked at various backends that support that project:
- the
diagrams-gtkpackage, based on the Cairo C library, renders to values provided by thegtkHaskell package, not those provided by thegi-gtkHaskell package; - the
diagrams-cairopackage, also based on Cairo, can render to a buffer in memory with the pixel format known asCAIRO_FORMAT_ARGB32. That format has alpha in the upper byte, then red, then green, then blue. The pixels are, however, stored native-endian; that is, on little endian x86_64 machine architecture, the order of the bytes is B G R A. Also, pre-multiplied alpha is used. (For example, 50% transparent red is0x80800000, not0x80ff0000.); and - the
diagrams-rasterificpackage, based on theRasterificHaskell package, renders to values of typeImage PixelRGBA8(provided by theJuicyPixelspackage). Pixels are stored in the order R G B A.
Ultimately, given the pixel format of a Pixbuf value (R G B A), I used diagrams-rasterific as the backend.
The first step was to yield an Image PixelRGBA8 from a Diagram B. That was straightforward, given the renderDia function:
|
1 2 3 4 5 6 7 8 9 10 |
import Codec.Picture.Types ( Image (..), PixelRGBA8 ) import Diagrams.Backend.Rasterific ( B, Rasterific (..), Options (..) ) import Diagrams.Prelude ( Diagram, dims2D, renderDia ) renderDiagramToImage :: Int -> Int -> Diagram B -> Image PixelRGBA8 renderDiagramToImage width height = let size = dims2D (fromIntegral width) (fromIntegral height) options = RasterificOptions size in renderDia Rasterific options |
Picture
The gi-gtk package provides:
|
1 2 3 4 |
pictureNewForPaintable :: (HasCallStack, MonadIO m, IsPaintable obj) => Maybe obj -> m Picture |
and the gi-gdk package provides Texture, which satisfies the IsPaintable constraint, and:
|
1 2 3 4 |
textureNewForPixbuf :: (HasCallStack, MonadIO m, IsPixbuf a) => a -> m Texture |
So, the final step was to yield a Picture value from a Texture value and, in turn, a Texture value from a Pixbuf:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import Diagrams.Backend.Rasterific ( B ) import Diagrams.Prelude ( Diagram ) import GI.Gdk ( textureNewForPixbuf ) import GI.GdkPixbuf ( Pixbuf ) import qualified GI.Gtk as Gtk import Draw ( drawGrid, backgroundWidth, backgroundHeight ) renderDiagramToPixbuf :: Int -> Int -> Diagram B -> IO Pixbuf renderDiagramToPixbuf width height diagram = imagePixelRGBA8ToPixbuf $ renderDiagramToImage width height diagram activate :: Gtk.Application -> IO () activate app = do ... texture <- textureNewForPixbuf =<< renderDiagramToPixbuf backgroundWidth backgroundHeight (drawGrid newGrid) picture <- Gtk.pictureNewForPaintable (Just texture) |
imagePixelRGBA8ToPixbuf
The missing link was a function that would yield a Pixbuf value given an Image PixelRGBA8 value. I bridged that link with:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import Codec.Picture.Types ( Image (..), PixelRGBA8 ) import qualified Data.Vector.Storable as SV import Foreign.Marshal.Alloc ( free, mallocBytes ) import Foreign.Marshal.Utils ( copyBytes ) import GI.GdkPixbuf ( Colorspace (..), Pixbuf, pixbufNewFromData ) imagePixelRGBA8ToPixbuf :: Image PixelRGBA8 -> IO Pixbuf imagePixelRGBA8ToPixbuf image = do let w = imageWidth image h = imageHeight image rowStride = w * 4 -- 4 bytes per PixelRGBA8 n = h * rowStride SV.unsafeWith (imageData image) $ \ptr -> do pixbufPtr <- mallocBytes n copyBytes pixbufPtr ptr n pixbufNewFromData pixbufPtr ColorspaceRgb True -- hasAlpha 8 -- bitsPerSample (fromIntegral w) -- width (fromIntegral h) -- height (fromIntegral rowStride) -- rowStride (Just free) -- destroyFn |
imageData image has type Data.Vector.Storable.Vector Word8. A buffer is allocated (mallocBytes n) and the bytes copied into it (copyBytes pixbufPtr ptr n). free is specified as the destroy function.
Mutable application state
Following an example presented by Mark Karpov and Jorge Galarze, I realised I would need a mutable state of the application. I used an IORef, as follows:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import Data.IORef ( IORef, newIORef ) activate :: Gtk.Application -> IORef Grid -> IO () activate app appState = do ... gameGrid <- readIORef appState texture <- textureNewForPixbuf =<< renderDiagramToPixbuf backgroundWidth backgroundHeight (drawGrid gameGrid) ... main :: IO () main = do appState <- newIORef emptyGrid app <- new Gtk.Application [ #applicationId := "com.pilgrem.gtk-picture" , On #activate (activate ?self appState) ] void $ app.run Nothing |
onMouseClick
The onMouseClick action needed to update the application state and the paintable attribute of the Picture value. In that regard, I had a couple of false starts.
First, I thought I needed to use getEventControllerWidget. The auto-generated Haddock documentation for package gi-gtk-4.0.9 has:
|
1 |
getEventControllerWidget :: (MonadIO m, IsEventController o) => o -> m Widget |
However, the Haddock documentation for module Data.GI.Base.Attributes has:
Whenever the attribute is represented as a pointer in the C side, it is often the case that the underlying C representation admits or returns
NULLas a valid value for the property. In these cases thegetoperation may returnMaybevalue, withNothingrepresenting theNULLpointer value (notable exceptions areGListandGSList, for whichNULLis represented simply as the empty list). This can be overridden in the introspection data, since sometimes attributes are non-nullable, even if the type would allow forNULL.
and the GTK4 documentation for gtk_event_controller_get_widget states that the return value can be NULL.
I found on one machine that get gestureClick #widget was an action yielding a Maybe Widget and on another machine, with the same dependencies on Haskell packages, that get gestureClick #widget was an action yielding a Widget. I concluded that something had been corrupted on the former machine. I solved the problem by starting from scratch on that machine: I deleted the Stack-supplied MSYS2 and the snapshot directory in the Stack root. I had Stack re-install the Stack-supplied MSYS2, updated it, and installed the necessary MSYS2 packages. I then re-built the Haskell package dependencies that depended on the C libraries provided by those MSYS2 packages.
Then, I realised that I could could sequence actions so that the Picture value had been specified before the onMouseClick action needed to refer to it:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
activate :: Gtk.Application -> IORef Grid -> IO () activate app appState = do ... picture <- Gtk.pictureNew ... let onMouseClick :: Gtk.GestureClickPressedCallback onMouseClick _nPress x y = do labelCoords `set` [ #label := coordsIntro <> showCoords x y ] whenJust (coordsToIsland x y) $ \(row, col) -> do oldGrid <- readIORef appState let gameGrid = updateGrid col row oldGrid writeIORef appState gameGrid texture <- textureNewForPixbuf =<< renderDiagramToPixbuf backgroundWidth backgroundHeight (drawGrid gameGrid) picture `set` [ #paintable := texture ] gestureClick <- new Gtk.GestureClick [ On #pressed onMouseClick ] ... #addController picture gestureClick |
gtk-dynamic-picture.exe
The resulting executable behaved as expected:
