@gjsify/adwaita-web 0.60.0 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/lib/types/canvas2d/canvas2d-bridge.d.ts +55 -0
- package/lib/types/canvas2d/index.d.ts +1 -0
- package/lib/types/elements/gtk-drop-down.d.ts +4 -0
- package/lib/types/elements/gtk-label.d.ts +4 -0
- package/lib/types/elements/gtk-text-view.d.ts +15 -0
- package/lib/types/file-chooser-host.d.ts +55 -0
- package/lib/types/file-chooser.spec.d.ts +1 -0
- package/lib/types/gtk-platform.spec.d.ts +1 -0
- package/lib/types/namespace/adw.d.ts +1 -0
- package/lib/types/namespace/gdk.d.ts +2 -0
- package/lib/types/namespace/gio.d.ts +3 -0
- package/lib/types/namespace/glib.d.ts +1 -0
- package/lib/types/namespace/gtk-enums.d.ts +22 -0
- package/lib/types/namespace/gtk-platform.d.ts +13 -0
- package/lib/types/namespace/gtk.d.ts +4 -1
- package/lib/types/platform-host.d.ts +2 -0
- package/lib/types/style-manager.d.ts +7 -0
- package/package.json +22 -9
- package/src/canvas2d/canvas2d-bridge.ts +218 -0
- package/src/canvas2d/index.ts +5 -0
- package/src/capabilities.mjs +1 -4
- package/src/construct-vectors.spec.ts +10 -1
- package/src/elements/gtk-drop-down.ts +10 -0
- package/src/elements/gtk-label.ts +10 -0
- package/src/elements/gtk-text-view.ts +40 -0
- package/src/file-chooser-host.ts +421 -0
- package/src/file-chooser.spec.ts +376 -0
- package/src/gobject-door.ts +7 -1
- package/src/gobject-elements.ts +4 -0
- package/src/gtk-drop-down.spec.ts +11 -0
- package/src/gtk-label.spec.ts +12 -0
- package/src/gtk-platform.spec.ts +19 -0
- package/src/gtk-text-view.spec.ts +22 -0
- package/src/namespace/adw.ts +1 -0
- package/src/namespace/gdk.ts +18 -1
- package/src/namespace/gio.ts +20 -2
- package/src/namespace/glib.ts +6 -1
- package/src/namespace/gtk-enums.ts +7 -2
- package/src/namespace/gtk-platform.ts +16 -0
- package/src/namespace/gtk.ts +12 -1
- package/src/platform-host.ts +88 -0
- package/src/shared-tree-builder.ts +40 -0
- package/src/shared-trees.spec.ts +36 -0
- package/src/style-manager.ts +10 -1
- package/src/test.browser.mts +4 -0
- package/src/widget-members.spec.ts +12 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// How the web door reaches the platform for `Gtk.StyleContext`, `Gtk.Settings`, `Gtk.show_uri` and
|
|
2
|
+
// `Gtk.EventControllerKey` (the behaviour is in `@gjsify/adwaita-core`, `gtk-platform.ts`).
|
|
3
|
+
//
|
|
4
|
+
// - styles: one `<style>` element per provider, kept in priority order at the end of `<head>`.
|
|
5
|
+
// - uris: `window.open` with `noopener`; a popup blocker may refuse it outside a user gesture.
|
|
6
|
+
// - keys: `keydown` / `keyup` on the element; a handled key gets `preventDefault()`.
|
|
7
|
+
|
|
8
|
+
import type { HostKeyEvent, PlatformHost } from '@gjsify/adwaita-core';
|
|
9
|
+
|
|
10
|
+
const SHIFT_MASK = 1 << 0;
|
|
11
|
+
const CONTROL_MASK = 1 << 2;
|
|
12
|
+
const ALT_MASK = 1 << 3;
|
|
13
|
+
const SUPER_MASK = 1 << 26;
|
|
14
|
+
|
|
15
|
+
const sheets = new Map<object, HTMLStyleElement>();
|
|
16
|
+
|
|
17
|
+
function modifierState(event: KeyboardEvent): number {
|
|
18
|
+
return (
|
|
19
|
+
(event.shiftKey ? SHIFT_MASK : 0) |
|
|
20
|
+
(event.ctrlKey ? CONTROL_MASK : 0) |
|
|
21
|
+
(event.altKey ? ALT_MASK : 0) |
|
|
22
|
+
(event.metaKey ? SUPER_MASK : 0)
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const webPlatformHost: PlatformHost = {
|
|
27
|
+
setStyles(list) {
|
|
28
|
+
if (typeof document === 'undefined') return;
|
|
29
|
+
const keep = new Set(list.map((entry) => entry.owner));
|
|
30
|
+
for (const [owner, element] of sheets) {
|
|
31
|
+
if (keep.has(owner)) continue;
|
|
32
|
+
element.remove();
|
|
33
|
+
sheets.delete(owner);
|
|
34
|
+
}
|
|
35
|
+
// Re-appending in order is what makes the later sheet win at equal specificity.
|
|
36
|
+
for (const { owner, css, priority } of list) {
|
|
37
|
+
let element = sheets.get(owner);
|
|
38
|
+
if (!element) {
|
|
39
|
+
element = document.createElement('style');
|
|
40
|
+
sheets.set(owner, element);
|
|
41
|
+
}
|
|
42
|
+
element.dataset.gtkPriority = String(priority);
|
|
43
|
+
if (element.textContent !== css) element.textContent = css;
|
|
44
|
+
document.head.append(element);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
openUri(uri) {
|
|
49
|
+
if (typeof window === 'undefined') return;
|
|
50
|
+
window.open(uri, '_blank', 'noopener');
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
fontName() {
|
|
54
|
+
if (typeof document === 'undefined' || !document.body) return null;
|
|
55
|
+
const style = getComputedStyle(document.body);
|
|
56
|
+
const family = style.fontFamily
|
|
57
|
+
.split(',')[0]
|
|
58
|
+
?.trim()
|
|
59
|
+
.replace(/^["']|["']$/g, '');
|
|
60
|
+
const px = Number.parseFloat(style.fontSize);
|
|
61
|
+
if (!family || !Number.isFinite(px)) return null;
|
|
62
|
+
return `${family} ${Math.round(px * 0.75 * 10) / 10}`;
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
listenKeys(widget, handler) {
|
|
66
|
+
const element = widget as HTMLElement;
|
|
67
|
+
// A bare element takes no keys until it can be focused.
|
|
68
|
+
if (!element.hasAttribute('tabindex')) element.setAttribute('tabindex', '-1');
|
|
69
|
+
const listen = (pressed: boolean) => (event: Event) => {
|
|
70
|
+
const key = event as KeyboardEvent;
|
|
71
|
+
const host: HostKeyEvent = {
|
|
72
|
+
key: key.key,
|
|
73
|
+
keycode: key.keyCode,
|
|
74
|
+
state: modifierState(key),
|
|
75
|
+
pressed,
|
|
76
|
+
};
|
|
77
|
+
if (handler(host)) key.preventDefault();
|
|
78
|
+
};
|
|
79
|
+
const down = listen(true);
|
|
80
|
+
const up = listen(false);
|
|
81
|
+
element.addEventListener('keydown', down);
|
|
82
|
+
element.addEventListener('keyup', up);
|
|
83
|
+
return () => {
|
|
84
|
+
element.removeEventListener('keydown', down);
|
|
85
|
+
element.removeEventListener('keyup', up);
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
};
|
|
@@ -420,11 +420,51 @@ function writeLook(el: HTMLElement, node: SharedTreeNode, menus?: SharedTreeNode
|
|
|
420
420
|
writeExtensions(el, node);
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
+
/** The `GtkGridLayoutChild` properties `layout { }` may carry, and the CSS Grid line each one is. */
|
|
424
|
+
const GRID_CELL: Readonly<Record<string, { property: 'gridRow' | 'gridColumn'; span: boolean }>> = {
|
|
425
|
+
row: { property: 'gridRow', span: false },
|
|
426
|
+
column: { property: 'gridColumn', span: false },
|
|
427
|
+
'row-span': { property: 'gridRow', span: true },
|
|
428
|
+
'column-span': { property: 'gridColumn', span: true },
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* ADR 0092's `layout { }`: the child's cell in a `Gtk.Grid`. GTK counts from 0 and CSS Grid from 1,
|
|
433
|
+
* and a span is `span N` after the start line. The one layout manager here is the grid, so a
|
|
434
|
+
* layout under any other parent, a key the grid child has no spelling for, or a value that is not
|
|
435
|
+
* a whole number is REFUSED by name: the cell would otherwise be dropped at exit 0.
|
|
436
|
+
*/
|
|
437
|
+
function writeGridCell(parent: HTMLElement, childEl: HTMLElement, child: SharedTreeNode): void {
|
|
438
|
+
const layout = child.layout ?? {};
|
|
439
|
+
if (parent.localName !== 'gtk-grid') {
|
|
440
|
+
throw new Error(
|
|
441
|
+
`layout (${Object.keys(layout).join(', ')}) on \`${child.tag}\` under \`${parent.localName}\`: ` +
|
|
442
|
+
"this renderer's only layout manager is the grid, so the placement would be dropped.",
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
const cell = { row: 0, column: 0, 'row-span': 1, 'column-span': 1 } as Record<string, number>;
|
|
446
|
+
for (const [key, value] of Object.entries(layout)) {
|
|
447
|
+
if (GRID_CELL[key] === undefined) {
|
|
448
|
+
throw new Error(
|
|
449
|
+
`layout \`${key}\` on \`${child.tag}\` reaches nothing: a grid child takes ` +
|
|
450
|
+
`${Object.keys(GRID_CELL).join(', ')}.`,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
|
454
|
+
throw new Error(`layout \`${key}: ${String(value)}\` on \`${child.tag}\` is not a whole number.`);
|
|
455
|
+
}
|
|
456
|
+
cell[key] = value;
|
|
457
|
+
}
|
|
458
|
+
childEl.style.gridRow = `${cell.row + 1} / span ${cell['row-span']}`;
|
|
459
|
+
childEl.style.gridColumn = `${cell.column + 1} / span ${cell['column-span']}`;
|
|
460
|
+
}
|
|
461
|
+
|
|
423
462
|
/** The built children of `node`, placed (`page`, `slot`) but not yet appended to `el`. */
|
|
424
463
|
function buildChildren(el: HTMLElement, node: SharedTreeNode, record: BuildRecord): HTMLElement[] {
|
|
425
464
|
const built: HTMLElement[] = [];
|
|
426
465
|
for (const child of node.children ?? []) {
|
|
427
466
|
const childEl = buildNode(child, record);
|
|
467
|
+
if (child.layout !== undefined) writeGridCell(el, childEl, child);
|
|
428
468
|
if (child.page !== undefined) writePage(el, childEl, child);
|
|
429
469
|
if (child.slot !== undefined) {
|
|
430
470
|
childEl.setAttribute('slot', child.slot);
|
package/src/shared-trees.spec.ts
CHANGED
|
@@ -364,6 +364,42 @@ export const AdwSharedTreesTest = async () => {
|
|
|
364
364
|
).toThrow('has no slot "middle"');
|
|
365
365
|
});
|
|
366
366
|
|
|
367
|
+
// ADR 0092's `layout { }`: a grid child's cell. GTK counts from 0, CSS Grid from 1.
|
|
368
|
+
await it('`layout { }` places a grid child in its cell, spans defaulting to one', () => {
|
|
369
|
+
mounted(
|
|
370
|
+
{
|
|
371
|
+
tag: 'GtkGrid',
|
|
372
|
+
children: [
|
|
373
|
+
{ tag: 'GtkLabel', id: 'a', layout: { row: 0, column: 0 } },
|
|
374
|
+
{ tag: 'GtkLabel', id: 'b', layout: { row: 1, column: 1, 'column-span': 2 } },
|
|
375
|
+
],
|
|
376
|
+
},
|
|
377
|
+
(root) => {
|
|
378
|
+
const a = root.querySelector<HTMLElement>('#a')!;
|
|
379
|
+
const b = root.querySelector<HTMLElement>('#b')!;
|
|
380
|
+
expect(a.style.gridRow).toBe('1 / span 1');
|
|
381
|
+
expect(a.style.gridColumn).toBe('1 / span 1');
|
|
382
|
+
expect(b.style.gridRow).toBe('2 / span 1');
|
|
383
|
+
expect(b.style.gridColumn).toBe('2 / span 2');
|
|
384
|
+
},
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
await it('`layout { }` under a parent that is not a grid is refused BY NAME', () => {
|
|
389
|
+
expect(() =>
|
|
390
|
+
mountSharedTree({ tag: 'GtkBox', children: [{ tag: 'GtkLabel', layout: { row: 0, column: 0 } }] }),
|
|
391
|
+
).toThrow('only layout manager is the grid');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
await it('a `layout { }` key or value the grid child cannot take is refused BY NAME', () => {
|
|
395
|
+
expect(() =>
|
|
396
|
+
mountSharedTree({ tag: 'GtkGrid', children: [{ tag: 'GtkLabel', layout: { colum: 0 } }] }),
|
|
397
|
+
).toThrow('layout `colum`');
|
|
398
|
+
expect(() =>
|
|
399
|
+
mountSharedTree({ tag: 'GtkGrid', children: [{ tag: 'GtkLabel', layout: { row: 1.5 } }] }),
|
|
400
|
+
).toThrow('is not a whole number');
|
|
401
|
+
});
|
|
402
|
+
|
|
367
403
|
// `child: Gtk.Label {…}` in a `.blp` authors `slot: 'child'` — the widget's own
|
|
368
404
|
// GObject property name — for every single-child widget the corpus never places
|
|
369
405
|
// one on. `<adw-clamp>` has no separate box to route into (every child is already
|
package/src/style-manager.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// core's (`style-manager.ts`); this file is the SOURCE: the root's `class` attribute and the media
|
|
4
4
|
// query are the two things that can flip it. Without a `document` (SSR) it is light and never changes.
|
|
5
5
|
|
|
6
|
-
import { StyleManagerBase, type StyleSource } from '@gjsify/adwaita-core';
|
|
6
|
+
import { ColorScheme, StyleManagerBase, type StyleSource } from '@gjsify/adwaita-core';
|
|
7
7
|
|
|
8
8
|
import { isAdwaitaDark } from './accent.js';
|
|
9
9
|
|
|
@@ -22,6 +22,15 @@ export const webStyleSource: StyleSource = {
|
|
|
22
22
|
},
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
// FORCE_DARK / FORCE_LIGHT write the same root class an app writes by hand; DEFAULT takes it away.
|
|
26
|
+
webStyleSource.force = (dark) => {
|
|
27
|
+
if (typeof document === 'undefined') return;
|
|
28
|
+
const root = document.documentElement;
|
|
29
|
+
root.classList.remove('theme-dark', 'theme-light');
|
|
30
|
+
if (dark !== null) root.classList.add(dark ? 'theme-dark' : 'theme-light');
|
|
31
|
+
};
|
|
32
|
+
|
|
25
33
|
export class AdwStyleManager extends StyleManagerBase {
|
|
34
|
+
static readonly ColorScheme = ColorScheme;
|
|
26
35
|
static override source = webStyleSource;
|
|
27
36
|
}
|
package/src/test.browser.mts
CHANGED
|
@@ -142,8 +142,10 @@ import { AdwConstructVectorsTest } from './construct-vectors.spec.js';
|
|
|
142
142
|
import { GObjectDoorTest } from './gobject-door.spec.js';
|
|
143
143
|
import { ApplicationTest } from './application.spec.js';
|
|
144
144
|
import { GdkTest } from './gdk.spec.js';
|
|
145
|
+
import { FileChooserTest } from './file-chooser.spec.js';
|
|
145
146
|
import { WidgetMembersTest } from './widget-members.spec.js';
|
|
146
147
|
import { GLibTest } from './glib.spec.js';
|
|
148
|
+
import { GtkPlatformTest } from './gtk-platform.spec.js';
|
|
147
149
|
import { StyleManagerTest } from './style-manager.spec.js';
|
|
148
150
|
import { GioActionsTest } from './gio.spec.js';
|
|
149
151
|
import { MenuActionsTest } from './menu-actions.spec.js';
|
|
@@ -163,8 +165,10 @@ run({
|
|
|
163
165
|
GObjectDoorTest,
|
|
164
166
|
ApplicationTest,
|
|
165
167
|
GdkTest,
|
|
168
|
+
FileChooserTest,
|
|
166
169
|
WidgetMembersTest,
|
|
167
170
|
GLibTest,
|
|
171
|
+
GtkPlatformTest,
|
|
168
172
|
StyleManagerTest,
|
|
169
173
|
GioActionsTest,
|
|
170
174
|
MenuActionsTest,
|
|
@@ -68,6 +68,18 @@ export const WidgetMembersTest = async () => {
|
|
|
68
68
|
expect(parent.get_parent()).toBe(null);
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
await it('adds, tests and removes a CSS class with the GTK names', () => {
|
|
72
|
+
const widget = make('WidgetMembersCss') as unknown as {
|
|
73
|
+
add_css_class(c: string): void;
|
|
74
|
+
remove_css_class(c: string): void;
|
|
75
|
+
has_css_class(c: string): boolean;
|
|
76
|
+
};
|
|
77
|
+
widget.add_css_class('dim-label');
|
|
78
|
+
expect(widget.has_css_class('dim-label')).toBe(true);
|
|
79
|
+
widget.remove_css_class('dim-label');
|
|
80
|
+
expect(widget.has_css_class('dim-label')).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
71
83
|
await it('takes an action group and drops it again with null', () => {
|
|
72
84
|
const widget = make('WidgetMembersActions');
|
|
73
85
|
const group = new Gio.SimpleActionGroup();
|