Search by

Native desktop GUI for PHP 8.5+ via libui-ng and FFI.

Package info

github.com/HelgeSverre/libui

pkg:composer/helgesverre/libui

Statistics

Installs: 31

Dependents: 1

Suggesters: 0

Stars: 6

Open Issues: 0

v0.8.1 2026-10-14 12:11 UTC

README

Native desktop GUIs in PHP — a typed, object-oriented binding to libui-ng driven by PHP's built-in FFI. Real native windows, widgets, dialogs, 2D drawing and text. No compiled PHP extension, no C toolchain on the user's machine.

CI Packagist Version PHP License

See the changelog for release notes.

Generative flow field Markdown editor with live preview

Install

composer require helgesverre/libui

Requires PHP 8.5+ with the FFI extension (enabled by default on the CLI). Prebuilt libui binaries ship inside the package for macOS, Linux (x86_64 + aarch64) and Windows — there's no compiler or build step. Linux just needs GTK 3 installed at runtime; see Platform support.

macOS Gatekeeper: the bundled binary is signed with a Developer ID and notarized, so it loads normally. If you build the dylib yourself or grab one from an unsigned source, macOS may block it with "library load disallowed by system policy." Clear the download quarantine with:

xattr -dr com.apple.quarantine vendor/helgesverre/libui/lib/darwin/libui.dylib

Quick start

<?php

require 'vendor/autoload.php';

use Libui\Ffi;
use Libui\Window;
use Libui\Box;
use Libui\Entry;
use Libui\Button;

Ffi::init();

$entry  = new Entry();
$button = (new Button('Greet'))->onClicked(fn () => print("Hi, {$entry->text()}!\n"));

(new Window('Greeter'))
    ->setChild(
        (new Box(padded: true))
            ->append($entry)
            ->append($button),
    )
    ->run();

Window::run() shows the window, runs the event loop, and tears everything down when it closes — so a single-window app is one call. For multiple windows or an app-level quit handler, use the App facade:

use Libui\App;

App::new()
    ->window($mainWindow)
    ->onShouldQuit(fn () => $document->isSaved())
    ->run();

Every widget is a typed class with fluent setters and IDE autocompletion ($slider->setValue(50), $entry->text(), $button->onClicked(...)), so you can discover the API as you type. Prefer a single declarative expression? Libui\Build (vbox/hbox/form/window) composes the same layout in one call.

What you can build

Generative flow field
Generative art — thousands of streamlines through an animated vector field.
Command palette
Command palette — a borderless Raycast-style launcher; fuzzy search + keyboard nav, all custom-drawn.
System monitor
Live dashboard — CPU charts, a gauge and sparklines from real load data.
Markdown editor
Markdown editor — a live rich-rendered preview, drawn with the text engine (no web view).
Colour picker
Colour picker — an HSV wheel of arc wedges with a live swatch and hex/RGB readouts.
Widget gallery
Widgets — tabs, forms, sliders, spinboxes, combos, date & colour pickers.

…plus 2D drawing, data grids, attributed text, timer animation, and native menus & dialogs. All the runnable sources are in examples/ — see Development to run them.

Features

  • 23 typed widget classes — windows, boxes, forms, grids, tabs, groups, buttons, checkboxes, radio buttons, entries (plain/password/search), multiline entries, labels, spinboxes, sliders, progress bars, combos, editable combos, date/time pickers, colour & font buttons, separators, menus and menu items.
  • 19 PHP enums + bit-flags (Align, TextWeight, DrawFillMode, …), generated 1:1 from libui's C enums.
  • Native dialogs — message boxes, open/save/folder pickers, plus a parent-bound Dialogs facade so you don't repeat the window on every call.
  • Safe menus — a Menu builder that enforces libui's "menus before the first window" rule (throws instead of silently breaking) and clean onClick() handlers.
  • Typed colours — one Color value type (Color::rgb(0x312B90), ::hex(), ::rgba(), ::rgb255(), named) accepted across brushes, text and colour buttons.
  • Custom 2D drawing — vector Paths (with line/circle/arc/roundedRect sugar), solid/gradient Brushes with typed Stops, a fluent StrokeParams builder, affine Matrix, clipping, and DrawContext fill/stroke shape helpers. Areas also support scrollTo() and beginUserWindowMove()/beginUserWindowResize() for scrollable canvases and borderless window dragging.
  • Attributed text — AttributedString with per-range colour/weight/italic/ underline attributes, a FontDescriptor, and a drawable TextLayout; plus a RichText builder + TextStyle value object for one-call styled text. OpenType features (ligatures, small-caps, …) are exposed via Text\OpenTypeFeatures and a Features attribute.
  • Data-grid table — Table::fromRows() / fromAssoc() for static data (no delegate), or a TableModelDelegate for dynamic data; checkbox/progress/button/ image columns, row-index click callbacks, sortable headers (onHeaderClicked() + setSortIndicator()), and automatic model lifetime.
  • Images — Image::fromPng() (via GD) or Image::fromRgba() for table image columns and area drawing.
  • Form binding — input widgets implement HasValue, and Form::values() / setValues() read and write a whole form at once, keyed by field label.
  • App lifecycle — App facade for multi-window apps + an onShouldQuit() handler, or Window::run() for one-call single-window apps. Windows also expose getContentSize() and getPosition().
  • Declarative layout — a Build facade (Build::vbox(), hbox(), form(), window(), stretchy()) composes a whole layout in one expression when you prefer that over chained setters.
  • Testable — a Testing\CallbackSpy and Testing\Inspect harness let you assert on widget state and callback invocations without spinning up the event loop.
  • Async helpers — the Loop class (defer/delay/repeat/cancel) over libui's native event loop, plus raw queueMain(), timer(), onShouldQuit().
  • Clipboard — Utils\Clipboard cross-platform copy/paste (best-effort, shells out to the platform tool).
  • All 299 libui functions callable — anything without a sugar wrapper is still reachable raw via Ffi::get()->ui…().

Async I/O

Non-blocking operations are fully supported. Use the Loop class to schedule callbacks on the event loop without freezing the GUI:

use Libui\Loop;

Loop::defer(fn () => print "This runs on the next tick\n");
Loop::delay(1000, fn () => print "This runs after 1 second\n");
$id = Loop::repeat(100, fn () => print "This runs every 100ms\n");
Loop::cancel($id); // stop a repeating/pending timer

(fn () => echo … is a syntax error — echo is a statement; use print, which is an expression, or a normal function () { … } body.)

Cancellation is lazy: after Loop::cancel() the callback never fires again, and the native timer stops on its next wake-up. For HTTP requests, drive an async HTTP client (ReactPHP, Amp, Guzzle) from a short Loop::repeat() tick and marshal results back with Loop::defer(). See examples/async_http.php for a complete example, and the user guide for the full async story.

Platform support

The loader resolves the right binary for the current OS + architecture from lib/<platform>/, overridable with the $LIBUI_LIB environment variable.

Platform Status Notes
macOS (arm64 + x86_64) Prebuilt, ships in the package Universal lib/darwin/libui.dylib; works out of the box.
Linux (x86_64 / aarch64) Prebuilt, ships in the package lib/linux-*/libui.so (x86_64 built on Ubuntu 22.04 for broad glibc compatibility). Needs GTK 3 installed at runtime (apt install libgtk-3-0). On an incompatible distro, run composer build-lib or point $LIBUI_LIB at a system libui.so.
Windows (x86_64) Prebuilt, ships in the package lib/windows-x86_64/libui.dll (built with MSVC); works out of the box.

Why not the ext-ui from php.net?

The PHP manual still documents a UI extension (pecl/ui). It is abandoned and PHP 7-only: the last release is 2.0.0 (July 2018), its C targets PHP 7's Zend API so it won't compile on PHP 8.x, and pecl install ui fails at configure on a modern machine. This package reaches the same goal a different way — it loads the maintained libui-ng library at runtime and calls it through FFI, so there's no extension to compile.

Coverage & limits

  • All 299 libui functions are callable (raw, via Ffi::get()->ui…()).
  • 23 of 26 widget types have typed classes. The other three are hand-written: uiControl is the base class, uiArea is the drawing adapter, and uiTable is the data grid.
  • Custom drawing covers paths (incl. line/circle/arc/roundedRect/bézier sugar), solid & gradient brushes with typed Stops, a fluent StrokeParams builder, DrawContext shape helpers, stroking, clipping and transforms; attributed text has a full sugar layer.
  • Image has a sugar class (fromPng/fromRgba); tables (factories, typed columns, row-index callbacks, automatic model lifetime), the App/Loop lifecycle, menus/dialogs, and Clipboard all have typed helpers too.
  • Still raw-only (no sugar yet): OpenGL areas, and a synchronous yes/no confirm() dialog (libui has no native modal — use Ui::msgBox or a custom window).

See the user guide for a task-by-task walkthrough and the gotchas (FFI lifetime, callback retention, string ownership, table teardown).

Development

For working on the library itself — clone it, build the native library, and run the tests:

git clone https://github.com/HelgeSverre/libui
cd libui
composer install
composer build-lib   # build lib/<platform>/libui.* from libui-ng

build-lib needs meson + ninja (brew install meson ninja), plus the GTK 3 dev headers on Linux (apt install libgtk-3-dev). On macOS the prebuilt dylib is already committed, so this is only needed to refresh it.

Native library (our libui-ng fork)

The bundled binaries are not built from upstream — they come from our maintained fork, HelgeSverre/libui-ng, pinned to a commit by LIBUI_REF in build-libui.sh. The fork carries fixes that aren't upstream yet, each as a reviewable commit:

  • color-emoji rendering in Windows custom drawing and native entry controls;
  • custom-chrome / borderless windows (drag handle, corner style, key focus);
  • a uiAttribute memory-leak fix (orphaned attributes on equal-value span merges).

patches/ keeps these as standalone diffs for history and review; since they're already baked into the pinned fork ref, build-libui.sh re-applies them as a no-op. To change native behaviour: commit to the fork, bump LIBUI_REF, then composer build-lib (which on macOS also re-signs the dylib with a Developer ID and which you can follow with composer notarize). See patches/README.md.

Run the examples

# Showcase
php examples/flowfield.php  # generative flow field (animated)
php examples/palette.php    # borderless Raycast-style command palette
php examples/monitor.php    # live system-monitor dashboard
php examples/markdown.php   # markdown editor + live rendered preview
php examples/colorpicker.php # an HSV colour wheel + picker

# Basics
php examples/form.php       # the greeter form
php examples/gallery.php    # the widget gallery (tabs of controls)
php examples/canvas.php     # custom 2D drawing — click-drag to paint
php examples/clock.php      # a timer-animated analogue clock
php examples/table.php      # a data grid
php examples/text.php       # attributed text
php examples/menu.php       # a menubar wired to dialogs

Composer scripts

composer test        # the full PHPUnit suite
composer gate        # @group gate — FFI::cdef accepts the generated header
composer smoke       # @group smoke — construct widgets, no event loop
composer stan        # PHPStan (level 6)
composer format      # Mago formatter
composer lint        # Mago linter
composer regen       # regenerate src/Native/libui.gen.h + src/Generated/** from ui.h
composer build-lib   # (re)build the native library
composer docs:api    # regenerate docs/API.md from the public API surface

Documentation

How it's built

A single generator parses libui-ng's ui.h (299 functions, ~98% regular naming) once and emits both the FFI header and the typed OO classes — "one parse, three tiers":

tools/generate.php  ──parses third_party/libui-ng/ui.h──▶
  src/Native/libui.gen.h      cleaned header, all 299 fns callable   (generated)
  src/Generated/<Widget>.php   23 typed widget classes               (generated)
  src/Generated/Enum/*.php      19 PHP enums + Flags/Modifiers        (generated)
  src/Generated/Ui.php          dialog facade (msgBox, openFile, …)   (generated)
  src/<Widget>.php              hand-editable sugar (extends Generated\<Widget>)
hand-written runtime + hard subsystems (never regenerated):
  src/Ffi.php  src/Control.php           singleton FFI, base class, callback retention
  src/Area.php src/Draw/*  src/Text/*    custom-draw surface + paths/brushes/text
  src/Table.php src/TableModel*.php       data grid (uiTableModelHandler vtable)

tools/annotations.php carries the ~2% the convention can't express. For the full design — the header transform, the runtime FFI rules, and the generated-vs-hand-written split — see docs/ARCHITECTURE.md and CONTRIBUTING.md.

License

MIT — see LICENSE. Bundles libui-ng (MIT); see THIRD_PARTY.md.