@gjsify/adwaita-web 0.55.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/README.md +8 -0
- package/dist/adwaita-web.css +137 -29
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/adw-status-page.spec.d.ts +1 -0
- package/lib/types/application.d.ts +24 -0
- package/lib/types/application.spec.d.ts +1 -0
- package/lib/types/attributes.d.ts +19 -0
- package/lib/types/elements/adw-action-row.d.ts +4 -0
- package/lib/types/elements/adw-application-window.d.ts +2 -0
- package/lib/types/elements/adw-combo-row.d.ts +4 -0
- package/lib/types/elements/adw-entry-row.d.ts +6 -0
- package/lib/types/elements/adw-expander-row.d.ts +2 -0
- package/lib/types/elements/adw-header-bar.d.ts +6 -0
- package/lib/types/elements/adw-overlay-split-view.d.ts +13 -0
- package/lib/types/elements/adw-sidebar.d.ts +3 -2
- package/lib/types/elements/adw-spin-row.d.ts +6 -0
- package/lib/types/elements/adw-switch-row.d.ts +4 -0
- package/lib/types/elements/adw-toggle-group.d.ts +17 -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 +26 -2
- package/lib/types/elements/gtk-stack.d.ts +3 -0
- package/lib/types/elements/gtk-switch.d.ts +3 -0
- package/lib/types/elements/gtk-window.d.ts +8 -2
- 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 +4 -1
- 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/row-line-clamp.d.ts +20 -0
- package/lib/types/row-tooltip.d.ts +8 -0
- package/lib/types/row-tooltip.spec.d.ts +1 -0
- package/lib/types/shared-tree-builder.d.ts +69 -20
- package/lib/types/shared-tree-mount.spec.d.ts +1 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/widget-size.spec.d.ts +1 -0
- package/package.json +11 -9
- package/scss/_action_row.scss +39 -15
- package/scss/_button_row.scss +5 -0
- package/scss/_expander_row.scss +36 -3
- package/scss/_headerbar.scss +2 -1
- package/scss/_preferences_row.scss +9 -0
- package/scss/_reset.scss +1 -1
- package/scss/_row.scss +16 -0
- package/scss/_status_page.scss +1 -1
- package/scss/_toggle_group.scss +28 -0
- package/scss/_widget.scss +14 -3
- package/scss/_window.scss +13 -1
- package/src/adw-action-rows.spec.ts +255 -0
- package/src/adw-header-bar.spec.ts +18 -0
- package/src/adw-status-page.spec.ts +124 -0
- package/src/application.spec.ts +75 -0
- package/src/application.ts +92 -0
- package/src/attributes.ts +52 -0
- package/src/capabilities.mjs +3 -0
- package/src/construct-vectors.spec.ts +156 -1
- package/src/elements/adw-action-row.ts +31 -2
- package/src/elements/adw-alert-dialog.ts +4 -1
- package/src/elements/adw-application-window.ts +5 -0
- package/src/elements/adw-button-row.ts +7 -1
- package/src/elements/adw-combo-row.ts +26 -1
- package/src/elements/adw-entry-row.ts +22 -12
- package/src/elements/adw-expander-row.ts +34 -1
- package/src/elements/adw-header-bar.ts +18 -4
- package/src/elements/adw-overlay-split-view.ts +29 -6
- package/src/elements/adw-preferences-row.ts +5 -2
- package/src/elements/adw-sidebar.ts +33 -5
- package/src/elements/adw-spin-row.ts +37 -2
- package/src/elements/adw-split-button.ts +5 -1
- package/src/elements/adw-status-page.ts +10 -2
- package/src/elements/adw-switch-row.ts +25 -1
- package/src/elements/adw-toggle-group.ts +119 -10
- package/src/elements/adw-view-stack.ts +14 -0
- package/src/elements/adw-window.ts +9 -4
- 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 +76 -7
- package/src/elements/gtk-stack.ts +8 -0
- package/src/elements/gtk-switch.ts +8 -0
- package/src/elements/gtk-window.ts +9 -3
- 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/gtk-window.spec.ts +12 -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 +11 -1
- package/src/keyboard-operable.spec.ts +47 -11
- 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/row-line-clamp.ts +74 -0
- package/src/row-tooltip.spec.ts +313 -0
- package/src/row-tooltip.ts +52 -0
- package/src/shared-tree-builder.ts +230 -42
- package/src/shared-tree-mount.spec.ts +130 -0
- package/src/shared-trees.spec.ts +15 -0
- package/src/split-button.spec.ts +2 -0
- package/src/styles.generated.ts +1 -1
- package/src/test.browser.mts +18 -0
- package/src/widget-size.spec.ts +117 -0
|
@@ -81,6 +81,7 @@ import {
|
|
|
81
81
|
normalizeMenuModel,
|
|
82
82
|
parseMenuModel,
|
|
83
83
|
} from '@gjsify/adwaita-core';
|
|
84
|
+
import { activateElementMenuItem, menuActionsOfElement } from '../application.js';
|
|
84
85
|
import type { AdwMenuActions, AdwMenuInput, AdwMenuModel, AdwMenuPath } from '@gjsify/adwaita-core';
|
|
85
86
|
|
|
86
87
|
import { GtkPopover } from './gtk-popover.js';
|
|
@@ -187,8 +188,11 @@ export class GtkPopoverMenu extends GtkPopover {
|
|
|
187
188
|
// with `gtk_popover_menu_set_open_submenu (widget, NULL)` (gtkpopovermenu.c:573-579):
|
|
188
189
|
// a menu that reopens three levels deep is one the reader cannot get out of.
|
|
189
190
|
this.subscribe((open) => {
|
|
190
|
-
if (open)
|
|
191
|
-
|
|
191
|
+
if (open) {
|
|
192
|
+
this._menuView.setActions(this._actions ?? menuActionsOfElement(this, this._model));
|
|
193
|
+
this._menuView.render();
|
|
194
|
+
this._menuView.focusableRows[0]?.focus();
|
|
195
|
+
} else this._menuView.reset();
|
|
192
196
|
});
|
|
193
197
|
}
|
|
194
198
|
|
|
@@ -308,6 +312,7 @@ export class GtkPopoverMenu extends GtkPopover {
|
|
|
308
312
|
this.popdown();
|
|
309
313
|
this.anchor?.focus();
|
|
310
314
|
if (target === null) return;
|
|
315
|
+
activateElementMenuItem(this, target);
|
|
311
316
|
this.dispatchEvent(
|
|
312
317
|
new CustomEvent('menu-item-activated', {
|
|
313
318
|
bubbles: true,
|
|
@@ -86,12 +86,13 @@
|
|
|
86
86
|
// Copyright (c) The GTK Team. LGPLv2.1+.
|
|
87
87
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
88
88
|
|
|
89
|
+
import { GtkAdjustment } from '@gjsify/adwaita-core';
|
|
89
90
|
import type { AdwAdjustment } from '@gjsify/adwaita-core';
|
|
90
91
|
|
|
91
92
|
import { AdwScrollShading } from '../scroll-shading.js';
|
|
92
93
|
import { bindSlottedChildren } from '../slotted-children.js';
|
|
93
94
|
import type { GtkScrollType } from './gtk-scrollbar.js';
|
|
94
|
-
import { GtkViewport } from './gtk-viewport.js';
|
|
95
|
+
import type { GtkViewport } from './gtk-viewport.js';
|
|
95
96
|
|
|
96
97
|
/**
|
|
97
98
|
* `INDICATOR_FADE_OUT_DELAY` — how long an indicator waits, after the last scroll, before
|
|
@@ -112,6 +113,8 @@ export type GtkPositionType = 'left' | 'right' | 'top' | 'bottom';
|
|
|
112
113
|
export type GtkDirectionType = 'forward' | 'backward';
|
|
113
114
|
|
|
114
115
|
const POLICIES: readonly GtkPolicyType[] = ['always', 'never', 'automatic', 'external'];
|
|
116
|
+
/** The nicks in `Gtk.PolicyType`'s numeric order, which is not the order above. */
|
|
117
|
+
const POLICY_BY_VALUE: readonly GtkPolicyType[] = ['always', 'automatic', 'never', 'external'];
|
|
115
118
|
const CORNERS: readonly GtkCornerType[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
|
116
119
|
|
|
117
120
|
/** An unknown nick is the pspec's default, which is what GtkBuilder's rejection leaves behind. */
|
|
@@ -198,11 +201,16 @@ function resolveScrollKey(event: KeyboardEvent): { scroll: GtkScrollType; horizo
|
|
|
198
201
|
export class GtkScrolledWindow extends HTMLElement {
|
|
199
202
|
private _viewport!: GtkViewport;
|
|
200
203
|
private _bars: Record<'horizontal' | 'vertical', HTMLElement> | null = null;
|
|
204
|
+
/** The size clamps `_writeSize` last wrote, which are the only ones it may clear. */
|
|
205
|
+
private readonly _wroteSizes = new Set<string>();
|
|
201
206
|
private _shading: AdwScrollShading | null = null;
|
|
202
207
|
private _observer: ResizeObserver | null = null;
|
|
203
208
|
private _hoverable: MediaQueryList | null = null;
|
|
204
209
|
private _fadeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
205
210
|
private _built = false;
|
|
211
|
+
/** What an unbuilt window answers before it has a viewport to read. */
|
|
212
|
+
private readonly _standIn: { hadjustment?: AdwAdjustment; vadjustment?: AdwAdjustment } = {};
|
|
213
|
+
private readonly _adopted: { hadjustment?: AdwAdjustment; vadjustment?: AdwAdjustment } = {};
|
|
206
214
|
|
|
207
215
|
static get observedAttributes() {
|
|
208
216
|
return [
|
|
@@ -238,6 +246,58 @@ export class GtkScrolledWindow extends HTMLElement {
|
|
|
238
246
|
this.setAttribute('vscrollbar-policy', value);
|
|
239
247
|
}
|
|
240
248
|
|
|
249
|
+
/** `gtk_scrolled_window_get_policy`: `[hscrollbar, vscrollbar]` as `Gtk.PolicyType` constants. */
|
|
250
|
+
get_policy(): [number, number] {
|
|
251
|
+
return [POLICY_BY_VALUE.indexOf(this.hscrollbarPolicy), POLICY_BY_VALUE.indexOf(this.vscrollbarPolicy)];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** `gtk_scrolled_window_set_policy`; a value that is not a `Gtk.PolicyType` throws, as GJS's enum marshalling does. */
|
|
255
|
+
set_policy(hscrollbar_policy: number, vscrollbar_policy: number): void {
|
|
256
|
+
const nick = (value: number, name: string): GtkPolicyType => {
|
|
257
|
+
const found = Number.isInteger(value) ? POLICY_BY_VALUE[value] : undefined;
|
|
258
|
+
if (found === undefined) throw new TypeError(`${value} is not a valid value for enum argument ${name}`);
|
|
259
|
+
return found;
|
|
260
|
+
};
|
|
261
|
+
const h = nick(hscrollbar_policy, 'hscrollbar_policy');
|
|
262
|
+
const v = nick(vscrollbar_policy, 'vscrollbar_policy');
|
|
263
|
+
this.hscrollbarPolicy = h;
|
|
264
|
+
this.vscrollbarPolicy = v;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** `GtkScrollable:hadjustment` as the window reports it: the one it was given, else the viewport's. */
|
|
268
|
+
get hadjustment(): AdwAdjustment {
|
|
269
|
+
return (
|
|
270
|
+
this._adopted.hadjustment ??
|
|
271
|
+
(this._viewport as GtkViewport | undefined)?.hadjustment ??
|
|
272
|
+
(this._standIn.hadjustment ??= new GtkAdjustment())
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
get vadjustment(): AdwAdjustment {
|
|
277
|
+
return (
|
|
278
|
+
this._adopted.vadjustment ??
|
|
279
|
+
(this._viewport as GtkViewport | undefined)?.vadjustment ??
|
|
280
|
+
(this._standIn.vadjustment ??= new GtkAdjustment())
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
get_hadjustment(): AdwAdjustment {
|
|
285
|
+
return this.hadjustment;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
get_vadjustment(): AdwAdjustment {
|
|
289
|
+
return this.vadjustment;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Adopts `adjustment` as what `get_hadjustment` answers; the scroll position keeps following the viewport. */
|
|
293
|
+
set_hadjustment(adjustment: AdwAdjustment): void {
|
|
294
|
+
this._adopted.hadjustment = adjustment;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
set_vadjustment(adjustment: AdwAdjustment): void {
|
|
298
|
+
this._adopted.vadjustment = adjustment;
|
|
299
|
+
}
|
|
300
|
+
|
|
241
301
|
/** `GtkScrolledWindow:window-placement` — where the contents sit against the bars. */
|
|
242
302
|
get windowPlacement(): GtkCornerType {
|
|
243
303
|
return normalizeCorner(this.getAttribute('window-placement'));
|
|
@@ -447,10 +507,10 @@ export class GtkScrolledWindow extends HTMLElement {
|
|
|
447
507
|
this.classList.toggle('overlay-scrolling', indicators);
|
|
448
508
|
// `gtk_scrolled_window_measure`'s four clamps, as the four CSS size requests they
|
|
449
509
|
// are: an unwritten one (-1) sets nothing at all.
|
|
450
|
-
this.
|
|
451
|
-
this.
|
|
452
|
-
this.
|
|
453
|
-
this.
|
|
510
|
+
this._writeSize('min-width', 'min-content-width');
|
|
511
|
+
this._writeSize('min-height', 'min-content-height');
|
|
512
|
+
this._writeSize('max-width', 'max-content-width');
|
|
513
|
+
this._writeSize('max-height', 'max-content-height');
|
|
454
514
|
for (const axis of AXES) {
|
|
455
515
|
const bar = bars[axis.bar];
|
|
456
516
|
const policy = normalizePolicy(this.getAttribute(axis.policy));
|
|
@@ -481,9 +541,18 @@ export class GtkScrolledWindow extends HTMLElement {
|
|
|
481
541
|
if (!indicators) this._clearFade();
|
|
482
542
|
}
|
|
483
543
|
|
|
484
|
-
|
|
544
|
+
/**
|
|
545
|
+
* One of the four clamps as inline style. An unwritten clamp clears only what THIS element
|
|
546
|
+
* wrote: `min-width` / `min-height` are also where a tree's `width-request` /
|
|
547
|
+
* `height-request` land (`GTK_WIDGET_SIZE_CSS`), and assigning '' unconditionally erased it.
|
|
548
|
+
* Where both are set the content clamp wins, which is the one the widget measures itself.
|
|
549
|
+
*/
|
|
550
|
+
private _writeSize(property: string, attribute: string): void {
|
|
485
551
|
const size = sizeRequest(this.getAttribute(attribute));
|
|
486
|
-
|
|
552
|
+
if (size !== null) this.style.setProperty(property, `${size}px`);
|
|
553
|
+
else if (this._wroteSizes.has(property)) this.style.removeProperty(property);
|
|
554
|
+
if (size !== null) this._wroteSizes.add(property);
|
|
555
|
+
else this._wroteSizes.delete(property);
|
|
487
556
|
}
|
|
488
557
|
|
|
489
558
|
/** The viewport's adjustment for one axis, which is where GTK reads the scrollbar's. */
|
|
@@ -91,6 +91,8 @@
|
|
|
91
91
|
// Copyright (c) The GTK Team. LGPLv2.1+.
|
|
92
92
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
93
93
|
|
|
94
|
+
import type { DispatchedSignals } from '../signals.js';
|
|
95
|
+
|
|
94
96
|
/** `GtkStackTransitionType`, spelled as the GIR's enum nicks (gtkstack.c:95-121). */
|
|
95
97
|
export type StackTransitionType =
|
|
96
98
|
| 'none'
|
|
@@ -224,6 +226,12 @@ export interface GtkStackPageInfo {
|
|
|
224
226
|
const PAGE_ATTRIBUTES = ['name', 'title', 'icon-name', 'use-underline', 'needs-attention'];
|
|
225
227
|
|
|
226
228
|
export class GtkStack extends HTMLElement {
|
|
229
|
+
/** The GTK signals this element dispatches, each with the DOM event it arrives as (ADR 0093). */
|
|
230
|
+
static readonly signals: DispatchedSignals = {
|
|
231
|
+
'notify::visible-child': 'notify::visible-child',
|
|
232
|
+
'notify::visible-child-name': 'notify::visible-child-name',
|
|
233
|
+
};
|
|
234
|
+
|
|
227
235
|
/** The child that is visible, or `null` — C's `priv->visible_child`. */
|
|
228
236
|
private _visible: HTMLElement | null = null;
|
|
229
237
|
/** C's `priv->last_visible_child`: the page being animated away. */
|
|
@@ -39,7 +39,15 @@
|
|
|
39
39
|
// Copyright (c) 2025 csm (adwaita-web). MIT License.
|
|
40
40
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
41
41
|
|
|
42
|
+
import type { DispatchedSignals } from '../signals.js';
|
|
43
|
+
|
|
42
44
|
export class GtkSwitch extends HTMLElement {
|
|
45
|
+
/** The GTK signals this element dispatches, each with the DOM event it arrives as (ADR 0093). */
|
|
46
|
+
static readonly signals: DispatchedSignals = {
|
|
47
|
+
'notify::active': 'notify::active',
|
|
48
|
+
'notify::state': 'notify::state',
|
|
49
|
+
};
|
|
50
|
+
|
|
43
51
|
private _input!: HTMLInputElement;
|
|
44
52
|
private _slider!: HTMLSpanElement;
|
|
45
53
|
private _initialized = false;
|
|
@@ -106,9 +106,15 @@ export class GtkWindow extends HTMLElement {
|
|
|
106
106
|
];
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/**
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
/**
|
|
110
|
+
* `Gtk.Window:title` — the window's title.
|
|
111
|
+
*
|
|
112
|
+
* The getter answers `''` for no title, not `null`: a `string | null` getter is not assignable to
|
|
113
|
+
* `HTMLElement.title`, and the emitted `.d.ts` then fails every consumer that does not set
|
|
114
|
+
* `skipLibCheck`. The setter still takes `null` to clear it, as GTK does.
|
|
115
|
+
*/
|
|
116
|
+
get title(): string {
|
|
117
|
+
return this.getAttribute('title') ?? '';
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
set title(value: string | null) {
|
package/src/glib.spec.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The GLib timers door of adwaita-web, held to the shared vectors (real GJS is their oracle, in
|
|
2
|
+
// adwaita-core's `glib-timers.gjs.spec.ts`). The door is the namespace module itself, so a member
|
|
3
|
+
// that stops being exported fails here.
|
|
4
|
+
|
|
5
|
+
import { GLIB_TIMER_VECTORS, driveGLibTimerVectors, type GLibTimersLike } from '@gjsify/adwaita-core/conformance';
|
|
6
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
7
|
+
|
|
8
|
+
import * as GLib from './namespace/glib.js';
|
|
9
|
+
|
|
10
|
+
export const GLibTest = async () => {
|
|
11
|
+
await driveGLibTimerVectors(
|
|
12
|
+
{
|
|
13
|
+
name: 'adwaita-web',
|
|
14
|
+
isOracle: false,
|
|
15
|
+
GLib: GLib as unknown as GLibTimersLike,
|
|
16
|
+
settle: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
17
|
+
quiet: async (body) => {
|
|
18
|
+
const original = console.error;
|
|
19
|
+
console.error = () => {};
|
|
20
|
+
try {
|
|
21
|
+
await body();
|
|
22
|
+
} finally {
|
|
23
|
+
console.error = original;
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{ describe, it, expect },
|
|
28
|
+
);
|
|
29
|
+
await describe('adwaita-web: GLib door', async () => {
|
|
30
|
+
await it('has a vector row for every vector', () => {
|
|
31
|
+
expect(GLIB_TIMER_VECTORS.length > 0).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// ADR 0096: the web door of the GObject subset, against the shared vectors and end to end.
|
|
2
|
+
import { parseBlueprint, projectToSharedNode } from '@gjsify/blueprint';
|
|
3
|
+
import { GOBJECT_VECTORS, driveGObjectVectors, type GObjectSubject } from '@gjsify/adwaita-core/conformance';
|
|
4
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
5
|
+
|
|
6
|
+
import { Adw, GObject } from './index.js';
|
|
7
|
+
import { tagForTypeName } from './gobject-door.js';
|
|
8
|
+
import type { SharedTreeNode } from '@gjsify/adwaita-core/conformance';
|
|
9
|
+
import { bindProperties } from '@gjsify/adwaita-core';
|
|
10
|
+
|
|
11
|
+
function tree(body: string): SharedTreeNode {
|
|
12
|
+
const { node, lost } = projectToSharedNode(
|
|
13
|
+
parseBlueprint(`using Gtk 4.0;\nusing Adw 1;\n\n${body}\n`, 'gobject-door.spec.blp'),
|
|
14
|
+
);
|
|
15
|
+
if (lost.length > 0) throw new Error(`the projection dropped ${lost.map((loss) => loss.kind).join(', ')}`);
|
|
16
|
+
return node as SharedTreeNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const subject: GObjectSubject = {
|
|
20
|
+
name: 'adwaita-web',
|
|
21
|
+
isOracle: false,
|
|
22
|
+
GObject: GObject as unknown as GObjectSubject['GObject'],
|
|
23
|
+
Widget: Adw.Bin as unknown as GObjectSubject['Widget'],
|
|
24
|
+
template: (source) => source.tree,
|
|
25
|
+
bind: (source, sourceProperty, target, targetProperty, flags) => {
|
|
26
|
+
bindProperties(source, sourceProperty, target, targetProperty, flags);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const Template = tree(`template $GoWebHexdump : Adw.Bin {
|
|
31
|
+
child: Gtk.Box {
|
|
32
|
+
Gtk.Switch sw { active: bind template.enabled bidirectional; }
|
|
33
|
+
Gtk.Button btn { clicked => $onClicked(); }
|
|
34
|
+
};
|
|
35
|
+
}`);
|
|
36
|
+
|
|
37
|
+
class GoWebHexdump extends Adw.Bin {
|
|
38
|
+
declare _sw: HTMLElement & { active: boolean };
|
|
39
|
+
declare _btn: HTMLElement;
|
|
40
|
+
declare enabled: boolean;
|
|
41
|
+
clicks = 0;
|
|
42
|
+
seenInConstructor = false;
|
|
43
|
+
constructor(params?: Record<string, unknown>) {
|
|
44
|
+
// @ts-expect-error the element's own constructor takes none; the registered class's takes the params
|
|
45
|
+
super(params);
|
|
46
|
+
this.seenInConstructor = this._btn !== undefined;
|
|
47
|
+
(this._btn as unknown as { connect(s: string, h: () => void): number }).connect('clicked', () => {
|
|
48
|
+
this.clicks += 10;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
onClicked(): void {
|
|
52
|
+
this.clicks++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
GObject.registerClass(
|
|
56
|
+
{
|
|
57
|
+
GTypeName: 'GoWebHexdump',
|
|
58
|
+
Template,
|
|
59
|
+
InternalChildren: ['sw', 'btn'],
|
|
60
|
+
Properties: { enabled: GObject.ParamSpec.boolean('enabled', '', '', GObject.ParamFlags.READWRITE, false) },
|
|
61
|
+
},
|
|
62
|
+
GoWebHexdump as never,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
export const GObjectDoorTest = async () => {
|
|
66
|
+
await driveGObjectVectors(subject, { describe, it, expect }, GOBJECT_VECTORS);
|
|
67
|
+
await describe('adwaita-web: GObject.registerClass end to end (ADR 0096)', async () => {
|
|
68
|
+
await it('derives the tag from the GTypeName', () => {
|
|
69
|
+
expect(tagForTypeName('Hexdump')).toBe('gjsify-hexdump');
|
|
70
|
+
expect(customElements.get('gjsify-go-web-hexdump')).toBe(GoWebHexdump);
|
|
71
|
+
});
|
|
72
|
+
await it('refuses a second class under the same tag', () => {
|
|
73
|
+
expect(() =>
|
|
74
|
+
GObject.registerClass({ GTypeName: 'GoWebHexdump' }, class extends Adw.Bin {} as never),
|
|
75
|
+
).toThrow('already defines');
|
|
76
|
+
});
|
|
77
|
+
await it('MEASURED: a bare element dispatches no notify before its first connect, a connected-through-the-door one does', () => {
|
|
78
|
+
const bare = document.createElement('gtk-switch') as HTMLElement & { active: boolean };
|
|
79
|
+
let heard = 0;
|
|
80
|
+
bare.addEventListener('notify::active', () => heard++);
|
|
81
|
+
bare.active = true;
|
|
82
|
+
expect(heard).toBe(0);
|
|
83
|
+
const el = new GoWebHexdump();
|
|
84
|
+
let viaDoor = 0;
|
|
85
|
+
(el._sw as unknown as { connect(s: string, h: () => void): number }).connect(
|
|
86
|
+
'notify::active',
|
|
87
|
+
() => viaDoor++,
|
|
88
|
+
);
|
|
89
|
+
el._sw.active = true;
|
|
90
|
+
expect(viaDoor).toBe(1);
|
|
91
|
+
});
|
|
92
|
+
for (const how of ['new', 'createElement'] as const) {
|
|
93
|
+
await it(`built with ${how}: this._x after super, children after connect, bind and handler work`, () => {
|
|
94
|
+
const el = (
|
|
95
|
+
how === 'new' ? new GoWebHexdump() : document.createElement('gjsify-go-web-hexdump')
|
|
96
|
+
) as GoWebHexdump;
|
|
97
|
+
expect(el._sw !== undefined).toBe(true);
|
|
98
|
+
expect(el.children.length).toBe(0);
|
|
99
|
+
expect(el.seenInConstructor).toBe(true);
|
|
100
|
+
// notify on a not-yet-connected element reaches the binding (ADR 0096 UNVERIFIED point)
|
|
101
|
+
el._sw.active = true;
|
|
102
|
+
expect(el.enabled).toBe(true);
|
|
103
|
+
el.enabled = false;
|
|
104
|
+
expect(el._sw.active).toBe(false);
|
|
105
|
+
document.body.append(el);
|
|
106
|
+
try {
|
|
107
|
+
expect(el.querySelector('gtk-switch') !== null).toBe(true);
|
|
108
|
+
expect(el.contains(el._btn)).toBe(true);
|
|
109
|
+
el._btn.dispatchEvent(new Event('click'));
|
|
110
|
+
expect(el.clicks).toBe(11);
|
|
111
|
+
el._sw.active = true;
|
|
112
|
+
expect(el.enabled).toBe(true);
|
|
113
|
+
} finally {
|
|
114
|
+
el.remove();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// The web door of the GObject subset (ADR 0096 § 1): how custom elements dispatch and listen for a
|
|
2
|
+
// named event, build a template, and take the built children.
|
|
3
|
+
//
|
|
4
|
+
// A registered class is a custom element. `registerClass` defines it under a tag derived from
|
|
5
|
+
// its `GTypeName`, and the instance is built in the order of GJS's `Gtk.Widget._init` by a class
|
|
6
|
+
// inserted between the class and its widget base, because the base is an element that knows
|
|
7
|
+
// nothing of GObject and `super(params)` is the only place a subclass hands control down.
|
|
8
|
+
//
|
|
9
|
+
// CHILDREN ARE ATTACHED AT THE FIRST `connectedCallback`, never in the constructor: the custom
|
|
10
|
+
// element rules forbid a constructor to give the element children or attributes when
|
|
11
|
+
// `createElement` or the parser creates it. They are created, and wired to their handlers and
|
|
12
|
+
// binds, from the constructor on, so `this._x.connect(…)` in a subclass constructor works.
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
constructRegistered,
|
|
16
|
+
type BuiltTemplate,
|
|
17
|
+
type ClassLike,
|
|
18
|
+
type GObjectDoor,
|
|
19
|
+
type RegisteredClass,
|
|
20
|
+
type TemplateScope,
|
|
21
|
+
} from '@gjsify/adwaita-core';
|
|
22
|
+
import { propertyOf } from '@gjsify/adwaita-core/tags';
|
|
23
|
+
|
|
24
|
+
import { dispatchElement, listenElement } from './gobject-elements.js';
|
|
25
|
+
import { buildTemplateTree, type BuiltTemplateTree } from './shared-tree-builder.js';
|
|
26
|
+
import { registerTemplateClass } from './template-classes.js';
|
|
27
|
+
|
|
28
|
+
type Ctor = new (...args: never[]) => HTMLElement;
|
|
29
|
+
|
|
30
|
+
const PENDING = new WeakMap<HTMLElement, BuiltTemplateTree[]>();
|
|
31
|
+
const CONSTRUCTED = new WeakSet<HTMLElement>();
|
|
32
|
+
const DEFINED_FOR = new Map<string, string>();
|
|
33
|
+
|
|
34
|
+
/** `Hexdump` → `gjsify-hexdump`, `GoVecNotify` → `gjsify-go-vec-notify`. */
|
|
35
|
+
export function tagForTypeName(typeName: string): string {
|
|
36
|
+
const kebab = typeName
|
|
37
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
38
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
|
|
39
|
+
.toLowerCase()
|
|
40
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
41
|
+
.replace(/^-+|-+$/g, '');
|
|
42
|
+
return `gjsify-${kebab}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isElementClass(klass: ClassLike): boolean {
|
|
46
|
+
return (klass as { prototype: object }).prototype instanceof HTMLElement;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The signals a registered element declares: its widget base's, one `notify::` per property, its own. */
|
|
50
|
+
function declareSignals(klass: ClassLike, info: RegisteredClass): void {
|
|
51
|
+
const inherited = (klass as unknown as { signals?: Record<string, string> }).signals ?? {};
|
|
52
|
+
const signals: Record<string, string> = { ...inherited };
|
|
53
|
+
for (const spec of info.properties) signals[`notify::${spec.name}`] = `notify::${spec.name}`;
|
|
54
|
+
for (const name of Object.keys(info.signals)) signals[name] = name;
|
|
55
|
+
Object.defineProperty(klass, 'signals', { value: signals, configurable: true, writable: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function attachPending(host: HTMLElement): void {
|
|
59
|
+
const pending = PENDING.get(host);
|
|
60
|
+
if (pending === undefined) return;
|
|
61
|
+
PENDING.delete(host);
|
|
62
|
+
for (const built of pending) {
|
|
63
|
+
built.finish();
|
|
64
|
+
host.append(...built.children);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const webDoor: GObjectDoor = {
|
|
69
|
+
name: 'adwaita-web',
|
|
70
|
+
|
|
71
|
+
dispatch(target, event, args) {
|
|
72
|
+
if (!(target instanceof HTMLElement)) throw new Error(`No signal '${event}' on object '${String(target)}'`);
|
|
73
|
+
dispatchElement(target, event, args);
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
listen(target, event, handler) {
|
|
77
|
+
if (!(target instanceof HTMLElement)) throw new Error(`No signal '${event}' on object '${String(target)}'`);
|
|
78
|
+
return listenElement(target, event, handler as never);
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
createFromTree(tree, scope: TemplateScope, host): BuiltTemplate {
|
|
82
|
+
return buildTemplateTree(tree, scope, host as HTMLElement);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
attach(host, built) {
|
|
86
|
+
const pending = PENDING.get(host as HTMLElement) ?? [];
|
|
87
|
+
pending.push(built as BuiltTemplateTree);
|
|
88
|
+
PENDING.set(host as HTMLElement, pending);
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
register(klass, info) {
|
|
92
|
+
// A class with no widget model beneath it is built by `GObject.Object`, which is the core's.
|
|
93
|
+
if (!isElementClass(klass)) return;
|
|
94
|
+
const tag = tagForTypeName(info.typeName);
|
|
95
|
+
const known = DEFINED_FOR.get(tag) ?? (customElements.get(tag) === undefined ? undefined : 'another class');
|
|
96
|
+
if (known !== undefined) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`GObject.registerClass: ${info.typeName} would be <${tag}>, which ${known} already defines. ` +
|
|
99
|
+
'Give the class a distinct GTypeName.',
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const Parent = Object.getPrototypeOf(klass) as Ctor;
|
|
104
|
+
class Constructing extends Parent {
|
|
105
|
+
constructor(params?: Record<string, unknown>) {
|
|
106
|
+
super();
|
|
107
|
+
// A registered base ran the whole chain already, for the most derived class.
|
|
108
|
+
if (CONSTRUCTED.has(this)) return;
|
|
109
|
+
CONSTRUCTED.add(this);
|
|
110
|
+
const rest = constructRegistered(this, params);
|
|
111
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
112
|
+
const member = propertyOf(key);
|
|
113
|
+
if (!(member in this)) throw new Error(`No property ${key} on <${tag}>`);
|
|
114
|
+
(this as unknown as Record<string, unknown>)[member] = value;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
Object.setPrototypeOf(klass, Constructing);
|
|
119
|
+
Object.setPrototypeOf((klass as { prototype: object }).prototype, Constructing.prototype);
|
|
120
|
+
|
|
121
|
+
const proto = (klass as { prototype: Record<string, unknown> }).prototype;
|
|
122
|
+
const inherited = proto.connectedCallback as (() => void) | undefined;
|
|
123
|
+
Object.defineProperty(proto, 'connectedCallback', {
|
|
124
|
+
configurable: true,
|
|
125
|
+
writable: true,
|
|
126
|
+
value(this: HTMLElement) {
|
|
127
|
+
attachPending(this);
|
|
128
|
+
inherited?.call(this);
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
declareSignals(klass, info);
|
|
133
|
+
customElements.define(tag, klass as unknown as CustomElementConstructor);
|
|
134
|
+
DEFINED_FOR.set(tag, info.typeName);
|
|
135
|
+
registerTemplateClass(info.typeName, tag);
|
|
136
|
+
},
|
|
137
|
+
};
|