helgesverre / libui
Native desktop GUI for PHP 8.5+ via libui-ng and FFI.
Requires
- php: >=8.5
- ext-ffi: *
Requires (Dev)
- carthage-software/mago: ^1.56
- phpstan/phpstan: ^2.3
- phpunit/phpunit: ^13.4
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Native desktop GUIs in PHP — a typed, object-oriented binding to
libui-ngdriven 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.
See the changelog for release notes.
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
…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
Dialogsfacade so you don't repeat the window on every call. - Safe menus — a
Menubuilder that enforces libui's "menus before the first window" rule (throws instead of silently breaking) and cleanonClick()handlers. - Typed colours — one
Colorvalue type (Color::rgb(0x312B90),::hex(),::rgba(),::rgb255(), named) accepted across brushes, text and colour buttons. - Custom 2D drawing — vector
Paths (withline/circle/arc/roundedRectsugar), solid/gradientBrushes with typedStops, a fluentStrokeParamsbuilder, affineMatrix, clipping, andDrawContextfill/stroke shape helpers.Areas also supportscrollTo()andbeginUserWindowMove()/beginUserWindowResize()for scrollable canvases and borderless window dragging. - Attributed text —
AttributedStringwith per-range colour/weight/italic/ underline attributes, aFontDescriptor, and a drawableTextLayout; plus aRichTextbuilder +TextStylevalue object for one-call styled text. OpenType features (ligatures, small-caps, …) are exposed viaText\OpenTypeFeaturesand aFeaturesattribute. - Data-grid table —
Table::fromRows()/fromAssoc()for static data (no delegate), or aTableModelDelegatefor dynamic data; checkbox/progress/button/ image columns, row-index click callbacks, sortable headers (onHeaderClicked()+setSortIndicator()), and automatic model lifetime. - Images —
Image::fromPng()(via GD) orImage::fromRgba()for table image columns and area drawing. - Form binding — input widgets implement
HasValue, andForm::values()/setValues()read and write a whole form at once, keyed by field label. - App lifecycle —
Appfacade for multi-window apps + anonShouldQuit()handler, orWindow::run()for one-call single-window apps. Windows also exposegetContentSize()andgetPosition(). - Declarative layout — a
Buildfacade (Build::vbox(),hbox(),form(),window(),stretchy()) composes a whole layout in one expression when you prefer that over chained setters. - Testable — a
Testing\CallbackSpyandTesting\Inspectharness let you assert on widget state and callback invocations without spinning up the event loop. - Async helpers — the
Loopclass (defer/delay/repeat/cancel) over libui's native event loop, plus rawqueueMain(),timer(),onShouldQuit(). - Clipboard —
Utils\Clipboardcross-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:
uiControlis the base class,uiAreais the drawing adapter, anduiTableis the data grid. - Custom drawing covers paths (incl.
line/circle/arc/roundedRect/bézier sugar), solid & gradient brushes with typedStops, a fluentStrokeParamsbuilder,DrawContextshape helpers, stroking, clipping and transforms; attributed text has a full sugar layer. Imagehas a sugar class (fromPng/fromRgba); tables (factories, typed columns, row-index callbacks, automatic model lifetime), theApp/Looplifecycle, menus/dialogs, andClipboardall have typed helpers too.- Still raw-only (no sugar yet): OpenGL areas, and a synchronous yes/no
confirm()dialog (libui has no native modal — useUi::msgBoxor 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
uiAttributememory-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
- docs/GUIDE.md — task-by-task user guide and gotchas.
- docs/API.md — generated API reference (
composer docs:api). - docs/ARCHITECTURE.md — how the binding and generator work.
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.





