@gjsify/adwaita-web 0.64.1-next.247 → 0.64.2-next.252
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 +40 -15
- package/dist/adwaita-web.css +39 -9
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/application.d.ts +1 -1
- package/lib/types/child-slot.spec.d.ts +1 -0
- package/lib/types/container-fill.spec.d.ts +1 -0
- package/lib/types/elements/adw-alert-dialog.d.ts +77 -17
- package/lib/types/elements/adw-toast-overlay.d.ts +8 -29
- package/lib/types/elements/adw-window-features.d.ts +8 -0
- package/lib/types/elements/gtk-application-window.d.ts +1 -4
- package/lib/types/elements/gtk-emoji-chooser.d.ts +2 -0
- package/lib/types/elements/gtk-image.d.ts +7 -0
- package/lib/types/elements/gtk-popover-menu.d.ts +2 -0
- package/lib/types/elements/gtk-popover.d.ts +5 -0
- package/lib/types/elements/gtk-stack-page.d.ts +28 -0
- package/lib/types/elements/gtk-stack.d.ts +11 -0
- package/lib/types/elements/gtk-window.d.ts +16 -1
- package/lib/types/gio-resource.spec.d.ts +1 -0
- package/lib/types/gobject-elements.d.ts +9 -0
- package/lib/types/icon-theme.d.ts +33 -7
- package/lib/types/index.d.ts +2 -2
- package/lib/types/learn6502-window.spec.d.ts +1 -0
- package/lib/types/namespace/adw.d.ts +1 -1
- package/lib/types/namespace/gio.d.ts +1 -0
- package/lib/types/namespace/gtk.d.ts +2 -0
- package/lib/types/shared-tree-builder.d.ts +31 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/widget-visible.spec.d.ts +1 -0
- package/lib/types/window-maximize.spec.d.ts +1 -0
- package/package.json +12 -12
- package/scss/_bin.scss +6 -3
- package/scss/_overlay.scss +11 -1
- package/scss/_reset.scss +1 -1
- package/scss/_stack.scss +8 -2
- package/scss/_toast.scss +13 -3
- package/scss/_window.scss +16 -0
- package/src/adw-alert-dialog.spec.ts +71 -5
- package/src/adw-bin.spec.ts +4 -3
- package/src/adw-header-bar.spec.ts +18 -0
- package/src/adw-toast-overlay.spec.ts +68 -24
- package/src/application.spec.ts +45 -0
- package/src/application.ts +15 -18
- package/src/capabilities.mjs +1 -3
- package/src/child-slot.spec.ts +71 -0
- package/src/construct-vectors.spec.ts +51 -6
- package/src/container-fill.spec.ts +70 -0
- package/src/elements/adw-alert-dialog.ts +174 -44
- package/src/elements/adw-application-window.ts +0 -3
- package/src/elements/adw-breakpoint-bin.ts +3 -0
- package/src/elements/adw-dialog.ts +3 -1
- package/src/elements/adw-header-bar.ts +4 -0
- package/src/elements/adw-toast-overlay.ts +35 -69
- package/src/elements/adw-window-features.ts +14 -2
- package/src/elements/gtk-application-window.ts +3 -8
- package/src/elements/gtk-drag-icon.ts +4 -0
- package/src/elements/gtk-emoji-chooser.ts +5 -0
- package/src/elements/gtk-flow-box-child.ts +3 -1
- package/src/elements/gtk-graphics-offload.ts +3 -1
- package/src/elements/gtk-image.ts +47 -6
- package/src/elements/gtk-list-box-row.ts +3 -1
- package/src/elements/gtk-overlay.ts +5 -4
- package/src/elements/gtk-paned.ts +7 -1
- package/src/elements/gtk-popover-bin.ts +3 -0
- package/src/elements/gtk-popover-menu.ts +5 -0
- package/src/elements/gtk-popover.ts +12 -0
- package/src/elements/gtk-search-bar.ts +5 -5
- package/src/elements/gtk-stack-page.ts +111 -0
- package/src/elements/gtk-stack.ts +50 -5
- package/src/elements/gtk-tree-expander.ts +3 -0
- package/src/elements/gtk-viewport.ts +4 -0
- package/src/elements/gtk-window-handle.ts +4 -0
- package/src/elements/gtk-window.ts +55 -6
- package/src/gio-resource.spec.ts +137 -0
- package/src/gobject-elements.ts +28 -0
- package/src/gtk-stack.spec.ts +170 -0
- package/src/icon-theme.spec.ts +8 -3
- package/src/icon-theme.ts +102 -18
- package/src/index.ts +2 -2
- package/src/keyboard-operable.spec.ts +3 -3
- package/src/learn6502-window.spec.ts +113 -0
- package/src/namespace/adw.ts +1 -1
- package/src/namespace/gio.ts +10 -0
- package/src/namespace/gtk.ts +2 -0
- package/src/shared-tree-builder.ts +105 -10
- package/src/shared-trees.spec.ts +15 -1
- package/src/slotted-children.spec.ts +24 -0
- package/src/slotted-children.ts +18 -2
- package/src/styles.generated.ts +1 -1
- package/src/test.browser.mts +12 -0
- package/src/value-lists.spec.ts +3 -3
- package/src/widget-visible.spec.ts +104 -0
- package/src/window-maximize.spec.ts +93 -0
package/src/icon-theme.spec.ts
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
//
|
|
5
5
|
// The order under test is GLib's (`g_themed_icon_update_names`, which `GtkImage`'s lookup
|
|
6
6
|
// goes through): `X` tries [X, X-symbolic], `X-symbolic` tries [X-symbolic, X], and a name
|
|
7
|
-
// neither answers draws `image-missing`.
|
|
7
|
+
// neither answers draws `image-missing`. Over the names the order is gtkicontheme.c's: the
|
|
8
|
+
// current theme (the stylesheet's glyphs) before hicolor, where a colour registration sits.
|
|
9
|
+
// The registry is module-level and outlives a test,
|
|
8
10
|
// so every case registers names of its own.
|
|
9
11
|
import { describe, expect, it } from '@gjsify/unit';
|
|
10
12
|
|
|
@@ -114,13 +116,16 @@ export const AdwIconThemeTest = async () => {
|
|
|
114
116
|
host.remove();
|
|
115
117
|
});
|
|
116
118
|
|
|
117
|
-
await it('X
|
|
119
|
+
await it("the theme's X-symbolic answers X and X-symbolic before the colour X, as on GTK", () => {
|
|
118
120
|
const NAME = 'gjsify-theme-both-probe';
|
|
119
121
|
registerColorIcon(NAME, COLOR_SVG);
|
|
120
122
|
registerIcon(`${NAME}-symbolic`, SYMBOLIC_SVG);
|
|
121
123
|
|
|
124
|
+
// gtkicontheme.c `real_choose_icon`: theme outer, name inner, and the colour icon is
|
|
125
|
+
// hicolor's, so the current theme's `X-symbolic` comes first (ADR 0115 Amendment 2).
|
|
122
126
|
const plain = mount(NAME);
|
|
123
|
-
expect(
|
|
127
|
+
expect(plain.el.classList.contains(COLOR_ICON_CLASS)).toBe(false);
|
|
128
|
+
expect(maskOf(plain.el).includes('m%203%203')).toBe(true);
|
|
124
129
|
plain.host.remove();
|
|
125
130
|
|
|
126
131
|
const symbolic = mount(`${NAME}-symbolic`);
|
package/src/icon-theme.ts
CHANGED
|
@@ -23,23 +23,43 @@
|
|
|
23
23
|
// is named after its application ID (`org.gnome.Builder`, adw-about-dialog.c:1215), which is
|
|
24
24
|
// not a CSS token and never could be a mask class, but is a perfectly good Map key.
|
|
25
25
|
//
|
|
26
|
+
// RESOURCE PATHS (ADR 0116 § 4). The names go through the `Gtk.IconTheme` resource paths an
|
|
27
|
+
// application added (`GtkApplication` adds `<resource-base-path>/icons/` at startup);
|
|
28
|
+
// `@gjsify/adwaita-core`'s `lookupResourceIcon` decides which file answers, as gtkicontheme.c
|
|
29
|
+
// does, and asks this module only whether the stylesheet holds a symbolic glyph. A colour
|
|
30
|
+
// registration stands in hicolor there, as the file an app ships in `hicolor/scalable/apps/`
|
|
31
|
+
// would: the stylesheet's `X-symbolic` answers `X` before it, as Adwaita's does on GNOME. A file
|
|
32
|
+
// answers as a blob URL of its bytes: a symbolic one is drawn as a mask in `currentColor`, the
|
|
33
|
+
// rest as-is. A browser draws SVG and PNG; an XPM or a compressed entry draws `image-missing`
|
|
34
|
+
// with a warning.
|
|
35
|
+
//
|
|
26
36
|
// Original implementation.
|
|
27
37
|
|
|
28
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
iconLookupNames,
|
|
40
|
+
lookupResourceIcon,
|
|
41
|
+
normalizeIconName,
|
|
42
|
+
resources_lookup_data,
|
|
43
|
+
type IconDirection,
|
|
44
|
+
} from '@gjsify/adwaita-core';
|
|
29
45
|
import { toDataUri } from '@gjsify/adwaita-icons/utils';
|
|
30
46
|
|
|
47
|
+
import { IconTheme } from './namespace/gtk-platform.js';
|
|
48
|
+
|
|
31
49
|
/** What one icon name resolves to, in the order GTK's theme would try it. */
|
|
32
50
|
export type IconLookup =
|
|
33
51
|
/** A registered full-colour image, drawn as-is. `url` is ready for `url("…")`. */
|
|
34
52
|
| { kind: 'color'; name: string; url: string }
|
|
53
|
+
/**
|
|
54
|
+
* A file under a `Gtk.IconTheme` resource path; `symbolic` files are masks in `currentColor`.
|
|
55
|
+
* `url` is `null` when the browser cannot draw the file (XPM, compressed): draw `image-missing`.
|
|
56
|
+
*/
|
|
57
|
+
| { kind: 'resource'; name: string; path: string; url: string | null; symbolic: boolean }
|
|
35
58
|
/** A symbolic glyph, drawn through the `.adw-icon--<name>` mask. `name` is normalized. */
|
|
36
59
|
| { kind: 'symbolic'; name: string }
|
|
37
60
|
/** Nothing in the theme answers this name — draw `image-missing`. */
|
|
38
61
|
| { kind: 'missing' };
|
|
39
62
|
|
|
40
|
-
/** The suffix `g_themed_icon_update_names` swaps to build the other variant. */
|
|
41
|
-
const SYMBOLIC_SUFFIX = /-symbolic$/;
|
|
42
|
-
|
|
43
63
|
/**
|
|
44
64
|
* Every name GTK's theme treats as a symbolic FILE (gtkicontheme.c `icon_name_is_symbolic`):
|
|
45
65
|
* the directional variants are symbolic too, so none of them is a colour icon.
|
|
@@ -82,33 +102,97 @@ function hasSymbolic(normalized: string): boolean {
|
|
|
82
102
|
* Resolve `iconName` as `gtk_icon_theme_lookup_by_gicon` would for a `GtkImage`. `null` when
|
|
83
103
|
* no name was given — the state where GTK draws nothing, not `image-missing`.
|
|
84
104
|
*
|
|
85
|
-
* When
|
|
86
|
-
* checking the cascade: the mask class falls back to `image-missing`
|
|
87
|
-
* and corrects itself when a later `registerIcon` lands, which is how
|
|
88
|
-
* before this module. The cascade is read only to choose between a
|
|
89
|
-
* symbolic glyph.
|
|
105
|
+
* When neither a colour registration nor a resource holds a name, the request is returned as a
|
|
106
|
+
* symbolic one WITHOUT checking the cascade: the mask class falls back to `image-missing`
|
|
107
|
+
* through CSS by itself and corrects itself when a later `registerIcon` lands, which is how
|
|
108
|
+
* `<gtk-image>` behaved before this module. The cascade is read only to choose between a
|
|
109
|
+
* symbolic glyph and a colour entry or file.
|
|
90
110
|
*/
|
|
91
|
-
export function lookupIcon(iconName: string | null | undefined): IconLookup | null {
|
|
111
|
+
export function lookupIcon(iconName: string | null | undefined, request: IconRequest = {}): IconLookup | null {
|
|
92
112
|
if (iconName === null || iconName === undefined || iconName === '') return null;
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
const color = colorIcons.get(colorName);
|
|
113
|
+
const found = resourceIcon(iconName, request);
|
|
114
|
+
if (found !== null) return found;
|
|
96
115
|
const symbolic = normalizeIconName(iconName);
|
|
116
|
+
return symbolic === '' ? { kind: 'missing' } : { kind: 'symbolic', name: symbolic };
|
|
117
|
+
}
|
|
97
118
|
|
|
98
|
-
|
|
99
|
-
|
|
119
|
+
/** What a `GtkImage` hands its theme besides the name: its size, scale and text direction. */
|
|
120
|
+
export interface IconRequest {
|
|
121
|
+
/** In CSS pixels; 16 when absent. */
|
|
122
|
+
readonly size?: number;
|
|
123
|
+
/** The device scale; 1 when absent. */
|
|
124
|
+
readonly scale?: number;
|
|
125
|
+
readonly direction?: IconDirection;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const theme = IconTheme.get_for_display(null);
|
|
129
|
+
|
|
130
|
+
/** Blob URLs of resource files, by resource path, for the current scan. */
|
|
131
|
+
const resourceUrls = new Map<string, string | null>();
|
|
132
|
+
|
|
133
|
+
theme.connect('changed', () => {
|
|
134
|
+
for (const url of resourceUrls.values()) if (url !== null) URL.revokeObjectURL(url);
|
|
135
|
+
resourceUrls.clear();
|
|
136
|
+
notifyIconThemeChanged();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const RESOURCE_TYPES: Record<string, string> = { '.svg': 'image/svg+xml', '.png': 'image/png' };
|
|
140
|
+
|
|
141
|
+
/** The file at `path` as a URL a browser draws, or `null` (warned once) when it cannot. */
|
|
142
|
+
function resourceUrl(path: string): string | null {
|
|
143
|
+
const cached = resourceUrls.get(path);
|
|
144
|
+
if (cached !== undefined) return cached;
|
|
145
|
+
let url: string | null = null;
|
|
146
|
+
const type = RESOURCE_TYPES[path.slice(path.lastIndexOf('.'))];
|
|
147
|
+
if (type === undefined) {
|
|
148
|
+
console.warn(`Gtk.IconTheme: ${path} is not an SVG or a PNG, which is all a browser draws (ADR 0116 § 4).`);
|
|
149
|
+
} else {
|
|
150
|
+
try {
|
|
151
|
+
const data = resources_lookup_data(path, 0).get_data();
|
|
152
|
+
url = URL.createObjectURL(new Blob([data as Uint8Array<ArrayBuffer>], { type }));
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.warn(`Gtk.IconTheme: ${path} cannot be drawn: ${(error as Error).message}`);
|
|
155
|
+
}
|
|
100
156
|
}
|
|
101
|
-
|
|
102
|
-
return
|
|
157
|
+
resourceUrls.set(path, url);
|
|
158
|
+
return url;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The lookup over the colour registrations and the theme's resource paths; `null` when neither
|
|
163
|
+
* answers any name, a `symbolic` entry when the current theme's glyph comes first.
|
|
164
|
+
*/
|
|
165
|
+
export function resourceIcon(iconName: string, request: IconRequest): IconLookup | null {
|
|
166
|
+
const match = lookupResourceIcon(theme, {
|
|
167
|
+
names: iconLookupNames(iconName, request.direction ?? null),
|
|
168
|
+
get size() {
|
|
169
|
+
return request.size ?? 16;
|
|
170
|
+
},
|
|
171
|
+
scale: request.scale ?? 1,
|
|
172
|
+
themeHas: (name) => hasSymbolic(normalizeIconName(name)),
|
|
173
|
+
hicolorHas: (name) => colorIcons.has(name),
|
|
174
|
+
});
|
|
175
|
+
if (match === null) return null;
|
|
176
|
+
if (match.kind === 'theme') return { kind: 'symbolic', name: normalizeIconName(match.name) };
|
|
177
|
+
if (match.kind === 'hicolor') return { kind: 'color', name: match.name, url: colorIcons.get(match.name)! };
|
|
178
|
+
return {
|
|
179
|
+
kind: 'resource',
|
|
180
|
+
name: match.name,
|
|
181
|
+
path: match.path,
|
|
182
|
+
url: resourceUrl(match.path),
|
|
183
|
+
symbolic: match.symbolic,
|
|
184
|
+
};
|
|
103
185
|
}
|
|
104
186
|
|
|
105
187
|
/**
|
|
106
188
|
* Whether `iconName` resolves to something other than `image-missing` — a registered colour
|
|
107
|
-
* icon, or a symbolic glyph compiled into the stylesheet or
|
|
189
|
+
* icon, a file under a resource path, or a symbolic glyph compiled into the stylesheet or
|
|
190
|
+
* registered at runtime.
|
|
108
191
|
*/
|
|
109
192
|
export function iconResolves(iconName: string): boolean {
|
|
110
193
|
const found = lookupIcon(iconName);
|
|
111
194
|
if (found === null || found.kind === 'missing') return false;
|
|
195
|
+
if (found.kind === 'resource') return found.url !== null;
|
|
112
196
|
return found.kind === 'color' || hasSymbolic(found.name);
|
|
113
197
|
}
|
|
114
198
|
|
package/src/index.ts
CHANGED
|
@@ -80,8 +80,8 @@ export type { AdwUndershootEdges } from './scroll-shading.js';
|
|
|
80
80
|
// replaying an authored tree — a storybook fixture, a devtools probe, the shared-trees
|
|
81
81
|
// spec itself — has a route to it that is not a `.spec.ts` import; see that file's header
|
|
82
82
|
// for why `mountSharedTree` and not `buildSharedTree` alone is the instantiation half.
|
|
83
|
-
export { buildSharedTree, mountSharedTree } from './shared-tree-builder.js';
|
|
84
|
-
export type { MountOptions, MountedSharedTree, SizeSource } from './shared-tree-builder.js';
|
|
83
|
+
export { buildSharedTree, buildWithSiblings, mountSharedTree } from './shared-tree-builder.js';
|
|
84
|
+
export type { BuildOptions, BuiltTree, MountOptions, MountedSharedTree, SizeSource } from './shared-tree-builder.js';
|
|
85
85
|
export { registerTemplateClass } from './template-classes.js';
|
|
86
86
|
|
|
87
87
|
// The stylesheet compiles a chosen SUBSET of `@gjsify/adwaita-icons` (the whole set is
|
|
@@ -314,8 +314,8 @@ export const AdwKeyboardOperableTest = async () => {
|
|
|
314
314
|
'<adw-alert-response id="ok">OK</adw-alert-response>' +
|
|
315
315
|
'<adw-alert-response id="cancel">Cancel</adw-alert-response>',
|
|
316
316
|
);
|
|
317
|
-
el.
|
|
318
|
-
el.
|
|
317
|
+
el.set_default_response('ok');
|
|
318
|
+
el.set_response_enabled('ok', false);
|
|
319
319
|
opener.focus();
|
|
320
320
|
el.setAttribute('open', '');
|
|
321
321
|
|
|
@@ -344,7 +344,7 @@ export const AdwKeyboardOperableTest = async () => {
|
|
|
344
344
|
'<adw-alert-response id="ok">OK</adw-alert-response>' +
|
|
345
345
|
'<adw-alert-response id="cancel">Cancel</adw-alert-response>',
|
|
346
346
|
);
|
|
347
|
-
el.
|
|
347
|
+
el.set_default_response('ok');
|
|
348
348
|
el.setAttribute('open', '');
|
|
349
349
|
|
|
350
350
|
// `adw_alert_dialog_grab_focus` tries the CONTENT first (adw-alert-dialog.c:397)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Learn6502's `main.window.blp`, reduced to the shapes that rendered short through
|
|
2
|
+
// `buildWithSiblings`: header-bar buttons after a single `[start]` (GtkBuilder packs an untyped
|
|
3
|
+
// child at start too), and `Overlay { child: Adw.ToastOverlay { Stack } }`. Every
|
|
4
|
+
// widget the file authors has to be in the document once the built window is attached — the
|
|
5
|
+
// failure was widgets that were built and then silently dropped on the way.
|
|
6
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
7
|
+
import { parseBlueprint, projectToSharedNode } from '@gjsify/blueprint';
|
|
8
|
+
|
|
9
|
+
import { buildWithSiblings } from './shared-tree-builder.js';
|
|
10
|
+
|
|
11
|
+
const BLUEPRINT = `using Gtk 4.0;
|
|
12
|
+
using Adw 1;
|
|
13
|
+
|
|
14
|
+
Adw.ApplicationWindow mainWindow {
|
|
15
|
+
content: Adw.ToolbarView toolbarView {
|
|
16
|
+
[top]
|
|
17
|
+
Adw.HeaderBar headerBar {
|
|
18
|
+
title-widget: Label titleLabel {
|
|
19
|
+
label: "Learn 6502";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
[start]
|
|
23
|
+
ToggleButton sidebarToggleButton {
|
|
24
|
+
icon-name: "sidebar-show-symbolic";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Button learnBackButton {
|
|
28
|
+
icon-name: "go-previous-symbolic";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Button unsavedChangesIndicator {
|
|
32
|
+
icon-name: "dot-symbolic";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
[end]
|
|
36
|
+
MenuButton menuButton {
|
|
37
|
+
icon-name: "open-menu-symbolic";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
content: Overlay mainOverlay {
|
|
42
|
+
child: Adw.ToastOverlay toastOverlay {
|
|
43
|
+
Stack layoutHost {
|
|
44
|
+
StackPage singlePage {
|
|
45
|
+
name: "single";
|
|
46
|
+
child: Adw.Bin singleContainer {
|
|
47
|
+
child: Label singleLabel {
|
|
48
|
+
label: "single";
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
StackPage threePage {
|
|
54
|
+
name: "three";
|
|
55
|
+
child: Adw.Bin threeColumnContainer {
|
|
56
|
+
child: Box threeColumns {
|
|
57
|
+
Label threeLabel {
|
|
58
|
+
label: "three";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
const AUTHORED = [
|
|
71
|
+
'toolbarView',
|
|
72
|
+
'headerBar',
|
|
73
|
+
'titleLabel',
|
|
74
|
+
'sidebarToggleButton',
|
|
75
|
+
'learnBackButton',
|
|
76
|
+
'unsavedChangesIndicator',
|
|
77
|
+
'menuButton',
|
|
78
|
+
'mainOverlay',
|
|
79
|
+
'toastOverlay',
|
|
80
|
+
'layoutHost',
|
|
81
|
+
'singleContainer',
|
|
82
|
+
'singleLabel',
|
|
83
|
+
'threeColumnContainer',
|
|
84
|
+
'threeColumns',
|
|
85
|
+
'threeLabel',
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
export const AdwLearn6502WindowTest = async () => {
|
|
89
|
+
await describe("Learn6502's main window through buildWithSiblings", async () => {
|
|
90
|
+
const { node, lost } = projectToSharedNode(parseBlueprint(BLUEPRINT, 'main.window.blp'));
|
|
91
|
+
|
|
92
|
+
await it('projects without losing anything', () => {
|
|
93
|
+
expect(lost.map((loss) => `${loss.kind}@${loss.line}`).join()).toBe('');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await it('puts every authored widget in the document, the start buttons at start', () => {
|
|
97
|
+
const { root } = buildWithSiblings(node);
|
|
98
|
+
document.body.append(root);
|
|
99
|
+
const missing = AUTHORED.filter((id) => !root.querySelector(`#${id}`)?.isConnected);
|
|
100
|
+
const start = ['sidebarToggleButton', 'learnBackButton', 'unsavedChangesIndicator'].filter(
|
|
101
|
+
(id) => root.querySelector(`#${id}`)?.parentElement?.className !== 'adw-header-bar-start',
|
|
102
|
+
);
|
|
103
|
+
const toast = root.querySelector('#toastOverlay');
|
|
104
|
+
const overlaid =
|
|
105
|
+
toast?.closest('.adw-overlay-child') === root.querySelector('#mainOverlay > .adw-overlay-child');
|
|
106
|
+
root.remove();
|
|
107
|
+
|
|
108
|
+
expect(missing.join()).toBe('');
|
|
109
|
+
expect(start.join()).toBe('');
|
|
110
|
+
expect(overlaid).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
};
|
package/src/namespace/adw.ts
CHANGED
|
@@ -111,4 +111,4 @@ export { AdwWindowTitle as WindowTitle } from '../elements/adw-window-title.js';
|
|
|
111
111
|
export { AdwWrapBox as WrapBox } from '../elements/adw-wrap-box.js';
|
|
112
112
|
export { AdwApplication as Application } from '../application.js';
|
|
113
113
|
export { AdwStyleManager as StyleManager } from '../style-manager.js';
|
|
114
|
-
export { ColorScheme } from '@gjsify/adwaita-core';
|
|
114
|
+
export { ColorScheme, Toast, ToastPriority } from '@gjsify/adwaita-core';
|
package/src/namespace/gio.ts
CHANGED
|
@@ -35,5 +35,15 @@ export {
|
|
|
35
35
|
promisify as _promisify,
|
|
36
36
|
} from '@gjsify/adwaita-core';
|
|
37
37
|
export { SimpleAction, SimpleActionGroup, ApplicationFlags } from '@gjsify/adwaita-core';
|
|
38
|
+
export {
|
|
39
|
+
GioResource as Resource,
|
|
40
|
+
ResourceError,
|
|
41
|
+
ResourceLookupFlags,
|
|
42
|
+
resources_enumerate_children,
|
|
43
|
+
resources_get_info,
|
|
44
|
+
resources_lookup_data,
|
|
45
|
+
resources_register,
|
|
46
|
+
resources_unregister,
|
|
47
|
+
} from '@gjsify/adwaita-core';
|
|
38
48
|
export { GioApplication as Application } from '../application.js';
|
|
39
49
|
export { Settings } from '@gjsify/app-settings/browser';
|
package/src/namespace/gtk.ts
CHANGED
|
@@ -86,6 +86,7 @@ export { GtkSeparator as Separator } from '../elements/gtk-separator.js';
|
|
|
86
86
|
export { GtkSpinButton as SpinButton } from '../elements/gtk-spin-button.js';
|
|
87
87
|
export { GtkSpinner as Spinner } from '../elements/gtk-spinner.js';
|
|
88
88
|
export { GtkStack as Stack } from '../elements/gtk-stack.js';
|
|
89
|
+
export { GtkStackPage as StackPage } from '../elements/gtk-stack-page.js';
|
|
89
90
|
export { GtkStackSidebar as StackSidebar } from '../elements/gtk-stack-sidebar.js';
|
|
90
91
|
export { GtkStackSwitcher as StackSwitcher } from '../elements/gtk-stack-switcher.js';
|
|
91
92
|
export { GtkStringList as StringList } from '@gjsify/adwaita-core';
|
|
@@ -101,6 +102,7 @@ export type { WindowCommon } from '@gjsify/adwaita-core';
|
|
|
101
102
|
export { GtkWindowControls as WindowControls } from '../elements/gtk-window-controls.js';
|
|
102
103
|
export { GtkViewport as Viewport } from '../elements/gtk-viewport.js';
|
|
103
104
|
export { GtkWindowHandle as WindowHandle } from '../elements/gtk-window-handle.js';
|
|
105
|
+
export { ApplicationInhibitFlags } from '@gjsify/adwaita-core';
|
|
104
106
|
export { GtkApplication as Application } from '../application.js';
|
|
105
107
|
export {
|
|
106
108
|
STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
@@ -55,7 +55,14 @@ import {
|
|
|
55
55
|
|
|
56
56
|
import { observeAdaptiveSize } from './breakpoints.js';
|
|
57
57
|
import { capabilities } from './capabilities.mjs';
|
|
58
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
endpointOf,
|
|
60
|
+
hasElementApi,
|
|
61
|
+
hasInstalledVisible,
|
|
62
|
+
installElementApi,
|
|
63
|
+
installSensitive,
|
|
64
|
+
installVisible,
|
|
65
|
+
} from './gobject-elements.js';
|
|
59
66
|
import { dispatchedSignalsOf } from './signals.js';
|
|
60
67
|
import { slottedChildrenOf } from './slotted-children.js';
|
|
61
68
|
import { isTemplateTag, templateTagFor } from './template-classes.js';
|
|
@@ -154,14 +161,93 @@ function isWritable(el: object, member: string): boolean {
|
|
|
154
161
|
* upgraded has declared no slots yet, so the refusal below belongs to the mount.
|
|
155
162
|
*/
|
|
156
163
|
export function buildSharedTree(node: SharedTreeNode, record: BuildRecord = newRecord()): HTMLElement {
|
|
164
|
+
refuseSiblings(node, 'buildSharedTree');
|
|
165
|
+
return buildTree(node, record).root;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** What {@link buildWithSiblings} hands back: the root element and the objects built beside it. */
|
|
169
|
+
export interface BuiltTree {
|
|
170
|
+
/** The element the tree roots at, as {@link buildSharedTree} returns it. */
|
|
171
|
+
root: HTMLElement;
|
|
172
|
+
/**
|
|
173
|
+
* The sibling objects by id — `Adw.AlertDialog unsavedChangesDialog { }` or `$Learn learn { }`
|
|
174
|
+
* written after the template. A sibling is built in the root's id scope (a setter or a bind
|
|
175
|
+
* may name it) but is no child of the root: the code beside the file decides where, or
|
|
176
|
+
* whether, it is placed.
|
|
177
|
+
*/
|
|
178
|
+
siblings: ReadonlyMap<string, HTMLElement>;
|
|
179
|
+
/** Every element the tree names by id, siblings included: what `GtkBuilder.get_object` answers for. */
|
|
180
|
+
objects: ReadonlyMap<string, HTMLElement>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** What {@link buildWithSiblings} takes beside the tree. */
|
|
184
|
+
export interface BuildOptions {
|
|
185
|
+
/** The object a handler NAME is looked up on (ADR 0093 § 3). */
|
|
186
|
+
scope?: Readonly<Record<string, unknown>>;
|
|
187
|
+
/** The size source a `breakpoints` node is driven from. */
|
|
188
|
+
observeSize?: SizeSource;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A tree with sibling roots (ADR 0093): the root element, and each sibling by id, all DETACHED
|
|
193
|
+
* like {@link buildSharedTree}'s result.
|
|
194
|
+
*
|
|
195
|
+
* {@link buildSharedTree}, {@link mountSharedTree} and {@link buildTemplateTree} refuse such a
|
|
196
|
+
* tree by name, because they return one element and the siblings would be built and then thrown
|
|
197
|
+
* away. A sibling with no id is refused too: nothing could ever ask for it, which is how a
|
|
198
|
+
* dropped object looks finished.
|
|
199
|
+
*/
|
|
200
|
+
export function buildWithSiblings(node: SharedTreeNode, options: BuildOptions = {}): BuiltTree {
|
|
201
|
+
const record = newRecord(options.scope, options.observeSize);
|
|
202
|
+
const built = buildTree(node, record);
|
|
203
|
+
// An element declares its slots when it is connected, so a detached tree cannot be checked
|
|
204
|
+
// as it stands. It is connected for the length of the check and handed back detached, as
|
|
205
|
+
// documented: a placement the renderer could not honour is refused here, like
|
|
206
|
+
// `mountSharedTree` refuses it, instead of surfacing as a widget missing from the page.
|
|
207
|
+
const probe = document.createElement('div');
|
|
208
|
+
probe.hidden = true;
|
|
209
|
+
probe.append(built.root, ...built.siblings.values());
|
|
210
|
+
document.body.append(probe);
|
|
211
|
+
try {
|
|
212
|
+
refuseUnknownSlots(record.placed);
|
|
213
|
+
refuseUnheldExtensions(record.extended);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
for (const dispose of record.disposers) dispose();
|
|
216
|
+
throw error;
|
|
217
|
+
} finally {
|
|
218
|
+
probe.remove();
|
|
219
|
+
for (const el of [built.root, ...built.siblings.values()]) el.remove();
|
|
220
|
+
}
|
|
221
|
+
return built;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function refuseSiblings(node: SharedTreeNode, door: string): void {
|
|
225
|
+
if ((node.siblings?.length ?? 0) === 0) return;
|
|
226
|
+
throw new Error(
|
|
227
|
+
`\`${node.tag}\` carries ${node.siblings!.length} sibling object(s), and \`${door}\` returns one object ` +
|
|
228
|
+
'so they would be built and lost. Use `buildWithSiblings`, which hands them back by id.',
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Every node, its siblings in the same id scope, then the binds and breakpoints that name them. */
|
|
233
|
+
function buildTree(node: SharedTreeNode, record: BuildRecord): BuiltTree {
|
|
157
234
|
// ADR 0093 § 2: the whole tree is checked against the capability table before an element is
|
|
158
235
|
// created. A refused `layout` is named with its node and the reason the table gives.
|
|
159
236
|
assertTreeConstructs('adwaita-web', capabilities, node);
|
|
160
237
|
if (node.menus !== undefined) record.menus = node.menus;
|
|
161
238
|
const root = buildNode(node, record);
|
|
239
|
+
const siblings = new Map<string, HTMLElement>();
|
|
240
|
+
for (const sibling of node.siblings ?? []) {
|
|
241
|
+
if (sibling.id === undefined) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`the sibling \`${sibling.tag}\` has no id, so nothing could ever ask for it: give it one in the .blp.`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
siblings.set(sibling.id, buildNode(sibling, record));
|
|
247
|
+
}
|
|
162
248
|
for (const bind of record.binds) bindProperty(bind, record);
|
|
163
249
|
for (const host of record.breakpointHosts) bindBreakpoints(host.el, host.node, record);
|
|
164
|
-
return root;
|
|
250
|
+
return { root, siblings, objects: new Map(record.ids) };
|
|
165
251
|
}
|
|
166
252
|
|
|
167
253
|
/** A fresh record, with the scope when the caller has one. */
|
|
@@ -344,7 +430,9 @@ export function writeProp(el: HTMLElement, prop: string, value: string | number
|
|
|
344
430
|
const member = propertyOf(prop);
|
|
345
431
|
const expand = GTK_WIDGET_EXPAND.find((name) => name === attr);
|
|
346
432
|
if (expand !== undefined && typeof value === 'boolean') el.setAttribute(expand, value ? '' : 'false');
|
|
347
|
-
else if (
|
|
433
|
+
else if (attr === 'visible' && typeof value === 'boolean' && hasInstalledVisible(el)) {
|
|
434
|
+
(el as unknown as Record<string, unknown>)[member] = value;
|
|
435
|
+
} else if (isTemplateTag(el.localName) && member in el) {
|
|
348
436
|
// An application class has no attributeChangedCallback: its properties are GObject properties,
|
|
349
437
|
// set the way `Gtk.Builder` sets them, after the constructor.
|
|
350
438
|
(el as unknown as Record<string, unknown>)[member] = value;
|
|
@@ -372,9 +460,14 @@ function buildNode(node: SharedTreeNode, record: BuildRecord): HTMLElement {
|
|
|
372
460
|
const el = document.createElement(node.extern === true ? templateTagFor(node.tag) : hostTagOf(node.tag));
|
|
373
461
|
// A template's children talk GObject to the template: `connect`, `emit`, `notify` (ADR 0096).
|
|
374
462
|
if (record.template !== undefined) installElementApi(el);
|
|
463
|
+
// `Gtk.Widget:visible`, which no element spells: a setter's target and an authored `false` need it.
|
|
464
|
+
installVisible(el);
|
|
375
465
|
// The id is how the TypeScript beside a `.blp` reaches this element
|
|
376
466
|
// (`root.querySelector('#…')`), the counterpart of `InternalChildren` on GTK.
|
|
377
467
|
if (node.id !== undefined) {
|
|
468
|
+
if (record.ids.has(node.id)) {
|
|
469
|
+
throw new Error(`Two nodes of this tree carry the id '${node.id}'; GtkBuilder refuses a duplicate id.`);
|
|
470
|
+
}
|
|
378
471
|
el.id = node.id;
|
|
379
472
|
record.ids.set(node.id, el);
|
|
380
473
|
}
|
|
@@ -492,6 +585,7 @@ export interface BuiltTemplateTree {
|
|
|
492
585
|
* belong to the first `connectedCallback`.
|
|
493
586
|
*/
|
|
494
587
|
export function buildTemplateTree(tree: SharedTreeNode, scope: TemplateScope, host: HTMLElement): BuiltTemplateTree {
|
|
588
|
+
refuseSiblings(tree, 'buildTemplateTree');
|
|
495
589
|
assertTreeConstructs('adwaita-web', capabilities, tree);
|
|
496
590
|
const record = newRecord(undefined, undefined, scope);
|
|
497
591
|
record.menus = tree.menus;
|
|
@@ -591,9 +685,9 @@ function writeExtensions(el: HTMLElement, node: SharedTreeNode): void {
|
|
|
591
685
|
|
|
592
686
|
/** The response API an element answers to — `<adw-alert-dialog>`'s, named after libadwaita's. */
|
|
593
687
|
interface ResponseReader {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
688
|
+
get_response_label(id: string): string | null;
|
|
689
|
+
get_response_appearance(id: string): string | null;
|
|
690
|
+
get_response_enabled(id: string): boolean;
|
|
597
691
|
}
|
|
598
692
|
|
|
599
693
|
/**
|
|
@@ -617,10 +711,10 @@ function refuseUnheldExtensions(extended: readonly ExtendedNode[]): void {
|
|
|
617
711
|
const reader = el as unknown as Partial<ResponseReader>;
|
|
618
712
|
for (const response of node.extensions?.responses ?? []) {
|
|
619
713
|
const held =
|
|
620
|
-
typeof reader.
|
|
621
|
-
reader.
|
|
622
|
-
reader.
|
|
623
|
-
reader.
|
|
714
|
+
typeof reader.get_response_label === 'function' &&
|
|
715
|
+
reader.get_response_label(response.id) === response.label &&
|
|
716
|
+
reader.get_response_appearance?.(response.id) === (response.appearance ?? 'default') &&
|
|
717
|
+
reader.get_response_enabled?.(response.id) === (response.enabled ?? true);
|
|
624
718
|
if (!held) {
|
|
625
719
|
throw new Error(
|
|
626
720
|
`<${el.localName}> did not register the response "${response.id}" as authored, so the ` +
|
|
@@ -696,6 +790,7 @@ export interface MountOptions {
|
|
|
696
790
|
* `buildSharedTree` is for a caller that already has somewhere of its own to attach it.
|
|
697
791
|
*/
|
|
698
792
|
export function mountSharedTree(node: SharedTreeNode, options: MountOptions = {}): MountedSharedTree {
|
|
793
|
+
refuseSiblings(node, 'mountSharedTree');
|
|
699
794
|
const { into } = options;
|
|
700
795
|
const host = into ?? document.createElement('div');
|
|
701
796
|
const record = newRecord(options.scope, options.observeSize);
|
package/src/shared-trees.spec.ts
CHANGED
|
@@ -54,7 +54,7 @@ import { ADWAITA_GALLERY_SHARED_TREES } from '../../../../scripts/adwaita-galler
|
|
|
54
54
|
|
|
55
55
|
import '@gjsify/adwaita-web';
|
|
56
56
|
|
|
57
|
-
import { mountSharedTree } from './shared-tree-builder.js';
|
|
57
|
+
import { buildWithSiblings, mountSharedTree } from './shared-tree-builder.js';
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
60
|
* Connect the authored root — these elements build on connect — run, and take it down again.
|
|
@@ -364,6 +364,20 @@ export const AdwSharedTreesTest = async () => {
|
|
|
364
364
|
).toThrow('has no slot "middle"');
|
|
365
365
|
});
|
|
366
366
|
|
|
367
|
+
// The detached door refuses it too: a root built and handed back is mounted by the
|
|
368
|
+
// caller, after the builder could still say anything.
|
|
369
|
+
await it('`buildWithSiblings` refuses the same placement, and hands a good tree back detached', () => {
|
|
370
|
+
const bad = { tag: 'AdwToolbarView', children: [{ tag: 'AdwBanner', slot: 'middle' }] };
|
|
371
|
+
expect(() => buildWithSiblings(bad)).toThrow('has no slot "middle"');
|
|
372
|
+
const { root, siblings } = buildWithSiblings({
|
|
373
|
+
tag: 'AdwToolbarView',
|
|
374
|
+
children: [{ tag: 'AdwBanner', slot: 'top' }],
|
|
375
|
+
siblings: [{ tag: 'GtkLabel', id: 'beside' }],
|
|
376
|
+
});
|
|
377
|
+
expect(root.isConnected || siblings.get('beside')!.isConnected).toBe(false);
|
|
378
|
+
expect(root.querySelector('adw-banner') !== null).toBe(true);
|
|
379
|
+
});
|
|
380
|
+
|
|
367
381
|
// ADR 0092's `layout { }`: a grid child's cell. GTK counts from 0, CSS Grid from 1.
|
|
368
382
|
await it('`layout { }` places a grid child in its cell, spans defaulting to one', () => {
|
|
369
383
|
mounted(
|
|
@@ -113,6 +113,30 @@ export const AdwSlottedChildrenTest = async () => {
|
|
|
113
113
|
expect(report).toBe('[1]');
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
+
// The DECLARED twin of the case above. `install()` replaces the host's children with
|
|
117
|
+
// its structure, and a child no slot took used to go with them — gone without a word.
|
|
118
|
+
await it('keeps a declared child no slot takes, and warns that it was not placed', () => {
|
|
119
|
+
const host = document.createElement('div');
|
|
120
|
+
const typo = probe('sied', 'a');
|
|
121
|
+
host.append(document.createTextNode('\n '), typo);
|
|
122
|
+
mounted(host);
|
|
123
|
+
const side = document.createElement('div');
|
|
124
|
+
side.className = 'side';
|
|
125
|
+
|
|
126
|
+
const warned: string[] = [];
|
|
127
|
+
const warn = console.warn;
|
|
128
|
+
console.warn = (...args: unknown[]) => void warned.push(args.join(' '));
|
|
129
|
+
try {
|
|
130
|
+
bindSlottedChildren(host, [{ name: 'side', into: side }]).install(side);
|
|
131
|
+
} finally {
|
|
132
|
+
console.warn = warn;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const report = `${placement(host, typo)} | ${warned.join()}`;
|
|
136
|
+
host.remove();
|
|
137
|
+
expect(report).toBe("[1] | 'sied' is not a valid child type of 'div'");
|
|
138
|
+
});
|
|
139
|
+
|
|
116
140
|
await it('re-adopts a child that was appended, removed and appended again', async () => {
|
|
117
141
|
const host = mounted(document.createElement('div'));
|
|
118
142
|
const side = document.createElement('div');
|
package/src/slotted-children.ts
CHANGED
|
@@ -113,6 +113,12 @@ export interface AdwSlottedChildren {
|
|
|
113
113
|
/** Where a bound host's slots can be read back from — see {@link slottedChildrenOf}. */
|
|
114
114
|
const BOUND = new WeakMap<Element, AdwSlottedChildren>();
|
|
115
115
|
|
|
116
|
+
/** An element or a text node with something in it — markup's indentation and comments are not. */
|
|
117
|
+
function isContent(node: Node): boolean {
|
|
118
|
+
if (node instanceof Element) return true;
|
|
119
|
+
return node.nodeType === Node.TEXT_NODE && (node.textContent ?? '').trim() !== '';
|
|
120
|
+
}
|
|
121
|
+
|
|
116
122
|
/**
|
|
117
123
|
* The slot binding `host` installed, if it has one.
|
|
118
124
|
*
|
|
@@ -184,9 +190,19 @@ export function bindSlottedChildren(
|
|
|
184
190
|
slots,
|
|
185
191
|
slotFor,
|
|
186
192
|
install(...nodes: readonly Node[]) {
|
|
187
|
-
|
|
193
|
+
const unplaced: Node[] = [];
|
|
194
|
+
for (const node of Array.from(host.childNodes)) {
|
|
195
|
+
if (route(node) !== null || node.parentNode !== host || !isContent(node)) continue;
|
|
196
|
+
// `replaceChildren` below would delete it, and a child the author wrote that
|
|
197
|
+
// vanishes without a word is the silent drop this used to be. It stays beside
|
|
198
|
+
// the structure, as a late child with the same slot does, and says so the
|
|
199
|
+
// way GtkBuilder does (GTK_BUILDER_WARN_INVALID_CHILD_TYPE).
|
|
200
|
+
unplaced.push(node);
|
|
201
|
+
const type = node instanceof Element ? (node.getAttribute('slot') ?? '') : '';
|
|
202
|
+
console.warn(`'${type}' is not a valid child type of '${host.localName}'`);
|
|
203
|
+
}
|
|
188
204
|
structure = new Set(nodes);
|
|
189
|
-
host.replaceChildren(...nodes);
|
|
205
|
+
host.replaceChildren(...nodes, ...unplaced);
|
|
190
206
|
// Armed AFTER `replaceChildren`, so its own records are never delivered: an
|
|
191
207
|
// observer armed first would see the whole of `structure` as added nodes and
|
|
192
208
|
// spend a microtask rejecting them.
|