@gjsify/adwaita-web 0.64.12-next.280 → 0.64.12-next.281

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.
@@ -1,5 +1,6 @@
1
1
  import '../file-chooser-host.js';
2
2
  export { GioMenu as Menu, GioMenuItem as MenuItem } from '@gjsify/adwaita-core';
3
+ export { ListModel } from '@gjsify/adwaita-core';
3
4
  export { GioFile as File, GioListStore as ListStore, GioOutputStream as OutputStream, GioFileOutputStream as FileOutputStream, FileCreateFlags, IOErrorEnum, promisify as _promisify, } from '@gjsify/adwaita-core';
4
5
  export { SimpleAction, SimpleActionGroup, ApplicationFlags } from '@gjsify/adwaita-core';
5
6
  export { GioResource as Resource, ResourceError, ResourceLookupFlags, resources_enumerate_children, resources_get_info, resources_lookup_data, resources_register, resources_unregister, } from '@gjsify/adwaita-core';
@@ -65,7 +65,7 @@ export { GtkStack as Stack } from '../elements/gtk-stack.js';
65
65
  export { GtkStackPage as StackPage } from '../elements/gtk-stack-page.js';
66
66
  export { GtkStackSidebar as StackSidebar } from '../elements/gtk-stack-sidebar.js';
67
67
  export { GtkStackSwitcher as StackSwitcher } from '../elements/gtk-stack-switcher.js';
68
- export { GtkStringList as StringList } from '@gjsify/adwaita-core';
68
+ export { GtkStringList as StringList, GtkStringObject as StringObject } from '@gjsify/adwaita-core';
69
69
  export { GtkSwitch as Switch } from '../elements/gtk-switch.js';
70
70
  export { GtkText as Text } from '../elements/gtk-text.js';
71
71
  export { GtkTextView as TextView } from '../elements/gtk-text-view.js';
@@ -34,6 +34,8 @@ interface BuildRecord {
34
34
  menus?: SharedTreeNode['menus'];
35
35
  /** Present when the tree is the template of a registered class (ADR 0096): handlers and binds go through it. */
36
36
  template?: TemplateScope;
37
+ /** A template's handler connections, made once every id exists: `object=` may name a node built later. */
38
+ connects?: (() => void)[];
37
39
  }
38
40
  /** Calls `onSize` with the host's measured size, now and on every change; returns its disposer. */
39
41
  export type SizeSource = (element: Element, onSize: (size: BreakpointSize) => void) => () => void;
@@ -141,6 +143,11 @@ export interface BuiltTemplateTree {
141
143
  * to their handlers and binds through `scope`, none appended and nothing written to `host`'s
142
144
  * attributes, because `host` is still inside its constructor. `finish` and the caller's own append
143
145
  * belong to the first `connectedCallback`.
146
+ *
147
+ * Objects written beside the template (`Adw.AlertDialog dialog { }`, `$Learn learn { }`) are built in
148
+ * the same id scope and named in `objects`, which is what `InternalChildren` and `Children` read —
149
+ * `gtk_widget_class_bind_template_child` reaches any object id of the template's builder. They are
150
+ * not in `children`: a sibling is an independent object, placed by the class or not at all.
144
151
  */
145
152
  export declare function buildTemplateTree(tree: SharedTreeNode, scope: TemplateScope, host: HTMLElement): BuiltTemplateTree;
146
153
  /** A tree {@link mountSharedTree} built and connected. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/adwaita-web",
3
- "version": "0.64.12-next.280",
3
+ "version": "0.64.12-next.281",
4
4
  "description": "Adwaita/Libadwaita web components for browser targets",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -58,14 +58,14 @@
58
58
  "@codemirror/language": "^6.12.4",
59
59
  "@codemirror/state": "^6.7.0",
60
60
  "@codemirror/view": "^6.43.4",
61
- "@gjsify/adwaita-core": "0.64.12-next.280",
62
- "@gjsify/adwaita-fonts": "0.64.12-next.280",
63
- "@gjsify/adwaita-icons": "0.64.12-next.280",
64
- "@gjsify/gtksource-core": "0.64.12-next.280",
61
+ "@gjsify/adwaita-core": "0.64.12-next.281",
62
+ "@gjsify/adwaita-fonts": "0.64.12-next.281",
63
+ "@gjsify/adwaita-icons": "0.64.12-next.281",
64
+ "@gjsify/gtksource-core": "0.64.12-next.281",
65
65
  "@lezer/highlight": "^1.2.3"
66
66
  },
67
67
  "peerDependencies": {
68
- "@gjsify/app-settings": "0.64.12-next.280"
68
+ "@gjsify/app-settings": "0.64.12-next.281"
69
69
  },
70
70
  "peerDependenciesMeta": {
71
71
  "@gjsify/app-settings": {
@@ -73,11 +73,11 @@
73
73
  }
74
74
  },
75
75
  "devDependencies": {
76
- "@gjsify/app-settings": "0.64.12-next.280",
77
- "@gjsify/blueprint": "0.64.12-next.280",
78
- "@gjsify/cli": "0.64.12-next.280",
79
- "@gjsify/unit": "0.64.12-next.280",
80
- "@gjsify/vite-plugin-blueprint": "0.64.12-next.280",
76
+ "@gjsify/app-settings": "0.64.12-next.281",
77
+ "@gjsify/blueprint": "0.64.12-next.281",
78
+ "@gjsify/cli": "0.64.12-next.281",
79
+ "@gjsify/unit": "0.64.12-next.281",
80
+ "@gjsify/vite-plugin-blueprint": "0.64.12-next.281",
81
81
  "sass": "^1.101.0",
82
82
  "typescript": "^6.0.3"
83
83
  },
@@ -115,5 +115,5 @@
115
115
  "url": "https://github.com/gjsify/gjsify/issues"
116
116
  },
117
117
  "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/web/adwaita-web#readme",
118
- "gitHead": "0a9185d91127072fce6aa54dc126e87e6ecd0c3d"
118
+ "gitHead": "c4d2e266c0d9849860b9828c14ebfe37b6d493b8"
119
119
  }
@@ -91,6 +91,45 @@ GObject.registerClass(
91
91
  GoWebSensitive as never,
92
92
  );
93
93
 
94
+ const SiblingsTemplate = tree(`template $GoWebSiblings : Adw.Bin {
95
+ child: Gtk.Box {
96
+ Gtk.Label caption { label: bind peer.title; }
97
+ };
98
+ }
99
+
100
+ Adw.AlertDialog unsavedChangesDialog { heading: "Save changes?"; }
101
+
102
+ $GoWebPeer peer { }`);
103
+
104
+ class GoWebPeer extends Adw.Bin {
105
+ declare title: string;
106
+ }
107
+ GObject.registerClass(
108
+ {
109
+ GTypeName: 'GoWebPeer',
110
+ Properties: { title: GObject.ParamSpec.string('title', '', '', GObject.ParamFlags.READWRITE, 'first') },
111
+ },
112
+ GoWebPeer as never,
113
+ );
114
+
115
+ class GoWebSiblings extends Adw.Bin {
116
+ declare _unsavedChangesDialog: HTMLElement & { heading: string };
117
+ declare _peer: GoWebPeer;
118
+ declare _caption: HTMLElement & { label: string };
119
+ constructor(params?: Record<string, unknown>) {
120
+ // @ts-expect-error the element's own constructor takes none; the registered class's takes the params
121
+ super(params);
122
+ }
123
+ }
124
+ GObject.registerClass(
125
+ {
126
+ GTypeName: 'GoWebSiblings',
127
+ Template: SiblingsTemplate,
128
+ InternalChildren: ['unsavedChangesDialog', 'peer', 'caption'],
129
+ },
130
+ GoWebSiblings as never,
131
+ );
132
+
94
133
  export const GObjectDoorTest = async () => {
95
134
  await driveGObjectVectors(subject, { describe, it, expect }, GOBJECT_VECTORS);
96
135
  await describe('adwaita-web: GObject.registerClass end to end (ADR 0096)', async () => {
@@ -98,6 +137,24 @@ export const GObjectDoorTest = async () => {
98
137
  expect(tagForTypeName('Hexdump')).toBe('gjsify-hexdump');
99
138
  expect(customElements.get('gjsify-go-web-hexdump')).toBe(GoWebHexdump);
100
139
  });
140
+ await it('reaches the objects written beside the template through InternalChildren, outside its children', () => {
141
+ const el = new GoWebSiblings();
142
+ expect(el._unsavedChangesDialog.localName).toBe('adw-alert-dialog');
143
+ expect(el._peer instanceof GoWebPeer).toBe(true);
144
+ document.body.append(el);
145
+ try {
146
+ // Independent objects, as in GTK: neither is a child of the template widget.
147
+ expect(el.contains(el._unsavedChangesDialog)).toBe(false);
148
+ expect(el.contains(el._peer)).toBe(false);
149
+ expect(el.contains(el._caption)).toBe(true);
150
+ // A template child names a sibling by id, and follows it.
151
+ expect(el._caption.label).toBe('first');
152
+ el._peer.title = 'second';
153
+ expect(el._caption.label).toBe('second');
154
+ } finally {
155
+ el.remove();
156
+ }
157
+ });
101
158
  await it('refuses a second class under the same tag', () => {
102
159
  expect(() =>
103
160
  GObject.registerClass({ GTypeName: 'GoWebHexdump' }, class extends Adw.Bin {} as never),
@@ -29,6 +29,9 @@ import { createAppInfo } from '@gjsify/adwaita-core';
29
29
  import { webPlatformHost } from '../platform-host.js';
30
30
 
31
31
  export { GioMenu as Menu, GioMenuItem as MenuItem } from '@gjsify/adwaita-core';
32
+ // ADR 0122 § 2: the interface a `Gtk.StringList` implements and an `Implements: [Gio.ListModel]`
33
+ // names. It is a token, not a class — the whole of the subset's interface support.
34
+ export { ListModel } from '@gjsify/adwaita-core';
32
35
  export {
33
36
  GioFile as File,
34
37
  GioListStore as ListStore,
@@ -11,14 +11,18 @@
11
11
  // which is what `<adw-radio>`'s own `why` in the ledger says. So the plain form takes the
12
12
  // GIR name and the grouped one keeps its flat export in `src/index.ts`.
13
13
  //
14
- // TWO MEMBERS HERE ARE NOT WIDGETS — `Gtk.Adjustment` and `Gtk.StringList`, the values an
15
- // `adjustment` and a `model` property take. ADR 0034 § Amendment 19 is what lets a
16
- // clause-2 namespace carry them, `CONSTRUCTIBLE_VALUES` in `scripts/value-types.mjs` is
17
- // where each one is declared, and the GIR answers for both in gtk-host's committed
18
- // `generated/value-types.mts`. They are re-exported from `@gjsify/adwaita-core`, which is
19
- // where the portable list and adjustment already live: the class IS that value wearing the
20
- // GIR spelling, so `model: ['a','b']` and `model: new Gtk.StringList({ strings: ['a','b'] })`
21
- // are the same write and nothing in this package had to learn a second input shape.
14
+ // THREE MEMBERS HERE ARE NOT WIDGETS — `Gtk.Adjustment`, `Gtk.StringList` and the
15
+ // `Gtk.StringObject` the list holds, the values an `adjustment` and a `model` property take.
16
+ // ADR 0034 § Amendment 19 is what lets a clause-2 namespace carry them,
17
+ // `CONSTRUCTIBLE_VALUES` in `scripts/value-types.mjs` is where each one is declared, and the
18
+ // GIR answers for all three in gtk-host's committed `generated/value-types.mts`. They are
19
+ // re-exported from `@gjsify/adwaita-core`, which is where the portable list and adjustment
20
+ // already live.
21
+ //
22
+ // `Gtk.StringList` IS A LIST MODEL, NOT AN ARRAY (ADR 0122). The first version of it was an
23
+ // `Array` subclass, which made `model: ['a','b']` and `model: new Gtk.StringList(…)` the same
24
+ // write; it is now what GTK ships — `Implements: [Gio.ListModel]`, items that are
25
+ // `Gtk.StringObject`s, read by `get_n_items` / `get_item` and never by `length` or `[0]`.
22
26
 
23
27
  export { GtkAdjustment as Adjustment } from '@gjsify/adwaita-core';
24
28
  // ADR 0100: the file dialog is no element either — `present`-less, it asks the driver
@@ -89,7 +93,7 @@ export { GtkStack as Stack } from '../elements/gtk-stack.js';
89
93
  export { GtkStackPage as StackPage } from '../elements/gtk-stack-page.js';
90
94
  export { GtkStackSidebar as StackSidebar } from '../elements/gtk-stack-sidebar.js';
91
95
  export { GtkStackSwitcher as StackSwitcher } from '../elements/gtk-stack-switcher.js';
92
- export { GtkStringList as StringList } from '@gjsify/adwaita-core';
96
+ export { GtkStringList as StringList, GtkStringObject as StringObject } from '@gjsify/adwaita-core';
93
97
  export { GtkSwitch as Switch } from '../elements/gtk-switch.js';
94
98
  export { GtkText as Text } from '../elements/gtk-text.js';
95
99
  export { GtkTextView as TextView } from '../elements/gtk-text-view.js';
@@ -101,6 +101,8 @@ interface BuildRecord {
101
101
  menus?: SharedTreeNode['menus'];
102
102
  /** Present when the tree is the template of a registered class (ADR 0096): handlers and binds go through it. */
103
103
  template?: TemplateScope;
104
+ /** A template's handler connections, made once every id exists: `object=` may name a node built later. */
105
+ connects?: (() => void)[];
104
106
  }
105
107
 
106
108
  /** Calls `onSize` with the host's measured size, now and on every change; returns its disposer. */
@@ -229,13 +231,8 @@ function refuseSiblings(node: SharedTreeNode, door: string): void {
229
231
  );
230
232
  }
231
233
 
232
- /** Every node, its siblings in the same id scope, then the binds and breakpoints that name them. */
233
- function buildTree(node: SharedTreeNode, record: BuildRecord): BuiltTree {
234
- // ADR 0093 § 2: the whole tree is checked against the capability table before an element is
235
- // created. A refused `layout` is named with its node and the reason the table gives.
236
- assertTreeConstructs('adwaita-web', capabilities, node, Object.keys(ARIA_OF));
237
- if (node.menus !== undefined) record.menus = node.menus;
238
- const root = buildNode(node, record);
234
+ /** The sibling objects of `node` built in `record`'s id scope, by id; each has to have one, or nothing could ask for it. */
235
+ function buildSiblings(node: SharedTreeNode, record: BuildRecord): Map<string, HTMLElement> {
239
236
  const siblings = new Map<string, HTMLElement>();
240
237
  for (const sibling of node.siblings ?? []) {
241
238
  if (sibling.id === undefined) {
@@ -245,6 +242,17 @@ function buildTree(node: SharedTreeNode, record: BuildRecord): BuiltTree {
245
242
  }
246
243
  siblings.set(sibling.id, buildNode(sibling, record));
247
244
  }
245
+ return siblings;
246
+ }
247
+
248
+ /** Every node, its siblings in the same id scope, then the binds and breakpoints that name them. */
249
+ function buildTree(node: SharedTreeNode, record: BuildRecord): BuiltTree {
250
+ // ADR 0093 § 2: the whole tree is checked against the capability table before an element is
251
+ // created. A refused `layout` is named with its node and the reason the table gives.
252
+ assertTreeConstructs('adwaita-web', capabilities, node, Object.keys(ARIA_OF));
253
+ if (node.menus !== undefined) record.menus = node.menus;
254
+ const root = buildNode(node, record);
255
+ const siblings = buildSiblings(node, record);
248
256
  for (const bind of record.binds) bindProperty(bind, record);
249
257
  for (const host of record.breakpointHosts) bindBreakpoints(host.el, host.node, record);
250
258
  return { root, siblings, objects: new Map(record.ids) };
@@ -265,7 +273,7 @@ function newRecord(
265
273
  disposers: [],
266
274
  ...(scope === undefined ? {} : { scope }),
267
275
  ...(observeSize === undefined ? {} : { observeSize }),
268
- ...(template === undefined ? {} : { template }),
276
+ ...(template === undefined ? {} : { template, connects: [] }),
269
277
  };
270
278
  }
271
279
 
@@ -606,9 +614,13 @@ export interface BuiltTemplateTree {
606
614
  * to their handlers and binds through `scope`, none appended and nothing written to `host`'s
607
615
  * attributes, because `host` is still inside its constructor. `finish` and the caller's own append
608
616
  * belong to the first `connectedCallback`.
617
+ *
618
+ * Objects written beside the template (`Adw.AlertDialog dialog { }`, `$Learn learn { }`) are built in
619
+ * the same id scope and named in `objects`, which is what `InternalChildren` and `Children` read —
620
+ * `gtk_widget_class_bind_template_child` reaches any object id of the template's builder. They are
621
+ * not in `children`: a sibling is an independent object, placed by the class or not at all.
609
622
  */
610
623
  export function buildTemplateTree(tree: SharedTreeNode, scope: TemplateScope, host: HTMLElement): BuiltTemplateTree {
611
- refuseSiblings(tree, 'buildTemplateTree');
612
624
  assertTreeConstructs('adwaita-web', capabilities, tree, Object.keys(ARIA_OF));
613
625
  const record = newRecord(undefined, undefined, scope);
614
626
  record.menus = tree.menus;
@@ -616,6 +628,8 @@ export function buildTemplateTree(tree: SharedTreeNode, scope: TemplateScope, ho
616
628
  collectNode(host, tree, record);
617
629
  bindSignals(host, tree, record);
618
630
  const children = buildChildren(host, tree, record);
631
+ buildSiblings(tree, record);
632
+ for (const connect of record.connects ?? []) connect();
619
633
  for (const bind of record.binds) bindProperty(bind, record);
620
634
  for (const breakpointHost of record.breakpointHosts) {
621
635
  bindBreakpoints(breakpointHost.el, breakpointHost.node, record);
@@ -648,21 +662,26 @@ function bindSignals(el: HTMLElement, node: SharedTreeNode, record: BuildRecord)
648
662
  `the handler '${signal.handler}' for '${name}' uses after, which is not in the GObject subset.`,
649
663
  );
650
664
  }
651
- const object =
652
- signal.object === undefined
653
- ? undefined
654
- : signal.object === 'template'
655
- ? record.template.instance
656
- : record.ids.get(signal.object);
657
- if (signal.object !== undefined && object === undefined) {
658
- throw new Error(
659
- `the handler '${signal.handler}' for '${name}' names the object '${signal.object}', which nothing in this tree has.`,
665
+ const template = record.template;
666
+ // `object=` is resolved when the whole tree exists, as `gtk_builder_connect_signals` does: it may
667
+ // name a node built after this one, or a sibling object written beside the template.
668
+ record.connects!.push(() => {
669
+ const object =
670
+ signal.object === undefined
671
+ ? undefined
672
+ : signal.object === 'template'
673
+ ? template.instance
674
+ : record.ids.get(signal.object);
675
+ if (signal.object !== undefined && object === undefined) {
676
+ throw new Error(
677
+ `the handler '${signal.handler}' for '${name}' names the object '${signal.object}', which nothing in this tree has.`,
678
+ );
679
+ }
680
+ (el as unknown as GObjectInstance).connect(
681
+ name,
682
+ template.handler(signal.handler, { object, flags: signal.flags }),
660
683
  );
661
- }
662
- (el as unknown as GObjectInstance).connect(
663
- name,
664
- record.template.handler(signal.handler, { object, flags: signal.flags }),
665
- );
684
+ });
666
685
  continue;
667
686
  }
668
687
  if (signal.object !== undefined || (signal.flags?.length ?? 0) > 0) {