haskellgtk3cairogtk2hs

How do I render into a gtk3 statusicon using cairo in a haskell application?


I would like render into the statusicon of my application.

I know I can make the statusicon display a pixbuf by setting statusIconPixbuf.

And I can create an empty pixbuf via pixbufNew and do things like filling it with a single color.

But how do render into that pixbuf using cairo?

Or is pixbuf not the right thing to use? Is there a better way to render into the statusicon?


Solution

  • Using a Pixbuf i need a way to render into it.

    The following solution takes a cairo Rendering render :: Render a and the desired X and Y dimension of the (square) Pixbuf (You can change this if you need to create non-square Pixbufs.)

    import qualified Foreign.Ptr as Pointer
    import qualified ByteString as B
    
    renderPixbuf :: Int -> Render a -> IO Pixbuf
    renderPixbuf size render = withImageSurface FormatARGB32 size size $ \surface -> do
        renderWith surface render
        stride <- imageSurfaceGetStride surface
        surfaceData <- swapRB stride <$> imageSurfaceGetData surface
        B.useAsCStringLen surfaceData $ \(pointer, _) -> do 
          let pointer' = Pointer.castPtr pointer
          pixbufNewFromData pointer' ColorspaceRgb True 8 size size stride
    

    It uses the function withImageSurface to create a surface for cairo to render to and then calls renderWith to do the actual rendering specified by render.

    The next two lines extract the image stride, i.e. the number of bytes in a line and the actual image data as ByteString.

    swapRB is a function that transforms the ByteString because somehow the red and blue channel are in the wrong order. See below for how this is done.

    In B.useAsCStringLen it gets low-level: It taks the ByteString returned by imageSurfaceGetData and converts it to a Ptr CUChar to create a new Pixbuf using pixbufNewFromData.

    That's it.

    swapRB is defined as follows:

    import Data.Word (Word8)
    import Data.List (unfoldr)
    
    splitAtIfNotNull :: Int -> B.ByteString -> Maybe (B.ByteString,B.ByteString)
    splitAtIfNotNull i string 
      | B.null string = Nothing
      | otherwise = Just $ B.splitAt i string 
    
    mapChunks :: (B.ByteString -> B.ByteString) -> Int -> B.ByteString -> B.ByteString
    mapChunks mapper chunkSize = B.concat . map mapper . unfoldr (splitAtIfNotNull chunkSize)
    
    swapRB :: Int -> B.ByteString -> B.ByteString
    swapRB = mapChunks swapRBforLine
      where swapRBforLine :: B.ByteString -> B.ByteString
            swapRBforLine = mapChunks (B.pack . swapRBforPixel . B.unpack) 4 
            swapRBforPixel :: [Word8] -> [Word8]
            swapRBforPixel [b,g,r,a] = [r,g,b,a]
            swapRBforPixel other = other
    

    It splits the ByteString of pixeldata into lines, then splits the lines into pixels each consisting of 4 bytes: one byte each for the channels red, green, blue, alpha. Innermost is the actual swapping:

    swapRBforPixel [b,g,r,a] = [r,g,b,a]