@gjsify/adwaita-web 0.61.0 → 0.62.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.
@@ -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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/adwaita-web",
3
- "version": "0.61.0",
3
+ "version": "0.62.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.61.0",
58
- "@gjsify/adwaita-fonts": "^0.61.0",
59
- "@gjsify/adwaita-icons": "^0.61.0",
60
- "@gjsify/gtksource-core": "^0.61.0",
61
+ "@gjsify/adwaita-core": "^0.62.0",
62
+ "@gjsify/adwaita-fonts": "^0.62.0",
63
+ "@gjsify/adwaita-icons": "^0.62.0",
64
+ "@gjsify/gtksource-core": "^0.62.0",
61
65
  "@lezer/highlight": "^1.2.3"
62
66
  },
63
67
  "peerDependencies": {
64
- "@gjsify/app-settings": "^0.61.0"
68
+ "@gjsify/app-settings": "^0.62.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.61.0",
73
- "@gjsify/blueprint": "^0.61.0",
74
- "@gjsify/cli": "^0.61.0",
75
- "@gjsify/unit": "^0.61.0",
76
- "@gjsify/vite-plugin-blueprint": "^0.61.0",
76
+ "@gjsify/app-settings": "^0.62.0",
77
+ "@gjsify/blueprint": "^0.62.0",
78
+ "@gjsify/cli": "^0.62.0",
79
+ "@gjsify/unit": "^0.62.0",
80
+ "@gjsify/vite-plugin-blueprint": "^0.62.0",
77
81
  "sass": "^1.101.0",
78
82
  "typescript": "^6.0.3"
79
83
  },
@@ -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';