react-x11 2.6.0 → 2.7.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.
Files changed (60) hide show
  1. package/README.md +5 -3
  2. package/package.json +10 -3
  3. package/src/activate.js +12 -0
  4. package/src/anchor.js +6 -0
  5. package/src/appearance.js +351 -28
  6. package/src/appearancehooks.js +5 -2
  7. package/src/application.js +41 -0
  8. package/src/cocoa/app.js +358 -20
  9. package/src/cocoa/bezels.js +51 -1
  10. package/src/cocoa/context2d.js +271 -25
  11. package/src/cocoa/dnd.js +347 -0
  12. package/src/cocoa/dock.js +39 -0
  13. package/src/cocoa/filepanels.js +155 -0
  14. package/src/cocoa/fonts.js +93 -2
  15. package/src/cocoa/globalmenu.js +41 -33
  16. package/src/cocoa/notifications.js +244 -0
  17. package/src/cocoa/permissions.js +74 -0
  18. package/src/cocoa/presenter.js +190 -2
  19. package/src/cocoa/statusitem.js +112 -0
  20. package/src/cocoa/window.js +85 -4
  21. package/src/components/Button.js +20 -1
  22. package/src/components/Checkbox.js +17 -2
  23. package/src/components/Menu.js +108 -38
  24. package/src/components/Radio.js +17 -2
  25. package/src/components/Select.js +159 -27
  26. package/src/components/Switch.js +8 -1
  27. package/src/components/native.js +99 -0
  28. package/src/components/theme.js +37 -20
  29. package/src/desktopsettings.js +34 -2
  30. package/src/dnd.js +92 -3
  31. package/src/errors.js +6 -3
  32. package/src/filedialog.js +81 -16
  33. package/src/index.d.ts +17 -1
  34. package/src/index.js +17 -0
  35. package/src/launcher.js +170 -0
  36. package/src/launcherhooks.js +81 -0
  37. package/src/nodes.js +553 -35
  38. package/src/notificationhooks.js +56 -0
  39. package/src/notifications.js +558 -0
  40. package/src/palette.js +144 -8
  41. package/src/permissionhooks.js +89 -0
  42. package/src/permissions.js +196 -0
  43. package/src/screens.js +39 -4
  44. package/src/style.d.ts +10 -4
  45. package/src/style.js +1 -0
  46. package/src/styles.js +161 -15
  47. package/src/textselection.js +1 -4
  48. package/src/trayhooks.js +90 -0
  49. package/src/types/appearance.d.ts +24 -0
  50. package/src/types/components.d.ts +10 -0
  51. package/src/types/elements.d.ts +14 -0
  52. package/src/types/events.d.ts +14 -0
  53. package/src/types/filedialog.d.ts +18 -7
  54. package/src/types/launcher.d.ts +43 -0
  55. package/src/types/notifications.d.ts +113 -0
  56. package/src/types/permissions.d.ts +100 -0
  57. package/src/types/style.d.ts +30 -2
  58. package/src/types/system.d.ts +5 -3
  59. package/src/types/tray.d.ts +54 -0
  60. package/src/windowid.js +23 -0
@@ -92,6 +92,61 @@ const clamp01 = (v) => Math.min(1, Math.max(0, Number(v) || 0));
92
92
  const PICT_OP = Object.freeze({ Src: 1, Over: 3 });
93
93
  const RENDER = Object.freeze({ PictOp: PICT_OP });
94
94
 
95
+ /**
96
+ * The path, recorded alongside the native one, so `stroke` can re-issue it
97
+ * in pieces — `CGContextStrokePath` is QUADRATIC in the number of subpaths
98
+ * in the path it is given (issue #456). Measured here, 13-vertex closed
99
+ * rings scattered over a 1024x1024 surface at a 2px line, one stroke call:
100
+ *
101
+ * 500 rings 20ms | 1000 rings 59ms | 2000 rings 208ms | 4000 rings 986ms
102
+ *
103
+ * Splitting the same geometry into strokes of a few hundred subpaths is
104
+ * linear in it: 14ms, 29ms, 56ms, 113ms. The driver is the subpath count,
105
+ * not the vertex count — one 26,000-vertex subpath strokes in 4ms where
106
+ * two thousand 13-vertex ones take 208. The full table, the two shapes
107
+ * left whole and why, are in docs/macos.md
108
+ * §"Stroking a path with many subpaths".
109
+ *
110
+ * Note the X11 context wants the opposite — there a stroke is an a8
111
+ * coverage mask over the path's bounding box uploaded with one PutImage,
112
+ * so a bigger path is fewer uploads over the same pixels. That is why this
113
+ * lives in the backend: a caller that batches for one backend pessimizes
114
+ * the other, and it cannot know which it is drawing on.
115
+ *
116
+ * The commands are a flat number array — `[op, ...args, op, ...args]` —
117
+ * reused across paths, so a path build is three pushes into a packed
118
+ * double array per point next to the napi call it already makes.
119
+ */
120
+ const P_MOVE = 0;
121
+ const P_LINE = 1;
122
+ const P_CURVE = 2;
123
+ const P_QUAD = 3;
124
+ const P_CLOSE = 4;
125
+ const P_RECT = 5;
126
+ const P_ROUND = 6;
127
+ const P_ARC = 7;
128
+ const P_ELLIPSE = 8;
129
+ /** how many numbers each op carries, and what it costs a chunk's budget */
130
+ const P_ARGS = [2, 2, 6, 4, 0, 4, 8, 6, 4];
131
+ const P_POINTS = [1, 1, 3, 2, 0, 4, 8, 8, 4];
132
+
133
+ /**
134
+ * A chunk closes at the first subpath boundary past either budget. Swept
135
+ * over the shapes above: 512 points is within 5% of the best chunk for
136
+ * every one of them, and the subpath cap catches the degenerate shape the
137
+ * point budget misses — thousands of 3- and 4-point subpaths, where 512
138
+ * points is already 128 strokes' worth of setup.
139
+ */
140
+ const STROKE_CHUNK_POINTS = 512;
141
+ const STROKE_CHUNK_SUBPATHS = 128;
142
+
143
+ /**
144
+ * The off switch, for a process that cannot reach the context — the same
145
+ * line `ctx.strokeChunking = false` draws, and what a bench comparing the
146
+ * two sets. Read once.
147
+ */
148
+ const NO_STROKE_CHUNKING = process.env.REACT_X11_NO_STROKE_CHUNKING === '1';
149
+
95
150
  /**
96
151
  * A solid ink for `drawGlyphs` — ntk's `createSolidPicture` answers an
97
152
  * XRender picture; here it is the colour itself. Premultiplied 0..1 in, as
@@ -136,6 +191,11 @@ export class CocoaContext2D {
136
191
  ctm: [1, 0, 0, 1, 0, 0],
137
192
  };
138
193
  this._onDirty = null;
194
+ // the recorded path, and whether the native one still matches it (a
195
+ // chunked stroke leaves only its last chunk behind)
196
+ this._cmds = [];
197
+ this._pathStale = false;
198
+ this._strokeChunking = !NO_STROKE_CHUNKING;
139
199
  }
140
200
 
141
201
  _s() {
@@ -152,6 +212,10 @@ export class CocoaContext2D {
152
212
  n.ctxSetGlobalAlpha(surface, st.globalAlpha);
153
213
  n.ctxSetLineDash(surface, st.dash, st.dashOffset);
154
214
  this._stack.length = 0;
215
+ // the path went with the surface it was built on; nothing may
216
+ // replay it onto the new one
217
+ this._cmds.length = 0;
218
+ this._pathStale = false;
155
219
  }
156
220
  return surface;
157
221
  }
@@ -218,6 +282,26 @@ export class CocoaContext2D {
218
282
  }
219
283
  }
220
284
 
285
+ /**
286
+ * Whether a stroke of a path with many subpaths may go out as several
287
+ * `CGContextStrokePath` calls — on by default, because the alternative
288
+ * is quadratic (see P_MOVE above) and every path small enough for the
289
+ * difference to be invisible is below the threshold anyway.
290
+ *
291
+ * Set it false for a path whose subpaths OVERLAP and whose seams have to
292
+ * composite exactly: chunked, a pixel the strokes of two subpaths each
293
+ * half cover is inked twice at half coverage rather than once at full,
294
+ * and reads a little lighter. `REACT_X11_NO_STROKE_CHUNKING=1` is the
295
+ * same switch for a whole process.
296
+ */
297
+ get strokeChunking() {
298
+ return this._strokeChunking;
299
+ }
300
+
301
+ set strokeChunking(value) {
302
+ this._strokeChunking = !!value;
303
+ }
304
+
221
305
  get font() {
222
306
  return this._state.font;
223
307
  }
@@ -389,20 +473,96 @@ export class CocoaContext2D {
389
473
 
390
474
  // --- paths ---------------------------------------------------------------
391
475
 
476
+ /**
477
+ * The surface to build or paint the current path on, with the native
478
+ * path restored first if a chunked stroke consumed it. Lazy on purpose:
479
+ * a caller that strokes and then starts a new path — which is every
480
+ * caller in a paint loop — never pays for the rebuild.
481
+ */
482
+ _path() {
483
+ const surface = this._s();
484
+ if (this._pathStale) {
485
+ this._pathStale = false;
486
+ this._native.ctxBeginPath(surface);
487
+ this._emit(surface, 0, this._cmds.length);
488
+ }
489
+ return surface;
490
+ }
491
+
492
+ /** replay recorded commands `[from, to)` into the native path */
493
+ _emit(surface, from, to) {
494
+ const c = this._cmds;
495
+ const n = this._native;
496
+ for (let i = from; i < to;) {
497
+ const op = c[i];
498
+ const a = i + 1;
499
+ if (op === P_MOVE) n.ctxMoveTo(surface, c[a], c[a + 1]);
500
+ else if (op === P_LINE) n.ctxLineTo(surface, c[a], c[a + 1]);
501
+ else if (op === P_CURVE)
502
+ n.ctxCurveTo(
503
+ surface,
504
+ c[a],
505
+ c[a + 1],
506
+ c[a + 2],
507
+ c[a + 3],
508
+ c[a + 4],
509
+ c[a + 5],
510
+ );
511
+ else if (op === P_QUAD)
512
+ n.ctxQuadTo(surface, c[a], c[a + 1], c[a + 2], c[a + 3]);
513
+ else if (op === P_CLOSE) n.ctxClosePath(surface);
514
+ else if (op === P_RECT)
515
+ n.ctxRect(surface, c[a], c[a + 1], c[a + 2], c[a + 3]);
516
+ else if (op === P_ROUND)
517
+ n.ctxRoundRect(
518
+ surface,
519
+ c[a],
520
+ c[a + 1],
521
+ c[a + 2],
522
+ c[a + 3],
523
+ c[a + 4],
524
+ c[a + 5],
525
+ c[a + 6],
526
+ c[a + 7],
527
+ );
528
+ else if (op === P_ARC)
529
+ n.ctxArc(
530
+ surface,
531
+ c[a],
532
+ c[a + 1],
533
+ c[a + 2],
534
+ c[a + 3],
535
+ c[a + 4],
536
+ !!c[a + 5],
537
+ );
538
+ else if (op === P_ELLIPSE)
539
+ n.ctxEllipse(surface, c[a], c[a + 1], c[a + 2], c[a + 3]);
540
+ i += 1 + P_ARGS[op];
541
+ }
542
+ }
543
+
392
544
  beginPath() {
545
+ this._cmds.length = 0;
546
+ this._pathStale = false;
393
547
  this._native.ctxBeginPath(this._s());
394
548
  }
395
549
 
396
550
  moveTo(x, y) {
397
- this._native.ctxMoveTo(this._s(), x, y);
551
+ const surface = this._path();
552
+ this._cmds.push(P_MOVE, x, y);
553
+ this._native.ctxMoveTo(surface, x, y);
398
554
  }
399
555
 
400
556
  lineTo(x, y) {
401
- this._native.ctxLineTo(this._s(), x, y);
557
+ const surface = this._path();
558
+ this._cmds.push(P_LINE, x, y);
559
+ this._native.ctxLineTo(surface, x, y);
402
560
  }
403
561
 
404
562
  rect(x, y, w, h) {
405
- this._native.ctxRect(this._s(), x, y, w, h);
563
+ const surface = this._path();
564
+ this._cmds.push(P_RECT, x, y, w, h);
565
+ this._native.ctxRect(surface, x, y, w, h);
406
566
  }
407
567
 
408
568
  roundRect(x, y, w, h, radii) {
@@ -413,37 +573,45 @@ export class CocoaContext2D {
413
573
  else if (r.length === 3) r = [r[0], r[1], r[2], r[1]];
414
574
  const cap = Math.min(Math.abs(w) / 2, Math.abs(h) / 2);
415
575
  const clamp = (v) => Math.max(0, Math.min(Number(v) || 0, cap));
416
- this._native.ctxRoundRect(
417
- this._s(),
418
- x,
419
- y,
420
- w,
421
- h,
576
+ const surface = this._path();
577
+ const [r0, r1, r2, r3] = [
422
578
  clamp(r[0]),
423
579
  clamp(r[1]),
424
580
  clamp(r[2]),
425
581
  clamp(r[3]),
426
- );
582
+ ];
583
+ this._cmds.push(P_ROUND, x, y, w, h, r0, r1, r2, r3);
584
+ this._native.ctxRoundRect(surface, x, y, w, h, r0, r1, r2, r3);
427
585
  }
428
586
 
429
587
  arc(x, y, radius, start, end, anticlockwise = false) {
430
- this._native.ctxArc(this._s(), x, y, radius, start, end, anticlockwise);
588
+ const surface = this._path();
589
+ this._cmds.push(P_ARC, x, y, radius, start, end, anticlockwise ? 1 : 0);
590
+ this._native.ctxArc(surface, x, y, radius, start, end, anticlockwise);
431
591
  }
432
592
 
433
593
  ellipse(x, y, rx, ry) {
434
- this._native.ctxEllipse(this._s(), x, y, rx, ry);
594
+ const surface = this._path();
595
+ this._cmds.push(P_ELLIPSE, x, y, rx, ry);
596
+ this._native.ctxEllipse(surface, x, y, rx, ry);
435
597
  }
436
598
 
437
599
  bezierCurveTo(c1x, c1y, c2x, c2y, x, y) {
438
- this._native.ctxCurveTo(this._s(), c1x, c1y, c2x, c2y, x, y);
600
+ const surface = this._path();
601
+ this._cmds.push(P_CURVE, c1x, c1y, c2x, c2y, x, y);
602
+ this._native.ctxCurveTo(surface, c1x, c1y, c2x, c2y, x, y);
439
603
  }
440
604
 
441
605
  quadraticCurveTo(cx, cy, x, y) {
442
- this._native.ctxQuadTo(this._s(), cx, cy, x, y);
606
+ const surface = this._path();
607
+ this._cmds.push(P_QUAD, cx, cy, x, y);
608
+ this._native.ctxQuadTo(surface, cx, cy, x, y);
443
609
  }
444
610
 
445
611
  closePath() {
446
- this._native.ctxClosePath(this._s());
612
+ const surface = this._path();
613
+ this._cmds.push(P_CLOSE);
614
+ this._native.ctxClosePath(surface);
447
615
  }
448
616
 
449
617
  // --- painting ------------------------------------------------------------
@@ -477,24 +645,95 @@ export class CocoaContext2D {
477
645
  _replayPath(path) {
478
646
  const cmds = path?._cmds;
479
647
  if (!Array.isArray(cmds)) return false;
480
- const n = this._native;
481
- const s = this._s();
482
- n.ctxBeginPath(s);
648
+ this.beginPath();
483
649
  for (const c of cmds) {
484
- if (c.type === 'M') n.ctxMoveTo(s, c.x, c.y);
485
- else if (c.type === 'L') n.ctxLineTo(s, c.x, c.y);
650
+ if (c.type === 'M') this.moveTo(c.x, c.y);
651
+ else if (c.type === 'L') this.lineTo(c.x, c.y);
486
652
  else if (c.type === 'C')
487
- n.ctxCurveTo(s, c.x1, c.y1, c.x2, c.y2, c.x, c.y);
488
- else if (c.type === 'Q') n.ctxQuadTo(s, c.x1, c.y1, c.x, c.y);
489
- else if (c.type === 'Z') n.ctxClosePath(s);
653
+ this.bezierCurveTo(c.x1, c.y1, c.x2, c.y2, c.x, c.y);
654
+ else if (c.type === 'Q') this.quadraticCurveTo(c.x1, c.y1, c.x, c.y);
655
+ else if (c.type === 'Z') this.closePath();
490
656
  }
491
657
  return true;
492
658
  }
493
659
 
660
+ /**
661
+ * Whether a stroke of the current path may be split into several
662
+ * `CGContextStrokePath` calls. Two things say no:
663
+ *
664
+ * - A hairline. At or below a device-space width of 1 CoreGraphics
665
+ * strokes through a path that is already linear in the subpath count
666
+ * — 2,000 rings cost 7ms whole — and splitting it is a 2x LOSS, the
667
+ * per-call setup with nothing to win back.
668
+ * - Anything that composites. Each chunk paints separately, so where two
669
+ * subpaths' strokes overlap, a translucent ink, a globalAlpha or a
670
+ * shadow blends twice and reads darker where one call blends the union
671
+ * once. An opaque ink is exact everywhere the coverage is full, which
672
+ * is what the geometry that gets big looks like; what is left is the
673
+ * antialiased fringe at those same overlaps, half-covered twice
674
+ * instead of covered once, which reads a little lighter — the one
675
+ * difference `strokeChunking` exists to turn off.
676
+ */
677
+ _chunkableStroke() {
678
+ if (!this._strokeChunking) return false;
679
+ const st = this._state;
680
+ const [a, b, c, d] = st.ctm;
681
+ const scale = Math.sqrt(Math.abs(a * d - b * c));
682
+ if (!(st.lineWidth * scale > 1)) return false;
683
+ if (st.globalAlpha < 1) return false;
684
+ if (parseColor(st.strokeStyle)[3] < 1) return false;
685
+ if (st.shadowBlur > 0 && parseColor(st.shadowColor)[3] > 0) return false;
686
+ return true;
687
+ }
688
+
689
+ /**
690
+ * Stroke the recorded path as a series of chunks, cut at subpath
691
+ * boundaries so every chunk carries the `moveTo` its segments start
692
+ * from. Answers false when there was nothing to split, leaving the
693
+ * native path untouched for the caller's single stroke.
694
+ */
695
+ _strokeChunks(surface) {
696
+ if (!this._chunkableStroke()) return false;
697
+ const cmds = this._cmds;
698
+ const n = this._native;
699
+ let start = 0;
700
+ let points = 0;
701
+ let subpaths = 0;
702
+ let split = false;
703
+ for (let i = 0; i < cmds.length;) {
704
+ const op = cmds[i];
705
+ if (
706
+ op === P_MOVE &&
707
+ i > start &&
708
+ (points >= STROKE_CHUNK_POINTS || subpaths >= STROKE_CHUNK_SUBPATHS)
709
+ ) {
710
+ n.ctxBeginPath(surface);
711
+ this._emit(surface, start, i);
712
+ n.ctxStroke(surface);
713
+ split = true;
714
+ start = i;
715
+ points = 0;
716
+ subpaths = 0;
717
+ }
718
+ if (op === P_MOVE) subpaths++;
719
+ points += P_POINTS[op];
720
+ i += 1 + P_ARGS[op];
721
+ }
722
+ if (!split) return false;
723
+ n.ctxBeginPath(surface);
724
+ this._emit(surface, start, cmds.length);
725
+ n.ctxStroke(surface);
726
+ // the native path is the last chunk now; _path() puts the whole one
727
+ // back if anything asks for it
728
+ this._pathStale = true;
729
+ return true;
730
+ }
731
+
494
732
  fill(pathOrRule, maybeRule) {
495
733
  const hasPath = pathOrRule != null && typeof pathOrRule === 'object';
496
734
  const rule = hasPath ? maybeRule : pathOrRule;
497
735
  if (hasPath && !this._replayPath(pathOrRule)) return;
736
+ this._path();
498
737
  const style = this._state.fillStyle;
499
738
  if (style instanceof LinearGradient) {
500
739
  const { coords, flat } = style._normalized();
@@ -518,7 +757,14 @@ export class CocoaContext2D {
518
757
  return;
519
758
  }
520
759
  this._applyStroke();
521
- this._native.ctxStroke(this._s());
760
+ // one call per chunk where the path has enough subpaths to be worth it
761
+ // — see P_MOVE and _chunkableStroke above — and one for everything
762
+ // else. The chunks are issued from the record, so a stroke that splits
763
+ // never pays for the restore a previous split owed: `_path()` is asked
764
+ // for the surface only on the whole-path route.
765
+ if (!this._strokeChunks(this._s())) {
766
+ this._native.ctxStroke(this._path());
767
+ }
522
768
  this._dirty();
523
769
  }
524
770
 
@@ -530,7 +776,7 @@ export class CocoaContext2D {
530
776
  ) {
531
777
  return;
532
778
  }
533
- this._native.ctxClip(this._s());
779
+ this._native.ctxClip(this._path());
534
780
  }
535
781
 
536
782
  fillRect(x, y, w, h) {