@gtkx/ffi 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -157
- package/dist/codegen/ffi-generator.js +4 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -5,242 +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
|
|
67
|
-
|
|
44
|
+
import { render, ApplicationWindow, Box, Button, Label, quit } from "@gtkx/react";
|
|
45
|
+
import { Orientation } from "@gtkx/ffi/gtk";
|
|
68
46
|
import { useState } from "react";
|
|
69
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
70
|
-
import { ApplicationWindow, Box, Button, Label, quit } from "@gtkx/react";
|
|
71
47
|
|
|
72
|
-
|
|
48
|
+
const App = () => {
|
|
73
49
|
const [count, setCount] = useState(0);
|
|
74
50
|
|
|
75
51
|
return (
|
|
76
|
-
<ApplicationWindow
|
|
77
|
-
|
|
78
|
-
defaultWidth={400}
|
|
79
|
-
defaultHeight={300}
|
|
80
|
-
onCloseRequest={quit}
|
|
81
|
-
>
|
|
82
|
-
<Box
|
|
83
|
-
orientation={Gtk.Orientation.VERTICAL}
|
|
84
|
-
spacing={12}
|
|
85
|
-
marginStart={20}
|
|
86
|
-
marginEnd={20}
|
|
87
|
-
marginTop={20}
|
|
88
|
-
marginBottom={20}
|
|
89
|
-
>
|
|
52
|
+
<ApplicationWindow title="Counter" onCloseRequest={quit}>
|
|
53
|
+
<Box orientation={Orientation.VERTICAL} spacing={12} margin={20}>
|
|
90
54
|
<Label.Root label={`Count: ${count}`} />
|
|
91
55
|
<Button label="Increment" onClicked={() => setCount((c) => c + 1)} />
|
|
92
56
|
</Box>
|
|
93
57
|
</ApplicationWindow>
|
|
94
58
|
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export const appId = "org.example.MyApp";
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
```tsx
|
|
101
|
-
// src/index.tsx
|
|
102
|
-
import { render } from "@gtkx/react";
|
|
103
|
-
import App, { appId } from "./app.js";
|
|
104
|
-
|
|
105
|
-
render(<App />, appId);
|
|
106
|
-
```
|
|
59
|
+
};
|
|
107
60
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
npx gtkx dev src/app.tsx
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
Or without HMR (production):
|
|
115
|
-
|
|
116
|
-
```bash
|
|
117
|
-
npx tsc -b && node dist/index.js
|
|
61
|
+
render(<App />, "org.example.Counter");
|
|
118
62
|
```
|
|
119
63
|
|
|
120
64
|
## Styling
|
|
121
65
|
|
|
122
|
-
Use `@gtkx/css` for CSS-in-JS styling:
|
|
123
|
-
|
|
124
66
|
```tsx
|
|
125
67
|
import { css } from "@gtkx/css";
|
|
126
68
|
import { Button } from "@gtkx/react";
|
|
127
69
|
|
|
128
|
-
const
|
|
70
|
+
const primary = css`
|
|
129
71
|
padding: 16px 32px;
|
|
130
72
|
border-radius: 24px;
|
|
131
73
|
background: linear-gradient(135deg, #3584e4, #9141ac);
|
|
132
74
|
color: white;
|
|
133
|
-
font-weight: bold;
|
|
134
75
|
`;
|
|
135
76
|
|
|
136
|
-
|
|
77
|
+
<Button label="Click me" cssClasses={[primary]} />
|
|
137
78
|
```
|
|
138
79
|
|
|
139
|
-
GTK also provides built-in
|
|
80
|
+
GTK also provides built-in classes like `suggested-action`, `destructive-action`, `card`, and `heading`.
|
|
140
81
|
|
|
141
82
|
## Testing
|
|
142
83
|
|
|
143
|
-
Use `@gtkx/testing` for Testing Library-style component tests:
|
|
144
|
-
|
|
145
84
|
```tsx
|
|
146
|
-
import { cleanup, render, screen, userEvent
|
|
85
|
+
import { cleanup, render, screen, userEvent } from "@gtkx/testing";
|
|
147
86
|
import { AccessibleRole } from "@gtkx/ffi/gtk";
|
|
148
|
-
import { App } from "./app.js";
|
|
149
87
|
|
|
150
|
-
|
|
151
|
-
afterEach(async () => {
|
|
152
|
-
await cleanup();
|
|
153
|
-
});
|
|
88
|
+
afterEach(() => cleanup());
|
|
154
89
|
|
|
155
|
-
test("increments count
|
|
90
|
+
test("increments count", async () => {
|
|
156
91
|
await render(<App />);
|
|
157
92
|
|
|
158
|
-
const button = await screen.findByRole(AccessibleRole.BUTTON, {
|
|
159
|
-
name: "Increment",
|
|
160
|
-
});
|
|
93
|
+
const button = await screen.findByRole(AccessibleRole.BUTTON, { name: "Increment" });
|
|
161
94
|
await userEvent.click(button);
|
|
162
95
|
|
|
163
96
|
await screen.findByText("Count: 1");
|
|
164
97
|
});
|
|
165
|
-
|
|
166
|
-
test("can also use fireEvent for low-level signals", async () => {
|
|
167
|
-
await render(<App />);
|
|
168
|
-
|
|
169
|
-
const button = await screen.findByRole(AccessibleRole.BUTTON, {
|
|
170
|
-
name: "Increment",
|
|
171
|
-
});
|
|
172
|
-
fireEvent(button, "clicked");
|
|
173
|
-
|
|
174
|
-
await screen.findByText("Count: 1");
|
|
175
|
-
});
|
|
176
98
|
```
|
|
177
99
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
**Queries** - Find elements in the rendered tree (all async):
|
|
181
|
-
|
|
182
|
-
- `findBy*` / `findAllBy*` - Waits for element to appear
|
|
183
|
-
|
|
184
|
-
Query types: `ByRole`, `ByText`, `ByLabelText`, `ByTestId`
|
|
185
|
-
|
|
186
|
-
**User Interactions**:
|
|
100
|
+
Queries: `findByRole`, `findByText`, `findByLabelText`, `findByTestId`
|
|
187
101
|
|
|
188
|
-
|
|
189
|
-
- `userEvent.dblClick(element)` - Simulate double click
|
|
190
|
-
- `userEvent.activate(element)` - Activate element (e.g., press Enter in input)
|
|
191
|
-
- `userEvent.type(element, text)` - Type text into input
|
|
192
|
-
- `userEvent.clear(element)` - Clear input text
|
|
193
|
-
- `userEvent.tab(element, options?)` - Simulate Tab navigation
|
|
194
|
-
- `userEvent.selectOptions(element, values)` - Select options in ComboBox/ListBox
|
|
195
|
-
- `userEvent.deselectOptions(element, values)` - Deselect options in ListBox
|
|
196
|
-
|
|
197
|
-
**Low-level Events**:
|
|
198
|
-
|
|
199
|
-
- `fireEvent(element, signalName, ...args)` - Emit any GTK signal with optional arguments
|
|
200
|
-
|
|
201
|
-
**Utilities**:
|
|
202
|
-
|
|
203
|
-
- `waitFor(callback)` - Wait for condition
|
|
204
|
-
- `waitForElementToBeRemoved(element)` - Wait for element removal
|
|
102
|
+
User events: `click`, `dblClick`, `type`, `clear`, `tab`, `selectOptions`
|
|
205
103
|
|
|
206
104
|
## Examples
|
|
207
105
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
106
|
+
| Example | Description |
|
|
107
|
+
| ------- | ----------- |
|
|
108
|
+
| [gtk4-demo](examples/gtk4-demo) | Widget showcase |
|
|
109
|
+
| [todo](examples/todo) | Todo app with tests |
|
|
211
110
|
|
|
212
111
|
```bash
|
|
213
|
-
cd examples/gtk4-demo
|
|
214
|
-
pnpm dev
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### Todo App
|
|
218
|
-
|
|
219
|
-
A todo app demonstrating `@gtkx/testing` with realistic component tests:
|
|
220
|
-
|
|
221
|
-
```bash
|
|
222
|
-
cd examples/todo
|
|
223
|
-
pnpm dev
|
|
224
|
-
pnpm test
|
|
112
|
+
cd examples/gtk4-demo && pnpm dev
|
|
225
113
|
```
|
|
226
114
|
|
|
227
115
|
## Packages
|
|
228
116
|
|
|
229
|
-
| Package
|
|
230
|
-
|
|
|
231
|
-
| [@gtkx/cli](packages/cli)
|
|
232
|
-
| [@gtkx/react](packages/react)
|
|
233
|
-
| [@gtkx/ffi](packages/ffi)
|
|
234
|
-
| [@gtkx/native](packages/native)
|
|
235
|
-
| [@gtkx/css](packages/css)
|
|
236
|
-
| [@gtkx/testing](packages/testing) | Testing utilities
|
|
237
|
-
| [@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 |
|
|
238
126
|
|
|
239
127
|
## Requirements
|
|
240
128
|
|
|
241
129
|
- Node.js 20+
|
|
242
|
-
- GTK4
|
|
243
|
-
- 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)
|
|
244
140
|
|
|
245
141
|
## License
|
|
246
142
|
|
|
@@ -572,14 +572,12 @@ export class CodeGenerator {
|
|
|
572
572
|
}
|
|
573
573
|
let current = cls.parent ? classMap.get(cls.parent) : undefined;
|
|
574
574
|
while (current) {
|
|
575
|
-
// Collect signals from the current parent class first
|
|
576
575
|
for (const signal of current.signals) {
|
|
577
576
|
if (!seenNames.has(signal.name)) {
|
|
578
577
|
allSignals.push(signal);
|
|
579
578
|
seenNames.add(signal.name);
|
|
580
579
|
}
|
|
581
580
|
}
|
|
582
|
-
// Then check if this parent has a cross-namespace grandparent
|
|
583
581
|
if (current.parent?.includes(".")) {
|
|
584
582
|
return { signals: allSignals, hasCrossNamespaceParent: true };
|
|
585
583
|
}
|
|
@@ -1111,6 +1109,8 @@ ${allArgs ? `${allArgs},` : ""}
|
|
|
1111
1109
|
lines.push(` if (ptr === null) return null;`);
|
|
1112
1110
|
}
|
|
1113
1111
|
if (isCyclic) {
|
|
1112
|
+
// For cyclic types, return a minimal NativeObject wrapper. Cast is necessary
|
|
1113
|
+
// because the full type interface isn't available without circular imports.
|
|
1114
1114
|
lines.push(` return { id: ptr } as unknown as ${baseReturnType};`);
|
|
1115
1115
|
}
|
|
1116
1116
|
else {
|
|
@@ -1263,6 +1263,8 @@ ${allArgs ? `${allArgs},` : ""}
|
|
|
1263
1263
|
kind: "class",
|
|
1264
1264
|
});
|
|
1265
1265
|
}
|
|
1266
|
+
// Signal handler catch-all overload must use `any` for compatibility with typed overloads.
|
|
1267
|
+
// The typed overloads have specific return types, and unknown is not assignable to them.
|
|
1266
1268
|
signalOverloads.push(` ${methodName}(signal: string, handler: (...args: any[]) => any, after?: boolean): number;`);
|
|
1267
1269
|
this.usesType = true;
|
|
1268
1270
|
this.usesGetObject = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtkx/ffi",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Generated TypeScript FFI bindings for GTK4 libraries",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gtk",
|
|
@@ -46,10 +46,10 @@
|
|
|
46
46
|
"dist"
|
|
47
47
|
],
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@gtkx/native": "0.3.
|
|
49
|
+
"@gtkx/native": "0.3.3"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@gtkx/gir": "0.3.
|
|
52
|
+
"@gtkx/gir": "0.3.3"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
55
|
"build": "tsc -b && cp ../../README.md .",
|