@gjsify/adwaita-web 0.56.0 → 0.57.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 (69) hide show
  1. package/dist/adwaita-web.css +8 -2
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/application.d.ts +24 -0
  4. package/lib/types/application.spec.d.ts +1 -0
  5. package/lib/types/elements/adw-application-window.d.ts +2 -0
  6. package/lib/types/elements/adw-view-stack.d.ts +3 -0
  7. package/lib/types/elements/checks.d.ts +6 -0
  8. package/lib/types/elements/gtk-application-window.d.ts +2 -0
  9. package/lib/types/elements/gtk-drop-down.d.ts +3 -0
  10. package/lib/types/elements/gtk-entry.d.ts +10 -0
  11. package/lib/types/elements/gtk-menu-button.d.ts +2 -0
  12. package/lib/types/elements/gtk-scrolled-window.d.ts +16 -0
  13. package/lib/types/elements/gtk-stack.d.ts +3 -0
  14. package/lib/types/elements/gtk-switch.d.ts +3 -0
  15. package/lib/types/glib.spec.d.ts +1 -0
  16. package/lib/types/gobject-door.d.ts +4 -0
  17. package/lib/types/gobject-door.spec.d.ts +1 -0
  18. package/lib/types/gobject-elements.d.ts +17 -0
  19. package/lib/types/gtksource/gtk-source-view.d.ts +77 -0
  20. package/lib/types/gtksource/gtk-source-view.spec.d.ts +2 -0
  21. package/lib/types/gtksource/web-editor-driver.d.ts +44 -0
  22. package/lib/types/index.d.ts +3 -0
  23. package/lib/types/menu-actions.spec.d.ts +1 -0
  24. package/lib/types/namespace/adw.d.ts +1 -0
  25. package/lib/types/namespace/gio.d.ts +2 -0
  26. package/lib/types/namespace/glib.d.ts +10 -0
  27. package/lib/types/namespace/gobject.d.ts +27 -0
  28. package/lib/types/namespace/gtk-enums.d.ts +22 -0
  29. package/lib/types/namespace/gtk.d.ts +2 -0
  30. package/lib/types/namespace/gtksource.d.ts +2 -0
  31. package/lib/types/shared-tree-builder.d.ts +20 -1
  32. package/lib/types/styles.generated.d.ts +1 -1
  33. package/package.json +11 -9
  34. package/scss/_reset.scss +1 -1
  35. package/src/application.spec.ts +75 -0
  36. package/src/application.ts +92 -0
  37. package/src/construct-vectors.spec.ts +155 -0
  38. package/src/elements/adw-application-window.ts +5 -0
  39. package/src/elements/adw-split-button.ts +5 -1
  40. package/src/elements/adw-view-stack.ts +14 -0
  41. package/src/elements/checks.ts +25 -1
  42. package/src/elements/gtk-application-window.ts +5 -0
  43. package/src/elements/gtk-drop-down.ts +16 -0
  44. package/src/elements/gtk-entry.ts +45 -7
  45. package/src/elements/gtk-menu-button.ts +12 -1
  46. package/src/elements/gtk-popover-menu.ts +7 -2
  47. package/src/elements/gtk-scrolled-window.ts +58 -0
  48. package/src/elements/gtk-stack.ts +8 -0
  49. package/src/elements/gtk-switch.ts +8 -0
  50. package/src/glib.spec.ts +34 -0
  51. package/src/gobject-door.spec.ts +119 -0
  52. package/src/gobject-door.ts +137 -0
  53. package/src/gobject-elements.ts +172 -0
  54. package/src/gtksource/gtk-source-view.spec.ts +69 -0
  55. package/src/gtksource/gtk-source-view.ts +379 -0
  56. package/src/gtksource/web-editor-driver.ts +260 -0
  57. package/src/index.ts +10 -0
  58. package/src/menu-actions.spec.ts +47 -0
  59. package/src/namespace/adw.ts +1 -0
  60. package/src/namespace/gio.ts +2 -0
  61. package/src/namespace/glib.ts +22 -0
  62. package/src/namespace/gobject.ts +37 -0
  63. package/src/namespace/gtk-enums.ts +15 -0
  64. package/src/namespace/gtk.ts +2 -0
  65. package/src/namespace/gtksource.ts +9 -0
  66. package/src/shared-tree-builder.ts +142 -7
  67. package/src/split-button.spec.ts +2 -0
  68. package/src/styles.generated.ts +1 -1
  69. package/src/test.browser.mts +10 -0
@@ -34,7 +34,15 @@
34
34
  // package's `exports` map ships only `.`, and a new subpath would buy nothing for a module
35
35
  // this small, most of which (the `SharedTreeNode` type) is erased at build anyway.
36
36
 
37
- import { createBreakpointDriver, parseBreakpointCondition, type BreakpointSize } from '@gjsify/adwaita-core';
37
+ import {
38
+ createBreakpointDriver,
39
+ menuAssignments,
40
+ parseBreakpointCondition,
41
+ type BindingFlag,
42
+ type BreakpointSize,
43
+ type GObjectInstance,
44
+ type TemplateScope,
45
+ } from '@gjsify/adwaita-core';
38
46
  import { assertTreeConstructs, type SharedTreeNode } from '@gjsify/adwaita-core/conformance';
39
47
  import {
40
48
  GTK_WIDGET_EXPAND,
@@ -47,6 +55,7 @@ import {
47
55
 
48
56
  import { observeAdaptiveSize } from './breakpoints.js';
49
57
  import { capabilities } from './capabilities.mjs';
58
+ import { endpointOf, hasElementApi, installElementApi } from './gobject-elements.js';
50
59
  import { dispatchedSignalsOf } from './signals.js';
51
60
  import { slottedChildrenOf } from './slotted-children.js';
52
61
  import { templateTagFor } from './template-classes.js';
@@ -81,6 +90,10 @@ interface BuildRecord {
81
90
  observeSize?: SizeSource;
82
91
  /** The object a signal handler's NAME is resolved against (ADR 0093 § 3), as `Gtk.BuilderScope` does. */
83
92
  scope?: Readonly<Record<string, unknown>>;
93
+ /** The root menus of the tree (ADR 0097), which a `menu-model: id` anywhere below resolves against. */
94
+ menus?: SharedTreeNode['menus'];
95
+ /** Present when the tree is the template of a registered class (ADR 0096): handlers and binds go through it. */
96
+ template?: TemplateScope;
84
97
  }
85
98
 
86
99
  /** Calls `onSize` with the host's measured size, now and on every change; returns its disposer. */
@@ -144,6 +157,7 @@ export function buildSharedTree(node: SharedTreeNode, record: BuildRecord = newR
144
157
  // ADR 0093 § 2: the whole tree is checked against the capability table before an element is
145
158
  // created. A refused `layout` is named with its node and the reason the table gives.
146
159
  assertTreeConstructs('adwaita-web', capabilities, node);
160
+ if (node.menus !== undefined) record.menus = node.menus;
147
161
  const root = buildNode(node, record);
148
162
  for (const bind of record.binds) bindProperty(bind, record);
149
163
  for (const host of record.breakpointHosts) bindBreakpoints(host.el, host.node, record);
@@ -151,7 +165,11 @@ export function buildSharedTree(node: SharedTreeNode, record: BuildRecord = newR
151
165
  }
152
166
 
153
167
  /** A fresh record, with the scope when the caller has one. */
154
- function newRecord(scope?: Readonly<Record<string, unknown>>, observeSize?: SizeSource): BuildRecord {
168
+ function newRecord(
169
+ scope?: Readonly<Record<string, unknown>>,
170
+ observeSize?: SizeSource,
171
+ template?: TemplateScope,
172
+ ): BuildRecord {
155
173
  return {
156
174
  placed: [],
157
175
  extended: [],
@@ -161,6 +179,7 @@ function newRecord(scope?: Readonly<Record<string, unknown>>, observeSize?: Size
161
179
  disposers: [],
162
180
  ...(scope === undefined ? {} : { scope }),
163
181
  ...(observeSize === undefined ? {} : { observeSize }),
182
+ ...(template === undefined ? {} : { template }),
164
183
  };
165
184
  }
166
185
 
@@ -232,6 +251,7 @@ function bindBreakpoints(el: HTMLElement, node: SharedTreeNode, record: BuildRec
232
251
  * `notify::…` only while connected, so a tree built but never attached follows once, at build.
233
252
  */
234
253
  function bindProperty(bind: PendingBind, record: BuildRecord): void {
254
+ if (record.template !== undefined) return bindThroughEngine(bind, record.template, record);
235
255
  const { el, property, source, sourceProperty, flags } = bind;
236
256
  const where = `<${el.localName}> \`${property}: bind ${source}.${sourceProperty}\``;
237
257
  const from = record.ids.get(source);
@@ -267,6 +287,39 @@ function bindProperty(bind: PendingBind, record: BuildRecord): void {
267
287
  from.addEventListener(event, follow);
268
288
  }
269
289
 
290
+ /**
291
+ * ADR 0096's `bind` inside the template of a registered class: the `template` source, the flags
292
+ * `bidirectional`, `inverted` and `no-sync-create`, all through the core's engine, which is
293
+ * per-instance and does not care whether an element is connected. An element end is wrapped so
294
+ * a GTK property name reaches its camelCase member ({@link endpointOf}), and a `notify::` on it
295
+ * is made to fire before the first connect (`ensureNotifying`).
296
+ */
297
+ function bindThroughEngine(bind: PendingBind, template: TemplateScope, record: BuildRecord): void {
298
+ const { el, property, source, sourceProperty, flags } = bind;
299
+ const where = `<${el.localName}> \`${property}: bind ${source}.${sourceProperty}\``;
300
+ const from = source === 'template' ? template.instance : record.ids.get(source);
301
+ if (from === undefined) {
302
+ throw new Error(`${where} names no object: nothing in this tree has the id '${source}'.`);
303
+ }
304
+ const known = ['bidirectional', 'inverted', 'no-sync-create'];
305
+ const unknown = flags.filter((flag) => !known.includes(flag));
306
+ if (unknown.length > 0) throw new Error(`${where} carries ${unknown.join(', ')}, which is not a bind flag.`);
307
+ if (hasElementApi(el) && !isWritable(el, propertyOf(property))) {
308
+ throw new Error(`${where}: <${el.localName}> has no writable property '${propertyOf(property)}'.`);
309
+ }
310
+ const engineFlags: BindingFlag[] = [];
311
+ if (flags.includes('bidirectional')) engineFlags.push('bidirectional');
312
+ if (flags.includes('inverted')) engineFlags.push('invert-boolean');
313
+ if (!flags.includes('no-sync-create')) engineFlags.push('sync-create');
314
+ template.bind(
315
+ endpointOf(from as HTMLElement) as unknown as GObjectInstance,
316
+ sourceProperty,
317
+ endpointOf(el) as unknown as GObjectInstance,
318
+ property,
319
+ engineFlags,
320
+ );
321
+ }
322
+
270
323
  /**
271
324
  * One authored property, written as the element reads it; a breakpoint setter takes the same door.
272
325
  *
@@ -313,12 +366,24 @@ export function writeProp(el: HTMLElement, prop: string, value: string | number
313
366
  function buildNode(node: SharedTreeNode, record: BuildRecord): HTMLElement {
314
367
  // An `extern` node is built by the class the application registered under its name (ADR 0093).
315
368
  const el = document.createElement(node.extern === true ? templateTagFor(node.tag) : hostTagOf(node.tag));
369
+ // A template's children talk GObject to the template: `connect`, `emit`, `notify` (ADR 0096).
370
+ if (record.template !== undefined) installElementApi(el);
316
371
  // The id is how the TypeScript beside a `.blp` reaches this element
317
372
  // (`root.querySelector('#…')`), the counterpart of `InternalChildren` on GTK.
318
373
  if (node.id !== undefined) {
319
374
  el.id = node.id;
320
375
  record.ids.set(node.id, el);
321
376
  }
377
+ collectNode(el, node, record);
378
+ writeLook(el, node, record.menus);
379
+ bindSignals(el, node, record);
380
+ if (node.extensions !== undefined) record.extended.push({ el, node });
381
+ for (const child of buildChildren(el, node, record)) el.append(child);
382
+ return el;
383
+ }
384
+
385
+ /** What a node asks to be wired once every id exists: its binds and its breakpoints. */
386
+ function collectNode(el: HTMLElement, node: SharedTreeNode, record: BuildRecord): void {
322
387
  for (const [property, binding] of Object.entries(node.bindings ?? {})) {
323
388
  record.binds.push({
324
389
  el,
@@ -328,15 +393,32 @@ function buildNode(node: SharedTreeNode, record: BuildRecord): HTMLElement {
328
393
  flags: binding.flags ?? [],
329
394
  });
330
395
  }
331
- for (const [prop, value] of Object.entries(node.props ?? {})) writeProp(el, prop, value);
332
396
  if (node.breakpoints !== undefined) record.breakpointHosts.push({ el, node });
397
+ }
398
+
399
+ /**
400
+ * What a node looks like: its authored properties, style classes and extensions. Separate from the
401
+ * rest because a custom element may not gain attributes or children in its constructor, which is
402
+ * where a registered class's host is built (see {@link buildTemplateTree}).
403
+ */
404
+ function writeLook(el: HTMLElement, node: SharedTreeNode, menus?: SharedTreeNode['menus']): void {
405
+ // A menu is a value, not an attribute: the id the projection keeps would parse as no menu.
406
+ for (const [prop, value] of Object.entries(node.props ?? {})) {
407
+ if (prop !== 'menu-model') writeProp(el, prop, value);
408
+ }
409
+ for (const [prop, model] of menuAssignments(node, menus)) {
410
+ (el as unknown as Record<string, unknown>)[propertyOf(prop)] = model;
411
+ }
333
412
  // `styleClasses` is `GtkWidget:css-classes`, and this renderer's door for it is the
334
413
  // `class` attribute — what `.title-1`, `.dimmed` and `.card` select on. Unread, a
335
414
  // `.blp`'s `styles ["title-1"]` reached the tree and never the page.
336
415
  if (node.styleClasses !== undefined && node.styleClasses.length > 0) el.classList.add(...node.styleClasses);
337
416
  writeExtensions(el, node);
338
- bindSignals(el, node, record);
339
- if (node.extensions !== undefined) record.extended.push({ el, node });
417
+ }
418
+
419
+ /** The built children of `node`, placed (`page`, `slot`) but not yet appended to `el`. */
420
+ function buildChildren(el: HTMLElement, node: SharedTreeNode, record: BuildRecord): HTMLElement[] {
421
+ const built: HTMLElement[] = [];
340
422
  for (const child of node.children ?? []) {
341
423
  const childEl = buildNode(child, record);
342
424
  if (child.page !== undefined) writePage(el, childEl, child);
@@ -344,9 +426,39 @@ function buildNode(node: SharedTreeNode, record: BuildRecord): HTMLElement {
344
426
  childEl.setAttribute('slot', child.slot);
345
427
  record.placed.push({ parent: el, child: childEl, slot: child.slot });
346
428
  }
347
- el.append(childEl);
429
+ built.push(childEl);
348
430
  }
349
- return el;
431
+ return built;
432
+ }
433
+
434
+ /** A template tree built for a registered class: its children are not attached, its look is not written. */
435
+ export interface BuiltTemplateTree {
436
+ readonly children: HTMLElement[];
437
+ /** Every object the template names with an `id`. */
438
+ readonly objects: Record<string, HTMLElement>;
439
+ /** Writes the root's own properties, classes and extensions onto the host — at its first connect. */
440
+ finish(): void;
441
+ }
442
+
443
+ /**
444
+ * The template of a registered class (ADR 0096 § 3), built into `host`: children created and wired
445
+ * to their handlers and binds through `scope`, none appended and nothing written to `host`'s
446
+ * attributes, because `host` is still inside its constructor. `finish` and the caller's own append
447
+ * belong to the first `connectedCallback`.
448
+ */
449
+ export function buildTemplateTree(tree: SharedTreeNode, scope: TemplateScope, host: HTMLElement): BuiltTemplateTree {
450
+ assertTreeConstructs('adwaita-web', capabilities, tree);
451
+ const record = newRecord(undefined, undefined, scope);
452
+ record.menus = tree.menus;
453
+ if (tree.id !== undefined) record.ids.set(tree.id, host);
454
+ collectNode(host, tree, record);
455
+ bindSignals(host, tree, record);
456
+ const children = buildChildren(host, tree, record);
457
+ for (const bind of record.binds) bindProperty(bind, record);
458
+ for (const breakpointHost of record.breakpointHosts) {
459
+ bindBreakpoints(breakpointHost.el, breakpointHost.node, record);
460
+ }
461
+ return { children, objects: Object.fromEntries(record.ids), finish: () => writeLook(host, tree, tree.menus) };
350
462
  }
351
463
 
352
464
  /**
@@ -368,6 +480,29 @@ function bindSignals(el: HTMLElement, node: SharedTreeNode, record: BuildRecord)
368
480
  `run. It dispatches: ${Object.keys(declared).join(', ') || 'none'}.`,
369
481
  );
370
482
  }
483
+ if (record.template !== undefined) {
484
+ if (signal.flags?.includes('after') === true) {
485
+ throw new Error(
486
+ `the handler '${signal.handler}' for '${name}' uses after, which is not in the GObject subset.`,
487
+ );
488
+ }
489
+ const object =
490
+ signal.object === undefined
491
+ ? undefined
492
+ : signal.object === 'template'
493
+ ? record.template.instance
494
+ : record.ids.get(signal.object);
495
+ if (signal.object !== undefined && object === undefined) {
496
+ throw new Error(
497
+ `the handler '${signal.handler}' for '${name}' names the object '${signal.object}', which nothing in this tree has.`,
498
+ );
499
+ }
500
+ (el as unknown as GObjectInstance).connect(
501
+ name,
502
+ record.template.handler(signal.handler, { object, flags: signal.flags }),
503
+ );
504
+ continue;
505
+ }
371
506
  if (signal.object !== undefined || (signal.flags?.length ?? 0) > 0) {
372
507
  throw new Error(
373
508
  `the handler '${signal.handler}' for '${name}' uses ${signal.object === undefined ? '' : 'an object '}` +
@@ -291,6 +291,8 @@ export const AdwSplitButtonTest = async () => {
291
291
  const { el, host } = mount({
292
292
  'menu-model': '[{"label":"Copy","action":"app.copy"},{"label":"Copy","action":"app.copy-special"}]',
293
293
  });
294
+ // GTK dims an item whose action is missing (0097 § 3), so the app declares both.
295
+ el.actions = { 'app.copy': {}, 'app.copy-special': {} };
294
296
  const activated: Array<{ label: string; action?: string; id: string; path: number[] }> = [];
295
297
  el.addEventListener('menu-activated', (event) => activated.push((event as CustomEvent).detail));
296
298