@gjsify/adwaita-web 0.60.0 → 0.61.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.
Files changed (43) hide show
  1. package/README.md +11 -0
  2. package/lib/types/elements/gtk-drop-down.d.ts +4 -0
  3. package/lib/types/elements/gtk-label.d.ts +4 -0
  4. package/lib/types/elements/gtk-text-view.d.ts +15 -0
  5. package/lib/types/file-chooser-host.d.ts +55 -0
  6. package/lib/types/file-chooser.spec.d.ts +1 -0
  7. package/lib/types/gtk-platform.spec.d.ts +1 -0
  8. package/lib/types/namespace/adw.d.ts +1 -0
  9. package/lib/types/namespace/gdk.d.ts +2 -0
  10. package/lib/types/namespace/gio.d.ts +3 -0
  11. package/lib/types/namespace/glib.d.ts +1 -0
  12. package/lib/types/namespace/gtk-enums.d.ts +22 -0
  13. package/lib/types/namespace/gtk-platform.d.ts +13 -0
  14. package/lib/types/namespace/gtk.d.ts +4 -1
  15. package/lib/types/platform-host.d.ts +2 -0
  16. package/lib/types/style-manager.d.ts +7 -0
  17. package/package.json +18 -9
  18. package/src/capabilities.mjs +1 -4
  19. package/src/construct-vectors.spec.ts +10 -1
  20. package/src/elements/gtk-drop-down.ts +10 -0
  21. package/src/elements/gtk-label.ts +10 -0
  22. package/src/elements/gtk-text-view.ts +40 -0
  23. package/src/file-chooser-host.ts +421 -0
  24. package/src/file-chooser.spec.ts +376 -0
  25. package/src/gobject-door.ts +7 -1
  26. package/src/gobject-elements.ts +4 -0
  27. package/src/gtk-drop-down.spec.ts +11 -0
  28. package/src/gtk-label.spec.ts +12 -0
  29. package/src/gtk-platform.spec.ts +19 -0
  30. package/src/gtk-text-view.spec.ts +22 -0
  31. package/src/namespace/adw.ts +1 -0
  32. package/src/namespace/gdk.ts +18 -1
  33. package/src/namespace/gio.ts +20 -2
  34. package/src/namespace/glib.ts +6 -1
  35. package/src/namespace/gtk-enums.ts +7 -2
  36. package/src/namespace/gtk-platform.ts +16 -0
  37. package/src/namespace/gtk.ts +12 -1
  38. package/src/platform-host.ts +88 -0
  39. package/src/shared-tree-builder.ts +40 -0
  40. package/src/shared-trees.spec.ts +36 -0
  41. package/src/style-manager.ts +10 -1
  42. package/src/test.browser.mts +4 -0
  43. package/src/widget-members.spec.ts +12 -0
@@ -0,0 +1,376 @@
1
+ // The file-chooser door of adwaita-web, held to the shared vectors (real GJS is their oracle, in
2
+ // adwaita-core's `file-chooser.gjs.spec.ts`) through both of its paths — the File System Access
3
+ // pickers and the Origin Private File System — plus what only a browser shows: the filter mapping,
4
+ // a missing user activation, the drawn dialog, the `<input>` copy and the worker write.
5
+ //
6
+ // The pickers are scripted fakes; the handles behind them are REAL OPFS handles in a directory of
7
+ // their own, so every read and write below goes through the browser's storage.
8
+
9
+ import {
10
+ driveFileChooserVectors,
11
+ FILE_CHOOSER_VECTORS,
12
+ type FileChooserNamespaces,
13
+ type FileChooserSubject,
14
+ type ScriptedPick,
15
+ } from '@gjsify/adwaita-core/conformance';
16
+ import { registerFileChooserDriver, type FileChooserRequest, type FileFilterSpec } from '@gjsify/adwaita-core';
17
+ import { describe, expect, it } from '@gjsify/unit';
18
+
19
+ import type { AdwDialog } from './elements/adw-dialog.js';
20
+ import type { GtkEntry } from './elements/gtk-entry.js';
21
+ import type { GtkListBox } from './elements/gtk-list-box.js';
22
+ import {
23
+ createWebFileChooserDriver,
24
+ filePickerOptions,
25
+ inputAccept,
26
+ presentOpfsDialog,
27
+ type FilePickerOptions,
28
+ type OpfsChoice,
29
+ type WebFileChooserEnv,
30
+ } from './file-chooser-host.js';
31
+ import * as Gio from './namespace/gio.js';
32
+ import * as GLib from './namespace/glib.js';
33
+ import * as Gtk from './namespace/gtk.js';
34
+
35
+ let scratchCount = 0;
36
+
37
+ /** A fresh OPFS directory; the caller removes it with {@link dropScratch}. */
38
+ async function scratch(): Promise<FileSystemDirectoryHandle> {
39
+ const root = await navigator.storage.getDirectory();
40
+ return root.getDirectoryHandle(`file-chooser-spec-${Date.now()}-${scratchCount++}`, { create: true });
41
+ }
42
+
43
+ async function dropScratch(dir: FileSystemDirectoryHandle): Promise<void> {
44
+ const root = await navigator.storage.getDirectory();
45
+ await root.removeEntry(dir.name, { recursive: true });
46
+ }
47
+
48
+ async function put(dir: FileSystemDirectoryHandle, name: string, contents: Uint8Array | string) {
49
+ const handle = await dir.getFileHandle(name, { create: true });
50
+ const stream = await handle.createWritable();
51
+ await stream.write(contents as FileSystemWriteChunkType);
52
+ await stream.close();
53
+ return handle;
54
+ }
55
+
56
+ async function text(dir: FileSystemDirectoryHandle, name: string): Promise<string> {
57
+ return (await (await dir.getFileHandle(name)).getFile()).text();
58
+ }
59
+
60
+ const abort = () => new DOMException('The user aborted a request.', 'AbortError');
61
+
62
+ /** The filters a File System Access options object carries, written the way the vectors write them. */
63
+ function optionGlobs(options: FilePickerOptions): string[][] {
64
+ const out = (options.types ?? []).map((type) =>
65
+ Object.entries(type.accept).flatMap(([mime, exts]) =>
66
+ exts.length > 0 ? exts.map((ext) => `*${ext}`) : [mime],
67
+ ),
68
+ );
69
+ if (options.types && !options.excludeAcceptAllOption) out.push(['*']);
70
+ return out;
71
+ }
72
+
73
+ function requestGlobs(request: FileChooserRequest | null): string[][] {
74
+ return (request?.filters ?? []).map((spec) => [
75
+ ...(spec.acceptAll ? ['*'] : []),
76
+ ...[...spec.patterns, ...spec.suffixes].map((ext) => `*.${ext}`),
77
+ ...spec.mimeTypes,
78
+ ]);
79
+ }
80
+
81
+ const namespaces = { GLib, Gio, Gtk } as unknown as FileChooserNamespaces;
82
+
83
+ /** A subject whose env opens the File System Access pickers, scripted. */
84
+ function pickerSubject(dir: FileSystemDirectoryHandle) {
85
+ let next: ScriptedPick = null;
86
+ let lastOptions: FilePickerOptions = {};
87
+ const env: WebFileChooserEnv = {
88
+ showOpenFilePicker: async (options) => {
89
+ lastOptions = options;
90
+ if (next === null) throw abort();
91
+ return [await put(dir, next.basename, next.contents ?? '')];
92
+ },
93
+ showSaveFilePicker: async (options) => {
94
+ lastOptions = options;
95
+ if (next === null) throw abort();
96
+ return dir.getFileHandle(next.basename, { create: true });
97
+ },
98
+ isActive: () => true,
99
+ opfsRoot: async () => dir,
100
+ choose: async () => null,
101
+ canStreamWrite: () => true,
102
+ };
103
+ const subject: FileChooserSubject = {
104
+ ...namespaces,
105
+ name: 'adwaita-web (File System Access)',
106
+ isOracle: false,
107
+ script: (pick) => void (next = pick),
108
+ pickerFilters: () => optionGlobs(lastOptions),
109
+ cancelled: () => ({ is_cancelled: () => true }),
110
+ };
111
+ return { env, subject };
112
+ }
113
+
114
+ /** A subject whose env has no pickers: OPFS, with the drawn dialog replaced by the script. */
115
+ function opfsSubject(dir: FileSystemDirectoryHandle) {
116
+ let next: ScriptedPick = null;
117
+ let lastRequest: FileChooserRequest | null = null;
118
+ const env: WebFileChooserEnv = {
119
+ isActive: () => true,
120
+ opfsRoot: async () => dir,
121
+ choose: async (kind, request) => {
122
+ lastRequest = request;
123
+ if (next === null) return null;
124
+ if (kind === 'open') await put(dir, next.basename, next.contents ?? '');
125
+ return { name: next.basename };
126
+ },
127
+ canStreamWrite: () => true,
128
+ };
129
+ const subject: FileChooserSubject = {
130
+ ...namespaces,
131
+ name: 'adwaita-web (OPFS)',
132
+ isOracle: false,
133
+ script: (pick) => void (next = pick),
134
+ pickerFilters: () => requestGlobs(lastRequest),
135
+ cancelled: () => ({ is_cancelled: () => true }),
136
+ };
137
+ return { env, subject };
138
+ }
139
+
140
+ async function withEnv<T>(env: WebFileChooserEnv, body: () => Promise<T>): Promise<T> {
141
+ const previous = registerFileChooserDriver(createWebFileChooserDriver(env));
142
+ try {
143
+ return await body();
144
+ } finally {
145
+ registerFileChooserDriver(previous);
146
+ }
147
+ }
148
+
149
+ const spec = (over: Partial<FileFilterSpec>): FileFilterSpec => ({
150
+ name: null,
151
+ patterns: [],
152
+ suffixes: [],
153
+ mimeTypes: [],
154
+ acceptAll: false,
155
+ ...over,
156
+ });
157
+
158
+ const asm = spec({ name: 'Assembly Files', patterns: ['asm', 's'] });
159
+ const all = spec({ name: 'All Files', acceptAll: true });
160
+
161
+ const request = (over: Partial<FileChooserRequest> = {}): FileChooserRequest => ({
162
+ title: null,
163
+ filters: [],
164
+ defaultFilter: null,
165
+ initialName: null,
166
+ parent: null,
167
+ ...over,
168
+ });
169
+
170
+ type Opened = { get_basename(): string; load_contents_async(c: unknown): Promise<[Uint8Array, unknown]> };
171
+ const openDialog = (filters?: unknown): Promise<Opened> =>
172
+ (new Gtk.FileDialog(filters ? { filters } : {}).open as unknown as (p: unknown, c: unknown) => Promise<Opened>)(
173
+ null,
174
+ null,
175
+ );
176
+
177
+ async function rejectionOf(pending: Promise<unknown>): Promise<{ dismissed: boolean; failed: boolean } | 'resolved'> {
178
+ try {
179
+ await pending;
180
+ return 'resolved';
181
+ } catch (error) {
182
+ const e = error as { matches(domain: unknown, code: number): boolean };
183
+ return {
184
+ dismissed: e.matches(Gtk.DialogError, Gtk.DialogError.DISMISSED),
185
+ failed: e.matches(Gtk.DialogError, Gtk.DialogError.FAILED),
186
+ };
187
+ }
188
+ }
189
+
190
+ async function readyDialog(): Promise<AdwDialog> {
191
+ for (let i = 0; i < 200; i++) {
192
+ const dialog = document.querySelector('adw-dialog[data-ready]') as AdwDialog | null;
193
+ if (dialog) return dialog;
194
+ await new Promise((resolve) => setTimeout(resolve, 10));
195
+ }
196
+ throw new Error('the OPFS dialog never set data-ready');
197
+ }
198
+
199
+ const rowNames = (dialog: AdwDialog) =>
200
+ [...dialog.querySelectorAll('gtk-list-box-row[data-name]')].map((row) => (row as HTMLElement).dataset.name);
201
+
202
+ export const FileChooserTest = async () => {
203
+ for (const make of [pickerSubject, opfsSubject]) {
204
+ const dir = await scratch();
205
+ const { env, subject } = make(dir);
206
+ try {
207
+ await withEnv(env, () => driveFileChooserVectors(subject, { describe, it, expect }, FILE_CHOOSER_VECTORS));
208
+ } finally {
209
+ await dropScratch(dir);
210
+ }
211
+ }
212
+
213
+ await describe('adwaita-web: the File System Access mapping', async () => {
214
+ await it('maps an extension filter to the octet-stream key and keeps the accept-all option', () => {
215
+ const options = filePickerOptions(request({ filters: [asm, all], initialName: 'a.asm' }), 'save');
216
+ expect(JSON.stringify(options)).toBe(
217
+ JSON.stringify({
218
+ types: [{ description: 'Assembly Files', accept: { 'application/octet-stream': ['.asm', '.s'] } }],
219
+ excludeAcceptAllOption: false,
220
+ suggestedName: 'a.asm',
221
+ }),
222
+ );
223
+ });
224
+
225
+ await it('excludes the accept-all option when no filter accepts everything, and keys MIME types', () => {
226
+ const text = spec({ name: 'Text', suffixes: ['txt'], mimeTypes: ['text/plain'] });
227
+ const options = filePickerOptions(request({ filters: [text], initialName: 'x.txt' }), 'open');
228
+ expect(JSON.stringify(options)).toBe(
229
+ JSON.stringify({
230
+ types: [{ description: 'Text', accept: { 'text/plain': ['.txt'] } }],
231
+ excludeAcceptAllOption: true,
232
+ }),
233
+ );
234
+ });
235
+
236
+ await it('gives <input accept> the extensions and MIME types, and nothing for accept-all', () => {
237
+ expect(JSON.stringify([inputAccept(asm), inputAccept(all), inputAccept(null)])).toBe(
238
+ JSON.stringify(['.asm,.s', null, null]),
239
+ );
240
+ });
241
+ });
242
+
243
+ await describe('adwaita-web: the driver', async () => {
244
+ await it('rejects with DialogError.FAILED without a user activation, before any picker opens', async () => {
245
+ const dir = await scratch();
246
+ let opened = false;
247
+ const { env } = pickerSubject(dir);
248
+ env.isActive = () => false;
249
+ const picker = env.showOpenFilePicker!;
250
+ env.showOpenFilePicker = (options) => ((opened = true), picker(options));
251
+ const result = await withEnv(env, () => rejectionOf(openDialog()));
252
+ await dropScratch(dir);
253
+ expect(JSON.stringify([result, opened])).toBe(JSON.stringify([{ dismissed: false, failed: true }, false]));
254
+ });
255
+
256
+ await it('maps AbortError to DISMISSED and any other picker error to FAILED', async () => {
257
+ const dir = await scratch();
258
+ const { env } = pickerSubject(dir);
259
+ const aborted = await withEnv(env, () => rejectionOf(openDialog()));
260
+ env.showOpenFilePicker = async () => {
261
+ throw new DOMException('blocked', 'SecurityError');
262
+ };
263
+ const blocked = await withEnv(env, () => rejectionOf(openDialog()));
264
+ await dropScratch(dir);
265
+ expect(JSON.stringify([aborted, blocked])).toBe(
266
+ JSON.stringify([
267
+ { dismissed: true, failed: false },
268
+ { dismissed: false, failed: true },
269
+ ]),
270
+ );
271
+ });
272
+
273
+ await it('copies an <input> pick into OPFS under a free name', async () => {
274
+ const dir = await scratch();
275
+ await put(dir, 'a.asm', 'old');
276
+ const env: WebFileChooserEnv = {
277
+ isActive: () => true,
278
+ opfsRoot: async () => dir,
279
+ choose: async () => ({ upload: new File(['new'], 'a.asm') }) as OpfsChoice,
280
+ canStreamWrite: () => true,
281
+ };
282
+ const file = await withEnv(env, () => openDialog());
283
+ const result = [file.get_basename(), await text(dir, 'a.asm'), await text(dir, 'a (2).asm')];
284
+ await dropScratch(dir);
285
+ expect(JSON.stringify(result)).toBe(JSON.stringify(['a (2).asm', 'old', 'new']));
286
+ });
287
+
288
+ await it('writes through a createSyncAccessHandle worker where createWritable is missing', async () => {
289
+ const dir = await scratch();
290
+ await put(dir, 'w.txt', 'a longer old content');
291
+ const driver = createWebFileChooserDriver({
292
+ isActive: () => true,
293
+ opfsRoot: async () => dir,
294
+ choose: async () => ({ name: 'w.txt' }),
295
+ canStreamWrite: () => false,
296
+ });
297
+ const handle = await driver.save(request({ initialName: 'w.txt' }));
298
+ const writer = await handle.openWriter();
299
+ await writer.write(new TextEncoder().encode('he'));
300
+ await writer.write(new TextEncoder().encode('llo'));
301
+ await writer.close();
302
+ const result = new TextDecoder().decode(await handle.read());
303
+ await dropScratch(dir);
304
+ expect(result).toBe('hello');
305
+ });
306
+
307
+ await it('never creates a download link to save', async () => {
308
+ const dir = await scratch();
309
+ const created: string[] = [];
310
+ const original = document.createElement;
311
+ document.createElement = function (this: Document, tag: string, options?: ElementCreationOptions) {
312
+ created.push(tag.toLowerCase());
313
+ return original.call(this, tag, options);
314
+ } as typeof document.createElement;
315
+ try {
316
+ const { env, subject } = opfsSubject(dir);
317
+ subject.script({ basename: 'd.txt' });
318
+ await withEnv(env, async () => {
319
+ const save = new Gtk.FileDialog({ initial_name: 'd.txt' }).save as unknown as (
320
+ p: unknown,
321
+ c: unknown,
322
+ ) => Promise<{ replace_contents_async: unknown }>;
323
+ await save(null, null);
324
+ });
325
+ } finally {
326
+ document.createElement = original;
327
+ await dropScratch(dir);
328
+ }
329
+ expect(created.includes('a')).toBe(false);
330
+ });
331
+ });
332
+
333
+ await describe('adwaita-web: the drawn OPFS dialog', async () => {
334
+ await it('lists what the current filter accepts and opens the selected row', async () => {
335
+ const dir = await scratch();
336
+ await put(dir, 'b.asm', '');
337
+ await put(dir, 'a.asm', '');
338
+ await put(dir, 'notes.txt', '');
339
+ const pending = presentOpfsDialog('open', request({ filters: [asm, all] }), dir);
340
+ const dialog = await readyDialog();
341
+ const names = rowNames(dialog);
342
+ const list = dialog.querySelector('gtk-list-box[selection-mode="single"]') as GtkListBox;
343
+ list.selectRow(list.rows[1]);
344
+ (dialog.querySelector('gtk-button[label="Open"]') as HTMLElement).click();
345
+ const choice = await pending;
346
+ const gone = !dialog.isConnected;
347
+ await dropScratch(dir);
348
+ expect(JSON.stringify([names, choice, gone])).toBe(
349
+ JSON.stringify([['a.asm', 'b.asm'], { name: 'b.asm' }, true]),
350
+ );
351
+ });
352
+
353
+ await it('saves under the name in the entry, prefilled with the initial name', async () => {
354
+ const dir = await scratch();
355
+ const pending = presentOpfsDialog('save', request({ initialName: 'p.asm' }), dir);
356
+ const dialog = await readyDialog();
357
+ const entry = dialog.querySelector('gtk-entry') as GtkEntry;
358
+ const prefilled = entry.value;
359
+ entry.value = 'q.asm';
360
+ (dialog.querySelector('gtk-button[label="Save"]') as HTMLElement).click();
361
+ const choice = await pending;
362
+ await dropScratch(dir);
363
+ expect(JSON.stringify([prefilled, choice])).toBe(JSON.stringify(['p.asm', { name: 'q.asm' }]));
364
+ });
365
+
366
+ await it('resolves null when the dialog closes without a choice', async () => {
367
+ const dir = await scratch();
368
+ const pending = presentOpfsDialog('open', request(), dir);
369
+ const dialog = await readyDialog();
370
+ dialog.close();
371
+ const choice = await pending;
372
+ await dropScratch(dir);
373
+ expect(choice).toBe(null);
374
+ });
375
+ });
376
+ };
@@ -19,12 +19,14 @@ import {
19
19
  type RegisteredClass,
20
20
  type TemplateScope,
21
21
  syncDirection,
22
+ widgetControllerMembers,
22
23
  widgetDisplayMembers,
23
24
  widgetTreeMembers,
24
25
  } from '@gjsify/adwaita-core';
25
26
  import { propertyOf } from '@gjsify/adwaita-core/tags';
26
27
 
27
28
  import { gdk } from './namespace/gdk.js';
29
+ import { webPlatformHost } from './platform-host.js';
28
30
  import { inheritStyles } from './registered-styles.js';
29
31
  import { dispatchElement, listenElement } from './gobject-elements.js';
30
32
  import { buildTemplateTree, type BuiltTemplateTree } from './shared-tree-builder.js';
@@ -127,7 +129,11 @@ export const webDoor: GObjectDoor = {
127
129
 
128
130
  const proto = (klass as { prototype: Record<string, unknown> }).prototype;
129
131
  // `this.get_display()`, `this.set_direction()`, `this.insert_action_group()` …, as on a Gtk.Widget; a class's own wins.
130
- for (const [name, member] of Object.entries({ ...widgetDisplayMembers(gdk), ...widgetTreeMembers() })) {
132
+ for (const [name, member] of Object.entries({
133
+ ...widgetDisplayMembers(gdk),
134
+ ...widgetTreeMembers(),
135
+ ...widgetControllerMembers(webPlatformHost),
136
+ })) {
131
137
  if (!(name in proto))
132
138
  Object.defineProperty(proto, name, { value: member, writable: true, configurable: true });
133
139
  }
@@ -148,6 +148,10 @@ export function installElementApi(el: HTMLElement): void {
148
148
  });
149
149
  define('emit', (signal: string, ...args: unknown[]): void => dispatchElement(el, signal, args));
150
150
  define('notify', (name: string): void => notifyElement(el, name));
151
+ // The `Gtk.Widget` CSS-class calls a template's TypeScript makes on its children.
152
+ define('add_css_class', (name: string): void => el.classList.add(name));
153
+ define('remove_css_class', (name: string): void => el.classList.remove(name));
154
+ define('has_css_class', (name: string): boolean => el.classList.contains(name));
151
155
  }
152
156
 
153
157
  const API_MEMBERS = new Set(['connect', 'disconnect', 'emit', 'notify']);
@@ -276,6 +276,17 @@ export const GtkDropDownTest = async () => {
276
276
  dd.remove();
277
277
  });
278
278
 
279
+ await it('get_selected / set_selected are the GTK spelling of `selected`', async () => {
280
+ const dd = makeDropDown();
281
+ dd.model = ['a', 'b', 'c'];
282
+ (dd as unknown as { set_selected(n: number): void }).set_selected(2);
283
+ expect(dd.selected).toBe(2);
284
+ expect((dd as unknown as { get_selected(): number }).get_selected()).toBe(2);
285
+ (dd as unknown as { set_selected(n: number): void }).set_selected(9);
286
+ expect(dd.selected).toBe(2);
287
+ dd.remove();
288
+ });
289
+
279
290
  await it('selected reflects to the attribute + marks the row', async () => {
280
291
  const dd = makeDropDown();
281
292
  dd.model = ['a', 'b'];
@@ -207,6 +207,18 @@ export const GtkLabelTest = async () => {
207
207
  });
208
208
  });
209
209
 
210
+ await describe('<gtk-label> get_label / set_label', async () => {
211
+ await it('are the GTK spelling of the `label` property', () => {
212
+ const { el, host } = mount('Hello');
213
+ const gtk = el as unknown as { get_label(): string; set_label(str: string): void };
214
+ expect(gtk.get_label()).toBe('Hello');
215
+ gtk.set_label('World');
216
+ expect(el.label).toBe('World');
217
+ expect(el.textContent).toBe('World');
218
+ host.remove();
219
+ });
220
+ });
221
+
210
222
  await describe('<gtk-label> xalign and justify', async () => {
211
223
  await it('centres the text in its box by default (xalign 0.5)', () => {
212
224
  const { el, host } = mount('Hi');
@@ -0,0 +1,19 @@
1
+ // The Gtk platform door of adwaita-web, held to the shared vectors. The door is the namespace module itself,
2
+ // so a member that stops being exported fails here.
3
+
4
+ import { GTK_PLATFORM_VECTORS, driveGtkPlatformVectors, type GtkPlatformLike } from '@gjsify/adwaita-core/conformance';
5
+ import { describe, expect, it } from '@gjsify/unit';
6
+
7
+ import * as Gtk from './namespace/gtk.js';
8
+
9
+ export const GtkPlatformTest = async () => {
10
+ await driveGtkPlatformVectors(
11
+ { name: 'adwaita-web', isOracle: false, Gtk: Gtk as unknown as GtkPlatformLike },
12
+ { describe, it, expect },
13
+ );
14
+ await describe('adwaita-web: Gtk platform door', async () => {
15
+ await it('has a vector row for every vector', () => {
16
+ expect(GTK_PLATFORM_VECTORS.length > 0).toBe(true);
17
+ });
18
+ });
19
+ };
@@ -53,6 +53,28 @@ export const GtkTextViewTest = async () => {
53
53
  second.remove();
54
54
  });
55
55
 
56
+ await it('`buffer.insert_at_cursor` appends at the caret and `cursor_position` follows it', () => {
57
+ const el = mount('');
58
+ expect(el.buffer.cursor_position).toBe(0);
59
+ el.buffer.insert_at_cursor('one', -1);
60
+ el.buffer.insert_at_cursor('\ntwo', -1);
61
+ expect(el.text).toBe('one\ntwo');
62
+ expect(el.buffer.cursor_position).toBe(7);
63
+ el.buffer.insert_at_cursor('xyz', 1);
64
+ expect(el.text).toBe('one\ntwox');
65
+ unmountAll();
66
+ });
67
+
68
+ await it('`buffer.set_text` replaces the text and `get_text` reads it', () => {
69
+ const el = mount('old');
70
+ el.buffer.set_text('new', -1);
71
+ expect(el.buffer.get_text()).toBe('new');
72
+ el.buffer.set_text('', 0);
73
+ expect(el.text).toBe('');
74
+ expect(el.buffer.cursor_position).toBe(0);
75
+ unmountAll();
76
+ });
77
+
56
78
  await it("re-emits the buffer's `changed` on an edit", () => {
57
79
  const el = mount('a');
58
80
  const seen: unknown[] = [];
@@ -111,3 +111,4 @@ export { AdwWindowTitle as WindowTitle } from '../elements/adw-window-title.js';
111
111
  export { AdwWrapBox as WrapBox } from '../elements/adw-wrap-box.js';
112
112
  export { AdwApplication as Application } from '../application.js';
113
113
  export { AdwStyleManager as StyleManager } from '../style-manager.js';
114
+ export { ColorScheme } from '@gjsify/adwaita-core';
@@ -5,7 +5,7 @@
5
5
  //
6
6
  // A TRUE SUBSET: everything else in Gdk is absent, and the `gi://Gdk` arm refuses it by name.
7
7
 
8
- import { createGdk } from '@gjsify/adwaita-core';
8
+ import { CURRENT_TIME as GDK_PLATFORM_CURRENT_TIME, GDK_KEYS, createGdk } from '@gjsify/adwaita-core';
9
9
 
10
10
  import { webClipboardHost } from '../clipboard-host.js';
11
11
 
@@ -15,3 +15,20 @@ export const Display = gdk.Display;
15
15
  export const Clipboard = gdk.Clipboard;
16
16
  export const ContentProvider = gdk.ContentProvider;
17
17
  export const ContentFormats = gdk.ContentFormats;
18
+
19
+ export const CURRENT_TIME = GDK_PLATFORM_CURRENT_TIME;
20
+ export const {
21
+ KEY_space,
22
+ KEY_a,
23
+ KEY_d,
24
+ KEY_e,
25
+ KEY_q,
26
+ KEY_s,
27
+ KEY_w,
28
+ KEY_Return,
29
+ KEY_Escape,
30
+ KEY_Left,
31
+ KEY_Up,
32
+ KEY_Right,
33
+ KEY_Down,
34
+ } = GDK_KEYS;
@@ -12,10 +12,28 @@
12
12
  // `check-vocabulary-alignment.mjs` derives their members from the elements this package
13
13
  // defines. GIO defines none: what lands here is the handful of GObject types an Adwaita
14
14
  // author CONSTRUCTS while building a tree — today the menu a `menuModel` property takes.
15
- // `Gio.File`, `Gio.Settings` and the rest of the library are NOT this
16
- // package's business; a namespace that grows past what the elements consume is a second,
15
+ // `Gio.Settings` is here (ADR 0099, the `localStorage` door); the rest of the library is NOT
16
+ // this package's business; a namespace that grows past what the elements consume is a second,
17
17
  // unheld vocabulary.
18
+ //
19
+ // THE ONE OTHER EXCEPTION IS ADR 0100: the `Gio.File` a `Gtk.FileDialog` hands back, with the
20
+ // `ListStore` its `filters` take, the streams `replace_async` opens, `FileCreateFlags`,
21
+ // `IOErrorEnum` and `_promisify`. The subset lives in `@gjsify/adwaita-core`
22
+ // (`file-chooser.ts`), which refuses everything past it by name; `../file-chooser-host.ts`
23
+ // is the browser's driver behind it.
24
+
25
+ import '../file-chooser-host.js';
18
26
 
19
27
  export { GioMenu as Menu, GioMenuItem as MenuItem } from '@gjsify/adwaita-core';
28
+ export {
29
+ GioFile as File,
30
+ GioListStore as ListStore,
31
+ GioOutputStream as OutputStream,
32
+ GioFileOutputStream as FileOutputStream,
33
+ FileCreateFlags,
34
+ IOErrorEnum,
35
+ promisify as _promisify,
36
+ } from '@gjsify/adwaita-core';
20
37
  export { SimpleAction, SimpleActionGroup, ApplicationFlags } from '@gjsify/adwaita-core';
21
38
  export { GioApplication as Application } from '../application.js';
39
+ export { Settings } from '@gjsify/app-settings/browser';
@@ -7,9 +7,14 @@
7
7
  // in `glib-paths.ts` of the core; the host has no filesystem, so `get_current_dir()` is `"/"` and
8
8
  // `get_system_data_dirs()` is `[]`. A port that knows better replaces the member in its own file.
9
9
  //
10
- // A TRUE SUBSET: everything else in GLib (`MainLoop`, `Source`, `timeout_add_seconds`, `Bytes`,
10
+ // `Bytes` and `Error` are the ADR 0100 subset a `Gio.File` reads and fails with (`get_data`,
11
+ // `matches`, `code`, `message`), re-exported from the core's `glib-bytes.ts` / `glib-error.ts`.
12
+ //
13
+ // A TRUE SUBSET: everything else in GLib (`MainLoop`, `Source`, `timeout_add_seconds`,
11
14
  // `get_home_dir`, …) is absent, and the `gi://GLib` arm refuses it by name.
12
15
 
16
+ export { GLibBytes as Bytes, GLibError as Error } from '@gjsify/adwaita-core';
17
+
13
18
  import {
14
19
  buildFilenamev,
15
20
  createGLibTimers,
@@ -1,10 +1,12 @@
1
1
  // The plain GTK enums an application reads off `Gtk` — `Gtk.PolicyType`, `Gtk.TextDirection`,
2
- // `Gtk.TextExtendSelection` and `Gtk.TextWindowType`, as value tables. The numbers are the typelib's (GIR declaration order),
3
- // and the same tables the NativeScript port publishes.
2
+ // `Gtk.TextExtendSelection`, `Gtk.TextWindowType` and `Gtk.License`, as value tables. The numbers
3
+ // are the typelib's (GIR declaration order), and the same tables the NativeScript port publishes.
4
4
  //
5
5
  // Reference: refs/gtk gtk/gtkenums.h (GtkPolicyType, GtkTextDirection), gtk/gtktextview.h (GtkTextWindowType)
6
6
  // Copyright (c) The GTK Team. LGPLv2.1+.
7
7
 
8
+ import { GTK_LICENSE } from '@gjsify/adwaita-core';
9
+
8
10
  /** `Gtk.PolicyType`: `ALWAYS` 0, `AUTOMATIC` 1, `NEVER` 2, `EXTERNAL` 3. */
9
11
  export const PolicyType = Object.freeze({
10
12
  ALWAYS: 0,
@@ -32,3 +34,6 @@ export const TextWindowType = Object.freeze({
32
34
 
33
35
  /** `Gtk.TextExtendSelection`: `WORD` 0 (a double click), `LINE` 1 (a triple click). */
34
36
  export const TextExtendSelection = Object.freeze({ WORD: 0, LINE: 1 } as const);
37
+
38
+ /** `Gtk.License`, the table `Adw.AboutDialog` reads. */
39
+ export const License = GTK_LICENSE;
@@ -0,0 +1,16 @@
1
+ // The web door's `Gtk.StyleContext`, `Gtk.Settings`, `Gtk.IconTheme`, `Gtk.show_uri` and
2
+ // `Gtk.EventControllerKey`: the behaviour is `@gjsify/adwaita-core`'s `gtk-platform.ts`, the browser
3
+ // is `../platform-host.ts`.
4
+
5
+ import { createGtkPlatform } from '@gjsify/adwaita-core';
6
+
7
+ import { webPlatformHost } from '../platform-host.js';
8
+
9
+ const platform = createGtkPlatform(webPlatformHost);
10
+
11
+ export const CssProvider = platform.CssProvider;
12
+ export const StyleContext = platform.StyleContext;
13
+ export const Settings = platform.Settings;
14
+ export const IconTheme = platform.IconTheme;
15
+ export const EventControllerKey = platform.EventControllerKey;
16
+ export const show_uri = platform.show_uri;
@@ -21,7 +21,10 @@
21
21
  // are the same write and nothing in this package had to learn a second input shape.
22
22
 
23
23
  export { GtkAdjustment as Adjustment } from '@gjsify/adwaita-core';
24
- export { PolicyType, TextDirection, TextExtendSelection, TextWindowType } from './gtk-enums.js';
24
+ // ADR 0100: the file dialog is no element either — `present`-less, it asks the driver
25
+ // `../file-chooser-host.ts` registers (imported by `./gio.ts`) and resolves a `Gio.File`.
26
+ export { GtkFileDialog as FileDialog, GtkFileFilter as FileFilter, DialogError } from '@gjsify/adwaita-core';
27
+ export { License, PolicyType, TextDirection, TextExtendSelection, TextWindowType } from './gtk-enums.js';
25
28
  export { GtkAboutDialog as AboutDialog } from '../elements/gtk-about-dialog.js';
26
29
  export { GtkActionBar as ActionBar } from '../elements/gtk-action-bar.js';
27
30
  export { GtkApplicationWindow as ApplicationWindow } from '../elements/gtk-application-window.js';
@@ -97,3 +100,11 @@ export { GtkWindowControls as WindowControls } from '../elements/gtk-window-cont
97
100
  export { GtkViewport as Viewport } from '../elements/gtk-viewport.js';
98
101
  export { GtkWindowHandle as WindowHandle } from '../elements/gtk-window-handle.js';
99
102
  export { GtkApplication as Application } from '../application.js';
103
+ export {
104
+ STYLE_PROVIDER_PRIORITY_APPLICATION,
105
+ STYLE_PROVIDER_PRIORITY_FALLBACK,
106
+ STYLE_PROVIDER_PRIORITY_SETTINGS,
107
+ STYLE_PROVIDER_PRIORITY_THEME,
108
+ STYLE_PROVIDER_PRIORITY_USER,
109
+ } from '@gjsify/adwaita-core';
110
+ export { CssProvider, EventControllerKey, IconTheme, show_uri, StyleContext, Settings } from './gtk-platform.js';