@gjsify/adwaita-web 0.61.0 → 0.63.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 +31 -0
- package/lib/types/canvas2d/canvas2d-bridge.d.ts +55 -0
- package/lib/types/canvas2d/index.d.ts +1 -0
- package/lib/types/elements/gtk-window.d.ts +33 -0
- package/lib/types/pango-markup.d.ts +10 -0
- package/lib/types/pango-markup.spec.d.ts +1 -0
- package/lib/types/source-view/copy-glyph.d.ts +6 -0
- package/lib/types/source-view/copy-glyph.spec.d.ts +1 -0
- package/package.json +15 -11
- package/src/application.spec.ts +33 -0
- package/src/application.ts +16 -0
- package/src/canvas2d/canvas2d-bridge.ts +218 -0
- package/src/canvas2d/index.ts +5 -0
- package/src/elements/adw-alert-dialog.ts +8 -5
- package/src/elements/adw-banner.ts +14 -9
- package/src/elements/gtk-window.ts +146 -11
- package/src/gtk-window.spec.ts +122 -0
- package/src/pango-markup.spec.ts +257 -0
- package/src/pango-markup.ts +266 -0
- package/src/source-view/adw-source-view.ts +2 -8
- package/src/source-view/copy-glyph.spec.ts +46 -0
- package/src/source-view/copy-glyph.ts +52 -0
- package/src/test.browser.mts +4 -0
package/README.md
CHANGED
|
@@ -130,6 +130,37 @@ auto-dismiss runs on the browser's timers by default; assign
|
|
|
130
130
|
defines) to drive toast lifetimes from your own clock — that is how the
|
|
131
131
|
package's own regression tests advance time without real timers.
|
|
132
132
|
|
|
133
|
+
## Pango markup (`use-markup`)
|
|
134
|
+
|
|
135
|
+
`<adw-banner>`'s `title`, `<adw-alert-dialog>`'s `heading` and `body` and
|
|
136
|
+
`<gtk-label>` / `<gtk-inscription>`'s `markup` read Pango markup — GTK's eleven
|
|
137
|
+
tags (`b`, `i`, `u`, `s`, `tt`, `big`, `small`, `sub`, `sup`, `span`,
|
|
138
|
+
`markup`), `<span>` attributes, and `&`/`<`/`>`/`"`/`'`
|
|
139
|
+
plus numeric character references.
|
|
140
|
+
|
|
141
|
+
Pango markup is **not** HTML, and nothing here is handed to `innerHTML`:
|
|
142
|
+
`renderPangoMarkup` (in `src/pango-markup.ts`) builds elements and text nodes
|
|
143
|
+
from the tree `parsePangoMarkup` (`@gjsify/adwaita-core`) returns. The banner
|
|
144
|
+
and the alert dialog therefore DRAW the runs:
|
|
145
|
+
|
|
146
|
+
```html
|
|
147
|
+
<adw-banner title="<b>Metered</b> connection" use-markup revealed></adw-banner>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Anything pango cannot parse comes back as **text**, exactly as
|
|
151
|
+
`gtk_label_set_markup` keeps an unparseable string: a bare `&`, a `<` in prose,
|
|
152
|
+
a mismatched tag — and, importantly, every tag outside pango's set. An
|
|
153
|
+
`<img src=x onerror=…>` in a banner title is shown, not run; there is no code
|
|
154
|
+
path that creates an element for a name the parser did not recognise, and a
|
|
155
|
+
`<span>` attribute becomes a CSS declaration (or is dropped), never a DOM
|
|
156
|
+
attribute.
|
|
157
|
+
|
|
158
|
+
Without `use-markup` every one of these strings is literal text — that is
|
|
159
|
+
`Adw.Banner:use-markup`'s deliberate departure from the libadwaita default of
|
|
160
|
+
`TRUE`, documented in `src/elements/adw-banner.ts`.
|
|
161
|
+
`scripts/check-adwaita-no-markup-sinks.mjs` holds the no-`innerHTML` rule for
|
|
162
|
+
the package's shipping sources.
|
|
163
|
+
|
|
133
164
|
## Theming a page without `<adw-window>`
|
|
134
165
|
|
|
135
166
|
The components read their colours from the `--window-*` CSS custom properties,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
type ReadyCallback = (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) => void;
|
|
2
|
+
type ResizeCallback = (width: number, height: number) => void;
|
|
3
|
+
type SignalHandler = (emitter: Canvas2DBridgeElement, ...args: unknown[]) => unknown;
|
|
4
|
+
export declare class Canvas2DBridgeElement extends HTMLElement {
|
|
5
|
+
static readonly GTypeName = "GjsifyCanvas2DBridge";
|
|
6
|
+
/** The signals `connect()` accepts, as `Gtk.Widget` names them. */
|
|
7
|
+
static readonly signals: Record<string, string>;
|
|
8
|
+
private _canvas;
|
|
9
|
+
private _ctx;
|
|
10
|
+
private _readyCallbacks;
|
|
11
|
+
private _resizeCallbacks;
|
|
12
|
+
private _handlers;
|
|
13
|
+
private _nextHandler;
|
|
14
|
+
private _observer;
|
|
15
|
+
private _width;
|
|
16
|
+
private _height;
|
|
17
|
+
private _drawFrame;
|
|
18
|
+
static get observedAttributes(): string[];
|
|
19
|
+
/** `Gtk.DrawingArea:content-width`, the minimum and natural width. Defaults to 0. */
|
|
20
|
+
get contentWidth(): number;
|
|
21
|
+
set contentWidth(v: number);
|
|
22
|
+
/** `Gtk.DrawingArea:content-height`, the minimum and natural height. Defaults to 0. */
|
|
23
|
+
get contentHeight(): number;
|
|
24
|
+
set contentHeight(v: number);
|
|
25
|
+
get content_width(): number;
|
|
26
|
+
set content_width(v: number);
|
|
27
|
+
get content_height(): number;
|
|
28
|
+
set content_height(v: number);
|
|
29
|
+
private _write;
|
|
30
|
+
/** The `<canvas>` backing this widget. Available after the first allocation. */
|
|
31
|
+
get canvas(): HTMLCanvasElement | null;
|
|
32
|
+
/** The browser's own 2D context; available after the first allocation. */
|
|
33
|
+
getContext(_id: '2d'): CanvasRenderingContext2D | null;
|
|
34
|
+
/** Runs `cb` once the 2D context is ready, synchronously if it already is. */
|
|
35
|
+
onReady(cb: ReadyCallback): void;
|
|
36
|
+
/** Runs `cb` on every allocation change, with the canvas dimensions already updated. */
|
|
37
|
+
onResize(cb: ResizeCallback): void;
|
|
38
|
+
/** The browser's own, here only so the call reads the same on every target. */
|
|
39
|
+
requestAnimationFrame(cb: FrameRequestCallback): number;
|
|
40
|
+
/** A no-op: the browser already has `requestAnimationFrame` and `performance`. */
|
|
41
|
+
installGlobals(): void;
|
|
42
|
+
/** `gtk_widget_queue_draw`: nothing to blit here, the canvas IS the screen. */
|
|
43
|
+
queue_draw(): void;
|
|
44
|
+
queueDraw(): void;
|
|
45
|
+
connect(signal: string, handler: SignalHandler): number;
|
|
46
|
+
disconnect(id: number): void;
|
|
47
|
+
connectedCallback(): void;
|
|
48
|
+
disconnectedCallback(): void;
|
|
49
|
+
attributeChangedCallback(name: string): void;
|
|
50
|
+
private _publishSize;
|
|
51
|
+
private _observe;
|
|
52
|
+
/** The GJS widget's `_onDraw`, minus the blit. */
|
|
53
|
+
private _allocated;
|
|
54
|
+
}
|
|
55
|
+
export { Canvas2DBridgeElement as Canvas2DBridge };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Canvas2DBridge, Canvas2DBridgeElement } from './canvas2d-bridge.js';
|
|
@@ -13,6 +13,12 @@ export declare class GtkWindow extends HTMLElement {
|
|
|
13
13
|
private _titleLabel;
|
|
14
14
|
/** Watches the strip for an author's titlebar arriving after the build. */
|
|
15
15
|
private _titlebarObserver;
|
|
16
|
+
/** `is-active` and `focus-widget`; `maximized` stays the attribute above. */
|
|
17
|
+
private readonly _members;
|
|
18
|
+
private readonly _closing;
|
|
19
|
+
private readonly _syncActive;
|
|
20
|
+
private readonly _onFocusIn;
|
|
21
|
+
private readonly _onFocusOut;
|
|
16
22
|
static get observedAttributes(): string[];
|
|
17
23
|
/**
|
|
18
24
|
* `Gtk.Window:title` — the window's title.
|
|
@@ -54,6 +60,9 @@ export declare class GtkWindow extends HTMLElement {
|
|
|
54
60
|
private _offByDefault;
|
|
55
61
|
connectedCallback(): void;
|
|
56
62
|
disconnectedCallback(): void;
|
|
63
|
+
private _watchActivity;
|
|
64
|
+
private _documentIsActive;
|
|
65
|
+
private _notifyMember;
|
|
57
66
|
/**
|
|
58
67
|
* A `slot="titlebar"` child adopted AFTER the build has to take the strip over from the
|
|
59
68
|
* server-side decoration, and `_render` is the one that notices — nothing else here runs
|
|
@@ -90,8 +99,32 @@ export declare class GtkWindow extends HTMLElement {
|
|
|
90
99
|
* it, hide the window when `hide-on-close` is set and DESTROY it otherwise. "Destroy" is
|
|
91
100
|
* the one word this port cannot mean — a page has no unref — so the default detaches the
|
|
92
101
|
* frame, which is the same observable outcome for everything above it.
|
|
102
|
+
*
|
|
103
|
+
* A frame that is not in the page is not realized, so there is nothing to close; a listener
|
|
104
|
+
* that calls `close()` again is ignored, as GTK ignores it. Answers whether the window went
|
|
105
|
+
* away (hidden or detached), `false` when a listener took the request over or it was ignored.
|
|
93
106
|
*/
|
|
94
107
|
close(): boolean;
|
|
108
|
+
/**
|
|
109
|
+
* `gtk_window_present`: show the frame. GTK also raises and focuses the toplevel, which is
|
|
110
|
+
* the window manager's to grant; a page owns no such surface, so this is only the first half.
|
|
111
|
+
*/
|
|
112
|
+
present(): void;
|
|
113
|
+
/** `Gtk.Window:is-active` — see the header: the document's focus, while this frame is shown. */
|
|
114
|
+
get isActive(): boolean;
|
|
115
|
+
/** `gtk_window_is_active`. */
|
|
116
|
+
is_active(): boolean;
|
|
117
|
+
/** `Gtk.Window:focus-widget`, read live from the document. */
|
|
118
|
+
get focusWidget(): Element | null;
|
|
119
|
+
/** `gtk_window_get_focus`: the element inside the frame that has focus, or `null`. */
|
|
120
|
+
get_focus(): Element | null;
|
|
121
|
+
/**
|
|
122
|
+
* `gtk_window_set_focus`. `null` clears the focus. An element outside the frame is refused:
|
|
123
|
+
* GTK's window only focuses its own descendants.
|
|
124
|
+
*/
|
|
125
|
+
set_focus(widget: Element | null): void;
|
|
126
|
+
/** `gtk_window_is_maximized`: the FRAME's state, not the browser window's. */
|
|
127
|
+
is_maximized(): boolean;
|
|
95
128
|
/**
|
|
96
129
|
* `gtk_window_maximize`. ASYNCHRONOUS in GTK — the GIR says so at gtkwindow.c:1044 — so
|
|
97
130
|
* the property is the thing a caller watches, and this writes it directly rather than
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render Pango markup into `target`, replacing its content — the one door every
|
|
3
|
+
* `use-markup` surface in this package goes through.
|
|
4
|
+
*
|
|
5
|
+
* The `null` branch is the C fallback and is the SECURITY property: markup that does not
|
|
6
|
+
* parse is written as TEXT, so an unrecognised tag can never become an element. Callers
|
|
7
|
+
* that want markup only when a caller has asked for it read that from their own flag and
|
|
8
|
+
* pass plain text here — this function never decides whether markup is meant.
|
|
9
|
+
*/
|
|
10
|
+
export declare function renderPangoMarkup(target: HTMLElement, markup: string): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const PangoMarkupTest: () => Promise<void>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A recognisable copy glyph rendered inline so the opt-in `source-view` subpath needs no
|
|
3
|
+
* icon CSS. `doc` is a parameter so a caller outside the main document (a shadow root in
|
|
4
|
+
* another frame) gets nodes its own tree can adopt.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createCopyGlyph(doc?: Document): SVGSVGElement;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CopyGlyphTest: () => Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/adwaita-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.63.0",
|
|
4
4
|
"description": "Adwaita/Libadwaita web components for browser targets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
"types": "./lib/types/source-view/index.d.ts",
|
|
15
15
|
"default": "./src/source-view/index.ts"
|
|
16
16
|
},
|
|
17
|
+
"./canvas2d": {
|
|
18
|
+
"types": "./lib/types/canvas2d/index.d.ts",
|
|
19
|
+
"default": "./src/canvas2d/index.ts"
|
|
20
|
+
},
|
|
17
21
|
"./fonts": {
|
|
18
22
|
"types": "./lib/types/fonts.d.ts",
|
|
19
23
|
"default": "./src/fonts.ts"
|
|
@@ -54,14 +58,14 @@
|
|
|
54
58
|
"@codemirror/language": "^6.12.4",
|
|
55
59
|
"@codemirror/state": "^6.7.0",
|
|
56
60
|
"@codemirror/view": "^6.43.4",
|
|
57
|
-
"@gjsify/adwaita-core": "^0.
|
|
58
|
-
"@gjsify/adwaita-fonts": "^0.
|
|
59
|
-
"@gjsify/adwaita-icons": "^0.
|
|
60
|
-
"@gjsify/gtksource-core": "^0.
|
|
61
|
+
"@gjsify/adwaita-core": "^0.63.0",
|
|
62
|
+
"@gjsify/adwaita-fonts": "^0.63.0",
|
|
63
|
+
"@gjsify/adwaita-icons": "^0.63.0",
|
|
64
|
+
"@gjsify/gtksource-core": "^0.63.0",
|
|
61
65
|
"@lezer/highlight": "^1.2.3"
|
|
62
66
|
},
|
|
63
67
|
"peerDependencies": {
|
|
64
|
-
"@gjsify/app-settings": "^0.
|
|
68
|
+
"@gjsify/app-settings": "^0.63.0"
|
|
65
69
|
},
|
|
66
70
|
"peerDependenciesMeta": {
|
|
67
71
|
"@gjsify/app-settings": {
|
|
@@ -69,11 +73,11 @@
|
|
|
69
73
|
}
|
|
70
74
|
},
|
|
71
75
|
"devDependencies": {
|
|
72
|
-
"@gjsify/app-settings": "^0.
|
|
73
|
-
"@gjsify/blueprint": "^0.
|
|
74
|
-
"@gjsify/cli": "^0.
|
|
75
|
-
"@gjsify/unit": "^0.
|
|
76
|
-
"@gjsify/vite-plugin-blueprint": "^0.
|
|
76
|
+
"@gjsify/app-settings": "^0.63.0",
|
|
77
|
+
"@gjsify/blueprint": "^0.63.0",
|
|
78
|
+
"@gjsify/cli": "^0.63.0",
|
|
79
|
+
"@gjsify/unit": "^0.63.0",
|
|
80
|
+
"@gjsify/vite-plugin-blueprint": "^0.63.0",
|
|
77
81
|
"sass": "^1.101.0",
|
|
78
82
|
"typescript": "^6.0.3"
|
|
79
83
|
},
|
package/src/application.spec.ts
CHANGED
|
@@ -63,6 +63,39 @@ export const ApplicationTest = async () => {
|
|
|
63
63
|
await it('Adw.Application is a Gtk.Application', () => {
|
|
64
64
|
expect(Adw.Application.prototype instanceof Gtk.Application).toBe(true);
|
|
65
65
|
});
|
|
66
|
+
await it('a keydown in the window runs the accelerator, with its target', () => {
|
|
67
|
+
const app = new Adw.Application({ application_id: 'org.gjsify.vec.WebAccel' });
|
|
68
|
+
const window = scene.window(app as never) as Element;
|
|
69
|
+
const hits: unknown[] = [];
|
|
70
|
+
for (const [owner, name] of [
|
|
71
|
+
[app, 'save'],
|
|
72
|
+
[window, 'open'],
|
|
73
|
+
[app, 'view'],
|
|
74
|
+
] as const) {
|
|
75
|
+
const action = new Gio.SimpleAction({ name });
|
|
76
|
+
action.connect('activate', (_a: unknown, parameter: unknown) => hits.push([name, parameter ?? null]));
|
|
77
|
+
(owner as unknown as { add_action(a: object): void }).add_action(action);
|
|
78
|
+
}
|
|
79
|
+
app.set_accels_for_action('app.save', ['<Primary>s']);
|
|
80
|
+
app.set_accels_for_action('win.open', ['<Primary><Shift>o']);
|
|
81
|
+
app.set_accels_for_action('app.view::list', ['<Primary>l']);
|
|
82
|
+
const press = (init: KeyboardEventInit): boolean => {
|
|
83
|
+
const event = new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init });
|
|
84
|
+
window.dispatchEvent(event);
|
|
85
|
+
return event.defaultPrevented;
|
|
86
|
+
};
|
|
87
|
+
expect(press({ key: 's', ctrlKey: true })).toBe(true);
|
|
88
|
+
expect(press({ key: 'O', ctrlKey: true, shiftKey: true })).toBe(true);
|
|
89
|
+
expect(press({ key: 'l', ctrlKey: true })).toBe(true);
|
|
90
|
+
expect(press({ key: 's' })).toBe(false);
|
|
91
|
+
expect(hits).toStrictEqual([
|
|
92
|
+
['save', null],
|
|
93
|
+
['open', null],
|
|
94
|
+
['view', 'list'],
|
|
95
|
+
]);
|
|
96
|
+
(window as unknown as { application: object | null }).application = null;
|
|
97
|
+
expect(press({ key: 's', ctrlKey: true })).toBe(false);
|
|
98
|
+
});
|
|
66
99
|
await it('quit() takes the windows it added off the page', async () => {
|
|
67
100
|
const app = new Adw.Application({ application_id: 'org.gjsify.vec.WebQuit' });
|
|
68
101
|
const window = scene.window(app as never) as Element;
|
package/src/application.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
activateWidgetAction as activateIn,
|
|
12
12
|
activateMenuItem,
|
|
13
13
|
applicationOfWindow,
|
|
14
|
+
dispatchAccelerator,
|
|
14
15
|
menuActionsFor,
|
|
15
16
|
installActionMap,
|
|
16
17
|
type AdwMenuItem,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
type AdwMenuModel,
|
|
19
20
|
type ApplicationHost,
|
|
20
21
|
} from '@gjsify/adwaita-core';
|
|
22
|
+
import { webPlatformHost } from './platform-host.js';
|
|
21
23
|
|
|
22
24
|
export const webHost: ApplicationHost = {
|
|
23
25
|
name: 'adwaita-web',
|
|
@@ -70,6 +72,19 @@ export class AdwApplication extends GtkApplication {}
|
|
|
70
72
|
|
|
71
73
|
type WindowApplication = { add_window(window: object): void; remove_window(window: object): void };
|
|
72
74
|
|
|
75
|
+
const ACCEL_LISTENERS = new WeakMap<object, () => void>();
|
|
76
|
+
|
|
77
|
+
/** Key presses in a window of an application run its accelerators; keydown of descendants bubbles here. */
|
|
78
|
+
function followAccelerators(window: object, application: WindowApplication | null): void {
|
|
79
|
+
ACCEL_LISTENERS.get(window)?.();
|
|
80
|
+
ACCEL_LISTENERS.delete(window);
|
|
81
|
+
if (application === null) return;
|
|
82
|
+
ACCEL_LISTENERS.set(
|
|
83
|
+
window,
|
|
84
|
+
webPlatformHost.listenKeys(window, (event) => dispatchAccelerator(window, event, parentOfElement)),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
73
88
|
/**
|
|
74
89
|
* `Gtk.Window:application` and the window's `Gio.ActionMap` (`win.` actions) on an element class.
|
|
75
90
|
* Setting the property is `add_window`, as on GTK; setting it to `null` is `remove_window`.
|
|
@@ -87,6 +102,7 @@ export function defineWindowApplication(proto: object): void {
|
|
|
87
102
|
if (current === value) return;
|
|
88
103
|
current?.remove_window(this);
|
|
89
104
|
value?.add_window(this);
|
|
105
|
+
followAccelerators(this, value);
|
|
90
106
|
},
|
|
91
107
|
});
|
|
92
108
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// <gjsify-canvas2d-bridge> — the browser half of `Canvas2DBridge` from `@gjsify/canvas2d` (ADR 0107).
|
|
2
|
+
//
|
|
3
|
+
// On GJS the bridge is a `Gtk.DrawingArea` that hands the app a Cairo-backed canvas. Here the
|
|
4
|
+
// canvas is a real `<canvas>` and `getContext('2d')` returns the browser's own context, so the
|
|
5
|
+
// size and resize semantics are the GJS widget's, name for name:
|
|
6
|
+
//
|
|
7
|
+
// · the canvas exists from the first allocation, sized to it in LOGICAL pixels (no DPR backing),
|
|
8
|
+
// · `onReady(cb)` runs once canvas and context exist, synchronously if they already do,
|
|
9
|
+
// · the size is NOT synced again until the allocation changes, so a ready callback that sized
|
|
10
|
+
// the canvas for a static render keeps its pixels,
|
|
11
|
+
// · an allocation change sets `canvas.width/height`, dispatches a DOM `resize` event on the
|
|
12
|
+
// canvas, then calls the `onResize` callbacks and the `resize` signal handlers.
|
|
13
|
+
//
|
|
14
|
+
// The canvas sits at the top-left at its natural size; the host clips the rest. That is the GJS
|
|
15
|
+
// widget's blit: the surface painted unscaled at (0, 0).
|
|
16
|
+
|
|
17
|
+
import { registerTemplateClass } from '../template-classes.js';
|
|
18
|
+
|
|
19
|
+
type ReadyCallback = (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) => void;
|
|
20
|
+
type ResizeCallback = (width: number, height: number) => void;
|
|
21
|
+
type SignalHandler = (emitter: Canvas2DBridgeElement, ...args: unknown[]) => unknown;
|
|
22
|
+
|
|
23
|
+
const TAG = 'gjsify-canvas2d-bridge';
|
|
24
|
+
|
|
25
|
+
/** `G_MAXINT`, the top of `g_param_spec_int`'s range. */
|
|
26
|
+
const MAX_INT = 2147483647;
|
|
27
|
+
|
|
28
|
+
function intAttribute(element: HTMLElement, name: string): number {
|
|
29
|
+
const raw = Number.parseInt(element.getAttribute(name) ?? '', 10);
|
|
30
|
+
return Number.isFinite(raw) ? raw : 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class Canvas2DBridgeElement extends HTMLElement {
|
|
34
|
+
static readonly GTypeName = 'GjsifyCanvas2DBridge';
|
|
35
|
+
/** The signals `connect()` accepts, as `Gtk.Widget` names them. */
|
|
36
|
+
static readonly signals: Record<string, string> = { resize: 'resize' };
|
|
37
|
+
|
|
38
|
+
private _canvas: HTMLCanvasElement | null = null;
|
|
39
|
+
private _ctx: CanvasRenderingContext2D | null = null;
|
|
40
|
+
private _readyCallbacks: ReadyCallback[] = [];
|
|
41
|
+
private _resizeCallbacks: ResizeCallback[] = [];
|
|
42
|
+
private _handlers = new Map<number, { signal: string; handler: SignalHandler }>();
|
|
43
|
+
private _nextHandler = 0;
|
|
44
|
+
private _observer: ResizeObserver | null = null;
|
|
45
|
+
private _width = -1;
|
|
46
|
+
private _height = -1;
|
|
47
|
+
private _drawFrame: number | null = null;
|
|
48
|
+
|
|
49
|
+
static get observedAttributes() {
|
|
50
|
+
return ['content-width', 'content-height'];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** `Gtk.DrawingArea:content-width`, the minimum and natural width. Defaults to 0. */
|
|
54
|
+
get contentWidth(): number {
|
|
55
|
+
return intAttribute(this, 'content-width');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
set contentWidth(v: number) {
|
|
59
|
+
this._write('content-width', Math.min(Math.max(Math.trunc(v) || 0, 0), MAX_INT));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** `Gtk.DrawingArea:content-height`, the minimum and natural height. Defaults to 0. */
|
|
63
|
+
get contentHeight(): number {
|
|
64
|
+
return intAttribute(this, 'content-height');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
set contentHeight(v: number) {
|
|
68
|
+
this._write('content-height', Math.min(Math.max(Math.trunc(v) || 0, 0), MAX_INT));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get content_width(): number {
|
|
72
|
+
return this.contentWidth;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
set content_width(v: number) {
|
|
76
|
+
this.contentWidth = v;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get content_height(): number {
|
|
80
|
+
return this.contentHeight;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
set content_height(v: number) {
|
|
84
|
+
this.contentHeight = v;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private _write(name: string, value: number): void {
|
|
88
|
+
if (this.getAttribute(name) !== String(value)) this.setAttribute(name, String(value));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The `<canvas>` backing this widget. Available after the first allocation. */
|
|
92
|
+
get canvas(): HTMLCanvasElement | null {
|
|
93
|
+
return this._canvas;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The browser's own 2D context; available after the first allocation. */
|
|
97
|
+
getContext(_id: '2d'): CanvasRenderingContext2D | null {
|
|
98
|
+
return this._ctx;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Runs `cb` once the 2D context is ready, synchronously if it already is. */
|
|
102
|
+
onReady(cb: ReadyCallback): void {
|
|
103
|
+
if (this._canvas && this._ctx) {
|
|
104
|
+
cb(this._canvas, this._ctx);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this._readyCallbacks.push(cb);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Runs `cb` on every allocation change, with the canvas dimensions already updated. */
|
|
111
|
+
onResize(cb: ResizeCallback): void {
|
|
112
|
+
this._resizeCallbacks.push(cb);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The browser's own, here only so the call reads the same on every target. */
|
|
116
|
+
requestAnimationFrame(cb: FrameRequestCallback): number {
|
|
117
|
+
return globalThis.requestAnimationFrame(cb);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A no-op: the browser already has `requestAnimationFrame` and `performance`. */
|
|
121
|
+
installGlobals(): void {}
|
|
122
|
+
|
|
123
|
+
/** `gtk_widget_queue_draw`: nothing to blit here, the canvas IS the screen. */
|
|
124
|
+
queue_draw(): void {}
|
|
125
|
+
|
|
126
|
+
queueDraw(): void {}
|
|
127
|
+
|
|
128
|
+
connect(signal: string, handler: SignalHandler): number {
|
|
129
|
+
if (!(signal in Canvas2DBridgeElement.signals)) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`<${TAG}> declares no signal '${signal}'. It emits: ${Object.keys(Canvas2DBridgeElement.signals).join(', ')}.`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const id = ++this._nextHandler;
|
|
135
|
+
this._handlers.set(id, { signal, handler });
|
|
136
|
+
return id;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
disconnect(id: number): void {
|
|
140
|
+
if (!this._handlers.delete(id)) throw new Error(`No handler with id ${id} is connected to <${TAG}>`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
connectedCallback() {
|
|
144
|
+
if (!this.style.display) this.style.display = 'block';
|
|
145
|
+
this.style.overflow = 'hidden';
|
|
146
|
+
this.style.position = 'relative';
|
|
147
|
+
this._publishSize();
|
|
148
|
+
this._observe();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
disconnectedCallback() {
|
|
152
|
+
this._observer?.disconnect();
|
|
153
|
+
this._observer = null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
attributeChangedCallback(name: string) {
|
|
157
|
+
if (name === 'content-width' || name === 'content-height') this._publishSize();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private _publishSize(): void {
|
|
161
|
+
const size: [string, number][] = [
|
|
162
|
+
['min-width', this.contentWidth],
|
|
163
|
+
['min-height', this.contentHeight],
|
|
164
|
+
];
|
|
165
|
+
for (const [property, value] of size) {
|
|
166
|
+
if (value > 0) this.style.setProperty(property, `${value}px`);
|
|
167
|
+
else this.style.removeProperty(property);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private _observe(): void {
|
|
172
|
+
if (this._observer) return;
|
|
173
|
+
this._observer = new ResizeObserver((entries) => {
|
|
174
|
+
for (const entry of entries) {
|
|
175
|
+
const box = entry.contentBoxSize?.[0];
|
|
176
|
+
this._allocated(
|
|
177
|
+
Math.round(box ? box.inlineSize : entry.contentRect.width),
|
|
178
|
+
Math.round(box ? box.blockSize : entry.contentRect.height),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
this._observer.observe(this);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The GJS widget's `_onDraw`, minus the blit. */
|
|
186
|
+
private _allocated(width: number, height: number): void {
|
|
187
|
+
if (!this._canvas) {
|
|
188
|
+
const canvas = document.createElement('canvas');
|
|
189
|
+
canvas.style.cssText = 'position:absolute;left:0;top:0;';
|
|
190
|
+
canvas.width = width;
|
|
191
|
+
canvas.height = height;
|
|
192
|
+
this._canvas = canvas;
|
|
193
|
+
this._ctx = canvas.getContext('2d');
|
|
194
|
+
this._width = width;
|
|
195
|
+
this._height = height;
|
|
196
|
+
this.replaceChildren(canvas);
|
|
197
|
+
const ready = this._readyCallbacks;
|
|
198
|
+
this._readyCallbacks = [];
|
|
199
|
+
if (this._ctx) for (const cb of ready) cb(canvas, this._ctx);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (this._width === width && this._height === height) return;
|
|
203
|
+
this._width = width;
|
|
204
|
+
this._height = height;
|
|
205
|
+
this._canvas.width = width;
|
|
206
|
+
this._canvas.height = height;
|
|
207
|
+
this._canvas.dispatchEvent(new Event('resize'));
|
|
208
|
+
for (const { signal, handler } of Array.from(this._handlers.values())) {
|
|
209
|
+
if (signal === 'resize') handler(this, width, height);
|
|
210
|
+
}
|
|
211
|
+
for (const cb of this._resizeCallbacks) cb(width, height);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (customElements.get(TAG) === undefined) customElements.define(TAG, Canvas2DBridgeElement);
|
|
216
|
+
registerTemplateClass(Canvas2DBridgeElement.GTypeName, TAG);
|
|
217
|
+
|
|
218
|
+
export { Canvas2DBridgeElement as Canvas2DBridge };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// @gjsify/adwaita-web/canvas2d — opt-in entry for `Canvas2DBridge` (ADR 0107). Importing this
|
|
2
|
+
// module defines <gjsify-canvas2d-bridge> and registers it as the template class
|
|
3
|
+
// `$GjsifyCanvas2DBridge`, so a `.blp` names it the way it names the GJS widget.
|
|
4
|
+
|
|
5
|
+
export { Canvas2DBridge, Canvas2DBridgeElement } from './canvas2d-bridge.js';
|
|
@@ -25,9 +25,11 @@
|
|
|
25
25
|
// `Adw.AlertDialog:heading-use-markup` and `:body-use-markup` both default to FALSE
|
|
26
26
|
// (`Adw.Banner:use-markup` is the one that defaults to TRUE, which is why
|
|
27
27
|
// `adw-banner.ts` documents a deliberate departure and this file does not need to).
|
|
28
|
-
// Opting in renders through `
|
|
29
|
-
//
|
|
30
|
-
//
|
|
28
|
+
// Opting in renders PANGO MARKUP through `renderPangoMarkup`: the string is PARSED by
|
|
29
|
+
// `@gjsify/adwaita-core` and turned into nodes one at a time, never handed to
|
|
30
|
+
// `innerHTML`. So opting in is no longer an assertion that the string is trusted —
|
|
31
|
+
// `<b>` is drawn bold whether or not the author vouched for it, and a tag pango does
|
|
32
|
+
// not know comes back as text.
|
|
31
33
|
//
|
|
32
34
|
// Buttons are stacked vertically by default (the Adw.AlertDialog default at medium
|
|
33
35
|
// sizes) and laid out horizontally when prefer-wide-layout is set and there are no more
|
|
@@ -56,6 +58,7 @@
|
|
|
56
58
|
import { AdwAlertResponses } from '@gjsify/adwaita-core';
|
|
57
59
|
import type { AdwResponseAppearance } from '@gjsify/adwaita-core';
|
|
58
60
|
|
|
61
|
+
import { renderPangoMarkup } from '../pango-markup.js';
|
|
59
62
|
import { bindEmptySections } from '../empty-sections.js';
|
|
60
63
|
import { bindSlottedChildren } from '../slotted-children.js';
|
|
61
64
|
import { AdwModalSurface } from './modal-surface.js';
|
|
@@ -463,12 +466,12 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
463
466
|
this.classList.toggle('open', this.open);
|
|
464
467
|
|
|
465
468
|
const heading = this.heading;
|
|
466
|
-
if (this.headingUseMarkup) this._headingEl
|
|
469
|
+
if (this.headingUseMarkup) renderPangoMarkup(this._headingEl, heading);
|
|
467
470
|
else this._headingEl.textContent = heading;
|
|
468
471
|
this._headingEl.hidden = heading.length === 0;
|
|
469
472
|
|
|
470
473
|
const body = this.body;
|
|
471
|
-
if (this.bodyUseMarkup) this._bodyEl
|
|
474
|
+
if (this.bodyUseMarkup) renderPangoMarkup(this._bodyEl, body);
|
|
472
475
|
else this._bodyEl.textContent = body;
|
|
473
476
|
this._bodyEl.hidden = body.length === 0;
|
|
474
477
|
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
parseBannerButtonStyle,
|
|
21
21
|
} from '@gjsify/adwaita-core';
|
|
22
22
|
|
|
23
|
+
import { renderPangoMarkup } from '../pango-markup.js';
|
|
24
|
+
|
|
23
25
|
export class AdwBanner extends HTMLElement {
|
|
24
26
|
private _titleEl!: HTMLSpanElement;
|
|
25
27
|
private _button!: HTMLButtonElement;
|
|
@@ -52,17 +54,20 @@ export class AdwBanner extends HTMLElement {
|
|
|
52
54
|
|
|
53
55
|
private _render() {
|
|
54
56
|
const title = this.getAttribute('title') ?? '';
|
|
55
|
-
// DELIBERATE DEPARTURE FROM THE C DEFAULT
|
|
56
|
-
//
|
|
57
|
-
// carries that as the spec
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
57
|
+
// DELIBERATE DEPARTURE FROM THE C DEFAULT, and the departure is now about
|
|
58
|
+
// SAFETY rather than about how markup is spelt. `Adw.Banner:use-markup`
|
|
59
|
+
// defaults to TRUE and `@gjsify/adwaita-core` carries that as the spec
|
|
60
|
+
// value — but a `title` attribute is an ATTRIBUTE, and a renderer that
|
|
61
|
+
// parsed it as markup would be one an author (or an injected value) could
|
|
62
|
+
// drive. Adopting the C default here would make every banner title markup
|
|
63
|
+
// by default for a widget that is not one in GTK, so on this renderer
|
|
64
|
+
// markup is OPT-IN and the default is text.
|
|
65
|
+
// Opting in renders PANGO markup — parsed by `renderPangoMarkup`, never
|
|
66
|
+
// handed to `innerHTML`, so nothing the title carries can become a node
|
|
67
|
+
// beyond the eleven tags pango itself knows.
|
|
63
68
|
// The title label carries `use-underline=False` in the template
|
|
64
69
|
//, so its underscores are literal — no stripping here.
|
|
65
|
-
if (this.hasAttribute('use-markup')) this._titleEl
|
|
70
|
+
if (this.hasAttribute('use-markup')) renderPangoMarkup(this._titleEl, title);
|
|
66
71
|
else this._titleEl.textContent = title;
|
|
67
72
|
|
|
68
73
|
// `label && label[0]` — a first-character test, so a label of
|