@gtkx/react 1.0.0-rc.3 → 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 (203) hide show
  1. package/README.md +8 -9
  2. package/dist/adw/config.d.ts +1 -0
  3. package/dist/adw/config.d.ts.map +1 -1
  4. package/dist/adw/config.js +1 -0
  5. package/dist/adw/config.js.map +1 -1
  6. package/dist/adw/dialog.d.ts +4 -4
  7. package/dist/adw/dialog.d.ts.map +1 -1
  8. package/dist/adw/dialog.js +16 -21
  9. package/dist/adw/dialog.js.map +1 -1
  10. package/dist/adw/element-behaviors.js +104 -57
  11. package/dist/adw/element-behaviors.js.map +1 -1
  12. package/dist/adw/element-config.d.ts +0 -5
  13. package/dist/adw/element-config.d.ts.map +1 -1
  14. package/dist/adw/element-config.js +25 -26
  15. package/dist/adw/element-config.js.map +1 -1
  16. package/dist/adw/index.d.ts +1 -0
  17. package/dist/adw/index.d.ts.map +1 -1
  18. package/dist/adw/index.js +1 -0
  19. package/dist/adw/index.js.map +1 -1
  20. package/dist/adw/prop-types.d.ts +35 -4
  21. package/dist/adw/prop-types.d.ts.map +1 -1
  22. package/dist/adw/prop-types.js.map +1 -1
  23. package/dist/bootstrap.js +1 -1
  24. package/dist/bootstrap.js.map +1 -1
  25. package/dist/components/application-window.d.ts +5 -0
  26. package/dist/components/application-window.d.ts.map +1 -0
  27. package/dist/components/application-window.js +7 -0
  28. package/dist/components/application-window.js.map +1 -0
  29. package/dist/components/application.d.ts +3 -8
  30. package/dist/components/application.d.ts.map +1 -1
  31. package/dist/components/application.js +22 -6
  32. package/dist/components/application.js.map +1 -1
  33. package/dist/components/element.d.ts +13 -2
  34. package/dist/components/element.d.ts.map +1 -1
  35. package/dist/components/element.js +39 -15
  36. package/dist/components/element.js.map +1 -1
  37. package/dist/components/portaled.d.ts +5 -0
  38. package/dist/components/portaled.d.ts.map +1 -0
  39. package/dist/components/portaled.js +8 -0
  40. package/dist/components/portaled.js.map +1 -0
  41. package/dist/components/window.d.ts +6 -5
  42. package/dist/components/window.d.ts.map +1 -1
  43. package/dist/components/window.js +17 -16
  44. package/dist/components/window.js.map +1 -1
  45. package/dist/config.d.ts +7 -2
  46. package/dist/config.d.ts.map +1 -1
  47. package/dist/config.js +5 -1
  48. package/dist/config.js.map +1 -1
  49. package/dist/element-behaviors.js +11 -2
  50. package/dist/element-behaviors.js.map +1 -1
  51. package/dist/element-config.d.ts +4 -3
  52. package/dist/element-config.d.ts.map +1 -1
  53. package/dist/element-config.js +19 -15
  54. package/dist/element-config.js.map +1 -1
  55. package/dist/hooks/use-bind-setting.d.ts +8 -7
  56. package/dist/hooks/use-bind-setting.d.ts.map +1 -1
  57. package/dist/hooks/use-bind-setting.js +2 -7
  58. package/dist/hooks/use-bind-setting.js.map +1 -1
  59. package/dist/hooks/use-merged-refs.d.ts +1 -4
  60. package/dist/hooks/use-merged-refs.d.ts.map +1 -1
  61. package/dist/hooks/use-merged-refs.js +1 -1
  62. package/dist/hooks/use-merged-refs.js.map +1 -1
  63. package/dist/hooks/use-parent-window.d.ts +3 -1
  64. package/dist/hooks/use-parent-window.d.ts.map +1 -1
  65. package/dist/hooks/use-parent-window.js +3 -1
  66. package/dist/hooks/use-parent-window.js.map +1 -1
  67. package/dist/hooks/use-presented-instance.d.ts +12 -0
  68. package/dist/hooks/use-presented-instance.d.ts.map +1 -0
  69. package/dist/hooks/use-presented-instance.js +27 -0
  70. package/dist/hooks/use-presented-instance.js.map +1 -0
  71. package/dist/hooks/use-property.d.ts +4 -3
  72. package/dist/hooks/use-property.d.ts.map +1 -1
  73. package/dist/hooks/use-property.js +1 -2
  74. package/dist/hooks/use-property.js.map +1 -1
  75. package/dist/hooks/use-setting.d.ts +2 -3
  76. package/dist/hooks/use-setting.d.ts.map +1 -1
  77. package/dist/hooks/use-setting.js +2 -3
  78. package/dist/hooks/use-setting.js.map +1 -1
  79. package/dist/hooks/use-signal.d.ts +15 -8
  80. package/dist/hooks/use-signal.d.ts.map +1 -1
  81. package/dist/hooks/use-signal.js +6 -8
  82. package/dist/hooks/use-signal.js.map +1 -1
  83. package/dist/index.d.ts +10 -2
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +8 -0
  86. package/dist/index.js.map +1 -1
  87. package/dist/internal.d.ts +8 -5
  88. package/dist/internal.d.ts.map +1 -1
  89. package/dist/internal.js +7 -5
  90. package/dist/internal.js.map +1 -1
  91. package/dist/prop-types.d.ts +88 -4
  92. package/dist/prop-types.d.ts.map +1 -1
  93. package/dist/prop-types.js.map +1 -1
  94. package/dist/reconciler/apply-props.d.ts +4 -4
  95. package/dist/reconciler/apply-props.d.ts.map +1 -1
  96. package/dist/reconciler/apply-props.js +62 -20
  97. package/dist/reconciler/apply-props.js.map +1 -1
  98. package/dist/reconciler/behaviors.d.ts +9 -20
  99. package/dist/reconciler/behaviors.d.ts.map +1 -1
  100. package/dist/reconciler/behaviors.js +44 -41
  101. package/dist/reconciler/behaviors.js.map +1 -1
  102. package/dist/reconciler/devtools.d.ts +6 -0
  103. package/dist/reconciler/devtools.d.ts.map +1 -0
  104. package/dist/reconciler/devtools.js +11 -0
  105. package/dist/reconciler/devtools.js.map +1 -0
  106. package/dist/reconciler/host-config.d.ts +1 -1
  107. package/dist/reconciler/host-config.d.ts.map +1 -1
  108. package/dist/reconciler/host-config.js +17 -26
  109. package/dist/reconciler/host-config.js.map +1 -1
  110. package/dist/reconciler/instance.d.ts.map +1 -1
  111. package/dist/reconciler/instance.js +6 -4
  112. package/dist/reconciler/instance.js.map +1 -1
  113. package/dist/reconciler/metadata.d.ts +2 -2
  114. package/dist/reconciler/metadata.d.ts.map +1 -1
  115. package/dist/reconciler/metadata.js +9 -11
  116. package/dist/reconciler/metadata.js.map +1 -1
  117. package/dist/reconciler/node.d.ts +4 -3
  118. package/dist/reconciler/node.d.ts.map +1 -1
  119. package/dist/reconciler/node.js +8 -7
  120. package/dist/reconciler/node.js.map +1 -1
  121. package/dist/reconciler/placement.js +20 -20
  122. package/dist/reconciler/placement.js.map +1 -1
  123. package/dist/reconciler/registry.d.ts +46 -32
  124. package/dist/reconciler/registry.d.ts.map +1 -1
  125. package/dist/reconciler/registry.js +10 -19
  126. package/dist/reconciler/registry.js.map +1 -1
  127. package/dist/reconciler/root-element.d.ts +1 -1
  128. package/dist/reconciler/root-element.d.ts.map +1 -1
  129. package/dist/reconciler/root-element.js +0 -1
  130. package/dist/reconciler/root-element.js.map +1 -1
  131. package/dist/reconciler/root.d.ts +9 -20
  132. package/dist/reconciler/root.d.ts.map +1 -1
  133. package/dist/reconciler/root.js +6 -19
  134. package/dist/reconciler/root.js.map +1 -1
  135. package/dist/reconciler/signals.d.ts +1 -2
  136. package/dist/reconciler/signals.d.ts.map +1 -1
  137. package/dist/reconciler/signals.js +3 -3
  138. package/dist/reconciler/signals.js.map +1 -1
  139. package/dist/reconciler/text.js +5 -5
  140. package/dist/reconciler/text.js.map +1 -1
  141. package/dist/utils/accessible-props.d.ts +57 -9
  142. package/dist/utils/accessible-props.d.ts.map +1 -1
  143. package/dist/utils/accessible-props.js +11 -13
  144. package/dist/utils/accessible-props.js.map +1 -1
  145. package/dist/utils/ref-prop.d.ts +0 -6
  146. package/dist/utils/ref-prop.d.ts.map +1 -1
  147. package/dist/utils/ref-prop.js +0 -6
  148. package/dist/utils/ref-prop.js.map +1 -1
  149. package/dist/utils/settings.d.ts +7 -0
  150. package/dist/utils/settings.d.ts.map +1 -1
  151. package/dist/utils/settings.js.map +1 -1
  152. package/dist/utils/variant.d.ts +33 -1
  153. package/dist/utils/variant.d.ts.map +1 -1
  154. package/dist/utils/variant.js.map +1 -1
  155. package/env.d.ts +11 -4
  156. package/package.json +5 -5
  157. package/src/adw/config.ts +1 -0
  158. package/src/adw/dialog.tsx +23 -25
  159. package/src/adw/element-behaviors.ts +131 -60
  160. package/src/adw/element-config.ts +25 -26
  161. package/src/adw/index.ts +1 -0
  162. package/src/adw/prop-types.ts +36 -3
  163. package/src/bootstrap.ts +1 -1
  164. package/src/components/application-window.tsx +10 -0
  165. package/src/components/application.tsx +32 -5
  166. package/src/components/element.tsx +52 -17
  167. package/src/components/portaled.tsx +14 -0
  168. package/src/components/window.tsx +25 -23
  169. package/src/config.ts +13 -8
  170. package/src/element-behaviors.ts +20 -1
  171. package/src/element-config.ts +19 -15
  172. package/src/hooks/use-bind-setting.ts +8 -7
  173. package/src/hooks/use-merged-refs.ts +1 -1
  174. package/src/hooks/use-parent-window.ts +3 -1
  175. package/src/hooks/use-presented-instance.tsx +50 -0
  176. package/src/hooks/use-property.ts +4 -3
  177. package/src/hooks/use-setting.ts +2 -3
  178. package/src/hooks/use-signal.ts +18 -11
  179. package/src/index.ts +10 -2
  180. package/src/internal.ts +8 -5
  181. package/src/prop-types.ts +119 -11
  182. package/src/reconciler/apply-props.ts +77 -24
  183. package/src/reconciler/behaviors.ts +77 -50
  184. package/src/reconciler/devtools.ts +18 -0
  185. package/src/reconciler/host-config.ts +20 -32
  186. package/src/reconciler/instance.ts +7 -4
  187. package/src/reconciler/metadata.ts +11 -14
  188. package/src/reconciler/node.ts +12 -9
  189. package/src/reconciler/placement.ts +25 -25
  190. package/src/reconciler/registry.ts +56 -39
  191. package/src/reconciler/root-element.ts +4 -2
  192. package/src/reconciler/root.ts +12 -20
  193. package/src/reconciler/signals.ts +2 -3
  194. package/src/reconciler/text.ts +6 -6
  195. package/src/utils/accessible-props.ts +70 -25
  196. package/src/utils/ref-prop.ts +0 -6
  197. package/src/utils/settings.ts +7 -0
  198. package/src/utils/variant.ts +33 -1
  199. package/dist/utils/accessible-metadata.d.ts +0 -12
  200. package/dist/utils/accessible-metadata.d.ts.map +0 -1
  201. package/dist/utils/accessible-metadata.js +0 -25
  202. package/dist/utils/accessible-metadata.js.map +0 -1
  203. package/src/utils/accessible-metadata.ts +0 -31
@@ -1,7 +1,7 @@
1
1
  import type * as GObject from "@gtkx/gi/gobject";
2
2
  import type { SignalHandler } from "@gtkx/runtime";
3
3
  import * as Gtk from "@gtkx/gi/gtk";
4
- import { drain, kebabCase } from "@gtkx/utils";
4
+ import { drain, isDeepEqual, kebabCase } from "@gtkx/utils";
5
5
  import type { ElementBehavior, Props } from "./registry.js";
6
6
  import { applyAccessibleProps, isAccessibleProp } from "../utils/accessible-props.js";
7
7
  import { type TypeInfo, typeInfoFor } from "./metadata.js";
@@ -16,9 +16,13 @@ type PropChange = { prev: Props; next: Props };
16
16
  const REACT_RESERVED_PROPS = new Set(["children", "ref", "key"]);
17
17
  const NOTIFY_PREFIX = "onNotify";
18
18
  const HANDLER_PREFIX = "on";
19
+ const HANDLER_NAME = /^on[A-Z]/;
19
20
  const flushDirty: Set<ElementNode> = new Set();
21
+ const accessibleDirty: Map<ElementNode, Props> = new Map();
22
+ const mapWatched: WeakSet<ElementNode> = new WeakSet();
23
+ const pendingMap: Set<ElementNode> = new Set();
20
24
 
21
- const isHandlerName = (name: string): boolean => /^on[A-Z]/.test(name);
25
+ const isHandlerName = (name: string): boolean => HANDLER_NAME.test(name);
22
26
 
23
27
  const signalForProp = (info: TypeInfo, name: string): string => {
24
28
  if (name === NOTIFY_PREFIX) {
@@ -102,29 +106,42 @@ const applyEntry = (node: ElementNode, info: TypeInfo, delta: PropDelta): void =
102
106
  };
103
107
 
104
108
  const eachChangedName = (prev: Props, next: Props, visit: (name: string) => void): void => {
105
- const names = new Set([...Object.keys(prev), ...Object.keys(next)]);
106
-
107
- for (const name of names) {
109
+ for (const name in prev) {
108
110
  visit(name);
109
111
  }
112
+
113
+ for (const name in next) {
114
+ if (!Object.hasOwn(prev, name)) {
115
+ visit(name);
116
+ }
117
+ }
110
118
  };
111
119
 
112
- const constructOnlyChangeError = (typeName: string, name: string): Error =>
120
+ const propChangeError = (typeName: string, name: string): Error =>
113
121
  new Error(
114
122
  `Cannot change the construct-only prop '${name}' of <${typeName}> after it is created. ` +
115
- "GTK accepts it only while the object is being built, so give the element a key that " +
123
+ "It is only accepted while the element is being built, so give the element a key that " +
116
124
  `changes with '${name}' and React will build a new one.`,
117
125
  );
118
126
 
119
- const assertConstructOnlyUnchanged = (typeName: string, prev: Props, next: Props): void => {
127
+ const hasAppliedValue = (value: unknown): boolean => (Array.isArray(value) ? value.length > 0 : value !== undefined);
128
+
129
+ const isConstructOnlyChange = (info: TypeInfo, name: string, change: PropChange): boolean =>
130
+ info.constructOnly.has(name) && !Object.is(change.prev[name], change.next[name]);
131
+
132
+ const isDeclaredConstructOnlyChange = (info: TypeInfo, name: string, change: PropChange): boolean =>
133
+ info.declaredConstructOnly.has(name) &&
134
+ hasAppliedValue(change.prev[name]) &&
135
+ !isDeepEqual(change.prev[name], change.next[name]);
136
+
137
+ const assertPropsCanChange = (typeName: string, prev: Props, next: Props): void => {
120
138
  const info = typeInfoFor(typeName);
139
+ const change: PropChange = { prev, next };
121
140
 
122
141
  eachChangedName(prev, next, (name) => {
123
- if (!info.constructOnly.has(name) || Object.is(prev[name], next[name])) {
124
- return;
142
+ if (isConstructOnlyChange(info, name, change) || isDeclaredConstructOnlyChange(info, name, change)) {
143
+ throw propChangeError(typeName, name);
125
144
  }
126
-
127
- throw constructOnlyChangeError(typeName, name);
128
145
  });
129
146
  };
130
147
 
@@ -213,22 +230,58 @@ const flushBehaviors = (): void => {
213
230
  });
214
231
  };
215
232
 
216
- const mountBehaviors = (node: ElementNode): void => {
217
- if (!typeInfoFor(node.typeName).hasMount) {
233
+ const applyAccessible = (object: GObject.Object, prev: Props | null, next: Props): void => {
234
+ if (object instanceof Gtk.Accessible) {
235
+ applyAccessibleProps(object, prev, next);
236
+ }
237
+ };
238
+
239
+ const markAccessible = (node: ElementNode, prev: Props): void => {
240
+ if (!accessibleDirty.has(node)) {
241
+ accessibleDirty.set(node, prev);
242
+ }
243
+ };
244
+
245
+ const hasAccessibleProp = (props: Props): boolean => {
246
+ for (const name in props) {
247
+ if (isAccessibleProp(name)) {
248
+ return true;
249
+ }
250
+ }
251
+
252
+ return false;
253
+ };
254
+
255
+ const watchMap = (node: ElementNode): void => {
256
+ const { object } = node;
257
+
258
+ if (mapWatched.has(node) || !(object instanceof Gtk.Widget) || !hasAccessibleProp(node.props)) {
218
259
  return;
219
260
  }
220
261
 
221
- eachBehavior(node, (behavior, context) => behavior.mount?.(node.object, context));
262
+ mapWatched.add(node);
263
+
264
+ object.connect("map", () => {
265
+ pendingMap.add(node);
266
+ setTimeout(settleAccessible, 0);
267
+ });
222
268
  };
223
269
 
224
- const unmountBehaviors = (node: ElementNode): void => {
225
- eachBehavior(node, (behavior, context) => behavior.unmount?.(node.object, context));
270
+ const settleAccessible = (): void => {
271
+ drain(pendingMap, (node) => {
272
+ if (node.object instanceof Gtk.Widget && node.object.getMapped()) {
273
+ applyAccessible(node.object, null, node.props);
274
+ }
275
+ });
226
276
  };
227
277
 
228
- const applyAccessible = (object: GObject.Object, prev: Props, next: Props): void => {
229
- if (object instanceof Gtk.Widget) {
230
- applyAccessibleProps(object, prev, next);
278
+ const flushAccessible = (): void => {
279
+ for (const [node, prev] of accessibleDirty) {
280
+ applyAccessible(node.object, prev, node.props);
281
+ watchMap(node);
231
282
  }
283
+
284
+ accessibleDirty.clear();
232
285
  };
233
286
 
234
287
  const applyElementProps = (node: ElementNode, prev: Props, next: Props): void => {
@@ -236,8 +289,8 @@ const applyElementProps = (node: ElementNode, prev: Props, next: Props): void =>
236
289
  const consumed = runBehaviorUpdates(node, info, prev, next);
237
290
  applyValueEntries(node, info, { prev, next }, consumed);
238
291
  restoreActionableSensitivity(node, info, prev, next);
239
- applyAccessible(node.object, prev, next);
240
292
  applyHandlers(node, info, prev, next);
293
+ markAccessible(node, prev);
241
294
  markFlush(node);
242
295
  node.props = next;
243
296
  };
@@ -262,10 +315,10 @@ const applyAdoptedProps = (target: SignalTarget, prev: Props, next: Props): void
262
315
 
263
316
  export {
264
317
  markFlush,
318
+ flushAccessible,
319
+ settleAccessible,
265
320
  flushBehaviors,
266
- mountBehaviors,
267
- unmountBehaviors,
268
321
  applyElementProps,
269
322
  applyAdoptedProps,
270
- assertConstructOnlyUnchanged,
323
+ assertPropsCanChange,
271
324
  };
@@ -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";
@@ -23,7 +32,7 @@ type ListHooks<P extends GObject.Object, I, H> = {
23
32
 
24
33
  type ListEntry = { item: unknown; handle: unknown };
25
34
  type ListState = { snapshot: unknown[]; entries: ListEntry[] };
26
- type DeferredState = { desired: unknown; present: boolean; applied: unknown };
35
+ type DeferredState = { desired: unknown; isPresent: boolean; applied: unknown };
27
36
  type CanApply<P extends GObject.Object, V> = (object: P, value: V) => boolean;
28
37
  type ChildSetter = GObject.Object & { setChild: (child: Gtk.Widget | null) => void };
29
38
  type ContentSetter<C extends Gtk.Widget> = GObject.Object & { setContent: (content: C | null) => void };
@@ -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 =
@@ -67,7 +76,6 @@ const slotAttach =
67
76
  return attach(object as P, child as C, info) ?? true;
68
77
  };
69
78
 
70
- /** Builds a behavior for a named child slot holding children of `childType`, claiming matches only. */
71
79
  const slot = <P extends GObject.Object, C extends GObject.Object>(
72
80
  slotName: string,
73
81
  childType: string,
@@ -94,7 +102,6 @@ const slot = <P extends GObject.Object, C extends GObject.Object>(
94
102
  return behavior;
95
103
  };
96
104
 
97
- /** Builds a scalar-prop behavior that invokes `apply` whenever the value changes, and claims the prop. */
98
105
  const value = <P extends GObject.Object, V>(
99
106
  prop: string,
100
107
  apply: ValueApply<P, V>,
@@ -119,48 +126,50 @@ const teardownList = <P extends GObject.Object, I, H>(
119
126
  return;
120
127
  }
121
128
 
122
- const remove = hooks.remove;
123
-
124
- if (remove === undefined) {
125
- return;
126
- }
127
-
128
129
  for (const entry of entries) {
129
- remove(object, entry.item as I, entry.handle as H);
130
+ hooks.remove?.(object, entry.item as I, entry.handle as H);
130
131
  }
131
132
  };
132
133
 
133
- /**
134
- * Builds an array-prop behavior that adds, removes, and clears its items, reapplying on structural
135
- * change. `add` may return a handle that the same item's later `remove` receives, for items whose
136
- * teardown needs what `add` produced (as VFL constraints need the objects the layout created).
137
- */
138
- const list = <P extends GObject.Object, I, H = void>(
134
+ const listUpdate = <P extends GObject.Object, I, H>(
139
135
  prop: string,
140
136
  hooks: ListHooks<P, I, H>,
141
- ): ElementBehavior<P> => {
137
+ ): NonNullable<ElementBehavior<P>["update"]> => {
142
138
  const { add } = hooks;
143
139
 
144
- return {
145
- createContext: (): ListState => ({ snapshot: [], entries: [] }),
146
- update: (object, _prev, next, context) => {
147
- const state = context as ListState;
148
- const raw = next[prop];
149
- 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 : [];
150
144
 
151
- if (isDeepEqual(state.snapshot, items)) {
152
- return [prop];
153
- }
145
+ if (isDeepEqual(state.snapshot, items)) {
146
+ return [prop];
147
+ }
154
148
 
155
- teardownList(object, state.entries, hooks);
156
- state.entries = items.map((item) => ({ item, handle: add?.(object, item as I) }));
157
- 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);
158
152
 
159
- return [prop];
160
- },
153
+ return [prop];
161
154
  };
162
155
  };
163
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
+
164
173
  const flushDeferred = <P extends GObject.Object, V>(
165
174
  object: GObject.Object,
166
175
  context: unknown,
@@ -169,7 +178,7 @@ const flushDeferred = <P extends GObject.Object, V>(
169
178
  ): void => {
170
179
  const state = context as DeferredState;
171
180
 
172
- if (!state.present || Object.is(state.applied, state.desired)) {
181
+ if (!state.isPresent || Object.is(state.applied, state.desired)) {
173
182
  return;
174
183
  }
175
184
 
@@ -184,17 +193,16 @@ const flushDeferred = <P extends GObject.Object, V>(
184
193
  state.applied = state.desired;
185
194
  };
186
195
 
187
- /** Builds a behavior for a prop applied after the surrounding commit, deferred until `canApply` returns true. */
188
196
  const deferred = <P extends GObject.Object, V>(
189
197
  prop: string,
190
198
  canApply?: CanApply<P, V>,
191
199
  ): ElementBehavior<P> => ({
192
200
  deferred: [prop],
193
- createContext: (): DeferredState => ({ desired: undefined, present: false, applied: undefined }),
201
+ initialize: (): DeferredState => ({ desired: undefined, isPresent: false, applied: undefined }),
194
202
  update: (_object, _prev, next, context) => {
195
203
  const state = context as DeferredState;
196
204
  state.desired = next[prop];
197
- state.present = next[prop] !== undefined;
205
+ state.isPresent = next[prop] !== undefined;
198
206
 
199
207
  return [prop];
200
208
  },
@@ -203,7 +211,6 @@ const deferred = <P extends GObject.Object, V>(
203
211
  },
204
212
  });
205
213
 
206
- /** Builds a behavior for a text prop kept in controlled-input sync: set when provided, never reset. */
207
214
  const controlledText = (prop: string): ElementBehavior =>
208
215
  value(prop, (object, next) => {
209
216
  if (!hasSameText(object, prop, next)) {
@@ -213,7 +220,6 @@ const controlledText = (prop: string): ElementBehavior =>
213
220
  }
214
221
  });
215
222
 
216
- /** Behavior for a container that installs its single child with `setChild`. */
217
223
  const childSetterSlot = <P extends ChildSetter>(): ElementBehavior<P> =>
218
224
  slot<P, Gtk.Widget>("children", "GtkWidget", {
219
225
  attach: (parent, child) => {
@@ -224,7 +230,6 @@ const childSetterSlot = <P extends ChildSetter>(): ElementBehavior<P> =>
224
230
  },
225
231
  });
226
232
 
227
- /** Behavior for a container that installs its single child with `setContent`. */
228
233
  const contentSetterSlot = <P extends ContentSetter<C>, C extends Gtk.Widget = Gtk.Widget>(
229
234
  childType = "GtkWidget",
230
235
  ): ElementBehavior<P> =>
@@ -237,7 +242,6 @@ const contentSetterSlot = <P extends ContentSetter<C>, C extends Gtk.Widget = Gt
237
242
  },
238
243
  });
239
244
 
240
- /** Behavior for a `GtkBox`-style container that orders children by sibling. */
241
245
  const boxSlot = <P extends BoxLike>(): ElementBehavior<P> =>
242
246
  slot<P, Gtk.Widget>("children", "GtkWidget", {
243
247
  attach: (box, child, info) => box.insertChildAfter(child, info.sibling as Gtk.Widget | null),
@@ -249,7 +253,6 @@ const boxSlot = <P extends BoxLike>(): ElementBehavior<P> =>
249
253
  },
250
254
  });
251
255
 
252
- /** Behavior for a container whose children are added and removed by a pair of methods. */
253
256
  const addRemoveSlot = <C extends GObject.Object, P extends GObject.Object>(
254
257
  slotName: string,
255
258
  childType: string,
@@ -257,7 +260,23 @@ const addRemoveSlot = <C extends GObject.Object, P extends GObject.Object>(
257
260
  remove: (parent: P, child: C) => void,
258
261
  ): ElementBehavior => slot<P, C>(slotName, childType, { attach: add, detach: remove });
259
262
 
260
- /** Behavior for a `children` slot whose attach call returns the page object the container adopts. */
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
+
261
280
  const adoptedChildrenSlot = <P extends GObject.Object, C extends GObject.Object>(
262
281
  childType: string,
263
282
  add: (parent: P, item: C) => unknown,
@@ -289,7 +308,7 @@ const wrappedRow = <W extends Gtk.Widget>(
289
308
 
290
309
  const removeWrappedRow = (
291
310
  Wrapper: new (props: Props) => Gtk.Widget,
292
- parent: IndexedInserter,
311
+ parent: IndexedChildHost<Gtk.Widget>,
293
312
  child: Gtk.Widget,
294
313
  rows: RowCache,
295
314
  ): void => {
@@ -300,8 +319,7 @@ const removeWrappedRow = (
300
319
  }
301
320
  };
302
321
 
303
- /** Behavior for an index-placed container that wraps each child in `Wrapper` before adding it. */
304
- const wrappingIndexedSlot = <W extends Gtk.Widget, P extends IndexedInserter>(
322
+ const wrappingIndexedSlot = <W extends Gtk.Widget, P extends IndexedChildHost<Gtk.Widget>>(
305
323
  Wrapper: new (props: Props) => W,
306
324
  setChild: (wrapper: W, inner: Gtk.Widget) => void,
307
325
  ): ElementBehavior<P> => ({
@@ -312,10 +330,18 @@ const wrappingIndexedSlot = <W extends Gtk.Widget, P extends IndexedInserter>(
312
330
  removeWrappedRow(Wrapper, parent, child, info.context as RowCache);
313
331
  },
314
332
  }),
315
- createContext: (): RowCache => new WeakMap(),
333
+ initialize: (): RowCache => new WeakMap(),
334
+ });
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),
316
340
  });
317
341
 
318
342
  export {
343
+ applicationCreator,
344
+ childMatcher,
319
345
  slot,
320
346
  value,
321
347
  list,
@@ -326,5 +352,6 @@ export {
326
352
  boxSlot,
327
353
  addRemoveSlot,
328
354
  adoptedChildrenSlot,
355
+ indexedSlot,
329
356
  wrappingIndexedSlot,
330
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,7 +1,7 @@
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";
4
- import { getOrInsert } from "@gtkx/utils";
3
+ import { type AnyClass, getClassType, typeName } from "@gtkx/runtime";
4
+ import { getOrInsert, packageVersion } 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";
@@ -10,20 +10,17 @@ import { Prop } from "../components/element.js";
10
10
  import {
11
11
  applyAdoptedProps,
12
12
  applyElementProps,
13
- assertConstructOnlyUnchanged,
13
+ assertPropsCanChange,
14
+ flushAccessible,
14
15
  flushBehaviors,
15
- mountBehaviors,
16
- unmountBehaviors,
17
16
  } from "./apply-props.js";
18
17
  import { attachChild, detachChild } from "./child-routing.js";
19
18
  import { resolveElementNode } from "./instance.js";
20
- import { typeInfoFor } from "./metadata.js";
21
19
  import {
22
20
  type AnyNode,
23
21
  createElementNode,
24
22
  createPropNode,
25
23
  createTextNode,
26
- type Dispatch,
27
24
  ELEMENT_KIND,
28
25
  type ElementNode,
29
26
  type Instance,
@@ -41,7 +38,7 @@ import {
41
38
  validateContentMix,
42
39
  } from "./text.js";
43
40
 
44
- /** A top-level container an element tree can be mounted into. */
41
+ /** What a tree is rendered into: any GObject, or the {@link RootElement} marker to render at the top level. */
45
42
  type Container = RootElement | GObject.Object;
46
43
 
47
44
  type PriorityTracker = {
@@ -55,9 +52,15 @@ const containerNodes: WeakMap<object, ElementNode> = new WeakMap();
55
52
  const priority = createPriorityTracker();
56
53
 
57
54
  const hostConfig = {
55
+ rendererPackageName: "@gtkx/react",
56
+ rendererVersion: packageVersion(import.meta.url, "@gtkx/react/package.json"),
58
57
  supportsMutation: true,
59
58
  supportsPersistence: false,
60
59
  supportsHydration: false,
60
+ supportsMicrotasks: true,
61
+ scheduleMicrotask: (fn: () => void): void => {
62
+ queueMicrotask(fn);
63
+ },
61
64
  isPrimaryRenderer: true,
62
65
  noTimeout: -1,
63
66
  scheduleTimeout: (fn: (...args: unknown[]) => unknown, delay?: number): ReturnType<typeof setTimeout> =>
@@ -71,18 +74,11 @@ const hostConfig = {
71
74
  attachChild(parent, child, null);
72
75
  },
73
76
  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
77
  if (instance.kind === ELEMENT_KIND) {
84
- mountBehaviors(instance);
78
+ validateContentMix(instance, props);
85
79
  }
80
+
81
+ return false;
86
82
  },
87
83
  shouldSetTextContent: (): boolean => false,
88
84
  getRootHostContext: (): Record<string, never> => HOST_CONTEXT,
@@ -92,6 +88,7 @@ const hostConfig = {
92
88
  resetAfterCommit: (): void => {
93
89
  flushTextHosts();
94
90
  flushBehaviors();
91
+ flushAccessible();
95
92
  },
96
93
  preparePortalMount: (): void => undefined,
97
94
  clearContainer: (): void => undefined,
@@ -135,7 +132,6 @@ const hostConfig = {
135
132
  detachDeletedInstance: (instance: Instance): void => {
136
133
  if (instance.kind === ELEMENT_KIND) {
137
134
  disconnectAllHandlers(instance);
138
- unmountBehaviors(instance);
139
135
  } else if (instance.kind === LAZY_KIND && instance.adopted !== null) {
140
136
  disconnectAllHandlers(lazyTarget(instance, instance.adopted));
141
137
  }
@@ -191,14 +187,14 @@ function createPriorityTracker(): PriorityTracker {
191
187
 
192
188
  const updateInstance = (instance: Instance, prev: Props, next: Props): void => {
193
189
  if (instance.kind === ELEMENT_KIND) {
194
- assertConstructOnlyUnchanged(instance.typeName, prev, next);
190
+ assertPropsCanChange(instance.typeName, prev, next);
195
191
  applyElementProps(instance, prev, next);
196
192
 
197
193
  return;
198
194
  }
199
195
 
200
196
  if (instance.kind === LAZY_KIND && instance.adopted !== null) {
201
- assertConstructOnlyUnchanged(instance.typeName, prev, next);
197
+ assertPropsCanChange(instance.typeName, prev, next);
202
198
  applyAdoptedProps(lazyTarget(instance, instance.adopted), prev, next);
203
199
  instance.props = next;
204
200
  }
@@ -234,22 +230,14 @@ const setWidgetVisible = (instance: Instance, isVisible: boolean): void => {
234
230
  }
235
231
  };
236
232
 
237
- const runDiscrete: Dispatch = (fn) => {
238
- try {
239
- return priority.withDiscrete(fn);
240
- } finally {
241
- reconciler.flushSyncWork();
242
- }
243
- };
244
-
245
233
  const adoptContainer = (container: GObject.Object): ElementNode => {
246
- const name = typeName(getInstanceType(container));
234
+ const name = typeName(getClassType(container.constructor as AnyClass));
247
235
 
248
236
  if (name === null) {
249
237
  throw new Error("Cannot adopt a container whose GType has no registered name");
250
238
  }
251
239
 
252
- return createElementNode(name, container, runDiscrete, null);
240
+ return createElementNode(name, container, priority.withDiscrete, null);
253
241
  };
254
242
 
255
243
  const getOrCreateContainerNode = (container: GObject.Object): ElementNode =>
@@ -260,7 +248,7 @@ const createNode = (type: string, props: Props): Instance => {
260
248
  return createPropNode(props.propName as string);
261
249
  }
262
250
 
263
- const node = resolveElementNode(type, props, runDiscrete);
251
+ const node = resolveElementNode(type, props, priority.withDiscrete);
264
252
 
265
253
  if (node.kind === ELEMENT_KIND) {
266
254
  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,
@@ -69,14 +72,14 @@ const createObject = (typeName: string, type: bigint, input: Props): GObject.Obj
69
72
  const resolveElementNode = (typeName: string, props: Props, dispatch: Dispatch): ElementNode | LazyNode => {
70
73
  const info = typeInfoFor(typeName);
71
74
 
72
- if (info.lazy) {
75
+ if (info.isLazy) {
73
76
  return createLazyNode(typeName, props, dispatch);
74
77
  }
75
78
 
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 };