Declarative GTK+ Programming with Haskell

Oskar Wickström

September 2018

My Project: FastCut

  • Video editor specialized for screencasts
  • Written in GHC Haskell
  • Desktop GUI application
  • Goals
    • Minimal, do one thing really well
    • Core application logic should be pure
    • Portable
    • Reasonably snappy
    • Open source once documented and more useful
FastCut

Desktop GUI Frameworks and Haskell

  • Imperative
    • GTK+
    • Qt
    • wxWidgets
    • FLTK
  • Declarative
    • Reflex
    • Threepenny-gui
    • (Custom WebKit with PureScript or Elm)

GTK+

  • Idiomatic GTK+ Programming
    • Create widgets imperatively
    • Set attributes
    • Attach handers (callbacks) to signals
  • GTK+ Builder and Glade
    • XML markup
    • WYSIWYG editor
    • Declarative for first render, then imperative
  • CSS support (limited)
  • GObject Introspection
GTK+ Logo

Haskell and GTK+

Haskell GTK+ Bindings

  • gtk2hs
    • Deprecated in favour of haskell-gi
  • haskell-gi
    • Mostly generated using GObject introspection
    • OverloadedLabels for methods, attributes, and signals
    • Includes many packages
    • gi-gtk is the library I’ll focus on today

“Hello, world!” with gi-gtk

main :: IO ()
main = do
  _ <- Gtk.init Nothing
  win <- new Gtk.Window [#title := "Hi there"]
  #resize win 200 150
  _ <- on win #destroy Gtk.mainQuit
  button <- new Gtk.Button [#label := "Click me"]
  _ <- on button
     #clicked
     (set button [#sensitive := False, #label := "Thanks for clicking me"])
  #add win button
  #showAll win
  Gtk.main

“Hello, world!”

Slightly Larger Example

An Editable List of Names

editableNamesList :: IORef [Text] -> IO Gtk.Widget

Modifying State in a Callback

editableNamesList namesRef = do
  list     <- new Gtk.ListBox []
  initialNames <- readIORef namesRef

  forM_ (zip initialNames [0 ..]) $ \(name, i) -> do
    textEntry <- new Gtk.Entry [#text := name]
    void . on textEntry #changed $ do
      newName <- get textEntry #text
      atomicModifyIORef' namesRef $ \oldNames ->
        let newNames = oldNames & ix i .~ newName
        in (newNames, ())
    #add list textEntry

  Gtk.toWidget list

Using the Editable List

main = do
  ...
  namesRef <- newIORef ["Alice", "Bob", "Carol"]
  list <- editableNamesList namesRef
  #add win list

An Editable List of Names with Updates

editableNamesList :: [Text] -> IO (Gtk.Widget, Chan [Text])

Sending State Updates on a Channel

editableNamesList initialNames = do
  updates  <- newChan
  namesRef <- newIORef initialNames
  list     <- new Gtk.ListBox []

  forM_ (zip initialNames [0 ..]) $ \(name, i) -> do
    textEntry <- new Gtk.Entry [#text := name]
    void . on textEntry #changed $ do
      newName <- get textEntry #text
      writeChan updates =<<
        (atomicModifyIORef' namesRef $ \oldNames ->
          let newNames = oldNames & ix i .~ newName
          in (newNames, newNames))
    #add list textEntry

  widget <- Gtk.toWidget list
  return (widget, updates)

Printing Updates

main = do
  ...
  (list, updates) <- editableNamesList ["Alice", "Bob", "Carol"]
  void . forkIO . forever $ do
    names <- readChan updates
    print names
  #add win list

Callback Problems

  • When the application grows, callbacks gets unwieldy
    • IORefs, scattered state and logic
    • Side effects
    • Impractical to test
    • Two phases: first construction, subsequent updates
  • GUI code should be:
    • A separate concern
    • Declarative
    • Concise
  • Core application logic should be:
    • Pure (ideally!)
    • Decoupled from GUI code

Going Declarative

Declarative GTK+

  • Declarative markup, pure functions from state to markup
  • Declarative event handling, not concurrency primitives
  • “Virtual DOM” patching
  • Not tied to any particular architecture

gi-gtk-declarative

  • github.com/owickstrom/gi-gtk-declarative
  • Very thin layer on top of gi-gtk
    • Uses OverloadedLabels and type-level goodies
    • Automatic support for declarative and patchable GTK+ widgets
    • Monadic markup builder for do-notation
  • Soon on a Hackage near you

Single Widgets

  • Single widgets (without children) are constructed using widget:

    widget Button []
    
    widget CheckButton []
    

Bins

  • In GTK+, a bin can only contain a single child widget

  • Other examples are:
    • Expander
    • Viewport
    • SearchBar
  • To embed many widgets in a bin, use an in-between container

Containers

  • Containers can contain zero or more child widgets
  • In this library, containers restrict the type of their children:

    • ListBox requires each child to be a ListBoxRow:

      container ListBox [] $ do
        bin ListBoxRow [] $ widget Button []
        bin ListBoxRow [] $ widget CheckButton []
      
    • Box requires each child to be a BoxChild:

      container Box [] $ do
        boxChild False False 0 $ widget Button []
        boxChild True True 0 $ widget CheckButton []
      

Attributes

  • Widget attributes:

    widget Button [#label := "Click Here"]
    
  • Bin attributes:

    bin ScrolledWindow [ #hscrollbarPolicy := PolicyTypeAutomatic ] $
      someSuperWideWidget
    
  • Container attributes:

    container ListBox [ #selectionMode := SelectionModeMultiple ] $
      children
    

Events

  • Using on, you can emit events:

    counterButton clickCount =
      let msg = "I've been clicked "
                <> Text.pack (show clickCount)
                <> " times."
      in widget
          Button
          [ #label := msg
          , on #clicked ButtonClicked
          ]

Events using IO

  • Some events need to be constructed in IO, to query widgets for attributes

    onM                          -- very simplified
      :: Gtk.SignalProxy widget
      -> (widget -> IO event)
      -> Attribute widget event
  • Example emitting events with chosen color:

    colorButton color =
      widget
      ColorButton
      [ #title := "Selected color"
      , #rgba := color
      , onM #colorSet (fmap ColorChanged . getColorButtonRgba)
      ]

Functors

  • Markup, Widget, Bin, and Container all have Functor instances
  • We can map other events to our application’s event type:

    data ButtonEvent = ButtonClicked
    
    clickyButton :: Text -> Widget ButtonEvent
    
    data MyEvent = Incr | Decr
    
    incrDecrButtons :: Widget MyEvent
    incrDecrButtons =
      container Box [#orientation := OrientationHorizontal] $ do
        boxChild True True 0 $ clickyButton "-1" $> Decr
        boxChild True True 0 $ clickyButton "+1" $> Incr

CSS Classes

  • CSS classes are added using classes:

      widget Button [classes ["big-button"], #label := "CLICK ME"]

GI.Gtk.Declarative.App.Simple

GI.Gtk.Declarative.App.Simple

  • State reducer
  • Inspired by PureScript’s Pux framework
  • Also comparable to earlier versions of The Elm Architecture

App

data App model event =
  App
    { update :: model -> event -> (model, IO (Maybe event))
    , view   :: model -> Widget event
    , inputs :: [Producer event IO ()]
    }

Running

run
  :: Typeable event
  => Text                 -- ^ Window title
  -> Maybe (Int32, Int32) -- ^ Optional window size
  -> App model event      -- ^ Application
  -> model                -- ^ Initial model
  -> IO ()

Revisiting “Hello, world!”

Imperative Style “Hello, world!”

main :: IO ()
main = do
  _ <- Gtk.init Nothing
  win <- new Gtk.Window [#title := "Hi there"]
  #resize win 200 150
  _ <- on win #destroy Gtk.mainQuit
  button <- new Gtk.Button [#label := "Click me"]
  _ <- on button
     #clicked
     (set button [#sensitive := False, #label := "Thanks for clicking me"])
  #add win button
  #showAll win
  Gtk.main

Model and Events

data Model = NotClicked | Clicked

data Event = ButtonClicked

View

view' :: Model -> Widget Event
view' = \case
  NotClicked ->
    widget Button [#label := "Click me", on #clicked ButtonClicked]
  Clicked ->
    widget Button [#sensitive := False, #label := "Thanks for clicking me"]

Update

update' :: Model -> Event -> (Model, IO (Maybe Event))
update' _ ButtonClicked = (Clicked, return Nothing)

Run

main :: IO ()
main = run "Hi there" (Just (200, 150)) app NotClicked
  where app = App {view = view', update = update', inputs = []}

Revisiting Editable Names

Editable List of Names, Imperative Style

editableNamesList initialNames = do
  updates  <- newChan
  namesRef <- newIORef initialNames
  list     <- new Gtk.ListBox []

  forM_ (zip initialNames [0 ..]) $ \(name, i) -> do
    textEntry <- new Gtk.Entry [#text := name]
    void . on textEntry #changed $ do
      newName <- get textEntry #text
      writeChan updates =<<
        (atomicModifyIORef' namesRef $ \oldNames ->
          let newNames = oldNames & ix i .~ newName
          in (newNames, newNames))
    #add list textEntry

  widget <- Gtk.toWidget list
  return (widget, updates)

Model and Events

data Model = Model [Text]

data Event = NameChanged Int Text

View

view' :: Model -> Widget Event
view' (Model names) =
  container ListBox [] (zipWithM_ renderNameEntry names [0 ..])
  where
    renderNameEntry name i =
      bin ListBoxRow [] $
        widget Entry [ #text := name
                     , onM #changed (fmap (NameChanged i) . entryGetText)
                     ]

Update

update' :: Model -> Event -> (Model, IO (Maybe Event))
update' (Model names) (NameChanged i newName) =
  ( Model (names & ix i .~ newName)
  , print newName $> Nothing
  )

Run

main :: IO ()
main = run "Editable Names"
           (Just (640, 480))
           app
           (Model ["Alice", "Bob", "Carol"])
  where app = App {view = view', update = update', inputs = []}

Implementation

Patchable

class Patchable widget where
  create :: widget e -> IO Gtk.Widget
  patch :: widget e1 -> widget e2 -> Patch

data Patch
  = Modify (Gtk.Widget -> IO ())
  | Replace (IO Gtk.Widget)
  | Keep

Heterogeneous Widgets

  • Widgets are wrapped in the dynamic Widget data structure:

    data Widget event where
      Widget
        :: ( Typeable widget
           , Patchable widget
           , Functor widget
           , EventSource widget
           )
        => widget event
        -> Widget event
  • Similar to Data.Dynamic

Patching Widgets

instance Patchable Widget where
  create (Widget w) = create w
  patch (Widget (w1 :: t1 e1)) (Widget (w2 :: t2 e2)) =
    case eqT @t1 @t2 of
      Just Refl -> patch w1 w2
      _         -> Replace (create w2)
  • “The class Typeable allows a concrete representation of a type to be calculated.”

Smart Constructors Returning Widgets

  • All smart constructors can return Widget values:

    widget Button [] :: Widget event
    
    bin ScrolledWindow [] _ :: Widget event
    
    container ScrolledWindow [] _ :: Widget event

Smart Constructors Returning Markup

  • They can also return specialized markup:

    textRow :: Text -> MarkupOf (Bin ListBoxRow Widget) Event ()
    textRow t =
      bin ListBoxRow [] $
        widget Label [ #label := t ]
    
    myList :: Widget Event
    myList =
      container ListBox [] $
        mapM textRow ["Foo", "Bar", "Baz"]

Experience Report from FastCut

GTK+ in FastCut

  • First try was imperative, got stuck directly
  • gi-gtk-declarative let me build with pure functions:
    • Complex timeline navigation
    • Timeline commands (insert, delete, etc)
  • Dropping down to imperative:
    • Custom window setup
    • CSS style contexts
    • Top-level key event listeners
    • Dialogs and prompts
    • Custom application architecture (indexed monad FSM)

Timeline View

timelineView :: Project -> Focus ft -> Widget (Event TimelineMode)
timelineView project focus =
  container Box [#orientation := OrientationVertical] $ do
    boxChild True True 0 $
      renderPreviewPane (firstCompositionPart focus (project ^. timeline))
    boxChild False False 0 $ container
      ScrolledWindow
      [ #hscrollbarPolicy := PolicyTypeAutomatic
      , #vscrollbarPolicy := PolicyTypeNever
      , classes ["timeline-container"]
      ]
      (renderComposition (applyFocus (project ^. timeline) focus))

Property-Based Testing in FastCut

  • Timeline navigation, insertion, and deletion; all pure functions
  • Property-based testing
    • Hedgehog
    • Generating 10000 sequences of commands
    • Each generated command is based on previous resulting state
    • Each sequence must end with a valid timeline and focus
    • Caught many bugs and corner cases I hadn’t thought of

Summary

Summary

  • Callback-centric GUI programming is hard
  • Use pure functions and data structures for your core application code
  • Make rendering a function Model -> Widget
  • GTK+ can be programmed in a declarative fashion using gi-gtk-declarative
    • Still experimental
    • I haven’t built anything large using App.Simple
    • I have done zero benchmarking
  • FastCut has been a joy to build in this style

Thank You!