@gtkx/react 1.2.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +6 -4
  2. package/dist/adw/dialog.js +2 -2
  3. package/dist/adw/dialog.js.map +1 -1
  4. package/dist/components/application.d.ts.map +1 -1
  5. package/dist/components/application.js +3 -3
  6. package/dist/components/application.js.map +1 -1
  7. package/dist/components/window.js +3 -3
  8. package/dist/components/window.js.map +1 -1
  9. package/dist/element-behaviors.js +130 -1
  10. package/dist/element-behaviors.js.map +1 -1
  11. package/dist/element-config.js +1 -1
  12. package/dist/element-config.js.map +1 -1
  13. package/dist/hooks/use-bind-setting.d.ts.map +1 -1
  14. package/dist/hooks/use-bind-setting.js +3 -2
  15. package/dist/hooks/use-bind-setting.js.map +1 -1
  16. package/dist/hooks/use-property.d.ts +1 -1
  17. package/dist/hooks/use-property.d.ts.map +1 -1
  18. package/dist/hooks/use-property.js +5 -1
  19. package/dist/hooks/use-property.js.map +1 -1
  20. package/dist/internal.d.ts +2 -0
  21. package/dist/internal.d.ts.map +1 -1
  22. package/dist/internal.js +2 -0
  23. package/dist/internal.js.map +1 -1
  24. package/dist/prop-types.d.ts +20 -2
  25. package/dist/prop-types.d.ts.map +1 -1
  26. package/dist/prop-types.js.map +1 -1
  27. package/dist/reconciler/apply-props.d.ts +2 -1
  28. package/dist/reconciler/apply-props.d.ts.map +1 -1
  29. package/dist/reconciler/apply-props.js +60 -24
  30. package/dist/reconciler/apply-props.js.map +1 -1
  31. package/dist/reconciler/behaviors.d.ts.map +1 -1
  32. package/dist/reconciler/behaviors.js +40 -9
  33. package/dist/reconciler/behaviors.js.map +1 -1
  34. package/dist/reconciler/host-config.d.ts.map +1 -1
  35. package/dist/reconciler/host-config.js +10 -2
  36. package/dist/reconciler/host-config.js.map +1 -1
  37. package/dist/reconciler/metadata.d.ts +6 -1
  38. package/dist/reconciler/metadata.d.ts.map +1 -1
  39. package/dist/reconciler/metadata.js +39 -6
  40. package/dist/reconciler/metadata.js.map +1 -1
  41. package/dist/reconciler/placement.js +4 -4
  42. package/dist/reconciler/placement.js.map +1 -1
  43. package/dist/reconciler/registry.d.ts +2 -0
  44. package/dist/reconciler/registry.d.ts.map +1 -1
  45. package/dist/reconciler/registry.js.map +1 -1
  46. package/dist/reconciler/root.d.ts.map +1 -1
  47. package/dist/reconciler/root.js.map +1 -1
  48. package/dist/reconciler/signals.d.ts +3 -2
  49. package/dist/reconciler/signals.d.ts.map +1 -1
  50. package/dist/reconciler/signals.js +49 -16
  51. package/dist/reconciler/signals.js.map +1 -1
  52. package/dist/reconciler/style.d.ts +7 -0
  53. package/dist/reconciler/style.d.ts.map +1 -0
  54. package/dist/reconciler/style.js +61 -0
  55. package/dist/reconciler/style.js.map +1 -0
  56. package/dist/reconciler/text.js +3 -3
  57. package/dist/reconciler/text.js.map +1 -1
  58. package/dist/utils/accessible-props.d.ts.map +1 -1
  59. package/dist/utils/accessible-props.js +13 -7
  60. package/dist/utils/accessible-props.js.map +1 -1
  61. package/env.d.ts +10 -7
  62. package/package.json +6 -5
  63. package/src/adw/dialog.tsx +2 -2
  64. package/src/components/application.tsx +18 -3
  65. package/src/components/window.tsx +3 -3
  66. package/src/element-behaviors.ts +180 -1
  67. package/src/element-config.ts +1 -1
  68. package/src/hooks/use-bind-setting.ts +3 -2
  69. package/src/hooks/use-property.ts +6 -2
  70. package/src/internal.ts +2 -0
  71. package/src/prop-types.ts +96 -1
  72. package/src/reconciler/apply-props.ts +74 -26
  73. package/src/reconciler/behaviors.ts +60 -12
  74. package/src/reconciler/host-config.ts +13 -2
  75. package/src/reconciler/metadata.ts +62 -6
  76. package/src/reconciler/placement.ts +4 -4
  77. package/src/reconciler/registry.ts +2 -0
  78. package/src/reconciler/root.ts +1 -1
  79. package/src/reconciler/signals.ts +62 -17
  80. package/src/reconciler/style.ts +84 -0
  81. package/src/reconciler/text.ts +3 -3
  82. package/src/utils/accessible-props.ts +15 -7
package/src/prop-types.ts CHANGED
@@ -3,7 +3,7 @@ import type * as Gdk from "@gtkx/gi/gdk";
3
3
  import type * as GLib from "@gtkx/gi/glib";
4
4
  import type * as GObject from "@gtkx/gi/gobject";
5
5
  import type * as Gtk from "@gtkx/gi/gtk";
6
- import type { ReactNode } from "react";
6
+ import type { CSSProperties, ReactNode } from "react";
7
7
 
8
8
  /** One entry of a `GMenu`'s `items` prop; `submenu` and `section` nest further menus. */
9
9
  type MenuItem = {
@@ -95,12 +95,95 @@ type ChildrenProps = {
95
95
  children?: ReactNode;
96
96
  };
97
97
 
98
+ /** Paint and typography declarations GTK4 CSS accepts, spelled the way React DOM spells them. */
99
+ type StyleProperties = Pick<
100
+ CSSProperties,
101
+ | "animation" |
102
+ "animationDelay" |
103
+ "animationDirection" |
104
+ "animationDuration" |
105
+ "animationFillMode" |
106
+ "animationIterationCount" |
107
+ "animationName" |
108
+ "animationPlayState" |
109
+ "animationTimingFunction" |
110
+ "background" |
111
+ "backgroundClip" |
112
+ "backgroundColor" |
113
+ "backgroundImage" |
114
+ "backgroundOrigin" |
115
+ "backgroundPosition" |
116
+ "backgroundRepeat" |
117
+ "backgroundSize" |
118
+ "border" |
119
+ "borderBottom" |
120
+ "borderColor" |
121
+ "borderLeft" |
122
+ "borderRadius" |
123
+ "borderRight" |
124
+ "borderStyle" |
125
+ "borderTop" |
126
+ "borderWidth" |
127
+ "boxShadow" |
128
+ "caretColor" |
129
+ "color" |
130
+ "filter" |
131
+ "font" |
132
+ "fontFamily" |
133
+ "fontSize" |
134
+ "fontStyle" |
135
+ "fontVariant" |
136
+ "fontWeight" |
137
+ "letterSpacing" |
138
+ "lineHeight" |
139
+ "margin" |
140
+ "marginBottom" |
141
+ "marginLeft" |
142
+ "marginRight" |
143
+ "marginTop" |
144
+ "minHeight" |
145
+ "minWidth" |
146
+ "opacity" |
147
+ "outline" |
148
+ "outlineColor" |
149
+ "outlineOffset" |
150
+ "outlineStyle" |
151
+ "outlineWidth" |
152
+ "padding" |
153
+ "paddingBottom" |
154
+ "paddingLeft" |
155
+ "paddingRight" |
156
+ "paddingTop" |
157
+ "textDecoration" |
158
+ "textDecorationColor" |
159
+ "textDecorationLine" |
160
+ "textDecorationStyle" |
161
+ "textShadow" |
162
+ "textTransform" |
163
+ "transform" |
164
+ "transformOrigin" |
165
+ "transition" |
166
+ "transitionDelay" |
167
+ "transitionDuration" |
168
+ "transitionProperty" |
169
+ "transitionTimingFunction"
170
+ >;
171
+
172
+ /**
173
+ * Style declarations for one widget. A key starting with `&` nests a block under a selector derived from
174
+ * it, such as `"&:hover"` or `"& label"`. GTK4 CSS covers paint and typography only, so layout properties
175
+ * such as `display`, `width` and `gap` are deliberately absent; use the widget's own layout props instead.
176
+ */
177
+ type Style = StyleProperties & Partial<Record<`&${string}`, StyleProperties>>;
178
+
98
179
  /** Objects a widget takes through a method call rather than a property. */
99
180
  type GtkWidgetProps = {
100
181
  /** `Gtk.EventController` elements added to the widget. */
101
182
  controllers?: ReactNode | null | undefined;
102
183
  /** `Gio.ActionGroup` elements inserted into the widget, each under its own `prefix`. */
103
184
  actionGroups?: ReactNode | null | undefined;
185
+ /** Style declarations applied to this widget alone, outranking any class in `cssClasses`. */
186
+ style?: Style | null | undefined;
104
187
  } & ChildrenProps;
105
188
 
106
189
  /** Props of an action group placed in a widget's `actionGroups` slot. */
@@ -163,6 +246,15 @@ type GtkScaleProps = {
163
246
  marks?: ScaleMark[] | null | undefined;
164
247
  };
165
248
 
249
+ /** Props of a `Gtk.ListBox` element. */
250
+ type GtkListBoxProps = {
251
+ /**
252
+ * Index of the row to select; `-1` or `null` selects none. Applied once the row exists, and
253
+ * re-applied whenever the box's own selection drifts from it. Left alone while the prop is absent.
254
+ */
255
+ selectedIndex?: number | null | undefined;
256
+ } & ChildrenProps;
257
+
166
258
  /** Props of a `Gtk.Calendar` element. */
167
259
  type GtkCalendarProps = {
168
260
  /** Days of the shown month drawn as marked, cleared and re-marked whenever the list changes. */
@@ -230,6 +322,8 @@ export {
230
322
  type DragSourceIcon,
231
323
  type ChildrenProps,
232
324
  type GtkWidgetProps,
325
+ type Style,
326
+ type StyleProperties,
233
327
  type ActionGroupProps,
234
328
  type ActionMapProps,
235
329
  type MenuProps,
@@ -239,6 +333,7 @@ export {
239
333
  type GtkConstraintLayoutProps,
240
334
  type GtkHeaderBarProps,
241
335
  type GtkScaleProps,
336
+ type GtkListBoxProps,
242
337
  type GtkCalendarProps,
243
338
  type GtkLevelBarProps,
244
339
  type GtkSizeGroupProps,
@@ -1,10 +1,11 @@
1
- import type * as GObject from "@gtkx/gi/gobject";
2
1
  import type { SignalHandler } from "@gtkx/runtime";
2
+ import * as GObject from "@gtkx/gi/gobject";
3
3
  import * as Gtk from "@gtkx/gi/gtk";
4
- import { drain, isDeepEqual, kebabCase } from "@gtkx/utils";
4
+ import { coerceObjectProperty, getInstanceType, signalForHandlerName, TYPE_INVALID } from "@gtkx/runtime";
5
+ import { drain, isDeepEqual, kebabCase, lowerFirst, unsanitizeIdentifier } from "@gtkx/utils";
5
6
  import type { ElementBehavior, Props } from "./registry.js";
6
7
  import { applyAccessibleProps, isAccessibleProp } from "../utils/accessible-props.js";
7
- import { type TypeInfo, typeInfoFor } from "./metadata.js";
8
+ import { getPropertyName, hasProperty, type TypeInfo, typeInfoFor } from "./metadata.js";
8
9
  import { type ElementNode, getOrCreateContext, type SignalTarget } from "./node.js";
9
10
  import { applyWrite, connectHandler, disconnectHandler } from "./signals.js";
10
11
  import { bufferText, hasSameText, isContentPaintableProp, markTextDirty, TEXT_PROP } from "./text.js";
@@ -15,29 +16,53 @@ type PropChange = { prev: Props; next: Props };
15
16
 
16
17
  const REACT_RESERVED_PROPS = new Set(["children", "ref", "key"]);
17
18
  const NOTIFY_PREFIX = "onNotify";
18
- const HANDLER_PREFIX = "on";
19
19
  const HANDLER_NAME = /^on[A-Z]/;
20
20
  const flushDirty: Set<ElementNode> = new Set();
21
21
  const accessibleDirty: Map<ElementNode, Props> = new Map();
22
- const mapWatched: WeakSet<ElementNode> = new WeakSet();
22
+ const mapWatched: WeakMap<ElementNode, () => void> = new WeakMap();
23
23
  const pendingMap: Set<ElementNode> = new Set();
24
24
 
25
25
  const isHandlerName = (name: string): boolean => HANDLER_NAME.test(name);
26
+ const notifiedAccessor = (name: string): string => lowerFirst(name.slice(NOTIFY_PREFIX.length));
26
27
 
27
- const signalForProp = (info: TypeInfo, name: string): string => {
28
+ const unknownSignalError = (typeName: string, name: string): Error =>
29
+ new Error(
30
+ `The handler prop '${name}' of <${typeName}> names no signal ${typeName} carries. Name a ` +
31
+ `signal the element carries, or declare the one '${name}' stands for on ${typeName} when ` +
32
+ "its class is registered.",
33
+ );
34
+
35
+ const lookedUpSignal = (target: SignalTarget, name: string): string => {
36
+ const type = getInstanceType(target.object);
37
+ const signal = type === TYPE_INVALID ? undefined : signalForHandlerName(type, name);
38
+
39
+ if (signal === undefined) {
40
+ throw unknownSignalError(target.typeName, name);
41
+ }
42
+
43
+ return signal;
44
+ };
45
+
46
+ const signalForProp = (target: SignalTarget, info: TypeInfo, name: string): string => {
28
47
  if (name === NOTIFY_PREFIX) {
29
48
  return "notify";
30
49
  }
31
50
 
32
51
  if (name.startsWith(NOTIFY_PREFIX) && name.length > NOTIFY_PREFIX.length) {
33
- return `notify::${kebabCase(name.slice(NOTIFY_PREFIX.length))}`;
52
+ const accessor = notifiedAccessor(name);
53
+ const property = getPropertyName(target.object, accessor) ?? unsanitizeIdentifier(kebabCase(accessor));
54
+
55
+ return `notify::${property}`;
34
56
  }
35
57
 
36
- return info.signals[name] ?? kebabCase(name.slice(HANDLER_PREFIX.length));
58
+ return info.signals[name] ?? lookedUpSignal(target, name);
37
59
  };
38
60
 
39
61
  const isReservedName = (name: string, info: TypeInfo): boolean =>
40
- REACT_RESERVED_PROPS.has(name) || isHandlerName(name) || isAccessibleProp(name) || info.constructOnly.has(name);
62
+ REACT_RESERVED_PROPS.has(name) ||
63
+ (isHandlerName(name) && !hasProperty(info, name)) ||
64
+ (isAccessibleProp(name) && !hasProperty(info, name)) ||
65
+ info.constructOnly.has(name);
41
66
 
42
67
  const isSkippedValueName = (name: string, info: TypeInfo, consumed: Set<string>): boolean =>
43
68
  isReservedName(name, info) || consumed.has(name);
@@ -47,8 +72,8 @@ const writeValue = (object: GObject.Object, name: string, value: unknown): void
47
72
  return;
48
73
  }
49
74
 
50
- applyWrite(() => {
51
- Reflect.set(object, name, value);
75
+ applyWrite(name, () => {
76
+ Reflect.set(object, name, coerceObjectProperty(object, name, value));
52
77
  });
53
78
  };
54
79
 
@@ -71,7 +96,7 @@ const applyBufferText = (buffer: Gtk.TextBuffer, text: string): void => {
71
96
  return;
72
97
  }
73
98
 
74
- applyWrite(() => {
99
+ applyWrite(TEXT_PROP, () => {
75
100
  buffer.beginIrreversibleAction();
76
101
  buffer.setText(text, -1);
77
102
  buffer.endIrreversibleAction();
@@ -190,24 +215,26 @@ const restoreActionableSensitivity = (node: ElementNode, info: TypeInfo, prev: P
190
215
  const desired = "sensitive" in next ? next.sensitive : info.defaults.sensitive;
191
216
 
192
217
  if (typeof desired === "boolean") {
193
- applyWrite(() => {
218
+ applyWrite("sensitive", () => {
194
219
  Reflect.set(node.object, "sensitive", desired);
195
220
  });
196
221
  }
197
222
  };
198
223
 
199
- const applyHandlers = (target: SignalTarget, info: TypeInfo, prev: Props, next: Props): void => {
200
- eachChangedName(prev, next, (name) => {
201
- if (!isHandlerName(name)) {
202
- return;
203
- }
224
+ const applyHandler = (target: SignalTarget, info: TypeInfo, name: string, next: Props): void => {
225
+ const value = next[name];
204
226
 
205
- const value = next[name];
227
+ if (typeof value === "function") {
228
+ connectHandler(target, name, signalForProp(target, info, name), value as SignalHandler);
229
+ } else {
230
+ disconnectHandler(target, name);
231
+ }
232
+ };
206
233
 
207
- if (typeof value === "function") {
208
- connectHandler(target, name, signalForProp(info, name), value as SignalHandler);
209
- } else {
210
- disconnectHandler(target, name);
234
+ const applyHandlers = (target: SignalTarget, info: TypeInfo, prev: Props, next: Props): void => {
235
+ eachChangedName(prev, next, (name) => {
236
+ if (isHandlerName(name) && !hasProperty(info, name)) {
237
+ applyHandler(target, info, name, next);
211
238
  }
212
239
  });
213
240
  };
@@ -230,6 +257,15 @@ const flushBehaviors = (): void => {
230
257
  });
231
258
  };
232
259
 
260
+ const teardownBehaviors = (node: ElementNode): void => {
261
+ flushDirty.delete(node);
262
+ unwatchMap(node);
263
+
264
+ for (const [behavior, context] of node.contexts) {
265
+ behavior.teardown?.(node.object, context);
266
+ }
267
+ };
268
+
233
269
  const applyAccessible = (object: GObject.Object, prev: Props | null, next: Props): void => {
234
270
  if (object instanceof Gtk.Accessible) {
235
271
  applyAccessibleProps(object, prev, next);
@@ -259,14 +295,25 @@ const watchMap = (node: ElementNode): void => {
259
295
  return;
260
296
  }
261
297
 
262
- mapWatched.add(node);
263
-
264
- object.connect("map", () => {
298
+ const onMapped = (): undefined => {
265
299
  pendingMap.add(node);
266
300
  setTimeout(settleAccessible, 0);
301
+ };
302
+
303
+ object.on("map", onMapped);
304
+
305
+ mapWatched.set(node, () => {
306
+ object.off("map", onMapped);
267
307
  });
268
308
  };
269
309
 
310
+ const unwatchMap = (node: ElementNode): void => {
311
+ mapWatched.get(node)?.();
312
+ mapWatched.delete(node);
313
+ pendingMap.delete(node);
314
+ accessibleDirty.delete(node);
315
+ };
316
+
270
317
  const settleAccessible = (): void => {
271
318
  drain(pendingMap, (node) => {
272
319
  if (node.object instanceof Gtk.Widget && node.object.getMapped()) {
@@ -318,6 +365,7 @@ export {
318
365
  flushAccessible,
319
366
  settleAccessible,
320
367
  flushBehaviors,
368
+ teardownBehaviors,
321
369
  applyElementProps,
322
370
  applyAdoptedProps,
323
371
  assertPropsCanChange,
@@ -10,8 +10,9 @@ import {
10
10
  typeFromName,
11
11
  typeIsA,
12
12
  } from "@gtkx/runtime";
13
- import { getOrInsert, isDeepEqual, structuredClone } from "@gtkx/utils";
13
+ import { getOrInsert, isDeepEqual, kebabCase, structuredClone, unsanitizeIdentifier } from "@gtkx/utils";
14
14
  import type { DetachInfo, ElementBehavior, PlaceInfo, Props } from "./registry.js";
15
+ import { getPropertyName } from "./metadata.js";
15
16
  import { applyWrite } from "./signals.js";
16
17
  import { hasSameText } from "./text.js";
17
18
 
@@ -32,7 +33,7 @@ type ListHooks<P extends GObject.Object, I, H> = {
32
33
 
33
34
  type ListEntry = { item: unknown; handle: unknown };
34
35
  type ListState = { snapshot: unknown[]; entries: ListEntry[] };
35
- type DeferredState = { desired: unknown; isPresent: boolean; applied: unknown };
36
+ type DeferredState = { desired: unknown; isPresent: boolean; isScheduled: boolean; disconnect: (() => void) | null };
36
37
  type CanApply<P extends GObject.Object, V> = (object: P, value: V) => boolean;
37
38
  type ChildSetter = GObject.Object & { setChild: (child: Gtk.Widget | null) => void };
38
39
  type ContentSetter<C extends Gtk.Widget> = GObject.Object & { setContent: (content: C | null) => void };
@@ -170,15 +171,13 @@ const list = <P extends GObject.Object, I, H = void>(
170
171
  return behavior;
171
172
  };
172
173
 
173
- const flushDeferred = <P extends GObject.Object, V>(
174
+ const settleDeferred = <P extends GObject.Object, V>(
174
175
  object: GObject.Object,
175
- context: unknown,
176
+ state: DeferredState,
176
177
  prop: string,
177
178
  canApply: CanApply<P, V> | undefined,
178
179
  ): void => {
179
- const state = context as DeferredState;
180
-
181
- if (!state.isPresent || Object.is(state.applied, state.desired)) {
180
+ if (!state.isPresent || Object.is(Reflect.get(object, prop), state.desired)) {
182
181
  return;
183
182
  }
184
183
 
@@ -186,11 +185,53 @@ const flushDeferred = <P extends GObject.Object, V>(
186
185
  return;
187
186
  }
188
187
 
189
- applyWrite(() => {
188
+ applyWrite(prop, () => {
190
189
  Reflect.set(object, prop, state.desired);
191
190
  });
191
+ };
192
+
193
+ const scheduleSettle = <P extends GObject.Object, V>(
194
+ object: GObject.Object,
195
+ state: DeferredState,
196
+ prop: string,
197
+ canApply: CanApply<P, V> | undefined,
198
+ ): void => {
199
+ if (state.isScheduled) {
200
+ return;
201
+ }
202
+
203
+ state.isScheduled = true;
204
+
205
+ queueMicrotask(() => {
206
+ state.isScheduled = false;
207
+
208
+ if (state.disconnect !== null) {
209
+ settleDeferred(object, state, prop, canApply);
210
+ }
211
+ });
212
+ };
192
213
 
193
- state.applied = state.desired;
214
+ const watchDrift = <P extends GObject.Object, V>(
215
+ object: GObject.Object,
216
+ state: DeferredState,
217
+ prop: string,
218
+ canApply: CanApply<P, V> | undefined,
219
+ ): void => {
220
+ if (state.disconnect !== null) {
221
+ return;
222
+ }
223
+
224
+ const signal = `notify::${getPropertyName(object, prop) ?? unsanitizeIdentifier(kebabCase(prop))}`;
225
+
226
+ const handler = (): undefined => {
227
+ scheduleSettle(object, state, prop, canApply);
228
+ };
229
+
230
+ object.on(signal, handler);
231
+
232
+ state.disconnect = (): void => {
233
+ object.off(signal, handler);
234
+ };
194
235
  };
195
236
 
196
237
  const deferred = <P extends GObject.Object, V>(
@@ -198,7 +239,7 @@ const deferred = <P extends GObject.Object, V>(
198
239
  canApply?: CanApply<P, V>,
199
240
  ): ElementBehavior<P> => ({
200
241
  deferred: [prop],
201
- initialize: (): DeferredState => ({ desired: undefined, isPresent: false, applied: undefined }),
242
+ initialize: (): DeferredState => ({ desired: undefined, isPresent: false, isScheduled: false, disconnect: null }),
202
243
  update: (_object, _prev, next, context) => {
203
244
  const state = context as DeferredState;
204
245
  state.desired = next[prop];
@@ -207,14 +248,21 @@ const deferred = <P extends GObject.Object, V>(
207
248
  return [prop];
208
249
  },
209
250
  flush: (object, context) => {
210
- flushDeferred(object, context, prop, canApply);
251
+ const state = context as DeferredState;
252
+ settleDeferred(object, state, prop, canApply);
253
+ watchDrift(object, state, prop, canApply);
254
+ },
255
+ teardown: (_object, context) => {
256
+ const state = context as DeferredState;
257
+ state.disconnect?.();
258
+ state.disconnect = null;
211
259
  },
212
260
  });
213
261
 
214
262
  const controlledText = (prop: string): ElementBehavior =>
215
263
  value(prop, (object, next) => {
216
264
  if (!hasSameText(object, prop, next)) {
217
- applyWrite(() => {
265
+ applyWrite(prop, () => {
218
266
  Reflect.set(object, prop, next);
219
267
  });
220
268
  }
@@ -14,6 +14,7 @@ import {
14
14
  assertPropsCanChange,
15
15
  flushAccessible,
16
16
  flushBehaviors,
17
+ teardownBehaviors,
17
18
  } from "./apply-props.js";
18
19
  import { attachChild, detachChild } from "./child-routing.js";
19
20
  import { resolveElementNode } from "./instance.js";
@@ -31,6 +32,7 @@ import {
31
32
  } from "./node.js";
32
33
  import { isRootElement, type RootElement } from "./root-element.js";
33
34
  import { disconnectAllHandlers } from "./signals.js";
35
+ import { releaseStyle } from "./style.js";
34
36
  import {
35
37
  didUpdateTextSurgically,
36
38
  enclosingHost,
@@ -133,7 +135,7 @@ const hostConfig = {
133
135
  unhideTextInstance: (): void => undefined,
134
136
  detachDeletedInstance: (instance: Instance): void => {
135
137
  if (instance.kind === ELEMENT_KIND) {
136
- disconnectAllHandlers(instance);
138
+ detachElement(instance);
137
139
  } else if (instance.kind === LAZY_KIND && instance.adopted !== null) {
138
140
  disconnectAllHandlers(lazyTarget(instance, instance.adopted));
139
141
  }
@@ -160,7 +162,7 @@ const hostConfig = {
160
162
  suspendInstance: (): void => undefined,
161
163
  waitForCommitToBeReady: (): null => null,
162
164
  NotPendingTransition: null,
163
- HostTransitionContext: createContext(null) as unknown as ReactReconciler.ReactContext<null>,
165
+ HostTransitionContext: createContext(null),
164
166
  };
165
167
 
166
168
  const reconciler: ReactReconciler.Reconciler<Container, Instance, TextNode, unknown, unknown, object> =
@@ -187,6 +189,15 @@ function createPriorityTracker(): PriorityTracker {
187
189
  };
188
190
  }
189
191
 
192
+ const detachElement = (instance: ElementNode): void => {
193
+ disconnectAllHandlers(instance);
194
+ teardownBehaviors(instance);
195
+
196
+ if (instance.object instanceof Gtk.Widget) {
197
+ releaseStyle(instance.object);
198
+ }
199
+ };
200
+
190
201
  const updateInstance = (instance: Instance, prev: Props, next: Props): void => {
191
202
  if (instance.kind === ELEMENT_KIND) {
192
203
  assertPropsCanChange(instance.typeName, prev, next);
@@ -1,10 +1,21 @@
1
- import { TYPE_INVALID, typeFromName, typeInterfaces, typeName, typeParent } from "@gtkx/runtime";
1
+ import type * as GObject from "@gtkx/gi/gobject";
2
+ import {
3
+ type AnyClass,
4
+ getClassType,
5
+ getDeclaredPropertyName,
6
+ TYPE_INVALID,
7
+ typeFromName,
8
+ typeInterfaces,
9
+ typeName,
10
+ typeParent,
11
+ } from "@gtkx/runtime";
2
12
  import { getOrInsert } from "@gtkx/utils";
3
- import { constructOnlyProps, constructProps, defaultProps, signals, userEventSignals } from "virtual:gtkx-config";
13
+ import { properties, type PropertyEntry, signals, userEventSignals } from "virtual:gtkx-config";
4
14
  import { deferredProps, type ElementBehavior, ELEMENTS } from "./registry.js";
5
15
 
6
16
  type TypeInfo = {
7
17
  typeName: string;
18
+ properties: Record<string, PropertyEntry>;
8
19
  signals: Record<string, string>;
9
20
  userEventSignals: Set<string>;
10
21
  behaviors: ElementBehavior[];
@@ -17,6 +28,12 @@ type TypeInfo = {
17
28
  defaults: Record<string, unknown>;
18
29
  };
19
30
 
31
+ const WRITABLE = 2;
32
+ const CONSTRUCT = 4;
33
+ const CONSTRUCT_ONLY = 8;
34
+ const NAME = 0;
35
+ const FLAGS = 1;
36
+ const DEFAULT_VALUE = 2;
20
37
  const ancestryCache: Map<string, string[]> = new Map();
21
38
  const typeInfoCache: Map<string, TypeInfo> = new Map();
22
39
 
@@ -60,11 +77,29 @@ const addAll = <T>(target: Set<T>, source: Iterable<T> | undefined): void => {
60
77
  const accumulateAncestor = (info: TypeInfo, ancestor: string): void => {
61
78
  Object.assign(info.signals, signals[ancestor] ?? {});
62
79
  addAll(info.userEventSignals, userEventSignals[ancestor]);
63
- addAll(info.constructOnly, constructOnlyProps[ancestor]);
64
- addAll(info.construct, constructProps[ancestor]);
65
80
  info.behaviors.push(...(ELEMENTS[ancestor]?.behaviors ?? []));
66
81
  };
67
82
 
83
+ const resolveProperty = (info: TypeInfo, name: string, entry: PropertyEntry): void => {
84
+ if ((entry[FLAGS] & CONSTRUCT_ONLY) !== 0) {
85
+ info.constructOnly.add(name);
86
+ }
87
+
88
+ if ((entry[FLAGS] & (WRITABLE | CONSTRUCT | CONSTRUCT_ONLY)) !== 0) {
89
+ info.construct.add(name);
90
+ }
91
+
92
+ if (entry.length > DEFAULT_VALUE) {
93
+ info.defaults[name] = entry[DEFAULT_VALUE];
94
+ }
95
+ };
96
+
97
+ const resolveProperties = (info: TypeInfo): void => {
98
+ for (const [name, entry] of Object.entries(info.properties)) {
99
+ resolveProperty(info, name, entry);
100
+ }
101
+ };
102
+
68
103
  const applyBehaviorFlags = (info: TypeInfo, behavior: ElementBehavior): void => {
69
104
  if (behavior.flush !== undefined) {
70
105
  info.hasFlush = true;
@@ -85,6 +120,7 @@ const buildTypeInfo = (name: string): TypeInfo => {
85
120
 
86
121
  const info: TypeInfo = {
87
122
  typeName: name,
123
+ properties: {},
88
124
  signals: {},
89
125
  userEventSignals: new Set(),
90
126
  behaviors: [],
@@ -105,12 +141,32 @@ const buildTypeInfo = (name: string): TypeInfo => {
105
141
  info.isLazy = chain.some((ancestor) => ELEMENTS[ancestor]?.isLazy === true);
106
142
 
107
143
  for (const ancestor of chain.toReversed()) {
108
- Object.assign(info.defaults, defaultProps[ancestor] ?? {});
144
+ Object.assign(info.properties, properties[ancestor] ?? {});
109
145
  }
110
146
 
147
+ resolveProperties(info);
148
+
111
149
  return info;
112
150
  };
113
151
 
114
152
  const typeInfoFor = (name: string): TypeInfo => getOrInsert(typeInfoCache, name, buildTypeInfo);
115
153
 
116
- export { typeInfoFor, type TypeInfo };
154
+ const getTypeInfo = (object: GObject.Object): TypeInfo | undefined => {
155
+ const name = typeName(getClassType(object.constructor as AnyClass));
156
+
157
+ return name === null ? undefined : typeInfoFor(name);
158
+ };
159
+
160
+ const hasProperty = (info: TypeInfo, accessor: string): boolean => Object.hasOwn(info.properties, accessor);
161
+
162
+ const propertyNameFor = (info: TypeInfo, accessor: string): string | undefined =>
163
+ info.properties[accessor]?.[NAME];
164
+
165
+ const getPropertyName = (object: GObject.Object, accessor: string): string | undefined => {
166
+ const info = getTypeInfo(object);
167
+
168
+ return (info === undefined ? undefined : propertyNameFor(info, accessor)) ??
169
+ getDeclaredPropertyName(object, accessor);
170
+ };
171
+
172
+ export { getPropertyName, hasProperty, typeInfoFor, type TypeInfo };
@@ -15,7 +15,7 @@ import {
15
15
  type PlaceableNode,
16
16
  type PlacedChild,
17
17
  } from "./node.js";
18
- import { applyWrite } from "./signals.js";
18
+ import { applyMutation, applyWrite } from "./signals.js";
19
19
  import { markTextDirty } from "./text.js";
20
20
 
21
21
  type AttachContext = { parent: ElementNode; entry: PlacedChild; index: number; sibling: GObject.Object | null };
@@ -78,7 +78,7 @@ const wireBufferView = (node: PlaceableNode, parent: ElementNode): void => {
78
78
  };
79
79
 
80
80
  const writeSlot = (parent: ElementNode, entry: PlacedChild, value: GObject.Object | null): void => {
81
- applyWrite(() => {
81
+ applyWrite(entry.slot, () => {
82
82
  Reflect.set(parent.object, entry.slot, value);
83
83
  });
84
84
  };
@@ -135,7 +135,7 @@ const runAttach = (parent: ElementNode, entry: PlacedChild, index: number, sibli
135
135
  };
136
136
 
137
137
  const attachEntry = (parent: ElementNode, entry: PlacedChild, index: number, sibling: GObject.Object | null): void => {
138
- applyWrite(() => {
138
+ applyMutation(() => {
139
139
  runAttach(parent, entry, index, sibling);
140
140
  });
141
141
  };
@@ -160,7 +160,7 @@ const runDetach = (parent: ElementNode, entry: PlacedChild): void => {
160
160
  };
161
161
 
162
162
  const detachEntry = (parent: ElementNode, entry: PlacedChild): void => {
163
- applyWrite(() => {
163
+ applyMutation(() => {
164
164
  runDetach(parent, entry);
165
165
  });
166
166
  };
@@ -54,6 +54,8 @@ 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
+ /** Releases whatever `initialize` or a later hook acquired, once the node is destroyed. */
58
+ teardown?: (object: T, context: unknown) => void;
57
59
  /** Props to withhold from the constructor, leaving them for a later hook to apply. */
58
60
  deferred?: string[];
59
61
  /**
@@ -171,7 +171,7 @@ const quit = (): typeof Gdk.EVENT_PROPAGATE | typeof Gdk.EVENT_STOP => {
171
171
  * @param container The GObject to render into, or {@link rootElement} to render at the top level.
172
172
  */
173
173
  const createPortal = (children: ReactNode, container: Container, key?: string): ReactPortal =>
174
- reconciler.createPortal(children, container, null, key ?? null) as unknown as ReactPortal;
174
+ reconciler.createPortal(children, container, null, key ?? null);
175
175
 
176
176
  export {
177
177
  setReconcilerErrorHandler,