@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.
@@ -1,5 +1,6 @@
1
1
  import * as net from "node:net";
2
- import { getNativeId, getNativeObject } from "@gtkx/ffi";
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 getNativeObject(widget.handle, Gtk.Editable).getText() ?? null;
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 app = getApplication();
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
- widgets = await testing.findAllByRole(app, p.value, p.options);
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
  }
@@ -1,8 +1,8 @@
1
1
  import type { TestingOption } from "./create.js";
2
- export interface TemplateContext {
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.14.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.8",
57
- "citty": "^0.1.6",
58
- "ejs": "^3.1.10",
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/ffi": "0.14.0",
62
- "@gtkx/mcp": "0.14.0",
63
- "@gtkx/react": "0.14.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.51.1",
69
- "@gtkx/testing": "0.14.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.14.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} onSortChange={handleSort}>
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={(_, row) => {
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
- ## Toggle Group View Switcher
692
+ ## Stack with Programmatic Navigation
693
693
 
694
694
  ```tsx
695
695
  import * as Gtk from "@gtkx/ffi/gtk";
696
- import { AdwToggleGroup, GtkBox, GtkLabel, GtkStack, x } from "@gtkx/react";
696
+ import { GtkBox, GtkButton, GtkLabel, GtkStack, x } from "@gtkx/react";
697
697
  import { useState } from "react";
698
698
 
699
- const ViewSwitcher = () => {
700
- const [view, setView] = useState("list");
699
+ const StackNavigation = () => {
700
+ const [page, setPage] = useState("home");
701
701
 
702
702
  return (
703
703
  <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12}>
704
- <AdwToggleGroup active={view} onToggled={setView} halign={Gtk.Align.CENTER}>
705
- <x.Toggle id="list" iconName="view-list-symbolic" tooltip="List view" />
706
- <x.Toggle id="grid" iconName="view-grid-symbolic" tooltip="Grid view" />
707
- </AdwToggleGroup>
708
- <GtkStack page={view} vexpand>
709
- <x.StackPage id="list">
710
- <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" />
711
711
  </x.StackPage>
712
- <x.StackPage id="grid">
713
- <GtkLabel label="Grid View Content" />
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="Middle" />
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} onSortChange={handleSort}>
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 value={count} onValueChanged={(sb) => setCount(sb.getValue())} />
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
- <x.ScaleMark value={0} label="Min" />
279
- <x.ScaleMark value={50} />
280
- <x.ScaleMark value={100} label="Max" />
281
- </GtkScale>
282
- ```
283
-
284
- **ScaleMark props:** `value` (required), `position`, `label`
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 onDaySelected={(cal) => setDate(cal.getDate())}>
291
- <x.CalendarMark day={15} />
292
- <x.CalendarMark day={20} />
293
- </GtkCalendar>
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 value={0.6}>
301
- <x.LevelBarOffset id="low" value={0.25} />
302
- <x.LevelBarOffset id="high" value={0.75} />
303
- <x.LevelBarOffset id="full" value={1.0} />
304
- </GtkLevelBar>
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
- <AdwToggleGroup active={viewMode} onToggled={setViewMode}>
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={(_, row) => {
541
- if (!row) return;
542
- const item = items[row.getIndex()];
543
- if (item) setSelected(item);
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 slots:** Use `id="sidebar"` for left pane, `id="content"` for right pane.
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 * as GObject from "@gtkx/ffi/gobject";
582
- import { typeFromName } from "@gtkx/ffi/gobject";
583
- import { GtkButton, GtkBox, GtkLabel } from "@gtkx/react";
584
- import { useState } from "react";
585
-
586
- const DraggableButton = ({ label }: { label: string }) => {
587
- const stringType = typeFromName("gchararray");
588
- const value = new GObject.Value();
589
- value.init(stringType);
590
- value.setString(label);
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={[stringType]}
607
- onDrop={(value: GObject.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"]}`