@gjsify/adwaita-web 0.56.0 → 0.58.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/gio.spec.d.ts +1 -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 +78 -0
- package/lib/types/gtksource/gtk-source-view.spec.d.ts +2 -0
- package/lib/types/gtksource/web-editor-driver.d.ts +53 -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 +2 -0
- package/lib/types/namespace/gio.d.ts +2 -0
- package/lib/types/namespace/glib.d.ts +15 -0
- package/lib/types/namespace/gobject.d.ts +28 -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/style-manager.d.ts +5 -0
- package/lib/types/style-manager.spec.d.ts +1 -0
- 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/gio.spec.ts +23 -0
- package/src/glib.spec.ts +49 -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 +141 -0
- package/src/gtksource/gtk-source-view.ts +386 -0
- package/src/gtksource/web-editor-driver.ts +346 -0
- package/src/index.ts +10 -0
- package/src/menu-actions.spec.ts +47 -0
- package/src/namespace/adw.ts +2 -0
- package/src/namespace/gio.ts +3 -1
- package/src/namespace/glib.ts +37 -0
- package/src/namespace/gobject.ts +38 -0
- package/src/namespace/gtk-enums.ts +15 -0
- package/src/namespace/gtk.ts +2 -0
- package/src/namespace/gtksource.ts +20 -0
- package/src/shared-tree-builder.ts +142 -7
- package/src/split-button.spec.ts +2 -0
- package/src/style-manager.spec.ts +36 -0
- package/src/style-manager.ts +27 -0
- package/src/styles.generated.ts +1 -1
- package/src/test.browser.mts +14 -0
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
|
|
23
23
|
import { ENTRY_ROW_MAX_LENGTH_LIMIT, clampEntryText, entryTextLength } from '@gjsify/adwaita-core';
|
|
24
24
|
|
|
25
|
+
import type { DispatchedSignals } from '../signals.js';
|
|
26
|
+
|
|
25
27
|
export class GtkEntry extends HTMLElement {
|
|
26
28
|
/**
|
|
27
29
|
* `protected`, not `private`: `GtkPasswordEntry` and `GtkSearchEntry` subclass this
|
|
@@ -32,9 +34,18 @@ export class GtkEntry extends HTMLElement {
|
|
|
32
34
|
protected _input!: HTMLInputElement;
|
|
33
35
|
private _initialized = false;
|
|
34
36
|
private _maxLength = 0;
|
|
37
|
+
/** The text last announced, so `changed` fires on a real change only — as `gtk_editable_changed` does. */
|
|
38
|
+
private _announced = '';
|
|
39
|
+
|
|
40
|
+
/** The GTK signals this element dispatches, each with the DOM event it arrives as (ADR 0093). */
|
|
41
|
+
static readonly signals: DispatchedSignals = {
|
|
42
|
+
changed: 'changed',
|
|
43
|
+
'notify::text': 'notify::text',
|
|
44
|
+
activate: 'activate',
|
|
45
|
+
};
|
|
35
46
|
|
|
36
47
|
static get observedAttributes() {
|
|
37
|
-
return ['value', 'placeholder-text', 'placeholder', 'width-request', 'type', 'disabled', 'maxlength'];
|
|
48
|
+
return ['value', 'text', 'placeholder-text', 'placeholder', 'width-request', 'type', 'disabled', 'maxlength'];
|
|
38
49
|
}
|
|
39
50
|
|
|
40
51
|
get value(): string {
|
|
@@ -43,8 +54,28 @@ export class GtkEntry extends HTMLElement {
|
|
|
43
54
|
|
|
44
55
|
set value(v: string) {
|
|
45
56
|
const clamped = clampEntryText(v ?? '', this._maxLength);
|
|
46
|
-
if (this._input)
|
|
47
|
-
|
|
57
|
+
if (this._input) {
|
|
58
|
+
this._input.value = clamped;
|
|
59
|
+
this._announceText();
|
|
60
|
+
} else this.setAttribute('value', clamped);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `Gtk.Editable:text`, the GObject name for {@link value}. */
|
|
64
|
+
get text(): string {
|
|
65
|
+
return this.value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
set text(v: string) {
|
|
69
|
+
this.value = v;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** `changed` then `notify::text`, once per real change of the text, typed or set from code. */
|
|
73
|
+
private _announceText(): void {
|
|
74
|
+
const text = this._input.value;
|
|
75
|
+
if (text === this._announced) return;
|
|
76
|
+
this._announced = text;
|
|
77
|
+
this.dispatchEvent(new CustomEvent('changed', { bubbles: true }));
|
|
78
|
+
this.dispatchEvent(new CustomEvent('notify::text', { bubbles: true, detail: { text } }));
|
|
48
79
|
}
|
|
49
80
|
|
|
50
81
|
/** `Gtk.Entry:max-length` — 0 means unlimited. Counted in CODE POINTS. */
|
|
@@ -56,7 +87,10 @@ export class GtkEntry extends HTMLElement {
|
|
|
56
87
|
this._maxLength = Number.isFinite(value)
|
|
57
88
|
? Math.min(ENTRY_ROW_MAX_LENGTH_LIMIT, Math.max(0, Math.trunc(value)))
|
|
58
89
|
: 0;
|
|
59
|
-
if (this._input)
|
|
90
|
+
if (this._input) {
|
|
91
|
+
this._input.value = clampEntryText(this._input.value, this._maxLength);
|
|
92
|
+
this._announceText();
|
|
93
|
+
}
|
|
60
94
|
}
|
|
61
95
|
|
|
62
96
|
/** `Gtk.Entry:text-length` — code points, not UTF-16 units. */
|
|
@@ -92,7 +126,7 @@ export class GtkEntry extends HTMLElement {
|
|
|
92
126
|
const input = document.createElement('input');
|
|
93
127
|
input.className = 'adw-entry';
|
|
94
128
|
input.type = this.getAttribute('type') || 'text';
|
|
95
|
-
input.value = clampEntryText(this.getAttribute('value') ?? '', this._maxLength);
|
|
129
|
+
input.value = clampEntryText(this.getAttribute('value') ?? this.getAttribute('text') ?? '', this._maxLength);
|
|
96
130
|
input.placeholder = this.placeholderAttribute();
|
|
97
131
|
this.applyWidthRequest();
|
|
98
132
|
input.disabled = this.hasAttribute('disabled');
|
|
@@ -101,6 +135,7 @@ export class GtkEntry extends HTMLElement {
|
|
|
101
135
|
input.addEventListener('input', () => {
|
|
102
136
|
const clamped = clampEntryText(input.value, this._maxLength);
|
|
103
137
|
if (clamped !== input.value) input.value = clamped;
|
|
138
|
+
this._announceText();
|
|
104
139
|
});
|
|
105
140
|
input.addEventListener('keydown', (e) => {
|
|
106
141
|
if (e.key === 'Enter') {
|
|
@@ -109,6 +144,7 @@ export class GtkEntry extends HTMLElement {
|
|
|
109
144
|
});
|
|
110
145
|
|
|
111
146
|
this._input = input;
|
|
147
|
+
this._announced = input.value;
|
|
112
148
|
this.replaceChildren(input);
|
|
113
149
|
}
|
|
114
150
|
|
|
@@ -129,8 +165,10 @@ export class GtkEntry extends HTMLElement {
|
|
|
129
165
|
return;
|
|
130
166
|
}
|
|
131
167
|
if (!this._input) return;
|
|
132
|
-
if (name === 'value'
|
|
133
|
-
|
|
168
|
+
if (name === 'value' || name === 'text') {
|
|
169
|
+
this._input.value = clampEntryText(value ?? '', this._maxLength);
|
|
170
|
+
this._announceText();
|
|
171
|
+
} else if (name === 'placeholder-text' || name === 'placeholder')
|
|
134
172
|
this._input.placeholder = this.placeholderAttribute();
|
|
135
173
|
else if (name === 'type') this._input.type = value || 'text';
|
|
136
174
|
else if (name === 'disabled') this._input.disabled = value !== null;
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
normalizeMenuModel,
|
|
35
35
|
parseMenuModel,
|
|
36
36
|
} from '@gjsify/adwaita-core';
|
|
37
|
+
import { activateElementMenuItem, menuActionsOfElement } from '../application.js';
|
|
37
38
|
import type { AdwMenuActions, AdwMenuInput, AdwMenuModel, SplitButtonDirection } from '@gjsify/adwaita-core';
|
|
38
39
|
|
|
39
40
|
import { PopoverMenuView } from './popover-menu.js';
|
|
@@ -197,6 +198,7 @@ export class GtkMenuButton extends HTMLElement {
|
|
|
197
198
|
if (item === null) return;
|
|
198
199
|
this._popoverEl.popdown();
|
|
199
200
|
this._buttonEl.focus();
|
|
201
|
+
activateElementMenuItem(this, item);
|
|
200
202
|
this.dispatchEvent(
|
|
201
203
|
new CustomEvent('menu-item-activated', {
|
|
202
204
|
bubbles: true,
|
|
@@ -246,7 +248,10 @@ export class GtkMenuButton extends HTMLElement {
|
|
|
246
248
|
private _onPopoverToggled(open: boolean): void {
|
|
247
249
|
this.classList.toggle('active', open);
|
|
248
250
|
this._buttonEl.setAttribute('aria-expanded', String(open));
|
|
249
|
-
if (open)
|
|
251
|
+
if (open) {
|
|
252
|
+
this._refreshActions();
|
|
253
|
+
this._menuView.focusableRows[0]?.focus();
|
|
254
|
+
}
|
|
250
255
|
// A dismissal returns the popup to its top page: a menu that reopens inside a
|
|
251
256
|
// submenu is one the reader cannot get out of.
|
|
252
257
|
else this._menuView.reset();
|
|
@@ -270,6 +275,12 @@ export class GtkMenuButton extends HTMLElement {
|
|
|
270
275
|
this._renderMenu();
|
|
271
276
|
}
|
|
272
277
|
|
|
278
|
+
/** The registry's answer, unless the application wrote `actions` itself (ADR 0097 § 2). */
|
|
279
|
+
private _refreshActions(): void {
|
|
280
|
+
this._menuView.setActions(this._actions ?? menuActionsOfElement(this, this._model));
|
|
281
|
+
this._menuView.render();
|
|
282
|
+
}
|
|
283
|
+
|
|
273
284
|
private _renderMenu(): void {
|
|
274
285
|
this._menuView.setMenuModel(this._model);
|
|
275
286
|
this._menuView.setTitle(this.getAttribute('menu-title') ?? this._menuTitle);
|
|
@@ -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,6 +86,7 @@
|
|
|
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';
|
|
@@ -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. */
|
|
@@ -205,6 +208,9 @@ export class GtkScrolledWindow extends HTMLElement {
|
|
|
205
208
|
private _hoverable: MediaQueryList | null = null;
|
|
206
209
|
private _fadeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
207
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 } = {};
|
|
208
214
|
|
|
209
215
|
static get observedAttributes() {
|
|
210
216
|
return [
|
|
@@ -240,6 +246,58 @@ export class GtkScrolledWindow extends HTMLElement {
|
|
|
240
246
|
this.setAttribute('vscrollbar-policy', value);
|
|
241
247
|
}
|
|
242
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
|
+
|
|
243
301
|
/** `GtkScrolledWindow:window-placement` — where the contents sit against the bars. */
|
|
244
302
|
get windowPlacement(): GtkCornerType {
|
|
245
303
|
return normalizeCorner(this.getAttribute('window-placement'));
|
|
@@ -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;
|
package/src/gio.spec.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The `Gio.SimpleActionGroup` door of adwaita-web, held to the shared vectors (real GJS is their
|
|
2
|
+
// oracle, in adwaita-core's `simple-action-group.gjs.spec.ts`). The door is the namespace module itself.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
SIMPLE_ACTION_GROUP_VECTORS,
|
|
6
|
+
driveSimpleActionGroupVectors,
|
|
7
|
+
type GioActionsLike,
|
|
8
|
+
} from '@gjsify/adwaita-core/conformance';
|
|
9
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
10
|
+
|
|
11
|
+
import * as Gio from './namespace/gio.js';
|
|
12
|
+
|
|
13
|
+
export const GioActionsTest = async () => {
|
|
14
|
+
await driveSimpleActionGroupVectors(
|
|
15
|
+
{ name: 'adwaita-web', isOracle: false, Gio: Gio as unknown as GioActionsLike },
|
|
16
|
+
{ describe, it, expect },
|
|
17
|
+
);
|
|
18
|
+
await describe('adwaita-web: Gio door', async () => {
|
|
19
|
+
await it('has a vector row for every vector', () => {
|
|
20
|
+
expect(SIMPLE_ACTION_GROUP_VECTORS.length > 0).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
};
|
package/src/glib.spec.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
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 {
|
|
6
|
+
GLIB_PATH_VECTORS,
|
|
7
|
+
GLIB_TIMER_VECTORS,
|
|
8
|
+
driveGLibPathVectors,
|
|
9
|
+
driveGLibTimerVectors,
|
|
10
|
+
type GLibPathsLike,
|
|
11
|
+
type GLibTimersLike,
|
|
12
|
+
} from '@gjsify/adwaita-core/conformance';
|
|
13
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
14
|
+
|
|
15
|
+
import * as GLib from './namespace/glib.js';
|
|
16
|
+
|
|
17
|
+
export const GLibTest = async () => {
|
|
18
|
+
await driveGLibTimerVectors(
|
|
19
|
+
{
|
|
20
|
+
name: 'adwaita-web',
|
|
21
|
+
isOracle: false,
|
|
22
|
+
GLib: GLib as unknown as GLibTimersLike,
|
|
23
|
+
settle: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
24
|
+
quiet: async (body) => {
|
|
25
|
+
const original = console.error;
|
|
26
|
+
console.error = () => {};
|
|
27
|
+
try {
|
|
28
|
+
await body();
|
|
29
|
+
} finally {
|
|
30
|
+
console.error = original;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{ describe, it, expect },
|
|
35
|
+
);
|
|
36
|
+
await driveGLibPathVectors(
|
|
37
|
+
{ name: 'adwaita-web', isOracle: false, GLib: GLib as unknown as GLibPathsLike },
|
|
38
|
+
{ describe, it, expect },
|
|
39
|
+
);
|
|
40
|
+
await describe('adwaita-web: GLib door', async () => {
|
|
41
|
+
await it('answers the host-bound path questions with the root and no data dirs', () => {
|
|
42
|
+
expect(GLib.get_current_dir()).toBe('/');
|
|
43
|
+
expect(GLib.get_system_data_dirs().length).toBe(0);
|
|
44
|
+
});
|
|
45
|
+
await it('has a vector row for every vector', () => {
|
|
46
|
+
expect(GLIB_TIMER_VECTORS.length > 0 && GLIB_PATH_VECTORS.length > 0).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
};
|
|
@@ -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
|
+
};
|