@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.
- package/README.md +6 -4
- package/dist/adw/dialog.js +2 -2
- package/dist/adw/dialog.js.map +1 -1
- package/dist/components/application.d.ts.map +1 -1
- package/dist/components/application.js +3 -3
- package/dist/components/application.js.map +1 -1
- package/dist/components/window.js +3 -3
- package/dist/components/window.js.map +1 -1
- package/dist/element-behaviors.js +130 -1
- package/dist/element-behaviors.js.map +1 -1
- package/dist/element-config.js +1 -1
- package/dist/element-config.js.map +1 -1
- package/dist/hooks/use-bind-setting.d.ts.map +1 -1
- package/dist/hooks/use-bind-setting.js +3 -2
- package/dist/hooks/use-bind-setting.js.map +1 -1
- package/dist/hooks/use-property.d.ts +1 -1
- package/dist/hooks/use-property.d.ts.map +1 -1
- package/dist/hooks/use-property.js +5 -1
- package/dist/hooks/use-property.js.map +1 -1
- package/dist/internal.d.ts +2 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -0
- package/dist/internal.js.map +1 -1
- package/dist/prop-types.d.ts +20 -2
- package/dist/prop-types.d.ts.map +1 -1
- package/dist/prop-types.js.map +1 -1
- package/dist/reconciler/apply-props.d.ts +2 -1
- package/dist/reconciler/apply-props.d.ts.map +1 -1
- package/dist/reconciler/apply-props.js +60 -24
- package/dist/reconciler/apply-props.js.map +1 -1
- package/dist/reconciler/behaviors.d.ts.map +1 -1
- package/dist/reconciler/behaviors.js +40 -9
- package/dist/reconciler/behaviors.js.map +1 -1
- package/dist/reconciler/host-config.d.ts.map +1 -1
- package/dist/reconciler/host-config.js +10 -2
- package/dist/reconciler/host-config.js.map +1 -1
- package/dist/reconciler/metadata.d.ts +6 -1
- package/dist/reconciler/metadata.d.ts.map +1 -1
- package/dist/reconciler/metadata.js +39 -6
- package/dist/reconciler/metadata.js.map +1 -1
- package/dist/reconciler/placement.js +4 -4
- package/dist/reconciler/placement.js.map +1 -1
- package/dist/reconciler/registry.d.ts +2 -0
- package/dist/reconciler/registry.d.ts.map +1 -1
- package/dist/reconciler/registry.js.map +1 -1
- package/dist/reconciler/root.d.ts.map +1 -1
- package/dist/reconciler/root.js.map +1 -1
- package/dist/reconciler/signals.d.ts +3 -2
- package/dist/reconciler/signals.d.ts.map +1 -1
- package/dist/reconciler/signals.js +49 -16
- package/dist/reconciler/signals.js.map +1 -1
- package/dist/reconciler/style.d.ts +7 -0
- package/dist/reconciler/style.d.ts.map +1 -0
- package/dist/reconciler/style.js +61 -0
- package/dist/reconciler/style.js.map +1 -0
- package/dist/reconciler/text.js +3 -3
- package/dist/reconciler/text.js.map +1 -1
- package/dist/utils/accessible-props.d.ts.map +1 -1
- package/dist/utils/accessible-props.js +13 -7
- package/dist/utils/accessible-props.js.map +1 -1
- package/env.d.ts +10 -7
- package/package.json +6 -5
- package/src/adw/dialog.tsx +2 -2
- package/src/components/application.tsx +18 -3
- package/src/components/window.tsx +3 -3
- package/src/element-behaviors.ts +180 -1
- package/src/element-config.ts +1 -1
- package/src/hooks/use-bind-setting.ts +3 -2
- package/src/hooks/use-property.ts +6 -2
- package/src/internal.ts +2 -0
- package/src/prop-types.ts +96 -1
- package/src/reconciler/apply-props.ts +74 -26
- package/src/reconciler/behaviors.ts +60 -12
- package/src/reconciler/host-config.ts +13 -2
- package/src/reconciler/metadata.ts +62 -6
- package/src/reconciler/placement.ts +4 -4
- package/src/reconciler/registry.ts +2 -0
- package/src/reconciler/root.ts +1 -1
- package/src/reconciler/signals.ts +62 -17
- package/src/reconciler/style.ts +84 -0
- package/src/reconciler/text.ts +3 -3
- 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 {
|
|
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:
|
|
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
|
|
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
|
-
|
|
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] ??
|
|
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) ||
|
|
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
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
|
174
|
+
const settleDeferred = <P extends GObject.Object, V>(
|
|
174
175
|
object: GObject.Object,
|
|
175
|
-
|
|
176
|
+
state: DeferredState,
|
|
176
177
|
prop: string,
|
|
177
178
|
canApply: CanApply<P, V> | undefined,
|
|
178
179
|
): void => {
|
|
179
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/src/reconciler/root.ts
CHANGED
|
@@ -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)
|
|
174
|
+
reconciler.createPortal(children, container, null, key ?? null);
|
|
175
175
|
|
|
176
176
|
export {
|
|
177
177
|
setReconcilerErrorHandler,
|