@gtkx/react 0.18.9 → 0.19.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 (174) hide show
  1. package/dist/generated/internal.d.ts +6 -0
  2. package/dist/generated/internal.d.ts.map +1 -1
  3. package/dist/generated/internal.js +331 -44
  4. package/dist/generated/internal.js.map +1 -1
  5. package/dist/generated/jsx.d.ts +178 -2
  6. package/dist/generated/jsx.d.ts.map +1 -1
  7. package/dist/generated/jsx.js.map +1 -1
  8. package/dist/host-config.d.ts.map +1 -1
  9. package/dist/host-config.js +46 -10
  10. package/dist/host-config.js.map +1 -1
  11. package/dist/jsx.d.ts +133 -13
  12. package/dist/jsx.d.ts.map +1 -1
  13. package/dist/jsx.js +41 -2
  14. package/dist/jsx.js.map +1 -1
  15. package/dist/metadata.d.ts +1 -0
  16. package/dist/metadata.d.ts.map +1 -1
  17. package/dist/metadata.js +3 -1
  18. package/dist/metadata.js.map +1 -1
  19. package/dist/node.d.ts +2 -0
  20. package/dist/node.d.ts.map +1 -1
  21. package/dist/node.js +22 -6
  22. package/dist/node.js.map +1 -1
  23. package/dist/nodes/column-view-column.d.ts +4 -1
  24. package/dist/nodes/column-view-column.d.ts.map +1 -1
  25. package/dist/nodes/column-view-column.js +29 -8
  26. package/dist/nodes/column-view-column.js.map +1 -1
  27. package/dist/nodes/column-view.d.ts +4 -3
  28. package/dist/nodes/column-view.d.ts.map +1 -1
  29. package/dist/nodes/column-view.js +44 -14
  30. package/dist/nodes/column-view.js.map +1 -1
  31. package/dist/nodes/drop-down.d.ts +12 -2
  32. package/dist/nodes/drop-down.d.ts.map +1 -1
  33. package/dist/nodes/drop-down.js +151 -5
  34. package/dist/nodes/drop-down.js.map +1 -1
  35. package/dist/nodes/event-controller.d.ts +1 -0
  36. package/dist/nodes/event-controller.d.ts.map +1 -1
  37. package/dist/nodes/event-controller.js +11 -3
  38. package/dist/nodes/event-controller.js.map +1 -1
  39. package/dist/nodes/fixed-child.d.ts +1 -2
  40. package/dist/nodes/fixed-child.d.ts.map +1 -1
  41. package/dist/nodes/fixed-child.js +21 -21
  42. package/dist/nodes/fixed-child.js.map +1 -1
  43. package/dist/nodes/font-dialog-button.d.ts +1 -1
  44. package/dist/nodes/font-dialog-button.d.ts.map +1 -1
  45. package/dist/nodes/font-dialog-button.js +8 -0
  46. package/dist/nodes/font-dialog-button.js.map +1 -1
  47. package/dist/nodes/grid-view.d.ts +6 -5
  48. package/dist/nodes/grid-view.d.ts.map +1 -1
  49. package/dist/nodes/grid-view.js +23 -18
  50. package/dist/nodes/grid-view.js.map +1 -1
  51. package/dist/nodes/internal/accessible.d.ts +5 -0
  52. package/dist/nodes/internal/accessible.d.ts.map +1 -0
  53. package/dist/nodes/internal/accessible.js +119 -0
  54. package/dist/nodes/internal/accessible.js.map +1 -0
  55. package/dist/nodes/internal/base-item-renderer.d.ts.map +1 -1
  56. package/dist/nodes/internal/base-item-renderer.js +0 -1
  57. package/dist/nodes/internal/base-item-renderer.js.map +1 -1
  58. package/dist/nodes/internal/construct.d.ts +10 -0
  59. package/dist/nodes/internal/construct.d.ts.map +1 -0
  60. package/dist/nodes/internal/construct.js +68 -0
  61. package/dist/nodes/internal/construct.js.map +1 -0
  62. package/dist/nodes/internal/header-item-renderer.d.ts +23 -0
  63. package/dist/nodes/internal/header-item-renderer.d.ts.map +1 -0
  64. package/dist/nodes/internal/header-item-renderer.js +87 -0
  65. package/dist/nodes/internal/header-item-renderer.js.map +1 -0
  66. package/dist/nodes/internal/header-renderer-manager.d.ts +13 -0
  67. package/dist/nodes/internal/header-renderer-manager.d.ts.map +1 -0
  68. package/dist/nodes/internal/header-renderer-manager.js +20 -0
  69. package/dist/nodes/internal/header-renderer-manager.js.map +1 -0
  70. package/dist/nodes/internal/list-store.d.ts +10 -11
  71. package/dist/nodes/internal/list-store.d.ts.map +1 -1
  72. package/dist/nodes/internal/list-store.js +28 -29
  73. package/dist/nodes/internal/list-store.js.map +1 -1
  74. package/dist/nodes/internal/sectioned-list-store.d.ts +50 -0
  75. package/dist/nodes/internal/sectioned-list-store.d.ts.map +1 -0
  76. package/dist/nodes/internal/sectioned-list-store.js +250 -0
  77. package/dist/nodes/internal/sectioned-list-store.js.map +1 -0
  78. package/dist/nodes/internal/selection-helpers.d.ts +12 -0
  79. package/dist/nodes/internal/selection-helpers.d.ts.map +1 -0
  80. package/dist/nodes/internal/selection-helpers.js +25 -0
  81. package/dist/nodes/internal/selection-helpers.js.map +1 -0
  82. package/dist/nodes/internal/selection-model-controller.d.ts.map +1 -1
  83. package/dist/nodes/internal/selection-model-controller.js +3 -0
  84. package/dist/nodes/internal/selection-model-controller.js.map +1 -1
  85. package/dist/nodes/internal/simple-list-store.d.ts +7 -12
  86. package/dist/nodes/internal/simple-list-store.d.ts.map +1 -1
  87. package/dist/nodes/internal/simple-list-store.js +58 -35
  88. package/dist/nodes/internal/simple-list-store.js.map +1 -1
  89. package/dist/nodes/internal/text-buffer-controller.d.ts +4 -0
  90. package/dist/nodes/internal/text-buffer-controller.d.ts.map +1 -1
  91. package/dist/nodes/internal/text-buffer-controller.js +49 -9
  92. package/dist/nodes/internal/text-buffer-controller.js.map +1 -1
  93. package/dist/nodes/internal/tree-store.d.ts +3 -0
  94. package/dist/nodes/internal/tree-store.d.ts.map +1 -1
  95. package/dist/nodes/internal/tree-store.js +55 -10
  96. package/dist/nodes/internal/tree-store.js.map +1 -1
  97. package/dist/nodes/list-section.d.ts +27 -0
  98. package/dist/nodes/list-section.d.ts.map +1 -0
  99. package/dist/nodes/list-section.js +43 -0
  100. package/dist/nodes/list-section.js.map +1 -0
  101. package/dist/nodes/list-view.d.ts +6 -3
  102. package/dist/nodes/list-view.d.ts.map +1 -1
  103. package/dist/nodes/list-view.js +54 -14
  104. package/dist/nodes/list-view.js.map +1 -1
  105. package/dist/nodes/models/list.d.ts +13 -5
  106. package/dist/nodes/models/list.d.ts.map +1 -1
  107. package/dist/nodes/models/list.js +135 -21
  108. package/dist/nodes/models/list.js.map +1 -1
  109. package/dist/nodes/shortcut.d.ts +3 -2
  110. package/dist/nodes/shortcut.d.ts.map +1 -1
  111. package/dist/nodes/shortcut.js +19 -4
  112. package/dist/nodes/shortcut.js.map +1 -1
  113. package/dist/nodes/text-anchor.d.ts.map +1 -1
  114. package/dist/nodes/text-anchor.js +7 -1
  115. package/dist/nodes/text-anchor.js.map +1 -1
  116. package/dist/nodes/text-tag.d.ts.map +1 -1
  117. package/dist/nodes/text-tag.js +5 -1
  118. package/dist/nodes/text-tag.js.map +1 -1
  119. package/dist/nodes/text-view.d.ts +1 -0
  120. package/dist/nodes/text-view.d.ts.map +1 -1
  121. package/dist/nodes/text-view.js +4 -0
  122. package/dist/nodes/text-view.js.map +1 -1
  123. package/dist/nodes/widget.d.ts +0 -2
  124. package/dist/nodes/widget.d.ts.map +1 -1
  125. package/dist/nodes/widget.js +44 -61
  126. package/dist/nodes/widget.js.map +1 -1
  127. package/dist/registry.d.ts.map +1 -1
  128. package/dist/registry.js +2 -2
  129. package/dist/registry.js.map +1 -1
  130. package/package.json +3 -3
  131. package/src/generated/internal.ts +333 -44
  132. package/src/generated/jsx.ts +178 -2
  133. package/src/host-config.ts +41 -10
  134. package/src/jsx.ts +166 -15
  135. package/src/metadata.ts +5 -1
  136. package/src/node.ts +20 -6
  137. package/src/nodes/column-view-column.ts +32 -8
  138. package/src/nodes/column-view.ts +59 -14
  139. package/src/nodes/drop-down.ts +182 -6
  140. package/src/nodes/event-controller.ts +11 -3
  141. package/src/nodes/fixed-child.ts +24 -23
  142. package/src/nodes/font-dialog-button.ts +10 -0
  143. package/src/nodes/grid-view.ts +29 -19
  144. package/src/nodes/internal/accessible.ts +156 -0
  145. package/src/nodes/internal/base-item-renderer.ts +0 -1
  146. package/src/nodes/internal/construct.ts +90 -0
  147. package/src/nodes/internal/header-item-renderer.ts +105 -0
  148. package/src/nodes/internal/header-renderer-manager.ts +33 -0
  149. package/src/nodes/internal/list-store.ts +32 -30
  150. package/src/nodes/internal/sectioned-list-store.ts +287 -0
  151. package/src/nodes/internal/selection-helpers.ts +35 -0
  152. package/src/nodes/internal/selection-model-controller.ts +4 -0
  153. package/src/nodes/internal/simple-list-store.ts +60 -43
  154. package/src/nodes/internal/text-buffer-controller.ts +51 -8
  155. package/src/nodes/internal/tree-store.ts +61 -9
  156. package/src/nodes/list-section.ts +64 -0
  157. package/src/nodes/list-view.ts +65 -14
  158. package/src/nodes/models/list.ts +147 -37
  159. package/src/nodes/shortcut.ts +22 -5
  160. package/src/nodes/text-anchor.ts +6 -1
  161. package/src/nodes/text-tag.ts +7 -1
  162. package/src/nodes/text-view.ts +5 -0
  163. package/src/nodes/widget.ts +45 -62
  164. package/src/registry.ts +4 -2
  165. package/dist/nodes/models/grid.d.ts +0 -28
  166. package/dist/nodes/models/grid.d.ts.map +0 -1
  167. package/dist/nodes/models/grid.js +0 -69
  168. package/dist/nodes/models/grid.js.map +0 -1
  169. package/dist/nodes/shortcut-controller.d.ts +0 -10
  170. package/dist/nodes/shortcut-controller.d.ts.map +0 -1
  171. package/dist/nodes/shortcut-controller.js +0 -23
  172. package/dist/nodes/shortcut-controller.js.map +0 -1
  173. package/src/nodes/models/grid.ts +0 -105
  174. package/src/nodes/shortcut-controller.ts +0 -27
@@ -54,7 +54,6 @@ export abstract class BaseItemRenderer<TStore = unknown> {
54
54
 
55
55
  protected createBox(): Gtk.Box {
56
56
  const box = new Gtk.Box(Gtk.Orientation.HORIZONTAL);
57
- box.setValign(Gtk.Align.CENTER);
58
57
 
59
58
  if (this.estimatedItemHeight !== null) {
60
59
  box.setSizeRequest(-1, this.estimatedItemHeight);
@@ -0,0 +1,90 @@
1
+ import { type Arg, call, type NativeHandle, registerNativeObject, setInstantiating, type Type } from "@gtkx/ffi";
2
+ import { typeFromName } from "@gtkx/ffi/gobject";
3
+ import { CONSTRUCT_ONLY_PROPS, CONSTRUCTOR_PROPS } from "../../generated/internal.js";
4
+ import type { Container, ContainerClass, Props } from "../../types.js";
5
+
6
+ type ConstructOnlyPropInfo = { girName: string; ffiType: Type };
7
+
8
+ /**
9
+ * Collects construct-only property metadata by walking the class hierarchy.
10
+ * Returns all active construct-only props (those with values set in the initial props)
11
+ * that are NOT already handled by the designated constructor parameters.
12
+ */
13
+ function collectActiveConstructOnlyProps(
14
+ containerClass: ContainerClass,
15
+ props: Props,
16
+ ): Array<{ girName: string; ffiType: Type; value: unknown }> {
17
+ const result: Array<{ girName: string; ffiType: Type; value: unknown }> = [];
18
+ const constructorParams = new Set(CONSTRUCTOR_PROPS[containerClass.glibTypeName] ?? []);
19
+
20
+ // biome-ignore lint/suspicious/noExplicitAny: Walking static prototype chain
21
+ let current: any = containerClass;
22
+ while (current?.glibTypeName) {
23
+ const propsForType: Record<string, ConstructOnlyPropInfo> | undefined =
24
+ CONSTRUCT_ONLY_PROPS[current.glibTypeName as string];
25
+ if (propsForType) {
26
+ for (const [camelName, meta] of Object.entries(propsForType)) {
27
+ if (constructorParams.has(camelName)) continue;
28
+ if (props[camelName] !== undefined) {
29
+ const rawValue = props[camelName];
30
+ const value =
31
+ meta.ffiType.type === "gobject" &&
32
+ rawValue &&
33
+ typeof rawValue === "object" &&
34
+ "handle" in rawValue
35
+ ? (rawValue as { handle: NativeHandle }).handle
36
+ : rawValue;
37
+ result.push({ girName: meta.girName, ffiType: meta.ffiType, value });
38
+ }
39
+ }
40
+ }
41
+ current = Object.getPrototypeOf(current);
42
+ }
43
+
44
+ return result;
45
+ }
46
+
47
+ /**
48
+ * Creates a container (widget or controller) with construct-only properties.
49
+ *
50
+ * When construct-only props are present that aren't handled by the designated
51
+ * constructor, uses `g_object_new` to set them during object construction.
52
+ * Otherwise falls back to the normal constructor.
53
+ */
54
+ export function createContainerWithConstructOnly(
55
+ containerClass: ContainerClass,
56
+ props: Props,
57
+ normalConstructor: () => Container,
58
+ ): Container {
59
+ const constructOnlyArgs = collectActiveConstructOnlyProps(containerClass, props);
60
+
61
+ if (constructOnlyArgs.length === 0) {
62
+ return normalConstructor();
63
+ }
64
+
65
+ const typeName = containerClass.glibTypeName;
66
+ const gtype = typeFromName(typeName);
67
+
68
+ const args: Arg[] = [{ type: { type: "int", size: 64, unsigned: true }, value: gtype, optional: false }];
69
+
70
+ for (const { girName, ffiType, value } of constructOnlyArgs) {
71
+ args.push({ type: { type: "string", ownership: "borrowed" }, value: girName, optional: false });
72
+ args.push({ type: ffiType, value, optional: false });
73
+ }
74
+
75
+ args.push({ type: { type: "null" }, value: null, optional: false });
76
+
77
+ const handle = call("libgobject-2.0.so.0", "g_object_new", args, {
78
+ type: "gobject",
79
+ ownership: "full",
80
+ }) as NativeHandle;
81
+
82
+ setInstantiating(true);
83
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic instantiation with isInstantiating flag
84
+ const instance = new (containerClass as any)() as Container & { handle: NativeHandle };
85
+ setInstantiating(false);
86
+ instance.handle = handle;
87
+ registerNativeObject(instance);
88
+
89
+ return instance;
90
+ }
@@ -0,0 +1,105 @@
1
+ import * as Gtk from "@gtkx/ffi/gtk";
2
+ import type { ReactNode } from "react";
3
+ import type Reconciler from "react-reconciler";
4
+ import { createFiberRoot } from "../../fiber-root.js";
5
+ import { reconciler } from "../../reconciler.js";
6
+ import type { SignalStore } from "./signal-store.js";
7
+
8
+ type RenderHeaderFn = (item: unknown) => ReactNode;
9
+
10
+ type ItemResolver = (id: string) => unknown;
11
+
12
+ export class HeaderItemRenderer {
13
+ private factory: Gtk.SignalListItemFactory;
14
+ private fiberRoots = new Map<object, Reconciler.FiberRoot>();
15
+ private tornDown = new Set<object>();
16
+ private renderFn: RenderHeaderFn | null = () => null;
17
+ private boundHeaders = new Map<string, object>();
18
+ private signalStore: SignalStore;
19
+ private resolveItem: ItemResolver | null = null;
20
+
21
+ constructor(signalStore: SignalStore) {
22
+ this.signalStore = signalStore;
23
+ this.factory = new Gtk.SignalListItemFactory();
24
+ this.initializeFactory();
25
+ }
26
+
27
+ public getFactory(): Gtk.SignalListItemFactory {
28
+ return this.factory;
29
+ }
30
+
31
+ public setResolveItem(resolver: ItemResolver | null): void {
32
+ this.resolveItem = resolver;
33
+ }
34
+
35
+ public setRenderFn(renderFn: RenderHeaderFn | null): void {
36
+ this.renderFn = renderFn;
37
+ this.rebindAllHeaders();
38
+ }
39
+
40
+ public dispose(): void {
41
+ this.signalStore.clear(this);
42
+ this.fiberRoots.clear();
43
+ this.tornDown.clear();
44
+ this.boundHeaders.clear();
45
+ }
46
+
47
+ private rebindAllHeaders(): void {
48
+ for (const [id, header] of this.boundHeaders) {
49
+ const fiberRoot = this.fiberRoots.get(header);
50
+ if (!fiberRoot) continue;
51
+ const item = this.resolveItem?.(id) ?? null;
52
+ const element = this.renderFn?.(item);
53
+ reconciler.getInstance().updateContainer(element, fiberRoot, null, () => {});
54
+ }
55
+ }
56
+
57
+ private initializeFactory(): void {
58
+ this.signalStore.set(this, this.factory, "setup", (listHeader: Gtk.ListHeader) => {
59
+ const box = new Gtk.Box(Gtk.Orientation.HORIZONTAL);
60
+ listHeader.setChild(box);
61
+ const fiberRoot = createFiberRoot(box);
62
+ this.fiberRoots.set(listHeader, fiberRoot);
63
+ const element = this.renderFn?.(null);
64
+ reconciler.getInstance().updateContainer(element, fiberRoot, null, () => {});
65
+ });
66
+
67
+ this.signalStore.set(this, this.factory, "bind", (listHeader: Gtk.ListHeader) => {
68
+ const fiberRoot = this.fiberRoots.get(listHeader);
69
+ if (!fiberRoot) return;
70
+
71
+ const stringObject = listHeader.getItem();
72
+ let id: string | null = null;
73
+ if (stringObject instanceof Gtk.StringObject) {
74
+ id = stringObject.getString();
75
+ }
76
+
77
+ if (id !== null) {
78
+ this.boundHeaders.set(id, listHeader);
79
+ }
80
+
81
+ const item = id !== null && this.resolveItem ? this.resolveItem(id) : null;
82
+ const element = this.renderFn?.(item);
83
+ reconciler.getInstance().updateContainer(element, fiberRoot, null, () => {});
84
+ });
85
+
86
+ this.signalStore.set(this, this.factory, "unbind", (listHeader: Gtk.ListHeader) => {
87
+ const stringObject = listHeader.getItem();
88
+ if (stringObject instanceof Gtk.StringObject) {
89
+ this.boundHeaders.delete(stringObject.getString());
90
+ }
91
+ });
92
+
93
+ this.signalStore.set(this, this.factory, "teardown", (listHeader: Gtk.ListHeader) => {
94
+ const fiberRoot = this.fiberRoots.get(listHeader);
95
+ if (fiberRoot) {
96
+ this.tornDown.add(listHeader);
97
+ reconciler.getInstance().updateContainer(null, fiberRoot, null, () => {});
98
+ queueMicrotask(() => {
99
+ this.fiberRoots.delete(listHeader);
100
+ this.tornDown.delete(listHeader);
101
+ });
102
+ }
103
+ });
104
+ }
105
+ }
@@ -0,0 +1,33 @@
1
+ import type * as Gtk from "@gtkx/ffi/gtk";
2
+ import type { ReactNode } from "react";
3
+ import { HeaderItemRenderer } from "./header-item-renderer.js";
4
+ import type { SignalStore } from "./signal-store.js";
5
+
6
+ type HeaderRendererConfig = {
7
+ signalStore: SignalStore;
8
+ isEnabled: () => boolean;
9
+ resolveItem: (id: string) => unknown;
10
+ setFactory: (factory: Gtk.SignalListItemFactory | null) => void;
11
+ };
12
+
13
+ export function updateHeaderRenderer(
14
+ current: HeaderItemRenderer | null,
15
+ config: HeaderRendererConfig,
16
+ renderFn: ((item: unknown) => ReactNode) | null | undefined,
17
+ ): HeaderItemRenderer | null {
18
+ if (renderFn) {
19
+ if (!current && config.isEnabled()) {
20
+ current = new HeaderItemRenderer(config.signalStore);
21
+ current.setResolveItem(config.resolveItem);
22
+ config.setFactory(current.getFactory());
23
+ }
24
+ if (current) {
25
+ current.setRenderFn(renderFn);
26
+ }
27
+ } else if (current) {
28
+ config.setFactory(null);
29
+ current.dispose();
30
+ current = null;
31
+ }
32
+ return current;
33
+ }
@@ -1,32 +1,33 @@
1
- import * as Gtk from "@gtkx/ffi/gtk";
1
+ import { SectionedListStore } from "./sectioned-list-store.js";
2
2
 
3
3
  type ItemUpdatedCallback = (id: string) => void;
4
4
 
5
- export class ListStore {
6
- private model = new Gtk.StringList();
7
- private ids: string[] = [];
8
- private idToIndex = new Map<string, number>();
5
+ export class ListStore extends SectionedListStore {
9
6
  private items = new Map<string, unknown>();
10
7
  private onItemUpdated: ItemUpdatedCallback | null = null;
11
- private pendingBatch: string[] | null = null;
12
8
 
13
9
  public setOnItemUpdated(callback: ItemUpdatedCallback | null): void {
14
10
  this.onItemUpdated = callback;
15
11
  }
16
12
 
17
- public beginBatch(): void {
18
- this.pendingBatch = [];
13
+ protected override getInitialPendingBatch(): string[] | null {
14
+ return null;
19
15
  }
20
16
 
21
- public flushBatch(): void {
22
- const batch = this.pendingBatch;
23
- this.pendingBatch = null;
24
- if (batch && batch.length > 0) {
25
- this.model.splice(0, 0, batch);
26
- }
17
+ protected override getModelString(itemId: string, _item: unknown): string {
18
+ return itemId;
19
+ }
20
+
21
+ protected override onItemAdded(itemId: string, item: unknown): void {
22
+ this.items.set(itemId, item);
23
+ }
24
+
25
+ protected override onItemRemoved(itemId: string): void {
26
+ this.items.delete(itemId);
27
27
  }
28
28
 
29
29
  public addItem(id: string, item: unknown): void {
30
+ this.flushRemovals();
30
31
  this.items.set(id, item);
31
32
 
32
33
  const existingIndex = this.idToIndex.get(id);
@@ -46,18 +47,8 @@ export class ListStore {
46
47
  }
47
48
  }
48
49
 
49
- public removeItem(id: string): void {
50
- const index = this.idToIndex.get(id);
51
- if (index === undefined) return;
52
-
53
- this.model.remove(index);
54
- this.ids.splice(index, 1);
55
- this.idToIndex.delete(id);
56
- this.rebuildIndices(index);
57
- this.items.delete(id);
58
- }
59
-
60
50
  public insertItemBefore(id: string, beforeId: string, item: unknown): void {
51
+ this.flushRemovals();
61
52
  this.items.set(id, item);
62
53
 
63
54
  const existingIndex = this.idToIndex.get(id);
@@ -93,13 +84,24 @@ export class ListStore {
93
84
  return this.items.get(id);
94
85
  }
95
86
 
96
- public getModel(): Gtk.StringList {
87
+ public getHeaderValue(itemId: string): unknown {
88
+ const sectionId = this.itemToSection.get(itemId);
89
+ if (sectionId) {
90
+ return this.headerValues.get(sectionId);
91
+ }
92
+ return undefined;
93
+ }
94
+
95
+ public getHeaderValueById(sectionId: string): unknown {
96
+ return this.headerValues.get(sectionId);
97
+ }
98
+
99
+ public getStringList(): import("@gtkx/ffi/gtk").StringList {
97
100
  return this.model;
98
101
  }
99
102
 
100
- private rebuildIndices(fromIndex: number): void {
101
- for (let i = fromIndex; i < this.ids.length; i++) {
102
- this.idToIndex.set(this.ids[i] as string, i);
103
- }
103
+ public getNItems(): number {
104
+ this.flushRemovals();
105
+ return this.ids.length;
104
106
  }
105
107
  }
@@ -0,0 +1,287 @@
1
+ import * as Gio from "@gtkx/ffi/gio";
2
+ import * as GObject from "@gtkx/ffi/gobject";
3
+ import * as Gtk from "@gtkx/ffi/gtk";
4
+
5
+ interface SectionData {
6
+ id: string;
7
+ model: Gtk.StringList;
8
+ itemIds: string[];
9
+ pendingBatch: string[] | null;
10
+ }
11
+
12
+ export abstract class SectionedListStore {
13
+ protected ids: string[] = [];
14
+ protected idToIndex = new Map<string, number>();
15
+ protected model = new Gtk.StringList();
16
+ protected pendingBatch: string[] | null = null;
17
+ private pendingRemovals: Set<string> | null = null;
18
+ private flushScheduled = false;
19
+
20
+ protected sectioned = false;
21
+ protected sections: SectionData[] = [];
22
+ protected sectionById = new Map<string, SectionData>();
23
+ protected itemToSection = new Map<string, string>();
24
+ protected headerValues = new Map<string, unknown>();
25
+ protected sectionContainer: Gio.ListStore | null = null;
26
+ protected flatModel: Gtk.FlattenListModel | null = null;
27
+
28
+ public enableSections(): void {
29
+ if (this.sectioned) return;
30
+ this.sectioned = true;
31
+ this.sectionContainer = new Gio.ListStore(GObject.typeFromName("GtkStringList"));
32
+ this.flatModel = new Gtk.FlattenListModel(this.sectionContainer);
33
+ }
34
+
35
+ public beginBatch(): void {
36
+ if (this.sectioned) {
37
+ for (const section of this.sections) {
38
+ section.pendingBatch = [];
39
+ }
40
+ } else {
41
+ this.pendingBatch = [];
42
+ }
43
+ }
44
+
45
+ public flushBatch(): void {
46
+ if (this.sectioned) {
47
+ for (const section of this.sections) {
48
+ const batch = section.pendingBatch;
49
+ section.pendingBatch = null;
50
+ if (batch && batch.length > 0) {
51
+ section.model.splice(0, 0, batch);
52
+ }
53
+ }
54
+ } else {
55
+ const batch = this.pendingBatch;
56
+ this.pendingBatch = null;
57
+ if (batch && batch.length > 0) {
58
+ this.model.splice(0, 0, batch);
59
+ }
60
+ }
61
+ }
62
+
63
+ public addSection(id: string, value: unknown): void {
64
+ if (!this.sectioned) this.enableSections();
65
+
66
+ this.headerValues.set(id, value);
67
+ if (this.sectionById.has(id)) return;
68
+
69
+ const sectionModel = new Gtk.StringList();
70
+ const section: SectionData = {
71
+ id,
72
+ model: sectionModel,
73
+ itemIds: [],
74
+ pendingBatch: this.getInitialPendingBatch(),
75
+ };
76
+ this.sections.push(section);
77
+ this.sectionById.set(id, section);
78
+ this.sectionContainer?.append(sectionModel);
79
+ }
80
+
81
+ public removeSection(id: string): void {
82
+ const section = this.sectionById.get(id);
83
+ if (!section) return;
84
+
85
+ for (const itemId of [...section.itemIds]) {
86
+ this.removeItemFromSection(itemId);
87
+ }
88
+
89
+ const sectionIndex = this.sections.indexOf(section);
90
+ if (sectionIndex >= 0) {
91
+ this.sections.splice(sectionIndex, 1);
92
+ this.sectionContainer?.remove(sectionIndex);
93
+ }
94
+ this.sectionById.delete(id);
95
+ this.headerValues.delete(id);
96
+ }
97
+
98
+ public removeItemFromSection(itemId: string): void {
99
+ const sectionId = this.itemToSection.get(itemId);
100
+ if (!sectionId) return;
101
+
102
+ const section = this.sectionById.get(sectionId);
103
+ if (!section) return;
104
+
105
+ const indexInSection = section.itemIds.indexOf(itemId);
106
+ if (indexInSection >= 0) {
107
+ section.itemIds.splice(indexInSection, 1);
108
+ section.model.remove(indexInSection);
109
+ }
110
+
111
+ this.itemToSection.delete(itemId);
112
+ this.onItemRemoved(itemId);
113
+ this.rebuildGlobalIndices();
114
+ }
115
+
116
+ public removeItem(id: string): void {
117
+ if (this.sectioned) {
118
+ this.removeItemFromSection(id);
119
+ return;
120
+ }
121
+
122
+ if (!this.idToIndex.has(id)) return;
123
+
124
+ if (!this.pendingRemovals) {
125
+ this.pendingRemovals = new Set();
126
+ if (!this.flushScheduled) {
127
+ this.flushScheduled = true;
128
+ queueMicrotask(() => this.flushRemovals());
129
+ }
130
+ }
131
+ this.pendingRemovals.add(id);
132
+ this.onItemRemoved(id);
133
+ }
134
+
135
+ public flushRemovals(): void {
136
+ this.flushScheduled = false;
137
+ const removals = this.pendingRemovals;
138
+ if (!removals || removals.size === 0) {
139
+ this.pendingRemovals = null;
140
+ return;
141
+ }
142
+ this.pendingRemovals = null;
143
+
144
+ const indices: number[] = [];
145
+ for (const id of removals) {
146
+ const index = this.idToIndex.get(id);
147
+ if (index !== undefined) {
148
+ indices.push(index);
149
+ this.idToIndex.delete(id);
150
+ }
151
+ }
152
+
153
+ if (indices.length === 0) return;
154
+
155
+ indices.sort((a, b) => a - b);
156
+
157
+ let i = indices.length - 1;
158
+ while (i >= 0) {
159
+ let rangeStart = indices[i] ?? 0;
160
+ const rangeEnd = rangeStart;
161
+
162
+ while (i > 0) {
163
+ const prev = indices[i - 1];
164
+ if (prev !== rangeStart - 1) break;
165
+ i--;
166
+ rangeStart = prev ?? 0;
167
+ }
168
+
169
+ const count = rangeEnd - rangeStart + 1;
170
+ this.model.splice(rangeStart, count);
171
+ this.ids.splice(rangeStart, count);
172
+
173
+ i--;
174
+ }
175
+
176
+ this.rebuildIndices(0);
177
+ }
178
+
179
+ public addItemToSection(sectionId: string, itemId: string, item: unknown): void {
180
+ const section = this.sectionById.get(sectionId);
181
+ if (!section) return;
182
+
183
+ this.onItemAdded(itemId, item);
184
+ this.itemToSection.set(itemId, sectionId);
185
+ section.itemIds.push(itemId);
186
+ this.rebuildGlobalIndices();
187
+
188
+ const modelString = this.getModelString(itemId, item);
189
+ if (section.pendingBatch) {
190
+ section.pendingBatch.push(modelString);
191
+ } else {
192
+ section.model.append(modelString);
193
+ }
194
+ }
195
+
196
+ public addItemsToSection(sectionId: string, items: { itemId: string; item: unknown }[]): void {
197
+ const section = this.sectionById.get(sectionId);
198
+ if (!section) return;
199
+ if (items.length === 0) return;
200
+
201
+ const modelStrings: string[] = [];
202
+ for (const { itemId, item } of items) {
203
+ this.onItemAdded(itemId, item);
204
+ this.itemToSection.set(itemId, sectionId);
205
+ section.itemIds.push(itemId);
206
+ modelStrings.push(this.getModelString(itemId, item));
207
+ }
208
+
209
+ this.rebuildGlobalIndices();
210
+
211
+ if (section.pendingBatch) {
212
+ for (const s of modelStrings) {
213
+ section.pendingBatch.push(s);
214
+ }
215
+ } else {
216
+ section.model.splice(section.model.getNItems(), 0, modelStrings);
217
+ }
218
+ }
219
+
220
+ public insertItemToSectionBefore(sectionId: string, itemId: string, beforeId: string, item: unknown): void {
221
+ const section = this.sectionById.get(sectionId);
222
+ if (!section) return;
223
+
224
+ this.onItemAdded(itemId, item);
225
+ this.itemToSection.set(itemId, sectionId);
226
+
227
+ const modelString = this.getModelString(itemId, item);
228
+ const beforeIndex = section.itemIds.indexOf(beforeId);
229
+ if (beforeIndex >= 0) {
230
+ section.itemIds.splice(beforeIndex, 0, itemId);
231
+ this.rebuildGlobalIndices();
232
+ section.model.splice(beforeIndex, 0, [modelString]);
233
+ } else {
234
+ section.itemIds.push(itemId);
235
+ this.rebuildGlobalIndices();
236
+ section.model.append(modelString);
237
+ }
238
+ }
239
+
240
+ public updateHeaderValue(sectionId: string, value: unknown): void {
241
+ this.headerValues.set(sectionId, value);
242
+ }
243
+
244
+ public isSectioned(): boolean {
245
+ return this.sectioned;
246
+ }
247
+
248
+ public getModel(): Gio.ListModel {
249
+ if (this.sectioned && this.flatModel) return this.flatModel;
250
+ return this.model;
251
+ }
252
+
253
+ public getIdAtIndex(index: number): string | null {
254
+ this.flushRemovals();
255
+ return this.ids[index] ?? null;
256
+ }
257
+
258
+ public getIndexById(id: string): number | null {
259
+ this.flushRemovals();
260
+ return this.idToIndex.get(id) ?? null;
261
+ }
262
+
263
+ protected rebuildGlobalIndices(): void {
264
+ this.ids = [];
265
+ this.idToIndex.clear();
266
+ for (const section of this.sections) {
267
+ for (const itemId of section.itemIds) {
268
+ this.idToIndex.set(itemId, this.ids.length);
269
+ this.ids.push(itemId);
270
+ }
271
+ }
272
+ }
273
+
274
+ protected rebuildIndices(fromIndex: number): void {
275
+ for (let i = fromIndex; i < this.ids.length; i++) {
276
+ this.idToIndex.set(this.ids[i] as string, i);
277
+ }
278
+ }
279
+
280
+ protected abstract getInitialPendingBatch(): string[] | null;
281
+
282
+ protected abstract getModelString(itemId: string, item: unknown): string;
283
+
284
+ protected onItemAdded(_itemId: string, _item: unknown): void {}
285
+
286
+ protected onItemRemoved(_itemId: string): void {}
287
+ }
@@ -0,0 +1,35 @@
1
+ import * as Gtk from "@gtkx/ffi/gtk";
2
+
3
+ type StoreWithIdLookup = {
4
+ getIdAtIndex(index: number): string | null;
5
+ };
6
+
7
+ type StoreWithIndexLookup = {
8
+ getIndexById(id: string): number | null;
9
+ getNItems(): number;
10
+ };
11
+
12
+ export function getSelectionFromStore(selection: Gtk.Bitset, store: StoreWithIdLookup): string[] {
13
+ const size = selection.getSize();
14
+ const ids: string[] = [];
15
+ for (let i = 0; i < size; i++) {
16
+ const index = selection.getNth(i);
17
+ const id = store.getIdAtIndex(index);
18
+ if (id !== null) {
19
+ ids.push(id);
20
+ }
21
+ }
22
+ return ids;
23
+ }
24
+
25
+ export function resolveSelectionIndices(ids: string[], store: StoreWithIndexLookup): Gtk.Bitset {
26
+ const nItems = store.getNItems();
27
+ const selected = new Gtk.Bitset();
28
+ for (const id of ids) {
29
+ const index = store.getIndexById(id);
30
+ if (index !== null && index < nItems) {
31
+ selected.add(index);
32
+ }
33
+ }
34
+ return selected;
35
+ }
@@ -54,6 +54,10 @@ export class SelectionModelController {
54
54
  return this.selectionModel;
55
55
  }
56
56
 
57
+ if (this.selectionModel.getModel() !== model) {
58
+ this.selectionModel.setModel(model);
59
+ }
60
+
57
61
  if (!oldProps || oldProps.onSelectionChanged !== newProps.onSelectionChanged) {
58
62
  this.initSelectionHandler(newProps.onSelectionChanged);
59
63
  }