@gtkx/react 0.3.4 → 0.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 CHANGED
@@ -41,7 +41,14 @@ Edit your code and see changes instantly—no restart needed.
41
41
  ### Example
42
42
 
43
43
  ```tsx
44
- import { render, ApplicationWindow, Box, Button, Label, quit } from "@gtkx/react";
44
+ import {
45
+ render,
46
+ ApplicationWindow,
47
+ Box,
48
+ Button,
49
+ Label,
50
+ quit,
51
+ } from "@gtkx/react";
45
52
  import { Orientation } from "@gtkx/ffi/gtk";
46
53
  import { useState } from "react";
47
54
 
@@ -74,7 +81,7 @@ const primary = css`
74
81
  color: white;
75
82
  `;
76
83
 
77
- <Button label="Click me" cssClasses={[primary]} />
84
+ <Button label="Click me" cssClasses={[primary]} />;
78
85
  ```
79
86
 
80
87
  GTK also provides built-in classes like `suggested-action`, `destructive-action`, `card`, and `heading`.
@@ -90,7 +97,9 @@ afterEach(() => cleanup());
90
97
  test("increments count", async () => {
91
98
  await render(<App />);
92
99
 
93
- const button = await screen.findByRole(AccessibleRole.BUTTON, { name: "Increment" });
100
+ const button = await screen.findByRole(AccessibleRole.BUTTON, {
101
+ name: "Increment",
102
+ });
94
103
  await userEvent.click(button);
95
104
 
96
105
  await screen.findByText("Count: 1");
@@ -103,10 +112,10 @@ User events: `click`, `dblClick`, `type`, `clear`, `tab`, `selectOptions`
103
112
 
104
113
  ## Examples
105
114
 
106
- | Example | Description |
107
- | ------- | ----------- |
108
- | [gtk4-demo](examples/gtk4-demo) | Widget showcase |
109
- | [todo](examples/todo) | Todo app with tests |
115
+ | Example | Description |
116
+ | ------------------------------- | ------------------- |
117
+ | [gtk4-demo](examples/gtk4-demo) | Widget showcase |
118
+ | [todo](examples/todo) | Todo app with tests |
110
119
 
111
120
  ```bash
112
121
  cd examples/gtk4-demo && pnpm dev
@@ -114,15 +123,15 @@ cd examples/gtk4-demo && pnpm dev
114
123
 
115
124
  ## Packages
116
125
 
117
- | Package | Description |
118
- | ------- | ----------- |
119
- | [@gtkx/cli](packages/cli) | CLI with HMR dev server |
120
- | [@gtkx/react](packages/react) | React reconciler and JSX components |
121
- | [@gtkx/ffi](packages/ffi) | TypeScript bindings for GTK4/GLib/GIO |
122
- | [@gtkx/native](packages/native) | Rust native module (libffi bridge) |
123
- | [@gtkx/css](packages/css) | CSS-in-JS styling |
124
- | [@gtkx/testing](packages/testing) | Testing utilities |
125
- | [@gtkx/gir](packages/gir) | GObject Introspection parser |
126
+ | Package | Description |
127
+ | --------------------------------- | ------------------------------------- |
128
+ | [@gtkx/cli](packages/cli) | CLI with HMR dev server |
129
+ | [@gtkx/react](packages/react) | React reconciler and JSX components |
130
+ | [@gtkx/ffi](packages/ffi) | TypeScript bindings for GTK4/GLib/GIO |
131
+ | [@gtkx/native](packages/native) | Rust native module (libffi bridge) |
132
+ | [@gtkx/css](packages/css) | CSS-in-JS styling |
133
+ | [@gtkx/testing](packages/testing) | Testing utilities |
134
+ | [@gtkx/gir](packages/gir) | GObject Introspection parser |
126
135
 
127
136
  ## Requirements
128
137
 
@@ -136,7 +145,6 @@ We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
136
145
 
137
146
  - [Report a bug](https://github.com/eugeniodepalo/gtkx/issues/new?template=bug_report.md)
138
147
  - [Request a feature](https://github.com/eugeniodepalo/gtkx/issues/new?template=feature_request.md)
139
- - [Read the Code of Conduct](CODE_OF_CONDUCT.md)
140
148
 
141
149
  ## License
142
150
 
package/dist/factory.js CHANGED
@@ -13,6 +13,7 @@ import { RootNode } from "./nodes/root.js";
13
13
  import { SlotNode } from "./nodes/slot.js";
14
14
  import { StackNode, StackPageNode } from "./nodes/stack.js";
15
15
  import { TextViewNode } from "./nodes/text-view.js";
16
+ import { ToggleButtonNode } from "./nodes/toggle-button.js";
16
17
  import { WidgetNode } from "./nodes/widget.js";
17
18
  import { WindowNode } from "./nodes/window.js";
18
19
  export { ROOT_NODE_CONTAINER } from "./nodes/root.js";
@@ -33,6 +34,7 @@ const SPECIALIZED_NODES = [
33
34
  WindowNode,
34
35
  AboutDialogNode,
35
36
  TextViewNode,
37
+ ToggleButtonNode,
36
38
  ApplicationMenuNode,
37
39
  PopoverMenuRootNode,
38
40
  PopoverMenuBarNode,
package/dist/node.js CHANGED
@@ -136,9 +136,15 @@ export class Node {
136
136
  for (const { key } of signalUpdates) {
137
137
  this.disconnectSignal(this.propKeyToEventName(key));
138
138
  }
139
+ if (propertyUpdates.length > 0) {
140
+ widget.freezeNotify();
141
+ }
139
142
  for (const { key, newValue } of propertyUpdates) {
140
143
  this.setProperty(widget, key, newValue);
141
144
  }
145
+ if (propertyUpdates.length > 0) {
146
+ widget.thawNotify();
147
+ }
142
148
  for (const { key, newValue } of signalUpdates) {
143
149
  if (typeof newValue === "function") {
144
150
  this.connectSignal(widget, this.propKeyToEventName(key), newValue);
@@ -177,7 +177,11 @@ export class MenuItemNode extends Node {
177
177
  this.onActivateCallback?.();
178
178
  });
179
179
  const app = getCurrentApp();
180
- app.addAction(getInterface(this.action, Gio.Action));
180
+ const action = getInterface(this.action, Gio.Action);
181
+ if (!action) {
182
+ throw new Error("Failed to get Gio.Action interface from SimpleAction");
183
+ }
184
+ app.addAction(action);
181
185
  this.entry.action = `app.${this.actionName}`;
182
186
  if (this.currentAccels) {
183
187
  this.updateAccels(this.currentAccels);
@@ -0,0 +1,25 @@
1
+ import type * as Gtk from "@gtkx/ffi/gtk";
2
+ import type { Props } from "../factory.js";
3
+ import { Node } from "../node.js";
4
+ type ToggleButtonState = {
5
+ lastPropsActive: boolean | undefined;
6
+ };
7
+ /**
8
+ * Specialized node for GtkToggleButton that prevents signal feedback loops.
9
+ *
10
+ * When multiple ToggleButtons share the same state (controlled components),
11
+ * React syncing the `active` prop via setActive() triggers the `toggled` signal.
12
+ * This node guards against that by tracking the expected active state and
13
+ * suppressing callbacks when the signal was caused by a programmatic update.
14
+ */
15
+ export declare class ToggleButtonNode extends Node<Gtk.ToggleButton, ToggleButtonState> {
16
+ static matches(type: string): boolean;
17
+ initialize(props: Props): void;
18
+ attachToParent(parent: Node): void;
19
+ attachToParentBefore(parent: Node, before: Node): void;
20
+ detachFromParent(parent: Node): void;
21
+ protected consumedProps(): Set<string>;
22
+ updateProps(_oldProps: Props, newProps: Props): void;
23
+ protected connectSignal(widget: Gtk.Widget, eventName: string, handler: (...args: unknown[]) => unknown): void;
24
+ }
25
+ export {};
@@ -0,0 +1,77 @@
1
+ import { isChildContainer } from "../container-interfaces.js";
2
+ import { Node } from "../node.js";
3
+ /**
4
+ * Specialized node for GtkToggleButton that prevents signal feedback loops.
5
+ *
6
+ * When multiple ToggleButtons share the same state (controlled components),
7
+ * React syncing the `active` prop via setActive() triggers the `toggled` signal.
8
+ * This node guards against that by tracking the expected active state and
9
+ * suppressing callbacks when the signal was caused by a programmatic update.
10
+ */
11
+ export class ToggleButtonNode extends Node {
12
+ static matches(type) {
13
+ return type === "ToggleButton.Root";
14
+ }
15
+ initialize(props) {
16
+ this.state = { lastPropsActive: undefined };
17
+ super.initialize(props);
18
+ }
19
+ attachToParent(parent) {
20
+ if (isChildContainer(parent)) {
21
+ parent.attachChild(this.widget);
22
+ return;
23
+ }
24
+ super.attachToParent(parent);
25
+ }
26
+ attachToParentBefore(parent, before) {
27
+ if (isChildContainer(parent)) {
28
+ const beforeWidget = before.getWidget();
29
+ if (beforeWidget) {
30
+ parent.insertChildBefore(this.widget, beforeWidget);
31
+ }
32
+ else {
33
+ parent.attachChild(this.widget);
34
+ }
35
+ return;
36
+ }
37
+ super.attachToParentBefore(parent, before);
38
+ }
39
+ detachFromParent(parent) {
40
+ if (isChildContainer(parent)) {
41
+ parent.detachChild(this.widget);
42
+ return;
43
+ }
44
+ super.detachFromParent(parent);
45
+ }
46
+ consumedProps() {
47
+ return new Set(["children", "active"]);
48
+ }
49
+ updateProps(_oldProps, newProps) {
50
+ const widget = this.getWidget();
51
+ if (!widget) {
52
+ super.updateProps(_oldProps, newProps);
53
+ return;
54
+ }
55
+ const newActive = newProps.active;
56
+ if (typeof newActive === "boolean") {
57
+ this.state.lastPropsActive = newActive;
58
+ if (widget.getActive() !== newActive) {
59
+ widget.setActive(newActive);
60
+ }
61
+ }
62
+ super.updateProps(_oldProps, newProps);
63
+ }
64
+ connectSignal(widget, eventName, handler) {
65
+ if (eventName === "toggled") {
66
+ const wrappedHandler = (...args) => {
67
+ if (this.widget?.getActive() === this.state.lastPropsActive) {
68
+ return;
69
+ }
70
+ return handler(...args);
71
+ };
72
+ super.connectSignal(widget, eventName, wrappedHandler);
73
+ return;
74
+ }
75
+ super.connectSignal(widget, eventName, handler);
76
+ }
77
+ }
@@ -1,3 +1,4 @@
1
+ import { beginBatch, endBatch } from "@gtkx/ffi";
1
2
  import React from "react";
2
3
  import ReactReconciler from "react-reconciler";
3
4
  import { beginCommit, endCommit } from "./batch.js";
@@ -56,11 +57,13 @@ class Reconciler {
56
57
  parent.insertBefore(child, beforeChild);
57
58
  },
58
59
  prepareForCommit: () => {
60
+ beginBatch();
59
61
  beginCommit();
60
62
  return null;
61
63
  },
62
64
  resetAfterCommit: () => {
63
65
  endCommit();
66
+ endBatch();
64
67
  },
65
68
  commitTextUpdate: (textInstance, oldText, newText) => {
66
69
  textInstance.updateProps({ label: oldText }, { label: newText });
package/dist/render.js CHANGED
@@ -24,8 +24,10 @@ export let container = null;
24
24
  export const render = (element, appId, flags) => {
25
25
  start(appId, flags);
26
26
  const instance = reconciler.getInstance();
27
- container = instance.createContainer(ROOT_NODE_CONTAINER, 0, null, false, null, "", (error, info) => {
28
- console.error("Uncaught error in GTKX application:", error, info);
29
- }, (_error, _info) => { }, (_error, _info) => { }, () => { }, null);
27
+ container = instance.createContainer(ROOT_NODE_CONTAINER, 0, null, false, null, "", (error) => {
28
+ throw error;
29
+ }, (error) => {
30
+ console.error("Error caught by ErrorBoundary:", error);
31
+ }, () => { }, () => { }, null);
30
32
  instance.updateContainer(element, container, null, () => { });
31
33
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtkx/react",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "Build GTK4 desktop applications with React and TypeScript",
5
5
  "keywords": [
6
6
  "gtk",
@@ -36,10 +36,10 @@
36
36
  ],
37
37
  "dependencies": {
38
38
  "react-reconciler": "0.33.0",
39
- "@gtkx/ffi": "0.3.4"
39
+ "@gtkx/ffi": "0.4.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@gtkx/gir": "0.3.4"
42
+ "@gtkx/gir": "0.4.0"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "react": "^19"