nano-ui: Immediate-mode GUI toolkit for Haskell

[ graphics, library, mit, program ] [ Propose Tags ] [ Report a vulnerability ]
Versions [RSS] 0.1.0.0, 0.1.0.1
Change log CHANGELOG.md
Dependencies base (>=4.20 && <4.23), bytestring (>=0.11 && <0.13), colonnade (>=1.2 && <1.3), containers (>=0.6.7 && <0.9), effectful-core (>=2.5 && <2.8), ghc-compact (>=0.1 && <0.2), hashable (>=1.4 && <1.6), hashtables (>=1.3 && <1.5), hexml (>=0.3.4 && <0.4), nano-ui, primitive (>=0.8 && <0.10), text (>=2.0 && <2.2), text-short (>=0.1.5 && <0.2), unordered-containers (>=0.2.19 && <0.3), vector (>=0.13 && <0.14) [details]
Tested with ghc ==9.10.3 || ==9.14.1
License MIT
Author goolord
Maintainer zacharyachurchill@gmail.com
Uploaded by goolord at 2026-09-17T17:02:24Z
Category Graphics
Home page https://github.com/goolord/nano-ui
Bug tracker https://github.com/goolord/nano-ui/issues
Source repo head: git clone https://github.com/goolord/nano-ui.git(packages/nano-ui)
Distributions
Reverse Dependencies 3 direct, 0 indirect [details]
Executables nano-ui-profile
Downloads 11 total (11 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user
Build status unknown [no reports yet]

Readme for nano-ui-0.1.0.0

[back to package description]

nano-ui

An immediate-mode GUI toolkit for Haskell.

A view is a function that runs every frame. Each widget is an ordinary effect that adds a layout node, reads this frame's input, and returns a result: Bool for a button, the new value for an input. There are no widget objects to keep and no callbacks to register. Widget state lives in a store keyed by each widget's position among its siblings (or by a key you give it with withKey), or in a model you pass through the view.

{-# LANGUAGE OverloadedStrings #-}

import Data.Text qualified as T
import NanoUI
import NanoUI.Backend.Sdl (defaultSdlOptions, runSdlApp)

main :: IO ()
main = runSdlApp defaultSdlOptions counter

counter :: NanoUI ()
counter = do
  (n, setN) <- useInt 0
  row $ do
    whenM (button "-") (setN (n - 1))
    label (T.pack (show n))
    whenM (button "+") (setN (n + 1))

Features

  • Text inputs and a multi-line text area, numeric fields, sliders, knobs, selects, combo boxes, sortable tables, trees, tabs, menus, context menus, modals, floating windows, pane grids, colour pickers, progress bars, sparklines, and drag and drop. customWidget and a canvas API cover anything else.
  • Row, column, and grid layout with scrolling. Layout options are Layout -> Layout modifiers, as in columnWith (gap 8 . padAll 12).
  • Scrollers take a wheel step and a glide time (setScrollTuning, setScrollStep), and move from code: scrollTo, scrollBy, scrollPages, and scrollIntoView. getScrollMetrics gives a virtualized list the viewport and offset it needs to pick its rows.
  • Keyboard focus and navigation for every control.
  • Shaped text in the SDL backend, with fallback fonts for other scripts and mixed left-to-right and right-to-left lines. richText wraps a paragraph of mixed styles and links, as in richText ["Read ", strong "the guide", " or ", hyperlink "faq" "the FAQ"].
  • Text fields with undo and redo, driven by TextCommand values that code can run too.
  • SVG icons (loadSvg, svgIcon) and a spinner.
  • Backends block on input and run a frame only when something needs redrawing. Each frame computes its damage against the previous one.
  • State in local hooks (useInt, useText, useState), in your own model, or in a reducer with NanoUI.Emit.
  • Eased and spring animation.
  • Themes, including ones built from Base16 schemes, changed for part of a view with styled and composable modifiers, as in styled (primary . buttonStyle (cornerRadius 6)) (button "Save"). disabledWhen switches widgets off.
  • NanoUI.Testing runs frames headlessly on scripted input, for tests.

Examples

Inputs are controlled: pass the current value and keep the result. A change you don't store is undone on the next frame.

greeter :: NanoUI ()
greeter = columnWith (gap 8 . padAll 16) $ do
  (name, setName) <- useText "world"
  (size, setSize) <- useFloat 16
  (shout, setShout) <- useFlag False

  heading "Greeter"
  setName =<< textInput name
  setSize =<< slider 10 48 size
  setShout =<< checkbox "Shout" shout

  let greeting = "Hello, " <> name <> "!"
  labelWith (fontSize size) (if shout then T.toUpper greeting else greeting)

For an Elm-style update function, the widgets in NanoUI.Emit emit messages instead of returning values, and runSdlAppReduce folds them into the model:

{-# LANGUAGE OverloadedStrings #-}

import Data.Text qualified as T
import NanoUI
import NanoUI.Backend.Sdl (defaultSdlOptions, runSdlAppReduce)
import NanoUI.Emit qualified as Emit

data Msg = Increment | Decrement

main :: IO ()
main = runSdlAppReduce defaultSdlOptions update 0 view

update :: Msg -> Int -> Int
update Increment n = n + 1
update Decrement n = n - 1

view :: Int -> NanoUI ()
view n = row $ do
  Emit.button "-" Decrement
  label (T.pack (show n))
  Emit.button "+" Increment

How it works

NanoUI is Eff '[Ui, IOE] from effectful, and widgets have types like Ui :> es => Eff es Bool, so a view can run in a larger effect stack. A frame:

  1. Resets the node and vertex arenas and runs the view. Widgets add layout nodes and read and write the widget store.
  2. Solves layout.
  3. Resolves pointer, keyboard, and focus against the new geometry.
  4. Paints into pinned vertex and index buffers, in background, content, overlay, and chrome layers.
  5. Computes damage against the previous frame and hands the draw list to the backend.

docs/rendering-pipeline.svg has the diagram. Per-frame code is profiled for allocation, and an inspection-testing suite checks that the vertex writers compile without dictionaries or tuples.

Packages

Package What it is
nano-ui Widgets, layout, input handling, and the draw list
nano-ui-sdl Window backend on SDL3, with TrueType fonts, installed-font lookup, and native file dialogs
nano-ui-rgfw Window backend on RGFW and OpenGL 3.2, with a bundled bitmap font and no system dependencies beyond windowing
nano-ui-rgfw-bindings Haskell bindings to RGFW
nano-ui-diagrams Line, bar, scatter, and area charts, and drawing with diagrams
nano-ui-form Validated forms built on ditto
nano-ui-demo Example applications

Running the demos

You need GHC 9.10 or later and Cabal. The SDL backend also needs SDL3, SDL3_ttf, and pkg-config. nix develop sets these up.

cabal run nano-ui-sdl-demo       # widget and chart tour
cabal run nano-ui-sdl-notepad    # text editor with menus and file dialogs
cabal run nano-ui-sdl-logs       # streaming log viewer
cabal run nano-ui-sdl-terminal   # terminal on /bin/sh (Linux and macOS)
cabal run nano-ui-rgfw-demo      # the RGFW backend

Documentation

Start with the NanoUI module documentation: it explains how widgets return values, how inputs keep their state, and how layout modifiers compose. docs/development.md covers building, testing, profiling, and the layout of this repository.

License

MIT