@gtkx/ffi 0.3.2 → 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 -154
- package/dist/codegen/ffi-generator.js +4 -2
- 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
|
|
|
@@ -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 .",
|