@gjsify/node-gi 0.64.0 → 0.64.2

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/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,237 @@ 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
+ connectSignal = addon.connectSignal;
98
+ emitSignal = addon.emitSignal;
99
+ disconnectSignal = addon.disconnectSignal;
100
+ setTemplateCallbackResolver = addon.setTemplateCallbackResolver;
101
+ setConstructCallback = addon.setConstructCallback;
102
+ setPropertySetCallback = addon.setPropertySetCallback;
103
+ storedPropertyNames = addon.storedPropertyNames;
104
+ logSetWriterFunc = addon.logSetWriterFunc;
105
+ logSetWriterDefault = addon.logSetWriterDefault;
106
+ bindPropertyFull = addon.bindPropertyFull;
107
+ bindingGroupBindFull = addon.bindingGroupBindFull;
108
+ setThreadLocale = addon.setThreadLocale;
109
+ textdomain = addon.textdomain;
110
+ bindtextdomain = addon.bindtextdomain;
111
+ localeCategories = addon.localeCategories;
112
+ probeHostOpenGL = addon.probeHostOpenGL;
113
+ currentGLStrings = addon.currentGLStrings;
114
+ loaded = true;
115
+ for (const hook of loadHooks.splice(0)) hook();
116
+ return native;
117
+ }
118
+
119
+ /** Whether the addon is loaded now — what the lazy host makes observable. */
120
+ export function isNativeLoaded() {
121
+ return loaded;
122
+ }
123
+
124
+ /**
125
+ * Run `hook` once the addon is loaded: now if it is, else straight after the first load. The
126
+ * L1 module-scope registrations (error builder, template resolver, …) go through this, so they
127
+ * run at import on desktop and at first use on a lazy host.
128
+ * @param {() => void} hook
129
+ */
130
+ export function onNativeLoaded(hook) {
131
+ if (loaded) hook();
132
+ else loadHooks.push(hook);
133
+ }
21
134
 
22
135
  // Straight re-exports of the native engine surface. Each contract — arguments,
23
136
  // ownership, the OUT/INOUT return-tuple convention, the keep-alive rules of the
24
137
  // main-loop group — is documented once in index.d.ts, the published type surface;
25
138
  // 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;
139
+ export let requireNamespace;
140
+ export let listInfoNames;
141
+ export let findInfo;
142
+ export let getConstantValue;
143
+ export let getEnumValues;
144
+ export let getErrorDomain;
145
+ export let errorDomainInfo;
146
+ export let setErrorBuilder;
147
+ export let prependSearchPath;
148
+ export let prependLibraryPath;
149
+ export let callFunction;
150
+ export let callMethod;
151
+ export let hasMethod;
152
+ export let hasClassMethod;
153
+ export let classMethodArity;
154
+ export let callStaticMethod;
155
+ export let constructStruct;
156
+ export let newObject;
157
+ export let registerClass;
45
158
 
46
159
  // Subclass an ALREADY-REGISTERED parent by its GType handle instead of a
47
160
  // `namespace.typeName`: a registered (dynamic) parent has no introspection entry, so it
48
161
  // cannot be resolved by name. Custom properties, signals and vfunc slots of registered
49
162
  // ancestors compose through ordinary GObject inheritance. The L1 `GObject.registerClass`
50
163
  // 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;
164
+ export let registerClassFromGType;
165
+
166
+ export let constructType;
167
+ export let callParentVfunc;
168
+ export let hasClassVfunc;
169
+ export let callClassVfunc;
170
+ export let getTemplateChild;
171
+ export let getProperty;
172
+ export let setProperty;
173
+ export let hasProperty;
174
+ export let getTypeName;
175
+ export let classInfoForTypeName;
176
+ export let getGType;
177
+ export let isInstanceOf;
178
+ export let isGObjectHandle;
179
+ export let newGValue;
180
+ export let callBoxedMethod;
181
+ export let isBoxedHandle;
182
+ export let boxedMemberKind;
183
+ export let getBoxedField;
184
+ export let setBoxedField;
185
+ export let boxedTypeName;
73
186
  /** TEST-ONLY: the address a boxed handle wraps, so an ownership rule about pointer
74
187
  * IDENTITY can be asserted instead of waited on (see marshal.cc BoxedAddress). */
75
- export const __boxedAddress = native.__boxedAddress;
76
- export const isParamSpecHandle = native.isParamSpecHandle;
188
+ export let __boxedAddress;
189
+ export let isParamSpecHandle;
77
190
 
78
191
  // True for a non-GObject GObject-fundamental handle (a GskRenderNode from
79
192
  // Gtk.Snapshot.to_node, a GdkEvent): introspected as object info, but ref-counted
80
193
  // through its own ref/unref funcs, NOT g_object_ref. L1 surfaces it as an opaque,
81
194
  // 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;
195
+ export let isFundamentalHandle;
196
+
197
+ export let paramSpecProp;
198
+ export let variantNew;
199
+ export let variantUnpack;
200
+ export let variantGetTypeString;
201
+ export let isVariantHandle;
202
+ export let startMainLoop;
203
+ export let iterateMainContext;
204
+ export let mainContextHasPending;
205
+ export let makePumpPendingCount;
206
+ export let pumpKick;
207
+ export let setMicrotaskDrain;
208
+ export let connectSignal;
209
+ export let emitSignal;
210
+ export let disconnectSignal;
211
+ export let setTemplateCallbackResolver;
99
212
 
100
213
  // Registers the L1 callback the engine's overridden `constructor` vfunc invokes to run a
101
214
  // registered class's JS constructor for a GObject that C instantiated (a GtkBuilder
102
215
  // composite-template InternalChild): given (instanceHandle, gtypeName) it
103
216
  // Reflect.constructs the class in adopt mode — see gi.js runCtorForCObject.
104
- export const setConstructCallback = native.setConstructCallback;
217
+ export let setConstructCallback;
105
218
 
106
219
  // Registers the L1 callback the engine's set_property vfunc invokes after storing a
107
220
  // custom property: given (instanceHandle, propertyName) it runs the class's own JS
108
221
  // setter — see gi.js runJsPropertySetter. The engine calls it only once the instance
109
222
  // HAS a wrapper, so a construct-time set never reaches it; those are replayed from
110
223
  // the base ctor over storedPropertyNames.
111
- export const setPropertySetCallback = native.setPropertySetCallback;
224
+ export let setPropertySetCallback;
112
225
 
113
226
  // The custom properties actually SET on an instance (the per-instance store's keys) —
114
227
  // what that construct-time replay has to push through the class's JS setters.
115
- export const storedPropertyNames = native.storedPropertyNames;
228
+ export let storedPropertyNames;
116
229
 
117
- export const logSetWriterFunc = native.logSetWriterFunc;
118
- export const logSetWriterDefault = native.logSetWriterDefault;
119
- export const bindPropertyFull = native.bindPropertyFull;
120
- export const bindingGroupBindFull = native.bindingGroupBindFull;
230
+ export let logSetWriterFunc;
231
+ export let logSetWriterDefault;
232
+ export let bindPropertyFull;
233
+ export let bindingGroupBindFull;
121
234
 
122
235
  // The libintl half of GjsPrivate — the binders GLib's GIR does not publish, plus
123
236
  // the LC_* constants read from the host's own <locale.h>. Consumed by
124
237
  // `./gettext.js`; loading THIS module is also what puts the process in the
125
238
  // 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;
239
+ export let setThreadLocale;
240
+ export let textdomain;
241
+ export let bindtextdomain;
242
+ export let localeCategories;
130
243
 
131
244
  // win32 only (undefined elsewhere): which OpenGL the process got — see gtk-runtime.js
132
245
  // activateBundledOpenGL. currentGLStrings asks the implementation behind the context
133
246
  // 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;
247
+ export let probeHostOpenGL;
248
+ export let currentGLStrings;
249
+
250
+ export { native as default };
136
251
 
137
- export default native;
252
+ // Last, so every `export let` above is initialised before the eager load assigns it.
253
+ if (!LAZY_NATIVE) ensureNative();
@@ -10,16 +10,8 @@
10
10
  // throws; this is the same wrapper, so `try { … } catch` means the same on either runtime
11
11
  // (ADR 0099 conformance row `errors`).
12
12
  //
13
- // Two deliberate differences from the GJS override:
14
- // • the constructor check is a Proxy `construct` trap — a node-gi class has no `_init` to wrap;
15
- // • `set_int` / `set_uint` range-check their value, which in GJS is the argument marshaller's
16
- // job. node-gi's marshaller wraps modulo 2^32 (`set_uint('k', -1)` stores 4294967295), a
17
- // general divergence recorded in status/open-todos/node-gi.md; this keeps the settings
18
- // surface honest without touching the addon, so it also holds in an already-built payload.
19
-
20
- const INT32_MIN = -(2 ** 31);
21
- const INT32_MAX = 2 ** 31 - 1;
22
- const UINT32_MAX = 2 ** 32 - 1;
13
+ // One deliberate difference from the GJS override: the constructor check is a Proxy `construct`
14
+ // trap — a node-gi class has no `_init` to wrap.
23
15
 
24
16
  const CHECKED = [
25
17
  'get_boolean',
@@ -53,12 +45,6 @@ const CHECKED = [
53
45
  'reset',
54
46
  ];
55
47
 
56
- /** The range a setter's JS number must be in; GJS's marshaller throws this exact message. */
57
- const RANGES = {
58
- set_int: [INT32_MIN, INT32_MAX, 'int32'],
59
- set_uint: [0, UINT32_MAX, 'uint32'],
60
- };
61
-
62
48
  /**
63
49
  * Patch `Gio.Settings` / `Gio.SettingsSchema` like GJS does, once, and return the class to hand
64
50
  * out (a Proxy whose `new` refuses a schema that is not installed instead of aborting).
@@ -87,16 +73,8 @@ export function createGioSettings(Gio) {
87
73
  for (const name of CHECKED) {
88
74
  const real = proto[name];
89
75
  if (typeof real !== 'function') continue;
90
- const range = RANGES[name];
91
76
  proto[name] = function (key, ...args) {
92
77
  checkKey(this, key);
93
- if (range !== undefined) {
94
- const [min, max, type] = range;
95
- const value = args[0];
96
- if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > max) {
97
- throw new Error(`Argument value: value is out of range for ${type}`);
98
- }
99
- }
100
78
  return real.call(this, key, ...args);
101
79
  };
102
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/node-gi",
3
- "version": "0.64.0",
3
+ "version": "0.64.2",
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
@@ -17,7 +17,8 @@
17
17
  // WrapBoxed; cairo_path_t is NOT ref-counted, so a borrowed path is COPIED via
18
18
  // the gjs CairoPath::copy_ptr dummy-context trick instead of ref'd). Slice:
19
19
  // Context (drawing ops, path copy/append, dash, matrix point transforms,
20
- // in-fill/in-stroke, getSource), Surface + ImageSurface (headless pixels),
20
+ // in-fill/in-stroke, getSource, the toy-text API: selectFontFace/setFontSize/
21
+ // showText/textPath/textExtents/fontExtents), Surface + ImageSurface (headless pixels),
21
22
  // SolidPattern, LinearGradient/RadialGradient (color stops), SurfacePattern
22
23
  // (extend/filter), Path handles. Deferred: region, PDF/SVG/PS surfaces,
23
24
  // text/font ops (showText/selectFontFace — canvas2d text rides PangoCairo).
@@ -620,6 +621,11 @@ void InitCairo(Napi::Env env, Napi::Object exports) {
620
621
  c.Set("paint", Napi::Function::New(env, CTX_0(cairo_paint)));
621
622
  c.Set("clip", Napi::Function::New(env, CTX_0(cairo_clip)));
622
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)));
623
629
 
624
630
  c.Set("setLineWidth", Napi::Function::New(env, CTX_1D(cairo_set_line_width)));
625
631
  c.Set("paintWithAlpha", Napi::Function::New(env, CTX_1D(cairo_paint_with_alpha)));
@@ -640,7 +646,10 @@ void InitCairo(Napi::Env env, Napi::Object exports) {
640
646
  c.Set("arc", Napi::Function::New(env, CTX_5D(cairo_arc)));
641
647
  c.Set("arcNegative", Napi::Function::New(env, CTX_5D(cairo_arc_negative)));
642
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)));
643
650
 
651
+ c.Set("pushGroupWithContent",
652
+ Napi::Function::New(env, CTX_ENUM(cairo_push_group_with_content, cairo_content_t)));
644
653
  c.Set("setOperator", Napi::Function::New(env, CTX_ENUM(cairo_set_operator, cairo_operator_t)));
645
654
  c.Set("setLineCap", Napi::Function::New(env, CTX_ENUM(cairo_set_line_cap, cairo_line_cap_t)));
646
655
  c.Set("setLineJoin", Napi::Function::New(env, CTX_ENUM(cairo_set_line_join, cairo_line_join_t)));
@@ -733,6 +742,109 @@ void InitCairo(Napi::Env env, Napi::Object exports) {
733
742
  CTX_EXTENTS("fillExtents", cairo_fill_extents)
734
743
  CTX_EXTENTS("strokeExtents", cairo_stroke_extents)
735
744
  CTX_EXTENTS("clipExtents", cairo_clip_extents)
745
+ // Toy text API (cairo_select_font_face & co), the GJS-compatible Context text ops.
746
+ // textExtents/fontExtents return the object shape gjs's cairo-context.cpp builds.
747
+ c.Set("selectFontFace", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
748
+ Napi::Env env = info.Env();
749
+ cairo_t* cr = CtxArg(env, info);
750
+ if (cr == nullptr) return env.Undefined();
751
+ std::string family = info[1].ToString().Utf8Value();
752
+ cairo_select_font_face(cr, family.c_str(),
753
+ static_cast<cairo_font_slant_t>(info[2].ToNumber().Int32Value()),
754
+ static_cast<cairo_font_weight_t>(info[3].ToNumber().Int32Value()));
755
+ CheckStatus(env, cairo_status(cr), "selectFontFace");
756
+ return env.Undefined();
757
+ }));
758
+ c.Set("setFontSize", Napi::Function::New(env, CTX_1D(cairo_set_font_size)));
759
+ c.Set("showText", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
760
+ Napi::Env env = info.Env();
761
+ cairo_t* cr = CtxArg(env, info);
762
+ if (cr == nullptr) return env.Undefined();
763
+ std::string text = info[1].ToString().Utf8Value();
764
+ cairo_show_text(cr, text.c_str());
765
+ CheckStatus(env, cairo_status(cr), "showText");
766
+ return env.Undefined();
767
+ }));
768
+ c.Set("textPath", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
769
+ Napi::Env env = info.Env();
770
+ cairo_t* cr = CtxArg(env, info);
771
+ if (cr == nullptr) return env.Undefined();
772
+ std::string text = info[1].ToString().Utf8Value();
773
+ cairo_text_path(cr, text.c_str());
774
+ CheckStatus(env, cairo_status(cr), "textPath");
775
+ return env.Undefined();
776
+ }));
777
+ c.Set("textExtents", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
778
+ Napi::Env env = info.Env();
779
+ cairo_t* cr = CtxArg(env, info);
780
+ if (cr == nullptr) return env.Undefined();
781
+ std::string text = info[1].ToString().Utf8Value();
782
+ cairo_text_extents_t e;
783
+ cairo_text_extents(cr, text.c_str(), &e);
784
+ if (!CheckStatus(env, cairo_status(cr), "textExtents")) return env.Undefined();
785
+ Napi::Object o = Napi::Object::New(env);
786
+ o.Set("xBearing", Napi::Number::New(env, e.x_bearing));
787
+ o.Set("yBearing", Napi::Number::New(env, e.y_bearing));
788
+ o.Set("width", Napi::Number::New(env, e.width));
789
+ o.Set("height", Napi::Number::New(env, e.height));
790
+ o.Set("xAdvance", Napi::Number::New(env, e.x_advance));
791
+ o.Set("yAdvance", Napi::Number::New(env, e.y_advance));
792
+ return o;
793
+ }));
794
+ c.Set("fontExtents", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
795
+ Napi::Env env = info.Env();
796
+ cairo_t* cr = CtxArg(env, info);
797
+ if (cr == nullptr) return env.Undefined();
798
+ cairo_font_extents_t e;
799
+ cairo_font_extents(cr, &e);
800
+ if (!CheckStatus(env, cairo_status(cr), "fontExtents")) return env.Undefined();
801
+ Napi::Object o = Napi::Object::New(env);
802
+ o.Set("ascent", Napi::Number::New(env, e.ascent));
803
+ o.Set("descent", Napi::Number::New(env, e.descent));
804
+ o.Set("height", Napi::Number::New(env, e.height));
805
+ o.Set("maxXAdvance", Napi::Number::New(env, e.max_x_advance));
806
+ o.Set("maxYAdvance", Napi::Number::New(env, e.max_y_advance));
807
+ return o;
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
+ }));
736
848
  c.Set("status", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
737
849
  Napi::Env env = info.Env();
738
850
  cairo_t* cr = CtxArg(env, info);
@@ -759,6 +871,26 @@ void InitCairo(Napi::Env env, Napi::Object exports) {
759
871
  CheckStatus(env, cairo_status(cr), "context");
760
872
  return env.Undefined();
761
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
+ }));
762
894
 
763
895
  c.Set("identityMatrix", Napi::Function::New(env, CTX_0(cairo_identity_matrix)));
764
896
  c.Set("newSubPath", Napi::Function::New(env, CTX_0(cairo_new_sub_path)));
@@ -905,6 +1037,35 @@ void InitCairo(Napi::Env env, Napi::Object exports) {
905
1037
  return MakeCairoHandle(env, p, CairoKind::Pattern);
906
1038
  }));
907
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
+
908
1069
  // ---- $dispose (Context / Surface / Pattern / Path) ----
909
1070
  // Eagerly destroy the underlying cairo object + mark the handle so the finalizer
910
1071
  // won't double-free and any further method call throws (a live-handle check).