react-x11 2.44.1 → 2.45.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.44.1",
3
+ "version": "2.45.0",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
6
  "files": [
package/src/cocoa/app.js CHANGED
@@ -1014,8 +1014,11 @@ export class CocoaApp {
1014
1014
  wnd?._shown?.(msg.seq);
1015
1015
  } else if (msg?.type === 'pane-rect') {
1016
1016
  pane.setPaneSize(msg.width, msg.height, msg.scale, msg.screen);
1017
+ if (typeof msg.live === 'boolean') pane.setLive(msg.live);
1017
1018
  this._flushPaneOutbox();
1018
1019
  this._afterInput();
1020
+ } else if (msg?.type === 'pane-live') {
1021
+ pane.setLive(msg.live === true);
1019
1022
  } else if (msg?.type === 'pane-menu-answer') {
1020
1023
  const answer = this._paneMenus?.get(msg.menu);
1021
1024
  this._paneMenus?.delete(msg.menu);
@@ -1687,6 +1690,9 @@ export class CocoaApp {
1687
1690
  const wnd = this._window(ev);
1688
1691
  if (!wnd || wnd.destroyed) return;
1689
1692
  wnd.liveResizing = ev.phase === 'begin';
1693
+ // and what lives in the window without being it hears: a `<Frame>`'s
1694
+ // pane, which has no window of its own to be resized live
1695
+ wnd.emit('liveresize', { live: wnd.liveResizing });
1690
1696
  }
1691
1697
 
1692
1698
  /**
@@ -50,6 +50,8 @@ export class CocoaPaneWindow {
50
50
  // polls it through the pane's ref, so both spellings answer
51
51
  this.id = this.windowId;
52
52
  this.windowNumber = this.windowId;
53
+ // the host's word, which the pane has no AppKit to hear (`setLive`)
54
+ this.liveResizing = false;
53
55
  this.scale = app.scale ?? 2;
54
56
  // Device pixels, as every window's attributes are (`windowAttributes`
55
57
  // multiplies the props through once): the size the pane is born at,
@@ -112,6 +114,20 @@ export class CocoaPaneWindow {
112
114
  * a host that does not say. A move alone is a `resize` that `moved`, the
113
115
  * event a window's own move is, so what is anchored here follows it.
114
116
  */
117
+ /**
118
+ * Whether the window the pane is in is being resized live, as the host
119
+ * says (`pane-rect`, `pane-live`, src/frame/index.js): what a window's
120
+ * `liveResizing` is in a process of its own, where AppKit brackets the
121
+ * drag of its edge. Core defers a content floor's measurement while it
122
+ * is (src/nodes/window/size.js), and catches it up the frame after it
123
+ * ends; and it is announced as a window announces its own.
124
+ */
125
+ setLive(live) {
126
+ if (this.destroyed || this.liveResizing === live) return;
127
+ this.liveResizing = live;
128
+ this.emit('liveresize', { live });
129
+ }
130
+
115
131
  setPaneSize(width, height, scale, screen) {
116
132
  if (this.destroyed) return;
117
133
  if (scale) this.scale = scale;
@@ -278,6 +278,12 @@ export class CocoaWindow {
278
278
  let list = this._handlers.get(name);
279
279
  if (!list) this._handlers.set(name, (list = []));
280
280
  list.push(fn);
281
+ // and the way to stop listening, for a listener that goes before the
282
+ // window does (`<Frame>`'s, for `liveresize`)
283
+ return () => {
284
+ const at = list.indexOf(fn);
285
+ if (at >= 0) list.splice(at, 1);
286
+ };
281
287
  }
282
288
 
283
289
  emit(name, ev) {
@@ -574,6 +574,20 @@ function PaneHostView({
574
574
  let acks = false;
575
575
  let inFlight = false;
576
576
  let waiting = null;
577
+ // And whether the window is being resized live — AppKit's drag of its
578
+ // edge, from its begin to its end (`liveResizing`) — which the pane
579
+ // has no window to hear. Core defers measuring a content floor for a
580
+ // drag's length and catches it up after it (src/nodes/window/size.js),
581
+ // and an element puts off what it would rather do once the drag ends:
582
+ // a pane measured its floors at every step, a document a pixel wide
583
+ // among them, and laid a document out whole at every rest of the drag.
584
+ // With every rect, and on its own where only that changed — the end
585
+ // of a drag comes after its last size.
586
+ const hostWindow = node.root?.window;
587
+ const liveNow = () => hostWindow?.liveResizing === true;
588
+ const offLive = hostWindow?.on?.('liveresize', (ev) =>
589
+ s.trySend?.({ type: 'pane-live', live: ev?.live === true }),
590
+ );
577
591
  const transmit = (rect) => {
578
592
  sent = rect;
579
593
  s.trySend?.({
@@ -581,6 +595,7 @@ function PaneHostView({
581
595
  width: rect.width,
582
596
  height: rect.height,
583
597
  scale: rect.scale,
598
+ live: liveNow(),
584
599
  ...(rect.screen && { screen: rect.screen }),
585
600
  });
586
601
  if (!acks) return;
@@ -685,6 +700,7 @@ function PaneHostView({
685
700
  waiting = null;
686
701
  offAnchor?.();
687
702
  offFocus?.();
703
+ if (typeof offLive === 'function') offLive();
688
704
  node.defaultCursor = undefined;
689
705
  node.root?.events?.refreshCursor();
690
706
  node.absolutize = origAbsolutize;
package/src/node.d.ts CHANGED
@@ -750,6 +750,20 @@ export declare class Node {
750
750
  * each scroll the pane blits, after layout.
751
751
  */
752
752
  viewportFixedRects(): Rect[] | null;
753
+ /**
754
+ * What this element draws under the viewport of the scroll pane it is in
755
+ * moved by (dx, dy) device pixels, and what the viewport shows is to stay
756
+ * where it is on screen: the pane's offsets move by as much, in the
757
+ * layout pass running now, after it measures what it holds and before it
758
+ * places it (CSS Scroll Anchoring 1) — a document laid out again at
759
+ * another width, whose text above the fold broke into more lines or
760
+ * fewer. Asked of the nearest scroll pane; shifts in one pass add up.
761
+ * Ask from the layout pass (`measureContent`); asked outside one, the
762
+ * shift waits for the next and asks for it. A `scrollTo` made since the
763
+ * last pass drops it, a `scrollIntoView` lands after it, and the result
764
+ * is clamped as any scroll. Returns whether a pane took it.
765
+ */
766
+ anchorScrollBy(dx: number, dy: number): boolean;
753
767
  /**
754
768
  * How far this element's own drawing reaches past its box, in device
755
769
  * pixels on every side — 0, the default, for an element that stays inside
@@ -137,6 +137,36 @@ export class NodeInvalidate {
137
137
  return null;
138
138
  }
139
139
 
140
+ /**
141
+ * What this element draws under the viewport of the scroll pane it is
142
+ * in moved by (dx, dy) device pixels, and what the viewport shows is to
143
+ * stay where it is on screen: the pane's offsets move by as much, in the
144
+ * layout pass running now, after it measures what it holds and before it
145
+ * places it (CSS Scroll Anchoring 1). A document laid out again at
146
+ * another width is the case: its text above the fold breaks into more
147
+ * lines or fewer, and the paragraph at the top of the viewport moves
148
+ * down or up the document by the difference — which, unanswered, scrolls
149
+ * the reader a page away from where they were.
150
+ *
151
+ * Asked of the nearest scroll pane, the one whose viewport the element
152
+ * is seen through; shifts asked of it in one pass add up. Ask from the
153
+ * layout pass — `measureContent`, or what it calls — where the pane
154
+ * places its content after; asked outside one, the shift waits for the
155
+ * next pass, and asks for it. A `scrollTo` the application made since
156
+ * the last pass drops it, since the shift was measured against the
157
+ * offsets before that scroll; a `scrollIntoView` lands after it; and the
158
+ * pane clamps the result to what it holds, as any scroll. Returns
159
+ * whether a pane took it: false where nothing scrolls the element.
160
+ */
161
+ anchorScrollBy(dx, dy) {
162
+ for (let n = this.parent; n; n = n.parent) {
163
+ if (typeof n._anchorScrollBy === 'function' && n.isScroller()) {
164
+ return n._anchorScrollBy(dx, dy);
165
+ }
166
+ }
167
+ return false;
168
+ }
169
+
140
170
  /**
141
171
  * How far this element's own drawing reaches past its box — device
142
172
  * pixels, on every side — or 0, the default, for one that stays inside
@@ -330,6 +330,10 @@ export const Scrollable = (Base) =>
330
330
  // the viewport grew, under it. A browser fires `scroll` for each, so
331
331
  // `onScroll` hears of them too, once the pass is over.
332
332
  const from = { x: this.scrollX, y: this.scrollY };
333
+ // What the content asked to keep in place lands before them all
334
+ // (`anchorScrollBy`): it moved under the viewport in this pass, and
335
+ // a scroll anyone asks for is from where it shows what it did
336
+ this._applyAnchorShift();
333
337
  // A scrollTo held for this pass lands first, so a node asked into view
334
338
  // in the same frame is brought in from where that scroll put the pane:
335
339
  // the order the two have on a laid-out pane, where scrollTo applies at
@@ -607,6 +611,10 @@ export const Scrollable = (Base) =>
607
611
  }
608
612
  this.scrollX = next.x;
609
613
  this.scrollY = next.y;
614
+ // a scroll asked for since the last pass is where the pane goes: a
615
+ // shift the content asks for in the next was measured against the
616
+ // offsets before it (`_applyAnchorShift`)
617
+ this._scrolledSincePass = true;
610
618
  if (moved) this.props.onScroll?.(this._scrollEvent());
611
619
  // A scroll reflows this viewport's contents and nothing else, and the
612
620
  // viewport clips them, so the damage is this node's own rect. It is a
@@ -707,6 +715,47 @@ export const Scrollable = (Base) =>
707
715
  return true;
708
716
  }
709
717
 
718
+ /**
719
+ * The pane's half of `Node.anchorScrollBy`: a shift of the offsets the
720
+ * content asked for, added up until the pass applies it
721
+ * (`_applyAnchorShift`). The pass measures the content again, since
722
+ * what moved under the viewport moved by being laid out; one asked
723
+ * outside a pass asks for one.
724
+ */
725
+ _anchorScrollBy(dx, dy) {
726
+ if (!Number.isFinite(dx) || !Number.isFinite(dy)) return false;
727
+ if (dx === 0 && dy === 0) return true;
728
+ const shift = (this._anchorShift ??= { x: 0, y: 0 });
729
+ shift.x += dx;
730
+ shift.y += dy;
731
+ this._scrollMeasureDirty = true;
732
+ const root = this.root;
733
+ if (root && !root._inFlush) root.invalidate(true, this, 'scroll');
734
+ return true;
735
+ }
736
+
737
+ /**
738
+ * Apply the shift the content asked for (`_anchorScrollBy`), before
739
+ * anything else the pass moves the offsets by: on each axis it scrolls,
740
+ * the clamp after takes it into the extent just measured. Its
741
+ * `onScroll` and its claim of the viewport are the pass's, as for a
742
+ * held `scrollTo`. A scroll the application asked for since the last
743
+ * pass drops it: the shift was measured against the offsets before
744
+ * that scroll, which is where the application wants the pane now.
745
+ */
746
+ _applyAnchorShift() {
747
+ const shift = this._anchorShift;
748
+ const scrolled = this._scrolledSincePass === true;
749
+ this._scrolledSincePass = false;
750
+ if (!shift) return;
751
+ this._anchorShift = null;
752
+ if (scrolled) return;
753
+ // `scrollX` is a distance from the start in either direction, and
754
+ // content that moved towards the end is followed the same way
755
+ this.scrollX += this.direction === 'rtl' ? -shift.x : shift.x;
756
+ this.scrollY += shift.y;
757
+ }
758
+
710
759
  /**
711
760
  * Answer a held request (`_holdScrollTo`) against the extent this pass
712
761
  * has just measured: each held axis's base, clamped, then each step
@@ -533,10 +533,12 @@ export function setMeasuringShrink(node, axis, out) {
533
533
  * It is still laid out, though, and a leaf's answer could reach something
534
534
  * that is read through its size. So it is marked only where it cannot:
535
535
  *
536
- * - every box from the root down to it is as wide as something other than
537
- * its content — the root offered no room, a width it names, a column
538
- * stretching it — so the width the leaf answers is one yoga replaces, and
539
- * nothing above it is sized from it;
536
+ * - every box from `node` down to it is as wide as something other than
537
+ * its content — `node` the width the pass gives it (no room, for the
538
+ * window; its column's, for a spine root; its own, for a box that sizes
539
+ * itself), and below it a width it names or a column stretching it — so
540
+ * the width the leaf answers is one yoga replaces, and nothing above it
541
+ * is sized from it;
540
542
  * - no box is handed a share of space the leaf's height changed, which is
541
543
  * how a height could become somebody's width — an aspect ratio, a column
542
544
  * that wraps, an image sized to the height it is given. Every column from
@@ -656,15 +658,28 @@ function stretchedAlong(node, axis) {
656
658
  * names are numbers — a height, a basis (a height, down a column), a floor or
657
659
  * a ceiling; and `'unknown'` for a share of something (a percentage), which
658
660
  * a pass with no height on offer resolves as it likes.
661
+ *
662
+ * A floor of nothing bounds nothing: a box as tall as its content is at
663
+ * least that tall already, so its height is its content's. It is how a box
664
+ * says it may shrink, and a browser's page has one on every box from the
665
+ * window down to its scroller, so that a page taller than the window
666
+ * scrolls. Read as a height of the box's own, it stopped the walk at the
667
+ * first of them, where the scroller gives way: the whole document in the
668
+ * scroller was set a word to a line each time the tree was measured, 100
669
+ * to 250 ms of a frame for a long Wikipedia article, and nothing read any
670
+ * of it. And it took the box pinned to both edges of such a column for one
671
+ * whose height nothing in flow decides, which it is.
659
672
  */
660
673
  function heightOf(yoga) {
674
+ const floor = yoga.getMinHeight();
661
675
  let own = false;
662
676
  for (const value of [
663
677
  yoga.getHeight(),
664
678
  yoga.getFlexBasis(),
665
- yoga.getMinHeight(),
679
+ floor.value === 0 ? null : floor,
666
680
  yoga.getMaxHeight(),
667
681
  ]) {
682
+ if (value === null) continue;
668
683
  if (value.unit === Yoga.UNIT_PERCENT) return 'unknown';
669
684
  if (value.unit === Yoga.UNIT_POINT) own = true;
670
685
  }
@@ -354,12 +354,28 @@ export class WindowSize {
354
354
  if (axis === 'width') {
355
355
  const shrunk = [];
356
356
  setMeasuringShrink(root, axis, shrunk);
357
+ // Nothing reads a root's own width — only heights travel up a spine
358
+ // (spine.js) — so what it holds is read only where a floor is written
359
+ // from it, as down the window's columns, and a leaf that is the root
360
+ // is laid out at the width its column offers, which is no answer of
361
+ // its own: a document in a scroller is a root when it changes.
362
+ const read = NO_UNREAD_WIDTHS || this._readEveryWidth;
363
+ const unread = [];
364
+ if (!read && root._measureFn) {
365
+ root._widthUnread = true;
366
+ unread.push(root);
367
+ } else if (!read) {
368
+ markUnreadLeaves(root, unread);
369
+ }
357
370
  this._scopedFloorPasses += 1;
358
- yoga.calculateLayout(widths.widthPass, undefined, dir);
359
- const span = contentSpan(root, axis, null, this);
360
- root._floorW = declaresOwnMinimum(root, axis)
361
- ? yoga.getComputedWidth()
362
- : span;
371
+ try {
372
+ yoga.calculateLayout(widths.widthPass, undefined, dir);
373
+ } finally {
374
+ for (const leaf of unread) leaf._widthUnread = false;
375
+ }
376
+ const span = contentSpan(root, axis, null, this, read);
377
+ const own = declaresOwnMinimum(root, axis);
378
+ if (own || read) root._floorW = own ? yoga.getComputedWidth() : span;
363
379
  restoreShrink(shrunk);
364
380
  return;
365
381
  }
@@ -443,9 +459,18 @@ export class WindowSize {
443
459
  if (axis === 'width') {
444
460
  const shrunk = [];
445
461
  setMeasuringShrink(boundary, axis, shrunk);
462
+ // its own extent is its style's, so what it holds is read only where
463
+ // a floor is written from it, as down the window's columns
464
+ const read = NO_UNREAD_WIDTHS || this._readEveryWidth;
465
+ const unread = [];
466
+ if (!read) markUnreadLeaves(boundary, unread);
446
467
  this._scopedFloorPasses += 1;
447
- yoga.calculateLayout(undefined, undefined, dir);
448
- contentSpan(boundary, axis, null, this);
468
+ try {
469
+ yoga.calculateLayout(undefined, undefined, dir);
470
+ } finally {
471
+ for (const leaf of unread) leaf._widthUnread = false;
472
+ }
473
+ contentSpan(boundary, axis, null, this, read);
449
474
  restoreShrink(shrunk);
450
475
  return;
451
476
  }