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.
- package/README.md +5 -3
- package/package.json +10 -3
- package/src/activate.js +12 -0
- package/src/anchor.js +6 -0
- package/src/appearance.js +351 -28
- package/src/appearancehooks.js +5 -2
- package/src/application.js +41 -0
- package/src/cocoa/app.js +367 -3
- package/src/cocoa/bezels.js +51 -1
- package/src/cocoa/dnd.js +358 -0
- package/src/cocoa/dock.js +39 -0
- package/src/cocoa/filepanels.js +155 -0
- package/src/cocoa/fonts.js +93 -2
- package/src/cocoa/globalmenu.js +41 -33
- package/src/cocoa/notifications.js +244 -0
- package/src/cocoa/permissions.js +74 -0
- package/src/cocoa/presenter.js +274 -33
- package/src/cocoa/promotion.js +708 -0
- package/src/cocoa/statusitem.js +112 -0
- package/src/cocoa/window.js +113 -4
- package/src/components/Button.js +20 -1
- package/src/components/Checkbox.js +17 -2
- package/src/components/Menu.js +108 -38
- package/src/components/Radio.js +17 -2
- package/src/components/Select.js +159 -27
- package/src/components/Switch.js +8 -1
- package/src/components/native.js +99 -0
- package/src/components/theme.js +37 -20
- package/src/desktopsettings.js +34 -2
- package/src/dnd.js +137 -11
- package/src/errors.js +6 -3
- package/src/filedialog.js +81 -16
- package/src/index.d.ts +29 -2
- package/src/index.js +17 -0
- package/src/launcher.js +170 -0
- package/src/launcherhooks.js +81 -0
- package/src/nodes.js +604 -37
- package/src/notificationhooks.js +56 -0
- package/src/notifications.js +558 -0
- package/src/palette.js +144 -8
- package/src/permissionhooks.js +89 -0
- package/src/permissions.js +196 -0
- package/src/style.d.ts +10 -4
- package/src/style.js +1 -0
- package/src/styles.js +161 -15
- package/src/textselection.js +1 -4
- package/src/trayhooks.js +90 -0
- package/src/types/appearance.d.ts +24 -0
- package/src/types/components.d.ts +10 -0
- package/src/types/elements.d.ts +14 -0
- package/src/types/events.d.ts +14 -0
- package/src/types/filedialog.d.ts +18 -7
- package/src/types/launcher.d.ts +43 -0
- package/src/types/notifications.d.ts +113 -0
- package/src/types/permissions.d.ts +100 -0
- package/src/types/style.d.ts +30 -2
- package/src/types/system.d.ts +5 -3
- package/src/types/tray.d.ts +54 -0
- package/src/windowid.js +23 -0
package/src/cocoa/presenter.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
/**
|