An open API service indexing awesome lists of open source software.

https://github.com/aether-lang-dev/aether-ui

Pseudo-declarative UI library for Aether
https://github.com/aether-lang-dev/aether-ui

Last synced: 4 days ago
JSON representation

Pseudo-declarative UI library for Aether

Awesome Lists containing this project

README

          

# Aether UI for Aether

Port of the [Perry](https://github.com/PerryTS/perry) UI framework to Aether.
Declarative widget DSL backed by GTK4 (Linux and FreeBSD), AppKit (macOS),
and a native Win32 backend (Windows) — the three backend implementations
share the same ABI declared in `aether_ui_backend.h`. Uses Aether's
trailing-block builder pattern.

## Credits

This module is a from-scratch Aether + C rewrite of the aether-ui Rust crates
from the [Perry project](https://github.com/PerryTS/perry) by the Perry
contributors. The Rust implementations (`aether-ui-gtk4`, `aether-ui-macos`, and
the core `aether-ui` crate) were used as reference for architecture, widget
API design, reactive state bindings, and platform-specific GTK4/AppKit
patterns. Based on commit
[`7f1e3f9`](https://github.com/PerryTS/perry/commit/7f1e3f979832c33d2da79970ea62bc1b74c2e31a)
of the `main` branch.

Portions Copyright (c) 2026 Perry Contributors collectively, and portions Copyright (c) 2026 Aether Contributors collectively. MIT License.

### Apps built on other people's ideas

Some apps under `apps/` port or borrow from existing projects. Each
carries its own `NOTICE` (and the upstream licence verbatim) beside its
source; the summary:

- **Maerkdown** (`apps/maerkdown`) — the word-as-widget markdown editor.
Its extended inline syntax (`++insertion++`, `||spoiler||`,
`==highlight==`, `^superscript^`, `~subscript~`) is taken from the
[Extended Markdown Syntax](https://github.com/kotaindah55/extended-markdown-syntax)
plugin for Obsidian by Kotaindah55 (Sheva Ihza),
[MIT](https://github.com/kotaindah55/extended-markdown-syntax/blob/main/LICENSE).
The delimiters and their meanings come from that project's documented
rules; no code was copied, and the parser is an independent
implementation over this editor's own document model.
- **Font Picker** (`apps/font_picker`) — a rule-for-rule port of
[Javascript Font Picker](https://www.jsfontpicker.com/), MIT, portions
Copyright (c) 2024-2025 Zygomatic.
- **Falling Blocks** (`apps/falling_blocks`) — derived from
[fallingblocks](https://github.com/SanderKlootwijk/fallingblocks) and
therefore **GPL-3.0**, unlike the rest of this repository. See the
header in that app's source before distributing binaries built from it.

## Quick start

### Linux (GTK4)

```bash
sudo apt install libgtk-4-dev # Debian/Ubuntu
./build.sh example_counter.ae build/counter
./build/counter
```

### FreeBSD / GhostBSD (GTK4)

Same GTK4 backend as Linux; `build.sh` detects FreeBSD and uses clang.

```bash
sudo pkg install gtk4 pkgconf # ensure a zlib.pc exists for freetype2 -> zlib
./build.sh example_counter.ae build/counter
./build/counter
```

`tests/spec_matrix.sh` needs no display of its own — on FreeBSD it starts a
private Xvfb (`pkg install xorg-vfbserver`), unprivileged, and any pre-set
`$DISPLAY` is respected instead.

### macOS (AppKit)

```bash
./build.sh example_counter.ae build/counter
./build/counter
```

### Windows (native Win32)

Build from an MSYS2 MinGW64 shell (no extra dev libraries — USER32, GDI+
and Common Controls ship with Windows itself):

```bash
./build.sh example_counter.ae build/counter
./build/counter.exe
```

Backend-level smoke tests (headless, no display needed) for any platform:

```bash
# widget + driver tests for current backend
gcc tests/test_widgets.c aether_ui_gtk4.c -o build/test_widgets $(pkg-config --cflags --libs gtk4) -I. -Itests -lpthread -lm && ./build/test_widgets # widget + driver tests for current backend
# microbenchmarks, CSV to stdout # microbenchmarks, CSV to stdout
```

See [docs/aether-ui-windows.md](docs/aether-ui-windows.md) for
Windows-specific details (DPI model, dark mode, widget mappings, known
limitations).

## DSL with Scope

Aether UI is a **"DSL with Scope"** — Matz's own name (he coined it when
asked to name the pattern) for the builder-block style: nested blocks that
describe structure declaratively while keeping full imperative power, with an
*implicit receiver* so children wire to their parent without explicit
plumbing. It runs in the Smalltalk-blocks / Ruby-Shoes / Groovy-SwingBuilder /
Kotlin-Compose / SwiftUI lineage — and, unlike a markup format, the blocks are
**executed code**, not parsed into a DOM for some later actioning. See
[Paul Hammant's "That Ruby and Groovy Language Feature"](https://paulhammant.com/2024/02/14/that-ruby-and-groovy-language-feature.html)
for the full tour, and Aether's own
[`docs/closures-and-builder-dsl.md`](https://github.com/aether-lang-org/aether/blob/main/docs/closures-and-builder-dsl.md)
for the mechanism (trailing blocks, the `_ctx` implicit-receiver convention,
and `builder … with` "configure then execute").

A UI is opened inside a **surface** scope. The surface's *kind* decides its
lifecycle (see [Surfaces](#surfaces-window--render_to--record) below):

```aether
import ui

main() {
counter = aether_ui.ui_state(0)

aether_ui.window("My App", 400, 200) {
aether_ui.root_vstack(10) {
aether_ui.text("Hello World")
aether_ui.text_bound(counter, "Count: ", "")
aether_ui.hstack(5) {
aether_ui.button("+1") callback {
aether_ui.ui_set(counter, aether_ui.ui_get(counter) + 1)
}
aether_ui.button("-1") callback {
aether_ui.ui_set(counter, aether_ui.ui_get(counter) - 1)
}
}
}
}
}
```

The `window(…) { … }` block builds the tree, then — because it's a `builder`
function whose body runs *after* the block — opens the window and runs the
event loop. No trailing `app_run(root)`: the surface *is* the entry point.

## Surfaces (window / render_to / record)

A **surface** is the ambient destination a widget/drawing block populates.
The kind decides lifecycle:

| Surface | Lifecycle | What it is |
|---------|-----------|------------|
| `window(title, w, h) { … }` | **lived** — runs the event loop, ends on window close | An on-screen interactive window. Absorbs the old `app_run`. |
| `render_to(target, w, h) { … }` | **bounded** — one render pass, returns | Draw into a target: pixel buffer, PNG, PDF, **paper**. No event loop. |
| `record(w, h) { … }` | **bounded** — captures, returns | A test/recording surface — inspect what was built. No event loop. |
| `window_run(title, w, h, root)` | lived | Explicit-root variant of `window` for trees built imperatively (e.g. a `root_grid` whose cells are `grid_place`'d in). |

Interactive verbs (`onclick`, `onhover`) used inside a **bounded** surface are
*diagnostic-inert*: they render but the handler never fires (there's no event
loop to deliver to). The diagnostic is **collected on the surface** by default
(read it with `surface_diagnostics(handle)`); routing it to stderr or a hard
fail is an explicit opt-in, never the default — the framework never writes to
a stream you didn't ask it to.

**Inside a surface block, use the context-attaching layout verbs** (`vstack`,
`hstack`, `zstack`, …) — **not** the `root_*` variants (`root_vstack`,
`root_hstack`). The `root_*` verbs are *detached*: they take no builder context
and so don't attach to the enclosing surface, leaving you with a window that
maps but renders blank. The `root_*` forms exist only for the explicit-root
`window_run(title, w, h, root)` path, where you build the tree imperatively and
hand the root in. Inside `window {…}` / `render_to {…}` / `record {…}`, always
`vstack` (which the compiler auto-parents to the surface via the `_ctx`
convention).

Why three verbs instead of one `app_run`? Because `app_run` welded together
three jobs — create the window, mount the tree, run the loop — and forced that
*lived* shape onto every program. Most surfaces aren't lived: a render-to-PNG,
a print-to-paper, a headless test needs no loop and ends by reaching `}`. Only
a live window has "a life of its own" that ends on an external event, so only
`window` carries the loop.

## Widgets available

| Widget | Aether function | GTK4 | AppKit | Win32 |
|-------------|-------------------------------------------------------|--------------------|-------------------------|----------------------------|
| Text | `ui.text("label")` | GtkLabel | NSTextField (label) | STATIC |
| Button | `ui.button("label") callback { }` | GtkButton | NSButton | BUTTON (BS_PUSHBUTTON) |
| VStack | `ui.vstack(spacing) { children }` | GtkBox vertical | NSStackView vertical | AetherUIStack (custom) |
| HStack | `ui.hstack(spacing) { children }` | GtkBox horizontal | NSStackView horizontal | AetherUIStack (custom) |
| Spacer | `ui.spacer()` | Expanding GtkBox | NSView flex filler | flex placeholder |
| Divider | `ui.divider()` | GtkSeparator | NSBox separator | GDI line (custom class) |
| TextField | `ui.textfield("hint") callback \|val\| { }` | GtkEntry | NSTextField | EDIT |
| SecureField | `ui.securefield("hint") callback \|val\| { }` | GtkPasswordEntry | NSSecureTextField | EDIT (ES_PASSWORD) |
| Toggle | `ui.toggle("label") callback \|active\| { }` | GtkCheckButton | NSButton (switch) | BUTTON (BS_AUTOCHECKBOX) |
| Slider | `ui.slider(min, max, init) callback \|val\|` | GtkScale | NSSlider | TRACKBAR (comctl32) |
| Picker | `ui.picker() callback \|idx\| { }` | GtkDropDown | NSPopUpButton | COMBOBOX (CBS_DROPDOWNLIST)|
| TextArea | `ui.textarea("hint") callback \|val\| { }` | GtkTextView | NSTextView | EDIT (ES_MULTILINE) |
| ProgressBar | `ui.progressbar(0.75)` | GtkProgressBar | NSProgressIndicator | PROGRESS (comctl32) |
| ScrollView | `ui.scrollview() { children }` | GtkScrolledWindow | NSScrollView | AetherUIStack + WS_VSCROLL |
| Grid | `ui.root_grid(cols, rspace, cspace)` + `grid_place(...)` | GtkGrid | NSGridView | AetherUIGrid (custom) |
| Menu bar | `ui.menu_bar()` + `menu()` + `menu_item()` | GMenu / GActionMap | NSMenu | HMENU (CreateMenu/SetMenu) |

## Reactive state

```aether
counter = aether_ui.ui_state(0) // create state cell
aether_ui.text_bound(counter, "Val: ", "") // auto-updating text
aether_ui.ui_set(counter, 42) // triggers re-render
val = aether_ui.ui_get(counter) // read current value
```

## Widget accessors

```aether
aether_ui.set_text(handle, "new text") // set textfield value
text = aether_ui.get_text(handle) // get textfield value
aether_ui.set_toggle(handle, 1) // set toggle on/off
aether_ui.set_slider(handle, 75.0) // set slider position
aether_ui.set_progress(handle, 0.5) // set progress bar
```

## Examples

| Example | Widgets demonstrated |
|---------|---------------------|
| `example_counter.ae` | text, button, hstack, vstack, spacer, divider, reactive state |
| `example_form.ae` | textfield, securefield, toggle, slider, textarea, progressbar |
| `example_picker.ae` | picker (dropdown), picker_add |
| `example_styled.ae` | form, section, zstack, bg_color, bg_gradient, font_size, corner_radius |
| `example_system.ae` | alert, clipboard, dark mode detection, sheet |
| `example_canvas.ae` | canvas drawing, fill_rect, stroke, on_hover, on_double_click |
| `example_testable.ae` | AetherUIDriver test server, sealed widgets, remote control banner |

## AetherUIDriver — automated UI testing, baked in

Aether UI ships with a built-in HTTP test server that lets any language
with an HTTP client drive the app:

```aether
aether_ui.enable_test_server(9222, root)
```

Or set `AETHER_UI_TEST_PORT=9222` in the environment before launching —
no code changes needed. A red "Under Remote Control" banner is injected
so a user can't mistake a test-driven session for a real one.

The HTTP API exposes `/widgets` (list + filter), `/widget/{id}` (state),
`/widget/{id}/click | set_text | toggle | set_value` (mutations), and
`/state/{id}` + `/state/{id}/set` (reactive-state cells). See the full
reference and end-to-end examples in
**[docs/aether-ui-testing.md](../../docs/aether-ui-testing.md)** —
including Bash, Python, and JavaScript test-suite skeletons plus the
`AETHER_UI_HEADLESS=1` flag for unattended CI.

For most native UI frameworks you have to bolt on Selenium/Appium. With
aether_ui it's part of the framework and works identically on macOS,
Linux, FreeBSD, and Windows via the shared
[`aether_ui_test_server.c`](aether_ui_test_server.c).

### Widget sealing

Mark widgets as non-automatable — the test server returns 403 for sealed widgets:

```aether
danger = aether_ui.button("Delete Everything") callback { ... }
aether_ui.seal_widget(danger)
```

This maps to Aether's `hide`/`seal` philosophy: the app author declares which
capabilities the test harness is denied, not the other way around.

## Architecture

| Layer | File | Role |
|-------|------|------|
| Aether DSL | `ui/module.ae` | Builder-pattern wrappers with `_ctx` auto-injection; surface verbs (`window`/`render_to`/`record`) |
| GTK4 backend | `aether_ui_gtk4.c` | Linux + FreeBSD: GTK4 C API calls, Cairo canvas, test server |
| macOS backend | `aether_ui_macos.m` | macOS: AppKit Objective-C |
| Win32 backend | `aether_ui_win32.c` | Windows: USER32 + GDI+ + Common Controls |
| C header | `aether_ui_backend.h` | Shared backend ABI — implemented by all three backends (four platforms; FreeBSD shares GTK4) |
| Build script | `build.sh` | Auto-detects platform (Darwin/Linux/FreeBSD/MinGW) |
| Test script | `test_automation.sh` | Example curl-based test suite (17 assertions) |
| Widget tests | `tests/test_widgets.c` | Cross-platform C-level smoke suite (40 assertions) |
| Driver tests | `tests/test_driver.sh` | HTTP integration against the embedded test server |
| Benchmarks | `benchmarks/bench_widgets.c` | CSV microbenchmarks — widget create, layout, state, canvas |

## Platform support

| Platform | Backend | Status |
|----------|---------------------------------|------------------------------------------------------------------------------------|
| Linux | GTK4 (`aether_ui_gtk4.c`) | Full — all widgets, canvas, events, styling, AetherUIDriver test server |
| macOS | AppKit (`aether_ui_macos.m`) | Full — all widgets, canvas, events, styling, AetherUIDriver test server |
| Windows | Native Win32 (`aether_ui_win32.c`) | Full — USER32 + GDI+ + Common Controls; per-monitor DPI v2; immersive dark mode; AetherUIDriver via winsock2 |
| FreeBSD | GTK4 (`aether_ui_gtk4.c`) | Full — shares the Linux backend; clang build, private-Xvfb spec runs |

"Full" above means the backend implements the whole widget/canvas/event/
styling surface plus AetherUIDriver — not that every suite is green on every
box. `tests/spec_matrix.sh` is the authority; run it on the platform you care
about. Most recent full runs: Linux 228/0, FreeBSD 223/0 (only `lismusic`,
which needs the sqlite contrib archive installed on that host).

## Status

All groups (1–7) plus AetherUIDriver are implemented on every backend.
`# widget + driver tests for current backend
gcc tests/test_widgets.c aether_ui_gtk4.c -o build/test_widgets $(pkg-config --cflags --libs gtk4) -I. -Itests -lpthread -lm && ./build/test_widgets` runs the cross-platform smoke suite and, on
Windows, the HTTP driver integration. `# microbenchmarks, CSV to stdout` prints a
CSV of per-operation latencies.