@gtkx/react 1.0.0-rc.4 → 1.1.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 (155) hide show
  1. package/README.md +5 -5
  2. package/dist/adw/dialog.d.ts +3 -4
  3. package/dist/adw/dialog.d.ts.map +1 -1
  4. package/dist/adw/dialog.js +18 -21
  5. package/dist/adw/dialog.js.map +1 -1
  6. package/dist/adw/element-behaviors.js +102 -54
  7. package/dist/adw/element-behaviors.js.map +1 -1
  8. package/dist/adw/element-config.d.ts +0 -6
  9. package/dist/adw/element-config.d.ts.map +1 -1
  10. package/dist/adw/element-config.js +15 -17
  11. package/dist/adw/element-config.js.map +1 -1
  12. package/dist/adw/prop-types.d.ts +34 -3
  13. package/dist/adw/prop-types.d.ts.map +1 -1
  14. package/dist/adw/prop-types.js.map +1 -1
  15. package/dist/components/application-window.d.ts +5 -0
  16. package/dist/components/application-window.d.ts.map +1 -0
  17. package/dist/components/application-window.js +7 -0
  18. package/dist/components/application-window.js.map +1 -0
  19. package/dist/components/application.d.ts +2 -8
  20. package/dist/components/application.d.ts.map +1 -1
  21. package/dist/components/application.js +31 -7
  22. package/dist/components/application.js.map +1 -1
  23. package/dist/components/element.d.ts +13 -1
  24. package/dist/components/element.d.ts.map +1 -1
  25. package/dist/components/element.js +39 -14
  26. package/dist/components/element.js.map +1 -1
  27. package/dist/components/portaled.d.ts +5 -0
  28. package/dist/components/portaled.d.ts.map +1 -0
  29. package/dist/components/portaled.js +8 -0
  30. package/dist/components/portaled.js.map +1 -0
  31. package/dist/components/window.d.ts +5 -5
  32. package/dist/components/window.d.ts.map +1 -1
  33. package/dist/components/window.js +16 -16
  34. package/dist/components/window.js.map +1 -1
  35. package/dist/config.d.ts +3 -1
  36. package/dist/config.d.ts.map +1 -1
  37. package/dist/config.js +2 -0
  38. package/dist/config.js.map +1 -1
  39. package/dist/element-behaviors.js +11 -1
  40. package/dist/element-behaviors.js.map +1 -1
  41. package/dist/element-config.d.ts.map +1 -1
  42. package/dist/element-config.js +7 -4
  43. package/dist/element-config.js.map +1 -1
  44. package/dist/hooks/use-merged-refs.d.ts +1 -4
  45. package/dist/hooks/use-merged-refs.d.ts.map +1 -1
  46. package/dist/hooks/use-merged-refs.js +1 -1
  47. package/dist/hooks/use-merged-refs.js.map +1 -1
  48. package/dist/hooks/use-presented-instance.d.ts +12 -0
  49. package/dist/hooks/use-presented-instance.d.ts.map +1 -0
  50. package/dist/hooks/use-presented-instance.js +27 -0
  51. package/dist/hooks/use-presented-instance.js.map +1 -0
  52. package/dist/hooks/use-property.d.ts +2 -1
  53. package/dist/hooks/use-property.d.ts.map +1 -1
  54. package/dist/hooks/use-property.js.map +1 -1
  55. package/dist/hooks/use-signal.d.ts +2 -1
  56. package/dist/hooks/use-signal.d.ts.map +1 -1
  57. package/dist/hooks/use-signal.js.map +1 -1
  58. package/dist/internal.d.ts +5 -3
  59. package/dist/internal.d.ts.map +1 -1
  60. package/dist/internal.js +5 -2
  61. package/dist/internal.js.map +1 -1
  62. package/dist/prop-types.d.ts +29 -5
  63. package/dist/prop-types.d.ts.map +1 -1
  64. package/dist/prop-types.js.map +1 -1
  65. package/dist/reconciler/apply-props.d.ts +4 -4
  66. package/dist/reconciler/apply-props.d.ts.map +1 -1
  67. package/dist/reconciler/apply-props.js +62 -20
  68. package/dist/reconciler/apply-props.js.map +1 -1
  69. package/dist/reconciler/behaviors.d.ts +9 -5
  70. package/dist/reconciler/behaviors.d.ts.map +1 -1
  71. package/dist/reconciler/behaviors.js +40 -22
  72. package/dist/reconciler/behaviors.js.map +1 -1
  73. package/dist/reconciler/devtools.d.ts +6 -0
  74. package/dist/reconciler/devtools.d.ts.map +1 -0
  75. package/dist/reconciler/devtools.js +11 -0
  76. package/dist/reconciler/devtools.js.map +1 -0
  77. package/dist/reconciler/host-config.d.ts.map +1 -1
  78. package/dist/reconciler/host-config.js +18 -25
  79. package/dist/reconciler/host-config.js.map +1 -1
  80. package/dist/reconciler/instance.d.ts.map +1 -1
  81. package/dist/reconciler/instance.js +5 -3
  82. package/dist/reconciler/instance.js.map +1 -1
  83. package/dist/reconciler/metadata.d.ts +1 -1
  84. package/dist/reconciler/metadata.d.ts.map +1 -1
  85. package/dist/reconciler/metadata.js +2 -4
  86. package/dist/reconciler/metadata.js.map +1 -1
  87. package/dist/reconciler/node.d.ts +3 -2
  88. package/dist/reconciler/node.d.ts.map +1 -1
  89. package/dist/reconciler/node.js +7 -6
  90. package/dist/reconciler/node.js.map +1 -1
  91. package/dist/reconciler/placement.js +20 -20
  92. package/dist/reconciler/placement.js.map +1 -1
  93. package/dist/reconciler/registry.d.ts +9 -7
  94. package/dist/reconciler/registry.d.ts.map +1 -1
  95. package/dist/reconciler/registry.js.map +1 -1
  96. package/dist/reconciler/root-element.d.ts +1 -0
  97. package/dist/reconciler/root-element.d.ts.map +1 -1
  98. package/dist/reconciler/root-element.js.map +1 -1
  99. package/dist/reconciler/root.d.ts +17 -3
  100. package/dist/reconciler/root.d.ts.map +1 -1
  101. package/dist/reconciler/root.js +22 -8
  102. package/dist/reconciler/root.js.map +1 -1
  103. package/dist/reconciler/signals.d.ts +1 -2
  104. package/dist/reconciler/signals.d.ts.map +1 -1
  105. package/dist/reconciler/signals.js +1 -1
  106. package/dist/reconciler/signals.js.map +1 -1
  107. package/dist/utils/accessible-props.d.ts +8 -8
  108. package/dist/utils/accessible-props.d.ts.map +1 -1
  109. package/dist/utils/accessible-props.js +11 -13
  110. package/dist/utils/accessible-props.js.map +1 -1
  111. package/dist/utils/settings.d.ts +2 -1
  112. package/dist/utils/settings.d.ts.map +1 -1
  113. package/dist/utils/settings.js.map +1 -1
  114. package/dist/utils/variant.d.ts +33 -1
  115. package/dist/utils/variant.d.ts.map +1 -1
  116. package/dist/utils/variant.js.map +1 -1
  117. package/package.json +14 -4
  118. package/src/adw/dialog.tsx +25 -25
  119. package/src/adw/element-behaviors.ts +129 -57
  120. package/src/adw/element-config.ts +15 -17
  121. package/src/adw/prop-types.ts +35 -2
  122. package/src/components/application-window.tsx +10 -0
  123. package/src/components/application.tsx +45 -6
  124. package/src/components/element.tsx +52 -16
  125. package/src/components/portaled.tsx +14 -0
  126. package/src/components/window.tsx +24 -23
  127. package/src/config.ts +10 -1
  128. package/src/element-behaviors.ts +20 -0
  129. package/src/element-config.ts +7 -4
  130. package/src/hooks/use-merged-refs.ts +1 -1
  131. package/src/hooks/use-presented-instance.tsx +50 -0
  132. package/src/hooks/use-property.ts +2 -1
  133. package/src/hooks/use-signal.ts +2 -1
  134. package/src/internal.ts +5 -3
  135. package/src/prop-types.ts +34 -7
  136. package/src/reconciler/apply-props.ts +77 -24
  137. package/src/reconciler/behaviors.ts +72 -30
  138. package/src/reconciler/devtools.ts +18 -0
  139. package/src/reconciler/host-config.ts +20 -30
  140. package/src/reconciler/instance.ts +6 -3
  141. package/src/reconciler/metadata.ts +3 -6
  142. package/src/reconciler/node.ts +10 -7
  143. package/src/reconciler/placement.ts +25 -25
  144. package/src/reconciler/registry.ts +9 -7
  145. package/src/reconciler/root-element.ts +4 -1
  146. package/src/reconciler/root.ts +38 -12
  147. package/src/reconciler/signals.ts +0 -1
  148. package/src/utils/accessible-props.ts +21 -24
  149. package/src/utils/settings.ts +2 -1
  150. package/src/utils/variant.ts +33 -1
  151. package/dist/utils/accessible-metadata.d.ts +0 -5
  152. package/dist/utils/accessible-metadata.d.ts.map +0 -1
  153. package/dist/utils/accessible-metadata.js +0 -18
  154. package/dist/utils/accessible-metadata.js.map +0 -1
  155. package/src/utils/accessible-metadata.ts +0 -24
@@ -1,6 +1,15 @@
1
1
  import type * as GObject from "@gtkx/gi/gobject";
2
2
  import type * as Gtk from "@gtkx/gi/gtk";
3
- import { getInstanceType, TYPE_INVALID, typeFromName, typeIsA } from "@gtkx/runtime";
3
+ import {
4
+ type AnyClass,
5
+ type ApplicationClass,
6
+ type CommandLineApplication,
7
+ createApplication,
8
+ getClassType,
9
+ TYPE_INVALID,
10
+ typeFromName,
11
+ typeIsA,
12
+ } from "@gtkx/runtime";
4
13
  import { getOrInsert, isDeepEqual, structuredClone } from "@gtkx/utils";
5
14
  import type { DetachInfo, ElementBehavior, PlaceInfo, Props } from "./registry.js";
6
15
  import { applyWrite } from "./signals.js";
@@ -36,9 +45,9 @@ type BoxLike = GObject.Object & {
36
45
 
37
46
  type RowCache = WeakMap<GObject.Object, Gtk.Widget>;
38
47
 
39
- type IndexedInserter = GObject.Object & {
40
- remove: (child: Gtk.Widget) => void;
41
- insert: (child: Gtk.Widget, position: number) => unknown;
48
+ type IndexedChildHost<C extends GObject.Object> = GObject.Object & {
49
+ remove: (child: C) => void;
50
+ insert: (child: C, position: number) => unknown;
42
51
  };
43
52
 
44
53
  const childTypeCache: Map<string, bigint> = new Map();
@@ -50,7 +59,7 @@ const childMatcher =
50
59
  (child) => {
51
60
  const type = childTypeFor(name);
52
61
 
53
- return type === TYPE_INVALID || typeIsA(getInstanceType(child), type);
62
+ return type === TYPE_INVALID || typeIsA(getClassType(child.constructor as AnyClass), type);
54
63
  };
55
64
 
56
65
  const slotAttach =
@@ -117,43 +126,50 @@ const teardownList = <P extends GObject.Object, I, H>(
117
126
  return;
118
127
  }
119
128
 
120
- const remove = hooks.remove;
121
-
122
- if (remove === undefined) {
123
- return;
124
- }
125
-
126
129
  for (const entry of entries) {
127
- remove(object, entry.item as I, entry.handle as H);
130
+ hooks.remove?.(object, entry.item as I, entry.handle as H);
128
131
  }
129
132
  };
130
133
 
131
- const list = <P extends GObject.Object, I, H = void>(
134
+ const listUpdate = <P extends GObject.Object, I, H>(
132
135
  prop: string,
133
136
  hooks: ListHooks<P, I, H>,
134
- ): ElementBehavior<P> => {
137
+ ): NonNullable<ElementBehavior<P>["update"]> => {
135
138
  const { add } = hooks;
136
139
 
137
- return {
138
- initialize: (): ListState => ({ snapshot: [], entries: [] }),
139
- update: (object, _prev, next, context) => {
140
- const state = context as ListState;
141
- const raw = next[prop];
142
- const items: unknown[] = Array.isArray(raw) ? raw : [];
140
+ return (object, _prev, next, context) => {
141
+ const state = context as ListState;
142
+ const raw = next[prop];
143
+ const items: unknown[] = Array.isArray(raw) ? raw : [];
143
144
 
144
- if (isDeepEqual(state.snapshot, items)) {
145
- return [prop];
146
- }
145
+ if (isDeepEqual(state.snapshot, items)) {
146
+ return [prop];
147
+ }
147
148
 
148
- teardownList(object, state.entries, hooks);
149
- state.entries = items.map((item) => ({ item, handle: add?.(object, item as I) }));
150
- state.snapshot = structuredClone(items);
149
+ teardownList(object, state.entries, hooks);
150
+ state.entries = items.map((item) => ({ item, handle: add?.(object, item as I) }));
151
+ state.snapshot = structuredClone(items);
151
152
 
152
- return [prop];
153
- },
153
+ return [prop];
154
154
  };
155
155
  };
156
156
 
157
+ const list = <P extends GObject.Object, I, H = void>(
158
+ prop: string,
159
+ hooks: ListHooks<P, I, H>,
160
+ ): ElementBehavior<P> => {
161
+ const behavior: ElementBehavior<P> = {
162
+ initialize: (): ListState => ({ snapshot: [], entries: [] }),
163
+ update: listUpdate(prop, hooks),
164
+ };
165
+
166
+ if (hooks.remove === undefined && hooks.clear === undefined) {
167
+ behavior.constructOnly = [prop];
168
+ }
169
+
170
+ return behavior;
171
+ };
172
+
157
173
  const flushDeferred = <P extends GObject.Object, V>(
158
174
  object: GObject.Object,
159
175
  context: unknown,
@@ -244,6 +260,23 @@ const addRemoveSlot = <C extends GObject.Object, P extends GObject.Object>(
244
260
  remove: (parent: P, child: C) => void,
245
261
  ): ElementBehavior => slot<P, C>(slotName, childType, { attach: add, detach: remove });
246
262
 
263
+ const indexedSlot = <P extends IndexedChildHost<C>, C extends GObject.Object>(
264
+ slotName: string,
265
+ childType: string,
266
+ ): ElementBehavior<P> =>
267
+ slot<P, C>(slotName, childType, {
268
+ attach: (parent, child, info) => {
269
+ parent.insert(child, info.index);
270
+ },
271
+ detach: (parent, child) => {
272
+ parent.remove(child);
273
+ },
274
+ reorder: (parent, child, info) => {
275
+ parent.remove(child);
276
+ parent.insert(child, info.index);
277
+ },
278
+ });
279
+
247
280
  const adoptedChildrenSlot = <P extends GObject.Object, C extends GObject.Object>(
248
281
  childType: string,
249
282
  add: (parent: P, item: C) => unknown,
@@ -275,7 +308,7 @@ const wrappedRow = <W extends Gtk.Widget>(
275
308
 
276
309
  const removeWrappedRow = (
277
310
  Wrapper: new (props: Props) => Gtk.Widget,
278
- parent: IndexedInserter,
311
+ parent: IndexedChildHost<Gtk.Widget>,
279
312
  child: Gtk.Widget,
280
313
  rows: RowCache,
281
314
  ): void => {
@@ -286,7 +319,7 @@ const removeWrappedRow = (
286
319
  }
287
320
  };
288
321
 
289
- const wrappingIndexedSlot = <W extends Gtk.Widget, P extends IndexedInserter>(
322
+ const wrappingIndexedSlot = <W extends Gtk.Widget, P extends IndexedChildHost<Gtk.Widget>>(
290
323
  Wrapper: new (props: Props) => W,
291
324
  setChild: (wrapper: W, inner: Gtk.Widget) => void,
292
325
  ): ElementBehavior<P> => ({
@@ -300,7 +333,15 @@ const wrappingIndexedSlot = <W extends Gtk.Widget, P extends IndexedInserter>(
300
333
  initialize: (): RowCache => new WeakMap(),
301
334
  });
302
335
 
336
+ const applicationCreator = <P extends GObject.Object & CommandLineApplication, C extends Props>(
337
+ base: ApplicationClass<P, C>,
338
+ ): ElementBehavior<P> => ({
339
+ create: (props) => createApplication(base, props as C),
340
+ });
341
+
303
342
  export {
343
+ applicationCreator,
344
+ childMatcher,
304
345
  slot,
305
346
  value,
306
347
  list,
@@ -311,5 +352,6 @@ export {
311
352
  boxSlot,
312
353
  addRemoveSlot,
313
354
  adoptedChildrenSlot,
355
+ indexedSlot,
314
356
  wrappingIndexedSlot,
315
357
  };
@@ -0,0 +1,18 @@
1
+ type DevToolsInjectable = {
2
+ injectIntoDevTools: (...args: never[]) => unknown;
3
+ };
4
+
5
+ const injection = { isDone: false };
6
+
7
+ const hasDevToolsHook = (): boolean => Reflect.get(globalThis, "__REACT_DEVTOOLS_GLOBAL_HOOK__") !== undefined;
8
+
9
+ const injectIntoDevTools = (reconciler: DevToolsInjectable): void => {
10
+ if (injection.isDone || !hasDevToolsHook()) {
11
+ return;
12
+ }
13
+
14
+ injection.isDone = true;
15
+ reconciler.injectIntoDevTools();
16
+ };
17
+
18
+ export { injectIntoDevTools };
@@ -1,29 +1,27 @@
1
1
  import type * as GObject from "@gtkx/gi/gobject";
2
2
  import * as Gtk from "@gtkx/gi/gtk";
3
- import { getInstanceType, typeName } from "@gtkx/runtime";
3
+ import { type AnyClass, getClassType, typeName } from "@gtkx/runtime";
4
4
  import { getOrInsert } from "@gtkx/utils";
5
5
  import { createContext } from "react";
6
6
  import ReactReconciler from "react-reconciler";
7
7
  import { DefaultEventPriority, DiscreteEventPriority, NoEventPriority } from "react-reconciler/constants.js";
8
8
  import type { Props } from "./registry.js";
9
+ import packageManifest from "../../package.json" with { type: "json" };
9
10
  import { Prop } from "../components/element.js";
10
11
  import {
11
12
  applyAdoptedProps,
12
13
  applyElementProps,
13
- assertConstructOnlyUnchanged,
14
+ assertPropsCanChange,
15
+ flushAccessible,
14
16
  flushBehaviors,
15
- mountBehaviors,
16
- unmountBehaviors,
17
17
  } from "./apply-props.js";
18
18
  import { attachChild, detachChild } from "./child-routing.js";
19
19
  import { resolveElementNode } from "./instance.js";
20
- import { typeInfoFor } from "./metadata.js";
21
20
  import {
22
21
  type AnyNode,
23
22
  createElementNode,
24
23
  createPropNode,
25
24
  createTextNode,
26
- type Dispatch,
27
25
  ELEMENT_KIND,
28
26
  type ElementNode,
29
27
  type Instance,
@@ -50,14 +48,21 @@ type PriorityTracker = {
50
48
  withDiscrete: <T>(fn: () => T) => T;
51
49
  };
52
50
 
51
+ const RENDERER_VERSION = packageManifest.version;
53
52
  const HOST_CONTEXT: Record<string, never> = {};
54
53
  const containerNodes: WeakMap<object, ElementNode> = new WeakMap();
55
54
  const priority = createPriorityTracker();
56
55
 
57
56
  const hostConfig = {
57
+ rendererPackageName: "@gtkx/react",
58
+ rendererVersion: RENDERER_VERSION,
58
59
  supportsMutation: true,
59
60
  supportsPersistence: false,
60
61
  supportsHydration: false,
62
+ supportsMicrotasks: true,
63
+ scheduleMicrotask: (fn: () => void): void => {
64
+ queueMicrotask(fn);
65
+ },
61
66
  isPrimaryRenderer: true,
62
67
  noTimeout: -1,
63
68
  scheduleTimeout: (fn: (...args: unknown[]) => unknown, delay?: number): ReturnType<typeof setTimeout> =>
@@ -71,18 +76,11 @@ const hostConfig = {
71
76
  attachChild(parent, child, null);
72
77
  },
73
78
  finalizeInitialChildren: (instance: Instance, _type: string, props: Props): boolean => {
74
- if (instance.kind !== ELEMENT_KIND) {
75
- return false;
76
- }
77
-
78
- validateContentMix(instance, props);
79
-
80
- return typeInfoFor(instance.typeName).hasMount;
81
- },
82
- commitMount: (instance: Instance): void => {
83
79
  if (instance.kind === ELEMENT_KIND) {
84
- mountBehaviors(instance);
80
+ validateContentMix(instance, props);
85
81
  }
82
+
83
+ return false;
86
84
  },
87
85
  shouldSetTextContent: (): boolean => false,
88
86
  getRootHostContext: (): Record<string, never> => HOST_CONTEXT,
@@ -92,6 +90,7 @@ const hostConfig = {
92
90
  resetAfterCommit: (): void => {
93
91
  flushTextHosts();
94
92
  flushBehaviors();
93
+ flushAccessible();
95
94
  },
96
95
  preparePortalMount: (): void => undefined,
97
96
  clearContainer: (): void => undefined,
@@ -135,7 +134,6 @@ const hostConfig = {
135
134
  detachDeletedInstance: (instance: Instance): void => {
136
135
  if (instance.kind === ELEMENT_KIND) {
137
136
  disconnectAllHandlers(instance);
138
- unmountBehaviors(instance);
139
137
  } else if (instance.kind === LAZY_KIND && instance.adopted !== null) {
140
138
  disconnectAllHandlers(lazyTarget(instance, instance.adopted));
141
139
  }
@@ -191,14 +189,14 @@ function createPriorityTracker(): PriorityTracker {
191
189
 
192
190
  const updateInstance = (instance: Instance, prev: Props, next: Props): void => {
193
191
  if (instance.kind === ELEMENT_KIND) {
194
- assertConstructOnlyUnchanged(instance.typeName, prev, next);
192
+ assertPropsCanChange(instance.typeName, prev, next);
195
193
  applyElementProps(instance, prev, next);
196
194
 
197
195
  return;
198
196
  }
199
197
 
200
198
  if (instance.kind === LAZY_KIND && instance.adopted !== null) {
201
- assertConstructOnlyUnchanged(instance.typeName, prev, next);
199
+ assertPropsCanChange(instance.typeName, prev, next);
202
200
  applyAdoptedProps(lazyTarget(instance, instance.adopted), prev, next);
203
201
  instance.props = next;
204
202
  }
@@ -234,22 +232,14 @@ const setWidgetVisible = (instance: Instance, isVisible: boolean): void => {
234
232
  }
235
233
  };
236
234
 
237
- const runDiscrete: Dispatch = (fn) => {
238
- try {
239
- return priority.withDiscrete(fn);
240
- } finally {
241
- reconciler.flushSyncWork();
242
- }
243
- };
244
-
245
235
  const adoptContainer = (container: GObject.Object): ElementNode => {
246
- const name = typeName(getInstanceType(container));
236
+ const name = typeName(getClassType(container.constructor as AnyClass));
247
237
 
248
238
  if (name === null) {
249
239
  throw new Error("Cannot adopt a container whose GType has no registered name");
250
240
  }
251
241
 
252
- return createElementNode(name, container, runDiscrete, null);
242
+ return createElementNode(name, container, priority.withDiscrete, null);
253
243
  };
254
244
 
255
245
  const getOrCreateContainerNode = (container: GObject.Object): ElementNode =>
@@ -260,7 +250,7 @@ const createNode = (type: string, props: Props): Instance => {
260
250
  return createPropNode(props.propName as string);
261
251
  }
262
252
 
263
- const node = resolveElementNode(type, props, runDiscrete);
253
+ const node = resolveElementNode(type, props, priority.withDiscrete);
264
254
 
265
255
  if (node.kind === ELEMENT_KIND) {
266
256
  containerNodes.set(node.object, node);
@@ -1,6 +1,6 @@
1
1
  import type * as GObject from "@gtkx/gi/gobject";
2
2
  import { getWrapperClass, TYPE_INVALID, typeFromName, typeIsA } from "@gtkx/runtime";
3
- import { pickBy } from "@gtkx/utils";
3
+ import { getOrInsert, pickBy } from "@gtkx/utils";
4
4
  import type { Props } from "./registry.js";
5
5
  import { type TypeInfo, typeInfoFor } from "./metadata.js";
6
6
  import {
@@ -24,6 +24,7 @@ const CONTENT_TYPE_NAMES: { kind: ContentKind; name: string }[] = [
24
24
  ];
25
25
 
26
26
  const getContentTypes = createContentTypeCache();
27
+ const contentKinds: Map<bigint, ContentKind | null> = new Map();
27
28
 
28
29
  function createContentTypeCache(): () => ContentType[] {
29
30
  let cached: ContentType[] | null = null;
@@ -35,7 +36,7 @@ function createContentTypeCache(): () => ContentType[] {
35
36
  };
36
37
  }
37
38
 
38
- const resolveContentKind = (type: bigint): ContentKind | null => {
39
+ const findContentKind = (type: bigint): ContentKind | null => {
39
40
  for (const entry of getContentTypes()) {
40
41
  if (entry.type !== TYPE_INVALID && typeIsA(type, entry.type)) {
41
42
  return entry.kind;
@@ -45,6 +46,8 @@ const resolveContentKind = (type: bigint): ContentKind | null => {
45
46
  return null;
46
47
  };
47
48
 
49
+ const contentKindFor = (type: bigint): ContentKind | null => getOrInsert(contentKinds, type, findContentKind);
50
+
48
51
  const constructInput = (info: TypeInfo, props: Props): Props =>
49
52
  pickBy(
50
53
  props,
@@ -76,7 +79,7 @@ const resolveElementNode = (typeName: string, props: Props, dispatch: Dispatch):
76
79
  const type = typeFromName(typeName);
77
80
  const object = createObject(typeName, type, constructInput(info, props));
78
81
 
79
- return createElementNode(typeName, object, dispatch, resolveContentKind(type));
82
+ return createElementNode(typeName, object, dispatch, contentKindFor(type));
80
83
  };
81
84
 
82
85
  export { resolveElementNode };
@@ -9,9 +9,9 @@ type TypeInfo = {
9
9
  userEventSignals: Set<string>;
10
10
  behaviors: ElementBehavior[];
11
11
  deferred: Set<string>;
12
+ declaredConstructOnly: Set<string>;
12
13
  isLazy: boolean;
13
14
  hasFlush: boolean;
14
- hasMount: boolean;
15
15
  constructOnly: Set<string>;
16
16
  construct: Set<string>;
17
17
  defaults: Record<string, unknown>;
@@ -70,11 +70,8 @@ const applyBehaviorFlags = (info: TypeInfo, behavior: ElementBehavior): void =>
70
70
  info.hasFlush = true;
71
71
  }
72
72
 
73
- if (behavior.mount !== undefined) {
74
- info.hasMount = true;
75
- }
76
-
77
73
  addAll(info.deferred, deferredProps(behavior));
74
+ addAll(info.declaredConstructOnly, behavior.constructOnly);
78
75
  };
79
76
 
80
77
  const resolveBehaviorFlags = (info: TypeInfo): void => {
@@ -92,9 +89,9 @@ const buildTypeInfo = (name: string): TypeInfo => {
92
89
  userEventSignals: new Set(),
93
90
  behaviors: [],
94
91
  deferred: new Set(),
92
+ declaredConstructOnly: new Set(),
95
93
  isLazy: false,
96
94
  hasFlush: false,
97
- hasMount: false,
98
95
  constructOnly: new Set(),
99
96
  construct: new Set(),
100
97
  defaults: {},
@@ -18,10 +18,10 @@ type SignalTarget = {
18
18
  type PlacedChild = {
19
19
  node: PlaceableNode;
20
20
  object: GObject.Object;
21
+ typeName: string;
21
22
  adopted: GObject.Object | null;
22
23
  slot: string;
23
24
  behavior: ElementBehavior | null;
24
- isAttached: boolean;
25
25
  };
26
26
 
27
27
  type ElementNode = SignalTarget & {
@@ -111,16 +111,18 @@ const createElementNode = (
111
111
 
112
112
  const createTextNode = (text: string): TextNode => ({ kind: TEXT_KIND, text, parent: null });
113
113
 
114
- const nodeObject = (node: PlaceableNode): GObject.Object | null => {
115
- if (node.kind === LAZY_KIND) {
116
- const [child] = node.children;
117
-
118
- return child === undefined ? null : nodeObject(child);
114
+ const leafElement = (node: PlaceableNode): ElementNode | null => {
115
+ if (node.kind !== LAZY_KIND) {
116
+ return node;
119
117
  }
120
118
 
121
- return node.object;
119
+ const [child] = node.children;
120
+
121
+ return child === undefined ? null : leafElement(child);
122
122
  };
123
123
 
124
+ const nodeObject = (node: PlaceableNode): GObject.Object | null => leafElement(node)?.object ?? null;
125
+
124
126
  const lazyTarget = (node: LazyNode, adopted: GObject.Object): SignalTarget => ({
125
127
  object: adopted,
126
128
  handlers: node.handlers,
@@ -141,6 +143,7 @@ export {
141
143
  createLazyNode,
142
144
  createElementNode,
143
145
  createTextNode,
146
+ leafElement,
144
147
  nodeObject,
145
148
  lazyTarget,
146
149
  getOrCreateContext,
@@ -1,6 +1,6 @@
1
1
  import * as GObject from "@gtkx/gi/gobject";
2
2
  import * as Gtk from "@gtkx/gi/gtk";
3
- import { getOrInsert, indexBeforeOrEnd, remove } from "@gtkx/utils";
3
+ import { getOrInsert, indexBeforeOrEnd } from "@gtkx/utils";
4
4
  import type { DetachInfo, ElementBehavior, PlaceInfo } from "./registry.js";
5
5
  import { applyAdoptedProps, markFlush } from "./apply-props.js";
6
6
  import { typeInfoFor } from "./metadata.js";
@@ -11,7 +11,7 @@ import {
11
11
  getOrCreateContext,
12
12
  LAZY_KIND,
13
13
  lazyTarget,
14
- nodeObject,
14
+ leafElement,
15
15
  type PlaceableNode,
16
16
  type PlacedChild,
17
17
  } from "./node.js";
@@ -21,13 +21,20 @@ import { markTextDirty } from "./text.js";
21
21
  type AttachContext = { parent: ElementNode; entry: PlacedChild; index: number; sibling: GObject.Object | null };
22
22
 
23
23
  const createEntry = (slot: string, node: PlaceableNode): PlacedChild | null => {
24
- const object = nodeObject(node);
24
+ const leaf = leafElement(node);
25
25
 
26
- if (object === null) {
26
+ if (leaf === null) {
27
27
  return null;
28
28
  }
29
29
 
30
- return { node, object, adopted: null, slot, behavior: null, isAttached: false };
30
+ return {
31
+ node,
32
+ object: leaf.object,
33
+ typeName: leaf.typeName,
34
+ adopted: null,
35
+ slot,
36
+ behavior: null,
37
+ };
31
38
  };
32
39
 
33
40
  const siblingAt = (entries: PlacedChild[], index: number): GObject.Object | null =>
@@ -80,7 +87,6 @@ const setObjectSlot = (parent: ElementNode, entry: PlacedChild): void => {
80
87
  writeSlot(parent, entry, entry.object);
81
88
  wireBufferView(entry.node, parent);
82
89
  entry.behavior = null;
83
- entry.isAttached = true;
84
90
  };
85
91
 
86
92
  const didAttach = (ctx: AttachContext, behavior: ElementBehavior): boolean => {
@@ -99,12 +105,19 @@ const didAttach = (ctx: AttachContext, behavior: ElementBehavior): boolean => {
99
105
 
100
106
  ctx.entry.behavior = behavior;
101
107
  adoptedFrom(ctx.parent, ctx.entry, behavior, claim);
102
- ctx.entry.isAttached = true;
103
108
  applyLazyProps(ctx.entry);
104
109
 
105
110
  return true;
106
111
  };
107
112
 
113
+ const unclaimedChildError = (parentTypeName: string, childTypeName: string): Error =>
114
+ new Error(
115
+ `<${childTypeName}> cannot be a child of <${parentTypeName}>. Pass it to the ` +
116
+ `<${parentTypeName}> prop that takes it, if there is one, portal it to rootElement with createPortal ` +
117
+ `if it does not belong inside <${parentTypeName}>, or register an attach behavior for ` +
118
+ `<${parentTypeName}> with defineElements from "@gtkx/react/config" if it belongs among its children.`,
119
+ );
120
+
108
121
  const runAttach = (parent: ElementNode, entry: PlacedChild, index: number, sibling: GObject.Object | null): void => {
109
122
  const ctx: AttachContext = { parent, entry, index, sibling };
110
123
 
@@ -114,9 +127,11 @@ const runAttach = (parent: ElementNode, entry: PlacedChild, index: number, sibli
114
127
  }
115
128
  }
116
129
 
117
- if (entry.slot !== DEFAULT_SLOT) {
118
- setObjectSlot(parent, entry);
130
+ if (entry.slot === DEFAULT_SLOT) {
131
+ throw unclaimedChildError(parent.typeName, entry.typeName);
119
132
  }
133
+
134
+ setObjectSlot(parent, entry);
120
135
  };
121
136
 
122
137
  const attachEntry = (parent: ElementNode, entry: PlacedChild, index: number, sibling: GObject.Object | null): void => {
@@ -136,9 +151,7 @@ const runDetach = (parent: ElementNode, entry: PlacedChild): void => {
136
151
  const behavior = entry.behavior;
137
152
 
138
153
  if (behavior === null) {
139
- if (entry.slot !== DEFAULT_SLOT) {
140
- writeSlot(parent, entry, null);
141
- }
154
+ writeSlot(parent, entry, null);
142
155
 
143
156
  return;
144
157
  }
@@ -147,12 +160,6 @@ const runDetach = (parent: ElementNode, entry: PlacedChild): void => {
147
160
  };
148
161
 
149
162
  const detachEntry = (parent: ElementNode, entry: PlacedChild): void => {
150
- if (!entry.isAttached) {
151
- return;
152
- }
153
-
154
- entry.isAttached = false;
155
-
156
163
  applyWrite(() => {
157
164
  runDetach(parent, entry);
158
165
  });
@@ -174,13 +181,6 @@ const getPosition = (entries: PlacedChild[], before: PlaceableNode | null): numb
174
181
 
175
182
  const placeNew = (parent: ElementNode, entry: PlacedChild, entries: PlacedChild[], index: number): void => {
176
183
  attachEntry(parent, entry, index, siblingAt(entries, index));
177
-
178
- if (!entry.isAttached) {
179
- remove(entries, entry);
180
-
181
- return;
182
- }
183
-
184
184
  const isInsertedBeforeEnd = index < entries.length - 1;
185
185
  const isCannotReorderInPlace = entry.behavior !== null && entry.behavior.reorder === undefined;
186
186
 
@@ -33,9 +33,9 @@ type DetachInfo = {
33
33
 
34
34
  /**
35
35
  * Customizes how one element type places children and applies props. Hooks other than `create` receive the
36
- * GObject instance; `update`, `flush`, `mount` and `unmount` also take the private per-node context
37
- * `initialize` built, and `attach`, `reorder` and `detach` read it off their info object. Subtypes inherit a
38
- * type's behaviors, except for `create`, which is consulted only for the type it is registered on.
36
+ * GObject instance; `update` and `flush` also take the private per-node context `initialize` built, and
37
+ * `attach`, `reorder` and `detach` read it off their info object. Subtypes inherit a type's behaviors,
38
+ * except for `create`, which is consulted only for the type it is registered on.
39
39
  */
40
40
  type ElementBehavior<T extends GObject.Object = GObject.Object> = {
41
41
  /** Builds the GObject from its construct props, for types whose constructor does more than set properties. */
@@ -54,12 +54,14 @@ type ElementBehavior<T extends GObject.Object = GObject.Object> = {
54
54
  update?: (object: T, prev: Props, next: Props, context: unknown) => Iterable<string> | undefined;
55
55
  /** Runs after the commit that touched the node, once every child has been placed. */
56
56
  flush?: (object: T, context: unknown) => void;
57
- /** Runs once the node and its initial children are attached. */
58
- mount?: (object: T, context: unknown) => void;
59
- /** Runs when React deletes the node. */
60
- unmount?: (object: T, context: unknown) => void;
61
57
  /** Props to withhold from the constructor, leaving them for a later hook to apply. */
62
58
  deferred?: string[];
59
+ /**
60
+ * Props the behavior can only apply while the element is being built, because the library exposes no
61
+ * counterpart to whatever applied them. Changing one after it has been applied throws instead of
62
+ * silently applying the new value on top of the old.
63
+ */
64
+ constructOnly?: string[];
63
65
  };
64
66
 
65
67
  /** A named export in a module, referenced as plain data (the module is never imported at runtime). */
@@ -1,5 +1,8 @@
1
1
  /** The type of the shared {@link rootElement} marker used as a default top-level container. */
2
- type RootElement = { [ROOT_ELEMENT_BRAND]: true };
2
+ type RootElement = {
3
+ /** Marks a container as the top level rather than a GObject; `isRootElement` tests for it. */
4
+ [ROOT_ELEMENT_BRAND]: true;
5
+ };
3
6
 
4
7
  const ROOT_ELEMENT_BRAND: unique symbol = Symbol.for("gtkx:root-element");
5
8
  /** A shared marker value used as the default top-level container for rendering and portals. */