react-x11 2.6.1 → 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.
Files changed (59) 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 +367 -3
  9. package/src/cocoa/bezels.js +51 -1
  10. package/src/cocoa/dnd.js +358 -0
  11. package/src/cocoa/dock.js +39 -0
  12. package/src/cocoa/filepanels.js +155 -0
  13. package/src/cocoa/fonts.js +93 -2
  14. package/src/cocoa/globalmenu.js +41 -33
  15. package/src/cocoa/notifications.js +244 -0
  16. package/src/cocoa/permissions.js +74 -0
  17. package/src/cocoa/presenter.js +274 -33
  18. package/src/cocoa/promotion.js +708 -0
  19. package/src/cocoa/statusitem.js +112 -0
  20. package/src/cocoa/window.js +113 -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 +137 -11
  31. package/src/errors.js +6 -3
  32. package/src/filedialog.js +81 -16
  33. package/src/index.d.ts +29 -2
  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 +604 -37
  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/style.d.ts +10 -4
  44. package/src/style.js +1 -0
  45. package/src/styles.js +161 -15
  46. package/src/textselection.js +1 -4
  47. package/src/trayhooks.js +90 -0
  48. package/src/types/appearance.d.ts +24 -0
  49. package/src/types/components.d.ts +10 -0
  50. package/src/types/elements.d.ts +14 -0
  51. package/src/types/events.d.ts +14 -0
  52. package/src/types/filedialog.d.ts +18 -7
  53. package/src/types/launcher.d.ts +43 -0
  54. package/src/types/notifications.d.ts +113 -0
  55. package/src/types/permissions.d.ts +100 -0
  56. package/src/types/style.d.ts +30 -2
  57. package/src/types/system.d.ts +5 -3
  58. package/src/types/tray.d.ts +54 -0
  59. package/src/windowid.js +23 -0
@@ -32,9 +32,10 @@ import {
32
32
  damageToPaint,
33
33
  intersectRects,
34
34
  } from '../nodes.js';
35
+ import { EASING_CONTROL_POINTS, TRANSITION_CONTROL_POINTS } from '../styles.js';
35
36
  import { CocoaContext2D } from './context2d.js';
36
37
 
37
- const RASTER_PAD = 2; // antialiasing/italic overhang outside the ink bounds
38
+ export const RASTER_PAD = 2; // antialiasing/italic overhang outside the ink bounds
38
39
 
39
40
  /**
40
41
  * A recording "context" for ntk's SvgView.draw: instead of rasterizing, it
@@ -324,9 +325,14 @@ const EDGE_PROPS = [
324
325
  'borderEndWidth',
325
326
  ];
326
327
 
327
- function stylePaintsPlain(node) {
328
+ /**
329
+ * Does `style` draw as a plain box — the property vocabulary of a layer
330
+ * (background, a uniform solid border, one radius), nothing that needs a
331
+ * raster? What decides whether a node is a PropBox on the layer presenter,
332
+ * and whether the surface presenter can promote it (src/cocoa/promotion.js).
333
+ */
334
+ export function stylePaintsPlain(node, style = node.style ?? {}) {
328
335
  if (node.kind !== 'box') return false;
329
- const style = node.style ?? {};
330
336
  if (style.backgroundImage || style.boxShadow || style.outlineWidth) {
331
337
  return false;
332
338
  }
@@ -339,13 +345,33 @@ function stylePaintsPlain(node) {
339
345
  return uniformRadius(style.borderRadius) !== null;
340
346
  }
341
347
 
342
- function uniformRadius(radius) {
348
+ /**
349
+ * Style property → the key path on a PropBox's own layer, for the
350
+ * animations the presenter takes off the frame clock and hands to the
351
+ * render server (docs/architecture/animation.md §4.2). Only what the layer
352
+ * expresses as a property of itself qualifies: a colour on `<text>` is a
353
+ * re-raster per frame, a layout property moves the siblings, and both stay
354
+ * on the JS loop. `scaled` values go out in points, as `_syncPropBox`
355
+ * sends them.
356
+ */
357
+ const ANIMATED_KEY_PATHS = Object.freeze({
358
+ backgroundColor: { keyPath: 'backgroundColor', colour: true },
359
+ borderColor: { keyPath: 'borderColor', colour: true },
360
+ borderWidth: { keyPath: 'borderWidth', scaled: true },
361
+ borderRadius: { keyPath: 'cornerRadius', scaled: true },
362
+ });
363
+
364
+ // the ids the bridge reports an animation's end under: unique per process,
365
+ // looked up per app (`_animationEnds`)
366
+ let animationSeq = 0;
367
+
368
+ export function uniformRadius(radius) {
343
369
  if (radius === undefined) return 0;
344
370
  if (typeof radius === 'number') return radius;
345
371
  return null; // per-corner shapes go to raster
346
372
  }
347
373
 
348
- class Visual {
374
+ export class Visual {
349
375
  constructor(presenter, node) {
350
376
  this.presenter = presenter;
351
377
  this.node = node;
@@ -389,7 +415,7 @@ class Visual {
389
415
  }
390
416
  }
391
417
 
392
- class RasterState {
418
+ export class RasterState {
393
419
  constructor() {
394
420
  this.surface = null;
395
421
  this.gen = 0;
@@ -430,6 +456,218 @@ class RasterState {
430
456
  }
431
457
  }
432
458
 
459
+ /**
460
+ * A plain box as the properties of its layer — what a PropBox visual sends
461
+ * on the layer presenter and what a promoted node's layer is set from on
462
+ * the surface presenter (src/cocoa/promotion.js): one function, so a box
463
+ * reads the same on either. `parentOrigin` is what `frame` is relative to,
464
+ * in window coordinates; everything goes out in points.
465
+ */
466
+ export function propBoxProps(node, app, scale, parentOrigin, order) {
467
+ const abs = node.abs;
468
+ const style = node.style ?? {};
469
+ const border = typeof style.borderWidth === 'number' ? style.borderWidth : 0;
470
+ const colour = (value) =>
471
+ value ? (app._parseColor(String(value)) ?? [0, 0, 0, 0]) : [0, 0, 0, 0];
472
+ return {
473
+ frame: [
474
+ (abs.x - parentOrigin.x) / scale,
475
+ (abs.y - parentOrigin.y) / scale,
476
+ Math.max(0, abs.width) / scale,
477
+ Math.max(0, abs.height) / scale,
478
+ ],
479
+ zPosition: order,
480
+ hidden: Boolean(node.hidden),
481
+ masksToBounds: Boolean(node.clipsChildren?.()) && !node.isScroller?.(),
482
+ cornerRadius: (uniformRadius(style.borderRadius) ?? 0) / scale,
483
+ backgroundColor: colour(style.backgroundColor),
484
+ borderWidth: border / scale,
485
+ borderColor: colour(style.borderColor),
486
+ };
487
+ }
488
+
489
+ // --- animations the render server runs ---------------------------------------
490
+ //
491
+ // The node model keeps deciding what is animating and when it ends
492
+ // (nodes.js `_retarget` / `_updateLoops`); what moves here is who
493
+ // interpolates. Taken means the node's style goes to its target — the
494
+ // layer's model value, sent by the next frame's property diff — and that
495
+ // frame attaches an explicit animation carrying the pixels there; no frame
496
+ // after it is scheduled for the property, and a loop costs no JS frames at
497
+ // all. Declined means the frame clock runs it exactly as before, so
498
+ // nothing here is load-bearing for correctness.
499
+ //
500
+ // Kept apart from the presenter because two of them hand animations over:
501
+ // the layer presenter, where every plain box has a layer, and the surface
502
+ // presenter's promotion (src/cocoa/promotion.js), where only the nodes
503
+ // that animate do. They differ in which node has a layer and agree on
504
+ // everything from there — `layerOf(node)` is the whole of the difference.
505
+
506
+ /**
507
+ * The animations one presenter has handed to the render server: what
508
+ * `take` accepted and is waiting for the frame that attaches it, and what
509
+ * is running on a layer. `onIdle(node)`, when given, hears that the last
510
+ * animation running for a node ended on the bridge's word — the one end
511
+ * no frame follows on its own.
512
+ */
513
+ export class LayerAnimations {
514
+ constructor({ native, app, scale, layerOf, onIdle = null }) {
515
+ this.native = native;
516
+ this.app = app;
517
+ this.scale = scale;
518
+ this.layerOf = layerOf;
519
+ this.onIdle = onIdle;
520
+ this.pending = new Map(); // node -> Map(prop -> entry)
521
+ this.live = new Map(); // node -> Map(id -> { prop, key, entry })
522
+ }
523
+
524
+ /**
525
+ * Take `prop`'s animation for `node`, or decline. Decided against the
526
+ * *target* style: a `:hover` that adds a shadow turns the node into a
527
+ * raster in the same swap that starts a fade, and a raster's background
528
+ * is in its bitmap, not on its layer.
529
+ */
530
+ take(node, prop, entry) {
531
+ const map = ANIMATED_KEY_PATHS[prop];
532
+ if (!map || !stylePaintsPlain(node, node._targetStyle ?? node.style)) {
533
+ return false;
534
+ }
535
+ if (this._value(map, entry.from) == null) return false;
536
+ if (this._value(map, entry.to) == null) return false;
537
+ let pending = this.pending.get(node);
538
+ if (!pending) this.pending.set(node, (pending = new Map()));
539
+ pending.set(prop, entry);
540
+ return true;
541
+ }
542
+
543
+ /** Stop what runs for `prop` on `node`: a loop the window lost sight of,
544
+ * a declaration that changed, a transition the clock takes back. */
545
+ cancel(node, prop) {
546
+ this.pending.get(node)?.delete(prop);
547
+ this._removeLive(node, prop);
548
+ }
549
+
550
+ /** Is anything taken for `node` — waiting for a frame, or running? */
551
+ has(node) {
552
+ return this.pending.has(node) || this.live.has(node);
553
+ }
554
+
555
+ _ends() {
556
+ return (this.app._animationEnds ??= new Map());
557
+ }
558
+
559
+ /** A style value as the layer takes it — a colour as components, a
560
+ * length in points — or null for one the layer cannot animate. */
561
+ _value(map, value) {
562
+ if (map.colour) {
563
+ return typeof value === 'string'
564
+ ? (this.app._parseColor(value) ?? null)
565
+ : null;
566
+ }
567
+ return typeof value === 'number'
568
+ ? value / (map.scaled ? this.scale : 1)
569
+ : null;
570
+ }
571
+
572
+ /** The frame's half: attach what `take` accepted to `layer`, after the
573
+ * model value went out, inside the same transaction. */
574
+ apply(node, layer) {
575
+ const pending = this.pending.get(node);
576
+ if (!pending) return;
577
+ this.pending.delete(node);
578
+ for (const [prop, entry] of pending) {
579
+ const map = ANIMATED_KEY_PATHS[prop];
580
+ const id = `rx${++animationSeq}`;
581
+ const key = `${prop}:${id}`;
582
+ const opts = { duration: entry.duration / 1000, id };
583
+ if (entry.loop) {
584
+ // a loop replaces whatever ran for the property: its declaration
585
+ // changed, and a loop restarts from the top when it does
586
+ this._removeLive(node, prop);
587
+ opts.from = this._value(map, entry.from);
588
+ opts.to = this._value(map, entry.to);
589
+ opts.timing = EASING_CONTROL_POINTS[entry.easing];
590
+ opts.repeat = Infinity;
591
+ opts.autoreverse = entry.alternate;
592
+ } else if (map.colour) {
593
+ // From where the pixels are — which is what "an interrupted
594
+ // transition reverses from where it got to" means here. A colour
595
+ // cannot be additive, so the one before it is replaced.
596
+ this._removeLive(node, prop);
597
+ const shown = this.native.presentationValue?.(layer, map.keyPath);
598
+ opts.from = Array.isArray(shown) ? shown : this._value(map, entry.from);
599
+ opts.to = this._value(map, entry.to);
600
+ opts.timing = TRANSITION_CONTROL_POINTS;
601
+ } else {
602
+ // Additive: a delta over the model value, (old − new) → 0, and the
603
+ // ones before it keep running and sum. Continuity on a retarget with
604
+ // nothing read back, however many are in flight.
605
+ opts.from = this._value(map, entry.from) - this._value(map, entry.to);
606
+ opts.to = 0;
607
+ opts.additive = true;
608
+ opts.timing = TRANSITION_CONTROL_POINTS;
609
+ }
610
+ this.native.addAnimation(layer, map.keyPath, opts, key);
611
+ // looked up after the removals above, which may have pruned the map
612
+ let live = this.live.get(node);
613
+ if (!live) this.live.set(node, (live = new Map()));
614
+ live.set(id, { prop, key, entry });
615
+ this._ends().set(id, (ev) => this._animationEnded(node, id, ev));
616
+ }
617
+ }
618
+
619
+ /** Every animation running for `prop` on `node`, off the layer and out
620
+ * of the books. */
621
+ _removeLive(node, prop) {
622
+ const live = this.live.get(node);
623
+ if (!live) return;
624
+ const layer = this.layerOf(node);
625
+ for (const [id, run] of live) {
626
+ if (run.prop !== prop) continue;
627
+ live.delete(id);
628
+ this._ends().delete(id);
629
+ if (layer) this.native.removeAnimation(layer, run.key);
630
+ }
631
+ if (live.size === 0) this.live.delete(node);
632
+ }
633
+
634
+ /** The bridge's `animation-end` for one of ours — it ran out, or CA
635
+ * dropped it. An older additive one ending changes nothing: the node
636
+ * checks the entry is still the one it holds. */
637
+ _animationEnded(node, id) {
638
+ this._ends().delete(id);
639
+ const live = this.live.get(node);
640
+ const run = live?.get(id);
641
+ if (!run) return;
642
+ live.delete(id);
643
+ if (live.size === 0) this.live.delete(node);
644
+ node._offloadEnded(run.prop, run.entry);
645
+ if (!this.has(node)) this.onIdle?.(node);
646
+ }
647
+
648
+ /** The layer is going — the visual is destroyed, or turns into a raster —
649
+ * and every animation on it goes with it. What was still waiting for a
650
+ * frame goes back to the frame clock when the node stays (`reclaim`);
651
+ * what was running is over, and the model shows. */
652
+ drop(node, reclaim) {
653
+ const pending = this.pending.get(node);
654
+ if (pending) {
655
+ this.pending.delete(node);
656
+ for (const [prop, entry] of pending) {
657
+ if (reclaim) node._offloadDeclined(prop, entry);
658
+ else node._offloadEnded(prop, entry);
659
+ }
660
+ }
661
+ const live = this.live.get(node);
662
+ if (!live) return;
663
+ this.live.delete(node);
664
+ for (const [id, run] of live) {
665
+ this._ends().delete(id);
666
+ node._offloadEnded(run.prop, run.entry);
667
+ }
668
+ }
669
+ }
670
+
433
671
  export class CocoaLayerPresenter {
434
672
  constructor(window) {
435
673
  this.window = window;
@@ -439,6 +677,13 @@ export class CocoaLayerPresenter {
439
677
  this.visuals = new Map(); // node -> Visual
440
678
  this.rasters = new Map(); // node -> RasterState
441
679
  this.bars = new Map(); // scroller node -> Map(axis -> { layer, raster })
680
+ // animations taken off the frame clock, by the node's own layer
681
+ this.animations = new LayerAnimations({
682
+ native: this.native,
683
+ app: window.app,
684
+ scale: this.scale,
685
+ layerOf: (node) => this.visuals.get(node)?.layer ?? null,
686
+ });
442
687
  // Claims since the last frame — taken at the top of `frame()`, the way
443
688
  // the X11 path takes its damage before painting, so a claim made from
444
689
  // inside a paint lands in the next frame instead of being cleared with
@@ -548,6 +793,7 @@ export class CocoaLayerPresenter {
548
793
  visual.destroy();
549
794
  this.visuals.delete(node);
550
795
  this._dropRaster(node);
796
+ this.animations.drop(node, false);
551
797
  const bars = this.bars.get(node);
552
798
  if (bars) {
553
799
  for (const entry of bars.values()) {
@@ -612,8 +858,14 @@ export class CocoaLayerPresenter {
612
858
  if (visual && visual.isRaster !== wantsRaster) {
613
859
  visual.destroy();
614
860
  this._dropRaster(node);
861
+ // a layer that turns into a raster takes its animations with it; the
862
+ // frame clock can still run them over the bitmap
863
+ if (wantsRaster) this.animations.drop(node, true);
615
864
  visual = null;
616
865
  }
866
+ if (wantsRaster && this.animations.pending.has(node)) {
867
+ this.animations.drop(node, true);
868
+ }
617
869
  if (!visual) {
618
870
  visual = new Visual(this, node);
619
871
  visual.isRaster = wantsRaster;
@@ -700,34 +952,23 @@ export class CocoaLayerPresenter {
700
952
 
701
953
  _syncPropBox(node, visual, parentOrigin, order) {
702
954
  const abs = node.abs;
703
- const style = node.style ?? {};
704
- const s = this.scale;
705
955
  visual.origin = { x: abs.x, y: abs.y };
706
- const border =
707
- typeof style.borderWidth === 'number' ? style.borderWidth : 0;
708
- visual.set({
709
- frame: [
710
- (abs.x - parentOrigin.x) / s,
711
- (abs.y - parentOrigin.y) / s,
712
- Math.max(0, abs.width) / s,
713
- Math.max(0, abs.height) / s,
714
- ],
715
- zPosition: order,
716
- hidden: Boolean(node.hidden),
717
- masksToBounds: Boolean(node.clipsChildren?.()) && !node.isScroller?.(),
718
- cornerRadius: (uniformRadius(style.borderRadius) ?? 0) / s,
719
- backgroundColor: style.backgroundColor
720
- ? (this.window.app._parseColor(String(style.backgroundColor)) ?? [
721
- 0, 0, 0, 0,
722
- ])
723
- : [0, 0, 0, 0],
724
- borderWidth: border / s,
725
- borderColor: style.borderColor
726
- ? (this.window.app._parseColor(String(style.borderColor)) ?? [
727
- 0, 0, 0, 0,
728
- ])
729
- : [0, 0, 0, 0],
730
- });
956
+ visual.set(
957
+ propBoxProps(node, this.window.app, this.scale, parentOrigin, order),
958
+ );
959
+ // after the model value went out, inside the same transaction
960
+ this.animations.apply(node, visual.layer);
961
+ }
962
+
963
+ /** The window's animation seam (src/cocoa/window.js): take `prop`'s
964
+ * animation for `node` off the frame clock, or decline. */
965
+ animate(node, prop, entry) {
966
+ return this.animations.take(node, prop, entry);
967
+ }
968
+
969
+ /** …and stop what runs for `prop` on `node`. */
970
+ cancel(node, prop) {
971
+ this.animations.cancel(node, prop);
731
972
  }
732
973
 
733
974
  /**