@gjsify/node-gi 0.64.1 → 0.64.4

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.
@@ -44,6 +44,25 @@
44
44
  /** @type {'nativescript'} */
45
45
  export const RUNTIME = 'nativescript';
46
46
 
47
+ /**
48
+ * Whether the addon may be loaded on first use instead of at import — see index.js.
49
+ * @type {boolean}
50
+ */
51
+ export const LAZY_NATIVE = true;
52
+
53
+ /**
54
+ * Whether `GObject.registerClass` may register a class whose JS parent is NOT a node-gi
55
+ * GObject class — `class MyLabel extends Label {}`, where `Label` is a
56
+ * `@nativescript/core` widget. GJS rejects that outright and so does node-gi on every
57
+ * other host; this is the ONE deliberate divergence, and it is deliberate because on
58
+ * NativeScript there is no GJS to be compatible with: node-gi is the only GI runtime that
59
+ * ever executes here, so a program written against this cannot be a program that behaves
60
+ * differently under GJS. See the FOREIGN-PARENT section in gi.js, ADR 0105 § 2 and
61
+ * docs/node-gi-gjs-surface.md.
62
+ * @type {boolean}
63
+ */
64
+ export const FOREIGN_PARENT_CLASSES = true;
65
+
47
66
  // Timers from the global scope: NativeScript has no node:timers, and there is no
48
67
  // GLib-backed replacement to avoid here. setImmediate is not a V8 builtin.
49
68
  export const setImmediate =
package/host.node.js CHANGED
@@ -55,6 +55,22 @@ export const RUNTIME =
55
55
  ? 'deno'
56
56
  : 'node';
57
57
 
58
+ /**
59
+ * Whether the addon may be loaded on first use instead of at import — see index.js.
60
+ * @type {boolean}
61
+ */
62
+ export const LAZY_NATIVE = false;
63
+
64
+ /**
65
+ * Whether `GObject.registerClass` may register a class whose JS parent is NOT a node-gi
66
+ * GObject class. FALSE here, and that is the point: on Node/Bun/Deno/GJS a program can be
67
+ * run under real GJS, so registerClass must keep throwing GJS's invalid-base error
68
+ * (refs/gjs/modules/core/overrides/GObject.js) rather than quietly accept a class GJS
69
+ * would reject. Only host.nativescript.js enables it — see the note there.
70
+ * @type {boolean}
71
+ */
72
+ export const FOREIGN_PARENT_CLASSES = false;
73
+
58
74
  // GJS host mode: `GjsifyNapi.init()` installs the loader as
59
75
  // `globalThis.__gjsifyNapiLoadAddon` and re-installs it if another consumer (the
60
76
  // @gjsify/napi L1) already captured + deleted it. Needs the GjsifyNapi typelib
package/index.d.ts CHANGED
@@ -542,6 +542,18 @@ export function emitSignal(handle: GObjectHandle, signalName: string, args?: unk
542
542
  /** Disconnect a previously connected signal handler. */
543
543
  export function disconnectSignal(handle: GObjectHandle, handlerId: number): void;
544
544
 
545
+ /**
546
+ * INTERNAL. Keep a callback alive for as long as `handle` is alive. Signal and
547
+ * template-callback GClosures hold their JS callback through a WEAK napi_ref, so
548
+ * a connected handler is not a GC root (which is what makes a handler capturing
549
+ * its own emitter collectable, as in GJS); this handle-keyed WeakMap is the
550
+ * ephemeron that keeps it callable meanwhile. `connectSignal` does it for you.
551
+ */
552
+ export function retainClosure(handle: GObjectHandle, key: number | symbol, fn: Function): void;
553
+
554
+ /** INTERNAL. Drop a closure retained by {@link retainClosure}. */
555
+ export function releaseClosure(handle: GObjectHandle, key: number | symbol): void;
556
+
545
557
  /**
546
558
  * Register the L1 resolver mapping a Gtk.Template `<signal handler="…">` handler
547
559
  * name to the instance's bound JS method (engine template-callback scope).
package/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  // a native package's JS entry is a loader, not a tsc artifact, and the repo
6
6
  // ignores `lib/`. The addon is Node-API (ABI-stable), so the same binary loads on
7
7
  // Node, Bun and Deno; runtime-specific behaviour is gated off RUNTIME below.
8
- import { RUNTIME, loadNativeHost } from '#host';
8
+ import { RUNTIME, loadNativeHost, LAZY_NATIVE } from '#host';
9
9
 
10
10
  /**
11
11
  * Which JS runtime we are on. Detection — and why each runtime gets the main-loop
@@ -17,121 +17,290 @@ export { RUNTIME };
17
17
  /** Whether we are on Node.js (the only runtime with the libuv main-loop bridge). */
18
18
  export const isNodeRuntime = RUNTIME === 'node';
19
19
 
20
- const native = loadNativeHost();
20
+ /**
21
+ * The native engine. Desktop runtimes load it HERE, while this module evaluates, exactly as
22
+ * before. A host that sets `LAZY_NATIVE` (NativeScript: dlopen of `libnode_gi.so` plus its
23
+ * closure costs real cold-start time) defers the load to the first `ensureNative()`, so an app
24
+ * that imports this package but never asks for GObject pays nothing. A failed load throws from
25
+ * `ensureNative()` every time it is asked — a selected real engine still fails loudly, there
26
+ * is no fallback here (ADR 0105 § 4).
27
+ * @type {Record<string, any>}
28
+ */
29
+ let native;
30
+ let loaded = false;
31
+ /** @type {Array<() => void>} */
32
+ const loadHooks = [];
33
+
34
+ /**
35
+ * Load the addon if that has not happened yet, and return it. The L1 layers call this before
36
+ * they touch any binding below.
37
+ * @returns {Record<string, any>}
38
+ */
39
+ export function ensureNative() {
40
+ if (loaded) return native;
41
+ const addon = loadNativeHost();
42
+ native = addon;
43
+ requireNamespace = addon.requireNamespace;
44
+ listInfoNames = addon.listInfoNames;
45
+ findInfo = addon.findInfo;
46
+ getConstantValue = addon.getConstantValue;
47
+ getEnumValues = addon.getEnumValues;
48
+ getErrorDomain = addon.getErrorDomain;
49
+ errorDomainInfo = addon.errorDomainInfo;
50
+ setErrorBuilder = addon.setErrorBuilder;
51
+ prependSearchPath = addon.prependSearchPath;
52
+ prependLibraryPath = addon.prependLibraryPath;
53
+ callFunction = addon.callFunction;
54
+ callMethod = addon.callMethod;
55
+ hasMethod = addon.hasMethod;
56
+ hasClassMethod = addon.hasClassMethod;
57
+ classMethodArity = addon.classMethodArity;
58
+ callStaticMethod = addon.callStaticMethod;
59
+ constructStruct = addon.constructStruct;
60
+ newObject = addon.newObject;
61
+ registerClass = addon.registerClass;
62
+ registerClassFromGType = addon.registerClassFromGType;
63
+ constructType = addon.constructType;
64
+ callParentVfunc = addon.callParentVfunc;
65
+ hasClassVfunc = addon.hasClassVfunc;
66
+ callClassVfunc = addon.callClassVfunc;
67
+ getTemplateChild = addon.getTemplateChild;
68
+ getProperty = addon.getProperty;
69
+ setProperty = addon.setProperty;
70
+ hasProperty = addon.hasProperty;
71
+ getTypeName = addon.getTypeName;
72
+ classInfoForTypeName = addon.classInfoForTypeName;
73
+ getGType = addon.getGType;
74
+ isInstanceOf = addon.isInstanceOf;
75
+ isGObjectHandle = addon.isGObjectHandle;
76
+ newGValue = addon.newGValue;
77
+ callBoxedMethod = addon.callBoxedMethod;
78
+ isBoxedHandle = addon.isBoxedHandle;
79
+ boxedMemberKind = addon.boxedMemberKind;
80
+ getBoxedField = addon.getBoxedField;
81
+ setBoxedField = addon.setBoxedField;
82
+ boxedTypeName = addon.boxedTypeName;
83
+ __boxedAddress = addon.__boxedAddress;
84
+ isParamSpecHandle = addon.isParamSpecHandle;
85
+ isFundamentalHandle = addon.isFundamentalHandle;
86
+ paramSpecProp = addon.paramSpecProp;
87
+ variantNew = addon.variantNew;
88
+ variantUnpack = addon.variantUnpack;
89
+ variantGetTypeString = addon.variantGetTypeString;
90
+ isVariantHandle = addon.isVariantHandle;
91
+ startMainLoop = addon.startMainLoop;
92
+ iterateMainContext = addon.iterateMainContext;
93
+ mainContextHasPending = addon.mainContextHasPending;
94
+ makePumpPendingCount = addon.makePumpPendingCount;
95
+ pumpKick = addon.pumpKick;
96
+ setMicrotaskDrain = addon.setMicrotaskDrain;
97
+ emitSignal = addon.emitSignal;
98
+ connectSignal = (handle, signalName, callback, after) => {
99
+ const id = addon.connectSignal(handle, signalName, callback, after);
100
+ retainClosure(handle, id, callback);
101
+ return id;
102
+ };
103
+ disconnectSignal = (handle, handlerId) => {
104
+ releaseClosure(handle, handlerId);
105
+ return addon.disconnectSignal(handle, handlerId);
106
+ };
107
+ setTemplateCallbackResolver = addon.setTemplateCallbackResolver;
108
+ setConstructCallback = addon.setConstructCallback;
109
+ setPropertySetCallback = addon.setPropertySetCallback;
110
+ storedPropertyNames = addon.storedPropertyNames;
111
+ logSetWriterFunc = addon.logSetWriterFunc;
112
+ logSetWriterDefault = addon.logSetWriterDefault;
113
+ bindPropertyFull = addon.bindPropertyFull;
114
+ bindingGroupBindFull = addon.bindingGroupBindFull;
115
+ setThreadLocale = addon.setThreadLocale;
116
+ textdomain = addon.textdomain;
117
+ bindtextdomain = addon.bindtextdomain;
118
+ localeCategories = addon.localeCategories;
119
+ probeHostOpenGL = addon.probeHostOpenGL;
120
+ currentGLStrings = addon.currentGLStrings;
121
+ loaded = true;
122
+ for (const hook of loadHooks.splice(0)) hook();
123
+ return native;
124
+ }
125
+
126
+ /** Whether the addon is loaded now — what the lazy host makes observable. */
127
+ export function isNativeLoaded() {
128
+ return loaded;
129
+ }
130
+
131
+ /**
132
+ * Run `hook` once the addon is loaded: now if it is, else straight after the first load. The
133
+ * L1 module-scope registrations (error builder, template resolver, …) go through this, so they
134
+ * run at import on desktop and at first use on a lazy host.
135
+ * @param {() => void} hook
136
+ */
137
+ export function onNativeLoaded(hook) {
138
+ if (loaded) hook();
139
+ else loadHooks.push(hook);
140
+ }
21
141
 
22
142
  // Straight re-exports of the native engine surface. Each contract — arguments,
23
143
  // ownership, the OUT/INOUT return-tuple convention, the keep-alive rules of the
24
144
  // main-loop group — is documented once in index.d.ts, the published type surface;
25
145
  // only what index.d.ts does not carry is noted here.
26
- export const requireNamespace = native.requireNamespace;
27
- export const listInfoNames = native.listInfoNames;
28
- export const findInfo = native.findInfo;
29
- export const getConstantValue = native.getConstantValue;
30
- export const getEnumValues = native.getEnumValues;
31
- export const getErrorDomain = native.getErrorDomain;
32
- export const errorDomainInfo = native.errorDomainInfo;
33
- export const setErrorBuilder = native.setErrorBuilder;
34
- export const prependSearchPath = native.prependSearchPath;
35
- export const prependLibraryPath = native.prependLibraryPath;
36
- export const callFunction = native.callFunction;
37
- export const callMethod = native.callMethod;
38
- export const hasMethod = native.hasMethod;
39
- export const hasClassMethod = native.hasClassMethod;
40
- export const classMethodArity = native.classMethodArity;
41
- export const callStaticMethod = native.callStaticMethod;
42
- export const constructStruct = native.constructStruct;
43
- export const newObject = native.newObject;
44
- export const registerClass = native.registerClass;
146
+ export let requireNamespace;
147
+ export let listInfoNames;
148
+ export let findInfo;
149
+ export let getConstantValue;
150
+ export let getEnumValues;
151
+ export let getErrorDomain;
152
+ export let errorDomainInfo;
153
+ export let setErrorBuilder;
154
+ export let prependSearchPath;
155
+ export let prependLibraryPath;
156
+ export let callFunction;
157
+ export let callMethod;
158
+ export let hasMethod;
159
+ export let hasClassMethod;
160
+ export let classMethodArity;
161
+ export let callStaticMethod;
162
+ export let constructStruct;
163
+ export let newObject;
164
+ export let registerClass;
45
165
 
46
166
  // Subclass an ALREADY-REGISTERED parent by its GType handle instead of a
47
167
  // `namespace.typeName`: a registered (dynamic) parent has no introspection entry, so it
48
168
  // cannot be resolved by name. Custom properties, signals and vfunc slots of registered
49
169
  // ancestors compose through ordinary GObject inheritance. The L1 `GObject.registerClass`
50
170
  // picks this variant when the nearest base is itself a registered class.
51
- export const registerClassFromGType = native.registerClassFromGType;
52
-
53
- export const constructType = native.constructType;
54
- export const callParentVfunc = native.callParentVfunc;
55
- export const hasClassVfunc = native.hasClassVfunc;
56
- export const callClassVfunc = native.callClassVfunc;
57
- export const getTemplateChild = native.getTemplateChild;
58
- export const getProperty = native.getProperty;
59
- export const setProperty = native.setProperty;
60
- export const hasProperty = native.hasProperty;
61
- export const getTypeName = native.getTypeName;
62
- export const classInfoForTypeName = native.classInfoForTypeName;
63
- export const getGType = native.getGType;
64
- export const isInstanceOf = native.isInstanceOf;
65
- export const isGObjectHandle = native.isGObjectHandle;
66
- export const newGValue = native.newGValue;
67
- export const callBoxedMethod = native.callBoxedMethod;
68
- export const isBoxedHandle = native.isBoxedHandle;
69
- export const boxedMemberKind = native.boxedMemberKind;
70
- export const getBoxedField = native.getBoxedField;
71
- export const setBoxedField = native.setBoxedField;
72
- export const boxedTypeName = native.boxedTypeName;
171
+ export let registerClassFromGType;
172
+
173
+ export let constructType;
174
+ export let callParentVfunc;
175
+ export let hasClassVfunc;
176
+ export let callClassVfunc;
177
+ export let getTemplateChild;
178
+ export let getProperty;
179
+ export let setProperty;
180
+ export let hasProperty;
181
+ export let getTypeName;
182
+ export let classInfoForTypeName;
183
+ export let getGType;
184
+ export let isInstanceOf;
185
+ export let isGObjectHandle;
186
+ export let newGValue;
187
+ export let callBoxedMethod;
188
+ export let isBoxedHandle;
189
+ export let boxedMemberKind;
190
+ export let getBoxedField;
191
+ export let setBoxedField;
192
+ export let boxedTypeName;
73
193
  /** TEST-ONLY: the address a boxed handle wraps, so an ownership rule about pointer
74
194
  * IDENTITY can be asserted instead of waited on (see marshal.cc BoxedAddress). */
75
- export const __boxedAddress = native.__boxedAddress;
76
- export const isParamSpecHandle = native.isParamSpecHandle;
195
+ export let __boxedAddress;
196
+ export let isParamSpecHandle;
77
197
 
78
198
  // True for a non-GObject GObject-fundamental handle (a GskRenderNode from
79
199
  // Gtk.Snapshot.to_node, a GdkEvent): introspected as object info, but ref-counted
80
200
  // through its own ref/unref funcs, NOT g_object_ref. L1 surfaces it as an opaque,
81
201
  // round-trippable pass-through handle.
82
- export const isFundamentalHandle = native.isFundamentalHandle;
83
-
84
- export const paramSpecProp = native.paramSpecProp;
85
- export const variantNew = native.variantNew;
86
- export const variantUnpack = native.variantUnpack;
87
- export const variantGetTypeString = native.variantGetTypeString;
88
- export const isVariantHandle = native.isVariantHandle;
89
- export const startMainLoop = native.startMainLoop;
90
- export const iterateMainContext = native.iterateMainContext;
91
- export const mainContextHasPending = native.mainContextHasPending;
92
- export const makePumpPendingCount = native.makePumpPendingCount;
93
- export const pumpKick = native.pumpKick;
94
- export const setMicrotaskDrain = native.setMicrotaskDrain;
95
- export const connectSignal = native.connectSignal;
96
- export const emitSignal = native.emitSignal;
97
- export const disconnectSignal = native.disconnectSignal;
98
- export const setTemplateCallbackResolver = native.setTemplateCallbackResolver;
202
+ export let isFundamentalHandle;
203
+
204
+ export let paramSpecProp;
205
+ export let variantNew;
206
+ export let variantUnpack;
207
+ export let variantGetTypeString;
208
+ export let isVariantHandle;
209
+ export let startMainLoop;
210
+ export let iterateMainContext;
211
+ export let mainContextHasPending;
212
+ export let makePumpPendingCount;
213
+ export let pumpKick;
214
+ export let setMicrotaskDrain;
215
+ // ---- signal-closure keep-alive (GJS's associate_closure, built in JS) ----
216
+ //
217
+ // A signal / template-callback GClosure holds its JS callback through a WEAK
218
+ // napi_ref (src/signals.cc), so a C-held closure is NOT a GC root. That is what
219
+ // makes `obj.connect('x', () => obj.y)` collectable instead of immortal, and it
220
+ // is GJS's design: its signal closures keep the callable unrooted and TRACE it
221
+ // from the owning wrapper (ObjectInstance::m_closures).
222
+ //
223
+ // node-gi has no tracing hook into V8, so the same edge is expressed as the one
224
+ // ephemeron a non-extensible External can take part in: a WeakMap keyed by the
225
+ // canonical handle. Handle reachable (JS holds the wrapper, or C owns the object
226
+ // and the toggle ref roots the handle) → callback reachable → a connected handler
227
+ // still fires. Handle reachable ONLY through the callback it retains → key and
228
+ // value die together, which is exactly the cycle that used to leak.
229
+ //
230
+ // Pruned on disconnect. A handler disconnected by a route that bypasses
231
+ // `disconnectSignal` (the introspected GObject.signal_handler_disconnect) leaves
232
+ // its entry behind until the handle itself goes — harmless, the closure is
233
+ // already gone, and the same divergence the gi.js handler-id registry documents.
234
+ const retainedClosures = new WeakMap();
235
+
236
+ /**
237
+ * Keep `fn` alive for as long as `handle` is alive. INTERNAL — the JS half of the
238
+ * weak closure refs in src/signals.cc.
239
+ * @param {object} handle canonical GObject handle (the WeakMap key / lifetime)
240
+ * @param {number | symbol} key handler id, or a unique key for a non-signal closure
241
+ * @param {Function} fn the callback the native closure holds weakly
242
+ */
243
+ export function retainClosure(handle, key, fn) {
244
+ let byKey = retainedClosures.get(handle);
245
+ if (byKey === undefined) {
246
+ byKey = new Map();
247
+ retainedClosures.set(handle, byKey);
248
+ }
249
+ byKey.set(key, fn);
250
+ }
251
+
252
+ /**
253
+ * Drop a retained closure (on disconnect). INTERNAL — see retainClosure.
254
+ * @param {object} handle canonical GObject handle
255
+ * @param {number | symbol} key the key it was retained under
256
+ */
257
+ export function releaseClosure(handle, key) {
258
+ retainedClosures.get(handle)?.delete(key);
259
+ }
260
+
261
+ export let connectSignal;
262
+ export let emitSignal;
263
+ export let disconnectSignal;
264
+ export let setTemplateCallbackResolver;
99
265
 
100
266
  // Registers the L1 callback the engine's overridden `constructor` vfunc invokes to run a
101
267
  // registered class's JS constructor for a GObject that C instantiated (a GtkBuilder
102
268
  // composite-template InternalChild): given (instanceHandle, gtypeName) it
103
269
  // Reflect.constructs the class in adopt mode — see gi.js runCtorForCObject.
104
- export const setConstructCallback = native.setConstructCallback;
270
+ export let setConstructCallback;
105
271
 
106
272
  // Registers the L1 callback the engine's set_property vfunc invokes after storing a
107
273
  // custom property: given (instanceHandle, propertyName) it runs the class's own JS
108
274
  // setter — see gi.js runJsPropertySetter. The engine calls it only once the instance
109
275
  // HAS a wrapper, so a construct-time set never reaches it; those are replayed from
110
276
  // the base ctor over storedPropertyNames.
111
- export const setPropertySetCallback = native.setPropertySetCallback;
277
+ export let setPropertySetCallback;
112
278
 
113
279
  // The custom properties actually SET on an instance (the per-instance store's keys) —
114
280
  // what that construct-time replay has to push through the class's JS setters.
115
- export const storedPropertyNames = native.storedPropertyNames;
281
+ export let storedPropertyNames;
116
282
 
117
- export const logSetWriterFunc = native.logSetWriterFunc;
118
- export const logSetWriterDefault = native.logSetWriterDefault;
119
- export const bindPropertyFull = native.bindPropertyFull;
120
- export const bindingGroupBindFull = native.bindingGroupBindFull;
283
+ export let logSetWriterFunc;
284
+ export let logSetWriterDefault;
285
+ export let bindPropertyFull;
286
+ export let bindingGroupBindFull;
121
287
 
122
288
  // The libintl half of GjsPrivate — the binders GLib's GIR does not publish, plus
123
289
  // the LC_* constants read from the host's own <locale.h>. Consumed by
124
290
  // `./gettext.js`; loading THIS module is also what puts the process in the
125
291
  // environment's locale (the addon's Init does it, as gjs's entry point does).
126
- export const setThreadLocale = native.setThreadLocale;
127
- export const textdomain = native.textdomain;
128
- export const bindtextdomain = native.bindtextdomain;
129
- export const localeCategories = native.localeCategories;
292
+ export let setThreadLocale;
293
+ export let textdomain;
294
+ export let bindtextdomain;
295
+ export let localeCategories;
130
296
 
131
297
  // win32 only (undefined elsewhere): which OpenGL the process got — see gtk-runtime.js
132
298
  // activateBundledOpenGL. currentGLStrings asks the implementation behind the context
133
299
  // current on this thread, so a caller can tell Mesa from a vendor driver.
134
- export const probeHostOpenGL = native.probeHostOpenGL;
135
- export const currentGLStrings = native.currentGLStrings;
300
+ export let probeHostOpenGL;
301
+ export let currentGLStrings;
302
+
303
+ export { native as default };
136
304
 
137
- export default native;
305
+ // Last, so every `export let` above is initialised before the eager load assigns it.
306
+ if (!LAZY_NATIVE) ensureNative();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/node-gi",
3
- "version": "0.64.1",
3
+ "version": "0.64.4",
4
4
  "description": "GObject-Introspection runtime for Node.js — load gi:// namespaces (GLib/GObject/Gio/…) with GJS-compatible semantics. Vendored + rewritten from node-gtk under MIT.",
5
5
  "type": "module",
6
6
  "license": "MIT",
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/src/cairo.cc CHANGED
@@ -621,6 +621,11 @@ void InitCairo(Napi::Env env, Napi::Object exports) {
621
621
  c.Set("paint", Napi::Function::New(env, CTX_0(cairo_paint)));
622
622
  c.Set("clip", Napi::Function::New(env, CTX_0(cairo_clip)));
623
623
  c.Set("clipPreserve", Napi::Function::New(env, CTX_0(cairo_clip_preserve)));
624
+ c.Set("resetClip", Napi::Function::New(env, CTX_0(cairo_reset_clip)));
625
+ c.Set("pushGroup", Napi::Function::New(env, CTX_0(cairo_push_group)));
626
+ c.Set("popGroupToSource", Napi::Function::New(env, CTX_0(cairo_pop_group_to_source)));
627
+ c.Set("copyPage", Napi::Function::New(env, CTX_0(cairo_copy_page)));
628
+ c.Set("showPage", Napi::Function::New(env, CTX_0(cairo_show_page)));
624
629
 
625
630
  c.Set("setLineWidth", Napi::Function::New(env, CTX_1D(cairo_set_line_width)));
626
631
  c.Set("paintWithAlpha", Napi::Function::New(env, CTX_1D(cairo_paint_with_alpha)));
@@ -641,7 +646,10 @@ void InitCairo(Napi::Env env, Napi::Object exports) {
641
646
  c.Set("arc", Napi::Function::New(env, CTX_5D(cairo_arc)));
642
647
  c.Set("arcNegative", Napi::Function::New(env, CTX_5D(cairo_arc_negative)));
643
648
  c.Set("curveTo", Napi::Function::New(env, CTX_6D(cairo_curve_to)));
649
+ c.Set("relCurveTo", Napi::Function::New(env, CTX_6D(cairo_rel_curve_to)));
644
650
 
651
+ c.Set("pushGroupWithContent",
652
+ Napi::Function::New(env, CTX_ENUM(cairo_push_group_with_content, cairo_content_t)));
645
653
  c.Set("setOperator", Napi::Function::New(env, CTX_ENUM(cairo_set_operator, cairo_operator_t)));
646
654
  c.Set("setLineCap", Napi::Function::New(env, CTX_ENUM(cairo_set_line_cap, cairo_line_cap_t)));
647
655
  c.Set("setLineJoin", Napi::Function::New(env, CTX_ENUM(cairo_set_line_join, cairo_line_join_t)));
@@ -798,6 +806,45 @@ void InitCairo(Napi::Env env, Napi::Object exports) {
798
806
  o.Set("maxYAdvance", Napi::Number::New(env, e.max_y_advance));
799
807
  return o;
800
808
  }));
809
+ // The font matrix is a plain `{xx, yx, xy, yy, x0, y0}` object in GJS — cairo's
810
+ // matrix type has no wrapper there (getMatrix/setMatrix stay unbound for that
811
+ // reason), so the font matrix travels as the six doubles.
812
+ c.Set("getFontMatrix", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
813
+ Napi::Env env = info.Env();
814
+ cairo_t* cr = CtxArg(env, info);
815
+ if (cr == nullptr) return env.Undefined();
816
+ cairo_matrix_t m;
817
+ cairo_get_font_matrix(cr, &m);
818
+ if (!CheckStatus(env, cairo_status(cr), "context")) return env.Undefined();
819
+ Napi::Object o = Napi::Object::New(env);
820
+ o.Set("xx", Napi::Number::New(env, m.xx));
821
+ o.Set("yx", Napi::Number::New(env, m.yx));
822
+ o.Set("xy", Napi::Number::New(env, m.xy));
823
+ o.Set("yy", Napi::Number::New(env, m.yy));
824
+ o.Set("x0", Napi::Number::New(env, m.x0));
825
+ o.Set("y0", Napi::Number::New(env, m.y0));
826
+ return o;
827
+ }));
828
+ c.Set("setFontMatrix", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
829
+ Napi::Env env = info.Env();
830
+ cairo_t* cr = CtxArg(env, info);
831
+ if (cr == nullptr) return env.Undefined();
832
+ if (!info[1].IsObject()) {
833
+ Napi::TypeError::New(env, "matrix must be an object").ThrowAsJavaScriptException();
834
+ return env.Undefined();
835
+ }
836
+ Napi::Object o = info[1].As<Napi::Object>();
837
+ cairo_matrix_t m;
838
+ m.xx = o.Get("xx").ToNumber().DoubleValue();
839
+ m.yx = o.Get("yx").ToNumber().DoubleValue();
840
+ m.xy = o.Get("xy").ToNumber().DoubleValue();
841
+ m.yy = o.Get("yy").ToNumber().DoubleValue();
842
+ m.x0 = o.Get("x0").ToNumber().DoubleValue();
843
+ m.y0 = o.Get("y0").ToNumber().DoubleValue();
844
+ cairo_set_font_matrix(cr, &m);
845
+ CheckStatus(env, cairo_status(cr), "context");
846
+ return env.Undefined();
847
+ }));
801
848
  c.Set("status", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
802
849
  Napi::Env env = info.Env();
803
850
  cairo_t* cr = CtxArg(env, info);
@@ -824,6 +871,26 @@ void InitCairo(Napi::Env env, Napi::Object exports) {
824
871
  CheckStatus(env, cairo_status(cr), "context");
825
872
  return env.Undefined();
826
873
  }));
874
+ c.Set("mask", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
875
+ Napi::Env env = info.Env();
876
+ cairo_t* cr = CtxArg(env, info);
877
+ if (cr == nullptr) return env.Undefined();
878
+ cairo_pattern_t* p = PatternFromExt(env, info[1]);
879
+ if (p == nullptr) return env.Undefined();
880
+ cairo_mask(cr, p);
881
+ CheckStatus(env, cairo_status(cr), "context");
882
+ return env.Undefined();
883
+ }));
884
+ c.Set("maskSurface", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
885
+ Napi::Env env = info.Env();
886
+ cairo_t* cr = CtxArg(env, info);
887
+ if (cr == nullptr) return env.Undefined();
888
+ cairo_surface_t* s = SurfaceFromExt(env, info[1]);
889
+ if (s == nullptr) return env.Undefined();
890
+ cairo_mask_surface(cr, s, Num(info, 2), Num(info, 3));
891
+ CheckStatus(env, cairo_status(cr), "context");
892
+ return env.Undefined();
893
+ }));
827
894
 
828
895
  c.Set("identityMatrix", Napi::Function::New(env, CTX_0(cairo_identity_matrix)));
829
896
  c.Set("newSubPath", Napi::Function::New(env, CTX_0(cairo_new_sub_path)));
@@ -970,6 +1037,35 @@ void InitCairo(Napi::Env env, Napi::Object exports) {
970
1037
  return MakeCairoHandle(env, p, CairoKind::Pattern);
971
1038
  }));
972
1039
 
1040
+ // popGroup: cairo_pop_group returns a NEW pattern (transfer-full) — the handle
1041
+ // adopts that ref directly, no extra reference (gjs wraps and then drops its own).
1042
+ c.Set("popGroup", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
1043
+ Napi::Env env = info.Env();
1044
+ cairo_t* cr = CtxArg(env, info);
1045
+ if (cr == nullptr) return env.Undefined();
1046
+ cairo_pattern_t* p = cairo_pop_group(cr);
1047
+ if (!CheckStatus(env, cairo_status(cr), "context")) {
1048
+ cairo_pattern_destroy(p);
1049
+ return env.Undefined();
1050
+ }
1051
+ return MakeCairoHandle(env, p, CairoKind::Pattern);
1052
+ }));
1053
+
1054
+ // getTarget/getGroupTarget: the surface belongs to the context — ref it so the
1055
+ // JS wrapper owns one (mirrors CairoSurface::from_c_ptr's kept reference).
1056
+ #define CTX_SURFACE_GETTER(NAME, CFN) \
1057
+ c.Set(NAME, Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value { \
1058
+ Napi::Env env = info.Env(); \
1059
+ cairo_t* cr = CtxArg(env, info); \
1060
+ if (cr == nullptr) return env.Undefined(); \
1061
+ cairo_surface_t* s = CFN(cr); \
1062
+ if (!CheckStatus(env, cairo_status(cr), "context")) return env.Undefined(); \
1063
+ cairo_surface_reference(s); \
1064
+ return MakeCairoHandle(env, s, CairoKind::Surface); \
1065
+ }));
1066
+ CTX_SURFACE_GETTER("getTarget", cairo_get_target)
1067
+ CTX_SURFACE_GETTER("getGroupTarget", cairo_get_group_target)
1068
+
973
1069
  // ---- $dispose (Context / Surface / Pattern / Path) ----
974
1070
  // Eagerly destroy the underlying cairo object + mark the handle so the finalizer
975
1071
  // won't double-free and any further method call throws (a live-handle check).
package/src/class.cc CHANGED
@@ -44,6 +44,8 @@ static GType UnwrapGType(Napi::Env env, Napi::Value v) {
44
44
  static GType TypeNameToGType(const std::string& t) {
45
45
  if (t == "string" || t == "utf8") return G_TYPE_STRING;
46
46
  if (t == "boolean" || t == "bool") return G_TYPE_BOOLEAN;
47
+ if (t == "char") return G_TYPE_CHAR;
48
+ if (t == "uchar") return G_TYPE_UCHAR;
47
49
  if (t == "int") return G_TYPE_INT;
48
50
  if (t == "uint") return G_TYPE_UINT;
49
51
  if (t == "int64") return G_TYPE_INT64;
@@ -95,6 +97,19 @@ static GParamSpec* BuildParamSpec(Napi::Env env, Napi::Object spec, std::string*
95
97
  : (def.IsNumber() ? NodeGiToBool(def) : FALSE);
96
98
  return g_param_spec_boolean(nm, nm, nm, d, flags);
97
99
  }
100
+ if (type == "char") {
101
+ return g_param_spec_char(nm, nm, nm, hasMin ? static_cast<gint8>(mn) : G_MININT8,
102
+ hasMax ? static_cast<gint8>(mx) : G_MAXINT8,
103
+ def.IsNumber() ? static_cast<gint8>(def.As<Napi::Number>().Int32Value()) : 0,
104
+ flags);
105
+ }
106
+ if (type == "uchar") {
107
+ return g_param_spec_uchar(nm, nm, nm, hasMin ? static_cast<guint8>(mn) : 0,
108
+ hasMax ? static_cast<guint8>(mx) : G_MAXUINT8,
109
+ def.IsNumber() ? static_cast<guint8>(def.As<Napi::Number>().Uint32Value())
110
+ : 0,
111
+ flags);
112
+ }
98
113
  if (type == "int") {
99
114
  return g_param_spec_int(nm, nm, nm, hasMin ? static_cast<gint>(mn) : G_MININT,
100
115
  hasMax ? static_cast<gint>(mx) : G_MAXINT,