Verified on macOS, Windows and Linux

The drawing is Keal.

Not bindings to a toolkit, not a wrapper round a C canvas. The rasteriser, the TrueType engine, the layout, the widgets, the theme and the docking are .keal files. The C underneath opens a window, reports what the user did, and puts a finished buffer of pixels on the screen. It does not draw anything.

hello.keal320 × 200 pt
import "keal-view/keal-view.keal"

val clicks = state(0)

runApp("Hello", 320, 200, { ->
    column([
        label("Clicked ${clicks.get()} times").fontSize(20.0).centered(),
        button("Click me", { -> clicks.set(clicks.get() + 1) }).kindOf(primary)
    ]).gaps(12.0).padAll(24.0).aligned(mainCenter, crossCenter).grows()
})
Clicked 3 times
Click me
Every pixel above came out of a loop written in Keal.
A docked workspace built with keal-view
Every pixel above — the rounded corners, the anti-aliased borders, the shadows, the glyphs — came out of a loop written in Keal.

87 to 89 % of a running keal-view program is Keal.

Which of the two it is depends on the backend it was built against, and none of the other 11 to 13 % puts a pixel anywhere. The whole C surface is one header: a framebuffer, a byte blob, an event struct, and accessors for each.

The Keal side is the whole framework: rasteriser, fonts, layout, widgets, theme, docking, menus, the run loop. The C side is one window, one event queue, and inline accessors: kv.h at 281 lines plus one backend of three — Cocoa 409, X11 567, Win32 573. The bar shows the largest of them.

Keal
5 598 lines
C
690-854 lines

Why that is even possible

One fact about the compiler decides the whole design. native """…""" pastes its C into the same translation unit as the compiled program, so a static inline function declared there is inlined into Keal's own code by the C compiler. A call across the boundary is not a call — it is the instruction it contains.

src/ffi.keal
extern func kvPxSet(x: Int, y: Int, argb: Int): Int = "kv_set"

That line is one bounds-checked store. Measured at about half a nanosecond per pixel from a Keal loop, which is what a C rasteriser costs, because after inlining it is the C rasteriser. So there was no reason to write the drawing in C, and every reason not to.

What is in it

Widgets

Labels, buttons, checkboxes, radios, toggles, segmented controls, selects, sliders, steppers, progress, fields, tabs, cards, panels, scrolls — and custom, handed a clipped canvas, its rectangle, the fonts, the theme and its own hover state.

Menus, dialogs and tooltips

openMenu, openDialog, openSheet and .tip("…"). Nothing has to be installed: the run loop composes that layer above the application's own overlay.

Layout

One axis of flexbox and no more. There is no shrinking — content that does not fit is clipped rather than squeezed, because a layout that silently compresses fails only on small windows, which is the last place anyone looks.

Text

A TrueType parser and rasteriser in Keal: cmap formats 0, 4, 6 and 12, glyf including composite glyphs, collections. Glyphs are filled by the signed-area method and rasterised at the size they will actually be drawn.

Drawing

Anti-aliasing is analytic, not sampled: a straight edge's coverage is its exact overlap with the pixel cell. There is no quality dial and no frame that is quietly cheaper than the last.

Theme

Dark and light, built from one surface ramp, one accent and three status hues rather than a list of hex codes. useTheme(lightTheme()) is the whole change, including for widgets written afterwards.

Points, not pixels

The rasteriser is the only thing that knows what a pixel is. One layout is correct on a Retina display and a projector; the numbers never change, the scale does.

An idle window costs nothing

Between frames the loop is inside kvWait, which blocks until the system has something to say. Not a low duty cycle — none. On a laptop that is worth more than drawing speed.

Docking that tells you where it will land

The arrangement is a binary tree of exactly two cases — a leaf holding tabs, and a split holding two children with a fraction — which is few enough that every arrangement it can be in is one you can reason about.

A panel is dragged by its tab; while it is moving, the highlight under the pointer says where letting go would put it — left, right, above, below, or as another tab — before it is let go. A panel is an ordinary view function and does not know it is docked.

examples/studio.keal
val dock = Dock(
    beside(
        leaf(["files"]),
        above(
            beside(leaf(["editor"]), leaf(["props", "preview"]), 0.68),
            leaf(["terminal"]),
            0.72),
        0.19))

dock.add(panelOf("files", "Files", { -> filesPanel() }))

Three platforms, one dozen functions

All three backends were written against the same dozen functions, and nothing above runtime/ changed to add the second and third — which is the point of having drawn the boundary where it is.

PlatformBackendState
macOSruntime/kv_cocoa.mCocoa. Verified — developed here
Windowsruntime/kv_win32.cWin32 and a BI_RGB DIB. Verified on Windows 10 22H2 and 11 22H2, MinGW-w64
Linuxruntime/kv_x11.cXlib only, no toolkit. Verified on Ubuntu 26.04 aarch64, under Wayland through XWayland

Windows and Linux were each verified by someone on a real machine, going through the porting checklist line by line, and between them it cost nineteen defects — twelve of which were above the backend, and so were on every platform including the one this was written on. Four things remain unverified and are recorded rather than glossed.

What that week found, and what it could not →

Running it

keal-view needs the Keal compiler beside it. There is no build system here: tools/build.sh picks this platform's backend, compiles it once, and hands the object file to keal build.

in a terminal
# the compiler, then the framework
git clone https://github.com/geneacta/keal
git clone https://github.com/geneacta/keal-view
cd keal && cargo build --release && cd ../keal-view

tools/build.sh examples/tour.keal    && build/tour       # start here
tools/build.sh examples/gallery.keal && build/gallery    # every widget
tools/test.sh                                   # 194 checks, no display

Two things every keal-view program understands for free

The first needs no display at all, which is how the framework is checked in continuous integration and how every picture on this site was made.

in a terminal
build/studio --snapshot frame.bmp 2          # one frame to a file, then exit
build/studio --snapshot frame.bmp 2 900 1900 # …at a size of your choosing
build/studio --window-id /tmp/id             # write its own window number out