Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Quick Start

Set up a project and open a window. The tutorials continue from here.

Prerequisites

  • GHC 9.10 through 9.14 (development uses 9.14)
  • Cabal 3.12+
  • SDL3 and SDL3_ttf 3.2+ for the SDL backend
  • pkg-config

Nix (Linux / macOS)

In the nano-ui repository, nix develop provides GHC, Cabal, SDL3, SDL3_ttf, and pkg-config.

Windows

Install SDL3 and SDL3_ttf through MSYS2 UCRT64:

pacman -S mingw-w64-ucrt-x86_64-sdl3 mingw-w64-ucrt-x86_64-sdl3-ttf mingw-w64-ucrt-x86_64-pkg-config

Then point PKG_CONFIG_PATH at MSYS2’s pkgconfig directory:

$env:PKG_CONFIG_PATH = "C:\msys64\ucrt64\lib\pkgconfig"

Add nano-ui to a project

nano-ui is on Hackage, but this book follows the repository, which may be ahead of the release. Pin the source in cabal.project:

source-repository-package
  type: git
  location: https://github.com/goolord/nano-ui
  subdir:
    packages/nano-ui
    packages/nano-ui-sdl

package nano-ui-sdl
  flags: +sdl

Add the other packages you need (nano-ui-emit, nano-ui-diagrams, and so on) to the subdir list and to your .cabal dependencies.

In my-app.cabal:

executable my-app
  main-is: Main.hs
  default-language: GHC2024
  default-extensions: OverloadedStrings
  ghc-options: -threaded -Wall
  build-depends:
      base >= 4.20 && < 5
    , nano-ui
    , nano-ui-sdl
    , text

Haddocks and Hoogle

Hackage has Haddocks for the released version. For the current source, from a checkout:

cabal haddock-project --output=haddocks --hoogle

Open haddocks/index.html. Building every package is memory-heavy; cabal haddock nano-ui --haddock-hoogle builds one. See Development Setup for Hoogle indexing.

Important

Compile executables with -threaded. Between frames the backend blocks in SDL’s event wait, a foreign call. Under the non-threaded runtime that call stops every Haskell thread, so Task and Stream workers make no progress while the window is idle.

Your first application

Create Main.hs:

module Main (main) where

import Data.Text qualified as T
import NanoUI
import NanoUI.Backend.Sdl

main :: IO ()
main =
  runSdlApp
    defaultSdlOptions
      { sdlWindowSettings =
          defaultWindowSettings {wsTitle = "Hello nano-ui"}
      }
    appView

appView :: NanoUI ()
appView = columnWith (gap 12 . padAll 20 . alignCenter) $ do
  heading "Counter Demo"

  -- State at this call site; allocated on the first frame only.
  n <- useVar (0 :: Int)

  rowWith (gap 8 . alignMid) $ do
    whenM (button "-") (modifyVar n (subtract 1))

    value <- readVar n
    labelWith (fontMono . fontSize 18) (T.pack (show value))

    whenM (button "+") (modifyVar n (+ 1))

  whenM (button "Reset") (writeVar n 0)

Run it:

cabal run -j1 my-app

-j1 keeps memory down; a normal project can use cabal’s defaults.

How it works

  1. runSdlApp initializes SDL3, creates a window with TrueType font rendering, and starts the frame loop.
  2. columnWith and rowWith are layout containers. Their argument composes layout modifiers (gap 12 . padAll 20); the leftmost modifier wins when two set the same property.
  3. useVar (0 :: Int) allocates a Var on the first frame and returns the same Var on later frames, matched by its call position in the view. The initial value is used only on the first frame.
  4. button "+" draws an interactive button and returns True only on the frame it is activated.
  5. modifyVar n (+ 1) updates the value. Reading n below the buttons means the label shows the new value in the same pass; reading it above them would cost a second pass.

The view runs every frame, so two rules follow:

  • Guard side effects with an event: whenM (button "Save") save. button "Save" >> save saves every frame.
  • Allocate long-lived resources (newVar, tasks, caches) in IO before runSdlApp, never inside the view.

Next: Tutorial 1: Build a Todo List.