@gtkx/cli 0.13.3 → 0.15.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/dev-server.d.ts
CHANGED
package/dist/dev-server.js
CHANGED
|
@@ -21,7 +21,7 @@ import { swcSsrRefresh } from "./vite-plugin-swc-ssr-refresh.js";
|
|
|
21
21
|
* import { render } from "@gtkx/react";
|
|
22
22
|
*
|
|
23
23
|
* const server = await createDevServer({
|
|
24
|
-
*
|
|
24
|
+
* entry: "./src/dev.tsx",
|
|
25
25
|
* });
|
|
26
26
|
*
|
|
27
27
|
* const mod = await server.ssrLoadModule("./src/dev.tsx");
|
package/dist/mcp-client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as net from "node:net";
|
|
2
2
|
import { getNativeId, getNativeObject } 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";
|
|
@@ -354,7 +355,28 @@ class McpClient {
|
|
|
354
355
|
if (!widget) {
|
|
355
356
|
throw widgetNotFoundError(p.widgetId);
|
|
356
357
|
}
|
|
357
|
-
const signalArgs = (p.args ?? [])
|
|
358
|
+
const signalArgs = (p.args ?? []).map((arg) => {
|
|
359
|
+
switch (arg.type) {
|
|
360
|
+
case "boolean":
|
|
361
|
+
return Value.newFromBoolean(arg.value);
|
|
362
|
+
case "int":
|
|
363
|
+
return Value.newFromInt(arg.value);
|
|
364
|
+
case "uint":
|
|
365
|
+
return Value.newFromUint(arg.value);
|
|
366
|
+
case "int64":
|
|
367
|
+
return Value.newFromInt64(arg.value);
|
|
368
|
+
case "uint64":
|
|
369
|
+
return Value.newFromUint64(arg.value);
|
|
370
|
+
case "float":
|
|
371
|
+
return Value.newFromFloat(arg.value);
|
|
372
|
+
case "double":
|
|
373
|
+
return Value.newFromDouble(arg.value);
|
|
374
|
+
case "string":
|
|
375
|
+
return Value.newFromString(arg.value);
|
|
376
|
+
default:
|
|
377
|
+
throw new McpError(McpErrorCode.INVALID_REQUEST, `Unknown argument type: ${arg.type}`);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
358
380
|
await testing.fireEvent(widget, p.signal, ...signalArgs);
|
|
359
381
|
return { success: true };
|
|
360
382
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtkx/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "CLI for GTKX - create and develop GTK4 React applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gtkx",
|
|
@@ -58,19 +58,19 @@
|
|
|
58
58
|
"ejs": "^3.1.10",
|
|
59
59
|
"react-refresh": "^0.18.0",
|
|
60
60
|
"vite": "^7.3.1",
|
|
61
|
-
"@gtkx/mcp": "0.
|
|
62
|
-
"@gtkx/
|
|
63
|
-
"@gtkx/
|
|
61
|
+
"@gtkx/mcp": "0.15.0",
|
|
62
|
+
"@gtkx/react": "0.15.0",
|
|
63
|
+
"@gtkx/ffi": "0.15.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/ejs": "^3.1.5",
|
|
67
67
|
"@types/react-refresh": "^0.14.7",
|
|
68
68
|
"memfs": "^4.51.1",
|
|
69
|
-
"@gtkx/testing": "0.
|
|
69
|
+
"@gtkx/testing": "0.15.0"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
72
|
"react": "^19",
|
|
73
|
-
"@gtkx/testing": "0.
|
|
73
|
+
"@gtkx/testing": "0.15.0"
|
|
74
74
|
},
|
|
75
75
|
"peerDependenciesMeta": {
|
|
76
76
|
"@gtkx/testing": {
|
|
@@ -127,14 +127,14 @@ const TodoList = () => {
|
|
|
127
127
|
|
|
128
128
|
return (
|
|
129
129
|
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} marginStart={16} marginEnd={16} marginTop={16} marginBottom={16}>
|
|
130
|
-
<GtkBox
|
|
130
|
+
<GtkBox spacing={8}>
|
|
131
131
|
<GtkEntry text={input} onChanged={(e) => setInput(e.getText())} hexpand placeholderText="New todo..." />
|
|
132
132
|
<GtkButton label="Add" onClicked={addTodo} cssClasses={["suggested-action"]} />
|
|
133
133
|
</GtkBox>
|
|
134
134
|
<GtkScrolledWindow vexpand cssClasses={["card"]}>
|
|
135
135
|
<x.ListView<Todo>
|
|
136
136
|
renderItem={(todo) => (
|
|
137
|
-
<GtkBox
|
|
137
|
+
<GtkBox spacing={8} marginStart={12} marginEnd={12} marginTop={8} marginBottom={8}>
|
|
138
138
|
<GtkLabel label={todo?.text ?? ""} hexpand halign={Gtk.Align.START} />
|
|
139
139
|
<GtkButton iconName="edit-delete-symbolic" cssClasses={["flat"]} onClicked={() => todo && deleteTodo(todo.id)} />
|
|
140
140
|
</GtkBox>
|
|
@@ -176,7 +176,7 @@ const SidebarNav = () => {
|
|
|
176
176
|
const [currentPage, setCurrentPage] = useState("home");
|
|
177
177
|
|
|
178
178
|
return (
|
|
179
|
-
<GtkPaned
|
|
179
|
+
<GtkPaned position={200}>
|
|
180
180
|
<x.Slot for={GtkPaned} id="startChild">
|
|
181
181
|
<GtkScrolledWindow cssClasses={["sidebar"]}>
|
|
182
182
|
<x.ListView<Page>
|
|
@@ -287,7 +287,7 @@ const SettingsPage = () => {
|
|
|
287
287
|
<AdwExpanderRow title="Notification Settings" subtitle="Configure alerts">
|
|
288
288
|
<AdwSwitchRow title="Sound" active />
|
|
289
289
|
<AdwSwitchRow title="Badges" active />
|
|
290
|
-
<AdwSwitchRow title="Lock Screen"
|
|
290
|
+
<AdwSwitchRow title="Lock Screen" />
|
|
291
291
|
</AdwExpanderRow>
|
|
292
292
|
</AdwPreferencesGroup>
|
|
293
293
|
</AdwPreferencesPage>
|
|
@@ -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 ?? ""} />} />
|
|
@@ -668,7 +668,7 @@ const FileBrowser = () => {
|
|
|
668
668
|
selected={selected ? [selected] : []}
|
|
669
669
|
onSelectionChanged={(ids) => setSelected(ids[0] ?? null)}
|
|
670
670
|
renderItem={(item) => (
|
|
671
|
-
<GtkBox
|
|
671
|
+
<GtkBox spacing={8}>
|
|
672
672
|
<GtkImage iconName={item?.isDirectory ? "folder-symbolic" : "text-x-generic-symbolic"} />
|
|
673
673
|
<GtkLabel label={item?.name ?? ""} halign={Gtk.Align.START} />
|
|
674
674
|
</GtkBox>
|
|
@@ -701,7 +701,15 @@ const ViewSwitcher = () => {
|
|
|
701
701
|
|
|
702
702
|
return (
|
|
703
703
|
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12}>
|
|
704
|
-
<AdwToggleGroup
|
|
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
|
+
>
|
|
705
713
|
<x.Toggle id="list" iconName="view-list-symbolic" tooltip="List view" />
|
|
706
714
|
<x.Toggle id="grid" iconName="view-grid-symbolic" tooltip="Grid view" />
|
|
707
715
|
</AdwToggleGroup>
|
|
@@ -52,7 +52,7 @@ GTK signals map to `on<SignalName>` props: `clicked` → `onClicked`, `toggled`
|
|
|
52
52
|
Some widgets require children in specific slots:
|
|
53
53
|
|
|
54
54
|
```tsx
|
|
55
|
-
<GtkPaned
|
|
55
|
+
<GtkPaned>
|
|
56
56
|
<x.Slot for={GtkPaned} id="startChild"><Sidebar /></x.Slot>
|
|
57
57
|
<x.Slot for={GtkPaned} id="endChild"><Content /></x.Slot>
|
|
58
58
|
</GtkPaned>
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
Linear layout (horizontal or vertical).
|
|
21
21
|
|
|
22
22
|
```tsx
|
|
23
|
-
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12}
|
|
23
|
+
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12}>
|
|
24
24
|
{children}
|
|
25
25
|
</GtkBox>
|
|
26
26
|
```
|
|
@@ -68,7 +68,7 @@ Custom tab widget:
|
|
|
68
68
|
```tsx
|
|
69
69
|
<x.NotebookPage>
|
|
70
70
|
<x.NotebookPageTab>
|
|
71
|
-
<GtkBox
|
|
71
|
+
<GtkBox spacing={4}>
|
|
72
72
|
<GtkImage iconName="folder-symbolic" />
|
|
73
73
|
<GtkLabel label="Files" />
|
|
74
74
|
</GtkBox>
|
|
@@ -81,7 +81,7 @@ Custom tab widget:
|
|
|
81
81
|
Resizable split with draggable divider. **Requires Slot components.**
|
|
82
82
|
|
|
83
83
|
```tsx
|
|
84
|
-
<GtkPaned
|
|
84
|
+
<GtkPaned position={280} shrinkStartChild={false}>
|
|
85
85
|
<x.Slot for={GtkPaned} id="startChild"><Sidebar /></x.Slot>
|
|
86
86
|
<x.Slot for={GtkPaned} id="endChild"><MainContent /></x.Slot>
|
|
87
87
|
</GtkPaned>
|
|
@@ -99,6 +99,22 @@ Stack widgets on top of each other. First child is base layer, additional childr
|
|
|
99
99
|
</GtkOverlay>
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
+
### GtkFixed
|
|
103
|
+
Absolute positioning. Use `x.FixedChild` wrapper for children.
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
<GtkFixed>
|
|
107
|
+
<x.FixedChild x={20} y={30}>
|
|
108
|
+
<GtkLabel label="Top Left" />
|
|
109
|
+
</x.FixedChild>
|
|
110
|
+
<x.FixedChild x={200} y={100}>
|
|
111
|
+
<GtkLabel label="Middle" />
|
|
112
|
+
</x.FixedChild>
|
|
113
|
+
</GtkFixed>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**FixedChild props:** `x`, `y` (pixel coordinates)
|
|
117
|
+
|
|
102
118
|
### GtkScrolledWindow
|
|
103
119
|
Scrollable container.
|
|
104
120
|
|
|
@@ -153,7 +169,7 @@ Grid-based virtual scrolling.
|
|
|
153
169
|
Table with sortable columns.
|
|
154
170
|
|
|
155
171
|
```tsx
|
|
156
|
-
<GtkColumnView estimatedRowHeight={48} sortColumn="name" sortOrder={Gtk.SortType.ASCENDING}
|
|
172
|
+
<GtkColumnView estimatedRowHeight={48} sortColumn="name" sortOrder={Gtk.SortType.ASCENDING} onSortChanged={handleSort}>
|
|
157
173
|
<x.ColumnViewColumn<Item>
|
|
158
174
|
title="Name"
|
|
159
175
|
id="name"
|
|
@@ -193,7 +209,7 @@ Hierarchical tree with expand/collapse.
|
|
|
193
209
|
selected={selectedId ? [selectedId] : []}
|
|
194
210
|
onSelectionChanged={(ids) => setSelectedId(ids[0])}
|
|
195
211
|
renderItem={(item, row) => (
|
|
196
|
-
<GtkBox
|
|
212
|
+
<GtkBox spacing={8}>
|
|
197
213
|
<GtkImage iconName={item?.isDirectory ? "folder-symbolic" : "text-x-generic-symbolic"} />
|
|
198
214
|
<GtkLabel label={item?.name ?? ""} />
|
|
199
215
|
</GtkBox>
|
|
@@ -256,7 +272,6 @@ Slider with optional marks.
|
|
|
256
272
|
|
|
257
273
|
```tsx
|
|
258
274
|
<GtkScale
|
|
259
|
-
orientation={Gtk.Orientation.HORIZONTAL}
|
|
260
275
|
drawValue
|
|
261
276
|
valuePos={Gtk.PositionType.TOP}
|
|
262
277
|
>
|
|
@@ -466,7 +481,7 @@ Expandable settings row with optional action widget.
|
|
|
466
481
|
</x.ExpanderRowAction>
|
|
467
482
|
<x.ExpanderRowRow>
|
|
468
483
|
<AdwSwitchRow title="Option 1" active />
|
|
469
|
-
<AdwSwitchRow title="Option 2"
|
|
484
|
+
<AdwSwitchRow title="Option 2" />
|
|
470
485
|
</x.ExpanderRowRow>
|
|
471
486
|
</AdwExpanderRow>
|
|
472
487
|
```
|
|
@@ -485,7 +500,16 @@ Input in list row.
|
|
|
485
500
|
Segmented button group for mutually exclusive options.
|
|
486
501
|
|
|
487
502
|
```tsx
|
|
488
|
-
|
|
503
|
+
const [viewMode, setViewMode] = useState("list");
|
|
504
|
+
|
|
505
|
+
<AdwToggleGroup
|
|
506
|
+
activeName={viewMode}
|
|
507
|
+
onNotify={(group, prop) => {
|
|
508
|
+
if (prop === "active-name") {
|
|
509
|
+
setViewMode(group.getActiveName() ?? "list");
|
|
510
|
+
}
|
|
511
|
+
}}
|
|
512
|
+
>
|
|
489
513
|
<x.Toggle id="list" iconName="view-list-symbolic" tooltip="List view" />
|
|
490
514
|
<x.Toggle id="grid" iconName="view-grid-symbolic" tooltip="Grid view" />
|
|
491
515
|
<x.Toggle id="flow" label="Flow" />
|
|
@@ -554,3 +578,192 @@ const [selected, setSelected] = useState(items[0]);
|
|
|
554
578
|
| `AdwSpinner` | Loading indicator |
|
|
555
579
|
| `AdwWindowTitle` | Title + subtitle for header bars |
|
|
556
580
|
| `AdwButtonRow` | Button styled as list row |
|
|
581
|
+
|
|
582
|
+
---
|
|
583
|
+
|
|
584
|
+
## Drag and Drop
|
|
585
|
+
|
|
586
|
+
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.
|
|
587
|
+
|
|
588
|
+
```tsx
|
|
589
|
+
import * as Gdk from "@gtkx/ffi/gdk";
|
|
590
|
+
import { Type, Value } from "@gtkx/ffi/gobject";
|
|
591
|
+
import { GtkButton, GtkBox, GtkLabel } from "@gtkx/react";
|
|
592
|
+
import { useState } from "react";
|
|
593
|
+
|
|
594
|
+
const DraggableButton = ({ label }: { label: string }) => {
|
|
595
|
+
return (
|
|
596
|
+
<GtkButton
|
|
597
|
+
label={label}
|
|
598
|
+
onDragPrepare={() => Gdk.ContentProvider.newForValue(Value.newFromString(label))}
|
|
599
|
+
/>
|
|
600
|
+
);
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const DropZone = () => {
|
|
604
|
+
const [dropped, setDropped] = useState<string | null>(null);
|
|
605
|
+
|
|
606
|
+
return (
|
|
607
|
+
<GtkBox
|
|
608
|
+
dropTypes={[Type.STRING]}
|
|
609
|
+
onDrop={(value: Value) => {
|
|
610
|
+
setDropped(value.getString());
|
|
611
|
+
return true;
|
|
612
|
+
}}
|
|
613
|
+
>
|
|
614
|
+
<GtkLabel label={dropped ?? "Drop here"} />
|
|
615
|
+
</GtkBox>
|
|
616
|
+
);
|
|
617
|
+
};
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
## GValue Factories
|
|
621
|
+
|
|
622
|
+
Create typed values for drag-and-drop and signal emission:
|
|
623
|
+
|
|
624
|
+
| Factory | Description |
|
|
625
|
+
| ------------------------------ | ----------------------------- |
|
|
626
|
+
| `Value.newFromString(str)` | String values |
|
|
627
|
+
| `Value.newFromDouble(num)` | 64-bit floating point |
|
|
628
|
+
| `Value.newFromInt(num)` | 32-bit signed integer |
|
|
629
|
+
| `Value.newFromBoolean(bool)` | Boolean values |
|
|
630
|
+
| `Value.newFromObject(obj)` | GObject instances |
|
|
631
|
+
| `Value.newFromBoxed(boxed)` | Boxed types (Gdk.RGBA, etc.) |
|
|
632
|
+
| `Value.newFromEnum(gtype, n)` | Enum values (requires GType) |
|
|
633
|
+
| `Value.newFromFlags(gtype, n)` | Flags values (requires GType) |
|
|
634
|
+
|
|
635
|
+
Type constants for `dropTypes`: `Type.STRING`, `Type.INT`, `Type.DOUBLE`, `Type.BOOLEAN`, `Type.OBJECT`.
|
|
636
|
+
|
|
637
|
+
## Custom Drawing
|
|
638
|
+
|
|
639
|
+
Render custom graphics with `GtkDrawingArea` using the `onDraw` callback:
|
|
640
|
+
|
|
641
|
+
```tsx
|
|
642
|
+
import { GtkDrawingArea } from "@gtkx/react";
|
|
643
|
+
import type { Context } from "@gtkx/ffi/cairo";
|
|
644
|
+
import * as Gtk from "@gtkx/ffi/gtk";
|
|
645
|
+
|
|
646
|
+
const Canvas = () => {
|
|
647
|
+
const handleDraw = (self: Gtk.DrawingArea, cr: Context, width: number, height: number) => {
|
|
648
|
+
cr.setSourceRgb(0.2, 0.4, 0.8);
|
|
649
|
+
cr.rectangle(10, 10, width - 20, height - 20);
|
|
650
|
+
cr.fill();
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
return <GtkDrawingArea contentWidth={400} contentHeight={300} onDraw={handleDraw} />;
|
|
654
|
+
};
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
Use `onGestureDragBegin`, `onGestureDragUpdate`, `onGestureDragEnd` for interactive drawing. Call `widget.queueDraw()` to trigger redraws.
|
|
658
|
+
|
|
659
|
+
## Adjustment
|
|
660
|
+
|
|
661
|
+
Configure adjustable widgets declaratively with `x.Adjustment`:
|
|
662
|
+
|
|
663
|
+
```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
|
+
};
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
Works with `GtkScale`, `GtkScrollbar`, `GtkScaleButton`, `GtkSpinButton`, `GtkListBox`.
|
|
683
|
+
|
|
684
|
+
**Props:** `value`, `lower`, `upper`, `stepIncrement`, `pageIncrement`, `pageSize`, `onValueChanged`
|
|
685
|
+
|
|
686
|
+
## TextBuffer
|
|
687
|
+
|
|
688
|
+
Configure `GtkTextView` buffers declaratively with `x.TextBuffer`:
|
|
689
|
+
|
|
690
|
+
```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!");
|
|
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
|
+
};
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
**Props:** `text`, `enableUndo`, `onTextChanged`, `onCanUndoChanged`, `onCanRedoChanged`
|
|
709
|
+
|
|
710
|
+
## SourceBuffer
|
|
711
|
+
|
|
712
|
+
Configure `GtkSourceView` buffers with syntax highlighting using `x.SourceBuffer`:
|
|
713
|
+
|
|
714
|
+
```tsx
|
|
715
|
+
import { x, GtkSourceView, GtkScrolledWindow } from "@gtkx/react";
|
|
716
|
+
import { useState } from "react";
|
|
717
|
+
|
|
718
|
+
const CodeEditor = () => {
|
|
719
|
+
const [code, setCode] = useState('console.log("Hello!");');
|
|
720
|
+
|
|
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
|
+
};
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
**Props:** `text`, `language`, `styleScheme`, `highlightSyntax`, `highlightMatchingBrackets`, `enableUndo`, `onTextChanged`, `onCanUndoChanged`, `onCanRedoChanged`, `onCursorMoved`
|
|
740
|
+
|
|
741
|
+
## Keyboard Shortcuts
|
|
742
|
+
|
|
743
|
+
Attach shortcuts with `x.ShortcutController` and `x.Shortcut`:
|
|
744
|
+
|
|
745
|
+
```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
|
+
};
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
**Scopes:** `LOCAL` (widget focus), `MANAGED` (parent managed), `GLOBAL` (window-wide)
|
|
766
|
+
|
|
767
|
+
**Trigger syntax:** `<Control>s`, `<Control><Shift>s`, `<Alt>F4`, `<Primary>q`, `F5`
|
|
768
|
+
|
|
769
|
+
**Multiple triggers:** `trigger={["F5", "<Control>r"]}`
|