@gtkx/runtime 1.0.0-rc.4 → 1.0.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 (133) hide show
  1. package/README.md +5 -5
  2. package/dist/application-class.d.ts +32 -0
  3. package/dist/application-class.d.ts.map +1 -0
  4. package/dist/application-class.js +68 -0
  5. package/dist/application-class.js.map +1 -0
  6. package/dist/arg.d.ts +2 -1
  7. package/dist/arg.d.ts.map +1 -1
  8. package/dist/arg.js +2 -1
  9. package/dist/arg.js.map +1 -1
  10. package/dist/bind.d.ts +11 -0
  11. package/dist/bind.d.ts.map +1 -1
  12. package/dist/bind.js +11 -0
  13. package/dist/bind.js.map +1 -1
  14. package/dist/callback.d.ts +2 -1
  15. package/dist/callback.d.ts.map +1 -1
  16. package/dist/callback.js +118 -20
  17. package/dist/callback.js.map +1 -1
  18. package/dist/closure.d.ts +26 -0
  19. package/dist/closure.d.ts.map +1 -0
  20. package/dist/closure.js +99 -0
  21. package/dist/closure.js.map +1 -0
  22. package/dist/descriptors.d.ts +74 -3
  23. package/dist/descriptors.d.ts.map +1 -1
  24. package/dist/descriptors.js +44 -0
  25. package/dist/descriptors.js.map +1 -1
  26. package/dist/error.d.ts +1 -0
  27. package/dist/error.d.ts.map +1 -1
  28. package/dist/error.js.map +1 -1
  29. package/dist/fn.d.ts +19 -2
  30. package/dist/fn.d.ts.map +1 -1
  31. package/dist/fn.js +64 -27
  32. package/dist/fn.js.map +1 -1
  33. package/dist/index.d.ts +14 -6
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +12 -4
  36. package/dist/index.js.map +1 -1
  37. package/dist/internal.d.ts +1 -0
  38. package/dist/internal.d.ts.map +1 -1
  39. package/dist/internal.js +1 -0
  40. package/dist/internal.js.map +1 -1
  41. package/dist/library.d.ts +2 -1
  42. package/dist/library.d.ts.map +1 -1
  43. package/dist/library.js +2 -1
  44. package/dist/library.js.map +1 -1
  45. package/dist/lifecycle.d.ts +43 -11
  46. package/dist/lifecycle.d.ts.map +1 -1
  47. package/dist/lifecycle.js +62 -19
  48. package/dist/lifecycle.js.map +1 -1
  49. package/dist/listeners.d.ts.map +1 -1
  50. package/dist/listeners.js +3 -1
  51. package/dist/listeners.js.map +1 -1
  52. package/dist/mixin.d.ts +3 -2
  53. package/dist/mixin.d.ts.map +1 -1
  54. package/dist/mixin.js +13 -1
  55. package/dist/mixin.js.map +1 -1
  56. package/dist/native-value.d.ts.map +1 -1
  57. package/dist/native-value.js +4 -3
  58. package/dist/native-value.js.map +1 -1
  59. package/dist/object.d.ts +34 -6
  60. package/dist/object.d.ts.map +1 -1
  61. package/dist/object.js +89 -25
  62. package/dist/object.js.map +1 -1
  63. package/dist/param-spec.d.ts +11 -0
  64. package/dist/param-spec.d.ts.map +1 -0
  65. package/dist/param-spec.js +149 -0
  66. package/dist/param-spec.js.map +1 -0
  67. package/dist/promisify.d.ts.map +1 -1
  68. package/dist/promisify.js +3 -2
  69. package/dist/promisify.js.map +1 -1
  70. package/dist/properties.d.ts +38 -7
  71. package/dist/properties.d.ts.map +1 -1
  72. package/dist/properties.js +305 -42
  73. package/dist/properties.js.map +1 -1
  74. package/dist/register-class.d.ts +117 -12
  75. package/dist/register-class.d.ts.map +1 -1
  76. package/dist/register-class.js +108 -76
  77. package/dist/register-class.js.map +1 -1
  78. package/dist/registry.d.ts +69 -19
  79. package/dist/registry.d.ts.map +1 -1
  80. package/dist/registry.js +70 -26
  81. package/dist/registry.js.map +1 -1
  82. package/dist/signal.d.ts +9 -2
  83. package/dist/signal.d.ts.map +1 -1
  84. package/dist/signal.js +63 -7
  85. package/dist/signal.js.map +1 -1
  86. package/dist/type.d.ts +2 -1
  87. package/dist/type.d.ts.map +1 -1
  88. package/dist/type.js +2 -0
  89. package/dist/type.js.map +1 -1
  90. package/dist/value.d.ts +8 -1
  91. package/dist/value.d.ts.map +1 -1
  92. package/dist/value.js +42 -13
  93. package/dist/value.js.map +1 -1
  94. package/dist/vfunc-call.d.ts +37 -0
  95. package/dist/vfunc-call.d.ts.map +1 -0
  96. package/dist/vfunc-call.js +168 -0
  97. package/dist/vfunc-call.js.map +1 -0
  98. package/dist/vfunc-seeds.d.ts +12 -0
  99. package/dist/vfunc-seeds.d.ts.map +1 -0
  100. package/dist/vfunc-seeds.js +19 -0
  101. package/dist/vfunc-seeds.js.map +1 -0
  102. package/dist/vfunc.d.ts +8 -0
  103. package/dist/vfunc.d.ts.map +1 -0
  104. package/dist/vfunc.js +23 -0
  105. package/dist/vfunc.js.map +1 -0
  106. package/package.json +4 -4
  107. package/src/application-class.ts +116 -0
  108. package/src/arg.ts +2 -1
  109. package/src/bind.ts +11 -0
  110. package/src/callback.ts +162 -22
  111. package/src/closure.ts +136 -0
  112. package/src/descriptors.ts +85 -24
  113. package/src/error.ts +1 -0
  114. package/src/fn.ts +92 -31
  115. package/src/index.ts +21 -6
  116. package/src/internal.ts +1 -0
  117. package/src/library.ts +2 -1
  118. package/src/lifecycle.ts +90 -23
  119. package/src/listeners.ts +3 -1
  120. package/src/mixin.ts +15 -2
  121. package/src/native-value.ts +5 -3
  122. package/src/object.ts +128 -34
  123. package/src/param-spec.ts +235 -0
  124. package/src/promisify.ts +4 -2
  125. package/src/properties.ts +458 -45
  126. package/src/register-class.ts +326 -109
  127. package/src/registry.ts +138 -40
  128. package/src/signal.ts +84 -14
  129. package/src/type.ts +3 -1
  130. package/src/value.ts +55 -10
  131. package/src/vfunc-call.ts +243 -0
  132. package/src/vfunc-seeds.ts +36 -0
  133. package/src/vfunc.ts +31 -0
package/src/lifecycle.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { keepAlive, quit as nativeQuit } from "@gtkx/native";
2
- import { blockMatchedSignalHandlers } from "./signal.js";
2
+ import { isDerivedApplication, type LocalCommandLineApplication, shutDownThroughRun } from "./application-class.js";
3
3
 
4
4
  /**
5
5
  * The GIO and GTK application surface {@link runApplication} and {@link quitApplication} drive, so any
@@ -9,13 +9,16 @@ import { blockMatchedSignalHandlers } from "./signal.js";
9
9
  type ApplicationLike = {
10
10
  /** Returns whether the application has already been registered. */
11
11
  getIsRegistered(): boolean;
12
+ /** Returns whether another process owns the application ID, making this one a remote instance. */
13
+ getIsRemote?(): boolean;
12
14
  /** Registers the application with the session, returning whether it succeeded. */
13
15
  register(cancellable: null): boolean;
14
16
  /** Emits `activate`, bringing up the application's initial user interface. */
15
17
  activate(): void;
16
- /** Quits the application, so `run` returns at the next main-loop iteration after `shutdown` runs. */
17
- quit(): void;
18
- /** Runs the application's main loop until it shuts down and returns the exit status. */
18
+ /**
19
+ * Runs GLib's own `g_application_run`, whose tail emits `shutdown`, destroys the application
20
+ * implementation and clears the registration.
21
+ */
19
22
  run(argv: string[]): number;
20
23
  /** Returns the windows currently attached to the application. */
21
24
  getWindows?(): object[];
@@ -23,9 +26,21 @@ type ApplicationLike = {
23
26
  removeWindow?(window: object): void;
24
27
  /** Connects a handler to the application's `activate` or `shutdown` signal. */
25
28
  on(signal: "activate" | "shutdown", handler: () => void): unknown;
29
+ /** Emits one of the application's own signals. */
30
+ emit(signal: "shutdown"): unknown;
31
+ };
32
+
33
+ /** What {@link runApplication} reports about the process it just started. */
34
+ type RunApplicationResult = {
35
+ /** Whether this process owns the application ID and may build a user interface. */
36
+ isPrimary: boolean;
37
+ /** The status GLib determined for the command line, which the process should exit with. */
38
+ exitStatus: number;
26
39
  };
27
40
 
28
41
  const shutdownCallbacks: (() => void)[] = [];
42
+ const startedApplications: WeakSet<object> = new WeakSet();
43
+ const shutDownApplications: WeakSet<object> = new WeakSet();
29
44
  /**
30
45
  * Runs every registered exit callback and shuts down the native runtime. Safe to
31
46
  * call more than once; only the first call takes effect.
@@ -60,13 +75,9 @@ const onExit = (callback: () => void): void => {
60
75
  shutdownCallbacks.push(callback);
61
76
  };
62
77
 
63
- /**
64
- * Registers the application if needed and activates it, keeping the runtime alive
65
- * while it is active and releasing it on shutdown.
66
- *
67
- * @param application The application to register and activate.
68
- */
69
- const runApplication = (application: ApplicationLike): void => {
78
+ const startApplication = (application: ApplicationLike & LocalCommandLineApplication, argv: string[]): number => {
79
+ startedApplications.add(application);
80
+
70
81
  application.on("activate", () => {
71
82
  keepAlive(true);
72
83
  });
@@ -75,22 +86,78 @@ const runApplication = (application: ApplicationLike): void => {
75
86
  keepAlive(false);
76
87
  });
77
88
 
78
- if (!application.getIsRegistered()) {
79
- application.register(null);
89
+ return application.runLocalCommandLine(argv)[2];
90
+ };
91
+
92
+ const restartApplication = (application: ApplicationLike): number => {
93
+ shutDownApplications.delete(application);
94
+
95
+ if (!application.register(null)) {
96
+ return 1;
80
97
  }
81
98
 
82
99
  application.activate();
100
+
101
+ return 0;
83
102
  };
84
103
 
85
104
  /**
86
- * Detaches every window from the application and runs its main loop until it shuts
87
- * down, so pending shutdown work completes before the process exits. Does nothing
88
- * when the application was never registered.
105
+ * Hands `argv` to GLib's own command line handling, which parses the application's options, prints
106
+ * `--help`, runs `handle-local-options`, registers the application, and either activates it or
107
+ * forwards the command line to the process that already owns the application ID. The runtime is
108
+ * held alive while the application is active and released on shutdown.
109
+ *
110
+ * `g_application_run()` is not what starts the application, because it would drive its own main loop
111
+ * and freeze Node; only the local command line handling it delegates to runs here. {@link
112
+ * quitApplication} calls it once no window holds the application open, to reach the teardown only it
113
+ * performs.
114
+ *
115
+ * GLib parses a given application's command line at most once, so starting an application that has
116
+ * already run registers and activates it instead of reading `argv` again.
117
+ *
118
+ * When another process already owns the application ID, this one registers as a remote instance: no
119
+ * user interface may be built here, because a remote application has no `GtkApplicationImpl` and
120
+ * attaching a window to it crashes.
121
+ *
122
+ * @param application The application to start.
123
+ * @param argv The command line, whose first entry names the program as `--help` should print it.
124
+ * @returns Whether this process may build a user interface, and the status to exit with.
125
+ */
126
+ const runApplication = (application: ApplicationLike, argv: string[]): RunApplicationResult => {
127
+ if (!isDerivedApplication(application)) {
128
+ throw new Error(
129
+ "runApplication: this application was not built by GTKX, so its command line cannot be " +
130
+ "parsed and it cannot be shut down safely; render <GtkApplication> or <AdwApplication>, " +
131
+ "or construct it with createApplication from @gtkx/runtime",
132
+ );
133
+ }
134
+
135
+ const exitStatus = startedApplications.has(application)
136
+ ? restartApplication(application)
137
+ : startApplication(application, argv);
138
+
139
+ const isPrimary = application.getIsRegistered() && application.getIsRemote?.() !== true;
140
+
141
+ if (isPrimary) {
142
+ keepAlive(true);
143
+ }
144
+
145
+ return { isPrimary, exitStatus };
146
+ };
147
+
148
+ /**
149
+ * Detaches every window from the application and runs GLib's own shutdown, which emits `shutdown`,
150
+ * destroys the application implementation and releases the D-Bus registration. Does nothing for an
151
+ * application that is not registered, so a repeated call is a no-op.
152
+ *
153
+ * GLib's own shutdown is reachable once per application: reaching it marks the application as
154
+ * quitting, which GLib never undoes. An application that has already quit falls back to emitting
155
+ * `shutdown`, which releases the runtime and leaves the registration for GLib to drop at finalize.
89
156
  *
90
157
  * @param application The application to shut down.
91
158
  */
92
159
  const quitApplication = (application: ApplicationLike): void => {
93
- if (!application.getIsRegistered()) {
160
+ if (!application.getIsRegistered() || shutDownApplications.has(application)) {
94
161
  return;
95
162
  }
96
163
 
@@ -100,12 +167,12 @@ const quitApplication = (application: ApplicationLike): void => {
100
167
  application.removeWindow?.(window);
101
168
  }
102
169
 
103
- application.on("shutdown", () => {
104
- application.quit();
105
- });
170
+ shutDownThroughRun(application);
106
171
 
107
- blockMatchedSignalHandlers(application, "activate");
108
- application.run([]);
172
+ if (application.getIsRegistered()) {
173
+ shutDownApplications.add(application);
174
+ application.emit("shutdown");
175
+ }
109
176
  };
110
177
 
111
- export { onExit, quit, runApplication, quitApplication, type ApplicationLike };
178
+ export { onExit, quit, runApplication, quitApplication, type ApplicationLike, type RunApplicationResult };
package/src/listeners.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { isSignalHandlerConnected, type SignalHandler } from "./signal.js";
1
+ import { isSignalHandlerConnected, type SignalHandler, untrackConnection } from "./signal.js";
2
2
 
3
3
  /**
4
4
  * The connect and disconnect surface {@link onSignal}, {@link onceSignal} and {@link offSignal}
@@ -81,6 +81,7 @@ function onceSignal(instance: SignalConnectable, signal: string, handler: Signal
81
81
 
82
82
  const wrapped: SignalHandler = (...args) => {
83
83
  untrackHandlerId(instance, signal, handlerId);
84
+ untrackConnection(instance, handlerId);
84
85
  instance.disconnect(handlerId);
85
86
 
86
87
  return handler(...args);
@@ -107,6 +108,7 @@ function offSignal(instance: SignalConnectable, signal: string, handler: SignalH
107
108
  }
108
109
 
109
110
  untrackHandlerId(instance, signal, handlerId);
111
+ untrackConnection(instance, handlerId);
110
112
 
111
113
  if (!isSignalHandlerConnected(instance, handlerId)) {
112
114
  return;
package/src/mixin.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AnyClass } from "@gtkx/utils";
1
+ import { type AnyClass, getParentClass } from "@gtkx/utils";
2
2
 
3
3
  /**
4
4
  * The signal plumbing every base class passed to a {@link Mixin} provides, so mixed-in interface
@@ -76,4 +76,17 @@ function installMixins(target: AnyClass, mixins: Mixin[]): void {
76
76
  }
77
77
  }
78
78
 
79
- export { installMixins, type MixinReceiver, type Mixin };
79
+ function dropLayerMembers(layer: AnyClass, names: Set<string>): void {
80
+ for (const name of names) {
81
+ Reflect.deleteProperty(layer.prototype, name);
82
+ }
83
+ }
84
+
85
+ function insertMixinLayer(target: AnyClass, mixin: Mixin, inheritedNames: Set<string>): void {
86
+ const layer = mixin(getParentClass(target) as AnyClass<MixinReceiver>);
87
+ dropLayerMembers(layer, inheritedNames);
88
+ Object.setPrototypeOf(target.prototype, layer.prototype);
89
+ Object.setPrototypeOf(target, layer);
90
+ }
91
+
92
+ export { insertMixinLayer, installMixins, type MixinReceiver, type Mixin };
@@ -1,6 +1,6 @@
1
1
  import type { Descriptor, ExternalObject, Handle } from "@gtkx/native";
2
2
  import type { ArrayDescriptor, FundamentalDescriptor, HashTableDescriptor, StructDescriptor } from "./descriptors.js";
3
- import { getWrapperClass, tryGetHandle, wrapHandle } from "./registry.js";
3
+ import { getHandle, getWrapperClass, wrapHandle, wrapObject } from "./registry.js";
4
4
  import { resolveDescriptorType } from "./type.js";
5
5
 
6
6
  type MarshalledKind = "object" | "struct" | "boxed" | "fundamental" | "array" | "hashtable";
@@ -76,7 +76,7 @@ function fromNative(descriptor: Descriptor, value: unknown): unknown {
76
76
 
77
77
  switch (descriptor.kind) {
78
78
  case "object": {
79
- return wrapHandle(value as ExternalObject<Handle> | null);
79
+ return wrapObject(value);
80
80
  }
81
81
  case "struct": {
82
82
  return wrapHandle(value as ExternalObject<Handle> | null, (descriptor as StructDescriptor).wrapperClass);
@@ -129,7 +129,9 @@ function toNative(descriptor: Descriptor, value: unknown): unknown {
129
129
  case "struct":
130
130
  case "boxed":
131
131
  case "fundamental": {
132
- return tryGetHandle(value as object | null | undefined) ?? null;
132
+ const instance = value as object | null | undefined;
133
+
134
+ return instance == null ? null : getHandle(instance);
133
135
  }
134
136
  case "array": {
135
137
  return collectionToNative(descriptor, value);
package/src/object.ts CHANGED
@@ -1,21 +1,25 @@
1
- import type { Descriptor, ExternalObject, Handle } from "@gtkx/native";
1
+ import { type Descriptor, type ExternalObject, type Handle, newObject } from "@gtkx/native";
2
+ import { type AnyClass, getParentClass } from "@gtkx/utils";
2
3
  import { bind } from "./bind.js";
3
- import { biguint64T, objectT, sizedArrayT, stringT, uint32T, voidT } from "./descriptors.js";
4
- import { LIB, VALUE_SIZE, VALUE_T } from "./library.js";
5
- import { getHandle } from "./registry.js";
4
+ import { objectT, stringT, voidT } from "./descriptors.js";
5
+ import { LIB, VALUE_T } from "./library.js";
6
+ import { type ConstructProperty, constructPropertyFor } from "./properties.js";
7
+ import { getHandle, registerWrapper } from "./registry.js";
6
8
  import { fromValue, newValueForDescriptor, toValue } from "./value.js";
7
9
 
8
- const gObjectNewWithProperties = bind(
9
- LIB,
10
- "g_object_new_with_properties",
11
- [
12
- biguint64T,
13
- uint32T,
14
- sizedArrayT(stringT("borrowed"), 1, "borrowed"),
15
- sizedArrayT(VALUE_T, 1, "borrowed", VALUE_SIZE),
16
- ],
17
- objectT("full"),
18
- );
10
+ /**
11
+ * One construct property a wrapper class accepts: the canonical `GObject` name it is set under,
12
+ * and the descriptor its value is marshalled through.
13
+ */
14
+ type ConstructBinding = [name: string, descriptor: Descriptor];
15
+ /** The construct properties a wrapper class accepts, keyed by the camelCased name callers give them. */
16
+ type ConstructBindings = Record<string, ConstructBinding>;
17
+ type ResolvedBindings = { generation: number; bindings: ConstructBindings };
18
+
19
+ const declaredBindings: WeakMap<AnyClass, ConstructBindings> = new WeakMap();
20
+ const resolvedBindings: WeakMap<AnyClass, ResolvedBindings> = new WeakMap();
21
+ const declarations = { generation: 0 };
22
+ const NO_BINDINGS: ConstructBindings = Object.create(null) as ConstructBindings;
19
23
 
20
24
  const gObjectGetProperty = bind(
21
25
  LIB,
@@ -31,37 +35,120 @@ const gObjectSetProperty = bind(
31
35
  voidT,
32
36
  );
33
37
 
38
+ function collectDeclaredBindings(cls: AnyClass): ConstructBindings[] {
39
+ const declared: ConstructBindings[] = [];
40
+ let current: AnyClass | null = cls;
41
+
42
+ while (current !== null) {
43
+ const own = declaredBindings.get(current);
44
+
45
+ if (own !== undefined) {
46
+ declared.push(own);
47
+ }
48
+
49
+ current = getParentClass(current);
50
+ }
51
+
52
+ return declared;
53
+ }
54
+
55
+ function mergeDeclaredBindings(cls: AnyClass): ConstructBindings {
56
+ const declared = collectDeclaredBindings(cls);
57
+ const merged: ConstructBindings = Object.create(null) as ConstructBindings;
58
+
59
+ for (let index = declared.length - 1; index >= 0; index--) {
60
+ Object.assign(merged, declared[index]);
61
+ }
62
+
63
+ return merged;
64
+ }
65
+
66
+ function constructBindingsFor(cls: AnyClass | undefined): ConstructBindings {
67
+ if (cls === undefined) {
68
+ return NO_BINDINGS;
69
+ }
70
+
71
+ const cached = resolvedBindings.get(cls);
72
+
73
+ if (cached?.generation === declarations.generation) {
74
+ return cached.bindings;
75
+ }
76
+
77
+ const bindings = mergeDeclaredBindings(cls);
78
+ resolvedBindings.set(cls, { generation: declarations.generation, bindings });
79
+
80
+ return bindings;
81
+ }
82
+
83
+ /**
84
+ * Declares the construct properties a wrapper class accepts, so `newObjectWithProperties`
85
+ * marshals each one through its descriptor rather than resolving it from the
86
+ * `GObject.ParamSpec` the type installs. A class inherits the declarations of its ancestors.
87
+ * Registering takes effect immediately, including for classes already constructed from and for
88
+ * subclasses that already inherited an earlier declaration.
89
+ *
90
+ * @param cls The wrapper class the properties are declared on.
91
+ * @param bindings CamelCased property names mapped to their canonical name and descriptor.
92
+ */
93
+ function registerConstructProperties(cls: AnyClass, bindings: ConstructBindings): void {
94
+ declaredBindings.set(cls, bindings);
95
+ declarations.generation += 1;
96
+ }
97
+
98
+ function constructPropertyForEntry(
99
+ source: { gtype: bigint; bindings: ConstructBindings; wrapper: object },
100
+ name: string,
101
+ value: unknown,
102
+ ): ConstructProperty | undefined {
103
+ if (value === undefined) {
104
+ return undefined;
105
+ }
106
+
107
+ const binding = source.bindings[name];
108
+
109
+ if (binding === undefined) {
110
+ return constructPropertyFor(source.gtype, name, value, source.wrapper);
111
+ }
112
+
113
+ return { name: binding[0], value: toValue(binding[1], value) };
114
+ }
115
+
34
116
  /**
35
117
  * Constructs a new GObject of the given type, setting the supplied construct
36
- * properties. Entries whose value is `undefined` or not a `[descriptor, value]`
37
- * pair are skipped.
118
+ * properties, and binds `wrapper` to it. A property the wrapper's class declares
119
+ * through `registerConstructProperties` is marshalled through its descriptor; any
120
+ * other one is marshalled through the `GObject.ParamSpec` the type installs under
121
+ * that name, dashed or camelCased, and is skipped when the type installs none.
122
+ * A value that ParamSpec would refuse throws before GObject sees it: a `TypeError`
123
+ * for a read-only property and for a value of a type the property cannot hold, and
124
+ * a `RangeError` for a value the ParamSpec rejects.
125
+ * Properties whose value is `undefined` are skipped. A type registered with
126
+ * `registerClass` binds the wrapper before its `constructed` slot runs, so an
127
+ * override of that slot already sees a usable instance.
38
128
  *
39
129
  * @param gtype The GType of the object to construct.
40
- * @param props Property names mapped to `[descriptor, value]` pairs.
130
+ * @param props Property names mapped to the values to set them to.
131
+ * @param wrapper The wrapper instance to bind to the new object.
41
132
  * @returns The handle of the newly created object.
42
133
  */
43
- function newObjectWithProperties(gtype: bigint, props: Record<string, unknown>): ExternalObject<Handle> {
134
+ function newObjectWithProperties(gtype: bigint, props: object, wrapper: object): ExternalObject<Handle> {
44
135
  const names: string[] = [];
45
136
  const values: ExternalObject<Handle>[] = [];
137
+ const bindings = constructBindingsFor(wrapper.constructor as AnyClass | undefined);
138
+ const source = { gtype, bindings, wrapper };
46
139
 
47
- for (const name in props) {
48
- const entry: unknown = props[name];
140
+ for (const name of Object.keys(props)) {
141
+ const property = constructPropertyForEntry(source, name, Reflect.get(props, name));
49
142
 
50
- if (!Array.isArray(entry)) {
51
- continue;
143
+ if (property !== undefined) {
144
+ names.push(property.name);
145
+ values.push(property.value);
52
146
  }
53
-
54
- const [descriptor, value] = entry as [Descriptor, unknown];
55
-
56
- if (value === undefined) {
57
- continue;
58
- }
59
-
60
- names.push(name);
61
- values.push(toValue(descriptor, value));
62
147
  }
63
148
 
64
- return gObjectNewWithProperties(gtype, names.length, names, values) as ExternalObject<Handle>;
149
+ newObject(gtype, names, values, wrapper, registerWrapper);
150
+
151
+ return getHandle(wrapper);
65
152
  }
66
153
 
67
154
  /**
@@ -92,4 +179,11 @@ function setObjectProperty(obj: object, propertyName: string, descriptor: Descri
92
179
  gObjectSetProperty(getHandle(obj), propertyName, toValue(descriptor, jsValue));
93
180
  }
94
181
 
95
- export { newObjectWithProperties, getObjectProperty, setObjectProperty };
182
+ export {
183
+ newObjectWithProperties,
184
+ getObjectProperty,
185
+ registerConstructProperties,
186
+ setObjectProperty,
187
+ type ConstructBinding,
188
+ type ConstructBindings,
189
+ };
@@ -0,0 +1,235 @@
1
+ import { type ExternalObject, type Handle, read } from "@gtkx/native";
2
+ import { bind } from "./bind.js";
3
+ import { biguint64T, booleanT, stringT, uint32T, voidT } from "./descriptors.js";
4
+ import { LIB, PARAM_T, VALUE_T } from "./library.js";
5
+ import { getInstanceType } from "./registry.js";
6
+ import {
7
+ getStrvType,
8
+ isTypedClass,
9
+ TYPE_BOOLEAN,
10
+ TYPE_BOXED,
11
+ TYPE_CHAR,
12
+ TYPE_DOUBLE,
13
+ TYPE_ENUM,
14
+ TYPE_FLAGS,
15
+ TYPE_FLOAT,
16
+ TYPE_INT,
17
+ TYPE_INT64,
18
+ TYPE_INTERFACE,
19
+ TYPE_INVALID,
20
+ TYPE_LONG,
21
+ TYPE_OBJECT,
22
+ TYPE_PARAM,
23
+ TYPE_POINTER,
24
+ TYPE_STRING,
25
+ TYPE_UCHAR,
26
+ TYPE_UINT,
27
+ TYPE_UINT64,
28
+ TYPE_ULONG,
29
+ TYPE_VARIANT,
30
+ typeFundamental,
31
+ typeIsA,
32
+ } from "./type.js";
33
+
34
+ type ValueGuard = (value: unknown) => boolean;
35
+ type ParamLayout = { flags: number; valueType: bigint };
36
+
37
+ const PARAM_READABLE = 1;
38
+ const PARAM_WRITABLE = 2;
39
+ const PARAM_CONSTRUCT_ONLY = 8;
40
+ const PARAM_LAX_VALIDATION = 16;
41
+ const READ_FLAGS = PARAM_READABLE | PARAM_WRITABLE | PARAM_CONSTRUCT_ONLY | PARAM_LAX_VALIDATION;
42
+ const FLAGS_BYTE_OFFSET = 16;
43
+ const VALUE_TYPE_BYTE_OFFSET = 24;
44
+ const LAYOUT_PROBE_NAME = "gtkx-param-layout";
45
+ const INT8_MINIMUM = -128;
46
+ const INT8_MAXIMUM = 127;
47
+ const UINT8_MAXIMUM = 255;
48
+ const INT32_MINIMUM = -2_147_483_648;
49
+ const INT32_MAXIMUM = 2_147_483_647;
50
+ const UINT32_MAXIMUM = 4_294_967_295;
51
+ const INT64_MINIMUM = -(2n ** 63n);
52
+ const INT64_MAXIMUM = 2n ** 63n - 1n;
53
+ const UINT64_MAXIMUM = 2n ** 64n - 1n;
54
+ const FLOAT_MAXIMUM = 3.4028234663852886e38;
55
+
56
+ const WRAPPED_FUNDAMENTALS: Set<bigint> = new Set([
57
+ TYPE_BOXED,
58
+ TYPE_INTERFACE,
59
+ TYPE_OBJECT,
60
+ TYPE_PARAM,
61
+ TYPE_VARIANT,
62
+ ]);
63
+
64
+ const SCALAR_GUARDS: Map<bigint, ValueGuard> = new Map([
65
+ [TYPE_BOOLEAN, isBooleanValue],
66
+ [TYPE_STRING, isStringValue],
67
+ [TYPE_CHAR, integerGuardFor(INT8_MINIMUM, INT8_MAXIMUM)],
68
+ [TYPE_UCHAR, integerGuardFor(0, UINT8_MAXIMUM)],
69
+ [TYPE_INT, integerGuardFor(INT32_MINIMUM, INT32_MAXIMUM)],
70
+ [TYPE_UINT, integerGuardFor(0, UINT32_MAXIMUM)],
71
+ [TYPE_ENUM, integerGuardFor(INT32_MINIMUM, INT32_MAXIMUM)],
72
+ [TYPE_FLAGS, integerGuardFor(0, UINT32_MAXIMUM)],
73
+ [TYPE_LONG, wideIntegerGuardFor(INT64_MINIMUM, INT64_MAXIMUM)],
74
+ [TYPE_ULONG, wideIntegerGuardFor(0n, UINT64_MAXIMUM)],
75
+ [TYPE_INT64, wideIntegerGuardFor(INT64_MINIMUM, INT64_MAXIMUM)],
76
+ [TYPE_UINT64, wideIntegerGuardFor(0n, UINT64_MAXIMUM)],
77
+ [TYPE_FLOAT, isFloatValue],
78
+ [TYPE_DOUBLE, isDoubleValue],
79
+ [TYPE_POINTER, isNullValue],
80
+ ]);
81
+
82
+ const layout = { wasChecked: false };
83
+ const paramValueValidate = bind(LIB, "g_param_value_validate", [PARAM_T, VALUE_T], booleanT);
84
+ const paramSpecUnref = bind(LIB, "g_param_spec_unref", [PARAM_T], voidT);
85
+ const paramSpecRefSink = bind(LIB, "g_param_spec_ref_sink", [PARAM_T], PARAM_T);
86
+
87
+ const newParamSpecBoolean = bind(
88
+ LIB,
89
+ "g_param_spec_boolean",
90
+ [stringT("borrowed"), stringT("borrowed"), stringT("borrowed"), booleanT, uint32T],
91
+ PARAM_T,
92
+ );
93
+
94
+ const getParamFlags = (pspec: ExternalObject<Handle>): number => read(pspec, uint32T, FLAGS_BYTE_OFFSET) as number;
95
+
96
+ const getParamValueType = (pspec: ExternalObject<Handle>): bigint =>
97
+ read(pspec, biguint64T, VALUE_TYPE_BYTE_OFFSET) as bigint;
98
+
99
+ const isParamWritable = (flags: number): boolean => (flags & PARAM_WRITABLE) !== 0;
100
+ const isParamConstructOnly = (flags: number): boolean => (flags & PARAM_CONSTRUCT_ONLY) !== 0;
101
+ const isParamLaxlyValidated = (flags: number): boolean => (flags & PARAM_LAX_VALIDATION) !== 0;
102
+
103
+ const wasParamValueModified = (pspec: ExternalObject<Handle>, value: ExternalObject<Handle>): boolean =>
104
+ paramValueValidate(pspec, value) as boolean;
105
+
106
+ function isBooleanValue(value: unknown): boolean {
107
+ return typeof value === "boolean";
108
+ }
109
+
110
+ function isStringValue(value: unknown): boolean {
111
+ return value == null || typeof value === "string";
112
+ }
113
+
114
+ function isStrvValue(value: unknown): boolean {
115
+ return value == null || (Array.isArray(value) && value.every((item) => typeof item === "string"));
116
+ }
117
+
118
+ function isDoubleValue(value: unknown): boolean {
119
+ return value == null || (typeof value === "number" && Number.isFinite(value));
120
+ }
121
+
122
+ function isFloatValue(value: unknown): boolean {
123
+ return isDoubleValue(value) && (value == null || Math.abs(value as number) <= FLOAT_MAXIMUM);
124
+ }
125
+
126
+ function isAnyValue(): boolean {
127
+ return true;
128
+ }
129
+
130
+ function isNullValue(value: unknown): boolean {
131
+ return value == null;
132
+ }
133
+
134
+ function isIntegerWithin(value: unknown, minimum: number, maximum: number): boolean {
135
+ return Number.isSafeInteger(value) && (value as number) >= minimum && (value as number) <= maximum;
136
+ }
137
+
138
+ function integerGuardFor(minimum: number, maximum: number): ValueGuard {
139
+ return (value) => value == null || isIntegerWithin(value, minimum, maximum);
140
+ }
141
+
142
+ function toWideInteger(value: unknown): bigint | undefined {
143
+ if (typeof value === "bigint") {
144
+ return value;
145
+ }
146
+
147
+ return Number.isSafeInteger(value) ? BigInt(value as number) : undefined;
148
+ }
149
+
150
+ function wideIntegerGuardFor(minimum: bigint, maximum: bigint): ValueGuard {
151
+ return (value) => {
152
+ if (value == null) {
153
+ return true;
154
+ }
155
+
156
+ const wide = toWideInteger(value);
157
+
158
+ return wide !== undefined && wide >= minimum && wide <= maximum;
159
+ };
160
+ }
161
+
162
+ function resolveGtype(value: unknown): bigint {
163
+ const instanceType = getInstanceType(value as object);
164
+
165
+ if (instanceType !== TYPE_INVALID) {
166
+ return instanceType;
167
+ }
168
+
169
+ return isTypedClass(value) ? value.__type__ : TYPE_INVALID;
170
+ }
171
+
172
+ function wrappedGuardFor(valueType: bigint): ValueGuard {
173
+ return (value) => value == null || typeIsA(resolveGtype(value), valueType);
174
+ }
175
+
176
+ function valueGuardFor(valueType: bigint): ValueGuard {
177
+ assertParamLayout();
178
+
179
+ if (valueType === getStrvType()) {
180
+ return isStrvValue;
181
+ }
182
+
183
+ const fundamental = typeFundamental(valueType);
184
+
185
+ if (WRAPPED_FUNDAMENTALS.has(fundamental)) {
186
+ return wrappedGuardFor(valueType);
187
+ }
188
+
189
+ return SCALAR_GUARDS.get(fundamental) ?? isAnyValue;
190
+ }
191
+
192
+ function readProbeLayout(flags: number): ParamLayout {
193
+ const probe = newParamSpecBoolean(LAYOUT_PROBE_NAME, null, null, false, flags) as ExternalObject<Handle>;
194
+ paramSpecRefSink(probe);
195
+
196
+ try {
197
+ return { flags: getParamFlags(probe), valueType: getParamValueType(probe) };
198
+ } finally {
199
+ paramSpecUnref(probe);
200
+ }
201
+ }
202
+
203
+ function isLayoutIntact(flags: number): boolean {
204
+ const layout = readProbeLayout(flags);
205
+
206
+ return (layout.flags & READ_FLAGS) === flags && layout.valueType === TYPE_BOOLEAN;
207
+ }
208
+
209
+ function assertParamLayout(): void {
210
+ if (layout.wasChecked) {
211
+ return;
212
+ }
213
+
214
+ layout.wasChecked = true;
215
+ const readWrite = PARAM_READABLE | PARAM_WRITABLE;
216
+
217
+ if (isLayoutIntact(readWrite) && isLayoutIntact(readWrite | PARAM_CONSTRUCT_ONLY | PARAM_LAX_VALIDATION)) {
218
+ return;
219
+ }
220
+
221
+ throw new Error(
222
+ "GParamSpec does not match the memory layout this build reads a property's flags and value type at",
223
+ );
224
+ }
225
+
226
+ export {
227
+ getParamFlags,
228
+ getParamValueType,
229
+ isParamConstructOnly,
230
+ isParamLaxlyValidated,
231
+ isParamWritable,
232
+ type ValueGuard,
233
+ valueGuardFor,
234
+ wasParamValueModified,
235
+ };