@gtkx/cli 0.15.0 → 0.16.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/dist/mcp-client.js +32 -18
- package/dist/templates.d.ts +2 -2
- package/package.json +10 -10
- package/templates/claude/EXAMPLES.md.ejs +18 -26
- package/templates/claude/WIDGETS.md.ejs +187 -142
package/dist/mcp-client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as net from "node:net";
|
|
2
|
-
import { getNativeId,
|
|
2
|
+
import { getNativeId, getNativeInterface } from "@gtkx/ffi";
|
|
3
3
|
import { Value } from "@gtkx/ffi/gobject";
|
|
4
4
|
import * as Gtk from "@gtkx/ffi/gtk";
|
|
5
5
|
import { DEFAULT_SOCKET_PATH, IpcRequestSchema, IpcResponseSchema, McpError, McpErrorCode, methodNotFoundError, widgetNotFoundError, } from "@gtkx/mcp";
|
|
@@ -46,7 +46,7 @@ const getWidgetText = (widget) => {
|
|
|
46
46
|
case Gtk.AccessibleRole.TEXT_BOX:
|
|
47
47
|
case Gtk.AccessibleRole.SEARCH_BOX:
|
|
48
48
|
case Gtk.AccessibleRole.SPIN_BUTTON:
|
|
49
|
-
return
|
|
49
|
+
return getNativeInterface(widget, Gtk.Editable)?.getText() ?? null;
|
|
50
50
|
case Gtk.AccessibleRole.GROUP:
|
|
51
51
|
return widget.getLabel?.() ?? null;
|
|
52
52
|
case Gtk.AccessibleRole.WINDOW:
|
|
@@ -93,10 +93,7 @@ const getWidgetById = (id) => {
|
|
|
93
93
|
};
|
|
94
94
|
const refreshWidgetRegistry = () => {
|
|
95
95
|
widgetRegistry.clear();
|
|
96
|
-
const
|
|
97
|
-
if (!app)
|
|
98
|
-
return;
|
|
99
|
-
const windows = app.getWindows();
|
|
96
|
+
const windows = Gtk.Window.listToplevels();
|
|
100
97
|
for (const window of windows) {
|
|
101
98
|
registerWidgets(window);
|
|
102
99
|
}
|
|
@@ -291,6 +288,15 @@ class McpClient {
|
|
|
291
288
|
}
|
|
292
289
|
refreshWidgetRegistry();
|
|
293
290
|
switch (method) {
|
|
291
|
+
case "app.getWindows": {
|
|
292
|
+
const windows = Gtk.Window.listToplevels();
|
|
293
|
+
return {
|
|
294
|
+
windows: windows.map((w) => ({
|
|
295
|
+
id: String(getNativeId(w.handle)),
|
|
296
|
+
title: w.getTitle?.() ?? null,
|
|
297
|
+
})),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
294
300
|
case "widget.getTree": {
|
|
295
301
|
const testing = await loadTestingModule();
|
|
296
302
|
return { tree: testing.prettyWidget(app, { includeIds: true, highlight: false }) };
|
|
@@ -300,9 +306,13 @@ class McpClient {
|
|
|
300
306
|
const p = params;
|
|
301
307
|
let widgets = [];
|
|
302
308
|
switch (p.queryType) {
|
|
303
|
-
case "role":
|
|
304
|
-
|
|
309
|
+
case "role": {
|
|
310
|
+
const roleValue = typeof p.value === "string"
|
|
311
|
+
? Gtk.AccessibleRole[p.value]
|
|
312
|
+
: p.value;
|
|
313
|
+
widgets = await testing.findAllByRole(app, roleValue, p.options);
|
|
305
314
|
break;
|
|
315
|
+
}
|
|
306
316
|
case "text":
|
|
307
317
|
widgets = await testing.findAllByText(app, String(p.value), p.options);
|
|
308
318
|
break;
|
|
@@ -356,25 +366,29 @@ class McpClient {
|
|
|
356
366
|
throw widgetNotFoundError(p.widgetId);
|
|
357
367
|
}
|
|
358
368
|
const signalArgs = (p.args ?? []).map((arg) => {
|
|
359
|
-
|
|
369
|
+
const isTypedArg = typeof arg === "object" && arg !== null && "type" in arg && "value" in arg;
|
|
370
|
+
const argType = isTypedArg ? arg.type : typeof arg;
|
|
371
|
+
const argValue = isTypedArg ? arg.value : arg;
|
|
372
|
+
switch (argType) {
|
|
360
373
|
case "boolean":
|
|
361
|
-
return Value.newFromBoolean(
|
|
374
|
+
return Value.newFromBoolean(argValue);
|
|
362
375
|
case "int":
|
|
363
|
-
return Value.newFromInt(
|
|
376
|
+
return Value.newFromInt(argValue);
|
|
364
377
|
case "uint":
|
|
365
|
-
return Value.newFromUint(
|
|
378
|
+
return Value.newFromUint(argValue);
|
|
366
379
|
case "int64":
|
|
367
|
-
return Value.newFromInt64(
|
|
380
|
+
return Value.newFromInt64(argValue);
|
|
368
381
|
case "uint64":
|
|
369
|
-
return Value.newFromUint64(
|
|
382
|
+
return Value.newFromUint64(argValue);
|
|
370
383
|
case "float":
|
|
371
|
-
return Value.newFromFloat(
|
|
384
|
+
return Value.newFromFloat(argValue);
|
|
372
385
|
case "double":
|
|
373
|
-
|
|
386
|
+
case "number":
|
|
387
|
+
return Value.newFromDouble(argValue);
|
|
374
388
|
case "string":
|
|
375
|
-
return Value.newFromString(
|
|
389
|
+
return Value.newFromString(argValue);
|
|
376
390
|
default:
|
|
377
|
-
throw new McpError(McpErrorCode.INVALID_REQUEST, `Unknown argument type: ${
|
|
391
|
+
throw new McpError(McpErrorCode.INVALID_REQUEST, `Unknown argument type: ${argType}`);
|
|
378
392
|
}
|
|
379
393
|
});
|
|
380
394
|
await testing.fireEvent(widget, p.signal, ...signalArgs);
|
package/dist/templates.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { TestingOption } from "./create.js";
|
|
2
|
-
export
|
|
2
|
+
export type TemplateContext = {
|
|
3
3
|
name: string;
|
|
4
4
|
appId: string;
|
|
5
5
|
title: string;
|
|
6
6
|
testing: TestingOption;
|
|
7
|
-
}
|
|
7
|
+
};
|
|
8
8
|
export declare const renderFile: (templateName: string, context: TemplateContext) => string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtkx/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "CLI for GTKX - create and develop GTK4 React applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gtkx",
|
|
@@ -53,24 +53,24 @@
|
|
|
53
53
|
],
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@clack/prompts": "^0.11.0",
|
|
56
|
-
"@swc/core": "^1.15.
|
|
57
|
-
"citty": "^0.
|
|
58
|
-
"ejs": "^
|
|
56
|
+
"@swc/core": "^1.15.10",
|
|
57
|
+
"citty": "^0.2.0",
|
|
58
|
+
"ejs": "^4.0.1",
|
|
59
59
|
"react-refresh": "^0.18.0",
|
|
60
60
|
"vite": "^7.3.1",
|
|
61
|
-
"@gtkx/mcp": "0.
|
|
62
|
-
"@gtkx/
|
|
63
|
-
"@gtkx/
|
|
61
|
+
"@gtkx/mcp": "0.16.0",
|
|
62
|
+
"@gtkx/ffi": "0.16.0",
|
|
63
|
+
"@gtkx/react": "0.16.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/ejs": "^3.1.5",
|
|
67
67
|
"@types/react-refresh": "^0.14.7",
|
|
68
|
-
"memfs": "^4.
|
|
69
|
-
"@gtkx/testing": "0.
|
|
68
|
+
"memfs": "^4.56.9",
|
|
69
|
+
"@gtkx/testing": "0.16.0"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
72
|
"react": "^19",
|
|
73
|
-
"@gtkx/testing": "0.
|
|
73
|
+
"@gtkx/testing": "0.16.0"
|
|
74
74
|
},
|
|
75
75
|
"peerDependenciesMeta": {
|
|
76
76
|
"@gtkx/testing": {
|
|
@@ -526,7 +526,7 @@ const NavigationDemo = () => {
|
|
|
526
526
|
return (
|
|
527
527
|
<AdwApplicationWindow title="Navigation Demo" defaultWidth={600} defaultHeight={400} onClose={quit}>
|
|
528
528
|
<AdwNavigationView history={history} onHistoryChanged={setHistory}>
|
|
529
|
-
<x.NavigationPage id="home" title="Home">
|
|
529
|
+
<x.NavigationPage for={AdwNavigationView} id="home" title="Home">
|
|
530
530
|
<AdwToolbarView>
|
|
531
531
|
<x.ToolbarTop><AdwHeaderBar /></x.ToolbarTop>
|
|
532
532
|
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER}>
|
|
@@ -535,7 +535,7 @@ const NavigationDemo = () => {
|
|
|
535
535
|
</GtkBox>
|
|
536
536
|
</AdwToolbarView>
|
|
537
537
|
</x.NavigationPage>
|
|
538
|
-
<x.NavigationPage id="settings" title="Settings" canPop>
|
|
538
|
+
<x.NavigationPage for={AdwNavigationView} id="settings" title="Settings" canPop>
|
|
539
539
|
<AdwToolbarView>
|
|
540
540
|
<x.ToolbarTop><AdwHeaderBar /></x.ToolbarTop>
|
|
541
541
|
<GtkLabel label="Settings page content" vexpand />
|
|
@@ -587,13 +587,13 @@ const SplitViewDemo = () => {
|
|
|
587
587
|
return (
|
|
588
588
|
<AdwApplicationWindow title="Split View Demo" defaultWidth={800} defaultHeight={500} onClose={quit}>
|
|
589
589
|
<AdwNavigationSplitView sidebarWidthFraction={0.33} minSidebarWidth={200} maxSidebarWidth={300}>
|
|
590
|
-
<x.NavigationPage id="sidebar" title="Mail">
|
|
590
|
+
<x.NavigationPage for={AdwNavigationSplitView} id="sidebar" title="Mail">
|
|
591
591
|
<AdwToolbarView>
|
|
592
592
|
<x.ToolbarTop><AdwHeaderBar /></x.ToolbarTop>
|
|
593
593
|
<GtkScrolledWindow vexpand>
|
|
594
594
|
<GtkListBox
|
|
595
595
|
cssClasses={["navigation-sidebar"]}
|
|
596
|
-
onRowSelected={(
|
|
596
|
+
onRowSelected={(row) => {
|
|
597
597
|
if (!row) return;
|
|
598
598
|
const item = items[row.getIndex()];
|
|
599
599
|
if (item) setSelected(item);
|
|
@@ -611,7 +611,7 @@ const SplitViewDemo = () => {
|
|
|
611
611
|
</AdwToolbarView>
|
|
612
612
|
</x.NavigationPage>
|
|
613
613
|
|
|
614
|
-
<x.NavigationPage id="content" title={selected?.title ?? ""}>
|
|
614
|
+
<x.NavigationPage for={AdwNavigationSplitView} id="content" title={selected?.title ?? ""}>
|
|
615
615
|
<AdwToolbarView>
|
|
616
616
|
<x.ToolbarTop><AdwHeaderBar /></x.ToolbarTop>
|
|
617
617
|
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} vexpand>
|
|
@@ -689,36 +689,28 @@ const FileBrowser = () => {
|
|
|
689
689
|
|
|
690
690
|
---
|
|
691
691
|
|
|
692
|
-
##
|
|
692
|
+
## Stack with Programmatic Navigation
|
|
693
693
|
|
|
694
694
|
```tsx
|
|
695
695
|
import * as Gtk from "@gtkx/ffi/gtk";
|
|
696
|
-
import {
|
|
696
|
+
import { GtkBox, GtkButton, GtkLabel, GtkStack, x } from "@gtkx/react";
|
|
697
697
|
import { useState } from "react";
|
|
698
698
|
|
|
699
|
-
const
|
|
700
|
-
const [
|
|
699
|
+
const StackNavigation = () => {
|
|
700
|
+
const [page, setPage] = useState("home");
|
|
701
701
|
|
|
702
702
|
return (
|
|
703
703
|
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12}>
|
|
704
|
-
<
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
halign={Gtk.Align.CENTER}
|
|
712
|
-
>
|
|
713
|
-
<x.Toggle id="list" iconName="view-list-symbolic" tooltip="List view" />
|
|
714
|
-
<x.Toggle id="grid" iconName="view-grid-symbolic" tooltip="Grid view" />
|
|
715
|
-
</AdwToggleGroup>
|
|
716
|
-
<GtkStack page={view} vexpand>
|
|
717
|
-
<x.StackPage id="list">
|
|
718
|
-
<GtkLabel label="List View Content" />
|
|
704
|
+
<GtkBox spacing={6} halign={Gtk.Align.CENTER}>
|
|
705
|
+
<GtkButton label="Home" onClicked={() => setPage("home")} />
|
|
706
|
+
<GtkButton label="Settings" onClicked={() => setPage("settings")} />
|
|
707
|
+
</GtkBox>
|
|
708
|
+
<GtkStack page={page} onPageChanged={setPage} vexpand>
|
|
709
|
+
<x.StackPage id="home">
|
|
710
|
+
<GtkLabel label="Home Content" />
|
|
719
711
|
</x.StackPage>
|
|
720
|
-
<x.StackPage id="
|
|
721
|
-
<GtkLabel label="
|
|
712
|
+
<x.StackPage id="settings">
|
|
713
|
+
<GtkLabel label="Settings Content" />
|
|
722
714
|
</x.StackPage>
|
|
723
715
|
</GtkStack>
|
|
724
716
|
</GtkBox>
|
|
@@ -60,7 +60,7 @@ Tabbed container with visible tabs.
|
|
|
60
60
|
```tsx
|
|
61
61
|
<GtkNotebook>
|
|
62
62
|
<x.NotebookPage label="Tab 1"><Content1 /></x.NotebookPage>
|
|
63
|
-
<x.NotebookPage label="Tab 2"><Content2 /></x.NotebookPage>
|
|
63
|
+
<x.NotebookPage label="Tab 2" tabExpand tabFill><Content2 /></x.NotebookPage>
|
|
64
64
|
</GtkNotebook>
|
|
65
65
|
```
|
|
66
66
|
|
|
@@ -77,6 +77,8 @@ Custom tab widget:
|
|
|
77
77
|
</x.NotebookPage>
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
+
**NotebookPage props:** `label`, `tabExpand`, `tabFill`
|
|
81
|
+
|
|
80
82
|
### GtkPaned
|
|
81
83
|
Resizable split with draggable divider. **Requires Slot components.**
|
|
82
84
|
|
|
@@ -88,7 +90,7 @@ Resizable split with draggable divider. **Requires Slot components.**
|
|
|
88
90
|
```
|
|
89
91
|
|
|
90
92
|
### GtkOverlay
|
|
91
|
-
Stack widgets on top of each other. First child is base layer, additional children need `OverlayChild` wrapper.
|
|
93
|
+
Stack widgets on top of each other. First child is base layer, additional children need `OverlayChild` wrapper. Multiple children supported per overlay.
|
|
92
94
|
|
|
93
95
|
```tsx
|
|
94
96
|
<GtkOverlay>
|
|
@@ -100,20 +102,20 @@ Stack widgets on top of each other. First child is base layer, additional childr
|
|
|
100
102
|
```
|
|
101
103
|
|
|
102
104
|
### GtkFixed
|
|
103
|
-
Absolute positioning. Use `x.FixedChild` wrapper for children.
|
|
105
|
+
Absolute positioning with optional 3D transforms. Use `x.FixedChild` wrapper for children.
|
|
104
106
|
|
|
105
107
|
```tsx
|
|
106
108
|
<GtkFixed>
|
|
107
109
|
<x.FixedChild x={20} y={30}>
|
|
108
110
|
<GtkLabel label="Top Left" />
|
|
109
111
|
</x.FixedChild>
|
|
110
|
-
<x.FixedChild x={200} y={100}>
|
|
111
|
-
<GtkLabel label="
|
|
112
|
+
<x.FixedChild x={200} y={100} transform={someGskTransform}>
|
|
113
|
+
<GtkLabel label="Transformed" />
|
|
112
114
|
</x.FixedChild>
|
|
113
115
|
</GtkFixed>
|
|
114
116
|
```
|
|
115
117
|
|
|
116
|
-
**FixedChild props:** `x`, `y` (pixel coordinates)
|
|
118
|
+
**FixedChild props:** `x`, `y` (pixel coordinates), `transform` (optional `Gsk.Transform`)
|
|
117
119
|
|
|
118
120
|
### GtkScrolledWindow
|
|
119
121
|
Scrollable container.
|
|
@@ -182,7 +184,7 @@ Table with sortable columns.
|
|
|
182
184
|
title="Size"
|
|
183
185
|
id="size"
|
|
184
186
|
fixedWidth={100}
|
|
185
|
-
renderCell={(item) => <GtkLabel label={item?.size ??
|
|
187
|
+
renderCell={(item) => <GtkLabel label={`${item?.size ?? 0} KB`} />}
|
|
186
188
|
/>
|
|
187
189
|
{items.map(item => <x.ListItem key={item.id} id={item.id} value={item} />)}
|
|
188
190
|
</GtkColumnView>
|
|
@@ -261,49 +263,67 @@ On/off switch.
|
|
|
261
263
|
```
|
|
262
264
|
|
|
263
265
|
### GtkSpinButton
|
|
264
|
-
Numeric input with increment/decrement.
|
|
266
|
+
Numeric input with increment/decrement. Adjustment props are set directly.
|
|
265
267
|
|
|
266
268
|
```tsx
|
|
267
|
-
<GtkSpinButton
|
|
269
|
+
<GtkSpinButton
|
|
270
|
+
value={count}
|
|
271
|
+
lower={0}
|
|
272
|
+
upper={100}
|
|
273
|
+
stepIncrement={1}
|
|
274
|
+
onValueChanged={setCount}
|
|
275
|
+
/>
|
|
268
276
|
```
|
|
269
277
|
|
|
270
278
|
### GtkScale
|
|
271
|
-
Slider with optional marks.
|
|
279
|
+
Slider with adjustment props and optional marks.
|
|
272
280
|
|
|
273
281
|
```tsx
|
|
274
282
|
<GtkScale
|
|
275
283
|
drawValue
|
|
276
284
|
valuePos={Gtk.PositionType.TOP}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
+
value={volume}
|
|
286
|
+
lower={0}
|
|
287
|
+
upper={100}
|
|
288
|
+
stepIncrement={1}
|
|
289
|
+
onValueChanged={setVolume}
|
|
290
|
+
marks={[
|
|
291
|
+
{ value: 0, label: "Min", position: Gtk.PositionType.BOTTOM },
|
|
292
|
+
{ value: 50, position: Gtk.PositionType.BOTTOM },
|
|
293
|
+
{ value: 100, label: "Max", position: Gtk.PositionType.BOTTOM },
|
|
294
|
+
]}
|
|
295
|
+
/>
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Adjustment props:** `value`, `lower`, `upper`, `stepIncrement`, `pageIncrement`, `pageSize`, `onValueChanged`
|
|
299
|
+
**ScaleMark type:** `{ value: number, position?: Gtk.PositionType, label?: string }`
|
|
285
300
|
|
|
286
301
|
### GtkCalendar
|
|
287
302
|
Date picker with markable days.
|
|
288
303
|
|
|
289
304
|
```tsx
|
|
290
|
-
<GtkCalendar
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
305
|
+
<GtkCalendar
|
|
306
|
+
onDaySelected={(cal) => setDate(cal.getDate())}
|
|
307
|
+
markedDays={[15, 20, 25]}
|
|
308
|
+
/>
|
|
294
309
|
```
|
|
295
310
|
|
|
296
311
|
### GtkLevelBar
|
|
297
312
|
Progress/level indicator with customizable thresholds.
|
|
298
313
|
|
|
299
314
|
```tsx
|
|
300
|
-
<GtkLevelBar
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
315
|
+
<GtkLevelBar
|
|
316
|
+
value={0.6}
|
|
317
|
+
offsets={[
|
|
318
|
+
{ id: "low", value: 0.25 },
|
|
319
|
+
{ id: "high", value: 0.75 },
|
|
320
|
+
{ id: "full", value: 1.0 },
|
|
321
|
+
]}
|
|
322
|
+
/>
|
|
305
323
|
```
|
|
306
324
|
|
|
325
|
+
**LevelBarOffset type:** `{ id: string, value: number }`
|
|
326
|
+
|
|
307
327
|
---
|
|
308
328
|
|
|
309
329
|
## Display
|
|
@@ -500,22 +520,17 @@ Input in list row.
|
|
|
500
520
|
Segmented button group for mutually exclusive options.
|
|
501
521
|
|
|
502
522
|
```tsx
|
|
503
|
-
const [
|
|
523
|
+
const [mode, setMode] = useState("list");
|
|
504
524
|
|
|
505
|
-
<AdwToggleGroup
|
|
506
|
-
activeName={viewMode}
|
|
507
|
-
onNotify={(group, prop) => {
|
|
508
|
-
if (prop === "active-name") {
|
|
509
|
-
setViewMode(group.getActiveName() ?? "list");
|
|
510
|
-
}
|
|
511
|
-
}}
|
|
512
|
-
>
|
|
525
|
+
<AdwToggleGroup activeName={mode} onActiveChanged={(_index, name) => setMode(name ?? "list")}>
|
|
513
526
|
<x.Toggle id="list" iconName="view-list-symbolic" tooltip="List view" />
|
|
514
527
|
<x.Toggle id="grid" iconName="view-grid-symbolic" tooltip="Grid view" />
|
|
515
528
|
<x.Toggle id="flow" label="Flow" />
|
|
516
529
|
</AdwToggleGroup>
|
|
517
530
|
```
|
|
518
531
|
|
|
532
|
+
**ToggleGroup props:** `activeName`, `active` (index), `onActiveChanged` (callback with index and name)
|
|
533
|
+
|
|
519
534
|
**Toggle props:** `id` (optional), `label`, `iconName`, `tooltip`, `enabled`
|
|
520
535
|
|
|
521
536
|
### AdwNavigationView
|
|
@@ -525,16 +540,16 @@ Stack-based navigation with history.
|
|
|
525
540
|
const [history, setHistory] = useState(["home"]);
|
|
526
541
|
|
|
527
542
|
<AdwNavigationView history={history} onHistoryChanged={setHistory}>
|
|
528
|
-
<x.NavigationPage id="home" title="Home">
|
|
543
|
+
<x.NavigationPage for={AdwNavigationView} id="home" title="Home">
|
|
529
544
|
<GtkButton label="Go to Details" onClicked={() => setHistory([...history, "details"])} />
|
|
530
545
|
</x.NavigationPage>
|
|
531
|
-
<x.NavigationPage id="details" title="Details" canPop>
|
|
546
|
+
<x.NavigationPage for={AdwNavigationView} id="details" title="Details" canPop>
|
|
532
547
|
<GtkLabel label="Details content" />
|
|
533
548
|
</x.NavigationPage>
|
|
534
549
|
</AdwNavigationView>
|
|
535
550
|
```
|
|
536
551
|
|
|
537
|
-
**NavigationPage props:** `id` (required), `title`, `canPop`. Control navigation via `history` array.
|
|
552
|
+
**NavigationPage props:** `for` (required, parent widget type), `id` (required), `title`, `canPop`. Control navigation via `history` array.
|
|
538
553
|
|
|
539
554
|
### AdwNavigationSplitView
|
|
540
555
|
Sidebar/content split layout for master-detail interfaces.
|
|
@@ -543,20 +558,20 @@ Sidebar/content split layout for master-detail interfaces.
|
|
|
543
558
|
const [selected, setSelected] = useState(items[0]);
|
|
544
559
|
|
|
545
560
|
<AdwNavigationSplitView sidebarWidthFraction={0.33} minSidebarWidth={200} maxSidebarWidth={300}>
|
|
546
|
-
<x.NavigationPage id="sidebar" title="Sidebar">
|
|
561
|
+
<x.NavigationPage for={AdwNavigationSplitView} id="sidebar" title="Sidebar">
|
|
547
562
|
<AdwToolbarView>
|
|
548
563
|
<x.ToolbarTop><AdwHeaderBar /></x.ToolbarTop>
|
|
549
|
-
<GtkListBox cssClasses={["navigation-sidebar"]} onRowSelected={(
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
564
|
+
<GtkListBox cssClasses={["navigation-sidebar"]} onRowSelected={(row) => {
|
|
565
|
+
if (!row) return;
|
|
566
|
+
const item = items[row.getIndex()];
|
|
567
|
+
if (item) setSelected(item);
|
|
568
|
+
}}>
|
|
554
569
|
{items.map((item) => <AdwActionRow key={item.id} title={item.title} />)}
|
|
555
570
|
</GtkListBox>
|
|
556
571
|
</AdwToolbarView>
|
|
557
572
|
</x.NavigationPage>
|
|
558
573
|
|
|
559
|
-
<x.NavigationPage id="content" title={selected?.title ?? ""}>
|
|
574
|
+
<x.NavigationPage for={AdwNavigationSplitView} id="content" title={selected?.title ?? ""}>
|
|
560
575
|
<AdwToolbarView>
|
|
561
576
|
<x.ToolbarTop><AdwHeaderBar /></x.ToolbarTop>
|
|
562
577
|
<GtkLabel label={selected?.title ?? ""} />
|
|
@@ -566,9 +581,40 @@ const [selected, setSelected] = useState(items[0]);
|
|
|
566
581
|
```
|
|
567
582
|
|
|
568
583
|
**Props:** `sidebarWidthFraction`, `minSidebarWidth`, `maxSidebarWidth`, `collapsed`, `showContent`.
|
|
569
|
-
**NavigationPage
|
|
584
|
+
**NavigationPage:** Use `for={AdwNavigationSplitView}` with `id="sidebar"` for left pane, `id="content"` for right pane.
|
|
570
585
|
**Selection:** Use `GtkListBox` with `onRowSelected` (single click) not `onRowActivated` (double click).
|
|
571
586
|
|
|
587
|
+
### AdwAlertDialog
|
|
588
|
+
Modern modal alert dialogs with response buttons.
|
|
589
|
+
|
|
590
|
+
```tsx
|
|
591
|
+
const [showDialog, setShowDialog] = useState(false);
|
|
592
|
+
|
|
593
|
+
{showDialog && (
|
|
594
|
+
<AdwAlertDialog
|
|
595
|
+
heading="Delete File?"
|
|
596
|
+
body="This action cannot be undone."
|
|
597
|
+
onResponse={(id) => {
|
|
598
|
+
if (id === "delete") handleDelete();
|
|
599
|
+
setShowDialog(false);
|
|
600
|
+
}}
|
|
601
|
+
>
|
|
602
|
+
<x.AlertDialogResponse id="cancel" label="Cancel" />
|
|
603
|
+
<x.AlertDialogResponse id="delete" label="Delete" appearance={Adw.ResponseAppearance.DESTRUCTIVE} />
|
|
604
|
+
</AdwAlertDialog>
|
|
605
|
+
)}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
**AlertDialogResponse props:** `id`, `label`, `appearance` (SUGGESTED, DESTRUCTIVE), `enabled`
|
|
609
|
+
|
|
610
|
+
### GtkColorDialogButton / GtkFontDialogButton
|
|
611
|
+
Color and font picker dialogs.
|
|
612
|
+
|
|
613
|
+
```tsx
|
|
614
|
+
<GtkColorDialogButton rgba={color} onRgbaChanged={setColor} title="Select Color" modal withAlpha />
|
|
615
|
+
<GtkFontDialogButton fontDesc={font} onFontDescChanged={setFont} title="Select Font" modal useFont useSize />
|
|
616
|
+
```
|
|
617
|
+
|
|
572
618
|
### Other Adwaita Widgets
|
|
573
619
|
|
|
574
620
|
| Widget | Description |
|
|
@@ -581,6 +627,26 @@ const [selected, setSelected] = useState(items[0]);
|
|
|
581
627
|
|
|
582
628
|
---
|
|
583
629
|
|
|
630
|
+
## Animations
|
|
631
|
+
|
|
632
|
+
Wrap widgets in `x.Animation` for declarative animations with spring or timed transitions:
|
|
633
|
+
|
|
634
|
+
```tsx
|
|
635
|
+
<x.Animation
|
|
636
|
+
initial={{ opacity: 0, scaleX: 0.8 }}
|
|
637
|
+
animate={{ opacity: 1, scaleX: 1 }}
|
|
638
|
+
transition={{ type: "spring", stiffness: 300, damping: 20 }}
|
|
639
|
+
onAnimationComplete={() => console.log("done")}
|
|
640
|
+
>
|
|
641
|
+
<GtkBox>...</GtkBox>
|
|
642
|
+
</x.Animation>
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
**Props:** `initial`, `animate`, `transition`, `onAnimationComplete`
|
|
646
|
+
**Transition types:** `{ type: "spring", stiffness, damping }` or `{ type: "timed", duration, easing }`
|
|
647
|
+
|
|
648
|
+
---
|
|
649
|
+
|
|
584
650
|
## Drag and Drop
|
|
585
651
|
|
|
586
652
|
All widgets support drag-and-drop through props. Use `onDragPrepare`, `onDragBegin`, and `onDragEnd` to make a widget draggable, and `dropTypes`, `onDrop`, `onDropEnter`, and `onDropLeave` to accept drops.
|
|
@@ -588,28 +654,23 @@ All widgets support drag-and-drop through props. Use `onDragPrepare`, `onDragBeg
|
|
|
588
654
|
```tsx
|
|
589
655
|
import * as Gdk from "@gtkx/ffi/gdk";
|
|
590
656
|
import { Type, Value } from "@gtkx/ffi/gobject";
|
|
591
|
-
import { GtkButton, GtkBox, GtkLabel } from "@gtkx/react";
|
|
592
|
-
import { useState } from "react";
|
|
593
657
|
|
|
594
|
-
const DraggableButton = ({ label }: { label: string }) =>
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
658
|
+
const DraggableButton = ({ label }: { label: string }) => (
|
|
659
|
+
<GtkButton
|
|
660
|
+
label={label}
|
|
661
|
+
onDragPrepare={() => Gdk.ContentProvider.newForValue(Value.newFromString(label))}
|
|
662
|
+
dragIcon={someTexture}
|
|
663
|
+
dragIconHotX={16}
|
|
664
|
+
dragIconHotY={16}
|
|
665
|
+
/>
|
|
666
|
+
);
|
|
602
667
|
|
|
603
668
|
const DropZone = () => {
|
|
604
669
|
const [dropped, setDropped] = useState<string | null>(null);
|
|
605
|
-
|
|
606
670
|
return (
|
|
607
671
|
<GtkBox
|
|
608
672
|
dropTypes={[Type.STRING]}
|
|
609
|
-
onDrop={(value: Value) => {
|
|
610
|
-
setDropped(value.getString());
|
|
611
|
-
return true;
|
|
612
|
-
}}
|
|
673
|
+
onDrop={(value: Value) => { setDropped(value.getString()); return true; }}
|
|
613
674
|
>
|
|
614
675
|
<GtkLabel label={dropped ?? "Drop here"} />
|
|
615
676
|
</GtkBox>
|
|
@@ -617,6 +678,8 @@ const DropZone = () => {
|
|
|
617
678
|
};
|
|
618
679
|
```
|
|
619
680
|
|
|
681
|
+
**Drag source props:** `dragIcon`, `dragIconHotX`, `dragIconHotY`
|
|
682
|
+
|
|
620
683
|
## GValue Factories
|
|
621
684
|
|
|
622
685
|
Create typed values for drag-and-drop and signal emission:
|
|
@@ -639,9 +702,7 @@ Type constants for `dropTypes`: `Type.STRING`, `Type.INT`, `Type.DOUBLE`, `Type.
|
|
|
639
702
|
Render custom graphics with `GtkDrawingArea` using the `onDraw` callback:
|
|
640
703
|
|
|
641
704
|
```tsx
|
|
642
|
-
import { GtkDrawingArea } from "@gtkx/react";
|
|
643
705
|
import type { Context } from "@gtkx/ffi/cairo";
|
|
644
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
645
706
|
|
|
646
707
|
const Canvas = () => {
|
|
647
708
|
const handleDraw = (self: Gtk.DrawingArea, cr: Context, width: number, height: number) => {
|
|
@@ -654,112 +715,96 @@ const Canvas = () => {
|
|
|
654
715
|
};
|
|
655
716
|
```
|
|
656
717
|
|
|
657
|
-
|
|
718
|
+
Add a `GtkGestureDrag` child for interactive drawing. Call `widget.queueDraw()` to trigger redraws.
|
|
658
719
|
|
|
659
|
-
##
|
|
720
|
+
## Event Controllers
|
|
660
721
|
|
|
661
|
-
|
|
722
|
+
Event controllers are added as children to any widget. They are auto-generated from GTK's introspection data.
|
|
662
723
|
|
|
663
724
|
```tsx
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
};
|
|
725
|
+
<GtkBox focusable>
|
|
726
|
+
<GtkEventControllerMotion
|
|
727
|
+
onEnter={(x, y) => console.log("Entered at", x, y)}
|
|
728
|
+
onMotion={(x, y) => setPosition({ x, y })}
|
|
729
|
+
onLeave={() => console.log("Left")}
|
|
730
|
+
/>
|
|
731
|
+
<GtkEventControllerKey
|
|
732
|
+
onKeyPressed={(keyval, keycode, state) => {
|
|
733
|
+
console.log("Key pressed:", keyval);
|
|
734
|
+
return false;
|
|
735
|
+
}}
|
|
736
|
+
/>
|
|
737
|
+
<GtkGestureClick onPressed={(nPress, x, y) => console.log("Clicked")} />
|
|
738
|
+
<GtkLabel label="Hover or type here" />
|
|
739
|
+
</GtkBox>
|
|
680
740
|
```
|
|
681
741
|
|
|
682
|
-
|
|
742
|
+
**Input controllers:** `GtkEventControllerMotion`, `GtkEventControllerKey`, `GtkEventControllerScroll`, `GtkEventControllerFocus`
|
|
683
743
|
|
|
684
|
-
**
|
|
744
|
+
**Gesture controllers:** `GtkGestureClick`, `GtkGestureDrag`, `GtkGestureLongPress`, `GtkGestureZoom`, `GtkGestureRotate`, `GtkGestureSwipe`, `GtkGestureStylus`, `GtkGesturePan`
|
|
685
745
|
|
|
686
|
-
|
|
746
|
+
**Drag-and-drop:** `GtkDragSource`, `GtkDropTarget`, `GtkDropControllerMotion`
|
|
687
747
|
|
|
688
|
-
|
|
748
|
+
## SearchBar
|
|
689
749
|
|
|
690
750
|
```tsx
|
|
691
|
-
|
|
692
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
693
|
-
import { useState } from "react";
|
|
751
|
+
const [searchActive, setSearchActive] = useState(false);
|
|
694
752
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
return (
|
|
699
|
-
<GtkScrolledWindow minContentHeight={200}>
|
|
700
|
-
<GtkTextView wrapMode={Gtk.WrapMode.WORD_CHAR}>
|
|
701
|
-
<x.TextBuffer text={text} enableUndo onTextChanged={setText} />
|
|
702
|
-
</GtkTextView>
|
|
703
|
-
</GtkScrolledWindow>
|
|
704
|
-
);
|
|
705
|
-
};
|
|
753
|
+
<GtkSearchBar searchModeEnabled={searchActive} onSearchModeChanged={setSearchActive}>
|
|
754
|
+
<GtkSearchEntry text={query} onSearchChanged={(entry) => setQuery(entry.getText())} />
|
|
755
|
+
</GtkSearchBar>
|
|
706
756
|
```
|
|
707
757
|
|
|
708
|
-
|
|
758
|
+
The `onSearchModeChanged` callback fires when search mode changes (e.g., user presses Escape).
|
|
709
759
|
|
|
710
|
-
##
|
|
760
|
+
## TextView / SourceView
|
|
711
761
|
|
|
712
|
-
|
|
762
|
+
Text content is provided as direct children. Use `x.TextTag` for formatting and `x.TextAnchor` for embedded widgets.
|
|
713
763
|
|
|
714
764
|
```tsx
|
|
715
|
-
|
|
716
|
-
|
|
765
|
+
<GtkTextView enableUndo onBufferChanged={(text) => console.log(text)}>
|
|
766
|
+
Normal text, <x.TextTag id="bold" weight={Pango.Weight.BOLD}>bold</x.TextTag>, and
|
|
767
|
+
<x.TextAnchor><GtkButton label="Click" /></x.TextAnchor> inline.
|
|
768
|
+
</GtkTextView>
|
|
769
|
+
```
|
|
717
770
|
|
|
718
|
-
|
|
719
|
-
const [code, setCode] = useState('console.log("Hello!");');
|
|
771
|
+
**TextView props:** `enableUndo`, `onBufferChanged`, `onTextInserted`, `onTextDeleted`, `onCanUndoChanged`, `onCanRedoChanged`
|
|
720
772
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
}
|
|
773
|
+
**TextTag props:** `id` (required), `priority`, `foreground`, `background`, `weight`, `style`, `underline`, `strikethrough`, `family`, `size`, `sizePoints`, `scale`, `justification`, `leftMargin`, `rightMargin`, `indent`, `editable`, `invisible`
|
|
774
|
+
|
|
775
|
+
**TextAnchor:** Embeds widgets inline with `children`
|
|
776
|
+
|
|
777
|
+
**TextPaintable:** Embeds images inline with `paintable` prop
|
|
778
|
+
|
|
779
|
+
```tsx
|
|
780
|
+
<GtkSourceView
|
|
781
|
+
showLineNumbers
|
|
782
|
+
highlightCurrentLine
|
|
783
|
+
language="typescript"
|
|
784
|
+
styleScheme="Adwaita-dark"
|
|
785
|
+
highlightSyntax
|
|
786
|
+
highlightMatchingBrackets
|
|
787
|
+
enableUndo
|
|
788
|
+
onBufferChanged={setCode}
|
|
789
|
+
>
|
|
790
|
+
{code}
|
|
791
|
+
</GtkSourceView>
|
|
737
792
|
```
|
|
738
793
|
|
|
739
|
-
**
|
|
794
|
+
**SourceView additional props:** `language`, `styleScheme`, `highlightSyntax`, `highlightMatchingBrackets`, `implicitTrailingNewline`, `onCursorMoved`, `onHighlightUpdated`
|
|
740
795
|
|
|
741
796
|
## Keyboard Shortcuts
|
|
742
797
|
|
|
743
798
|
Attach shortcuts with `x.ShortcutController` and `x.Shortcut`:
|
|
744
799
|
|
|
745
800
|
```tsx
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
return (
|
|
754
|
-
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} focusable>
|
|
755
|
-
<x.ShortcutController scope={Gtk.ShortcutScope.LOCAL}>
|
|
756
|
-
<x.Shortcut trigger="<Control>equal" onActivate={() => setCount((c) => c + 1)} />
|
|
757
|
-
<x.Shortcut trigger="<Control>minus" onActivate={() => setCount((c) => c - 1)} />
|
|
758
|
-
</x.ShortcutController>
|
|
759
|
-
<GtkLabel label={`Count: ${count}`} />
|
|
760
|
-
</GtkBox>
|
|
761
|
-
);
|
|
762
|
-
};
|
|
801
|
+
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} focusable>
|
|
802
|
+
<x.ShortcutController scope={Gtk.ShortcutScope.LOCAL}>
|
|
803
|
+
<x.Shortcut trigger="<Control>equal" onActivate={() => setCount((c) => c + 1)} />
|
|
804
|
+
<x.Shortcut trigger="<Control>minus" onActivate={() => setCount((c) => c - 1)} />
|
|
805
|
+
</x.ShortcutController>
|
|
806
|
+
<GtkLabel label={`Count: ${count}`} />
|
|
807
|
+
</GtkBox>
|
|
763
808
|
```
|
|
764
809
|
|
|
765
810
|
**Scopes:** `LOCAL` (widget focus), `MANAGED` (parent managed), `GLOBAL` (window-wide)
|