@gtkx/react 0.3.2 → 0.3.4
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 +53 -154
- package/dist/batch.d.ts +12 -0
- package/dist/batch.js +12 -0
- package/dist/factory.d.ts +7 -0
- package/dist/factory.js +10 -7
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/nodes/column-view.js +9 -5
- package/dist/nodes/dropdown.d.ts +18 -3
- package/dist/nodes/dropdown.js +13 -12
- package/dist/nodes/list.js +6 -3
- package/dist/nodes/menu.js +2 -2
- package/dist/portal.js +2 -0
- package/dist/predicates.d.ts +9 -0
- package/dist/predicates.js +9 -0
- package/dist/reconciler.d.ts +5 -0
- package/dist/reconciler.js +5 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -5,239 +5,138 @@
|
|
|
5
5
|
<h1 align="center">GTKX</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>Build native GTK4 desktop
|
|
8
|
+
<strong>Build native GTK4 desktop apps with React</strong>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://eugeniodepalo.github.io/gtkx">Documentation</a> ·
|
|
13
13
|
<a href="#quick-start">Quick Start</a> ·
|
|
14
|
-
<a href="#examples">Examples</a>
|
|
14
|
+
<a href="#examples">Examples</a> ·
|
|
15
|
+
<a href="#contributing">Contributing</a>
|
|
15
16
|
</p>
|
|
16
17
|
|
|
17
18
|
---
|
|
18
19
|
|
|
19
|
-
GTKX
|
|
20
|
+
GTKX lets you build native Linux desktop applications using React and TypeScript. Write familiar React code that renders as native GTK4 widgets—no Electron, no web views.
|
|
20
21
|
|
|
21
22
|
## Features
|
|
22
23
|
|
|
23
|
-
- **React
|
|
24
|
-
- **Hot
|
|
25
|
-
- **Native
|
|
26
|
-
- **CLI
|
|
27
|
-
- **CSS-in-JS
|
|
28
|
-
- **Testing
|
|
24
|
+
- **React** — Hooks, state, props, and components you already know
|
|
25
|
+
- **Hot Reload** — Edit code and see changes instantly via Vite
|
|
26
|
+
- **Native** — Direct FFI bindings to GTK4 via Rust and libffi
|
|
27
|
+
- **CLI** — `npx @gtkx/cli@latest create` scaffolds a ready-to-go project
|
|
28
|
+
- **CSS-in-JS** — Emotion-style `css` template literals for GTK styling
|
|
29
|
+
- **Testing** — Testing Library-style `screen`, `userEvent`, and queries
|
|
29
30
|
|
|
30
31
|
## Quick Start
|
|
31
32
|
|
|
32
|
-
Create a new GTKX app with a single command:
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
npx @gtkx/cli@latest create
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
This launches an interactive wizard that sets up your project with TypeScript, your preferred package manager, and optional testing support.
|
|
39
|
-
|
|
40
|
-
You can also pass options directly:
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
npx @gtkx/cli@latest create my-app --app-id com.example.myapp --pm pnpm --testing vitest
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Then start developing with HMR:
|
|
47
|
-
|
|
48
33
|
```bash
|
|
34
|
+
npx @gtkx/cli@latest create my-app
|
|
49
35
|
cd my-app
|
|
50
36
|
npm run dev
|
|
51
37
|
```
|
|
52
38
|
|
|
53
|
-
Edit your code and see changes instantly
|
|
54
|
-
|
|
55
|
-
### Manual Setup
|
|
39
|
+
Edit your code and see changes instantly—no restart needed.
|
|
56
40
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
npm install @gtkx/cli @gtkx/react @gtkx/ffi react
|
|
61
|
-
npm install -D @types/react typescript
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Create your first app:
|
|
41
|
+
### Example
|
|
65
42
|
|
|
66
43
|
```tsx
|
|
44
|
+
import { render, ApplicationWindow, Box, Button, Label, quit } from "@gtkx/react";
|
|
45
|
+
import { Orientation } from "@gtkx/ffi/gtk";
|
|
67
46
|
import { useState } from "react";
|
|
68
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
69
|
-
import { ApplicationWindow, Box, Button, Label, quit } from "@gtkx/react";
|
|
70
47
|
|
|
71
|
-
|
|
48
|
+
const App = () => {
|
|
72
49
|
const [count, setCount] = useState(0);
|
|
73
50
|
|
|
74
51
|
return (
|
|
75
|
-
<ApplicationWindow
|
|
76
|
-
|
|
77
|
-
defaultWidth={400}
|
|
78
|
-
defaultHeight={300}
|
|
79
|
-
onCloseRequest={quit}
|
|
80
|
-
>
|
|
81
|
-
<Box
|
|
82
|
-
orientation={Gtk.Orientation.VERTICAL}
|
|
83
|
-
spacing={12}
|
|
84
|
-
marginStart={20}
|
|
85
|
-
marginEnd={20}
|
|
86
|
-
marginTop={20}
|
|
87
|
-
marginBottom={20}
|
|
88
|
-
>
|
|
52
|
+
<ApplicationWindow title="Counter" onCloseRequest={quit}>
|
|
53
|
+
<Box orientation={Orientation.VERTICAL} spacing={12} margin={20}>
|
|
89
54
|
<Label.Root label={`Count: ${count}`} />
|
|
90
55
|
<Button label="Increment" onClicked={() => setCount((c) => c + 1)} />
|
|
91
56
|
</Box>
|
|
92
57
|
</ApplicationWindow>
|
|
93
58
|
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export const appId = "org.example.MyApp";
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
```tsx
|
|
100
|
-
import { render } from "@gtkx/react";
|
|
101
|
-
import App, { appId } from "./app.js";
|
|
102
|
-
|
|
103
|
-
render(<App />, appId);
|
|
104
|
-
```
|
|
59
|
+
};
|
|
105
60
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
```bash
|
|
109
|
-
npx gtkx dev src/app.tsx
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
Or without HMR (production):
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
npx tsc -b && node dist/index.js
|
|
61
|
+
render(<App />, "org.example.Counter");
|
|
116
62
|
```
|
|
117
63
|
|
|
118
64
|
## Styling
|
|
119
65
|
|
|
120
|
-
Use `@gtkx/css` for CSS-in-JS styling:
|
|
121
|
-
|
|
122
66
|
```tsx
|
|
123
67
|
import { css } from "@gtkx/css";
|
|
124
68
|
import { Button } from "@gtkx/react";
|
|
125
69
|
|
|
126
|
-
const
|
|
70
|
+
const primary = css`
|
|
127
71
|
padding: 16px 32px;
|
|
128
72
|
border-radius: 24px;
|
|
129
73
|
background: linear-gradient(135deg, #3584e4, #9141ac);
|
|
130
74
|
color: white;
|
|
131
|
-
font-weight: bold;
|
|
132
75
|
`;
|
|
133
76
|
|
|
134
|
-
|
|
77
|
+
<Button label="Click me" cssClasses={[primary]} />
|
|
135
78
|
```
|
|
136
79
|
|
|
137
|
-
GTK also provides built-in
|
|
80
|
+
GTK also provides built-in classes like `suggested-action`, `destructive-action`, `card`, and `heading`.
|
|
138
81
|
|
|
139
82
|
## Testing
|
|
140
83
|
|
|
141
|
-
Use `@gtkx/testing` for Testing Library-style component tests:
|
|
142
|
-
|
|
143
84
|
```tsx
|
|
144
|
-
import { cleanup, render, screen, userEvent
|
|
85
|
+
import { cleanup, render, screen, userEvent } from "@gtkx/testing";
|
|
145
86
|
import { AccessibleRole } from "@gtkx/ffi/gtk";
|
|
146
|
-
import { App } from "./app.js";
|
|
147
87
|
|
|
148
|
-
afterEach(
|
|
149
|
-
await cleanup();
|
|
150
|
-
});
|
|
88
|
+
afterEach(() => cleanup());
|
|
151
89
|
|
|
152
|
-
test("increments count
|
|
90
|
+
test("increments count", async () => {
|
|
153
91
|
await render(<App />);
|
|
154
92
|
|
|
155
|
-
const button = await screen.findByRole(AccessibleRole.BUTTON, {
|
|
156
|
-
name: "Increment",
|
|
157
|
-
});
|
|
93
|
+
const button = await screen.findByRole(AccessibleRole.BUTTON, { name: "Increment" });
|
|
158
94
|
await userEvent.click(button);
|
|
159
95
|
|
|
160
96
|
await screen.findByText("Count: 1");
|
|
161
97
|
});
|
|
162
|
-
|
|
163
|
-
test("can also use fireEvent for low-level signals", async () => {
|
|
164
|
-
await render(<App />);
|
|
165
|
-
|
|
166
|
-
const button = await screen.findByRole(AccessibleRole.BUTTON, {
|
|
167
|
-
name: "Increment",
|
|
168
|
-
});
|
|
169
|
-
fireEvent(button, "clicked");
|
|
170
|
-
|
|
171
|
-
await screen.findByText("Count: 1");
|
|
172
|
-
});
|
|
173
98
|
```
|
|
174
99
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
**Queries** - Find elements in the rendered tree (all async):
|
|
178
|
-
|
|
179
|
-
- `findBy*` / `findAllBy*` - Waits for element to appear
|
|
180
|
-
|
|
181
|
-
Query types: `ByRole`, `ByText`, `ByLabelText`, `ByTestId`
|
|
182
|
-
|
|
183
|
-
**User Interactions**:
|
|
100
|
+
Queries: `findByRole`, `findByText`, `findByLabelText`, `findByTestId`
|
|
184
101
|
|
|
185
|
-
|
|
186
|
-
- `userEvent.dblClick(element)` - Simulate double click
|
|
187
|
-
- `userEvent.activate(element)` - Activate element (e.g., press Enter in input)
|
|
188
|
-
- `userEvent.type(element, text)` - Type text into input
|
|
189
|
-
- `userEvent.clear(element)` - Clear input text
|
|
190
|
-
- `userEvent.tab(element, options?)` - Simulate Tab navigation
|
|
191
|
-
- `userEvent.selectOptions(element, values)` - Select options in ComboBox/ListBox
|
|
192
|
-
- `userEvent.deselectOptions(element, values)` - Deselect options in ListBox
|
|
193
|
-
|
|
194
|
-
**Low-level Events**:
|
|
195
|
-
|
|
196
|
-
- `fireEvent(element, signalName, ...args)` - Emit any GTK signal with optional arguments
|
|
197
|
-
|
|
198
|
-
**Utilities**:
|
|
199
|
-
|
|
200
|
-
- `waitFor(callback)` - Wait for condition
|
|
201
|
-
- `waitForElementToBeRemoved(element)` - Wait for element removal
|
|
102
|
+
User events: `click`, `dblClick`, `type`, `clear`, `tab`, `selectOptions`
|
|
202
103
|
|
|
203
104
|
## Examples
|
|
204
105
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
106
|
+
| Example | Description |
|
|
107
|
+
| ------- | ----------- |
|
|
108
|
+
| [gtk4-demo](examples/gtk4-demo) | Widget showcase |
|
|
109
|
+
| [todo](examples/todo) | Todo app with tests |
|
|
208
110
|
|
|
209
111
|
```bash
|
|
210
|
-
cd examples/gtk4-demo
|
|
211
|
-
pnpm dev
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
### Todo App
|
|
215
|
-
|
|
216
|
-
A todo app demonstrating `@gtkx/testing` with realistic component tests:
|
|
217
|
-
|
|
218
|
-
```bash
|
|
219
|
-
cd examples/todo
|
|
220
|
-
pnpm dev
|
|
221
|
-
pnpm test
|
|
112
|
+
cd examples/gtk4-demo && pnpm dev
|
|
222
113
|
```
|
|
223
114
|
|
|
224
115
|
## Packages
|
|
225
116
|
|
|
226
|
-
| Package
|
|
227
|
-
|
|
|
228
|
-
| [@gtkx/cli](packages/cli)
|
|
229
|
-
| [@gtkx/react](packages/react)
|
|
230
|
-
| [@gtkx/ffi](packages/ffi)
|
|
231
|
-
| [@gtkx/native](packages/native)
|
|
232
|
-
| [@gtkx/css](packages/css)
|
|
233
|
-
| [@gtkx/testing](packages/testing) | Testing utilities
|
|
234
|
-
| [@gtkx/gir](packages/gir)
|
|
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 |
|
|
235
126
|
|
|
236
127
|
## Requirements
|
|
237
128
|
|
|
238
129
|
- Node.js 20+
|
|
239
|
-
- GTK4
|
|
240
|
-
- Linux
|
|
130
|
+
- GTK4 (`gtk4-devel` on Fedora, `libgtk-4-dev` on Ubuntu)
|
|
131
|
+
- Linux
|
|
132
|
+
|
|
133
|
+
## Contributing
|
|
134
|
+
|
|
135
|
+
We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
136
|
+
|
|
137
|
+
- [Report a bug](https://github.com/eugeniodepalo/gtkx/issues/new?template=bug_report.md)
|
|
138
|
+
- [Request a feature](https://github.com/eugeniodepalo/gtkx/issues/new?template=feature_request.md)
|
|
139
|
+
- [Read the Code of Conduct](CODE_OF_CONDUCT.md)
|
|
241
140
|
|
|
242
141
|
## License
|
|
243
142
|
|
package/dist/batch.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
type FlushCallback = () => void;
|
|
2
|
+
/**
|
|
3
|
+
* Marks the beginning of a React reconciler commit phase.
|
|
4
|
+
* While in commit, flush callbacks are deferred until endCommit is called.
|
|
5
|
+
*/
|
|
2
6
|
export declare const beginCommit: () => void;
|
|
7
|
+
/**
|
|
8
|
+
* Marks the end of a React reconciler commit phase.
|
|
9
|
+
* Executes all pending flush callbacks that were deferred during the commit.
|
|
10
|
+
*/
|
|
3
11
|
export declare const endCommit: () => void;
|
|
12
|
+
/**
|
|
13
|
+
* Schedules a callback to be executed, deferring it if currently in a commit phase.
|
|
14
|
+
* This ensures GTK state updates happen after React has finished its batch of changes.
|
|
15
|
+
*/
|
|
4
16
|
export declare const scheduleFlush: (callback: FlushCallback) => void;
|
|
5
17
|
export {};
|
package/dist/batch.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
const pendingFlushes = new Set();
|
|
2
2
|
let inCommit = false;
|
|
3
|
+
/**
|
|
4
|
+
* Marks the beginning of a React reconciler commit phase.
|
|
5
|
+
* While in commit, flush callbacks are deferred until endCommit is called.
|
|
6
|
+
*/
|
|
3
7
|
export const beginCommit = () => {
|
|
4
8
|
inCommit = true;
|
|
5
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* Marks the end of a React reconciler commit phase.
|
|
12
|
+
* Executes all pending flush callbacks that were deferred during the commit.
|
|
13
|
+
*/
|
|
6
14
|
export const endCommit = () => {
|
|
7
15
|
inCommit = false;
|
|
8
16
|
if (pendingFlushes.size > 0) {
|
|
@@ -15,6 +23,10 @@ export const endCommit = () => {
|
|
|
15
23
|
});
|
|
16
24
|
}
|
|
17
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* Schedules a callback to be executed, deferring it if currently in a commit phase.
|
|
28
|
+
* This ensures GTK state updates happen after React has finished its batch of changes.
|
|
29
|
+
*/
|
|
18
30
|
export const scheduleFlush = (callback) => {
|
|
19
31
|
if (inCommit) {
|
|
20
32
|
pendingFlushes.add(callback);
|
package/dist/factory.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
2
2
|
import type { Node } from "./node.js";
|
|
3
3
|
import { type ROOT_NODE_CONTAINER } from "./nodes/root.js";
|
|
4
|
+
/**
|
|
5
|
+
* Generic props type for React component properties passed to GTK nodes.
|
|
6
|
+
*/
|
|
4
7
|
export type Props = Record<string, unknown>;
|
|
5
8
|
export { ROOT_NODE_CONTAINER } from "./nodes/root.js";
|
|
9
|
+
/**
|
|
10
|
+
* Creates a Node instance for the given JSX element type.
|
|
11
|
+
* Matches the type against registered node classes and initializes with props.
|
|
12
|
+
*/
|
|
6
13
|
export declare const createNode: (type: string, props: Props, existingWidget?: Gtk.Widget | typeof ROOT_NODE_CONTAINER) => Node;
|
package/dist/factory.js
CHANGED
|
@@ -16,9 +16,7 @@ import { TextViewNode } from "./nodes/text-view.js";
|
|
|
16
16
|
import { WidgetNode } from "./nodes/widget.js";
|
|
17
17
|
import { WindowNode } from "./nodes/window.js";
|
|
18
18
|
export { ROOT_NODE_CONTAINER } from "./nodes/root.js";
|
|
19
|
-
const
|
|
20
|
-
RootNode,
|
|
21
|
-
// Virtual nodes (no widget)
|
|
19
|
+
const VIRTUAL_NODES = [
|
|
22
20
|
ColumnViewColumnNode,
|
|
23
21
|
ColumnViewItemNode,
|
|
24
22
|
ListItemNode,
|
|
@@ -30,14 +28,16 @@ const NODE_CLASSES = [
|
|
|
30
28
|
MenuSectionNode,
|
|
31
29
|
MenuSubmenuNode,
|
|
32
30
|
SlotNode,
|
|
33
|
-
|
|
31
|
+
];
|
|
32
|
+
const SPECIALIZED_NODES = [
|
|
34
33
|
WindowNode,
|
|
35
34
|
AboutDialogNode,
|
|
36
35
|
TextViewNode,
|
|
37
36
|
ApplicationMenuNode,
|
|
38
37
|
PopoverMenuRootNode,
|
|
39
38
|
PopoverMenuBarNode,
|
|
40
|
-
|
|
39
|
+
];
|
|
40
|
+
const CONTAINER_NODES = [
|
|
41
41
|
ActionBarNode,
|
|
42
42
|
FlowBoxNode,
|
|
43
43
|
ListBoxNode,
|
|
@@ -48,9 +48,12 @@ const NODE_CLASSES = [
|
|
|
48
48
|
ListViewNode,
|
|
49
49
|
NotebookNode,
|
|
50
50
|
StackNode,
|
|
51
|
-
// Catch-all (must be last)
|
|
52
|
-
WidgetNode,
|
|
53
51
|
];
|
|
52
|
+
const NODE_CLASSES = [RootNode, ...VIRTUAL_NODES, ...SPECIALIZED_NODES, ...CONTAINER_NODES, WidgetNode];
|
|
53
|
+
/**
|
|
54
|
+
* Creates a Node instance for the given JSX element type.
|
|
55
|
+
* Matches the type against registered node classes and initializes with props.
|
|
56
|
+
*/
|
|
54
57
|
export const createNode = (type, props, existingWidget) => {
|
|
55
58
|
for (const NodeClass of NODE_CLASSES) {
|
|
56
59
|
if (NodeClass.matches(type, existingWidget)) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export { createRef } from "@gtkx/ffi";
|
|
2
|
+
/**
|
|
3
|
+
* @private Internal symbol used to identify the root container node.
|
|
4
|
+
* This is an internal API used only by @gtkx/testing. Do not use directly.
|
|
5
|
+
*/
|
|
6
|
+
export { ROOT_NODE_CONTAINER } from "./factory.js";
|
|
2
7
|
export * from "./generated/jsx.js";
|
|
3
8
|
export { createPortal } from "./portal.js";
|
|
4
9
|
export { reconciler } from "./reconciler.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export { createRef } from "@gtkx/ffi";
|
|
2
|
+
/**
|
|
3
|
+
* @private Internal symbol used to identify the root container node.
|
|
4
|
+
* This is an internal API used only by @gtkx/testing. Do not use directly.
|
|
5
|
+
*/
|
|
6
|
+
export { ROOT_NODE_CONTAINER } from "./factory.js";
|
|
2
7
|
export * from "./generated/jsx.js";
|
|
3
8
|
export { createPortal } from "./portal.js";
|
|
4
9
|
export { reconciler } from "./reconciler.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { getObject, getObjectId } from "@gtkx/ffi";
|
|
1
|
+
import { getInterface, getObject, getObjectId } from "@gtkx/ffi";
|
|
2
|
+
import * as Gio from "@gtkx/ffi/gio";
|
|
2
3
|
import * as GObject from "@gtkx/ffi/gobject";
|
|
3
4
|
import * as Gtk from "@gtkx/ffi/gtk";
|
|
4
5
|
import { scheduleFlush } from "../batch.js";
|
|
@@ -11,7 +12,9 @@ export class ColumnViewNode extends Node {
|
|
|
11
12
|
return type === "ColumnView.Root";
|
|
12
13
|
}
|
|
13
14
|
initialize(props) {
|
|
14
|
-
//
|
|
15
|
+
// State must be initialized before super.initialize() since updateProps accesses this.state.
|
|
16
|
+
// GTK objects can't be created yet because this.widget doesn't exist until super.initialize().
|
|
17
|
+
// The null placeholders are immediately replaced after super.initialize() completes.
|
|
15
18
|
this.state = {
|
|
16
19
|
stringList: null,
|
|
17
20
|
selectionModel: null,
|
|
@@ -30,9 +33,9 @@ export class ColumnViewNode extends Node {
|
|
|
30
33
|
};
|
|
31
34
|
super.initialize(props);
|
|
32
35
|
const stringList = new Gtk.StringList([]);
|
|
33
|
-
const sortListModel = new Gtk.SortListModel(stringList, this.widget.getSorter());
|
|
36
|
+
const sortListModel = new Gtk.SortListModel(getInterface(stringList, Gio.ListModel), this.widget.getSorter());
|
|
34
37
|
sortListModel.setIncremental(true);
|
|
35
|
-
const selectionModel = new Gtk.SingleSelection(sortListModel);
|
|
38
|
+
const selectionModel = new Gtk.SingleSelection(getInterface(sortListModel, Gio.ListModel));
|
|
36
39
|
this.widget.setModel(selectionModel);
|
|
37
40
|
this.state.stringList = stringList;
|
|
38
41
|
this.state.sortListModel = sortListModel;
|
|
@@ -218,7 +221,8 @@ export class ColumnViewColumnNode extends Node {
|
|
|
218
221
|
}
|
|
219
222
|
columnView = null;
|
|
220
223
|
initialize(props) {
|
|
221
|
-
//
|
|
224
|
+
// Unlike ColumnViewNode, we can create GTK objects before super.initialize() here
|
|
225
|
+
// since this is a virtual node (no widget created by parent class).
|
|
222
226
|
const factory = new Gtk.SignalListItemFactory();
|
|
223
227
|
const column = new Gtk.ColumnViewColumn(props.title, factory);
|
|
224
228
|
const columnId = props.id ?? null;
|
package/dist/nodes/dropdown.d.ts
CHANGED
|
@@ -2,10 +2,24 @@ import * as Gtk from "@gtkx/ffi/gtk";
|
|
|
2
2
|
import { type ItemContainer } from "../container-interfaces.js";
|
|
3
3
|
import type { Props } from "../factory.js";
|
|
4
4
|
import { Node } from "../node.js";
|
|
5
|
-
|
|
5
|
+
type ItemLabelFn = (item: unknown) => string;
|
|
6
|
+
interface DropDownState {
|
|
7
|
+
store: DropDownStore;
|
|
8
|
+
onSelectionChanged?: (item: unknown, index: number) => void;
|
|
9
|
+
}
|
|
10
|
+
declare class DropDownStore {
|
|
11
|
+
private stringList;
|
|
12
|
+
private items;
|
|
13
|
+
private labelFn;
|
|
14
|
+
constructor(labelFn: ItemLabelFn);
|
|
15
|
+
getModel(): Gtk.StringList;
|
|
16
|
+
append(item: unknown): void;
|
|
17
|
+
remove(item: unknown): void;
|
|
18
|
+
getItem(index: number): unknown;
|
|
19
|
+
get length(): number;
|
|
20
|
+
}
|
|
21
|
+
export declare class DropDownNode extends Node<Gtk.DropDown, DropDownState> implements ItemContainer<unknown> {
|
|
6
22
|
static matches(type: string): boolean;
|
|
7
|
-
private store;
|
|
8
|
-
private onSelectionChanged?;
|
|
9
23
|
initialize(props: Props): void;
|
|
10
24
|
addItem(item: unknown): void;
|
|
11
25
|
insertItemBefore(item: unknown, _beforeItem: unknown): void;
|
|
@@ -22,3 +36,4 @@ export declare class DropDownItemNode extends Node<never> {
|
|
|
22
36
|
attachToParent(parent: Node): void;
|
|
23
37
|
detachFromParent(parent: Node): void;
|
|
24
38
|
}
|
|
39
|
+
export {};
|
package/dist/nodes/dropdown.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getInterface } from "@gtkx/ffi";
|
|
2
|
+
import * as Gio from "@gtkx/ffi/gio";
|
|
1
3
|
import * as Gtk from "@gtkx/ffi/gtk";
|
|
2
4
|
import { isItemContainer } from "../container-interfaces.js";
|
|
3
5
|
import { Node } from "../node.js";
|
|
@@ -35,31 +37,30 @@ export class DropDownNode extends Node {
|
|
|
35
37
|
static matches(type) {
|
|
36
38
|
return type === "DropDown.Root";
|
|
37
39
|
}
|
|
38
|
-
store;
|
|
39
|
-
onSelectionChanged;
|
|
40
40
|
initialize(props) {
|
|
41
|
-
super.initialize(props);
|
|
42
41
|
const labelFn = props.itemLabel ?? ((item) => String(item));
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
this.
|
|
46
|
-
|
|
42
|
+
const store = new DropDownStore(labelFn);
|
|
43
|
+
const onSelectionChanged = props.onSelectionChanged;
|
|
44
|
+
this.state = { store, onSelectionChanged };
|
|
45
|
+
super.initialize(props);
|
|
46
|
+
this.widget.setModel(getInterface(store.getModel(), Gio.ListModel));
|
|
47
|
+
if (onSelectionChanged) {
|
|
47
48
|
const handler = () => {
|
|
48
49
|
const index = this.widget.getSelected();
|
|
49
|
-
const item = this.store.getItem(index);
|
|
50
|
-
this.onSelectionChanged?.(item, index);
|
|
50
|
+
const item = this.state.store.getItem(index);
|
|
51
|
+
this.state.onSelectionChanged?.(item, index);
|
|
51
52
|
};
|
|
52
53
|
this.connectSignal(this.widget, "notify::selected", handler);
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
addItem(item) {
|
|
56
|
-
this.store.append(item);
|
|
57
|
+
this.state.store.append(item);
|
|
57
58
|
}
|
|
58
59
|
insertItemBefore(item, _beforeItem) {
|
|
59
60
|
this.addItem(item);
|
|
60
61
|
}
|
|
61
62
|
removeItem(item) {
|
|
62
|
-
this.store.remove(item);
|
|
63
|
+
this.state.store.remove(item);
|
|
63
64
|
}
|
|
64
65
|
consumedProps() {
|
|
65
66
|
const consumed = super.consumedProps();
|
|
@@ -69,7 +70,7 @@ export class DropDownNode extends Node {
|
|
|
69
70
|
}
|
|
70
71
|
updateProps(oldProps, newProps) {
|
|
71
72
|
if (oldProps.onSelectionChanged !== newProps.onSelectionChanged) {
|
|
72
|
-
this.onSelectionChanged = newProps.onSelectionChanged;
|
|
73
|
+
this.state.onSelectionChanged = newProps.onSelectionChanged;
|
|
73
74
|
}
|
|
74
75
|
super.updateProps(oldProps, newProps);
|
|
75
76
|
}
|
package/dist/nodes/list.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { getObject, getObjectId } from "@gtkx/ffi";
|
|
1
|
+
import { getInterface, getObject, getObjectId } from "@gtkx/ffi";
|
|
2
|
+
import * as Gio from "@gtkx/ffi/gio";
|
|
2
3
|
import * as Gtk from "@gtkx/ffi/gtk";
|
|
3
4
|
import { scheduleFlush } from "../batch.js";
|
|
4
5
|
import { isItemContainer } from "../container-interfaces.js";
|
|
@@ -10,7 +11,9 @@ export class ListViewNode extends Node {
|
|
|
10
11
|
return type === "ListView.Root" || type === "GridView.Root";
|
|
11
12
|
}
|
|
12
13
|
initialize(props) {
|
|
13
|
-
//
|
|
14
|
+
// State must be initialized before super.initialize() since updateProps accesses this.state.
|
|
15
|
+
// GTK objects can't be created yet because this.widget doesn't exist until super.initialize().
|
|
16
|
+
// The null placeholders are immediately replaced after super.initialize() completes.
|
|
14
17
|
this.state = {
|
|
15
18
|
stringList: null,
|
|
16
19
|
selectionModel: null,
|
|
@@ -22,7 +25,7 @@ export class ListViewNode extends Node {
|
|
|
22
25
|
};
|
|
23
26
|
super.initialize(props);
|
|
24
27
|
const stringList = new Gtk.StringList([]);
|
|
25
|
-
const selectionModel = new Gtk.SingleSelection(stringList);
|
|
28
|
+
const selectionModel = new Gtk.SingleSelection(getInterface(stringList, Gio.ListModel));
|
|
26
29
|
const factory = new Gtk.SignalListItemFactory();
|
|
27
30
|
this.state.stringList = stringList;
|
|
28
31
|
this.state.selectionModel = selectionModel;
|
package/dist/nodes/menu.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getCurrentApp } from "@gtkx/ffi";
|
|
1
|
+
import { getCurrentApp, getInterface } from "@gtkx/ffi";
|
|
2
2
|
import * as Gio from "@gtkx/ffi/gio";
|
|
3
3
|
import * as GObject from "@gtkx/ffi/gobject";
|
|
4
4
|
import * as Gtk from "@gtkx/ffi/gtk";
|
|
@@ -177,7 +177,7 @@ export class MenuItemNode extends Node {
|
|
|
177
177
|
this.onActivateCallback?.();
|
|
178
178
|
});
|
|
179
179
|
const app = getCurrentApp();
|
|
180
|
-
app.addAction(this.action);
|
|
180
|
+
app.addAction(getInterface(this.action, Gio.Action));
|
|
181
181
|
this.entry.action = `app.${this.actionName}`;
|
|
182
182
|
if (this.currentAccels) {
|
|
183
183
|
this.updateAccels(this.currentAccels);
|
package/dist/portal.js
CHANGED
|
@@ -18,6 +18,8 @@ import { ROOT_NODE_CONTAINER } from "./factory.js";
|
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
20
|
export const createPortal = (children, container, key) => {
|
|
21
|
+
// ReactPortal is an opaque type but we need to construct it manually for custom reconcilers.
|
|
22
|
+
// The shape matches React's internal portal representation.
|
|
21
23
|
return {
|
|
22
24
|
$$typeof: Symbol.for("react.portal"),
|
|
23
25
|
key: key ?? null,
|
package/dist/predicates.d.ts
CHANGED
|
@@ -8,7 +8,16 @@ interface SingleChild extends Gtk.Widget {
|
|
|
8
8
|
interface Removable extends Gtk.Widget {
|
|
9
9
|
remove(child: unknown): void;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Type guard that checks if a GTK widget supports appending children via an append method.
|
|
13
|
+
*/
|
|
11
14
|
export declare const isAppendable: (widget: Gtk.Widget) => widget is Appendable;
|
|
15
|
+
/**
|
|
16
|
+
* Type guard that checks if a GTK widget supports a single child via setChild method.
|
|
17
|
+
*/
|
|
12
18
|
export declare const isSingleChild: (widget: Gtk.Widget) => widget is SingleChild;
|
|
19
|
+
/**
|
|
20
|
+
* Type guard that checks if a GTK widget supports removing children via a remove method.
|
|
21
|
+
*/
|
|
13
22
|
export declare const isRemovable: (widget: Gtk.Widget) => widget is Removable;
|
|
14
23
|
export {};
|
package/dist/predicates.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guard that checks if a GTK widget supports appending children via an append method.
|
|
3
|
+
*/
|
|
1
4
|
export const isAppendable = (widget) => "append" in widget && typeof widget.append === "function";
|
|
5
|
+
/**
|
|
6
|
+
* Type guard that checks if a GTK widget supports a single child via setChild method.
|
|
7
|
+
*/
|
|
2
8
|
export const isSingleChild = (widget) => "setChild" in widget && typeof widget.setChild === "function";
|
|
9
|
+
/**
|
|
10
|
+
* Type guard that checks if a GTK widget supports removing children via a remove method.
|
|
11
|
+
*/
|
|
3
12
|
export const isRemovable = (widget) => "remove" in widget && typeof widget.remove === "function";
|
package/dist/reconciler.d.ts
CHANGED
|
@@ -21,6 +21,11 @@ declare class Reconciler {
|
|
|
21
21
|
*/
|
|
22
22
|
getInstance(): ReconcilerInstance;
|
|
23
23
|
private createHostConfig;
|
|
24
|
+
/**
|
|
25
|
+
* Creates a React context compatible with the reconciler's internal type.
|
|
26
|
+
* Cast is necessary because React.Context and ReactReconciler.ReactContext
|
|
27
|
+
* are structurally identical at runtime but declared as separate types.
|
|
28
|
+
*/
|
|
24
29
|
private createReconcilerContext;
|
|
25
30
|
private createNodeFromContainer;
|
|
26
31
|
}
|
package/dist/reconciler.js
CHANGED
|
@@ -99,6 +99,11 @@ class Reconciler {
|
|
|
99
99
|
waitForCommitToBeReady: () => null,
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Creates a React context compatible with the reconciler's internal type.
|
|
104
|
+
* Cast is necessary because React.Context and ReactReconciler.ReactContext
|
|
105
|
+
* are structurally identical at runtime but declared as separate types.
|
|
106
|
+
*/
|
|
102
107
|
createReconcilerContext(value) {
|
|
103
108
|
const context = React.createContext(value);
|
|
104
109
|
return context;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtkx/react",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
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.
|
|
39
|
+
"@gtkx/ffi": "0.3.4"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@gtkx/gir": "0.3.
|
|
42
|
+
"@gtkx/gir": "0.3.4"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"react": "^19"
|