react-x11 2.4.0 → 2.6.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/package.json +4 -3
- package/src/Reconciler.js +6 -0
- package/src/cocoa/app.js +216 -14
- package/src/cocoa/context2d.js +37 -4
- package/src/cocoa/panehost.js +20 -2
- package/src/cocoa/panewindow.js +61 -9
- package/src/cocoa/presenter.js +191 -16
- package/src/cocoa/surface.js +235 -0
- package/src/cocoa/window.js +123 -13
- package/src/events.js +23 -5
- package/src/index.d.ts +25 -0
- package/src/nodes.js +849 -138
- package/src/ntk.d.ts +63 -4
- package/src/ntk.js +30 -0
- package/src/paintcache.js +71 -11
- package/src/svgnodes.js +4 -3
- package/src/types/elements.d.ts +13 -0
package/src/cocoa/window.js
CHANGED
|
@@ -26,6 +26,12 @@ export class CocoaWindow {
|
|
|
26
26
|
this._surfaceGen = 0;
|
|
27
27
|
this._ctx = null;
|
|
28
28
|
this._dirty = false;
|
|
29
|
+
// AppKit's occlusion state, as the delegate reports it (`_visible`).
|
|
30
|
+
this._occluded = false;
|
|
31
|
+
// This window's frame clock: when it last painted, and how often it may
|
|
32
|
+
// — the period of the display it is on (`_refreshFrameInterval`).
|
|
33
|
+
this._rafLast = 0;
|
|
34
|
+
this._frameInterval = 0;
|
|
29
35
|
|
|
30
36
|
const s = this.scale;
|
|
31
37
|
// Snapped to whole POINTS: AppKit rounds window sizes to the point
|
|
@@ -73,8 +79,18 @@ export class CocoaWindow {
|
|
|
73
79
|
this.windowNumber = this._native.windowNumber(this._h);
|
|
74
80
|
this._layer = this._native.windowRootLayer(this._h);
|
|
75
81
|
this._refreshOrigin();
|
|
82
|
+
this._refreshFrameInterval();
|
|
76
83
|
if (attributes.sizeHints) this.setSizeHints(attributes.sizeHints);
|
|
77
84
|
|
|
85
|
+
// How many damage rects a frame may keep before merging them (nodes.js,
|
|
86
|
+
// MAX_DAMAGE_RECTS is the X11 answer). A pass here costs one CoreGraphics
|
|
87
|
+
// clip and a culled walk, where an X pass costs the server a clip mask,
|
|
88
|
+
// so a frame in which a clock, a graph and a status row all ticked keeps
|
|
89
|
+
// the three small rects instead of the box around them — which on a
|
|
90
|
+
// large tree was most of the window, painted for three cells' worth of
|
|
91
|
+
// change.
|
|
92
|
+
this.damageRectCap = 16;
|
|
93
|
+
|
|
78
94
|
// The retained layer presenter (docs/macos.md Tier L), behind
|
|
79
95
|
// REACT_X11_COCOA_PRESENTER=layers while the surface path is the
|
|
80
96
|
// measured default. Its two hooks exist only in this mode, so the
|
|
@@ -115,6 +131,18 @@ export class CocoaWindow {
|
|
|
115
131
|
this._screenOrigin = { x: this.x, y: this.y };
|
|
116
132
|
}
|
|
117
133
|
|
|
134
|
+
/**
|
|
135
|
+
* How often this window may paint: the period of the display it is on,
|
|
136
|
+
* asked of the app (`frameIntervalFor`), which reads the screen list the
|
|
137
|
+
* bridge reported. Re-read whenever the window moves, because a drag
|
|
138
|
+
* from a 120Hz panel to a 60Hz monitor halves the rate it is worth
|
|
139
|
+
* painting at — and a window that straddles two answers for the one
|
|
140
|
+
* under its centre.
|
|
141
|
+
*/
|
|
142
|
+
_refreshFrameInterval() {
|
|
143
|
+
this._frameInterval = this.app.frameIntervalFor(this);
|
|
144
|
+
}
|
|
145
|
+
|
|
118
146
|
/** Native geometry changed (delegate event, points). */
|
|
119
147
|
_nativeResized(points) {
|
|
120
148
|
const s = this.scale;
|
|
@@ -123,6 +151,13 @@ export class CocoaWindow {
|
|
|
123
151
|
this.x = Math.round(points.x * s);
|
|
124
152
|
this.y = Math.round(points.y * s);
|
|
125
153
|
this._screenOrigin = { x: this.x, y: this.y };
|
|
154
|
+
this._refreshFrameInterval();
|
|
155
|
+
// AppKit's inLiveResize, as the delegate reported it: the renderer
|
|
156
|
+
// answers a live tick with the layout floors it has and measures fresh
|
|
157
|
+
// ones after the drag (nodes.js, `_deferContentFloors`). Cleared by
|
|
158
|
+
// the pump (`_endLiveResizes`), because the pump cannot run while the
|
|
159
|
+
// resize loop owns the thread — a tick of it is the drag being over.
|
|
160
|
+
if (points.live === true) this.liveResizing = true;
|
|
126
161
|
}
|
|
127
162
|
|
|
128
163
|
resize(width, height) {
|
|
@@ -144,6 +179,7 @@ export class CocoaWindow {
|
|
|
144
179
|
this.y = Math.round(y);
|
|
145
180
|
this._native.setWindowFrame(this._h, x / s, y / s, null, null);
|
|
146
181
|
this._screenOrigin = { x: this.x, y: this.y };
|
|
182
|
+
this._refreshFrameInterval();
|
|
147
183
|
}
|
|
148
184
|
|
|
149
185
|
// --- lifecycle -----------------------------------------------------------
|
|
@@ -151,10 +187,17 @@ export class CocoaWindow {
|
|
|
151
187
|
map() {
|
|
152
188
|
if (this.destroyed) return;
|
|
153
189
|
this.mapped = true;
|
|
190
|
+
// Showing is a claim that the window is on glass; if it comes up behind
|
|
191
|
+
// another application's window, AppKit's occlusion event says so on the
|
|
192
|
+
// next pump and the frames wait from then. Reset here rather than kept,
|
|
193
|
+
// so a window that was hidden behind one, unmapped and mapped again
|
|
194
|
+
// does not wait on an event that may already have been delivered.
|
|
195
|
+
this._occluded = false;
|
|
154
196
|
// A popup must not take the keyboard from its owner; a toplevel's first
|
|
155
197
|
// map is the app coming up and takes it.
|
|
156
198
|
this._native.showWindow(this._h, !this._popup);
|
|
157
199
|
this._refreshOrigin();
|
|
200
|
+
this._refreshFrameInterval();
|
|
158
201
|
}
|
|
159
202
|
|
|
160
203
|
unmap() {
|
|
@@ -169,7 +212,7 @@ export class CocoaWindow {
|
|
|
169
212
|
this.mapped = false;
|
|
170
213
|
this.app._unregisterWindow(this);
|
|
171
214
|
this._native.destroyWindow2(this._h);
|
|
172
|
-
this.
|
|
215
|
+
this._releaseBacking();
|
|
173
216
|
}
|
|
174
217
|
|
|
175
218
|
// --- window-manager-ish surface (feature-detected by nodes.js) -----------
|
|
@@ -232,6 +275,14 @@ export class CocoaWindow {
|
|
|
232
275
|
* the just-shown frame's damage across — a damage-sized memcpy replacing
|
|
233
276
|
* a window-sized upload. Falls back to the single plain surface where
|
|
234
277
|
* IOSurface creation fails.
|
|
278
|
+
*
|
|
279
|
+
* A new size retires the pair, and the retired pair is released on the
|
|
280
|
+
* spot (`_releaseBacking`): a resize tick allocates two window-sized
|
|
281
|
+
* IOSurfaces, 20MB at 900x700@2x, and left to the handles' finalizers a
|
|
282
|
+
* forty-tick drag held 800MB until a collection happened to run — the
|
|
283
|
+
* `rss +80MB` docs/macos.md measured. The layer keeps its own reference
|
|
284
|
+
* to whichever IOSurface it is still showing, so the free is safe while
|
|
285
|
+
* that frame is on glass.
|
|
235
286
|
*/
|
|
236
287
|
_ensureSurface() {
|
|
237
288
|
const w = this.width;
|
|
@@ -242,7 +293,7 @@ export class CocoaWindow {
|
|
|
242
293
|
this._surfaceSize?.height !== h
|
|
243
294
|
) {
|
|
244
295
|
const hadSurface = Boolean(this._surface);
|
|
245
|
-
this.
|
|
296
|
+
this._releaseBacking();
|
|
246
297
|
try {
|
|
247
298
|
const a = this._native.createSurfaceIOSurface(w, h, this.scale);
|
|
248
299
|
const b = this._native.createSurfaceIOSurface(w, h, this.scale);
|
|
@@ -258,20 +309,41 @@ export class CocoaWindow {
|
|
|
258
309
|
this._surfaceSize = { width: w, height: h };
|
|
259
310
|
this._surfaceGen++;
|
|
260
311
|
this._flushDamage = 'full';
|
|
261
|
-
// A replaced backing surface holds nothing
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
312
|
+
// A replaced backing surface holds nothing but what the flush now
|
|
313
|
+
// painting puts on it. Whether that is enough is decided when the
|
|
314
|
+
// flush reports its rects (`noteFrameDamage`) — not here, and not by
|
|
315
|
+
// queueing a full frame behind this one. That used to be the answer,
|
|
316
|
+
// and it made every tick of a live resize two full frames: the resize
|
|
317
|
+
// event's own unbounded repaint, then this one, painting the same
|
|
318
|
+
// pixels again. Worse, inside AppKit's resize loop no microtask runs
|
|
319
|
+
// until the drag ends, so a drag of forty ticks queued forty full
|
|
320
|
+
// frames that all ran on the mouse release — the freeze after a
|
|
321
|
+
// resize, measured at seconds on a large tree.
|
|
322
|
+
if (hadSurface) this._freshSurface = true;
|
|
271
323
|
}
|
|
272
324
|
return this._surface;
|
|
273
325
|
}
|
|
274
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Free the backing store now — the swapchain pair, or the plain surface
|
|
329
|
+
* the fallback holds — rather than when V8 collects the handles. Bridges
|
|
330
|
+
* before 0.4 have no `releaseSurface`; there the finalizer is still the
|
|
331
|
+
* only owner, and this is the drop it always was.
|
|
332
|
+
*/
|
|
333
|
+
_releaseBacking() {
|
|
334
|
+
const release = this._native.releaseSurface;
|
|
335
|
+
if (typeof release === 'function') {
|
|
336
|
+
if (this._chain) {
|
|
337
|
+
release.call(this._native, this._chain.back.handle);
|
|
338
|
+
release.call(this._native, this._chain.front.handle);
|
|
339
|
+
} else if (this._surface) {
|
|
340
|
+
release.call(this._native, this._surface);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
this._chain = null;
|
|
344
|
+
this._surface = null;
|
|
345
|
+
}
|
|
346
|
+
|
|
275
347
|
/**
|
|
276
348
|
* The per-flush painted rects (nodes.js's swapchain seam), accumulated
|
|
277
349
|
* until the next present: they are what the flip's catch-up copy covers.
|
|
@@ -279,6 +351,22 @@ export class CocoaWindow {
|
|
|
279
351
|
*/
|
|
280
352
|
noteFrameDamage(rects) {
|
|
281
353
|
if (this._presenter) return;
|
|
354
|
+
if (this._freshSurface) {
|
|
355
|
+
this._freshSurface = false;
|
|
356
|
+
// A full flush painted every pixel of the new surface, and a resize
|
|
357
|
+
// event's flush is one (nodes.js, the 'resize' listener). A bounded
|
|
358
|
+
// one left garbage outside its rects: hold the present until the full
|
|
359
|
+
// frame asked for here lands, so the garbage is never on glass.
|
|
360
|
+
if (rects) {
|
|
361
|
+
this._holdPresent = true;
|
|
362
|
+
const node = this._reactX11Node;
|
|
363
|
+
if (node && !node.destroyed) node.invalidate(false, null, 'resize');
|
|
364
|
+
} else {
|
|
365
|
+
this._holdPresent = false;
|
|
366
|
+
}
|
|
367
|
+
} else if (!rects) {
|
|
368
|
+
this._holdPresent = false;
|
|
369
|
+
}
|
|
282
370
|
if (this._flushDamage === 'full') return;
|
|
283
371
|
if (!rects) {
|
|
284
372
|
this._flushDamage = 'full';
|
|
@@ -287,6 +375,25 @@ export class CocoaWindow {
|
|
|
287
375
|
(this._flushDamage ??= []).push(...rects);
|
|
288
376
|
}
|
|
289
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Whether anyone can see this window: mapped by the renderer, and not
|
|
380
|
+
* ordered out or miniaturized by the user. A window that fails this owes
|
|
381
|
+
* no frames — its callbacks wait in the app's queue (`_tickFrames`) and
|
|
382
|
+
* its last paint stays unpresented until it is back on glass, where one
|
|
383
|
+
* catch-up frame covers everything that changed in between.
|
|
384
|
+
*
|
|
385
|
+
* Occlusion by another application's window counts too: a window that
|
|
386
|
+
* is entirely behind one is visible by `isVisible`'s measure and still
|
|
387
|
+
* costs every frame its tree produces, and AppKit knows the difference.
|
|
388
|
+
* `windowDidChangeOcclusionState` arrives as the bridge's
|
|
389
|
+
* `window-occlusion` event (`CocoaApp._routeOcclusion`), `visible` being
|
|
390
|
+
* "some pixel of it is on glass"; `_occluded` is the last word of it.
|
|
391
|
+
*/
|
|
392
|
+
_visible() {
|
|
393
|
+
if (this.destroyed || !this.mapped || this._occluded) return false;
|
|
394
|
+
return this._native.windowIsVisible(this._h) !== false;
|
|
395
|
+
}
|
|
396
|
+
|
|
290
397
|
getContext() {
|
|
291
398
|
if (!this._ctx) {
|
|
292
399
|
this._ctx = new CocoaContext2D(
|
|
@@ -345,13 +452,16 @@ export class CocoaWindow {
|
|
|
345
452
|
}
|
|
346
453
|
|
|
347
454
|
requestAnimationFrame(cb) {
|
|
348
|
-
return this.app._requestFrame(cb);
|
|
455
|
+
return this.app._requestFrame(cb, this);
|
|
349
456
|
}
|
|
350
457
|
|
|
351
458
|
/** Push the backing surface at the WindowServer, if anything drew. */
|
|
352
459
|
present() {
|
|
353
460
|
if (this._presenter) return; // layers upload as they sync
|
|
354
461
|
if (!this._dirty || !this._surface || this.destroyed) return;
|
|
462
|
+
// …and if anyone would see it. `_dirty` stays set, so the pump asks
|
|
463
|
+
// again next tick and the frame goes out the moment the window is back.
|
|
464
|
+
if (this._holdPresent || !this._visible()) return;
|
|
355
465
|
this._dirty = false;
|
|
356
466
|
if (this._chain) {
|
|
357
467
|
const shown = this._chain.back;
|
package/src/events.js
CHANGED
|
@@ -78,7 +78,16 @@ class SyntheticEvent {
|
|
|
78
78
|
// by, and `localX` below subtracts an `abs` divided the same way
|
|
79
79
|
// (src/scale.js). Everything *internal* — hit testing, drag
|
|
80
80
|
// thresholds, the scroll accumulator — keeps reading `native`.
|
|
81
|
-
|
|
81
|
+
//
|
|
82
|
+
// **The target's** unit, not the window's, so that a subtree zoomed by
|
|
83
|
+
// a `scale` prop reads its own: `ev.x` then compares with the target's
|
|
84
|
+
// `getClientRects()`, which divides by the same factor, and `localX`
|
|
85
|
+
// lands on the styles that node was written with. This is CSS `zoom`'s
|
|
86
|
+
// trade-off rather than a transform's, and the corner it costs is
|
|
87
|
+
// named in docs/scale.md — a handler on an *unzoomed* ancestor reads a
|
|
88
|
+
// coordinate in the zoomed descendant's unit, because the target is
|
|
89
|
+
// what decides. `nativeEvent` is the way back to the window's pixels.
|
|
90
|
+
const s = target?.scale ?? manager.scale;
|
|
82
91
|
this._manager = manager;
|
|
83
92
|
this._targetNode = target;
|
|
84
93
|
this.type = type;
|
|
@@ -802,8 +811,14 @@ export class EventManager {
|
|
|
802
811
|
// `ev.deltaX/Y` are logical (what handlers read); the scroll they
|
|
803
812
|
// become moves device pixels, and truncating *after* the multiply is
|
|
804
813
|
// what keeps the blit on whole device pixels at fractional scales.
|
|
805
|
-
|
|
806
|
-
|
|
814
|
+
// The target's scale, the same unit the event's coordinates are in
|
|
815
|
+
// (`SyntheticEvent`), so a notch over a subtree zoomed by a `scale`
|
|
816
|
+
// prop moves a notch of *its* pixels — the content under the pointer
|
|
817
|
+
// travels the distance the zoom says, which is what CSS `zoom` does
|
|
818
|
+
// and what a pane whose rows are twice the size needs.
|
|
819
|
+
const wheelScale = target.scale;
|
|
820
|
+
const owedX = this._wheelOwed.x + ev.deltaX * wheelScale;
|
|
821
|
+
const owedY = this._wheelOwed.y + ev.deltaY * wheelScale;
|
|
807
822
|
const dx = Math.trunc(owedX);
|
|
808
823
|
const dy = Math.trunc(owedY);
|
|
809
824
|
this._wheelOwed = { x: owedX - dx, y: owedY - dy };
|
|
@@ -822,9 +837,12 @@ export class EventManager {
|
|
|
822
837
|
for (let n = target; n; n = n.parent) {
|
|
823
838
|
if (n.canScroll?.(dx, dy)) {
|
|
824
839
|
// built-in scrollers take the device delta whole; a registered
|
|
825
|
-
// element's own scrollBy speaks the public (logical) unit
|
|
840
|
+
// element's own scrollBy speaks the public (logical) unit — and
|
|
841
|
+
// *that node's* logical unit, since `scrollBy` multiplies by its
|
|
842
|
+
// own scale on the way back in, which is the only division that
|
|
843
|
+
// round-trips exactly across a scale boundary
|
|
826
844
|
if (n._scrollByDevice) n._scrollByDevice(dx, dy);
|
|
827
|
-
else n.scrollBy({ x: dx /
|
|
845
|
+
else n.scrollBy({ x: dx / n.scale, y: dy / n.scale });
|
|
828
846
|
break;
|
|
829
847
|
}
|
|
830
848
|
if (n === this.node) break;
|
package/src/index.d.ts
CHANGED
|
@@ -180,6 +180,31 @@ export interface RootOptions {
|
|
|
180
180
|
* is a backend.
|
|
181
181
|
*/
|
|
182
182
|
backend?: 'auto' | 'x11' | 'cocoa';
|
|
183
|
+
/**
|
|
184
|
+
* The Cocoa backend's knobs (docs/macos.md). `presenter` picks the frame
|
|
185
|
+
* path: `'surface'` (the measured default — one bitmap per window, the
|
|
186
|
+
* X11 paint machinery over an IOSurface swapchain) or `'layers'` (one
|
|
187
|
+
* CALayer per drawn node, opt-in while it is measured).
|
|
188
|
+
* `frameInterval` is how often a scheduled frame may paint, in ms. By
|
|
189
|
+
* default each window paces itself on the display it is on — 8.3ms on
|
|
190
|
+
* a 120Hz panel, 16.7 on a 60Hz monitor, the screen's own refresh rate
|
|
191
|
+
* as the bridge reports it, 16 where the OS cannot say. A number here
|
|
192
|
+
* applies to every window instead. `pumpInterval` is the AppKit event
|
|
193
|
+
* pump's cadence, in ms (8 by default), which is the floor under input
|
|
194
|
+
* latency. Ignored off macOS and when {@link RootOptions.app} is passed.
|
|
195
|
+
*/
|
|
196
|
+
cocoa?: {
|
|
197
|
+
presenter?: 'surface' | 'layers';
|
|
198
|
+
frameInterval?: number;
|
|
199
|
+
pumpInterval?: number;
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* The size, in logical pixels, under which a `<text>` is painted as a
|
|
203
|
+
* strip of its ink where its lines are instead of as glyphs — a
|
|
204
|
+
* zoomed-out view's labels, which nobody can read and which cost a glyph
|
|
205
|
+
* run each. 6 by default; 0 paints glyphs at every size.
|
|
206
|
+
*/
|
|
207
|
+
textStripBelow?: number;
|
|
183
208
|
/** `':1'`, `'host:0.0'`, or a unix socket path. Defaults to `$DISPLAY`. */
|
|
184
209
|
display?: string;
|
|
185
210
|
/**
|