Skip to content

Commit 0c6c6ac

Browse files
Merge branch 'image-widget'
2 parents dfd35af + 1dedc7f commit 0c6c6ac

77 files changed

Lines changed: 7706 additions & 942 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Changelog
2+
3+
All notable changes to **php-gui** are documented here.
4+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to semantic versioning.
5+
6+
---
7+
8+
## [v1.9] — 2026-04-26
9+
10+
A foundation release. Closes the four production blockers from the v1.0 plan and ships twelve new essential widgets. Tests grew from 159 → **453 passing assertions** across 30 files.
11+
12+
If you're upgrading from v1.8, the changes are **backwards-compatible at the public API level** — all v1.8 code continues to work, but you also get safer defaults, a faster event loop, and a much larger widget toolbox.
13+
14+
### Highlights
15+
16+
- **Twelve new widgets**`Text`, `Scrollbar`, `Listbox`, `Radiobutton` (with `RadioGroup`), `Scale`, `Spinbox`, `Progressbar`, `Notebook`, `Treeview`, `PanedWindow`, `LabelFrame`, `Separator`. The library now covers the full table-stakes set for desktop apps.
17+
- **Animated GIFs** — the `Image` widget now plays multi-frame GIFs with proper disposal and transparency compositing.
18+
- **JPEG + BMP via GD**`Image` transcodes formats Tk core can't read into temp PNGs.
19+
- **Arbitrary widget nesting** — Frames inside Frames inside Windows now work the way you'd expect.
20+
- **Tcl-injection eliminated** — every widget now safely quotes user-supplied strings.
21+
- **Event loop is in-process** — no more `/tmp/phpgui_callback.txt`, no 100 ms input lag, no cross-process collisions.
22+
23+
---
24+
25+
### Added — New widgets
26+
27+
#### `Text`
28+
Multi-line editor with safe value routing through Tcl variables, `setText` / `getText` / `append` / `insertAt` / `clear`, length and line counts, `setState('disabled')` for read-only log views (still accepts `append()`). Strict index validation on `insertAt()` blocks Tcl injection through index expressions.
29+
30+
#### `Scrollbar`
31+
Wraps `ttk::scrollbar` with manual `bindTo($target)` two-way wiring, plus a one-call `Scrollbar::attachTo($target, $orient)` factory that creates, binds, and packs in one shot. Both orientations.
32+
33+
#### `Listbox`
34+
Selectable list with all four Tk select modes (`browse`, `single`, `multiple`, `extended`). Full add/remove/replace/clear, all selection accessors, `<<ListboxSelect>>` virtual-event binding via `onSelect()`. Items round-trip Tcl-special characters literally.
35+
36+
#### `Radiobutton` + `RadioGroup`
37+
First-class `RadioGroup` object backing a single Tcl variable — solves the silent foot-gun where two radios accidentally share `-variable` names. Per-radio `command` callback receives the value; group-level `onChange` fires for both user clicks and programmatic `setValue()`.
38+
39+
#### `Scale`
40+
Slider input with `from`/`to`/`orient`, `getValue`/`setValue`, and `onChange` that fires uniformly for user drag and programmatic updates.
41+
42+
#### `Spinbox`
43+
Bounded numeric input or fixed enumeration via `values`. `onChange` is wired to `<Return>` and `<FocusOut>` in addition to Tk's `-command`, so direct typing fires the handler.
44+
45+
#### `Progressbar`
46+
Both determinate (`setValue` / `step`) and indeterminate (`start(intervalMs)` / `stop`) modes; `setMode()` switches at runtime; all inputs validated.
47+
48+
#### `Notebook`
49+
Tabbed container wrapping `ttk::notebook`. `addTab` / `selectTab` / `selectPage` / `setTabState` (`normal` / `disabled` / `hidden`); `onTabChange` bound to `<<NotebookTabChanged>>`. Enforces page-is-child invariant at `addTab()` time so the silent "empty pane" Tk failure mode can't happen.
50+
51+
#### `Treeview`
52+
Hierarchical list / multi-column table — the single biggest missing widget for desktop apps. Two operating modes:
53+
- Flat tables with named columns and headings
54+
- Hierarchies (folder trees, outlines)
55+
56+
Positional and keyed value APIs, full selection management (single + multi), `onSelect` and `onDoubleClick` (with click-coordinate-to-row resolution), column / heading configuration, `delete` cascades to descendants. All values go through safe-quoting; row IDs are deterministic and Tcl-safe.
57+
58+
#### `PanedWindow`
59+
Resizable split container; `addPane` with `weight`-based space distribution, `setSashPosition` / `getSashPosition`, both orientations.
60+
61+
#### `LabelFrame`
62+
Titled bordered container — a `Frame` whose top edge is broken by a small caption. Optional title (empty produces a plain bordered frame).
63+
64+
#### `Separator`
65+
Thin horizontal or vertical divider line.
66+
67+
---
68+
69+
### Added — Image widget improvements
70+
71+
- **Animated GIFs** — full multi-frame playback with disposal handling, on-load frame compositing onto a logical-screen-sized canvas, and per-frame snapshot pre-decoding so frame swaps never flash transparent gaps. New `getFrameCount()` / `isAnimated()`.
72+
- **JPEG + BMP support** — transparent transcoding through PHP's GD extension to a temp PNG before Tk loads it. Original path is preserved by `getPath()`; the temp PNG is unlinked automatically on `setPath()` and `destroy()`.
73+
- **Safe Tcl quoting** for all option values; paths route through Tcl variables so spaces, brackets, `$`, and quotes never break command parsing.
74+
75+
---
76+
77+
### Added — Window event hooks
78+
79+
- `Window::onClose(callable)` — runs your handler before the application exits. Returning `false` vetoes the close. Useful for "save unsaved changes?" prompts.
80+
- `Window::onResize(callable)` — fires `(int $width, int $height)` on actual resizes (descendant `<Configure>` events and same-size duplicates are filtered out).
81+
82+
---
83+
84+
### Added — Public API additions
85+
86+
- `AbstractWidget::tclQuote(string)` — central safe-quoting helper.
87+
- `AbstractWidget::buildOptionString(array $skip)` — replaces every per-widget option-formatter; routes every value through `tclQuote()`.
88+
- `AbstractWidget::getParentId()` / `getTclPath()` — used internally by factories like `Scrollbar::attachTo`, also useful for advanced custom widgets.
89+
- `ProcessTCL::drainPendingCallbacks()` — drains the new in-process callback queue (returns count for tests).
90+
- `ProcessTCL::shouldQuit()` — reads the new `::phpgui_quit` flag.
91+
- `ProcessTCL::unregisterCallback($id)` — releases a previously-registered callback.
92+
93+
---
94+
95+
### Changed
96+
97+
- **Event loop replaced**`/tmp/phpgui_callback.txt` polling is gone. Tcl-side handlers now `lappend ::phpgui_pending $id`; the PHP event loop drains that list every tick. Loop cadence dropped from 100 ms to 10 ms (~100 Hz responsiveness). Lost-callback bug fixed (queue holds an arbitrary number of IDs); cross-process collision fixed (no shared filesystem state).
98+
- **Quit signal**`/tmp/phpgui_quit.txt` removed; `::exit_app` now sets `::phpgui_quit 1`.
99+
- **`Application::quit()`** — no longer calls `exit(0)`. Flips the run flag and tears down WebViews, then `Application::run()` returns to the caller. You can finalise (logging, cleanup) before the script ends.
100+
- **Widget destruction**`AbstractWidget::destroy()` now also calls `unregisterCallback($this->id)`. `Menu::destroy()` cascades through its per-command callback IDs and submenus. `Window::destroy()` frees the `onClose` / `onResize` IDs.
101+
- **Widget nesting**`AbstractWidget` now tracks the full Tk path (`getTclPath()`) instead of assuming one level under root. Frames inside Frames inside Windows finally work as advertised.
102+
103+
---
104+
105+
### Fixed
106+
107+
- **Tcl injection across all widgets.** Before v1.9, a label whose text contained `Hello"; destroy .; "` would execute the embedded `destroy` command. Now every widget runs user-supplied strings through `tclQuote()` (or, where possible, sets a Tcl variable directly via FFI). Documented in `docs/security.md`. Regression suite in `tests/widgets_test/TclInjectionTest.php`.
108+
- **Lost callbacks.** Two events fired between event-loop ticks no longer collapse to one.
109+
- **Callback memory leaks.** Closures registered by destroyed widgets are now released.
110+
- **`docs/Frame.md` was aspirational.** The Tcl-path refactor makes its nesting examples actually work.
111+
- **Animated GIFs flickered transparent dots.** Pre-decoded frame snapshots remove the per-tick reload flash.
112+
- **GIF `setPath` after animation produced "couldn't recognize data" errors.** Reset `-format` to `{}` when swapping to non-GIFs.
113+
- **Spinbox `value` shorthand was clobbering `-values`.** Tk partial-matched `-value` to `-values` and overwrote the enumeration list.
114+
115+
---
116+
117+
### Documentation
118+
119+
New pages in `docs/`:
120+
121+
- `security.md` — Tcl-quoting policy and threat model.
122+
- `event-loop.md` — IPC redesign, test patterns, callback lifecycle.
123+
- `Image.md`, `Text.md`, `Scrollbar.md`, `Listbox.md`, `Radiobutton.md`, `Scale.md`, `Spinbox.md`, `Progressbar.md`, `Notebook.md`, `Treeview.md`, `PanedWindow.md`, `LabelFrame.md`, `Separator.md`.
124+
125+
`docs/Window.md` updated with `onClose` / `onResize` examples. `docs/_sidebar.md` updated to list every new page.
126+
127+
---
128+
129+
### Tests
130+
131+
- 159 → **453 passing assertions** across 30 files.
132+
- New suites: `EventLoopTest`, `ImageTest`, `LabelFrameTest`, `ListboxTest`, `NestingTest`, `NotebookTest`, `PanedWindowTest`, `ProgressbarTest`, `RadiobuttonTest`, `ScaleTest`, `ScrollbarTest`, `SeparatorTest`, `SpinboxTest`, `TclInjectionTest`, `TextTest`, `TreeviewTest`, `WindowEventsTest`.
133+
134+
Run any suite with `php tests/widgets_test/<Name>Test.php` — exits non-zero on failure.
135+
136+
---
137+
138+
### Upgrade notes
139+
140+
No breaking changes. If you were relying on the temp-file behaviour (`/tmp/phpgui_callback.txt`, `/tmp/phpgui_quit.txt`) from outside the library, those files no longer exist. If you called `Application::quit()` and counted on `exit(0)`, add an explicit `exit;` at the end of your script.
141+
142+
---
143+
144+
## [v1.8] — earlier
145+
146+
See `git log v1.7..v1.8`.
147+
148+
## Earlier releases
149+
150+
Earlier release tags (`v1.0` through `v1.7`) are in the git history.

CLAUDE.md

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,51 +4,77 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Project Overview
66

7-
PHP GUI library providing cross-platform desktop GUI development using Tcl/Tk via PHP's FFI extension. Requires PHP 8.1+ with `ext-ffi`.
7+
PHP GUI library for cross-platform desktop apps. Two rendering modes share one event loop:
8+
9+
- **Native widgets** — Tcl/Tk loaded via PHP FFI (forms, dialogs, system controls).
10+
- **WebView** — HTML/CSS/JS frontend driven by a bundled `webview_helper` subprocess (Tauri-like, with a JS↔PHP bridge).
11+
12+
Requires PHP 8.1+ with `ext-ffi` (`ffi.enable=true` in `php.ini`). Native libraries for both modes are bundled under `src/lib/` — no system packages required on Linux/macOS/Windows (Linux WebView still needs `libwebkit2gtk-4.1-dev`).
813

914
## Commands
1015

1116
```bash
12-
# Install dependencies
13-
composer install
14-
15-
# Run the example app
16-
php example.php
17+
composer install # install (no runtime deps; sets up autoload)
18+
php example.php # run example app
1719

18-
# Run tests (no test framework — plain PHP scripts)
19-
php tests/index_test.php
20-
php tests/widgets_test/WindowTest.php
20+
# Tests — plain PHP scripts, no PHPUnit. Each script exits 1 on failure.
21+
php tests/index_test.php # integration smoke test
22+
php tests/widgets_test/WindowTest.php # single widget suite
23+
php tests/webview/WebViewWidgetTest.php # webview suite (needs helper binary)
2124
```
2225

23-
There is no PHPUnit, linter, or build system configured.
26+
There is no linter, build system, or PHPUnit. Tests use the in-repo `tests/TestRunner.php` (suite + `assert*` helpers, summary on exit).
2427

2528
## Architecture
2629

27-
**FFI Bridge Pattern**: PHP classes → `ProcessTCL` (FFI singleton) → native Tcl/Tk C library.
30+
### Two processes, one event loop
31+
32+
```
33+
PHP process ──FFI──▶ libtcl/libtk (native widgets, in-process)
34+
35+
└──proc_open──▶ webview_helper (separate native window, JSON-over-stdio IPC)
36+
```
37+
38+
`Application::run()` is the single event loop driving both: it calls Tcl `update`, polls callback temp files, and pumps stdin/stdout for any registered WebView helpers.
39+
40+
### Native-widget path
41+
42+
- **`ProcessTCL`** (`src/ProcessTCL.php`) — FFI singleton. Loads the platform-specific Tcl shared library from `src/lib/`, executes Tcl commands, and owns the callback registry (unique ID → PHP closure).
43+
- **`AbstractWidget`** (`src/Widget/AbstractWidget.php`) — base for all Tcl/Tk widgets. Generates IDs via `uniqid()`, manages parent paths, exposes `pack()` / `grid()` / `place()`, converts PHP option arrays into Tcl option strings.
44+
- **Widget hierarchy**`Window` and `TopLevel` are root (null parent). All others (`Button`, `Label`, `Input`/`Entry`, `Frame`, `Menu`, `Canvas`, `Checkbutton`, `Combobox`, `Menubutton`, `Message`, `Image`) require a parent widget ID. `Input` wraps `Entry`.
45+
46+
#### Tcl callback bridge (the central pattern)
2847

29-
### Core Components
48+
When a Tcl event fires, the bound Tcl command writes the callback ID to `/tmp/phpgui_callback.txt`. The event loop tails this file, looks up the ID in `ProcessTCL`'s registry, and invokes the PHP closure. A second temp file (`/tmp/phpgui_quit.txt`) signals quit. **Every interactive widget (Button `command`, Input `onEnter`, Menu commands, etc.) goes through this file-based bridge** — when adding a new event type, follow the same pattern and register via `ProcessTCL`.
3049

31-
- **`ProcessTCL`** — Singleton that loads the native Tcl library via FFI and executes Tcl commands. Handles platform-specific library paths (`.so`, `.dll`, `.dylib`). Manages a callback registry mapping unique IDs to PHP closures.
50+
### WebView path
3251

33-
- **`Application`** — Event loop. Initializes Tcl/Tk, then continuously calls `update` while polling temp files for callback triggers (`/tmp/phpgui_callback.txt`) and quit signals (`/tmp/phpgui_quit.txt`).
52+
- **`ProcessWebView`** (`src/ProcessWebView.php`) — analogous to `ProcessTCL`, but spawns the `webview_helper` binary via `proc_open()` and speaks newline-delimited JSON on stdin/stdout. Non-blocking reads buffered through a single read buffer.
53+
- **`Widget\WebView`** (`src/Widget/WebView.php`) — does **not** extend `AbstractWidget` (it owns a separate OS window, not a Tcl/Tk widget). Registered with `Application::addWebView()` so the event loop pumps it.
54+
- **Helper binary** — prebuilt per platform under `src/lib/`: `webview_helper_linux_x86_64`, `webview_helper` (macOS), and a Windows variant. Wraps WebKitGTK / WKWebView / WebView2 with a uniform JSON protocol. Installed/copied by `src/Install/LibraryInstaller.php` and `scripts/install-webview-helper.php`.
55+
- **JS↔PHP bridge**`WebView::bind($name, $cb)` exposes PHP to JS as `invoke(name, ...args)`; `WebView::emit($event, $data)` pushes events to the page (`onPhpEvent(name, cb)` on the JS side). Both flow over the same JSON IPC channel.
56+
- **Frontend serving**`serveFromDisk($dir)` registers a custom URI scheme per platform (`phpgui://` on Linux, `https://phpgui.localhost/` via virtual host on Windows, `loadFileURL:allowingReadAccess:` on macOS). `serveVite($distDir)` auto-detects a running Vite dev server (HMR) vs. production build. `enableFetchProxy()` routes `fetch()` calls through PHP to bypass CORS on `phpgui://`/`file://` origins — must be called **before** `serveFromDisk()` / `serveVite()`.
3457

35-
- **`AbstractWidget`** — Base class for all widgets. Assigns unique IDs via `uniqid()`, manages parent-child relationships, provides layout methods (`pack()`, `grid()`, `place()`), and converts PHP option arrays to Tcl option strings.
58+
### Native libraries (`src/lib/`)
3659

37-
### Event Handling
60+
| Platform | Tcl/Tk | WebView helper |
61+
|---|---|---|
62+
| Linux x86-64 | `libtcl8.6.so`, `libtk8.6.so` | `webview_helper_linux_x86_64` |
63+
| macOS | `libtcl9.0.dylib`, `libtk9.0.dylib`, `libtommath.1.dylib` | `webview_helper` |
64+
| Windows | `windows/bin/tcl86t.dll` | (under `windows/`) |
3865

39-
Callbacks use a temp-file bridge: when a Tcl event fires, it writes a callback ID to `/tmp/phpgui_callback.txt`. The `Application` event loop detects this, looks up the registered PHP closure, and executes it. This is the central pattern — all interactive widgets (Button, Input, Menu) use this mechanism.
66+
Linux Tcl libs are rebuilt against an older glibc via `build/rebuild-linux-libs.dockerfile` to stay compatible with glibc 2.34+. Don't replace these by-hand — use the dockerfile.
4067

41-
### Widget Hierarchy
68+
### Namespace & autoload
4269

43-
All widgets extend `AbstractWidget`. `Window` and `TopLevel` are root-level (null parent). All others (Button, Label, Input, Frame, Menu, Canvas, etc.) require a parent widget. `Input` is an alias/wrapper around `Entry`.
70+
PSR-4: `PhpGui\``src/`. Widgets under `PhpGui\Widget\`. Test helper under `PhpGuiTest\``tests/`.
4471

45-
### Native Libraries
72+
## Tests
4673

47-
Pre-compiled Tcl libraries are bundled in `src/lib/`:
48-
- Linux: `libtcl8.6.so`
49-
- macOS: `libtcl9.0.dylib`
50-
- Windows: `windows/bin/tcl86t.dll`
74+
Each test file is standalone: `require` `vendor/autoload.php` and `TestRunner.php`, call `TestRunner::suite('Name')`, run assertions, end with `TestRunner::summary()` (which `exit(1)`s on any failure).
5175

52-
### Namespace & Autoloading
76+
Common assertions:
77+
- `TestRunner::assert(bool, msg)` / `assertEqual(expected, actual, msg)`
78+
- `TestRunner::assertWidgetExists(".widgetPath", msg)` — checks via Tcl `winfo exists`
5379

54-
PSR-4: `PhpGui\``src/`. Widgets are under `PhpGui\Widget\`.
80+
Callbacks can be triggered without a real GUI event by calling `ProcessTCL::getInstance()->executeCallback($widgetId)` directly (see `tests/index_test.php` for the pattern). WebView tests under `tests/webview/` exercise the helper subprocess and IPC; they require the helper binary to be present.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ Native widgets render as real OS controls using Tcl/Tk under the hood. The PHP A
115115
| `Menubutton` | Standalone menu button | [](docs/Menubutton.md) |
116116
| `Canvas` | Drawing surface for shapes and images | [](docs/Canvas.md) |
117117
| `Message` | Multi-line text display | [](docs/Message.md) |
118-
| `Image` | Display images inside windows | |
118+
| `Image` | Display images inside windows | [](docs/Image.md) |
119119

120120
### Layout
121121

assets/54396379.png

7.77 KB
Loading

assets/example.png

504 Bytes
Loading

assets/happy-cat.gif

1.05 MB
Loading

0 commit comments

Comments
 (0)