@gjsify/adwaita-web 0.64.1-next.241 → 0.64.1-next.244
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 +42 -4
- package/dist/adwaita-web.css +9 -1
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/canvas2d/canvas2d-bridge.d.ts +18 -0
- package/lib/types/canvas2d-bridge.spec.d.ts +1 -0
- package/lib/types/elements/adw-action-row.d.ts +1 -0
- package/lib/types/elements/adw-bottom-sheet.d.ts +1 -0
- package/lib/types/elements/adw-navigation-view.d.ts +1 -0
- package/lib/types/elements/adw-switch-row.d.ts +1 -0
- package/lib/types/elements/gtk-image.d.ts +12 -3
- package/lib/types/elements/gtk-scrolled-window.d.ts +10 -0
- package/lib/types/gobject-elements.d.ts +7 -0
- package/lib/types/icon-registry.d.ts +3 -1
- package/lib/types/icon-theme.d.ts +53 -0
- package/lib/types/icon-theme.spec.d.ts +1 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/namespace/gtk.d.ts +1 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/package.json +12 -12
- package/scss/_about_dialog.scss +2 -2
- package/scss/_icon.scss +18 -0
- package/src/adw-window.spec.ts +7 -0
- package/src/application.spec.ts +3 -3
- package/src/canvas2d/canvas2d-bridge.ts +51 -1
- package/src/canvas2d-bridge.spec.ts +47 -0
- package/src/elements/adw-about-dialog.ts +4 -2
- package/src/elements/adw-action-row.ts +6 -0
- package/src/elements/adw-bottom-sheet.ts +6 -0
- package/src/elements/adw-navigation-view.ts +6 -0
- package/src/elements/adw-switch-row.ts +6 -0
- package/src/elements/gtk-image.ts +55 -24
- package/src/elements/gtk-scrolled-window.ts +37 -1
- package/src/elements/gtk-window-controls.ts +1 -1
- package/src/elements/gtk-window.ts +1 -1
- package/src/file-chooser-host.ts +2 -0
- package/src/file-chooser.spec.ts +3 -5
- package/src/gobject-door.spec.ts +44 -0
- package/src/gobject-elements.ts +22 -0
- package/src/gtk-font-dialog-button.spec.ts +3 -1
- package/src/gtk-scrolled-window.spec.ts +77 -0
- package/src/icon-registry.ts +13 -7
- package/src/icon-theme.spec.ts +182 -0
- package/src/icon-theme.ts +178 -0
- package/src/index.ts +3 -0
- package/src/namespace/gtk.ts +2 -0
- package/src/navigation-view.spec.ts +4 -4
- package/src/shared-tree-builder.ts +2 -1
- package/src/styles.generated.ts +1 -1
- package/src/test.browser.mts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/adwaita-web",
|
|
3
|
-
"version": "0.64.1-next.
|
|
3
|
+
"version": "0.64.1-next.244",
|
|
4
4
|
"description": "Adwaita/Libadwaita web components for browser targets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -58,14 +58,14 @@
|
|
|
58
58
|
"@codemirror/language": "^6.12.4",
|
|
59
59
|
"@codemirror/state": "^6.7.0",
|
|
60
60
|
"@codemirror/view": "^6.43.4",
|
|
61
|
-
"@gjsify/adwaita-core": "0.64.1-next.
|
|
62
|
-
"@gjsify/adwaita-fonts": "0.64.1-next.
|
|
63
|
-
"@gjsify/adwaita-icons": "0.64.1-next.
|
|
64
|
-
"@gjsify/gtksource-core": "0.64.1-next.
|
|
61
|
+
"@gjsify/adwaita-core": "0.64.1-next.244",
|
|
62
|
+
"@gjsify/adwaita-fonts": "0.64.1-next.244",
|
|
63
|
+
"@gjsify/adwaita-icons": "0.64.1-next.244",
|
|
64
|
+
"@gjsify/gtksource-core": "0.64.1-next.244",
|
|
65
65
|
"@lezer/highlight": "^1.2.3"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
|
-
"@gjsify/app-settings": "0.64.1-next.
|
|
68
|
+
"@gjsify/app-settings": "0.64.1-next.244"
|
|
69
69
|
},
|
|
70
70
|
"peerDependenciesMeta": {
|
|
71
71
|
"@gjsify/app-settings": {
|
|
@@ -73,11 +73,11 @@
|
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@gjsify/app-settings": "0.64.1-next.
|
|
77
|
-
"@gjsify/blueprint": "0.64.1-next.
|
|
78
|
-
"@gjsify/cli": "0.64.1-next.
|
|
79
|
-
"@gjsify/unit": "0.64.1-next.
|
|
80
|
-
"@gjsify/vite-plugin-blueprint": "0.64.1-next.
|
|
76
|
+
"@gjsify/app-settings": "0.64.1-next.244",
|
|
77
|
+
"@gjsify/blueprint": "0.64.1-next.244",
|
|
78
|
+
"@gjsify/cli": "0.64.1-next.244",
|
|
79
|
+
"@gjsify/unit": "0.64.1-next.244",
|
|
80
|
+
"@gjsify/vite-plugin-blueprint": "0.64.1-next.244",
|
|
81
81
|
"sass": "^1.101.0",
|
|
82
82
|
"typescript": "^6.0.3"
|
|
83
83
|
},
|
|
@@ -115,5 +115,5 @@
|
|
|
115
115
|
"url": "https://github.com/gjsify/gjsify/issues"
|
|
116
116
|
},
|
|
117
117
|
"homepage": "https://github.com/gjsify/gjsify/tree/main/packages/web/adwaita-web#readme",
|
|
118
|
-
"gitHead": "
|
|
118
|
+
"gitHead": "9a6ca179c1a74477f23791c5b8f6d5a7bffaee61"
|
|
119
119
|
}
|
package/scss/_about_dialog.scss
CHANGED
|
@@ -149,10 +149,10 @@ adw-about-dialog {
|
|
|
149
149
|
height: 96px;
|
|
150
150
|
margin-top: var(--spacing-s);
|
|
151
151
|
margin-bottom: var(--spacing-m);
|
|
152
|
-
//
|
|
152
|
+
// A symbolic glyph scales with the box; the fill stays `.adw-icon`'s `currentColor`,
|
|
153
|
+
// which `.adw-icon-color` turns off for a registered colour application icon.
|
|
153
154
|
-webkit-mask-size: contain;
|
|
154
155
|
mask-size: contain;
|
|
155
|
-
background-color: currentColor;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
// --- App name (title-1) ---
|
package/scss/_icon.scss
CHANGED
|
@@ -96,3 +96,21 @@
|
|
|
96
96
|
.adw-icon:not([class*='adw-icon--']) {
|
|
97
97
|
background-color: transparent;
|
|
98
98
|
}
|
|
99
|
+
|
|
100
|
+
// A FULL-COLOUR icon — what `registerColorIcon()` registers and `<gtk-image>` resolves a
|
|
101
|
+
// name to before (or, for `-symbolic` names, after) the symbolic glyph, in the order GTK's
|
|
102
|
+
// theme tries them. It is drawn as-is, the way GTK draws `hicolor/…/apps/<app-id>.svg`: no
|
|
103
|
+
// mask and no `currentColor` fill, only the image the element puts in `background-image`.
|
|
104
|
+
//
|
|
105
|
+
// The modifier is NOT `adw-icon--…`, so the rule above never sees it as a mask class.
|
|
106
|
+
// Two classes beat both `:where(.adw-icon)` and the one-class sizing rules a context sets
|
|
107
|
+
// (`.adw-about-dialog-icon`'s mask-size), so an icon box drawn in colour never picks up a
|
|
108
|
+
// leftover mask from them.
|
|
109
|
+
.adw-icon.adw-icon-color {
|
|
110
|
+
mask-image: none;
|
|
111
|
+
-webkit-mask-image: none;
|
|
112
|
+
background-color: transparent;
|
|
113
|
+
background-position: center;
|
|
114
|
+
background-repeat: no-repeat;
|
|
115
|
+
background-size: contain;
|
|
116
|
+
}
|
package/src/adw-window.spec.ts
CHANGED
|
@@ -54,6 +54,10 @@ export const AdwWindowTest = async () => {
|
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
await it('keeps 360×200 as a minimum and fills the viewport without an authored size', async () => {
|
|
57
|
+
// The window fills its containing block, which is the viewport only once the
|
|
58
|
+
// browser's default 8px body margin is gone (an app resets it, the test page does not).
|
|
59
|
+
const margin = document.body.style.margin;
|
|
60
|
+
document.body.style.margin = '0';
|
|
57
61
|
const win = makeWindow();
|
|
58
62
|
expect(win.style.minWidth).toBe('360px');
|
|
59
63
|
expect(win.style.minHeight).toBe('200px');
|
|
@@ -62,6 +66,7 @@ export const AdwWindowTest = async () => {
|
|
|
62
66
|
expect(Math.round(win.getBoundingClientRect().width)).toBe(document.documentElement.clientWidth);
|
|
63
67
|
expect(Math.round(win.getBoundingClientRect().height)).toBe(window.innerHeight);
|
|
64
68
|
win.remove();
|
|
69
|
+
document.body.style.margin = margin;
|
|
65
70
|
});
|
|
66
71
|
|
|
67
72
|
await it('width-request/height-request override the minimum; gtk-window has none', async () => {
|
|
@@ -117,6 +122,8 @@ export const AdwWindowTest = async () => {
|
|
|
117
122
|
|
|
118
123
|
win.remove();
|
|
119
124
|
document.body.appendChild(win);
|
|
125
|
+
// 360 is a MINIMUM, so a 200px box needs the request lowered as well.
|
|
126
|
+
win.setAttribute('width-request', '100');
|
|
120
127
|
win.setAttribute('width', '200');
|
|
121
128
|
await settle();
|
|
122
129
|
|
package/src/application.spec.ts
CHANGED
|
@@ -53,8 +53,6 @@ const subject: ApplicationSubject = {
|
|
|
53
53
|
|
|
54
54
|
export const ApplicationTest = async () => {
|
|
55
55
|
await driveApplicationVectors(subject, { describe, it, expect });
|
|
56
|
-
// The page is shared with every other suite: a window left in it moves their layout.
|
|
57
|
-
for (const element of made.splice(0)) element.remove();
|
|
58
56
|
|
|
59
57
|
await describe('adwaita-web: Application (ADR 0098 § 3)', async () => {
|
|
60
58
|
await it('has a vector for every unlocked vfunc', () => {
|
|
@@ -102,7 +100,9 @@ export const ApplicationTest = async () => {
|
|
|
102
100
|
app.connect('activate', () => app.quit());
|
|
103
101
|
await app.runAsync([]);
|
|
104
102
|
expect(window.isConnected).toBe(false);
|
|
105
|
-
made.
|
|
103
|
+
made.splice(made.indexOf(window), 1);
|
|
106
104
|
});
|
|
107
105
|
});
|
|
106
|
+
// The page is shared with every other suite: a window left in it moves their layout.
|
|
107
|
+
for (const element of made.splice(0)) element.remove();
|
|
108
108
|
};
|
|
@@ -63,7 +63,55 @@ export class Canvas2DBridgeElement extends HostElement {
|
|
|
63
63
|
private _drawFrame: number | null = null;
|
|
64
64
|
|
|
65
65
|
static get observedAttributes() {
|
|
66
|
-
return ['content-width', 'content-height'];
|
|
66
|
+
return ['content-width', 'content-height', 'focusable', 'can-focus'];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* `Gtk.Widget:focusable`, whether the widget can hold the keyboard focus. `Gtk.DrawingArea`
|
|
71
|
+
* leaves it FALSE, so it is read by PRESENCE; a blueprint's `focusable: true` writes it.
|
|
72
|
+
*/
|
|
73
|
+
get focusable(): boolean {
|
|
74
|
+
return this.hasAttribute('focusable');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
set focusable(v: boolean) {
|
|
78
|
+
this.toggleAttribute('focusable', !!v);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** `Gtk.Widget:can-focus`, TRUE by default, so it is read by VALUE. */
|
|
82
|
+
get canFocus(): boolean {
|
|
83
|
+
return this.getAttribute('can-focus') !== 'false';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
set canFocus(v: boolean) {
|
|
87
|
+
this.setAttribute('can-focus', v ? 'true' : 'false');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get can_focus(): boolean {
|
|
91
|
+
return this.canFocus;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
set can_focus(v: boolean) {
|
|
95
|
+
this.canFocus = v;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* `gtk_widget_grab_focus`: moves the keyboard focus here and says whether it landed.
|
|
100
|
+
* FALSE when the widget is not focusable or cannot focus, as in GTK.
|
|
101
|
+
*/
|
|
102
|
+
grab_focus(): boolean {
|
|
103
|
+
if (!this.focusable || !this.canFocus) return false;
|
|
104
|
+
this.focus();
|
|
105
|
+
return this.ownerDocument.activeElement === this;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
grabFocus(): boolean {
|
|
109
|
+
return this.grab_focus();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private _syncFocus(): void {
|
|
113
|
+
if (this.focusable && this.canFocus) this.tabIndex = 0;
|
|
114
|
+
else this.removeAttribute('tabindex');
|
|
67
115
|
}
|
|
68
116
|
|
|
69
117
|
/** `Gtk.DrawingArea:content-width`, the minimum and natural width. Defaults to 0. */
|
|
@@ -161,6 +209,7 @@ export class Canvas2DBridgeElement extends HostElement {
|
|
|
161
209
|
this.style.overflow = 'hidden';
|
|
162
210
|
this.style.position = 'relative';
|
|
163
211
|
this._publishSize();
|
|
212
|
+
this._syncFocus();
|
|
164
213
|
this._observe();
|
|
165
214
|
}
|
|
166
215
|
|
|
@@ -171,6 +220,7 @@ export class Canvas2DBridgeElement extends HostElement {
|
|
|
171
220
|
|
|
172
221
|
attributeChangedCallback(name: string) {
|
|
173
222
|
if (name === 'content-width' || name === 'content-height') this._publishSize();
|
|
223
|
+
else this._syncFocus();
|
|
174
224
|
}
|
|
175
225
|
|
|
176
226
|
private _publishSize(): void {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// DOM-level tests for <gjsify-canvas2d-bridge>: the keyboard focus a game console needs for its keys.
|
|
2
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
3
|
+
|
|
4
|
+
import type { Canvas2DBridgeElement } from './canvas2d/canvas2d-bridge.js';
|
|
5
|
+
|
|
6
|
+
function mount(attrs: Record<string, string> = {}): { el: Canvas2DBridgeElement; host: HTMLElement } {
|
|
7
|
+
const host = document.createElement('div');
|
|
8
|
+
document.body.appendChild(host);
|
|
9
|
+
const el = document.createElement('gjsify-canvas2d-bridge') as Canvas2DBridgeElement;
|
|
10
|
+
for (const [name, value] of Object.entries(attrs)) el.setAttribute(name, value);
|
|
11
|
+
host.appendChild(el);
|
|
12
|
+
return { el, host };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const Canvas2DBridgeTest = async () => {
|
|
16
|
+
await describe('<gjsify-canvas2d-bridge> focus', async () => {
|
|
17
|
+
await it('is not focusable by default, so grab_focus returns false', () => {
|
|
18
|
+
const { el, host } = mount();
|
|
19
|
+
expect(el.focusable).toBe(false);
|
|
20
|
+
expect(el.grab_focus()).toBe(false);
|
|
21
|
+
expect(document.activeElement === el).toBe(false);
|
|
22
|
+
host.remove();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
await it('grab_focus takes the keyboard focus when focusable', () => {
|
|
26
|
+
const { el, host } = mount({ focusable: '' });
|
|
27
|
+
expect(el.grab_focus()).toBe(true);
|
|
28
|
+
expect(document.activeElement === el).toBe(true);
|
|
29
|
+
host.remove();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await it('a focusable set after connect makes the element focusable', () => {
|
|
33
|
+
const { el, host } = mount();
|
|
34
|
+
el.focusable = true;
|
|
35
|
+
expect(el.grab_focus()).toBe(true);
|
|
36
|
+
host.remove();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await it('can-focus false refuses the focus', () => {
|
|
40
|
+
const { el, host } = mount({ focusable: '' });
|
|
41
|
+
el.canFocus = false;
|
|
42
|
+
expect(el.can_focus).toBe(false);
|
|
43
|
+
expect(el.grab_focus()).toBe(false);
|
|
44
|
+
host.remove();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
};
|
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
// Properties / attributes (all reflected, mirroring the AdwAboutDialog GObject
|
|
20
20
|
// properties of the same name, hyphenated):
|
|
21
21
|
// application-name the application's display name (shown as a title-1)
|
|
22
|
-
// application-icon
|
|
23
|
-
//
|
|
22
|
+
// application-icon an icon name, by convention the application ID; drawn in
|
|
23
|
+
// colour once `registerColorIcon(id, …)` registered it, else
|
|
24
|
+
// as a symbolic glyph or `image-missing` (`<gtk-image>`'s
|
|
25
|
+
// lookup); absent one, no icon is drawn
|
|
24
26
|
// developer-name the developer / vendor line below the name
|
|
25
27
|
// version the version, shown as a clickable pill
|
|
26
28
|
// comments a longer description (Details page)
|
|
@@ -27,6 +27,7 @@ import { ActionRowState } from '@gjsify/adwaita-core';
|
|
|
27
27
|
import { bindEmptySections } from '../empty-sections.js';
|
|
28
28
|
import { applyRowTooltip } from '../row-tooltip.js';
|
|
29
29
|
import { bindSlottedChildren } from '../slotted-children.js';
|
|
30
|
+
import { installElementApi } from '../gobject-elements.js';
|
|
30
31
|
import type { DispatchedSignals } from '../signals.js';
|
|
31
32
|
import { type AdwRowActivation, attachRowActivation } from './row-activation.js';
|
|
32
33
|
import { applyRowLineClamp, parseRowLinesAttribute } from '../row-line-clamp.js';
|
|
@@ -97,6 +98,11 @@ export class AdwActionRow extends HTMLElement {
|
|
|
97
98
|
return this._state.activatable;
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
constructor() {
|
|
102
|
+
super();
|
|
103
|
+
installElementApi(this);
|
|
104
|
+
}
|
|
105
|
+
|
|
100
106
|
connectedCallback() {
|
|
101
107
|
if (this._initialized) {
|
|
102
108
|
// Re-entering a document: `disconnectedCallback` dropped the sensitivity
|
|
@@ -64,6 +64,7 @@ import type {
|
|
|
64
64
|
} from '@gjsify/adwaita-core';
|
|
65
65
|
|
|
66
66
|
import { bindSlottedChildren, type AdwSlot } from '../slotted-children.js';
|
|
67
|
+
import { installElementApi } from '../gobject-elements.js';
|
|
67
68
|
import type { DispatchedSignals } from '../signals.js';
|
|
68
69
|
|
|
69
70
|
/** The persistent content. Child of <adw-bottom-sheet>; consumed at connect time. */
|
|
@@ -209,6 +210,11 @@ export class AdwBottomSheet extends HTMLElement {
|
|
|
209
210
|
return outcome;
|
|
210
211
|
}
|
|
211
212
|
|
|
213
|
+
constructor() {
|
|
214
|
+
super();
|
|
215
|
+
installElementApi(this);
|
|
216
|
+
}
|
|
217
|
+
|
|
212
218
|
connectedCallback() {
|
|
213
219
|
if (this._initialized) return;
|
|
214
220
|
this._initialized = true;
|
|
@@ -55,6 +55,7 @@ import { BACK_BUTTON_FALLBACK_TOOLTIP, NavigationViewState, describeNavigationDi
|
|
|
55
55
|
import type { AdwNavigationPageProps, NavigationDiagnostic, NavigationStackChange } from '@gjsify/adwaita-core';
|
|
56
56
|
|
|
57
57
|
import { bindSlottedChildren } from '../slotted-children.js';
|
|
58
|
+
import { installElementApi } from '../gobject-elements.js';
|
|
58
59
|
import type { DispatchedSignals } from '../signals.js';
|
|
59
60
|
|
|
60
61
|
/** The page properties the core owns, read off the element's attributes. */
|
|
@@ -215,6 +216,11 @@ export class AdwNavigationView extends HTMLElement {
|
|
|
215
216
|
return this._state.backButtonTooltip();
|
|
216
217
|
}
|
|
217
218
|
|
|
219
|
+
constructor() {
|
|
220
|
+
super();
|
|
221
|
+
installElementApi(this);
|
|
222
|
+
}
|
|
223
|
+
|
|
218
224
|
connectedCallback() {
|
|
219
225
|
if (this._initialized) return;
|
|
220
226
|
this._initialized = true;
|
|
@@ -29,6 +29,7 @@ import { SwitchRowState, deriveRowLabels } from '@gjsify/adwaita-core';
|
|
|
29
29
|
import { bindEmptySections } from '../empty-sections.js';
|
|
30
30
|
import { applyRowTooltip } from '../row-tooltip.js';
|
|
31
31
|
import { bindSlottedChildren } from '../slotted-children.js';
|
|
32
|
+
import { installElementApi } from '../gobject-elements.js';
|
|
32
33
|
import type { DispatchedSignals } from '../signals.js';
|
|
33
34
|
import { applyRowLineClamp, parseRowLinesAttribute } from '../row-line-clamp.js';
|
|
34
35
|
|
|
@@ -75,6 +76,11 @@ export class AdwSwitchRow extends HTMLElement {
|
|
|
75
76
|
this.toggleAttribute('active', !!value);
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
constructor() {
|
|
80
|
+
super();
|
|
81
|
+
installElementApi(this);
|
|
82
|
+
}
|
|
83
|
+
|
|
78
84
|
connectedCallback() {
|
|
79
85
|
if (this._initialized) return;
|
|
80
86
|
this._initialized = true;
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
// <gtk-image> — the
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// <gtk-image> — the icon node as an element. A symbolic icon is a CSS-masked box whose
|
|
2
|
+
// glyph comes from a generated `.adw-icon--<name>` class and whose colour is
|
|
3
|
+
// `currentColor`, so it themes with whatever contains it. A full-colour icon registered
|
|
4
|
+
// through `registerColorIcon` is the same box with the image as its background and no
|
|
5
|
+
// mask. Which of the two a name reaches is decided once, in `../icon-theme.ts`, in the
|
|
6
|
+
// order GTK's own theme tries them; the symbolic name derivation and its guard live once
|
|
7
|
+
// in `@gjsify/adwaita-core`'s {@link normalizeIconName}, the markup once here.
|
|
6
8
|
//
|
|
7
9
|
// Attributes:
|
|
8
|
-
// icon-name —
|
|
9
|
-
//
|
|
10
|
-
// id) never reaches a class — it
|
|
11
|
-
// is what GTK draws for it, rather
|
|
10
|
+
// icon-name — icon name, `-symbolic` suffix optional. A name that is neither a
|
|
11
|
+
// registered colour icon nor a single CSS token (a space, a quote, an
|
|
12
|
+
// unregistered reverse-DNS application id) never reaches a class — it
|
|
13
|
+
// draws {@link MISSING_ICON_NAME}, which is what GTK draws for it, rather
|
|
14
|
+
// than injecting markup.
|
|
12
15
|
// size — rendered edge length in px. Absent leaves the stylesheet's 16px in
|
|
13
16
|
// charge; a context that sizes its own icons through CSS
|
|
14
17
|
// (`.adw-about-dialog-icon`, the split button's 14px arrows) keeps doing
|
|
@@ -30,7 +33,9 @@
|
|
|
30
33
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
|
|
31
34
|
// name derivation composed from @gjsify/adwaita-core.
|
|
32
35
|
|
|
33
|
-
import { normalizeIconName
|
|
36
|
+
import { normalizeIconName } from '@gjsify/adwaita-core';
|
|
37
|
+
|
|
38
|
+
import { lookupIcon, onIconThemeChanged } from '../icon-theme.js';
|
|
34
39
|
|
|
35
40
|
/**
|
|
36
41
|
* The generated mask-class prefix — the one place the `--` spelling appears. `icon-registry`
|
|
@@ -46,6 +51,13 @@ export const MASK_CLASS_PREFIX = 'adw-icon--';
|
|
|
46
51
|
*/
|
|
47
52
|
export const MISSING_ICON_NAME = 'image-missing';
|
|
48
53
|
|
|
54
|
+
/**
|
|
55
|
+
* The modifier a colour icon carries. Deliberately NOT under {@link MASK_CLASS_PREFIX}: the
|
|
56
|
+
* stale-class sweep below and `.adw-icon:not([class*='adw-icon--'])` both read that prefix
|
|
57
|
+
* as "a mask is applied", and a colour icon has none.
|
|
58
|
+
*/
|
|
59
|
+
export const COLOR_ICON_CLASS = 'adw-icon-color';
|
|
60
|
+
|
|
49
61
|
export class GtkImage extends HTMLElement {
|
|
50
62
|
static get observedAttributes() {
|
|
51
63
|
return ['icon-name', 'size'];
|
|
@@ -62,9 +74,10 @@ export class GtkImage extends HTMLElement {
|
|
|
62
74
|
}
|
|
63
75
|
|
|
64
76
|
/**
|
|
65
|
-
* The name
|
|
66
|
-
*
|
|
67
|
-
*
|
|
77
|
+
* The name the SYMBOLIC lookup works with — `''` when the icon-name is absent, empty,
|
|
78
|
+
* or not usable as one CSS token. A registered colour icon can still draw for a name
|
|
79
|
+
* that answers `''` here (`org.gnome.Builder`); whether a name draws anything but
|
|
80
|
+
* `image-missing` is `isIconAvailable`'s question.
|
|
68
81
|
*
|
|
69
82
|
* `''` does NOT mean the box draws nothing: an unusable name that was given still
|
|
70
83
|
* draws {@link MISSING_ICON_NAME}. Whether an icon was ASKED FOR is the raw
|
|
@@ -85,10 +98,18 @@ export class GtkImage extends HTMLElement {
|
|
|
85
98
|
else this.setAttribute('size', String(value));
|
|
86
99
|
}
|
|
87
100
|
|
|
101
|
+
private _unsubscribe: (() => void) | null = null;
|
|
102
|
+
|
|
88
103
|
connectedCallback() {
|
|
104
|
+
this._unsubscribe ??= onIconThemeChanged(() => this._render());
|
|
89
105
|
this._render();
|
|
90
106
|
}
|
|
91
107
|
|
|
108
|
+
disconnectedCallback() {
|
|
109
|
+
this._unsubscribe?.();
|
|
110
|
+
this._unsubscribe = null;
|
|
111
|
+
}
|
|
112
|
+
|
|
92
113
|
attributeChangedCallback() {
|
|
93
114
|
this._render();
|
|
94
115
|
}
|
|
@@ -102,20 +123,30 @@ export class GtkImage extends HTMLElement {
|
|
|
102
123
|
// would skip the entry after each hit.
|
|
103
124
|
const stale = [...this.classList].filter((existing) => existing.startsWith(MASK_CLASS_PREFIX));
|
|
104
125
|
this.classList.remove(...stale);
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
126
|
+
const found = lookupIcon(this.getAttribute('icon-name'));
|
|
127
|
+
this.classList.toggle(COLOR_ICON_CLASS, found?.kind === 'color');
|
|
128
|
+
if (found?.kind === 'color') {
|
|
129
|
+
// Drawn as-is: `_icon.scss` drops the mask and the `currentColor` fill for this
|
|
130
|
+
// modifier, so only the image paints. `url` is a data-URI or a resolved href;
|
|
131
|
+
// the quote and backslash escapes keep it one CSS string either way.
|
|
132
|
+
this.style.backgroundImage = `url("${found.url.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}")`;
|
|
133
|
+
} else {
|
|
134
|
+
this.style.removeProperty('background-image');
|
|
135
|
+
}
|
|
136
|
+
if (found?.kind === 'symbolic') {
|
|
137
|
+
this.classList.add(`${MASK_CLASS_PREFIX}${found.name}`);
|
|
138
|
+
} else if (found?.kind === 'missing') {
|
|
139
|
+
// A name was GIVEN and nothing answers it — not a colour icon, and not one CSS
|
|
140
|
+
// token (a space, a quote, an unregistered reverse-DNS application id). GTK has
|
|
141
|
+
// no such state: `gtk_icon_theme_lookup_icon` never returns NULL, it returns the
|
|
142
|
+
// always-available `image-missing` (refs/gtk/gtk/gtkicontheme.c —
|
|
143
|
+
// `gtk_icon_paintable_get_icon_name`'s own docs say so). So the same glyph is
|
|
144
|
+
// drawn here rather than an invisible 16px hole, which is what
|
|
145
|
+
// `<adw-button-row start-icon-name="a b">` reserved and showed.
|
|
115
146
|
this.classList.add(`${MASK_CLASS_PREFIX}${MISSING_ICON_NAME}`);
|
|
116
147
|
}
|
|
117
148
|
|
|
118
|
-
//
|
|
149
|
+
// An icon box has no text, so assistive tech has nothing to announce — the
|
|
119
150
|
// accessible name belongs to the control that HOSTS the icon.
|
|
120
151
|
this.setAttribute('aria-hidden', 'true');
|
|
121
152
|
|
|
@@ -394,6 +394,36 @@ export class GtkScrolledWindow extends HTMLElement {
|
|
|
394
394
|
return this._bars?.vertical ?? null;
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
+
/** `Gtk.ScrolledWindow:child` — the widget inside the viewport, or the one waiting to be adopted. */
|
|
398
|
+
get child(): Element | null {
|
|
399
|
+
return this._built ? this._viewport.firstElementChild : this.firstElementChild;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
set child(value: Element | null) {
|
|
403
|
+
this.set_child(value);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
get_child(): Element | null {
|
|
407
|
+
return this.child;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* `gtk_scrolled_window_set_child`: replace the child, which sits in the viewport. Before the
|
|
412
|
+
* first connect there is no viewport yet, so the child waits in the light DOM where
|
|
413
|
+
* `connectedCallback` adopts it the way it adopts a declared one.
|
|
414
|
+
*/
|
|
415
|
+
set_child(child: Element | null): void {
|
|
416
|
+
const current = this.child;
|
|
417
|
+
if (child === current) return;
|
|
418
|
+
if (this._built) {
|
|
419
|
+
this._viewport.replaceChildren(...(child === null ? [] : [child]));
|
|
420
|
+
} else {
|
|
421
|
+
current?.remove();
|
|
422
|
+
if (child !== null) this.appendChild(child);
|
|
423
|
+
}
|
|
424
|
+
this.dispatchEvent(new CustomEvent('notify::child', { bubbles: true, detail: { child } }));
|
|
425
|
+
}
|
|
426
|
+
|
|
397
427
|
/** The `<gtk-viewport>` the child sits in — the one GTK's `set_child` builds for itself. */
|
|
398
428
|
get viewport(): GtkViewport {
|
|
399
429
|
return this._viewport;
|
|
@@ -408,7 +438,13 @@ export class GtkScrolledWindow extends HTMLElement {
|
|
|
408
438
|
horizontal.setAttribute('orientation', 'horizontal');
|
|
409
439
|
vertical.setAttribute('orientation', 'vertical');
|
|
410
440
|
this._bars = { horizontal, vertical };
|
|
411
|
-
|
|
441
|
+
// `Gtk.ScrolledWindow:child` is a widget PROPERTY, so an authored `child: …` names the
|
|
442
|
+
// slot by that name; the bare child is the default. Both land in the viewport.
|
|
443
|
+
bindSlottedChildren(this, [{ name: 'child', into: this._viewport }, { into: this._viewport }]).install(
|
|
444
|
+
this._viewport,
|
|
445
|
+
vertical,
|
|
446
|
+
horizontal,
|
|
447
|
+
);
|
|
412
448
|
this.setAttribute('role', 'generic');
|
|
413
449
|
// A bar is a VIEW of the viewport's adjustment, so the viewport's
|
|
414
450
|
// `value-changed` is what moves the content and a bar's is what moves the
|
|
@@ -280,7 +280,7 @@ export class GtkWindowControls extends HTMLElement {
|
|
|
280
280
|
if (token === 'maximize') {
|
|
281
281
|
if (!sovereign || !this._windowFlag(window, 'resizable')) return null;
|
|
282
282
|
// :347-350 — the glyph and the label follow the window's state, not the token.
|
|
283
|
-
const maximized = window.hasAttribute('maximized');
|
|
283
|
+
const maximized = window.hasAttribute('maximized') && window.getAttribute('maximized') !== 'false';
|
|
284
284
|
return this._controlButton(
|
|
285
285
|
maximized
|
|
286
286
|
? { ...TOKEN_BUTTONS.maximize, icon: RESTORE_ICON_NAME, label: RESTORE_LABEL }
|
|
@@ -281,7 +281,7 @@ export class GtkWindow extends HTMLElement {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
private _offByDefault(name: string): boolean {
|
|
284
|
-
return this.hasAttribute(name);
|
|
284
|
+
return this.hasAttribute(name) && this.getAttribute(name) !== 'false';
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
connectedCallback() {
|
package/src/file-chooser-host.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
dialogFailedError,
|
|
23
23
|
dismissedError,
|
|
24
24
|
fileFilterAccepts,
|
|
25
|
+
GLibError,
|
|
25
26
|
registerFileChooserDriver,
|
|
26
27
|
type FileChooserDriver,
|
|
27
28
|
type FileChooserHandle,
|
|
@@ -207,6 +208,7 @@ function isAbort(error: unknown): boolean {
|
|
|
207
208
|
|
|
208
209
|
/** The `Gtk.DialogError` a platform rejection becomes: `AbortError` is the user closing the picker. */
|
|
209
210
|
function dialogRejection(error: unknown) {
|
|
211
|
+
if (error instanceof GLibError) return error;
|
|
210
212
|
if (isAbort(error)) return dismissedError();
|
|
211
213
|
const e = error as { name?: string; message?: string } | null;
|
|
212
214
|
if (e && typeof e.message === 'string') return dialogFailedError(`${e.name ?? 'Error'}: ${e.message}`);
|
package/src/file-chooser.spec.ts
CHANGED
|
@@ -316,11 +316,9 @@ export const FileChooserTest = async () => {
|
|
|
316
316
|
const { env, subject } = opfsSubject(dir);
|
|
317
317
|
subject.script({ basename: 'd.txt' });
|
|
318
318
|
await withEnv(env, async () => {
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
) => Promise<{ replace_contents_async: unknown }>;
|
|
323
|
-
await save(null, null);
|
|
319
|
+
const dialog = new Gtk.FileDialog({ initial_name: 'd.txt' });
|
|
320
|
+
// Called on the dialog: a detached `save` has no `this`, which is not what is under test.
|
|
321
|
+
await (dialog.save as unknown as (p: unknown, c: unknown) => Promise<unknown>)(null, null);
|
|
324
322
|
});
|
|
325
323
|
} finally {
|
|
326
324
|
document.createElement = original;
|
package/src/gobject-door.spec.ts
CHANGED
|
@@ -62,6 +62,34 @@ GObject.registerClass(
|
|
|
62
62
|
GoWebHexdump as never,
|
|
63
63
|
);
|
|
64
64
|
|
|
65
|
+
const SensitiveTemplate = tree(`template $GoWebSensitive : Adw.Bin {
|
|
66
|
+
child: Gtk.Box {
|
|
67
|
+
Gtk.Button btn { sensitive: bind template.enabled; }
|
|
68
|
+
Gtk.Switch sw { sensitive: bind template.enabled inverted; }
|
|
69
|
+
Gtk.Label lbl { sensitive: bind template.enabled; }
|
|
70
|
+
};
|
|
71
|
+
}`);
|
|
72
|
+
|
|
73
|
+
class GoWebSensitive extends Adw.Bin {
|
|
74
|
+
declare _btn: HTMLButtonElement;
|
|
75
|
+
declare _sw: HTMLElement & { disabled: boolean };
|
|
76
|
+
declare _lbl: HTMLElement;
|
|
77
|
+
declare enabled: boolean;
|
|
78
|
+
constructor(params?: Record<string, unknown>) {
|
|
79
|
+
// @ts-expect-error the element's own constructor takes none; the registered class's takes the params
|
|
80
|
+
super(params);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
GObject.registerClass(
|
|
84
|
+
{
|
|
85
|
+
GTypeName: 'GoWebSensitive',
|
|
86
|
+
Template: SensitiveTemplate,
|
|
87
|
+
InternalChildren: ['btn', 'sw', 'lbl'],
|
|
88
|
+
Properties: { enabled: GObject.ParamSpec.boolean('enabled', '', '', GObject.ParamFlags.READWRITE, true) },
|
|
89
|
+
},
|
|
90
|
+
GoWebSensitive as never,
|
|
91
|
+
);
|
|
92
|
+
|
|
65
93
|
export const GObjectDoorTest = async () => {
|
|
66
94
|
await driveGObjectVectors(subject, { describe, it, expect }, GOBJECT_VECTORS);
|
|
67
95
|
await describe('adwaita-web: GObject.registerClass end to end (ADR 0096)', async () => {
|
|
@@ -116,4 +144,20 @@ export const GObjectDoorTest = async () => {
|
|
|
116
144
|
});
|
|
117
145
|
}
|
|
118
146
|
});
|
|
147
|
+
await describe('adwaita-web: Gtk.Widget:sensitive in a template bind', async () => {
|
|
148
|
+
await it('binds sensitive to a template property, following it both ways of the flag', () => {
|
|
149
|
+
const el = new GoWebSensitive();
|
|
150
|
+
expect(el._btn.hasAttribute('disabled')).toBe(false);
|
|
151
|
+
expect(el._lbl.getAttribute('aria-disabled')).toBe(null);
|
|
152
|
+
el.enabled = false;
|
|
153
|
+
expect(el._btn.hasAttribute('disabled')).toBe(true);
|
|
154
|
+
expect((el._btn as unknown as { sensitive: boolean }).sensitive).toBe(false);
|
|
155
|
+
expect(el._lbl.getAttribute('aria-disabled')).toBe('true');
|
|
156
|
+
expect(el._sw.hasAttribute('disabled')).toBe(false);
|
|
157
|
+
el.enabled = true;
|
|
158
|
+
expect(el._btn.hasAttribute('disabled')).toBe(false);
|
|
159
|
+
expect(el._lbl.getAttribute('aria-disabled')).toBe(null);
|
|
160
|
+
expect(el._sw.hasAttribute('disabled')).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
119
163
|
};
|