@gjsify/adwaita-web 0.56.0 → 0.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adwaita-web.css +8 -2
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/application.d.ts +24 -0
- package/lib/types/application.spec.d.ts +1 -0
- package/lib/types/elements/adw-application-window.d.ts +2 -0
- package/lib/types/elements/adw-view-stack.d.ts +3 -0
- package/lib/types/elements/checks.d.ts +6 -0
- package/lib/types/elements/gtk-application-window.d.ts +2 -0
- package/lib/types/elements/gtk-drop-down.d.ts +3 -0
- package/lib/types/elements/gtk-entry.d.ts +10 -0
- package/lib/types/elements/gtk-menu-button.d.ts +2 -0
- package/lib/types/elements/gtk-scrolled-window.d.ts +16 -0
- package/lib/types/elements/gtk-stack.d.ts +3 -0
- package/lib/types/elements/gtk-switch.d.ts +3 -0
- package/lib/types/gio.spec.d.ts +1 -0
- package/lib/types/glib.spec.d.ts +1 -0
- package/lib/types/gobject-door.d.ts +4 -0
- package/lib/types/gobject-door.spec.d.ts +1 -0
- package/lib/types/gobject-elements.d.ts +17 -0
- package/lib/types/gtksource/gtk-source-view.d.ts +78 -0
- package/lib/types/gtksource/gtk-source-view.spec.d.ts +2 -0
- package/lib/types/gtksource/web-editor-driver.d.ts +53 -0
- package/lib/types/index.d.ts +3 -0
- package/lib/types/menu-actions.spec.d.ts +1 -0
- package/lib/types/namespace/adw.d.ts +2 -0
- package/lib/types/namespace/gio.d.ts +2 -0
- package/lib/types/namespace/glib.d.ts +15 -0
- package/lib/types/namespace/gobject.d.ts +28 -0
- package/lib/types/namespace/gtk-enums.d.ts +22 -0
- package/lib/types/namespace/gtk.d.ts +2 -0
- package/lib/types/namespace/gtksource.d.ts +2 -0
- package/lib/types/shared-tree-builder.d.ts +20 -1
- package/lib/types/style-manager.d.ts +5 -0
- package/lib/types/style-manager.spec.d.ts +1 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/package.json +11 -9
- package/scss/_reset.scss +1 -1
- package/src/application.spec.ts +75 -0
- package/src/application.ts +92 -0
- package/src/construct-vectors.spec.ts +155 -0
- package/src/elements/adw-application-window.ts +5 -0
- package/src/elements/adw-split-button.ts +5 -1
- package/src/elements/adw-view-stack.ts +14 -0
- package/src/elements/checks.ts +25 -1
- package/src/elements/gtk-application-window.ts +5 -0
- package/src/elements/gtk-drop-down.ts +16 -0
- package/src/elements/gtk-entry.ts +45 -7
- package/src/elements/gtk-menu-button.ts +12 -1
- package/src/elements/gtk-popover-menu.ts +7 -2
- package/src/elements/gtk-scrolled-window.ts +58 -0
- package/src/elements/gtk-stack.ts +8 -0
- package/src/elements/gtk-switch.ts +8 -0
- package/src/gio.spec.ts +23 -0
- package/src/glib.spec.ts +49 -0
- package/src/gobject-door.spec.ts +119 -0
- package/src/gobject-door.ts +137 -0
- package/src/gobject-elements.ts +172 -0
- package/src/gtksource/gtk-source-view.spec.ts +141 -0
- package/src/gtksource/gtk-source-view.ts +386 -0
- package/src/gtksource/web-editor-driver.ts +346 -0
- package/src/index.ts +10 -0
- package/src/menu-actions.spec.ts +47 -0
- package/src/namespace/adw.ts +2 -0
- package/src/namespace/gio.ts +3 -1
- package/src/namespace/glib.ts +37 -0
- package/src/namespace/gobject.ts +38 -0
- package/src/namespace/gtk-enums.ts +15 -0
- package/src/namespace/gtk.ts +2 -0
- package/src/namespace/gtksource.ts +20 -0
- package/src/shared-tree-builder.ts +142 -7
- package/src/split-button.spec.ts +2 -0
- package/src/style-manager.spec.ts +36 -0
- package/src/style-manager.ts +27 -0
- package/src/styles.generated.ts +1 -1
- package/src/test.browser.mts +14 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// `connect` / `disconnect` / `emit` / `notify` on the elements a template builds (ADR 0096 § 3).
|
|
2
|
+
//
|
|
3
|
+
// A registered class gets these from the core. The widgets it holds are plain custom elements,
|
|
4
|
+
// which talk in DOM events, so a template instance that binds to one or connects a handler needs
|
|
5
|
+
// the same four methods on it — over the events the element DECLARES in `static signals`, never
|
|
6
|
+
// over a name it merely might dispatch.
|
|
7
|
+
//
|
|
8
|
+
// NOTIFY ON AN ELEMENT THAT IS NOT CONNECTED (the gap ADR 0093 Progress row 4 names, measured
|
|
9
|
+
// in `gobject-door.spec.ts`): an element dispatches `notify::<property>` from its
|
|
10
|
+
// `attributeChangedCallback`, which returns early until the element has been connected once. The
|
|
11
|
+
// children of a template are built in the constructor and attached on the first
|
|
12
|
+
// `connectedCallback`, so every write to them in between is silent, and a `bidirectional` bind
|
|
13
|
+
// never hears its target. {@link ensureNotifying} closes it per instance: the property's own
|
|
14
|
+
// setter is wrapped, and a change the element did not announce itself is announced here. When
|
|
15
|
+
// the element does announce, nothing is added, so a connected element still notifies once.
|
|
16
|
+
|
|
17
|
+
import { attributeOf, propertyOf } from '@gjsify/adwaita-core/tags';
|
|
18
|
+
|
|
19
|
+
import { dispatchedSignalsOf } from './signals.js';
|
|
20
|
+
|
|
21
|
+
type Handler = (emitter: HTMLElement, ...args: unknown[]) => unknown;
|
|
22
|
+
|
|
23
|
+
/** Events {@link dispatchElement} created, whose `detail` is the argument list of an `emit`. */
|
|
24
|
+
const EMITTED = new WeakSet<Event>();
|
|
25
|
+
const NOTIFYING = new WeakMap<Element, Set<string>>();
|
|
26
|
+
const WITH_API = new WeakSet<Element>();
|
|
27
|
+
let nextHandlerId = 0;
|
|
28
|
+
|
|
29
|
+
/** Whether `el` got its four methods from {@link installElementApi} (a registered class has the core's). */
|
|
30
|
+
export function hasElementApi(el: object): boolean {
|
|
31
|
+
return WITH_API.has(el as Element);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The DOM event `signal` arrives as on `el`, or a refusal naming what the element does declare. */
|
|
35
|
+
function eventOf(el: Element, signal: string): string {
|
|
36
|
+
const declared = dispatchedSignalsOf(el);
|
|
37
|
+
const event =
|
|
38
|
+
declared[signal] ??
|
|
39
|
+
(signal.startsWith('notify::')
|
|
40
|
+
? declared[`notify::${attributeOf(signal.slice('notify::'.length))}`]
|
|
41
|
+
: undefined);
|
|
42
|
+
if (event === undefined) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`<${el.localName}> declares no signal '${signal}'. It dispatches: ${Object.keys(declared).join(', ') || 'none'}.`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return event;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The nearest descriptor of `member` above `el`'s own properties. */
|
|
51
|
+
function inheritedDescriptor(el: object, member: string): PropertyDescriptor | undefined {
|
|
52
|
+
for (
|
|
53
|
+
let at = Object.getPrototypeOf(el) as object | null;
|
|
54
|
+
at !== null;
|
|
55
|
+
at = Object.getPrototypeOf(at) as object | null
|
|
56
|
+
) {
|
|
57
|
+
const descriptor = Object.getOwnPropertyDescriptor(at, member);
|
|
58
|
+
if (descriptor !== undefined) return descriptor;
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function notifyElement(el: HTMLElement, name: string): void {
|
|
64
|
+
const member = propertyOf(name);
|
|
65
|
+
el.dispatchEvent(
|
|
66
|
+
new CustomEvent(eventOf(el, `notify::${name}`), {
|
|
67
|
+
bubbles: true,
|
|
68
|
+
detail: { [member]: (el as unknown as Record<string, unknown>)[member] },
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Makes every change of `property` on `el` reach a `notify::<property>` listener, connected or not. */
|
|
74
|
+
export function ensureNotifying(el: HTMLElement, property: string): void {
|
|
75
|
+
const member = propertyOf(property);
|
|
76
|
+
const patched = NOTIFYING.get(el) ?? new Set<string>();
|
|
77
|
+
if (patched.has(member)) return;
|
|
78
|
+
patched.add(member);
|
|
79
|
+
NOTIFYING.set(el, patched);
|
|
80
|
+
if (Object.getOwnPropertyDescriptor(el, member) !== undefined) return;
|
|
81
|
+
const inherited = inheritedDescriptor(el, member);
|
|
82
|
+
if (inherited?.get === undefined || inherited.set === undefined) return;
|
|
83
|
+
const { get, set } = inherited;
|
|
84
|
+
const event = eventOf(el, `notify::${property}`);
|
|
85
|
+
Object.defineProperty(el, member, {
|
|
86
|
+
configurable: true,
|
|
87
|
+
enumerable: false,
|
|
88
|
+
get() {
|
|
89
|
+
return get.call(this);
|
|
90
|
+
},
|
|
91
|
+
set(this: HTMLElement, value: unknown) {
|
|
92
|
+
const before = get.call(this);
|
|
93
|
+
let announced = false;
|
|
94
|
+
const mark = (domEvent: Event) => {
|
|
95
|
+
if (domEvent.target === this) announced = true;
|
|
96
|
+
};
|
|
97
|
+
this.addEventListener(event, mark);
|
|
98
|
+
try {
|
|
99
|
+
set.call(this, value);
|
|
100
|
+
} finally {
|
|
101
|
+
this.removeEventListener(event, mark);
|
|
102
|
+
}
|
|
103
|
+
if (!announced && get.call(this) !== before) notifyElement(this, property);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Calls `handler(el, ...args)` for `signal`; returns the disconnect. */
|
|
109
|
+
export function listenElement(el: HTMLElement, signal: string, handler: Handler): () => void {
|
|
110
|
+
const event = eventOf(el, signal);
|
|
111
|
+
const notification = signal.startsWith('notify::');
|
|
112
|
+
if (notification) ensureNotifying(el, signal.slice('notify::'.length));
|
|
113
|
+
const listener = (domEvent: Event) => {
|
|
114
|
+
// A notification bubbles, and a parent must not hear its child's property as its own.
|
|
115
|
+
if (notification && domEvent.target !== el) return;
|
|
116
|
+
handler(el, ...(EMITTED.has(domEvent) ? ((domEvent as CustomEvent).detail as unknown[]) : []));
|
|
117
|
+
};
|
|
118
|
+
el.addEventListener(event, listener);
|
|
119
|
+
return () => el.removeEventListener(event, listener);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function dispatchElement(el: HTMLElement, signal: string, args: readonly unknown[]): void {
|
|
123
|
+
const event = eventOf(el, signal);
|
|
124
|
+
const domEvent = new CustomEvent(event, { detail: args, bubbles: event.startsWith('notify::') });
|
|
125
|
+
EMITTED.add(domEvent);
|
|
126
|
+
el.dispatchEvent(domEvent);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Gives `el` the instance API of a registered class, over the events it declares. */
|
|
130
|
+
export function installElementApi(el: HTMLElement): void {
|
|
131
|
+
// A registered class already has the core's four methods on its prototype.
|
|
132
|
+
if (typeof (el as unknown as Record<string, unknown>).connect === 'function') return;
|
|
133
|
+
WITH_API.add(el);
|
|
134
|
+
const connections = new Map<number, () => void>();
|
|
135
|
+
const define = (name: string, value: unknown) =>
|
|
136
|
+
Object.defineProperty(el, name, { value, configurable: true, writable: true, enumerable: false });
|
|
137
|
+
define('connect', (signal: string, handler: Handler): number => {
|
|
138
|
+
const unlisten = listenElement(el, signal, handler);
|
|
139
|
+
const id = ++nextHandlerId;
|
|
140
|
+
connections.set(id, unlisten);
|
|
141
|
+
return id;
|
|
142
|
+
});
|
|
143
|
+
define('disconnect', (id: number): void => {
|
|
144
|
+
const unlisten = connections.get(id);
|
|
145
|
+
if (unlisten === undefined) throw new Error(`No handler with id ${id} is connected to <${el.localName}>`);
|
|
146
|
+
connections.delete(id);
|
|
147
|
+
unlisten();
|
|
148
|
+
});
|
|
149
|
+
define('emit', (signal: string, ...args: unknown[]): void => dispatchElement(el, signal, args));
|
|
150
|
+
define('notify', (name: string): void => notifyElement(el, name));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const API_MEMBERS = new Set(['connect', 'disconnect', 'emit', 'notify']);
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The end of a bind the core's engine drives. An element speaks camelCase members where a `.blp`
|
|
157
|
+
* says `dash-name`, so a built element is wrapped to translate the name; a registered class
|
|
158
|
+
* already has the core's accessors and aliases and is returned as it is.
|
|
159
|
+
*/
|
|
160
|
+
export function endpointOf(el: HTMLElement): HTMLElement {
|
|
161
|
+
if (!hasElementApi(el)) return el;
|
|
162
|
+
return new Proxy(el, {
|
|
163
|
+
get(target, key) {
|
|
164
|
+
if (typeof key !== 'string' || API_MEMBERS.has(key)) return Reflect.get(target, key, target);
|
|
165
|
+
const value = Reflect.get(target, propertyOf(key), target);
|
|
166
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
167
|
+
},
|
|
168
|
+
set(target, key, value) {
|
|
169
|
+
return Reflect.set(target, typeof key === 'string' ? propertyOf(key) : key, value, target);
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
2
|
+
import {
|
|
3
|
+
GUTTER_PAINT_VECTORS,
|
|
4
|
+
GTKSOURCE_BUFFER_VECTORS,
|
|
5
|
+
GTKSOURCE_VIEW_DEFAULT_VECTORS,
|
|
6
|
+
GTKSOURCE_VIEW_SURFACE_VECTORS,
|
|
7
|
+
type GtkSourceViewSurfaceLike,
|
|
8
|
+
type GtkSourceBufferLike,
|
|
9
|
+
type GutterPaintSurface,
|
|
10
|
+
type ViewLike,
|
|
11
|
+
} from '@gjsify/gtksource-core/conformance';
|
|
12
|
+
|
|
13
|
+
import * as GObject from '../namespace/gobject.js';
|
|
14
|
+
import * as Gtk from '../namespace/gtk.js';
|
|
15
|
+
import * as GtkSource from '../namespace/gtksource.js';
|
|
16
|
+
|
|
17
|
+
import './gtk-source-view.js';
|
|
18
|
+
import { GtkSourceView } from './gtk-source-view.js';
|
|
19
|
+
|
|
20
|
+
export const GtkSourceViewTest = async () => {
|
|
21
|
+
await describe('<gtk-source-view>: GtkSource.View', async () => {
|
|
22
|
+
for (const vector of GTKSOURCE_BUFFER_VECTORS) {
|
|
23
|
+
await it(`GtkSource.Buffer: ${vector.rule}`, () => {
|
|
24
|
+
expect(vector.observe(GtkSource as unknown as GtkSourceBufferLike)).toStrictEqual(vector.shows);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const vector of GTKSOURCE_VIEW_DEFAULT_VECTORS) {
|
|
29
|
+
await it(`${vector.property} defaults to ${vector.shows}`, () => {
|
|
30
|
+
const view = new GtkSourceView();
|
|
31
|
+
expect((view as unknown as Record<string, unknown>)[vector.member]).toStrictEqual(vector.shows);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const vector of GTKSOURCE_VIEW_SURFACE_VECTORS) {
|
|
36
|
+
await it(`surface: ${vector.rule}`, () => {
|
|
37
|
+
expect(vector.observe({ Gtk, GtkSource } as unknown as GtkSourceViewSurfaceLike)).toStrictEqual(
|
|
38
|
+
vector.shows,
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const task = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
44
|
+
for (const vector of GUTTER_PAINT_VECTORS) {
|
|
45
|
+
await it(`gutter paint: ${vector.rule}`, async () => {
|
|
46
|
+
const views: HTMLElement[] = [];
|
|
47
|
+
const harness = {
|
|
48
|
+
async present(view: ViewLike) {
|
|
49
|
+
document.body.append(view as unknown as HTMLElement);
|
|
50
|
+
views.push(view as unknown as HTMLElement);
|
|
51
|
+
await task();
|
|
52
|
+
},
|
|
53
|
+
settle: task,
|
|
54
|
+
};
|
|
55
|
+
try {
|
|
56
|
+
expect(
|
|
57
|
+
await vector.observe({ Gtk, GtkSource, GObject } as unknown as GutterPaintSurface, harness),
|
|
58
|
+
).toStrictEqual(vector.shows);
|
|
59
|
+
} finally {
|
|
60
|
+
for (const view of views) view.remove();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await it("paints a renderer's text, in a column left of the line numbers when its position is negative", async () => {
|
|
66
|
+
const view = document.createElement('gtk-source-view') as GtkSourceView;
|
|
67
|
+
view.showLineNumbers = true;
|
|
68
|
+
document.body.append(view);
|
|
69
|
+
view.buffer.text = 'a\nb';
|
|
70
|
+
const renderer = new GtkSource.GutterRendererText({ width_request: 24, margin_start: 2, margin_end: 3 });
|
|
71
|
+
(renderer as unknown as { vfunc_query_data(lines: unknown, line: number): void }).vfunc_query_data =
|
|
72
|
+
function (this: { text: string }, _lines, line) {
|
|
73
|
+
this.text = `r${line}`;
|
|
74
|
+
};
|
|
75
|
+
view.get_gutter(Gtk.TextWindowType.LEFT)!.insert(renderer, -1);
|
|
76
|
+
await task();
|
|
77
|
+
const columns = view.querySelectorAll<HTMLElement>('.gsv-columns > *');
|
|
78
|
+
expect([...columns].map((column) => column.className)).toStrictEqual(['gsv-column', 'gsv-numbers']);
|
|
79
|
+
const first = columns[0];
|
|
80
|
+
expect([...first.querySelectorAll('.gsv-cell')].map((cell) => cell.textContent)).toStrictEqual([
|
|
81
|
+
'r0',
|
|
82
|
+
'r1',
|
|
83
|
+
]);
|
|
84
|
+
expect([first.style.minWidth, first.style.paddingLeft, first.style.paddingRight]).toStrictEqual([
|
|
85
|
+
'24px',
|
|
86
|
+
'2px',
|
|
87
|
+
'3px',
|
|
88
|
+
]);
|
|
89
|
+
expect(view.classList.contains('has-columns')).toBe(true);
|
|
90
|
+
view.remove();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await it('paints markup as styled spans and drops the column with its renderer', async () => {
|
|
94
|
+
const view = document.createElement('gtk-source-view') as GtkSourceView;
|
|
95
|
+
document.body.append(view);
|
|
96
|
+
view.buffer.text = 'a';
|
|
97
|
+
const renderer = new GtkSource.GutterRendererText({ width_request: 10 });
|
|
98
|
+
(renderer as unknown as { vfunc_query_data(lines: unknown, line: number): void }).vfunc_query_data =
|
|
99
|
+
function (this: { markup: string }) {
|
|
100
|
+
this.markup = '<b>x</b> & y';
|
|
101
|
+
};
|
|
102
|
+
const gutter = view.get_gutter(Gtk.TextWindowType.LEFT)!;
|
|
103
|
+
gutter.insert(renderer, 0);
|
|
104
|
+
await task();
|
|
105
|
+
const cell = view.querySelector<HTMLElement>('.gsv-cell')!;
|
|
106
|
+
expect(cell.textContent).toBe('x & y');
|
|
107
|
+
expect(cell.querySelector<HTMLElement>('span')!.style.fontWeight).toBe('bold');
|
|
108
|
+
gutter.remove(renderer);
|
|
109
|
+
await task();
|
|
110
|
+
expect(view.querySelector('.gsv-column')).toBe(null);
|
|
111
|
+
expect(view.classList.contains('has-columns')).toBe(false);
|
|
112
|
+
view.remove();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await it('writes through attributes with GTK semantics', () => {
|
|
116
|
+
const view = document.createElement('gtk-source-view') as GtkSourceView;
|
|
117
|
+
view.setAttribute('show-line-numbers', 'true');
|
|
118
|
+
view.setAttribute('indent-width', '4');
|
|
119
|
+
view.setAttribute('left-margin', '12');
|
|
120
|
+
expect(view.showLineNumbers).toBe(true);
|
|
121
|
+
expect(view.indentWidth).toBe(4);
|
|
122
|
+
expect(view.leftMargin).toBe(12);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await it('refuses a value that is not a boolean, by name', () => {
|
|
126
|
+
const view = new GtkSourceView();
|
|
127
|
+
expect(() => (view.editable = 'maybe')).toThrow(/GtkSource.View.editable/);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await it('shows the buffer text in the textarea and takes a user edit back into the buffer', () => {
|
|
131
|
+
const view = document.createElement('gtk-source-view') as GtkSourceView;
|
|
132
|
+
document.body.append(view);
|
|
133
|
+
view.buffer.text = 'LDA #$01';
|
|
134
|
+
expect(view.textarea.value).toBe('LDA #$01');
|
|
135
|
+
view.textarea.value = 'LDA #$02';
|
|
136
|
+
view.textarea.dispatchEvent(new Event('input'));
|
|
137
|
+
expect(view.buffer.text).toBe('LDA #$02');
|
|
138
|
+
view.remove();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
};
|