@gjsify/adwaita-web 0.64.12-next.279 → 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.
- package/lib/types/elements/adw-alert-dialog.d.ts +0 -8
- package/lib/types/elements/adw-overlay-split-view.d.ts +5 -0
- package/lib/types/elements/adw-view-stack.d.ts +20 -0
- package/lib/types/elements/adw-view-switcher-bar.d.ts +4 -0
- package/lib/types/elements/gtk-box.d.ts +16 -0
- package/lib/types/elements/gtk-menu-button.d.ts +5 -0
- package/lib/types/elements/gtk-scrolled-window.d.ts +15 -2
- package/lib/types/elements/gtk-stack.d.ts +17 -0
- package/lib/types/elements/gtk-window.d.ts +14 -8
- package/lib/types/gobject-elements.d.ts +2 -0
- package/lib/types/gtksource/gtk-source-view.d.ts +2 -1
- package/lib/types/index.d.ts +1 -0
- package/lib/types/namespace/adw.d.ts +1 -1
- package/lib/types/namespace/gdk.d.ts +1 -0
- package/lib/types/namespace/gio.d.ts +1 -0
- package/lib/types/namespace/graphene.d.ts +8 -0
- package/lib/types/namespace/gtk.d.ts +1 -1
- package/lib/types/shared-tree-builder.d.ts +7 -0
- package/lib/types/widget-element.d.ts +25 -0
- package/package.json +12 -12
- package/src/adw-view-switcher-bar.spec.ts +12 -0
- package/src/elements/adw-alert-dialog.ts +0 -8
- package/src/elements/adw-overlay-split-view.ts +17 -0
- package/src/elements/adw-view-stack.ts +50 -0
- package/src/elements/adw-view-switcher-bar.ts +10 -0
- package/src/elements/gtk-box.ts +49 -1
- package/src/elements/gtk-menu-button.ts +17 -0
- package/src/elements/gtk-scrolled-window.ts +60 -15
- package/src/elements/gtk-stack.ts +42 -0
- package/src/elements/gtk-window.ts +43 -10
- package/src/gdk.spec.ts +9 -0
- package/src/gobject-door.spec.ts +134 -0
- package/src/gobject-elements.ts +26 -2
- package/src/gtk-box.spec.ts +38 -0
- package/src/gtk-menu-button.spec.ts +15 -0
- package/src/gtk-stack.spec.ts +26 -0
- package/src/gtk-window.spec.ts +35 -1
- package/src/gtksource/gtk-source-view.ts +18 -3
- package/src/index.ts +2 -0
- package/src/namespace/adw.ts +1 -1
- package/src/namespace/gdk.ts +2 -0
- package/src/namespace/gio.ts +3 -0
- package/src/namespace/graphene.ts +26 -0
- package/src/namespace/gtk.ts +13 -9
- package/src/shared-tree-builder.ts +42 -23
- package/src/split-views.spec.ts +11 -0
- package/src/style-manager.spec.ts +14 -0
- package/src/style-manager.ts +5 -0
- package/src/view-stack.spec.ts +41 -0
- package/src/widget-element.ts +53 -1
- package/src/widget-members.spec.ts +95 -0
|
@@ -210,7 +210,9 @@ export class GtkScrolledWindow extends WidgetElement {
|
|
|
210
210
|
private _fadeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
211
211
|
private _built = false;
|
|
212
212
|
/** What an unbuilt window answers before it has a viewport to read. */
|
|
213
|
-
private readonly _standIn: { hadjustment?:
|
|
213
|
+
private readonly _standIn: { hadjustment?: GtkAdjustment; vadjustment?: GtkAdjustment } = {};
|
|
214
|
+
private readonly _heard = new WeakSet<object>();
|
|
215
|
+
private _pushing = false;
|
|
214
216
|
private readonly _adopted: { hadjustment?: AdwAdjustment; vadjustment?: AdwAdjustment } = {};
|
|
215
217
|
|
|
216
218
|
static get observedAttributes() {
|
|
@@ -265,21 +267,18 @@ export class GtkScrolledWindow extends WidgetElement {
|
|
|
265
267
|
this.vscrollbarPolicy = v;
|
|
266
268
|
}
|
|
267
269
|
|
|
268
|
-
/**
|
|
270
|
+
/**
|
|
271
|
+
* `GtkScrollable:hadjustment` as the window reports it: the one it was given, else its own.
|
|
272
|
+
*
|
|
273
|
+
* Either is ONE object for the window's life, observable like GTK's: the viewport's
|
|
274
|
+
* numbers are configured into it, and a write to its value scrolls the content.
|
|
275
|
+
*/
|
|
269
276
|
get hadjustment(): AdwAdjustment {
|
|
270
|
-
return (
|
|
271
|
-
this._adopted.hadjustment ??
|
|
272
|
-
(this._viewport as GtkViewport | undefined)?.hadjustment ??
|
|
273
|
-
(this._standIn.hadjustment ??= new GtkAdjustment())
|
|
274
|
-
);
|
|
277
|
+
return this._adjustmentOf('hadjustment');
|
|
275
278
|
}
|
|
276
279
|
|
|
277
280
|
get vadjustment(): AdwAdjustment {
|
|
278
|
-
return (
|
|
279
|
-
this._adopted.vadjustment ??
|
|
280
|
-
(this._viewport as GtkViewport | undefined)?.vadjustment ??
|
|
281
|
-
(this._standIn.vadjustment ??= new GtkAdjustment())
|
|
282
|
-
);
|
|
281
|
+
return this._adjustmentOf('vadjustment');
|
|
283
282
|
}
|
|
284
283
|
|
|
285
284
|
get_hadjustment(): AdwAdjustment {
|
|
@@ -290,13 +289,55 @@ export class GtkScrolledWindow extends WidgetElement {
|
|
|
290
289
|
return this.vadjustment;
|
|
291
290
|
}
|
|
292
291
|
|
|
293
|
-
/** Adopts `adjustment` as what `get_hadjustment` answers;
|
|
292
|
+
/** Adopts `adjustment` as what `get_hadjustment` answers; it is kept in step with the viewport both ways. */
|
|
294
293
|
set_hadjustment(adjustment: AdwAdjustment): void {
|
|
295
|
-
this.
|
|
294
|
+
this._adopt('hadjustment', adjustment);
|
|
296
295
|
}
|
|
297
296
|
|
|
298
297
|
set_vadjustment(adjustment: AdwAdjustment): void {
|
|
299
|
-
this.
|
|
298
|
+
this._adopt('vadjustment', adjustment);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private _adjustmentOf(key: 'hadjustment' | 'vadjustment'): AdwAdjustment {
|
|
302
|
+
const adopted = this._adopted[key];
|
|
303
|
+
if (adopted !== undefined) return adopted;
|
|
304
|
+
let own = this._standIn[key];
|
|
305
|
+
if (own === undefined) {
|
|
306
|
+
own = this._standIn[key] = new GtkAdjustment();
|
|
307
|
+
this._listen(key, own);
|
|
308
|
+
this._push(key);
|
|
309
|
+
}
|
|
310
|
+
return own;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private _adopt(key: 'hadjustment' | 'vadjustment', adjustment: AdwAdjustment): void {
|
|
314
|
+
this._adopted[key] = adjustment;
|
|
315
|
+
this._listen(key, adjustment);
|
|
316
|
+
this._push(key);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** A write to the adjustment's value moves the scrollport; a plain object has no signal to hear. */
|
|
320
|
+
private _listen(key: 'hadjustment' | 'vadjustment', adjustment: AdwAdjustment): void {
|
|
321
|
+
if (!(adjustment instanceof GtkAdjustment) || this._heard.has(adjustment)) return;
|
|
322
|
+
this._heard.add(adjustment);
|
|
323
|
+
adjustment.connect('value-changed', () => {
|
|
324
|
+
if (this._pushing || this._adjustmentOf(key) !== adjustment || !this._viewport) return;
|
|
325
|
+
this._viewport.scrollAxis(key === 'hadjustment' ? 'horizontal' : 'vertical', adjustment.value);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Configures the window's adjustment of one axis from the viewport's numbers. */
|
|
330
|
+
private _push(key: 'hadjustment' | 'vadjustment'): void {
|
|
331
|
+
const viewport = this._viewport as GtkViewport | undefined;
|
|
332
|
+
const adjustment = this._adopted[key] ?? this._standIn[key];
|
|
333
|
+
if (!viewport?.isConnected || !(adjustment instanceof GtkAdjustment)) return;
|
|
334
|
+
const v = viewport[key];
|
|
335
|
+
this._pushing = true;
|
|
336
|
+
try {
|
|
337
|
+
adjustment.configure(v.value, v.lower, v.upper, v.stepIncrement, v.pageIncrement, v.pageSize);
|
|
338
|
+
} finally {
|
|
339
|
+
this._pushing = false;
|
|
340
|
+
}
|
|
300
341
|
}
|
|
301
342
|
|
|
302
343
|
/** `GtkScrolledWindow:window-placement` — where the contents sit against the bars. */
|
|
@@ -533,6 +574,8 @@ export class GtkScrolledWindow extends WidgetElement {
|
|
|
533
574
|
// bars are not connected, so this pass has no adjustment to read and no geometry to
|
|
534
575
|
// decide — `connectedCallback` ends with a `_sync()` of its own, once they are.
|
|
535
576
|
if (bars === null || !this._viewport.isConnected || !bars.vertical.isConnected) return;
|
|
577
|
+
this._push('hadjustment');
|
|
578
|
+
this._push('vadjustment');
|
|
536
579
|
const placement = this.windowPlacement;
|
|
537
580
|
const indicators = this._useIndicators;
|
|
538
581
|
this.classList.toggle('frame', this.hasFrame);
|
|
@@ -621,6 +664,8 @@ export class GtkScrolledWindow extends WidgetElement {
|
|
|
621
664
|
const bars = this._bars;
|
|
622
665
|
if (bars === null) return;
|
|
623
666
|
const detail = (event as CustomEvent<{ hadjustment: AdwAdjustment; vadjustment: AdwAdjustment }>).detail;
|
|
667
|
+
this._push('hadjustment');
|
|
668
|
+
this._push('vadjustment');
|
|
624
669
|
for (const axis of AXES) {
|
|
625
670
|
const adjustment = detail[axis.key];
|
|
626
671
|
(bars[axis.bar] as HTMLElement & { adjustment: AdwAdjustment }).adjustment = adjustment;
|
|
@@ -387,6 +387,48 @@ export class GtkStack extends WidgetElement {
|
|
|
387
387
|
child.remove();
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
+
/**
|
|
391
|
+
* `gtk_stack_remove` under its GIR name. With no argument it stays `ChildNode.remove()`:
|
|
392
|
+
* the stack leaves its own parent.
|
|
393
|
+
*/
|
|
394
|
+
override remove(child?: HTMLElement): void {
|
|
395
|
+
if (child === undefined) {
|
|
396
|
+
super.remove();
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
this.removePage(child);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** `gtk_stack_add_titled` under its GIR name. */
|
|
403
|
+
add_titled(child: HTMLElement, name: string | null, title: string | null): HTMLElement {
|
|
404
|
+
return this.addTitled(child, name, title);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** `gtk_stack_add_named` under its GIR name. */
|
|
408
|
+
add_named(child: HTMLElement, name: string | null): HTMLElement {
|
|
409
|
+
return this.addNamed(child, name);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** `gtk_stack_get_visible_child_name`. */
|
|
413
|
+
get_visible_child_name(): string | null {
|
|
414
|
+
return this.visibleChildName;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** `gtk_stack_set_visible_child_name`. */
|
|
418
|
+
set_visible_child_name(name: string): void {
|
|
419
|
+
this.setVisibleChildName(name);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** `gtk_stack_get_visible_child`. */
|
|
423
|
+
get_visible_child(): HTMLElement | null {
|
|
424
|
+
return this.visibleChild;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** `gtk_stack_set_visible_child`. */
|
|
428
|
+
set_visible_child(child: HTMLElement): void {
|
|
429
|
+
this.setVisibleChild(child);
|
|
430
|
+
}
|
|
431
|
+
|
|
390
432
|
/**
|
|
391
433
|
* `gtk_stack_set_visible_child` (gtkstack.c:2244-2281), with `gtk_stack_set_visible_
|
|
392
434
|
* child_full`'s per-call transition (gtkstack.c:2306-2334). A child that is not in
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
// Copyright (c) The GTK Team. LGPLv2.1+.
|
|
97
97
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
98
98
|
|
|
99
|
-
import { CloseRequestState, WindowMemberState, type WindowMemberProperty } from '@gjsify/adwaita-core';
|
|
99
|
+
import { CloseRequestState, WindowMemberState, stepDefaultSize, type WindowMemberProperty } from '@gjsify/adwaita-core';
|
|
100
100
|
|
|
101
101
|
import { defineWindowApplication, type GtkApplication } from '../application.js';
|
|
102
102
|
import { installElementApi } from '../gobject-elements.js';
|
|
@@ -171,14 +171,6 @@ export class GtkWindow extends WidgetElement {
|
|
|
171
171
|
declare get_application: () => GtkApplication | null;
|
|
172
172
|
declare set_application: (application: GtkApplication | null) => void;
|
|
173
173
|
|
|
174
|
-
/** `GObject.Object.connect`: installed per instance by `installElementApi`; refuses an undeclared signal. */
|
|
175
|
-
declare connect: (signal: string, handler: (emitter: this, ...args: any[]) => unknown) => number;
|
|
176
|
-
/** `GObject.Object.disconnect`: throws for an id that is not connected. */
|
|
177
|
-
declare disconnect: (id: number) => void;
|
|
178
|
-
/** `GObject.Object.emit`; `close-request` is refused, call `close()`. */
|
|
179
|
-
declare emit: (signal: string, ...args: unknown[]) => void;
|
|
180
|
-
/** `GObject.Object.notify`. */
|
|
181
|
-
declare notify: (name: string) => void;
|
|
182
174
|
/** `GObject.Object.get_property`, by GTK's dashed name. */
|
|
183
175
|
declare get_property: (name: string) => unknown;
|
|
184
176
|
/** `GObject.Object.set_property`; a read-only property throws. */
|
|
@@ -281,6 +273,26 @@ export class GtkWindow extends WidgetElement {
|
|
|
281
273
|
this._write('deletable', String(value));
|
|
282
274
|
}
|
|
283
275
|
|
|
276
|
+
/** `Gtk.Window:hide-on-close` — `close()` hides the window instead of destroying it. */
|
|
277
|
+
get hide_on_close(): boolean {
|
|
278
|
+
return this._offByDefault('hide-on-close');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
set hide_on_close(value: boolean) {
|
|
282
|
+
if (this._offByDefault('hide-on-close') === value) return;
|
|
283
|
+
this._write('hide-on-close', String(value));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** `gtk_window_get_hide_on_close`. */
|
|
287
|
+
get_hide_on_close(): boolean {
|
|
288
|
+
return this.hide_on_close;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** `gtk_window_set_hide_on_close`. */
|
|
292
|
+
set_hide_on_close(setting: boolean): void {
|
|
293
|
+
this.hide_on_close = setting;
|
|
294
|
+
}
|
|
295
|
+
|
|
284
296
|
/**
|
|
285
297
|
* `setAttribute` for a write that changes nothing. Every GObject setter here returns
|
|
286
298
|
* early when the value is already right (gtkwindow.c:1015-1017, :1045-1046, :894-895),
|
|
@@ -540,7 +552,7 @@ export class GtkWindow extends WidgetElement {
|
|
|
540
552
|
close(): boolean {
|
|
541
553
|
const outcome = this._closing.close({
|
|
542
554
|
realized: this.isConnected,
|
|
543
|
-
hideOnClose: this.
|
|
555
|
+
hideOnClose: this.hide_on_close,
|
|
544
556
|
emit: () => {
|
|
545
557
|
const request = new CustomEvent('close-request', { bubbles: true, cancelable: true });
|
|
546
558
|
this.dispatchEvent(request);
|
|
@@ -613,6 +625,27 @@ export class GtkWindow extends WidgetElement {
|
|
|
613
625
|
this._members.reportFocus(this.get_focus());
|
|
614
626
|
}
|
|
615
627
|
|
|
628
|
+
/** `gtk_window_get_default_size`: `[width, height]`, `[0, 0]` while neither attribute is set. */
|
|
629
|
+
get_default_size(): [number, number] {
|
|
630
|
+
const read = (name: string): number => {
|
|
631
|
+
const value = Number.parseInt(this.getAttribute(name) ?? '', 10);
|
|
632
|
+
return Number.isFinite(value) ? value : 0;
|
|
633
|
+
};
|
|
634
|
+
return [read('default-width'), read('default-height')];
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* `gtk_window_set_default_size`: a value below -1 is refused (GTK's `g_return_if_fail`), an
|
|
639
|
+
* equal one does not notify, and `default-height` notifies before `default-width`.
|
|
640
|
+
*/
|
|
641
|
+
set_default_size(width: number, height: number): void {
|
|
642
|
+
const step = stepDefaultSize(this.get_default_size(), width, height);
|
|
643
|
+
if (step === null) return;
|
|
644
|
+
for (const property of step.changed) {
|
|
645
|
+
this._write(property, String(property === 'default-width' ? step.width : step.height));
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
616
649
|
/** `gtk_window_is_maximized`: the FRAME's state, not the browser window's. */
|
|
617
650
|
is_maximized(): boolean {
|
|
618
651
|
return this.maximized;
|
package/src/gdk.spec.ts
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
driveGdkClipboardVectors,
|
|
9
|
+
driveRGBAVectors,
|
|
10
|
+
RGBA_VECTORS,
|
|
11
|
+
type GdkRGBALike,
|
|
9
12
|
GDK_CLIPBOARD_VECTORS,
|
|
10
13
|
type GdkClipboardNamespaces,
|
|
11
14
|
} from '@gjsify/adwaita-core/conformance';
|
|
@@ -51,6 +54,12 @@ export const GdkTest = async () => {
|
|
|
51
54
|
),
|
|
52
55
|
);
|
|
53
56
|
|
|
57
|
+
await driveRGBAVectors(
|
|
58
|
+
{ name: 'adwaita-web', isOracle: false, Gdk: Gdk as unknown as GdkRGBALike },
|
|
59
|
+
{ describe, it, expect },
|
|
60
|
+
RGBA_VECTORS,
|
|
61
|
+
);
|
|
62
|
+
|
|
54
63
|
await describe('adwaita-web: Gdk clipboard door', async () => {
|
|
55
64
|
const mounted = () => {
|
|
56
65
|
const view = document.createElement('gtk-source-view') as GtkSourceView;
|
package/src/gobject-door.spec.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { describe, expect, it } from '@gjsify/unit';
|
|
|
5
5
|
|
|
6
6
|
import { Adw, GObject } from './index.js';
|
|
7
7
|
import { tagForTypeName } from './gobject-door.js';
|
|
8
|
+
import { listenElement } from './gobject-elements.js';
|
|
8
9
|
import type { SharedTreeNode } from '@gjsify/adwaita-core/conformance';
|
|
9
10
|
import { bindProperties } from '@gjsify/adwaita-core';
|
|
10
11
|
|
|
@@ -90,6 +91,45 @@ GObject.registerClass(
|
|
|
90
91
|
GoWebSensitive as never,
|
|
91
92
|
);
|
|
92
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
|
+
|
|
93
133
|
export const GObjectDoorTest = async () => {
|
|
94
134
|
await driveGObjectVectors(subject, { describe, it, expect }, GOBJECT_VECTORS);
|
|
95
135
|
await describe('adwaita-web: GObject.registerClass end to end (ADR 0096)', async () => {
|
|
@@ -97,6 +137,24 @@ export const GObjectDoorTest = async () => {
|
|
|
97
137
|
expect(tagForTypeName('Hexdump')).toBe('gjsify-hexdump');
|
|
98
138
|
expect(customElements.get('gjsify-go-web-hexdump')).toBe(GoWebHexdump);
|
|
99
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
|
+
});
|
|
100
158
|
await it('refuses a second class under the same tag', () => {
|
|
101
159
|
expect(() =>
|
|
102
160
|
GObject.registerClass({ GTypeName: 'GoWebHexdump' }, class extends Adw.Bin {} as never),
|
|
@@ -160,4 +218,80 @@ export const GObjectDoorTest = async () => {
|
|
|
160
218
|
expect(el._sw.hasAttribute('disabled')).toBe(true);
|
|
161
219
|
});
|
|
162
220
|
});
|
|
221
|
+
await describe('adwaita-web: a signal is per instance, as in GObject', async () => {
|
|
222
|
+
const nested = (tag: string) => {
|
|
223
|
+
const parent = document.createElement(tag) as HTMLElement;
|
|
224
|
+
const child = document.createElement(tag) as HTMLElement;
|
|
225
|
+
parent.append(child);
|
|
226
|
+
document.body.append(parent);
|
|
227
|
+
return { parent, child };
|
|
228
|
+
};
|
|
229
|
+
await it('notify::visible-child-name of a child stack is not heard on the parent', () => {
|
|
230
|
+
const { parent, child } = nested('adw-view-stack');
|
|
231
|
+
let heard = 0;
|
|
232
|
+
listenElement(parent, 'notify::visible-child-name', () => heard++);
|
|
233
|
+
child.dispatchEvent(new CustomEvent('notify::visible-child-name', { bubbles: true }));
|
|
234
|
+
expect(heard).toBe(0);
|
|
235
|
+
parent.dispatchEvent(new CustomEvent('notify::visible-child-name', { bubbles: true }));
|
|
236
|
+
expect(heard).toBe(1);
|
|
237
|
+
parent.remove();
|
|
238
|
+
});
|
|
239
|
+
await it('activated of a child row is not heard on the parent, but still bubbles in the DOM', () => {
|
|
240
|
+
const { parent, child } = nested('adw-action-row');
|
|
241
|
+
let heard = 0;
|
|
242
|
+
let bubbled = 0;
|
|
243
|
+
listenElement(parent, 'activated', () => heard++);
|
|
244
|
+
parent.addEventListener('activated', () => bubbled++);
|
|
245
|
+
child.dispatchEvent(new CustomEvent('activated', { bubbles: true }));
|
|
246
|
+
expect(heard).toBe(0);
|
|
247
|
+
expect(bubbled).toBe(1);
|
|
248
|
+
parent.dispatchEvent(new CustomEvent('activated', { bubbles: true }));
|
|
249
|
+
expect(heard).toBe(1);
|
|
250
|
+
parent.remove();
|
|
251
|
+
});
|
|
252
|
+
await it('clicked of a nested button is not heard on the outer one; a click on plain content is', () => {
|
|
253
|
+
const { parent, child } = nested('gtk-button');
|
|
254
|
+
const inner = document.createElement('span');
|
|
255
|
+
parent.append(inner);
|
|
256
|
+
let heard = 0;
|
|
257
|
+
listenElement(parent, 'clicked', () => heard++);
|
|
258
|
+
child.dispatchEvent(new Event('click', { bubbles: true }));
|
|
259
|
+
expect(heard).toBe(0);
|
|
260
|
+
inner.dispatchEvent(new Event('click', { bubbles: true }));
|
|
261
|
+
expect(heard).toBe(1);
|
|
262
|
+
parent.remove();
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
await describe('adwaita-web: a bare element has the GObject signal API', async () => {
|
|
266
|
+
await it('connects to a signal on createElement, and disconnects by id', () => {
|
|
267
|
+
const stack = document.createElement('adw-view-stack') as HTMLElement & {
|
|
268
|
+
connect(signal: string, handler: () => void): number;
|
|
269
|
+
disconnect(id: number): void;
|
|
270
|
+
};
|
|
271
|
+
document.body.append(stack);
|
|
272
|
+
let heard = 0;
|
|
273
|
+
const id = stack.connect('notify::visible-child-name', () => heard++);
|
|
274
|
+
stack.dispatchEvent(new CustomEvent('notify::visible-child-name'));
|
|
275
|
+
expect(heard).toBe(1);
|
|
276
|
+
stack.disconnect(id);
|
|
277
|
+
stack.dispatchEvent(new CustomEvent('notify::visible-child-name'));
|
|
278
|
+
expect(heard).toBe(1);
|
|
279
|
+
stack.remove();
|
|
280
|
+
});
|
|
281
|
+
await it('connects to clicked on a bare button', () => {
|
|
282
|
+
const button = document.createElement('gtk-button') as HTMLElement & {
|
|
283
|
+
connect(signal: string, handler: () => void): number;
|
|
284
|
+
};
|
|
285
|
+
let heard = 0;
|
|
286
|
+
button.connect('clicked', () => heard++);
|
|
287
|
+
button.click();
|
|
288
|
+
expect(heard).toBe(1);
|
|
289
|
+
});
|
|
290
|
+
await it('refuses a signal the element does not declare', () => {
|
|
291
|
+
const button = document.createElement('gtk-button') as HTMLElement & {
|
|
292
|
+
connect(signal: string, handler: () => void): number;
|
|
293
|
+
};
|
|
294
|
+
expect(() => button.connect('no-such-signal', () => {})).toThrow();
|
|
295
|
+
});
|
|
296
|
+
});
|
|
163
297
|
};
|
package/src/gobject-elements.ts
CHANGED
|
@@ -106,6 +106,18 @@ export function ensureNotifying(el: HTMLElement, property: string): void {
|
|
|
106
106
|
});
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* The element a bubbled DOM event is the GTK signal of: the nearest one from `target` up that
|
|
111
|
+
* declares `signal` as `event`. A click on plain content inside a button is the button's `clicked`;
|
|
112
|
+
* a click on a nested button is that button's, and a signal is per instance in GObject.
|
|
113
|
+
*/
|
|
114
|
+
function signalOwner(target: EventTarget | null, signal: string, event: string): Element | null {
|
|
115
|
+
for (let at = target instanceof Element ? target : null; at !== null; at = at.parentElement) {
|
|
116
|
+
if (dispatchedSignalsOf(at)[signal] === event) return at;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
109
121
|
/** Calls `handler(el, ...args)` for `signal`; returns the disconnect. */
|
|
110
122
|
export function listenElement(el: HTMLElement, signal: string, handler: Handler): () => void {
|
|
111
123
|
const event = eventOf(el, signal);
|
|
@@ -114,6 +126,7 @@ export function listenElement(el: HTMLElement, signal: string, handler: Handler)
|
|
|
114
126
|
const listener = (domEvent: Event) => {
|
|
115
127
|
// A notification bubbles, and a parent must not hear its child's property as its own.
|
|
116
128
|
if (notification && domEvent.target !== el) return;
|
|
129
|
+
if (!notification && !EMITTED.has(domEvent) && signalOwner(domEvent.target, signal, event) !== el) return;
|
|
117
130
|
const handled = handler(el, ...(EMITTED.has(domEvent) ? ((domEvent as CustomEvent).detail as unknown[]) : []));
|
|
118
131
|
// A GTK handler of a boolean-handled signal (`close-request`) returns true to take it over,
|
|
119
132
|
// which on a cancelable DOM event is `preventDefault()`; the handlers after it do not run.
|
|
@@ -198,8 +211,10 @@ export function installVisible(el: HTMLElement): void {
|
|
|
198
211
|
|
|
199
212
|
/** Gives `el` the instance API of a registered class, over the events it declares. */
|
|
200
213
|
export function installElementApi(el: HTMLElement): void {
|
|
201
|
-
// A registered class already has the core's four methods on its prototype
|
|
202
|
-
|
|
214
|
+
// A registered class already has the core's four methods on its prototype; the stubs of
|
|
215
|
+
// `defineLazyElementApi` are not those, they exist to run this.
|
|
216
|
+
const inherited = (el as unknown as Record<string, unknown>).connect;
|
|
217
|
+
if (typeof inherited === 'function' && !LAZY_API.has(inherited)) return;
|
|
203
218
|
WITH_API.add(el);
|
|
204
219
|
const connections = new Map<number, () => void>();
|
|
205
220
|
const define = (name: string, value: unknown) =>
|
|
@@ -237,6 +252,15 @@ export function installElementApi(el: HTMLElement): void {
|
|
|
237
252
|
define('has_css_class', (name: string): boolean => el.classList.contains(name));
|
|
238
253
|
}
|
|
239
254
|
|
|
255
|
+
const LAZY_API = new WeakSet<object>();
|
|
256
|
+
|
|
257
|
+
/** Marks the self-installing `connect` / `disconnect` / `emit` / `notify` of `proto` ({@link WidgetElement}). */
|
|
258
|
+
export function markLazyElementApi(proto: object): void {
|
|
259
|
+
for (const name of ['connect', 'disconnect', 'emit', 'notify']) {
|
|
260
|
+
LAZY_API.add((proto as Record<string, object>)[name]);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
240
264
|
const API_MEMBERS = new Set(['connect', 'disconnect', 'emit', 'notify', 'get_property', 'set_property']);
|
|
241
265
|
|
|
242
266
|
/**
|
package/src/gtk-box.spec.ts
CHANGED
|
@@ -33,6 +33,10 @@ function mount(count = 3): { box: GtkBox; kids: HTMLElement[]; host: HTMLElement
|
|
|
33
33
|
return { box, kids, host };
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/** The children by position in `kids`, `x` for the one extra. */
|
|
37
|
+
const order = (box: Element, kids: Element[], extra?: Element): string =>
|
|
38
|
+
[...box.children].map((el) => (el === extra ? 'x' : kids.indexOf(el))).join(',');
|
|
39
|
+
|
|
36
40
|
const rect = (el: Element) => el.getBoundingClientRect();
|
|
37
41
|
|
|
38
42
|
/** Author `value` the way markup does: absent is no attribute at all. */
|
|
@@ -138,6 +142,40 @@ export const GtkBoxTest = async () => {
|
|
|
138
142
|
],
|
|
139
143
|
};
|
|
140
144
|
|
|
145
|
+
await it('remove(child) detaches a direct child and leaves a stranger alone', () => {
|
|
146
|
+
const { box, kids, host } = mount(3);
|
|
147
|
+
const warn = console.warn;
|
|
148
|
+
const warnings: unknown[] = [];
|
|
149
|
+
console.warn = (...args: unknown[]) => void warnings.push(args);
|
|
150
|
+
try {
|
|
151
|
+
box.remove(kids[1]);
|
|
152
|
+
expect(order(box, kids)).toBe('0,2');
|
|
153
|
+
box.remove(document.createElement('div'));
|
|
154
|
+
expect(box.children.length).toBe(2);
|
|
155
|
+
expect(warnings.length).toBe(1);
|
|
156
|
+
box.remove();
|
|
157
|
+
expect(box.isConnected).toBe(false);
|
|
158
|
+
} finally {
|
|
159
|
+
console.warn = warn;
|
|
160
|
+
host.remove();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await it('insert_child_after and reorder_child_after follow the NULL = FIRST rule', () => {
|
|
165
|
+
const { box, kids, host } = mount(3);
|
|
166
|
+
try {
|
|
167
|
+
const extra = document.createElement('div');
|
|
168
|
+
expect(box.insert_child_after(extra, null)).toBe(true);
|
|
169
|
+
expect(order(box, kids, extra)).toBe('x,0,1,2');
|
|
170
|
+
expect(box.insert_child_after(extra, kids[0])).toBe(false);
|
|
171
|
+
expect(box.reorder_child_after(extra, kids[2])).toBe(true);
|
|
172
|
+
expect(order(box, kids, extra)).toBe('0,1,2,x');
|
|
173
|
+
expect(box.reorder_child_after(document.createElement('div'), null)).toBe(false);
|
|
174
|
+
} finally {
|
|
175
|
+
host.remove();
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
141
179
|
await it('mounts upgraded elements, stacked by the authored orientation and spacing', () => {
|
|
142
180
|
const { root, unmount } = mountSharedTree(tree);
|
|
143
181
|
try {
|
|
@@ -177,6 +177,21 @@ export const GtkMenuButtonTest = async () => {
|
|
|
177
177
|
unmountAll();
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
+
await it('menu_model / get_menu_model / set_menu_model mirror menuModel', () => {
|
|
181
|
+
const el = document.createElement('gtk-menu-button') as MenuButtonElement & {
|
|
182
|
+
menu_model: AdwMenuInput;
|
|
183
|
+
get_menu_model(): unknown;
|
|
184
|
+
set_menu_model(v: AdwMenuInput): void;
|
|
185
|
+
};
|
|
186
|
+
document.body.appendChild(el);
|
|
187
|
+
el.set_menu_model(['Save', 'Quit']);
|
|
188
|
+
expect(el.get_menu_model()).toBe(el.menuModel);
|
|
189
|
+
expect(el.querySelectorAll('.adw-popover-item').length).toBe(2);
|
|
190
|
+
el.menu_model = ['Save'];
|
|
191
|
+
expect(el.querySelectorAll('.adw-popover-item').length).toBe(1);
|
|
192
|
+
unmountAll();
|
|
193
|
+
});
|
|
194
|
+
|
|
180
195
|
await it('a malformed attribute is NOT a refusal — a typo may not stop the upgrade', () => {
|
|
181
196
|
// The other half of the same rule: `parseMenuModel` stays total, so this
|
|
182
197
|
// element upgrades with no menu instead of throwing.
|
package/src/gtk-stack.spec.ts
CHANGED
|
@@ -599,4 +599,30 @@ export const GtkStackTest = async () => {
|
|
|
599
599
|
}
|
|
600
600
|
});
|
|
601
601
|
});
|
|
602
|
+
|
|
603
|
+
await describe('<gtk-stack> GIR method names', async () => {
|
|
604
|
+
await it('add_named / add_titled / the visible-child accessors follow the C functions', () => {
|
|
605
|
+
const { el, host } = mount();
|
|
606
|
+
const [a, b] = [1, 2].map(() => document.createElement('div'));
|
|
607
|
+
el.add_named(a!, 'a');
|
|
608
|
+
el.add_titled(b!, 'b', 'Beta');
|
|
609
|
+
expect(el.get_visible_child_name()).toBe('a');
|
|
610
|
+
el.set_visible_child_name('b');
|
|
611
|
+
expect(el.get_visible_child()).toBe(b!);
|
|
612
|
+
el.set_visible_child(a!);
|
|
613
|
+
expect(el.get_visible_child_name()).toBe('a');
|
|
614
|
+
host.remove();
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
await it('remove(child) removes the page; remove() still detaches the stack', () => {
|
|
618
|
+
const { el, host } = mount();
|
|
619
|
+
const a = document.createElement('div');
|
|
620
|
+
el.add_named(a, 'a');
|
|
621
|
+
el.remove(a);
|
|
622
|
+
expect(a.parentElement).toBe(null);
|
|
623
|
+
el.remove();
|
|
624
|
+
expect(el.parentElement).toBe(null);
|
|
625
|
+
host.remove();
|
|
626
|
+
});
|
|
627
|
+
});
|
|
602
628
|
};
|
package/src/gtk-window.spec.ts
CHANGED
|
@@ -36,7 +36,7 @@ export function webWindowSubject(tag: string): { subject: WindowSubject; dispose
|
|
|
36
36
|
create({ hideOnClose }) {
|
|
37
37
|
// Never in a page: not realized, which is GTK's state for a window before `present()`.
|
|
38
38
|
const el = document.createElement(tag) as GtkWindow;
|
|
39
|
-
if (hideOnClose) el.
|
|
39
|
+
if (hideOnClose) el.hide_on_close = true;
|
|
40
40
|
el.append(document.createElement('button'), document.createElement('button'));
|
|
41
41
|
return el;
|
|
42
42
|
},
|
|
@@ -65,6 +65,8 @@ export function webWindowSubject(tag: string): { subject: WindowSubject; dispose
|
|
|
65
65
|
maximize: (w) => (w as GtkWindow).maximize(),
|
|
66
66
|
unmaximize: (w) => (w as GtkWindow).unmaximize(),
|
|
67
67
|
isMaximized: (w) => (w as GtkWindow).is_maximized(),
|
|
68
|
+
getDefaultSize: (w) => (w as GtkWindow).get_default_size(),
|
|
69
|
+
setDefaultSize: (w, width, height) => (w as GtkWindow).set_default_size(width, height),
|
|
68
70
|
isVisible: (w) => (w as GtkWindow).isConnected && !(w as GtkWindow).hidden,
|
|
69
71
|
focusables: (w) => [...(w as GtkWindow).querySelectorAll('button')] as unknown as [Element, Element],
|
|
70
72
|
setFocus: (w, widget) => (w as GtkWindow).set_focus(widget as Element | null),
|
|
@@ -244,6 +246,38 @@ export const GtkWindowTest = async () => {
|
|
|
244
246
|
host.remove();
|
|
245
247
|
});
|
|
246
248
|
|
|
249
|
+
await it('`hide_on_close` is the real property, default false, and close() follows it', async () => {
|
|
250
|
+
const { el, host } = mount();
|
|
251
|
+
expect(el.hide_on_close).toBe(false);
|
|
252
|
+
expect(el.get_hide_on_close()).toBe(false);
|
|
253
|
+
el.set_hide_on_close(true);
|
|
254
|
+
expect(el.hide_on_close).toBe(true);
|
|
255
|
+
expect(el.getAttribute('hide-on-close')).toBe('true');
|
|
256
|
+
el.close();
|
|
257
|
+
expect(el.isConnected).toBe(true);
|
|
258
|
+
expect(el.hidden).toBe(true);
|
|
259
|
+
el.hidden = false;
|
|
260
|
+
el.hide_on_close = false;
|
|
261
|
+
expect(el.close()).toBe(true);
|
|
262
|
+
expect(el.isConnected).toBe(false);
|
|
263
|
+
host.remove();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await it('notify::hide-on-close fires on change only', async () => {
|
|
267
|
+
const { el, host } = mount();
|
|
268
|
+
let seen = 0;
|
|
269
|
+
el.connect('notify::hide-on-close', () => void seen++);
|
|
270
|
+
el.hide_on_close = false;
|
|
271
|
+
expect(seen).toBe(0);
|
|
272
|
+
el.set_hide_on_close(true);
|
|
273
|
+
expect(seen).toBe(1);
|
|
274
|
+
el.hide_on_close = true;
|
|
275
|
+
expect(seen).toBe(1);
|
|
276
|
+
el.hide_on_close = false;
|
|
277
|
+
expect(seen).toBe(2);
|
|
278
|
+
host.remove();
|
|
279
|
+
});
|
|
280
|
+
|
|
247
281
|
await it('a handler that cancels `close-request` takes the whole thing over', async () => {
|
|
248
282
|
// gtkwindow.c:1300-1320 — `g_signal_stop_emission_by_name` on the default handler.
|
|
249
283
|
const { el, host } = mount();
|