@gtkx/react 0.1.40 → 0.1.42

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.
@@ -0,0 +1,5 @@
1
+ type FlushCallback = () => void;
2
+ export declare const beginCommit: () => void;
3
+ export declare const endCommit: () => void;
4
+ export declare const scheduleFlush: (callback: FlushCallback) => void;
5
+ export {};
package/dist/batch.js ADDED
@@ -0,0 +1,20 @@
1
+ const pendingFlushes = new Set();
2
+ let inCommit = false;
3
+ export const beginCommit = () => {
4
+ inCommit = true;
5
+ };
6
+ export const endCommit = () => {
7
+ inCommit = false;
8
+ for (const callback of pendingFlushes) {
9
+ callback();
10
+ }
11
+ pendingFlushes.clear();
12
+ };
13
+ export const scheduleFlush = (callback) => {
14
+ if (inCommit) {
15
+ pendingFlushes.add(callback);
16
+ }
17
+ else {
18
+ callback();
19
+ }
20
+ };
@@ -315,11 +315,10 @@ ${widgetPropsContent}
315
315
  lines.push("");
316
316
  lines.push(`\t/**`);
317
317
  lines.push(`\t * Render function for list items.`);
318
- lines.push(`\t * Called with null during setup/unbind and with the actual item during bind.`);
319
- lines.push(`\t * The ref callback must be attached to the root widget to set it as the list item's child.`);
318
+ lines.push(`\t * Called with null during setup (for loading state) and with the actual item during bind.`);
320
319
  lines.push(`\t */`);
321
320
  lines.push(`\t// biome-ignore lint/suspicious/noExplicitAny: allows typed renderItem callbacks`);
322
- lines.push(`\trenderItem: (item: any, ref: import("react").RefCallback<Gtk.Widget>) => import("react").ReactElement;`);
321
+ lines.push(`\trenderItem: (item: any) => import("react").ReactElement;`);
323
322
  }
324
323
  if (isDropDownWidget(widget.name)) {
325
324
  lines.push("");
@@ -0,0 +1,3 @@
1
+ import type * as Gtk from "@gtkx/ffi/gtk";
2
+ import type Reconciler from "react-reconciler";
3
+ export declare const createFiberRoot: (container?: Gtk.Widget) => Reconciler.FiberRoot;
@@ -0,0 +1,6 @@
1
+ import { ROOT_NODE_CONTAINER } from "./factory.js";
2
+ import { reconciler } from "./reconciler.js";
3
+ export const createFiberRoot = (container) => {
4
+ const instance = reconciler.getInstance();
5
+ return instance.createContainer(container ?? ROOT_NODE_CONTAINER, 0, null, false, null, "", (error) => console.error("Fiber root render error:", error), () => { }, () => { }, () => { }, null);
6
+ };
@@ -566,7 +566,7 @@ export interface GridViewProps extends ListBaseProps {
566
566
  singleClickActivate?: boolean;
567
567
  tabBehavior?: Gtk.ListTabBehavior;
568
568
  onActivate?: (self: Gtk.GridView, position: number) => void;
569
- renderItem: (item: any, ref: import("react").RefCallback<Gtk.Widget>) => import("react").ReactElement;
569
+ renderItem: (item: any) => import("react").ReactElement;
570
570
  ref?: Ref<Gtk.GridView>;
571
571
  }
572
572
  export interface HeaderBarProps extends WidgetProps {
@@ -724,7 +724,7 @@ export interface ListViewProps extends ListBaseProps {
724
724
  singleClickActivate?: boolean;
725
725
  tabBehavior?: Gtk.ListTabBehavior;
726
726
  onActivate?: (self: Gtk.ListView, position: number) => void;
727
- renderItem: (item: any, ref: import("react").RefCallback<Gtk.Widget>) => import("react").ReactElement;
727
+ renderItem: (item: any) => import("react").ReactElement;
728
728
  ref?: Ref<Gtk.ListView>;
729
729
  }
730
730
  export interface LockButtonProps extends ButtonProps {
@@ -7,11 +7,13 @@ export declare class ColumnViewNode extends Node<Gtk.ColumnView> {
7
7
  private selectionModel;
8
8
  private items;
9
9
  private columns;
10
+ private committedLength;
10
11
  constructor(type: string, props: Props, app: Gtk.Application);
11
12
  getItems(): unknown[];
12
13
  addColumn(column: ColumnViewColumnNode): void;
13
14
  removeColumn(column: ColumnViewColumnNode): void;
14
15
  insertColumnBefore(column: ColumnViewColumnNode, before: ColumnViewColumnNode): void;
16
+ private syncStringList;
15
17
  addItem(item: unknown): void;
16
18
  insertItemBefore(item: unknown, beforeItem: unknown): void;
17
19
  removeItem(item: unknown): void;
@@ -23,7 +25,7 @@ export declare class ColumnViewColumnNode extends Node {
23
25
  private factory;
24
26
  private renderCell;
25
27
  private columnView;
26
- private fiberRoots;
28
+ private listItemCache;
27
29
  constructor(type: string, props: Props, app: Gtk.Application);
28
30
  getGtkColumn(): Gtk.ColumnViewColumn;
29
31
  setColumnView(columnView: ColumnViewNode | null): void;
@@ -1,7 +1,9 @@
1
1
  import { getObject, getObjectId } from "@gtkx/ffi";
2
2
  import * as Gtk from "@gtkx/ffi/gtk";
3
- import { createFiberRoot, updateSync } from "../flush-sync.js";
3
+ import { scheduleFlush } from "../batch.js";
4
+ import { createFiberRoot } from "../fiber-root.js";
4
5
  import { Node } from "../node.js";
6
+ import { reconciler } from "../reconciler.js";
5
7
  export class ColumnViewNode extends Node {
6
8
  static matches(type) {
7
9
  return type === "ColumnView.Root";
@@ -10,6 +12,7 @@ export class ColumnViewNode extends Node {
10
12
  selectionModel;
11
13
  items = [];
12
14
  columns = [];
15
+ committedLength = 0;
13
16
  constructor(type, props, app) {
14
17
  super(type, props, app);
15
18
  this.stringList = new Gtk.StringList([]);
@@ -43,24 +46,33 @@ export class ColumnViewNode extends Node {
43
46
  this.widget.insertColumn(beforeIndex, column.getGtkColumn());
44
47
  column.setColumnView(this);
45
48
  }
49
+ syncStringList = () => {
50
+ const newLength = this.items.length;
51
+ if (newLength === this.committedLength)
52
+ return;
53
+ const placeholders = Array.from({ length: newLength }, () => "");
54
+ this.stringList.splice(0, this.committedLength, placeholders);
55
+ this.committedLength = newLength;
56
+ };
46
57
  addItem(item) {
47
58
  this.items.push(item);
48
- this.stringList.append("");
59
+ scheduleFlush(this.syncStringList);
49
60
  }
50
61
  insertItemBefore(item, beforeItem) {
51
62
  const beforeIndex = this.items.indexOf(beforeItem);
52
63
  if (beforeIndex === -1) {
53
- this.addItem(item);
54
- return;
64
+ this.items.push(item);
65
+ }
66
+ else {
67
+ this.items.splice(beforeIndex, 0, item);
55
68
  }
56
- this.items.splice(beforeIndex, 0, item);
57
- this.stringList.splice(beforeIndex, 0, [""]);
69
+ scheduleFlush(this.syncStringList);
58
70
  }
59
71
  removeItem(item) {
60
72
  const index = this.items.indexOf(item);
61
73
  if (index !== -1) {
62
74
  this.items.splice(index, 1);
63
- this.stringList.remove(index);
75
+ scheduleFlush(this.syncStringList);
64
76
  }
65
77
  }
66
78
  }
@@ -75,7 +87,7 @@ export class ColumnViewColumnNode extends Node {
75
87
  factory;
76
88
  renderCell;
77
89
  columnView = null;
78
- fiberRoots = new Map();
90
+ listItemCache = new Map();
79
91
  constructor(type, props, app) {
80
92
  super(type, props, app);
81
93
  this.factory = new Gtk.SignalListItemFactory();
@@ -91,51 +103,43 @@ export class ColumnViewColumnNode extends Node {
91
103
  this.gtkColumn.setFixedWidth(props.fixedWidth);
92
104
  }
93
105
  this.factory.connect("setup", (_self, listItemObj) => {
94
- const listItem = getObject(listItemObj, Gtk.ListItem);
95
- const id = getObjectId(listItemObj);
96
- const fiberRoot = createFiberRoot();
97
- this.fiberRoots.set(id, fiberRoot);
98
- let rootWidget = null;
99
- const ref = (widget) => {
100
- if (widget && !rootWidget) {
101
- rootWidget = widget;
102
- listItem.setChild(widget);
103
- }
104
- };
105
- const element = this.renderCell(null, ref);
106
- updateSync(element, fiberRoot);
106
+ const listItem = getObject(listItemObj.ptr, Gtk.ListItem);
107
+ const id = getObjectId(listItemObj.ptr);
108
+ const box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
109
+ listItem.setChild(box);
110
+ const fiberRoot = createFiberRoot(box);
111
+ this.listItemCache.set(id, { box, fiberRoot });
112
+ const element = this.renderCell(null);
113
+ reconciler.getInstance().updateContainer(element, fiberRoot, null, () => { });
107
114
  });
108
115
  this.factory.connect("bind", (_self, listItemObj) => {
109
- const listItem = getObject(listItemObj, Gtk.ListItem);
110
- const id = getObjectId(listItemObj);
111
- const fiberRoot = this.fiberRoots.get(id);
112
- if (!fiberRoot)
116
+ const listItem = getObject(listItemObj.ptr, Gtk.ListItem);
117
+ const id = getObjectId(listItemObj.ptr);
118
+ const info = this.listItemCache.get(id);
119
+ if (!info)
113
120
  return;
114
121
  const position = listItem.getPosition();
115
122
  if (this.columnView) {
116
123
  const items = this.columnView.getItems();
117
124
  const item = items[position];
118
- const ref = () => { };
119
- const element = this.renderCell(item ?? null, ref);
120
- updateSync(element, fiberRoot);
125
+ const element = this.renderCell(item ?? null);
126
+ reconciler.getInstance().updateContainer(element, info.fiberRoot, null, () => { });
121
127
  }
122
128
  });
123
129
  this.factory.connect("unbind", (_self, listItemObj) => {
124
- const id = getObjectId(listItemObj);
125
- const fiberRoot = this.fiberRoots.get(id);
126
- if (!fiberRoot)
130
+ const id = getObjectId(listItemObj.ptr);
131
+ const info = this.listItemCache.get(id);
132
+ if (!info)
127
133
  return;
128
- const ref = () => { };
129
- const element = this.renderCell(null, ref);
130
- updateSync(element, fiberRoot);
134
+ reconciler.getInstance().updateContainer(null, info.fiberRoot, null, () => { });
131
135
  });
132
136
  this.factory.connect("teardown", (_self, listItemObj) => {
133
- const id = getObjectId(listItemObj);
134
- const fiberRoot = this.fiberRoots.get(id);
135
- if (!fiberRoot)
136
- return;
137
- updateSync(null, fiberRoot);
138
- this.fiberRoots.delete(id);
137
+ const id = getObjectId(listItemObj.ptr);
138
+ const info = this.listItemCache.get(id);
139
+ if (info) {
140
+ reconciler.getInstance().updateContainer(null, info.fiberRoot, null, () => { });
141
+ this.listItemCache.delete(id);
142
+ }
139
143
  });
140
144
  }
141
145
  getGtkColumn() {
@@ -8,8 +8,10 @@ export declare class ListViewNode extends Node<Gtk.ListView | Gtk.GridView> {
8
8
  private factory;
9
9
  private items;
10
10
  private renderItem;
11
- private fiberRoots;
11
+ private listItemCache;
12
+ private committedLength;
12
13
  constructor(type: string, props: Props, app: Gtk.Application);
14
+ private syncStringList;
13
15
  addItem(item: unknown): void;
14
16
  insertItemBefore(item: unknown, beforeItem: unknown): void;
15
17
  removeItem(item: unknown): void;
@@ -1,7 +1,9 @@
1
1
  import { getObject, getObjectId } from "@gtkx/ffi";
2
2
  import * as Gtk from "@gtkx/ffi/gtk";
3
- import { createFiberRoot, updateSync } from "../flush-sync.js";
3
+ import { scheduleFlush } from "../batch.js";
4
+ import { createFiberRoot } from "../fiber-root.js";
4
5
  import { Node } from "../node.js";
6
+ import { reconciler } from "../reconciler.js";
5
7
  export class ListViewNode extends Node {
6
8
  static matches(type) {
7
9
  return type === "ListView.Root" || type === "GridView.Root";
@@ -11,7 +13,8 @@ export class ListViewNode extends Node {
11
13
  factory;
12
14
  items = [];
13
15
  renderItem;
14
- fiberRoots = new Map();
16
+ listItemCache = new Map();
17
+ committedLength = 0;
15
18
  constructor(type, props, app) {
16
19
  super(type, props, app);
17
20
  this.stringList = new Gtk.StringList([]);
@@ -19,70 +22,71 @@ export class ListViewNode extends Node {
19
22
  this.factory = new Gtk.SignalListItemFactory();
20
23
  this.renderItem = props.renderItem;
21
24
  this.factory.connect("setup", (_self, listItemObj) => {
22
- const listItem = getObject(listItemObj, Gtk.ListItem);
23
- const id = getObjectId(listItemObj);
24
- const fiberRoot = createFiberRoot();
25
- this.fiberRoots.set(id, fiberRoot);
26
- let rootWidget = null;
27
- const ref = (widget) => {
28
- if (widget && !rootWidget) {
29
- rootWidget = widget;
30
- listItem.setChild(widget);
31
- }
32
- };
33
- const element = this.renderItem(null, ref);
34
- updateSync(element, fiberRoot);
25
+ const listItem = getObject(listItemObj.ptr, Gtk.ListItem);
26
+ const id = getObjectId(listItemObj.ptr);
27
+ const box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
28
+ listItem.setChild(box);
29
+ const fiberRoot = createFiberRoot(box);
30
+ this.listItemCache.set(id, { box, fiberRoot });
31
+ const element = this.renderItem(null);
32
+ reconciler.getInstance().updateContainer(element, fiberRoot, null, () => { });
35
33
  });
36
34
  this.factory.connect("bind", (_self, listItemObj) => {
37
- const listItem = getObject(listItemObj, Gtk.ListItem);
38
- const id = getObjectId(listItemObj);
39
- const fiberRoot = this.fiberRoots.get(id);
40
- if (!fiberRoot)
35
+ const listItem = getObject(listItemObj.ptr, Gtk.ListItem);
36
+ const id = getObjectId(listItemObj.ptr);
37
+ const info = this.listItemCache.get(id);
38
+ if (!info)
41
39
  return;
42
40
  const position = listItem.getPosition();
43
41
  const item = this.items[position];
44
- const ref = () => { };
45
- const element = this.renderItem(item ?? null, ref);
46
- updateSync(element, fiberRoot);
42
+ const element = this.renderItem(item);
43
+ reconciler.getInstance().updateContainer(element, info.fiberRoot, null, () => { });
47
44
  });
48
45
  this.factory.connect("unbind", (_self, listItemObj) => {
49
- const id = getObjectId(listItemObj);
50
- const fiberRoot = this.fiberRoots.get(id);
51
- if (!fiberRoot)
46
+ const id = getObjectId(listItemObj.ptr);
47
+ const info = this.listItemCache.get(id);
48
+ if (!info)
52
49
  return;
53
- const ref = () => { };
54
- const element = this.renderItem(null, ref);
55
- updateSync(element, fiberRoot);
50
+ reconciler.getInstance().updateContainer(null, info.fiberRoot, null, () => { });
56
51
  });
57
52
  this.factory.connect("teardown", (_self, listItemObj) => {
58
- const id = getObjectId(listItemObj);
59
- const fiberRoot = this.fiberRoots.get(id);
60
- if (!fiberRoot)
61
- return;
62
- updateSync(null, fiberRoot);
63
- this.fiberRoots.delete(id);
53
+ const id = getObjectId(listItemObj.ptr);
54
+ const info = this.listItemCache.get(id);
55
+ if (info) {
56
+ reconciler.getInstance().updateContainer(null, info.fiberRoot, null, () => { });
57
+ this.listItemCache.delete(id);
58
+ }
64
59
  });
65
60
  this.widget.setModel(this.selectionModel);
66
61
  this.widget.setFactory(this.factory);
67
62
  }
63
+ syncStringList = () => {
64
+ const newLength = this.items.length;
65
+ if (newLength === this.committedLength)
66
+ return;
67
+ const placeholders = Array.from({ length: newLength }, () => "");
68
+ this.stringList.splice(0, this.committedLength, placeholders);
69
+ this.committedLength = newLength;
70
+ };
68
71
  addItem(item) {
69
72
  this.items.push(item);
70
- this.stringList.append("");
73
+ scheduleFlush(this.syncStringList);
71
74
  }
72
75
  insertItemBefore(item, beforeItem) {
73
76
  const beforeIndex = this.items.indexOf(beforeItem);
74
77
  if (beforeIndex === -1) {
75
- this.addItem(item);
76
- return;
78
+ this.items.push(item);
79
+ }
80
+ else {
81
+ this.items.splice(beforeIndex, 0, item);
77
82
  }
78
- this.items.splice(beforeIndex, 0, item);
79
- this.stringList.splice(beforeIndex, 0, [""]);
83
+ scheduleFlush(this.syncStringList);
80
84
  }
81
85
  removeItem(item) {
82
86
  const index = this.items.indexOf(item);
83
87
  if (index !== -1) {
84
88
  this.items.splice(index, 1);
85
- this.stringList.remove(index);
89
+ scheduleFlush(this.syncStringList);
86
90
  }
87
91
  }
88
92
  consumedProps() {
@@ -1,6 +1,7 @@
1
1
  import { getCurrentApp } from "@gtkx/ffi";
2
2
  import React from "react";
3
3
  import ReactReconciler from "react-reconciler";
4
+ import { beginCommit, endCommit } from "./batch.js";
4
5
  import { createNode } from "./factory.js";
5
6
  class Reconciler {
6
7
  instance;
@@ -20,7 +21,9 @@ class Reconciler {
20
21
  getRootHostContext: () => ({}),
21
22
  getChildHostContext: (parentHostContext) => parentHostContext,
22
23
  shouldSetTextContent: () => false,
23
- createInstance: (type, props) => createNode(type, props, getCurrentApp()),
24
+ createInstance: (type, props) => {
25
+ return createNode(type, props, getCurrentApp());
26
+ },
24
27
  createTextInstance: (text) => createNode("Label.Root", { label: text }, getCurrentApp()),
25
28
  appendInitialChild: (parent, child) => parent.appendChild(child),
26
29
  finalizeInitialChildren: () => true,
@@ -45,8 +48,13 @@ class Reconciler {
45
48
  const parent = this.createNodeFromContainer(container);
46
49
  parent.insertBefore(child, beforeChild);
47
50
  },
48
- prepareForCommit: () => null,
49
- resetAfterCommit: () => { },
51
+ prepareForCommit: () => {
52
+ beginCommit();
53
+ return null;
54
+ },
55
+ resetAfterCommit: () => {
56
+ endCommit();
57
+ },
50
58
  commitTextUpdate: (textInstance, oldText, newText) => {
51
59
  textInstance.updateProps({ label: oldText }, { label: newText });
52
60
  },
package/dist/types.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import type * as Gtk from "@gtkx/ffi/gtk";
2
- import type { ReactElement, ReactNode, RefCallback } from "react";
1
+ import type { ReactElement, ReactNode } from "react";
3
2
  export interface SlotProps {
4
3
  children?: ReactNode;
5
4
  }
@@ -12,17 +11,16 @@ export interface GridChildProps extends SlotProps {
12
11
  columnSpan?: number;
13
12
  rowSpan?: number;
14
13
  }
15
- export type RenderItemFn<T> = (item: T | null, ref: RefCallback<Gtk.Widget>) => ReactElement;
14
+ export type RenderItemFn<T> = (item: T | null) => ReactElement;
16
15
  export interface ListViewRenderProps<T = unknown> {
17
16
  renderItem: RenderItemFn<T>;
18
17
  }
19
- export type RenderCellFn<T> = (item: T | null, ref: RefCallback<Gtk.Widget>) => ReactElement;
20
18
  export interface ColumnViewColumnProps {
21
19
  title?: string;
22
20
  expand?: boolean;
23
21
  resizable?: boolean;
24
22
  fixedWidth?: number;
25
- renderCell: (item: any, ref: RefCallback<Gtk.Widget>) => ReactElement;
23
+ renderCell: (item: any) => ReactElement;
26
24
  }
27
25
  export interface NotebookPageProps extends SlotProps {
28
26
  label: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtkx/react",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
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.40"
39
+ "@gtkx/ffi": "0.1.42"
40
40
  },
41
41
  "devDependencies": {
42
- "@gtkx/gir": "0.1.40"
42
+ "@gtkx/gir": "0.1.42"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "react": "^19"
@@ -1,4 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import type Reconciler from "react-reconciler";
3
- export declare const updateSync: (element: ReactNode, fiberRoot: Reconciler.FiberRoot) => void;
4
- export declare const createFiberRoot: () => Reconciler.FiberRoot;
@@ -1,27 +0,0 @@
1
- import { ROOT_NODE_CONTAINER } from "./factory.js";
2
- import { reconciler } from "./reconciler.js";
3
- export const updateSync = (element, fiberRoot) => {
4
- const instance = reconciler.getInstance();
5
- const instanceAny = instance;
6
- if (typeof instanceAny.flushSync === "function") {
7
- instanceAny.flushSync(() => {
8
- instance.updateContainer(element, fiberRoot, null, () => { });
9
- });
10
- }
11
- else {
12
- if (typeof instanceAny.updateContainerSync === "function") {
13
- instanceAny.updateContainerSync(element, fiberRoot, null, () => { });
14
- }
15
- else {
16
- instance.updateContainer(element, fiberRoot, null, () => { });
17
- }
18
- if (typeof instanceAny.flushSyncWork === "function") {
19
- instanceAny.flushSyncWork();
20
- }
21
- }
22
- instance.flushPassiveEffects();
23
- };
24
- export const createFiberRoot = () => {
25
- const instance = reconciler.getInstance();
26
- return instance.createContainer(ROOT_NODE_CONTAINER, 0, null, false, null, "", (error) => console.error("List item render error:", error), () => { }, () => { }, () => { }, null);
27
- };