You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: CLAUDE.md
+52-26Lines changed: 52 additions & 26 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,51 +4,77 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
4
5
5
## Project Overview
6
6
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`).
8
13
9
14
## Commands
10
15
11
16
```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
17
19
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)
21
24
```
22
25
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).
`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)
28
47
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`.
30
49
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
32
51
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()`.
34
57
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/`)
36
59
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`|
| Windows |`windows/bin/tcl86t.dll`| (under `windows/`) |
38
65
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.
40
67
41
-
### Widget Hierarchy
68
+
### Namespace & autoload
42
69
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/`.
44
71
45
-
### Native Libraries
72
+
##Tests
46
73
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).
-`TestRunner::assertWidgetExists(".widgetPath", msg)` — checks via Tcl `winfo exists`
53
79
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.
The **Image** widget displays an image inside a parent window. Internally it is a Tk `label` whose `-image` is a Tk photo image loaded from disk, so it supports the same layout managers as any other widget (`pack`, `place`, `grid`).
|`$parentId`|`string`|`getId()` of the parent widget. |
16
+
|`$options`|`array`| Configuration options — see table below. `path` is required. |
17
+
18
+
Throws `InvalidArgumentException` if `path` is missing, and `RuntimeException` if the file does not exist or its extension is not a supported image format.
| JPEG, BMP | Transparently transcoded to a temp PNG via PHP's GD extension before being given to Tk. |
43
+
44
+
JPEG and BMP support requires `ext-gd` to be enabled (the default in most PHP builds). The transcoded PNG lives in `sys_get_temp_dir()` for as long as the widget exists and is unlinked automatically by `setPath()` and `destroy()`.
45
+
46
+
---
47
+
48
+
### Animated GIFs
49
+
50
+
Multi-frame GIFs play automatically. The widget parses the GIF for per-frame delays from the Graphic Control Extension blocks, then drives a Tcl `after`-based loop that swaps the photo's `-format "gif -index N"` on each tick. The loop runs entirely inside Tk's event loop (which `Application::run()` already pumps), so there is no PHP round-trip per frame.
51
+
52
+
```php
53
+
$loader = new Image($window->getId(), ['path' => 'assets/spinner.gif']);
54
+
$loader->pack();
55
+
56
+
$loader->isAnimated(); // true
57
+
$loader->getFrameCount(); // e.g. 12
58
+
```
59
+
60
+
The animation is cancelled automatically on `destroy()`, and on `setPath()` to either a non-GIF or a different GIF (a fresh loop is started for the new file). Frame delays under 20ms are clamped to 100ms to avoid busy-loops on GIFs that encode "0" to mean "as fast as possible".
61
+
62
+
---
63
+
64
+
### Examples
65
+
66
+
**Display a PNG:**
67
+
```php
68
+
use PhpGui\Widget\Image;
69
+
70
+
$logo = new Image($window->getId(), ['path' => __DIR__ . '/assets/logo.png']);
71
+
$logo->pack(['pady' => 20]);
72
+
```
73
+
74
+
**Swap the image at runtime:**
75
+
```php
76
+
$photo = new Image($window->getId(), ['path' => 'avatars/default.png']);
|`setPath()`|`(string $path): void`| Reloads pixels from a new file. Cancels any running animation and starts a new one if the file is an animated GIF. |
105
+
|`getPath()`|`(): string`| Returns the currently loaded image path (with normalized separators). |
106
+
|`getWidth()`|`(): int`| Width of the loaded image in pixels (`image width`). |
107
+
|`getHeight()`|`(): int`| Height of the loaded image in pixels (`image height`). |
108
+
|`getFrameCount()`|`(): int`| Total frames in the loaded image (1 for non-animated). |
109
+
|`isAnimated()`|`(): bool`| True when an animation loop is scheduled for this image. |
|`destroy()`|`(): void`| Removes the label **and** frees the underlying photo image from Tk's image table. |
114
+
115
+
---
116
+
117
+
### Notes
118
+
119
+
- Each `Image` instance owns its own Tk photo image (`phpgui_photo_<id>`). Always call `destroy()` when you replace or discard the widget — photo images are not garbage-collected with their containing label.
120
+
-`setPath()` reuses the same photo image, so any other label currently bound to it will also update.
121
+
- Paths containing spaces, brackets, or `$` are handled safely; the path is passed via a Tcl variable rather than interpolated into the command.
0 commit comments