Skip to content

Commit ce7e94d

Browse files
Merge pull request #21 from developersharif/image-widget
Image widget updated with gif , jpg support
2 parents dfd35af + 74b6bc7 commit ce7e94d

29 files changed

Lines changed: 1389 additions & 463 deletions

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

docs/Image.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Image Widget
2+
3+
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`).
4+
5+
---
6+
7+
### Constructor
8+
9+
```php
10+
new Image(string $parentId, array $options)
11+
```
12+
13+
| Parameter | Type | Description |
14+
|-------------|----------|----------------------------------------------|
15+
| `$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.
19+
20+
---
21+
22+
### Options
23+
24+
| Key | Type | Description |
25+
|----------|----------|---------------------------------------------------------------------|
26+
| `path` | `string` | **Required.** Filesystem path to the image file. |
27+
| `bg` | `string` | Background color shown around the image. |
28+
| `relief` | `string` | Border style: `flat`, `raised`, `sunken`, `groove`, `ridge`. |
29+
| `padx` | `int` | Horizontal internal padding. |
30+
| `pady` | `int` | Vertical internal padding. |
31+
| `cursor` | `string` | Cursor shown when hovering the image. |
32+
33+
Any other key is forwarded as a Tk `-key value` pair on the underlying label.
34+
35+
---
36+
37+
### Supported formats
38+
39+
| Format | How |
40+
|-------------------|---------------------------------------------------------------------------------------|
41+
| PNG, GIF, PPM/PGM | Loaded directly by Tk's `image create photo`. |
42+
| 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']);
77+
$photo->pack();
78+
79+
$btn = new Button($window->getId(), [
80+
'text' => 'Load avatar',
81+
'command' => fn() => $photo->setPath('avatars/user-42.png'),
82+
]);
83+
$btn->pack();
84+
```
85+
86+
**Add a frame and padding:**
87+
```php
88+
$image = new Image($window->getId(), [
89+
'path' => 'screenshot.png',
90+
'relief' => 'sunken',
91+
'padx' => 8,
92+
'pady' => 8,
93+
'bg' => '#222',
94+
]);
95+
$image->pack(['padx' => 12, 'pady' => 12]);
96+
```
97+
98+
---
99+
100+
### Methods
101+
102+
| Method | Signature | Description |
103+
|---------------|----------------------------|-----------------------------------------------------------------------------|
104+
| `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. |
110+
| `pack()` | `(array $opts = []): void` | Inherited. Pack layout manager. |
111+
| `place()` | `(array $opts = []): void` | Inherited. Place layout manager. |
112+
| `grid()` | `(array $opts = []): void` | Inherited. Grid layout manager. |
113+
| `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.

docs/_sidebar.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- [Entry](Entry.md)
1414
- [Frame](Frame.md)
1515
- [Canvas](Canvas.md)
16+
- [Image](Image.md)
1617
- [Menu](Menu.md)
1718
- [Menubutton](Menubutton.md)
1819
- [Checkbutton](Checkbutton.md)

0 commit comments

Comments
 (0)