react-x11 2.2.1 → 2.3.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,236 @@
1
+ // The pane process's "window" on the Cocoa backend — docs/frame.md's child
2
+ // half, with the X server replaced by shared memory.
3
+ //
4
+ // On X11 a pane renders into a real window of its own and the host embeds
5
+ // it; here there is no server to share, so the pane paints into IOSurfaces
6
+ // created shared (kIOSurfaceIsGlobal) and presents by message: the host
7
+ // looks the id up and points the pane's sublayer at it. Same swapchain
8
+ // discipline as CocoaWindow — draw into back, flip, catch the new back up
9
+ // by the damage — because the host's WindowServer reads the shown buffer
10
+ // asynchronously either way.
11
+ //
12
+ // What the node tree sees is the ordinary window contract: getContext,
13
+ // present, scrollRegion, noteFrameDamage, requestAnimationFrame, events
14
+ // via emit. Geometry and input arrive as channel messages (the host owns
15
+ // layout and hit-testing — this is CPU offloading, not isolation), and the
16
+ // only outbound traffic is pane-present.
17
+ import { CocoaContext2D } from './context2d.js';
18
+
19
+ let nextPaneId = 1;
20
+
21
+ export class CocoaPaneWindow {
22
+ constructor(app, attributes = {}) {
23
+ this.app = app;
24
+ this._native = app._native;
25
+ this.destroyed = false;
26
+ // a real-looking id so the shared ready/handshake path (which polls
27
+ // windowIdOf) fires; the host on this backend never dereferences it
28
+ this.windowId = 0xc0c0a000 + nextPaneId++;
29
+ // windowIdOf reads `.id` (the ntk Window field) — the ready handshake
30
+ // polls it through the pane's ref, so both spellings answer
31
+ this.id = this.windowId;
32
+ this.windowNumber = this.windowId;
33
+ this.scale = app.scale ?? 2;
34
+ this.width = Math.max(
35
+ 1,
36
+ Math.round((attributes.width ?? 400) * this.scale),
37
+ );
38
+ this.height = Math.max(
39
+ 1,
40
+ Math.round((attributes.height ?? 300) * this.scale),
41
+ );
42
+ this._listeners = new Map();
43
+ this._surface = null;
44
+ this._surfaceGen = 0;
45
+ this._surfaceSize = null;
46
+ this._ring = null;
47
+ this._drawIndex = 0;
48
+ this._shownIndex = -1;
49
+ this._ctx = null;
50
+ this._dirty = false;
51
+ this._flushDamage = 'full';
52
+ this._seq = 0;
53
+ this._reactX11Node = null;
54
+ app._registerWindow(this);
55
+ }
56
+
57
+ // --- the channel-facing half --------------------------------------------
58
+
59
+ /** The host's layout answer: logical size plus the display scale. */
60
+ setPaneSize(width, height, scale) {
61
+ if (this.destroyed) return;
62
+ if (scale) this.scale = scale;
63
+ const w = Math.max(1, Math.round(width * this.scale));
64
+ const h = Math.max(1, Math.round(height * this.scale));
65
+ if (w === this.width && h === this.height) return;
66
+ this.width = w;
67
+ this.height = h;
68
+ this.emit('resize', {
69
+ width: w,
70
+ height: h,
71
+ x: 0,
72
+ y: 0,
73
+ moved: false,
74
+ resized: true,
75
+ });
76
+ }
77
+
78
+ // --- the window contract -------------------------------------------------
79
+
80
+ on(name, fn) {
81
+ let set = this._listeners.get(name);
82
+ if (!set) this._listeners.set(name, (set = new Set()));
83
+ set.add(fn);
84
+ return () => set.delete(fn);
85
+ }
86
+
87
+ emit(name, ev) {
88
+ for (const fn of [...(this._listeners.get(name) ?? [])]) fn(ev);
89
+ }
90
+
91
+ map() {}
92
+
93
+ focus() {}
94
+
95
+ setTitle() {}
96
+
97
+ requestAnimationFrame(cb) {
98
+ return this.app._requestFrame(cb);
99
+ }
100
+
101
+ frameInFlight() {
102
+ return false;
103
+ }
104
+
105
+ // Three buffers, not two. A pane is cross-process: the host keeps
106
+ // scanning the last buffer it was handed until it processes the next
107
+ // present message, and nothing here waits for that. With two buffers the
108
+ // next paint (and the catch-up copy) lands in the very buffer the host is
109
+ // still displaying — every present tears it, the flash. A third buffer is
110
+ // always at least two presents behind what the host shows, so the pane
111
+ // never writes a buffer the host might still be reading. Same-process
112
+ // windows need only two because Core Animation latches the front buffer.
113
+ static RING = 3;
114
+
115
+ _ensureSurface() {
116
+ const w = this.width;
117
+ const h = this.height;
118
+ if (
119
+ !this._ring ||
120
+ this._surfaceSize?.width !== w ||
121
+ this._surfaceSize?.height !== h
122
+ ) {
123
+ const hadSurface = Boolean(this._ring);
124
+ this._ring = [];
125
+ for (let i = 0; i < CocoaPaneWindow.RING; i += 1) {
126
+ const s = this._native.createSurfaceIOSurface(w, h, this.scale, true);
127
+ this._native.ctxClearRect(s.handle, 0, 0, w, h);
128
+ this._ring.push(s);
129
+ }
130
+ this._drawIndex = 0;
131
+ this._shownIndex = -1;
132
+ this._native.surfaceLock(this._ring[0].handle);
133
+ this._surface = this._ring[0].handle;
134
+ this._surfaceSize = { width: w, height: h };
135
+ this._surfaceGen++;
136
+ this._flushDamage = 'full';
137
+ if (hadSurface) {
138
+ queueMicrotask(() => {
139
+ const node = this._reactX11Node;
140
+ if (node && !node.destroyed) node.invalidate(true, null, 'resize');
141
+ });
142
+ }
143
+ }
144
+ return this._surface;
145
+ }
146
+
147
+ getContext() {
148
+ if (!this._ctx) {
149
+ this._ctx = new CocoaContext2D(
150
+ this._native,
151
+ () => this._ensureSurface(),
152
+ () => {
153
+ this._ensureSurface();
154
+ return this._surfaceGen;
155
+ },
156
+ );
157
+ this._ctx._fonts = this.app.fonts;
158
+ this._ctx._onDirty = () => {
159
+ this._dirty = true;
160
+ };
161
+ }
162
+ return this._ctx;
163
+ }
164
+
165
+ noteFrameDamage(rects) {
166
+ if (this._flushDamage === 'full') return;
167
+ if (!rects) {
168
+ this._flushDamage = 'full';
169
+ return;
170
+ }
171
+ (this._flushDamage ??= []).push(...rects);
172
+ }
173
+
174
+ scrollRegion(rect, dx, dy) {
175
+ if (!this._surface) return false;
176
+ if (!Number.isInteger(dx) || !Number.isInteger(dy)) return false;
177
+ const moved = this._native.scrollSurface(
178
+ this._surface,
179
+ Math.round(rect.x),
180
+ Math.round(rect.y),
181
+ Math.round(rect.width),
182
+ Math.round(rect.height),
183
+ dx,
184
+ dy,
185
+ );
186
+ if (moved) this._dirty = true;
187
+ return Boolean(moved);
188
+ }
189
+
190
+ /** Flip and tell the host, instead of touching any layer of our own. */
191
+ present() {
192
+ if (!this._dirty || !this._ring || this.destroyed) return;
193
+ this._dirty = false;
194
+ const shown = this._ring[this._drawIndex];
195
+ this._native.surfaceUnlock(shown.handle);
196
+ const damage = this._flushDamage;
197
+ this._flushDamage = null;
198
+ this.app._paneSend?.({
199
+ type: 'pane-present',
200
+ seq: ++this._seq,
201
+ id: shown.iosurfaceId,
202
+ width: this.width,
203
+ height: this.height,
204
+ });
205
+ this._shownIndex = this._drawIndex;
206
+ // the next buffer round the ring — two behind what the host will be
207
+ // showing, so it is safe to write even before the host has switched
208
+ this._drawIndex = (this._drawIndex + 1) % CocoaPaneWindow.RING;
209
+ const next = this._ring[this._drawIndex];
210
+ this._surface = next.handle;
211
+ this._surfaceGen++;
212
+ this._native.surfaceLock(next.handle);
213
+ // A full-repaint frame overwrites everything next, so no catch-up is
214
+ // needed. A partial frame paints only its damage, so `next` must first
215
+ // hold the last complete frame underneath — and the WHOLE of it, not
216
+ // just this frame's rects, because in a three-buffer ring `next` was
217
+ // last drawn two presents ago and is stale everywhere. A full copy of
218
+ // the just-shown buffer is the complete background; the safe target is
219
+ // what triple buffering buys.
220
+ if (damage !== 'full' && damage) {
221
+ this._native.copySurfaceRegion(shown.handle, next.handle, null);
222
+ }
223
+ }
224
+
225
+ snapshot() {
226
+ return false; // no window of our own to capture; the host's shows it
227
+ }
228
+
229
+ destroy() {
230
+ if (this.destroyed) return;
231
+ this.destroyed = true;
232
+ this.app._unregisterWindow(this);
233
+ this._ring = null;
234
+ this._surface = null;
235
+ }
236
+ }