@gjsify/gtk-host 0.52.0 → 0.54.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.
@@ -4,6 +4,60 @@ export declare function registerWidget(descriptor: WidgetDescriptor): void;
4
4
  export declare function registerWidgets(descriptors: readonly WidgetDescriptor[]): void;
5
5
  export declare function lookupWidget(tag: string): WidgetDescriptor;
6
6
  export declare const hasWidget: (tag: string) => boolean;
7
+ /** A row's class, in the shape every consumer here needs it. */
8
+ export type InstalledClass = GObject.ObjectClass & (new (props?: Record<string, unknown>) => GObject.Object);
9
+ /**
10
+ * The class the RUNNING typelib has for this row, or `null` when it has none.
11
+ *
12
+ * The general form of "does this GTK have that class", and the only honest place
13
+ * to ask it: the generated table is produced from ONE platform's GIR (#1446), and
14
+ * a platform does not build every class that GIR describes — GTK `#ifdef`s its
15
+ * Unix print stack out on Windows, so `GtkPrintUnixDialog` and
16
+ * `GtkPageSetupUnixDialog` are rows whose class is `undefined` there and a real
17
+ * class on Linux and macOS. Nothing narrower is available: the vocabulary comes
18
+ * from one GIR pool with no second platform to compare against, and a claim
19
+ * derived from a NAME (`*Unix*`) breaks precisely when it matters, on the next
20
+ * absent class nobody spelled that way.
21
+ *
22
+ * THE CAST is where the gap between the declared type and the runtime is
23
+ * admitted, ONCE, with the reason. `ctor` is typed as promising a constructor
24
+ * unconditionally, which is what lets `new Klass(props)` read as a constructor
25
+ * call; the promise is true on every host the table was generated on and false
26
+ * on a host that omits a class, and the admission belongs beside the question
27
+ * rather than at each of the dozen call sites.
28
+ *
29
+ * WHY THE ROW'S OWN `ctor()` IS THE ORACLE, and it is a measured trap rather
30
+ * than a stylistic choice. `GObject.type_from_name` is the one-call version of
31
+ * this question and it answers WRONG in the direction that costs a working row:
32
+ * measured on gjs 1.88.1 / Homebrew GTK 4.24.0, `type_from_name('GtkPrintUnixDialog')`
33
+ * answers **0** while `Gtk.PrintUnixDialog` is present and constructs — a typelib
34
+ * registers a class when the namespace member is first READ, not when the
35
+ * typelib loads. `ctor()` IS that read, which is why it answers correctly in
36
+ * both directions and why nothing here calls `type_from_name`. It also costs
37
+ * nothing to ask per row: an oracle over the WHOLE table has to force the
38
+ * registration of every class it walks, whereas a row is only ever asked about
39
+ * when something is about to use it.
40
+ */
41
+ export declare function classOf(descriptor: {
42
+ readonly ctor: () => unknown;
43
+ }): InstalledClass | null;
44
+ /** Whether this host has the class a row names — a table sweep's way to not be a row. */
45
+ export declare const hasClass: (descriptor: {
46
+ readonly ctor: () => unknown;
47
+ }) => boolean;
48
+ /**
49
+ * {@link classOf}, refusing BY NAME rather than handing back `undefined`.
50
+ *
51
+ * The single gate every read of a row's class goes through, so an absent class is
52
+ * one named refusal instead of six anonymous `TypeError`s — dereferencing it
53
+ * produced `Cannot read properties of undefined (reading 'list_properties')` and
54
+ * `… (reading '$gtype')` with no row in either message, which is how a
55
+ * one-platform-generated table arrived as six failures that named one class.
56
+ */
57
+ export declare function requireClass(descriptor: {
58
+ readonly gtype: string;
59
+ readonly ctor: () => unknown;
60
+ }): InstalledClass;
7
61
  /** Every registered GType name — the conformance suite walks this, so coverage is data. */
8
62
  export declare const registeredTags: () => string[];
9
63
  /**
@@ -10,9 +10,9 @@ export declare const gated: (gate: DiagnosticsGate, name: string, body: () => Pr
10
10
  * failure mode rather than taste.
11
11
  *
12
12
  * A capability can only be PROBED — nothing about `(os, env)` answers "is the Gtk
13
- * typelib reachable from this process", and `@gjsify/unit` is Tier 1 and must not
14
- * import `gi://` to find out. A probe that answers "no" makes the whole suite STAND
15
- * DOWN, and `requireAxes` cannot catch that: it only holds axes the host MATCHES.
13
+ * typelib reachable from this process". A probe that answers "no" makes the whole
14
+ * suite STAND DOWN, and `requireAxes` cannot catch that: it only holds axes the
15
+ * host MATCHES (`GJSIFY_TEST_EXPECT_AXES` can, but only on a leg that sets it).
16
16
  * So a container that lost its GTK would run zero tests and report success — the
17
17
  * green-that-checked-nothing shape, arriving through the very gate meant to widen
18
18
  * coverage. Measured once already: gated on `'Gjs'` alone, this suite built for the
@@ -13,11 +13,7 @@ export interface HostText extends HostNodeBase {
13
13
  readonly kind: 'text';
14
14
  data: string;
15
15
  }
16
- /**
17
- * A position marker. Vue's `createComment` and Svelte's comment markers land here.
18
- * An anchor NEVER enters the GTK tree; `insert` resolves forward past it to the
19
- * next node that actually owns a widget.
20
- */
16
+ /** A position marker. Vue's `createComment` and Svelte's comment markers land here. */
21
17
  export interface HostAnchor extends HostNodeBase {
22
18
  readonly kind: 'anchor';
23
19
  data: string;
@@ -36,9 +32,9 @@ export interface HostElement extends HostNodeBase {
36
32
  /**
37
33
  * signal name -> the one native handler, and the prop that owns it.
38
34
  *
39
- * One handler per signal name, ever. The owner is recorded because two props
40
- * can resolve to the same signal (`onClicked` and `on:clicked`), and the
41
- * second used to disconnect the first without saying so.
35
+ * One handler per signal name, ever. The owner is recorded because two props can
36
+ * resolve to the same signal (`onClicked` and `on:clicked`), and the second used
37
+ * to disconnect the first without saying so.
42
38
  */
43
39
  handlers: Map<string, {
44
40
  id: number;
@@ -54,10 +50,10 @@ export interface HostElement extends HostNodeBase {
54
50
  /**
55
51
  * The authored ARIA object, or null — the accessibility GTK holds for this widget.
56
52
  *
57
- * A field of its own and NOT a key in `props`, for the reason `layout` is one: `props`
58
- * is replayed into `g_object_new` by `materialize`, and none of these names is a
59
- * construct property. It is the LAST authored value, so a patch can compute which
60
- * slots the new object no longer sets and reset exactly those.
53
+ * A field of its own and NOT a key in `props`, because `props` is replayed into
54
+ * `g_object_new` by `materialize` and none of these names is a construct property. It is
55
+ * the LAST authored value, so a patch can compute which slots the new object no longer
56
+ * sets and reset exactly those.
61
57
  */
62
58
  accessibility: Record<string, unknown> | null;
63
59
  /** True once text CHILDREN wrote the sink, so removing the last one clears it
@@ -66,11 +62,10 @@ export interface HostElement extends HostNodeBase {
66
62
  /**
67
63
  * True while this element is actually IN its parent's GTK tree.
68
64
  *
69
- * Owning a widget is not the same thing: every framework builds bottom-up,
70
- * so a subtree is materialised long before it is inserted. Deriving "is my
71
- * sibling in the tree" from `widget !== null` made the remove-all policy
72
- * detach non-children and re-add already-parented ones — two Adwaita
73
- * criticals per replay, at exit 0.
65
+ * Owning a widget is not the same thing: every framework builds bottom-up, so a subtree
66
+ * is materialised long before it is inserted. Deriving "is my sibling in the tree" from
67
+ * `widget !== null` made the remove-all policy detach non-children and re-add
68
+ * already-parented ones — two Adwaita criticals per replay, at exit 0.
74
69
  */
75
70
  attached: boolean;
76
71
  /** True once `destroy` has torn this element down. It never comes back. */
@@ -78,19 +73,18 @@ export interface HostElement extends HostNodeBase {
78
73
  /**
79
74
  * Children this element already had before the host adopted it.
80
75
  *
81
- * Only an ADOPTED root has any: a renderer mounts into a container the
82
- * application built, and those children are in the GTK tree while being absent
83
- * from the shadow tree. Placement offsets past them, or the first insertion
84
- * computes "first" and the whole rendered tree lands ABOVE the app's own chrome.
76
+ * Only an ADOPTED root has any: a renderer mounts into a container the application built,
77
+ * and those children are in the GTK tree while being absent from the shadow tree.
78
+ * Placement offsets past them, or the first insertion computes "first" and the whole
79
+ * rendered tree lands ABOVE the app's own chrome.
85
80
  */
86
81
  foreign: readonly Gtk.Widget[];
87
82
  /**
88
83
  * A PORTAL node's subscription to its parent's `root`, or null.
89
84
  *
90
- * Only a `portal` placement ever has one, and it is per-attachment state
91
- * exactly like `wrapper` — a side table keyed on the element would be a second
92
- * tree that can disagree with this one. See {@link NodePlacement}'s portal arm
93
- * for what the subscription is for and what it was measured against.
85
+ * Only a `portal` placement ever has one, and it is per-attachment state exactly like
86
+ * `wrapper` — a side table keyed on the element would be a second tree that can disagree
87
+ * with this one. See {@link NodePlacement}'s portal arm for what the subscription is for.
94
88
  */
95
89
  portalWatch: {
96
90
  widget: Gtk.Widget;
@@ -101,36 +95,36 @@ export type HostNode = HostElement | HostText | HostAnchor;
101
95
  /**
102
96
  * The axis ABOVE `ChildPolicy`, and the two are orthogonal on purpose.
103
97
  *
104
- * `ChildPolicy` answers "how does this parent adopt a child". It presumes the
105
- * answer to a question nobody had to ask until now: *does this node go into its
106
- * parent at all?* For every widget in the table it does. For an `Adw.Dialog` it
107
- * does not, and the difference is not a degradation the parent can absorb —
108
- * MEASURED on libadwaita 1.9.3 / GTK 4.22.4 / gjs 1.88.1, `box.append(dialog)`
109
- * with the box ROOTED IN A WINDOW reaches
98
+ * `ChildPolicy` answers "how does this parent adopt a child". It presumes the answer
99
+ * to a question nobody had to ask until now: *does this node go into its parent at
100
+ * all?* For every widget in the table it does. For an `Adw.Dialog` it does not, and
101
+ * the difference is not a degradation the parent can absorb — MEASURED on libadwaita
102
+ * 1.9.3 / GTK 4.22.4 / gjs 1.88.1, `box.append(dialog)` with the box ROOTED IN A WINDOW
103
+ * reaches
110
104
  *
111
105
  * Adwaita-ERROR **: Trying to add AdwDialog 0x… to GtkBox 0x…. Use
112
106
  * adw_dialog_present() to show dialogs.
113
107
  *
114
- * which is `g_error()`: SIGABRT, exit 134, a core dump. Not an exception a `try`
115
- * in the reconciler can catch and not a warning a diagnostics gate can count.
108
+ * which is `g_error()`: SIGABRT, exit 134, a core dump. Not an exception a `try` in
109
+ * the reconciler can catch and not a warning a diagnostics gate can count.
116
110
  * `adw_dialog_root()` raises it from the ROOT vfunc, which is why a DETACHED box
117
- * accepts the very same append in silence at exit 0 — re-testing this on a bare
118
- * box "disproves" it and puts the append back.
111
+ * accepts the very same append in silence at exit 0 — re-testing this on a bare box
112
+ * "disproves" it and puts the append back.
119
113
  *
120
- * THE ABORT IS THE LOUD HALF OF A CLASS, and its quiet half arrives through the
121
- * same door (ADR 0054). MEASURED on the same libraries, one process per case:
114
+ * THE ABORT IS THE LOUD HALF OF A CLASS, and its quiet half arrives through the same
115
+ * door (ADR 0054). MEASURED on the same libraries, one process per case:
122
116
  * `box.append(new Gtk.Window())` with the box rooted is **exit 0, silent**, and
123
117
  * afterwards `win.get_parent()` is the box while `win.get_root()` is the window
124
118
  * ITSELF — a `GtkRoot` with a parent, which is a contradiction GTK states nowhere.
125
- * Presenting that window then draws it as a toplevel AND leaves it in the box's
126
- * child list, so the container measures and allocates a window. One node kind
127
- * aborts, its neighbour says nothing; both are the same question answered wrong,
128
- * so both are answered here.
119
+ * Presenting that window then draws it as a toplevel AND leaves it in the box's child
120
+ * list, so the container measures and allocates a window. One node kind aborts, its
121
+ * neighbour says nothing; both are the same question answered wrong, so both are
122
+ * answered here.
129
123
  *
130
124
  * So the descriptor declares the axis, and `policies.ts` is the only reader.
131
- * `parented` is every other widget and is what an ABSENT `placement` means; the
132
- * union still cannot be forgotten because there is exactly one normaliser
133
- * (`placementOf`) and every switch over it ends in `unhandledPlacement`.
125
+ * `parented` is every other widget and is what an ABSENT `placement` means; the union
126
+ * still cannot be forgotten because there is exactly one normaliser (`placementOf`)
127
+ * and every switch over it ends in `unhandledPlacement`.
134
128
  */
135
129
  export type NodePlacement =
136
130
  /**
@@ -152,19 +146,17 @@ export type NodePlacement =
152
146
  * Three measurements shape the two method names, all on libadwaita 1.9.3:
153
147
  *
154
148
  * - `present` TAKES THE PARENT WIDGET (`adw_dialog_present` arity 1, against
155
- * `gtk_window_present`'s 0). That is what makes this a portal rather than a
156
- * toplevel: the node has a place in the shadow tree and a different place in
157
- * the GTK tree, and the parent is what joins them.
158
- * - `close` IS THE UNCONDITIONAL ONE, `force_close` and not `close`. An
159
- * unmount is not a user request: with `can-close: false` (which is how
160
- * `onRequestClose` is honoured one layer up) `adw_dialog_close()` returns
161
- * FALSE, emits `close-attempt` and LEAVES THE DIALOG ON SCREEN — measured.
162
- * `force_close()` closes it and emits `closed`.
163
- * - AND `force_close` IS SAFE ON A NODE THAT WAS NEVER PRESENTED, where
164
- * `close` is not: measured, `close()` on an unpresented dialog answers
165
- * `Adwaita-CRITICAL **: Trying to close AdwDialog 0x… that's not presented`
166
- * at exit 0, and `force_close()` is silent. So the host needs no
167
- * "is it up?" probe before retracting one.
149
+ * `gtk_window_present`'s 0). That is what makes this a portal rather than a toplevel:
150
+ * the node has a place in the shadow tree and a different place in the GTK tree, and
151
+ * the parent is what joins them.
152
+ * - `close` IS THE UNCONDITIONAL ONE, `force_close` and not `close`. An unmount is not
153
+ * a user request: with `can-close: false` (how `onRequestClose` is honoured one layer
154
+ * up) `adw_dialog_close()` returns FALSE, emits `close-attempt` and LEAVES THE DIALOG
155
+ * ON SCREEN — measured. `force_close()` closes it and emits `closed`.
156
+ * - `force_close` IS SAFE ON A NODE THAT WAS NEVER PRESENTED, where `close` is not:
157
+ * measured, `close()` on an unpresented dialog answers `Adwaita-CRITICAL **: Trying to
158
+ * close AdwDialog 0x… that's not presented` at exit 0, and `force_close()` is silent.
159
+ * So the host needs no "is it up?" probe before retracting one.
168
160
  */
169
161
  | {
170
162
  readonly kind: 'portal';
@@ -174,34 +166,33 @@ export type NodePlacement =
174
166
  /**
175
167
  * The node IS a root: `present()` / `destroy()`, and no parent anywhere.
176
168
  *
177
- * WHAT SEPARATES IT FROM A PORTAL IS THE ARITY, and that is a fact about the
178
- * two libraries rather than a convention: measured, `adw_dialog_present` takes
179
- * 1 argument and `gtk_window_present` takes 0. A portal has two positions in
180
- * the tree and the parent is what joins them; a toplevel has ONE, its own, so
181
- * there is nothing to present it against. `descriptorProblems()` holds both
182
- * arities, so the two arms cannot be swapped by a copy/paste.
169
+ * WHAT SEPARATES IT FROM A PORTAL IS THE ARITY, and that is a fact about the two
170
+ * libraries rather than a convention: measured, `adw_dialog_present` takes 1 argument
171
+ * and `gtk_window_present` takes 0. A portal has two positions in the tree and the
172
+ * parent is what joins them; a toplevel has ONE, its own, so there is nothing to
173
+ * present it against. `descriptorProblems()` holds both arities, so the two arms
174
+ * cannot be swapped by a copy/paste.
183
175
  *
184
- * MEMBERSHIP IS `Gtk.Root`, measured with `GObject.type_is_a` and not read
185
- * from documentation — `GtkWindow` and `AdwApplicationWindow` down to
186
- * `GtkPrintUnixDialog`. It is GTK's own word for "this widget is a toplevel",
187
- * which is why the generic code asks it instead of forming a second opinion,
188
- * and `placement.spec.ts` is where the membership is walked and counted.
176
+ * MEMBERSHIP IS `Gtk.Root`, measured with `GObject.type_is_a` and not read from
177
+ * documentation — `GtkWindow` and `AdwApplicationWindow` down to
178
+ * `GtkPrintUnixDialog`. It is GTK's own word for "this widget is a toplevel", which
179
+ * is why the generic code asks it instead of forming a second opinion, and
180
+ * `placement.spec.ts` walks and counts the membership.
189
181
  *
190
- * `close: 'destroy'` IS THE FORCED ONE, the same choice the portal arm makes
191
- * and for the same measured reason. On GTK 4.22.4: `gtk_window_close()` emits
192
- * `close-request`, and a handler returning TRUE leaves the window mapped and
193
- * visible — which is exactly how an application vetoes a user's close. An
194
- * unmount is not a user request, so the conditional call is the wrong one;
195
- * `gtk_window_destroy()` takes it down regardless. Both are silent on a window
196
- * that was never presented (measured), so the host needs no "is it up?" probe.
182
+ * `close: 'destroy'` IS THE FORCED ONE, the same choice the portal arm makes and for
183
+ * the same measured reason. On GTK 4.22.4: `gtk_window_close()` emits `close-request`,
184
+ * and a handler returning TRUE leaves the window mapped and visible — exactly how an
185
+ * application vetoes a user's close. An unmount is not a user request, so the
186
+ * conditional call is the wrong one; `gtk_window_destroy()` takes it down regardless.
187
+ * Both are silent on a window that was never presented (measured), so the host needs
188
+ * no "is it up?" probe.
197
189
  *
198
- * AND THE FORCED CALL IS TERMINAL HERE, which is where this arm stops being a
199
- * copy of the portal one. MEASURED: `present()` after either `close()` or
200
- * `destroy()` answers `Gtk-WARNING **: A window is shown after it has been
201
- * destroyed. This will leave the window in an inconsistent state.` — GTK4's
202
- * default `close-request` handler destroys the window, so `close()` is a
203
- * conditional destroy rather than a hide. A retracted toplevel is therefore
204
- * gone for good, and a re-mount is a fresh widget (which is what `rebuild`
190
+ * AND THE FORCED CALL IS TERMINAL HERE, which is where this arm stops being a copy of
191
+ * the portal one. MEASURED: `present()` after either `close()` or `destroy()` answers
192
+ * `Gtk-WARNING **: A window is shown after it has been destroyed. This will leave the
193
+ * window in an inconsistent state.` — GTK4's default `close-request` handler destroys
194
+ * the window, so `close()` is a conditional destroy rather than a hide. A retracted
195
+ * toplevel is gone for good, and a re-mount is a fresh widget (which is what `rebuild`
205
196
  * already does for a construct-only write).
206
197
  */
207
198
  | {
@@ -426,6 +417,20 @@ export type ChildPolicy = {
426
417
  export interface GeneratedWidget {
427
418
  readonly gtype: string;
428
419
  readonly tag: string;
420
+ /**
421
+ * Availability-CONDITIONAL, and the declared type deliberately does not say so.
422
+ *
423
+ * A row comes from one platform's GIR, so on a platform whose GTK does not
424
+ * build the class this reads `Gtk.PrintUnixDialog` and gets `undefined` —
425
+ * the whole of GTK's Unix print stack is compiled out on win32 (#1446).
426
+ * Widening the signature to `| undefined` would force every `ctor()` reader
427
+ * to handle the absence, and they all handle it through ONE predicate
428
+ * instead (`classOf`/`requireClass`, `registry.ts`); annotating the signature
429
+ * as well would be a second statement of the same fact that nothing checks.
430
+ * The gap this leaves is real and deliberate: a reader that bypasses both
431
+ * gets `undefined` where the type promised a class — which is why every
432
+ * call site refuses rather than dereferences.
433
+ */
429
434
  readonly ctor: () => GObject.ObjectClass & (new (props?: Record<string, unknown>) => GObject.Object);
430
435
  }
431
436
  export interface WidgetDescriptor {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/gtk-host",
3
- "version": "0.52.0",
3
+ "version": "0.54.0",
4
4
  "description": "Framework-agnostic GTK4/Adwaita host: the element model UI-framework renderers bind to",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -75,8 +75,8 @@
75
75
  "build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
76
76
  "build:types": "gjsify tsc",
77
77
  "build:test": "gjsify run build:test:gjs",
78
- "build:test:node": "gjsify build src/test.mts --app node --outfile dist/test.node.mjs --define __VUE_OPTIONS_API__=false --define __VUE_PROD_DEVTOOLS__=false --define __VUE_PROD_HYDRATION_MISMATCH_DETAILS__=false --define \"process.env.NODE_ENV='production'\" --exclude-globals navigator",
79
- "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs --define __VUE_OPTIONS_API__=false --define __VUE_PROD_DEVTOOLS__=false --define __VUE_PROD_HYDRATION_MISMATCH_DETAILS__=false --define \"process.env.NODE_ENV='production'\" --exclude-globals navigator",
78
+ "build:test:node": "gjsify build src/test.mts --app node --outfile dist/test.node.mjs --define __VUE_OPTIONS_API__=false --define __VUE_PROD_DEVTOOLS__=false --define __VUE_PROD_HYDRATION_MISMATCH_DETAILS__=false --define \"process.env.NODE_ENV='production'\"",
79
+ "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs --define __VUE_OPTIONS_API__=false --define __VUE_PROD_DEVTOOLS__=false --define __VUE_PROD_HYDRATION_MISMATCH_DETAILS__=false --define \"process.env.NODE_ENV='production'\"",
80
80
  "test": "gjsify run build:gjsify && gjsify run build:test && gjsify run test:gjs",
81
81
  "test:gjs": "gjsify run test.gjs.mjs",
82
82
  "test:gjs-on-node": "gjsify run build:gjsify && gjsify run build:test:node && node dist/test.node.mjs",
@@ -102,7 +102,8 @@
102
102
  "@girs/gtk-4.0": "5.4.0",
103
103
  "@girs/pango-1.0": "5.4.0",
104
104
  "@girs/pangocairo-1.0": "5.4.0",
105
- "@gjsify/adwaita-core": "^0.52.0"
105
+ "@gjsify/adwaita-core": "^0.54.0",
106
+ "@gjsify/utils": "^0.54.0"
106
107
  },
107
108
  "peerDependencies": {
108
109
  "@vue/runtime-core": "^3.5.0",
@@ -125,10 +126,10 @@
125
126
  }
126
127
  },
127
128
  "devDependencies": {
128
- "@gjsify/cli": "^0.52.0",
129
+ "@gjsify/cli": "^0.54.0",
129
130
  "@vue/runtime-core": "^3.5.0",
130
131
  "solid-js": "^1.9.0",
131
- "@gjsify/unit": "^0.52.0",
132
+ "@gjsify/unit": "^0.54.0",
132
133
  "@types/node": "^25.9.2",
133
134
  "typescript": "^6.0.3",
134
135
  "@gjsify/node-gi": "file:../../node-gi/node-gi",