@gjsify/adwaita-web 0.56.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/dist/adwaita-web.css +8 -2
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/application.d.ts +24 -0
- package/lib/types/application.spec.d.ts +1 -0
- package/lib/types/elements/adw-application-window.d.ts +2 -0
- package/lib/types/elements/adw-view-stack.d.ts +3 -0
- package/lib/types/elements/checks.d.ts +6 -0
- package/lib/types/elements/gtk-application-window.d.ts +2 -0
- package/lib/types/elements/gtk-drop-down.d.ts +3 -0
- package/lib/types/elements/gtk-entry.d.ts +10 -0
- package/lib/types/elements/gtk-menu-button.d.ts +2 -0
- package/lib/types/elements/gtk-scrolled-window.d.ts +16 -0
- package/lib/types/elements/gtk-stack.d.ts +3 -0
- package/lib/types/elements/gtk-switch.d.ts +3 -0
- package/lib/types/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 +3 -0
- 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/shared-tree-builder.d.ts +20 -1
- package/lib/types/styles.generated.d.ts +1 -1
- package/package.json +11 -9
- package/scss/_reset.scss +1 -1
- package/src/application.spec.ts +75 -0
- package/src/application.ts +92 -0
- package/src/construct-vectors.spec.ts +155 -0
- package/src/elements/adw-application-window.ts +5 -0
- package/src/elements/adw-split-button.ts +5 -1
- package/src/elements/adw-view-stack.ts +14 -0
- package/src/elements/checks.ts +25 -1
- package/src/elements/gtk-application-window.ts +5 -0
- package/src/elements/gtk-drop-down.ts +16 -0
- package/src/elements/gtk-entry.ts +45 -7
- package/src/elements/gtk-menu-button.ts +12 -1
- package/src/elements/gtk-popover-menu.ts +7 -2
- package/src/elements/gtk-scrolled-window.ts +58 -0
- package/src/elements/gtk-stack.ts +8 -0
- package/src/elements/gtk-switch.ts +8 -0
- package/src/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/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 +10 -0
- 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/shared-tree-builder.ts +142 -7
- package/src/split-button.spec.ts +2 -0
- package/src/styles.generated.ts +1 -1
- package/src/test.browser.mts +10 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// `connect` / `disconnect` / `emit` / `notify` on the elements a template builds (ADR 0096 § 3).
|
|
2
|
+
//
|
|
3
|
+
// A registered class gets these from the core. The widgets it holds are plain custom elements,
|
|
4
|
+
// which talk in DOM events, so a template instance that binds to one or connects a handler needs
|
|
5
|
+
// the same four methods on it — over the events the element DECLARES in `static signals`, never
|
|
6
|
+
// over a name it merely might dispatch.
|
|
7
|
+
//
|
|
8
|
+
// NOTIFY ON AN ELEMENT THAT IS NOT CONNECTED (the gap ADR 0093 Progress row 4 names, measured
|
|
9
|
+
// in `gobject-door.spec.ts`): an element dispatches `notify::<property>` from its
|
|
10
|
+
// `attributeChangedCallback`, which returns early until the element has been connected once. The
|
|
11
|
+
// children of a template are built in the constructor and attached on the first
|
|
12
|
+
// `connectedCallback`, so every write to them in between is silent, and a `bidirectional` bind
|
|
13
|
+
// never hears its target. {@link ensureNotifying} closes it per instance: the property's own
|
|
14
|
+
// setter is wrapped, and a change the element did not announce itself is announced here. When
|
|
15
|
+
// the element does announce, nothing is added, so a connected element still notifies once.
|
|
16
|
+
|
|
17
|
+
import { attributeOf, propertyOf } from '@gjsify/adwaita-core/tags';
|
|
18
|
+
|
|
19
|
+
import { dispatchedSignalsOf } from './signals.js';
|
|
20
|
+
|
|
21
|
+
type Handler = (emitter: HTMLElement, ...args: unknown[]) => unknown;
|
|
22
|
+
|
|
23
|
+
/** Events {@link dispatchElement} created, whose `detail` is the argument list of an `emit`. */
|
|
24
|
+
const EMITTED = new WeakSet<Event>();
|
|
25
|
+
const NOTIFYING = new WeakMap<Element, Set<string>>();
|
|
26
|
+
const WITH_API = new WeakSet<Element>();
|
|
27
|
+
let nextHandlerId = 0;
|
|
28
|
+
|
|
29
|
+
/** Whether `el` got its four methods from {@link installElementApi} (a registered class has the core's). */
|
|
30
|
+
export function hasElementApi(el: object): boolean {
|
|
31
|
+
return WITH_API.has(el as Element);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The DOM event `signal` arrives as on `el`, or a refusal naming what the element does declare. */
|
|
35
|
+
function eventOf(el: Element, signal: string): string {
|
|
36
|
+
const declared = dispatchedSignalsOf(el);
|
|
37
|
+
const event =
|
|
38
|
+
declared[signal] ??
|
|
39
|
+
(signal.startsWith('notify::')
|
|
40
|
+
? declared[`notify::${attributeOf(signal.slice('notify::'.length))}`]
|
|
41
|
+
: undefined);
|
|
42
|
+
if (event === undefined) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`<${el.localName}> declares no signal '${signal}'. It dispatches: ${Object.keys(declared).join(', ') || 'none'}.`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return event;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The nearest descriptor of `member` above `el`'s own properties. */
|
|
51
|
+
function inheritedDescriptor(el: object, member: string): PropertyDescriptor | undefined {
|
|
52
|
+
for (
|
|
53
|
+
let at = Object.getPrototypeOf(el) as object | null;
|
|
54
|
+
at !== null;
|
|
55
|
+
at = Object.getPrototypeOf(at) as object | null
|
|
56
|
+
) {
|
|
57
|
+
const descriptor = Object.getOwnPropertyDescriptor(at, member);
|
|
58
|
+
if (descriptor !== undefined) return descriptor;
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function notifyElement(el: HTMLElement, name: string): void {
|
|
64
|
+
const member = propertyOf(name);
|
|
65
|
+
el.dispatchEvent(
|
|
66
|
+
new CustomEvent(eventOf(el, `notify::${name}`), {
|
|
67
|
+
bubbles: true,
|
|
68
|
+
detail: { [member]: (el as unknown as Record<string, unknown>)[member] },
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Makes every change of `property` on `el` reach a `notify::<property>` listener, connected or not. */
|
|
74
|
+
export function ensureNotifying(el: HTMLElement, property: string): void {
|
|
75
|
+
const member = propertyOf(property);
|
|
76
|
+
const patched = NOTIFYING.get(el) ?? new Set<string>();
|
|
77
|
+
if (patched.has(member)) return;
|
|
78
|
+
patched.add(member);
|
|
79
|
+
NOTIFYING.set(el, patched);
|
|
80
|
+
if (Object.getOwnPropertyDescriptor(el, member) !== undefined) return;
|
|
81
|
+
const inherited = inheritedDescriptor(el, member);
|
|
82
|
+
if (inherited?.get === undefined || inherited.set === undefined) return;
|
|
83
|
+
const { get, set } = inherited;
|
|
84
|
+
const event = eventOf(el, `notify::${property}`);
|
|
85
|
+
Object.defineProperty(el, member, {
|
|
86
|
+
configurable: true,
|
|
87
|
+
enumerable: false,
|
|
88
|
+
get() {
|
|
89
|
+
return get.call(this);
|
|
90
|
+
},
|
|
91
|
+
set(this: HTMLElement, value: unknown) {
|
|
92
|
+
const before = get.call(this);
|
|
93
|
+
let announced = false;
|
|
94
|
+
const mark = (domEvent: Event) => {
|
|
95
|
+
if (domEvent.target === this) announced = true;
|
|
96
|
+
};
|
|
97
|
+
this.addEventListener(event, mark);
|
|
98
|
+
try {
|
|
99
|
+
set.call(this, value);
|
|
100
|
+
} finally {
|
|
101
|
+
this.removeEventListener(event, mark);
|
|
102
|
+
}
|
|
103
|
+
if (!announced && get.call(this) !== before) notifyElement(this, property);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Calls `handler(el, ...args)` for `signal`; returns the disconnect. */
|
|
109
|
+
export function listenElement(el: HTMLElement, signal: string, handler: Handler): () => void {
|
|
110
|
+
const event = eventOf(el, signal);
|
|
111
|
+
const notification = signal.startsWith('notify::');
|
|
112
|
+
if (notification) ensureNotifying(el, signal.slice('notify::'.length));
|
|
113
|
+
const listener = (domEvent: Event) => {
|
|
114
|
+
// A notification bubbles, and a parent must not hear its child's property as its own.
|
|
115
|
+
if (notification && domEvent.target !== el) return;
|
|
116
|
+
handler(el, ...(EMITTED.has(domEvent) ? ((domEvent as CustomEvent).detail as unknown[]) : []));
|
|
117
|
+
};
|
|
118
|
+
el.addEventListener(event, listener);
|
|
119
|
+
return () => el.removeEventListener(event, listener);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function dispatchElement(el: HTMLElement, signal: string, args: readonly unknown[]): void {
|
|
123
|
+
const event = eventOf(el, signal);
|
|
124
|
+
const domEvent = new CustomEvent(event, { detail: args, bubbles: event.startsWith('notify::') });
|
|
125
|
+
EMITTED.add(domEvent);
|
|
126
|
+
el.dispatchEvent(domEvent);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Gives `el` the instance API of a registered class, over the events it declares. */
|
|
130
|
+
export function installElementApi(el: HTMLElement): void {
|
|
131
|
+
// A registered class already has the core's four methods on its prototype.
|
|
132
|
+
if (typeof (el as unknown as Record<string, unknown>).connect === 'function') return;
|
|
133
|
+
WITH_API.add(el);
|
|
134
|
+
const connections = new Map<number, () => void>();
|
|
135
|
+
const define = (name: string, value: unknown) =>
|
|
136
|
+
Object.defineProperty(el, name, { value, configurable: true, writable: true, enumerable: false });
|
|
137
|
+
define('connect', (signal: string, handler: Handler): number => {
|
|
138
|
+
const unlisten = listenElement(el, signal, handler);
|
|
139
|
+
const id = ++nextHandlerId;
|
|
140
|
+
connections.set(id, unlisten);
|
|
141
|
+
return id;
|
|
142
|
+
});
|
|
143
|
+
define('disconnect', (id: number): void => {
|
|
144
|
+
const unlisten = connections.get(id);
|
|
145
|
+
if (unlisten === undefined) throw new Error(`No handler with id ${id} is connected to <${el.localName}>`);
|
|
146
|
+
connections.delete(id);
|
|
147
|
+
unlisten();
|
|
148
|
+
});
|
|
149
|
+
define('emit', (signal: string, ...args: unknown[]): void => dispatchElement(el, signal, args));
|
|
150
|
+
define('notify', (name: string): void => notifyElement(el, name));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const API_MEMBERS = new Set(['connect', 'disconnect', 'emit', 'notify']);
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The end of a bind the core's engine drives. An element speaks camelCase members where a `.blp`
|
|
157
|
+
* says `dash-name`, so a built element is wrapped to translate the name; a registered class
|
|
158
|
+
* already has the core's accessors and aliases and is returned as it is.
|
|
159
|
+
*/
|
|
160
|
+
export function endpointOf(el: HTMLElement): HTMLElement {
|
|
161
|
+
if (!hasElementApi(el)) return el;
|
|
162
|
+
return new Proxy(el, {
|
|
163
|
+
get(target, key) {
|
|
164
|
+
if (typeof key !== 'string' || API_MEMBERS.has(key)) return Reflect.get(target, key, target);
|
|
165
|
+
const value = Reflect.get(target, propertyOf(key), target);
|
|
166
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
167
|
+
},
|
|
168
|
+
set(target, key, value) {
|
|
169
|
+
return Reflect.set(target, typeof key === 'string' ? propertyOf(key) : key, value, target);
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
2
|
+
import {
|
|
3
|
+
GTKSOURCE_BUFFER_VECTORS,
|
|
4
|
+
GTKSOURCE_VIEW_DEFAULT_VECTORS,
|
|
5
|
+
GTKSOURCE_VIEW_SURFACE_VECTORS,
|
|
6
|
+
type GtkSourceViewSurfaceLike,
|
|
7
|
+
type GtkSourceBufferLike,
|
|
8
|
+
} from '@gjsify/gtksource-core/conformance';
|
|
9
|
+
|
|
10
|
+
import * as Gtk from '../namespace/gtk.js';
|
|
11
|
+
import * as GtkSource from '../namespace/gtksource.js';
|
|
12
|
+
|
|
13
|
+
import './gtk-source-view.js';
|
|
14
|
+
import { GtkSourceView } from './gtk-source-view.js';
|
|
15
|
+
|
|
16
|
+
export const GtkSourceViewTest = async () => {
|
|
17
|
+
await describe('<gtk-source-view>: GtkSource.View', async () => {
|
|
18
|
+
for (const vector of GTKSOURCE_BUFFER_VECTORS) {
|
|
19
|
+
await it(`GtkSource.Buffer: ${vector.rule}`, () => {
|
|
20
|
+
expect(vector.observe(GtkSource as unknown as GtkSourceBufferLike)).toStrictEqual(vector.shows);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const vector of GTKSOURCE_VIEW_DEFAULT_VECTORS) {
|
|
25
|
+
await it(`${vector.property} defaults to ${vector.shows}`, () => {
|
|
26
|
+
const view = new GtkSourceView();
|
|
27
|
+
expect((view as unknown as Record<string, unknown>)[vector.member]).toStrictEqual(vector.shows);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const vector of GTKSOURCE_VIEW_SURFACE_VECTORS) {
|
|
32
|
+
await it(`surface: ${vector.rule}`, () => {
|
|
33
|
+
expect(vector.observe({ Gtk, GtkSource } as unknown as GtkSourceViewSurfaceLike)).toStrictEqual(
|
|
34
|
+
vector.shows,
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await it('refuses get_gutter by name until slice 6', () => {
|
|
40
|
+
expect(() => new GtkSourceView().get_gutter(Gtk.TextWindowType.LEFT)).toThrow(/get_gutter/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await it('writes through attributes with GTK semantics', () => {
|
|
44
|
+
const view = document.createElement('gtk-source-view') as GtkSourceView;
|
|
45
|
+
view.setAttribute('show-line-numbers', 'true');
|
|
46
|
+
view.setAttribute('indent-width', '4');
|
|
47
|
+
view.setAttribute('left-margin', '12');
|
|
48
|
+
expect(view.showLineNumbers).toBe(true);
|
|
49
|
+
expect(view.indentWidth).toBe(4);
|
|
50
|
+
expect(view.leftMargin).toBe(12);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await it('refuses a value that is not a boolean, by name', () => {
|
|
54
|
+
const view = new GtkSourceView();
|
|
55
|
+
expect(() => (view.editable = 'maybe')).toThrow(/GtkSource.View.editable/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await it('shows the buffer text in the textarea and takes a user edit back into the buffer', () => {
|
|
59
|
+
const view = document.createElement('gtk-source-view') as GtkSourceView;
|
|
60
|
+
document.body.append(view);
|
|
61
|
+
view.buffer.text = 'LDA #$01';
|
|
62
|
+
expect(view.textarea.value).toBe('LDA #$01');
|
|
63
|
+
view.textarea.value = 'LDA #$02';
|
|
64
|
+
view.textarea.dispatchEvent(new Event('input'));
|
|
65
|
+
expect(view.buffer.text).toBe('LDA #$02');
|
|
66
|
+
view.remove();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
};
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
// <gtk-source-view> — `GtkSource.View`, built on `@gjsify/gtksource-core`.
|
|
2
|
+
//
|
|
3
|
+
// WHY NOT <adw-source-view>. That element is CodeMirror with its own grammar; this one reads the
|
|
4
|
+
// same `.lang` and style-scheme files GtkSourceView does, through the same `EditorSession` the
|
|
5
|
+
// Android port runs, so GNOME, Android and the web have one source of truth for highlighting.
|
|
6
|
+
//
|
|
7
|
+
// THE SURFACE IS A TRUE SUBSET of GtkSource.View: the names and semantics below are GtkSourceView's,
|
|
8
|
+
// and what is absent is absent. The editing itself is the browser's (see `web-editor-driver.ts`).
|
|
9
|
+
//
|
|
10
|
+
// IN THIS SLICE: `buffer`, `auto-indent`, `indent-width` (held and read back, as on Android),
|
|
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`. Not yet: gutters (`get_gutter` throws).
|
|
14
|
+
//
|
|
15
|
+
// Reference: GtkSourceView 5 gtksourceview.c, upstream GNOME/gtksourceview (properties and defaults)
|
|
16
|
+
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
17
|
+
|
|
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';
|
|
21
|
+
|
|
22
|
+
import { WebEditorDriver } from './web-editor-driver.js';
|
|
23
|
+
|
|
24
|
+
const STYLE_ID = 'gjsify-gtk-source-view-style';
|
|
25
|
+
|
|
26
|
+
const STYLE = `
|
|
27
|
+
gtk-source-view {
|
|
28
|
+
display: flex;
|
|
29
|
+
flex: 1 1 auto;
|
|
30
|
+
min-width: 0;
|
|
31
|
+
min-height: 12em;
|
|
32
|
+
height: 100%;
|
|
33
|
+
box-sizing: border-box;
|
|
34
|
+
position: relative;
|
|
35
|
+
overflow: hidden;
|
|
36
|
+
background: var(--gsv-bg, var(--view-bg-color));
|
|
37
|
+
color: var(--gsv-fg, var(--view-fg-color));
|
|
38
|
+
font: inherit;
|
|
39
|
+
--gsv-line-height: 1.4;
|
|
40
|
+
}
|
|
41
|
+
gtk-source-view.monospace { font-family: var(--monospace-font-family, ui-monospace, monospace); }
|
|
42
|
+
gtk-source-view .gsv-gutter {
|
|
43
|
+
display: none;
|
|
44
|
+
flex: none;
|
|
45
|
+
overflow: hidden;
|
|
46
|
+
box-sizing: content-box;
|
|
47
|
+
padding: var(--gsv-margin-top, 0px) var(--gsv-gutter-padding, 8px) 0;
|
|
48
|
+
width: var(--gsv-gutter-width, 2ch);
|
|
49
|
+
text-align: end;
|
|
50
|
+
line-height: var(--gsv-line-height);
|
|
51
|
+
color: var(--gsv-number-fg);
|
|
52
|
+
background: var(--gsv-number-bg);
|
|
53
|
+
user-select: none;
|
|
54
|
+
}
|
|
55
|
+
gtk-source-view.show-line-numbers .gsv-gutter { display: block; }
|
|
56
|
+
gtk-source-view .gsv-number.current { color: var(--gsv-current-number-fg); background: var(--gsv-current-number-bg); }
|
|
57
|
+
gtk-source-view .gsv-stage { position: relative; flex: 1 1 auto; min-width: 0; }
|
|
58
|
+
gtk-source-view .gsv-backdrop,
|
|
59
|
+
gtk-source-view .gsv-area {
|
|
60
|
+
position: absolute;
|
|
61
|
+
inset: 0;
|
|
62
|
+
margin: 0;
|
|
63
|
+
border: 0;
|
|
64
|
+
box-sizing: border-box;
|
|
65
|
+
padding: var(--gsv-margin-top, 0px) var(--gsv-margin-right, 0px) var(--gsv-margin-bottom, 0px) var(--gsv-margin-left, 0px);
|
|
66
|
+
font: inherit;
|
|
67
|
+
line-height: var(--gsv-line-height);
|
|
68
|
+
white-space: pre;
|
|
69
|
+
tab-size: 8;
|
|
70
|
+
letter-spacing: normal;
|
|
71
|
+
}
|
|
72
|
+
gtk-source-view .gsv-backdrop { overflow: hidden; pointer-events: none; }
|
|
73
|
+
gtk-source-view .gsv-line { min-height: calc(1em * var(--gsv-line-height)); }
|
|
74
|
+
gtk-source-view .gsv-line.current { background: var(--gsv-current-line); }
|
|
75
|
+
gtk-source-view .gsv-area {
|
|
76
|
+
resize: none;
|
|
77
|
+
overflow: auto;
|
|
78
|
+
outline: none;
|
|
79
|
+
background: transparent;
|
|
80
|
+
color: transparent;
|
|
81
|
+
caret-color: var(--gsv-fg, currentColor);
|
|
82
|
+
}
|
|
83
|
+
gtk-source-view.cursor-hidden .gsv-area { caret-color: transparent; }
|
|
84
|
+
gtk-source-view .gsv-area::selection { background: var(--gsv-selection); color: transparent; }
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
function ensureStyleInjected(): void {
|
|
88
|
+
if (typeof document === 'undefined' || document.getElementById(STYLE_ID) !== null) return;
|
|
89
|
+
const style = document.createElement('style');
|
|
90
|
+
style.id = STYLE_ID;
|
|
91
|
+
style.textContent = STYLE;
|
|
92
|
+
document.head.appendChild(style);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type Coerce = (value: unknown, name: string) => boolean | number;
|
|
96
|
+
|
|
97
|
+
/** attribute → [property, default, coercion] */
|
|
98
|
+
const ATTRIBUTES: Readonly<Record<string, readonly [string, boolean | number, Coerce]>> = {
|
|
99
|
+
'auto-indent': ['autoIndent', false, toBoolean],
|
|
100
|
+
'indent-width': ['indentWidth', -1, toNumber],
|
|
101
|
+
'show-line-numbers': ['showLineNumbers', false, toBoolean],
|
|
102
|
+
'highlight-current-line': ['highlightCurrentLine', false, toBoolean],
|
|
103
|
+
monospace: ['monospace', false, toBoolean],
|
|
104
|
+
editable: ['editable', true, toBoolean],
|
|
105
|
+
'cursor-visible': ['cursorVisible', true, toBoolean],
|
|
106
|
+
'left-margin': ['leftMargin', 0, toNumber],
|
|
107
|
+
'right-margin': ['rightMargin', 0, toNumber],
|
|
108
|
+
'top-margin': ['topMargin', 0, toNumber],
|
|
109
|
+
'bottom-margin': ['bottomMargin', 0, toNumber],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const OBSERVED_ATTRIBUTES = [
|
|
113
|
+
'auto-indent',
|
|
114
|
+
'indent-width',
|
|
115
|
+
'show-line-numbers',
|
|
116
|
+
'highlight-current-line',
|
|
117
|
+
'monospace',
|
|
118
|
+
'editable',
|
|
119
|
+
'cursor-visible',
|
|
120
|
+
'left-margin',
|
|
121
|
+
'right-margin',
|
|
122
|
+
'top-margin',
|
|
123
|
+
'bottom-margin',
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
export class GtkSourceView extends HTMLElement {
|
|
127
|
+
static get observedAttributes(): string[] {
|
|
128
|
+
return OBSERVED_ATTRIBUTES;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private readonly session: EditorSession;
|
|
132
|
+
private readonly driver: WebEditorDriver;
|
|
133
|
+
private unsubscribe: (() => void) | null = null;
|
|
134
|
+
private built = false;
|
|
135
|
+
private readonly gutter = document.createElement('div');
|
|
136
|
+
private readonly backdrop = document.createElement('div');
|
|
137
|
+
private readonly area = document.createElement('textarea');
|
|
138
|
+
private readonly vadj = new GtkAdjustment();
|
|
139
|
+
private readonly hadj = new GtkAdjustment();
|
|
140
|
+
private direction = 1;
|
|
141
|
+
|
|
142
|
+
constructor() {
|
|
143
|
+
super();
|
|
144
|
+
this.gutter.className = 'gsv-gutter';
|
|
145
|
+
this.backdrop.className = 'gsv-backdrop';
|
|
146
|
+
this.area.className = 'gsv-area';
|
|
147
|
+
this.area.wrap = 'off';
|
|
148
|
+
this.area.spellcheck = false;
|
|
149
|
+
this.area.autocapitalize = 'off';
|
|
150
|
+
this.area.autocomplete = 'off';
|
|
151
|
+
this.driver = new WebEditorDriver({
|
|
152
|
+
root: this,
|
|
153
|
+
area: this.area,
|
|
154
|
+
backdrop: this.backdrop,
|
|
155
|
+
gutter: this.gutter,
|
|
156
|
+
});
|
|
157
|
+
this.session = new EditorSession(this.driver, undefined, adwaitaColorScheme());
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private syncAdjustments(): void {
|
|
161
|
+
const { area } = this;
|
|
162
|
+
this.vadj.configure(area.scrollTop, 0, area.scrollHeight, 20, area.clientHeight, area.clientHeight);
|
|
163
|
+
this.hadj.configure(area.scrollLeft, 0, area.scrollWidth, 20, area.clientWidth, area.clientWidth);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private readonly onAreaScroll = (): void => this.syncAdjustments();
|
|
167
|
+
|
|
168
|
+
connectedCallback(): void {
|
|
169
|
+
ensureStyleInjected();
|
|
170
|
+
this.area.addEventListener('scroll', this.onAreaScroll);
|
|
171
|
+
if (!this.built) {
|
|
172
|
+
this.built = true;
|
|
173
|
+
const stage = document.createElement('div');
|
|
174
|
+
stage.className = 'gsv-stage';
|
|
175
|
+
stage.append(this.backdrop, this.area);
|
|
176
|
+
this.append(this.gutter, stage);
|
|
177
|
+
}
|
|
178
|
+
this.driver.attach();
|
|
179
|
+
this.unsubscribe = onAdwaitaColorSchemeChanged(() => this.session.setColorScheme(adwaitaColorScheme()));
|
|
180
|
+
this.session.setColorScheme(adwaitaColorScheme());
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
disconnectedCallback(): void {
|
|
184
|
+
this.area.removeEventListener('scroll', this.onAreaScroll);
|
|
185
|
+
this.unsubscribe?.();
|
|
186
|
+
this.unsubscribe = null;
|
|
187
|
+
this.driver.detach();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
attributeChangedCallback(name: string, _old: string | null, value: string | null): void {
|
|
191
|
+
const [property, fallback, coerce] = ATTRIBUTES[name];
|
|
192
|
+
// Presence is true for a boolean written as a bare attribute (`toggleAttribute`).
|
|
193
|
+
const next =
|
|
194
|
+
value === null
|
|
195
|
+
? fallback
|
|
196
|
+
: value === '' && typeof fallback === 'boolean'
|
|
197
|
+
? true
|
|
198
|
+
: coerce(value, `GtkSource.View.${property}`);
|
|
199
|
+
(this as unknown as Record<string, unknown>)[property] = next;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** `GtkSource.View:buffer`. */
|
|
203
|
+
get buffer(): Buffer {
|
|
204
|
+
return this.session.buffer;
|
|
205
|
+
}
|
|
206
|
+
set buffer(value: Buffer) {
|
|
207
|
+
this.session.buffer = value;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
get autoIndent(): boolean {
|
|
211
|
+
return this.session.autoIndent;
|
|
212
|
+
}
|
|
213
|
+
set autoIndent(value: boolean | string) {
|
|
214
|
+
this.session.autoIndent = toBoolean(value, 'GtkSource.View.autoIndent');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Held and read back; a textarea has no indent step to apply it to (ADR 0094). */
|
|
218
|
+
get indentWidth(): number {
|
|
219
|
+
return this.session.indentWidth;
|
|
220
|
+
}
|
|
221
|
+
set indentWidth(value: number | string) {
|
|
222
|
+
this.session.indentWidth = toNumber(value, 'GtkSource.View.indentWidth');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
get showLineNumbers(): boolean {
|
|
226
|
+
return this.session.showLineNumbers;
|
|
227
|
+
}
|
|
228
|
+
set showLineNumbers(value: boolean | string) {
|
|
229
|
+
this.session.showLineNumbers = toBoolean(value, 'GtkSource.View.showLineNumbers');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
get highlightCurrentLine(): boolean {
|
|
233
|
+
return this.session.highlightCurrentLine;
|
|
234
|
+
}
|
|
235
|
+
set highlightCurrentLine(value: boolean | string) {
|
|
236
|
+
this.session.highlightCurrentLine = toBoolean(value, 'GtkSource.View.highlightCurrentLine');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
get monospace(): boolean {
|
|
240
|
+
return this.session.monospace;
|
|
241
|
+
}
|
|
242
|
+
set monospace(value: boolean | string) {
|
|
243
|
+
this.session.monospace = toBoolean(value, 'GtkSource.View.monospace');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
get editable(): boolean {
|
|
247
|
+
return this.session.editable;
|
|
248
|
+
}
|
|
249
|
+
set editable(value: boolean | string) {
|
|
250
|
+
this.session.editable = toBoolean(value, 'GtkSource.View.editable');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** `GtkTextView:cursor-visible` — whether the insertion cursor is drawn; TRUE by default. */
|
|
254
|
+
get cursorVisible(): boolean {
|
|
255
|
+
return this.session.cursorVisible;
|
|
256
|
+
}
|
|
257
|
+
set cursorVisible(value: boolean | string) {
|
|
258
|
+
this.session.cursorVisible = toBoolean(value, 'GtkSource.View.cursorVisible');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// The GJS snake_case names of the same properties.
|
|
262
|
+
get cursor_visible(): boolean {
|
|
263
|
+
return this.cursorVisible;
|
|
264
|
+
}
|
|
265
|
+
set cursor_visible(value: boolean) {
|
|
266
|
+
this.cursorVisible = value;
|
|
267
|
+
}
|
|
268
|
+
get_cursor_visible(): boolean {
|
|
269
|
+
return this.cursorVisible;
|
|
270
|
+
}
|
|
271
|
+
set_cursor_visible(value: boolean): void {
|
|
272
|
+
this.cursorVisible = value;
|
|
273
|
+
}
|
|
274
|
+
get_editable(): boolean {
|
|
275
|
+
return this.editable;
|
|
276
|
+
}
|
|
277
|
+
set_editable(value: boolean): void {
|
|
278
|
+
this.editable = value;
|
|
279
|
+
}
|
|
280
|
+
get highlight_current_line(): boolean {
|
|
281
|
+
return this.highlightCurrentLine;
|
|
282
|
+
}
|
|
283
|
+
set highlight_current_line(value: boolean) {
|
|
284
|
+
this.highlightCurrentLine = value;
|
|
285
|
+
}
|
|
286
|
+
get show_line_numbers(): boolean {
|
|
287
|
+
return this.showLineNumbers;
|
|
288
|
+
}
|
|
289
|
+
set show_line_numbers(value: boolean) {
|
|
290
|
+
this.showLineNumbers = value;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** `GtkScrollable:vadjustment` — one stable adjustment that follows the textarea's scroll. */
|
|
294
|
+
get vadjustment(): GtkAdjustment {
|
|
295
|
+
return this.vadj;
|
|
296
|
+
}
|
|
297
|
+
get hadjustment(): GtkAdjustment {
|
|
298
|
+
return this.hadj;
|
|
299
|
+
}
|
|
300
|
+
get_vadjustment(): GtkAdjustment {
|
|
301
|
+
return this.vadj;
|
|
302
|
+
}
|
|
303
|
+
get_hadjustment(): GtkAdjustment {
|
|
304
|
+
return this.hadj;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** `gtk_widget_set_direction`: held, and mirrored onto `dir` for LTR and RTL. */
|
|
308
|
+
set_direction(direction: number): void {
|
|
309
|
+
if (![0, 1, 2].includes(direction)) {
|
|
310
|
+
throw new TypeError(`${direction} is not a valid value for enum argument dir`);
|
|
311
|
+
}
|
|
312
|
+
this.direction = direction;
|
|
313
|
+
if (direction === 0) this.removeAttribute('dir');
|
|
314
|
+
else this.dir = direction === 2 ? 'rtl' : 'ltr';
|
|
315
|
+
}
|
|
316
|
+
get_direction(): number {
|
|
317
|
+
return this.direction;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** A view has no widget children of its own here: its parts are the browser's. */
|
|
321
|
+
get_first_child(): null {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
get_next_sibling(): Element | null {
|
|
325
|
+
return this.nextElementSibling;
|
|
326
|
+
}
|
|
327
|
+
get_parent(): Element | null {
|
|
328
|
+
return this.parentElement;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Gutters are slice 6 (ADR 0094). */
|
|
332
|
+
get_gutter(_window_type: number): never {
|
|
333
|
+
throw new Error('GtkSource.View.get_gutter is not implemented');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
get leftMargin(): number {
|
|
337
|
+
return this.session.leftMargin;
|
|
338
|
+
}
|
|
339
|
+
set leftMargin(value: number | string) {
|
|
340
|
+
this.session.leftMargin = toNumber(value, 'GtkSource.View.leftMargin');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
get rightMargin(): number {
|
|
344
|
+
return this.session.rightMargin;
|
|
345
|
+
}
|
|
346
|
+
set rightMargin(value: number | string) {
|
|
347
|
+
this.session.rightMargin = toNumber(value, 'GtkSource.View.rightMargin');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
get topMargin(): number {
|
|
351
|
+
return this.session.topMargin;
|
|
352
|
+
}
|
|
353
|
+
set topMargin(value: number | string) {
|
|
354
|
+
this.session.topMargin = toNumber(value, 'GtkSource.View.topMargin');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
get bottomMargin(): number {
|
|
358
|
+
return this.session.bottomMargin;
|
|
359
|
+
}
|
|
360
|
+
set bottomMargin(value: number | string) {
|
|
361
|
+
this.session.bottomMargin = toNumber(value, 'GtkSource.View.bottomMargin');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** GJS `connect(name, cb) → id`; the handler's first argument is this view. */
|
|
365
|
+
connect(name: string, callback: (self: GtkSourceView, ...args: never[]) => void): number {
|
|
366
|
+
return this.session.connect(name, ((_session: unknown, ...args: never[]) => callback(this, ...args)) as never);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
disconnect(id: number): void {
|
|
370
|
+
this.session.disconnect(id);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** The inner textarea, for focus and tests. */
|
|
374
|
+
get textarea(): HTMLTextAreaElement {
|
|
375
|
+
return this.area;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
customElements.define('gtk-source-view', GtkSourceView);
|