@gtkx/react 0.1.21 → 0.1.22

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
@@ -1,391 +1,208 @@
1
- # @gtkx/react
1
+ <p align="center">
2
+ <img src="logo.svg" alt="GTKX Logo" width="128" height="128">
3
+ </p>
2
4
 
3
- React integration layer for GTKX. This package provides a custom React reconciler that renders React components as native GTK4 widgets.
5
+ <h1 align="center">GTKX</h1>
4
6
 
5
- ## Installation
7
+ <p align="center">
8
+ <strong>Build native GTK4 desktop applications with React and TypeScript</strong>
9
+ </p>
6
10
 
7
- ```bash
8
- pnpm add @gtkx/react react
9
- ```
10
-
11
- ### Peer Dependencies
11
+ <p align="center">
12
+ <a href="https://eugeniodepalo.github.io/gtkx">Documentation</a> ·
13
+ <a href="#quick-start">Quick Start</a> ·
14
+ <a href="#examples">Examples</a>
15
+ </p>
12
16
 
13
- - `react` ^19.0.0
17
+ ---
14
18
 
15
- ### System Requirements
19
+ GTKX bridges React's component model with GTK4's native widget system. Write familiar React code and render it as native Linux desktop applications with full access to GTK4 widgets, signals, and styling.
16
20
 
17
- - Linux with GTK4 libraries
18
- - Node.js 20+
19
- - Rust toolchain (for building `@gtkx/native`)
20
-
21
- ```bash
22
- # Fedora
23
- sudo dnf install gtk4-devel
21
+ ## Features
24
22
 
25
- # Ubuntu/Debian
26
- sudo apt install libgtk-4-dev
27
- ```
23
+ - **React Components** — Use React hooks, state, and component patterns you already know
24
+ - **Type-Safe** Full TypeScript support with auto-generated types from GTK4 introspection data
25
+ - **Native Performance** — Direct FFI bindings to GTK4 via Rust and libffi
26
+ - **CSS-in-JS Styling** — Emotion-style `css` template literals for GTK widgets
27
+ - **Testing Library** — Familiar `screen`, `userEvent`, and query APIs for testing components
28
28
 
29
29
  ## Quick Start
30
30
 
31
- ```tsx
32
- import { ApplicationWindow, Button, Box, Label, quit, render } from "@gtkx/react";
33
- import * as Gtk from "@gtkx/ffi/gtk";
34
- import { useState } from "react";
35
-
36
- const Counter = () => {
37
- const [count, setCount] = useState(0);
38
-
39
- return (
40
- <Box valign={Gtk.Align.CENTER} halign={Gtk.Align.CENTER} spacing={10}>
41
- <Label.Root label={`Count: ${count}`} cssClasses={["title-2"]} />
42
- <Button label="Increment" onClicked={() => setCount(c => c + 1)} />
43
- </Box>
44
- );
45
- };
46
-
47
- // Export app instance for use in dialogs
48
- export const app = render(
49
- <ApplicationWindow title="My App" defaultWidth={800} defaultHeight={600} onCloseRequest={quit}>
50
- <Counter />
51
- </ApplicationWindow>,
52
- "com.example.myapp"
53
- );
54
- ```
55
-
56
- Run with:
57
-
58
31
  ```bash
59
- npx tsx src/index.tsx
60
- ```
61
-
62
- ## API
63
-
64
- ### `render(element, applicationId)`
65
-
66
- Renders a React element tree as a GTK4 application. Returns the GTK Application instance.
67
-
68
- - `element` — Root React element (typically `ApplicationWindow`)
69
- - `applicationId` — Unique identifier in reverse-DNS format (e.g., `com.example.app`)
70
-
71
- ```tsx
72
- export const app = render(<App />, "com.example.myapp");
73
- ```
74
-
75
- ### `quit()`
76
-
77
- Signals the application to close. Returns `false` (useful as a direct event handler for `onCloseRequest`).
78
-
79
- ```tsx
80
- <ApplicationWindow onCloseRequest={quit}>...</ApplicationWindow>
81
- ```
82
-
83
- ### `createPortal(element)`
84
-
85
- Renders a React element outside the normal component tree (useful for dialogs).
86
-
87
- ```tsx
88
- {showDialog && createPortal(<AboutDialog ... />)}
89
- ```
90
-
91
- ### `createRef()`
92
-
93
- Creates a reference for FFI output parameters.
94
-
95
- ```tsx
96
- import { createRef } from "@gtkx/react";
97
-
98
- const ref = createRef();
99
- someGtkFunction(ref);
100
- console.log(ref.value);
101
- ```
102
-
103
- ## Widgets
104
-
105
- ### Container Widgets
106
-
107
- **ApplicationWindow** — Main application window
108
-
109
- ```tsx
110
- <ApplicationWindow title="Window Title" defaultWidth={800} defaultHeight={600} onCloseRequest={quit}>
111
- {children}
112
- </ApplicationWindow>
113
- ```
114
-
115
- **Box** — Arranges children in a row or column
116
-
117
- ```tsx
118
- <Box orientation={Gtk.Orientation.VERTICAL} spacing={10}>
119
- <Button label="First" />
120
- <Button label="Second" />
121
- </Box>
122
- ```
123
-
124
- **Grid** — Arranges children in a grid layout
125
-
126
- ```tsx
127
- <Grid.Root columnSpacing={10} rowSpacing={10}>
128
- <Grid.Child column={0} row={0}><Button label="(0,0)" /></Grid.Child>
129
- <Grid.Child column={1} row={0}><Button label="(1,0)" /></Grid.Child>
130
- <Grid.Child column={0} row={1} columnSpan={2}>
131
- <Button label="Spans 2 columns" hexpand />
132
- </Grid.Child>
133
- </Grid.Root>
134
- ```
135
-
136
- **ScrolledWindow** — Adds scrollbars to content
137
-
138
- ```tsx
139
- <ScrolledWindow vexpand hexpand>
140
- <TextView />
141
- </ScrolledWindow>
142
- ```
32
+ # Install dependencies
33
+ pnpm add @gtkx/react react
143
34
 
144
- **Paned** Resizable split container
35
+ # For styling (optional)
36
+ pnpm add @gtkx/css
145
37
 
146
- ```tsx
147
- <Paned.Root wideHandle>
148
- <Paned.StartChild>
149
- <Box>{/* Left */}</Box>
150
- </Paned.StartChild>
151
- <Paned.EndChild>
152
- <Box>{/* Right */}</Box>
153
- </Paned.EndChild>
154
- </Paned.Root>
38
+ # For testing (optional)
39
+ pnpm add -D @gtkx/testing
155
40
  ```
156
41
 
157
- ### Input Widgets
158
-
159
- **Button** — Clickable button
42
+ Create your first app:
160
43
 
161
44
  ```tsx
162
- <Button label="Click me" onClicked={() => console.log("Clicked!")} />
163
- ```
164
-
165
- **ToggleButton** — Button with on/off state
45
+ // index.tsx
46
+ import { render } from "@gtkx/react";
47
+ import { App } from "./app.js";
166
48
 
167
- ```tsx
168
- <ToggleButton.Root label={active ? "ON" : "OFF"} active={active} onToggled={() => setActive(a => !a)} />
49
+ render(<App />, "org.example.MyApp");
169
50
  ```
170
51
 
171
- **CheckButton** — Checkbox with label
172
-
173
52
  ```tsx
174
- <CheckButton.Root label="Accept terms" active={checked} onToggled={() => setChecked(c => !c)} />
175
- ```
53
+ // app.tsx
54
+ import { ApplicationWindow, Box, Button, Label, quit } from "@gtkx/react";
55
+ import { Orientation } from "@gtkx/ffi/gtk";
56
+ import { useState } from "react";
176
57
 
177
- **Switch** On/off toggle (must return `true` from `onStateSet`)
58
+ export const App = () => {
59
+ const [count, setCount] = useState(0);
178
60
 
179
- ```tsx
180
- <Switch
181
- active={enabled}
182
- onStateSet={(self, state) => {
183
- setEnabled(state);
184
- return true;
185
- }}
186
- />
61
+ return (
62
+ <ApplicationWindow
63
+ title="My App"
64
+ defaultWidth={400}
65
+ defaultHeight={300}
66
+ onCloseRequest={quit}
67
+ >
68
+ <Box orientation={Orientation.VERTICAL} spacing={12} margin={20}>
69
+ <Label.Root label={`Count: ${count}`} />
70
+ <Button
71
+ label="Increment"
72
+ onClicked={() => setCount((c) => c + 1)}
73
+ />
74
+ </Box>
75
+ </ApplicationWindow>
76
+ );
77
+ };
187
78
  ```
188
79
 
189
- **Entry** — Single-line text input
80
+ Run with:
190
81
 
191
- ```tsx
192
- <Entry placeholderText="Enter text..." />
193
- <PasswordEntry placeholderText="Password..." />
194
- <SearchEntry placeholderText="Search..." />
82
+ ```bash
83
+ pnpm tsx index.tsx
195
84
  ```
196
85
 
197
- **SpinButton** — Numeric input with increment/decrement
198
-
199
- ```tsx
200
- // Adjustment args: value, lower, upper, stepIncrement, pageIncrement, pageSize
201
- const adjustment = useMemo(() => new Gtk.Adjustment(50, 0, 100, 1, 10, 0), []);
202
- <SpinButton adjustment={adjustment} onValueChanged={(self) => setValue(self.getValue())} />
203
- ```
86
+ ## Styling
204
87
 
205
- **Scale** Horizontal or vertical slider
88
+ Use `@gtkx/css` for CSS-in-JS styling:
206
89
 
207
90
  ```tsx
208
- <Scale hexpand drawValue adjustment={adjustment} onValueChanged={(self) => setValue(self.getValue())} />
209
- ```
91
+ import { css } from "@gtkx/css";
92
+ import { Button } from "@gtkx/react";
210
93
 
211
- ### Display Widgets
94
+ const primaryButton = css`
95
+ padding: 16px 32px;
96
+ border-radius: 24px;
97
+ background: linear-gradient(135deg, #3584e4, #9141ac);
98
+ color: white;
99
+ font-weight: bold;
100
+ `;
212
101
 
213
- **Label** Text display
214
-
215
- ```tsx
216
- <Label.Root label="Hello, World!" cssClasses={["title-2"]} />
102
+ const MyButton = () => (
103
+ <Button label="Click me" cssClasses={[primaryButton]} />
104
+ );
217
105
  ```
218
106
 
219
- **ProgressBar** Progress indicator
107
+ GTK also provides built-in CSS classes like `suggested-action`, `destructive-action`, `card`, and `heading`.
220
108
 
221
- ```tsx
222
- <ProgressBar fraction={0.5} showText />
223
- ```
109
+ ## Testing
224
110
 
225
- **Spinner** Loading indicator
111
+ Use `@gtkx/testing` for Testing Library-style component tests:
226
112
 
227
113
  ```tsx
228
- <Spinner spinning={isLoading} />
229
- ```
114
+ import { cleanup, render, screen, userEvent, fireEvent } from "@gtkx/testing";
115
+ import { AccessibleRole } from "@gtkx/ffi/gtk";
116
+ import { afterEach, describe, expect, it } from "vitest";
117
+ import { App } from "./app.js";
230
118
 
231
- ### Layout Widgets
119
+ describe("Counter", () => {
120
+ afterEach(() => cleanup());
232
121
 
233
- **HeaderBar** Title bar with buttons
122
+ it("increments count when clicking button", async () => {
123
+ render(<App />);
234
124
 
235
- ```tsx
236
- <HeaderBar.Root>
237
- <HeaderBar.TitleWidget>
238
- <Label.Root label="App Title" />
239
- </HeaderBar.TitleWidget>
240
- </HeaderBar.Root>
241
- ```
125
+ const button = await screen.findByRole(AccessibleRole.BUTTON, {
126
+ name: "Increment",
127
+ });
128
+ await userEvent.click(button);
242
129
 
243
- **CenterBox** Positions children at start, center, and end
130
+ await screen.findByText("Count: 1");
131
+ });
244
132
 
245
- ```tsx
246
- <CenterBox.Root>
247
- <CenterBox.StartWidget><Button label="Left" /></CenterBox.StartWidget>
248
- <CenterBox.CenterWidget><Label.Root label="Center" /></CenterBox.CenterWidget>
249
- <CenterBox.EndWidget><Button label="Right" /></CenterBox.EndWidget>
250
- </CenterBox.Root>
251
- ```
133
+ it("can also use fireEvent for synchronous events", async () => {
134
+ render(<App />);
252
135
 
253
- ### List Widgets
136
+ const button = await screen.findByRole(AccessibleRole.BUTTON, {
137
+ name: "Increment",
138
+ });
139
+ fireEvent.click(button);
254
140
 
255
- **ListBox** Simple vertical list
256
-
257
- ```tsx
258
- <ListBox selectionMode={Gtk.SelectionMode.SINGLE}>
259
- <ListBoxRow><Label.Root label="Item 1" /></ListBoxRow>
260
- <ListBoxRow><Label.Root label="Item 2" /></ListBoxRow>
261
- </ListBox>
262
- ```
263
-
264
- **DropDown** — Dropdown selector
265
-
266
- ```tsx
267
- <DropDown.Root
268
- itemLabel={(item) => item.name}
269
- onSelectionChanged={(item, index) => setSelected(item)}
270
- >
271
- {items.map(item => <DropDown.Item key={item.id} item={item} />)}
272
- </DropDown.Root>
141
+ await screen.findByText("Count: 1");
142
+ });
143
+ });
273
144
  ```
274
145
 
275
- **ListView** Virtualized list for large datasets
146
+ ### Available APIs
276
147
 
277
- ```tsx
278
- <ListView.Root renderItem={(item) => {
279
- if (!item) return new Gtk.Box();
280
- return new Gtk.Label({ label: item.name });
281
- }}>
282
- {items.map(item => <ListView.Item item={item} key={item.id} />)}
283
- </ListView.Root>
284
- ```
148
+ **Queries** - Find elements in the rendered tree:
149
+ - `getBy*` / `getAllBy*` - Throws if not found
150
+ - `queryBy*` / `queryAllBy*` - Returns null/empty array if not found
151
+ - `findBy*` / `findAllBy*` - Async, waits for element
285
152
 
286
- ### Dialog Widgets
153
+ Query types: `ByRole`, `ByText`, `ByLabelText`, `ByTestId`
287
154
 
288
- **AboutDialog** — Application about dialog (React component)
155
+ **User Interactions**:
156
+ - `userEvent.click(element)` - Simulate click
157
+ - `userEvent.dblClick(element)` - Simulate double click
158
+ - `userEvent.type(element, text)` - Type text into input
159
+ - `userEvent.clear(element)` - Clear input text
160
+ - `userEvent.setup()` - Create reusable instance
289
161
 
290
- ```tsx
291
- {showAbout && createPortal(
292
- <AboutDialog
293
- programName="My App"
294
- version="1.0.0"
295
- onCloseRequest={() => { setShowAbout(false); return false; }}
296
- />
297
- )}
298
- ```
162
+ **Low-level Events**:
163
+ - `fireEvent(element, signalName)` - Emit any GTK signal
164
+ - `fireEvent.click(element)` - Emit clicked signal
165
+ - `fireEvent.activate(element)` - Emit activate signal
299
166
 
300
- **Async Dialogs** — AlertDialog, FileDialog, ColorDialog, FontDialog (imperative)
167
+ **Utilities**:
168
+ - `waitFor(callback)` - Wait for condition
169
+ - `waitForElementToBeRemoved(element)` - Wait for element removal
301
170
 
302
- ```tsx
303
- import { app } from "./index.js";
304
-
305
- const openFile = async () => {
306
- const dialog = new Gtk.FileDialog();
307
- dialog.setTitle("Open File");
308
- try {
309
- const file = await dialog.open(app.getActiveWindow());
310
- console.log(file.getPath());
311
- } catch {
312
- // Cancelled
313
- }
314
- };
315
- ```
171
+ ## Examples
316
172
 
317
- ## Named Slots
173
+ ### Counter
318
174
 
319
- Some GTK widgets have named child positions, handled via compound components:
175
+ A minimal counter app demonstrating state management:
320
176
 
321
- ```tsx
322
- <Frame.Root>
323
- <Frame.LabelWidget>
324
- <Label.Root label="Custom Label" />
325
- </Frame.LabelWidget>
326
- <Frame.Child>
327
- <Box>{/* Content */}</Box>
328
- </Frame.Child>
329
- </Frame.Root>
330
-
331
- <Expander.Root label="Click to expand">
332
- <Expander.Child>
333
- <Box>Hidden content</Box>
334
- </Expander.Child>
335
- </Expander.Root>
177
+ ```bash
178
+ turbo start --filter=counter-example
336
179
  ```
337
180
 
338
- ## Using GTK Enums
181
+ ### GTK4 Demo
339
182
 
340
- Import enums from `@gtkx/ffi/gtk`:
183
+ A comprehensive showcase of GTK4 widgets and features:
341
184
 
342
- ```tsx
343
- import * as Gtk from "@gtkx/ffi/gtk";
344
-
345
- <Box orientation={Gtk.Orientation.VERTICAL} />
346
- <ListBox selectionMode={Gtk.SelectionMode.SINGLE} />
347
- <Revealer transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN} />
185
+ ```bash
186
+ turbo start --filter=gtk4-demo
348
187
  ```
349
188
 
350
- ## Hooks Support
351
-
352
- Standard React hooks work as expected:
353
-
354
- ```tsx
355
- import { useState, useEffect, useRef } from "react";
189
+ ## Packages
356
190
 
357
- const Counter = () => {
358
- const [count, setCount] = useState(0);
191
+ | Package | Description |
192
+ |---------|-------------|
193
+ | [@gtkx/react](packages/react) | React reconciler and JSX components |
194
+ | [@gtkx/ffi](packages/ffi) | TypeScript FFI bindings for GTK4 |
195
+ | [@gtkx/native](packages/native) | Rust native module for FFI bridge |
196
+ | [@gtkx/css](packages/css) | CSS-in-JS styling for GTK widgets |
197
+ | [@gtkx/testing](packages/testing) | Testing utilities for GTKX components |
198
+ | [@gtkx/gir](packages/gir) | GObject Introspection parser for codegen |
359
199
 
360
- useEffect(() => {
361
- console.log(`Count: ${count}`);
362
- }, [count]);
200
+ ## Requirements
363
201
 
364
- return (
365
- <Box spacing={10}>
366
- <Label.Root label={`Count: ${count}`} />
367
- <Button label="Increment" onClicked={() => setCount(c => c + 1)} />
368
- </Box>
369
- );
370
- };
371
- ```
372
-
373
- ## Architecture
374
-
375
- ```
376
- ┌─────────────────────────────────┐
377
- │ Your React Application │
378
- ├─────────────────────────────────┤
379
- │ @gtkx/react (React Reconciler) │
380
- ├─────────────────────────────────┤
381
- │ @gtkx/ffi (TypeScript FFI) │
382
- ├─────────────────────────────────┤
383
- │ @gtkx/native (Rust Bridge) │
384
- ├─────────────────────────────────┤
385
- │ GTK4 / GLib │
386
- └─────────────────────────────────┘
387
- ```
202
+ - Node.js 20+
203
+ - GTK4 development libraries
204
+ - Linux (GTK4 is Linux-native)
388
205
 
389
206
  ## License
390
207
 
391
- [MPL-2.0](../../LICENSE)
208
+ [MPL-2.0](LICENSE)
@@ -1,5 +1,5 @@
1
1
  import type { GirClass, GirNamespace, TypeMapper } from "@gtkx/gir";
2
- export interface JsxGeneratorOptions {
2
+ interface JsxGeneratorOptions {
3
3
  prettierConfig?: unknown;
4
4
  }
5
5
  export declare class JsxGenerator {
@@ -35,3 +35,4 @@ export declare class JsxGenerator {
35
35
  private generateJsxNamespace;
36
36
  private formatCode;
37
37
  }
38
+ export {};
@@ -1,13 +1,14 @@
1
1
  import type * as Gtk from "@gtkx/ffi/gtk";
2
- export interface Appendable extends Gtk.Widget {
2
+ interface Appendable extends Gtk.Widget {
3
3
  append(child: unknown): void;
4
4
  }
5
- export interface SingleChild extends Gtk.Widget {
5
+ interface SingleChild extends Gtk.Widget {
6
6
  setChild(child: unknown): void;
7
7
  }
8
- export interface Removable extends Gtk.Widget {
8
+ interface Removable extends Gtk.Widget {
9
9
  remove(child: unknown): void;
10
10
  }
11
11
  export declare const isAppendable: (widget: Gtk.Widget) => widget is Appendable;
12
12
  export declare const isSingleChild: (widget: Gtk.Widget) => widget is SingleChild;
13
13
  export declare const isRemovable: (widget: Gtk.Widget) => widget is Removable;
14
+ export {};
@@ -8,7 +8,7 @@ type SuspenseInstance = never;
8
8
  type PublicInstance = Gtk.Widget;
9
9
  type FormInstance = never;
10
10
  type ReconcilerInstance = ReactReconciler.Reconciler<Container, Node, TextInstance, SuspenseInstance, FormInstance, PublicInstance>;
11
- export declare class Reconciler {
11
+ declare class Reconciler {
12
12
  private instance;
13
13
  private app;
14
14
  constructor();
@@ -3,7 +3,7 @@ import React from "react";
3
3
  import ReactReconciler from "react-reconciler";
4
4
  import { createNode } from "./factory.js";
5
5
  import { WidgetWrapper } from "./nodes/widget.js";
6
- export class Reconciler {
6
+ class Reconciler {
7
7
  instance;
8
8
  app = null;
9
9
  constructor() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtkx/react",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Build GTK4 desktop applications with React and TypeScript",
5
5
  "keywords": [
6
6
  "gtk",
@@ -40,17 +40,17 @@
40
40
  ],
41
41
  "dependencies": {
42
42
  "react-reconciler": "0.33.0",
43
- "@gtkx/ffi": "0.1.21"
43
+ "@gtkx/ffi": "0.1.22"
44
44
  },
45
45
  "devDependencies": {
46
- "@gtkx/gir": "0.1.21"
46
+ "@gtkx/gir": "0.1.22"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": "^19"
50
50
  },
51
51
  "scripts": {
52
- "build": "tsc -b",
52
+ "build": "tsc -b && cp ../../README.md .",
53
53
  "codegen": "tsx scripts/codegen.ts",
54
- "test": "xvfb-run -a vitest run"
54
+ "test": "GDK_BACKEND=x11 xvfb-run -a vitest run"
55
55
  }
56
56
  }