@gtkx/react 1.0.0-rc.2 → 1.0.0-rc.3
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/dist/adw/element-behaviors.js +6 -36
- package/dist/adw/element-behaviors.js.map +1 -1
- package/dist/adw/element-config.d.ts +3 -1
- package/dist/adw/element-config.d.ts.map +1 -1
- package/dist/adw/element-config.js +26 -36
- package/dist/adw/element-config.js.map +1 -1
- package/dist/components/window.d.ts.map +1 -1
- package/dist/components/window.js +13 -2
- package/dist/components/window.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/element-config.d.ts.map +1 -1
- package/dist/element-config.js +3 -0
- package/dist/element-config.js.map +1 -1
- package/dist/hooks/use-signal.d.ts +4 -0
- package/dist/hooks/use-signal.d.ts.map +1 -1
- package/dist/hooks/use-signal.js +4 -0
- package/dist/hooks/use-signal.js.map +1 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +1 -0
- package/dist/internal.js.map +1 -1
- package/dist/reconciler/apply-props.d.ts.map +1 -1
- package/dist/reconciler/apply-props.js +12 -6
- package/dist/reconciler/apply-props.js.map +1 -1
- package/dist/reconciler/behaviors.d.ts.map +1 -1
- package/dist/reconciler/behaviors.js +7 -2
- package/dist/reconciler/behaviors.js.map +1 -1
- package/dist/reconciler/host-config.d.ts.map +1 -1
- package/dist/reconciler/host-config.js +2 -6
- package/dist/reconciler/host-config.js.map +1 -1
- package/dist/reconciler/placement.d.ts.map +1 -1
- package/dist/reconciler/placement.js +24 -8
- package/dist/reconciler/placement.js.map +1 -1
- package/dist/reconciler/registry.d.ts +22 -7
- package/dist/reconciler/registry.d.ts.map +1 -1
- package/dist/reconciler/registry.js +25 -15
- package/dist/reconciler/registry.js.map +1 -1
- package/dist/reconciler/root.d.ts +2 -2
- package/dist/reconciler/root.d.ts.map +1 -1
- package/dist/reconciler/root.js +1 -4
- package/dist/reconciler/root.js.map +1 -1
- package/dist/reconciler/signals.d.ts +3 -4
- package/dist/reconciler/signals.d.ts.map +1 -1
- package/dist/reconciler/signals.js +13 -25
- package/dist/reconciler/signals.js.map +1 -1
- package/dist/reconciler/text.d.ts.map +1 -1
- package/dist/reconciler/text.js +13 -8
- package/dist/reconciler/text.js.map +1 -1
- package/package.json +4 -4
- package/src/adw/element-behaviors.ts +6 -36
- package/src/adw/element-config.ts +27 -36
- package/src/components/window.tsx +15 -2
- package/src/config.ts +8 -1
- package/src/element-config.ts +3 -0
- package/src/hooks/use-signal.ts +4 -0
- package/src/internal.ts +1 -0
- package/src/reconciler/apply-props.ts +12 -6
- package/src/reconciler/behaviors.ts +8 -2
- package/src/reconciler/host-config.ts +2 -7
- package/src/reconciler/placement.ts +29 -9
- package/src/reconciler/registry.ts +31 -17
- package/src/reconciler/root.ts +3 -6
- package/src/reconciler/signals.ts +15 -36
- package/src/reconciler/text.ts +15 -8
|
@@ -1,5 +1,18 @@
|
|
|
1
|
-
import { type ElementConfig, internal, type ModuleExport } from "../reconciler/registry.js";
|
|
1
|
+
import { type ElementConfig, forTypes, internal, type ModuleExport } from "../reconciler/registry.js";
|
|
2
2
|
|
|
3
|
+
const CHILD_SETTER_TYPES: string[] = [
|
|
4
|
+
"AdwBin",
|
|
5
|
+
"AdwClamp",
|
|
6
|
+
"AdwClampScrollable",
|
|
7
|
+
"AdwNavigationPage",
|
|
8
|
+
"AdwSplitButton",
|
|
9
|
+
"AdwStatusPage",
|
|
10
|
+
"AdwTabOverview",
|
|
11
|
+
"AdwToastOverlay",
|
|
12
|
+
"AdwToggle",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const CONTENT_SETTER_TYPES: string[] = ["AdwBottomSheet", "AdwFlap", "AdwOverlaySplitView"];
|
|
3
16
|
const childrenProps = internal("ChildrenProps");
|
|
4
17
|
const breakpointsProps = adw("AdwBreakpointsProps");
|
|
5
18
|
const preferencesRowProps = adw("AdwPreferencesRowProps");
|
|
@@ -10,42 +23,14 @@ const preferencesRowProps = adw("AdwPreferencesRowProps");
|
|
|
10
23
|
* parent. Codegen imports this module, so it must never reach the GObject bindings.
|
|
11
24
|
*/
|
|
12
25
|
const BUILTIN_ELEMENTS: Record<string, ElementConfig> = {
|
|
13
|
-
|
|
14
|
-
props: childrenProps,
|
|
15
|
-
},
|
|
16
|
-
AdwClamp: {
|
|
17
|
-
props: childrenProps,
|
|
18
|
-
},
|
|
19
|
-
AdwClampScrollable: {
|
|
20
|
-
props: childrenProps,
|
|
21
|
-
},
|
|
22
|
-
AdwNavigationPage: {
|
|
23
|
-
props: childrenProps,
|
|
24
|
-
},
|
|
25
|
-
AdwSplitButton: {
|
|
26
|
+
...forTypes(CHILD_SETTER_TYPES, {
|
|
26
27
|
props: childrenProps,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
},
|
|
31
|
-
AdwTabOverview: {
|
|
28
|
+
omitProps: ["child"],
|
|
29
|
+
}),
|
|
30
|
+
...forTypes(CONTENT_SETTER_TYPES, {
|
|
32
31
|
props: childrenProps,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
props: childrenProps,
|
|
36
|
-
},
|
|
37
|
-
AdwToggle: {
|
|
38
|
-
props: childrenProps,
|
|
39
|
-
},
|
|
40
|
-
AdwBottomSheet: {
|
|
41
|
-
props: childrenProps,
|
|
42
|
-
},
|
|
43
|
-
AdwFlap: {
|
|
44
|
-
props: childrenProps,
|
|
45
|
-
},
|
|
46
|
-
AdwOverlaySplitView: {
|
|
47
|
-
props: childrenProps,
|
|
48
|
-
},
|
|
32
|
+
omitProps: ["content"],
|
|
33
|
+
}),
|
|
49
34
|
AdwViewStackPage: {
|
|
50
35
|
lazy: true,
|
|
51
36
|
},
|
|
@@ -58,15 +43,19 @@ const BUILTIN_ELEMENTS: Record<string, ElementConfig> = {
|
|
|
58
43
|
AdwDialog: {
|
|
59
44
|
props: breakpointsProps,
|
|
60
45
|
component: adw("createDialogComponent"),
|
|
46
|
+
omitProps: ["child"],
|
|
61
47
|
},
|
|
62
48
|
AdwApplicationWindow: {
|
|
63
49
|
props: breakpointsProps,
|
|
50
|
+
omitProps: ["content"],
|
|
64
51
|
},
|
|
65
52
|
AdwWindow: {
|
|
66
53
|
props: breakpointsProps,
|
|
54
|
+
omitProps: ["content"],
|
|
67
55
|
},
|
|
68
56
|
AdwBreakpointBin: {
|
|
69
57
|
props: breakpointsProps,
|
|
58
|
+
omitProps: ["child"],
|
|
70
59
|
},
|
|
71
60
|
AdwActionRow: {
|
|
72
61
|
props: preferencesRowProps,
|
|
@@ -79,6 +68,7 @@ const BUILTIN_ELEMENTS: Record<string, ElementConfig> = {
|
|
|
79
68
|
},
|
|
80
69
|
AdwNavigationSplitView: {
|
|
81
70
|
props: childrenProps,
|
|
71
|
+
omitProps: ["content"],
|
|
82
72
|
},
|
|
83
73
|
AdwLeaflet: {
|
|
84
74
|
props: childrenProps,
|
|
@@ -115,6 +105,7 @@ const BUILTIN_ELEMENTS: Record<string, ElementConfig> = {
|
|
|
115
105
|
},
|
|
116
106
|
AdwToolbarView: {
|
|
117
107
|
props: adw("AdwToolbarViewProps"),
|
|
108
|
+
omitProps: ["content"],
|
|
118
109
|
},
|
|
119
110
|
AdwHeaderBar: {
|
|
120
111
|
props: internal("GtkHeaderBarProps"),
|
|
@@ -137,4 +128,4 @@ function adw(name: string): ModuleExport {
|
|
|
137
128
|
return { module: "@gtkx/react/adw", export: name };
|
|
138
129
|
}
|
|
139
130
|
|
|
140
|
-
export { BUILTIN_ELEMENTS };
|
|
131
|
+
export { CHILD_SETTER_TYPES, CONTENT_SETTER_TYPES, BUILTIN_ELEMENTS };
|
|
@@ -2,11 +2,24 @@ import type * as Gtk from "@gtkx/gi/gtk";
|
|
|
2
2
|
import { type ElementType, type ReactNode, type Ref, useLayoutEffect, useState } from "react";
|
|
3
3
|
import { useMergedRef } from "../hooks/use-merged-refs.js";
|
|
4
4
|
import { ParentWindowContext } from "../hooks/use-parent-window.js";
|
|
5
|
+
import { applyWrite } from "../reconciler/signals.js";
|
|
5
6
|
|
|
6
7
|
type WindowComponentProps = {
|
|
7
8
|
ref?: Ref<Gtk.Window | null> | undefined;
|
|
8
9
|
};
|
|
9
10
|
|
|
11
|
+
const presentWindow = (window: Gtk.Window): void => {
|
|
12
|
+
applyWrite(() => {
|
|
13
|
+
window.present();
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const destroyWindow = (window: Gtk.Window): void => {
|
|
18
|
+
applyWrite(() => {
|
|
19
|
+
window.destroy();
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
|
|
10
23
|
const createWindowComponent = (Component: ElementType): ((props: WindowComponentProps) => ReactNode) => {
|
|
11
24
|
return ({ ref, ...rest }: WindowComponentProps): ReactNode => {
|
|
12
25
|
const [window, setWindow] = useState<Gtk.Window | null>(null);
|
|
@@ -17,10 +30,10 @@ const createWindowComponent = (Component: ElementType): ((props: WindowComponent
|
|
|
17
30
|
return;
|
|
18
31
|
}
|
|
19
32
|
|
|
20
|
-
window
|
|
33
|
+
presentWindow(window);
|
|
21
34
|
|
|
22
35
|
return () => {
|
|
23
|
-
window
|
|
36
|
+
destroyWindow(window);
|
|
24
37
|
};
|
|
25
38
|
}, [window]);
|
|
26
39
|
|
package/src/config.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
export { BUILTIN_ELEMENTS } from "./element-config.js";
|
|
2
2
|
export type { DetachInfo, ElementBehavior, ElementConfig, ModuleExport, PlaceInfo } from "./reconciler/registry.js";
|
|
3
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
defineBehavior,
|
|
5
|
+
defineElements,
|
|
6
|
+
ELEMENTS,
|
|
7
|
+
forTypes,
|
|
8
|
+
internal,
|
|
9
|
+
mergeElementConfigs,
|
|
10
|
+
} from "./reconciler/registry.js";
|
package/src/element-config.ts
CHANGED
|
@@ -32,6 +32,7 @@ const SINGLE_CHILD_TYPES: string[] = [
|
|
|
32
32
|
const BUILTIN_ELEMENTS: Record<string, ElementConfig> = {
|
|
33
33
|
...forTypes(SINGLE_CHILD_TYPES, {
|
|
34
34
|
props: internal("ChildrenProps"),
|
|
35
|
+
omitProps: ["child"],
|
|
35
36
|
}),
|
|
36
37
|
...forTypes(["GtkHeaderBar", "GtkActionBar"], {
|
|
37
38
|
props: internal("GtkHeaderBarProps"),
|
|
@@ -39,6 +40,7 @@ const BUILTIN_ELEMENTS: Record<string, ElementConfig> = {
|
|
|
39
40
|
GtkWindow: {
|
|
40
41
|
props: internal("ChildrenProps"),
|
|
41
42
|
component: internal("createWindowComponent"),
|
|
43
|
+
omitProps: ["child"],
|
|
42
44
|
},
|
|
43
45
|
GtkLabel: {
|
|
44
46
|
props: internal("ChildrenProps"),
|
|
@@ -84,6 +86,7 @@ const BUILTIN_ELEMENTS: Record<string, ElementConfig> = {
|
|
|
84
86
|
},
|
|
85
87
|
GtkOverlay: {
|
|
86
88
|
props: internal("GtkOverlayProps"),
|
|
89
|
+
omitProps: ["child"],
|
|
87
90
|
},
|
|
88
91
|
GtkShortcutController: {
|
|
89
92
|
props: internal("GtkShortcutControllerProps"),
|
package/src/hooks/use-signal.ts
CHANGED
|
@@ -25,6 +25,10 @@ type UseSignalOptions = {
|
|
|
25
25
|
* Each emission runs the handler from the latest render, so it does not have to be stable and a
|
|
26
26
|
* changing handler never reconnects the signal.
|
|
27
27
|
*
|
|
28
|
+
* On React 19.2 this does not hold inside a component wrapped in `memo` or `forwardRef`, where every emission
|
|
29
|
+
* runs the handler captured on the first render. Keep the calling component unwrapped, or read the values the
|
|
30
|
+
* handler needs off the GObject itself. React fixes this on the 19.3 line.
|
|
31
|
+
*
|
|
28
32
|
* @param object The GObject (or ref to one) to connect to.
|
|
29
33
|
* @param signal The signal name, optionally with a detail suffix.
|
|
30
34
|
* @param handler The callback invoked when the signal is emitted.
|
package/src/internal.ts
CHANGED
|
@@ -9,5 +9,6 @@ export { useObjectValue } from "./hooks/use-object-value.js";
|
|
|
9
9
|
export type * from "./prop-types.js";
|
|
10
10
|
export { isRootElement } from "./reconciler/root-element.js";
|
|
11
11
|
export { createReconcilerRoot, type ReconcilerRoot, setReconcilerErrorHandler } from "./reconciler/root.js";
|
|
12
|
+
export { applyWrite } from "./reconciler/signals.js";
|
|
12
13
|
export { getAccessibleMetadata } from "./utils/accessible-metadata.js";
|
|
13
14
|
export { type RefProp, resolveRefProp } from "./utils/ref-prop.js";
|
|
@@ -6,7 +6,7 @@ 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";
|
|
8
8
|
import { type ElementNode, getOrCreateContext, type SignalTarget } from "./node.js";
|
|
9
|
-
import { connectHandler, disconnectHandler } from "./signals.js";
|
|
9
|
+
import { applyWrite, connectHandler, disconnectHandler } from "./signals.js";
|
|
10
10
|
import { bufferText, hasSameText, isContentPaintableProp, markTextDirty, TEXT_PROP } from "./text.js";
|
|
11
11
|
|
|
12
12
|
type PropDelta = { name: string; value: unknown; prevValue: unknown };
|
|
@@ -43,7 +43,9 @@ const writeValue = (object: GObject.Object, name: string, value: unknown): void
|
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
applyWrite(() => {
|
|
47
|
+
Reflect.set(object, name, value);
|
|
48
|
+
});
|
|
47
49
|
};
|
|
48
50
|
|
|
49
51
|
const resetPlain = (object: GObject.Object, info: TypeInfo, name: string): void => {
|
|
@@ -65,9 +67,11 @@ const applyBufferText = (buffer: Gtk.TextBuffer, text: string): void => {
|
|
|
65
67
|
return;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
applyWrite(() => {
|
|
71
|
+
buffer.beginIrreversibleAction();
|
|
72
|
+
buffer.setText(text, -1);
|
|
73
|
+
buffer.endIrreversibleAction();
|
|
74
|
+
});
|
|
71
75
|
};
|
|
72
76
|
|
|
73
77
|
const isBufferText = (node: ElementNode, name: string): node is ElementNode & { object: Gtk.TextBuffer } =>
|
|
@@ -169,7 +173,9 @@ const restoreActionableSensitivity = (node: ElementNode, info: TypeInfo, prev: P
|
|
|
169
173
|
const desired = "sensitive" in next ? next.sensitive : info.defaults.sensitive;
|
|
170
174
|
|
|
171
175
|
if (typeof desired === "boolean") {
|
|
172
|
-
|
|
176
|
+
applyWrite(() => {
|
|
177
|
+
Reflect.set(node.object, "sensitive", desired);
|
|
178
|
+
});
|
|
173
179
|
}
|
|
174
180
|
};
|
|
175
181
|
|
|
@@ -3,6 +3,7 @@ import type * as Gtk from "@gtkx/gi/gtk";
|
|
|
3
3
|
import { getInstanceType, TYPE_INVALID, typeFromName, typeIsA } from "@gtkx/runtime";
|
|
4
4
|
import { getOrInsert, isDeepEqual, structuredClone } from "@gtkx/utils";
|
|
5
5
|
import type { DetachInfo, ElementBehavior, PlaceInfo, Props } from "./registry.js";
|
|
6
|
+
import { applyWrite } from "./signals.js";
|
|
6
7
|
import { hasSameText } from "./text.js";
|
|
7
8
|
|
|
8
9
|
type SlotHooks<P extends GObject.Object, C extends GObject.Object> = {
|
|
@@ -176,7 +177,10 @@ const flushDeferred = <P extends GObject.Object, V>(
|
|
|
176
177
|
return;
|
|
177
178
|
}
|
|
178
179
|
|
|
179
|
-
|
|
180
|
+
applyWrite(() => {
|
|
181
|
+
Reflect.set(object, prop, state.desired);
|
|
182
|
+
});
|
|
183
|
+
|
|
180
184
|
state.applied = state.desired;
|
|
181
185
|
};
|
|
182
186
|
|
|
@@ -203,7 +207,9 @@ const deferred = <P extends GObject.Object, V>(
|
|
|
203
207
|
const controlledText = (prop: string): ElementBehavior =>
|
|
204
208
|
value(prop, (object, next) => {
|
|
205
209
|
if (!hasSameText(object, prop, next)) {
|
|
206
|
-
|
|
210
|
+
applyWrite(() => {
|
|
211
|
+
Reflect.set(object, prop, next);
|
|
212
|
+
});
|
|
207
213
|
}
|
|
208
214
|
});
|
|
209
215
|
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
type TextNode,
|
|
33
33
|
} from "./node.js";
|
|
34
34
|
import { isRootElement, type RootElement } from "./root-element.js";
|
|
35
|
-
import {
|
|
35
|
+
import { disconnectAllHandlers } from "./signals.js";
|
|
36
36
|
import {
|
|
37
37
|
didUpdateTextSurgically,
|
|
38
38
|
enclosingHost,
|
|
@@ -88,15 +88,10 @@ const hostConfig = {
|
|
|
88
88
|
getRootHostContext: (): Record<string, never> => HOST_CONTEXT,
|
|
89
89
|
getChildHostContext: (parent: Record<string, never>): Record<string, never> => parent,
|
|
90
90
|
getPublicInstance: (instance: Instance): object => getPublicInstance(instance),
|
|
91
|
-
prepareForCommit: (): null =>
|
|
92
|
-
beginSuppression();
|
|
93
|
-
|
|
94
|
-
return null;
|
|
95
|
-
},
|
|
91
|
+
prepareForCommit: (): null => null,
|
|
96
92
|
resetAfterCommit: (): void => {
|
|
97
93
|
flushTextHosts();
|
|
98
94
|
flushBehaviors();
|
|
99
|
-
endSuppression();
|
|
100
95
|
},
|
|
101
96
|
preparePortalMount: (): void => undefined,
|
|
102
97
|
clearContainer: (): void => undefined,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type PlaceableNode,
|
|
16
16
|
type PlacedChild,
|
|
17
17
|
} from "./node.js";
|
|
18
|
+
import { applyWrite } from "./signals.js";
|
|
18
19
|
import { markTextDirty } from "./text.js";
|
|
19
20
|
|
|
20
21
|
type AttachContext = { parent: ElementNode; entry: PlacedChild; index: number; sibling: GObject.Object | null };
|
|
@@ -69,8 +70,14 @@ const wireBufferView = (node: PlaceableNode, parent: ElementNode): void => {
|
|
|
69
70
|
markTextDirty(node);
|
|
70
71
|
};
|
|
71
72
|
|
|
73
|
+
const writeSlot = (parent: ElementNode, entry: PlacedChild, value: GObject.Object | null): void => {
|
|
74
|
+
applyWrite(() => {
|
|
75
|
+
Reflect.set(parent.object, entry.slot, value);
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
72
79
|
const setObjectSlot = (parent: ElementNode, entry: PlacedChild): void => {
|
|
73
|
-
|
|
80
|
+
writeSlot(parent, entry, entry.object);
|
|
74
81
|
wireBufferView(entry.node, parent);
|
|
75
82
|
entry.behavior = null;
|
|
76
83
|
entry.attached = true;
|
|
@@ -98,7 +105,7 @@ const didAttach = (ctx: AttachContext, behavior: ElementBehavior): boolean => {
|
|
|
98
105
|
return true;
|
|
99
106
|
};
|
|
100
107
|
|
|
101
|
-
const
|
|
108
|
+
const runAttach = (parent: ElementNode, entry: PlacedChild, index: number, sibling: GObject.Object | null): void => {
|
|
102
109
|
const ctx: AttachContext = { parent, entry, index, sibling };
|
|
103
110
|
|
|
104
111
|
for (const behavior of typeInfoFor(parent.typeName).behaviors) {
|
|
@@ -112,6 +119,12 @@ const attachEntry = (parent: ElementNode, entry: PlacedChild, index: number, sib
|
|
|
112
119
|
}
|
|
113
120
|
};
|
|
114
121
|
|
|
122
|
+
const attachEntry = (parent: ElementNode, entry: PlacedChild, index: number, sibling: GObject.Object | null): void => {
|
|
123
|
+
applyWrite(() => {
|
|
124
|
+
runAttach(parent, entry, index, sibling);
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
115
128
|
const detachInfo = (entry: PlacedChild, context: unknown): DetachInfo => ({
|
|
116
129
|
slot: entry.slot,
|
|
117
130
|
adopted: entry.adopted,
|
|
@@ -119,17 +132,12 @@ const detachInfo = (entry: PlacedChild, context: unknown): DetachInfo => ({
|
|
|
119
132
|
context,
|
|
120
133
|
});
|
|
121
134
|
|
|
122
|
-
const
|
|
123
|
-
if (!entry.attached) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
entry.attached = false;
|
|
135
|
+
const runDetach = (parent: ElementNode, entry: PlacedChild): void => {
|
|
128
136
|
const behavior = entry.behavior;
|
|
129
137
|
|
|
130
138
|
if (behavior === null) {
|
|
131
139
|
if (entry.slot !== DEFAULT_SLOT) {
|
|
132
|
-
|
|
140
|
+
writeSlot(parent, entry, null);
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
return;
|
|
@@ -138,6 +146,18 @@ const detachEntry = (parent: ElementNode, entry: PlacedChild): void => {
|
|
|
138
146
|
behavior.detach?.(parent.object, entry.object, detachInfo(entry, getOrCreateContext(parent, behavior)));
|
|
139
147
|
};
|
|
140
148
|
|
|
149
|
+
const detachEntry = (parent: ElementNode, entry: PlacedChild): void => {
|
|
150
|
+
if (!entry.attached) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
entry.attached = false;
|
|
155
|
+
|
|
156
|
+
applyWrite(() => {
|
|
157
|
+
runDetach(parent, entry);
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
|
|
141
161
|
const rebuild = (parent: ElementNode, entries: PlacedChild[]): void => {
|
|
142
162
|
for (const entry of entries) {
|
|
143
163
|
detachEntry(parent, entry);
|
|
@@ -50,14 +50,17 @@ type ModuleExport = { module: string; export: string };
|
|
|
50
50
|
/**
|
|
51
51
|
* Per-element configuration keyed by GLib type name: whether the element is lazy (its GObject is
|
|
52
52
|
* created by its parent container, as pages and layout children are), the custom behaviors bound to
|
|
53
|
-
* its type, an optional component that wraps the generated element,
|
|
54
|
-
* generated props extend
|
|
53
|
+
* its type, an optional component that wraps the generated element, the base props interface its
|
|
54
|
+
* generated props extend, and the GObject properties to leave out of the generated props (those a
|
|
55
|
+
* behavior already writes from children, such as the `child` and `content` properties). `component`,
|
|
56
|
+
* `props` and `omitProps` are inert at runtime; they are read only by codegen.
|
|
55
57
|
*/
|
|
56
58
|
type ElementConfig<T extends GObject.Object = GObject.Object> = {
|
|
57
59
|
lazy?: boolean;
|
|
58
60
|
behaviors?: ElementBehavior<T>[];
|
|
59
61
|
component?: ModuleExport;
|
|
60
62
|
props?: ModuleExport;
|
|
63
|
+
omitProps?: string[];
|
|
61
64
|
};
|
|
62
65
|
|
|
63
66
|
/**
|
|
@@ -76,22 +79,19 @@ const mergeBehaviors = (base: ElementConfig, added: ElementBehavior[], isPrepend
|
|
|
76
79
|
};
|
|
77
80
|
|
|
78
81
|
const mergeConfigEntry = (base: ElementConfig, added: ElementConfig<never>, isPrepended = false): ElementConfig => {
|
|
79
|
-
const
|
|
82
|
+
const { behaviors, lazy, omitProps, ...rest } = added;
|
|
83
|
+
const entry: ElementConfig = { ...base, ...rest };
|
|
80
84
|
|
|
81
|
-
if (
|
|
82
|
-
entry.behaviors = mergeBehaviors(
|
|
85
|
+
if (behaviors !== undefined) {
|
|
86
|
+
entry.behaviors = mergeBehaviors(base, behaviors as ElementBehavior[], isPrepended);
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
if (
|
|
86
|
-
entry.
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (added.component !== undefined) {
|
|
90
|
-
entry.component = added.component;
|
|
89
|
+
if (omitProps !== undefined) {
|
|
90
|
+
entry.omitProps = [...(base.omitProps ?? []), ...omitProps];
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
if (
|
|
94
|
-
entry.
|
|
93
|
+
if (lazy === true) {
|
|
94
|
+
entry.lazy = true;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
return entry;
|
|
@@ -99,8 +99,9 @@ const mergeConfigEntry = (base: ElementConfig, added: ElementConfig<never>, isPr
|
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
101
|
* Merges maps of {@link ElementConfig} keyed by GLib type name into one, concatenating each type's
|
|
102
|
-
* behaviors in the order the maps are given (an earlier map's behaviors come first)
|
|
103
|
-
* lazy flag, component, and props seen. Use it to combine an app's element config
|
|
102
|
+
* behaviors and omitted props in the order the maps are given (an earlier map's behaviors come first)
|
|
103
|
+
* and taking the last lazy flag, component, and props seen. Use it to combine an app's element config
|
|
104
|
+
* with its behaviors.
|
|
104
105
|
*/
|
|
105
106
|
const mergeElementConfigs = (...maps: Record<string, ElementConfig<never>>[]): Record<string, ElementConfig> => {
|
|
106
107
|
const merged: Record<string, ElementConfig> = {};
|
|
@@ -131,12 +132,24 @@ const registerElements = (
|
|
|
131
132
|
|
|
132
133
|
/**
|
|
133
134
|
* Identity helper that types the module named by the `elements` entry of `gtkx.config.ts`, enabling
|
|
134
|
-
* editor autocompletion and type checking. Key each entry by GLib type name and
|
|
135
|
-
*
|
|
135
|
+
* editor autocompletion and type checking. Key each entry by GLib type name and write each behavior
|
|
136
|
+
* with {@link defineBehavior} so its hooks receive the concrete GObject class.
|
|
136
137
|
*/
|
|
137
138
|
const defineElements = (elements: Record<string, ElementConfig<never>>): Record<string, ElementConfig<never>> =>
|
|
138
139
|
elements;
|
|
139
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Types one behavior against the GObject class it applies to, so every hook's object parameter is
|
|
143
|
+
* inferred rather than annotated by hand. Pass the class as the type argument.
|
|
144
|
+
*
|
|
145
|
+
* ```ts
|
|
146
|
+
* defineElements({
|
|
147
|
+
* GtkFrame: { behaviors: [defineBehavior<Gtk.Frame>({ attach: (frame, child) => frame.setChild(child) })] },
|
|
148
|
+
* });
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
const defineBehavior = <T extends GObject.Object>(hooks: ElementBehavior<T>): ElementBehavior<never> => hooks;
|
|
152
|
+
|
|
140
153
|
/** Spreads one config across many GLib type names. */
|
|
141
154
|
const forTypes = (types: string[], config: ElementConfig<never>): Record<string, ElementConfig<never>> =>
|
|
142
155
|
Object.fromEntries(types.map((type) => [type, config]));
|
|
@@ -150,6 +163,7 @@ export {
|
|
|
150
163
|
mergeElementConfigs,
|
|
151
164
|
registerElements,
|
|
152
165
|
defineElements,
|
|
166
|
+
defineBehavior,
|
|
153
167
|
forTypes,
|
|
154
168
|
internal,
|
|
155
169
|
type Props,
|
package/src/reconciler/root.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import type { ErrorInfo, ReactNode } from "react";
|
|
1
|
+
import type { ErrorInfo, ReactNode, ReactPortal } from "react";
|
|
2
2
|
import * as Gdk from "@gtkx/gi/gdk";
|
|
3
3
|
import { createLogger, type Logger } from "@gtkx/utils";
|
|
4
4
|
import { ConcurrentRoot } from "react-reconciler/constants.js";
|
|
5
5
|
import { type Container, reconciler } from "./host-config.js";
|
|
6
6
|
import { rootElement } from "./root-element.js";
|
|
7
|
-
import { endSuppressionNow } from "./signals.js";
|
|
8
7
|
|
|
9
8
|
type OpaqueRoot = { [opaqueRoot]: true };
|
|
10
9
|
|
|
@@ -59,12 +58,10 @@ function createErrorHandlerSlot(): ErrorHandlerSlot {
|
|
|
59
58
|
const setReconcilerErrorHandler = (handler: ErrorHandler): ErrorHandler | null => errorHandlerSlot.set(handler);
|
|
60
59
|
|
|
61
60
|
const rethrowUncaughtRenderError = (error: unknown): never => {
|
|
62
|
-
endSuppressionNow();
|
|
63
61
|
throw error;
|
|
64
62
|
};
|
|
65
63
|
|
|
66
64
|
const logCaughtRenderError = (error: unknown): void => {
|
|
67
|
-
endSuppressionNow();
|
|
68
65
|
log.error("caught render error", error);
|
|
69
66
|
};
|
|
70
67
|
|
|
@@ -161,8 +158,8 @@ const quit = (): typeof Gdk.EVENT_STOP => {
|
|
|
161
158
|
* @param key An optional stable key.
|
|
162
159
|
* @returns A React portal.
|
|
163
160
|
*/
|
|
164
|
-
const createPortal = (children: ReactNode, container: Container, key?: string):
|
|
165
|
-
|
|
161
|
+
const createPortal = (children: ReactNode, container: Container, key?: string): ReactPortal =>
|
|
162
|
+
reconciler.createPortal(children, container, null, key ?? null) as unknown as ReactPortal;
|
|
166
163
|
|
|
167
164
|
export {
|
|
168
165
|
setReconcilerErrorHandler,
|
|
@@ -3,40 +3,20 @@ import { camelCase } from "@gtkx/utils";
|
|
|
3
3
|
import type { HandlerRecord, SignalTarget } from "./node.js";
|
|
4
4
|
import { type TypeInfo, typeInfoFor } from "./metadata.js";
|
|
5
5
|
|
|
6
|
-
type SuppressionCounter = {
|
|
7
|
-
beginSuppression: () => void;
|
|
8
|
-
endSuppression: () => void;
|
|
9
|
-
endSuppressionNow: () => void;
|
|
10
|
-
isSuppressed: () => boolean;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
6
|
const NOTIFY_DETAIL_PREFIX = "notify::";
|
|
14
|
-
const
|
|
15
|
-
const beginSuppression: () => void = suppressionCounter.beginSuppression;
|
|
16
|
-
const endSuppression: () => void = suppressionCounter.endSuppression;
|
|
17
|
-
const endSuppressionNow: () => void = suppressionCounter.endSuppressionNow;
|
|
18
|
-
const isSuppressed: () => boolean = suppressionCounter.isSuppressed;
|
|
19
|
-
|
|
20
|
-
function createSuppressionCounter(): SuppressionCounter {
|
|
21
|
-
let depth = 0;
|
|
22
|
-
|
|
23
|
-
const release = (): void => {
|
|
24
|
-
if (depth > 0) {
|
|
25
|
-
depth -= 1;
|
|
26
|
-
}
|
|
27
|
-
};
|
|
7
|
+
const writeState: { depth: number } = { depth: 0 };
|
|
28
8
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
}
|
|
9
|
+
const isApplyingWrite = (): boolean => writeState.depth > 0;
|
|
10
|
+
|
|
11
|
+
const applyWrite = <T>(write: () => T): T => {
|
|
12
|
+
writeState.depth += 1;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
return write();
|
|
16
|
+
} finally {
|
|
17
|
+
writeState.depth = Math.max(0, writeState.depth - 1);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
40
20
|
|
|
41
21
|
const getNotifyProperty = (signal: string): string | null =>
|
|
42
22
|
signal.startsWith(NOTIFY_DETAIL_PREFIX) ? camelCase(signal.slice(NOTIFY_DETAIL_PREFIX.length)) : null;
|
|
@@ -56,7 +36,7 @@ const invokeHandler = (
|
|
|
56
36
|
|
|
57
37
|
const wrapHandler = (target: SignalTarget, record: HandlerRecord, notifyProperty: string | null): SignalHandler =>
|
|
58
38
|
(...args: unknown[]): unknown => {
|
|
59
|
-
if (record.blockable &&
|
|
39
|
+
if (record.blockable && isApplyingWrite()) {
|
|
60
40
|
return undefined;
|
|
61
41
|
}
|
|
62
42
|
|
|
@@ -104,9 +84,8 @@ const disconnectAllHandlers = (target: SignalTarget): void => {
|
|
|
104
84
|
};
|
|
105
85
|
|
|
106
86
|
export {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
endSuppressionNow,
|
|
87
|
+
applyWrite,
|
|
88
|
+
isApplyingWrite,
|
|
110
89
|
connectHandler,
|
|
111
90
|
disconnectHandler,
|
|
112
91
|
disconnectAllHandlers,
|
package/src/reconciler/text.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { drain, indexBeforeOrEnd } from "@gtkx/utils";
|
|
|
5
5
|
import type { ContentChild, ContentKind, ElementNode, ParentNode, TextNode } from "./node.js";
|
|
6
6
|
import type { Props } from "./registry.js";
|
|
7
7
|
import { ELEMENT_KIND, TEXT_KIND } from "./node.js";
|
|
8
|
+
import { applyWrite } from "./signals.js";
|
|
8
9
|
|
|
9
10
|
type OffsetResult = { found: boolean; offset: number };
|
|
10
11
|
|
|
@@ -319,13 +320,15 @@ const rebuildBuffer = (node: ElementNode): void => {
|
|
|
319
320
|
return;
|
|
320
321
|
}
|
|
321
322
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
323
|
+
applyWrite(() => {
|
|
324
|
+
buffer.setText("", -1);
|
|
325
|
+
const build: BufferBuild = { buffer, view: node.bufferView, marks: [] };
|
|
326
|
+
insertContent(build, node.content);
|
|
325
327
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
328
|
+
for (const mark of build.marks) {
|
|
329
|
+
placeMark(buffer, mark.node, mark.offset);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
329
332
|
};
|
|
330
333
|
|
|
331
334
|
const rebuildLabel = (node: ElementNode): void => {
|
|
@@ -381,8 +384,12 @@ const didUpdateTextSurgically = (host: ElementNode, node: TextNode, oldText: str
|
|
|
381
384
|
}
|
|
382
385
|
|
|
383
386
|
const start = located.offset;
|
|
384
|
-
|
|
385
|
-
|
|
387
|
+
|
|
388
|
+
applyWrite(() => {
|
|
389
|
+
buffer.delete(buffer.getIterAtOffset(start), buffer.getIterAtOffset(start + charLength(oldText)));
|
|
390
|
+
buffer.insert(buffer.getIterAtOffset(start), newText, -1);
|
|
391
|
+
});
|
|
392
|
+
|
|
386
393
|
const startIter = buffer.getIterAtOffset(start);
|
|
387
394
|
const endIter = buffer.getIterAtOffset(start + charLength(newText));
|
|
388
395
|
applyEnclosingTags(buffer, node, startIter, endIter);
|