react-x11 2.5.0 → 2.6.1
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 +240 -31
- package/src/cocoa/context2d.js +287 -26
- 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 +9 -3
- 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/paintcache.js +71 -11
- package/src/screens.js +39 -4
- package/src/svgnodes.js +4 -3
- package/src/types/elements.d.ts +13 -0
package/src/paintcache.js
CHANGED
|
@@ -22,6 +22,14 @@
|
|
|
22
22
|
// Multi-colour drawings bake their colours in, which is right, because those
|
|
23
23
|
// colours belong to the drawing. `SvgView.paintKind` decides which is which.
|
|
24
24
|
//
|
|
25
|
+
// Coverage needs a backend that composites a mask through a colour, which
|
|
26
|
+
// X Render does and the Cocoa backend's surfaces do not (src/cocoa/
|
|
27
|
+
// surface.js — no `a8`). There a mono drawing is cached as argb32 with its
|
|
28
|
+
// colour in the key: one entry per colour it is seen in rather than one for
|
|
29
|
+
// all of them, which is still one render per colour instead of one per
|
|
30
|
+
// cell per frame. A coverage-only plan — a blurred shadow, whose blur is a
|
|
31
|
+
// pass over the mask — stays live on that backend.
|
|
32
|
+
//
|
|
25
33
|
// ## Self-limiting, on purpose
|
|
26
34
|
//
|
|
27
35
|
// X gives no back-pressure: the first sign of overspending is `BadAlloc` on
|
|
@@ -36,6 +44,12 @@
|
|
|
36
44
|
// `typeof wnd?.scrollRegion !== 'function'` guard for ntk without #139.
|
|
37
45
|
import * as ntk from 'ntk';
|
|
38
46
|
|
|
47
|
+
// The surface this cache draws into is `react-x11/ntk`'s: ntk's pixmap on
|
|
48
|
+
// an X connection, the app's own (`app.createSurface`) on a backend that
|
|
49
|
+
// makes them — the Cocoa backend's CG bitmap. One import, so the cache
|
|
50
|
+
// names neither.
|
|
51
|
+
import { Surface } from './ntk.js';
|
|
52
|
+
|
|
39
53
|
/** Stale pixels are undebuggable, and every other optimization here has an
|
|
40
54
|
* escape hatch — see NO_SCROLL_BLIT. */
|
|
41
55
|
const DISABLED = process.env.REACT_X11_NO_PAINT_CACHE === '1';
|
|
@@ -145,6 +159,9 @@ export class PaintCache {
|
|
|
145
159
|
this.app = app;
|
|
146
160
|
this.budget = budget;
|
|
147
161
|
this.verify = verify;
|
|
162
|
+
/** whether an `a8` entry can be painted through a colour here — X
|
|
163
|
+
* Render composites a mask through the fill; nothing else does yet */
|
|
164
|
+
this.coverage = Boolean(app?.display?.Render);
|
|
148
165
|
/** key -> entry. Map iteration is insertion order, so re-inserting on
|
|
149
166
|
* access makes this an LRU list for free — same trick as getGlyphPage. */
|
|
150
167
|
this.entries = new Map();
|
|
@@ -193,7 +210,7 @@ export class PaintCache {
|
|
|
193
210
|
return this.drawing(ctx, {
|
|
194
211
|
...plan,
|
|
195
212
|
label: `<${node.kind}>`,
|
|
196
|
-
draw: (sctx, box) => node.paintCached(sctx, box),
|
|
213
|
+
draw: (sctx, box, ink) => node.paintCached(sctx, box, ink),
|
|
197
214
|
live: () => node.paintContent(ctx),
|
|
198
215
|
});
|
|
199
216
|
}
|
|
@@ -211,9 +228,22 @@ export class PaintCache {
|
|
|
211
228
|
*
|
|
212
229
|
* `maxPixels` overrides the per-item cap for a caller whose drawing is
|
|
213
230
|
* legitimately box-sized rather than icon-sized.
|
|
231
|
+
*
|
|
232
|
+
* `draw` is handed the ink a mono drawing paints in as its third
|
|
233
|
+
* argument: white into a coverage surface, where only the alpha survives
|
|
234
|
+
* and the tint arrives at blit time, and the tint itself on a backend
|
|
235
|
+
* without coverage, where the entry bakes its colour and carries it in
|
|
236
|
+
* the key (`_bakeTint`).
|
|
214
237
|
*/
|
|
215
238
|
drawing(ctx, plan) {
|
|
216
239
|
if (!isDeviceSpace(ctx)) return plan.live(ctx);
|
|
240
|
+
if (plan.format === 'a8' && !this.coverage) {
|
|
241
|
+
// A plan whose `after` pass works on the mask — a blurred shadow —
|
|
242
|
+
// has no argb32 equivalent: it paints live, which is what the frame
|
|
243
|
+
// owed with no cache at all.
|
|
244
|
+
if (plan.after) return plan.live(ctx);
|
|
245
|
+
plan = this._bakeTint(plan);
|
|
246
|
+
}
|
|
217
247
|
|
|
218
248
|
if (plan.width * plan.height > (plan.maxPixels ?? MAX_ITEM_PIXELS)) {
|
|
219
249
|
this.stats.tooBig++;
|
|
@@ -254,9 +284,30 @@ export class PaintCache {
|
|
|
254
284
|
return this._blit(ctx, entry, plan);
|
|
255
285
|
}
|
|
256
286
|
|
|
287
|
+
/**
|
|
288
|
+
* A coverage plan on a backend that cannot composite coverage, as an
|
|
289
|
+
* argb32 plan that paints its tint: the same pixels, one entry per colour
|
|
290
|
+
* instead of one per drawing.
|
|
291
|
+
*/
|
|
292
|
+
_bakeTint(plan) {
|
|
293
|
+
const tint = plan.tint;
|
|
294
|
+
return {
|
|
295
|
+
...plan,
|
|
296
|
+
format: 'argb32',
|
|
297
|
+
key: `${plan.key}|ink:${tint}`,
|
|
298
|
+
draw: (sctx, box) => plan.draw(sctx, box, tint),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** The colour a drawing paints in: white into coverage, the tint into a
|
|
303
|
+
* baked entry, and nothing a multi-colour drawing reads. */
|
|
304
|
+
static inkFor(plan) {
|
|
305
|
+
return plan.format === 'a8' ? '#ffffff' : (plan.tint ?? '#ffffff');
|
|
306
|
+
}
|
|
307
|
+
|
|
257
308
|
_render(plan) {
|
|
258
309
|
try {
|
|
259
|
-
const surface = new
|
|
310
|
+
const surface = new Surface(this.app, {
|
|
260
311
|
width: plan.width,
|
|
261
312
|
height: plan.height,
|
|
262
313
|
format: plan.format,
|
|
@@ -264,7 +315,11 @@ export class PaintCache {
|
|
|
264
315
|
const box = { x: 0, y: 0, width: plan.width, height: plan.height };
|
|
265
316
|
const state = { digest: 0x811c9dc5 };
|
|
266
317
|
surface.render((sctx) =>
|
|
267
|
-
plan.draw(
|
|
318
|
+
plan.draw(
|
|
319
|
+
this.verify ? recordingContext(sctx, state) : sctx,
|
|
320
|
+
box,
|
|
321
|
+
PaintCache.inkFor(plan),
|
|
322
|
+
),
|
|
268
323
|
);
|
|
269
324
|
// `after` may hand back a *different* surface than it was given — a
|
|
270
325
|
// blurred shadow bakes its convolution into a second one and destroys
|
|
@@ -310,18 +365,17 @@ export class PaintCache {
|
|
|
310
365
|
const state = { digest: 0x811c9dc5 };
|
|
311
366
|
let scratch = null;
|
|
312
367
|
try {
|
|
313
|
-
scratch = new
|
|
368
|
+
scratch = new Surface(this.app, {
|
|
314
369
|
width: plan.width,
|
|
315
370
|
height: plan.height,
|
|
316
371
|
format: plan.format,
|
|
317
372
|
});
|
|
318
373
|
scratch.render((sctx) =>
|
|
319
|
-
plan.draw(
|
|
320
|
-
|
|
321
|
-
y: 0,
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}),
|
|
374
|
+
plan.draw(
|
|
375
|
+
recordingContext(sctx, state),
|
|
376
|
+
{ x: 0, y: 0, width: plan.width, height: plan.height },
|
|
377
|
+
PaintCache.inkFor(plan),
|
|
378
|
+
),
|
|
325
379
|
);
|
|
326
380
|
} catch {
|
|
327
381
|
return; // verification is best-effort; never break a frame over it
|
|
@@ -365,6 +419,12 @@ export class PaintCache {
|
|
|
365
419
|
* cannot, and the answer is simply that nothing is cached. */
|
|
366
420
|
export const paintCacheSupported = () => typeof ntk.Surface === 'function';
|
|
367
421
|
|
|
422
|
+
/** Whether `app` can make an offscreen surface at all: an X connection
|
|
423
|
+
* with the Render extension (ntk's pixmap surface), or a backend with a
|
|
424
|
+
* surface of its own — the Cocoa app's `createSurface` (docs/macos.md). */
|
|
425
|
+
const canMakeSurfaces = (app) =>
|
|
426
|
+
Boolean(app?.display?.Render) || typeof app?.createSurface === 'function';
|
|
427
|
+
|
|
368
428
|
/**
|
|
369
429
|
* The cache for an app, created on first use. Null when caching is off, when
|
|
370
430
|
* the installed ntk is too old, or when the app cannot make surfaces — the
|
|
@@ -372,6 +432,6 @@ export const paintCacheSupported = () => typeof ntk.Surface === 'function';
|
|
|
372
432
|
* there must paint live.
|
|
373
433
|
*/
|
|
374
434
|
export function paintCacheFor(app) {
|
|
375
|
-
if (DISABLED || !paintCacheSupported() || !app
|
|
435
|
+
if (DISABLED || !paintCacheSupported() || !canMakeSurfaces(app)) return null;
|
|
376
436
|
return (app._paintCache ??= new PaintCache(app));
|
|
377
437
|
}
|
package/src/screens.js
CHANGED
|
@@ -72,12 +72,17 @@
|
|
|
72
72
|
* Letting the WM have the last word costs a clamped-to-the-edge window one
|
|
73
73
|
* correction it would have made anyway.
|
|
74
74
|
*
|
|
75
|
-
* **A per-monitor work area.** `_NET_WORKAREA` is one rect for the
|
|
76
|
-
* virtual desktop. Deriving a real per-monitor one means reading
|
|
75
|
+
* **A per-monitor work area, on X11.** `_NET_WORKAREA` is one rect for the
|
|
76
|
+
* whole virtual desktop. Deriving a real per-monitor one means reading
|
|
77
77
|
* `_NET_WM_STRUT_PARTIAL` off every window on the screen and intersecting
|
|
78
78
|
* the reservations that fall on each head — a full window-tree walk, redone
|
|
79
79
|
* whenever any panel changes. `available` below is the per-axis
|
|
80
80
|
* approximation instead, and says so.
|
|
81
|
+
*
|
|
82
|
+
* A backend that *does* know each monitor's usable rect says so directly
|
|
83
|
+
* instead: a `visible` rect on the monitor record, which `usable()` prefers
|
|
84
|
+
* over the whole-desktop compromise. Cocoa's `NSScreen.visibleFrame` is
|
|
85
|
+
* exactly that, so the macOS backend never goes through the approximation.
|
|
81
86
|
*/
|
|
82
87
|
|
|
83
88
|
import { requireExtension } from './extensions.js';
|
|
@@ -206,9 +211,36 @@ function monitorAt(monitors, point) {
|
|
|
206
211
|
return best;
|
|
207
212
|
}
|
|
208
213
|
|
|
214
|
+
/** The overlap of two rects, or `null` where they do not touch. */
|
|
215
|
+
function intersect(a, b) {
|
|
216
|
+
const x0 = Math.max(a.x, b.x);
|
|
217
|
+
const y0 = Math.max(a.y, b.y);
|
|
218
|
+
const x1 = Math.min(a.x + a.width, b.x + b.width);
|
|
219
|
+
const y1 = Math.min(a.y + a.height, b.y + b.height);
|
|
220
|
+
if (x1 <= x0 || y1 <= y0) return null;
|
|
221
|
+
return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
|
|
222
|
+
}
|
|
223
|
+
|
|
209
224
|
/**
|
|
210
|
-
* The
|
|
211
|
-
*
|
|
225
|
+
* The usable part of one monitor.
|
|
226
|
+
*
|
|
227
|
+
* Two ways to get there, and the first one is the real answer:
|
|
228
|
+
*
|
|
229
|
+
* - A monitor record may carry its **own** usable rect as `visible`, in the
|
|
230
|
+
* same screen coordinates as the monitor itself. That is what Cocoa's
|
|
231
|
+
* `NSScreen.visibleFrame` is — per screen, minus that screen's menu bar
|
|
232
|
+
* and Dock — and it is taken as a rect, intersected with the monitor in
|
|
233
|
+
* case a backend reports one that overhangs.
|
|
234
|
+
* - Otherwise the monitor rect clamped **per axis** by `_NET_WORKAREA`,
|
|
235
|
+
* which is one rect for the whole virtual desktop and so cannot be
|
|
236
|
+
* positioned against a single head — see the note at the top of the file.
|
|
237
|
+
*
|
|
238
|
+
* The distinction matters the moment the monitors differ in size. A global
|
|
239
|
+
* work area no wider than the primary, applied as a width bound to a wider
|
|
240
|
+
* second display, moves that display's right edge inward by the difference
|
|
241
|
+
* and every anchored popup with it. On X11 the property spans the virtual
|
|
242
|
+
* desktop and the clamp is a no-op on the width, which is why the
|
|
243
|
+
* approximation held there and only there.
|
|
212
244
|
*
|
|
213
245
|
* Always **only** a rect. A monitor record carries a name, a primary flag and
|
|
214
246
|
* physical sizes as well, and spreading it here put all of that inside
|
|
@@ -221,6 +253,7 @@ function usable(monitor, work) {
|
|
|
221
253
|
width: monitor.width,
|
|
222
254
|
height: monitor.height,
|
|
223
255
|
};
|
|
256
|
+
if (monitor.visible) return intersect(rect, monitor.visible) ?? rect;
|
|
224
257
|
if (!work) return rect;
|
|
225
258
|
rect.width = Math.min(rect.width, work.width);
|
|
226
259
|
rect.height = Math.min(rect.height, work.height);
|
|
@@ -756,6 +789,8 @@ export function endScreens(app) {
|
|
|
756
789
|
* `monitors` entries may carry the RandR fields (`name`, `primary`,
|
|
757
790
|
* `widthMM`, `heightMM`, `refreshRate`, `rotation`) as well as the rect, so
|
|
758
791
|
* a test can state a named two-head desktop without a server that has RandR.
|
|
792
|
+
* An entry may also carry its own `visible` rect — the monitor's usable
|
|
793
|
+
* area, which takes precedence over the whole-desktop `workArea`.
|
|
759
794
|
*/
|
|
760
795
|
export function setScreensForTests(app, { monitors = null, workArea = null }) {
|
|
761
796
|
let session = sessions.get(app);
|
package/src/svgnodes.js
CHANGED
|
@@ -294,13 +294,14 @@ export class SvgNode extends Node {
|
|
|
294
294
|
};
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
paintCached(ctx, box) {
|
|
297
|
+
paintCached(ctx, box, ink = '#ffffff') {
|
|
298
298
|
const view = this._ensureView();
|
|
299
299
|
if (!view) return;
|
|
300
300
|
// Into a coverage surface, only the alpha of a paint survives, and the
|
|
301
|
-
// tint arrives at blit time — so any opaque colour renders the same
|
|
301
|
+
// tint arrives at blit time — so any opaque colour renders the same
|
|
302
|
+
// mask, and the cache says white then, or the tint where it bakes colour.
|
|
302
303
|
view.draw(ctx, box.x, box.y, box.width, box.height, {
|
|
303
|
-
color: view.paintKind === 'mono' ?
|
|
304
|
+
color: view.paintKind === 'mono' ? ink : this._currentColor(),
|
|
304
305
|
});
|
|
305
306
|
}
|
|
306
307
|
}
|
package/src/types/elements.d.ts
CHANGED
|
@@ -188,6 +188,19 @@ export interface DrawnProps<T = DrawnNode>
|
|
|
188
188
|
SelectionProps<T>,
|
|
189
189
|
EventHandlers<T> {
|
|
190
190
|
ref?: Ref<T>;
|
|
191
|
+
/**
|
|
192
|
+
* Zoom this subtree, CSS `zoom` rather than a transform: every length
|
|
193
|
+
* under here — and this element's *own* style — is multiplied by it, so
|
|
194
|
+
* `scale={2}` is a card twice the size with text shaped at twice the size
|
|
195
|
+
* inside it, not a 1x picture stretched. Nested props multiply, and the
|
|
196
|
+
* display scale is the floor they multiply
|
|
197
|
+
* ([scale.md](scale.md#a-subtree-of-its-own)).
|
|
198
|
+
*
|
|
199
|
+
* A positive number; leaving it out means 1. `<window>` and `<popup>`
|
|
200
|
+
* take no `scale`: a real X window is its own root, so a menu opened from
|
|
201
|
+
* a zoomed card comes up at the app's own size.
|
|
202
|
+
*/
|
|
203
|
+
scale?: number;
|
|
191
204
|
}
|
|
192
205
|
|
|
193
206
|
/**
|