@gjsify/adwaita-web 0.55.0 → 0.57.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 +8 -0
- package/dist/adwaita-web.css +137 -29
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/adw-status-page.spec.d.ts +1 -0
- package/lib/types/application.d.ts +24 -0
- package/lib/types/application.spec.d.ts +1 -0
- package/lib/types/attributes.d.ts +19 -0
- package/lib/types/elements/adw-action-row.d.ts +4 -0
- package/lib/types/elements/adw-application-window.d.ts +2 -0
- package/lib/types/elements/adw-combo-row.d.ts +4 -0
- package/lib/types/elements/adw-entry-row.d.ts +6 -0
- package/lib/types/elements/adw-expander-row.d.ts +2 -0
- package/lib/types/elements/adw-header-bar.d.ts +6 -0
- package/lib/types/elements/adw-overlay-split-view.d.ts +13 -0
- package/lib/types/elements/adw-sidebar.d.ts +3 -2
- package/lib/types/elements/adw-spin-row.d.ts +6 -0
- package/lib/types/elements/adw-switch-row.d.ts +4 -0
- package/lib/types/elements/adw-toggle-group.d.ts +17 -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 +26 -2
- package/lib/types/elements/gtk-stack.d.ts +3 -0
- package/lib/types/elements/gtk-switch.d.ts +3 -0
- package/lib/types/elements/gtk-window.d.ts +8 -2
- 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 +77 -0
- package/lib/types/gtksource/gtk-source-view.spec.d.ts +2 -0
- package/lib/types/gtksource/web-editor-driver.d.ts +44 -0
- package/lib/types/index.d.ts +4 -1
- package/lib/types/menu-actions.spec.d.ts +1 -0
- package/lib/types/namespace/adw.d.ts +1 -0
- package/lib/types/namespace/gio.d.ts +2 -0
- package/lib/types/namespace/glib.d.ts +10 -0
- package/lib/types/namespace/gobject.d.ts +27 -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/row-line-clamp.d.ts +20 -0
- package/lib/types/row-tooltip.d.ts +8 -0
- package/lib/types/row-tooltip.spec.d.ts +1 -0
- package/lib/types/shared-tree-builder.d.ts +69 -20
- package/lib/types/shared-tree-mount.spec.d.ts +1 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/widget-size.spec.d.ts +1 -0
- package/package.json +11 -9
- package/scss/_action_row.scss +39 -15
- package/scss/_button_row.scss +5 -0
- package/scss/_expander_row.scss +36 -3
- package/scss/_headerbar.scss +2 -1
- package/scss/_preferences_row.scss +9 -0
- package/scss/_reset.scss +1 -1
- package/scss/_row.scss +16 -0
- package/scss/_status_page.scss +1 -1
- package/scss/_toggle_group.scss +28 -0
- package/scss/_widget.scss +14 -3
- package/scss/_window.scss +13 -1
- package/src/adw-action-rows.spec.ts +255 -0
- package/src/adw-header-bar.spec.ts +18 -0
- package/src/adw-status-page.spec.ts +124 -0
- package/src/application.spec.ts +75 -0
- package/src/application.ts +92 -0
- package/src/attributes.ts +52 -0
- package/src/capabilities.mjs +3 -0
- package/src/construct-vectors.spec.ts +156 -1
- package/src/elements/adw-action-row.ts +31 -2
- package/src/elements/adw-alert-dialog.ts +4 -1
- package/src/elements/adw-application-window.ts +5 -0
- package/src/elements/adw-button-row.ts +7 -1
- package/src/elements/adw-combo-row.ts +26 -1
- package/src/elements/adw-entry-row.ts +22 -12
- package/src/elements/adw-expander-row.ts +34 -1
- package/src/elements/adw-header-bar.ts +18 -4
- package/src/elements/adw-overlay-split-view.ts +29 -6
- package/src/elements/adw-preferences-row.ts +5 -2
- package/src/elements/adw-sidebar.ts +33 -5
- package/src/elements/adw-spin-row.ts +37 -2
- package/src/elements/adw-split-button.ts +5 -1
- package/src/elements/adw-status-page.ts +10 -2
- package/src/elements/adw-switch-row.ts +25 -1
- package/src/elements/adw-toggle-group.ts +119 -10
- package/src/elements/adw-view-stack.ts +14 -0
- package/src/elements/adw-window.ts +9 -4
- 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 +76 -7
- package/src/elements/gtk-stack.ts +8 -0
- package/src/elements/gtk-switch.ts +8 -0
- package/src/elements/gtk-window.ts +9 -3
- package/src/glib.spec.ts +34 -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/gtk-window.spec.ts +12 -0
- package/src/gtksource/gtk-source-view.spec.ts +69 -0
- package/src/gtksource/gtk-source-view.ts +379 -0
- package/src/gtksource/web-editor-driver.ts +260 -0
- package/src/index.ts +11 -1
- package/src/keyboard-operable.spec.ts +47 -11
- package/src/menu-actions.spec.ts +47 -0
- package/src/namespace/adw.ts +1 -0
- package/src/namespace/gio.ts +2 -0
- package/src/namespace/glib.ts +22 -0
- package/src/namespace/gobject.ts +37 -0
- package/src/namespace/gtk-enums.ts +15 -0
- package/src/namespace/gtk.ts +2 -0
- package/src/namespace/gtksource.ts +9 -0
- package/src/row-line-clamp.ts +74 -0
- package/src/row-tooltip.spec.ts +313 -0
- package/src/row-tooltip.ts +52 -0
- package/src/shared-tree-builder.ts +230 -42
- package/src/shared-tree-mount.spec.ts +130 -0
- package/src/shared-trees.spec.ts +15 -0
- package/src/split-button.spec.ts +2 -0
- package/src/styles.generated.ts +1 -1
- package/src/test.browser.mts +18 -0
- package/src/widget-size.spec.ts +117 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// The browser half of `GtkSource.View`: a `<textarea>` that owns the caret, selection, input,
|
|
2
|
+
// IME and undo, with the highlight painted on a backdrop behind its transparent text and the
|
|
3
|
+
// line numbers in a gutter beside it. The structure mirrors the Android driver
|
|
4
|
+
// (`packages/nativescript-bridge/gtksource/src/native-editor.android.ts`): the platform edits
|
|
5
|
+
// text natively, and this class reports what the user did to an `EditorHost`.
|
|
6
|
+
//
|
|
7
|
+
// NO WORD WRAP, as on Android: `wrap="off"` and `white-space: pre` keep every logical line on
|
|
8
|
+
// one layout line, so the backdrop and the gutter stay aligned by a fixed line height.
|
|
9
|
+
|
|
10
|
+
import type { EditorDriver, EditorHost, EditorLayout, EditorPalette, StyledRun } from '@gjsify/gtksource-core';
|
|
11
|
+
import { digitCount, LineStore } from '@gjsify/gtksource-core';
|
|
12
|
+
|
|
13
|
+
const GUTTER_PADDING_PX = 8;
|
|
14
|
+
|
|
15
|
+
/** A signed ARGB int (the core's colour unit) as a CSS colour. */
|
|
16
|
+
function cssColor(argb: number): string {
|
|
17
|
+
const alpha = ((argb >>> 24) & 0xff) / 255;
|
|
18
|
+
return `rgba(${(argb >>> 16) & 0xff}, ${(argb >>> 8) & 0xff}, ${argb & 0xff}, ${alpha})`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface EditorParts {
|
|
22
|
+
readonly root: HTMLElement;
|
|
23
|
+
readonly area: HTMLTextAreaElement;
|
|
24
|
+
readonly backdrop: HTMLElement;
|
|
25
|
+
readonly gutter: HTMLElement;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class WebEditorDriver implements EditorDriver {
|
|
29
|
+
private host: EditorHost | null = null;
|
|
30
|
+
private text = '';
|
|
31
|
+
private caret = 0;
|
|
32
|
+
private reported = { start: 0, end: 0 };
|
|
33
|
+
private layout: EditorLayout | null = null;
|
|
34
|
+
private readonly runs = new LineStore<readonly StyledRun[]>();
|
|
35
|
+
private scheduled = false;
|
|
36
|
+
private listening: Array<() => void> = [];
|
|
37
|
+
private palette: EditorPalette | null = null;
|
|
38
|
+
private attached = false;
|
|
39
|
+
|
|
40
|
+
constructor(private readonly parts: EditorParts) {}
|
|
41
|
+
|
|
42
|
+
bind(host: EditorHost): void {
|
|
43
|
+
this.host = host;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Starts listening to the textarea; the counterpart of the Android `attach()`. */
|
|
47
|
+
attach(): void {
|
|
48
|
+
if (this.listening.length > 0) return;
|
|
49
|
+
this.attached = true;
|
|
50
|
+
const { area, backdrop, gutter } = this.parts;
|
|
51
|
+
const on = <T extends EventTarget>(target: T, type: string, listener: EventListener) => {
|
|
52
|
+
target.addEventListener(type, listener);
|
|
53
|
+
this.listening.push(() => target.removeEventListener(type, listener));
|
|
54
|
+
};
|
|
55
|
+
on(area, 'input', () => this.onInput());
|
|
56
|
+
on(area, 'scroll', () => {
|
|
57
|
+
backdrop.scrollTop = area.scrollTop;
|
|
58
|
+
backdrop.scrollLeft = area.scrollLeft;
|
|
59
|
+
(gutter.firstElementChild as HTMLElement | null)?.style.setProperty(
|
|
60
|
+
'transform',
|
|
61
|
+
`translateY(${-area.scrollTop}px)`,
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
on(area, 'select', () => this.onSelection());
|
|
65
|
+
on(area, 'keyup', () => this.onSelection());
|
|
66
|
+
on(area, 'mouseup', () => this.onSelection());
|
|
67
|
+
on(area, 'keydown', (event) => this.onKey(event as KeyboardEvent));
|
|
68
|
+
// Firefox and Chromium fire `selectionchange` on different targets for a textarea.
|
|
69
|
+
on(area.ownerDocument, 'selectionchange', () => {
|
|
70
|
+
if (area.ownerDocument.activeElement === area) this.onSelection();
|
|
71
|
+
});
|
|
72
|
+
if (this.layout) this.applyLayout(this.layout);
|
|
73
|
+
if (this.palette) this.applyPalette(this.palette);
|
|
74
|
+
this.scheduleRender();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
detach(): void {
|
|
78
|
+
this.attached = false;
|
|
79
|
+
for (const stop of this.listening.splice(0)) stop();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- what the user did -------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
private onInput(): void {
|
|
85
|
+
const next = this.parts.area.value;
|
|
86
|
+
const prev = this.text;
|
|
87
|
+
let prefix = 0;
|
|
88
|
+
const shortest = Math.min(prev.length, next.length);
|
|
89
|
+
while (prefix < shortest && prev.charCodeAt(prefix) === next.charCodeAt(prefix)) prefix++;
|
|
90
|
+
let suffix = 0;
|
|
91
|
+
while (
|
|
92
|
+
suffix < shortest - prefix &&
|
|
93
|
+
prev.charCodeAt(prev.length - 1 - suffix) === next.charCodeAt(next.length - 1 - suffix)
|
|
94
|
+
) {
|
|
95
|
+
suffix++;
|
|
96
|
+
}
|
|
97
|
+
this.text = next;
|
|
98
|
+
this.host?.onNativeEdit(prefix, prev.length - prefix - suffix, next.slice(prefix, next.length - suffix));
|
|
99
|
+
this.onSelection();
|
|
100
|
+
this.scheduleRender();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private onSelection(): void {
|
|
104
|
+
const { area } = this.parts;
|
|
105
|
+
const start = area.selectionStart ?? 0;
|
|
106
|
+
const end = area.selectionEnd ?? start;
|
|
107
|
+
if (start === this.reported.start && end === this.reported.end) return;
|
|
108
|
+
this.reported = { start, end };
|
|
109
|
+
this.caret = end;
|
|
110
|
+
this.host?.onNativeSelection(start, end);
|
|
111
|
+
this.scheduleRender();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** `accepts-tab` is GTK's default; a textarea would move focus, so Tab inserts a tab. */
|
|
115
|
+
private onKey(event: KeyboardEvent): void {
|
|
116
|
+
if (event.key !== 'Tab' || event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) return;
|
|
117
|
+
if (this.parts.area.readOnly) return;
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
// `insertText` keeps the edit on the browser's own undo stack and fires `input`.
|
|
120
|
+
if (!this.parts.area.ownerDocument.execCommand('insertText', false, '\t')) {
|
|
121
|
+
const { area } = this.parts;
|
|
122
|
+
area.setRangeText('\t', area.selectionStart, area.selectionEnd, 'end');
|
|
123
|
+
this.onInput();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- what to show ------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
setText(text: string): void {
|
|
130
|
+
this.text = text;
|
|
131
|
+
this.parts.area.value = text;
|
|
132
|
+
this.runs.clear();
|
|
133
|
+
this.scheduleRender();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
replaceRange(start: number, end: number, text: string): void {
|
|
137
|
+
const { area } = this.parts;
|
|
138
|
+
this.text = this.text.slice(0, start) + text + this.text.slice(end);
|
|
139
|
+
// 'preserve' leaves the selection to the session, which sets it explicitly afterwards.
|
|
140
|
+
area.setRangeText(text, start, end, 'preserve');
|
|
141
|
+
this.scheduleRender();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
setSelection(start: number, end: number): void {
|
|
145
|
+
this.reported = { start, end };
|
|
146
|
+
this.caret = end;
|
|
147
|
+
this.parts.area.setSelectionRange(start, end);
|
|
148
|
+
this.scheduleRender();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
setLineCount(): void {
|
|
152
|
+
this.scheduleRender();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setLayout(layout: EditorLayout): void {
|
|
156
|
+
this.layout = layout;
|
|
157
|
+
if (this.attached) this.applyLayout(layout);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Attributes and styles on the host wait for `attach()`: a custom element's constructor may not write them. */
|
|
161
|
+
private applyLayout(layout: EditorLayout): void {
|
|
162
|
+
const { root, area } = this.parts;
|
|
163
|
+
area.readOnly = !layout.editable;
|
|
164
|
+
root.classList.toggle('monospace', layout.monospace);
|
|
165
|
+
root.classList.toggle('show-line-numbers', layout.showLineNumbers);
|
|
166
|
+
root.classList.toggle('cursor-hidden', !layout.cursorVisible);
|
|
167
|
+
root.style.setProperty('--gsv-margin-left', `${layout.leftMargin}px`);
|
|
168
|
+
root.style.setProperty('--gsv-margin-right', `${layout.rightMargin}px`);
|
|
169
|
+
root.style.setProperty('--gsv-margin-top', `${layout.topMargin}px`);
|
|
170
|
+
root.style.setProperty('--gsv-margin-bottom', `${layout.bottomMargin}px`);
|
|
171
|
+
this.scheduleRender();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
setPalette(palette: EditorPalette): void {
|
|
175
|
+
this.palette = palette;
|
|
176
|
+
if (this.attached) this.applyPalette(palette);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private applyPalette(palette: EditorPalette): void {
|
|
180
|
+
const { style } = this.parts.root;
|
|
181
|
+
style.setProperty('--gsv-fg', cssColor(palette.foreground));
|
|
182
|
+
style.setProperty('--gsv-bg', cssColor(palette.background));
|
|
183
|
+
style.setProperty('--gsv-selection', cssColor(palette.selectionBackground));
|
|
184
|
+
style.setProperty('--gsv-current-line', cssColor(palette.currentLineBackground));
|
|
185
|
+
style.setProperty('--gsv-number-fg', cssColor(palette.lineNumberForeground));
|
|
186
|
+
style.setProperty('--gsv-number-bg', cssColor(palette.lineNumberBackground));
|
|
187
|
+
style.setProperty('--gsv-current-number-fg', cssColor(palette.currentLineNumberForeground));
|
|
188
|
+
style.setProperty('--gsv-current-number-bg', cssColor(palette.currentLineNumberBackground));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
spliceLines(first: number, removed: number, inserted: number): void {
|
|
192
|
+
this.runs.splice(first, removed, inserted);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
paintLine(line: number, _lineStart: number, runs: readonly StyledRun[]): void {
|
|
196
|
+
this.runs.set(line, runs);
|
|
197
|
+
this.scheduleRender();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
clearAll(): void {
|
|
201
|
+
this.runs.clear();
|
|
202
|
+
this.scheduleRender();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// --- painting ----------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
/** One repaint per task: a burst of `paintLine` calls from one edit costs one DOM rebuild. */
|
|
208
|
+
private scheduleRender(): void {
|
|
209
|
+
if (this.scheduled || !this.attached) return;
|
|
210
|
+
this.scheduled = true;
|
|
211
|
+
queueMicrotask(() => {
|
|
212
|
+
this.scheduled = false;
|
|
213
|
+
this.render();
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private render(): void {
|
|
218
|
+
const { backdrop, gutter, root } = this.parts;
|
|
219
|
+
const lines = this.text.split('\n');
|
|
220
|
+
const highlight = this.layout?.highlightCurrentLine === true;
|
|
221
|
+
const caretLine = this.text.slice(0, this.caret).split('\n').length - 1;
|
|
222
|
+
|
|
223
|
+
const rows: HTMLElement[] = [];
|
|
224
|
+
const numbers: HTMLElement[] = [];
|
|
225
|
+
lines.forEach((line, index) => {
|
|
226
|
+
const row = document.createElement('div');
|
|
227
|
+
row.className = highlight && index === caretLine ? 'gsv-line current' : 'gsv-line';
|
|
228
|
+
let at = 0;
|
|
229
|
+
for (const run of this.runs.get(index) ?? []) {
|
|
230
|
+
if (run.start > at) row.append(line.slice(at, run.start));
|
|
231
|
+
const span = document.createElement('span');
|
|
232
|
+
span.textContent = line.slice(run.start, run.end);
|
|
233
|
+
const { style } = run;
|
|
234
|
+
if (style.foreground !== undefined) span.style.color = cssColor(style.foreground);
|
|
235
|
+
if (style.background !== undefined) span.style.backgroundColor = cssColor(style.background);
|
|
236
|
+
if (style.bold) span.style.fontWeight = 'bold';
|
|
237
|
+
if (style.italic) span.style.fontStyle = 'italic';
|
|
238
|
+
const decorations = [style.underline ? 'underline' : '', style.strikethrough ? 'line-through' : ''];
|
|
239
|
+
span.style.textDecoration = decorations.filter(Boolean).join(' ');
|
|
240
|
+
row.append(span);
|
|
241
|
+
at = run.end;
|
|
242
|
+
}
|
|
243
|
+
if (at < line.length) row.append(line.slice(at));
|
|
244
|
+
rows.push(row);
|
|
245
|
+
|
|
246
|
+
const number = document.createElement('div');
|
|
247
|
+
number.className = highlight && index === caretLine ? 'gsv-number current' : 'gsv-number';
|
|
248
|
+
number.textContent = String(index + 1);
|
|
249
|
+
numbers.push(number);
|
|
250
|
+
});
|
|
251
|
+
backdrop.replaceChildren(...rows);
|
|
252
|
+
const column = document.createElement('div');
|
|
253
|
+
column.className = 'gsv-numbers';
|
|
254
|
+
column.append(...numbers);
|
|
255
|
+
column.style.transform = `translateY(${-this.parts.area.scrollTop}px)`;
|
|
256
|
+
gutter.replaceChildren(column);
|
|
257
|
+
root.style.setProperty('--gsv-gutter-width', `${digitCount(lines.length) + 0}ch`);
|
|
258
|
+
root.style.setProperty('--gsv-gutter-padding', `${GUTTER_PADDING_PX}px`);
|
|
259
|
+
}
|
|
260
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -81,7 +81,7 @@ export type { AdwUndershootEdges } from './scroll-shading.js';
|
|
|
81
81
|
// spec itself — has a route to it that is not a `.spec.ts` import; see that file's header
|
|
82
82
|
// for why `mountSharedTree` and not `buildSharedTree` alone is the instantiation half.
|
|
83
83
|
export { buildSharedTree, mountSharedTree } from './shared-tree-builder.js';
|
|
84
|
-
export type { MountedSharedTree, SizeSource } from './shared-tree-builder.js';
|
|
84
|
+
export type { MountOptions, MountedSharedTree, SizeSource } from './shared-tree-builder.js';
|
|
85
85
|
export { registerTemplateClass } from './template-classes.js';
|
|
86
86
|
|
|
87
87
|
// The stylesheet compiles a chosen SUBSET of `@gjsify/adwaita-icons` (the whole set is
|
|
@@ -119,6 +119,16 @@ export * as Gtk from './namespace/gtk.js';
|
|
|
119
119
|
// program here as on GJS — `./namespace/gio.ts` says what does not go in it.
|
|
120
120
|
export * as Gio from './namespace/gio.js';
|
|
121
121
|
|
|
122
|
+
// GLib's timers (`idle_add`, `timeout_add`, `source_remove`): `./namespace/glib.ts` says what is absent.
|
|
123
|
+
export * as GLib from './namespace/glib.js';
|
|
124
|
+
|
|
125
|
+
// The fourth, and the one that makes an unmodified GNOME component class run (ADR 0096): the GObject
|
|
126
|
+
// subset, `registerClass` first. `./namespace/gobject.ts` says what it is.
|
|
127
|
+
export * as GObject from './namespace/gobject.js';
|
|
128
|
+
|
|
129
|
+
// GtkSource (ADR 0094 amendment): `GtkSource.View` over `@gjsify/gtksource-core`, `./namespace/gtksource.ts`.
|
|
130
|
+
export * as GtkSource from './namespace/gtksource.js';
|
|
131
|
+
|
|
122
132
|
// WHAT DID NOT MOVE INTO THE NAMESPACE, and the rule that decides it. A member exists
|
|
123
133
|
// for an element whose GIR tag names a real widget; `WEB_ELEMENT_ALIGNMENT` declares the
|
|
124
134
|
// rest `webOnly`, meaning no widget in the reference vocabulary stands behind it, so
|
|
@@ -622,21 +622,57 @@ export const AdwKeyboardOperableTest = async () => {
|
|
|
622
622
|
// Prose cannot hold that: this line fails the commit that adds it.
|
|
623
623
|
// `<adw-inline-view-switcher>` has the same gap (status/open-todos/README.md).
|
|
624
624
|
// `active-name` joined the list without touching the axis: it picks a toggle.
|
|
625
|
-
|
|
625
|
+
// `sensitive` joined it for the same reason — it gates every button's `disabled`,
|
|
626
|
+
// never the axis. `keyboard-operable.spec.ts` below now pins the roving walk
|
|
627
|
+
// against a disabled toggle, which is what made that attribute safe to add.
|
|
628
|
+
expect([...AdwToggleGroup.observedAttributes]).toStrictEqual([
|
|
629
|
+
'active',
|
|
630
|
+
'active-name',
|
|
631
|
+
'flat',
|
|
632
|
+
'round',
|
|
633
|
+
'sensitive',
|
|
634
|
+
]);
|
|
626
635
|
});
|
|
627
636
|
|
|
628
637
|
await it('adw-toggle has no state a roving walk would have to skip', async () => {
|
|
629
|
-
// `<adw-toggle-group>`
|
|
630
|
-
//
|
|
631
|
-
//
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
// in the walk to step over it.
|
|
635
|
-
//
|
|
638
|
+
// `<adw-toggle-group>` FILTERS its buttons for `attachRovingFocus`, which is only
|
|
639
|
+
// necessary once some `<adw-toggle>` attribute can produce a disabled button.
|
|
640
|
+
// `enabled` is that attribute (`AdwToggle:enabled`), so this list is now the
|
|
641
|
+
// record of WHICH attributes can — `hidden` cannot, and no other one can.
|
|
642
|
+
// Grow it and the filter has to grow with it: an unskipped disabled toggle is a
|
|
643
|
+
// `focus()` the browser refuses, with nothing in the walk to step over it. The
|
|
644
|
+
// skip itself is pinned in the case below.
|
|
636
645
|
//
|
|
637
|
-
// `tooltip`
|
|
638
|
-
//
|
|
639
|
-
expect([...AdwToggle.observedAttributes]).toStrictEqual(['label', 'icon-name', 'tooltip']);
|
|
646
|
+
// `tooltip` is not a counterexample: it sets `title` and `aria-label`, which
|
|
647
|
+
// change neither reachability nor rendering.
|
|
648
|
+
expect([...AdwToggle.observedAttributes]).toStrictEqual(['label', 'icon-name', 'tooltip', 'enabled']);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
await it('adw-toggle-group arrow skips a disabled toggle', async () => {
|
|
652
|
+
// `AdwToggle:enabled` reached the web element with #1818, and the filter
|
|
653
|
+
// `roving-focus.ts` asks of every caller is the price of it — the same obligation
|
|
654
|
+
// `<adw-sidebar>` already pays ("arrow skips a disabled row", above). A disabled
|
|
655
|
+
// `<button>` cannot take focus, so leaving it in the walk strands the user on a
|
|
656
|
+
// `focus()` the browser refuses.
|
|
657
|
+
withWidget(
|
|
658
|
+
() => {
|
|
659
|
+
const el = document.createElement('adw-toggle-group');
|
|
660
|
+
el.innerHTML =
|
|
661
|
+
'<adw-toggle label="One"></adw-toggle>' +
|
|
662
|
+
'<adw-toggle label="Two" enabled="false"></adw-toggle>' +
|
|
663
|
+
'<adw-toggle label="Three"></adw-toggle>';
|
|
664
|
+
return el;
|
|
665
|
+
},
|
|
666
|
+
(el) => {
|
|
667
|
+
const items = rovingCase('adw-toggle-group').items(el);
|
|
668
|
+
expect(items.length).toBe(3);
|
|
669
|
+
expect((items[1] as HTMLButtonElement).disabled).toBe(true);
|
|
670
|
+
|
|
671
|
+
items[0].focus();
|
|
672
|
+
press(items[0], 'ArrowRight');
|
|
673
|
+
expect(document.activeElement).toBe(items[2]);
|
|
674
|
+
},
|
|
675
|
+
);
|
|
640
676
|
});
|
|
641
677
|
|
|
642
678
|
await it('adw-toggle-group notifies once per arrow, through the click path', async () => {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// ADR 0097 §§ 1, 3 on the browser: the action resolution from the owning `gtk-menu-button`, and a
|
|
2
|
+
// tree's menus reaching its `menuModel`.
|
|
3
|
+
import {
|
|
4
|
+
MENU_ACTION_VECTORS,
|
|
5
|
+
MENU_TREE_VECTORS,
|
|
6
|
+
driveMenuActionVectors,
|
|
7
|
+
registryMenuActionScene,
|
|
8
|
+
type SharedTreeNode,
|
|
9
|
+
} from '@gjsify/adwaita-core/conformance';
|
|
10
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
11
|
+
|
|
12
|
+
import { parentOfElement } from './application.js';
|
|
13
|
+
import { buildSharedTree } from './shared-tree-builder.js';
|
|
14
|
+
import type { GtkMenuButton } from './elements/gtk-menu-button.js';
|
|
15
|
+
|
|
16
|
+
const made: Element[] = [];
|
|
17
|
+
|
|
18
|
+
export const MenuActionsTest = async () => {
|
|
19
|
+
expect(MENU_ACTION_VECTORS.length > 0).toBe(true);
|
|
20
|
+
await driveMenuActionVectors(
|
|
21
|
+
'adwaita-web (gtk-menu-button)',
|
|
22
|
+
registryMenuActionScene(() => {
|
|
23
|
+
const outer = document.createElement('div');
|
|
24
|
+
const button = document.createElement('gtk-menu-button');
|
|
25
|
+
outer.append(button);
|
|
26
|
+
document.body.append(outer);
|
|
27
|
+
made.push(outer);
|
|
28
|
+
return { outer, button };
|
|
29
|
+
}, parentOfElement),
|
|
30
|
+
{ describe, it, expect },
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
await describe('adwaita-web: menus in a shared tree (ADR 0097 § 1)', async () => {
|
|
34
|
+
for (const { result, rule, tree } of MENU_TREE_VECTORS) {
|
|
35
|
+
await it(rule, () => {
|
|
36
|
+
const node = tree as unknown as SharedTreeNode;
|
|
37
|
+
if ('refused' in result) {
|
|
38
|
+
expect(() => buildSharedTree(node)).toThrow(result.refused);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const button = buildSharedTree(node) as unknown as GtkMenuButton;
|
|
42
|
+
expect(button.menuModel.map((entry) => entry.kind).join(',')).toBe(result.kinds.join(','));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
for (const element of made.splice(0)) element.remove();
|
|
47
|
+
};
|
package/src/namespace/adw.ts
CHANGED
|
@@ -109,3 +109,4 @@ export { AdwViewSwitcherBar as ViewSwitcherBar } from '../elements/adw-view-swit
|
|
|
109
109
|
export { AdwWindow as Window } from '../elements/adw-window.js';
|
|
110
110
|
export { AdwWindowTitle as WindowTitle } from '../elements/adw-window-title.js';
|
|
111
111
|
export { AdwWrapBox as WrapBox } from '../elements/adw-wrap-box.js';
|
|
112
|
+
export { AdwApplication as Application } from '../application.js';
|
package/src/namespace/gio.ts
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// The GLib half of the vocabulary: the main-loop sources a GNOME app schedules work with —
|
|
2
|
+
// `idle_add`, `timeout_add`, `source_remove` and the `PRIORITY_*` / `SOURCE_*` constants. The
|
|
3
|
+
// behaviour lives once in `@gjsify/adwaita-core` (`glib-timers.ts`, which states where a host
|
|
4
|
+
// event loop cannot honour GLib); this file is the namespace door over the host's `setTimeout`.
|
|
5
|
+
//
|
|
6
|
+
// A TRUE SUBSET: everything else in GLib (`MainLoop`, `Source`, `timeout_add_seconds`, `Bytes`,
|
|
7
|
+
// `build_filenamev`, …) is absent, and the `gi://GLib` arm refuses it by name.
|
|
8
|
+
|
|
9
|
+
import { createGLibTimers } from '@gjsify/adwaita-core';
|
|
10
|
+
|
|
11
|
+
const glib = createGLibTimers();
|
|
12
|
+
|
|
13
|
+
export const PRIORITY_HIGH = glib.PRIORITY_HIGH;
|
|
14
|
+
export const PRIORITY_DEFAULT = glib.PRIORITY_DEFAULT;
|
|
15
|
+
export const PRIORITY_HIGH_IDLE = glib.PRIORITY_HIGH_IDLE;
|
|
16
|
+
export const PRIORITY_DEFAULT_IDLE = glib.PRIORITY_DEFAULT_IDLE;
|
|
17
|
+
export const PRIORITY_LOW = glib.PRIORITY_LOW;
|
|
18
|
+
export const SOURCE_REMOVE = glib.SOURCE_REMOVE;
|
|
19
|
+
export const SOURCE_CONTINUE = glib.SOURCE_CONTINUE;
|
|
20
|
+
export const idle_add = glib.idle_add;
|
|
21
|
+
export const timeout_add = glib.timeout_add;
|
|
22
|
+
export const source_remove = glib.source_remove;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// The GObject half of `@gjsify/adwaita-web`'s vocabulary (ADR 0096) — `GObject.registerClass`,
|
|
2
|
+
// `GObject.ParamSpec` and the rest of the subset, the way `./adw.ts` and `./gtk.ts` export their
|
|
3
|
+
// members. `export * as GObject from './namespace/gobject.js'` in `src/index.ts` makes it the
|
|
4
|
+
// namespace. Everything is the core's (`@gjsify/adwaita-core`); this port supplies only the door
|
|
5
|
+
// to custom elements, in `../gobject-door.ts`.
|
|
6
|
+
//
|
|
7
|
+
// `Object` is re-exported under its GIR name, so it is bound to a local first: a module-level
|
|
8
|
+
// `export const Object` would shadow the global one for this file.
|
|
9
|
+
|
|
10
|
+
import { createGObject } from '@gjsify/adwaita-core';
|
|
11
|
+
|
|
12
|
+
import { webDoor } from '../gobject-door.js';
|
|
13
|
+
|
|
14
|
+
const gobject = createGObject(webDoor);
|
|
15
|
+
|
|
16
|
+
const GObjectObject = gobject.Object;
|
|
17
|
+
|
|
18
|
+
export { GObjectObject as Object };
|
|
19
|
+
export const registerClass = gobject.registerClass;
|
|
20
|
+
export const type_ensure = gobject.type_ensure;
|
|
21
|
+
export const ParamSpec = gobject.ParamSpec;
|
|
22
|
+
export const ParamFlags = gobject.ParamFlags;
|
|
23
|
+
export const TYPE_STRING = gobject.TYPE_STRING;
|
|
24
|
+
export const TYPE_BOOLEAN = gobject.TYPE_BOOLEAN;
|
|
25
|
+
export const TYPE_INT = gobject.TYPE_INT;
|
|
26
|
+
export const TYPE_UINT = gobject.TYPE_UINT;
|
|
27
|
+
export const TYPE_DOUBLE = gobject.TYPE_DOUBLE;
|
|
28
|
+
export const GTypeName = gobject.GTypeName;
|
|
29
|
+
export const GTypeFlags = gobject.GTypeFlags;
|
|
30
|
+
export const interfaces = gobject.interfaces;
|
|
31
|
+
export const properties = gobject.properties;
|
|
32
|
+
export const signals = gobject.signals;
|
|
33
|
+
export const requires = gobject.requires;
|
|
34
|
+
export const __gtkTemplate__ = gobject.__gtkTemplate__;
|
|
35
|
+
export const __gtkCssName__ = gobject.__gtkCssName__;
|
|
36
|
+
export const __gtkChildren__ = gobject.__gtkChildren__;
|
|
37
|
+
export const __gtkInternalChildren__ = gobject.__gtkInternalChildren__;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// The plain GTK enums an application reads off `Gtk` — `Gtk.PolicyType`, `Gtk.TextDirection` and
|
|
2
|
+
// `Gtk.TextWindowType`, as value tables. The numbers are the typelib's (GIR declaration order),
|
|
3
|
+
// and the same tables the NativeScript port publishes.
|
|
4
|
+
//
|
|
5
|
+
// Reference: refs/gtk gtk/gtkenums.h (GtkPolicyType, GtkTextDirection), gtk/gtktextview.h (GtkTextWindowType)
|
|
6
|
+
// Copyright (c) The GTK Team. LGPLv2.1+.
|
|
7
|
+
|
|
8
|
+
/** `Gtk.PolicyType`: `ALWAYS` 0, `AUTOMATIC` 1, `NEVER` 2, `EXTERNAL` 3. */
|
|
9
|
+
export const PolicyType = Object.freeze({ ALWAYS: 0, AUTOMATIC: 1, NEVER: 2, EXTERNAL: 3 } as const);
|
|
10
|
+
|
|
11
|
+
/** `Gtk.TextDirection`: `NONE` 0, `LTR` 1, `RTL` 2. */
|
|
12
|
+
export const TextDirection = Object.freeze({ NONE: 0, LTR: 1, RTL: 2 } as const);
|
|
13
|
+
|
|
14
|
+
/** `Gtk.TextWindowType`: `WIDGET` 1, `TEXT` 2, `LEFT` 3, `RIGHT` 4, `TOP` 5, `BOTTOM` 6. */
|
|
15
|
+
export const TextWindowType = Object.freeze({ WIDGET: 1, TEXT: 2, LEFT: 3, RIGHT: 4, TOP: 5, BOTTOM: 6 } as const);
|
package/src/namespace/gtk.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
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, TextWindowType } from './gtk-enums.js';
|
|
24
25
|
export { GtkAboutDialog as AboutDialog } from '../elements/gtk-about-dialog.js';
|
|
25
26
|
export { GtkActionBar as ActionBar } from '../elements/gtk-action-bar.js';
|
|
26
27
|
export { GtkApplicationWindow as ApplicationWindow } from '../elements/gtk-application-window.js';
|
|
@@ -95,3 +96,4 @@ export { GtkWindow as Window } from '../elements/gtk-window.js';
|
|
|
95
96
|
export { GtkWindowControls as WindowControls } from '../elements/gtk-window-controls.js';
|
|
96
97
|
export { GtkViewport as Viewport } from '../elements/gtk-viewport.js';
|
|
97
98
|
export { GtkWindowHandle as WindowHandle } from '../elements/gtk-window-handle.js';
|
|
99
|
+
export { GtkApplication as Application } from '../application.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The GtkSource vocabulary of `@gjsify/adwaita-web` (ADR 0094 amendment): `GtkSource.View`,
|
|
2
|
+
// `Buffer`, `LanguageManager`, `StyleSchemeManager`, over `@gjsify/gtksource-core`, the same names
|
|
3
|
+
// the NativeScript port publishes. `export * as GtkSource from './namespace/gtksource.js'` in
|
|
4
|
+
// `src/index.ts` makes it the namespace the `gi://GtkSource` arm answers from.
|
|
5
|
+
//
|
|
6
|
+
// A TRUE SUBSET: GtkSource's gutters, completion, search and the rest are absent, and the arm
|
|
7
|
+
// refuses each by name.
|
|
8
|
+
export { Buffer, Language, LanguageManager, StyleScheme, StyleSchemeManager, init } from '@gjsify/gtksource-core';
|
|
9
|
+
export { GtkSourceView as View } from '../gtksource/gtk-source-view.js';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Row line-clamping utilities — shared by adw-action-row, adw-switch-row,
|
|
2
|
+
// adw-combo-row, adw-spin-row, adw-expander-row.
|
|
3
|
+
//
|
|
4
|
+
// The clamping uses the package's established pattern: CSS custom properties
|
|
5
|
+
// (--adw-row-title-lines, --adw-row-subtitle-lines) + a class toggle
|
|
6
|
+
// (.adw-row-clamp on the text container). See gtk-label.ts for the same
|
|
7
|
+
// pattern with --gtk-label-lines and .adw-label-clamp.
|
|
8
|
+
//
|
|
9
|
+
// Upstream semantics (AdwActionRow, AdwExpanderRow):
|
|
10
|
+
// - title-lines / subtitle-lines default to 0
|
|
11
|
+
// - 0 = unlimited lines (wrap freely, no ellipsis)
|
|
12
|
+
// - n > 0 = clamp to n lines with ellipsis
|
|
13
|
+
// - Negative values are invalid per the pspec (range 0..G_MAXINT) and
|
|
14
|
+
// treated as 0 (unlimited) — the property system clamps them.
|
|
15
|
+
|
|
16
|
+
export interface RowLineClampConfig {
|
|
17
|
+
titleEl: HTMLElement;
|
|
18
|
+
subtitleEl: HTMLElement;
|
|
19
|
+
textContainerEl: HTMLElement; // The element that gets .adw-row-clamp
|
|
20
|
+
getTitleLines: () => number;
|
|
21
|
+
getSubtitleLines: () => number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Apply or remove line clamping based on title-lines/subtitle-lines values.
|
|
26
|
+
* Uses CSS custom properties and class toggle (not inline styles).
|
|
27
|
+
*/
|
|
28
|
+
export function applyRowLineClamp(config: RowLineClampConfig): void {
|
|
29
|
+
const { titleEl, subtitleEl, textContainerEl, getTitleLines, getSubtitleLines } = config;
|
|
30
|
+
|
|
31
|
+
const titleLines = getTitleLines();
|
|
32
|
+
const subtitleLines = getSubtitleLines();
|
|
33
|
+
|
|
34
|
+
const titleClamped = titleLines > 0;
|
|
35
|
+
const subtitleClamped = subtitleLines > 0;
|
|
36
|
+
const anyClamped = titleClamped || subtitleClamped;
|
|
37
|
+
|
|
38
|
+
// Toggle the clamping class on the text container
|
|
39
|
+
textContainerEl.classList.toggle('adw-row-clamp', anyClamped);
|
|
40
|
+
|
|
41
|
+
// Set custom properties for the clamp line counts
|
|
42
|
+
if (titleClamped) {
|
|
43
|
+
titleEl.style.setProperty('--adw-row-title-lines', String(titleLines));
|
|
44
|
+
} else {
|
|
45
|
+
titleEl.style.removeProperty('--adw-row-title-lines');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (subtitleClamped) {
|
|
49
|
+
subtitleEl.style.setProperty('--adw-row-subtitle-lines', String(subtitleLines));
|
|
50
|
+
} else {
|
|
51
|
+
subtitleEl.style.removeProperty('--adw-row-subtitle-lines');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The ellipsize overflow is shared (end ellipsis for all non-none modes)
|
|
55
|
+
// Set it on the text container so the clamp rule can read it
|
|
56
|
+
if (anyClamped) {
|
|
57
|
+
textContainerEl.style.setProperty('--adw-row-ellipsize-overflow', 'ellipsis');
|
|
58
|
+
} else {
|
|
59
|
+
textContainerEl.style.removeProperty('--adw-row-ellipsize-overflow');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parse a lines attribute value following AdwActionRow semantics:
|
|
65
|
+
* - Missing/null/empty → 0 (unlimited)
|
|
66
|
+
* - Invalid/non-numeric → 0 (unlimited)
|
|
67
|
+
* - n > 0 → n (clamp to n lines)
|
|
68
|
+
* - n <= 0 → 0 (unlimited; pspec range is 0..G_MAXINT)
|
|
69
|
+
*/
|
|
70
|
+
export function parseRowLinesAttribute(value: string | null): number {
|
|
71
|
+
if (value === null || value === '') return 0;
|
|
72
|
+
const parsed = Number.parseInt(value, 10);
|
|
73
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
74
|
+
}
|