@gtkx/react 0.1.46 → 0.1.48

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/node.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as GObject from "@gtkx/ffi/gobject";
2
2
  import * as Gtk from "@gtkx/ffi/gtk";
3
- import { CONSTRUCTOR_PARAMS, SETTER_GETTERS } from "./generated/jsx.js";
3
+ import { CONSTRUCTOR_PARAMS, PROP_SETTERS, SETTER_GETTERS } from "./generated/jsx.js";
4
4
  import { isAppendable, isRemovable, isSingleChild } from "./predicates.js";
5
5
  const extractConstructorArgs = (type, props) => {
6
6
  const params = CONSTRUCTOR_PARAMS[type];
@@ -147,7 +147,9 @@ export class Node {
147
147
  this.signalHandlers.set(eventName, handlerId);
148
148
  }
149
149
  setProperty(widget, key, value) {
150
- const setterName = `set${key.charAt(0).toUpperCase()}${key.slice(1)}`;
150
+ const setterName = PROP_SETTERS[this.widgetType]?.[key];
151
+ if (!setterName)
152
+ return;
151
153
  const setter = widget[setterName];
152
154
  if (typeof setter !== "function")
153
155
  return;
@@ -1,34 +1,58 @@
1
1
  import * as Gtk from "@gtkx/ffi/gtk";
2
2
  import type { Props } from "../factory.js";
3
3
  import { Node } from "../node.js";
4
+ import type { ColumnSortFn } from "../types.js";
4
5
  export declare class ColumnViewNode extends Node<Gtk.ColumnView> {
5
6
  static matches(type: string): boolean;
6
7
  private stringList;
7
8
  private selectionModel;
9
+ private sortListModel;
8
10
  private items;
9
11
  private columns;
10
12
  private committedLength;
13
+ private sortColumn;
14
+ private sortOrder;
15
+ private sortFn;
16
+ private isSorting;
17
+ private onSortChange;
18
+ private sorterChangedHandlerId;
19
+ private lastNotifiedColumn;
20
+ private lastNotifiedOrder;
11
21
  constructor(type: string, props: Props, app: Gtk.Application);
22
+ private connectSorterChangedSignal;
23
+ private waitForSortComplete;
24
+ private disconnectSorterChangedSignal;
25
+ private notifySortChange;
12
26
  getItems(): unknown[];
13
- addColumn(column: ColumnViewColumnNode): void;
27
+ getSortFn(): ColumnSortFn<unknown, string> | null;
28
+ compareItems(a: unknown, b: unknown, columnId: string): number;
29
+ addColumn(columnNode: ColumnViewColumnNode): void;
30
+ private applySortByColumn;
31
+ findColumnById(id: string): ColumnViewColumnNode | undefined;
14
32
  removeColumn(column: ColumnViewColumnNode): void;
15
33
  insertColumnBefore(column: ColumnViewColumnNode, before: ColumnViewColumnNode): void;
16
34
  private syncStringList;
17
35
  addItem(item: unknown): void;
18
36
  insertItemBefore(item: unknown, beforeItem: unknown): void;
19
37
  removeItem(item: unknown): void;
38
+ protected consumedProps(): Set<string>;
39
+ updateProps(oldProps: Props, newProps: Props): void;
20
40
  }
21
41
  export declare class ColumnViewColumnNode extends Node {
22
42
  static matches(type: string): boolean;
23
43
  protected isVirtual(): boolean;
24
- private gtkColumn;
44
+ private column;
25
45
  private factory;
26
46
  private renderCell;
27
47
  private columnView;
28
48
  private listItemCache;
49
+ private columnId;
50
+ private sorter;
29
51
  constructor(type: string, props: Props, app: Gtk.Application);
30
- getGtkColumn(): Gtk.ColumnViewColumn;
52
+ getColumn(): Gtk.ColumnViewColumn;
53
+ getId(): string | null;
31
54
  setColumnView(columnView: ColumnViewNode | null): void;
55
+ updateSorterFromRoot(): void;
32
56
  attachToParent(parent: Node): void;
33
57
  attachToParentBefore(parent: Node, before: Node): void;
34
58
  detachFromParent(parent: Node): void;
@@ -1,4 +1,5 @@
1
1
  import { getObject, getObjectId } from "@gtkx/ffi";
2
+ import * as GObject from "@gtkx/ffi/gobject";
2
3
  import * as Gtk from "@gtkx/ffi/gtk";
3
4
  import { scheduleFlush } from "../batch.js";
4
5
  import { createFiberRoot } from "../fiber-root.js";
@@ -10,29 +11,123 @@ export class ColumnViewNode extends Node {
10
11
  }
11
12
  stringList;
12
13
  selectionModel;
14
+ sortListModel;
13
15
  items = [];
14
16
  columns = [];
15
17
  committedLength = 0;
18
+ sortColumn = null;
19
+ sortOrder = Gtk.SortType.ASCENDING;
20
+ sortFn = null;
21
+ isSorting = false;
22
+ onSortChange = null;
23
+ sorterChangedHandlerId = null;
24
+ lastNotifiedColumn = null;
25
+ lastNotifiedOrder = Gtk.SortType.ASCENDING;
16
26
  constructor(type, props, app) {
17
27
  super(type, props, app);
18
28
  this.stringList = new Gtk.StringList([]);
19
- this.selectionModel = new Gtk.SingleSelection(this.stringList);
29
+ this.sortListModel = new Gtk.SortListModel(this.stringList, this.widget.getSorter());
30
+ this.sortListModel.setIncremental(true);
31
+ this.selectionModel = new Gtk.SingleSelection(this.sortListModel);
20
32
  this.widget.setModel(this.selectionModel);
33
+ this.sortColumn = props.sortColumn ?? null;
34
+ this.sortOrder = props.sortOrder ?? Gtk.SortType.ASCENDING;
35
+ this.sortFn = props.sortFn ?? null;
36
+ this.onSortChange =
37
+ props.onSortChange ?? null;
38
+ this.connectSorterChangedSignal();
39
+ }
40
+ connectSorterChangedSignal() {
41
+ const sorter = this.widget.getSorter();
42
+ if (!sorter || !this.onSortChange)
43
+ return;
44
+ this.sorterChangedHandlerId = sorter.connect("changed", () => {
45
+ this.waitForSortComplete(() => this.notifySortChange());
46
+ });
47
+ }
48
+ waitForSortComplete(callback) {
49
+ const pending = this.sortListModel.getPending();
50
+ if (pending === 0) {
51
+ callback();
52
+ }
53
+ else {
54
+ // Sorting still in progress, check again after a short delay
55
+ setTimeout(() => this.waitForSortComplete(callback), 10);
56
+ }
57
+ }
58
+ disconnectSorterChangedSignal() {
59
+ if (this.sorterChangedHandlerId === null)
60
+ return;
61
+ const sorter = this.widget.getSorter();
62
+ if (sorter) {
63
+ GObject.signalHandlerDisconnect(sorter, this.sorterChangedHandlerId);
64
+ }
65
+ this.sorterChangedHandlerId = null;
66
+ }
67
+ notifySortChange() {
68
+ if (!this.onSortChange)
69
+ return;
70
+ const baseSorter = this.widget.getSorter();
71
+ if (!baseSorter)
72
+ return;
73
+ const sorter = getObject(baseSorter.ptr, Gtk.ColumnViewSorter);
74
+ const column = sorter.getPrimarySortColumn();
75
+ const order = sorter.getPrimarySortOrder();
76
+ const columnId = column?.getId() ?? null;
77
+ // Deduplicate: only notify if the sort state actually changed
78
+ if (columnId === this.lastNotifiedColumn && order === this.lastNotifiedOrder) {
79
+ return;
80
+ }
81
+ this.lastNotifiedColumn = columnId;
82
+ this.lastNotifiedOrder = order;
83
+ this.onSortChange(columnId, order);
21
84
  }
22
85
  getItems() {
23
86
  return this.items;
24
87
  }
25
- addColumn(column) {
26
- this.columns.push(column);
27
- const gtkColumn = column.getGtkColumn();
28
- this.widget.appendColumn(gtkColumn);
29
- column.setColumnView(this);
88
+ getSortFn() {
89
+ return this.sortFn;
90
+ }
91
+ compareItems(a, b, columnId) {
92
+ if (this.isSorting || !this.sortFn)
93
+ return 0;
94
+ this.isSorting = true;
95
+ try {
96
+ return this.sortFn(a, b, columnId);
97
+ }
98
+ finally {
99
+ this.isSorting = false;
100
+ }
101
+ }
102
+ addColumn(columnNode) {
103
+ this.columns.push(columnNode);
104
+ const column = columnNode.getColumn();
105
+ this.widget.appendColumn(column);
106
+ columnNode.setColumnView(this);
107
+ if (columnNode.getId() === this.sortColumn && this.sortColumn !== null) {
108
+ this.applySortByColumn();
109
+ }
110
+ }
111
+ applySortByColumn() {
112
+ if (this.sortColumn === null) {
113
+ this.widget.sortByColumn(this.sortOrder, null);
114
+ return;
115
+ }
116
+ if (!this.columns)
117
+ return;
118
+ const column = this.columns.find((c) => c.getId() === this.sortColumn);
119
+ if (column) {
120
+ this.widget.sortByColumn(this.sortOrder, column.getColumn());
121
+ }
122
+ }
123
+ findColumnById(id) {
124
+ return this.columns.find((c) => c.getId() === id);
30
125
  }
31
126
  removeColumn(column) {
32
127
  const index = this.columns.indexOf(column);
33
128
  if (index !== -1) {
34
129
  this.columns.splice(index, 1);
35
- this.widget.removeColumn(column.getGtkColumn());
130
+ this.widget.removeColumn(column.getColumn());
36
131
  column.setColumnView(null);
37
132
  }
38
133
  }
@@ -43,15 +138,16 @@ export class ColumnViewNode extends Node {
43
138
  return;
44
139
  }
45
140
  this.columns.splice(beforeIndex, 0, column);
46
- this.widget.insertColumn(beforeIndex, column.getGtkColumn());
141
+ this.widget.insertColumn(beforeIndex, column.getColumn());
47
142
  column.setColumnView(this);
48
143
  }
49
144
  syncStringList = () => {
50
145
  const newLength = this.items.length;
51
146
  if (newLength === this.committedLength)
52
147
  return;
53
- const placeholders = Array.from({ length: newLength }, () => "");
54
- this.stringList.splice(0, this.committedLength, placeholders);
148
+ // Store indices as strings so we can map back to items in the sorter
149
+ const indices = Array.from({ length: newLength }, (_, i) => String(i));
150
+ this.stringList.splice(0, this.committedLength, indices);
55
151
  this.committedLength = newLength;
56
152
  };
57
153
  addItem(item) {
@@ -75,6 +171,46 @@ export class ColumnViewNode extends Node {
75
171
  scheduleFlush(this.syncStringList);
76
172
  }
77
173
  }
174
+ consumedProps() {
175
+ const consumed = super.consumedProps();
176
+ consumed.add("sortColumn");
177
+ consumed.add("sortOrder");
178
+ consumed.add("onSortChange");
179
+ consumed.add("sortFn");
180
+ return consumed;
181
+ }
182
+ updateProps(oldProps, newProps) {
183
+ super.updateProps(oldProps, newProps);
184
+ const newSortColumn = newProps.sortColumn ?? null;
185
+ const newSortOrder = newProps.sortOrder ?? Gtk.SortType.ASCENDING;
186
+ const newSortFn = newProps.sortFn ?? null;
187
+ const newOnSortChange = newProps.onSortChange ?? null;
188
+ if (oldProps.onSortChange !== newProps.onSortChange) {
189
+ const hadCallback = this.onSortChange !== null;
190
+ this.onSortChange = newOnSortChange;
191
+ const hasCallback = this.onSortChange !== null;
192
+ // Connect or disconnect the signal handler as needed
193
+ if (!hadCallback && hasCallback) {
194
+ this.connectSorterChangedSignal();
195
+ }
196
+ else if (hadCallback && !hasCallback) {
197
+ this.disconnectSorterChangedSignal();
198
+ }
199
+ }
200
+ if (oldProps.sortFn !== newProps.sortFn) {
201
+ this.sortFn = newSortFn;
202
+ if (this.columns) {
203
+ for (const column of this.columns) {
204
+ column.updateSorterFromRoot();
205
+ }
206
+ }
207
+ }
208
+ if (oldProps.sortColumn !== newProps.sortColumn || oldProps.sortOrder !== newProps.sortOrder) {
209
+ this.sortColumn = newSortColumn;
210
+ this.sortOrder = newSortOrder;
211
+ this.applySortByColumn();
212
+ }
213
+ }
78
214
  }
79
215
  export class ColumnViewColumnNode extends Node {
80
216
  static matches(type) {
@@ -83,24 +219,30 @@ export class ColumnViewColumnNode extends Node {
83
219
  isVirtual() {
84
220
  return true;
85
221
  }
86
- gtkColumn;
222
+ column;
87
223
  factory;
88
224
  renderCell;
89
225
  columnView = null;
90
226
  listItemCache = new Map();
227
+ columnId = null;
228
+ sorter = null;
91
229
  constructor(type, props, app) {
92
230
  super(type, props, app);
93
231
  this.factory = new Gtk.SignalListItemFactory();
94
- this.gtkColumn = new Gtk.ColumnViewColumn(props.title, this.factory);
232
+ this.column = new Gtk.ColumnViewColumn(props.title, this.factory);
95
233
  this.renderCell = props.renderCell;
234
+ this.columnId = props.id ?? null;
235
+ if (this.columnId !== null) {
236
+ this.column.setId(this.columnId);
237
+ }
96
238
  if (props.expand !== undefined) {
97
- this.gtkColumn.setExpand(props.expand);
239
+ this.column.setExpand(props.expand);
98
240
  }
99
241
  if (props.resizable !== undefined) {
100
- this.gtkColumn.setResizable(props.resizable);
242
+ this.column.setResizable(props.resizable);
101
243
  }
102
244
  if (props.fixedWidth !== undefined) {
103
- this.gtkColumn.setFixedWidth(props.fixedWidth);
245
+ this.column.setFixedWidth(props.fixedWidth);
104
246
  }
105
247
  this.factory.connect("setup", (_self, listItemObj) => {
106
248
  const listItem = getObject(listItemObj.ptr, Gtk.ListItem);
@@ -142,11 +284,48 @@ export class ColumnViewColumnNode extends Node {
142
284
  }
143
285
  });
144
286
  }
145
- getGtkColumn() {
146
- return this.gtkColumn;
287
+ getColumn() {
288
+ return this.column;
289
+ }
290
+ getId() {
291
+ return this.columnId;
147
292
  }
148
293
  setColumnView(columnView) {
149
294
  this.columnView = columnView;
295
+ this.updateSorterFromRoot();
296
+ }
297
+ updateSorterFromRoot() {
298
+ if (!this.columnView || this.columnId === null) {
299
+ this.column.setSorter(null);
300
+ this.sorter = null;
301
+ return;
302
+ }
303
+ const rootSortFn = this.columnView.getSortFn();
304
+ if (rootSortFn === null) {
305
+ this.column.setSorter(null);
306
+ this.sorter = null;
307
+ return;
308
+ }
309
+ const columnId = this.columnId;
310
+ const columnView = this.columnView;
311
+ const wrappedSortFn = (_a, _b) => {
312
+ const items = columnView.getItems();
313
+ // _a and _b are GtkStringObject pointers - get the string content (indices)
314
+ const stringObjA = getObject(_a, Gtk.StringObject);
315
+ const stringObjB = getObject(_b, Gtk.StringObject);
316
+ const indexA = Number.parseInt(stringObjA.getString(), 10);
317
+ const indexB = Number.parseInt(stringObjB.getString(), 10);
318
+ if (Number.isNaN(indexA) || Number.isNaN(indexB))
319
+ return 0;
320
+ const itemA = items[indexA] ?? null;
321
+ const itemB = items[indexB] ?? null;
322
+ if (itemA === null || itemB === null)
323
+ return 0;
324
+ const result = columnView.compareItems(itemA, itemB, columnId);
325
+ return typeof result === "number" ? result : 0;
326
+ };
327
+ this.sorter = new Gtk.CustomSorter(wrappedSortFn);
328
+ this.column.setSorter(this.sorter);
150
329
  }
151
330
  attachToParent(parent) {
152
331
  if (parent instanceof ColumnViewNode) {
@@ -173,25 +352,30 @@ export class ColumnViewColumnNode extends Node {
173
352
  consumed.add("expand");
174
353
  consumed.add("resizable");
175
354
  consumed.add("fixedWidth");
355
+ consumed.add("id");
176
356
  return consumed;
177
357
  }
178
358
  updateProps(oldProps, newProps) {
179
359
  if (oldProps.renderCell !== newProps.renderCell) {
180
360
  this.renderCell = newProps.renderCell;
181
361
  }
182
- if (!this.gtkColumn)
362
+ if (!this.column)
183
363
  return;
184
364
  if (oldProps.title !== newProps.title) {
185
- this.gtkColumn.setTitle(newProps.title);
365
+ this.column.setTitle(newProps.title);
186
366
  }
187
367
  if (oldProps.expand !== newProps.expand) {
188
- this.gtkColumn.setExpand(newProps.expand);
368
+ this.column.setExpand(newProps.expand);
189
369
  }
190
370
  if (oldProps.resizable !== newProps.resizable) {
191
- this.gtkColumn.setResizable(newProps.resizable);
371
+ this.column.setResizable(newProps.resizable);
192
372
  }
193
373
  if (oldProps.fixedWidth !== newProps.fixedWidth) {
194
- this.gtkColumn.setFixedWidth(newProps.fixedWidth);
374
+ this.column.setFixedWidth(newProps.fixedWidth);
375
+ }
376
+ if (oldProps.id !== newProps.id) {
377
+ this.columnId = newProps.id ?? null;
378
+ this.column.setId(this.columnId);
195
379
  }
196
380
  }
197
381
  }
package/dist/types.d.ts CHANGED
@@ -1,4 +1,9 @@
1
+ import type { SortType } from "@gtkx/ffi/gtk";
1
2
  import type { ReactElement, ReactNode } from "react";
3
+ /**
4
+ * Props for slot components that accept children.
5
+ * Used by container widgets that render child elements in designated slots.
6
+ */
2
7
  export interface SlotProps {
3
8
  children?: ReactNode;
4
9
  }
@@ -19,11 +24,20 @@ export type RenderItemFn<T> = (item: T | null) => ReactElement;
19
24
  export interface ListViewRenderProps<T = unknown> {
20
25
  renderItem: RenderItemFn<T>;
21
26
  }
27
+ /**
28
+ * Comparison function for sorting items by column.
29
+ * Returns negative if a < b, 0 if a === b, positive if a > b.
30
+ * @param a - First item to compare
31
+ * @param b - Second item to compare
32
+ * @param columnId - The ID of the column being sorted
33
+ */
34
+ export type ColumnSortFn<T, C extends string = string> = (a: T, b: T, columnId: C) => number;
22
35
  export interface ColumnViewColumnProps {
23
36
  title?: string;
24
37
  expand?: boolean;
25
38
  resizable?: boolean;
26
39
  fixedWidth?: number;
40
+ id?: string;
27
41
  /**
28
42
  * Render function for column cells.
29
43
  * Called with null during setup (for loading state) and with the actual item during bind.
@@ -31,6 +45,12 @@ export interface ColumnViewColumnProps {
31
45
  */
32
46
  renderCell: (item: any) => ReactElement;
33
47
  }
48
+ export interface ColumnViewRootProps<C extends string = string> {
49
+ sortColumn?: C | null;
50
+ sortOrder?: SortType;
51
+ onSortChange?: (column: C | null, order: SortType) => void;
52
+ sortFn?: ColumnSortFn<any, C>;
53
+ }
34
54
  export interface NotebookPageProps extends SlotProps {
35
55
  label: string;
36
56
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtkx/react",
3
- "version": "0.1.46",
3
+ "version": "0.1.48",
4
4
  "description": "Build GTK4 desktop applications with React and TypeScript",
5
5
  "keywords": [
6
6
  "gtk",
@@ -36,10 +36,10 @@
36
36
  ],
37
37
  "dependencies": {
38
38
  "react-reconciler": "0.33.0",
39
- "@gtkx/ffi": "0.1.46"
39
+ "@gtkx/ffi": "0.1.48"
40
40
  },
41
41
  "devDependencies": {
42
- "@gtkx/gir": "0.1.46"
42
+ "@gtkx/gir": "0.1.48"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "react": "^19"