@gtkx/runtime 1.2.2 → 1.4.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.
Files changed (101) hide show
  1. package/README.md +6 -4
  2. package/dist/arg.d.ts +6 -1
  3. package/dist/arg.d.ts.map +1 -1
  4. package/dist/arg.js +2 -1
  5. package/dist/arg.js.map +1 -1
  6. package/dist/closure.d.ts.map +1 -1
  7. package/dist/closure.js +2 -11
  8. package/dist/closure.js.map +1 -1
  9. package/dist/descriptors.d.ts +21 -3
  10. package/dist/descriptors.d.ts.map +1 -1
  11. package/dist/descriptors.js +50 -21
  12. package/dist/descriptors.js.map +1 -1
  13. package/dist/field.d.ts +42 -0
  14. package/dist/field.d.ts.map +1 -0
  15. package/dist/field.js +41 -0
  16. package/dist/field.js.map +1 -0
  17. package/dist/fn.d.ts +2 -0
  18. package/dist/fn.d.ts.map +1 -1
  19. package/dist/fn.js +50 -10
  20. package/dist/fn.js.map +1 -1
  21. package/dist/index.d.ts +14 -6
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +13 -5
  24. package/dist/index.js.map +1 -1
  25. package/dist/mixin.d.ts +2 -1
  26. package/dist/mixin.d.ts.map +1 -1
  27. package/dist/mixin.js +1 -1
  28. package/dist/mixin.js.map +1 -1
  29. package/dist/native-value.d.ts +9 -3
  30. package/dist/native-value.d.ts.map +1 -1
  31. package/dist/native-value.js +86 -9
  32. package/dist/native-value.js.map +1 -1
  33. package/dist/object.d.ts +14 -6
  34. package/dist/object.d.ts.map +1 -1
  35. package/dist/object.js +21 -10
  36. package/dist/object.js.map +1 -1
  37. package/dist/param-spec.d.ts +32 -1
  38. package/dist/param-spec.d.ts.map +1 -1
  39. package/dist/param-spec.js +67 -5
  40. package/dist/param-spec.js.map +1 -1
  41. package/dist/promisify.d.ts +13 -1
  42. package/dist/promisify.d.ts.map +1 -1
  43. package/dist/promisify.js +14 -1
  44. package/dist/promisify.js.map +1 -1
  45. package/dist/properties.d.ts +48 -5
  46. package/dist/properties.d.ts.map +1 -1
  47. package/dist/properties.js +191 -20
  48. package/dist/properties.js.map +1 -1
  49. package/dist/regex.d.ts +25 -0
  50. package/dist/regex.d.ts.map +1 -0
  51. package/dist/regex.js +59 -0
  52. package/dist/regex.js.map +1 -0
  53. package/dist/register-class.d.ts +165 -15
  54. package/dist/register-class.d.ts.map +1 -1
  55. package/dist/register-class.js +175 -19
  56. package/dist/register-class.js.map +1 -1
  57. package/dist/registry.d.ts +48 -4
  58. package/dist/registry.d.ts.map +1 -1
  59. package/dist/registry.js +147 -3
  60. package/dist/registry.js.map +1 -1
  61. package/dist/signal.d.ts +11 -1
  62. package/dist/signal.d.ts.map +1 -1
  63. package/dist/signal.js +66 -6
  64. package/dist/signal.js.map +1 -1
  65. package/dist/t.d.ts +11 -0
  66. package/dist/t.d.ts.map +1 -1
  67. package/dist/t.js +3 -0
  68. package/dist/t.js.map +1 -1
  69. package/dist/type.d.ts +2 -1
  70. package/dist/type.d.ts.map +1 -1
  71. package/dist/type.js +5 -1
  72. package/dist/type.js.map +1 -1
  73. package/dist/value.d.ts +48 -4
  74. package/dist/value.d.ts.map +1 -1
  75. package/dist/value.js +172 -16
  76. package/dist/value.js.map +1 -1
  77. package/dist/variant.d.ts +75 -26
  78. package/dist/variant.d.ts.map +1 -1
  79. package/dist/variant.js +44 -29
  80. package/dist/variant.js.map +1 -1
  81. package/package.json +5 -5
  82. package/src/arg.ts +6 -1
  83. package/src/closure.ts +2 -14
  84. package/src/descriptors.ts +74 -21
  85. package/src/field.ts +76 -0
  86. package/src/fn.ts +73 -17
  87. package/src/index.ts +46 -5
  88. package/src/mixin.ts +1 -1
  89. package/src/native-value.ts +146 -14
  90. package/src/object.ts +23 -11
  91. package/src/param-spec.ts +96 -8
  92. package/src/promisify.ts +27 -1
  93. package/src/properties.ts +280 -32
  94. package/src/regex.ts +92 -0
  95. package/src/register-class.ts +485 -42
  96. package/src/registry.ts +219 -4
  97. package/src/signal.ts +117 -4
  98. package/src/t.ts +13 -0
  99. package/src/type.ts +7 -0
  100. package/src/value.ts +268 -14
  101. package/src/variant.ts +184 -53
@@ -3,9 +3,10 @@ import {
3
3
  registerClass as nativeRegisterClass,
4
4
  type RegisterClassInterface as NativeRegisterClassInterface,
5
5
  type RegisterClassOptions as NativeRegisterClassOptions,
6
+ type RegisterClassSignal as NativeRegisterClassSignal,
6
7
  type RegisterClassVfunc as NativeRegisterClassVfunc,
7
8
  } from "@gtkx/native";
8
- import { type AnyClass, getParentClass, walkClassChain } from "@gtkx/utils";
9
+ import { type AnyClass, getParentClass, kebabCase, walkClassChain } from "@gtkx/utils";
9
10
  import { wrapCallback } from "./callback.js";
10
11
  import { insertMixinLayer } from "./mixin.js";
11
12
  import {
@@ -19,13 +20,35 @@ import {
19
20
  toNativeProperties,
20
21
  } from "./properties.js";
21
22
  import {
23
+ getClassStructClass,
22
24
  getClassType,
23
25
  getInterfaceMixin,
26
+ getTypeClassHandle,
24
27
  markDerivedClass,
25
28
  registerClassType,
26
29
  type VfuncDescriptor,
30
+ wrapHandle,
27
31
  } from "./registry.js";
28
- import { TYPE_INTERFACE, TYPE_INVALID, typeFundamental, typeInterfaces, typeIsA } from "./type.js";
32
+ import {
33
+ connectClosureSignal,
34
+ type DeclaredSignalTypes,
35
+ emitDeclaredSignal,
36
+ getSignalBaseName,
37
+ overrideSignalClassClosure,
38
+ signalForHandlerName,
39
+ type SignalHandler,
40
+ signalIdFor,
41
+ } from "./signal.js";
42
+ import {
43
+ TYPE_INTERFACE,
44
+ TYPE_INVALID,
45
+ TYPE_NONE,
46
+ type TypedClass,
47
+ typeFundamental,
48
+ typeInterfaces,
49
+ typeIsA,
50
+ typeName,
51
+ } from "./type.js";
29
52
  import { findClassVfuncDescriptor, findInterfaceVfuncDescriptor } from "./vfunc.js";
30
53
 
31
54
  /**
@@ -69,16 +92,58 @@ type InstalledNames<TProperties> = string extends keyof TProperties
69
92
  ? never
70
93
  : Camelized<Dashed<keyof TProperties & string>>;
71
94
 
95
+ /**
96
+ * The base names the signal surface takes for the signals `RegisterClassOptions.signals` declares:
97
+ * each name as written, plus its canonical spelling with underscores turned into dashes, which
98
+ * GObject knows the signal by too.
99
+ */
100
+ type DeclaredSignalBase<TSignals> = (keyof TSignals & string) | Dashed<keyof TSignals & string>;
101
+ /**
102
+ * The names `connect` and `emit` take for one of the signals `RegisterClassOptions.signals`
103
+ * declares: each spelling of each name, plus its detailed form for a signal emitted with a
104
+ * `::detail` suffix.
105
+ */
106
+ type DeclaredSignalName<TSignals> = DeclaredSignalBase<TSignals> | `${DeclaredSignalBase<TSignals>}::${string}`;
107
+
108
+ /**
109
+ * The `connect` and `emit` signatures instances gain for the signals
110
+ * `RegisterClassOptions.signals` declares, widening the inherited ones, which take only the names
111
+ * introspection knows. A class declaring no signals gains nothing, since no name reaches the
112
+ * added signatures.
113
+ */
114
+ type DeclaredSignalMethods<TSignals> = {
115
+ /**
116
+ * Type-level map from declared signal name to handler signature, feeding the hooks that
117
+ * address a signal by name, such as `useSignal` from `@gtkx/react`; no value ever carries it.
118
+ */
119
+ __signals__?: Record<DeclaredSignalBase<TSignals>, (...args: never[]) => unknown>;
120
+ /** Connects a handler to a signal `RegisterClassOptions.signals` declared. */
121
+ connect(
122
+ signal: DeclaredSignalName<TSignals>,
123
+ handler: (...args: never[]) => unknown,
124
+ isAfter?: boolean,
125
+ ): number;
126
+ /** Emits a signal `RegisterClassOptions.signals` declared. */
127
+ emit(sigName: DeclaredSignalName<TSignals>, ...args: unknown[]): unknown;
128
+ /** Connects a handler to a signal `RegisterClassOptions.signals` declared, for `off` to take off. */
129
+ on(sigName: DeclaredSignalName<TSignals>, callback: (...args: unknown[]) => unknown, isAfter?: boolean): unknown;
130
+ /** Connects a handler to a signal `RegisterClassOptions.signals` declared for one emission. */
131
+ once(sigName: DeclaredSignalName<TSignals>, callback: (...args: unknown[]) => unknown, isAfter?: boolean): unknown;
132
+ /** Disconnects a handler `on` or `once` connected to a signal `RegisterClassOptions.signals` declared. */
133
+ off(sigName: DeclaredSignalName<TSignals>, callback: (...args: unknown[]) => unknown): unknown;
134
+ };
135
+
72
136
  /**
73
137
  * An instance of a registered class: everything the class itself declares, plus the property map the
74
138
  * hooks that address a property by name, such as `useProperty` from `@gtkx/react`, read the installed
75
139
  * names off. Each name is typed with the value type the class declares for the member of that name,
76
140
  * so a property the class does not `declare` contributes nothing.
77
141
  */
78
- type RegisteredInstance<TInstance, TProperties> = TInstance & {
79
- /** Type-level map from installed property name to value type; no value ever carries it. */
80
- __properties__: Pick<TInstance, InstalledNames<TProperties> & keyof TInstance>;
81
- };
142
+ type RegisteredInstance<TInstance, TProperties, TSignals> = TInstance &
143
+ DeclaredSignalMethods<TSignals> & {
144
+ /** Type-level map from installed property name to value type; no value ever carries it. */
145
+ __properties__: Pick<TInstance, InstalledNames<TProperties> & keyof TInstance>;
146
+ };
82
147
 
83
148
  /**
84
149
  * The construct signature and prototype a registered class carries, both giving
@@ -92,20 +157,91 @@ type RegisteredConstructor<TClass, TArgs extends unknown[], TInstance> = {
92
157
  ? new (...args: TArgs) => TInstance
93
158
  : abstract new (...args: TArgs) => TInstance);
94
159
 
160
+ /** The statics a registered class keeps from the class it was, joined with {@link RegisteredConstructor}. */
161
+ type RegisteredParts<TClass, TArgs extends unknown[], TInstance> = Omit<TClass, "prototype"> &
162
+ RegisteredConstructor<TClass, TArgs, TInstance>;
163
+
95
164
  /**
96
165
  * The class {@link registerClass} hands back: the same class, with the same statics, whose instances
97
- * carry the properties `RegisterClassOptions.properties` installed. Binding the call to a name, rather
98
- * than discarding it, is what carries those names into the type system.
166
+ * carry the properties `RegisterClassOptions.properties` installed and take the signals
167
+ * `RegisterClassOptions.signals` declares by name. Binding the call to a name, rather than
168
+ * discarding it, is what carries those names into the type system.
99
169
  */
100
- type RegisteredClass<TClass extends AnyClass, TProperties> =
170
+ type RegisteredClass<TClass extends AnyClass, TProperties, TSignals> =
101
171
  TClass extends abstract new (...args: infer TArgs) => infer TInstance
102
- ? Omit<TClass, "prototype"> & RegisteredConstructor<TClass, TArgs, RegisteredInstance<TInstance, TProperties>>
172
+ ? RegisteredParts<TClass, TArgs, RegisteredInstance<TInstance, TProperties, TSignals>>
103
173
  : never;
104
174
 
175
+ /**
176
+ * A GType in the form `RegisterClassOptions.signals` takes one: the numeric GType itself, such as
177
+ * `TYPE_STRING` from `@gtkx/gi/gobject`, or a class carrying one, which is any generated wrapper
178
+ * class and any class an earlier {@link registerClass} call registered.
179
+ */
180
+ type SignalGType = bigint | AnyClass<TypedClass>;
181
+
182
+ /**
183
+ * One signal `RegisterClassOptions.signals` creates on the new type, sitting under the signal's
184
+ * name. Every part is optional: `{}` declares a signal with no arguments and no return value that
185
+ * runs its handlers in the default `RUN_FIRST` stage.
186
+ */
187
+ type SignalSpec = {
188
+ /**
189
+ * `GObject.SignalFlags` bit mask for the signal, defaulting to `RUN_FIRST`. `DETAILED` lets
190
+ * handlers connect to and emissions name a `::detail` suffix.
191
+ */
192
+ flags?: number;
193
+ /** GType of each argument an emission carries, defaulting to none. */
194
+ paramTypes?: SignalGType[];
195
+ /** GType of the value an emission returns, defaulting to none. */
196
+ returnType?: SignalGType;
197
+ /**
198
+ * How the emission combines what its handlers return, limited to the two accumulators GObject
199
+ * ships: `"first-wins"` stops the emission at the first handler and keeps its result, and
200
+ * `"true-handled"` runs handlers until one returns `true`, which requires a boolean
201
+ * `returnType`. Without one, every handler runs and the last result stands.
202
+ */
203
+ accumulator?: "first-wins" | "true-handled";
204
+ };
205
+
105
206
  /** What {@link registerClass} adds to the new GType beyond the vtable slots it discovers on the class. */
106
- type RegisterClassOptions<TInstance extends object, TProperties extends Record<string, PropertySpec>> = {
107
- /** Name to register the new GType under, defaulting to the class's own name. */
207
+ type RegisterClassOptions<
208
+ TInstance extends object,
209
+ TProperties extends Record<string, PropertySpec>,
210
+ TSignals extends Record<string, SignalSpec>,
211
+ > = {
212
+ /**
213
+ * Name to register the new GType under, defaulting to the class's own name. Either way the
214
+ * name has to be a valid GType name: at least three characters, starting with a letter or
215
+ * underscore, the rest letters, digits, `-`, `_` or `+`. Any other name throws a `TypeError`.
216
+ */
108
217
  typeName?: string;
218
+ /**
219
+ * Registers the new GType abstract, the way `G_TYPE_FLAG_ABSTRACT` marks a C type: the class
220
+ * still serves as a parent for further registered subclasses, which instantiate as usual, but
221
+ * constructing it directly throws, whether from JavaScript or from a native caller.
222
+ */
223
+ abstract?: boolean;
224
+ /**
225
+ * Name instances of the new type carry in CSS, applied through `gtk_widget_class_set_css_name`
226
+ * from inside the type's `class_init`, so every instance is born with it, wherever it is
227
+ * created from. Requires the class to extend `Gtk.Widget`; registering a non-widget with a
228
+ * `cssName` throws.
229
+ */
230
+ cssName?: string;
231
+ /**
232
+ * Hook run once, synchronously, while `registerClass` registers the type, after its
233
+ * `class_init` has installed the vfuncs, properties and signals declared here. It receives the
234
+ * new type's class struct wrapped in its generated GTypeStruct wrapper, so class-level setup
235
+ * calls such as `Gtk.WidgetClass.installAction`, `Gtk.WidgetClass.addShortcut` and
236
+ * `Gtk.WidgetClass.setLayoutManagerType` have somewhere to land. The wrapper serves the
237
+ * members of every struct in the parent chain on one object: a widget subclass sees
238
+ * `Gtk.WidgetClass` and `GObject.ObjectClass` members alike, so the parameter can be declared
239
+ * as whichever of those types the hook needs. The class struct belongs to the type system for
240
+ * the life of the process, so keeping the wrapper around past the hook is safe, if rarely
241
+ * useful. An exception the hook throws propagates out of `registerClass`, with the type
242
+ * already registered: GObject offers no way to unregister a static type.
243
+ */
244
+ classInit?(typeStruct: object): void;
109
245
  /**
110
246
  * Interfaces the new type implements on top of the ones it inherits, given as the interface values
111
247
  * themselves, such as `Gio.ListModel`. Their vtable slots are filled from the `vfunc`-prefixed methods on
@@ -117,10 +253,15 @@ type RegisterClassOptions<TInstance extends object, TProperties extends Record<s
117
253
  * Properties to install on the new type, keyed by the name JavaScript addresses each one by and
118
254
  * valued with the `GObject.ParamSpec` describing it. A key is read in camelCase however it is
119
255
  * written, so `dewPoint`, `dew_point` and `dew-point` all name the same member, and the ParamSpec
120
- * has to carry the canonical spelling of that name, `dew-point`, or registration throws: the
121
- * ParamSpec's name is the one GObject emits `notify` with, and a name the key does not spell
122
- * reaches nothing that listens for it. Every property gains prototype accessors, one for the key
123
- * as written, one for it with dashes turned into underscores and one for it in camelCase, each
256
+ * has to carry a name that reads back as that same member, such as `dew-point`, or registration
257
+ * throws: the ParamSpec's name is the one GObject emits `notify` with, and a name the key does
258
+ * not spell reaches nothing that listens for it. A word starting with a digit is a word of its
259
+ * own on either side of that reading, so `level2Depth` takes a ParamSpec named `level-2-depth`
260
+ * as readily as one named `level2-depth`, the way `WebKit.Settings` names
261
+ * `enable-2d-canvas-acceleration` for its `enable2dCanvasAcceleration` member. An uppercase
262
+ * letter in the ParamSpec's own name is refused, since GObject notifies under that spelling
263
+ * and nothing else reads it back. Every property gains prototype accessors, one for the key as
264
+ * written, one for it with dashes turned into underscores and one for it in camelCase, each
124
265
  * unless the class already defines that name. They serve the value from storage of their own on
125
266
  * the instance, which is also what the type's `get_property` and `set_property` slots read and
126
267
  * write, so a value set from JavaScript, from `g_object_set_property` and at construction all
@@ -169,10 +310,34 @@ type RegisterClassOptions<TInstance extends object, TProperties extends Record<s
169
310
  * and write it rather than the generated storage.
170
311
  */
171
312
  properties?: TProperties;
313
+ /**
314
+ * Signals to create on the new type, keyed by signal name and valued with the
315
+ * {@link SignalSpec} describing each one. A name has to start with a lowercase letter, continue
316
+ * in lowercase letters, digits, `-` and `_`, and be new to the type: one an ancestor type or a
317
+ * listed interface already carries throws, and so does one carrying an uppercase letter, which
318
+ * GObject would carry under that exact spelling, out of reach of both its dashed spelling and
319
+ * its `on<SignalName>` default handler. The two word separators spell the same signal, so a
320
+ * signal declared as `data_changed` is connected to and emitted as `data-changed` too.
321
+ *
322
+ * Instances connect and emit by name through the same `connect`, `on`, `once`, `off` and
323
+ * `emit` surface inherited signals use: `registerClass` wraps `connect` and `emit` on the
324
+ * class's prototype to serve the declared names, unless the class defines the member itself,
325
+ * and hands every other name to the inherited implementation. A handler receives the
326
+ * emission's arguments without the leading emitter, matching a generated signal, and what it
327
+ * returns becomes the emission's return value when the signal declares one.
328
+ *
329
+ * The declared parameter GTypes rule the emission: `emit` takes exactly one argument per
330
+ * declared parameter, throwing a `TypeError` for any other count, and converts each argument
331
+ * into a `GValue` of the declared type, throwing for a value that type cannot hold. The signals
332
+ * are created with no class closure of their own, but a method named `on<SignalName>`
333
+ * becomes the signal's default handler, the way every `on`-prefixed method that names a
334
+ * signal the type carries does; see {@link registerClass}.
335
+ */
336
+ signals?: TSignals;
172
337
  };
173
338
 
174
339
  /** {@link RegisterClassOptions} with the widest instance and property types {@link registerClass} accepts. */
175
- type AnyRegisterClassOptions = RegisterClassOptions<object, Record<string, PropertySpec>>;
340
+ type AnyRegisterClassOptions = RegisterClassOptions<object, Record<string, PropertySpec>, Record<string, SignalSpec>>;
176
341
  type VfuncFn = NativeRegisterClassVfunc["fn"];
177
342
  type DiscoveredVfunc = VfuncDescriptor & { methodName: string; fn: VfuncFn };
178
343
  type MethodTable = Map<string, VfuncFn>;
@@ -197,10 +362,18 @@ type PropertyVfuncSpec = {
197
362
  };
198
363
 
199
364
  type ArgPatch = { isCallerAllocated: true } | { isCallScoped: true };
365
+ type DeclaredSignals = { native: NativeRegisterClassSignal[]; table: Map<string, DeclaredSignalTypes> };
366
+
367
+ type SignalMethodHost = {
368
+ connect?: (signal: string, handler: SignalHandler, isAfter?: boolean) => number;
369
+ emit?: (sigName: string, ...args: unknown[]) => unknown;
370
+ };
200
371
 
201
372
  const INSTANCE_ARG_INDEX = 0;
202
373
  const VALUE_ARG_INDEX = 2;
203
374
  const TEARDOWN_VFUNC_NAMES: Set<string> = new Set(["dispose", "finalize"]);
375
+ const ASYNC_INITABLE_TYPE_NAME = "GAsyncInitable";
376
+ const INIT_ASYNC_METHOD_NAME = "vfuncInitAsync";
204
377
 
205
378
  const PROPERTY_VFUNC_SPECS: PropertyVfuncSpec[] = [
206
379
  { methodName: GET_PROPERTY_VFUNC, isValueOut: true, makeDispatch: makeGetProperty },
@@ -208,6 +381,9 @@ const PROPERTY_VFUNC_SPECS: PropertyVfuncSpec[] = [
208
381
  ];
209
382
 
210
383
  const PROPERTY_VFUNC_NAMES: Set<string> = new Set(PROPERTY_VFUNC_SPECS.map((spec) => spec.methodName));
384
+ const TYPE_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9\-_+]{2,}$/;
385
+ const UPPER_CASE_PATTERN = /[A-Z]/;
386
+ const SIGNAL_OVERRIDE_PATTERN = /^on[A-Z]/;
211
387
 
212
388
  /**
213
389
  * Registers a subclass of a wrapper class as a new GType, wiring up any class and interface
@@ -215,10 +391,19 @@ const PROPERTY_VFUNC_NAMES: Set<string> = new Set(PROPERTY_VFUNC_SPECS.map((spec
215
391
  * `RegisterClassOptions.implements` names.
216
392
  *
217
393
  * Throws when the class does not extend a registered wrapper class, when it has no derivable type
218
- * name, when an entry in `RegisterClassOptions.implements` is not a registered interface, when a
394
+ * name or the name is not a valid GType name, when an entry in
395
+ * `RegisterClassOptions.implements` is not a registered interface, when a
219
396
  * listed interface has a prerequisite that neither the parent type nor another listed interface meets,
220
- * and when an entry in `RegisterClassOptions.properties` names its `GObject.ParamSpec` something other
221
- * than the canonical spelling of the key it sits under.
397
+ * when the list names `Gio.AsyncInitable` as an interface the parent type does not already
398
+ * implement and no method on the chain fills `vfuncInitAsync`, since the default `init_async`
399
+ * would run `vfuncInit` on a worker thread, when an entry in
400
+ * `RegisterClassOptions.properties` names its `GObject.ParamSpec` something the key it sits under
401
+ * does not spell, when an entry in `RegisterClassOptions.signals` carries an invalid name, a name
402
+ * spelled with an uppercase letter rather than dashed, a name the type already knows, a GType
403
+ * that cannot hold a value, or an accumulator the spec does not admit, and when
404
+ * `RegisterClassOptions.cssName` is
405
+ * given for a class that does not extend `Gtk.Widget`. An exception thrown by
406
+ * `RegisterClassOptions.classInit` also propagates, after the type has already been registered.
222
407
  *
223
408
  * A slot is filled from the `vfunc`-prefixed methods on the class's prototype chain, up to but not
224
409
  * including the registered ancestor the class extends, so a method an intermediate base class
@@ -228,6 +413,19 @@ const PROPERTY_VFUNC_NAMES: Set<string> = new Set(PROPERTY_VFUNC_SPECS.map((spec
228
413
  * Declare every slot as a method: a class field holding a function, such as `vfuncGetNItems = () => 1`,
229
414
  * is assigned to each instance after registration and never reaches the vtable.
230
415
  *
416
+ * A method named `on<SignalName>` — the signal's name in camelCase after the `on`, so `onClicked`
417
+ * for `clicked` and `onItemsChanged` for `items-changed` — becomes that signal's default handler
418
+ * when the type carries the signal, whether an ancestor type or an implemented interface brings it
419
+ * or `RegisterClassOptions.signals` declares it. The method is installed as a class-closure
420
+ * override, so it runs on every emission, on the instances a native caller creates included, in
421
+ * the stage the signal's flags name rather than alongside connected handlers. It receives the
422
+ * emission's arguments without the leading emitter, with `this` bound to the emitter, and what it
423
+ * returns becomes the emission's result when the signal declares one. The same discovery walks the
424
+ * prototype chain vfunc discovery walks, and a subclass registering its own `on<SignalName>`
425
+ * replaces the handler for its instances, where `super.on<SignalName>()` reaches the replaced one.
426
+ * An `on`-prefixed method naming no signal the type carries is left alone as the ordinary method
427
+ * it is.
428
+ *
231
429
  * An override of `vfuncConstructed` runs from inside the base constructor, before JavaScript
232
430
  * installs the subclass's field initializers and runs its constructor body, so a field still
233
431
  * reads `undefined` there and reading a `#private` field throws. Declare state the override
@@ -243,7 +441,11 @@ const PROPERTY_VFUNC_NAMES: Set<string> = new Set(PROPERTY_VFUNC_SPECS.map((spec
243
441
  function registerClass<
244
442
  T extends AnyClass,
245
443
  TProperties extends Record<string, PropertySpec> = Record<never, PropertySpec>,
246
- >(klass: T, options?: RegisterClassOptions<T["prototype"], TProperties>): RegisteredClass<T, TProperties>;
444
+ TSignals extends Record<string, SignalSpec> = Record<never, SignalSpec>,
445
+ >(
446
+ klass: T,
447
+ options?: RegisterClassOptions<T["prototype"], TProperties, TSignals>,
448
+ ): RegisteredClass<T, TProperties, TSignals>;
247
449
 
248
450
  function registerClass(klass: AnyClass, options: AnyRegisterClassOptions = {}): AnyClass {
249
451
  const parentType = resolveParentType(klass);
@@ -252,16 +454,14 @@ function registerClass(klass: AnyClass, options: AnyRegisterClassOptions = {}):
252
454
  throw new TypeError(`registerClass: ${klass.name} must extend a registered wrapper class`);
253
455
  }
254
456
 
255
- const name = options.typeName ?? klass.name;
256
-
257
- if (!name) {
258
- throw new Error("registerClass: cannot derive a GType name (anonymous class with no typeName option)");
259
- }
260
-
457
+ const name = resolveTypeName(klass, options);
261
458
  const declaredTypes = resolveInterfaceTypes(klass, options.implements ?? []);
262
459
  const adoptedTypes = declaredTypes.filter((gtype) => !typeIsA(parentType, gtype));
263
460
  const properties = options.properties ?? {};
264
- const { methods, inheritedNames } = collectInstanceMembers(klass);
461
+ const signals = resolveDeclaredSignals(klass, options.signals ?? {});
462
+ const members = collectInstanceMembers(klass);
463
+ const { methods, inheritedNames } = members;
464
+ checkAsyncInitable(klass, adoptedTypes, methods);
265
465
  const dispatch = buildPropertyDispatch({ klass, properties, adoptedTypes });
266
466
 
267
467
  const classVfuncs = [
@@ -271,15 +471,54 @@ function registerClass(klass: AnyClass, options: AnyRegisterClassOptions = {}):
271
471
 
272
472
  const claimedMethodNames = new Set(classVfuncs.map((vfunc) => vfunc.methodName));
273
473
  const interfaceBindings = discoverInterfaceBindings(methods, parentType, declaredTypes, claimedMethodNames);
274
- const nativeOptions = toNativeOptions(classVfuncs, interfaceBindings, properties);
474
+
475
+ const nativeOptions = withNativeSignals(
476
+ toNativeOptions(classVfuncs, interfaceBindings, properties, options),
477
+ signals.native,
478
+ );
479
+
275
480
  const newType: bigint = nativeRegisterClass(name, parentType, nativeOptions);
276
481
  registerClassType(klass, newType);
277
482
  markDerivedClass(klass);
483
+ installSignalOverrides(newType, methods);
278
484
  applyInterfaceMixins(klass, adoptedTypes, inheritedNames);
485
+ installDeclaredSignalMethods(klass, signals.table, members.names);
486
+ invokeClassInit(options, newType);
279
487
 
280
488
  return klass;
281
489
  }
282
490
 
491
+ function invokeClassInit(options: AnyRegisterClassOptions, newType: bigint): void {
492
+ if (options.classInit === undefined) {
493
+ return;
494
+ }
495
+
496
+ const structClass = getClassStructClass(newType);
497
+
498
+ if (structClass === undefined) {
499
+ throw new Error("registerClass: no ancestor of the new type registers a class struct wrapper");
500
+ }
501
+
502
+ options.classInit(wrapHandle(getTypeClassHandle(newType), structClass));
503
+ }
504
+
505
+ function resolveTypeName(klass: AnyClass, options: AnyRegisterClassOptions): string {
506
+ const name = options.typeName ?? klass.name;
507
+
508
+ if (!name) {
509
+ throw new TypeError("registerClass: cannot derive a GType name (anonymous class with no typeName option)");
510
+ }
511
+
512
+ if (!TYPE_NAME_PATTERN.test(name)) {
513
+ throw new TypeError(
514
+ `registerClass: '${name}' is not a valid GType name (a letter or underscore, then at ` +
515
+ "least two more characters, all from A-Z, a-z, 0-9, '-', '_' and '+')",
516
+ );
517
+ }
518
+
519
+ return name;
520
+ }
521
+
283
522
  function resolveInterfaceType(klass: AnyClass, entry: AnyClass): bigint {
284
523
  const gtype = getClassType(entry);
285
524
 
@@ -298,6 +537,17 @@ function resolveInterfaceTypes(klass: AnyClass, entries: AnyClass[]): bigint[] {
298
537
  return [...new Set(entries.map((entry) => resolveInterfaceType(klass, entry)))];
299
538
  }
300
539
 
540
+ function checkAsyncInitable(klass: AnyClass, adoptedTypes: bigint[], methods: MethodTable): void {
541
+ const isAsyncInitable = adoptedTypes.some((gtype) => typeName(gtype) === ASYNC_INITABLE_TYPE_NAME);
542
+
543
+ if (isAsyncInitable && !methods.has(INIT_ASYNC_METHOD_NAME)) {
544
+ throw new TypeError(
545
+ `registerClass: ${klass.name} implements Gio.AsyncInitable without overriding 'vfuncInitAsync'; ` +
546
+ "the default 'init_async' would run 'vfuncInit' on a worker thread",
547
+ );
548
+ }
549
+ }
550
+
301
551
  function applyInterfaceMixins(klass: AnyClass, adoptedTypes: bigint[], inheritedNames: Set<string>): void {
302
552
  for (const gtype of adoptedTypes) {
303
553
  const mixin = getInterfaceMixin(gtype);
@@ -527,6 +777,184 @@ function buildPropertyVfunc(
527
777
  };
528
778
  }
529
779
 
780
+ const canonicalSignalName = (name: string): string => name.replaceAll("_", "-");
781
+
782
+ function assertLowerCaseSignalName(klass: AnyClass, name: string): void {
783
+ if (!UPPER_CASE_PATTERN.test(name)) {
784
+ return;
785
+ }
786
+
787
+ throw new TypeError(
788
+ `registerClass: ${klass.name} declares the signal '${name}'; GObject would carry it under that ` +
789
+ "exact spelling, out of reach of both its dashed spelling and its default handler; " +
790
+ `declare it as '${kebabCase(name)}'`,
791
+ );
792
+ }
793
+
794
+ function overriddenSignalId(type: bigint, methodName: string): number {
795
+ if (!SIGNAL_OVERRIDE_PATTERN.test(methodName)) {
796
+ return 0;
797
+ }
798
+
799
+ const signal = signalForHandlerName(type, methodName);
800
+
801
+ return signal === undefined ? 0 : signalIdFor(type, signal);
802
+ }
803
+
804
+ function installSignalOverrides(newType: bigint, methods: MethodTable): void {
805
+ for (const [methodName, fn] of methods) {
806
+ const signalId = overriddenSignalId(newType, methodName);
807
+
808
+ if (signalId === 0) {
809
+ continue;
810
+ }
811
+
812
+ const handler = fn as (...args: unknown[]) => unknown;
813
+
814
+ overrideSignalClassClosure(newType, signalId, (...args: unknown[]) =>
815
+ handler.apply(args[0], args.slice(1)),
816
+ );
817
+ }
818
+ }
819
+
820
+ function resolveSignalGType(klass: AnyClass, signalName: string, role: string, entry: SignalGType): bigint {
821
+ if (typeof entry === "bigint") {
822
+ return entry;
823
+ }
824
+
825
+ const gtype = getClassType(entry);
826
+
827
+ if (gtype === TYPE_INVALID) {
828
+ throw new TypeError(
829
+ `registerClass: signal '${signalName}' of ${klass.name} names a class with no registered ` +
830
+ `GType as its ${role}`,
831
+ );
832
+ }
833
+
834
+ return gtype;
835
+ }
836
+
837
+ function resolveSignalReturnType(klass: AnyClass, name: string, spec: SignalSpec): bigint | undefined {
838
+ if (spec.returnType === undefined) {
839
+ return undefined;
840
+ }
841
+
842
+ const returnType = resolveSignalGType(klass, name, "return type", spec.returnType);
843
+
844
+ return returnType === TYPE_NONE ? undefined : returnType;
845
+ }
846
+
847
+ function resolveDeclaredSignal(
848
+ klass: AnyClass,
849
+ name: string,
850
+ spec: SignalSpec,
851
+ ): { native: NativeRegisterClassSignal; declared: DeclaredSignalTypes } {
852
+ const paramTypes = (spec.paramTypes ?? []).map((entry, index) =>
853
+ resolveSignalGType(klass, name, `parameter ${String(index)}`, entry),
854
+ );
855
+
856
+ const returnType = resolveSignalReturnType(klass, name, spec);
857
+ const native: NativeRegisterClassSignal = { name, paramTypes };
858
+ const declared: DeclaredSignalTypes = { paramTypes };
859
+
860
+ if (spec.flags !== undefined) {
861
+ native.flags = spec.flags;
862
+ }
863
+
864
+ if (spec.accumulator !== undefined) {
865
+ native.accumulator = spec.accumulator;
866
+ }
867
+
868
+ if (returnType !== undefined) {
869
+ native.returnType = returnType;
870
+ declared.returnType = returnType;
871
+ }
872
+
873
+ return { native, declared };
874
+ }
875
+
876
+ function resolveDeclaredSignals(klass: AnyClass, signals: Record<string, SignalSpec>): DeclaredSignals {
877
+ const native: NativeRegisterClassSignal[] = [];
878
+ const table: Map<string, DeclaredSignalTypes> = new Map();
879
+
880
+ for (const [name, spec] of Object.entries(signals)) {
881
+ assertLowerCaseSignalName(klass, name);
882
+ const resolved = resolveDeclaredSignal(klass, name, spec);
883
+ native.push(resolved.native);
884
+ table.set(canonicalSignalName(name), resolved.declared);
885
+ }
886
+
887
+ return { native, table };
888
+ }
889
+
890
+ function inheritedSignalMethod<T>(inherited: T | undefined, signal: string): T {
891
+ if (inherited === undefined) {
892
+ throw new Error(`Unknown signal '${signal}'`);
893
+ }
894
+
895
+ return inherited;
896
+ }
897
+
898
+ function installDeclaredConnect(
899
+ proto: SignalMethodHost,
900
+ findDeclared: (signal: string) => DeclaredSignalTypes | undefined,
901
+ ): void {
902
+ const inheritedConnect = proto.connect;
903
+
904
+ proto.connect = function connect(
905
+ this: object,
906
+ signal: string,
907
+ handler: SignalHandler,
908
+ isAfter?: boolean,
909
+ ): number {
910
+ if (findDeclared(signal) === undefined) {
911
+ return inheritedSignalMethod(inheritedConnect, signal).call(this, signal, handler, isAfter);
912
+ }
913
+
914
+ return connectClosureSignal(this, signal, handler, isAfter ?? false);
915
+ };
916
+ }
917
+
918
+ function installDeclaredEmit(
919
+ proto: SignalMethodHost,
920
+ findDeclared: (signal: string) => DeclaredSignalTypes | undefined,
921
+ ): void {
922
+ const inheritedEmit = proto.emit;
923
+
924
+ proto.emit = function emit(this: object, sigName: string, ...args: unknown[]): unknown {
925
+ const declared = findDeclared(sigName);
926
+
927
+ if (declared === undefined) {
928
+ return inheritedSignalMethod(inheritedEmit, sigName).call(this, sigName, ...args);
929
+ }
930
+
931
+ return emitDeclaredSignal(this, sigName, declared, args);
932
+ };
933
+ }
934
+
935
+ function installDeclaredSignalMethods(
936
+ klass: AnyClass,
937
+ table: Map<string, DeclaredSignalTypes>,
938
+ definedNames: Set<string>,
939
+ ): void {
940
+ if (table.size === 0) {
941
+ return;
942
+ }
943
+
944
+ const proto = klass.prototype as SignalMethodHost;
945
+
946
+ const findDeclared = (signal: string): DeclaredSignalTypes | undefined =>
947
+ table.get(canonicalSignalName(getSignalBaseName(signal)));
948
+
949
+ if (!definedNames.has("connect")) {
950
+ installDeclaredConnect(proto, findDeclared);
951
+ }
952
+
953
+ if (!definedNames.has("emit")) {
954
+ installDeclaredEmit(proto, findDeclared);
955
+ }
956
+ }
957
+
530
958
  function toNativeInterface(binding: InterfaceVfuncBinding): NativeRegisterClassInterface {
531
959
  const nativeInterface: NativeRegisterClassInterface = {
532
960
  type: binding.gtype,
@@ -542,34 +970,49 @@ function toNativeInterface(binding: InterfaceVfuncBinding): NativeRegisterClassI
542
970
  return nativeInterface;
543
971
  }
544
972
 
973
+ function withNativeSignals(
974
+ options: NativeRegisterClassOptions | undefined,
975
+ signals: NativeRegisterClassSignal[],
976
+ ): NativeRegisterClassOptions | undefined {
977
+ if (signals.length === 0) {
978
+ return options;
979
+ }
980
+
981
+ return { ...options, signals };
982
+ }
983
+
984
+ function applyNativeTypeOptions(options: NativeRegisterClassOptions, source: AnyRegisterClassOptions): void {
985
+ if (source.abstract ?? false) {
986
+ options.abstract = true;
987
+ }
988
+
989
+ if (source.cssName !== undefined) {
990
+ options.cssName = source.cssName;
991
+ }
992
+ }
993
+
545
994
  function toNativeOptions(
546
995
  classVfuncs: DiscoveredVfunc[],
547
996
  interfaceBindings: InterfaceVfuncBinding[],
548
997
  properties: Record<string, PropertySpec>,
998
+ source: AnyRegisterClassOptions,
549
999
  ): NativeRegisterClassOptions | undefined {
550
- const hasInterfaces = interfaceBindings.length > 0;
551
- const hasClassVfuncs = classVfuncs.length > 0;
552
- const hasProperties = Object.keys(properties).length > 0;
553
-
554
- if (!hasClassVfuncs && !hasInterfaces && !hasProperties) {
555
- return undefined;
556
- }
557
-
558
1000
  const options: NativeRegisterClassOptions = {};
1001
+ applyNativeTypeOptions(options, source);
559
1002
 
560
- if (hasProperties) {
1003
+ if (Object.keys(properties).length > 0) {
561
1004
  options.properties = toNativeProperties(properties);
562
1005
  }
563
1006
 
564
- if (hasClassVfuncs) {
1007
+ if (classVfuncs.length > 0) {
565
1008
  options.vfuncs = [...classVfuncs];
566
1009
  }
567
1010
 
568
- if (hasInterfaces) {
1011
+ if (interfaceBindings.length > 0) {
569
1012
  options.interfaces = interfaceBindings.map((binding) => toNativeInterface(binding));
570
1013
  }
571
1014
 
572
- return options;
1015
+ return Object.keys(options).length > 0 ? options : undefined;
573
1016
  }
574
1017
 
575
- export { type Interface, registerClass };
1018
+ export { type Interface, registerClass, type SignalGType, type SignalSpec };