react-x11 2.17.0 → 2.18.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 +1 -1
- package/src/cocoa/app.js +9 -5
- package/src/cocoa/glarea.js +48 -10
- package/src/cocoa/window.js +21 -0
- package/src/events.js +3 -1
- package/src/glnodes.js +19 -9
- package/src/nodes/hittest.js +5 -2
- package/src/types/style.d.ts +8 -1
package/package.json
CHANGED
package/src/cocoa/app.js
CHANGED
|
@@ -41,7 +41,7 @@ import { CocoaFontManager } from './fonts.js';
|
|
|
41
41
|
import { releaseImageUpload } from '../backend/context2d.js';
|
|
42
42
|
import { CocoaSurface } from './surface.js';
|
|
43
43
|
import { CocoaSymbols } from './symbols.js';
|
|
44
|
-
import { CocoaWindow } from './window.js';
|
|
44
|
+
import { CocoaWindow, FRAME_SLACK_MS } from './window.js';
|
|
45
45
|
import { decodeKey, modifierMask } from './keymap.js';
|
|
46
46
|
import { loadNative } from './native.js';
|
|
47
47
|
import { requestAppKit, threadedChannel } from './threaded.js';
|
|
@@ -55,9 +55,6 @@ import { requestAppKit, threadedChannel } from './threaded.js';
|
|
|
55
55
|
// bridge that does not report it: a 60Hz floor every Mac clears.
|
|
56
56
|
const RAF_INTERVAL_MS = 16;
|
|
57
57
|
const PUMP_INTERVAL_MS = 8;
|
|
58
|
-
// How early a pump tick may take a frame that is not quite due, in ms — the
|
|
59
|
-
// drift of a timer, not a fraction of the pump (`_frameDue`).
|
|
60
|
-
const FRAME_SLACK_MS = 1;
|
|
61
58
|
// Threaded mode's live-resize handshake (windowkit/appkit#53): how long
|
|
62
59
|
// AppKit may hold a resize tick for a frame painted at the new size, in ms.
|
|
63
60
|
// The edge moves when the delegate returns, so the budget is how long a
|
|
@@ -906,6 +903,13 @@ export class CocoaApp {
|
|
|
906
903
|
* tick alone still does is pump AppKit's events, which is what
|
|
907
904
|
* `pumpInterval` stays the cadence of. With no pump at all — threaded
|
|
908
905
|
* mode, where `_pumpInterval` is Infinity — every frame is one of these.
|
|
906
|
+
*
|
|
907
|
+
* The wait is rounded **up** to whole milliseconds, because `setTimeout`
|
|
908
|
+
* only counts those: a 120Hz panel's 8.333ms gate rounded to 8 fired
|
|
909
|
+
* before the clock would take the frame, which cost a second timer per
|
|
910
|
+
* frame to cover the last third of a millisecond. Rounding up costs the
|
|
911
|
+
* grid nothing — `_frameDue` anchors the next slot to the last one, not
|
|
912
|
+
* to when the frame ran — and lands on the first tick the clock accepts.
|
|
909
913
|
*/
|
|
910
914
|
_armFrameTimer(wait, now) {
|
|
911
915
|
if (!(wait > 0 && wait < this._pumpInterval)) return;
|
|
@@ -922,7 +926,7 @@ export class CocoaApp {
|
|
|
922
926
|
this._tickFrames();
|
|
923
927
|
this._presentAll();
|
|
924
928
|
},
|
|
925
|
-
Math.max(1, Math.
|
|
929
|
+
Math.max(1, Math.ceil(wait)),
|
|
926
930
|
);
|
|
927
931
|
}
|
|
928
932
|
|
package/src/cocoa/glarea.js
CHANGED
|
@@ -231,6 +231,16 @@ export class CocoaGLArea {
|
|
|
231
231
|
return this.parent.requestAnimationFrame(cb);
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
/**
|
|
235
|
+
* The owning window's clock, asked where a gate of `interval` ms lands
|
|
236
|
+
* on its grid (`CocoaWindow.nextFrameAt`) — the swap gate phase-locks to
|
|
237
|
+
* it so the two do not stack. A parent with no clock (a bare stub in a
|
|
238
|
+
* test) answers one interval from now, the old swap-relative wait.
|
|
239
|
+
*/
|
|
240
|
+
nextFrameAt(interval) {
|
|
241
|
+
return this.parent.nextFrameAt?.(interval) ?? performance.now() + interval;
|
|
242
|
+
}
|
|
243
|
+
|
|
234
244
|
getContext(kind, config) {
|
|
235
245
|
if (kind !== 'opengl' || this.destroyed) return null;
|
|
236
246
|
if (!this._context) {
|
|
@@ -262,7 +272,10 @@ function createGLAreaContext(runtime, area, config) {
|
|
|
262
272
|
let back = null;
|
|
263
273
|
let width = 0;
|
|
264
274
|
let height = 0;
|
|
265
|
-
|
|
275
|
+
// The moment the swap gate reopens, on the window clock's grid — not a
|
|
276
|
+
// boolean a timer flips, so a frame that arrives a fraction of a
|
|
277
|
+
// millisecond before its own timer still reads the true state.
|
|
278
|
+
let gateOpenAt = -Infinity;
|
|
266
279
|
let gateTimer = null;
|
|
267
280
|
let destroyed = false;
|
|
268
281
|
|
|
@@ -289,7 +302,17 @@ function createGLAreaContext(runtime, area, config) {
|
|
|
289
302
|
// one, so the honest gate is one display period per swap — the same
|
|
290
303
|
// timer-reopened gate the XQuartz CGL flavor uses (no backpressure
|
|
291
304
|
// exists on either).
|
|
292
|
-
|
|
305
|
+
//
|
|
306
|
+
// One period *per frame*, though, not per swap: timed from the swap the
|
|
307
|
+
// gate reopened at `slot + drawCost + period`, which overshoots the
|
|
308
|
+
// window clock's next slot however cheap the frame, and the clock then
|
|
309
|
+
// rounded it up to the slot after — two display periods, a 120Hz panel
|
|
310
|
+
// pinned at 60fps whatever the scene cost (issue #631). Anchored to the
|
|
311
|
+
// slot the frame was due at (`CocoaWindow.nextFrameAt`) the two gates
|
|
312
|
+
// are on one grid: the clock cannot take a frame the gate would refuse,
|
|
313
|
+
// because both are the same instant, and the gate still never lets two
|
|
314
|
+
// frames into one period.
|
|
315
|
+
ctx.canRender = (now = performance.now()) => !destroyed && now >= gateOpenAt;
|
|
293
316
|
|
|
294
317
|
ctx.makeCurrent = () => {
|
|
295
318
|
if (destroyed) return;
|
|
@@ -305,6 +328,26 @@ function createGLAreaContext(runtime, area, config) {
|
|
|
305
328
|
runtime.gl.bindFramebuffer(target, fb == null ? (back?.fbo ?? 0) : fb);
|
|
306
329
|
};
|
|
307
330
|
|
|
331
|
+
// A frame refused by the gate has nothing else to wake it, so the gate
|
|
332
|
+
// says when it opens. `setTimeout` counts whole milliseconds and a
|
|
333
|
+
// display period is rarely one (8.333 at 120Hz), so a timer that lands
|
|
334
|
+
// early re-arms rather than announcing a gate that is still shut.
|
|
335
|
+
const armGate = () => {
|
|
336
|
+
if (gateTimer || destroyed) return;
|
|
337
|
+
const wait = gateOpenAt - performance.now();
|
|
338
|
+
gateTimer = setTimeout(
|
|
339
|
+
() => {
|
|
340
|
+
gateTimer = null;
|
|
341
|
+
if (destroyed) return;
|
|
342
|
+
if (performance.now() < gateOpenAt) armGate();
|
|
343
|
+
else ctx.onFrameAvailable?.();
|
|
344
|
+
},
|
|
345
|
+
Math.max(1, Math.ceil(wait)),
|
|
346
|
+
);
|
|
347
|
+
// the timer must not hold the process open for an idle scene
|
|
348
|
+
gateTimer.unref?.();
|
|
349
|
+
};
|
|
350
|
+
|
|
308
351
|
ctx.SwapBuffers = () => {
|
|
309
352
|
if (destroyed || !back) return;
|
|
310
353
|
runtime.gl.flush();
|
|
@@ -312,14 +355,9 @@ function createGLAreaContext(runtime, area, config) {
|
|
|
312
355
|
const shown = back;
|
|
313
356
|
back = front;
|
|
314
357
|
front = shown;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
gateClosed = false;
|
|
319
|
-
ctx.onFrameAvailable?.();
|
|
320
|
-
}, runtime.frameInterval);
|
|
321
|
-
// the timer must not hold the process open for an idle scene
|
|
322
|
-
gateTimer.unref?.();
|
|
358
|
+
// one display period from the frame's slot, not from this moment
|
|
359
|
+
gateOpenAt = area.nextFrameAt(runtime.frameInterval);
|
|
360
|
+
armGate();
|
|
323
361
|
};
|
|
324
362
|
|
|
325
363
|
ctx._resized = () => {
|
package/src/cocoa/window.js
CHANGED
|
@@ -12,6 +12,11 @@ import { CocoaLayerPresenter } from './presenter.js';
|
|
|
12
12
|
import { CocoaPromotion } from './promotion.js';
|
|
13
13
|
|
|
14
14
|
let nextWindowId = 1;
|
|
15
|
+
// The slack a frame clock allows under its interval, in ms: a timer's drift
|
|
16
|
+
// and no more (`CocoaApp._frameDue`, `_frameWait`, `nextFrameAt`). It lives
|
|
17
|
+
// here because the window is the clock — `_rafLast` and `_frameInterval`
|
|
18
|
+
// are its fields — and the app imports it from this side.
|
|
19
|
+
export const FRAME_SLACK_MS = 1;
|
|
15
20
|
// How long a worker's flip may hold its window's next frame (`_armFence`).
|
|
16
21
|
// The release is reported once the replacing frame has committed, a
|
|
17
22
|
// fraction of a millisecond later, and a window must not freeze on a report
|
|
@@ -910,6 +915,22 @@ export class CocoaWindow {
|
|
|
910
915
|
return this.app._requestFrame(cb, this);
|
|
911
916
|
}
|
|
912
917
|
|
|
918
|
+
/**
|
|
919
|
+
* The earliest moment this window's clock will hand out another frame,
|
|
920
|
+
* for a gate of `interval` ms — the display's period by default, which
|
|
921
|
+
* is the clock's own.
|
|
922
|
+
*
|
|
923
|
+
* It is the grid `_frameDue` decides on, not the wall clock: the anchor
|
|
924
|
+
* is the slot the running frame was *due* at, so the answer does not
|
|
925
|
+
* move with how long that frame took or how late its timer fired. A
|
|
926
|
+
* second gate that wants to compose with this clock rather than be
|
|
927
|
+
* rounded up by it has to land on this grid — which is what a
|
|
928
|
+
* `<glarea>`'s swap gate reads it for (src/cocoa/glarea.js).
|
|
929
|
+
*/
|
|
930
|
+
nextFrameAt(interval = this._frameInterval) {
|
|
931
|
+
return this._rafLast + interval - FRAME_SLACK_MS;
|
|
932
|
+
}
|
|
933
|
+
|
|
913
934
|
/**
|
|
914
935
|
* Push the backing surface at the WindowServer, if anything drew — and
|
|
915
936
|
* tell the window node what it cost. The flip itself is cheap, and the
|
package/src/events.js
CHANGED
|
@@ -1270,7 +1270,9 @@ export class EventManager {
|
|
|
1270
1270
|
// arrive
|
|
1271
1271
|
if (!rect?.width || !rect.height) continue;
|
|
1272
1272
|
if (node.hidden || node.style.display === 'none') continue;
|
|
1273
|
-
|
|
1273
|
+
// neither value lets the pointer arrive at the node itself
|
|
1274
|
+
const pe = node.style.pointerEvents;
|
|
1275
|
+
if (pe === 'none' || pe === 'box-none') continue;
|
|
1274
1276
|
const eta =
|
|
1275
1277
|
speed < ATTENTION_MIN_SPEED
|
|
1276
1278
|
? attentionEta(native.x, native.y, 0, 0, rect)
|
package/src/glnodes.js
CHANGED
|
@@ -376,18 +376,21 @@ export class GlAreaNode extends Node {
|
|
|
376
376
|
typeof this.window.requestAnimationFrame === 'function'
|
|
377
377
|
? (cb) => this.window.requestAnimationFrame(cb)
|
|
378
378
|
: (cb) => setImmediate(cb);
|
|
379
|
-
|
|
379
|
+
// the frame clock's own timestamp, not a fresh reading: a gate that
|
|
380
|
+
// composes with that clock is answered from the same moment it is
|
|
381
|
+
// (`canRender` on the Cocoa surface, issue #631)
|
|
382
|
+
schedule((now) => {
|
|
380
383
|
this._frameScheduled = false;
|
|
381
|
-
this._drawFrame();
|
|
384
|
+
this._drawFrame(now);
|
|
382
385
|
});
|
|
383
386
|
}
|
|
384
387
|
|
|
385
|
-
_drawFrame() {
|
|
388
|
+
_drawFrame(now) {
|
|
386
389
|
const pacer = this._pacer;
|
|
387
390
|
pacer.began();
|
|
388
391
|
let drawn = false;
|
|
389
392
|
try {
|
|
390
|
-
drawn = this._drawFrameNow();
|
|
393
|
+
drawn = this._drawFrameNow(now);
|
|
391
394
|
} finally {
|
|
392
395
|
pacer.ended(undefined, drawn);
|
|
393
396
|
}
|
|
@@ -397,14 +400,14 @@ export class GlAreaNode extends Node {
|
|
|
397
400
|
}
|
|
398
401
|
|
|
399
402
|
/** The frame itself; true when it drew. */
|
|
400
|
-
_drawFrameNow() {
|
|
403
|
+
_drawFrameNow(now) {
|
|
401
404
|
const gl = this.gl;
|
|
402
405
|
if (!gl || this.destroyed) return false;
|
|
403
406
|
const direct = gl.backend === 'direct';
|
|
404
407
|
// On the direct backend every buffer may still be held by the display,
|
|
405
408
|
// and drawing into one before it comes back would paint what is on
|
|
406
409
|
// screen. `onFrameAvailable` asks for this frame again when one frees.
|
|
407
|
-
if (direct && gl.canRender && !gl.canRender()) {
|
|
410
|
+
if (direct && gl.canRender && !gl.canRender(now)) {
|
|
408
411
|
this._frameRefused = true;
|
|
409
412
|
return false;
|
|
410
413
|
}
|
|
@@ -415,8 +418,9 @@ export class GlAreaNode extends Node {
|
|
|
415
418
|
const { width, height } = this.rect;
|
|
416
419
|
// x/y are where the node's origin sits in the drawable being drawn
|
|
417
420
|
// into (DrawInfo's contract) — a <glarea> draws into its own X window,
|
|
418
|
-
// so that is the origin.
|
|
419
|
-
|
|
421
|
+
// so that is the origin. width/height are device pixels; `scale` takes
|
|
422
|
+
// them back to logical ones.
|
|
423
|
+
const info = { width, height, x: 0, y: 0, scale: this.scale, node: this };
|
|
420
424
|
if (!this._created) {
|
|
421
425
|
this._created = true;
|
|
422
426
|
this.props.onCreated?.(gl, info);
|
|
@@ -478,7 +482,9 @@ export class GlAreaNode extends Node {
|
|
|
478
482
|
* by those. With no GL surface — not made yet, or given up after
|
|
479
483
|
* `onError` — only the children answer, and a point between them is the
|
|
480
484
|
* tree's. Hidden, or `pointerEvents: 'none'` here or above, lets the
|
|
481
|
-
* pointer through to what the tree has behind, as it does for any node
|
|
485
|
+
* pointer through to what the tree has behind, as it does for any node;
|
|
486
|
+
* `'box-none'` here does the same for the surface alone and still lets
|
|
487
|
+
* its children take the pointer.
|
|
482
488
|
*/
|
|
483
489
|
hitSurface(x, y) {
|
|
484
490
|
const panes = this._overlay?.panes.length ?? 0;
|
|
@@ -507,6 +513,10 @@ export class GlAreaNode extends Node {
|
|
|
507
513
|
if (hit) return hit;
|
|
508
514
|
}
|
|
509
515
|
}
|
|
516
|
+
// `box-none`: the children above answered for themselves, and the
|
|
517
|
+
// surface between them is not a target — the point goes on to the tree
|
|
518
|
+
// behind, as it does for any node with that value
|
|
519
|
+
if (this.style?.pointerEvents === 'box-none') return null;
|
|
510
520
|
return this.window ? this : null;
|
|
511
521
|
}
|
|
512
522
|
|
package/src/nodes/hittest.js
CHANGED
|
@@ -170,16 +170,19 @@ export class NodeHitTest {
|
|
|
170
170
|
return null;
|
|
171
171
|
}
|
|
172
172
|
const inside = this.containsPoint(x, y);
|
|
173
|
+
// `box-none`: the children are targets and this node is not — a point
|
|
174
|
+
// over its own area goes to whatever is behind it
|
|
175
|
+
const self = this.style.pointerEvents !== 'box-none';
|
|
173
176
|
// the children are culled on the strict rect — slop grows this node's
|
|
174
177
|
// target, not the region its clip lets through
|
|
175
178
|
if (!inside && this.clipsChildren()) {
|
|
176
|
-
return this.containsPointWithSlop(x, y) ? this : null;
|
|
179
|
+
return self && this.containsPointWithSlop(x, y) ? this : null;
|
|
177
180
|
}
|
|
178
181
|
const order = this.paintOrder();
|
|
179
182
|
for (let i = order.length - 1; i >= 0; i--) {
|
|
180
183
|
const hit = order[i].hitTest(x, y);
|
|
181
184
|
if (hit) return hit;
|
|
182
185
|
}
|
|
183
|
-
return inside || this.containsPointWithSlop(x, y) ? this : null;
|
|
186
|
+
return self && (inside || this.containsPointWithSlop(x, y)) ? this : null;
|
|
184
187
|
}
|
|
185
188
|
}
|
package/src/types/style.d.ts
CHANGED
|
@@ -72,7 +72,14 @@ export type JustifyItems = 'flex-start' | 'center' | 'flex-end' | 'stretch';
|
|
|
72
72
|
export type JustifySelf = 'auto' | JustifyItems;
|
|
73
73
|
export type Overflow = 'visible' | 'hidden' | 'scroll';
|
|
74
74
|
export type BorderStyle = 'solid' | 'dashed';
|
|
75
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Whether the pointer can land on a node. `'none'`: not on it nor anything
|
|
77
|
+
* inside it. `'box-none'` (React Native's): not on the node itself, but on
|
|
78
|
+
* its children — a press on the node's own area goes through to what is
|
|
79
|
+
* behind it. The shape an overlay wants when it holds controls but must not
|
|
80
|
+
* swallow the clicks between them, `<glarea>` included.
|
|
81
|
+
*/
|
|
82
|
+
export type PointerEvents = 'auto' | 'none' | 'box-none';
|
|
76
83
|
export type TextAlign = 'left' | 'right' | 'center' | 'start' | 'end';
|
|
77
84
|
export type FontStyle = 'normal' | 'italic' | 'oblique';
|
|
78
85
|
export type FontWeight = number | 'normal' | 'bold';
|