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.
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.
$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
| Key | Meaning |
|---|---|
bg surface elevated | window, card and raised backgrounds |
text muted | body and secondary text |
accent onAccent | primary colour and the text on top of it |
border | outlines and dividers |
radius | corner radius, pixels |
font title | body and heading size, points |
row | list and popup row height |
dark | 1 for dark themes (drives native chrome) |
Universal style shortcuts
$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);
| Container | What 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
| Factory | Widget | Extras |
|---|---|---|
heading($text) / sub($text) / label($text) | Label | — |
button($text, $role, $onClick) | Button | onClick() |
link($text, $onClick) | Link | onClick() |
input($text = "") | Input | content() / setValue() / onChange() |
checkbox($text, $on = false) | Checkbox | checked() / setChecked() / toggle() |
list($items) / combo($items) | ListBox / ComboBox | add / addAll / items / count / clear / remove / selected / select / selectedText |
progress($pct = 0) | ProgressBar | progress($pct) / percent() |
slider($value = 0) | Slider | value($v) / current() / range($lo,$hi) / onChange($v) |
divider() | Divider | — |
Every widget shares the same methods, and they chain
$app->button("Danger", "danger")->radius(20)->accent(0xE040FB)->font(12, true);
Window
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:
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
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.
onClick() / onChange() take a
closure directly — $btn->onClick(function() { ... }) — and capture
anything with use.