@gtkx/cli 0.15.0 → 0.17.1

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.17.1",
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.17.1",
62
+ "@gtkx/react": "0.17.1",
63
+ "@gtkx/ffi": "0.17.1"
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.10",
69
+ "@gtkx/testing": "0.17.1"
70
70
  },
71
71
  "peerDependencies": {
72
72
  "react": "^19",
73
- "@gtkx/testing": "0.15.0"
73
+ "@gtkx/testing": "0.17.1"
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,39 +689,66 @@ 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>
725
717
  );
726
718
  };
727
719
  ```
720
+
721
+ ---
722
+
723
+ ## Animated Card with Toggle
724
+
725
+ ```tsx
726
+ import * as Gtk from "@gtkx/ffi/gtk";
727
+ import { GtkBox, GtkButton, GtkLabel, x } from "@gtkx/react";
728
+ import { useState } from "react";
729
+
730
+ const AnimatedCard = () => {
731
+ const [visible, setVisible] = useState(true);
732
+
733
+ return (
734
+ <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} marginStart={16} marginEnd={16} marginTop={16} marginBottom={16}>
735
+ <GtkButton label={visible ? "Hide Card" : "Show Card"} onClicked={() => setVisible(!visible)} halign={Gtk.Align.START} />
736
+ {visible && (
737
+ <x.Animation
738
+ mode="spring"
739
+ initial={{ opacity: 0, scale: 0.8, translateY: -20 }}
740
+ animate={{ opacity: 1, scale: 1, translateY: 0 }}
741
+ exit={{ opacity: 0, scale: 0.8, translateY: 20 }}
742
+ transition={{ damping: 0.7, stiffness: 200 }}
743
+ animateOnMount
744
+ >
745
+ <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={8} cssClasses={["card"]} marginStart={12} marginEnd={12} marginTop={8} marginBottom={8}>
746
+ <GtkLabel label="Animated Card" cssClasses={["title-4"]} halign={Gtk.Align.START} />
747
+ <GtkLabel label="This card animates in with a spring effect and fades out when dismissed." wrap />
748
+ </GtkBox>
749
+ </x.Animation>
750
+ )}
751
+ </GtkBox>
752
+ );
753
+ };
754
+ ```
@@ -67,6 +67,20 @@ Some widgets require children in specific slots:
67
67
  </GtkHeaderBar>
68
68
  ```
69
69
 
70
+ ### Animations
71
+
72
+ ```tsx
73
+ <x.Animation
74
+ mode="spring"
75
+ initial={{ opacity: 0, scale: 0.8 }}
76
+ animate={{ opacity: 1, scale: 1 }}
77
+ transition={{ damping: 0.8, stiffness: 200 }}
78
+ animateOnMount
79
+ >
80
+ <GtkLabel label="Animated!" />
81
+ </x.Animation>
82
+ ```
83
+
70
84
  ## Key Constraints
71
85
 
72
86
  - GTK is single-threaded: all widget operations on main thread
@@ -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,31 @@ 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
+ mode="spring"
637
+ initial={{ opacity: 0, scale: 0.8 }}
638
+ animate={{ opacity: 1, scale: 1 }}
639
+ transition={{ damping: 0.8, stiffness: 200, mass: 1 }}
640
+ animateOnMount
641
+ onAnimationComplete={() => console.log("done")}
642
+ >
643
+ <GtkBox>...</GtkBox>
644
+ </x.Animation>
645
+ ```
646
+
647
+ **Props:** `mode` (`"spring"` | `"timed"`), `initial`, `animate`, `exit`, `transition`, `animateOnMount`, `onAnimationStart`, `onAnimationComplete`
648
+
649
+ **Spring transition:** `damping`, `stiffness`, `mass`, `initialVelocity`, `clamp`, `delay`
650
+
651
+ **Timed transition:** `duration`, `easing` (from `Adw.Easing`), `delay`, `repeat`, `reverse`, `alternate`
652
+
653
+ ---
654
+
584
655
  ## Drag and Drop
585
656
 
586
657
  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 +659,23 @@ All widgets support drag-and-drop through props. Use `onDragPrepare`, `onDragBeg
588
659
  ```tsx
589
660
  import * as Gdk from "@gtkx/ffi/gdk";
590
661
  import { Type, Value } from "@gtkx/ffi/gobject";
591
- import { GtkButton, GtkBox, GtkLabel } from "@gtkx/react";
592
- import { useState } from "react";
593
662
 
594
- const DraggableButton = ({ label }: { label: string }) => {
595
- return (
596
- <GtkButton
597
- label={label}
598
- onDragPrepare={() => Gdk.ContentProvider.newForValue(Value.newFromString(label))}
599
- />
600
- );
601
- };
663
+ const DraggableButton = ({ label }: { label: string }) => (
664
+ <GtkButton
665
+ label={label}
666
+ onDragPrepare={() => Gdk.ContentProvider.newForValue(Value.newFromString(label))}
667
+ dragIcon={someTexture}
668
+ dragIconHotX={16}
669
+ dragIconHotY={16}
670
+ />
671
+ );
602
672
 
603
673
  const DropZone = () => {
604
674
  const [dropped, setDropped] = useState<string | null>(null);
605
-
606
675
  return (
607
676
  <GtkBox
608
677
  dropTypes={[Type.STRING]}
609
- onDrop={(value: Value) => {
610
- setDropped(value.getString());
611
- return true;
612
- }}
678
+ onDrop={(value: Value) => { setDropped(value.getString()); return true; }}
613
679
  >
614
680
  <GtkLabel label={dropped ?? "Drop here"} />
615
681
  </GtkBox>
@@ -617,6 +683,8 @@ const DropZone = () => {
617
683
  };
618
684
  ```
619
685
 
686
+ **Drag source props:** `dragIcon`, `dragIconHotX`, `dragIconHotY`
687
+
620
688
  ## GValue Factories
621
689
 
622
690
  Create typed values for drag-and-drop and signal emission:
@@ -639,9 +707,7 @@ Type constants for `dropTypes`: `Type.STRING`, `Type.INT`, `Type.DOUBLE`, `Type.
639
707
  Render custom graphics with `GtkDrawingArea` using the `onDraw` callback:
640
708
 
641
709
  ```tsx
642
- import { GtkDrawingArea } from "@gtkx/react";
643
710
  import type { Context } from "@gtkx/ffi/cairo";
644
- import * as Gtk from "@gtkx/ffi/gtk";
645
711
 
646
712
  const Canvas = () => {
647
713
  const handleDraw = (self: Gtk.DrawingArea, cr: Context, width: number, height: number) => {
@@ -654,112 +720,96 @@ const Canvas = () => {
654
720
  };
655
721
  ```
656
722
 
657
- Use `onGestureDragBegin`, `onGestureDragUpdate`, `onGestureDragEnd` for interactive drawing. Call `widget.queueDraw()` to trigger redraws.
723
+ Add a `GtkGestureDrag` child for interactive drawing. Call `widget.queueDraw()` to trigger redraws.
658
724
 
659
- ## Adjustment
725
+ ## Event Controllers
660
726
 
661
- Configure adjustable widgets declaratively with `x.Adjustment`:
727
+ Event controllers are added as children to any widget. They are auto-generated from GTK's introspection data.
662
728
 
663
729
  ```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
- };
730
+ <GtkBox focusable>
731
+ <GtkEventControllerMotion
732
+ onEnter={(x, y) => console.log("Entered at", x, y)}
733
+ onMotion={(x, y) => setPosition({ x, y })}
734
+ onLeave={() => console.log("Left")}
735
+ />
736
+ <GtkEventControllerKey
737
+ onKeyPressed={(keyval, keycode, state) => {
738
+ console.log("Key pressed:", keyval);
739
+ return false;
740
+ }}
741
+ />
742
+ <GtkGestureClick onPressed={(nPress, x, y) => console.log("Clicked")} />
743
+ <GtkLabel label="Hover or type here" />
744
+ </GtkBox>
680
745
  ```
681
746
 
682
- Works with `GtkScale`, `GtkScrollbar`, `GtkScaleButton`, `GtkSpinButton`, `GtkListBox`.
747
+ **Input controllers:** `GtkEventControllerMotion`, `GtkEventControllerKey`, `GtkEventControllerScroll`, `GtkEventControllerFocus`
683
748
 
684
- **Props:** `value`, `lower`, `upper`, `stepIncrement`, `pageIncrement`, `pageSize`, `onValueChanged`
749
+ **Gesture controllers:** `GtkGestureClick`, `GtkGestureDrag`, `GtkGestureLongPress`, `GtkGestureZoom`, `GtkGestureRotate`, `GtkGestureSwipe`, `GtkGestureStylus`, `GtkGesturePan`
685
750
 
686
- ## TextBuffer
751
+ **Drag-and-drop:** `GtkDragSource`, `GtkDropTarget`, `GtkDropControllerMotion`
687
752
 
688
- Configure `GtkTextView` buffers declaratively with `x.TextBuffer`:
753
+ ## SearchBar
689
754
 
690
755
  ```tsx
691
- import { x, GtkTextView, GtkScrolledWindow } from "@gtkx/react";
692
- import * as Gtk from "@gtkx/ffi/gtk";
693
- import { useState } from "react";
694
-
695
- const TextEditor = () => {
696
- const [text, setText] = useState("Hello, World!");
756
+ const [searchActive, setSearchActive] = useState(false);
697
757
 
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
- };
758
+ <GtkSearchBar searchModeEnabled={searchActive} onSearchModeChanged={setSearchActive}>
759
+ <GtkSearchEntry text={query} onSearchChanged={(entry) => setQuery(entry.getText())} />
760
+ </GtkSearchBar>
706
761
  ```
707
762
 
708
- **Props:** `text`, `enableUndo`, `onTextChanged`, `onCanUndoChanged`, `onCanRedoChanged`
763
+ The `onSearchModeChanged` callback fires when search mode changes (e.g., user presses Escape).
709
764
 
710
- ## SourceBuffer
765
+ ## TextView / SourceView
711
766
 
712
- Configure `GtkSourceView` buffers with syntax highlighting using `x.SourceBuffer`:
767
+ Text content is provided as direct children. Use `x.TextTag` for formatting and `x.TextAnchor` for embedded widgets.
713
768
 
714
769
  ```tsx
715
- import { x, GtkSourceView, GtkScrolledWindow } from "@gtkx/react";
716
- import { useState } from "react";
770
+ <GtkTextView enableUndo onBufferChanged={(text) => console.log(text)}>
771
+ Normal text, <x.TextTag id="bold" weight={Pango.Weight.BOLD}>bold</x.TextTag>, and
772
+ <x.TextAnchor><GtkButton label="Click" /></x.TextAnchor> inline.
773
+ </GtkTextView>
774
+ ```
717
775
 
718
- const CodeEditor = () => {
719
- const [code, setCode] = useState('console.log("Hello!");');
776
+ **TextView props:** `enableUndo`, `onBufferChanged`, `onTextInserted`, `onTextDeleted`, `onCanUndoChanged`, `onCanRedoChanged`
720
777
 
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
- };
778
+ **TextTag props:** `id` (required), `priority`, `foreground`, `background`, `weight`, `style`, `underline`, `strikethrough`, `family`, `size`, `sizePoints`, `scale`, `justification`, `leftMargin`, `rightMargin`, `indent`, `editable`, `invisible`
779
+
780
+ **TextAnchor:** Embeds widgets inline with `children`
781
+
782
+ **TextPaintable:** Embeds images inline with `paintable` prop
783
+
784
+ ```tsx
785
+ <GtkSourceView
786
+ showLineNumbers
787
+ highlightCurrentLine
788
+ language="typescript"
789
+ styleScheme="Adwaita-dark"
790
+ highlightSyntax
791
+ highlightMatchingBrackets
792
+ enableUndo
793
+ onBufferChanged={setCode}
794
+ >
795
+ {code}
796
+ </GtkSourceView>
737
797
  ```
738
798
 
739
- **Props:** `text`, `language`, `styleScheme`, `highlightSyntax`, `highlightMatchingBrackets`, `enableUndo`, `onTextChanged`, `onCanUndoChanged`, `onCanRedoChanged`, `onCursorMoved`
799
+ **SourceView additional props:** `language`, `styleScheme`, `highlightSyntax`, `highlightMatchingBrackets`, `implicitTrailingNewline`, `onCursorMoved`, `onHighlightUpdated`
740
800
 
741
801
  ## Keyboard Shortcuts
742
802
 
743
803
  Attach shortcuts with `x.ShortcutController` and `x.Shortcut`:
744
804
 
745
805
  ```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
- };
806
+ <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} focusable>
807
+ <x.ShortcutController scope={Gtk.ShortcutScope.LOCAL}>
808
+ <x.Shortcut trigger="<Control>equal" onActivate={() => setCount((c) => c + 1)} />
809
+ <x.Shortcut trigger="<Control>minus" onActivate={() => setCount((c) => c - 1)} />
810
+ </x.ShortcutController>
811
+ <GtkLabel label={`Count: ${count}`} />
812
+ </GtkBox>
763
813
  ```
764
814
 
765
815
  **Scopes:** `LOCAL` (widget focus), `MANAGED` (parent managed), `GLOBAL` (window-wide)