@gtkx/react 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +26 -62
  2. package/dist/components/compound.d.ts +40 -0
  3. package/dist/components/compound.d.ts.map +1 -0
  4. package/dist/components/compound.js +46 -0
  5. package/dist/components/compound.js.map +1 -0
  6. package/dist/components/list.d.ts +42 -2
  7. package/dist/components/list.d.ts.map +1 -1
  8. package/dist/components/list.js +42 -1
  9. package/dist/components/list.js.map +1 -1
  10. package/dist/components/slot-widget.d.ts +15 -0
  11. package/dist/components/slot-widget.d.ts.map +1 -0
  12. package/dist/components/slot-widget.js +37 -0
  13. package/dist/components/slot-widget.js.map +1 -0
  14. package/dist/errors.d.ts +6 -0
  15. package/dist/errors.d.ts.map +1 -1
  16. package/dist/errors.js +8 -6
  17. package/dist/errors.js.map +1 -1
  18. package/dist/generated/compounds.d.ts +2672 -0
  19. package/dist/generated/compounds.d.ts.map +1 -0
  20. package/dist/generated/compounds.js +2624 -0
  21. package/dist/generated/compounds.js.map +1 -0
  22. package/dist/generated/internal.d.ts +2 -2
  23. package/dist/generated/internal.d.ts.map +1 -1
  24. package/dist/generated/internal.js +2751 -4748
  25. package/dist/generated/internal.js.map +1 -1
  26. package/dist/generated/jsx.d.ts +2042 -4592
  27. package/dist/generated/jsx.d.ts.map +1 -1
  28. package/dist/generated/jsx.js +919 -3478
  29. package/dist/generated/jsx.js.map +1 -1
  30. package/dist/generated/registry.d.ts +1 -0
  31. package/dist/generated/registry.d.ts.map +1 -1
  32. package/dist/generated/registry.js +0 -1
  33. package/dist/generated/registry.js.map +1 -1
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/jsx.d.ts +116 -414
  39. package/dist/jsx.d.ts.map +1 -1
  40. package/dist/jsx.js +5 -328
  41. package/dist/jsx.js.map +1 -1
  42. package/dist/metadata.d.ts +1 -1
  43. package/dist/metadata.d.ts.map +1 -1
  44. package/dist/metadata.js.map +1 -1
  45. package/dist/nodes/alert-dialog.d.ts +14 -0
  46. package/dist/nodes/alert-dialog.d.ts.map +1 -0
  47. package/dist/nodes/alert-dialog.js +41 -0
  48. package/dist/nodes/alert-dialog.js.map +1 -0
  49. package/dist/nodes/animation.d.ts +5 -4
  50. package/dist/nodes/animation.d.ts.map +1 -1
  51. package/dist/nodes/animation.js +65 -49
  52. package/dist/nodes/animation.js.map +1 -1
  53. package/dist/nodes/column-view-column.js +2 -2
  54. package/dist/nodes/column-view-column.js.map +1 -1
  55. package/dist/nodes/container-slot.d.ts +3 -1
  56. package/dist/nodes/container-slot.d.ts.map +1 -1
  57. package/dist/nodes/container-slot.js +28 -16
  58. package/dist/nodes/container-slot.js.map +1 -1
  59. package/dist/nodes/drawing-area.d.ts +3 -1
  60. package/dist/nodes/drawing-area.d.ts.map +1 -1
  61. package/dist/nodes/drawing-area.js +20 -22
  62. package/dist/nodes/drawing-area.js.map +1 -1
  63. package/dist/nodes/event-controller.d.ts.map +1 -1
  64. package/dist/nodes/event-controller.js +3 -7
  65. package/dist/nodes/event-controller.js.map +1 -1
  66. package/dist/nodes/fixed-child.d.ts +1 -0
  67. package/dist/nodes/fixed-child.d.ts.map +1 -1
  68. package/dist/nodes/fixed-child.js +13 -0
  69. package/dist/nodes/fixed-child.js.map +1 -1
  70. package/dist/nodes/grid-child.d.ts +1 -0
  71. package/dist/nodes/grid-child.d.ts.map +1 -1
  72. package/dist/nodes/grid-child.js +13 -0
  73. package/dist/nodes/grid-child.js.map +1 -1
  74. package/dist/nodes/internal/construct.js +2 -2
  75. package/dist/nodes/internal/construct.js.map +1 -1
  76. package/dist/nodes/internal/widget.d.ts.map +1 -1
  77. package/dist/nodes/internal/widget.js +5 -9
  78. package/dist/nodes/internal/widget.js.map +1 -1
  79. package/dist/nodes/list-item-node.d.ts +6 -6
  80. package/dist/nodes/list-item-node.d.ts.map +1 -1
  81. package/dist/nodes/list-item-node.js +27 -5
  82. package/dist/nodes/list-item-node.js.map +1 -1
  83. package/dist/nodes/notebook-page.js +2 -2
  84. package/dist/nodes/notebook-page.js.map +1 -1
  85. package/dist/nodes/overlay-child.d.ts +2 -0
  86. package/dist/nodes/overlay-child.d.ts.map +1 -1
  87. package/dist/nodes/overlay-child.js +29 -8
  88. package/dist/nodes/overlay-child.js.map +1 -1
  89. package/dist/nodes/spin-row.d.ts +14 -0
  90. package/dist/nodes/spin-row.d.ts.map +1 -0
  91. package/dist/nodes/spin-row.js +46 -0
  92. package/dist/nodes/spin-row.js.map +1 -0
  93. package/dist/nodes/switch-row.d.ts +11 -0
  94. package/dist/nodes/switch-row.d.ts.map +1 -0
  95. package/dist/nodes/switch-row.js +15 -0
  96. package/dist/nodes/switch-row.js.map +1 -0
  97. package/dist/nodes/text-anchor.d.ts.map +1 -1
  98. package/dist/nodes/text-anchor.js +10 -0
  99. package/dist/nodes/text-anchor.js.map +1 -1
  100. package/dist/nodes/text-tag.d.ts.map +1 -1
  101. package/dist/nodes/text-tag.js +45 -39
  102. package/dist/nodes/text-tag.js.map +1 -1
  103. package/dist/nodes/toggle-group.d.ts +12 -6
  104. package/dist/nodes/toggle-group.d.ts.map +1 -1
  105. package/dist/nodes/toggle-group.js +53 -4
  106. package/dist/nodes/toggle-group.js.map +1 -1
  107. package/dist/nodes/widget.d.ts.map +1 -1
  108. package/dist/nodes/widget.js +7 -14
  109. package/dist/nodes/widget.js.map +1 -1
  110. package/dist/registry.d.ts.map +1 -1
  111. package/dist/registry.js +7 -5
  112. package/dist/registry.js.map +1 -1
  113. package/dist/types.d.ts +1 -0
  114. package/dist/types.d.ts.map +1 -1
  115. package/dist/use-property.d.ts +29 -0
  116. package/dist/use-property.d.ts.map +1 -0
  117. package/dist/use-property.js +44 -0
  118. package/dist/use-property.js.map +1 -0
  119. package/dist/use-setting.d.ts +36 -0
  120. package/dist/use-setting.d.ts.map +1 -0
  121. package/dist/use-setting.js +68 -0
  122. package/dist/use-setting.js.map +1 -0
  123. package/package.json +3 -3
  124. package/src/components/compound.tsx +57 -0
  125. package/src/components/list.tsx +59 -2
  126. package/src/components/slot-widget.tsx +46 -0
  127. package/src/errors.ts +8 -7
  128. package/src/generated/compounds.ts +2741 -0
  129. package/src/generated/internal.ts +2752 -4754
  130. package/src/generated/jsx.ts +2495 -5012
  131. package/src/generated/registry.ts +2 -1
  132. package/src/index.ts +2 -0
  133. package/src/jsx.ts +121 -443
  134. package/src/metadata.ts +1 -1
  135. package/src/nodes/alert-dialog.ts +55 -0
  136. package/src/nodes/animation.ts +67 -60
  137. package/src/nodes/column-view-column.ts +2 -2
  138. package/src/nodes/container-slot.ts +30 -17
  139. package/src/nodes/drawing-area.ts +23 -32
  140. package/src/nodes/event-controller.ts +3 -7
  141. package/src/nodes/fixed-child.ts +13 -0
  142. package/src/nodes/grid-child.ts +13 -0
  143. package/src/nodes/internal/construct.ts +2 -2
  144. package/src/nodes/internal/widget.ts +6 -12
  145. package/src/nodes/list-item-node.ts +33 -9
  146. package/src/nodes/notebook-page.ts +2 -2
  147. package/src/nodes/overlay-child.ts +30 -9
  148. package/src/nodes/spin-row.ts +72 -0
  149. package/src/nodes/switch-row.ts +26 -0
  150. package/src/nodes/text-anchor.ts +9 -0
  151. package/src/nodes/text-tag.ts +45 -40
  152. package/src/nodes/toggle-group.ts +63 -9
  153. package/src/nodes/widget.ts +6 -13
  154. package/src/registry.ts +7 -5
  155. package/src/types.ts +1 -0
  156. package/src/use-property.ts +58 -0
  157. package/src/use-setting.ts +96 -0
@@ -161,11 +161,11 @@ export class NotebookPageNode extends VirtualNode<NotebookPageProps, WidgetNode<
161
161
  if (!page) return;
162
162
 
163
163
  if (this.props.tabExpand !== undefined) {
164
- page.setTabExpand(this.props.tabExpand);
164
+ page.tabExpand = this.props.tabExpand;
165
165
  }
166
166
 
167
167
  if (this.props.tabFill !== undefined) {
168
- page.setTabFill(this.props.tabFill);
168
+ page.tabFill = this.props.tabFill;
169
169
  }
170
170
  }
171
171
 
@@ -1,6 +1,7 @@
1
1
  import * as Gtk from "@gtkx/ffi/gtk";
2
2
  import type { OverlayChildProps } from "../jsx.js";
3
3
  import type { Node } from "../node.js";
4
+ import { isRemovable } from "./internal/predicates.js";
4
5
  import { hasChanged } from "./internal/props.js";
5
6
  import { VirtualNode } from "./virtual.js";
6
7
  import { WidgetNode } from "./widget.js";
@@ -32,6 +33,7 @@ export class OverlayChildNode extends VirtualNode<OverlayChildProps, WidgetNode<
32
33
  super.appendChild(child);
33
34
 
34
35
  if (this.parent) {
36
+ this.detachFromGtkParent(child);
35
37
  this.attachToParent(this.parent.container, child.container);
36
38
  }
37
39
  }
@@ -40,19 +42,12 @@ export class OverlayChildNode extends VirtualNode<OverlayChildProps, WidgetNode<
40
42
  super.insertBefore(child, before);
41
43
 
42
44
  if (this.parent) {
43
- this.attachToParent(this.parent.container, child.container);
45
+ this.reinsertAllChildren();
44
46
  }
45
47
  }
46
48
 
47
49
  public override removeChild(child: WidgetNode): void {
48
- if (this.parent) {
49
- const widget = child.container;
50
- const currentParent = widget.getParent();
51
- if (currentParent && currentParent === this.parent.container) {
52
- this.parent.container.removeOverlay(widget);
53
- }
54
- }
55
-
50
+ this.detachFromGtkParent(child);
56
51
  super.removeChild(child);
57
52
  }
58
53
 
@@ -98,6 +93,32 @@ export class OverlayChildNode extends VirtualNode<OverlayChildProps, WidgetNode<
98
93
  }
99
94
  }
100
95
 
96
+ private detachFromGtkParent(child: WidgetNode): void {
97
+ const currentParent = child.container.getParent();
98
+ if (currentParent !== null) {
99
+ if (currentParent instanceof Gtk.Overlay) {
100
+ currentParent.removeOverlay(child.container);
101
+ } else if (isRemovable(currentParent)) {
102
+ currentParent.remove(child.container);
103
+ } else {
104
+ child.container.unparent();
105
+ }
106
+ }
107
+ }
108
+
109
+ private reinsertAllChildren(): void {
110
+ if (!this.parent) return;
111
+ const parent = this.parent.container;
112
+
113
+ for (const child of this.children) {
114
+ this.detachFromGtkParent(child);
115
+ }
116
+
117
+ for (const child of this.children) {
118
+ this.attachToParent(parent, child.container);
119
+ }
120
+ }
121
+
101
122
  private detachAllChildren(parent: Gtk.Overlay): void {
102
123
  for (const child of this.children) {
103
124
  const currentParent = child.container.getParent();
@@ -0,0 +1,72 @@
1
+ import type * as Adw from "@gtkx/ffi/adw";
2
+ import * as Gtk from "@gtkx/ffi/gtk";
3
+ import type { AdjustableProps } from "../jsx.js";
4
+ import { filterProps, hasChanged } from "./internal/props.js";
5
+ import { WidgetNode } from "./widget.js";
6
+
7
+ type SpinRowProps = AdjustableProps & {
8
+ onValueChanged?: ((value: number, self: Adw.SpinRow) => void) | null;
9
+ };
10
+
11
+ const OWN_PROPS = ["value", "lower", "upper", "stepIncrement", "pageIncrement", "pageSize", "onValueChanged"] as const;
12
+
13
+ export class SpinRowNode extends WidgetNode<Adw.SpinRow, SpinRowProps> {
14
+ private adjustment: Gtk.Adjustment | null = null;
15
+
16
+ public override commitUpdate(oldProps: SpinRowProps | null, newProps: SpinRowProps): void {
17
+ super.commitUpdate(oldProps ? filterProps(oldProps, OWN_PROPS) : null, filterProps(newProps, OWN_PROPS));
18
+ this.applyOwnProps(oldProps, newProps);
19
+ }
20
+
21
+ private ensureAdjustment(props: SpinRowProps): Gtk.Adjustment {
22
+ if (!this.adjustment) {
23
+ this.adjustment = new Gtk.Adjustment(
24
+ props.value ?? 0,
25
+ props.lower ?? 0,
26
+ props.upper ?? 100,
27
+ props.stepIncrement ?? 1,
28
+ props.pageIncrement ?? 10,
29
+ props.pageSize ?? 0,
30
+ );
31
+
32
+ this.container.setAdjustment(this.adjustment);
33
+ }
34
+
35
+ return this.adjustment;
36
+ }
37
+
38
+ private applyOwnProps(oldProps: SpinRowProps | null, newProps: SpinRowProps): void {
39
+ const adjustment = this.ensureAdjustment(newProps);
40
+
41
+ if (hasChanged(oldProps, newProps, "onValueChanged")) {
42
+ const { onValueChanged } = newProps;
43
+ this.signalStore.set(
44
+ this,
45
+ this.container,
46
+ "notify::value",
47
+ onValueChanged ? (self: Adw.SpinRow) => onValueChanged(self.getValue(), self) : undefined,
48
+ );
49
+ }
50
+
51
+ if (!oldProps) return;
52
+
53
+ if (hasChanged(oldProps, newProps, "lower")) {
54
+ adjustment.setLower(newProps.lower ?? 0);
55
+ }
56
+ if (hasChanged(oldProps, newProps, "upper")) {
57
+ adjustment.setUpper(newProps.upper ?? 100);
58
+ }
59
+ if (hasChanged(oldProps, newProps, "stepIncrement")) {
60
+ adjustment.setStepIncrement(newProps.stepIncrement ?? 1);
61
+ }
62
+ if (hasChanged(oldProps, newProps, "pageIncrement")) {
63
+ adjustment.setPageIncrement(newProps.pageIncrement ?? 10);
64
+ }
65
+ if (hasChanged(oldProps, newProps, "pageSize")) {
66
+ adjustment.setPageSize(newProps.pageSize ?? 0);
67
+ }
68
+ if (hasChanged(oldProps, newProps, "value") && newProps.value !== undefined) {
69
+ adjustment.setValue(newProps.value);
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,26 @@
1
+ import type * as Adw from "@gtkx/ffi/adw";
2
+ import { hasChanged } from "./internal/props.js";
3
+ import { WidgetNode } from "./widget.js";
4
+
5
+ type SwitchRowProps = {
6
+ onActiveChanged?: ((active: boolean, self: Adw.SwitchRow) => void) | null;
7
+ };
8
+
9
+ export class SwitchRowNode extends WidgetNode<Adw.SwitchRow, SwitchRowProps> {
10
+ public override commitUpdate(oldProps: SwitchRowProps | null, newProps: SwitchRowProps): void {
11
+ super.commitUpdate(oldProps, newProps);
12
+ this.applyOwnProps(oldProps, newProps);
13
+ }
14
+
15
+ private applyOwnProps(oldProps: SwitchRowProps | null, newProps: SwitchRowProps): void {
16
+ if (hasChanged(oldProps, newProps, "onActiveChanged")) {
17
+ const { onActiveChanged } = newProps;
18
+ this.signalStore.set(
19
+ this,
20
+ this.container,
21
+ "notify::active",
22
+ onActiveChanged ? (self: Adw.SwitchRow) => onActiveChanged(self.getActive(), self) : undefined,
23
+ );
24
+ }
25
+ }
26
+ }
@@ -1,6 +1,7 @@
1
1
  import * as Gtk from "@gtkx/ffi/gtk";
2
2
  import type { TextAnchorProps } from "../jsx.js";
3
3
  import type { Node } from "../node.js";
4
+ import { isRemovable } from "./internal/predicates.js";
4
5
  import { TEXT_OBJECT_REPLACEMENT, type TextContentParent } from "./text-content.js";
5
6
  import { isTextContentParent } from "./text-segment.js";
6
7
  import { VirtualNode } from "./virtual.js";
@@ -66,6 +67,14 @@ export class TextAnchorNode extends VirtualNode<TextAnchorProps, Node & TextCont
66
67
  super.appendChild(child);
67
68
 
68
69
  if (this.textView && this.anchor && child.container) {
70
+ const currentParent = child.container.getParent();
71
+ if (currentParent !== null) {
72
+ if (isRemovable(currentParent)) {
73
+ currentParent.remove(child.container);
74
+ } else {
75
+ child.container.unparent();
76
+ }
77
+ }
69
78
  this.textView.addChildAtAnchor(child.container, this.anchor);
70
79
  }
71
80
  }
@@ -8,45 +8,45 @@ import { TextPaintableNode } from "./text-paintable.js";
8
8
  import { isTextContentParent, TextSegmentNode } from "./text-segment.js";
9
9
  import { VirtualNode } from "./virtual.js";
10
10
 
11
- const STYLE_PROPS: Partial<Record<keyof TextTagProps, keyof Gtk.TextTag>> = {
11
+ const STYLE_PROPS: Partial<Record<keyof TextTagProps, keyof Gtk.TextTag | string>> = {
12
12
  background: "setBackground",
13
- backgroundFullHeight: "setBackgroundFullHeight",
13
+ backgroundFullHeight: "backgroundFullHeight",
14
14
  foreground: "setForeground",
15
- family: "setFamily",
16
- font: "setFont",
17
- sizePoints: "setSizePoints",
18
- size: "setSize",
19
- scale: "setScale",
20
- weight: "setWeight",
21
- style: "setStyle",
22
- stretch: "setStretch",
23
- variant: "setVariant",
24
- strikethrough: "setStrikethrough",
25
- underline: "setUnderline",
26
- overline: "setOverline",
27
- rise: "setRise",
28
- letterSpacing: "setLetterSpacing",
29
- lineHeight: "setLineHeight",
30
- leftMargin: "setLeftMargin",
31
- rightMargin: "setRightMargin",
32
- indent: "setIndent",
33
- pixelsAboveLines: "setPixelsAboveLines",
34
- pixelsBelowLines: "setPixelsBelowLines",
35
- pixelsInsideWrap: "setPixelsInsideWrap",
36
- justification: "setJustification",
37
- direction: "setDirection",
38
- wrapMode: "setWrapMode",
39
- editable: "setEditable",
40
- invisible: "setInvisible",
41
- allowBreaks: "setAllowBreaks",
42
- insertHyphens: "setInsertHyphens",
43
- fallback: "setFallback",
44
- accumulativeMargin: "setAccumulativeMargin",
15
+ family: "family",
16
+ font: "font",
17
+ sizePoints: "sizePoints",
18
+ size: "size",
19
+ scale: "scale",
20
+ weight: "weight",
21
+ style: "style",
22
+ stretch: "stretch",
23
+ variant: "variant",
24
+ strikethrough: "strikethrough",
25
+ underline: "underline",
26
+ overline: "overline",
27
+ rise: "rise",
28
+ letterSpacing: "letterSpacing",
29
+ lineHeight: "lineHeight",
30
+ leftMargin: "leftMargin",
31
+ rightMargin: "rightMargin",
32
+ indent: "indent",
33
+ pixelsAboveLines: "pixelsAboveLines",
34
+ pixelsBelowLines: "pixelsBelowLines",
35
+ pixelsInsideWrap: "pixelsInsideWrap",
36
+ justification: "justification",
37
+ direction: "direction",
38
+ wrapMode: "wrapMode",
39
+ editable: "editable",
40
+ invisible: "invisible",
41
+ allowBreaks: "allowBreaks",
42
+ insertHyphens: "insertHyphens",
43
+ fallback: "fallback",
44
+ accumulativeMargin: "accumulativeMargin",
45
45
  paragraphBackground: "setParagraphBackground",
46
- showSpaces: "setShowSpaces",
47
- textTransform: "setTextTransform",
48
- fontFeatures: "setFontFeatures",
49
- language: "setLanguage",
46
+ showSpaces: "showSpaces",
47
+ textTransform: "textTransform",
48
+ fontFeatures: "fontFeatures",
49
+ language: "language",
50
50
  };
51
51
 
52
52
  type TextTagParent = Node & TextContentParent;
@@ -225,10 +225,15 @@ export class TextTagNode
225
225
  for (const prop of Object.keys(STYLE_PROPS) as (keyof TextTagProps)[]) {
226
226
  if (hasChanged(oldProps, newProps, prop)) {
227
227
  const value = newProps[prop];
228
- const method = STYLE_PROPS[prop];
229
- if (value !== undefined && method) {
230
- const setter = this.tag[method] as (value: unknown) => void;
231
- setter.call(this.tag, value);
228
+ const target = STYLE_PROPS[prop];
229
+ if (value !== undefined && target) {
230
+ const tag = this.tag as unknown as Record<string, unknown>;
231
+ const member = tag[target];
232
+ if (typeof member === "function") {
233
+ (member as (v: unknown) => void).call(this.tag, value);
234
+ } else {
235
+ tag[target] = value;
236
+ }
232
237
  }
233
238
  }
234
239
  }
@@ -1,19 +1,41 @@
1
- import type * as Adw from "@gtkx/ffi/adw";
2
- import type { AdwToggleGroupProps } from "../jsx.js";
1
+ import * as Adw from "@gtkx/ffi/adw";
2
+ import type * as Gtk from "@gtkx/ffi/gtk";
3
+ import type { AdwToggleGroupProps, ToggleProps } from "../jsx.js";
4
+ import type { Container, Props } from "../types.js";
5
+ import { createContainerWithProperties } from "./internal/construct.js";
3
6
  import { filterProps, hasChanged } from "./internal/props.js";
4
7
  import { WidgetNode } from "./widget.js";
5
8
 
6
- const OWN_PROPS = ["onActiveChanged"] as const;
9
+ const DEFERRED_PROPS = ["activeName", "active"] as const;
10
+ const OWN_PROPS = ["onActiveChanged", "toggles", ...DEFERRED_PROPS] as const;
7
11
 
8
- type ToggleGroupProps = Pick<AdwToggleGroupProps, (typeof OWN_PROPS)[number]>;
12
+ type OwnProps = Pick<AdwToggleGroupProps, (typeof OWN_PROPS)[number]>;
9
13
 
10
- export class ToggleGroupNode extends WidgetNode<Adw.ToggleGroup, ToggleGroupProps> {
11
- public override commitUpdate(oldProps: ToggleGroupProps | null, newProps: ToggleGroupProps): void {
12
- super.commitUpdate(oldProps ? filterProps(oldProps, OWN_PROPS) : null, filterProps(newProps, OWN_PROPS));
13
- this.applyOwnProps(oldProps, newProps);
14
+ export class ToggleGroupNode extends WidgetNode<Adw.ToggleGroup, OwnProps> {
15
+ private managedToggles: Adw.Toggle[] = [];
16
+
17
+ public static override createContainer(props: Props, containerClass: typeof Gtk.Widget): Container | null {
18
+ const { activeName: _, active: __, ...rest } = props;
19
+ return createContainerWithProperties(containerClass, rest);
14
20
  }
15
21
 
16
- private applyOwnProps(oldProps: ToggleGroupProps | null, newProps: ToggleGroupProps): void {
22
+ public override commitUpdate(oldProps: OwnProps | null, newProps: OwnProps): void {
23
+ if (hasChanged(oldProps, newProps, "toggles")) {
24
+ this.syncToggles(newProps.toggles ?? []);
25
+ }
26
+
27
+ super.commitUpdate(oldProps ? filterProps(oldProps, OWN_PROPS) : null, filterProps(newProps, OWN_PROPS));
28
+
29
+ if (hasChanged(oldProps, newProps, "activeName")) {
30
+ this.container.setActiveName(newProps.activeName ?? null);
31
+ }
32
+
33
+ if (hasChanged(oldProps, newProps, "active")) {
34
+ if (newProps.active != null) {
35
+ this.container.setActive(newProps.active);
36
+ }
37
+ }
38
+
17
39
  if (hasChanged(oldProps, newProps, "onActiveChanged")) {
18
40
  const callback = newProps.onActiveChanged;
19
41
  this.signalStore.set(
@@ -24,4 +46,36 @@ export class ToggleGroupNode extends WidgetNode<Adw.ToggleGroup, ToggleGroupProp
24
46
  );
25
47
  }
26
48
  }
49
+
50
+ public override detachDeletedInstance(): void {
51
+ this.clearToggles();
52
+ super.detachDeletedInstance();
53
+ }
54
+
55
+ private syncToggles(newToggles: ToggleProps[]): void {
56
+ this.clearToggles();
57
+
58
+ for (const toggleProps of newToggles) {
59
+ const toggle = new Adw.Toggle();
60
+ applyToggleProps(toggle, toggleProps);
61
+ this.container.add(toggle);
62
+ this.managedToggles.push(toggle);
63
+ }
64
+ }
65
+
66
+ private clearToggles(): void {
67
+ for (const toggle of this.managedToggles) {
68
+ this.container.remove(toggle);
69
+ }
70
+ this.managedToggles = [];
71
+ }
72
+ }
73
+
74
+ function applyToggleProps(toggle: Adw.Toggle, props: ToggleProps): void {
75
+ if (props.id != null) toggle.setName(props.id);
76
+ if (props.label != null) toggle.setLabel(props.label);
77
+ if (props.iconName != null) toggle.setIconName(props.iconName);
78
+ if (props.tooltip !== undefined) toggle.setTooltip(props.tooltip);
79
+ if (props.enabled !== undefined) toggle.setEnabled(props.enabled);
80
+ if (props.useUnderline !== undefined) toggle.setUseUnderline(props.useUnderline);
27
81
  }
@@ -297,23 +297,16 @@ export class WidgetNode<
297
297
  }
298
298
 
299
299
  private setProperty(key: string, value: unknown): void {
300
- const propMeta = resolvePropMeta(this.container, key);
301
- if (!propMeta) return;
300
+ const propName = resolvePropMeta(this.container, key);
301
+ if (!propName) return;
302
302
 
303
- const [getterName, setterName] = propMeta;
304
- const setter = this.container[setterName as keyof typeof this.container];
305
- if (!setter || typeof setter !== "function") return;
303
+ const target = this.container as Record<string, unknown>;
306
304
 
307
- if (getterName && findProperty(this.container, key) instanceof ParamSpecString) {
308
- const getter = this.container[getterName as keyof typeof this.container];
309
-
310
- if (getter && typeof getter === "function") {
311
- const currentValue = getter.call(this.container);
312
- if (currentValue === value) return;
313
- }
305
+ if (findProperty(this.container, key) instanceof ParamSpecString) {
306
+ if (target[propName] === value) return;
314
307
  }
315
308
 
316
- setter.call(this.container, value);
309
+ target[propName] = value;
317
310
  }
318
311
 
319
312
  private insertBeforeReorderable(container: ReorderableWidget, child: WidgetNode, before: WidgetNode): void {
package/src/registry.ts CHANGED
@@ -5,7 +5,7 @@ import * as WebKit from "@gtkx/ffi/webkit";
5
5
  import type { Node } from "./node.js";
6
6
  import { AdjustableNode } from "./nodes/adjustable.js";
7
7
 
8
- import { AlertDialogResponseNode } from "./nodes/alert-dialog-response.js";
8
+ import { AlertDialogNode } from "./nodes/alert-dialog.js";
9
9
  import { AnimationNode } from "./nodes/animation.js";
10
10
  import { ApplicationNode } from "./nodes/application.js";
11
11
  import { CalendarNode } from "./nodes/calendar.js";
@@ -35,14 +35,15 @@ import { SearchBarNode } from "./nodes/search-bar.js";
35
35
  import { ShortcutNode } from "./nodes/shortcut.js";
36
36
  import { SlotNode } from "./nodes/slot.js";
37
37
  import { SourceViewNode } from "./nodes/source-view.js";
38
+ import { SpinRowNode } from "./nodes/spin-row.js";
38
39
  import { StackNode } from "./nodes/stack.js";
39
40
  import { StackPageNode } from "./nodes/stack-page.js";
41
+ import { SwitchRowNode } from "./nodes/switch-row.js";
40
42
  import { TextAnchorNode } from "./nodes/text-anchor.js";
41
43
  import { TextPaintableNode } from "./nodes/text-paintable.js";
42
44
  import { TextSegmentNode } from "./nodes/text-segment.js";
43
45
  import { TextTagNode } from "./nodes/text-tag.js";
44
46
  import { TextViewNode } from "./nodes/text-view.js";
45
- import { ToggleNode } from "./nodes/toggle.js";
46
47
  import { ToggleGroupNode } from "./nodes/toggle-group.js";
47
48
  import { WebViewNode } from "./nodes/web-view.js";
48
49
  import { WidgetNode } from "./nodes/widget.js";
@@ -72,8 +73,7 @@ type NodeRegistryEntry = [RegistryKey, NodeClass];
72
73
 
73
74
  export const NODE_REGISTRY: NodeRegistryEntry[] = [
74
75
  ["ContainerSlot", ContainerSlotNode],
75
- ["AlertDialogResponse", AlertDialogResponseNode],
76
- ["Animation", AnimationNode],
76
+ [["AdwTimedAnimation", "AdwSpringAnimation"], AnimationNode],
77
77
  ["ColumnViewColumn", ColumnViewColumnNode],
78
78
  ["FixedChild", FixedChildNode],
79
79
  ["GridChild", GridChildNode],
@@ -89,14 +89,16 @@ export const NODE_REGISTRY: NodeRegistryEntry[] = [
89
89
  ["TextPaintable", TextPaintableNode],
90
90
  ["TextSegment", TextSegmentNode],
91
91
  ["TextTag", TextTagNode],
92
- ["Toggle", ToggleNode],
93
92
  [Gtk.Application, ApplicationNode],
94
93
  [Gtk.EventController, EventControllerNode],
95
94
  [GtkSource.View, SourceViewNode],
96
95
  [Gtk.TextView, TextViewNode],
97
96
  [WebKit.WebView, WebViewNode],
97
+ [Adw.AlertDialog, AlertDialogNode],
98
98
  [Adw.Dialog, DialogNode],
99
99
  [Gtk.Window, WindowNode],
100
+ [Adw.SpinRow, SpinRowNode],
101
+ [Adw.SwitchRow, SwitchRowNode],
100
102
  [Gtk.Scale, ScaleNode],
101
103
  [Gtk.LevelBar, LevelBarNode],
102
104
  [Gtk.ScrolledWindow, ScrolledWindowNode],
package/src/types.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type * as Gtk from "@gtkx/ffi/gtk";
2
2
 
3
+ /** @internal */
3
4
  export type Container = Gtk.Widget | Gtk.Application | Gtk.EventController | Gtk.ListItem | Gtk.ListHeader;
4
5
 
5
6
  export type Props = Record<string, unknown>;
@@ -0,0 +1,58 @@
1
+ import * as GObject from "@gtkx/ffi/gobject";
2
+ import { useEffect, useState } from "react";
3
+
4
+ type ReadableKey<T> = {
5
+ [K in keyof T]: K extends string ? (T[K] extends (...args: unknown[]) => unknown ? never : K) : never;
6
+ }[keyof T];
7
+
8
+ const toKebabCase = (str: string): string =>
9
+ str.replace(/[A-Z]/g, (c, i: number) => (i === 0 ? c.toLowerCase() : `-${c.toLowerCase()}`));
10
+
11
+ /**
12
+ * Subscribes to a GObject property and returns its current value as React state.
13
+ *
14
+ * Connects to the `notify::property-name` signal on `obj` and re-renders
15
+ * whenever the property changes. The initial value is read synchronously
16
+ * at mount time. Disconnects automatically on unmount or when inputs change.
17
+ *
18
+ * When `obj` is `null` or `undefined`, the hook is inactive and returns
19
+ * `undefined`. This allows safe usage with nullable objects without
20
+ * violating React's rules of hooks.
21
+ *
22
+ * @param obj - The GObject instance to observe, or null/undefined to disable
23
+ * @param propertyName - The property name matching an ES6 accessor on the object
24
+ * @returns The current property value, or undefined when obj is null/undefined
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * const app = useApplication();
29
+ * const activeWindow = useProperty(app, "activeWindow");
30
+ * const title = useProperty(activeWindow, "title");
31
+ * ```
32
+ */
33
+ export function useProperty<T extends GObject.Object, K extends ReadableKey<T>>(
34
+ obj: T | null | undefined,
35
+ propertyName: K,
36
+ ): T[K] | undefined {
37
+ const [value, setValue] = useState<T[K] | undefined>(() => (obj ? (obj[propertyName] as T[K]) : undefined));
38
+
39
+ useEffect(() => {
40
+ if (!obj) {
41
+ setValue(undefined);
42
+ return;
43
+ }
44
+
45
+ setValue(obj[propertyName] as T[K]);
46
+
47
+ const signal = `notify::${toKebabCase(propertyName as string)}`;
48
+ const handlerId = obj.connect(signal, () => {
49
+ setValue(obj[propertyName] as T[K]);
50
+ });
51
+
52
+ return () => {
53
+ GObject.signalHandlerDisconnect(obj, handlerId);
54
+ };
55
+ }, [obj, propertyName]);
56
+
57
+ return value;
58
+ }
@@ -0,0 +1,96 @@
1
+ import * as Gio from "@gtkx/ffi/gio";
2
+ import * as GObject from "@gtkx/ffi/gobject";
3
+ import { useCallback, useEffect, useMemo, useState } from "react";
4
+
5
+ interface SettingTypeMap {
6
+ boolean: boolean;
7
+ int: number;
8
+ double: number;
9
+ string: string;
10
+ strv: string[];
11
+ }
12
+
13
+ type SettingType = keyof SettingTypeMap;
14
+
15
+ const GETTERS: Record<SettingType, string> = {
16
+ boolean: "getBoolean",
17
+ int: "getInt",
18
+ double: "getDouble",
19
+ string: "getString",
20
+ strv: "getStrv",
21
+ };
22
+
23
+ const SETTERS: Record<SettingType, string> = {
24
+ boolean: "setBoolean",
25
+ int: "setInt",
26
+ double: "setDouble",
27
+ string: "setString",
28
+ strv: "setStrv",
29
+ };
30
+
31
+ type SettingAccessor = (key: string, value?: unknown) => unknown;
32
+
33
+ function readSetting(settings: Gio.Settings, key: string, type: SettingType): unknown {
34
+ const getter = (settings as unknown as Record<string, SettingAccessor>)[GETTERS[type]] as SettingAccessor;
35
+ return getter.call(settings, key);
36
+ }
37
+
38
+ function writeSetting(settings: Gio.Settings, key: string, type: SettingType, value: unknown): void {
39
+ const setter = (settings as unknown as Record<string, SettingAccessor>)[SETTERS[type]] as SettingAccessor;
40
+ setter.call(settings, key, value);
41
+ }
42
+
43
+ /**
44
+ * Subscribes to a GSettings key and returns its current value alongside a
45
+ * setter, similar to `useState`.
46
+ *
47
+ * Creates a `Gio.Settings` instance for the given schema (stable across
48
+ * re-renders), connects to `changed::key`, and re-renders whenever the
49
+ * setting changes. The initial value is read synchronously at mount time.
50
+ * Calling the returned setter writes the new value to GSettings, which in
51
+ * turn triggers a re-render through the `changed` signal.
52
+ *
53
+ * @param schemaId - The GSettings schema ID (e.g. `"org.gnome.desktop.interface"`)
54
+ * @param key - The settings key in kebab-case (e.g. `"color-scheme"`)
55
+ * @param type - The value type, used to select the appropriate GSettings getter/setter
56
+ * @returns A `[value, setValue]` tuple kept in sync with the GSettings backend
57
+ *
58
+ * @example
59
+ * ```tsx
60
+ * const [colorScheme, setColorScheme] = useSetting("org.gnome.desktop.interface", "color-scheme", "string");
61
+ * ```
62
+ *
63
+ * @example
64
+ * ```tsx
65
+ * const [fontSize, setFontSize] = useSetting("com.example.myapp", "font-size", "int");
66
+ * ```
67
+ */
68
+ export function useSetting<T extends SettingType>(
69
+ schemaId: string,
70
+ key: string,
71
+ type: T,
72
+ ): [SettingTypeMap[T], (value: SettingTypeMap[T]) => void] {
73
+ const settings = useMemo(() => new Gio.Settings(schemaId), [schemaId]);
74
+ const [value, setValue] = useState<SettingTypeMap[T]>(() => readSetting(settings, key, type) as SettingTypeMap[T]);
75
+
76
+ useEffect(() => {
77
+ setValue(readSetting(settings, key, type) as SettingTypeMap[T]);
78
+
79
+ const handlerId = settings.connect(`changed::${key}`, () => {
80
+ setValue(readSetting(settings, key, type) as SettingTypeMap[T]);
81
+ });
82
+
83
+ return () => {
84
+ GObject.signalHandlerDisconnect(settings, handlerId);
85
+ };
86
+ }, [settings, key, type]);
87
+
88
+ const set = useCallback(
89
+ (newValue: SettingTypeMap[T]) => {
90
+ writeSetting(settings, key, type, newValue);
91
+ },
92
+ [settings, key, type],
93
+ );
94
+
95
+ return [value, set];
96
+ }