react-x11 2.34.0 → 2.36.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 +2 -2
- package/src/backend/context2d.js +94 -3
- package/src/cocoa/presenter.js +148 -13
- package/src/cocoa/promotion.js +391 -39
- package/src/cocoa/sprites.js +168 -19
- package/src/cocoa/window.js +4 -0
- package/src/components/ProgressBar.js +7 -3
- package/src/nodes/animation.js +171 -12
- package/src/nodes/scrollable.js +3 -0
- package/src/nodes/window/flush.js +9 -0
- package/src/types/system.d.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.36.0",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"yoga-layout": "^3.2.1"
|
|
110
110
|
},
|
|
111
111
|
"optionalDependencies": {
|
|
112
|
-
"@windowkit/appkit": "^0.
|
|
112
|
+
"@windowkit/appkit": "^0.22.0",
|
|
113
113
|
"@windowkit/wayland": "^3.1.1",
|
|
114
114
|
"@windowkit/win32": "^0.0.6",
|
|
115
115
|
"dbus-native": "^0.16.0",
|
package/src/backend/context2d.js
CHANGED
|
@@ -194,6 +194,19 @@ class LinearGradient {
|
|
|
194
194
|
|
|
195
195
|
const clamp01 = (v) => Math.min(1, Math.max(0, Number(v) || 0));
|
|
196
196
|
|
|
197
|
+
// What `textAlign`, `textBaseline` and `direction` take. A value outside
|
|
198
|
+
// them is ignored and the last one stays, as canvas has it.
|
|
199
|
+
const TEXT_ALIGNS = new Set(['start', 'end', 'left', 'right', 'center']);
|
|
200
|
+
const TEXT_BASELINES = new Set([
|
|
201
|
+
'top',
|
|
202
|
+
'hanging',
|
|
203
|
+
'middle',
|
|
204
|
+
'alphabetic',
|
|
205
|
+
'ideographic',
|
|
206
|
+
'bottom',
|
|
207
|
+
]);
|
|
208
|
+
const DIRECTIONS = new Set(['ltr', 'rtl', 'inherit']);
|
|
209
|
+
|
|
197
210
|
/**
|
|
198
211
|
* The Render ops text draws with, numbered as XRender numbers them so a
|
|
199
212
|
* caller's `ctx.Render?.PictOp?.Over ?? 3` reads the same on both
|
|
@@ -428,6 +441,9 @@ export class BackendContext2D {
|
|
|
428
441
|
dash: [],
|
|
429
442
|
dashOffset: 0,
|
|
430
443
|
font: '10px sans-serif',
|
|
444
|
+
textAlign: 'start',
|
|
445
|
+
textBaseline: 'alphabetic',
|
|
446
|
+
direction: 'inherit',
|
|
431
447
|
shadowBlur: 0,
|
|
432
448
|
shadowOffsetX: 0,
|
|
433
449
|
shadowOffsetY: 0,
|
|
@@ -654,6 +670,37 @@ export class BackendContext2D {
|
|
|
654
670
|
this._state.font = String(value);
|
|
655
671
|
}
|
|
656
672
|
|
|
673
|
+
/** Where `fillText`'s x is on the text: its start, end, left, right or
|
|
674
|
+
* centre. */
|
|
675
|
+
get textAlign() {
|
|
676
|
+
return this._state.textAlign;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
set textAlign(value) {
|
|
680
|
+
if (TEXT_ALIGNS.has(value)) this._state.textAlign = value;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** Where `fillText`'s y is on the text: its alphabetic baseline, or the
|
|
684
|
+
* top, middle or bottom of its em box, or its hanging or ideographic
|
|
685
|
+
* baseline. */
|
|
686
|
+
get textBaseline() {
|
|
687
|
+
return this._state.textBaseline;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
set textBaseline(value) {
|
|
691
|
+
if (TEXT_BASELINES.has(value)) this._state.textBaseline = value;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** Which way `start` and `end` face. `inherit`, with no element to take
|
|
695
|
+
* it from, is the text's own: right to left where it starts so. */
|
|
696
|
+
get direction() {
|
|
697
|
+
return this._state.direction;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
set direction(value) {
|
|
701
|
+
if (DIRECTIONS.has(value)) this._state.direction = value;
|
|
702
|
+
}
|
|
703
|
+
|
|
657
704
|
get shadowBlur() {
|
|
658
705
|
return this._state.shadowBlur;
|
|
659
706
|
}
|
|
@@ -1932,9 +1979,53 @@ export class BackendContext2D {
|
|
|
1932
1979
|
fillText(text, x, y) {
|
|
1933
1980
|
const layout = this._fontLayout(text);
|
|
1934
1981
|
if (!layout) return;
|
|
1935
|
-
// canvas fillText's y is the baseline
|
|
1936
|
-
|
|
1937
|
-
|
|
1982
|
+
// canvas fillText's y is on the baseline `textBaseline` names, and x on
|
|
1983
|
+
// the side `textAlign` does; a layout draws from its top left
|
|
1984
|
+
const line = layout.lines[0];
|
|
1985
|
+
const baseline = line?.baseline ?? 0;
|
|
1986
|
+
this._drawLayout(
|
|
1987
|
+
layout,
|
|
1988
|
+
x + this._alignOffset(layout, line),
|
|
1989
|
+
y - baseline + this._baselineOffset(line),
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
/** How far left of x the text starts: ntk's `_alignOffset`, so a label
|
|
1994
|
+
* centred on X11 is centred here too. */
|
|
1995
|
+
_alignOffset(layout, line) {
|
|
1996
|
+
let align = this._state.textAlign;
|
|
1997
|
+
if (align === 'start' || align === 'end') {
|
|
1998
|
+
const direction = this._state.direction;
|
|
1999
|
+
const rtl =
|
|
2000
|
+
direction === 'rtl' ||
|
|
2001
|
+
(direction === 'inherit' &&
|
|
2002
|
+
!!line?.runs?.find((r) => r.start === 0)?.rtl);
|
|
2003
|
+
align = (align === 'start') !== rtl ? 'left' : 'right';
|
|
2004
|
+
}
|
|
2005
|
+
const width = layout.width ?? 0;
|
|
2006
|
+
if (align === 'center') return -width / 2;
|
|
2007
|
+
if (align === 'right') return -width;
|
|
2008
|
+
return 0;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
/** How far below y the alphabetic baseline is: ntk's `_baselineOffset`,
|
|
2012
|
+
* from the face's ascent and descent. */
|
|
2013
|
+
_baselineOffset(line) {
|
|
2014
|
+
const ascent = line?.ascent ?? 0;
|
|
2015
|
+
const descent = line?.descent ?? 0;
|
|
2016
|
+
switch (this._state.textBaseline) {
|
|
2017
|
+
case 'top':
|
|
2018
|
+
return ascent;
|
|
2019
|
+
case 'hanging':
|
|
2020
|
+
return ascent * 0.8;
|
|
2021
|
+
case 'middle':
|
|
2022
|
+
return (ascent - descent) / 2;
|
|
2023
|
+
case 'bottom':
|
|
2024
|
+
case 'ideographic':
|
|
2025
|
+
return -descent;
|
|
2026
|
+
default:
|
|
2027
|
+
return 0;
|
|
2028
|
+
}
|
|
1938
2029
|
}
|
|
1939
2030
|
|
|
1940
2031
|
_fontLayout(text, color) {
|
package/src/cocoa/presenter.js
CHANGED
|
@@ -29,7 +29,12 @@ import { cssColorStraight } from 'ntk/color';
|
|
|
29
29
|
import { Node } from '../nodes/node.js';
|
|
30
30
|
import { addDamageRect, damageToPaint } from '../nodes/damage.js';
|
|
31
31
|
import { intersectRects } from '../nodes/rects.js';
|
|
32
|
-
import {
|
|
32
|
+
import { now as animationNow } from '../nodes/animation.js';
|
|
33
|
+
import {
|
|
34
|
+
EASING_CONTROL_POINTS,
|
|
35
|
+
TRANSITION_CONTROL_POINTS,
|
|
36
|
+
resolveBorderWidths,
|
|
37
|
+
} from '../styles.js';
|
|
33
38
|
import { BackendContext2D } from '../backend/context2d.js';
|
|
34
39
|
|
|
35
40
|
export const RASTER_PAD = 2; // antialiasing/italic overhang outside the ink bounds
|
|
@@ -361,6 +366,33 @@ const ANIMATED_KEY_PATHS = Object.freeze({
|
|
|
361
366
|
opacity: { keyPath: 'opacity' },
|
|
362
367
|
});
|
|
363
368
|
|
|
369
|
+
/**
|
|
370
|
+
* An inset of a node out of the flow moves that node and nothing else — no
|
|
371
|
+
* sibling, not its parent — so a loop on one is its layer's position: an
|
|
372
|
+
* offset added to where the layout put the node, which is at the inset's
|
|
373
|
+
* declared value while the render server runs the loop (the style is the
|
|
374
|
+
* model). Only for a node no pointer lands on (`pointerEvents: 'none'`),
|
|
375
|
+
* since input is hit against the layout's rect, which holds still: an
|
|
376
|
+
* indeterminate bar's block, a highlight that sweeps along a track. Which
|
|
377
|
+
* axis, and which way a larger value moves the layer; `start` and `end` are
|
|
378
|
+
* mirrored under RTL.
|
|
379
|
+
*/
|
|
380
|
+
const INSET_KEY_PATHS = Object.freeze({
|
|
381
|
+
left: { keyPath: 'position.x', axis: 'x', sign: 1 },
|
|
382
|
+
right: { keyPath: 'position.x', axis: 'x', sign: -1 },
|
|
383
|
+
start: { keyPath: 'position.x', axis: 'x', sign: 1, logical: true },
|
|
384
|
+
end: { keyPath: 'position.x', axis: 'x', sign: -1, logical: true },
|
|
385
|
+
top: { keyPath: 'position.y', axis: 'y', sign: 1 },
|
|
386
|
+
bottom: { keyPath: 'position.y', axis: 'y', sign: -1 },
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
/** Does a loop on `prop` move its node, where a presenter runs it
|
|
390
|
+
* (`INSET_KEY_PATHS`)? */
|
|
391
|
+
export const movesNode = (prop) => Object.hasOwn(INSET_KEY_PATHS, prop);
|
|
392
|
+
|
|
393
|
+
// a length or a share of the room: `12`, `'-40%'`
|
|
394
|
+
const PERCENT = /^\s*(-?(?:\d+\.?\d*|\.\d+))%\s*$/;
|
|
395
|
+
|
|
364
396
|
// the ids the bridge reports an animation's end under: unique per process,
|
|
365
397
|
// looked up per app (`_animationEnds`)
|
|
366
398
|
let animationSeq = 0;
|
|
@@ -544,12 +576,16 @@ export class LayerAnimations {
|
|
|
544
576
|
* is in its bitmap, not on its layer.
|
|
545
577
|
*/
|
|
546
578
|
take(node, prop, entry) {
|
|
547
|
-
const map = ANIMATED_KEY_PATHS[prop];
|
|
579
|
+
const map = ANIMATED_KEY_PATHS[prop] ?? INSET_KEY_PATHS[prop];
|
|
548
580
|
if (!map || !stylePaintsPlain(node, node._targetStyle ?? node.style)) {
|
|
549
581
|
return false;
|
|
550
582
|
}
|
|
551
|
-
if (
|
|
552
|
-
|
|
583
|
+
if (map.axis) {
|
|
584
|
+
if (!this._insetOffsets(node, prop, entry)) return false;
|
|
585
|
+
} else {
|
|
586
|
+
if (this._value(map, entry.from) == null) return false;
|
|
587
|
+
if (this._value(map, entry.to) == null) return false;
|
|
588
|
+
}
|
|
553
589
|
let pending = this.pending.get(node);
|
|
554
590
|
if (!pending) this.pending.set(node, (pending = new Map()));
|
|
555
591
|
pending.set(prop, entry);
|
|
@@ -559,7 +595,12 @@ export class LayerAnimations {
|
|
|
559
595
|
/** Stop what runs for `prop` on `node`: a loop the window lost sight of,
|
|
560
596
|
* a declaration that changed, a transition the clock takes back. */
|
|
561
597
|
cancel(node, prop) {
|
|
562
|
-
this.pending.get(node)
|
|
598
|
+
const pending = this.pending.get(node);
|
|
599
|
+
// …and nothing left waiting is no entry at all: an empty map still
|
|
600
|
+
// answers `has`, and a node with nothing to run went onto a layer it
|
|
601
|
+
// never gave back — a loop that went out of sight between its offer
|
|
602
|
+
// and its frame did
|
|
603
|
+
if (pending?.delete(prop) && pending.size === 0) this.pending.delete(node);
|
|
563
604
|
this._removeLive(node, prop);
|
|
564
605
|
}
|
|
565
606
|
|
|
@@ -585,6 +626,78 @@ export class LayerAnimations {
|
|
|
585
626
|
: null;
|
|
586
627
|
}
|
|
587
628
|
|
|
629
|
+
/**
|
|
630
|
+
* A loop on an inset of `node` as its layer's offsets from where the
|
|
631
|
+
* layout puts it, `[from, to]` in points — or null where it cannot be
|
|
632
|
+
* one: not a loop, a node in the flow or one a pointer can land on, an
|
|
633
|
+
* inset with no declared value to rest at, or a value that is neither a
|
|
634
|
+
* length nor a percentage. A percentage is a share of the parent's
|
|
635
|
+
* padding box, which is what the layout places a node out of the flow
|
|
636
|
+
* in, so the offsets change when the parent's size does (`follow`).
|
|
637
|
+
*/
|
|
638
|
+
_insetOffsets(node, prop, entry) {
|
|
639
|
+
const map = INSET_KEY_PATHS[prop];
|
|
640
|
+
const style = node._targetStyle ?? node.style ?? {};
|
|
641
|
+
if (!entry.loop || style.position !== 'absolute') return null;
|
|
642
|
+
if (style.pointerEvents !== 'none') return null;
|
|
643
|
+
const parent = node.parent;
|
|
644
|
+
if (!parent?.abs) return null;
|
|
645
|
+
const edges = resolveBorderWidths(parent.style ?? {}, parent.direction);
|
|
646
|
+
const room =
|
|
647
|
+
map.axis === 'x'
|
|
648
|
+
? parent.abs.width - edges.left - edges.right
|
|
649
|
+
: parent.abs.height - edges.top - edges.bottom;
|
|
650
|
+
const length = (value) => {
|
|
651
|
+
if (typeof value === 'number')
|
|
652
|
+
return Number.isFinite(value) ? value : null;
|
|
653
|
+
const share = typeof value === 'string' ? PERCENT.exec(value) : null;
|
|
654
|
+
return share ? (Number(share[1]) / 100) * room : null;
|
|
655
|
+
};
|
|
656
|
+
const rest = length(style[prop]);
|
|
657
|
+
const from = length(entry.from);
|
|
658
|
+
const to = length(entry.to);
|
|
659
|
+
if (rest === null || from === null || to === null) return null;
|
|
660
|
+
const sign = map.logical && node.direction === 'rtl' ? -map.sign : map.sign;
|
|
661
|
+
return [
|
|
662
|
+
(sign * (from - rest)) / this.scale,
|
|
663
|
+
(sign * (to - rest)) / this.scale,
|
|
664
|
+
];
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* The offsets of a loop moving `node` as the layout has them now: one
|
|
669
|
+
* whose parent changed size runs again where it has got to in its cycle
|
|
670
|
+
* (`headStart`), and one that cannot run there any more goes back to the
|
|
671
|
+
* clock. Before `apply`, which attaches what this hands back.
|
|
672
|
+
*/
|
|
673
|
+
follow(node, layer) {
|
|
674
|
+
const live = this.live.get(node);
|
|
675
|
+
if (!live) return;
|
|
676
|
+
for (const [id, run] of [...live]) {
|
|
677
|
+
if (!run.offsets) continue;
|
|
678
|
+
const offsets = this._insetOffsets(node, run.prop, run.entry);
|
|
679
|
+
if (
|
|
680
|
+
offsets &&
|
|
681
|
+
offsets[0] === run.offsets[0] &&
|
|
682
|
+
offsets[1] === run.offsets[1]
|
|
683
|
+
) {
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
live.delete(id);
|
|
687
|
+
this._ends().delete(id);
|
|
688
|
+
this.native.removeAnimation(layer, run.key);
|
|
689
|
+
if (!offsets) {
|
|
690
|
+
node._offloadEnded(run.prop, run.entry);
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
run.entry.headStart = animationNow() - run.entry.start;
|
|
694
|
+
let pending = this.pending.get(node);
|
|
695
|
+
if (!pending) this.pending.set(node, (pending = new Map()));
|
|
696
|
+
pending.set(run.prop, run.entry);
|
|
697
|
+
}
|
|
698
|
+
if (live.size === 0) this.live.delete(node);
|
|
699
|
+
}
|
|
700
|
+
|
|
588
701
|
/** The frame's half: attach what `take` accepted to `layer`, after the
|
|
589
702
|
* model value went out, inside the same transaction. */
|
|
590
703
|
apply(node, layer) {
|
|
@@ -592,23 +705,43 @@ export class LayerAnimations {
|
|
|
592
705
|
if (!pending) return;
|
|
593
706
|
this.pending.delete(node);
|
|
594
707
|
for (const [prop, entry] of pending) {
|
|
595
|
-
const map = ANIMATED_KEY_PATHS[prop];
|
|
708
|
+
const map = ANIMATED_KEY_PATHS[prop] ?? INSET_KEY_PATHS[prop];
|
|
596
709
|
const id = `rx${++animationSeq}`;
|
|
597
710
|
const key = `${prop}:${id}`;
|
|
598
711
|
const opts = { duration: entry.duration / 1000, id };
|
|
712
|
+
let offsets = null;
|
|
713
|
+
if (map.axis) {
|
|
714
|
+
// an inset: the layer's offset from where the layout put it, added
|
|
715
|
+
// to its position — as the layout has the parent now, which may not
|
|
716
|
+
// be what it was at the swap
|
|
717
|
+
offsets = this._insetOffsets(node, prop, entry);
|
|
718
|
+
if (!offsets) {
|
|
719
|
+
node._offloadDeclined(prop, entry);
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
599
723
|
if (entry.loop) {
|
|
600
724
|
// a loop replaces whatever ran for the property: its declaration
|
|
601
725
|
// changed, and a loop restarts from the top when it does
|
|
602
726
|
this._removeLive(node, prop);
|
|
603
|
-
|
|
604
|
-
|
|
727
|
+
if (offsets) {
|
|
728
|
+
[opts.from, opts.to] = offsets;
|
|
729
|
+
opts.additive = true;
|
|
730
|
+
} else {
|
|
731
|
+
opts.from = this._value(map, entry.from);
|
|
732
|
+
opts.to = this._value(map, entry.to);
|
|
733
|
+
}
|
|
605
734
|
opts.timing = EASING_CONTROL_POINTS[entry.easing];
|
|
606
735
|
opts.repeat = Infinity;
|
|
607
736
|
opts.autoreverse = entry.alternate;
|
|
608
737
|
// CA's two halves of one CSS delay: a begin time the layer waits
|
|
609
|
-
// for showing `from`, or a time offset that starts it that far in
|
|
610
|
-
|
|
611
|
-
|
|
738
|
+
// for showing `from`, or a time offset that starts it that far in.
|
|
739
|
+
// A loop the clock has been running comes over where it has got to
|
|
740
|
+
// (`headStart`, nodes/animation.js `_offerLoopsAgain`), which is
|
|
741
|
+
// the same delay that much shorter.
|
|
742
|
+
const wait = (entry.delay ?? 0) - (entry.headStart ?? 0);
|
|
743
|
+
if (wait > 0) opts.delay = wait / 1000;
|
|
744
|
+
else if (wait < 0) opts.timeOffset = -wait / 1000;
|
|
612
745
|
} else if (map.colour) {
|
|
613
746
|
// From where the pixels are — which is what "an interrupted
|
|
614
747
|
// transition reverses from where it got to" means here. A colour
|
|
@@ -636,7 +769,7 @@ export class LayerAnimations {
|
|
|
636
769
|
// looked up after the removals above, which may have pruned the map
|
|
637
770
|
let live = this.live.get(node);
|
|
638
771
|
if (!live) this.live.set(node, (live = new Map()));
|
|
639
|
-
live.set(id, { prop, key, entry });
|
|
772
|
+
live.set(id, { prop, key, entry, offsets });
|
|
640
773
|
this._ends().set(id, (ev) => this._animationEnded(node, id, ev));
|
|
641
774
|
}
|
|
642
775
|
}
|
|
@@ -983,7 +1116,9 @@ export class CocoaLayerPresenter {
|
|
|
983
1116
|
visual.set(
|
|
984
1117
|
propBoxProps(node, this.window.app, this.scale, parentOrigin, order),
|
|
985
1118
|
);
|
|
986
|
-
// after the model value went out, inside the same transaction
|
|
1119
|
+
// after the model value went out, inside the same transaction: what
|
|
1120
|
+
// moves the node, as the layout has its parent now, then what waits
|
|
1121
|
+
this.animations.follow(node, visual.layer);
|
|
987
1122
|
this.animations.apply(node, visual.layer);
|
|
988
1123
|
}
|
|
989
1124
|
|