react-x11 2.7.0 → 2.8.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 +2 -2
- package/src/cocoa/app.js +52 -4
- package/src/cocoa/dnd.js +12 -1
- package/src/cocoa/presenter.js +243 -190
- package/src/cocoa/promotion.js +708 -0
- package/src/cocoa/window.js +29 -1
- package/src/dnd.js +70 -33
- package/src/index.d.ts +12 -1
- package/src/nodes.js +55 -6
package/src/cocoa/window.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { CocoaContext2D } from './context2d.js';
|
|
10
10
|
import { CocoaDropTransport, dragSpec } from './dnd.js';
|
|
11
11
|
import { CocoaLayerPresenter } from './presenter.js';
|
|
12
|
+
import { CocoaPromotion } from './promotion.js';
|
|
12
13
|
|
|
13
14
|
let nextWindowId = 1;
|
|
14
15
|
|
|
@@ -110,6 +111,22 @@ export class CocoaWindow {
|
|
|
110
111
|
this._presenter.animate(node, prop, entry);
|
|
111
112
|
this.cancelNodeAnimation = (node, prop) =>
|
|
112
113
|
this._presenter.cancel(node, prop);
|
|
114
|
+
} else if (app._promote) {
|
|
115
|
+
// Layer promotion (src/cocoa/promotion.js): the surface presenter
|
|
116
|
+
// keeps the frame, and the nodes that animate get a layer of their
|
|
117
|
+
// own above it. The same two animation hooks as layers mode; the
|
|
118
|
+
// invalidate channel, for what the promoted rasters repaint; and the
|
|
119
|
+
// frame's word in before the paint, where a node is moved onto or off
|
|
120
|
+
// its layer and the bitmap under it claimed in the same frame.
|
|
121
|
+
this._promotion = new CocoaPromotion(this);
|
|
122
|
+
this.animateNode = (node, prop, entry) =>
|
|
123
|
+
this._promotion.animate(node, prop, entry);
|
|
124
|
+
this.cancelNodeAnimation = (node, prop) =>
|
|
125
|
+
this._promotion.cancel(node, prop);
|
|
126
|
+
this.noteInvalidate = (damage, layoutChanged) =>
|
|
127
|
+
this._promotion.noteInvalidate(damage, layoutChanged);
|
|
128
|
+
this.prepareFrame = (root, layoutRan) =>
|
|
129
|
+
this._promotion.frame(root, layoutRan);
|
|
113
130
|
}
|
|
114
131
|
app._registerWindow(this);
|
|
115
132
|
}
|
|
@@ -224,6 +241,7 @@ export class CocoaWindow {
|
|
|
224
241
|
this.app.cancelAttention(this._attentionRequest);
|
|
225
242
|
this._attentionRequest = null;
|
|
226
243
|
}
|
|
244
|
+
this._promotion?.destroy();
|
|
227
245
|
this._native.destroyWindow2(this._h);
|
|
228
246
|
this._releaseBacking();
|
|
229
247
|
}
|
|
@@ -312,13 +330,23 @@ export class CocoaWindow {
|
|
|
312
330
|
* The source side: hand a DragSession's gesture to an NSDraggingSession
|
|
313
331
|
* (see src/cocoa/dnd.js for what the spec carries). Returns at once; the
|
|
314
332
|
* session reports back as `drag-session-*` events.
|
|
333
|
+
*
|
|
334
|
+
* And the pump stops here. AppKit tracks the gesture on this thread, so
|
|
335
|
+
* `pump2` does not return until the drop and no timer of ours runs in
|
|
336
|
+
* between — the frame that shows the drag has begun (a `<popup
|
|
337
|
+
* dragPreview>` mounted by `onDragStart`, a source dimmed by
|
|
338
|
+
* `:dragging`) has to go out on the way past. Motion is otherwise paced
|
|
339
|
+
* on the frame clock and not flushed per event (`_routeMotion`); this is
|
|
340
|
+
* the one motion whose answer has no next tick to wait for.
|
|
315
341
|
*/
|
|
316
342
|
beginDrag(session) {
|
|
317
343
|
if (this.destroyed) return null;
|
|
318
|
-
|
|
344
|
+
const began = this._native.beginDrag(
|
|
319
345
|
this._h,
|
|
320
346
|
dragSpec(session, this._native, this.scale),
|
|
321
347
|
);
|
|
348
|
+
this.app._afterInput();
|
|
349
|
+
return began;
|
|
322
350
|
}
|
|
323
351
|
|
|
324
352
|
setTransientFor() {
|
package/src/dnd.js
CHANGED
|
@@ -258,6 +258,13 @@ export class DropSession {
|
|
|
258
258
|
// FIFO gate: messages queue behind atom interning and XdndEnter's
|
|
259
259
|
// async type-list resolution, so a Position never overtakes its Enter.
|
|
260
260
|
this._chain = Promise.resolve();
|
|
261
|
+
// The lane an enter/over dispatch schedules its renders in. Continuous
|
|
262
|
+
// by default, because a drag hovering is a stream like the pointer's
|
|
263
|
+
// own and the frame clock paces it. A transport whose backend *stops*
|
|
264
|
+
// that clock for the duration of the drag raises it — the cocoa one
|
|
265
|
+
// does (src/cocoa/dnd.js), because inside AppKit's tracking loop a
|
|
266
|
+
// render scheduled for the next frame lands after the drop.
|
|
267
|
+
this.hoverPriority = ContinuousEventPriority;
|
|
261
268
|
this._reset();
|
|
262
269
|
}
|
|
263
270
|
|
|
@@ -470,7 +477,7 @@ export class DropSession {
|
|
|
470
477
|
action: this.requestedAction,
|
|
471
478
|
freeze: false,
|
|
472
479
|
};
|
|
473
|
-
runWithPriority(
|
|
480
|
+
runWithPriority(this.hoverPriority, () => {
|
|
474
481
|
this._updateDragPath(path, native);
|
|
475
482
|
// onDragOver may override the declarative answer, synchronously —
|
|
476
483
|
// same latency budget as any event handler, no render awaited
|
|
@@ -1258,20 +1265,6 @@ export class DragSession {
|
|
|
1258
1265
|
Array.isArray(actions) && actions.length > 0 ? actions : ['copy'];
|
|
1259
1266
|
this.currentAction = this.actions[0];
|
|
1260
1267
|
this._resolved = new Map();
|
|
1261
|
-
const ev = this.node.events.dispatch('DragStart', source, native, {
|
|
1262
|
-
types: this.types,
|
|
1263
|
-
action: this.currentAction,
|
|
1264
|
-
source: 'internal',
|
|
1265
|
-
screenX: (native.rootx ?? native.x) / this.node.events.scale,
|
|
1266
|
-
screenY: (native.rooty ?? native.y) / this.node.events.scale,
|
|
1267
|
-
});
|
|
1268
|
-
if (ev.defaultPrevented) {
|
|
1269
|
-
this._reset();
|
|
1270
|
-
return false;
|
|
1271
|
-
}
|
|
1272
|
-
this.phase = 'dragging';
|
|
1273
|
-
source.setStyleState(':dragging', true);
|
|
1274
|
-
this._setCursor('grab');
|
|
1275
1268
|
// A backend with a drag session of its own (the cocoa backend's
|
|
1276
1269
|
// NSDraggingSession, src/cocoa/dnd.js) takes the gesture from here:
|
|
1277
1270
|
// the pointer's motion and release stop arriving and come back as
|
|
@@ -1279,7 +1272,34 @@ export class DragSession {
|
|
|
1279
1272
|
// comes through that window's destination events, routed to this
|
|
1280
1273
|
// session's live payload by `app._activeDrag`.
|
|
1281
1274
|
const wnd = this.node.window;
|
|
1282
|
-
|
|
1275
|
+
const nativeSession = typeof wnd?.beginDrag === 'function';
|
|
1276
|
+
// …and it owns the thread for the *whole* gesture, so anything this
|
|
1277
|
+
// dispatch schedules for later has no later: the `<popup dragPreview>`
|
|
1278
|
+
// an `onDragStart` setState renders would otherwise be created after
|
|
1279
|
+
// the drop. Discrete priority puts the update in the one lane a
|
|
1280
|
+
// backend can land by hand from inside a callback (`flushSyncWork`,
|
|
1281
|
+
// src/cocoa/app.js `_afterInput`). Where the frame clock keeps running,
|
|
1282
|
+
// the motion this arrived on is paced like any other and the update
|
|
1283
|
+
// keeps the priority the dispatcher gave it.
|
|
1284
|
+
const startEvent = () =>
|
|
1285
|
+
this.node.events.dispatch('DragStart', source, native, {
|
|
1286
|
+
types: this.types,
|
|
1287
|
+
action: this.currentAction,
|
|
1288
|
+
source: 'internal',
|
|
1289
|
+
screenX: (native.rootx ?? native.x) / this.node.events.scale,
|
|
1290
|
+
screenY: (native.rooty ?? native.y) / this.node.events.scale,
|
|
1291
|
+
});
|
|
1292
|
+
const ev = nativeSession
|
|
1293
|
+
? runWithPriority(DiscreteEventPriority, startEvent)
|
|
1294
|
+
: startEvent();
|
|
1295
|
+
if (ev.defaultPrevented) {
|
|
1296
|
+
this._reset();
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
this.phase = 'dragging';
|
|
1300
|
+
source.setStyleState(':dragging', true);
|
|
1301
|
+
this._setCursor('grab');
|
|
1302
|
+
if (nativeSession) {
|
|
1283
1303
|
this._nativeSession = true;
|
|
1284
1304
|
this.app._activeDrag = this;
|
|
1285
1305
|
try {
|
|
@@ -1295,7 +1315,12 @@ export class DragSession {
|
|
|
1295
1315
|
}
|
|
1296
1316
|
|
|
1297
1317
|
/** `drag-session-moved` on a native session: the source's `onDrag`, in
|
|
1298
|
-
* global device pixels, with whether a window of ours has accepted.
|
|
1318
|
+
* global device pixels, with whether a window of ours has accepted.
|
|
1319
|
+
*
|
|
1320
|
+
* Discrete priority, like the start: this is the only news of the gesture
|
|
1321
|
+
* that arrives while the native session owns the thread, so the render it
|
|
1322
|
+
* schedules has to be landable from inside the callback. A preview that
|
|
1323
|
+
* follows the pointer is exactly a render per position. */
|
|
1299
1324
|
nativeMoved(ev) {
|
|
1300
1325
|
if (this.phase !== 'dragging' || !this._nativeSession) return;
|
|
1301
1326
|
const s = this.node.scale;
|
|
@@ -1312,18 +1337,20 @@ export class DragSession {
|
|
|
1312
1337
|
};
|
|
1313
1338
|
const source = this.source;
|
|
1314
1339
|
if (source && !source.destroyed && source.props.onDrag) {
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1340
|
+
runWithPriority(DiscreteEventPriority, () =>
|
|
1341
|
+
callHandler(
|
|
1342
|
+
source,
|
|
1343
|
+
'onDrag',
|
|
1344
|
+
source.props.onDrag,
|
|
1345
|
+
this.node.events._makeEvent('drag', native, source, {
|
|
1346
|
+
types: this.types,
|
|
1347
|
+
action: this.currentAction,
|
|
1348
|
+
source: this.localSession ? 'internal' : 'external',
|
|
1349
|
+
accepted: this.accepted,
|
|
1350
|
+
screenX: rootX / this.node.events.scale,
|
|
1351
|
+
screenY: rootY / this.node.events.scale,
|
|
1352
|
+
}),
|
|
1353
|
+
),
|
|
1327
1354
|
);
|
|
1328
1355
|
}
|
|
1329
1356
|
}
|
|
@@ -1343,10 +1370,20 @@ export class DragSession {
|
|
|
1343
1370
|
const rootY = Math.round((ev.y ?? 0) * s);
|
|
1344
1371
|
const operation =
|
|
1345
1372
|
ev.operation && ev.operation !== 'none' ? ev.operation : null;
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1373
|
+
// the release, and the last callback before the thread comes back:
|
|
1374
|
+
// `onDragEnd` takes the preview down, and that is a discrete answer to
|
|
1375
|
+
// the button like any other
|
|
1376
|
+
runWithPriority(DiscreteEventPriority, () =>
|
|
1377
|
+
this._end(
|
|
1378
|
+
{
|
|
1379
|
+
x: rootX - origin.x,
|
|
1380
|
+
y: rootY - origin.y,
|
|
1381
|
+
rootx: rootX,
|
|
1382
|
+
rooty: rootY,
|
|
1383
|
+
},
|
|
1384
|
+
ev.dropped ? operation : null,
|
|
1385
|
+
Boolean(ev.dropped),
|
|
1386
|
+
),
|
|
1350
1387
|
);
|
|
1351
1388
|
}
|
|
1352
1389
|
|
package/src/index.d.ts
CHANGED
|
@@ -188,7 +188,17 @@ export interface RootOptions {
|
|
|
188
188
|
* The Cocoa backend's knobs (docs/macos.md). `presenter` picks the frame
|
|
189
189
|
* path: `'surface'` (the measured default — one bitmap per window, the
|
|
190
190
|
* X11 paint machinery over an IOSurface swapchain) or `'layers'` (one
|
|
191
|
-
* CALayer per drawn node, opt-in while it is measured).
|
|
191
|
+
* CALayer per drawn node, opt-in while it is measured). `promote` is the
|
|
192
|
+
* surface presenter's layer promotion: a plain `<box>` with a transition
|
|
193
|
+
* or a loop on its colour, border or radius gets a CALayer of its own
|
|
194
|
+
* above the bitmap for as long as it animates, and the render server
|
|
195
|
+
* draws the motion — no frames, and it keeps moving while the JS thread
|
|
196
|
+
* is busy. On by default where the bridge draws a layer's colour and a
|
|
197
|
+
* rastered one alike (`@windowkit/appkit` >= 0.5.1, which says so with
|
|
198
|
+
* `colorSpace()`; off on 0.5.0, where the two shades differed); `true`
|
|
199
|
+
* turns it on regardless, `false` keeps every animation on the frame
|
|
200
|
+
* clock.
|
|
201
|
+
* `REACT_X11_COCOA_PROMOTE=1` / `=0` say the same from the environment.
|
|
192
202
|
* `frameInterval` is how often a scheduled frame may paint, in ms. By
|
|
193
203
|
* default each window paces itself on the display it is on — 8.3ms on
|
|
194
204
|
* a 120Hz panel, 16.7 on a 60Hz monitor, the screen's own refresh rate
|
|
@@ -208,6 +218,7 @@ export interface RootOptions {
|
|
|
208
218
|
*/
|
|
209
219
|
cocoa?: {
|
|
210
220
|
presenter?: 'surface' | 'layers';
|
|
221
|
+
promote?: boolean;
|
|
211
222
|
frameInterval?: number;
|
|
212
223
|
pumpInterval?: number;
|
|
213
224
|
appName?: string;
|
package/src/nodes.js
CHANGED
|
@@ -1913,6 +1913,10 @@ export class Node {
|
|
|
1913
1913
|
this._floorMeasureMode = null;
|
|
1914
1914
|
this.root = null; // owning WindowNode once attached
|
|
1915
1915
|
this.hidden = false;
|
|
1916
|
+
// Composited on a layer of its own above the window's bitmap, by a
|
|
1917
|
+
// presenter that can (src/cocoa/promotion.js): the paint walk leaves a
|
|
1918
|
+
// hole where it is, and the presenter draws it — the `<glarea>` idiom.
|
|
1919
|
+
this._promoted = false;
|
|
1916
1920
|
this.destroyed = false;
|
|
1917
1921
|
// absolute rect within the owning window, filled by absolutize()
|
|
1918
1922
|
this.abs = { x: 0, y: 0, width: 0, height: 0 };
|
|
@@ -2285,10 +2289,11 @@ export class Node {
|
|
|
2285
2289
|
|
|
2286
2290
|
// --- the presenter's half of an animation ---------------------------------
|
|
2287
2291
|
//
|
|
2288
|
-
//
|
|
2289
|
-
//
|
|
2290
|
-
//
|
|
2291
|
-
//
|
|
2292
|
+
// Two feature-detected hooks on the window (src/cocoa/window.js: the
|
|
2293
|
+
// layer presenter, and the surface presenter's layer promotion —
|
|
2294
|
+
// src/cocoa/promotion.js): `animateNode(node, prop, entry)` answers true
|
|
2295
|
+
// when the presenter will run the entry itself, `cancelNodeAnimation(node,
|
|
2296
|
+
// prop)` stops what it runs for the property, and the presenter calls back
|
|
2292
2297
|
// through `_offloadEnded` / `_offloadDeclined` below. An entry the
|
|
2293
2298
|
// presenter took is `offloaded`: it stays in `_anim` — so a retarget, a
|
|
2294
2299
|
// loop-stop rule and `sameAnimation` all see it — but it contributes no
|
|
@@ -5315,6 +5320,7 @@ export class Node {
|
|
|
5315
5320
|
ctx.clip();
|
|
5316
5321
|
}
|
|
5317
5322
|
for (const child of order) {
|
|
5323
|
+
if (child._promoted) continue; // on a layer of its own: a hole here
|
|
5318
5324
|
if (child._offscreen()) continue;
|
|
5319
5325
|
if (child._outsideDamage()) continue;
|
|
5320
5326
|
child.paint(ctx);
|
|
@@ -11552,6 +11558,13 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
11552
11558
|
watchDesktopSettings(this.app, () => this._refreshLoops()),
|
|
11553
11559
|
];
|
|
11554
11560
|
this._loopVisibilityChanged();
|
|
11561
|
+
// The window has a presenter now, which it did not when a loop declared
|
|
11562
|
+
// at mount started on the clock (`_setRoot` runs before `realize`):
|
|
11563
|
+
// every loop is asked again here, so one a presenter can take moves
|
|
11564
|
+
// over on the window's first frame rather than at the next swap that
|
|
11565
|
+
// happens to re-resolve its style. Where nothing can take it, the
|
|
11566
|
+
// second look at an unchanged declaration is a no-op.
|
|
11567
|
+
this._refreshLoops();
|
|
11555
11568
|
}
|
|
11556
11569
|
|
|
11557
11570
|
_unwatchLoops() {
|
|
@@ -11725,8 +11738,19 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
11725
11738
|
// A retained presenter keeps a per-node diff instead of damage rects,
|
|
11726
11739
|
// and this is the one channel every change already announces itself on
|
|
11727
11740
|
// (docs/macos.md §"One renderer, two presenters"). Feature-detected: an
|
|
11728
|
-
// ntk window has no ear here and the X11 path is byte-identical.
|
|
11729
|
-
|
|
11741
|
+
// ntk window has no ear here and the X11 path is byte-identical. A
|
|
11742
|
+
// presenter that answers `true` has taken the claim onto a layer of its
|
|
11743
|
+
// own (src/cocoa/promotion.js): the bitmap owes nothing for it, and the
|
|
11744
|
+
// frame that is still owed — for the presenter's half, `prepareFrame` —
|
|
11745
|
+
// paints nothing unless something else claims.
|
|
11746
|
+
const taken =
|
|
11747
|
+
this.window?.noteInvalidate?.(damage, layoutChanged, reason) === true;
|
|
11748
|
+
if (taken && !layoutChanged) {
|
|
11749
|
+
this._damage ??= [];
|
|
11750
|
+
this.needsPaint = true;
|
|
11751
|
+
this._scheduleFrame();
|
|
11752
|
+
return;
|
|
11753
|
+
}
|
|
11730
11754
|
if (layoutChanged) {
|
|
11731
11755
|
this.needsLayout = true;
|
|
11732
11756
|
// The content floors are measured from the tree, so anything that
|
|
@@ -11832,6 +11856,11 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
11832
11856
|
this._damage = addDamageRect(this._damage, bounds, this._damageRectCap());
|
|
11833
11857
|
}
|
|
11834
11858
|
this.needsPaint = true;
|
|
11859
|
+
this._scheduleFrame();
|
|
11860
|
+
}
|
|
11861
|
+
|
|
11862
|
+
/** A frame, on the window's clock. */
|
|
11863
|
+
_scheduleFrame() {
|
|
11835
11864
|
// Recorded before the `_scheduled` gate, not inside it: the debt is
|
|
11836
11865
|
// "this window has damage", which a discrete event may pay off early
|
|
11837
11866
|
// (see frames.js). Tying it to whether a callback is outstanding would
|
|
@@ -11959,6 +11988,13 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
11959
11988
|
for (const node of this._reflowed) node._reflowBefore = null;
|
|
11960
11989
|
this._reflowed.clear();
|
|
11961
11990
|
}
|
|
11991
|
+
// A presenter compositing part of the tree on layers of its own — the
|
|
11992
|
+
// surface presenter's promoted nodes (src/cocoa/promotion.js) — gets
|
|
11993
|
+
// its word in here: after layout, so it sees where everything landed,
|
|
11994
|
+
// and before the damage is taken, so a node it moves onto or off a
|
|
11995
|
+
// layer claims the bitmap under it in this very frame. Feature-detected
|
|
11996
|
+
// like `presentFrame`; an ntk window has no such half.
|
|
11997
|
+
this.window.prepareFrame?.(this, layoutRan);
|
|
11962
11998
|
// any node this pass laid out may be what an open popup is anchored to
|
|
11963
11999
|
if (layoutRan) this._notifyAnchorChange();
|
|
11964
12000
|
// after layout (the claims above included), before the damage is taken:
|
|
@@ -12504,6 +12540,11 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
12504
12540
|
if (!check(child)) return false;
|
|
12505
12541
|
continue;
|
|
12506
12542
|
}
|
|
12543
|
+
// A node on a layer of its own (src/cocoa/promotion.js) has no
|
|
12544
|
+
// pixels in the bitmap the band is cut from, so nothing of it can
|
|
12545
|
+
// be dragged along — a pulsing toast over a list is what promotion
|
|
12546
|
+
// is for, and this is the half of it that keeps the pan a blit.
|
|
12547
|
+
if (child._promoted) continue;
|
|
12507
12548
|
if (rectsOverlap(child._subtreeBounds(), vp)) return false;
|
|
12508
12549
|
}
|
|
12509
12550
|
return true;
|
|
@@ -12706,6 +12747,14 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
12706
12747
|
this._lastReasons = EMPTY_REASONS;
|
|
12707
12748
|
}
|
|
12708
12749
|
if (damage === FULL_DAMAGE || !damage) return null;
|
|
12750
|
+
// an empty list: every claim this frame made was answered on a layer of
|
|
12751
|
+
// its own (`invalidate`, the presenter's `true`), and the bitmap paints
|
|
12752
|
+
// nothing — which is not the same as nothing having been claimed
|
|
12753
|
+
if (damage.length === 0) {
|
|
12754
|
+
this._lastDamageRects = [];
|
|
12755
|
+
this._lastDamage = { x: 0, y: 0, width: 0, height: 0 };
|
|
12756
|
+
return [];
|
|
12757
|
+
}
|
|
12709
12758
|
const rects = [];
|
|
12710
12759
|
for (const claimed of damage) {
|
|
12711
12760
|
const clamped = this._clampDamage(claimed, width, height);
|