@gtkx/react 0.6.1 → 0.8.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.
Files changed (84) hide show
  1. package/dist/batch.d.ts +4 -1
  2. package/dist/batch.js +19 -10
  3. package/dist/codegen/jsx-generator.d.ts +4 -4
  4. package/dist/codegen/jsx-generator.js +24 -27
  5. package/dist/container-interfaces.d.ts +19 -6
  6. package/dist/container-interfaces.js +26 -6
  7. package/dist/errors.d.ts +8 -0
  8. package/dist/errors.js +38 -0
  9. package/dist/factory.js +9 -3
  10. package/dist/generated/jsx.d.ts +38 -26
  11. package/dist/generated/jsx.js +12 -2
  12. package/dist/index.js +3 -1
  13. package/dist/node.d.ts +5 -0
  14. package/dist/node.js +62 -6
  15. package/dist/nodes/action-bar.d.ts +2 -6
  16. package/dist/nodes/action-bar.js +3 -12
  17. package/dist/nodes/column-view.d.ts +19 -44
  18. package/dist/nodes/column-view.js +70 -243
  19. package/dist/nodes/combo-row.d.ts +5 -0
  20. package/dist/nodes/combo-row.js +6 -0
  21. package/dist/nodes/drop-down.d.ts +9 -0
  22. package/dist/nodes/drop-down.js +12 -0
  23. package/dist/nodes/flow-box.d.ts +4 -3
  24. package/dist/nodes/flow-box.js +26 -10
  25. package/dist/nodes/grid.d.ts +15 -15
  26. package/dist/nodes/grid.js +21 -64
  27. package/dist/nodes/header-bar.d.ts +34 -11
  28. package/dist/nodes/header-bar.js +55 -24
  29. package/dist/nodes/indexed-child-container.d.ts +16 -0
  30. package/dist/nodes/indexed-child-container.js +22 -0
  31. package/dist/nodes/list-box.d.ts +4 -3
  32. package/dist/nodes/list-box.js +33 -6
  33. package/dist/nodes/list-item-factory.d.ts +19 -0
  34. package/dist/nodes/list-item-factory.js +58 -0
  35. package/dist/nodes/list-view.d.ts +24 -0
  36. package/dist/nodes/list-view.js +46 -0
  37. package/dist/nodes/menu.d.ts +25 -19
  38. package/dist/nodes/menu.js +30 -59
  39. package/dist/nodes/notebook.d.ts +13 -14
  40. package/dist/nodes/notebook.js +18 -56
  41. package/dist/nodes/paged-stack.d.ts +39 -0
  42. package/dist/nodes/paged-stack.js +54 -0
  43. package/dist/nodes/selectable-list.d.ts +41 -0
  44. package/dist/nodes/selectable-list.js +228 -0
  45. package/dist/nodes/stack-page-props.d.ts +11 -0
  46. package/dist/nodes/stack-page-props.js +23 -0
  47. package/dist/nodes/stack.d.ts +14 -28
  48. package/dist/nodes/stack.js +30 -142
  49. package/dist/nodes/string-list-container.d.ts +41 -0
  50. package/dist/nodes/string-list-container.js +90 -0
  51. package/dist/nodes/string-list-item.d.ts +15 -0
  52. package/dist/nodes/string-list-item.js +48 -0
  53. package/dist/nodes/string-list-store.d.ts +13 -0
  54. package/dist/nodes/string-list-store.js +44 -0
  55. package/dist/nodes/text-view.d.ts +1 -1
  56. package/dist/nodes/text-view.js +1 -5
  57. package/dist/nodes/toggle-button.d.ts +1 -1
  58. package/dist/nodes/toggle-button.js +1 -3
  59. package/dist/nodes/view-stack.d.ts +9 -0
  60. package/dist/nodes/view-stack.js +28 -0
  61. package/dist/nodes/virtual-item.d.ts +20 -0
  62. package/dist/nodes/virtual-item.js +57 -0
  63. package/dist/nodes/virtual-slot.d.ts +25 -0
  64. package/dist/nodes/virtual-slot.js +71 -0
  65. package/dist/nodes/widget.d.ts +0 -3
  66. package/dist/nodes/widget.js +0 -28
  67. package/dist/nodes/window.d.ts +1 -1
  68. package/dist/nodes/window.js +9 -15
  69. package/dist/predicates.d.ts +8 -0
  70. package/dist/predicates.js +8 -0
  71. package/dist/props.d.ts +7 -5
  72. package/dist/props.js +11 -9
  73. package/dist/reconciler/host-config.d.ts +19 -0
  74. package/dist/reconciler/host-config.js +89 -0
  75. package/dist/reconciler.d.ts +2 -26
  76. package/dist/reconciler.js +15 -106
  77. package/dist/render.d.ts +3 -4
  78. package/dist/render.js +6 -4
  79. package/dist/types.d.ts +22 -19
  80. package/package.json +6 -5
  81. package/dist/nodes/dropdown.d.ts +0 -39
  82. package/dist/nodes/dropdown.js +0 -103
  83. package/dist/nodes/list.d.ts +0 -43
  84. package/dist/nodes/list.js +0 -153
@@ -51,28 +51,29 @@ class MenuContainerNode extends Node {
51
51
  }
52
52
  onMenuRebuilt() { }
53
53
  }
54
- export class PopoverMenuRootNode extends MenuContainerNode {
55
- static matches(type) {
56
- return type === "PopoverMenu.Root";
57
- }
54
+ class MenuWidgetNode extends MenuContainerNode {
58
55
  initialize(props) {
59
- this.widget = new Gtk.PopoverMenu(this.menu);
56
+ this.widget = this.createMenuWidget(this.menu);
60
57
  super.initialize(props);
61
58
  }
62
59
  onMenuRebuilt() {
63
60
  this.widget.setMenuModel(this.menu);
64
61
  }
65
62
  }
66
- export class PopoverMenuBarNode extends MenuContainerNode {
63
+ export class PopoverMenuRootNode extends MenuWidgetNode {
67
64
  static matches(type) {
68
- return type === "PopoverMenuBar";
65
+ return type === "PopoverMenu.Root";
69
66
  }
70
- initialize(props) {
71
- this.widget = new Gtk.PopoverMenuBar(this.menu);
72
- super.initialize(props);
67
+ createMenuWidget(menu) {
68
+ return new Gtk.PopoverMenu(menu);
73
69
  }
74
- onMenuRebuilt() {
75
- this.widget.setMenuModel(this.menu);
70
+ }
71
+ export class PopoverMenuBarNode extends MenuWidgetNode {
72
+ static matches(type) {
73
+ return type === "PopoverMenuBar";
74
+ }
75
+ createMenuWidget(menu) {
76
+ return new Gtk.PopoverMenuBar(menu);
76
77
  }
77
78
  }
78
79
  export class ApplicationMenuNode extends MenuContainerNode {
@@ -98,6 +99,7 @@ export class ApplicationMenuNode extends MenuContainerNode {
98
99
  }
99
100
  }
100
101
  export class MenuItemNode extends Node {
102
+ static consumedPropNames = ["label", "onActivate", "accels"];
101
103
  static matches(type) {
102
104
  return type === "Menu.Item";
103
105
  }
@@ -131,13 +133,6 @@ export class MenuItemNode extends Node {
131
133
  this.cleanupAction();
132
134
  this.isAttached = false;
133
135
  }
134
- consumedProps() {
135
- const consumed = super.consumedProps();
136
- consumed.add("label");
137
- consumed.add("onActivate");
138
- consumed.add("accels");
139
- return consumed;
140
- }
141
136
  isFieldInitializationIncomplete() {
142
137
  return !this.entry;
143
138
  }
@@ -173,7 +168,7 @@ export class MenuItemNode extends Node {
173
168
  this.action = new Gio.SimpleAction(this.actionName);
174
169
  this.signalHandlerId = this.action.connect("activate", () => this.invokeCurrentCallback());
175
170
  const app = getCurrentApp();
176
- const action = getInterface(this.action, Gio.Action);
171
+ const action = getInterface(this.action.id, Gio.Action);
177
172
  if (!action) {
178
173
  throw new Error("Failed to get Gio.Action interface from SimpleAction");
179
174
  }
@@ -207,15 +202,19 @@ export class MenuItemNode extends Node {
207
202
  app.setAccelsForAction(`app.${this.actionName}`, accelArray);
208
203
  }
209
204
  }
210
- export class MenuSectionNode extends MenuContainerNode {
211
- static matches(type) {
212
- return type === "Menu.Section";
205
+ class MenuContainerItemNode extends MenuContainerNode {
206
+ static consumedPropNames = ["label"];
207
+ entryType = "section";
208
+ matchType = "";
209
+ static matches(_type) {
210
+ return false;
213
211
  }
214
212
  isVirtual() {
215
213
  return true;
216
214
  }
217
215
  entry = { type: "section" };
218
216
  initialize(props) {
217
+ this.entry = { type: this.entryType };
219
218
  this.entry.menu = this.menu;
220
219
  this.entry.label = props.label;
221
220
  super.initialize(props);
@@ -230,11 +229,6 @@ export class MenuSectionNode extends MenuContainerNode {
230
229
  return;
231
230
  parent.removeMenuEntry(this.entry);
232
231
  }
233
- consumedProps() {
234
- const consumed = super.consumedProps();
235
- consumed.add("label");
236
- return consumed;
237
- }
238
232
  updateProps(oldProps, newProps) {
239
233
  if (oldProps.label !== newProps.label && this.entry) {
240
234
  this.entry.label = newProps.label;
@@ -242,38 +236,15 @@ export class MenuSectionNode extends MenuContainerNode {
242
236
  super.updateProps(oldProps, newProps);
243
237
  }
244
238
  }
245
- export class MenuSubmenuNode extends MenuContainerNode {
239
+ export class MenuSectionNode extends MenuContainerItemNode {
240
+ entryType = "section";
246
241
  static matches(type) {
247
- return type === "Menu.Submenu";
248
- }
249
- isVirtual() {
250
- return true;
251
- }
252
- entry = { type: "submenu" };
253
- initialize(props) {
254
- this.entry.menu = this.menu;
255
- this.entry.label = props.label;
256
- super.initialize(props);
257
- }
258
- attachToParent(parent) {
259
- if (!isMenuContainer(parent))
260
- return;
261
- parent.addMenuEntry(this.entry);
262
- }
263
- detachFromParent(parent) {
264
- if (!isMenuContainer(parent))
265
- return;
266
- parent.removeMenuEntry(this.entry);
267
- }
268
- consumedProps() {
269
- const consumed = super.consumedProps();
270
- consumed.add("label");
271
- return consumed;
242
+ return type === "Menu.Section";
272
243
  }
273
- updateProps(oldProps, newProps) {
274
- if (oldProps.label !== newProps.label && this.entry) {
275
- this.entry.label = newProps.label;
276
- }
277
- super.updateProps(oldProps, newProps);
244
+ }
245
+ export class MenuSubmenuNode extends MenuContainerItemNode {
246
+ entryType = "submenu";
247
+ static matches(type) {
248
+ return type === "Menu.Submenu";
278
249
  }
279
250
  }
@@ -2,6 +2,7 @@ import * as Gtk from "@gtkx/ffi/gtk";
2
2
  import { type ChildContainer, type PageContainer } from "../container-interfaces.js";
3
3
  import type { Props } from "../factory.js";
4
4
  import { Node } from "../node.js";
5
+ import { VirtualSlotNode } from "./virtual-slot.js";
5
6
  export declare class NotebookNode extends Node<Gtk.Notebook> implements PageContainer, ChildContainer {
6
7
  static matches(type: string): boolean;
7
8
  addPage(child: Gtk.Widget, label: string): void;
@@ -12,20 +13,18 @@ export declare class NotebookNode extends Node<Gtk.Notebook> implements PageCont
12
13
  insertChildBefore(child: Gtk.Widget, before: Gtk.Widget): void;
13
14
  detachChild(child: Gtk.Widget): void;
14
15
  }
15
- export declare class NotebookPageNode extends Node {
16
+ type NotebookPageProps = {
17
+ label: string;
18
+ };
19
+ export declare class NotebookPageNode extends VirtualSlotNode<PageContainer, NotebookPageProps> {
20
+ static consumedPropNames: string[];
16
21
  static matches(type: string): boolean;
17
- protected isVirtual(): boolean;
18
- private label;
19
- private childWidget;
20
- private parentContainer;
21
- initialize(props: Props): void;
22
- getLabel(): string;
23
- setChildWidget(widget: Gtk.Widget): void;
24
- getChildWidget(): Gtk.Widget | null;
25
- appendChild(child: Node): void;
26
- attachToParent(parent: Node): void;
27
- attachToParentBefore(parent: Node, before: Node): void;
28
- detachFromParent(parent: Node): void;
29
- protected consumedProps(): Set<string>;
22
+ protected isValidContainer(parent: Node): parent is Node & PageContainer;
23
+ protected extractSlotProps(props: Props): NotebookPageProps;
24
+ protected addToContainer(container: PageContainer, child: Gtk.Widget, props: NotebookPageProps): void;
25
+ protected insertBeforeInContainer(container: PageContainer, child: Gtk.Widget, props: NotebookPageProps, before: Gtk.Widget): void;
26
+ protected removeFromContainer(container: PageContainer, child: Gtk.Widget): void;
27
+ protected updateInContainer(container: PageContainer, child: Gtk.Widget, props: NotebookPageProps): void;
30
28
  updateProps(oldProps: Props, newProps: Props): void;
31
29
  }
30
+ export {};
@@ -1,7 +1,7 @@
1
1
  import * as Gtk from "@gtkx/ffi/gtk";
2
2
  import { isPageContainer } from "../container-interfaces.js";
3
3
  import { Node } from "../node.js";
4
- import { getStringProp } from "../props.js";
4
+ import { VirtualSlotNode } from "./virtual-slot.js";
5
5
  export class NotebookNode extends Node {
6
6
  static matches(type) {
7
7
  return type === "Notebook" || type === "Notebook.Root";
@@ -49,71 +49,33 @@ export class NotebookNode extends Node {
49
49
  this.removePage(child);
50
50
  }
51
51
  }
52
- export class NotebookPageNode extends Node {
52
+ export class NotebookPageNode extends VirtualSlotNode {
53
+ static consumedPropNames = ["label"];
53
54
  static matches(type) {
54
55
  return type === "Notebook.Page";
55
56
  }
56
- isVirtual() {
57
- return true;
57
+ isValidContainer(parent) {
58
+ return isPageContainer(parent);
58
59
  }
59
- label = "";
60
- childWidget = null;
61
- parentContainer = null;
62
- initialize(props) {
63
- this.label = getStringProp(props, "label", "");
64
- super.initialize(props);
60
+ extractSlotProps(props) {
61
+ return {
62
+ label: props.label ?? "",
63
+ };
65
64
  }
66
- getLabel() {
67
- return this.label;
65
+ addToContainer(container, child, props) {
66
+ container.addPage(child, props.label);
68
67
  }
69
- setChildWidget(widget) {
70
- this.childWidget = widget;
68
+ insertBeforeInContainer(container, child, props, before) {
69
+ container.insertPageBefore(child, props.label, before);
71
70
  }
72
- getChildWidget() {
73
- return this.childWidget;
71
+ removeFromContainer(container, child) {
72
+ container.removePage(child);
74
73
  }
75
- appendChild(child) {
76
- const childWidget = child.getWidget();
77
- if (childWidget) {
78
- this.childWidget = childWidget;
79
- }
80
- }
81
- attachToParent(parent) {
82
- if (isPageContainer(parent) && this.childWidget) {
83
- this.parentContainer = parent;
84
- parent.addPage(this.childWidget, this.label);
85
- }
86
- }
87
- attachToParentBefore(parent, before) {
88
- if (isPageContainer(parent) && this.childWidget) {
89
- this.parentContainer = parent;
90
- const beforePage = before instanceof NotebookPageNode ? before.getChildWidget() : before.getWidget();
91
- if (beforePage) {
92
- parent.insertPageBefore(this.childWidget, this.label, beforePage);
93
- }
94
- else {
95
- parent.addPage(this.childWidget, this.label);
96
- }
97
- }
98
- }
99
- detachFromParent(parent) {
100
- if (isPageContainer(parent) && this.childWidget) {
101
- parent.removePage(this.childWidget);
102
- this.parentContainer = null;
103
- }
104
- }
105
- consumedProps() {
106
- const consumed = super.consumedProps();
107
- consumed.add("label");
108
- return consumed;
74
+ updateInContainer(container, child, props) {
75
+ container.updatePageLabel(child, props.label);
109
76
  }
110
77
  updateProps(oldProps, newProps) {
111
- if (oldProps.label !== newProps.label) {
112
- this.label = getStringProp(newProps, "label", "");
113
- if (this.parentContainer && this.childWidget) {
114
- this.parentContainer.updatePageLabel(this.childWidget, this.label);
115
- }
116
- }
78
+ this.updateSlotPropsIfChanged(oldProps, newProps, ["label"]);
117
79
  super.updateProps(oldProps, newProps);
118
80
  }
119
81
  }
@@ -0,0 +1,39 @@
1
+ import type * as Gtk from "@gtkx/ffi/gtk";
2
+ import type { ChildContainer, StackPageContainer, StackPageProps } from "../container-interfaces.js";
3
+ import type { Props } from "../factory.js";
4
+ import { Node } from "../node.js";
5
+ import { type StackPageLike } from "./stack-page-props.js";
6
+ type StackWidget = Gtk.Widget & {
7
+ getChildByName(name: string): Gtk.Widget | null;
8
+ setVisibleChild(child: Gtk.Widget): void;
9
+ remove(child: Gtk.Widget): void;
10
+ getPage(child: Gtk.Widget): StackPageLike;
11
+ };
12
+ /**
13
+ * Abstract node for paged stack widgets (Gtk.Stack, Adw.ViewStack).
14
+ * Handles visible child deferral and common page operations.
15
+ */
16
+ export declare abstract class PagedStackNode<T extends StackWidget> extends Node<T> implements StackPageContainer, ChildContainer {
17
+ static consumedPropNames: string[];
18
+ private pendingVisibleChildName;
19
+ /**
20
+ * Add a page to the stack widget. Must be implemented by subclasses
21
+ * due to API differences between Gtk.Stack and Adw.ViewStack.
22
+ */
23
+ abstract addStackPage(child: Gtk.Widget, props: StackPageProps): void;
24
+ /**
25
+ * Add a child directly to the stack widget (without page props).
26
+ * Must be implemented by subclasses due to API differences.
27
+ */
28
+ protected abstract addChildToWidget(child: Gtk.Widget): void;
29
+ protected applyPendingVisibleChild(): void;
30
+ insertStackPageBefore(child: Gtk.Widget, props: StackPageProps, _beforeChild: Gtk.Widget): void;
31
+ removeStackPage(child: Gtk.Widget): void;
32
+ updateStackPageProps(child: Gtk.Widget, props: StackPageProps): void;
33
+ attachChild(child: Gtk.Widget): void;
34
+ insertChildBefore(child: Gtk.Widget, _before: Gtk.Widget): void;
35
+ detachChild(child: Gtk.Widget): void;
36
+ private setVisibleChildOrDefer;
37
+ updateProps(oldProps: Props, newProps: Props): void;
38
+ }
39
+ export {};
@@ -0,0 +1,54 @@
1
+ import { Node } from "../node.js";
2
+ import { applyStackPageProps } from "./stack-page-props.js";
3
+ /**
4
+ * Abstract node for paged stack widgets (Gtk.Stack, Adw.ViewStack).
5
+ * Handles visible child deferral and common page operations.
6
+ */
7
+ export class PagedStackNode extends Node {
8
+ static consumedPropNames = ["visibleChildName"];
9
+ pendingVisibleChildName = null;
10
+ applyPendingVisibleChild() {
11
+ if (this.pendingVisibleChildName !== null) {
12
+ const child = this.widget.getChildByName(this.pendingVisibleChildName);
13
+ if (child) {
14
+ this.widget.setVisibleChild(child);
15
+ this.pendingVisibleChildName = null;
16
+ }
17
+ }
18
+ }
19
+ insertStackPageBefore(child, props, _beforeChild) {
20
+ this.addStackPage(child, props);
21
+ }
22
+ removeStackPage(child) {
23
+ this.widget.remove(child);
24
+ }
25
+ updateStackPageProps(child, props) {
26
+ const page = this.widget.getPage(child);
27
+ applyStackPageProps(page, props);
28
+ }
29
+ attachChild(child) {
30
+ this.addChildToWidget(child);
31
+ }
32
+ insertChildBefore(child, _before) {
33
+ this.addChildToWidget(child);
34
+ }
35
+ detachChild(child) {
36
+ this.widget.remove(child);
37
+ }
38
+ setVisibleChildOrDefer(name) {
39
+ const child = this.widget.getChildByName(name);
40
+ if (child) {
41
+ this.widget.setVisibleChild(child);
42
+ this.pendingVisibleChildName = null;
43
+ }
44
+ else {
45
+ this.pendingVisibleChildName = name;
46
+ }
47
+ }
48
+ updateProps(oldProps, newProps) {
49
+ if (newProps.visibleChildName !== undefined) {
50
+ this.setVisibleChildOrDefer(newProps.visibleChildName);
51
+ }
52
+ super.updateProps(oldProps, newProps);
53
+ }
54
+ }
@@ -0,0 +1,41 @@
1
+ import * as Gtk from "@gtkx/ffi/gtk";
2
+ import type { ItemContainer } from "../container-interfaces.js";
3
+ import type { Props } from "../factory.js";
4
+ import { Node } from "../node.js";
5
+ export type SelectableListState = {
6
+ itemsById: Map<string, unknown>;
7
+ itemOrder: string[];
8
+ committedOrder: string[];
9
+ stringList: Gtk.StringList;
10
+ selectionModel: Gtk.SingleSelection | Gtk.MultiSelection;
11
+ selectionMode: Gtk.SelectionMode;
12
+ selected: string[];
13
+ onSelectionChanged?: (ids: string[]) => void;
14
+ selectionHandlerId: number | null;
15
+ needsSync: boolean;
16
+ needsInitialSelection: boolean;
17
+ };
18
+ type SelectableListWidget = Gtk.Widget & {
19
+ setModel(model: Gtk.SelectionModel): void;
20
+ };
21
+ export declare abstract class SelectableListNode<T extends SelectableListWidget, S extends SelectableListState> extends Node<T, S> implements ItemContainer<unknown> {
22
+ protected initializeSelectionState(props: Props): SelectableListState;
23
+ protected applySelectionModel(): void;
24
+ protected cleanupSelection(): void;
25
+ protected updateSelectionProps(oldProps: Props, newProps: Props): void;
26
+ private recreateSelectionModel;
27
+ getItems(): unknown[];
28
+ getItemById(id: string): unknown;
29
+ addItem(id: string, data: unknown): void;
30
+ insertItemBefore(id: string, data: unknown, beforeId: string): void;
31
+ removeItem(id: string): void;
32
+ updateItem(id: string, data: unknown): void;
33
+ protected scheduleSync(): void;
34
+ protected syncModel: () => void;
35
+ protected applyInitialSelection(): void;
36
+ protected applySelection(ids: string[]): void;
37
+ protected getSelectedIds(): string[];
38
+ protected connectSelectionHandler(): void;
39
+ protected disconnectSelectionHandler(): void;
40
+ }
41
+ export {};
@@ -0,0 +1,228 @@
1
+ import { getInterface } from "@gtkx/ffi";
2
+ import * as Gio from "@gtkx/ffi/gio";
3
+ import * as GObject from "@gtkx/ffi/gobject";
4
+ import * as Gtk from "@gtkx/ffi/gtk";
5
+ import { scheduleFlush } from "../batch.js";
6
+ import { Node } from "../node.js";
7
+ import { getCallbackChange } from "../props.js";
8
+ export class SelectableListNode extends Node {
9
+ initializeSelectionState(props) {
10
+ const selectionMode = props.selectionMode ?? Gtk.SelectionMode.SINGLE;
11
+ const stringList = new Gtk.StringList([]);
12
+ const listModel = getInterface(stringList.id, Gio.ListModel);
13
+ const selectionModel = selectionMode === Gtk.SelectionMode.MULTIPLE
14
+ ? new Gtk.MultiSelection(listModel)
15
+ : new Gtk.SingleSelection(listModel);
16
+ if (selectionModel instanceof Gtk.SingleSelection) {
17
+ selectionModel.setAutoselect(false);
18
+ selectionModel.setCanUnselect(true);
19
+ }
20
+ return {
21
+ itemsById: new Map(),
22
+ itemOrder: [],
23
+ committedOrder: [],
24
+ stringList,
25
+ selectionModel,
26
+ selectionMode,
27
+ selected: props.selected ?? [],
28
+ onSelectionChanged: props.onSelectionChanged,
29
+ selectionHandlerId: null,
30
+ needsSync: false,
31
+ needsInitialSelection: true,
32
+ };
33
+ }
34
+ applySelectionModel() {
35
+ this.widget.setModel(this.state.selectionModel);
36
+ }
37
+ cleanupSelection() {
38
+ this.disconnectSelectionHandler();
39
+ }
40
+ updateSelectionProps(oldProps, newProps) {
41
+ const oldSelectionMode = oldProps.selectionMode;
42
+ const newSelectionMode = newProps.selectionMode;
43
+ const effectiveOldMode = oldSelectionMode ?? Gtk.SelectionMode.SINGLE;
44
+ const effectiveNewMode = newSelectionMode ?? Gtk.SelectionMode.SINGLE;
45
+ if (effectiveOldMode !== effectiveNewMode) {
46
+ this.recreateSelectionModel(effectiveNewMode);
47
+ }
48
+ const oldCallback = oldProps.onSelectionChanged;
49
+ const newCallback = newProps.onSelectionChanged;
50
+ const change = getCallbackChange(oldCallback, newCallback);
51
+ if (change.action !== "none") {
52
+ this.state.onSelectionChanged = change.callback;
53
+ if (change.action === "disconnect") {
54
+ this.disconnectSelectionHandler();
55
+ }
56
+ else if (change.action === "connect") {
57
+ this.connectSelectionHandler();
58
+ }
59
+ }
60
+ const oldSelected = oldProps.selected;
61
+ const newSelected = newProps.selected;
62
+ if (oldSelected !== newSelected && newSelected !== undefined) {
63
+ this.state.selected = newSelected;
64
+ this.applySelection(newSelected);
65
+ }
66
+ }
67
+ recreateSelectionModel(newSelectionMode) {
68
+ const currentSelection = this.getSelectedIds();
69
+ const hadHandler = this.state.selectionHandlerId !== null;
70
+ this.disconnectSelectionHandler();
71
+ const listModel = getInterface(this.state.stringList.id, Gio.ListModel);
72
+ const newSelectionModel = newSelectionMode === Gtk.SelectionMode.MULTIPLE
73
+ ? new Gtk.MultiSelection(listModel)
74
+ : new Gtk.SingleSelection(listModel);
75
+ if (newSelectionModel instanceof Gtk.SingleSelection) {
76
+ newSelectionModel.setAutoselect(false);
77
+ newSelectionModel.setCanUnselect(true);
78
+ }
79
+ this.state.selectionModel = newSelectionModel;
80
+ this.state.selectionMode = newSelectionMode;
81
+ this.applySelectionModel();
82
+ this.applySelection(currentSelection);
83
+ if (hadHandler && this.state.onSelectionChanged) {
84
+ this.connectSelectionHandler();
85
+ }
86
+ }
87
+ getItems() {
88
+ return this.state.itemOrder.map((id) => this.state.itemsById.get(id));
89
+ }
90
+ getItemById(id) {
91
+ return this.state.itemsById.get(id);
92
+ }
93
+ addItem(id, data) {
94
+ this.state.itemsById.set(id, data);
95
+ const existingIndex = this.state.itemOrder.indexOf(id);
96
+ if (existingIndex !== -1) {
97
+ this.state.itemOrder.splice(existingIndex, 1);
98
+ }
99
+ this.state.itemOrder.push(id);
100
+ this.scheduleSync();
101
+ }
102
+ insertItemBefore(id, data, beforeId) {
103
+ this.state.itemsById.set(id, data);
104
+ const existingIndex = this.state.itemOrder.indexOf(id);
105
+ if (existingIndex !== -1) {
106
+ this.state.itemOrder.splice(existingIndex, 1);
107
+ }
108
+ const beforeIndex = this.state.itemOrder.indexOf(beforeId);
109
+ if (beforeIndex === -1) {
110
+ this.state.itemOrder.push(id);
111
+ }
112
+ else {
113
+ this.state.itemOrder.splice(beforeIndex, 0, id);
114
+ }
115
+ this.scheduleSync();
116
+ }
117
+ removeItem(id) {
118
+ const index = this.state.itemOrder.indexOf(id);
119
+ if (index !== -1) {
120
+ this.state.itemOrder.splice(index, 1);
121
+ this.state.itemsById.delete(id);
122
+ this.scheduleSync();
123
+ }
124
+ }
125
+ updateItem(id, data) {
126
+ if (this.state.itemsById.has(id)) {
127
+ this.state.itemsById.set(id, data);
128
+ }
129
+ }
130
+ scheduleSync() {
131
+ if (!this.state.needsSync) {
132
+ this.state.needsSync = true;
133
+ scheduleFlush(this.syncModel);
134
+ }
135
+ }
136
+ syncModel = () => {
137
+ if (!this.state.needsSync)
138
+ return;
139
+ this.state.needsSync = false;
140
+ const oldOrder = this.state.committedOrder;
141
+ const newOrder = this.state.itemOrder;
142
+ let firstDiff = 0;
143
+ const minLen = Math.min(oldOrder.length, newOrder.length);
144
+ while (firstDiff < minLen && oldOrder[firstDiff] === newOrder[firstDiff]) {
145
+ firstDiff++;
146
+ }
147
+ let oldEndOffset = 0;
148
+ let newEndOffset = 0;
149
+ while (oldEndOffset < oldOrder.length - firstDiff &&
150
+ newEndOffset < newOrder.length - firstDiff &&
151
+ oldOrder[oldOrder.length - 1 - oldEndOffset] === newOrder[newOrder.length - 1 - newEndOffset]) {
152
+ oldEndOffset++;
153
+ newEndOffset++;
154
+ }
155
+ const removeCount = oldOrder.length - firstDiff - oldEndOffset;
156
+ const addItems = newOrder.slice(firstDiff, newOrder.length - newEndOffset);
157
+ if (removeCount > 0 || addItems.length > 0) {
158
+ this.state.stringList.splice(firstDiff, removeCount, addItems);
159
+ }
160
+ this.state.committedOrder = [...newOrder];
161
+ if (this.state.needsInitialSelection && this.state.itemOrder.length > 0) {
162
+ this.state.needsInitialSelection = false;
163
+ queueMicrotask(() => {
164
+ if (this.hasParent()) {
165
+ this.applyInitialSelection();
166
+ }
167
+ });
168
+ }
169
+ };
170
+ applyInitialSelection() {
171
+ this.applySelection(this.state.selected);
172
+ if (this.state.onSelectionChanged) {
173
+ this.connectSelectionHandler();
174
+ this.state.onSelectionChanged(this.getSelectedIds());
175
+ }
176
+ }
177
+ applySelection(ids) {
178
+ if (this.state.selectionMode === Gtk.SelectionMode.MULTIPLE) {
179
+ const multiSelection = this.state.selectionModel;
180
+ multiSelection.unselectAll();
181
+ for (const id of ids) {
182
+ const index = this.state.itemOrder.indexOf(id);
183
+ if (index !== -1) {
184
+ multiSelection.selectItem(index, false);
185
+ }
186
+ }
187
+ }
188
+ else {
189
+ const singleSelection = this.state.selectionModel;
190
+ const firstId = ids[0];
191
+ if (firstId !== undefined) {
192
+ const index = this.state.itemOrder.indexOf(firstId);
193
+ if (index !== -1) {
194
+ singleSelection.setSelected(index);
195
+ }
196
+ }
197
+ else {
198
+ singleSelection.setSelected(Gtk.INVALID_LIST_POSITION);
199
+ }
200
+ }
201
+ }
202
+ getSelectedIds() {
203
+ const selection = this.state.selectionModel.getSelection();
204
+ const count = Number(selection.getSize());
205
+ const selectedIds = [];
206
+ for (let i = 0; i < count; i++) {
207
+ const position = selection.getNth(i);
208
+ const id = this.state.itemOrder[position];
209
+ if (id !== undefined) {
210
+ selectedIds.push(id);
211
+ }
212
+ }
213
+ return selectedIds;
214
+ }
215
+ connectSelectionHandler() {
216
+ if (this.state.selectionHandlerId !== null)
217
+ return;
218
+ this.state.selectionHandlerId = this.state.selectionModel.connect("selection-changed", () => {
219
+ this.state.onSelectionChanged?.(this.getSelectedIds());
220
+ });
221
+ }
222
+ disconnectSelectionHandler() {
223
+ if (this.state.selectionHandlerId !== null) {
224
+ GObject.signalHandlerDisconnect(this.state.selectionModel, this.state.selectionHandlerId);
225
+ this.state.selectionHandlerId = null;
226
+ }
227
+ }
228
+ }
@@ -0,0 +1,11 @@
1
+ import type { StackPageProps } from "../container-interfaces.js";
2
+ export type StackPageLike = {
3
+ setName(name: string): void;
4
+ setTitle(title: string): void;
5
+ setIconName(iconName: string): void;
6
+ setNeedsAttention(needsAttention: boolean): void;
7
+ setVisible(visible: boolean): void;
8
+ setUseUnderline(useUnderline: boolean): void;
9
+ setBadgeNumber?(badgeNumber: number): void;
10
+ };
11
+ export declare function applyStackPageProps(page: StackPageLike, props: StackPageProps): void;