ui.hphp 2.0

A small, beautiful desktop toolkit. One theme restyles the whole window, widgets place themselves in a column or a row, and everything is owner-drawn so it looks the same on every machine. No dependencies: hphp build links the whole Win32 layer into one exe.

stdlib — embedded Win32 native themed auto-layout chainable

Hello GUI

import "ui";

$app = new App("My App");
$app->heading("Hello, world");
$app->sub("This is HolyPHP UI 2.0.");
$app->button("Click me", "primary", function() use ($app) {
    $app->msg("It works.");
});
$app->run();

No coordinates. The heading, the subtitle and the button stack themselves, and every one of them follows the same theme.

Themes

A theme is a universal style: colours, corner radius, type size, row height. Change it at any time — every widget follows immediately.

void theme(string|map $which, map $over = null)
$app->theme("dark");        // dark (default)
$app->theme("light");
$app->theme("midnight");
$app->theme("ocean");
$app->theme("forest");
$app->theme("rose");
$app->theme("mono");

// override single keys, on top of a preset …
$app->theme("dark", ["accent" => 0xFF5722]);

// … or roll a whole theme from scratch
$app->theme(["bg" => 0x101014, "surface" => 0x171A21,
               "text" => 0xE8EAF0, "accent" => 0x7C5CFF,
               "dark" => 1]);

Theme keys

KeyMeaning
bg surface elevatedwindow, card and raised backgrounds
text mutedbody and secondary text
accent onAccentprimary colour and the text on top of it
borderoutlines and dividers
radiuscorner radius, pixels
font titlebody and heading size, points
rowlist and popup row height
dark1 for dark themes (drives native chrome)

Universal style shortcuts

App pad(int $px) // window padding + widget padding App gap(int $px) // space between widgets App radius(int $px) // corner radius everywhere App font(int $pt, bool $bold = false) App style(string $role) // default button role
$app->theme("midnight")->pad(28)->gap(12)->radius(12)->font(11);

Roles

Six ready-made button styles. Pass one to a factory, or make it the default for everything that follows.

$app->button("Save");                 // subtle (the default)
$app->button("Save", "primary");       // filled with the accent colour
$app->button("Delete", "danger");
$app->button("Done", "success");
$app->button("More", "ghost");         // no fill until hovered
$app->link("Read the docs");           // a text link

$app->style("primary");                 // every button from here on

Layout

The window stacks its children top to bottom. To put things side by side, put them inside a row() — a row lays its children out across and wraps when it runs out of room. Every container grows to fit what you put inside it.

$cols = $app->row();
$left  = $cols->column(400);
$right = $cols->column(400);

$form = $left->card(400);          // rounded surface
$form->label("Email");
$form->input("you@example.com");
$form->checkbox("Remember me", true);
$form->button("Sign in", "primary", $handler);

$sec = $right->section("Preferences", 400);   // captioned outline
$sec->slider(60);
$sec->progress(35);
ContainerWhat it is
column($w = 0)vertical box
row($w = 0)horizontal box, wraps
card($w = 0, $h = 0)rounded surface, auto height
section($caption, $w = 0, $h = 0)captioned outline
panel($x, $y, $w, $h)free-form, absolute

Absolute placement still works and never disturbs the flow:

$app->button("Reset", "ghost")->move(20, 20, 90, 0);

Widgets

FactoryWidgetExtras
heading($text) / sub($text) / label($text)Label—
button($text, $role, $onClick)ButtononClick()
link($text, $onClick)LinkonClick()
input($text = "")Inputcontent() / setValue() / onChange()
checkbox($text, $on = false)Checkboxchecked() / setChecked() / toggle()
list($items) / combo($items)ListBox / ComboBoxadd / addAll / items / count / clear / remove / selected / select / selectedText
progress($pct = 0)ProgressBarprogress($pct) / percent()
slider($value = 0)Slidervalue($v) / current() / range($lo,$hi) / onChange($v)
divider()Divider—

Every widget shares the same methods, and they chain

UIObject text(string $t) string content() UIObject setValue(string $v) UIObject show() / hide() bool enabled() / disable() UIObject move(int $x, int $y, int $w = 0, int $h = 0) UIObject size(int $w, int $h) UIObject focus() array rect() // [x, y, w, h] as painted, in window coordinates UIObject font(int $pt, bool $bold = false) UIObject color(int $rgb) / bg(int $rgb) / accent(int $rgb) UIObject radius(int $px) UIObject style(string $role) UIObject onClick(fn) / onChange(fn) / onKey(fn)
$app->button("Danger", "danger")->radius(20)->accent(0xE040FB)->font(12, true);

Window

void title(string $t) / size(int $w, int $h) / pos(int $x, int $y) bool alive() void run() // blocking event loop void tick(int $ms = 15) // pump events once, keep your own loop void close() / quit() void every(int $ms, fn) void onClose(fn) // return false to veto void onKey(fn) // fn(int $vk, int $char)

Scrolling

A window whose content is taller than its client area scrolls with the mouse wheel, PageUp/PageDown and Ctrl+Home/End. A script can also drive it:

array scroll() // [offset, contentHeight, viewportHeight] App scrollBy(int $dy) / scrollTo(int $y) int scrollOffset() / contentHeight()
Tip: run() blocks like a normal desktop app (best with hphp build). tick() returns every ~15 ms so your script keeps control and console output stays alive (best with hphp run).

Menus

$file = $app->menu("File");
$file->item("Open", function() use ($app) {
    $f = $app->openFile("Text|*.txt;HolyPHP|*.hphp");
    if ($f != "") { $app->msg("Opened " . $f); }
});
$file->separator();
$file->item("Quit", function() use ($app) { $app->close(); });

Dialogs & system

void msg(string $text, string $title = "HolyPHP") void warn(string $text, string $title = "Warning") void error(string $text, string $title = "Error") bool ask(string $text, string $title = "Question") string openFile(string $patterns = "All files|*.*") // "" = cancelled string saveFile(string $patterns) string folder() int color(int $initial = 0x4C7DFF) // 0xRRGGBB, null = cancelled void clip(string $text) string paste()

Timers

$ticks = 0;
$app->every(400, function() use (&$ticks, $bar, $label) {
    $ticks = $ticks + 1;              // by-ref capture
    $pct = ($ticks * 5) % 100;
    $bar->progress($pct);
    $label->text("{$pct}% complete");   // repaints one widget
});

Colours

Colours are plain 0xRRGGBB integers everywhere.

$app->button("Paint", "", 20, 20)->bg(0x1E88E5)->color(0xFFFFFF);

Two helpers build them: ui_rgb($r, $g, $b) packs channels and ui_channel($rgb, $i) reads one back.

Full tour

examples/ui_demo.hphp is a complete app — themed header, a role row, two columns of forms and lists, meters, menus, dialogs and a live timer. examples/ui_gallery.hphp shows every widget in every theme.

hphp build examples/ui_demo.hphp -o ui_demo.exe
hphp run   examples/ui_gallery.hphp

How it works

The Win32 layer lives in the runtime (hphp_ui.c): a handle table, a window proc that routes events to HolyPHP closures, and guarded callback invocation — an exception inside a handler prints and keeps the app alive instead of unwinding through C. Every control is owner-drawn from one paint routine, so the theme is the single source of truth. Callbacks run on the GUI thread; long work belongs in a timer.

Tip: onClick() / onChange() take a closure directly — $btn->onClick(function() { ... }) — and capture anything with use.