@gjsify/node-gi 0.13.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +818 -5
- package/binding.gyp +55 -19
- package/cairo.d.ts +187 -0
- package/cairo.js +570 -0
- package/gettext.d.ts +65 -0
- package/gettext.js +128 -0
- package/gi.d.ts +178 -0
- package/gi.js +2018 -23
- package/globals.d.ts +7 -33
- package/globals.js +40 -51
- package/gtk-runtime.d.ts +17 -0
- package/gtk-runtime.js +245 -0
- package/index.d.ts +306 -12
- package/index.js +447 -14
- package/overrides/_signals.js +167 -0
- package/overrides/gio-dbus.js +749 -0
- package/overrides/mainloop.js +60 -0
- package/package.json +45 -4
- package/src/addon.cc +98 -2074
- package/src/cairo.cc +926 -0
- package/src/calls.cc +1425 -0
- package/src/class.cc +1170 -0
- package/src/common.h +624 -0
- package/src/loop.cc +766 -0
- package/src/marshal.cc +1738 -0
- package/src/object.cc +814 -0
- package/src/private.cc +351 -0
- package/src/repo.cc +297 -0
- package/src/signals.cc +535 -0
- package/src/template.cc +116 -0
- package/src/toggle.cc +718 -0
- package/src/variant.cc +587 -0
- package/system.d.ts +49 -0
- package/system.js +116 -0
package/README.md
CHANGED
|
@@ -9,11 +9,34 @@ It loads `gi://` namespaces (GLib, GObject, Gio, …) via `libgirepository` and
|
|
|
9
9
|
exposes them with GJS-compatible semantics, so the same source builds and runs
|
|
10
10
|
on both GJS and Node via `gjsify build --app {gjs,node}`.
|
|
11
11
|
|
|
12
|
+
> **Status: product (Tier 2 —
|
|
13
|
+
> [ADR 0005](../../../docs/adr/0005-node-gi-scope.md)).** node-gi graduated
|
|
14
|
+
> from Tier 3 on 2026-07-14 once the four gate items landed: the
|
|
15
|
+
> toggle-ref/multi-env teardown crash fixed, vfunc OUT/INOUT chain-up, the
|
|
16
|
+
> GTK/Cairo layer, and a second real consumer (`@gjsify/sqlite`'s suite runs on
|
|
17
|
+
> node-gi). Best-effort now: tested, released on the train, breaking changes ship
|
|
18
|
+
> with a minor + changelog note. **Dependency isolation is still an invariant:**
|
|
19
|
+
> no Tier-1/2 `@gjsify/*` package may take a hard dependency (`dependencies` /
|
|
20
|
+
> `optionalDependencies`) on `@gjsify/node-gi` — the reverse bridge would double
|
|
21
|
+
> the runtime test matrix. The sanctioned seams stay a devDependency
|
|
22
|
+
> (`--runtime node` dev flows, the `@gjsify/sqlite` consumer) and the conditional
|
|
23
|
+
> `--app node` build injection — enforced by `scripts/audit-runtimes.mjs` in CI.
|
|
24
|
+
|
|
12
25
|
> **Status: milestone 1 (headless core) — in progress.** The native engine over
|
|
13
26
|
> the modern `girepository-2.0` API now does: resolve the default repository,
|
|
14
27
|
> `require` a namespace, enumerate its infos; call namespace-level functions and
|
|
15
28
|
> **instance methods** (own + implemented-interface methods, up the parent chain)
|
|
16
|
-
> with value marshalling (numbers, booleans, strings, GObjects, enums/flags)
|
|
29
|
+
> with value marshalling (numbers, booleans, strings, GObjects, enums/flags),
|
|
30
|
+
> including **OUT and INOUT parameters** surfaced per the GJS return-tuple
|
|
31
|
+
> convention (`[returnValue?, ...outArgs]` — one value bare, several as an Array),
|
|
32
|
+
> containers, struct OUT params and **caller-allocates OUT structs** — boxed
|
|
33
|
+
> (incl. the GValue auto-unbox) AND plain non-boxed C structs (the engine
|
|
34
|
+
> g_malloc0's the struct, the callee fills it in place, JS gets a field-readable
|
|
35
|
+
> handle that owns the storage — e.g. the `PangoRectangle`s of
|
|
36
|
+
> `PangoLayout.get_pixel_extents()`, the canvas2d `measureText` path). A JS
|
|
37
|
+
> `Uint8Array` (or `Buffer`/`DataView`/`ArrayBuffer`) passed where a **`GLib.Bytes`
|
|
38
|
+
> IN-arg** is expected is copied into a fresh GBytes and released per transfer
|
|
39
|
+
> after the call, exactly as GJS (`GdkPixbuf.Pixbuf.new_from_bytes(pixels, …)`);
|
|
17
40
|
> construct GObjects and read/write properties (GValue round-trip); connect /
|
|
18
41
|
> emit / disconnect signals (incl. detailed names like `notify::prop`); and
|
|
19
42
|
> **register GObject subclasses** (subtype + construct-by-type, inheriting the
|
|
@@ -25,11 +48,20 @@ on both GJS and Node via `gjsify build --app {gjs,node}`.
|
|
|
25
48
|
> access, `action.get_name()` methods, `.connect()/.emit()/.disconnect()`, and
|
|
26
49
|
> enums / flags / constants (`Gio.BusType.SESSION`, `GLib.PRIORITY_DEFAULT`);
|
|
27
50
|
> constructor/static methods (`Gio.File.new_for_path(...)`); and both snake_case
|
|
28
|
-
> and camelCase accessors.
|
|
51
|
+
> and camelCase accessors. The L1 layer also surfaces a **GJS-shaped
|
|
52
|
+
> `GObject.registerClass(meta, class)` decorator** (with `GObject.ParamSpec` /
|
|
53
|
+
> `ParamFlags` / `SignalFlags`): a JS `class extends GObject.Object { … }` with
|
|
54
|
+
> `Properties` / `Signals` / `vfunc_*` methods becomes a constructor whose
|
|
55
|
+
> instances carry both the user methods and the GObject property/signal surface.
|
|
56
|
+
> A **libuv↔GLib mainloop bridge** (`startMainLoop`,
|
|
29
57
|
> auto-attached by `requireGi`) nests Node's libuv loop inside the GLib loop, so a
|
|
30
58
|
> blocking `GLib.MainLoop.run()` keeps Node's timers/I/O alive — including the
|
|
31
59
|
> **boxed/struct slice** that needs (`GLib.MainLoop.new(...)` → a boxed handle →
|
|
32
|
-
> `.run()`/`.quit()`).
|
|
60
|
+
> `.run()`/`.quit()`). The same call arms the **uv-driven auto-pump** for the
|
|
61
|
+
> NON-blocking case: pending GLib sources (Gio async completions, GLib
|
|
62
|
+
> timeouts/idles, DBus) dispatch from Node's own event loop, so a plain
|
|
63
|
+
> `node bundle.mjs` that `await`s a Gio async op needs no explicit mainloop —
|
|
64
|
+
> matching GJS, where the GLib loop is the process loop. **JS functions marshal as GI callbacks** via an ffi
|
|
33
65
|
> closure (`GLib.timeout_add`/`idle_add` fire from the loop, the boolean return
|
|
34
66
|
> drives source continuation; the hidden user_data/destroy slots are auto-filled).
|
|
35
67
|
> register GObject subclasses (subtype + construct-by-type, inheriting the
|
|
@@ -59,17 +91,168 @@ vendored as-is — gjsify ships its own dual (GJS + Node) example/test infra.
|
|
|
59
91
|
(Fedora: `glib2-devel gobject-introspection-devel gcc-c++`;
|
|
60
92
|
Debian/Ubuntu: `libglib2.0-dev libgirepository-2.0-dev g++`)
|
|
61
93
|
- At runtime, the target libraries' typelibs must be installed (same as `gi://`
|
|
62
|
-
under GJS)
|
|
94
|
+
under GJS) — **except on macOS arm64**, where the optional
|
|
95
|
+
[`@gjsify/gtk-runtime-darwin-arm64`](../gtk-runtime-darwin-arm64) bundle ships a
|
|
96
|
+
relocated GTK/GI runtime so the display-free surface works with no Homebrew GTK
|
|
97
|
+
(Phase 2). node-gi auto-detects the bundle at load and prepends its typelib dir
|
|
98
|
+
to the GIRepository search path (`gtk-runtime.js`).
|
|
99
|
+
- **Node.js ≥ 20, Bun ≥ 1.3, or Deno ≥ 2** — the addon is Node-API, so one binary
|
|
100
|
+
runs on all three (see [Runtimes](#runtimes-node--bun--deno)).
|
|
101
|
+
|
|
102
|
+
## Runtimes (Node / Bun / Deno)
|
|
103
|
+
|
|
104
|
+
The engine is a **Node-API** addon, so the same binary loads and runs on **Node,
|
|
105
|
+
Bun and Deno** — Node-API is their common native ABI (no separate bindings, no Rust
|
|
106
|
+
rewrite). `index.js` detects the runtime and prefers a shipped
|
|
107
|
+
`prebuilds/<platform>-<arch>/node_gi.node` over a local `build/` (Deno runs no
|
|
108
|
+
postinstall build, so a prebuild is its only install path — stage one with
|
|
109
|
+
`npm run build:prebuild`).
|
|
110
|
+
|
|
111
|
+
Two libuv-coupled subsystems are portable across all three:
|
|
112
|
+
|
|
113
|
+
- **GC bridge** — the toggle-ref teardown drain uses a `napi_threadsafe_function`
|
|
114
|
+
(core Node-API), not node-gtk's raw `uv_async_t` (Deno exports no libuv symbols;
|
|
115
|
+
Bun panics on `uv_async_init`).
|
|
116
|
+
- **Main loop** — Node runs BOTH directions: the uv-nesting bridge co-pumps
|
|
117
|
+
Node's own event loop during a blocking GLib loop, and the **uv-driven
|
|
118
|
+
auto-pump** dispatches pending GLib sources from Node's loop when NO blocking
|
|
119
|
+
GLib loop runs (async Gio + `await` just works, no explicit loop). Bun/Deno
|
|
120
|
+
co-pump the non-blocking case by hand, iterating the default GLib context from
|
|
121
|
+
a runtime timer (`startMainContextPump` from `@gjsify/node-gi/gi`), so GIO
|
|
122
|
+
async callbacks / GLib timeouts / DBus fire while the runtime's own loop stays
|
|
123
|
+
live.
|
|
124
|
+
|
|
125
|
+
| Capability | Node | Bun | Deno |
|
|
126
|
+
|---|:--:|:--:|:--:|
|
|
127
|
+
| introspection, marshalling, enums, variants | ✅ | ✅ | ✅ |
|
|
128
|
+
| GObject create / properties / signals | ✅ | ✅ | ✅ |
|
|
129
|
+
| `registerClass` / subclass / vfunc chain-up | ✅ | ✅ | ✅ |
|
|
130
|
+
| toggle-ref GC + cross-thread teardown | ✅ | ✅ | ✅ |
|
|
131
|
+
| GLib async with NO mainloop (timeouts, GIO async, DBus) | ✅ auto | via `startMainContextPump` | via `startMainContextPump` |
|
|
132
|
+
| blocking `GLib.MainLoop.run()` / `Gio.Application.runAsync()` | ✅ | ✅ | ✅ |
|
|
133
|
+
| Promise continuations drain **during** a blocking GLib loop (async DBus replies, `await` chains) | ✅ | ✅ | ✅ |
|
|
134
|
+
| Node timers/I/O alive **during** a blocking GLib loop (uv co-pump) | ✅ | — | — |
|
|
135
|
+
|
|
136
|
+
Bun reaches full parity with Node on the core surface; Deno passes the same
|
|
137
|
+
conformance subset since 2.9 (the marshalling/async N-API quirks that excluded
|
|
138
|
+
`arrays`/`async-error` on Deno 2.1 are fixed upstream) — the Node-only co-pump
|
|
139
|
+
cases are the remaining, by-design gap. The authoritative full suite runs on Node.
|
|
140
|
+
|
|
141
|
+
Promise draining during a blocking loop is cross-runtime because the engine runs
|
|
142
|
+
a **microtask checkpoint at every outermost loop-dispatched GLib→JS callback
|
|
143
|
+
boundary**: Node's `napi_make_callback` performs it natively; Bun's and Deno's
|
|
144
|
+
do not, so on those runtimes the engine invokes the runtime's own drain
|
|
145
|
+
primitive (`bun:jsc` `drainMicrotasks` / Deno `core.runMicrotasks`), registered
|
|
146
|
+
at addon load (`src/loop.cc` `NodeGiMaybeDrainMicrotasks`). Without it, an
|
|
147
|
+
**async** (Promise-returning) DBus method handler exported via
|
|
148
|
+
`Gio.DBusExportedObject.wrapJSObject` never sent its reply on Bun/Deno while a
|
|
149
|
+
blocking `run()` owned the thread — the client timed out — while sync handlers
|
|
150
|
+
worked (regression: `test/dbus-async.test.mjs`). Runtime *timers/I/O* during a
|
|
151
|
+
blocking loop remain Node-only (the uv co-pump).
|
|
63
152
|
|
|
64
153
|
## Build & test
|
|
65
154
|
|
|
66
155
|
```bash
|
|
67
156
|
npm install # builds the native addon via node-gyp (install script)
|
|
68
|
-
npm test # node --test (
|
|
157
|
+
npm test # node --test (full suite, Node — authoritative)
|
|
158
|
+
npm run test:gc # node --test --expose-gc (toggle-ref GC-stress leg)
|
|
159
|
+
npm run test:bun # conformance subset on Bun (needs `bun`)
|
|
160
|
+
npm run test:deno # conformance subset on Deno (needs `deno`)
|
|
161
|
+
npm run build:prebuild # node-gyp rebuild + stage prebuilds/<platform>-<arch>/
|
|
69
162
|
# or rebuild explicitly:
|
|
70
163
|
npm run rebuild
|
|
71
164
|
```
|
|
72
165
|
|
|
166
|
+
The load order prefers a staged `prebuilds/<platform>-<arch>/node_gi.node` over
|
|
167
|
+
`build/Release` (the consumer/Deno install path). Local verification always runs
|
|
168
|
+
the **just-built** addon instead: the Node test scripts pin `NODE_GI_NATIVE=build`
|
|
169
|
+
and the bun/deno runner defaults to it — without that, a stale staged prebuild
|
|
170
|
+
silently shadows your build. CI's cross-runtime job sets `NODE_GI_NATIVE=prebuild`
|
|
171
|
+
to keep validating the prebuild load path with a freshly staged binary.
|
|
172
|
+
`NODE_GI_NATIVE` accepts `build`, `prebuild`, or an explicit path to a
|
|
173
|
+
`node_gi.node`.
|
|
174
|
+
|
|
175
|
+
Two debug-only env vars instrument the toggle-ref / teardown machinery (both
|
|
176
|
+
parsed once at first use, zero cost when unset — never set them in production):
|
|
177
|
+
|
|
178
|
+
- `NODE_GI_TOGGLE_DEBUG=1` — stderr tracing of the GC bridge: owner-env claim,
|
|
179
|
+
drain-TSFN create/release, shutdown-flag flips, teardown enqueue/drop, drain
|
|
180
|
+
runs/skips (with JS-availability), and the C→JS trampoline skips at env
|
|
181
|
+
teardown; each line carries the emitting thread.
|
|
182
|
+
- `NODE_GI_TOGGLE_TEARDOWN_DELAY_MS=<n>` — test-only latency seam (clamped to
|
|
183
|
+
10s): the drain defers queued idle teardowns younger than `n` ms (re-waking
|
|
184
|
+
itself), which deterministically parks teardowns — with a pending drain wake —
|
|
185
|
+
across the event loop's exit. That is the regression vehicle for the
|
|
186
|
+
env-cleanup drain race (`test/gc-cross-thread.test.mjs`, "teardown drain
|
|
187
|
+
during env cleanup never aborts").
|
|
188
|
+
|
|
189
|
+
## Conformance (golden-diff)
|
|
190
|
+
|
|
191
|
+
The exactness oracle for GJS parity: small self-contained `gi://` programs
|
|
192
|
+
under `conformance/programs/*.conf.mjs` run UNCHANGED on all four runtimes —
|
|
193
|
+
gjs natively (`gjs -m`, ambient `print`), node/bun/deno via a lightweight
|
|
194
|
+
generated runtime twin (the `globals.js` shim + `requireGi`, no bundler) — and
|
|
195
|
+
every runtime's **stdout must be byte-identical to the committed golden**
|
|
196
|
+
(`conformance/golden/<name>.txt`). **gjs is the reference**: the goldens ARE
|
|
197
|
+
the gjs output, and a gjs↔golden drift fails loudly (either GJS changed or the
|
|
198
|
+
golden is stale — never paper over it).
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
npm run test:conformance # full matrix (gjs × node × bun × deno)
|
|
202
|
+
node scripts/conformance.mjs --runtimes=gjs,node # runtime subset (gjs/node never auto-skip)
|
|
203
|
+
node scripts/conformance.mjs --filter=variant # program subset
|
|
204
|
+
node scripts/conformance.mjs --update-golden # regenerate goldens from gjs
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Adding a program: drop `conformance/programs/<name>.conf.mjs` — default
|
|
208
|
+
imports of the exact shape `import Gio from 'gi://Gio?version=2.0';` only
|
|
209
|
+
(regex-rewritable to `requireGi`), output via the GJS-ambient `print()`,
|
|
210
|
+
strictly deterministic (no versions, paths, hostnames, timing; the runner sets
|
|
211
|
+
`LC_ALL=C`), ends cleanly — then `--update-golden`, eyeball the golden for
|
|
212
|
+
determinism, and commit both. Every feature PR extends this suite.
|
|
213
|
+
|
|
214
|
+
The ledger contract (`conformance/ledger.json`) is strict: every known-failing
|
|
215
|
+
program×runtime combo is a **committed entry**
|
|
216
|
+
`{ "program", "runtime", "reason", "issue"? }` — a failing combo *not* in the
|
|
217
|
+
ledger fails the run, and a passing combo still *in* the ledger fails as a
|
|
218
|
+
stale entry (remove it). Exit 0 means zero unexpected results; there are no
|
|
219
|
+
silent exclusions (bun/deno merely report `skipped` when not installed — gjs
|
|
220
|
+
and node never skip).
|
|
221
|
+
|
|
222
|
+
### Tier B — GJS installed-tests port
|
|
223
|
+
|
|
224
|
+
The breadth oracle: GJS's own installed-tests
|
|
225
|
+
(`refs/gjs/installed-tests/js/testGIMarshalling.js`) encode GJS's marshalling
|
|
226
|
+
behavior against the purpose-built `GIMarshallingTests-1.0` typelib.
|
|
227
|
+
`gimarshalling/testGIMarshalling.port.mjs` is a near-verbatim port of that
|
|
228
|
+
file to `node:test` via a minimal jasmine shim
|
|
229
|
+
(`gimarshalling/jasmine-shim.mjs`), mapping the WHOLE upstream surface:
|
|
230
|
+
already-green sections run live, everything else is a `describeSkip` stub
|
|
231
|
+
naming the upstream section. Assertions are never weakened.
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
npm run test:gimarshalling # builds the pinned typelibs if missing, then runs the port
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`scripts/build-gi-test-typelibs.mjs` builds the test typelibs reproducibly
|
|
238
|
+
from GNOME's gobject-introspection-tests project at the **pinned revision**
|
|
239
|
+
GJS itself tests against (`PINNED_REV`, copied from
|
|
240
|
+
`refs/gjs/subprojects/gobject-introspection-tests.wrap`) into the gitignored
|
|
241
|
+
`.gi-tests/` (meson + ninja required; cairo disabled; Regress builds too).
|
|
242
|
+
The launcher (`scripts/gimarshalling.mjs`) sets `GI_TYPELIB_PATH` /
|
|
243
|
+
`LD_LIBRARY_PATH` before spawning `node --test` — dlopen cannot pick up late
|
|
244
|
+
env changes — and pins `NODE_GI_NATIVE=build`. The port files are named
|
|
245
|
+
`*.port.mjs` so the default `npm test` glob never picks them up.
|
|
246
|
+
|
|
247
|
+
**Skip contract (strict, mirrors the tier-A ledger):** every skipped
|
|
248
|
+
spec/suite carries a reason — a phase-2.x roadmap item from the taxonomy at
|
|
249
|
+
the top of the port file (e.g. `phase 2.1 BigInt-64-bit`), an upstream issue
|
|
250
|
+
URL, or a `FIDELITY-BUG: …` note — and the reason is reported in the
|
|
251
|
+
`node:test` output (`# SKIP <reason>`). A bare skip throws (`pending()`,
|
|
252
|
+
`itSkip`, `describeSkip` all require the reason; `xit` must chain
|
|
253
|
+
`.pend(reason)`). Later marshalling PRs un-skip their sections — this port is
|
|
254
|
+
phase 2's acceptance gate.
|
|
255
|
+
|
|
73
256
|
## Usage (milestone 1)
|
|
74
257
|
|
|
75
258
|
```js
|
|
@@ -194,6 +377,286 @@ The mainloop bridge (`startMainLoop`) is auto-attached the first time `requireGi
|
|
|
194
377
|
loads a namespace, so `GLib.MainLoop.run()` / `Gio.Application.run()` block as
|
|
195
378
|
they do under GJS while Node's timers, I/O and signal handlers keep running.
|
|
196
379
|
|
|
380
|
+
A blocking loop is **optional**: the same call arms the uv-driven auto-pump, so
|
|
381
|
+
pending GLib sources dispatch from Node's own event loop too — async Gio work
|
|
382
|
+
awaited at top level behaves exactly as under `gjs -m`:
|
|
383
|
+
|
|
384
|
+
```js
|
|
385
|
+
import { requireGi } from '@gjsify/node-gi/gi';
|
|
386
|
+
|
|
387
|
+
const Gio = requireGi('Gio', '2.0');
|
|
388
|
+
const file = Gio.File.new_for_path('/etc/hostname');
|
|
389
|
+
// No GLib.MainLoop.run() anywhere: the GTask completion dispatches from Node's
|
|
390
|
+
// loop, the in-flight op keeps the process alive, then Node exits normally.
|
|
391
|
+
const [ok, contents] = await new Promise((resolve, reject) => {
|
|
392
|
+
file.load_contents_async(null, (_source, res) => {
|
|
393
|
+
try { resolve(file.load_contents_finish(res)); } catch (e) { reject(e); }
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Process-lifetime semantics follow Node conventions: an in-flight async op
|
|
399
|
+
(a pending `GAsyncReadyCallback`) and an armed GLib timeout keep the process
|
|
400
|
+
alive — like Node's own pending I/O and timers — so a REPEATING GLib timeout
|
|
401
|
+
keeps the process running like `setInterval` (under `gjs -m` the process would
|
|
402
|
+
instead exit once the module settles; remove the source to release the process).
|
|
403
|
+
A *passive* fd source with no pending op (e.g. only a listening
|
|
404
|
+
`Gio.SocketService`) does not keep the process alive on its own, and a
|
|
405
|
+
purely-sync program still exits immediately.
|
|
406
|
+
|
|
407
|
+
#### `GLib.Variant` (build + unpack, GJS semantics)
|
|
408
|
+
|
|
409
|
+
`new GLib.Variant(signature, value)` recursively builds a GVariant from a type
|
|
410
|
+
signature, and the wrapper exposes the GJS unpack flavours — the contract
|
|
411
|
+
GAction / GSettings / DBus payloads expect:
|
|
412
|
+
|
|
413
|
+
```js
|
|
414
|
+
const GLib = requireGi('GLib', '2.0');
|
|
415
|
+
|
|
416
|
+
new GLib.Variant('s', 'hi').deepUnpack(); // 'hi'
|
|
417
|
+
new GLib.Variant('as', ['a', 'b']).deepUnpack(); // ['a', 'b']
|
|
418
|
+
new GLib.Variant('(si)', ['x', 1]).deepUnpack(); // ['x', 1]
|
|
419
|
+
|
|
420
|
+
const v = new GLib.Variant('a{sv}', {
|
|
421
|
+
name: new GLib.Variant('s', 'Ada'),
|
|
422
|
+
age: new GLib.Variant('i', 36),
|
|
423
|
+
});
|
|
424
|
+
v.get_type_string(); // 'a{sv}'
|
|
425
|
+
v.deepUnpack(); // { name: Variant, age: Variant } (one level; values stay Variants)
|
|
426
|
+
v.recursiveUnpack(); // { name: 'Ada', age: 36 } (fully plain JS)
|
|
427
|
+
v.unpack(); // single level; children stay Variants
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Supported type strings: the basics `b y n q i u x t h d s o g`, `v` (variant),
|
|
431
|
+
`m*` (maybe), `a*` arrays (incl. the `as` strv + `ay` bytestring fast-paths and
|
|
432
|
+
`a{..}` dictionaries), `(...)` tuples and `{kv}` dict-entries. Built Variants
|
|
433
|
+
round-trip as GObject arguments/properties/signal values — e.g. a
|
|
434
|
+
`Gio.SimpleAction` state:
|
|
435
|
+
|
|
436
|
+
```js
|
|
437
|
+
const Gio = requireGi('Gio', '2.0');
|
|
438
|
+
const action = Gio.SimpleAction.new_stateful('counter', null, new GLib.Variant('i', 0));
|
|
439
|
+
action.get_state().deepUnpack(); // 0
|
|
440
|
+
action.change_state(new GLib.Variant('i', 5));
|
|
441
|
+
action.get_state().deepUnpack(); // 5
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
#### `GObject.registerClass` (GJS-shaped decorator)
|
|
445
|
+
|
|
446
|
+
`requireGi('GObject')` carries the GJS runtime statics — `registerClass`,
|
|
447
|
+
`ParamSpec`, `ParamFlags`, `SignalFlags` — layered over the introspected
|
|
448
|
+
namespace, so you subclass a GObject the same way you would under GJS:
|
|
449
|
+
|
|
450
|
+
```js
|
|
451
|
+
const GObject = requireGi('GObject', '2.0');
|
|
452
|
+
|
|
453
|
+
const Counter = GObject.registerClass(
|
|
454
|
+
{
|
|
455
|
+
GTypeName: 'Counter',
|
|
456
|
+
Properties: {
|
|
457
|
+
// CONSTRUCT so the value is set before vfunc_constructed runs.
|
|
458
|
+
count: GObject.ParamSpec.int(
|
|
459
|
+
'count', 'Count', 'A counter',
|
|
460
|
+
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT,
|
|
461
|
+
0, 100, 0,
|
|
462
|
+
),
|
|
463
|
+
},
|
|
464
|
+
Signals: { 'changed': { param_types: ['int'] } },
|
|
465
|
+
},
|
|
466
|
+
class Counter extends GObject.Object {
|
|
467
|
+
increment() { this.count += 1; this.emit('changed', this.count); }
|
|
468
|
+
vfunc_constructed() { /* runs during construction; `this` is the instance */ }
|
|
469
|
+
},
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
const c = new Counter({ count: 5 });
|
|
473
|
+
c.connect('changed', (n) => console.log('now', n));
|
|
474
|
+
c.increment(); // logs "now 6"
|
|
475
|
+
console.log(c.count); // 6 (custom property)
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
`new Counter(props)` constructs the GObject (`constructType`) and wraps it so the
|
|
479
|
+
user class's own prototype methods resolve FIRST, then the GObject property/signal
|
|
480
|
+
surface. `registerClass(class)` (no meta) is also accepted; the GTypeName then
|
|
481
|
+
defaults to the class name. The parent namespace/type is read from the class's
|
|
482
|
+
`extends` (its `$gtypeName`), so it works for both `GObject.Object` and real GI
|
|
483
|
+
classes (`class extends Gio.SimpleAction { … }`).
|
|
484
|
+
|
|
485
|
+
Caveats (this is the no-toggle-ref object model): the user class's JS constructor
|
|
486
|
+
body is not run — GObject-idiomatic init belongs in `vfunc_constructed`;
|
|
487
|
+
instances are Proxies over a native handle (but `instanceof` still works — it is
|
|
488
|
+
resolved through the GObject type system, see the `instanceof` note below);
|
|
489
|
+
**plain (non-GObject-property) JS instance fields do NOT cross the
|
|
490
|
+
vfunc↔instance boundary** — inside a vfunc, `this` is a distinct wrapper over the
|
|
491
|
+
same GObject (the native engine mints a fresh handle per call, so there is no
|
|
492
|
+
shared per-instance JS object yet), so use GObject **properties** for any state
|
|
493
|
+
that must be visible both inside a vfunc and on the instance (those live in C and
|
|
494
|
+
are consistent; the unified instance identity arrives with the toggle-ref work);
|
|
495
|
+
a JS↔GObject reference cycle on a custom instance leaks (the same cycle-leak
|
|
496
|
+
caveat the signal/vfunc layer carries); and multi-level registered subclass
|
|
497
|
+
chains (registering a subclass of a registered subclass) are not yet supported.
|
|
498
|
+
|
|
499
|
+
#### `GObject` conveniences (signals, `GObject.Value`, `Object.new`)
|
|
500
|
+
|
|
501
|
+
`requireGi('GObject')` carries the GJS `GObject.js` convenience surface on top of
|
|
502
|
+
introspection:
|
|
503
|
+
|
|
504
|
+
```js
|
|
505
|
+
const GObject = requireGi('GObject', '2.0');
|
|
506
|
+
const Gio = requireGi('Gio', '2.0');
|
|
507
|
+
|
|
508
|
+
// By-function signal ops — node-gi connects through private closures, so the
|
|
509
|
+
// (function → handler id) mapping is recorded at connect() time.
|
|
510
|
+
const action = new Gio.SimpleAction({ name: 'a', enabled: true });
|
|
511
|
+
const onChange = () => { /* … */ };
|
|
512
|
+
action.connect('notify::enabled', onChange);
|
|
513
|
+
GObject.signal_handlers_block_by_func(action, onChange); // → count blocked
|
|
514
|
+
GObject.signal_handlers_unblock_by_func(action, onChange); // → count unblocked
|
|
515
|
+
GObject.signal_handlers_disconnect_by_func(action, onChange); // → count disconnected
|
|
516
|
+
action.block_signal_handler(id); action.unblock_signal_handler(id); // by id
|
|
517
|
+
action.stop_emission_by_name('notify::enabled'); // from within a handler
|
|
518
|
+
|
|
519
|
+
// GObject.Value — an explicit GValue you can build, set, read, copy and pass IN.
|
|
520
|
+
const v = new GObject.Value();
|
|
521
|
+
v.init(GObject.TYPE_INT); v.set_int(42); v.get_int(); // → 42
|
|
522
|
+
const s = new GObject.Value(GObject.TYPE_STRING, 'hi'); // 2-arg convenience
|
|
523
|
+
v instanceof GObject.Value; // → true
|
|
524
|
+
|
|
525
|
+
// Construct a GObject from a runtime GType.
|
|
526
|
+
const made = GObject.Object.new(Gio.SimpleAction.$gtype, { name: 'made' });
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Also present: `GObject.ParamFlags` / `SignalFlags` (full introspected bitfields),
|
|
530
|
+
the fundamental `GObject.TYPE_*` GTypes, `GObject.AccumulatorType`, and
|
|
531
|
+
`GObject.signal_connect` / `signal_connect_after` / `signal_emit_by_name`.
|
|
532
|
+
|
|
533
|
+
`bind_property_full` / `BindingGroup.bind_full` work with **real JS transform
|
|
534
|
+
functions** (the engine marshals them as C `GBindingTransformFunc` trampolines,
|
|
535
|
+
the same architecture gjs uses via GjsPrivate — see `src/private.cc`): a
|
|
536
|
+
transform-to converts the bound value, returning `[false, …]` leaves the target
|
|
537
|
+
unchanged, a bidirectional transform-from converts back, and `null` transforms
|
|
538
|
+
give a plain copy binding. Verified byte-identical to gjs by the
|
|
539
|
+
`gclosure-in-args` conformance program. The related raw primitives
|
|
540
|
+
`GObject.signal_connect_closure` / `GObject.source_set_closure` accept a plain
|
|
541
|
+
JS function wherever a `GObject.Closure` IN-argument is expected (the engine
|
|
542
|
+
marshals it as a real GClosure).
|
|
543
|
+
|
|
544
|
+
**Kept-throw** (a clear, actionable error, not a crash): `ParamSpec.enum`
|
|
545
|
+
/ `flags` / `char` / `uchar` / `long` / `ulong` / `param` are not yet buildable (the
|
|
546
|
+
native param-spec builder covers int/uint/int64/uint64/double/float/string/boolean/
|
|
547
|
+
object/boxed).
|
|
548
|
+
|
|
549
|
+
#### `GLib` conveniences (`log_structured`, one-shot idle/timeout)
|
|
550
|
+
|
|
551
|
+
`requireGi('GLib')` carries `GLib.log_structured(domain, level, fields)` (packs
|
|
552
|
+
string / `Uint8Array` / `GLib.Variant` fields into an `a{sv}` and hands it to
|
|
553
|
+
`g_log_variant`) and the one-shot source helpers `idle_add_once` /
|
|
554
|
+
`timeout_add_once` / `timeout_add_seconds_once` (the callback runs once, then the
|
|
555
|
+
source is auto-removed).
|
|
556
|
+
|
|
557
|
+
`GLib.log_set_writer_func(fn)` installs a **JS `GLogWriterFunc`** as the
|
|
558
|
+
process structured-log writer, with gjs semantics (node-gi ships the same
|
|
559
|
+
thread-guarded C wrapper gjs routes through GjsPrivate — `src/private.cc`): the
|
|
560
|
+
writer receives `(logLevel, fields)` where `fields` is a plain object whose
|
|
561
|
+
values are `Uint8Array`s of the field bytes (`null` for empty fields) —
|
|
562
|
+
byte-for-byte the shape gjs's `{...stringFields.recursiveUnpack()}` produces —
|
|
563
|
+
and its returned `GLib.LogWriterOutput` drives the handled/unhandled fallback.
|
|
564
|
+
`GLib.log_set_writer_default()` detaches the JS writer (later logs fall back to
|
|
565
|
+
`g_log_writer_default`). Verified byte-identical to gjs by the `log-writer`
|
|
566
|
+
conformance program. Two contracts, identical under gjs: the underlying
|
|
567
|
+
`g_log_set_writer_func` may only ever be called ONCE per process — a second
|
|
568
|
+
`GLib.log_set_writer_func(fn)` call aborts inside GLib itself (install one
|
|
569
|
+
writer per process; `log_set_writer_default()` detaches the JS side but cannot
|
|
570
|
+
re-arm a new install) — and an off-thread log falls back to the default writer
|
|
571
|
+
in C (JS is never entered from a foreign thread).
|
|
572
|
+
|
|
573
|
+
#### `Gio.DBus` (client proxy, name owning + object export)
|
|
574
|
+
|
|
575
|
+
`requireGi('Gio')` carries the GJS DBus surface — both halves. The **client**
|
|
576
|
+
half: `Gio.DBusProxy.makeProxyWrapper(interfaceXml)` parses the interface XML and
|
|
577
|
+
returns a proxy constructor whose instances expose each method as `NameSync` (sync),
|
|
578
|
+
`NameRemote` (raw async callback) and `NameAsync` (Promise), each property as a
|
|
579
|
+
getter/setter, and each signal via `connectSignal` / `disconnectSignal` (the same
|
|
580
|
+
pure-JS `_signals` mixin GJS uses). `Gio.DBus.session` / `Gio.DBus.system` are the
|
|
581
|
+
bus getters; `Gio.DBus.own_name` / `unown_name` / `watch_name` / `unwatch_name`
|
|
582
|
+
own and watch bus names. The **export** half
|
|
583
|
+
(`Gio.DBusExportedObject.wrapJSObject` — exporting a JS object AS a DBus
|
|
584
|
+
service) is described below the example.
|
|
585
|
+
|
|
586
|
+
```js
|
|
587
|
+
const Gio = requireGi('Gio', '2.0');
|
|
588
|
+
|
|
589
|
+
const Proxy = Gio.DBusProxy.makeProxyWrapper(`<node>
|
|
590
|
+
<interface name="org.freedesktop.DBus">
|
|
591
|
+
<method name="GetId"><arg type="s" direction="out"/></method>
|
|
592
|
+
<signal name="NameOwnerChanged"><arg type="s"/><arg type="s"/><arg type="s"/></signal>
|
|
593
|
+
</interface>
|
|
594
|
+
</node>`);
|
|
595
|
+
|
|
596
|
+
const proxy = new Proxy(Gio.DBus.session, 'org.freedesktop.DBus', '/org/freedesktop/DBus');
|
|
597
|
+
const [busId] = proxy.GetIdSync(); // synchronous method
|
|
598
|
+
proxy.GetIdRemote((result, error) => { /* … */ }); // async, raw callback
|
|
599
|
+
const [id2] = await proxy.GetIdAsync(); // async, Promise (drains after run())
|
|
600
|
+
proxy.connectSignal('NameOwnerChanged', (p, sender, [name, oldOwner, newOwner]) => { /* … */ });
|
|
601
|
+
|
|
602
|
+
const id = Gio.DBus.own_name(Gio.BusType.SESSION, 'org.example.App',
|
|
603
|
+
Gio.BusNameOwnerFlags.NONE, null, (conn, name) => { /* acquired */ }, null);
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
Async replies / signals / name callbacks dispatch from the default main context —
|
|
607
|
+
either a blocking `GLib.MainLoop.run()` or, with no loop anywhere, the uv-driven
|
|
608
|
+
auto-pump (an `await proxy.GetIdAsync()` at top level settles like any other
|
|
609
|
+
async Gio op). A `NameAsync` **Promise** `.then` drains *while* a node-gi loop
|
|
610
|
+
blocks on all three runtimes (the reply's GI callback settles the Promise and
|
|
611
|
+
the microtask checkpoint at that loop-dispatched boundary runs the
|
|
612
|
+
continuation). One Node-only divergence remains: when the blocking `run()` is
|
|
613
|
+
itself entered inside a live async scope (module top-level evaluation, an
|
|
614
|
+
`await`, `node:test`), V8 refuses the nested checkpoint (node-gtk #442/#121) —
|
|
615
|
+
the reply still fires and settles the Promise, so it resolves once `run()`
|
|
616
|
+
returns; defer the blocking run to a macrotask (what `runAsync` does) or drive
|
|
617
|
+
the method through the raw `NameRemote` callback. Bun/Deno do not share that
|
|
618
|
+
nesting restriction: their registered drain primitives run even under a
|
|
619
|
+
top-level blocking `run()`.
|
|
620
|
+
|
|
621
|
+
**Object export** (`Gio.DBusExportedObject.wrapJSObject` — exporting a JS
|
|
622
|
+
object AS a DBus service) works with GJS semantics. GJS builds it on
|
|
623
|
+
`GjsPrivate.DBusImplementation` (a GJS-internal C type, absent on a plain
|
|
624
|
+
Node/GI host); node-gi instead drives the **introspectable**
|
|
625
|
+
`g_dbus_connection_register_object_with_closures2` (GLib ≥ 2.84) — the
|
|
626
|
+
method-call / get-property / set-property vtable slots are plain JS functions
|
|
627
|
+
the engine marshals as real **GClosure IN-arguments**:
|
|
628
|
+
|
|
629
|
+
```js
|
|
630
|
+
const service = {
|
|
631
|
+
Level: 7, // property (read via the interface XML signature)
|
|
632
|
+
Echo(s) { return `echo:${s}`; }, // method (an Async variant + Promise return also work)
|
|
633
|
+
Boom() { throw new Error('kaboom'); }, // a throw becomes a DBus error (org.gnome.gjs.JSError.*)
|
|
634
|
+
};
|
|
635
|
+
const impl = Gio.DBusExportedObject.wrapJSObject(interfaceXml, service);
|
|
636
|
+
impl.export(Gio.DBus.session, '/org/example/App');
|
|
637
|
+
impl.emit_signal('Pinged', new GLib.Variant('(s)', ['ping!']));
|
|
638
|
+
impl.emit_property_changed('Level', new GLib.Variant('i', 8)); // updates proxy caches
|
|
639
|
+
impl.unexport(); // releases the registration + its closures
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
The impl surface matches GJS (`export` / `unexport` / `unexport_from_connection`
|
|
643
|
+
/ `emit_signal` / `emit_property_changed` / `flush` / `get_object_path`, plus
|
|
644
|
+
node-gi's usual camelCase aliases). The full round-trip — exported method,
|
|
645
|
+
property get/set through `org.freedesktop.DBus.Properties`, a throwing method
|
|
646
|
+
returning a DBus error, `emit_signal`, `emit_property_changed` updating a
|
|
647
|
+
proxy's cached property, and unexport — runs byte-identical to gjs
|
|
648
|
+
(`dbus/export-scenario.mjs`, cross-checked against `gjs -m` under
|
|
649
|
+
`dbus-run-session` by `npm run test:dbus`). Lifetime: the registration ref+sinks
|
|
650
|
+
its closures, so the service object lives exactly as long as the registration
|
|
651
|
+
(surviving GC with no JS references) and becomes collectable after
|
|
652
|
+
`unexport()` — guarded by the `--expose-gc` leg of the dbus suite. A method
|
|
653
|
+
handler receives the GJS-appended trailing `Gio.UnixFDList` argument (`null`
|
|
654
|
+
when the call carries no fds — verified against gjs; an actual fd-carrying
|
|
655
|
+
call arrives as a wrapped `UnixFDList` but deeper fd extraction is untested).
|
|
656
|
+
As under GJS, a **sync** self-call from the exporting process deadlocks the
|
|
657
|
+
shared main loop that must also service the incoming request — use the async
|
|
658
|
+
`NameRemote` forms.
|
|
659
|
+
|
|
197
660
|
### GJS ambient globals (`@gjsify/node-gi/globals`)
|
|
198
661
|
|
|
199
662
|
GJS source relies on globals that exist implicitly under `gjs` — `print`,
|
|
@@ -208,6 +671,13 @@ print('hello', 1, true); // → stdout, GJS String()-join
|
|
|
208
671
|
const GLib = imports.gi.GLib; // legacy imports.gi (honours .versions)
|
|
209
672
|
imports.gi.versions.Gtk = '4.0';
|
|
210
673
|
console.log(imports.gettext.gettext('x')); // no-translation passthrough
|
|
674
|
+
|
|
675
|
+
// Legacy script modules many older GJS sources use:
|
|
676
|
+
const emitter = {};
|
|
677
|
+
imports.signals.addSignalMethods(emitter); // the pure-JS Signals mixin
|
|
678
|
+
emitter.connect('ready', () => imports.mainloop.quit());
|
|
679
|
+
imports.mainloop.timeout_add(50, () => { emitter.emit('ready'); return false; });
|
|
680
|
+
imports.mainloop.run(); // thin GLib.MainLoop wrapper
|
|
211
681
|
```
|
|
212
682
|
|
|
213
683
|
A follow-up `--app node` build step will inject this automatically for any
|
|
@@ -218,3 +688,346 @@ The remaining GJS-compatible surface (`import GLib from 'gi://GLib?version=2.0'`
|
|
|
218
688
|
`const GLib = imports.gi.GLib`, the core overrides, `_promisify`, the legacy
|
|
219
689
|
`imports.*` modules) is layered on top of this engine in the gjsify bundler
|
|
220
690
|
integration and subsequent drops.
|
|
691
|
+
|
|
692
|
+
### cairo (`@gjsify/node-gi/cairo`)
|
|
693
|
+
|
|
694
|
+
cairo is a **foreign struct** in GObject-Introspection: GI does not know the
|
|
695
|
+
layout of `cairo_t` / `cairo_surface_t` / `cairo_pattern_t`, so it delegates their
|
|
696
|
+
marshalling to a module. GJS ships a native cairo binding + a foreign-struct
|
|
697
|
+
registration so that a GI function taking/returning a cairo pointer (e.g. a
|
|
698
|
+
`Gtk.DrawingArea` draw-func's `cairo_t`) round-trips to/from the JS cairo objects.
|
|
699
|
+
node-gi ports that seam: the same drawing code runs on GJS (native cairo) and Node
|
|
700
|
+
(this binding). An npm `cairo` package cannot stand in — a foreign cairo argument
|
|
701
|
+
must marshal through the SAME module the engine's foreign-struct seam knows about.
|
|
702
|
+
|
|
703
|
+
```js
|
|
704
|
+
import cairo from '@gjsify/node-gi/cairo'; // bare `cairo` on the --app node build
|
|
705
|
+
import { requireGi } from '@gjsify/node-gi/gi';
|
|
706
|
+
|
|
707
|
+
// Headless drawing — read the pixels back with getData().
|
|
708
|
+
const surface = new cairo.ImageSurface(cairo.Format.ARGB32, 64, 48);
|
|
709
|
+
const cr = new cairo.Context(surface);
|
|
710
|
+
cr.setSourceRGB(0.8, 0.1, 0.1);
|
|
711
|
+
cr.rectangle(8, 8, 20, 16);
|
|
712
|
+
cr.fill();
|
|
713
|
+
cr.$dispose();
|
|
714
|
+
surface.flush();
|
|
715
|
+
const pixels = surface.getData(); // Uint8Array (stride * height), ARGB32
|
|
716
|
+
|
|
717
|
+
// The foreign seam: a GI function taking a cairo_t marshals the Context through.
|
|
718
|
+
const PangoCairo = requireGi('PangoCairo', '1.0');
|
|
719
|
+
const layout = PangoCairo.create_layout(new cairo.Context(surface));
|
|
720
|
+
|
|
721
|
+
// A Gtk.DrawingArea draw-func receives a cairo_t → a live cairo.Context:
|
|
722
|
+
// area.set_draw_func((_area, ctx, w, h) => { ctx.setSourceRGB(1, 0, 0); … });
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
Ported this slice: `cairo.Context` (drawing + transform ops incl.
|
|
726
|
+
`identityMatrix` and the `userToDevice[Distance]` / `deviceToUser[Distance]`
|
|
727
|
+
point transforms, state getters, `setDash`/`getDashCount` (+ a net-new
|
|
728
|
+
`getDash`), `inFill`/`inStroke`, `newSubPath`, `copyPath`/`copyPathFlat`/
|
|
729
|
+
`appendPath` (owned `cairo.Path` handles), `getSource` with concrete-subclass
|
|
730
|
+
fan-out, `$dispose`), `cairo.Surface` + `cairo.ImageSurface` (`getData`/
|
|
731
|
+
`getWidth`/`getHeight`/`getStride`/`getFormat`/`flush`/`writeToPNG`/
|
|
732
|
+
`createFromPNG`), the patterns — `cairo.SolidPattern`,
|
|
733
|
+
`cairo.LinearGradient`/`cairo.RadialGradient` (`addColorStopRGB[A]` via the
|
|
734
|
+
shared `cairo.Gradient` base), `cairo.SurfacePattern`
|
|
735
|
+
(`setExtend`/`getExtend`/`setFilter`/`getFilter`) — the opaque `cairo.Path`,
|
|
736
|
+
and the enums (`Format`, `Operator`, `Content`, `Extend`, `Filter`, …). This is
|
|
737
|
+
the full surface `@gjsify/canvas2d-core` draws through (headless Canvas 2D).
|
|
738
|
+
The native binding paints **byte-for-byte identically to GJS** (verified
|
|
739
|
+
pixel-for-pixel against `gjs -m`, incl. gradients / repeating surface patterns /
|
|
740
|
+
dashed strokes / path round-trips — `test/cairo-canvas2d.test.mjs`). Deferred:
|
|
741
|
+
region objects, the PDF/SVG/PS surfaces, and the text/font ops
|
|
742
|
+
(`showText`/`selectFontFace` — canvas2d text rides PangoCairo instead).
|
|
743
|
+
|
|
744
|
+
Building on that seam, the **LIVE `@gjsify/canvas2d` `Canvas2DBridge`** — a
|
|
745
|
+
`Gtk.DrawingArea` that wraps an `HTMLCanvasElement` 2D context and blits its Cairo
|
|
746
|
+
surface onto the widget each frame — realizes, draws and blits UNCHANGED on
|
|
747
|
+
node-gi under a display: an app draws via the standard `canvas.getContext('2d')`
|
|
748
|
+
DOM API in `bridge.onReady`, the GTK draw_func fires, the bridge blits
|
|
749
|
+
(`cr.setSourceSurface` + `cr.paint`) and the rAF (`add_tick_callback`) path ticks.
|
|
750
|
+
The same source builds `--app gjs` and `--app node` and prints byte-identical
|
|
751
|
+
output, pixels read back off the canvas included — `test/canvas2d-bridge.test.mjs`
|
|
752
|
+
+ `fixtures/canvas2d-bridge-app.ts`. It is a **local/dev verification** (see the
|
|
753
|
+
run recipe in the test header), NOT wired into CI: the LIVE bridge pulls the whole
|
|
754
|
+
`@gjsify/canvas2d` gi:// graph, so it needs the full gjsify workspace built with a
|
|
755
|
+
current-source `@gjsify/cli` (the bare-`cairo`→`@gjsify/node-gi/cairo` and
|
|
756
|
+
register-inline fixes the published CLI predates) plus a display and the addon — a
|
|
757
|
+
heavyweight from-scratch rebuild not worth gating a minimal CI container on. The
|
|
758
|
+
test self-skips in the default `npm test` (no display). One node-only note: a
|
|
759
|
+
mapped `Gtk.DrawingArea`'s live `GdkFrameClock` stays an active GLib source after
|
|
760
|
+
`app.quit()`, so — matching the documented lifetime divergence — a node-gi GTK
|
|
761
|
+
program that must terminate exits explicitly (`process.exit(0)`), whereas `gjs -m`
|
|
762
|
+
exits on module completion.
|
|
763
|
+
|
|
764
|
+
### The live `@gjsify/event-bridge` dispatches DOM events on node-gi
|
|
765
|
+
|
|
766
|
+
The GTK→DOM event bridge (`@gjsify/event-bridge`'s `attachEventControllers`) —
|
|
767
|
+
which attaches GTK4 `EventControllerMotion`/`GestureClick`/`EventControllerScroll`/
|
|
768
|
+
`EventControllerKey`/`EventControllerFocus` to a widget and dispatches W3C DOM
|
|
769
|
+
events (Mouse/Pointer/Keyboard/Wheel/FocusEvent) — runs UNCHANGED on node-gi. The
|
|
770
|
+
shared fixture presents a `Gtk.DrawingArea`, attaches the controllers, and drives a
|
|
771
|
+
SYNTHESIZED event through each live `Gtk.EventController*` via `emit(signal, …)`
|
|
772
|
+
(the same path the GJS `event-bridge.spec.ts` drives), then asserts the dispatched
|
|
773
|
+
DOM event's type / coords / `getModifierState` / key / code. The `Gdk.ModifierType`
|
|
774
|
+
flags and `Gdk.keyval_name`/`Gdk.keyval_to_unicode` marshalling produce
|
|
775
|
+
byte-identical DOM events under node-gi and `gjs -m` — the same source builds
|
|
776
|
+
`--app gjs` and `--app node` and prints the committed golden
|
|
777
|
+
(`test/event-bridge.test.mjs` + `fixtures/event-bridge-app.ts`). Every golden line
|
|
778
|
+
is deterministic + display-independent (coords clamp to a fixed 400x300 allocation;
|
|
779
|
+
key/code/modifiers derive from the Gdk marshalling), so byte-parity is stable.
|
|
780
|
+
|
|
781
|
+
**`instanceof` across the GObject hierarchy (GJS parity):** `instanceof` for GObject
|
|
782
|
+
wrapper classes is wired through the GObject type system — each per-GType wrapper
|
|
783
|
+
carries a `Symbol.hasInstance` that resolves via `g_type_is_a` (native
|
|
784
|
+
`isInstanceOf`), so `new Gtk.EventControllerMotion() instanceof
|
|
785
|
+
Gtk.EventControllerMotion` is `true`, and so is a base class (`… instanceof
|
|
786
|
+
Gtk.EventController`), an implemented interface (`simpleAction instanceof Gio.Action`)
|
|
787
|
+
and a `registerClass` subclass against its leaf / base / interface — while a sibling
|
|
788
|
+
type, an unrelated class, a boxed/`Variant` handle, `null` or a plain object stay
|
|
789
|
+
`false`. `test/instanceof.test.mjs` + the cross-runtime golden
|
|
790
|
+
`conformance/programs/instanceof-hierarchy.conf.mjs` (gjs/node/bun/deno byte-identical)
|
|
791
|
+
guard it. The event-bridge fixture retrieves controllers by ADD ORDER off
|
|
792
|
+
`widget.observe_controllers()` as a stylistic choice (identity is preserved and
|
|
793
|
+
`emit()` resolves the signal by the live GType) — no longer forced by a gap.
|
|
794
|
+
(A second gap the fixture originally routed around — `new
|
|
795
|
+
Gdk.Rectangle()` threw `no static method 'new'` — is FIXED: `new <BoxedStruct>()`
|
|
796
|
+
now zero-allocates with GJS `gi/boxed.cpp` semantics when the struct has no `new`
|
|
797
|
+
constructor (`Graphene.Rect`/`Point`, `Gdk.Rectangle`, `Gdk.RGBA` — the
|
|
798
|
+
`@gjsify/devtools` screenshot chain), routes to `new` when it exists, and throws
|
|
799
|
+
a clear error for args without one; `test/struct-construct.test.mjs` guards it,
|
|
800
|
+
gjs-parity included. The fixture still reads the presented window's real
|
|
801
|
+
allocation — simpler and display-truthful.)
|
|
802
|
+
|
|
803
|
+
**Run it (needs a display + a built workspace + the `gjsify` CLI; self-skips
|
|
804
|
+
otherwise):**
|
|
805
|
+
|
|
806
|
+
```sh
|
|
807
|
+
# node-gi (--app node) — the authoritative check vs the committed golden:
|
|
808
|
+
export GJSIFY_BIN="$(git rev-parse --show-toplevel)/packages/infra/cli/lib/index.js"
|
|
809
|
+
xvfb-run -a dbus-run-session -- \
|
|
810
|
+
env GSK_RENDERER=cairo GDK_BACKEND=x11 LIBGL_ALWAYS_SOFTWARE=1 GTK_A11Y=none \
|
|
811
|
+
NODE_GI_NATIVE=build NODE_GI_EB_SKIP_GJS=1 GJSIFY_BIN="$GJSIFY_BIN" \
|
|
812
|
+
node --test test/event-bridge.test.mjs
|
|
813
|
+
# Drop NODE_GI_EB_SKIP_GJS to additionally re-prove the golden IS gjs's own output
|
|
814
|
+
# (builds + runs --app gjs; needs the workspace CLI, not the published one).
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
`GJSIFY_BIN` must point at the WORKSPACE-built `@gjsify/cli` (`packages/infra/cli/lib/index.js`),
|
|
818
|
+
not the published `@gjsify/cli` — the fixture is built with current source, which
|
|
819
|
+
carries this session's bundler fixes the published `0.18.0` predates.
|
|
820
|
+
### WebGL / `Gtk.GLArea` (the gwebgl seam)
|
|
821
|
+
|
|
822
|
+
**Definitive: a `Gtk.GLArea` realizes and hands JS a LIVE, CURRENT GL context
|
|
823
|
+
under node-gi on a headless software-GL display.** Verified end-to-end by
|
|
824
|
+
`test/webgl-glarea.test.mjs` + the ONE dual-runtime source
|
|
825
|
+
`fixtures/webgl-glarea-app.ts`: a presented `Gtk.ApplicationWindow` holding a
|
|
826
|
+
`Gtk.GLArea` configured exactly like `@gjsify/webgl`'s `WebGLBridge`
|
|
827
|
+
(`set_use_es(true)`, `set_required_version(3, 2)`, depth + stencil) realizes
|
|
828
|
+
with `get_error() === null`, an **OpenGL ES 3.2** context
|
|
829
|
+
(`Gdk.GLContext.get_current()` non-null in both `realize` and `render`), and
|
|
830
|
+
the `gwebgl` Vala bridge (`new Gwebgl.WebGLRenderingContextBase()` — the native
|
|
831
|
+
class `@gjsify/webgl` wraps) works through it: `getString(GL_VERSION/…)`, a
|
|
832
|
+
`getParameterx` **GVariant** round-trip, and a real WebGL draw —
|
|
833
|
+
`clearColor(1,0,0,1)` + `clear` + `readPixels` reading back the exact
|
|
834
|
+
`255,0,0,255` pixel. The committed golden is byte-identical between `gjs -m`
|
|
835
|
+
and `node` (the gjs gold-standard leg re-proves it wherever `gjs` is present;
|
|
836
|
+
`NODE_GI_WEBGL_SKIP_GJS=1` skips that leg).
|
|
837
|
+
|
|
838
|
+
The GL/display env the golden is pinned to (software GL, no GPU needed):
|
|
839
|
+
|
|
840
|
+
```bash
|
|
841
|
+
# X11 (Xvfb or a real display) + mesa llvmpipe + GTK compositing off GL:
|
|
842
|
+
xvfb-run -a dbus-run-session -- \
|
|
843
|
+
env GSK_RENDERER=cairo GDK_BACKEND=x11 LIBGL_ALWAYS_SOFTWARE=1 GTK_A11Y=none \
|
|
844
|
+
NODE_GI_NATIVE=build node --test test/webgl-glarea.test.mjs
|
|
845
|
+
# GL under llvmpipe: "OpenGL ES 3.2 Mesa …" / "llvmpipe (LLVM …)".
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
**The FULL `@gjsify/webgl` `WebGLBridge` runs too** (same test file, second
|
|
849
|
+
fixture `fixtures/webgl-bridge-app.ts`): the complete TS WebGL stack UNCHANGED —
|
|
850
|
+
`WebGLBridge` (a `registerClass` `Gtk.GLArea` subclass), `onReady` handing out
|
|
851
|
+
`HTMLCanvasElement` + `WebGLRenderingContext` (constants GHashTable, the
|
|
852
|
+
`_init()` `getParameterx` GVariant reads, eager WebGL1+2 context construction),
|
|
853
|
+
and browser-standard `clearColor(0,0,1,1)` + `clear` + `readPixels` reading the
|
|
854
|
+
blue clear back (`bridge-pixel(0,0): 0,0,255,255`), byte-identical gjs ↔ node.
|
|
855
|
+
Shader/buffer/texture breadth (a three.js triangle/teapot) is the remaining
|
|
856
|
+
follow-up — the seam + context stack are proven.
|
|
857
|
+
|
|
858
|
+
The tests self-skip without a display, without a `gjsify` CLI, or without the
|
|
859
|
+
committed `Gwebgl-0.1` prebuild (`packages/framework/webgl/prebuilds/linux-*`,
|
|
860
|
+
which the test itself puts on `GI_TYPELIB_PATH`/`LD_LIBRARY_PATH`); the bridge
|
|
861
|
+
test additionally skips when `@gjsify/webgl` is not built. The fixture build
|
|
862
|
+
needs a WORKSPACE-built `@gjsify/cli` (point `GJSIFY_BIN` at
|
|
863
|
+
`packages/infra/cli/lib/index.js` after
|
|
864
|
+
`gjsify workspace @gjsify/cli build --with-dependencies`), like
|
|
865
|
+
`canvas2d-bridge`; the gjs gold-standard leg additionally needs the workspace
|
|
866
|
+
register libs built (`--app gjs` force-inlines `<pkg>/register`). Two engine
|
|
867
|
+
gaps this spike fixed on the way (headless regression coverage in
|
|
868
|
+
`test/gerror-return.test.mjs`): GError-typed RETURNS
|
|
869
|
+
(`Gtk.GLArea.get_error()` — `GI_TYPE_TAG_ERROR` → a field-readable GLib.Error
|
|
870
|
+
boxed) and literal-first method-name resolution (Vala GIRs carry camelCase
|
|
871
|
+
names — Gwebgl's `getString` — which the unconditional camelCase→snake_case
|
|
872
|
+
alias destroyed; the engine now resolves the literal name first, alias second).
|
|
873
|
+
|
|
874
|
+
### Excalibur.js renders through WebGL on node-gi (the GTK-bridge capstone)
|
|
875
|
+
|
|
876
|
+
**Definitive: a REAL WebGL game engine — Excalibur 0.32, the engine behind the
|
|
877
|
+
`excalibur-jelly-jumper` showcase and the PixelRPG map-editor — boots, runs its
|
|
878
|
+
clock, and renders frames through `@gjsify/webgl`'s `WebGLBridge` UNCHANGED
|
|
879
|
+
under node-gi** (`test/excalibur-webgl.test.mjs` + the ONE dual-runtime source
|
|
880
|
+
`fixtures/excalibur-webgl-app.ts`). `new ex.Engine({ canvasElement })` builds
|
|
881
|
+
against the bridge's `HTMLCanvasElement` (WebGL2 context), `engine.start()`
|
|
882
|
+
resolves, the engine's real render pipeline runs (shader compile/link,
|
|
883
|
+
`bufferData`, VAOs, `vertexAttribPointer`, `drawArrays`/`drawElements`,
|
|
884
|
+
`clearBufferfv` at `RenderTarget.blitToScreen`) for 5 frames, and the committed
|
|
885
|
+
golden asserts the pixels read back off the GL framebuffer — the screen-centered
|
|
886
|
+
blue Actor and the red engine clear color — **byte-identical between `gjs -m`
|
|
887
|
+
and `node`**. The DOM surface (document/HTMLCanvasElement/ResizeObserver/
|
|
888
|
+
matchMedia/XHR) comes from the SAME `@gjsify/*` registers the gjs build injects,
|
|
889
|
+
via the `--app node` explicit-`--globals` reverse-bridge injection.
|
|
890
|
+
|
|
891
|
+
Excalibur's real GL + engine usage exposed four core gaps, all fixed at the
|
|
892
|
+
engine (each with regression coverage):
|
|
893
|
+
|
|
894
|
+
- **`GVariant 'ay'` rejects `null`** (`src/variant.cc`) — GJS packs
|
|
895
|
+
`new GLib.Variant('ay', null)` as the EMPTY byte array (`GLib.Bytes(null)`);
|
|
896
|
+
node-gi threw. Exposing call: Excalibur's `texImage2D(..., null)`
|
|
897
|
+
blank-texture allocation at renderer init (`Uint8ArrayToVariant(null)`).
|
|
898
|
+
- **Unknown members must be `undefined`, not a throw-on-call thunk**
|
|
899
|
+
(`src/calls.cc` `hasMethod` + the L1 wrapper `get`) — real consumers
|
|
900
|
+
feature-detect optional native methods (`typeof gl.clearBufferfv ===
|
|
901
|
+
'function'` gates `@gjsify/webgl`'s clearBuffer emulation, hit at
|
|
902
|
+
`blitToScreen`); the old always-a-function proxy made that detection lie,
|
|
903
|
+
then threw mid-frame. `hasMethod(handle, name)` resolves through the SAME
|
|
904
|
+
literal-first/snake-alias walk `callMethod` uses.
|
|
905
|
+
- **Signal dispatch now runs the microtask checkpoint at its boundary**
|
|
906
|
+
(`src/signals.cc`, `napi_make_callback`) — GJS drains the promise-job queue
|
|
907
|
+
when the outermost JS frame exits; promise chains resolved inside a
|
|
908
|
+
loop-dispatched signal handler (Excalibur's whole `engine.start()` boot,
|
|
909
|
+
queued from the GLArea `render`/`onReady` dispatch) previously lingered
|
|
910
|
+
until the libuv↔GLib bridge's prepare-phase drain.
|
|
911
|
+
- **The uv co-pump source must not outrank GTK painting** (`src/loop.cc`) —
|
|
912
|
+
at the default `G_PRIORITY_DEFAULT` a busy Node loop STARVED
|
|
913
|
+
`GDK_PRIORITY_REDRAW`: Excalibur's `requestIdleCallback` polyfill (a
|
|
914
|
+
self-re-arming 1 ms `setTimeout`, run perpetually by its GarbageCollector)
|
|
915
|
+
kept the UvLoopSource ready on every GLib iteration, so ticks/renders/rAF
|
|
916
|
+
froze while plain GLib timeouts kept firing. The source now sits below
|
|
917
|
+
redraw (`G_PRIORITY_HIGH_IDLE + 30`): rendering outranks Node timers,
|
|
918
|
+
browser-like, and Node I/O still runs in every frame gap.
|
|
919
|
+
|
|
920
|
+
Run it (a LOCAL/dev verification like the other display suites — self-skips
|
|
921
|
+
without a display / CLI / prebuild / built workspace):
|
|
922
|
+
|
|
923
|
+
```sh
|
|
924
|
+
export GJSIFY_BIN="$(git rev-parse --show-toplevel)/packages/infra/cli/lib/index.js"
|
|
925
|
+
xvfb-run -a dbus-run-session -- \
|
|
926
|
+
env -u FORCE_COLOR GSK_RENDERER=cairo GDK_BACKEND=x11 LIBGL_ALWAYS_SOFTWARE=1 \
|
|
927
|
+
GTK_A11Y=none NODE_GI_NATIVE=build GJSIFY_BIN="$GJSIFY_BIN" \
|
|
928
|
+
node --test test/excalibur-webgl.test.mjs
|
|
929
|
+
# Drop NODE_GI_EXCALIBUR_SKIP_GJS to additionally re-prove the golden IS gjs's
|
|
930
|
+
# own byte-output (builds + runs --app gjs).
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
The FULL `excalibur-jelly-jumper` showcase builds `--app node`
|
|
934
|
+
(`gjsify run build:node`; `gjsify.example.runtimes` includes `node`) and gets
|
|
935
|
+
remarkably far on node-gi: the GTK window presents, the devtools control plane
|
|
936
|
+
exports over DBus, `Gst.init` runs and every `ex.Sound` constructs its
|
|
937
|
+
Gst-backed `AudioContext` (this exposed + fixed the nullable-array `null`
|
|
938
|
+
marshalling — `Gst.init(null)`), and Excalibur boots into resource loading.
|
|
939
|
+
The remaining blocker is POLYFILL ROUTING, not marshalling: on Node the
|
|
940
|
+
GLOBAL `fetch` is the native undici one (the register convention never
|
|
941
|
+
overrides an existing native), and `@excaliburjs/plugin-tiled`'s fileLoader
|
|
942
|
+
feeds it the root-relative `/res/…` paths that only OUR GJS fetch/XHR resolve
|
|
943
|
+
against the program dir — undici rejects them (`Failed to parse URL`), the
|
|
944
|
+
Tiled map never loads, and scene init fails. Making the reverse bridge route
|
|
945
|
+
`fetch` (and friends) to the `@gjsify/*` polyfills over the runtime natives is
|
|
946
|
+
the follow-up that unlocks the full game. (`jsdom` — plugin-tiled's node-side
|
|
947
|
+
DOMParser fallback — is aliased to `@gjsify/empty` in `build:node`, mirroring
|
|
948
|
+
the plugin's own `"browser": { "jsdom": false }`.)
|
|
949
|
+
|
|
950
|
+
### A real Adwaita WINDOW realizes + renders (the GTK-GUI capstone)
|
|
951
|
+
|
|
952
|
+
Beyond the display-free conformance: an UNCHANGED `Adw.Application` +
|
|
953
|
+
`Adw.ApplicationWindow` (HeaderBar / WindowTitle / StatusPage) not only
|
|
954
|
+
constructs + presents but **realizes and RENDERS a surface** through the GSK
|
|
955
|
+
renderer on node-gi — the same in-process capture path `@gjsify/devtools`'
|
|
956
|
+
`Screenshot` uses: `Gtk.WidgetPaintable` → `Gtk.Snapshot.to_node()` →
|
|
957
|
+
`Gsk.Renderer.render_texture` → `Gdk.Texture.save_to_png_bytes`. A non-empty PNG
|
|
958
|
+
is the unambiguous proof that a `GdkSurface` was allocated + a GSK render tree
|
|
959
|
+
rasterised — not reachable by any headless program. Guarded by
|
|
960
|
+
`test/windowing.test.mjs` + `test/windowing-interactive.test.mjs` (an
|
|
961
|
+
`Adw.ApplicationWindow` that RESPONDS to a `Gio.SimpleAction` + a
|
|
962
|
+
`Gtk.Button::clicked` through the node-gi signal chain) + `test/widgets.test.mjs`
|
|
963
|
+
(the Adwaita widget breadth below) — all self-skip without a display on Linux (the
|
|
964
|
+
win32/darwin GDK backend supplies its own display, so they run there), wired into
|
|
965
|
+
the Linux `gtk-smoke` + the Windows windowing CI jobs. The
|
|
966
|
+
`showcases/gtk/node-gi-window` showcase runs the SAME single source on both GJS
|
|
967
|
+
and Node and screenshots the live window over the `org.gjsify.Devtools` DBus
|
|
968
|
+
surface.
|
|
969
|
+
|
|
970
|
+
This exposed one core gap, fixed at the engine:
|
|
971
|
+
|
|
972
|
+
- **Non-GObject GObject-fundamentals wrap through their introspected ref/unref,
|
|
973
|
+
not `WrapGObject`** (`src/object.cc` `MakeFundamentalHandle` + the
|
|
974
|
+
`src/marshal.cc` return branch). `Gtk.Snapshot.to_node()` returns a
|
|
975
|
+
`GskRenderNode` — introspected as OBJECT_INFO but a GObject FUNDAMENTAL
|
|
976
|
+
(`gi_object_info_get_fundamental`), ref-counted via `gsk_render_node_ref/unref`,
|
|
977
|
+
NOT `g_object_ref`, with `G_IS_OBJECT` FALSE. Routing it through `WrapGObject`
|
|
978
|
+
ran the toggle-ref/qdata dance on a non-GObject → a cascade of
|
|
979
|
+
`g_object_*: assertion 'G_IS_OBJECT (object)' failed` criticals AND a leaked ref.
|
|
980
|
+
It now gets a type-tagged External carrying the raw pointer + the introspected
|
|
981
|
+
unref func as the finalizer hint (`isFundamentalHandle` / L1 `wrapFundamental`,
|
|
982
|
+
an opaque round-trippable pass-through) — GParamSpec + GValue keep their
|
|
983
|
+
dedicated branches, this catches the rest.
|
|
984
|
+
|
|
985
|
+
### Windows: the FULL-windowing GTK runtime bundle
|
|
986
|
+
|
|
987
|
+
The batteries-included [`@gjsify/gtk-runtime-win32-x64`](../gtk-runtime-win32-x64)
|
|
988
|
+
bundle has two closures: the DEFAULT display-free set (loadable DLLs + typelibs)
|
|
989
|
+
and the `--windowing` SUPERSET that adds the runtime DATA a real GTK window needs
|
|
990
|
+
on Windows — the gdk-pixbuf loaders + `loaders.cache`, compiled GSettings schemas
|
|
991
|
+
(`gschemas.compiled`), the Adwaita/hicolor icon themes + `icon-theme.cache`, and
|
|
992
|
+
Fontconfig config/cache. node-gi's loader (`gtk-runtime.js`
|
|
993
|
+
`maybeWireGtkWindowingEnv`) detects the windowing data via the `gschemas.compiled`
|
|
994
|
+
marker and wires the env (`GSETTINGS_SCHEMA_DIR` / `GDK_PIXBUF_MODULE_FILE` /
|
|
995
|
+
`XDG_DATA_DIRS` / `FONTCONFIG_*`); a display-free bundle carries no marker, so the
|
|
996
|
+
wiring is a strict no-op and that load is byte-unchanged.
|
|
997
|
+
|
|
998
|
+
### Adwaita widget breadth realizes + reacts + renders
|
|
999
|
+
|
|
1000
|
+
Beyond one window's chrome: a representative slice of the REAL Libadwaita widget
|
|
1001
|
+
set constructs, RENDERS and REACTS on node-gi. `test/widgets.test.mjs` builds an
|
|
1002
|
+
`Adw.PreferencesPage` / `Adw.PreferencesGroup` of `Adw.ActionRow`, `Adw.SwitchRow`,
|
|
1003
|
+
`Adw.EntryRow`, `Adw.ComboRow` (a `Gtk.StringList` model), `Adw.SpinRow` (a
|
|
1004
|
+
`Gtk.Adjustment`) and `Adw.ExpanderRow`, plus a `Gtk.ListBox` and a dismissible
|
|
1005
|
+
`Adw.Toast` via an `Adw.ToastOverlay`. Two tiers, robust on a runner whose surface
|
|
1006
|
+
may or may not realize:
|
|
1007
|
+
|
|
1008
|
+
- **DumpTree** — the widget tree contains every expected type (`AdwSwitchRow`,
|
|
1009
|
+
`AdwComboRow`, `AdwEntryRow`, `AdwSpinRow`, `AdwExpanderRow`, `AdwPreferencesPage`,
|
|
1010
|
+
`GtkListBox`, …), read via the runtime GType (`$typeName`) the `@gjsify/devtools`
|
|
1011
|
+
DumpTree uses — so each class constructs + parents correctly through node-gi.
|
|
1012
|
+
- **Interaction** — toggling the switch, changing the combo selection, moving the
|
|
1013
|
+
spin value, expanding the expander and setting the entry text all drive
|
|
1014
|
+
OBSERVABLE property changes AND fire their paired `notify::<prop>` handlers, and a
|
|
1015
|
+
toast add → `dismiss()` fires `::dismissed`. This surfaces the `Gtk.StringList` /
|
|
1016
|
+
`Gtk.Adjustment` model marshalling (GListModel + object construct props),
|
|
1017
|
+
`notify::` property signals and the boxed-model paths — all display-independent, so
|
|
1018
|
+
the interaction + DumpTree tier holds headless on Windows CI.
|
|
1019
|
+
- **Render** — when the surface realizes, the whole preferences surface rasterises
|
|
1020
|
+
through the GSK renderer (a non-empty PNG), the strong proof the broad widget set
|
|
1021
|
+
renders, not just constructs.
|
|
1022
|
+
|
|
1023
|
+
The same widgets back the `showcases/gtk/node-gi-window` "Settings" view (an
|
|
1024
|
+
`Adw.ViewStack` page reachable from the bottom `Adw.ViewSwitcherBar`, beside the
|
|
1025
|
+
counter). No engine change and no windowing-bundle change were needed: the rows are
|
|
1026
|
+
core GTK4/Adwaita widgets backed by the already-bundled `gtk-4-*.dll` / `libadwaita-1`
|
|
1027
|
+
and the full Adwaita icon theme + compiled schemas the `--windowing` bundle already
|
|
1028
|
+
ships.
|
|
1029
|
+
|
|
1030
|
+
Scoped out of the capstone fixture itself (deliberately): the Gst audio
|
|
1031
|
+
DECODE/playback path (`decodeAudioData`, `autoaudiosink`) — construction is
|
|
1032
|
+
proven by the showcase run above, the streaming pipeline is its own follow-up
|
|
1033
|
+
surface.
|