@gjsify/adwaita-web 0.64.12-next.279 → 0.64.12-next.281
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/lib/types/elements/adw-alert-dialog.d.ts +0 -8
- package/lib/types/elements/adw-overlay-split-view.d.ts +5 -0
- package/lib/types/elements/adw-view-stack.d.ts +20 -0
- package/lib/types/elements/adw-view-switcher-bar.d.ts +4 -0
- package/lib/types/elements/gtk-box.d.ts +16 -0
- package/lib/types/elements/gtk-menu-button.d.ts +5 -0
- package/lib/types/elements/gtk-scrolled-window.d.ts +15 -2
- package/lib/types/elements/gtk-stack.d.ts +17 -0
- package/lib/types/elements/gtk-window.d.ts +14 -8
- package/lib/types/gobject-elements.d.ts +2 -0
- package/lib/types/gtksource/gtk-source-view.d.ts +2 -1
- package/lib/types/index.d.ts +1 -0
- package/lib/types/namespace/adw.d.ts +1 -1
- package/lib/types/namespace/gdk.d.ts +1 -0
- package/lib/types/namespace/gio.d.ts +1 -0
- package/lib/types/namespace/graphene.d.ts +8 -0
- package/lib/types/namespace/gtk.d.ts +1 -1
- package/lib/types/shared-tree-builder.d.ts +7 -0
- package/lib/types/widget-element.d.ts +25 -0
- package/package.json +12 -12
- package/src/adw-view-switcher-bar.spec.ts +12 -0
- package/src/elements/adw-alert-dialog.ts +0 -8
- package/src/elements/adw-overlay-split-view.ts +17 -0
- package/src/elements/adw-view-stack.ts +50 -0
- package/src/elements/adw-view-switcher-bar.ts +10 -0
- package/src/elements/gtk-box.ts +49 -1
- package/src/elements/gtk-menu-button.ts +17 -0
- package/src/elements/gtk-scrolled-window.ts +60 -15
- package/src/elements/gtk-stack.ts +42 -0
- package/src/elements/gtk-window.ts +43 -10
- package/src/gdk.spec.ts +9 -0
- package/src/gobject-door.spec.ts +134 -0
- package/src/gobject-elements.ts +26 -2
- package/src/gtk-box.spec.ts +38 -0
- package/src/gtk-menu-button.spec.ts +15 -0
- package/src/gtk-stack.spec.ts +26 -0
- package/src/gtk-window.spec.ts +35 -1
- package/src/gtksource/gtk-source-view.ts +18 -3
- package/src/index.ts +2 -0
- package/src/namespace/adw.ts +1 -1
- package/src/namespace/gdk.ts +2 -0
- package/src/namespace/gio.ts +3 -0
- package/src/namespace/graphene.ts +26 -0
- package/src/namespace/gtk.ts +13 -9
- package/src/shared-tree-builder.ts +42 -23
- package/src/split-views.spec.ts +11 -0
- package/src/style-manager.spec.ts +14 -0
- package/src/style-manager.ts +5 -0
- package/src/view-stack.spec.ts +41 -0
- package/src/widget-element.ts +53 -1
- package/src/widget-members.spec.ts +95 -0
|
@@ -168,12 +168,27 @@ export class GtkSourceView extends WidgetElement {
|
|
|
168
168
|
});
|
|
169
169
|
this.session = new EditorSession(this.driver, undefined, adwaitaColorScheme());
|
|
170
170
|
this.session.bindGutters(this.gutters);
|
|
171
|
+
// A write to an adjustment's value scrolls the textarea, as on GTK; the sync below is
|
|
172
|
+
// guarded so the scroll it causes does not write the value back.
|
|
173
|
+
this.vadj.connect('value-changed', () => {
|
|
174
|
+
if (!this.syncing) this.area.scrollTop = this.vadj.value;
|
|
175
|
+
});
|
|
176
|
+
this.hadj.connect('value-changed', () => {
|
|
177
|
+
if (!this.syncing) this.area.scrollLeft = this.hadj.value;
|
|
178
|
+
});
|
|
171
179
|
}
|
|
172
180
|
|
|
181
|
+
private syncing = false;
|
|
182
|
+
|
|
173
183
|
private syncAdjustments(): void {
|
|
174
184
|
const { area } = this;
|
|
175
|
-
this.
|
|
176
|
-
|
|
185
|
+
this.syncing = true;
|
|
186
|
+
try {
|
|
187
|
+
this.vadj.configure(area.scrollTop, 0, area.scrollHeight, 20, area.clientHeight, area.clientHeight);
|
|
188
|
+
this.hadj.configure(area.scrollLeft, 0, area.scrollWidth, 20, area.clientWidth, area.clientWidth);
|
|
189
|
+
} finally {
|
|
190
|
+
this.syncing = false;
|
|
191
|
+
}
|
|
177
192
|
}
|
|
178
193
|
|
|
179
194
|
private readonly onAreaScroll = (): void => this.syncAdjustments();
|
|
@@ -375,7 +390,7 @@ export class GtkSourceView extends WidgetElement {
|
|
|
375
390
|
}
|
|
376
391
|
|
|
377
392
|
/** GJS `connect(name, cb) → id`; the handler's first argument is this view. */
|
|
378
|
-
connect(name: string, callback: (self:
|
|
393
|
+
connect(name: string, callback: (self: this, ...args: never[]) => unknown): number {
|
|
379
394
|
return this.session.connect(name, ((_session: unknown, ...args: never[]) => callback(this, ...args)) as never);
|
|
380
395
|
}
|
|
381
396
|
|
package/src/index.ts
CHANGED
|
@@ -120,6 +120,8 @@ export * as Gio from './namespace/gio.js';
|
|
|
120
120
|
|
|
121
121
|
// Gdk's clipboard (`Display`, `Clipboard`, `ContentProvider`): `./namespace/gdk.ts` says what is absent.
|
|
122
122
|
export * as Gdk from './namespace/gdk.js';
|
|
123
|
+
// Graphene's `Point`, what `Gtk.Widget.compute_point` takes and returns: `./namespace/graphene.ts`.
|
|
124
|
+
export * as Graphene from './namespace/graphene.js';
|
|
123
125
|
// GLib's timers (`idle_add`, `timeout_add`, `source_remove`): `./namespace/glib.ts` says what is absent.
|
|
124
126
|
export * as GLib from './namespace/glib.js';
|
|
125
127
|
|
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, Toast, ToastPriority } from '@gjsify/adwaita-core';
|
|
114
|
+
export { AccentColor, ColorScheme, Toast, ToastPriority } from '@gjsify/adwaita-core';
|
package/src/namespace/gdk.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { CURRENT_TIME as GDK_PLATFORM_CURRENT_TIME, GDK_KEYS, createGdk } from '
|
|
|
9
9
|
|
|
10
10
|
import { webClipboardHost } from '../clipboard-host.js';
|
|
11
11
|
|
|
12
|
+
export { RGBA } from '@gjsify/adwaita-core';
|
|
13
|
+
|
|
12
14
|
export const gdk = createGdk(webClipboardHost);
|
|
13
15
|
|
|
14
16
|
export const Display = gdk.Display;
|
package/src/namespace/gio.ts
CHANGED
|
@@ -29,6 +29,9 @@ import { createAppInfo } from '@gjsify/adwaita-core';
|
|
|
29
29
|
import { webPlatformHost } from '../platform-host.js';
|
|
30
30
|
|
|
31
31
|
export { GioMenu as Menu, GioMenuItem as MenuItem } from '@gjsify/adwaita-core';
|
|
32
|
+
// ADR 0122 § 2: the interface a `Gtk.StringList` implements and an `Implements: [Gio.ListModel]`
|
|
33
|
+
// names. It is a token, not a class — the whole of the subset's interface support.
|
|
34
|
+
export { ListModel } from '@gjsify/adwaita-core';
|
|
32
35
|
export {
|
|
33
36
|
GioFile as File,
|
|
34
37
|
GioListStore as ListStore,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The Graphene half of `@gjsify/adwaita-web`'s vocabulary: `Graphene.Point`, the type
|
|
2
|
+
// `Gtk.Widget.compute_point` takes and returns. The behaviour lives once in `@gjsify/adwaita-core`
|
|
3
|
+
// (`graphene.ts`).
|
|
4
|
+
//
|
|
5
|
+
// `compute_point` is installed on `WidgetElement` HERE rather than in `widget-element.ts`: that file
|
|
6
|
+
// is the helper every element sits one hop below, and a value import of core there would mark every
|
|
7
|
+
// element core-backed. Loading this namespace is what puts the method on a built-in.
|
|
8
|
+
//
|
|
9
|
+
// A TRUE SUBSET: everything else in Graphene is absent, and the `gi://Graphene` arm refuses it by name.
|
|
10
|
+
|
|
11
|
+
import { computePoint, Point } from '@gjsify/adwaita-core';
|
|
12
|
+
|
|
13
|
+
import { WidgetElement } from '../widget-element.js';
|
|
14
|
+
|
|
15
|
+
declare module '../widget-element.js' {
|
|
16
|
+
interface WidgetElement {
|
|
17
|
+
/** `gtk_widget_compute_point`: `[false, (0, 0)]` when the two share no tree. */
|
|
18
|
+
compute_point(target: WidgetElement, point: Point): [boolean, Point];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
WidgetElement.prototype.compute_point = function (this: WidgetElement, target, point) {
|
|
23
|
+
return computePoint(this, target, point);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export { Point };
|
package/src/namespace/gtk.ts
CHANGED
|
@@ -11,14 +11,18 @@
|
|
|
11
11
|
// which is what `<adw-radio>`'s own `why` in the ledger says. So the plain form takes the
|
|
12
12
|
// GIR name and the grouped one keeps its flat export in `src/index.ts`.
|
|
13
13
|
//
|
|
14
|
-
//
|
|
15
|
-
// `adjustment` and a `model` property take.
|
|
16
|
-
// clause-2 namespace carry them,
|
|
17
|
-
// where each one is declared, and the
|
|
18
|
-
// `generated/value-types.mts`. They are
|
|
19
|
-
// where the portable list and adjustment
|
|
20
|
-
//
|
|
21
|
-
//
|
|
14
|
+
// THREE MEMBERS HERE ARE NOT WIDGETS — `Gtk.Adjustment`, `Gtk.StringList` and the
|
|
15
|
+
// `Gtk.StringObject` the list holds, the values an `adjustment` and a `model` property take.
|
|
16
|
+
// ADR 0034 § Amendment 19 is what lets a clause-2 namespace carry them,
|
|
17
|
+
// `CONSTRUCTIBLE_VALUES` in `scripts/value-types.mjs` is where each one is declared, and the
|
|
18
|
+
// GIR answers for all three in gtk-host's committed `generated/value-types.mts`. They are
|
|
19
|
+
// re-exported from `@gjsify/adwaita-core`, which is where the portable list and adjustment
|
|
20
|
+
// already live.
|
|
21
|
+
//
|
|
22
|
+
// `Gtk.StringList` IS A LIST MODEL, NOT AN ARRAY (ADR 0122). The first version of it was an
|
|
23
|
+
// `Array` subclass, which made `model: ['a','b']` and `model: new Gtk.StringList(…)` the same
|
|
24
|
+
// write; it is now what GTK ships — `Implements: [Gio.ListModel]`, items that are
|
|
25
|
+
// `Gtk.StringObject`s, read by `get_n_items` / `get_item` and never by `length` or `[0]`.
|
|
22
26
|
|
|
23
27
|
export { GtkAdjustment as Adjustment } from '@gjsify/adwaita-core';
|
|
24
28
|
// ADR 0100: the file dialog is no element either — `present`-less, it asks the driver
|
|
@@ -89,7 +93,7 @@ export { GtkStack as Stack } from '../elements/gtk-stack.js';
|
|
|
89
93
|
export { GtkStackPage as StackPage } from '../elements/gtk-stack-page.js';
|
|
90
94
|
export { GtkStackSidebar as StackSidebar } from '../elements/gtk-stack-sidebar.js';
|
|
91
95
|
export { GtkStackSwitcher as StackSwitcher } from '../elements/gtk-stack-switcher.js';
|
|
92
|
-
export { GtkStringList as StringList } from '@gjsify/adwaita-core';
|
|
96
|
+
export { GtkStringList as StringList, GtkStringObject as StringObject } from '@gjsify/adwaita-core';
|
|
93
97
|
export { GtkSwitch as Switch } from '../elements/gtk-switch.js';
|
|
94
98
|
export { GtkText as Text } from '../elements/gtk-text.js';
|
|
95
99
|
export { GtkTextView as TextView } from '../elements/gtk-text-view.js';
|
|
@@ -101,6 +101,8 @@ interface BuildRecord {
|
|
|
101
101
|
menus?: SharedTreeNode['menus'];
|
|
102
102
|
/** Present when the tree is the template of a registered class (ADR 0096): handlers and binds go through it. */
|
|
103
103
|
template?: TemplateScope;
|
|
104
|
+
/** A template's handler connections, made once every id exists: `object=` may name a node built later. */
|
|
105
|
+
connects?: (() => void)[];
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
/** Calls `onSize` with the host's measured size, now and on every change; returns its disposer. */
|
|
@@ -229,13 +231,8 @@ function refuseSiblings(node: SharedTreeNode, door: string): void {
|
|
|
229
231
|
);
|
|
230
232
|
}
|
|
231
233
|
|
|
232
|
-
/**
|
|
233
|
-
function
|
|
234
|
-
// ADR 0093 § 2: the whole tree is checked against the capability table before an element is
|
|
235
|
-
// created. A refused `layout` is named with its node and the reason the table gives.
|
|
236
|
-
assertTreeConstructs('adwaita-web', capabilities, node, Object.keys(ARIA_OF));
|
|
237
|
-
if (node.menus !== undefined) record.menus = node.menus;
|
|
238
|
-
const root = buildNode(node, record);
|
|
234
|
+
/** The sibling objects of `node` built in `record`'s id scope, by id; each has to have one, or nothing could ask for it. */
|
|
235
|
+
function buildSiblings(node: SharedTreeNode, record: BuildRecord): Map<string, HTMLElement> {
|
|
239
236
|
const siblings = new Map<string, HTMLElement>();
|
|
240
237
|
for (const sibling of node.siblings ?? []) {
|
|
241
238
|
if (sibling.id === undefined) {
|
|
@@ -245,6 +242,17 @@ function buildTree(node: SharedTreeNode, record: BuildRecord): BuiltTree {
|
|
|
245
242
|
}
|
|
246
243
|
siblings.set(sibling.id, buildNode(sibling, record));
|
|
247
244
|
}
|
|
245
|
+
return siblings;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Every node, its siblings in the same id scope, then the binds and breakpoints that name them. */
|
|
249
|
+
function buildTree(node: SharedTreeNode, record: BuildRecord): BuiltTree {
|
|
250
|
+
// ADR 0093 § 2: the whole tree is checked against the capability table before an element is
|
|
251
|
+
// created. A refused `layout` is named with its node and the reason the table gives.
|
|
252
|
+
assertTreeConstructs('adwaita-web', capabilities, node, Object.keys(ARIA_OF));
|
|
253
|
+
if (node.menus !== undefined) record.menus = node.menus;
|
|
254
|
+
const root = buildNode(node, record);
|
|
255
|
+
const siblings = buildSiblings(node, record);
|
|
248
256
|
for (const bind of record.binds) bindProperty(bind, record);
|
|
249
257
|
for (const host of record.breakpointHosts) bindBreakpoints(host.el, host.node, record);
|
|
250
258
|
return { root, siblings, objects: new Map(record.ids) };
|
|
@@ -265,7 +273,7 @@ function newRecord(
|
|
|
265
273
|
disposers: [],
|
|
266
274
|
...(scope === undefined ? {} : { scope }),
|
|
267
275
|
...(observeSize === undefined ? {} : { observeSize }),
|
|
268
|
-
...(template === undefined ? {} : { template }),
|
|
276
|
+
...(template === undefined ? {} : { template, connects: [] }),
|
|
269
277
|
};
|
|
270
278
|
}
|
|
271
279
|
|
|
@@ -606,9 +614,13 @@ export interface BuiltTemplateTree {
|
|
|
606
614
|
* to their handlers and binds through `scope`, none appended and nothing written to `host`'s
|
|
607
615
|
* attributes, because `host` is still inside its constructor. `finish` and the caller's own append
|
|
608
616
|
* belong to the first `connectedCallback`.
|
|
617
|
+
*
|
|
618
|
+
* Objects written beside the template (`Adw.AlertDialog dialog { }`, `$Learn learn { }`) are built in
|
|
619
|
+
* the same id scope and named in `objects`, which is what `InternalChildren` and `Children` read —
|
|
620
|
+
* `gtk_widget_class_bind_template_child` reaches any object id of the template's builder. They are
|
|
621
|
+
* not in `children`: a sibling is an independent object, placed by the class or not at all.
|
|
609
622
|
*/
|
|
610
623
|
export function buildTemplateTree(tree: SharedTreeNode, scope: TemplateScope, host: HTMLElement): BuiltTemplateTree {
|
|
611
|
-
refuseSiblings(tree, 'buildTemplateTree');
|
|
612
624
|
assertTreeConstructs('adwaita-web', capabilities, tree, Object.keys(ARIA_OF));
|
|
613
625
|
const record = newRecord(undefined, undefined, scope);
|
|
614
626
|
record.menus = tree.menus;
|
|
@@ -616,6 +628,8 @@ export function buildTemplateTree(tree: SharedTreeNode, scope: TemplateScope, ho
|
|
|
616
628
|
collectNode(host, tree, record);
|
|
617
629
|
bindSignals(host, tree, record);
|
|
618
630
|
const children = buildChildren(host, tree, record);
|
|
631
|
+
buildSiblings(tree, record);
|
|
632
|
+
for (const connect of record.connects ?? []) connect();
|
|
619
633
|
for (const bind of record.binds) bindProperty(bind, record);
|
|
620
634
|
for (const breakpointHost of record.breakpointHosts) {
|
|
621
635
|
bindBreakpoints(breakpointHost.el, breakpointHost.node, record);
|
|
@@ -648,21 +662,26 @@ function bindSignals(el: HTMLElement, node: SharedTreeNode, record: BuildRecord)
|
|
|
648
662
|
`the handler '${signal.handler}' for '${name}' uses after, which is not in the GObject subset.`,
|
|
649
663
|
);
|
|
650
664
|
}
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
665
|
+
const template = record.template;
|
|
666
|
+
// `object=` is resolved when the whole tree exists, as `gtk_builder_connect_signals` does: it may
|
|
667
|
+
// name a node built after this one, or a sibling object written beside the template.
|
|
668
|
+
record.connects!.push(() => {
|
|
669
|
+
const object =
|
|
670
|
+
signal.object === undefined
|
|
671
|
+
? undefined
|
|
672
|
+
: signal.object === 'template'
|
|
673
|
+
? template.instance
|
|
674
|
+
: record.ids.get(signal.object);
|
|
675
|
+
if (signal.object !== undefined && object === undefined) {
|
|
676
|
+
throw new Error(
|
|
677
|
+
`the handler '${signal.handler}' for '${name}' names the object '${signal.object}', which nothing in this tree has.`,
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
(el as unknown as GObjectInstance).connect(
|
|
681
|
+
name,
|
|
682
|
+
template.handler(signal.handler, { object, flags: signal.flags }),
|
|
660
683
|
);
|
|
661
|
-
}
|
|
662
|
-
(el as unknown as GObjectInstance).connect(
|
|
663
|
-
name,
|
|
664
|
-
record.template.handler(signal.handler, { object, flags: signal.flags }),
|
|
665
|
-
);
|
|
684
|
+
});
|
|
666
685
|
continue;
|
|
667
686
|
}
|
|
668
687
|
if (signal.object !== undefined || (signal.flags?.length ?? 0) > 0) {
|
package/src/split-views.spec.ts
CHANGED
|
@@ -110,6 +110,17 @@ export const AdwSplitViewsTest = async () => {
|
|
|
110
110
|
host.remove();
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
await it('show_sidebar / get_show_sidebar / set_show_sidebar mirror showSidebar', () => {
|
|
114
|
+
const { view, host } = mount();
|
|
115
|
+
expect(view.get_show_sidebar()).toBe(true);
|
|
116
|
+
view.set_show_sidebar(false);
|
|
117
|
+
expect(view.showSidebar).toBe(false);
|
|
118
|
+
expect(view.show_sidebar).toBe(false);
|
|
119
|
+
view.show_sidebar = true;
|
|
120
|
+
expect(view.showSidebar).toBe(true);
|
|
121
|
+
host.remove();
|
|
122
|
+
});
|
|
123
|
+
|
|
113
124
|
await it('round-trips the property setter through the attribute', () => {
|
|
114
125
|
const { view, host } = mount();
|
|
115
126
|
view.showSidebar = false;
|
|
@@ -39,6 +39,20 @@ export const StyleManagerTest = async () => {
|
|
|
39
39
|
globalThis.matchMedia = real;
|
|
40
40
|
}
|
|
41
41
|
});
|
|
42
|
+
await it('high_contrast follows prefers-contrast: more', () => {
|
|
43
|
+
const real = globalThis.matchMedia;
|
|
44
|
+
try {
|
|
45
|
+
globalThis.matchMedia = ((query: string) => ({
|
|
46
|
+
matches: query.includes('prefers-contrast'),
|
|
47
|
+
media: query,
|
|
48
|
+
})) as never;
|
|
49
|
+
expect(webStyleSource.highContrast?.()).toBe(true);
|
|
50
|
+
globalThis.matchMedia = ((query: string) => ({ matches: false, media: query })) as never;
|
|
51
|
+
expect(webStyleSource.highContrast?.()).toBe(false);
|
|
52
|
+
} finally {
|
|
53
|
+
globalThis.matchMedia = real;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
42
56
|
await it('has a vector row for every vector', () => {
|
|
43
57
|
expect(STYLE_MANAGER_VECTORS.length > 0).toBe(true);
|
|
44
58
|
});
|
package/src/style-manager.ts
CHANGED
|
@@ -14,14 +14,19 @@ export const webStyleSource: StyleSource = {
|
|
|
14
14
|
subscribe(changed) {
|
|
15
15
|
if (typeof document === 'undefined') return () => {};
|
|
16
16
|
const media = globalThis.matchMedia?.('(prefers-color-scheme: dark)');
|
|
17
|
+
const contrast = globalThis.matchMedia?.('(prefers-contrast: more)');
|
|
17
18
|
media?.addEventListener('change', changed);
|
|
19
|
+
contrast?.addEventListener('change', changed);
|
|
18
20
|
const observer = new MutationObserver(changed);
|
|
19
21
|
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
|
20
22
|
return () => {
|
|
21
23
|
media?.removeEventListener('change', changed);
|
|
24
|
+
contrast?.removeEventListener('change', changed);
|
|
22
25
|
observer.disconnect();
|
|
23
26
|
};
|
|
24
27
|
},
|
|
28
|
+
highContrast: () => globalThis.matchMedia?.('(prefers-contrast: more)').matches === true,
|
|
29
|
+
// A page cannot read the desktop's accent (CSS `AccentColor` is uneven, see ADR 0078): no system accent.
|
|
25
30
|
supportsColorSchemes() {
|
|
26
31
|
const matches = (query: string) => globalThis.matchMedia?.(query).matches === true;
|
|
27
32
|
return matches('(prefers-color-scheme: dark)') || matches('(prefers-color-scheme: light)');
|
package/src/view-stack.spec.ts
CHANGED
|
@@ -260,4 +260,45 @@ export const AdwViewStackTest = async () => {
|
|
|
260
260
|
host.remove();
|
|
261
261
|
});
|
|
262
262
|
});
|
|
263
|
+
|
|
264
|
+
await describe('<adw-view-stack> GIR method names', async () => {
|
|
265
|
+
await it('add_titled_with_icon, add_titled and add_named add pages in order', () => {
|
|
266
|
+
const { stack, host } = mountStack([]);
|
|
267
|
+
const [a, b, c] = [1, 2, 3].map(() => document.createElement('div'));
|
|
268
|
+
stack.add_titled_with_icon(a!, 'a', 'A', 'folder-symbolic');
|
|
269
|
+
stack.add_titled(b!, 'b', 'B');
|
|
270
|
+
stack.add_named(c!, 'c');
|
|
271
|
+
expect(stack.pages.map((page) => page.name)).toStrictEqual(['a', 'b', 'c']);
|
|
272
|
+
expect(stack.pages[0]!.icon).toBe('folder');
|
|
273
|
+
expect(stack.pages[1]!.title).toBe('B');
|
|
274
|
+
expect(stack.get_visible_child_name()).toBe('a');
|
|
275
|
+
host.remove();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
await it('remove(child) takes the CHILD, and no argument is still ChildNode.remove()', () => {
|
|
279
|
+
const { stack, host } = mountStack([]);
|
|
280
|
+
const [a, b] = [1, 2].map(() => document.createElement('div'));
|
|
281
|
+
stack.add_titled(a!, 'a', 'A');
|
|
282
|
+
stack.add_titled(b!, 'b', 'B');
|
|
283
|
+
stack.remove(b!);
|
|
284
|
+
expect(stack.pages.map((page) => page.name)).toStrictEqual(['a']);
|
|
285
|
+
expect(b!.parentElement).toBe(null);
|
|
286
|
+
stack.remove();
|
|
287
|
+
expect(stack.parentElement).toBe(null);
|
|
288
|
+
host.remove();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
await it('get/set_visible_child(_name) select, and get_visible_child_name is null for none', () => {
|
|
292
|
+
const { stack, host } = mountStack([]);
|
|
293
|
+
expect(stack.get_visible_child_name()).toBe(null);
|
|
294
|
+
const [a, b] = [1, 2].map(() => document.createElement('div'));
|
|
295
|
+
stack.add_titled(a!, 'a', 'A');
|
|
296
|
+
stack.add_titled(b!, 'b', 'B');
|
|
297
|
+
stack.set_visible_child_name('b');
|
|
298
|
+
expect(stack.get_visible_child()).toBe(b!);
|
|
299
|
+
stack.set_visible_child(a!);
|
|
300
|
+
expect(stack.get_visible_child_name()).toBe('a');
|
|
301
|
+
host.remove();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
263
304
|
};
|
package/src/widget-element.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// Reference: GTK gtk/gtkwidget.c (gtk_widget_set_visible, gtk_widget_set_tooltip_text)
|
|
17
17
|
// Copyright (c) GNOME contributors (GTK). LGPLv2.1+.
|
|
18
18
|
|
|
19
|
-
import { WIDGET_VISIBLE_GETTERS, notifyElement } from './gobject-elements.js';
|
|
19
|
+
import { WIDGET_VISIBLE_GETTERS, installElementApi, markLazyElementApi, notifyElement } from './gobject-elements.js';
|
|
20
20
|
import { dispatchedSignalsOf, type DispatchedSignals } from './signals.js';
|
|
21
21
|
import { isMapped } from './widget-mapping.js';
|
|
22
22
|
|
|
@@ -41,12 +41,64 @@ export class WidgetElement extends HTMLElement {
|
|
|
41
41
|
set_visible(visible: boolean): void {
|
|
42
42
|
this.visible = visible;
|
|
43
43
|
}
|
|
44
|
+
/** `gtk_widget_get_first_child`: the first child element, `null` for a leaf. */
|
|
45
|
+
get_first_child(): Element | null {
|
|
46
|
+
return this.firstElementChild;
|
|
47
|
+
}
|
|
48
|
+
/** `gtk_widget_get_last_child`. */
|
|
49
|
+
get_last_child(): Element | null {
|
|
50
|
+
return this.lastElementChild;
|
|
51
|
+
}
|
|
52
|
+
/** `gtk_widget_get_next_sibling`. */
|
|
53
|
+
get_next_sibling(): Element | null {
|
|
54
|
+
return this.nextElementSibling;
|
|
55
|
+
}
|
|
56
|
+
/** `gtk_widget_get_prev_sibling`. */
|
|
57
|
+
get_prev_sibling(): Element | null {
|
|
58
|
+
return this.previousElementSibling;
|
|
59
|
+
}
|
|
60
|
+
/** `gtk_widget_get_parent`. */
|
|
61
|
+
get_parent(): Element | null {
|
|
62
|
+
return this.parentElement;
|
|
63
|
+
}
|
|
64
|
+
/** `gtk_widget_get_width`: the layout box in CSS px; 0 until the widget is rendered. */
|
|
65
|
+
get_width(): number {
|
|
66
|
+
return this.offsetWidth;
|
|
67
|
+
}
|
|
68
|
+
/** `gtk_widget_get_height`: the layout box in CSS px; 0 until the widget is rendered. */
|
|
69
|
+
get_height(): number {
|
|
70
|
+
return this.offsetHeight;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The GObject signal API (ADR 0096 § 3), over the events the element declares in `static signals`:
|
|
74
|
+
* `connect` refuses a signal it does not declare, `disconnect` throws for an id that is not
|
|
75
|
+
* connected. Each of the four installs the real ones on the instance and repeats itself there
|
|
76
|
+
* (`installElementApi`), so a bare `document.createElement()` answers like a template's child
|
|
77
|
+
* and an element nobody connects to pays nothing.
|
|
78
|
+
*/
|
|
79
|
+
connect(signal: string, handler: (emitter: this, ...args: any[]) => unknown): number {
|
|
80
|
+
installElementApi(this);
|
|
81
|
+
return this.connect(signal, handler);
|
|
82
|
+
}
|
|
83
|
+
disconnect(id: number): void {
|
|
84
|
+
installElementApi(this);
|
|
85
|
+
this.disconnect(id);
|
|
86
|
+
}
|
|
87
|
+
emit(signal: string, ...args: unknown[]): void {
|
|
88
|
+
installElementApi(this);
|
|
89
|
+
this.emit(signal, ...args);
|
|
90
|
+
}
|
|
91
|
+
notify(name: string): void {
|
|
92
|
+
installElementApi(this);
|
|
93
|
+
this.notify(name);
|
|
94
|
+
}
|
|
44
95
|
/** `gtk_widget_get_mapped`: on screen, which is in the document and rendered. */
|
|
45
96
|
get_mapped(): boolean {
|
|
46
97
|
return isMapped(this);
|
|
47
98
|
}
|
|
48
99
|
}
|
|
49
100
|
|
|
101
|
+
markLazyElementApi(WidgetElement.prototype);
|
|
50
102
|
WIDGET_VISIBLE_GETTERS.add(Object.getOwnPropertyDescriptor(WidgetElement.prototype, 'visible')!.get!);
|
|
51
103
|
|
|
52
104
|
export class TooltipElement extends WidgetElement {
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
import { describe, expect, it } from '@gjsify/unit';
|
|
5
5
|
|
|
6
|
+
import * as Graphene from './namespace/graphene.js';
|
|
7
|
+
import * as Gtk from './namespace/gtk.js';
|
|
6
8
|
import * as GObject from './namespace/gobject.js';
|
|
7
9
|
import * as Gio from './namespace/gio.js';
|
|
8
10
|
import { writeProp } from './shared-tree-builder.js';
|
|
@@ -16,6 +18,9 @@ interface WidgetLike extends HTMLElement {
|
|
|
16
18
|
get_next_sibling(): Element | null;
|
|
17
19
|
get_prev_sibling(): Element | null;
|
|
18
20
|
get_parent(): Element | null;
|
|
21
|
+
get_width(): number;
|
|
22
|
+
get_height(): number;
|
|
23
|
+
compute_point(target: Element, point: Graphene.Point): [boolean, Graphene.Point];
|
|
19
24
|
insert_action_group(name: string, group: unknown): void;
|
|
20
25
|
}
|
|
21
26
|
|
|
@@ -68,6 +73,21 @@ export const WidgetMembersTest = async () => {
|
|
|
68
73
|
expect(parent.get_parent()).toBe(null);
|
|
69
74
|
});
|
|
70
75
|
|
|
76
|
+
await it('walks the element tree of a built-in widget with the GTK link names', () => {
|
|
77
|
+
const box = new Gtk.Box() as unknown as WidgetLike;
|
|
78
|
+
const a = new Gtk.Label() as unknown as WidgetLike;
|
|
79
|
+
const b = new Gtk.Label() as unknown as WidgetLike;
|
|
80
|
+
expect(box.get_first_child()).toBe(null);
|
|
81
|
+
box.append(a, b);
|
|
82
|
+
expect(box.get_first_child()).toBe(a);
|
|
83
|
+
expect(box.get_last_child()).toBe(b);
|
|
84
|
+
expect(a.get_next_sibling()).toBe(b);
|
|
85
|
+
expect(b.get_prev_sibling()).toBe(a);
|
|
86
|
+
expect(b.get_next_sibling()).toBe(null);
|
|
87
|
+
expect(a.get_parent()).toBe(box);
|
|
88
|
+
expect(box.get_parent()).toBe(null);
|
|
89
|
+
});
|
|
90
|
+
|
|
71
91
|
await it('adds, tests and removes a CSS class with the GTK names', () => {
|
|
72
92
|
const widget = make('WidgetMembersCss') as unknown as {
|
|
73
93
|
add_css_class(c: string): void;
|
|
@@ -80,6 +100,81 @@ export const WidgetMembersTest = async () => {
|
|
|
80
100
|
expect(widget.has_css_class('dim-label')).toBe(false);
|
|
81
101
|
});
|
|
82
102
|
|
|
103
|
+
await it('computes a point from the bounding boxes, on a registered class and a built-in element', () => {
|
|
104
|
+
const placed = <T extends Element>(element: T, left: number, top: number): T => {
|
|
105
|
+
element.getBoundingClientRect = () => ({ left, top }) as DOMRect;
|
|
106
|
+
return element;
|
|
107
|
+
};
|
|
108
|
+
const scrolled = placed(new Gtk.ScrolledWindow(), 10, 100);
|
|
109
|
+
const registered = placed(make('WidgetMembersPoint'), 14, 130);
|
|
110
|
+
const builtIn = placed(new Gtk.Label(), 11, 105);
|
|
111
|
+
scrolled.append(registered, builtIn);
|
|
112
|
+
document.body.append(scrolled);
|
|
113
|
+
|
|
114
|
+
const [ok, point] = registered.compute_point(scrolled, Graphene.Point.zero());
|
|
115
|
+
expect(ok).toBe(true);
|
|
116
|
+
expect(point.x).toBe(4);
|
|
117
|
+
expect(point.y).toBe(30);
|
|
118
|
+
const [okBuiltIn, shifted] = builtIn.compute_point(scrolled, new Graphene.Point({ x: 2, y: 3 }));
|
|
119
|
+
expect(okBuiltIn).toBe(true);
|
|
120
|
+
expect(shifted.x).toBe(3);
|
|
121
|
+
expect(shifted.y).toBe(8);
|
|
122
|
+
scrolled.remove();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await it('measures a real layout: size, offset inside a scrolled window, and the offset after scrolling', async () => {
|
|
126
|
+
const host = document.createElement('div');
|
|
127
|
+
host.style.width = '200px';
|
|
128
|
+
host.style.height = '100px';
|
|
129
|
+
document.body.append(host);
|
|
130
|
+
const scrolled = new Gtk.ScrolledWindow();
|
|
131
|
+
scrolled.style.height = '100px';
|
|
132
|
+
host.append(scrolled);
|
|
133
|
+
const labels: Gtk.Label[] = [];
|
|
134
|
+
for (let i = 0; i < 10; i++) {
|
|
135
|
+
const label = new Gtk.Label();
|
|
136
|
+
// An empty label has no content for the viewport to overflow, so Firefox does not scroll it.
|
|
137
|
+
label.setAttribute('label', `Row ${i}`);
|
|
138
|
+
label.style.height = '40px';
|
|
139
|
+
scrolled.append(label);
|
|
140
|
+
labels.push(label);
|
|
141
|
+
}
|
|
142
|
+
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
143
|
+
|
|
144
|
+
expect(scrolled.get_height()).toBe(100);
|
|
145
|
+
expect(scrolled.get_width()).toBe(200);
|
|
146
|
+
expect(labels[0].get_height()).toBe(40);
|
|
147
|
+
|
|
148
|
+
const label = labels[5];
|
|
149
|
+
const [ok, before] = label.compute_point(scrolled, Graphene.Point.zero());
|
|
150
|
+
expect(ok).toBe(true);
|
|
151
|
+
scrolled.viewport.scrollAxis('vertical', 80);
|
|
152
|
+
const [okAfter, after] = label.compute_point(scrolled, Graphene.Point.zero());
|
|
153
|
+
expect(okAfter).toBe(true);
|
|
154
|
+
expect(before.y - after.y).toBe(80);
|
|
155
|
+
host.remove();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await it('reports 0 for width and height while a widget is not rendered', () => {
|
|
159
|
+
const loose = new Gtk.Label();
|
|
160
|
+
expect(loose.get_width()).toBe(0);
|
|
161
|
+
expect(loose.get_height()).toBe(0);
|
|
162
|
+
const registered = make('WidgetMembersSize');
|
|
163
|
+
expect(registered.get_width()).toBe(0);
|
|
164
|
+
expect(registered.get_height()).toBe(0);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await it('computes no point between widgets that share no tree', () => {
|
|
168
|
+
const attached = make('WidgetMembersPointAttached');
|
|
169
|
+
const loose = make('WidgetMembersPointLoose');
|
|
170
|
+
document.body.append(attached);
|
|
171
|
+
const [ok, point] = loose.compute_point(attached, new Graphene.Point({ x: 5, y: 5 }));
|
|
172
|
+
expect(ok).toBe(false);
|
|
173
|
+
expect(point.x).toBe(0);
|
|
174
|
+
expect(point.y).toBe(0);
|
|
175
|
+
attached.remove();
|
|
176
|
+
});
|
|
177
|
+
|
|
83
178
|
await it('takes an action group and drops it again with null', () => {
|
|
84
179
|
const widget = make('WidgetMembersActions');
|
|
85
180
|
const group = new Gio.SimpleActionGroup();
|