@gtkx/react 1.3.0 → 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 +5 -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 +93 -3
- 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/prop-types.d.ts +9 -1
- 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 +59 -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 +2 -1
- 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 +2 -1
- package/dist/reconciler/style.d.ts.map +1 -1
- package/dist/reconciler/style.js +4 -3
- package/dist/reconciler/style.js.map +1 -1
- package/dist/reconciler/text.js +3 -3
- package/dist/reconciler/text.js.map +1 -1
- package/env.d.ts +10 -7
- package/package.json +6 -6
- 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 +122 -3
- 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/prop-types.ts +10 -0
- package/src/reconciler/apply-props.ts +73 -26
- package/src/reconciler/behaviors.ts +60 -12
- package/src/reconciler/host-config.ts +3 -1
- 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 +4 -3
- package/src/reconciler/text.ts +3 -3
package/src/element-behaviors.ts
CHANGED
|
@@ -34,7 +34,13 @@ import {
|
|
|
34
34
|
registerElements,
|
|
35
35
|
} from "./reconciler/registry.js";
|
|
36
36
|
import { applyWrite } from "./reconciler/signals.js";
|
|
37
|
-
import { applyStyle, styleClass } from "./reconciler/style.js";
|
|
37
|
+
import { applyStyle, CSS_CLASSES_PROP, styleClass } from "./reconciler/style.js";
|
|
38
|
+
|
|
39
|
+
type SelectedIndexState = { desired: number | undefined; isScheduled: boolean; disconnect: (() => void) | null };
|
|
40
|
+
|
|
41
|
+
const SELECTED_INDEX_PROP = "selectedIndex";
|
|
42
|
+
const SELECTION_SIGNAL = "selected-rows-changed";
|
|
43
|
+
const NO_SELECTION = -1;
|
|
38
44
|
|
|
39
45
|
const BUILTIN_BEHAVIORS: Record<string, ElementConfig<never>> = {
|
|
40
46
|
...forTypes(SINGLE_CHILD_TYPES, {
|
|
@@ -114,6 +120,7 @@ const BUILTIN_BEHAVIORS: Record<string, ElementConfig<never>> = {
|
|
|
114
120
|
wrappingIndexedSlot(Gtk.ListBoxRow, (row, inner) => {
|
|
115
121
|
row.setChild(inner);
|
|
116
122
|
}),
|
|
123
|
+
selectedIndexBehavior(),
|
|
117
124
|
],
|
|
118
125
|
},
|
|
119
126
|
GtkFlowBox: {
|
|
@@ -430,8 +437,8 @@ const didWriteCssClasses = (widget: Gtk.Widget, prev: Props, next: Props, classN
|
|
|
430
437
|
return false;
|
|
431
438
|
}
|
|
432
439
|
|
|
433
|
-
applyWrite(() => {
|
|
434
|
-
Reflect.set(widget,
|
|
440
|
+
applyWrite(CSS_CLASSES_PROP, () => {
|
|
441
|
+
Reflect.set(widget, CSS_CLASSES_PROP, withStyleClass(next.cssClasses, className));
|
|
435
442
|
});
|
|
436
443
|
|
|
437
444
|
return true;
|
|
@@ -460,6 +467,118 @@ function styleBehavior(): ElementBehavior<Gtk.Widget> {
|
|
|
460
467
|
return { update: updateStyle };
|
|
461
468
|
}
|
|
462
469
|
|
|
470
|
+
function selectedRowIndex(box: Gtk.ListBox): number {
|
|
471
|
+
return box.getSelectedRow()?.getIndex() ?? NO_SELECTION;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function selectedIndexError(value: number): Error {
|
|
475
|
+
return new Error(
|
|
476
|
+
"The 'selectedIndex' of a <GtkListBox> must be a whole number, or -1 to select no row; " +
|
|
477
|
+
`received ${String(value)}.`,
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function desiredIndex(value: unknown): number | undefined {
|
|
482
|
+
if (value === null) {
|
|
483
|
+
return NO_SELECTION;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (typeof value !== "number") {
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (!Number.isSafeInteger(value)) {
|
|
491
|
+
throw selectedIndexError(value);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return value;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function selectRowAt(box: Gtk.ListBox, row: Gtk.ListBoxRow): void {
|
|
498
|
+
applyWrite(SELECTED_INDEX_PROP, () => {
|
|
499
|
+
box.selectRow(row);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function clearSelection(box: Gtk.ListBox): void {
|
|
504
|
+
applyWrite(SELECTED_INDEX_PROP, () => {
|
|
505
|
+
box.unselectAll();
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function applySelectedIndex(box: Gtk.ListBox, state: SelectedIndexState): void {
|
|
510
|
+
const { desired } = state;
|
|
511
|
+
|
|
512
|
+
if (desired === undefined || selectedRowIndex(box) === desired) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (desired < 0) {
|
|
517
|
+
clearSelection(box);
|
|
518
|
+
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const row = box.getRowAtIndex(desired);
|
|
523
|
+
|
|
524
|
+
if (row !== null) {
|
|
525
|
+
selectRowAt(box, row);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function scheduleSelectedIndex(box: Gtk.ListBox, state: SelectedIndexState): void {
|
|
530
|
+
if (state.isScheduled) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
state.isScheduled = true;
|
|
535
|
+
|
|
536
|
+
queueMicrotask(() => {
|
|
537
|
+
state.isScheduled = false;
|
|
538
|
+
|
|
539
|
+
if (state.disconnect !== null) {
|
|
540
|
+
applySelectedIndex(box, state);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function watchSelectionDrift(box: Gtk.ListBox, state: SelectedIndexState): void {
|
|
546
|
+
if (state.disconnect !== null) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const handler = (): undefined => {
|
|
551
|
+
scheduleSelectedIndex(box, state);
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
box.on(SELECTION_SIGNAL, handler);
|
|
555
|
+
|
|
556
|
+
state.disconnect = (): void => {
|
|
557
|
+
box.off(SELECTION_SIGNAL, handler);
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function selectedIndexBehavior(): ElementBehavior<Gtk.ListBox> {
|
|
562
|
+
return {
|
|
563
|
+
deferred: [SELECTED_INDEX_PROP],
|
|
564
|
+
initialize: (): SelectedIndexState => ({ desired: undefined, isScheduled: false, disconnect: null }),
|
|
565
|
+
update: (_box, _prev, next, context) => {
|
|
566
|
+
(context as SelectedIndexState).desired = desiredIndex(next[SELECTED_INDEX_PROP]);
|
|
567
|
+
|
|
568
|
+
return [SELECTED_INDEX_PROP];
|
|
569
|
+
},
|
|
570
|
+
flush: (box, context) => {
|
|
571
|
+
applySelectedIndex(box, context as SelectedIndexState);
|
|
572
|
+
watchSelectionDrift(box, context as SelectedIndexState);
|
|
573
|
+
},
|
|
574
|
+
teardown: (_box, context) => {
|
|
575
|
+
const state = context as SelectedIndexState;
|
|
576
|
+
state.disconnect?.();
|
|
577
|
+
state.disconnect = null;
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
463
582
|
const addMainOption = (application: Gtk.Application, option: MainOption): void => {
|
|
464
583
|
application.addMainOption(
|
|
465
584
|
option.longName,
|
package/src/element-config.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as Gio from "@gtkx/gi/gio";
|
|
|
3
3
|
import { kebabCase } from "@gtkx/utils";
|
|
4
4
|
import { useLayoutEffect } from "react";
|
|
5
5
|
import type { SettingsSchema, SettingsSchemaKeys } from "../utils/settings.js";
|
|
6
|
+
import { getPropertyName } from "../reconciler/metadata.js";
|
|
6
7
|
import { type RefProp, resolveRefProp } from "../utils/ref-prop.js";
|
|
7
8
|
import { useSettings } from "./use-setting.js";
|
|
8
9
|
|
|
@@ -32,7 +33,6 @@ function useBindSetting<K extends SettingsSchemaKeys>({
|
|
|
32
33
|
flags = Gio.SettingsBindFlags.DEFAULT,
|
|
33
34
|
}: UseBindSettingOptions<K>): void {
|
|
34
35
|
const settings = useSettings(schema);
|
|
35
|
-
const propertyName = kebabCase(property);
|
|
36
36
|
|
|
37
37
|
useLayoutEffect(() => {
|
|
38
38
|
const resolved = resolveRefProp(object);
|
|
@@ -41,12 +41,13 @@ function useBindSetting<K extends SettingsSchemaKeys>({
|
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
const propertyName = getPropertyName(resolved, property) ?? kebabCase(property);
|
|
44
45
|
settings.bind(key, resolved, propertyName, flags);
|
|
45
46
|
|
|
46
47
|
return () => {
|
|
47
48
|
Gio.Settings.unbind(resolved, propertyName);
|
|
48
49
|
};
|
|
49
|
-
}, [settings, key, object,
|
|
50
|
+
}, [settings, key, object, property, flags]);
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
export { useBindSetting };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type * as GObject from "@gtkx/gi/gobject";
|
|
2
2
|
import { kebabCase } from "@gtkx/utils";
|
|
3
|
-
import
|
|
3
|
+
import { getPropertyName } from "../reconciler/metadata.js";
|
|
4
|
+
import { type RefProp, resolveRefProp } from "../utils/ref-prop.js";
|
|
4
5
|
import { useObjectValue } from "./use-object-value.js";
|
|
5
6
|
|
|
6
7
|
/** The property map `T` declares, from camelCase property name to value type. */
|
|
@@ -18,7 +19,10 @@ function useProperty<T extends GObject.Object, P extends PropertyName<T>>(
|
|
|
18
19
|
object: RefProp<T>,
|
|
19
20
|
propertyName: P & string,
|
|
20
21
|
): T[P] | undefined {
|
|
21
|
-
|
|
22
|
+
const resolved = resolveRefProp(object);
|
|
23
|
+
const name = (resolved === null ? undefined : getPropertyName(resolved, propertyName)) ?? kebabCase(propertyName);
|
|
24
|
+
|
|
25
|
+
return useObjectValue(object, `notify::${name}`, (obj) => obj?.[propertyName]);
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export { useProperty };
|
package/src/prop-types.ts
CHANGED
|
@@ -246,6 +246,15 @@ type GtkScaleProps = {
|
|
|
246
246
|
marks?: ScaleMark[] | null | undefined;
|
|
247
247
|
};
|
|
248
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
|
+
|
|
249
258
|
/** Props of a `Gtk.Calendar` element. */
|
|
250
259
|
type GtkCalendarProps = {
|
|
251
260
|
/** Days of the shown month drawn as marked, cleared and re-marked whenever the list changes. */
|
|
@@ -324,6 +333,7 @@ export {
|
|
|
324
333
|
type GtkConstraintLayoutProps,
|
|
325
334
|
type GtkHeaderBarProps,
|
|
326
335
|
type GtkScaleProps,
|
|
336
|
+
type GtkListBoxProps,
|
|
327
337
|
type GtkCalendarProps,
|
|
328
338
|
type GtkLevelBarProps,
|
|
329
339
|
type GtkSizeGroupProps,
|
|
@@ -1,11 +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 { coerceObjectProperty } from "@gtkx/runtime";
|
|
5
|
-
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";
|
|
6
6
|
import type { ElementBehavior, Props } from "./registry.js";
|
|
7
7
|
import { applyAccessibleProps, isAccessibleProp } from "../utils/accessible-props.js";
|
|
8
|
-
import { type TypeInfo, typeInfoFor } from "./metadata.js";
|
|
8
|
+
import { getPropertyName, hasProperty, type TypeInfo, typeInfoFor } from "./metadata.js";
|
|
9
9
|
import { type ElementNode, getOrCreateContext, type SignalTarget } from "./node.js";
|
|
10
10
|
import { applyWrite, connectHandler, disconnectHandler } from "./signals.js";
|
|
11
11
|
import { bufferText, hasSameText, isContentPaintableProp, markTextDirty, TEXT_PROP } from "./text.js";
|
|
@@ -16,29 +16,53 @@ type PropChange = { prev: Props; next: Props };
|
|
|
16
16
|
|
|
17
17
|
const REACT_RESERVED_PROPS = new Set(["children", "ref", "key"]);
|
|
18
18
|
const NOTIFY_PREFIX = "onNotify";
|
|
19
|
-
const HANDLER_PREFIX = "on";
|
|
20
19
|
const HANDLER_NAME = /^on[A-Z]/;
|
|
21
20
|
const flushDirty: Set<ElementNode> = new Set();
|
|
22
21
|
const accessibleDirty: Map<ElementNode, Props> = new Map();
|
|
23
|
-
const mapWatched:
|
|
22
|
+
const mapWatched: WeakMap<ElementNode, () => void> = new WeakMap();
|
|
24
23
|
const pendingMap: Set<ElementNode> = new Set();
|
|
25
24
|
|
|
26
25
|
const isHandlerName = (name: string): boolean => HANDLER_NAME.test(name);
|
|
26
|
+
const notifiedAccessor = (name: string): string => lowerFirst(name.slice(NOTIFY_PREFIX.length));
|
|
27
27
|
|
|
28
|
-
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 => {
|
|
29
47
|
if (name === NOTIFY_PREFIX) {
|
|
30
48
|
return "notify";
|
|
31
49
|
}
|
|
32
50
|
|
|
33
51
|
if (name.startsWith(NOTIFY_PREFIX) && name.length > NOTIFY_PREFIX.length) {
|
|
34
|
-
|
|
52
|
+
const accessor = notifiedAccessor(name);
|
|
53
|
+
const property = getPropertyName(target.object, accessor) ?? unsanitizeIdentifier(kebabCase(accessor));
|
|
54
|
+
|
|
55
|
+
return `notify::${property}`;
|
|
35
56
|
}
|
|
36
57
|
|
|
37
|
-
return info.signals[name] ??
|
|
58
|
+
return info.signals[name] ?? lookedUpSignal(target, name);
|
|
38
59
|
};
|
|
39
60
|
|
|
40
61
|
const isReservedName = (name: string, info: TypeInfo): boolean =>
|
|
41
|
-
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);
|
|
42
66
|
|
|
43
67
|
const isSkippedValueName = (name: string, info: TypeInfo, consumed: Set<string>): boolean =>
|
|
44
68
|
isReservedName(name, info) || consumed.has(name);
|
|
@@ -48,7 +72,7 @@ const writeValue = (object: GObject.Object, name: string, value: unknown): void
|
|
|
48
72
|
return;
|
|
49
73
|
}
|
|
50
74
|
|
|
51
|
-
applyWrite(() => {
|
|
75
|
+
applyWrite(name, () => {
|
|
52
76
|
Reflect.set(object, name, coerceObjectProperty(object, name, value));
|
|
53
77
|
});
|
|
54
78
|
};
|
|
@@ -72,7 +96,7 @@ const applyBufferText = (buffer: Gtk.TextBuffer, text: string): void => {
|
|
|
72
96
|
return;
|
|
73
97
|
}
|
|
74
98
|
|
|
75
|
-
applyWrite(() => {
|
|
99
|
+
applyWrite(TEXT_PROP, () => {
|
|
76
100
|
buffer.beginIrreversibleAction();
|
|
77
101
|
buffer.setText(text, -1);
|
|
78
102
|
buffer.endIrreversibleAction();
|
|
@@ -191,24 +215,26 @@ const restoreActionableSensitivity = (node: ElementNode, info: TypeInfo, prev: P
|
|
|
191
215
|
const desired = "sensitive" in next ? next.sensitive : info.defaults.sensitive;
|
|
192
216
|
|
|
193
217
|
if (typeof desired === "boolean") {
|
|
194
|
-
applyWrite(() => {
|
|
218
|
+
applyWrite("sensitive", () => {
|
|
195
219
|
Reflect.set(node.object, "sensitive", desired);
|
|
196
220
|
});
|
|
197
221
|
}
|
|
198
222
|
};
|
|
199
223
|
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
if (!isHandlerName(name)) {
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
224
|
+
const applyHandler = (target: SignalTarget, info: TypeInfo, name: string, next: Props): void => {
|
|
225
|
+
const value = next[name];
|
|
205
226
|
|
|
206
|
-
|
|
227
|
+
if (typeof value === "function") {
|
|
228
|
+
connectHandler(target, name, signalForProp(target, info, name), value as SignalHandler);
|
|
229
|
+
} else {
|
|
230
|
+
disconnectHandler(target, name);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
207
233
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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);
|
|
212
238
|
}
|
|
213
239
|
});
|
|
214
240
|
};
|
|
@@ -231,6 +257,15 @@ const flushBehaviors = (): void => {
|
|
|
231
257
|
});
|
|
232
258
|
};
|
|
233
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
|
+
|
|
234
269
|
const applyAccessible = (object: GObject.Object, prev: Props | null, next: Props): void => {
|
|
235
270
|
if (object instanceof Gtk.Accessible) {
|
|
236
271
|
applyAccessibleProps(object, prev, next);
|
|
@@ -260,14 +295,25 @@ const watchMap = (node: ElementNode): void => {
|
|
|
260
295
|
return;
|
|
261
296
|
}
|
|
262
297
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
object.connect("map", () => {
|
|
298
|
+
const onMapped = (): undefined => {
|
|
266
299
|
pendingMap.add(node);
|
|
267
300
|
setTimeout(settleAccessible, 0);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
object.on("map", onMapped);
|
|
304
|
+
|
|
305
|
+
mapWatched.set(node, () => {
|
|
306
|
+
object.off("map", onMapped);
|
|
268
307
|
});
|
|
269
308
|
};
|
|
270
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
|
+
|
|
271
317
|
const settleAccessible = (): void => {
|
|
272
318
|
drain(pendingMap, (node) => {
|
|
273
319
|
if (node.object instanceof Gtk.Widget && node.object.getMapped()) {
|
|
@@ -319,6 +365,7 @@ export {
|
|
|
319
365
|
flushAccessible,
|
|
320
366
|
settleAccessible,
|
|
321
367
|
flushBehaviors,
|
|
368
|
+
teardownBehaviors,
|
|
322
369
|
applyElementProps,
|
|
323
370
|
applyAdoptedProps,
|
|
324
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";
|
|
@@ -161,7 +162,7 @@ const hostConfig = {
|
|
|
161
162
|
suspendInstance: (): void => undefined,
|
|
162
163
|
waitForCommitToBeReady: (): null => null,
|
|
163
164
|
NotPendingTransition: null,
|
|
164
|
-
HostTransitionContext: createContext(null)
|
|
165
|
+
HostTransitionContext: createContext(null),
|
|
165
166
|
};
|
|
166
167
|
|
|
167
168
|
const reconciler: ReactReconciler.Reconciler<Container, Instance, TextNode, unknown, unknown, object> =
|
|
@@ -190,6 +191,7 @@ function createPriorityTracker(): PriorityTracker {
|
|
|
190
191
|
|
|
191
192
|
const detachElement = (instance: ElementNode): void => {
|
|
192
193
|
disconnectAllHandlers(instance);
|
|
194
|
+
teardownBehaviors(instance);
|
|
193
195
|
|
|
194
196
|
if (instance.object instanceof Gtk.Widget) {
|
|
195
197
|
releaseStyle(instance.object);
|
|
@@ -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 };
|