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/presenter.js
CHANGED
|
@@ -19,12 +19,19 @@
|
|
|
19
19
|
//
|
|
20
20
|
// Sibling order is zPosition, assigned from the node's own paintOrder() —
|
|
21
21
|
// no sublayer-list surgery, ever. Dirt arrives on the invalidate channel
|
|
22
|
-
// (`noteInvalidate`): a node means that node,
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// cheaper than
|
|
22
|
+
// (`noteInvalidate`): a node means that node, a bare rect means that part
|
|
23
|
+
// of every raster it touches, null means everything (the same "no bound
|
|
24
|
+
// named repaints everything" rule the X11 damage model has), and geometry
|
|
25
|
+
// is re-diffed every frame because comparing four numbers is cheaper than
|
|
26
|
+
// knowing.
|
|
26
27
|
import { cssColorStraight } from 'ntk';
|
|
27
28
|
|
|
29
|
+
import {
|
|
30
|
+
Node,
|
|
31
|
+
addDamageRect,
|
|
32
|
+
damageToPaint,
|
|
33
|
+
intersectRects,
|
|
34
|
+
} from '../nodes.js';
|
|
28
35
|
import { CocoaContext2D } from './context2d.js';
|
|
29
36
|
|
|
30
37
|
const RASTER_PAD = 2; // antialiasing/italic overhang outside the ink bounds
|
|
@@ -254,8 +261,27 @@ class ShapeRecorder {
|
|
|
254
261
|
}
|
|
255
262
|
}
|
|
256
263
|
|
|
257
|
-
/**
|
|
264
|
+
/**
|
|
265
|
+
* Paint everything a node draws itself — Node.paint minus the children.
|
|
266
|
+
*
|
|
267
|
+
* An element that overrides `paint` (every drawing element in
|
|
268
|
+
* @react-x11/components does: `super.paint(ctx)` for the box, then the
|
|
269
|
+
* scene) has its content nowhere but in that override, so the override is
|
|
270
|
+
* what a raster replays — with the children held back by `_ownPaintOnly`,
|
|
271
|
+
* the one presenter-side flag `Node._paintChildren` honours, because they
|
|
272
|
+
* have visuals of their own. Everything else paints piecewise, which is
|
|
273
|
+
* what `Node.paint` would do minus the child walk.
|
|
274
|
+
*/
|
|
258
275
|
function paintSelf(node, ctx) {
|
|
276
|
+
if (node.paint !== Node.prototype.paint) {
|
|
277
|
+
node._ownPaintOnly = true;
|
|
278
|
+
try {
|
|
279
|
+
node.paint(ctx);
|
|
280
|
+
} finally {
|
|
281
|
+
node._ownPaintOnly = false;
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
259
285
|
node._paintShadow(ctx);
|
|
260
286
|
node._paintBackground(ctx);
|
|
261
287
|
node.paintContent(ctx);
|
|
@@ -263,6 +289,26 @@ function paintSelf(node, ctx) {
|
|
|
263
289
|
node._paintOutline(ctx);
|
|
264
290
|
}
|
|
265
291
|
|
|
292
|
+
// Past this many bare-rect claims in one frame the overlap test costs more
|
|
293
|
+
// than it saves, and the answer is the one a null claim gives: everything.
|
|
294
|
+
const MAX_DIRTY_RECTS = 64;
|
|
295
|
+
|
|
296
|
+
// The pass list of a raster that repaints in full: one pass, unbounded.
|
|
297
|
+
const FULL_PASS = Object.freeze([null]);
|
|
298
|
+
|
|
299
|
+
/** A rect grown outward to whole pixels — a pass clears and clips to its
|
|
300
|
+
* edges, and a fractional edge would antialias the clip into a seam. */
|
|
301
|
+
function wholePixels(rect) {
|
|
302
|
+
const x = Math.floor(rect.x);
|
|
303
|
+
const y = Math.floor(rect.y);
|
|
304
|
+
return {
|
|
305
|
+
x,
|
|
306
|
+
y,
|
|
307
|
+
width: Math.ceil(rect.x + rect.width) - x,
|
|
308
|
+
height: Math.ceil(rect.y + rect.height) - y,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
266
312
|
const EDGE_PROPS = [
|
|
267
313
|
'borderTopColor',
|
|
268
314
|
'borderRightColor',
|
|
@@ -354,6 +400,9 @@ class RasterState {
|
|
|
354
400
|
|
|
355
401
|
ensure(presenter, width, height, scale) {
|
|
356
402
|
if (!this.surface || this.width !== width || this.height !== height) {
|
|
403
|
+
// the layer holds its own copy of what it shows, so the bitmap a
|
|
404
|
+
// resize retires can go now rather than with the handle's finalizer
|
|
405
|
+
this.release(presenter.native);
|
|
357
406
|
this.surface = presenter.native.createSurface(width, height, scale);
|
|
358
407
|
this.width = width;
|
|
359
408
|
this.height = height;
|
|
@@ -369,6 +418,16 @@ class RasterState {
|
|
|
369
418
|
}
|
|
370
419
|
return this.ctx;
|
|
371
420
|
}
|
|
421
|
+
|
|
422
|
+
/** Free the bitmap now (bridge 0.4's `releaseSurface`); older bridges
|
|
423
|
+
* free it from the handle's finalizer, and this is then just the drop. */
|
|
424
|
+
release(native) {
|
|
425
|
+
const surface = this.surface;
|
|
426
|
+
this.surface = null;
|
|
427
|
+
if (surface && typeof native.releaseSurface === 'function') {
|
|
428
|
+
native.releaseSurface(surface);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
372
431
|
}
|
|
373
432
|
|
|
374
433
|
export class CocoaLayerPresenter {
|
|
@@ -380,8 +439,14 @@ export class CocoaLayerPresenter {
|
|
|
380
439
|
this.visuals = new Map(); // node -> Visual
|
|
381
440
|
this.rasters = new Map(); // node -> RasterState
|
|
382
441
|
this.bars = new Map(); // scroller node -> Map(axis -> { layer, raster })
|
|
383
|
-
|
|
442
|
+
// Claims since the last frame — taken at the top of `frame()`, the way
|
|
443
|
+
// the X11 path takes its damage before painting, so a claim made from
|
|
444
|
+
// inside a paint lands in the next frame instead of being cleared with
|
|
445
|
+
// this one.
|
|
446
|
+
this.dirty = new Set(); // nodes
|
|
447
|
+
this.dirtyRects = []; // bare rects, window coordinates
|
|
384
448
|
this.dirtyAll = true; // first frame rasters everything
|
|
449
|
+
this._claims = null; // the frame in progress: { all, nodes, rects }
|
|
385
450
|
this.rootVisual = {
|
|
386
451
|
layer: window._layer,
|
|
387
452
|
};
|
|
@@ -402,12 +467,77 @@ export class CocoaLayerPresenter {
|
|
|
402
467
|
// say which node that was. Everything re-rasters; a scroll names its
|
|
403
468
|
// node and stays off this path.
|
|
404
469
|
this.dirtyAll = true;
|
|
470
|
+
} else if (damage.width > 0 && damage.height > 0) {
|
|
471
|
+
// A bare rect with no layout behind it is a claim about pixels — an
|
|
472
|
+
// element's `invalidate(false, rect)` for the box a dragged node
|
|
473
|
+
// moved through, the region `scrollContents` shifts, the strip an
|
|
474
|
+
// animation ticks in — and it cannot name the node it came from
|
|
475
|
+
// either. The frame repaints that part of every raster visual whose
|
|
476
|
+
// ink the rect touches (the same conservative answer the damage model
|
|
477
|
+
// gives a rect: whatever draws there repaints, clipped to it);
|
|
478
|
+
// property boxes carry no raster and re-diff every frame regardless.
|
|
479
|
+
// Copied, because a caller's rect is very often a live `abs` about
|
|
480
|
+
// to be laid out.
|
|
481
|
+
if (this.dirtyRects.length >= MAX_DIRTY_RECTS) {
|
|
482
|
+
this.dirtyAll = true;
|
|
483
|
+
} else {
|
|
484
|
+
this.dirtyRects.push({
|
|
485
|
+
x: damage.x,
|
|
486
|
+
y: damage.y,
|
|
487
|
+
width: damage.width,
|
|
488
|
+
height: damage.height,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
405
491
|
}
|
|
406
492
|
}
|
|
407
493
|
|
|
494
|
+
/** The claims a frame consumes, cleared for the next one. */
|
|
495
|
+
_takeClaims() {
|
|
496
|
+
const claims = {
|
|
497
|
+
all: this.dirtyAll,
|
|
498
|
+
nodes: this.dirty,
|
|
499
|
+
rects: this.dirtyAll ? [] : this.dirtyRects,
|
|
500
|
+
};
|
|
501
|
+
this.dirtyAll = false;
|
|
502
|
+
this.dirty = new Set();
|
|
503
|
+
this.dirtyRects = [];
|
|
504
|
+
return claims;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* What this frame repaints of the raster covering `rect` for `node`: null
|
|
509
|
+
* for nothing, `FULL_PASS` for all of it, otherwise the window-space rects
|
|
510
|
+
* the frame's bare claims cover inside it, shaped exactly as the X11 path
|
|
511
|
+
* shapes its damage list: whole pixels, disjoint, at most a few of them,
|
|
512
|
+
* and one box instead of several when the several fill most of it —
|
|
513
|
+
* because a node painted in two overlapping passes blends translucent ink
|
|
514
|
+
* over itself, and because each pass is a full replay of the node's paint
|
|
515
|
+
* that only its own culling makes cheap. A pan's shifted region and the
|
|
516
|
+
* two strips beside it come out as the one pass they are.
|
|
517
|
+
*/
|
|
518
|
+
_rasterPasses(node, rect, sizeChanged) {
|
|
519
|
+
const claims = this._claims;
|
|
520
|
+
if (sizeChanged || !claims || claims.all || claims.nodes.has(node)) {
|
|
521
|
+
return FULL_PASS;
|
|
522
|
+
}
|
|
523
|
+
let passes = null;
|
|
524
|
+
for (const claimed of claims.rects) {
|
|
525
|
+
const hit = intersectRects(wholePixels(claimed), rect);
|
|
526
|
+
if (hit) passes = addDamageRect(passes, hit);
|
|
527
|
+
}
|
|
528
|
+
return passes && damageToPaint(passes);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Does this frame re-raster the visual covering `rect` for `node`? */
|
|
532
|
+
_needsRaster(node, rect) {
|
|
533
|
+
return this._rasterPasses(node, rect, false) !== null;
|
|
534
|
+
}
|
|
535
|
+
|
|
408
536
|
/** The whole frame: one walk, one transaction, property diffs only. */
|
|
409
537
|
frame(windowNode) {
|
|
410
538
|
const native = this.native;
|
|
539
|
+
const claims = (this._claims = this._takeClaims());
|
|
540
|
+
let presented = false;
|
|
411
541
|
native.txBegin({ disableActions: true });
|
|
412
542
|
try {
|
|
413
543
|
this._syncWindowBackground(windowNode);
|
|
@@ -417,21 +547,42 @@ export class CocoaLayerPresenter {
|
|
|
417
547
|
if (!seen.has(node)) {
|
|
418
548
|
visual.destroy();
|
|
419
549
|
this.visuals.delete(node);
|
|
420
|
-
this.
|
|
550
|
+
this._dropRaster(node);
|
|
421
551
|
const bars = this.bars.get(node);
|
|
422
552
|
if (bars) {
|
|
423
553
|
for (const entry of bars.values()) {
|
|
424
554
|
native.removeFromSuperlayer(entry.layer);
|
|
555
|
+
entry.raster.release(native);
|
|
425
556
|
}
|
|
426
557
|
this.bars.delete(node);
|
|
427
558
|
}
|
|
428
559
|
}
|
|
429
560
|
}
|
|
561
|
+
presented = true;
|
|
430
562
|
} finally {
|
|
431
563
|
native.txCommit();
|
|
564
|
+
this._claims = null;
|
|
565
|
+
// a frame that threw half-way presents what it got to; the claims it
|
|
566
|
+
// was answering are still owed, so the next frame answers them again
|
|
567
|
+
if (!presented) this._restoreClaims(claims);
|
|
432
568
|
}
|
|
433
|
-
|
|
434
|
-
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
_restoreClaims(claims) {
|
|
572
|
+
this.dirtyAll ||= claims.all;
|
|
573
|
+
for (const node of claims.nodes) this.dirty.add(node);
|
|
574
|
+
if (this.dirtyRects.length + claims.rects.length > MAX_DIRTY_RECTS) {
|
|
575
|
+
this.dirtyAll = true;
|
|
576
|
+
} else {
|
|
577
|
+
this.dirtyRects.push(...claims.rects);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
_dropRaster(node) {
|
|
582
|
+
const raster = this.rasters.get(node);
|
|
583
|
+
if (!raster) return;
|
|
584
|
+
raster.release(this.native);
|
|
585
|
+
this.rasters.delete(node);
|
|
435
586
|
}
|
|
436
587
|
|
|
437
588
|
_syncWindowBackground(windowNode) {
|
|
@@ -460,7 +611,7 @@ export class CocoaLayerPresenter {
|
|
|
460
611
|
let visual = this.visuals.get(node);
|
|
461
612
|
if (visual && visual.isRaster !== wantsRaster) {
|
|
462
613
|
visual.destroy();
|
|
463
|
-
this.
|
|
614
|
+
this._dropRaster(node);
|
|
464
615
|
visual = null;
|
|
465
616
|
}
|
|
466
617
|
if (!visual) {
|
|
@@ -590,9 +741,8 @@ export class CocoaLayerPresenter {
|
|
|
590
741
|
const s = this.scale;
|
|
591
742
|
if (
|
|
592
743
|
!sizeChanged &&
|
|
593
|
-
|
|
594
|
-
!this.
|
|
595
|
-
visual.shapeSignature
|
|
744
|
+
visual.shapeSignature &&
|
|
745
|
+
!this._needsRaster(node, rect)
|
|
596
746
|
) {
|
|
597
747
|
return true; // shapes are current
|
|
598
748
|
}
|
|
@@ -738,13 +888,38 @@ export class CocoaLayerPresenter {
|
|
|
738
888
|
}
|
|
739
889
|
this._dropSvgShapes(visual);
|
|
740
890
|
}
|
|
741
|
-
|
|
891
|
+
const passes = this._rasterPasses(node, rect, sizeChanged);
|
|
892
|
+
if (!passes) return;
|
|
742
893
|
const ctx = raster.ensure(this, rect.width, rect.height, this.window.scale);
|
|
894
|
+
// The bitmap is this visual's composition cache, the way the window's
|
|
895
|
+
// backing store is the surface presenter's: a pass over part of it
|
|
896
|
+
// clears and clips to that part and leaves the rest as last frame drew
|
|
897
|
+
// it, and `paintDamage()` names the pass, so an element culls a drag
|
|
898
|
+
// step or an animation tick exactly as it does on X11 (docs/extending.md,
|
|
899
|
+
// "Drawing a scene into one node") instead of replaying its whole scene
|
|
900
|
+
// into a clip that throws almost all of it away. The full pass is the
|
|
901
|
+
// same loop with nothing to clip.
|
|
902
|
+
const root = node.root;
|
|
743
903
|
ctx.save();
|
|
744
904
|
try {
|
|
745
|
-
ctx.clearRect(0, 0, rect.width, rect.height);
|
|
746
905
|
ctx.translate(-rect.x, -rect.y);
|
|
747
|
-
|
|
906
|
+
for (const pass of passes) {
|
|
907
|
+
const area = pass ?? rect;
|
|
908
|
+
ctx.save();
|
|
909
|
+
try {
|
|
910
|
+
if (pass) {
|
|
911
|
+
ctx.beginPath();
|
|
912
|
+
ctx.rect(area.x, area.y, area.width, area.height);
|
|
913
|
+
ctx.clip();
|
|
914
|
+
}
|
|
915
|
+
ctx.clearRect(area.x, area.y, area.width, area.height);
|
|
916
|
+
if (root) root._paintDamage = pass;
|
|
917
|
+
paintSelf(node, ctx);
|
|
918
|
+
} finally {
|
|
919
|
+
if (root) root._paintDamage = null;
|
|
920
|
+
ctx.restore();
|
|
921
|
+
}
|
|
922
|
+
}
|
|
748
923
|
} finally {
|
|
749
924
|
ctx.restore();
|
|
750
925
|
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// An offscreen drawing surface on the Cocoa backend — ntk's `Surface`
|
|
2
|
+
// contract (draw once, composite many, and shift a retained band in place)
|
|
3
|
+
// over one @windowkit/appkit CG bitmap. `react-x11/ntk`'s `Surface` hands
|
|
4
|
+
// out one of these when the app it is given is a Cocoa app (the app's
|
|
5
|
+
// `createSurface` seam, src/cocoa/app.js), so a component allocates its
|
|
6
|
+
// buffer the same way on both backends and names neither:
|
|
7
|
+
//
|
|
8
|
+
// const surface = new Surface(app, { width, height }); // device pixels
|
|
9
|
+
// const ctx = surface.getContext('2d'); // a CocoaContext2D
|
|
10
|
+
// ctx.fillRect(0, 0, width, height);
|
|
11
|
+
// surface.copyWithin({ x: 0, y: 0, width, height }, 0, -rowHeight);
|
|
12
|
+
// windowCtx.drawImage(surface, x, y); // one composite
|
|
13
|
+
//
|
|
14
|
+
// What is the same as ntk's: the constructor, `width`/`height`/`format`/
|
|
15
|
+
// `depth`/`bytes`, `getContext`, `render`, `clear`, `copyWithin` down to its
|
|
16
|
+
// clamping (ntk#252 — integer deltas, the band that survives, false when
|
|
17
|
+
// nothing does), `destroy`/`Symbol.dispose`, and `drawImage` taking the
|
|
18
|
+
// surface as a source. What differs is stated here, because this is where
|
|
19
|
+
// it lives:
|
|
20
|
+
//
|
|
21
|
+
// - **One graphics state per surface.** CoreGraphics keeps the CTM, the
|
|
22
|
+
// clip and the path in the bitmap context itself, where an X connection
|
|
23
|
+
// keeps them per Picture/GC. So `getContext('2d')` answers the same
|
|
24
|
+
// context every time — a JS object over that one state, nothing to free,
|
|
25
|
+
// and `destroy()` on it is a no-op — and `render()` brackets its callback
|
|
26
|
+
// in save/restore from the identity transform, so a one-shot draw leaves
|
|
27
|
+
// no residue for the next painter, which is what ntk gets from building a
|
|
28
|
+
// fresh context per call.
|
|
29
|
+
// - **`format: 'a8'` is not here yet.** Coverage surfaces are what the paint
|
|
30
|
+
// cache's masks and the shadows use, and both stay on their X path; every
|
|
31
|
+
// consumer that allocates a surface of its own asks for argb32. Asking
|
|
32
|
+
// for a8 throws rather than answering a colour surface that would
|
|
33
|
+
// composite differently.
|
|
34
|
+
// - **No Picture.** `picture()` is X's compositing handle; here a surface
|
|
35
|
+
// composites through `ctx.drawImage`, and asking for the picture says so.
|
|
36
|
+
// - **Freed on `destroy()`.** The bridge's `releaseSurface` (0.4) frees the
|
|
37
|
+
// bitmap on the call and hands its bytes back to V8's account; the
|
|
38
|
+
// handle's finalizer stays as the safety net for a surface that is
|
|
39
|
+
// dropped without one. Under an older bridge the memory goes with the
|
|
40
|
+
// next GC, as it always did.
|
|
41
|
+
//
|
|
42
|
+
// Units are device pixels, like the window's backing store: a caller sizes
|
|
43
|
+
// one from `contentBox()` numbers, which are device pixels already
|
|
44
|
+
// (docs/scale.md). The bridge is told the app's scale so the bitmap carries
|
|
45
|
+
// it — inert for a `drawImage` source, right for a layer's contents.
|
|
46
|
+
import { CocoaContext2D } from './context2d.js';
|
|
47
|
+
|
|
48
|
+
export class CocoaSurface {
|
|
49
|
+
constructor(app, { width, height, format = 'argb32' } = {}) {
|
|
50
|
+
if (
|
|
51
|
+
!Number.isInteger(width) ||
|
|
52
|
+
!Number.isInteger(height) ||
|
|
53
|
+
width <= 0 ||
|
|
54
|
+
height <= 0
|
|
55
|
+
) {
|
|
56
|
+
throw new Error('Surface: width and height must be positive integers');
|
|
57
|
+
}
|
|
58
|
+
if (format === 'a8') {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"Surface: format 'a8' (a coverage surface) is not on the cocoa " +
|
|
61
|
+
'backend yet — allocate argb32, which every backend has, and ' +
|
|
62
|
+
'tint through fillStyle/globalAlpha; track docs/macos.md ' +
|
|
63
|
+
'"Custom drawing on a layer tree".',
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (format !== 'argb32') {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Surface: unknown format ${JSON.stringify(format)} (argb32 or a8)`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
this.app = app;
|
|
72
|
+
this.width = width;
|
|
73
|
+
this.height = height;
|
|
74
|
+
this.format = format;
|
|
75
|
+
this.depth = 32;
|
|
76
|
+
this._native = app._native;
|
|
77
|
+
this._fonts = app.fonts ?? null;
|
|
78
|
+
this._ctx = null;
|
|
79
|
+
this._destroyed = false;
|
|
80
|
+
this._surfaceHandle = this._native.createSurface(
|
|
81
|
+
width,
|
|
82
|
+
height,
|
|
83
|
+
app.scale ?? 1,
|
|
84
|
+
);
|
|
85
|
+
// a fresh bitmap's contents are the allocator's; a surface that is only
|
|
86
|
+
// partly drawn must composite nothing where nothing was drawn
|
|
87
|
+
this.clear();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** bytes of backing storage — what a cache budgets against */
|
|
91
|
+
get bytes() {
|
|
92
|
+
return this.width * this.height * 4;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** X's compositing handle, which this backend does not have. */
|
|
96
|
+
picture() {
|
|
97
|
+
throw new Error(
|
|
98
|
+
'Surface: a surface on the cocoa backend has no XRender Picture — ' +
|
|
99
|
+
'composite it with ctx.drawImage(surface, x, y), which takes a ' +
|
|
100
|
+
'surface directly on both backends.',
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_handle() {
|
|
105
|
+
if (this._destroyed) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
'Surface: destroyed — a context on a destroyed surface cannot ' +
|
|
108
|
+
'draw; allocate a new Surface and draw into that.',
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return this._surfaceHandle;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_context() {
|
|
115
|
+
if (!this._ctx) {
|
|
116
|
+
this._ctx = new CocoaContext2D(
|
|
117
|
+
this._native,
|
|
118
|
+
() => this._handle(),
|
|
119
|
+
() => 1,
|
|
120
|
+
);
|
|
121
|
+
this._ctx._fonts = this._fonts;
|
|
122
|
+
}
|
|
123
|
+
return this._ctx;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The 2d context on the bitmap — the same one every time, since the
|
|
128
|
+
* bitmap has one graphics state (see the header). ntk's contract has the
|
|
129
|
+
* caller owning it and owing it a `destroy()`; that call is honoured as a
|
|
130
|
+
* no-op, so a caller written against ntk needs no branch.
|
|
131
|
+
*/
|
|
132
|
+
getContext(name = '2d') {
|
|
133
|
+
this._handle();
|
|
134
|
+
if (name !== '2d') {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Surface: getContext(${JSON.stringify(name)}) — a surface on the ` +
|
|
137
|
+
"cocoa backend has a '2d' context and nothing else.",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return this._context();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Draw into the surface through a context that starts clean — identity
|
|
145
|
+
* transform, the fill and line state as they were — and leaves the
|
|
146
|
+
* surface's state as it found it: the save/restore bracket stands in for
|
|
147
|
+
* the per-call context ntk builds and destroys.
|
|
148
|
+
*/
|
|
149
|
+
render(fn) {
|
|
150
|
+
const ctx = this.getContext('2d');
|
|
151
|
+
ctx.save();
|
|
152
|
+
try {
|
|
153
|
+
ctx.resetTransform();
|
|
154
|
+
fn(ctx);
|
|
155
|
+
} finally {
|
|
156
|
+
ctx.restore();
|
|
157
|
+
}
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Reset every pixel to fully transparent, whatever transform a live
|
|
162
|
+
* context holds — the clear is issued from the identity. */
|
|
163
|
+
clear() {
|
|
164
|
+
if (this._destroyed) return this;
|
|
165
|
+
const ctx = this._context();
|
|
166
|
+
ctx.save();
|
|
167
|
+
try {
|
|
168
|
+
ctx.resetTransform();
|
|
169
|
+
ctx.clearRect(0, 0, this.width, this.height);
|
|
170
|
+
} finally {
|
|
171
|
+
ctx.restore();
|
|
172
|
+
}
|
|
173
|
+
return this;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Scroll the pixels of `src` (surface coordinates, `{x, y, width,
|
|
178
|
+
* height}`) by (dx, dy) in place: one in-place copy of the band that
|
|
179
|
+
* survives the shift (the bridge's `scrollSurface`, a memmove per row), in
|
|
180
|
+
* place of redrawing everything that merely moved. True when the copy was
|
|
181
|
+
* issued; false means "nothing survives the shift here" and the caller
|
|
182
|
+
* repaints `src` exactly as it would have without this method.
|
|
183
|
+
*
|
|
184
|
+
* ntk#252's contract, clamp for clamp: refused when the delta is
|
|
185
|
+
* fractional (a sub-pixel shift changes every pixel), when it is zero,
|
|
186
|
+
* when nothing of `src` survives after clamping to the surface, or on a
|
|
187
|
+
* destroyed surface. The band is `clamped src ∩ (clamped src + delta)`,
|
|
188
|
+
* so nothing outside `src` is written; the overlap is safe because the
|
|
189
|
+
* copy walks rows in the direction that reads before it overwrites.
|
|
190
|
+
*/
|
|
191
|
+
copyWithin(src, dx, dy) {
|
|
192
|
+
if (this._destroyed) return false;
|
|
193
|
+
if (!Number.isInteger(dx) || !Number.isInteger(dy)) return false;
|
|
194
|
+
if (dx === 0 && dy === 0) return false;
|
|
195
|
+
const x0 = Math.max(0, Math.floor(src.x));
|
|
196
|
+
const y0 = Math.max(0, Math.floor(src.y));
|
|
197
|
+
const x1 = Math.min(this.width, Math.ceil(src.x + src.width));
|
|
198
|
+
const y1 = Math.min(this.height, Math.ceil(src.y + src.height));
|
|
199
|
+
const dstX0 = Math.max(x0, x0 + dx);
|
|
200
|
+
const dstY0 = Math.max(y0, y0 + dy);
|
|
201
|
+
const dstX1 = Math.min(x1, x1 + dx);
|
|
202
|
+
const dstY1 = Math.min(y1, y1 + dy);
|
|
203
|
+
// written as the positive test so a NaN edge (a rect with no numbers
|
|
204
|
+
// in it) is a refusal too, never a native call with garbage
|
|
205
|
+
if (!(dstX1 > dstX0 && dstY1 > dstY0)) return false;
|
|
206
|
+
return Boolean(
|
|
207
|
+
this._native.scrollSurface(
|
|
208
|
+
this._surfaceHandle,
|
|
209
|
+
x0,
|
|
210
|
+
y0,
|
|
211
|
+
x1 - x0,
|
|
212
|
+
y1 - y0,
|
|
213
|
+
dx,
|
|
214
|
+
dy,
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
destroy() {
|
|
220
|
+
if (this._destroyed) return;
|
|
221
|
+
this._destroyed = true;
|
|
222
|
+
const handle = this._surfaceHandle;
|
|
223
|
+
this._surfaceHandle = null;
|
|
224
|
+
this._ctx = null;
|
|
225
|
+
if (typeof this._native.releaseSurface === 'function') {
|
|
226
|
+
this._native.releaseSurface(handle);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
[Symbol.dispose]() {
|
|
231
|
+
this.destroy();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export default CocoaSurface;
|