@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.
- package/dist/adwaita-web.css +8 -2
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/application.d.ts +24 -0
- package/lib/types/application.spec.d.ts +1 -0
- package/lib/types/elements/adw-application-window.d.ts +2 -0
- package/lib/types/elements/adw-view-stack.d.ts +3 -0
- package/lib/types/elements/checks.d.ts +6 -0
- package/lib/types/elements/gtk-application-window.d.ts +2 -0
- package/lib/types/elements/gtk-drop-down.d.ts +3 -0
- package/lib/types/elements/gtk-entry.d.ts +10 -0
- package/lib/types/elements/gtk-menu-button.d.ts +2 -0
- package/lib/types/elements/gtk-scrolled-window.d.ts +16 -0
- package/lib/types/elements/gtk-stack.d.ts +3 -0
- package/lib/types/elements/gtk-switch.d.ts +3 -0
- package/lib/types/glib.spec.d.ts +1 -0
- package/lib/types/gobject-door.d.ts +4 -0
- package/lib/types/gobject-door.spec.d.ts +1 -0
- package/lib/types/gobject-elements.d.ts +17 -0
- package/lib/types/gtksource/gtk-source-view.d.ts +77 -0
- package/lib/types/gtksource/gtk-source-view.spec.d.ts +2 -0
- package/lib/types/gtksource/web-editor-driver.d.ts +44 -0
- package/lib/types/index.d.ts +3 -0
- package/lib/types/menu-actions.spec.d.ts +1 -0
- package/lib/types/namespace/adw.d.ts +1 -0
- package/lib/types/namespace/gio.d.ts +2 -0
- package/lib/types/namespace/glib.d.ts +10 -0
- package/lib/types/namespace/gobject.d.ts +27 -0
- package/lib/types/namespace/gtk-enums.d.ts +22 -0
- package/lib/types/namespace/gtk.d.ts +2 -0
- package/lib/types/namespace/gtksource.d.ts +2 -0
- package/lib/types/shared-tree-builder.d.ts +20 -1
- package/lib/types/styles.generated.d.ts +1 -1
- package/package.json +11 -9
- package/scss/_reset.scss +1 -1
- package/src/application.spec.ts +75 -0
- package/src/application.ts +92 -0
- package/src/construct-vectors.spec.ts +155 -0
- package/src/elements/adw-application-window.ts +5 -0
- package/src/elements/adw-split-button.ts +5 -1
- package/src/elements/adw-view-stack.ts +14 -0
- package/src/elements/checks.ts +25 -1
- package/src/elements/gtk-application-window.ts +5 -0
- package/src/elements/gtk-drop-down.ts +16 -0
- package/src/elements/gtk-entry.ts +45 -7
- package/src/elements/gtk-menu-button.ts +12 -1
- package/src/elements/gtk-popover-menu.ts +7 -2
- package/src/elements/gtk-scrolled-window.ts +58 -0
- package/src/elements/gtk-stack.ts +8 -0
- package/src/elements/gtk-switch.ts +8 -0
- package/src/glib.spec.ts +34 -0
- package/src/gobject-door.spec.ts +119 -0
- package/src/gobject-door.ts +137 -0
- package/src/gobject-elements.ts +172 -0
- package/src/gtksource/gtk-source-view.spec.ts +69 -0
- package/src/gtksource/gtk-source-view.ts +379 -0
- package/src/gtksource/web-editor-driver.ts +260 -0
- package/src/index.ts +10 -0
- package/src/menu-actions.spec.ts +47 -0
- package/src/namespace/adw.ts +1 -0
- package/src/namespace/gio.ts +2 -0
- package/src/namespace/glib.ts +22 -0
- package/src/namespace/gobject.ts +37 -0
- package/src/namespace/gtk-enums.ts +15 -0
- package/src/namespace/gtk.ts +2 -0
- package/src/namespace/gtksource.ts +9 -0
- package/src/shared-tree-builder.ts +142 -7
- package/src/split-button.spec.ts +2 -0
- package/src/styles.generated.ts +1 -1
- 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 {
|
|
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(
|
|
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
|
-
|
|
339
|
-
|
|
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
|
-
|
|
429
|
+
built.push(childEl);
|
|
348
430
|
}
|
|
349
|
-
return
|
|
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 '}` +
|
package/src/split-button.spec.ts
CHANGED
|
@@ -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
|
|