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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-x11",
3
- "version": "2.34.0",
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.19.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",
@@ -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; layouts draw from their top
1936
- const baseline = layout.lines[0]?.baseline ?? 0;
1937
- this._drawLayout(layout, x, y - baseline);
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) {
@@ -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 { EASING_CONTROL_POINTS, TRANSITION_CONTROL_POINTS } from '../styles.js';
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 (this._value(map, entry.from) == null) return false;
552
- if (this._value(map, entry.to) == null) return false;
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)?.delete(prop);
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
- opts.from = this._value(map, entry.from);
604
- opts.to = this._value(map, entry.to);
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
- if (entry.delay > 0) opts.delay = entry.delay / 1000;
611
- else if (entry.delay < 0) opts.timeOffset = -entry.delay / 1000;
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