@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.
@@ -1,5 +1,5 @@
1
1
  import * as net from "node:net";
2
- import { getNativeId, getNativeObject } from "@gtkx/ffi";
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 getNativeObject(widget.handle, Gtk.Editable).getText() ?? null;
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 app = getApplication();
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
- 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);
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
- switch (arg.type) {
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(arg.value);
374
+ return Value.newFromBoolean(argValue);
362
375
  case "int":
363
- return Value.newFromInt(arg.value);
376
+ return Value.newFromInt(argValue);
364
377
  case "uint":
365
- return Value.newFromUint(arg.value);
378
+ return Value.newFromUint(argValue);
366
379
  case "int64":
367
- return Value.newFromInt64(arg.value);
380
+ return Value.newFromInt64(argValue);
368
381
  case "uint64":
369
- return Value.newFromUint64(arg.value);
382
+ return Value.newFromUint64(argValue);
370
383
  case "float":
371
- return Value.newFromFloat(arg.value);
384
+ return Value.newFromFloat(argValue);
372
385
  case "double":
373
- return Value.newFromDouble(arg.value);
386
+ case "number":
387
+ return Value.newFromDouble(argValue);
374
388
  case "string":
375
- return Value.newFromString(arg.value);
389
+ return Value.newFromString(argValue);
376
390
  default:
377
- throw new McpError(McpErrorCode.INVALID_REQUEST, `Unknown argument type: ${arg.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);
@@ -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.15.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/mcp": "0.15.0",
62
- "@gtkx/react": "0.15.0",
63
- "@gtkx/ffi": "0.15.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.15.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.15.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={(_, 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,36 +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
705
- activeName={view}
706
- onNotify={(group, prop) => {
707
- if (prop === "active-name") {
708
- setView(group.getActiveName() ?? "list");
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="grid">
721
- <GtkLabel label="Grid View Content" />
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="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.
@@ -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,22 +520,17 @@ Input in list row.
500
520
  Segmented button group for mutually exclusive options.
501
521
 
502
522
  ```tsx
503
- const [viewMode, setViewMode] = useState("list");
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={(_, row) => {
550
- if (!row) return;
551
- const item = items[row.getIndex()];
552
- if (item) setSelected(item);
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 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.
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
- return (
596
- <GtkButton
597
- label={label}
598
- onDragPrepare={() => Gdk.ContentProvider.newForValue(Value.newFromString(label))}
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
- Use `onGestureDragBegin`, `onGestureDragUpdate`, `onGestureDragEnd` for interactive drawing. Call `widget.queueDraw()` to trigger redraws.
718
+ Add a `GtkGestureDrag` child for interactive drawing. Call `widget.queueDraw()` to trigger redraws.
658
719
 
659
- ## Adjustment
720
+ ## Event Controllers
660
721
 
661
- Configure adjustable widgets declaratively with `x.Adjustment`:
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
- import { x, GtkScale, GtkBox, GtkLabel } from "@gtkx/react";
665
- import * as Gtk from "@gtkx/ffi/gtk";
666
- import { useState } from "react";
667
-
668
- const VolumeControl = () => {
669
- const [volume, setVolume] = useState(50);
670
-
671
- return (
672
- <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12}>
673
- <GtkScale drawValue hexpand>
674
- <x.Adjustment value={volume} lower={0} upper={100} stepIncrement={1} pageIncrement={10} onValueChanged={setVolume} />
675
- </GtkScale>
676
- <GtkLabel label={`Volume: ${Math.round(volume)}%`} />
677
- </GtkBox>
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
- Works with `GtkScale`, `GtkScrollbar`, `GtkScaleButton`, `GtkSpinButton`, `GtkListBox`.
742
+ **Input controllers:** `GtkEventControllerMotion`, `GtkEventControllerKey`, `GtkEventControllerScroll`, `GtkEventControllerFocus`
683
743
 
684
- **Props:** `value`, `lower`, `upper`, `stepIncrement`, `pageIncrement`, `pageSize`, `onValueChanged`
744
+ **Gesture controllers:** `GtkGestureClick`, `GtkGestureDrag`, `GtkGestureLongPress`, `GtkGestureZoom`, `GtkGestureRotate`, `GtkGestureSwipe`, `GtkGestureStylus`, `GtkGesturePan`
685
745
 
686
- ## TextBuffer
746
+ **Drag-and-drop:** `GtkDragSource`, `GtkDropTarget`, `GtkDropControllerMotion`
687
747
 
688
- Configure `GtkTextView` buffers declaratively with `x.TextBuffer`:
748
+ ## SearchBar
689
749
 
690
750
  ```tsx
691
- import { x, GtkTextView, GtkScrolledWindow } from "@gtkx/react";
692
- import * as Gtk from "@gtkx/ffi/gtk";
693
- import { useState } from "react";
751
+ const [searchActive, setSearchActive] = useState(false);
694
752
 
695
- const TextEditor = () => {
696
- const [text, setText] = useState("Hello, World!");
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
- **Props:** `text`, `enableUndo`, `onTextChanged`, `onCanUndoChanged`, `onCanRedoChanged`
758
+ The `onSearchModeChanged` callback fires when search mode changes (e.g., user presses Escape).
709
759
 
710
- ## SourceBuffer
760
+ ## TextView / SourceView
711
761
 
712
- Configure `GtkSourceView` buffers with syntax highlighting using `x.SourceBuffer`:
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
- import { x, GtkSourceView, GtkScrolledWindow } from "@gtkx/react";
716
- import { useState } from "react";
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
- const CodeEditor = () => {
719
- const [code, setCode] = useState('console.log("Hello!");');
771
+ **TextView props:** `enableUndo`, `onBufferChanged`, `onTextInserted`, `onTextDeleted`, `onCanUndoChanged`, `onCanRedoChanged`
720
772
 
721
- return (
722
- <GtkScrolledWindow minContentHeight={300}>
723
- <GtkSourceView showLineNumbers highlightCurrentLine tabWidth={4}>
724
- <x.SourceBuffer
725
- text={code}
726
- language="typescript"
727
- styleScheme="Adwaita-dark"
728
- highlightSyntax
729
- highlightMatchingBrackets
730
- enableUndo
731
- onTextChanged={setCode}
732
- />
733
- </GtkSourceView>
734
- </GtkScrolledWindow>
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
- **Props:** `text`, `language`, `styleScheme`, `highlightSyntax`, `highlightMatchingBrackets`, `enableUndo`, `onTextChanged`, `onCanUndoChanged`, `onCanRedoChanged`, `onCursorMoved`
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
- import { x, GtkBox, GtkLabel } from "@gtkx/react";
747
- import * as Gtk from "@gtkx/ffi/gtk";
748
- import { useState } from "react";
749
-
750
- const App = () => {
751
- const [count, setCount] = useState(0);
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)