@gtkx/cli 0.14.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 +45 -9
- package/dist/templates.d.ts +2 -2
- package/package.json +10 -10
- package/templates/claude/EXAMPLES.md.ejs +19 -19
- package/templates/claude/WIDGETS.md.ejs +264 -65
package/dist/mcp-client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as net from "node:net";
|
|
2
|
-
import { getNativeId,
|
|
2
|
+
import { getNativeId, getNativeInterface } from "@gtkx/ffi";
|
|
3
|
+
import { Value } from "@gtkx/ffi/gobject";
|
|
3
4
|
import * as Gtk from "@gtkx/ffi/gtk";
|
|
4
5
|
import { DEFAULT_SOCKET_PATH, IpcRequestSchema, IpcResponseSchema, McpError, McpErrorCode, methodNotFoundError, widgetNotFoundError, } from "@gtkx/mcp";
|
|
5
6
|
import { getApplication } from "@gtkx/react";
|
|
@@ -45,7 +46,7 @@ const getWidgetText = (widget) => {
|
|
|
45
46
|
case Gtk.AccessibleRole.TEXT_BOX:
|
|
46
47
|
case Gtk.AccessibleRole.SEARCH_BOX:
|
|
47
48
|
case Gtk.AccessibleRole.SPIN_BUTTON:
|
|
48
|
-
return
|
|
49
|
+
return getNativeInterface(widget, Gtk.Editable)?.getText() ?? null;
|
|
49
50
|
case Gtk.AccessibleRole.GROUP:
|
|
50
51
|
return widget.getLabel?.() ?? null;
|
|
51
52
|
case Gtk.AccessibleRole.WINDOW:
|
|
@@ -92,10 +93,7 @@ const getWidgetById = (id) => {
|
|
|
92
93
|
};
|
|
93
94
|
const refreshWidgetRegistry = () => {
|
|
94
95
|
widgetRegistry.clear();
|
|
95
|
-
const
|
|
96
|
-
if (!app)
|
|
97
|
-
return;
|
|
98
|
-
const windows = app.getWindows();
|
|
96
|
+
const windows = Gtk.Window.listToplevels();
|
|
99
97
|
for (const window of windows) {
|
|
100
98
|
registerWidgets(window);
|
|
101
99
|
}
|
|
@@ -290,6 +288,15 @@ class McpClient {
|
|
|
290
288
|
}
|
|
291
289
|
refreshWidgetRegistry();
|
|
292
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
|
+
}
|
|
293
300
|
case "widget.getTree": {
|
|
294
301
|
const testing = await loadTestingModule();
|
|
295
302
|
return { tree: testing.prettyWidget(app, { includeIds: true, highlight: false }) };
|
|
@@ -299,9 +306,13 @@ class McpClient {
|
|
|
299
306
|
const p = params;
|
|
300
307
|
let widgets = [];
|
|
301
308
|
switch (p.queryType) {
|
|
302
|
-
case "role":
|
|
303
|
-
|
|
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);
|
|
304
314
|
break;
|
|
315
|
+
}
|
|
305
316
|
case "text":
|
|
306
317
|
widgets = await testing.findAllByText(app, String(p.value), p.options);
|
|
307
318
|
break;
|
|
@@ -354,7 +365,32 @@ class McpClient {
|
|
|
354
365
|
if (!widget) {
|
|
355
366
|
throw widgetNotFoundError(p.widgetId);
|
|
356
367
|
}
|
|
357
|
-
const signalArgs = (p.args ?? [])
|
|
368
|
+
const signalArgs = (p.args ?? []).map((arg) => {
|
|
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) {
|
|
373
|
+
case "boolean":
|
|
374
|
+
return Value.newFromBoolean(argValue);
|
|
375
|
+
case "int":
|
|
376
|
+
return Value.newFromInt(argValue);
|
|
377
|
+
case "uint":
|
|
378
|
+
return Value.newFromUint(argValue);
|
|
379
|
+
case "int64":
|
|
380
|
+
return Value.newFromInt64(argValue);
|
|
381
|
+
case "uint64":
|
|
382
|
+
return Value.newFromUint64(argValue);
|
|
383
|
+
case "float":
|
|
384
|
+
return Value.newFromFloat(argValue);
|
|
385
|
+
case "double":
|
|
386
|
+
case "number":
|
|
387
|
+
return Value.newFromDouble(argValue);
|
|
388
|
+
case "string":
|
|
389
|
+
return Value.newFromString(argValue);
|
|
390
|
+
default:
|
|
391
|
+
throw new McpError(McpErrorCode.INVALID_REQUEST, `Unknown argument type: ${argType}`);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
358
394
|
await testing.fireEvent(widget, p.signal, ...signalArgs);
|
|
359
395
|
return { success: true };
|
|
360
396
|
}
|
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/
|
|
62
|
-
"@gtkx/
|
|
63
|
-
"@gtkx/react": "0.
|
|
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": {
|
|
@@ -339,7 +339,7 @@ const FileTable = () => {
|
|
|
339
339
|
|
|
340
340
|
return (
|
|
341
341
|
<GtkScrolledWindow vexpand cssClasses={["card"]}>
|
|
342
|
-
<GtkColumnView estimatedRowHeight={48} sortColumn={sortColumn} sortOrder={sortOrder}
|
|
342
|
+
<GtkColumnView estimatedRowHeight={48} sortColumn={sortColumn} sortOrder={sortOrder} onSortChanged={handleSort}>
|
|
343
343
|
<x.ColumnViewColumn<FileItem> title="Name" id="name" expand sortable renderCell={(f) => <GtkLabel label={f?.name ?? ""} />} />
|
|
344
344
|
<x.ColumnViewColumn<FileItem> title="Size" id="size" fixedWidth={100} sortable renderCell={(f) => <GtkLabel label={`${f?.size ?? 0} KB`} />} />
|
|
345
345
|
<x.ColumnViewColumn<FileItem> title="Modified" id="modified" fixedWidth={120} sortable renderCell={(f) => <GtkLabel label={f?.modified ?? ""} />} />
|
|
@@ -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,28 +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
|
-
<GtkStack page={
|
|
709
|
-
<x.StackPage id="
|
|
710
|
-
<GtkLabel label="
|
|
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" />
|
|
711
711
|
</x.StackPage>
|
|
712
|
-
<x.StackPage id="
|
|
713
|
-
<GtkLabel label="
|
|
712
|
+
<x.StackPage id="settings">
|
|
713
|
+
<GtkLabel label="Settings Content" />
|
|
714
714
|
</x.StackPage>
|
|
715
715
|
</GtkStack>
|
|
716
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.
|
|
@@ -169,7 +171,7 @@ Grid-based virtual scrolling.
|
|
|
169
171
|
Table with sortable columns.
|
|
170
172
|
|
|
171
173
|
```tsx
|
|
172
|
-
<GtkColumnView estimatedRowHeight={48} sortColumn="name" sortOrder={Gtk.SortType.ASCENDING}
|
|
174
|
+
<GtkColumnView estimatedRowHeight={48} sortColumn="name" sortOrder={Gtk.SortType.ASCENDING} onSortChanged={handleSort}>
|
|
173
175
|
<x.ColumnViewColumn<Item>
|
|
174
176
|
title="Name"
|
|
175
177
|
id="name"
|
|
@@ -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,13 +520,17 @@ Input in list row.
|
|
|
500
520
|
Segmented button group for mutually exclusive options.
|
|
501
521
|
|
|
502
522
|
```tsx
|
|
503
|
-
|
|
523
|
+
const [mode, setMode] = useState("list");
|
|
524
|
+
|
|
525
|
+
<AdwToggleGroup activeName={mode} onActiveChanged={(_index, name) => setMode(name ?? "list")}>
|
|
504
526
|
<x.Toggle id="list" iconName="view-list-symbolic" tooltip="List view" />
|
|
505
527
|
<x.Toggle id="grid" iconName="view-grid-symbolic" tooltip="Grid view" />
|
|
506
528
|
<x.Toggle id="flow" label="Flow" />
|
|
507
529
|
</AdwToggleGroup>
|
|
508
530
|
```
|
|
509
531
|
|
|
532
|
+
**ToggleGroup props:** `activeName`, `active` (index), `onActiveChanged` (callback with index and name)
|
|
533
|
+
|
|
510
534
|
**Toggle props:** `id` (optional), `label`, `iconName`, `tooltip`, `enabled`
|
|
511
535
|
|
|
512
536
|
### AdwNavigationView
|
|
@@ -516,16 +540,16 @@ Stack-based navigation with history.
|
|
|
516
540
|
const [history, setHistory] = useState(["home"]);
|
|
517
541
|
|
|
518
542
|
<AdwNavigationView history={history} onHistoryChanged={setHistory}>
|
|
519
|
-
<x.NavigationPage id="home" title="Home">
|
|
543
|
+
<x.NavigationPage for={AdwNavigationView} id="home" title="Home">
|
|
520
544
|
<GtkButton label="Go to Details" onClicked={() => setHistory([...history, "details"])} />
|
|
521
545
|
</x.NavigationPage>
|
|
522
|
-
<x.NavigationPage id="details" title="Details" canPop>
|
|
546
|
+
<x.NavigationPage for={AdwNavigationView} id="details" title="Details" canPop>
|
|
523
547
|
<GtkLabel label="Details content" />
|
|
524
548
|
</x.NavigationPage>
|
|
525
549
|
</AdwNavigationView>
|
|
526
550
|
```
|
|
527
551
|
|
|
528
|
-
**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.
|
|
529
553
|
|
|
530
554
|
### AdwNavigationSplitView
|
|
531
555
|
Sidebar/content split layout for master-detail interfaces.
|
|
@@ -534,20 +558,20 @@ Sidebar/content split layout for master-detail interfaces.
|
|
|
534
558
|
const [selected, setSelected] = useState(items[0]);
|
|
535
559
|
|
|
536
560
|
<AdwNavigationSplitView sidebarWidthFraction={0.33} minSidebarWidth={200} maxSidebarWidth={300}>
|
|
537
|
-
<x.NavigationPage id="sidebar" title="Sidebar">
|
|
561
|
+
<x.NavigationPage for={AdwNavigationSplitView} id="sidebar" title="Sidebar">
|
|
538
562
|
<AdwToolbarView>
|
|
539
563
|
<x.ToolbarTop><AdwHeaderBar /></x.ToolbarTop>
|
|
540
|
-
<GtkListBox cssClasses={["navigation-sidebar"]} onRowSelected={(
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
564
|
+
<GtkListBox cssClasses={["navigation-sidebar"]} onRowSelected={(row) => {
|
|
565
|
+
if (!row) return;
|
|
566
|
+
const item = items[row.getIndex()];
|
|
567
|
+
if (item) setSelected(item);
|
|
568
|
+
}}>
|
|
545
569
|
{items.map((item) => <AdwActionRow key={item.id} title={item.title} />)}
|
|
546
570
|
</GtkListBox>
|
|
547
571
|
</AdwToolbarView>
|
|
548
572
|
</x.NavigationPage>
|
|
549
573
|
|
|
550
|
-
<x.NavigationPage id="content" title={selected?.title ?? ""}>
|
|
574
|
+
<x.NavigationPage for={AdwNavigationSplitView} id="content" title={selected?.title ?? ""}>
|
|
551
575
|
<AdwToolbarView>
|
|
552
576
|
<x.ToolbarTop><AdwHeaderBar /></x.ToolbarTop>
|
|
553
577
|
<GtkLabel label={selected?.title ?? ""} />
|
|
@@ -557,9 +581,40 @@ const [selected, setSelected] = useState(items[0]);
|
|
|
557
581
|
```
|
|
558
582
|
|
|
559
583
|
**Props:** `sidebarWidthFraction`, `minSidebarWidth`, `maxSidebarWidth`, `collapsed`, `showContent`.
|
|
560
|
-
**NavigationPage
|
|
584
|
+
**NavigationPage:** Use `for={AdwNavigationSplitView}` with `id="sidebar"` for left pane, `id="content"` for right pane.
|
|
561
585
|
**Selection:** Use `GtkListBox` with `onRowSelected` (single click) not `onRowActivated` (double click).
|
|
562
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
|
+
|
|
563
618
|
### Other Adwaita Widgets
|
|
564
619
|
|
|
565
620
|
| Widget | Description |
|
|
@@ -572,44 +627,188 @@ const [selected, setSelected] = useState(items[0]);
|
|
|
572
627
|
|
|
573
628
|
---
|
|
574
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
|
+
|
|
575
650
|
## Drag and Drop
|
|
576
651
|
|
|
577
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.
|
|
578
653
|
|
|
579
654
|
```tsx
|
|
580
655
|
import * as Gdk from "@gtkx/ffi/gdk";
|
|
581
|
-
import
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
return (
|
|
593
|
-
<GtkButton
|
|
594
|
-
label={label}
|
|
595
|
-
onDragPrepare={() => Gdk.ContentProvider.newForValue(value)}
|
|
596
|
-
/>
|
|
597
|
-
);
|
|
598
|
-
};
|
|
656
|
+
import { Type, Value } from "@gtkx/ffi/gobject";
|
|
657
|
+
|
|
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
|
+
);
|
|
599
667
|
|
|
600
668
|
const DropZone = () => {
|
|
601
669
|
const [dropped, setDropped] = useState<string | null>(null);
|
|
602
|
-
const stringType = typeFromName("gchararray");
|
|
603
|
-
|
|
604
670
|
return (
|
|
605
671
|
<GtkBox
|
|
606
|
-
dropTypes={[
|
|
607
|
-
onDrop={(value:
|
|
608
|
-
setDropped(value.getString());
|
|
609
|
-
return true;
|
|
610
|
-
}}
|
|
672
|
+
dropTypes={[Type.STRING]}
|
|
673
|
+
onDrop={(value: Value) => { setDropped(value.getString()); return true; }}
|
|
611
674
|
>
|
|
612
675
|
<GtkLabel label={dropped ?? "Drop here"} />
|
|
613
676
|
</GtkBox>
|
|
614
677
|
);
|
|
615
678
|
};
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
**Drag source props:** `dragIcon`, `dragIconHotX`, `dragIconHotY`
|
|
682
|
+
|
|
683
|
+
## GValue Factories
|
|
684
|
+
|
|
685
|
+
Create typed values for drag-and-drop and signal emission:
|
|
686
|
+
|
|
687
|
+
| Factory | Description |
|
|
688
|
+
| ------------------------------ | ----------------------------- |
|
|
689
|
+
| `Value.newFromString(str)` | String values |
|
|
690
|
+
| `Value.newFromDouble(num)` | 64-bit floating point |
|
|
691
|
+
| `Value.newFromInt(num)` | 32-bit signed integer |
|
|
692
|
+
| `Value.newFromBoolean(bool)` | Boolean values |
|
|
693
|
+
| `Value.newFromObject(obj)` | GObject instances |
|
|
694
|
+
| `Value.newFromBoxed(boxed)` | Boxed types (Gdk.RGBA, etc.) |
|
|
695
|
+
| `Value.newFromEnum(gtype, n)` | Enum values (requires GType) |
|
|
696
|
+
| `Value.newFromFlags(gtype, n)` | Flags values (requires GType) |
|
|
697
|
+
|
|
698
|
+
Type constants for `dropTypes`: `Type.STRING`, `Type.INT`, `Type.DOUBLE`, `Type.BOOLEAN`, `Type.OBJECT`.
|
|
699
|
+
|
|
700
|
+
## Custom Drawing
|
|
701
|
+
|
|
702
|
+
Render custom graphics with `GtkDrawingArea` using the `onDraw` callback:
|
|
703
|
+
|
|
704
|
+
```tsx
|
|
705
|
+
import type { Context } from "@gtkx/ffi/cairo";
|
|
706
|
+
|
|
707
|
+
const Canvas = () => {
|
|
708
|
+
const handleDraw = (self: Gtk.DrawingArea, cr: Context, width: number, height: number) => {
|
|
709
|
+
cr.setSourceRgb(0.2, 0.4, 0.8);
|
|
710
|
+
cr.rectangle(10, 10, width - 20, height - 20);
|
|
711
|
+
cr.fill();
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
return <GtkDrawingArea contentWidth={400} contentHeight={300} onDraw={handleDraw} />;
|
|
715
|
+
};
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
Add a `GtkGestureDrag` child for interactive drawing. Call `widget.queueDraw()` to trigger redraws.
|
|
719
|
+
|
|
720
|
+
## Event Controllers
|
|
721
|
+
|
|
722
|
+
Event controllers are added as children to any widget. They are auto-generated from GTK's introspection data.
|
|
723
|
+
|
|
724
|
+
```tsx
|
|
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>
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
**Input controllers:** `GtkEventControllerMotion`, `GtkEventControllerKey`, `GtkEventControllerScroll`, `GtkEventControllerFocus`
|
|
743
|
+
|
|
744
|
+
**Gesture controllers:** `GtkGestureClick`, `GtkGestureDrag`, `GtkGestureLongPress`, `GtkGestureZoom`, `GtkGestureRotate`, `GtkGestureSwipe`, `GtkGestureStylus`, `GtkGesturePan`
|
|
745
|
+
|
|
746
|
+
**Drag-and-drop:** `GtkDragSource`, `GtkDropTarget`, `GtkDropControllerMotion`
|
|
747
|
+
|
|
748
|
+
## SearchBar
|
|
749
|
+
|
|
750
|
+
```tsx
|
|
751
|
+
const [searchActive, setSearchActive] = useState(false);
|
|
752
|
+
|
|
753
|
+
<GtkSearchBar searchModeEnabled={searchActive} onSearchModeChanged={setSearchActive}>
|
|
754
|
+
<GtkSearchEntry text={query} onSearchChanged={(entry) => setQuery(entry.getText())} />
|
|
755
|
+
</GtkSearchBar>
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
The `onSearchModeChanged` callback fires when search mode changes (e.g., user presses Escape).
|
|
759
|
+
|
|
760
|
+
## TextView / SourceView
|
|
761
|
+
|
|
762
|
+
Text content is provided as direct children. Use `x.TextTag` for formatting and `x.TextAnchor` for embedded widgets.
|
|
763
|
+
|
|
764
|
+
```tsx
|
|
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
|
+
```
|
|
770
|
+
|
|
771
|
+
**TextView props:** `enableUndo`, `onBufferChanged`, `onTextInserted`, `onTextDeleted`, `onCanUndoChanged`, `onCanRedoChanged`
|
|
772
|
+
|
|
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>
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
**SourceView additional props:** `language`, `styleScheme`, `highlightSyntax`, `highlightMatchingBrackets`, `implicitTrailingNewline`, `onCursorMoved`, `onHighlightUpdated`
|
|
795
|
+
|
|
796
|
+
## Keyboard Shortcuts
|
|
797
|
+
|
|
798
|
+
Attach shortcuts with `x.ShortcutController` and `x.Shortcut`:
|
|
799
|
+
|
|
800
|
+
```tsx
|
|
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>
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
**Scopes:** `LOCAL` (widget focus), `MANAGED` (parent managed), `GLOBAL` (window-wide)
|
|
811
|
+
|
|
812
|
+
**Trigger syntax:** `<Control>s`, `<Control><Shift>s`, `<Alt>F4`, `<Primary>q`, `F5`
|
|
813
|
+
|
|
814
|
+
**Multiple triggers:** `trigger={["F5", "<Control>r"]}`
|