react-x11 2.49.0 → 2.51.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/src/gloverlay.js CHANGED
@@ -86,6 +86,7 @@
86
86
  import { cssColorStraight } from 'ntk/color';
87
87
 
88
88
  import { FULL_DAMAGE, addDamageRect } from './nodes/damage.js';
89
+ import { radiusOf } from './styles.js';
89
90
  import {
90
91
  innerPixels,
91
92
  intersectRects,
@@ -1200,7 +1201,7 @@ export class GlOverlay {
1200
1201
  }
1201
1202
  const area = this.area;
1202
1203
  // a rounded surface clips its children to an arc, and the arc stays put
1203
- if ((area.style?.borderRadius ?? 0) > 0) return false;
1204
+ if (radiusOf(area.style, area.abs) > 0) return false;
1204
1205
  // the whole pixels of the surface's box: a fractional edge is an
1205
1206
  // antialiased clip, and that does not move either
1206
1207
  const box = innerPixels(area.abs);
package/src/node.d.ts CHANGED
@@ -624,6 +624,11 @@ export declare class Node {
624
624
  * with none of its own is set in. An element that draws text inherits
625
625
  * exactly what `<text>` inherits by asking. */
626
626
  resolvedTextStyle(): TextStyle;
627
+ /** A colour from this node's style as one a context can paint: the value
628
+ * itself, or — for `currentColor` — the node's ink, the colour
629
+ * `resolvedTextStyle()` resolves. Everything core paints from a style
630
+ * colour asks here; an element painting one of its own should too. */
631
+ resolveColor<T>(value: T): T | string;
627
632
  /** The flattened `style` prop with the active state blocks overlaid.
628
633
  * Everything that paints or lays out reads this, never `props`. */
629
634
  readonly style: Style;
@@ -11,9 +11,11 @@ import {
11
11
  transitionFor,
12
12
  interpolate,
13
13
  ease,
14
+ isColor,
14
15
  isLayoutProp,
15
16
  TEXT_LAYOUT_PROPS,
16
17
  } from '../styles.js';
18
+ import { isCurrentColor } from '../currentcolor.js';
17
19
  import { GRID_CONTAINER_PROPS, GRID_ITEM_PROPS } from '../grid.js';
18
20
  import { isPlaced } from '../layouts.js';
19
21
  import { desktopSettings, watchDesktopSettings } from '../desktopsettings.js';
@@ -85,11 +87,16 @@ export class NodeAnimation {
85
87
  // transitions start on later changes, which is CSS's rule too.
86
88
  if (this._placed) {
87
89
  for (const prop of Object.keys(target)) {
88
- const to = target[prop];
89
- const from = displayed[prop];
90
+ let to = target[prop];
91
+ let from = displayed[prop];
90
92
  if (from === to || from === undefined) continue;
91
93
  const duration = transitionFor(target, prop);
92
94
  if (duration <= 0) continue;
95
+ if (isCurrentColor(from) || isCurrentColor(to)) {
96
+ from = this._swapInk(prop, from, displayed);
97
+ to = this._swapInk(prop, to, target);
98
+ if (from === to) continue;
99
+ }
93
100
  if (interpolate(from, to, 0.5) === null) continue; // no midpoint: snap
94
101
  const entry = {
95
102
  from,
@@ -260,6 +267,27 @@ export class NodeAnimation {
260
267
  return this.style;
261
268
  }
262
269
 
270
+ /**
271
+ * A `currentColor` end of a transition as the colour it is at the swap:
272
+ * the ink `style` gives this node, or on `color` itself the ink it
273
+ * inherits. The keyword has no midpoint of its own, so the transition
274
+ * runs between the two colours as they are now — and lands on the style,
275
+ * where the keyword follows the ink again. An ink that moves mid-flight
276
+ * is caught up with at the end, not chased: a `color` that should carry
277
+ * a border with it is the property to transition.
278
+ */
279
+ _swapInk(prop, value, style) {
280
+ if (!isCurrentColor(value)) return value;
281
+ const inherited = this.inheritedTextStyle.color;
282
+ if (prop === 'color') return inherited;
283
+ // the displayed style's ink is cached; the target's is not yet
284
+ return style === this.style
285
+ ? this.resolvedTextStyle().color
286
+ : isColor(style.color)
287
+ ? style.color
288
+ : inherited;
289
+ }
290
+
263
291
  _animatedValues() {
264
292
  const values = {};
265
293
  for (const [prop, a] of this._anim) {
@@ -388,7 +416,7 @@ export class NodeAnimation {
388
416
  const specs =
389
417
  target.animation == null
390
418
  ? null
391
- : animationsOf(target, 'a style', this.scale);
419
+ : animationsOf(target, 'a style', this.scale, this._units);
392
420
  if (!specs && !this._loops) return false;
393
421
  this._loops = specs;
394
422
  if (!specs) this.root?._forgetLoopNode(this);
@@ -5,9 +5,11 @@
5
5
  import {
6
6
  DEFAULT_FOCUS_RING,
7
7
  isColor,
8
+ radiusOf,
8
9
  resolveBorderWidths,
9
10
  resolveBorderColors,
10
11
  } from '../styles.js';
12
+ import { isCurrentColor } from '../currentcolor.js';
11
13
  import {
12
14
  blurKernel,
13
15
  gradientSpec,
@@ -61,6 +63,19 @@ let warnedSideRadius = false;
61
63
 
62
64
  /** Box decorations, installed onto `Node.prototype` by node.js. */
63
65
  export class NodeBoxPaint {
66
+ /**
67
+ * A colour from this node's style as a colour a context can paint: the
68
+ * value itself, or — for CSS's `currentColor` — this node's resolved ink,
69
+ * its own `color` over what it inherits. Everything that paints a style
70
+ * colour asks here, on every backend, so the keyword means one thing
71
+ * wherever it is written; a registered element painting a colour of its
72
+ * own style asks the same. A node whose style paints in the keyword
73
+ * repaints when its ink moves (`_inkMoved`).
74
+ */
75
+ resolveColor(value) {
76
+ return isCurrentColor(value) ? this.resolvedTextStyle().color : value;
77
+ }
78
+
64
79
  /**
65
80
  * The focus ring this node would draw: its own `outline*` style, falling
66
81
  * back to the theme's focus-ring tokens for anything it leaves out. Null
@@ -80,7 +95,9 @@ export class NodeBoxPaint {
80
95
  if (!(resolved > 0)) return null;
81
96
  return {
82
97
  width: resolved,
83
- color: style.outlineColor ?? theme?.focusRing ?? DEFAULT_FOCUS_RING.color,
98
+ color: this.resolveColor(
99
+ style.outlineColor ?? theme?.focusRing ?? DEFAULT_FOCUS_RING.color,
100
+ ),
84
101
  offset:
85
102
  style.outlineOffset ??
86
103
  theme?.focusRingOffset ??
@@ -138,7 +155,7 @@ export class NodeBoxPaint {
138
155
  // stroked centred on the path, like the border, so half the width sits
139
156
  // inside the offset gap and half outside it
140
157
  const grow = offset + width / 2;
141
- const radius = this.style.borderRadius ?? 0;
158
+ const radius = radiusOf(this.style, this.abs);
142
159
  ctx.strokeStyle = outline.color;
143
160
  ctx.lineWidth = width;
144
161
  ctx.beginPath();
@@ -162,7 +179,8 @@ export class NodeBoxPaint {
162
179
  // under an element that covers this pass with opaque pixels, this fill
163
180
  // is never seen (`WindowNode._coverFor`)
164
181
  if (this.root?._coverChain?.has(this)) return;
165
- const { backgroundColor, borderRadius = 0 } = this.style;
182
+ const backgroundColor = this.resolveColor(this.style.backgroundColor);
183
+ const borderRadius = radiusOf(this.style, this.abs);
166
184
  const fill = (style) => {
167
185
  ctx.fillStyle = style;
168
186
  if (borderRadius > 0) {
@@ -202,7 +220,11 @@ export class NodeBoxPaint {
202
220
  const spec = gradientSpec(value, this.scale);
203
221
  if (!spec) return null;
204
222
  const { x, y, width, height } = rect;
205
- const key = `${value}|${x},${y},${width},${height}`;
223
+ // a stop in `currentColor` is the ink, which can move under a value
224
+ // that did not, so the ink is part of what the gradient is keyed by
225
+ const inked = spec.stops.some((stop) => isCurrentColor(stop.color));
226
+ const ink = inked ? this.resolvedTextStyle().color : '';
227
+ const key = `${value}|${x},${y},${width},${height}|${ink}`;
206
228
  if (this._gradient?.key === key) return this._gradient.value;
207
229
  const line = linearGradientGeometry(spec, rect);
208
230
  if (!line) return null;
@@ -213,7 +235,7 @@ export class NodeBoxPaint {
213
235
  line.y1,
214
236
  );
215
237
  for (const [offset, color] of line.stops)
216
- gradient.addColorStop(offset, color);
238
+ gradient.addColorStop(offset, inked ? this.resolveColor(color) : color);
217
239
  this._gradient = { key, value: gradient };
218
240
  return gradient;
219
241
  }
@@ -235,7 +257,7 @@ export class NodeBoxPaint {
235
257
  if (!value || value === 'none') return;
236
258
  const shadows = shadowSpecs(value, this.scale);
237
259
  if (!shadows?.length) return;
238
- const radius = this.style.borderRadius ?? 0;
260
+ const radius = radiusOf(this.style, this.abs);
239
261
  for (let i = shadows.length - 1; i >= 0; i--) {
240
262
  const shadow = shadows[i];
241
263
  // CSS's `currentColor`: this node's *resolved* ink, its own `color`
@@ -404,11 +426,15 @@ export class NodeBoxPaint {
404
426
  }
405
427
 
406
428
  _paintBorder(ctx) {
407
- const { borderRadius = 0 } = this.style;
429
+ const borderRadius = radiusOf(this.style, this.abs);
408
430
  // Both through the node's resolved direction: a `borderStartWidth` lays
409
431
  // out on one side and has to paint on the same one.
410
432
  const w = resolveBorderWidths(this.style, this.direction);
411
433
  const colors = resolveBorderColors(this.style, this.direction);
434
+ colors.top = this.resolveColor(colors.top);
435
+ colors.right = this.resolveColor(colors.right);
436
+ colors.bottom = this.resolveColor(colors.bottom);
437
+ colors.left = this.resolveColor(colors.left);
412
438
  const uniform =
413
439
  w.top === w.right &&
414
440
  w.top === w.bottom &&
@@ -469,7 +495,7 @@ export class NodeBoxPaint {
469
495
  * strokes halfway.
470
496
  */
471
497
  _paintBorderSides(ctx, w, colors) {
472
- if (DEV && (this.style.borderRadius ?? 0) > 0 && !warnedSideRadius) {
498
+ if (DEV && radiusOf(this.style, this.abs) > 0 && !warnedSideRadius) {
473
499
  warnedSideRadius = true;
474
500
  console.warn(
475
501
  'react-x11: borderRadius needs uniform borders — same width and ' +
@@ -5,12 +5,14 @@
5
5
  import {
6
6
  applyLayoutStyle,
7
7
  isColor,
8
+ paintsCurrentColor,
8
9
  textStyleFrom,
9
10
  DEFAULT_TEXT_STYLE,
10
11
  localTextStyleChanged,
11
12
  resolvedTextDelta,
12
13
  TEXT_REMEASURE,
13
14
  } from '../styles.js';
15
+ import { UNIT_EM, UNIT_REM } from '../shorthands.js';
14
16
  import { Yoga } from '../yoga.js';
15
17
  import { scaleOf } from '../scale.js';
16
18
  import { baseTheme } from '../palette.js';
@@ -372,8 +374,22 @@ export class NodeCascade {
372
374
  const before = this._resolvedText;
373
375
  if (before === undefined) return 0;
374
376
  this._resolvedText = undefined;
375
- const cost = resolvedTextDelta(before, this.resolvedTextStyle());
376
- if (cost !== 0) this._textStyleMoved(cost);
377
+ // A length in `em` is a length in the font size this node inherits,
378
+ // which is what this walk exists because of — so a node that wrote one
379
+ // resolves its style again here, before its own text is, since a
380
+ // `fontSize: '1.5em'` is part of what that text is set in.
381
+ if (this._emMoved()) this._unitsMoved();
382
+ const after = this.resolvedTextStyle();
383
+ const cost = resolvedTextDelta(before, after);
384
+ if (cost !== 0) {
385
+ this._textStyleMoved(cost);
386
+ // A node painting in `currentColor` resolved its ink to do it, so the
387
+ // walk reaches it — and its own style says nothing about the change,
388
+ // which was written on an ancestor or in its own `color`.
389
+ if (before.color !== after.color && paintsCurrentColor(this.style)) {
390
+ this._inkMoved();
391
+ }
392
+ }
377
393
  return cost;
378
394
  }
379
395
 
@@ -405,6 +421,19 @@ export class NodeCascade {
405
421
  }
406
422
  }
407
423
 
424
+ /**
425
+ * The ink moved under a node that paints something in it — a border, a
426
+ * fill, a gradient stop or a shadow in `currentColor`. A repaint of the
427
+ * node, bounded to it, and a box needs it although it draws no text
428
+ * (`BoxNode._textStyleMoved`): the ancestor whose `color` moved claimed
429
+ * its own box, which covers this one on a surface but names no node to a
430
+ * retained presenter re-rastering by node, and no other window to a
431
+ * `<popup>` written under it.
432
+ */
433
+ _inkMoved() {
434
+ this.root?.invalidate(false, this, 'ink');
435
+ }
436
+
408
437
  /**
409
438
  * Whether this node's ancestry is complete, so a `$token` that does not
410
439
  * resolve is a mistake rather than a node that has not been placed yet.
@@ -508,7 +537,9 @@ export class NodeCascade {
508
537
  // and the server's copy of that colour has to follow with it — otherwise
509
538
  // the next resize fills the new area in the old scheme.
510
539
  if (this.isWindow) this._syncWindowBackground();
511
- if (this._usesTokens) {
540
+ // A relative length reads the palette too: `rem` is its `fontSize`,
541
+ // and `em` is what that size became by the time it reached this node.
542
+ if (this._usesTokens || this._unitDeps & (UNIT_REM | UNIT_EM)) {
512
543
  const before = this.style;
513
544
  this._syncStyle(this.props, mounting);
514
545
  // a token change reaches the node without React re-rendering it, so
@@ -516,6 +547,15 @@ export class NodeCascade {
516
547
  if (localTextStyleChanged(this.style, before)) {
517
548
  this._textContentChanged();
518
549
  }
550
+ // …and so does the layout half. A token in a layout value —
551
+ // `padding: '$gutter'` — resolves right here, on attach or on a
552
+ // palette swap, and yoga was last handed the style the constructor
553
+ // had, with its tokens stripped. Same tail as `_sizeQueriesChanged`
554
+ // and `_rescaled`; without it a themed gutter reached `this.style`
555
+ // and the paint, and the layout kept the un-themed value.
556
+ if (this.yoga && this.style !== before) {
557
+ applyLayoutStyle(this.yoga, this.style, before);
558
+ }
519
559
  if (!mounting) this.root?.invalidate(true, null, 'theme');
520
560
  }
521
561
  // The palette is the floor under the cascade, so a theme swap moves the
@@ -624,4 +664,11 @@ export class WindowCascade {
624
664
  /** Like a `<box>`: a window fills and never letters, so it is the top of
625
665
  * the cascade rather than a reader of it. */
626
666
  _textStyleMoved() {}
667
+
668
+ /** …except a fill in `currentColor`, which the server keeps a copy of for
669
+ * what a resize exposes. */
670
+ _inkMoved() {
671
+ this._syncWindowBackground();
672
+ this.invalidate(false, null, 'ink');
673
+ }
627
674
  }
@@ -86,8 +86,10 @@ export const INVALIDATE_REASONS = new Set([
86
86
  'shadow', // a boxShadow got smaller: where it *was* still owes a repaint
87
87
  'outline', // …and the same for an outline a style swap took away
88
88
  'theme', // a theme/token change restyled a subtree
89
+ 'ink', // the ink under a currentColor moved: its `color` or an ancestor's
89
90
  'direction', // the reading direction moved: sides, glyph order, bar edge
90
91
  'scale', // a `scale` prop zoomed a subtree: every length in it moved
92
+ 'units', // a font size an `em` length read moved, and the style with it
91
93
  'animation', // a transition frame
92
94
  'scroll', // scrollTo/scrollBy/scrollIntoView, textarea/textinput panning
93
95
  'text', // text content, input value or caret editing
@@ -5,7 +5,7 @@
5
5
  import { Yoga } from '../yoga.js';
6
6
  import { callHandler } from '../errors.js';
7
7
  import { DAMAGE_SLOP, layoutDiff } from './damage.js';
8
- import { resolveBorderWidths } from '../styles.js';
8
+ import { radiusOf, resolveBorderWidths } from '../styles.js';
9
9
  import { insetRect, intersectRects, outerPixels, outside } from './rects.js';
10
10
  import { DEV } from './util.js';
11
11
 
@@ -612,25 +612,17 @@ export class NodeLayout {
612
612
  // (`roundedCorners`), so a resize that moves that moves all four
613
613
  // corners — and their outline and shadow with them — and the ones at the
614
614
  // edges that held are in no band along the edges that moved.
615
- const radius = style.borderRadius ?? 0;
615
+ // A percentage radius moves with the box for the same reason: it is a
616
+ // share of the shorter side (`radiusOf`), read off each rect.
616
617
  const now = this.abs;
617
- if (
618
- radius > 0 &&
619
- Math.min(radius, old.width / 2, old.height / 2) !==
620
- Math.min(radius, now.width / 2, now.height / 2)
621
- ) {
622
- return null;
623
- }
618
+ const corner = (rect) =>
619
+ Math.min(radiusOf(style, rect), rect.width / 2, rect.height / 2);
620
+ const radius = Math.max(radiusOf(style, old), radiusOf(style, now));
621
+ if (radius > 0 && corner(old) !== corner(now)) return null;
624
622
  const bw = resolveBorderWidths(style, this.direction);
625
623
  const reach =
626
- Math.max(
627
- style.borderRadius ?? 0,
628
- bw.top,
629
- bw.right,
630
- bw.bottom,
631
- bw.left,
632
- grow,
633
- ) + DAMAGE_SLOP;
624
+ Math.max(radius, bw.top, bw.right, bw.bottom, bw.left, grow) +
625
+ DAMAGE_SLOP;
634
626
  const bands = [];
635
627
  const oldBottom = old.y + old.height;
636
628
  const newBottom = now.y + now.height;
@@ -16,7 +16,7 @@
16
16
  // mistake it can make is not firing.
17
17
 
18
18
  import { cssColorStraight } from 'ntk/color';
19
- import { EMPTY_STYLE, resolveBorderWidths } from '../styles.js';
19
+ import { EMPTY_STYLE, radiusOf, resolveBorderWidths } from '../styles.js';
20
20
  import { isPaintedColor } from './boxpaint.js';
21
21
  import { DAMAGE_SLOP, FULL_DAMAGE, addDamageRect } from './damage.js';
22
22
  import { NodePaint } from './paint.js';
@@ -100,7 +100,7 @@ function cornerSquares(box, radius) {
100
100
  * Null when nothing is left.
101
101
  */
102
102
  function clipBoxOf(node) {
103
- const radius = node.style?.borderRadius ?? 0;
103
+ const radius = radiusOf(node.style, node.abs);
104
104
  return innerPixels(radius > 0 ? insetRect(node.abs, radius) : node.abs);
105
105
  }
106
106
 
@@ -166,9 +166,9 @@ export class NodeMoveBlit {
166
166
  const style = node.style ?? EMPTY_STYLE;
167
167
  if (style.display === 'none') return null;
168
168
  if (style.opacity !== undefined && !(style.opacity >= 1)) return null;
169
- const radius = style.borderRadius ?? 0;
169
+ const radius = radiusOf(style, node.abs);
170
170
  let own = null;
171
- if (isOpaqueColor(style.backgroundColor)) {
171
+ if (isOpaqueColor(node.resolveColor(style.backgroundColor))) {
172
172
  own = node.abs;
173
173
  holes.push(...cornerSquares(node.abs, radius));
174
174
  } else {
@@ -581,7 +581,7 @@ export class WindowMoveBlit {
581
581
  if (ring > 0) {
582
582
  const inner = insetRect(
583
583
  n.abs,
584
- Math.max(ring, style.borderRadius ?? 0) + DAMAGE_SLOP,
584
+ Math.max(ring, radiusOf(style, n.abs)) + DAMAGE_SLOP,
585
585
  );
586
586
  const outer = insetRect(n.abs, -DAMAGE_SLOP);
587
587
  for (const piece of outside(outer, inner)) over(piece);
package/src/nodes/node.js CHANGED
@@ -227,6 +227,14 @@ export class Node {
227
227
  // the layout that threw, with the style value that named it: flexbox
228
228
  // until the style names a different one
229
229
  this._layoutAbandoned = null;
230
+ // what the style's relative lengths read, and the context they read it
231
+ // through during a swap (`_noteUnits` in styling.js)
232
+ this._unitDeps = 0;
233
+ this._emBasis = undefined;
234
+ this._units = null;
235
+ this._unitsNow = null;
236
+ this._makeUnits = null;
237
+ this._sizeQueried = false;
230
238
  this._syncStyle(props);
231
239
  this.yoga = yoga ? createLayoutNode() : null;
232
240
  if (this.yoga) {
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { isPlaced } from '../layouts.js';
6
6
  import { Surface } from '../offscreen.js';
7
+ import { radiusOf } from '../styles.js';
7
8
  import { isPaintedColor } from './boxpaint.js';
8
9
  import { DAMAGE_SLOP } from './damage.js';
9
10
  import { DRAWN_KINDS } from './kinds.js';
@@ -357,7 +358,7 @@ export class NodePaint {
357
358
  // a context with no `roundRect` has always clipped a rounded box to its
358
359
  // rectangle, the way `_roundedPath` degrades
359
360
  const radius =
360
- typeof ctx.roundRect === 'function' ? (this.style.borderRadius ?? 0) : 0;
361
+ typeof ctx.roundRect === 'function' ? radiusOf(this.style, this.abs) : 0;
361
362
  if (radius > 0 && this._paintRoundedChildren(ctx, order, radius)) return;
362
363
  const clip = this._childrenCanOverflow();
363
364
  if (clip) {
@@ -661,7 +662,8 @@ export class WindowPaint {
661
662
  * Shape extension's hard 1-bit edge.
662
663
  */
663
664
  _paintWindowBackground(ctx, damage, width, height) {
664
- const { backgroundColor, borderRadius = 0 } = this.style;
665
+ const backgroundColor = this.resolveColor(this.style.backgroundColor);
666
+ const borderRadius = radiusOf(this.style, { width, height });
665
667
  if (DEV && this.style.boxShadow) devWarnWindowShadow(this.kind);
666
668
  // A `backgroundImage` works wherever a `backgroundColor` does, which is
667
669
  // the rule worth having — over the whole window, in window coordinates,
@@ -183,42 +183,54 @@ export class NodeQueries {
183
183
  // what pushes that into the subtree; only the node-local text props are
184
184
  // left to notice here
185
185
  this._syncStyle(this.props);
186
+ this._restyled(before, 'applied when a query block started matching');
187
+ }
188
+
189
+ /**
190
+ * What a style resolved again outside a commit owes, past the swap itself:
191
+ * the node-local text props, and the layout. Returns whether the layout
192
+ * moved. A resize, a container settling, a font size a relative length
193
+ * read — none of them is React re-rendering the node, so nothing else
194
+ * applies it.
195
+ *
196
+ * A block that moved a layout property changed the tree the content floors
197
+ * were measured from — the debt a style change from React leaves too
198
+ * (`invalidate`, reason 'props'). It has to be marked as a *content*
199
+ * change: the live-resize deferral takes a plain `_floorsDirty` for the
200
+ * drag itself and lays out against the floors in hand, which are the old
201
+ * arrangement's, and by the time the catch-up looks the dirty flags are
202
+ * spent and no leaf's height moved — so the floor of a card that turned
203
+ * from a row into a column would stay the row's, and yoga would squeeze the
204
+ * column down to it.
205
+ *
206
+ * This can run in a frame, so a value the layout refuses is dropped and
207
+ * reported rather than thrown: nothing would catch it. `how` is the clause
208
+ * the report says it was applied by.
209
+ */
210
+ _restyled(before, how) {
186
211
  if (localTextStyleChanged(this.style, before)) this._textContentChanged();
187
- if (this.yoga && this.style !== before) {
188
- // A block that moved a layout property changed the tree the content
189
- // floors were measured from — the debt a style change from React
190
- // leaves too (`invalidate`, reason 'props'). It has to be marked as a
191
- // *content* change: the live-resize deferral takes a plain
192
- // `_floorsDirty` for the drag itself and lays out against the floors
193
- // in hand, which are the old arrangement's, and by the time the
194
- // catch-up looks the dirty flags are spent and no leaf's height moved
195
- // — so the floor of a card that turned from a row into a column would
196
- // stay the row's, and yoga would squeeze the column down to it.
197
- //
198
- // This runs in a frame, so a value the layout refuses is dropped and
199
- // reported rather than thrown: nothing would catch it.
200
- const refused = [];
201
- const moved = applyLayoutStyle(this.yoga, this.style, before, refused);
202
- for (const { key, value, error } of refused) {
203
- // an enum's own message already says what it is and what it takes
204
- const said = error.message.startsWith('react-x11:')
205
- ? error.message
206
- : `react-x11: invalid ${key} ${JSON.stringify(value)} ` +
207
- `(${error.message})`;
208
- reportStyleProblem(
209
- this,
210
- `${said} in <${this.kind} style>, applied when a query block ` +
211
- 'started matching',
212
- 'The declaration is dropped, as CSS drops one it cannot parse',
213
- );
214
- }
215
- if (moved && this.root) {
216
- this.root._floorsDirty = true;
217
- this.root._floorsContentDirty = true;
218
- this.root._floorsSources.add(this);
219
- this.root._floorsOtherSources.add(this);
220
- }
212
+ if (!this.yoga || this.style === before) return false;
213
+ const refused = [];
214
+ const moved = applyLayoutStyle(this.yoga, this.style, before, refused);
215
+ for (const { key, value, error } of refused) {
216
+ // an enum's own message already says what it is and what it takes
217
+ const said = error.message.startsWith('react-x11:')
218
+ ? error.message
219
+ : `react-x11: invalid ${key} ${JSON.stringify(value)} ` +
220
+ `(${error.message})`;
221
+ reportStyleProblem(
222
+ this,
223
+ `${said} in <${this.kind} style>, ${how}`,
224
+ 'The declaration is dropped, as CSS drops one it cannot parse',
225
+ );
226
+ }
227
+ if (moved && this.root) {
228
+ this.root._floorsDirty = true;
229
+ this.root._floorsContentDirty = true;
230
+ this.root._floorsSources.add(this);
231
+ this.root._floorsOtherSources.add(this);
221
232
  }
233
+ return moved;
222
234
  }
223
235
  }
224
236
 
@@ -247,7 +247,10 @@ export const Scrollable = (Base) =>
247
247
  // NaN, which a scroll pane in there measured its content from and
248
248
  // threw on, out of the frame. The box itself has its empty rect;
249
249
  // what is inside is placed when it is shown and laid out again.
250
- if (this.style.display !== 'none') {
250
+ // Both routes to it: the style, and the `hidden` React sets for
251
+ // `<Activity>` and `<Suspense>` (`setHidden`), which reaches yoga
252
+ // without touching the style.
253
+ if (this.style.display !== 'none' && !this.hidden) {
251
254
  this._absolutizeChildren(
252
255
  this.abs.x,
253
256
  this.abs.y,
@@ -7,7 +7,7 @@
7
7
  // applies the blits at flush. Every failure falls back to repainting the
8
8
  // viewport, so the fast path can cost correctness nothing.
9
9
 
10
- import { EMPTY_STYLE, resolveBorderWidths } from '../styles.js';
10
+ import { EMPTY_STYLE, radiusOf, resolveBorderWidths } from '../styles.js';
11
11
  import {
12
12
  DAMAGE_SLOP,
13
13
  MAX_DAMAGE_RECTS,
@@ -648,7 +648,10 @@ export class NodeScrollBlit {
648
648
  _paintsNothingOwn() {
649
649
  if (this.kind !== 'box' || this.isScroller?.()) return false;
650
650
  const style = this.style ?? EMPTY_STYLE;
651
- if (isPaintedColor(style.backgroundColor) || style.backgroundImage) {
651
+ if (
652
+ isPaintedColor(this.resolveColor(style.backgroundColor)) ||
653
+ style.backgroundImage
654
+ ) {
652
655
  return false;
653
656
  }
654
657
  if (style.opacity !== undefined && style.opacity < 1) return false;
@@ -763,7 +766,7 @@ export class WindowScrollBlit {
763
766
  blitBorder.right > 0 ||
764
767
  blitBorder.bottom > 0 ||
765
768
  blitBorder.left > 0 ||
766
- node.style.borderRadius > 0
769
+ radiusOf(node.style, node.abs) > 0
767
770
  ) {
768
771
  return;
769
772
  }
@@ -1101,7 +1104,7 @@ export class WindowScrollBlit {
1101
1104
  bw.right,
1102
1105
  bw.bottom,
1103
1106
  bw.left,
1104
- node.style?.borderRadius ?? 0,
1107
+ radiusOf(node.style, node.abs),
1105
1108
  );
1106
1109
  if (!rectContains(insetRect(node.abs, inset), vp)) return;
1107
1110
  // A scroll container's children *are* the scrolled content, which is
@@ -1229,7 +1232,7 @@ export class WindowScrollBlit {
1229
1232
  _cornerPins(node, vp, upTo = this) {
1230
1233
  let out = null;
1231
1234
  for (let n = node.parent; n && n !== upTo; n = n.parent) {
1232
- const radius = n.style?.borderRadius ?? 0;
1235
+ const radius = radiusOf(n.style, n.abs);
1233
1236
  if (!(radius > 0) || !n.abs) continue;
1234
1237
  for (const square of cornerSquares(n.abs, radius)) {
1235
1238
  if (!rectsOverlap(square, vp)) continue;
@@ -1295,7 +1298,7 @@ export class WindowScrollBlit {
1295
1298
  // squares is translation-safe, and so is an element blit that
1296
1299
  // pins the squares it reaches into (`_cornerPins`) — repaired
1297
1300
  // where they are and where the copy dragged them.
1298
- const radius = child.style?.borderRadius ?? 0;
1301
+ const radius = radiusOf(child.style, child.abs);
1299
1302
  if (radius > 0 && !cornersPinned(child.abs, radius, vp, pinned)) {
1300
1303
  return false;
1301
1304
  }
@@ -1407,7 +1410,7 @@ export class WindowScrollBlit {
1407
1410
  break;
1408
1411
  }
1409
1412
  if (n.clipsChildren()) {
1410
- const radius = n.style?.borderRadius ?? 0;
1413
+ const radius = radiusOf(n.style, n.abs);
1411
1414
  cover = intersectRects(
1412
1415
  cover,
1413
1416
  radius > 0 ? insetRect(n.abs, radius) : n.abs,