@gjsify/adwaita-web 0.57.0 → 0.59.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/lib/types/clipboard-host.d.ts +7 -0
- package/lib/types/gdk.spec.d.ts +1 -0
- package/lib/types/gio.spec.d.ts +1 -0
- package/lib/types/gtksource/gtk-source-view.d.ts +11 -4
- package/lib/types/gtksource/web-editor-driver.d.ts +18 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/namespace/adw.d.ts +1 -0
- package/lib/types/namespace/gdk.d.ts +7 -0
- package/lib/types/namespace/gio.d.ts +1 -1
- package/lib/types/namespace/glib.d.ts +5 -0
- package/lib/types/namespace/gobject.d.ts +2 -0
- package/lib/types/namespace/gtk-enums.d.ts +5 -0
- package/lib/types/namespace/gtk.d.ts +1 -1
- package/lib/types/namespace/gtksource.d.ts +1 -1
- package/lib/types/style-manager.d.ts +5 -0
- package/lib/types/style-manager.spec.d.ts +1 -0
- package/package.json +9 -9
- package/src/application.spec.ts +1 -1
- package/src/clipboard-host.ts +47 -0
- package/src/gdk.spec.ts +122 -0
- package/src/gio.spec.ts +23 -0
- package/src/glib.spec.ts +17 -2
- package/src/gobject-door.ts +7 -0
- package/src/gtksource/gtk-source-view.spec.ts +154 -5
- package/src/gtksource/gtk-source-view.ts +35 -12
- package/src/gtksource/web-editor-driver.ts +144 -15
- package/src/index.ts +2 -0
- package/src/namespace/adw.ts +1 -0
- package/src/namespace/gdk.ts +17 -0
- package/src/namespace/gio.ts +2 -2
- package/src/namespace/glib.ts +17 -2
- package/src/namespace/gobject.ts +2 -0
- package/src/namespace/gtk-enums.ts +24 -5
- package/src/namespace/gtk.ts +1 -1
- package/src/namespace/gtksource.ts +14 -3
- package/src/style-manager.spec.ts +36 -0
- package/src/style-manager.ts +27 -0
- package/src/test.browser.mts +6 -0
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { describe, expect, it } from '@gjsify/unit';
|
|
2
2
|
import {
|
|
3
|
+
GUTTER_PAINT_VECTORS,
|
|
3
4
|
GTKSOURCE_BUFFER_VECTORS,
|
|
5
|
+
GTKSOURCE_STOP_VECTORS,
|
|
4
6
|
GTKSOURCE_VIEW_DEFAULT_VECTORS,
|
|
5
7
|
GTKSOURCE_VIEW_SURFACE_VECTORS,
|
|
6
8
|
type GtkSourceViewSurfaceLike,
|
|
7
9
|
type GtkSourceBufferLike,
|
|
10
|
+
type GtkSourceStopGestures,
|
|
11
|
+
type GtkSourceStopSurface,
|
|
12
|
+
type GutterPaintSurface,
|
|
13
|
+
type ViewLike,
|
|
8
14
|
} from '@gjsify/gtksource-core/conformance';
|
|
9
15
|
|
|
16
|
+
import * as GObject from '../namespace/gobject.js';
|
|
10
17
|
import * as Gtk from '../namespace/gtk.js';
|
|
11
18
|
import * as GtkSource from '../namespace/gtksource.js';
|
|
12
19
|
|
|
@@ -30,14 +37,156 @@ export const GtkSourceViewTest = async () => {
|
|
|
30
37
|
|
|
31
38
|
for (const vector of GTKSOURCE_VIEW_SURFACE_VECTORS) {
|
|
32
39
|
await it(`surface: ${vector.rule}`, () => {
|
|
33
|
-
expect(
|
|
34
|
-
vector.
|
|
35
|
-
|
|
40
|
+
expect(
|
|
41
|
+
vector.observe({
|
|
42
|
+
Gtk,
|
|
43
|
+
GtkSource,
|
|
44
|
+
} as unknown as GtkSourceViewSurfaceLike),
|
|
45
|
+
).toStrictEqual(vector.shows);
|
|
36
46
|
});
|
|
37
47
|
}
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
// The driver listens to the textarea only while the view is in a document.
|
|
50
|
+
const mounted = (view: unknown): GtkSourceView => {
|
|
51
|
+
const element = view as GtkSourceView;
|
|
52
|
+
if (!element.isConnected) document.body.append(element);
|
|
53
|
+
return element;
|
|
54
|
+
};
|
|
55
|
+
const gestures: GtkSourceStopGestures = {
|
|
56
|
+
extendSelection: (view, offset) => {
|
|
57
|
+
const area = mounted(view).textarea;
|
|
58
|
+
area.setSelectionRange(offset, offset);
|
|
59
|
+
const down = new MouseEvent('mousedown', {
|
|
60
|
+
detail: 2,
|
|
61
|
+
button: 0,
|
|
62
|
+
cancelable: true,
|
|
63
|
+
bubbles: true,
|
|
64
|
+
});
|
|
65
|
+
area.dispatchEvent(down);
|
|
66
|
+
return down.defaultPrevented ? [area.selectionStart, area.selectionEnd] : 'native';
|
|
67
|
+
},
|
|
68
|
+
copyClipboard: (view) => {
|
|
69
|
+
const copy = new Event('copy', { cancelable: true, bubbles: true });
|
|
70
|
+
mounted(view).textarea.dispatchEvent(copy);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
for (const vector of GTKSOURCE_STOP_VECTORS) {
|
|
74
|
+
await it(`stopping an emission: ${vector.rule}`, () => {
|
|
75
|
+
const seen = vector.observe({ GtkSource, GObject } as unknown as GtkSourceStopSurface, gestures);
|
|
76
|
+
for (const view of document.body.querySelectorAll('gtk-source-view')) view.remove();
|
|
77
|
+
expect(seen).toStrictEqual(vector.shows);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const task = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
82
|
+
for (const vector of GUTTER_PAINT_VECTORS) {
|
|
83
|
+
await it(`gutter paint: ${vector.rule}`, async () => {
|
|
84
|
+
const views: HTMLElement[] = [];
|
|
85
|
+
const harness = {
|
|
86
|
+
async present(view: ViewLike) {
|
|
87
|
+
document.body.append(view as unknown as HTMLElement);
|
|
88
|
+
views.push(view as unknown as HTMLElement);
|
|
89
|
+
await task();
|
|
90
|
+
},
|
|
91
|
+
settle: task,
|
|
92
|
+
};
|
|
93
|
+
try {
|
|
94
|
+
expect(
|
|
95
|
+
await vector.observe({ Gtk, GtkSource, GObject } as unknown as GutterPaintSurface, harness),
|
|
96
|
+
).toStrictEqual(vector.shows);
|
|
97
|
+
} finally {
|
|
98
|
+
for (const view of views) view.remove();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await it("paints a renderer's text, in a column left of the line numbers when its position is negative", async () => {
|
|
104
|
+
const view = document.createElement('gtk-source-view') as GtkSourceView;
|
|
105
|
+
view.showLineNumbers = true;
|
|
106
|
+
document.body.append(view);
|
|
107
|
+
view.buffer.text = 'a\nb';
|
|
108
|
+
const renderer = new GtkSource.GutterRendererText({
|
|
109
|
+
width_request: 24,
|
|
110
|
+
margin_start: 2,
|
|
111
|
+
margin_end: 3,
|
|
112
|
+
});
|
|
113
|
+
(
|
|
114
|
+
renderer as unknown as {
|
|
115
|
+
vfunc_query_data(lines: unknown, line: number): void;
|
|
116
|
+
}
|
|
117
|
+
).vfunc_query_data = function (this: { text: string }, _lines, line) {
|
|
118
|
+
this.text = `r${line}`;
|
|
119
|
+
};
|
|
120
|
+
view.get_gutter(Gtk.TextWindowType.LEFT)!.insert(renderer, -1);
|
|
121
|
+
await task();
|
|
122
|
+
const columns = view.querySelectorAll<HTMLElement>('.gsv-columns > *');
|
|
123
|
+
expect([...columns].map((column) => column.className)).toStrictEqual(['gsv-column', 'gsv-numbers']);
|
|
124
|
+
const first = columns[0];
|
|
125
|
+
expect([...first.querySelectorAll('.gsv-cell')].map((cell) => cell.textContent)).toStrictEqual([
|
|
126
|
+
'r0',
|
|
127
|
+
'r1',
|
|
128
|
+
]);
|
|
129
|
+
expect([first.style.minWidth, first.style.paddingLeft, first.style.paddingRight]).toStrictEqual([
|
|
130
|
+
'24px',
|
|
131
|
+
'2px',
|
|
132
|
+
'3px',
|
|
133
|
+
]);
|
|
134
|
+
expect(view.classList.contains('has-columns')).toBe(true);
|
|
135
|
+
view.remove();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await it('paints the RIGHT gutter as a column right of the text, apart from the left one', async () => {
|
|
139
|
+
const view = document.createElement('gtk-source-view') as GtkSourceView;
|
|
140
|
+
document.body.append(view);
|
|
141
|
+
view.buffer.text = 'a\nb';
|
|
142
|
+
const renderer = new GtkSource.GutterRendererText({ width_request: 12 });
|
|
143
|
+
(
|
|
144
|
+
renderer as unknown as {
|
|
145
|
+
vfunc_query_data(lines: unknown, line: number): void;
|
|
146
|
+
}
|
|
147
|
+
).vfunc_query_data = function (this: { text: string }, _lines, line) {
|
|
148
|
+
this.text = `R${line}`;
|
|
149
|
+
};
|
|
150
|
+
const right = view.get_gutter(Gtk.TextWindowType.RIGHT)!;
|
|
151
|
+
right.insert(renderer, 0);
|
|
152
|
+
await task();
|
|
153
|
+
const element = view.querySelector<HTMLElement>('.gsv-gutter-right')!;
|
|
154
|
+
expect([...element.querySelectorAll('.gsv-cell')].map((cell) => cell.textContent)).toStrictEqual([
|
|
155
|
+
'R0',
|
|
156
|
+
'R1',
|
|
157
|
+
]);
|
|
158
|
+
expect(view.classList.contains('has-right-columns')).toBe(true);
|
|
159
|
+
expect(view.classList.contains('has-columns')).toBe(false);
|
|
160
|
+
expect(view.lastElementChild).toBe(element);
|
|
161
|
+
right.remove(renderer);
|
|
162
|
+
await task();
|
|
163
|
+
expect(view.classList.contains('has-right-columns')).toBe(false);
|
|
164
|
+
view.remove();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await it('paints markup as styled spans and drops the column with its renderer', async () => {
|
|
168
|
+
const view = document.createElement('gtk-source-view') as GtkSourceView;
|
|
169
|
+
document.body.append(view);
|
|
170
|
+
view.buffer.text = 'a';
|
|
171
|
+
const renderer = new GtkSource.GutterRendererText({ width_request: 10 });
|
|
172
|
+
(
|
|
173
|
+
renderer as unknown as {
|
|
174
|
+
vfunc_query_data(lines: unknown, line: number): void;
|
|
175
|
+
}
|
|
176
|
+
).vfunc_query_data = function (this: { markup: string }) {
|
|
177
|
+
this.markup = '<b>x</b> & y';
|
|
178
|
+
};
|
|
179
|
+
const gutter = view.get_gutter(Gtk.TextWindowType.LEFT)!;
|
|
180
|
+
gutter.insert(renderer, 0);
|
|
181
|
+
await task();
|
|
182
|
+
const cell = view.querySelector<HTMLElement>('.gsv-cell')!;
|
|
183
|
+
expect(cell.textContent).toBe('x & y');
|
|
184
|
+
expect(cell.querySelector<HTMLElement>('span')!.style.fontWeight).toBe('bold');
|
|
185
|
+
gutter.remove(renderer);
|
|
186
|
+
await task();
|
|
187
|
+
expect(view.querySelector('.gsv-column')).toBe(null);
|
|
188
|
+
expect(view.classList.contains('has-columns')).toBe(false);
|
|
189
|
+
view.remove();
|
|
41
190
|
});
|
|
42
191
|
|
|
43
192
|
await it('writes through attributes with GTK semantics', () => {
|
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
//
|
|
10
10
|
// IN THIS SLICE: `buffer`, `auto-indent`, `indent-width` (held and read back, as on Android),
|
|
11
11
|
// `show-line-numbers`, `highlight-current-line`, `monospace`, `editable`, `cursor-visible`, the four
|
|
12
|
-
// margins, `connect`/`disconnect`, the GJS snake_case accessors, `vadjustment`/`hadjustment`,
|
|
13
|
-
// `set_direction`, `get_first_child`/`get_next_sibling
|
|
12
|
+
// margins, `connect`/`connect_after`/`disconnect`, `extend-selection` and `copy-clipboard` (stoppable), the GJS snake_case accessors, `vadjustment`/`hadjustment`,
|
|
13
|
+
// `set_direction`, `get_first_child`/`get_next_sibling`, `get_gutter` (both gutters paint their renderers' `vfunc_query_data`).
|
|
14
14
|
//
|
|
15
15
|
// Reference: GtkSourceView 5 gtksourceview.c, upstream GNOME/gtksourceview (properties and defaults)
|
|
16
16
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
17
17
|
|
|
18
18
|
import { adwaitaColorScheme, GtkAdjustment, onAdwaitaColorSchemeChanged } from '@gjsify/adwaita-core';
|
|
19
|
-
import type { Buffer } from '@gjsify/gtksource-core';
|
|
20
|
-
import { EditorSession, toBoolean, toNumber } from '@gjsify/gtksource-core';
|
|
19
|
+
import type { Buffer, Gutter } from '@gjsify/gtksource-core';
|
|
20
|
+
import { EditorSession, GutterSet, STOP_EMISSION, toBoolean, toNumber } from '@gjsify/gtksource-core';
|
|
21
21
|
|
|
22
22
|
import { WebEditorDriver } from './web-editor-driver.js';
|
|
23
23
|
|
|
@@ -44,15 +44,22 @@ gtk-source-view .gsv-gutter {
|
|
|
44
44
|
flex: none;
|
|
45
45
|
overflow: hidden;
|
|
46
46
|
box-sizing: content-box;
|
|
47
|
-
padding: var(--gsv-margin-top, 0px)
|
|
48
|
-
width: var(--gsv-gutter-width, 2ch);
|
|
47
|
+
padding: var(--gsv-margin-top, 0px) 0 0;
|
|
49
48
|
text-align: end;
|
|
50
49
|
line-height: var(--gsv-line-height);
|
|
51
50
|
color: var(--gsv-number-fg);
|
|
52
51
|
background: var(--gsv-number-bg);
|
|
53
52
|
user-select: none;
|
|
54
53
|
}
|
|
55
|
-
gtk-source-view.show-line-numbers .gsv-gutter
|
|
54
|
+
gtk-source-view.show-line-numbers .gsv-gutter,
|
|
55
|
+
gtk-source-view.has-columns .gsv-gutter { display: block; }
|
|
56
|
+
gtk-source-view .gsv-gutter-right { display: none; }
|
|
57
|
+
gtk-source-view.has-right-columns .gsv-gutter-right { display: block; }
|
|
58
|
+
gtk-source-view .gsv-columns { display: flex; }
|
|
59
|
+
gtk-source-view .gsv-numbers,
|
|
60
|
+
gtk-source-view .gsv-column { flex: none; box-sizing: content-box; }
|
|
61
|
+
gtk-source-view .gsv-numbers { width: var(--gsv-gutter-width, 2ch); padding: 0 var(--gsv-gutter-padding, 8px); }
|
|
62
|
+
gtk-source-view .gsv-cell { height: calc(1em * var(--gsv-line-height)); white-space: pre; text-align: start; }
|
|
56
63
|
gtk-source-view .gsv-number.current { color: var(--gsv-current-number-fg); background: var(--gsv-current-number-bg); }
|
|
57
64
|
gtk-source-view .gsv-stage { position: relative; flex: 1 1 auto; min-width: 0; }
|
|
58
65
|
gtk-source-view .gsv-backdrop,
|
|
@@ -133,15 +140,18 @@ export class GtkSourceView extends HTMLElement {
|
|
|
133
140
|
private unsubscribe: (() => void) | null = null;
|
|
134
141
|
private built = false;
|
|
135
142
|
private readonly gutter = document.createElement('div');
|
|
143
|
+
private readonly rightGutter = document.createElement('div');
|
|
136
144
|
private readonly backdrop = document.createElement('div');
|
|
137
145
|
private readonly area = document.createElement('textarea');
|
|
138
146
|
private readonly vadj = new GtkAdjustment();
|
|
139
147
|
private readonly hadj = new GtkAdjustment();
|
|
140
148
|
private direction = 1;
|
|
149
|
+
private readonly gutters = new GutterSet(this);
|
|
141
150
|
|
|
142
151
|
constructor() {
|
|
143
152
|
super();
|
|
144
153
|
this.gutter.className = 'gsv-gutter';
|
|
154
|
+
this.rightGutter.className = 'gsv-gutter gsv-gutter-right';
|
|
145
155
|
this.backdrop.className = 'gsv-backdrop';
|
|
146
156
|
this.area.className = 'gsv-area';
|
|
147
157
|
this.area.wrap = 'off';
|
|
@@ -153,8 +163,10 @@ export class GtkSourceView extends HTMLElement {
|
|
|
153
163
|
area: this.area,
|
|
154
164
|
backdrop: this.backdrop,
|
|
155
165
|
gutter: this.gutter,
|
|
166
|
+
rightGutter: this.rightGutter,
|
|
156
167
|
});
|
|
157
168
|
this.session = new EditorSession(this.driver, undefined, adwaitaColorScheme());
|
|
169
|
+
this.session.bindGutters(this.gutters);
|
|
158
170
|
}
|
|
159
171
|
|
|
160
172
|
private syncAdjustments(): void {
|
|
@@ -173,7 +185,7 @@ export class GtkSourceView extends HTMLElement {
|
|
|
173
185
|
const stage = document.createElement('div');
|
|
174
186
|
stage.className = 'gsv-stage';
|
|
175
187
|
stage.append(this.backdrop, this.area);
|
|
176
|
-
this.append(this.gutter, stage);
|
|
188
|
+
this.append(this.gutter, stage, this.rightGutter);
|
|
177
189
|
}
|
|
178
190
|
this.driver.attach();
|
|
179
191
|
this.unsubscribe = onAdwaitaColorSchemeChanged(() => this.session.setColorScheme(adwaitaColorScheme()));
|
|
@@ -328,9 +340,9 @@ export class GtkSourceView extends HTMLElement {
|
|
|
328
340
|
return this.parentElement;
|
|
329
341
|
}
|
|
330
342
|
|
|
331
|
-
/**
|
|
332
|
-
get_gutter(
|
|
333
|
-
|
|
343
|
+
/** `null` for any side but LEFT and RIGHT, as in GtkSourceView (ADR 0103). */
|
|
344
|
+
get_gutter(window_type: number): Gutter | null {
|
|
345
|
+
return this.gutters.get(window_type);
|
|
334
346
|
}
|
|
335
347
|
|
|
336
348
|
get leftMargin(): number {
|
|
@@ -362,10 +374,21 @@ export class GtkSourceView extends HTMLElement {
|
|
|
362
374
|
}
|
|
363
375
|
|
|
364
376
|
/** GJS `connect(name, cb) → id`; the handler's first argument is this view. */
|
|
365
|
-
connect(name: string, callback: (self: GtkSourceView, ...args: never[]) =>
|
|
377
|
+
connect(name: string, callback: (self: GtkSourceView, ...args: never[]) => unknown): number {
|
|
366
378
|
return this.session.connect(name, ((_session: unknown, ...args: never[]) => callback(this, ...args)) as never);
|
|
367
379
|
}
|
|
368
380
|
|
|
381
|
+
/** `g_signal_connect_after`: runs once the plain handlers and the default of the emission have run. */
|
|
382
|
+
connect_after(name: string, callback: (self: GtkSourceView, ...args: never[]) => unknown): number {
|
|
383
|
+
return this.session.connect_after(name, ((_session: unknown, ...args: never[]) =>
|
|
384
|
+
callback(this, ...args)) as never);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** `GObject.signal_stop_emission_by_name(view, …)`: the session owns the emissions. */
|
|
388
|
+
[STOP_EMISSION](detailedSignal: string): void {
|
|
389
|
+
this.session[STOP_EMISSION](detailedSignal);
|
|
390
|
+
}
|
|
391
|
+
|
|
369
392
|
disconnect(id: number): void {
|
|
370
393
|
this.session.disconnect(id);
|
|
371
394
|
}
|
|
@@ -7,8 +7,18 @@
|
|
|
7
7
|
// NO WORD WRAP, as on Android: `wrap="off"` and `white-space: pre` keep every logical line on
|
|
8
8
|
// one layout line, so the backdrop and the gutter stay aligned by a fixed line height.
|
|
9
9
|
|
|
10
|
-
import type {
|
|
11
|
-
|
|
10
|
+
import type {
|
|
11
|
+
EditorDriver,
|
|
12
|
+
EditorHost,
|
|
13
|
+
EditorLayout,
|
|
14
|
+
EditorPalette,
|
|
15
|
+
GutterCell,
|
|
16
|
+
GutterColumn,
|
|
17
|
+
StyledRun,
|
|
18
|
+
} from '@gjsify/gtksource-core';
|
|
19
|
+
import { digitCount, LineStore, WINDOW_LEFT, WINDOW_RIGHT } from '@gjsify/gtksource-core';
|
|
20
|
+
|
|
21
|
+
import { interceptCopy } from '../clipboard-host.js';
|
|
12
22
|
|
|
13
23
|
const GUTTER_PADDING_PX = 8;
|
|
14
24
|
|
|
@@ -23,16 +33,20 @@ export interface EditorParts {
|
|
|
23
33
|
readonly area: HTMLTextAreaElement;
|
|
24
34
|
readonly backdrop: HTMLElement;
|
|
25
35
|
readonly gutter: HTMLElement;
|
|
36
|
+
/** The RIGHT gutter's element, right of the text. */
|
|
37
|
+
readonly rightGutter: HTMLElement;
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
export class WebEditorDriver implements EditorDriver {
|
|
41
|
+
readonly gutterSides = [WINDOW_LEFT, WINDOW_RIGHT];
|
|
29
42
|
private host: EditorHost | null = null;
|
|
30
43
|
private text = '';
|
|
31
44
|
private caret = 0;
|
|
32
45
|
private reported = { start: 0, end: 0 };
|
|
33
46
|
private layout: EditorLayout | null = null;
|
|
34
47
|
private readonly runs = new LineStore<readonly StyledRun[]>();
|
|
35
|
-
private scheduled =
|
|
48
|
+
private scheduled: 'none' | 'gutter' | 'all' = 'none';
|
|
49
|
+
private numbers: HTMLElement | null = null;
|
|
36
50
|
private listening: Array<() => void> = [];
|
|
37
51
|
private palette: EditorPalette | null = null;
|
|
38
52
|
private attached = false;
|
|
@@ -56,15 +70,30 @@ export class WebEditorDriver implements EditorDriver {
|
|
|
56
70
|
on(area, 'scroll', () => {
|
|
57
71
|
backdrop.scrollTop = area.scrollTop;
|
|
58
72
|
backdrop.scrollLeft = area.scrollLeft;
|
|
59
|
-
(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
for (const side of [gutter, this.parts.rightGutter]) {
|
|
74
|
+
(side.firstElementChild as HTMLElement | null)?.style.setProperty(
|
|
75
|
+
'transform',
|
|
76
|
+
`translateY(${-area.scrollTop}px)`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
// Only the lines in view are asked; a scroll brings new ones.
|
|
80
|
+
if (this.hasColumns || this.hasRightColumns) this.scheduleRender('gutter');
|
|
63
81
|
});
|
|
64
82
|
on(area, 'select', () => this.onSelection());
|
|
65
83
|
on(area, 'keyup', () => this.onSelection());
|
|
66
84
|
on(area, 'mouseup', () => this.onSelection());
|
|
67
85
|
on(area, 'keydown', (event) => this.onKey(event as KeyboardEvent));
|
|
86
|
+
on(area, 'mousedown', (event) => this.onMouseDown(event as MouseEvent));
|
|
87
|
+
on(area, 'copy', (event) => {
|
|
88
|
+
// A handler of `copy-clipboard` stopped the emission: the default, the copy, does not happen.
|
|
89
|
+
// One that put content on the Gdk clipboard meanwhile wrote it into this event
|
|
90
|
+
// (`clipboard-host.ts`); the browser must not replace it with the selection.
|
|
91
|
+
let ran = false;
|
|
92
|
+
const wrote = interceptCopy((event as ClipboardEvent).clipboardData, () => {
|
|
93
|
+
ran = this.host?.onNativeCopy() ?? false;
|
|
94
|
+
});
|
|
95
|
+
if (!ran || wrote) event.preventDefault();
|
|
96
|
+
});
|
|
68
97
|
// Firefox and Chromium fire `selectionchange` on different targets for a textarea.
|
|
69
98
|
on(area.ownerDocument, 'selectionchange', () => {
|
|
70
99
|
if (area.ownerDocument.activeElement === area) this.onSelection();
|
|
@@ -111,6 +140,20 @@ export class WebEditorDriver implements EditorDriver {
|
|
|
111
140
|
this.scheduleRender();
|
|
112
141
|
}
|
|
113
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The second and third press of a multi-click: GTK asks `extend-selection` before it selects the word
|
|
145
|
+
* or line. The first press has already placed the caret, which is the location asked about.
|
|
146
|
+
*/
|
|
147
|
+
private onMouseDown(event: MouseEvent): void {
|
|
148
|
+
if (event.button !== 0 || (event.detail !== 2 && event.detail !== 3)) return;
|
|
149
|
+
const { area } = this.parts;
|
|
150
|
+
const range = this.host?.onNativeExtendSelection(event.detail === 2 ? 0 : 1, area.selectionStart ?? 0);
|
|
151
|
+
if (!range) return;
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
area.setSelectionRange(range[0], range[1]);
|
|
154
|
+
this.onSelection();
|
|
155
|
+
}
|
|
156
|
+
|
|
114
157
|
/** `accepts-tab` is GTK's default; a textarea would move focus, so Tab inserts a tab. */
|
|
115
158
|
private onKey(event: KeyboardEvent): void {
|
|
116
159
|
if (event.key !== 'Tab' || event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) return;
|
|
@@ -188,6 +231,10 @@ export class WebEditorDriver implements EditorDriver {
|
|
|
188
231
|
style.setProperty('--gsv-current-number-bg', cssColor(palette.currentLineNumberBackground));
|
|
189
232
|
}
|
|
190
233
|
|
|
234
|
+
invalidateGutter(): void {
|
|
235
|
+
this.scheduleRender('gutter');
|
|
236
|
+
}
|
|
237
|
+
|
|
191
238
|
spliceLines(first: number, removed: number, inserted: number): void {
|
|
192
239
|
this.runs.splice(first, removed, inserted);
|
|
193
240
|
}
|
|
@@ -205,17 +252,24 @@ export class WebEditorDriver implements EditorDriver {
|
|
|
205
252
|
// --- painting ----------------------------------------------------------------------------
|
|
206
253
|
|
|
207
254
|
/** One repaint per task: a burst of `paintLine` calls from one edit costs one DOM rebuild. */
|
|
208
|
-
private scheduleRender(): void {
|
|
209
|
-
if (
|
|
210
|
-
this.scheduled
|
|
255
|
+
private scheduleRender(only: 'gutter' | 'all' = 'all'): void {
|
|
256
|
+
if (!this.attached) return;
|
|
257
|
+
const pending = this.scheduled;
|
|
258
|
+
this.scheduled = only === 'all' || pending === 'all' ? 'all' : 'gutter';
|
|
259
|
+
if (pending !== 'none') return;
|
|
211
260
|
queueMicrotask(() => {
|
|
212
|
-
this.scheduled
|
|
213
|
-
this.
|
|
261
|
+
const level = this.scheduled;
|
|
262
|
+
this.scheduled = 'none';
|
|
263
|
+
if (level === 'all') this.render();
|
|
264
|
+
else if (level === 'gutter') this.renderGutter();
|
|
214
265
|
});
|
|
215
266
|
}
|
|
216
267
|
|
|
268
|
+
private hasColumns = false;
|
|
269
|
+
private hasRightColumns = false;
|
|
270
|
+
|
|
217
271
|
private render(): void {
|
|
218
|
-
const { backdrop
|
|
272
|
+
const { backdrop } = this.parts;
|
|
219
273
|
const lines = this.text.split('\n');
|
|
220
274
|
const highlight = this.layout?.highlightCurrentLine === true;
|
|
221
275
|
const caretLine = this.text.slice(0, this.caret).split('\n').length - 1;
|
|
@@ -252,9 +306,84 @@ export class WebEditorDriver implements EditorDriver {
|
|
|
252
306
|
const column = document.createElement('div');
|
|
253
307
|
column.className = 'gsv-numbers';
|
|
254
308
|
column.append(...numbers);
|
|
255
|
-
|
|
256
|
-
|
|
309
|
+
this.numbers = column;
|
|
310
|
+
const { root } = this.parts;
|
|
257
311
|
root.style.setProperty('--gsv-gutter-width', `${digitCount(lines.length) + 0}ch`);
|
|
258
312
|
root.style.setProperty('--gsv-gutter-padding', `${GUTTER_PADDING_PX}px`);
|
|
313
|
+
this.renderGutter();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** The lines in view, or all of them where nothing is laid out (a detached element, a test DOM). */
|
|
317
|
+
private visibleLines(count: number): [number, number] {
|
|
318
|
+
const { area, backdrop } = this.parts;
|
|
319
|
+
const row = backdrop.firstElementChild;
|
|
320
|
+
const height = row === null ? 0 : row.getBoundingClientRect().height;
|
|
321
|
+
if (height <= 0 || area.clientHeight <= 0) return [0, count - 1];
|
|
322
|
+
const top = Math.max(0, area.scrollTop - (this.layout?.topMargin ?? 0));
|
|
323
|
+
const first = Math.min(count - 1, Math.floor(top / height));
|
|
324
|
+
const last = Math.min(count - 1, Math.max(first, Math.ceil((top + area.clientHeight) / height) - 1));
|
|
325
|
+
return [first, last];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private cellElement(cell: GutterCell): HTMLElement {
|
|
329
|
+
const element = document.createElement('div');
|
|
330
|
+
element.className = 'gsv-cell';
|
|
331
|
+
if (cell.runs === null) {
|
|
332
|
+
element.textContent = cell.text;
|
|
333
|
+
return element;
|
|
334
|
+
}
|
|
335
|
+
for (const run of cell.runs) {
|
|
336
|
+
const span = document.createElement('span');
|
|
337
|
+
span.textContent = run.text;
|
|
338
|
+
if (run.bold) span.style.fontWeight = 'bold';
|
|
339
|
+
if (run.italic) span.style.fontStyle = 'italic';
|
|
340
|
+
const decorations = [run.underline ? 'underline' : '', run.strikethrough ? 'line-through' : ''];
|
|
341
|
+
span.style.textDecoration = decorations.filter(Boolean).join(' ');
|
|
342
|
+
element.append(span);
|
|
343
|
+
}
|
|
344
|
+
return element;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private columnElement(column: GutterColumn, first: number): HTMLElement {
|
|
348
|
+
const element = document.createElement('div');
|
|
349
|
+
element.className = 'gsv-column';
|
|
350
|
+
element.style.minWidth = `${column.widthRequest}px`;
|
|
351
|
+
element.style.paddingLeft = `${column.marginStart}px`;
|
|
352
|
+
element.style.paddingRight = `${column.marginEnd}px`;
|
|
353
|
+
const spacer = document.createElement('div');
|
|
354
|
+
spacer.style.height = `calc(${first} * 1em * var(--gsv-line-height))`;
|
|
355
|
+
element.append(spacer, ...column.cells.map((cell) => this.cellElement(cell)));
|
|
356
|
+
return element;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** The gutter: the renderers left of the line numbers, the numbers, the renderers right of them. */
|
|
360
|
+
private renderGutter(): void {
|
|
361
|
+
const { gutter, rightGutter, root, area } = this.parts;
|
|
362
|
+
const host = this.host;
|
|
363
|
+
const count = this.text.split('\n').length;
|
|
364
|
+
const [first, last] = this.visibleLines(count);
|
|
365
|
+
const columns = host?.queryGutter(first, last) ?? [];
|
|
366
|
+
this.hasColumns = columns.length > 0;
|
|
367
|
+
const showNumbers = this.layout?.showLineNumbers === true;
|
|
368
|
+
root.classList.toggle('has-columns', this.hasColumns);
|
|
369
|
+
const elements: HTMLElement[] = [];
|
|
370
|
+
for (const column of columns.filter((c) => c.position < 0)) elements.push(this.columnElement(column, first));
|
|
371
|
+
if (showNumbers && this.numbers !== null) elements.push(this.numbers);
|
|
372
|
+
for (const column of columns.filter((c) => c.position >= 0)) elements.push(this.columnElement(column, first));
|
|
373
|
+
const wrapper = document.createElement('div');
|
|
374
|
+
wrapper.className = 'gsv-columns';
|
|
375
|
+
wrapper.append(...elements);
|
|
376
|
+
wrapper.style.transform = `translateY(${-area.scrollTop}px)`;
|
|
377
|
+
gutter.replaceChildren(wrapper);
|
|
378
|
+
|
|
379
|
+
// The RIGHT gutter holds renderers only: the line numbers are a LEFT-side feature.
|
|
380
|
+
const right = host?.queryGutter(first, last, WINDOW_RIGHT) ?? [];
|
|
381
|
+
this.hasRightColumns = right.length > 0;
|
|
382
|
+
root.classList.toggle('has-right-columns', this.hasRightColumns);
|
|
383
|
+
const rightWrapper = document.createElement('div');
|
|
384
|
+
rightWrapper.className = 'gsv-columns';
|
|
385
|
+
rightWrapper.append(...right.map((column) => this.columnElement(column, first)));
|
|
386
|
+
rightWrapper.style.transform = `translateY(${-area.scrollTop}px)`;
|
|
387
|
+
rightGutter.replaceChildren(rightWrapper);
|
|
259
388
|
}
|
|
260
389
|
}
|
package/src/index.ts
CHANGED
|
@@ -119,6 +119,8 @@ 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
|
+
// Gdk's clipboard (`Display`, `Clipboard`, `ContentProvider`): `./namespace/gdk.ts` says what is absent.
|
|
123
|
+
export * as Gdk from './namespace/gdk.js';
|
|
122
124
|
// GLib's timers (`idle_add`, `timeout_add`, `source_remove`): `./namespace/glib.ts` says what is absent.
|
|
123
125
|
export * as GLib from './namespace/glib.js';
|
|
124
126
|
|
package/src/namespace/adw.ts
CHANGED
|
@@ -110,3 +110,4 @@ 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
112
|
export { AdwApplication as Application } from '../application.js';
|
|
113
|
+
export { AdwStyleManager as StyleManager } from '../style-manager.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// The Gdk half of `@gjsify/adwaita-web`'s vocabulary, as far as the clipboard goes (ADR 0096
|
|
2
|
+
// Amendment 2): `Display.get_default().get_clipboard()`, `Clipboard.set_content` / `set` and
|
|
3
|
+
// `ContentProvider.new_for_value`. The behaviour lives once in `@gjsify/adwaita-core` (`gdk.ts`); this
|
|
4
|
+
// file hands it the browser's clipboard, `../clipboard-host.ts`.
|
|
5
|
+
//
|
|
6
|
+
// A TRUE SUBSET: everything else in Gdk is absent, and the `gi://Gdk` arm refuses it by name.
|
|
7
|
+
|
|
8
|
+
import { createGdk } from '@gjsify/adwaita-core';
|
|
9
|
+
|
|
10
|
+
import { webClipboardHost } from '../clipboard-host.js';
|
|
11
|
+
|
|
12
|
+
export const gdk = createGdk(webClipboardHost);
|
|
13
|
+
|
|
14
|
+
export const Display = gdk.Display;
|
|
15
|
+
export const Clipboard = gdk.Clipboard;
|
|
16
|
+
export const ContentProvider = gdk.ContentProvider;
|
|
17
|
+
export const ContentFormats = gdk.ContentFormats;
|
package/src/namespace/gio.ts
CHANGED
|
@@ -12,10 +12,10 @@
|
|
|
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
|
|
15
|
+
// `Gio.File`, `Gio.Settings` and the rest of the library are NOT this
|
|
16
16
|
// package's business; a namespace that grows past what the elements consume is a second,
|
|
17
17
|
// unheld vocabulary.
|
|
18
18
|
|
|
19
19
|
export { GioMenu as Menu, GioMenuItem as MenuItem } from '@gjsify/adwaita-core';
|
|
20
|
-
export { SimpleAction, ApplicationFlags } from '@gjsify/adwaita-core';
|
|
20
|
+
export { SimpleAction, SimpleActionGroup, ApplicationFlags } from '@gjsify/adwaita-core';
|
|
21
21
|
export { GioApplication as Application } from '../application.js';
|
package/src/namespace/glib.ts
CHANGED
|
@@ -3,10 +3,20 @@
|
|
|
3
3
|
// behaviour lives once in `@gjsify/adwaita-core` (`glib-timers.ts`, which states where a host
|
|
4
4
|
// event loop cannot honour GLib); this file is the namespace door over the host's `setTimeout`.
|
|
5
5
|
//
|
|
6
|
+
// The path helpers (`MAXUINT32`, `build_filenamev`, `get_current_dir`, `get_system_data_dirs`) live
|
|
7
|
+
// in `glib-paths.ts` of the core; the host has no filesystem, so `get_current_dir()` is `"/"` and
|
|
8
|
+
// `get_system_data_dirs()` is `[]`. A port that knows better replaces the member in its own file.
|
|
9
|
+
//
|
|
6
10
|
// A TRUE SUBSET: everything else in GLib (`MainLoop`, `Source`, `timeout_add_seconds`, `Bytes`,
|
|
7
|
-
// `
|
|
11
|
+
// `get_home_dir`, …) is absent, and the `gi://GLib` arm refuses it by name.
|
|
8
12
|
|
|
9
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
buildFilenamev,
|
|
15
|
+
createGLibTimers,
|
|
16
|
+
getCurrentDir,
|
|
17
|
+
getSystemDataDirs,
|
|
18
|
+
MAXUINT32 as GLIB_MAXUINT32,
|
|
19
|
+
} from '@gjsify/adwaita-core';
|
|
10
20
|
|
|
11
21
|
const glib = createGLibTimers();
|
|
12
22
|
|
|
@@ -20,3 +30,8 @@ export const SOURCE_CONTINUE = glib.SOURCE_CONTINUE;
|
|
|
20
30
|
export const idle_add = glib.idle_add;
|
|
21
31
|
export const timeout_add = glib.timeout_add;
|
|
22
32
|
export const source_remove = glib.source_remove;
|
|
33
|
+
|
|
34
|
+
export const MAXUINT32 = GLIB_MAXUINT32;
|
|
35
|
+
export const build_filenamev = buildFilenamev;
|
|
36
|
+
export const get_current_dir = getCurrentDir;
|
|
37
|
+
export const get_system_data_dirs = getSystemDataDirs;
|
package/src/namespace/gobject.ts
CHANGED
|
@@ -18,8 +18,10 @@ const GObjectObject = gobject.Object;
|
|
|
18
18
|
export { GObjectObject as Object };
|
|
19
19
|
export const registerClass = gobject.registerClass;
|
|
20
20
|
export const type_ensure = gobject.type_ensure;
|
|
21
|
+
export const signal_stop_emission_by_name = gobject.signal_stop_emission_by_name;
|
|
21
22
|
export const ParamSpec = gobject.ParamSpec;
|
|
22
23
|
export const ParamFlags = gobject.ParamFlags;
|
|
24
|
+
export const Value = gobject.Value;
|
|
23
25
|
export const TYPE_STRING = gobject.TYPE_STRING;
|
|
24
26
|
export const TYPE_BOOLEAN = gobject.TYPE_BOOLEAN;
|
|
25
27
|
export const TYPE_INT = gobject.TYPE_INT;
|
|
@@ -1,15 +1,34 @@
|
|
|
1
|
-
// The plain GTK enums an application reads off `Gtk` — `Gtk.PolicyType`, `Gtk.TextDirection
|
|
2
|
-
// `Gtk.TextWindowType`, as value tables. The numbers are the typelib's (GIR declaration order),
|
|
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
3
|
// 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
8
|
/** `Gtk.PolicyType`: `ALWAYS` 0, `AUTOMATIC` 1, `NEVER` 2, `EXTERNAL` 3. */
|
|
9
|
-
export const PolicyType = Object.freeze({
|
|
9
|
+
export const PolicyType = Object.freeze({
|
|
10
|
+
ALWAYS: 0,
|
|
11
|
+
AUTOMATIC: 1,
|
|
12
|
+
NEVER: 2,
|
|
13
|
+
EXTERNAL: 3,
|
|
14
|
+
} as const);
|
|
10
15
|
|
|
11
16
|
/** `Gtk.TextDirection`: `NONE` 0, `LTR` 1, `RTL` 2. */
|
|
12
|
-
export const TextDirection = Object.freeze({
|
|
17
|
+
export const TextDirection = Object.freeze({
|
|
18
|
+
NONE: 0,
|
|
19
|
+
LTR: 1,
|
|
20
|
+
RTL: 2,
|
|
21
|
+
} as const);
|
|
13
22
|
|
|
14
23
|
/** `Gtk.TextWindowType`: `WIDGET` 1, `TEXT` 2, `LEFT` 3, `RIGHT` 4, `TOP` 5, `BOTTOM` 6. */
|
|
15
|
-
export const TextWindowType = Object.freeze({
|
|
24
|
+
export const TextWindowType = Object.freeze({
|
|
25
|
+
WIDGET: 1,
|
|
26
|
+
TEXT: 2,
|
|
27
|
+
LEFT: 3,
|
|
28
|
+
RIGHT: 4,
|
|
29
|
+
TOP: 5,
|
|
30
|
+
BOTTOM: 6,
|
|
31
|
+
} as const);
|
|
32
|
+
|
|
33
|
+
/** `Gtk.TextExtendSelection`: `WORD` 0 (a double click), `LINE` 1 (a triple click). */
|
|
34
|
+
export const TextExtendSelection = Object.freeze({ WORD: 0, LINE: 1 } as const);
|