@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.
@@ -1,8 +1,11 @@
1
1
  import Gtk from 'gi://Gtk?version=4.0';
2
+ import { type FontFaceFailure } from '@gjsify/utils/font-map';
2
3
  import { type FontFamilyMatch } from './font-families.js';
3
4
  import { type FontSource, type ResolveFontSourcesOptions } from './font-dir.js';
4
5
  import { type PlanUiFontOptions, type UiFontPlan, type UiFontPolicy } from './ui-font.js';
5
6
  export { describeFontFamilyMatch, matchFontFamilies, matchFontFamily, type FontFamilyMatch, type FontFamilyMatchKind, } from './font-families.js';
7
+ export { CAIRO_FONT_TYPE_FT, isUnsupportedByFontMap } from '@gjsify/utils/font-map';
8
+ export type { FontFaceFailure } from '@gjsify/utils/font-map';
6
9
  export { ADWAITA_UI_FONT_FAMILY, GNOME_UI_FONT_POINT_SIZE, UI_FONT_POLICIES, planUiFont, planUiFontPolicy, type PlanUiFontOptions, type PlanUiFontPolicyOptions, type UiFontPlan, type UiFontPlanKind, type UiFontPolicy, } from './ui-font.js';
7
10
  export { resolveFontSources, type FontSource, type ResolveFontSourcesOptions } from './font-dir.js';
8
11
  /**
@@ -36,11 +39,6 @@ export interface InitFontsOptions extends Omit<ResolveFontSourcesOptions, 'env'>
36
39
  */
37
40
  readonly expectedFamilies?: readonly string[];
38
41
  }
39
- /** A face the font map would not take, and why. */
40
- export interface FontFaceFailure {
41
- readonly path: string;
42
- readonly message: string;
43
- }
44
42
  /**
45
43
  * A {@link FontSource} and the faces IT contributed.
46
44
  *
@@ -61,6 +59,14 @@ export interface FontSourceOutcome extends FontSource {
61
59
  readonly registered: readonly string[];
62
60
  /** Faces from THIS directory the font map declined — see {@link isUnsupportedByFontMap}. */
63
61
  readonly declined: readonly string[];
62
+ /**
63
+ * Faces from THIS directory that are on the map and that no layout can be served by — the map had
64
+ * already resolved their family to the fallback and caches that answer. Disjoint from
65
+ * {@link declined} and a subset of {@link registered}; `FontFaceRegistration.unreachable` in
66
+ * `@gjsify/utils/font-map` owns the reading and the fresh-map fallback that normally clears this
67
+ * list, so a non-empty one means that fallback was unavailable and the substitution is real.
68
+ */
69
+ readonly unreachable: readonly string[];
64
70
  /** Faces from THIS directory that failed otherwise, plus the directory itself if unreadable. */
65
71
  readonly failed: readonly FontFaceFailure[];
66
72
  }
@@ -98,6 +104,19 @@ export interface InitFontsResult {
98
104
  * {@link isUnsupportedByFontMap}.
99
105
  */
100
106
  readonly declined: readonly string[];
107
+ /**
108
+ * Faces that are on the map and that no layout can be served by, across every {@link sources}
109
+ * entry: the map resolved their family to the fallback before the face arrived and caches that
110
+ * answer, so registering the face changed `list_families()` and nothing else. A SUBSET of
111
+ * {@link registered} — the face really is on the map and `get_family` answers its family — which
112
+ * is why it is reported beside the flat lists rather than inside them.
113
+ *
114
+ * EMPTY wherever the fontconfig fallback took over, which is the whole point of it: then a fresh
115
+ * map serves the faces and there is nothing left to say. Non-empty means that fallback was
116
+ * unavailable (`PANGOCAIRO_BACKEND` pinned, no fontconfig configuration, no fc backend) and the
117
+ * substitution will happen whatever the application does next.
118
+ */
119
+ readonly unreachable: readonly string[];
101
120
  /**
102
121
  * Faces that failed for any other reason, across every {@link sources} entry. Each was warned
103
122
  * about; none threw.
@@ -133,31 +152,18 @@ export interface InitFontsResult {
133
152
  * the map holds afterwards, which is what a match reads.
134
153
  */
135
154
  readonly matches: readonly FontFamilyMatch[];
155
+ /**
156
+ * `true` when this call REPLACED the process's default font map with a fontconfig-backed one
157
+ * so the faces could be registered at all — see `probeFontconfigMap` in `@gjsify/utils/font-map`.
158
+ *
159
+ * Reported because it is a whole-application consequence of one call: every widget created
160
+ * afterwards renders through FreeType instead of the platform rasteriser. It happens only
161
+ * where the default map DECLINED registration (a CoreText map — `gjsify run` on a Homebrew
162
+ * GTK), the faces brought a family that map does not already hold, fontconfig has a
163
+ * configuration of its own, and nobody pinned `PANGOCAIRO_BACKEND` (ADR 0038 § Amendment 5).
164
+ */
165
+ readonly fontconfigFallback: boolean;
136
166
  }
137
- /**
138
- * Did the font map decline runtime registration outright?
139
- *
140
- * `pango_font_map_add_font_file()` is a vfunc, and the CoreText map implements none — so on macOS
141
- * the call falls through to the base implementation, which answers `G_IO_ERROR_NOT_SUPPORTED`.
142
- * That is not a failure to report: macOS is already correct declaratively, because the bundle's
143
- * `ATSApplicationFontsPath` had the OS activate the staged directory before the process started,
144
- * and the ordering makes the runtime call the wrong tool there rather than merely a redundant one
145
- * — `pango_core_text_font_map_changed()` only bumps a serial, there is no
146
- * `kCTFontManagerRegisteredFontsChangedNotification` observer and no re-scan path in
147
- * `pangocoretext-fontmap.c`, so a face registered after the map initialises could not be recovered
148
- * by poking it anyway.
149
- *
150
- * Keyed on the ERROR rather than on `process.platform`, which is the difference between a
151
- * capability test and a guess about who is asking. It costs no `gjsify.os` declaration (this
152
- * package makes no OS decision, and ADR 0018's candidate set is derived from the code that reads
153
- * the host), and it stays right for any other map that declines. The OS name was never the thing
154
- * being asked — which stopped being hypothetical: the backend is whichever one is selected rather
155
- * than one per platform, and `@gjsify/node-gi`'s loader selects `PANGOCAIRO_BACKEND=fc` on the
156
- * bundled windowing runtime (ADR 0038 § Amendment 3), so a darwin process on it reaches this
157
- * arm's other side and REGISTERS the faces. A `process.platform` branch would have had to be
158
- * found and reversed instead.
159
- */
160
- export declare function isUnsupportedByFontMap(error: unknown): boolean;
161
167
  /**
162
168
  * Register every face in the application's shipped font directory with the default font map.
163
169
  *
@@ -165,8 +171,11 @@ export declare function isUnsupportedByFontMap(error: unknown): boolean;
165
171
  * fontconfig backend rather than tidiness: it caches the FONTSET resolved for a description and
166
172
  * `add_font_file` does not invalidate it, so a `Pango.Layout` that measured the family first keeps
167
173
  * measuring the fallback for the life of the process even though the family is then in
168
- * `list_families()`. win32 clears its cache instead and recovers, so registering early is free
169
- * there and unrecoverable-if-missed on Linux (measured both ways; `fonts.spec.ts`).
174
+ * `list_families()`. win32 clears its cache instead and recovers, so registering early is free there
175
+ * and unrecoverable-if-missed on Linux (measured both ways; `fonts.spec.ts`). Calling it late is no
176
+ * longer SILENTLY wrong, though: `registerFontFaces` measures what it registered, replaces the default
177
+ * map when a face turned out to be unserved, and reports what it could not rescue in
178
+ * {@link InitFontsResult.unreachable} — see the file header for what that leaves of the rule.
170
179
  *
171
180
  * Nothing here is eager: this
172
181
  * package is the element model renderers bind to and owns no application lifecycle, and a
@@ -24,23 +24,22 @@ export declare function setEventHandler(el: HostElement, prop: string, next: ((.
24
24
  /**
25
25
  * The ARIA object, diffed against the one it replaces.
26
26
  *
27
- * ONE GROUPED PROP RATHER THAN 53 FLAT `aria*` ONES, and the reasons are measured rather
28
- * than aesthetic. (1) TypeScript exempts every hyphen-containing JSX attribute from
29
- * excess-property checking — the measurement `emit-types.mts` already records for the
30
- * kebab property spellings — so a flat `aria-labell` would be accepted in silence, while a
31
- * key inside a fresh object literal IS checked. (2) The ARIA names are not the widget's
32
- * properties and collide with them: `label`, `orientation`, `checked`, `expanded` and
33
- * `selected` are all both, and `orientation` is settable on a `GtkLabel` that implements no
34
- * `GtkOrientable` at all — one object keeps the two vocabularies apart by construction
35
- * instead of by prefix. (3) The other surface over this same vocabulary already spells it
36
- * this way: a `.blp` author writes `accessibility { label: "…"; }` (ADR 0034). (4) `setProp`
37
- * already has this shape for `layout` — a structured, non-ParamSpec prop routed to its own
38
- * mechanism — so this adds one reserved key to the host, not 53.
27
+ * ONE GROUPED PROP RATHER THAN 53 FLAT `aria*` ONES, for measured reasons. (1)
28
+ * TypeScript exempts every hyphen-containing JSX attribute from excess-property checking —
29
+ * the measurement `emit-types.mts` already records for the kebab property spellings — so a
30
+ * flat `aria-labell` would be accepted in silence, while a key inside a fresh object literal
31
+ * IS checked. (2) The ARIA names collide with the widget's own properties: `label`,
32
+ * `orientation`, `checked`, `expanded` and `selected` are all both, and `orientation` is
33
+ * settable on a `GtkLabel` that implements no `GtkOrientable` at all — one object keeps the
34
+ * two vocabularies apart by construction instead of by prefix. (3) The other surface over
35
+ * this vocabulary already spells it this way: a `.blp` author writes `accessibility {
36
+ * label: "…"; }` (ADR 0034). (4) `setProp` already has this shape for `layout`, so this adds
37
+ * one reserved key to the host, not 53.
39
38
  *
40
39
  * REFUSED ON THE CLASS, not on the widget: every framework builds bottom-up, so props are
41
40
  * authored long before materialisation, and asking the instance would report
42
- * `<gtk-string-list accessibility={…}>` from inside a later `materialize` rather than at the
43
- * call that wrote it.
41
+ * `<gtk-string-list accessibility={…}>` from inside a later `materialize` rather than at
42
+ * the call that wrote it.
44
43
  *
45
44
  * The plan is built BEFORE anything is recorded, for the reason the property path states:
46
45
  * `el.accessibility` is replayed verbatim by `replayInto`, so a refused value kept there
@@ -55,32 +54,23 @@ export declare function insert(node: HostNode, parent: HostElement, anchor?: Hos
55
54
  /**
56
55
  * TWO facts, not one: it says it is an element, and it carries a descriptor.
57
56
  *
58
- * `kind` alone is a plain string a GObject wrapper could carry; the descriptor is
59
- * what every host op actually dereferences. Checking both keeps the refusal from
60
- * being either paranoid or fooled.
57
+ * `kind` alone is a plain string a GObject wrapper could carry; the descriptor is what
58
+ * every host op actually dereferences. Checking both keeps the refusal from being either
59
+ * paranoid or fooled.
61
60
  *
62
- * Exported because the Vue adapter had these same two lines VERBATIM, and its own
63
- * comment said so — it needs the question before `adopt`, to tell a host parent
64
- * from the raw widget `<Teleport :to="el">` hands through. A predicate stated in
65
- * two places is a predicate that can disagree with itself about what a node is.
61
+ * Exported because the Vue adapter needs the question before `adopt`, to tell a host
62
+ * parent from the raw widget `<Teleport :to="el">` hands through.
66
63
  */
67
64
  export declare function isHostElement(value: unknown): value is HostElement;
68
65
  /**
69
66
  * Name a foreign value the way its owner would recognise it.
70
67
  *
71
- * ONE function, because there were two and each was wrong for the other's input.
72
- * The host's read `constructor.$gtype`, so it could not name a DOM element — the
73
- * single most likely thing to arrive from a Solid app that imported `<Dynamic>`
74
- * from `solid-js/web`. The Solid adapter's read `tagName`, so it answered
75
- * `a Object` for the GObject wrapper `<Teleport :to="el">` hands over. Both are
76
- * asking "what did I get instead of a node", so both answers belong to one place.
77
- *
78
- * ORDER IS LOAD-BEARING, and it is measured rather than reasoned: under gjs
79
- * 1.88.1 `({}).constructor.$gtype` EXISTS and `GObject.type_name` calls it
80
- * `JSObject`. So a `$gtype`-first version names every object literal — and every
81
- * DOM-ish stub, which is an object literal — `JSObject`, and the host's own
82
- * documented `a plain object` fallback was unreachable for exactly the shape it
83
- * names. `tagName` first, and `JSObject` treated as the absence of a GType.
68
+ * ORDER IS LOAD-BEARING, and it is measured rather than reasoned: under gjs 1.88.1
69
+ * `({}).constructor.$gtype` EXISTS and `GObject.type_name` calls it `JSObject`. A
70
+ * `$gtype`-first version would therefore name every object literal — and every DOM-ish
71
+ * stub, which is an object literal — `JSObject`, and the `a plain object` fallback would be
72
+ * unreachable for exactly the shape it names. `tagName` first, `JSObject` treated as the
73
+ * absence of a GType.
84
74
  */
85
75
  export declare function describeValue(value: unknown): string;
86
76
  /** Detach only — reversible. Frameworks move nodes; `remove` must not destroy one. */
@@ -88,30 +78,25 @@ export declare function remove(node: HostNode): void;
88
78
  /**
89
79
  * Destroy every child of `parent`, bounded.
90
80
  *
91
- * Exported because both adapters wrote their OWN `childSnapshot` to do exactly this
92
- * — byte-identical to each other, and both walking `next` raw. That is the one walk
93
- * the bound above exists to make unforgettable, and it is the walk where the
94
- * malformed link actually appears: `insert(node, parent, node)` is a defined DOM
95
- * no-op, so Solid's adjacent-swap fast path emits it against a CONTAINER's child
96
- * list. Unbounded, that is a hang rather than GTK's usual exit 0.
97
- *
98
- * This replaces a `clearContainer` that had no callers anywhere and DETACHED rather
99
- * than destroyed — the wrong half for every caller that wanted it, and a name React's
100
- * own HostConfig already uses for the destroying kind.
81
+ * Every walk over `next` goes through `childSnapshot` so the bound cannot be forgotten at a
82
+ * new call site, and this is the walk where the malformed link actually appears: `insert(
83
+ * node, parent, node)` is a defined DOM no-op, so Solid's adjacent-swap fast path emits it
84
+ * against a CONTAINER's child list. Unbounded, that is a hang rather than GTK's usual
85
+ * exit 0.
101
86
  */
102
87
  export declare function destroyChildren(parent: HostElement): void;
103
88
  /**
104
89
  * Disconnect a node's handlers WITHOUT touching the tree.
105
90
  *
106
- * The narrow half of `destroy`, and it exists because a framework can tell us "this
107
- * node is gone" while still holding its sibling links: Solid disposes a per-node
108
- * scope BEFORE its reconciler runs, and `reconcileArrays` opens with
109
- * `getNextSibling(last)`. Unlinking there made every trailing insertion append at
110
- * the end of the parent instead of before the marker.
91
+ * The narrow half of `destroy`, and it exists because a framework can tell us "this node is
92
+ * gone" while still holding its sibling links: Solid disposes a per-node scope BEFORE its
93
+ * reconciler runs, and `reconcileArrays` opens with `getNextSibling(last)`. Unlinking there
94
+ * made every trailing insertion append at the end of the parent instead of before the
95
+ * marker.
111
96
  *
112
- * The leak this closes is about handlers, not links — GJS blocks JS callbacks
113
- * during GC, so an undisconnected handler outlives its widget. The framework's own
114
- * `removeNode` still does the unlinking, in its own order.
97
+ * The leak this closes is about handlers, not links — GJS blocks JS callbacks during GC, so
98
+ * an undisconnected handler outlives its widget. The framework's own `removeNode` still
99
+ * does the unlinking, in its own order.
115
100
  */
116
101
  export declare function disconnectHandlers(el: HostElement): void;
117
102
  /**
@@ -128,43 +113,36 @@ export declare function destroy(node: HostNode): void;
128
113
  /**
129
114
  * Put a host tree inside an existing GTK container the application owns.
130
115
  *
131
- * The container is resolved through the SAME table as every other parent —
132
- * `nearestRegistered` walks the real type hierarchy, so an application's own
133
- * `GObject.registerClass` subclass inherits its ancestor's policy. Guessing a
134
- * method name here would be the generic `add` that GTK4 deliberately removed.
116
+ * The container is resolved through the SAME table as every other parent, so an
117
+ * application's own `GObject.registerClass` subclass inherits its ancestor's policy.
118
+ * Guessing a method name here would be the generic `add` that GTK4 deliberately removed.
135
119
  */
136
120
  export declare function mountRoot(el: HostElement, container: GObject.Object): void;
137
121
  /**
138
122
  * The materialised widget of a host node — for a `ref`, or for a test.
139
123
  *
140
- * ONE implementation. The Solid and React adapters each carried this function,
141
- * identical apart from two comment lines, and Vue carried none — so a Vue app had
142
- * no supported way to reach a widget at all and its only route was
143
- * `materialize()`, which is exactly the call the `destroyed` guard below exists to
144
- * intercept.
124
+ * ONE implementation, because a caller reaching `materialize()` directly is exactly what the
125
+ * `destroyed` guard below exists to intercept.
145
126
  *
146
- * The flag is exact and the obvious heuristic is not: `materialize` would happily
147
- * build a fresh, propertyless, unparented widget for a destroyed node — `destroy`
148
- * clears `props` and `layout` precisely so one cannot look re-materialisable — and
149
- * "no widget, not attached, no props" also describes a brand-new element.
127
+ * The flag is exact and the obvious heuristic is not: `materialize` would happily build a
128
+ * fresh, propertyless, unparented widget for a destroyed node — `destroy` clears `props`
129
+ * and `layout` precisely so one cannot look re-materialisable — and "no widget, not
130
+ * attached, no props" also describes a brand-new element.
150
131
  */
151
132
  export declare function widgetOf(node: HostNode): Gtk.Widget;
152
133
  /**
153
134
  * An off-screen container the host owns — the analogue of the DOM's detached `<div>`.
154
135
  *
155
- * `<KeepAlive>` and `<Suspense>` ask a renderer for one: `KeepAliveImpl.setup`
156
- * opens with `createElement("div")` and `SuspenseImpl` does the same for its
157
- * `hiddenContainer`. Forwarded as a TAG, "div" reached `lookupWidget` and threw
158
- * `unknown-tag` inside `callWithErrorHandling`, whose production arm is
159
- * `console.error` with no rethrow: `mount()` returned normally, the container held
160
- * zero children, GTK said nothing, exit 0.
161
- *
162
- * The Vue adapter answered that with `adopt(new Gtk.Box())` — the one runtime
163
- * `gi://` import and the one concrete widget class in any adapter, i.e. the widget
164
- * knowledge ADR 0027 § 7 forbids. Which widget backs it is the table's business
165
- * (`makeDetachedContainer`), and this op is what leaves the adapter with nothing to
166
- * know: the deactivated subtree is really unparented from the visible tree and
167
- * really held alive, so reactivating it moves the SAME widgets back.
136
+ * `<KeepAlive>` and `<Suspense>` ask a renderer for one: `KeepAliveImpl.setup` opens with
137
+ * `createElement("div")` and `SuspenseImpl` does the same for its `hiddenContainer`.
138
+ * Forwarded as a TAG, "div" reached `lookupWidget` and threw `unknown-tag` inside
139
+ * `callWithErrorHandling`, whose production arm is `console.error` with no rethrow:
140
+ * `mount()` returned normally, the container held zero children, GTK said nothing, exit 0.
141
+ *
142
+ * Which widget backs it is the table's business (`makeDetachedContainer`), and this op is
143
+ * what leaves the adapter with nothing to know — the widget knowledge ADR 0027 § 7 forbids.
144
+ * The deactivated subtree is really unparented from the visible tree and really held
145
+ * alive, so reactivating it moves the SAME widgets back.
168
146
  */
169
147
  export declare const createDetachedContainer: () => HostElement;
170
148
  /**
@@ -3,9 +3,9 @@ export { GtkHostError } from './errors.js';
3
3
  export { adopt, createAnchor, createDetachedContainer, createElement, createText, destroyChildren, destroy, disconnectHandlers, firstChild, insert, isHostElement, isText, materialize, mountRoot, nextSibling, parentNode, prevSibling, remove, setElementText, setEventHandler, setProp, setSlot, setText, widgetOf, } from './host.js';
4
4
  export { addressOf, isPortal, placementOf, portalOf, reorderMode } from './policies.js';
5
5
  export { toSignalName, isEventProp } from './signals.js';
6
- export { constructOnlyNames, isConstructOnly, isWritable, lookupEnumNick, paramSpecs, toPropertyName, } from './props.js';
6
+ export { constructOnlyNames, gtypeOfName, isConstructOnly, isWritable, lookupEnumNick, paramSpecs, toPropertyName, } from './props.js';
7
7
  export { assertInjective, tagOf } from './tags.js';
8
8
  export { blankReason, checkRendered, probeEnabled, runHostProbe, runHostProbeApp, shotEvidence, type CaptureWidget, type HostProbe, type HostProbeApp, type ProbeCheck, type ShotEvidence, } from './probe.js';
9
9
  export type { AccessibilityAttribute, ElementChild, ElementChildren, JsxAttributes, RawSignalAttributes, SlotAttribute, VueAttributes, WithOnce, } from './attrs.js';
10
- export { clearRegistry, hasWidget, lookupWidget, nearestRegistered, registerWidget, registerWidgets, registeredTags, } from './registry.js';
10
+ export { clearRegistry, classOf, hasClass, hasWidget, lookupWidget, nearestRegistered, registerWidget, registerWidgets, registeredTags, requireClass, type InstalledClass, } from './registry.js';
11
11
  export { ADW_DESCRIPTORS, BUILTIN_DESCRIPTORS, CURATED_DESCRIPTORS, GENERATED_PROVENANCE, GENERATED_WIDGETS, GTK_DESCRIPTORS, mergeGenerated, registerBuiltinWidgets, tableProvenance, } from './descriptors/index.js';