react-x11 2.22.13 → 2.23.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 +1 -1
- package/src/events.js +28 -6
- package/src/frame/index.js +7 -2
- package/src/node.d.ts +9 -0
- package/src/nodes/animation.js +8 -2
- package/src/nodes/layouthost.js +3 -2
- package/src/nodes/queries.js +20 -1
- package/src/nodes/scrollable.js +19 -8
- package/src/nodes/text.js +6 -2
- package/src/nodes/window/floors.js +7 -9
- package/src/palette.js +19 -0
- package/src/styles.js +202 -16
- package/src/types/nodes.d.ts +2 -1
package/package.json
CHANGED
package/src/events.js
CHANGED
|
@@ -1213,7 +1213,7 @@ export class EventManager {
|
|
|
1213
1213
|
n.props.onMouseEnter?.(this._makeEvent('mouseEnter', native, n));
|
|
1214
1214
|
}
|
|
1215
1215
|
this.hoverPath = newPath;
|
|
1216
|
-
this._updateCursor(newPath);
|
|
1216
|
+
this._updateCursor(newPath, native);
|
|
1217
1217
|
}
|
|
1218
1218
|
|
|
1219
1219
|
/**
|
|
@@ -1322,14 +1322,32 @@ export class EventManager {
|
|
|
1322
1322
|
callHandler(node, 'unstable_onAttention', handler, ev);
|
|
1323
1323
|
}
|
|
1324
1324
|
|
|
1325
|
-
/** Apply the deepest hovered node's
|
|
1326
|
-
*
|
|
1327
|
-
|
|
1325
|
+
/** Apply the deepest hovered node's cursor to the window: its `cursor`
|
|
1326
|
+
* style, or what it says of the point under the pointer (`cursorAt`), or
|
|
1327
|
+
* its `defaultCursor` — and failing all three, its parent's. Asked on
|
|
1328
|
+
* every motion. Feature-detected: needs ntk with Window.setCursor
|
|
1329
|
+
* (> 3.1.0). */
|
|
1330
|
+
_updateCursor(path, native = null) {
|
|
1328
1331
|
const wnd = this.node.window;
|
|
1329
1332
|
if (typeof wnd.setCursor !== 'function') return;
|
|
1333
|
+
// An element that draws what is inside it — a document, a graph, an
|
|
1334
|
+
// editor — is one node with many cursors, and only it knows which part
|
|
1335
|
+
// is a link, a handle or text: it answers for the point, in the device
|
|
1336
|
+
// pixels `abs` is in, as `textIndexAt` does. While a capture holds the
|
|
1337
|
+
// pointer it is not asked — hover is frozen for the gesture and the
|
|
1338
|
+
// point may be anywhere — and keeps what it named where the gesture
|
|
1339
|
+
// began, while a style the press changed still shows.
|
|
1340
|
+
const asking = native != null && !this._captured();
|
|
1341
|
+
const named = (this._pointCursors ??= new WeakMap());
|
|
1330
1342
|
let cursor = null;
|
|
1331
1343
|
for (let i = path.length - 1; i >= 0; i--) {
|
|
1332
|
-
const
|
|
1344
|
+
const node = path[i];
|
|
1345
|
+
let c = node.style.cursor;
|
|
1346
|
+
if (c == null && node.cursorAt) {
|
|
1347
|
+
if (asking) named.set(node, node.cursorAt(native.x, native.y) ?? null);
|
|
1348
|
+
c = named.get(node) ?? null;
|
|
1349
|
+
}
|
|
1350
|
+
c ??= node.defaultCursor;
|
|
1333
1351
|
if (c != null) {
|
|
1334
1352
|
cursor = c;
|
|
1335
1353
|
break;
|
|
@@ -1687,7 +1705,11 @@ export class EventManager {
|
|
|
1687
1705
|
// …and nothing scrolls to meet it either: the node is coming back to
|
|
1688
1706
|
// the arrangement it left, and this reveal's layout has not run, so a
|
|
1689
1707
|
// scroll here would be computed from a rect that does not exist yet.
|
|
1690
|
-
|
|
1708
|
+
// Nor for a press: the node is under the pointer, so the part that
|
|
1709
|
+
// matters is on screen already, and a document taller than its pane
|
|
1710
|
+
// was scrolled to its top by every click on it — the drag that
|
|
1711
|
+
// followed selecting from there.
|
|
1712
|
+
if (!restoring && reason !== 'pointer') this._scrollIntoView(node);
|
|
1691
1713
|
if (this.keyboardFocused) {
|
|
1692
1714
|
this._defaultFocusOn = true;
|
|
1693
1715
|
node.defaultFocus?.({ reason, backwards });
|
package/src/frame/index.js
CHANGED
|
@@ -45,6 +45,7 @@ import React, {
|
|
|
45
45
|
|
|
46
46
|
import { useAppOrNull } from '../appcontext.js';
|
|
47
47
|
import { canEmbed } from '../embedding.js';
|
|
48
|
+
import { WHEEL_NOTCH_PX } from '../events.js';
|
|
48
49
|
import { FrameEnv } from './env.js';
|
|
49
50
|
import { CallbackTable, PROTOCOL } from './protocol.js';
|
|
50
51
|
|
|
@@ -607,9 +608,13 @@ function PaneHostView({
|
|
|
607
608
|
onMouseDown: forward('mousedown', (ev) => ({ keycode: ev.button ?? 1 })),
|
|
608
609
|
onMouseUp: forward('mouseup', (ev) => ({ keycode: ev.button ?? 1 })),
|
|
609
610
|
onMouseMove: forward('mousemove', () => ({})),
|
|
611
|
+
// The host's event is in pixels — events.js turned each notch into
|
|
612
|
+
// WHEEL_NOTCH_PX of them — and the pane's window turns what it is sent
|
|
613
|
+
// into pixels again, so it is sent notches: sent the pixels, one notch
|
|
614
|
+
// scrolled a page 2304 of them.
|
|
610
615
|
onWheel: forward('wheel', (ev) => ({
|
|
611
|
-
deltaX: ev.deltaX ?? 0,
|
|
612
|
-
deltaY: ev.deltaY ?? 0,
|
|
616
|
+
deltaX: (ev.deltaX ?? 0) / WHEEL_NOTCH_PX,
|
|
617
|
+
deltaY: (ev.deltaY ?? 0) / WHEEL_NOTCH_PX,
|
|
613
618
|
deltaMode: ev.deltaMode ?? 'line',
|
|
614
619
|
smooth: Boolean(ev.smooth),
|
|
615
620
|
source: 'forwarded',
|
package/src/node.d.ts
CHANGED
|
@@ -699,6 +699,15 @@ export declare class Node {
|
|
|
699
699
|
/** The cursor to show over this element when nothing in the style says
|
|
700
700
|
* otherwise — `'text'` for something editable. A `cursor` style wins. */
|
|
701
701
|
defaultCursor?: string;
|
|
702
|
+
/**
|
|
703
|
+
* The cursor for a point inside this element, for one that draws parts
|
|
704
|
+
* only it can tell apart — a link in a document, a handle in a graph, the
|
|
705
|
+
* text in an editor. Asked on every motion over it, with the pointer in
|
|
706
|
+
* device pixels, the unit `abs` is in. Null says nothing in particular is
|
|
707
|
+
* there, and `defaultCursor` answers. A `cursor` style still wins, and a
|
|
708
|
+
* capture holding the pointer freezes the cursor with the rest of hover.
|
|
709
|
+
*/
|
|
710
|
+
cursorAt?(x: number, y: number): string | null;
|
|
702
711
|
/**
|
|
703
712
|
* The ARIA role this element is when the application writes none — the
|
|
704
713
|
* registered-element counterpart of `<textinput>` defaulting to
|
package/src/nodes/animation.js
CHANGED
|
@@ -138,8 +138,14 @@ export class NodeAnimation {
|
|
|
138
138
|
}
|
|
139
139
|
// A grid reads its tracks off the style, where yoga never sees them, so
|
|
140
140
|
// a change to one asks the algorithm again — and gives one that threw
|
|
141
|
-
// another go.
|
|
142
|
-
|
|
141
|
+
// another go. Asked of a layout host alone, the only node it acts on:
|
|
142
|
+
// seven properties read off two styles of every shape is a scan every
|
|
143
|
+
// node paid on each of the several swaps a mount makes, 3% of it.
|
|
144
|
+
if (
|
|
145
|
+
this.yoga &&
|
|
146
|
+
(this._host !== null || this._layoutAbandoned !== null) &&
|
|
147
|
+
gridContainerMoved(displayed, this.style)
|
|
148
|
+
) {
|
|
143
149
|
if (this._host !== null) this._hostChanged();
|
|
144
150
|
else if (this._layoutAbandoned !== null) {
|
|
145
151
|
this._layoutAbandoned = null;
|
package/src/nodes/layouthost.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
createLayoutNode,
|
|
9
9
|
isMeasuringExactly,
|
|
10
10
|
measuringExactly,
|
|
11
|
+
setLayoutValue,
|
|
11
12
|
} from '../styles.js';
|
|
12
13
|
import { Yoga } from '../yoga.js';
|
|
13
14
|
import {
|
|
@@ -342,11 +343,11 @@ export class NodeLayoutHost {
|
|
|
342
343
|
// A floor written while it was a flex item means nothing to a layout,
|
|
343
344
|
// which sizes the child itself — and would outlast it as a minimum.
|
|
344
345
|
if (child._floorMinW != null) {
|
|
345
|
-
cy
|
|
346
|
+
setLayoutValue(cy, 'minWidth', child.style.minWidth);
|
|
346
347
|
child._floorMinW = undefined;
|
|
347
348
|
}
|
|
348
349
|
if (child._floorMinH != null) {
|
|
349
|
-
cy
|
|
350
|
+
setLayoutValue(cy, 'minHeight', child.style.minHeight);
|
|
350
351
|
child._floorMinH = undefined;
|
|
351
352
|
}
|
|
352
353
|
child._hostSlot = null;
|
package/src/nodes/queries.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
localTextStyleChanged,
|
|
8
8
|
containerAnswers,
|
|
9
9
|
} from '../styles.js';
|
|
10
|
+
import { reportStyleProblem } from '../errors.js';
|
|
10
11
|
import { DEV } from './util.js';
|
|
11
12
|
|
|
12
13
|
/** How many extra layout passes a flush spends settling `@container` blocks
|
|
@@ -193,7 +194,25 @@ export class NodeQueries {
|
|
|
193
194
|
// catch-up looks the dirty flags are spent and no leaf's height moved
|
|
194
195
|
// — so the floor of a card that turned from a row into a column would
|
|
195
196
|
// stay the row's, and yoga would squeeze the column down to it.
|
|
196
|
-
|
|
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) {
|
|
197
216
|
this.root._floorsDirty = true;
|
|
198
217
|
this.root._floorsContentDirty = true;
|
|
199
218
|
this.root._floorsSources.add(this);
|
package/src/nodes/scrollable.js
CHANGED
|
@@ -889,14 +889,8 @@ export const Scrollable = (Base) =>
|
|
|
889
889
|
? this.yoga.getComputedWidth() - left - w
|
|
890
890
|
: left;
|
|
891
891
|
const far = near + w;
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
if (top < this.scrollY) this.scrollY = top;
|
|
896
|
-
if (far > this.scrollX + this.abs.width) {
|
|
897
|
-
this.scrollX = far - this.abs.width;
|
|
898
|
-
}
|
|
899
|
-
if (near < this.scrollX) this.scrollX = near;
|
|
892
|
+
this.scrollY = nearestScroll(this.scrollY, this.abs.height, top, bottom);
|
|
893
|
+
this.scrollX = nearestScroll(this.scrollX, this.abs.width, near, far);
|
|
900
894
|
}
|
|
901
895
|
|
|
902
896
|
paint(ctx) {
|
|
@@ -1035,6 +1029,23 @@ export const Scrollable = (Base) =>
|
|
|
1035
1029
|
}
|
|
1036
1030
|
};
|
|
1037
1031
|
|
|
1032
|
+
/**
|
|
1033
|
+
* Where one axis of a viewport scrolls to bring `[start, end)` into view,
|
|
1034
|
+
* the way CSSOM View's `block: nearest` does it: a span already inside, or
|
|
1035
|
+
* one that covers the viewport, leaves it where it is — nothing more of it
|
|
1036
|
+
* can be shown — and one partly or wholly out comes in by the edge that
|
|
1037
|
+
* moves it least, which is its near edge where it fits and its far edge
|
|
1038
|
+
* where it is the larger. A document taller than its pane was scrolled to
|
|
1039
|
+
* its top, wherever it had been read to.
|
|
1040
|
+
*/
|
|
1041
|
+
function nearestScroll(scroll, size, start, end) {
|
|
1042
|
+
const before = start < scroll;
|
|
1043
|
+
const after = end > scroll + size;
|
|
1044
|
+
if (before === after) return scroll;
|
|
1045
|
+
const larger = end - start > size;
|
|
1046
|
+
return before !== larger ? start : end - size;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1038
1049
|
// An arrow key scrolls by a wheel notch — literally the one events.js
|
|
1039
1050
|
// converts a notch into, so the two input routes agree about what one step
|
|
1040
1051
|
// is however far a notch turns out to be.
|
package/src/nodes/text.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// <text> and its chunks: shaped, wrapped and elided text, the line bands a
|
|
2
2
|
// selection paints, and the glyph strip.
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
lineHeightOf,
|
|
6
|
+
localTextStyleChanged,
|
|
7
|
+
TEXT_REMEASURE,
|
|
8
|
+
} from '../styles.js';
|
|
5
9
|
import { cssColorStraight } from 'ntk/color';
|
|
6
10
|
import { hooks as a11yHooks } from '../a11y.js';
|
|
7
11
|
import { codePointAtOffset, codeUnitOffsets } from '../textrange.js';
|
|
@@ -419,7 +423,7 @@ export class TextNode extends Node {
|
|
|
419
423
|
layout = fonts.layout(spans, base, {
|
|
420
424
|
maxWidth: Number.isFinite(maxWidth) ? maxWidth : undefined,
|
|
421
425
|
align: this.style.textAlign,
|
|
422
|
-
lineHeight: this.style.lineHeight,
|
|
426
|
+
lineHeight: lineHeightOf(this.style.lineHeight),
|
|
423
427
|
maxLines: Number.isFinite(maxLines) ? maxLines : undefined,
|
|
424
428
|
// 'clip' is ntk's default, so an unset property and the CSS default
|
|
425
429
|
// are the same request rather than two paths through the layout.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// minimums. Free functions over nodes; WindowNode drives them from size.js,
|
|
4
4
|
// and a layout host from layouthost.js.
|
|
5
5
|
|
|
6
|
+
import { setLayoutValue } from '../../styles.js';
|
|
6
7
|
import { Yoga } from '../../yoga.js';
|
|
7
8
|
import { NO_CHILDREN } from '../util.js';
|
|
8
9
|
|
|
@@ -688,7 +689,9 @@ export function freezeWidths(node, out) {
|
|
|
688
689
|
|
|
689
690
|
/** …and back to the width the style asks for, on exactly what was pinned. */
|
|
690
691
|
export function restoreWidths(frozen) {
|
|
691
|
-
for (const child of frozen)
|
|
692
|
+
for (const child of frozen) {
|
|
693
|
+
setLayoutValue(child.yoga, 'width', child.style.width);
|
|
694
|
+
}
|
|
692
695
|
}
|
|
693
696
|
|
|
694
697
|
/**
|
|
@@ -909,14 +912,9 @@ export function writeFloors(node, axis) {
|
|
|
909
912
|
if (extent > 0) floor = extent;
|
|
910
913
|
}
|
|
911
914
|
if ((horizontal ? child._floorMinW : child._floorMinH) === floor) continue;
|
|
912
|
-
|
|
913
|
-
if (horizontal)
|
|
914
|
-
|
|
915
|
-
child._floorMinW = floor;
|
|
916
|
-
} else {
|
|
917
|
-
child.yoga.setMinHeight(value);
|
|
918
|
-
child._floorMinH = floor;
|
|
919
|
-
}
|
|
915
|
+
setLayoutValue(child.yoga, own, floor ?? child.style[own]);
|
|
916
|
+
if (horizontal) child._floorMinW = floor;
|
|
917
|
+
else child._floorMinH = floor;
|
|
920
918
|
}
|
|
921
919
|
}
|
|
922
920
|
|
package/src/palette.js
CHANGED
|
@@ -503,6 +503,25 @@ const desktopPalettes = new Map();
|
|
|
503
503
|
* `'no-preference'` means *use your own default*, which is the light one.
|
|
504
504
|
*/
|
|
505
505
|
export function paletteFor(appearance) {
|
|
506
|
+
// An appearance is a frozen snapshot, replaced whole when the desktop
|
|
507
|
+
// changes (appearance.js), so the last one asked about answers by
|
|
508
|
+
// identity. `baseTheme()` asks with the same snapshot for every node
|
|
509
|
+
// created — each one detached, with no theme above it yet — and spelling
|
|
510
|
+
// the key to the palettes below was 200 ns of each on a desktop with an
|
|
511
|
+
// accent: a thousand rows scrolling into a table, a document mounting.
|
|
512
|
+
if (appearance === lastAppearance) return lastPalette;
|
|
513
|
+
const palette = paletteOf(appearance);
|
|
514
|
+
if (Object.isFrozen(appearance)) {
|
|
515
|
+
lastAppearance = appearance;
|
|
516
|
+
lastPalette = palette;
|
|
517
|
+
}
|
|
518
|
+
return palette;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
let lastAppearance = null;
|
|
522
|
+
let lastPalette = null;
|
|
523
|
+
|
|
524
|
+
function paletteOf(appearance) {
|
|
506
525
|
const scheme = appearance.colorScheme === 'dark' ? DarkTheme : DefaultTheme;
|
|
507
526
|
if (appearance.palette) return withDesktopPalette(scheme, appearance.palette);
|
|
508
527
|
const { accent, accentText, selection } = appearance;
|
package/src/styles.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
GRID_CONTAINER_PROPS,
|
|
12
12
|
GRID_ITEM_PROPS,
|
|
13
13
|
} from './grid.js';
|
|
14
|
-
import { Yoga } from './yoga.js';
|
|
14
|
+
import { Yoga, layoutLoaded } from './yoga.js';
|
|
15
15
|
|
|
16
16
|
const FLEX_DIRECTION = {
|
|
17
17
|
row: Yoga.FLEX_DIRECTION_ROW,
|
|
@@ -855,6 +855,19 @@ const GRID_VALUES = new Set([...GRID_CONTAINER_PROPS, ...GRID_ITEM_PROPS]);
|
|
|
855
855
|
* grammar is concerned), so this checks the shape and never the colours.
|
|
856
856
|
*/
|
|
857
857
|
function validateValue(key, value, where) {
|
|
858
|
+
const text = TEXT_VALUE_CHECKS.get(key);
|
|
859
|
+
if (text !== undefined) {
|
|
860
|
+
// unset, the way a conditional writes it
|
|
861
|
+
if (value == null || value === '' || isToken(value)) return;
|
|
862
|
+
const [valid, expected] = text;
|
|
863
|
+
if (!valid(value)) {
|
|
864
|
+
throw new Error(
|
|
865
|
+
`react-x11: invalid ${key} ${JSON.stringify(value)} in ${where} ` +
|
|
866
|
+
`(expected ${expected})`,
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
858
871
|
if (key === 'letterSpacing') {
|
|
859
872
|
if (
|
|
860
873
|
value != null &&
|
|
@@ -913,6 +926,26 @@ function validateValue(key, value, where) {
|
|
|
913
926
|
}
|
|
914
927
|
return;
|
|
915
928
|
}
|
|
929
|
+
// A layout value the layout cannot take — `width: 'hidden'`, `padding:
|
|
930
|
+
// 'auto'`, `flexDirection: 'sideways'` — throws where it is applied, and
|
|
931
|
+
// inside a query block that is a frame, not a commit: a block that starts
|
|
932
|
+
// matching on a resize took the process down. So it is asked here, of the
|
|
933
|
+
// same appliers, wherever it is written. A `$token` is not a length until
|
|
934
|
+
// it resolves, and is answered by the frame then (`_sizeQueriesChanged`).
|
|
935
|
+
const refused =
|
|
936
|
+
isToken(value) || mentionsToken(value) ? null : layoutRefusal(key, value);
|
|
937
|
+
if (refused) {
|
|
938
|
+
// an enum's own message lists the choices; yoga's names only itself
|
|
939
|
+
if (refused.message.startsWith('react-x11:')) {
|
|
940
|
+
refused.message += `\n in ${where}`;
|
|
941
|
+
throw refused;
|
|
942
|
+
}
|
|
943
|
+
const auto = layoutRefusal(key, 'auto') === null ? ", or 'auto'" : '';
|
|
944
|
+
throw new Error(
|
|
945
|
+
`react-x11: invalid ${key} ${JSON.stringify(value)} in ${where} ` +
|
|
946
|
+
`(expected a number of pixels or a percentage like '50%'${auto})`,
|
|
947
|
+
);
|
|
948
|
+
}
|
|
916
949
|
const grid = GRID_VALUES.has(key);
|
|
917
950
|
if (!grid && key !== 'backgroundImage' && key !== 'boxShadow') return;
|
|
918
951
|
try {
|
|
@@ -1566,11 +1599,15 @@ const TOKEN_IN_VALUE = /\$[A-Za-z_][A-Za-z0-9_-]*/g;
|
|
|
1566
1599
|
const mentionsToken = (v) =>
|
|
1567
1600
|
typeof v === 'string' && v.charCodeAt(0) !== 36 && v.includes('$');
|
|
1568
1601
|
|
|
1569
|
-
/** The nested blocks a token can hide in, one level down: a state block
|
|
1570
|
-
* whose values are style values, and `animation`, whose
|
|
1571
|
-
* per-property declarations *whose* values are style
|
|
1572
|
-
* walked by everything that looks for a `$name`, so a
|
|
1573
|
-
* by the same rules as a themed hover
|
|
1602
|
+
/** The nested blocks a token can hide in, one level down: a state block or
|
|
1603
|
+
* a query block, whose values are style values, and `animation`, whose
|
|
1604
|
+
* values are the per-property declarations *whose* values are style
|
|
1605
|
+
* values. All are walked by everything that looks for a `$name`, so a
|
|
1606
|
+
* themed loop resolves by the same rules as a themed hover — and a themed
|
|
1607
|
+
* wide layout too. The query blocks were missed: `'@width >= 600':
|
|
1608
|
+
* { padding: '$gutter' }` handed yoga the string `'$gutter'` the moment
|
|
1609
|
+
* it matched, which threw from the frame. */
|
|
1610
|
+
const holdsStyle = (key) => isState(key) || isQuery(key);
|
|
1574
1611
|
const animationBlocks = (style) =>
|
|
1575
1612
|
style.animation && typeof style.animation === 'object'
|
|
1576
1613
|
? Object.values(style.animation).filter((e) => e && typeof e === 'object')
|
|
@@ -1580,7 +1617,7 @@ export function styleUsesTokens(style) {
|
|
|
1580
1617
|
for (const key of Object.keys(style)) {
|
|
1581
1618
|
const v = style[key];
|
|
1582
1619
|
if (isToken(v) || mentionsToken(v)) return true;
|
|
1583
|
-
if (key
|
|
1620
|
+
if (holdsStyle(key) && v && styleUsesTokens(v)) return true;
|
|
1584
1621
|
}
|
|
1585
1622
|
return animationBlocks(style).some(styleUsesTokens);
|
|
1586
1623
|
}
|
|
@@ -1594,7 +1631,7 @@ export function tokenNames(style, out = new Set()) {
|
|
|
1594
1631
|
if (isToken(v)) out.add(v);
|
|
1595
1632
|
else if (mentionsToken(v))
|
|
1596
1633
|
for (const m of v.match(TOKEN_IN_VALUE) ?? []) out.add(m);
|
|
1597
|
-
else if (key
|
|
1634
|
+
else if (holdsStyle(key) && v) tokenNames(v, out);
|
|
1598
1635
|
}
|
|
1599
1636
|
for (const block of animationBlocks(style)) tokenNames(block, out);
|
|
1600
1637
|
return out;
|
|
@@ -1624,7 +1661,7 @@ export function stripTokens(style) {
|
|
|
1624
1661
|
if (key === 'animation' && animationBlocks(style).some(styleUsesTokens)) {
|
|
1625
1662
|
continue;
|
|
1626
1663
|
}
|
|
1627
|
-
out[key] = key
|
|
1664
|
+
out[key] = holdsStyle(key) && v ? stripTokens(v) : v;
|
|
1628
1665
|
}
|
|
1629
1666
|
return out;
|
|
1630
1667
|
}
|
|
@@ -1691,7 +1728,7 @@ export function resolveTokens(
|
|
|
1691
1728
|
else if (strict) {
|
|
1692
1729
|
found.push(unknownToken(unknown, theme, `${where} ${key}`));
|
|
1693
1730
|
}
|
|
1694
|
-
} else if (key
|
|
1731
|
+
} else if (holdsStyle(key) && v) {
|
|
1695
1732
|
out[key] = resolveTokens(v, theme, `${where} ${key}`, strict, found);
|
|
1696
1733
|
} else if (key === 'animation' && v && typeof v === 'object') {
|
|
1697
1734
|
const loops = {};
|
|
@@ -2011,9 +2048,22 @@ const NO_STYLE = Object.freeze({});
|
|
|
2011
2048
|
* name of both styles was most of the cost of a node that mounts — a
|
|
2012
2049
|
* virtualized list replaces its rows at input rate.
|
|
2013
2050
|
*
|
|
2051
|
+
* `problems` is for a caller React is not under — a query block that
|
|
2052
|
+
* starts matching mid-frame — where a throw would leave the frame and take
|
|
2053
|
+
* the process with it. A value the layout cannot take is then dropped, the
|
|
2054
|
+
* property going back to its default as CSS drops a declaration it cannot
|
|
2055
|
+
* parse, and `{ key, value, error }` is pushed for the caller to report.
|
|
2056
|
+
* Without it the value throws, which is what a commit wants: React routes
|
|
2057
|
+
* it to a boundary.
|
|
2058
|
+
*
|
|
2014
2059
|
* @returns true if any layout-affecting prop changed
|
|
2015
2060
|
*/
|
|
2016
|
-
export function applyLayoutStyle(
|
|
2061
|
+
export function applyLayoutStyle(
|
|
2062
|
+
yogaNode,
|
|
2063
|
+
props,
|
|
2064
|
+
oldProps = NO_STYLE,
|
|
2065
|
+
problems = null,
|
|
2066
|
+
) {
|
|
2017
2067
|
let changed = false;
|
|
2018
2068
|
// A placed node's insets — sticky's thresholds, or whatever a registered
|
|
2019
2069
|
// position reads them as — are the placement's, never offsets: yoga gets
|
|
@@ -2030,7 +2080,12 @@ export function applyLayoutStyle(yogaNode, props, oldProps = NO_STYLE) {
|
|
|
2030
2080
|
? !samePosition(props.position, oldProps.position)
|
|
2031
2081
|
: props[key] !== oldProps[key];
|
|
2032
2082
|
if (!moved && !(flipped && inset)) continue;
|
|
2033
|
-
|
|
2083
|
+
const value = placed && inset ? undefined : props[key];
|
|
2084
|
+
// `null` is unset, the way a conditional writes it — `wide ? 240 :
|
|
2085
|
+
// null` — and as it is for every other property in this vocabulary;
|
|
2086
|
+
// yoga's own setters throw on it
|
|
2087
|
+
if (problems === null) applier(yogaNode, value ?? undefined);
|
|
2088
|
+
else applyOrDrop(applier, yogaNode, key, value ?? undefined, problems);
|
|
2034
2089
|
changed = true;
|
|
2035
2090
|
}
|
|
2036
2091
|
// …and the ones the new style dropped, which go back to yoga's default
|
|
@@ -2051,6 +2106,72 @@ export function applyLayoutStyle(yogaNode, props, oldProps = NO_STYLE) {
|
|
|
2051
2106
|
return changed;
|
|
2052
2107
|
}
|
|
2053
2108
|
|
|
2109
|
+
/**
|
|
2110
|
+
* One layout property set on its own — a width put back after the content
|
|
2111
|
+
* floors pinned it, a minimum restored under a floor — by the rules
|
|
2112
|
+
* `applyLayoutStyle` sets it by: `null` is unset, and a value the layout
|
|
2113
|
+
* refuses is dropped. A refused one was reported where it was first
|
|
2114
|
+
* applied, and throwing here would throw from inside a layout pass.
|
|
2115
|
+
*/
|
|
2116
|
+
export function setLayoutValue(yogaNode, key, value) {
|
|
2117
|
+
const applier = LAYOUT_APPLIER_OF.get(key);
|
|
2118
|
+
try {
|
|
2119
|
+
applier(yogaNode, value ?? undefined);
|
|
2120
|
+
} catch {
|
|
2121
|
+
applier(yogaNode, undefined);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
/** One declaration, dropped rather than thrown when the layout refuses it
|
|
2126
|
+
* (`applyLayoutStyle`'s `problems`). */
|
|
2127
|
+
function applyOrDrop(applier, yogaNode, key, value, problems) {
|
|
2128
|
+
try {
|
|
2129
|
+
applier(yogaNode, value);
|
|
2130
|
+
} catch (error) {
|
|
2131
|
+
applier(yogaNode, undefined);
|
|
2132
|
+
problems.push({ key, value, error });
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
// A yoga node no layout runs on, for development to ask whether a value is
|
|
2137
|
+
// one the layout can take: the question the frame asks, so the two cannot
|
|
2138
|
+
// disagree about what a length is.
|
|
2139
|
+
let scratchNode = null;
|
|
2140
|
+
|
|
2141
|
+
// What the layout has already taken, per property, so development asks
|
|
2142
|
+
// yoga once per spelling rather than once per style it checks: a list of a
|
|
2143
|
+
// thousand rows checks the same six values a thousand times, and a setter
|
|
2144
|
+
// is a call into WebAssembly. A property that takes one number takes them
|
|
2145
|
+
// all — a length, a factor — and one that refuses them is an enum, so
|
|
2146
|
+
// numbers are remembered as one entry.
|
|
2147
|
+
const takenValues = new Map(
|
|
2148
|
+
[...LAYOUT_APPLIER_OF.keys()].map((key) => [key, new Set()]),
|
|
2149
|
+
);
|
|
2150
|
+
const NUMBERS = Symbol('numbers');
|
|
2151
|
+
const TAKEN_MAX = 256;
|
|
2152
|
+
|
|
2153
|
+
/** What applying `value` to `key` throws, or null when the layout takes
|
|
2154
|
+
* it — and for a key that is not a layout property. */
|
|
2155
|
+
function layoutRefusal(key, value) {
|
|
2156
|
+
const taken = takenValues.get(key);
|
|
2157
|
+
if (taken === undefined || value == null) return null;
|
|
2158
|
+
if (taken.has(typeof value === 'number' ? NUMBERS : value)) return null;
|
|
2159
|
+
if (scratchNode === null) {
|
|
2160
|
+
// `createStyles` validates at module scope, before `createRoot` has
|
|
2161
|
+
// loaded the engine — and the commit validates the same style again
|
|
2162
|
+
if (!layoutLoaded()) return null;
|
|
2163
|
+
scratchNode = Yoga.Node.create();
|
|
2164
|
+
}
|
|
2165
|
+
try {
|
|
2166
|
+
LAYOUT_APPLIER_OF.get(key)(scratchNode, value);
|
|
2167
|
+
} catch (error) {
|
|
2168
|
+
return error;
|
|
2169
|
+
}
|
|
2170
|
+
if (typeof value === 'number') taken.add(NUMBERS);
|
|
2171
|
+
else if (taken.size < TAKEN_MAX) taken.add(value);
|
|
2172
|
+
return null;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2054
2175
|
/** Two `position` values asking for the same thing. A registered one written
|
|
2055
2176
|
* with its options is an object, and an inline object is a new one every
|
|
2056
2177
|
* render — which must not cost a layout pass per commit. */
|
|
@@ -2098,16 +2219,81 @@ export function paintPropsChanged(props, oldProps = {}) {
|
|
|
2098
2219
|
);
|
|
2099
2220
|
}
|
|
2100
2221
|
|
|
2222
|
+
// The text values a style can get wrong in a way the engines do not survive:
|
|
2223
|
+
// a `fontStyle` of `7` threw from inside ntk's face matching, a `lineHeight`
|
|
2224
|
+
// of `'24px'` made a paragraph NaN pixels tall — thrown from the layout
|
|
2225
|
+
// pass, where nothing catches it — and a `fontSize` of `'20px'` set no text
|
|
2226
|
+
// at all. A number is a number or a numeric string, the way the coercion
|
|
2227
|
+
// these always went through read one; anything else is not the property,
|
|
2228
|
+
// and text inherits past it, as CSS inherits past a declaration it cannot
|
|
2229
|
+
// parse. Development says so where the style is written (`validateValue`).
|
|
2230
|
+
const NUMERIC_TEXT = /^\s*[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?\s*$/i;
|
|
2231
|
+
const numericOf = (v) =>
|
|
2232
|
+
typeof v === 'number'
|
|
2233
|
+
? v
|
|
2234
|
+
: typeof v === 'string' && NUMERIC_TEXT.test(v)
|
|
2235
|
+
? Number(v)
|
|
2236
|
+
: NaN;
|
|
2237
|
+
const FONT_WEIGHT_WORDS = new Set(['normal', 'bold', 'bolder', 'lighter']);
|
|
2238
|
+
const FONT_STYLE = /^(?:normal|italic|oblique)\b/;
|
|
2239
|
+
|
|
2240
|
+
const isFontSize = (v) => {
|
|
2241
|
+
const n = numericOf(v);
|
|
2242
|
+
return n >= 0 && n < Infinity;
|
|
2243
|
+
};
|
|
2244
|
+
const isFontWeight = (v) => {
|
|
2245
|
+
if (FONT_WEIGHT_WORDS.has(v)) return true;
|
|
2246
|
+
const n = numericOf(v);
|
|
2247
|
+
return n >= 1 && n <= 1000;
|
|
2248
|
+
};
|
|
2249
|
+
const isFontStyle = (v) => typeof v === 'string' && FONT_STYLE.test(v);
|
|
2250
|
+
const isFontFamily = (v) => typeof v === 'string' && v.trim() !== '';
|
|
2251
|
+
|
|
2252
|
+
/** A `lineHeight` a layout can multiply by, or undefined for the font's own. */
|
|
2253
|
+
export function lineHeightOf(value) {
|
|
2254
|
+
const n = numericOf(value);
|
|
2255
|
+
return n >= 0 && n < Infinity ? n : undefined;
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
/** What each of them is, for development's message. */
|
|
2259
|
+
const TEXT_VALUE_CHECKS = new Map([
|
|
2260
|
+
['fontSize', [isFontSize, 'a number of pixels, like fontSize: 14']],
|
|
2261
|
+
[
|
|
2262
|
+
'fontWeight',
|
|
2263
|
+
[
|
|
2264
|
+
isFontWeight,
|
|
2265
|
+
"a weight from 1 to 1000, like 600, or 'normal', 'bold', 'bolder' or " +
|
|
2266
|
+
"'lighter'",
|
|
2267
|
+
],
|
|
2268
|
+
],
|
|
2269
|
+
['fontStyle', [isFontStyle, "'normal', 'italic' or 'oblique'"]],
|
|
2270
|
+
[
|
|
2271
|
+
'fontFamily',
|
|
2272
|
+
[isFontFamily, "a family's name, like 'Inter' or 'monospace'"],
|
|
2273
|
+
],
|
|
2274
|
+
[
|
|
2275
|
+
'lineHeight',
|
|
2276
|
+
[
|
|
2277
|
+
(v) => lineHeightOf(v) !== undefined,
|
|
2278
|
+
"a multiple of the font's own line height, like lineHeight: 1.5",
|
|
2279
|
+
],
|
|
2280
|
+
],
|
|
2281
|
+
]);
|
|
2282
|
+
|
|
2101
2283
|
/** Resolved text style (TextLayout base style) from props + inherited. */
|
|
2102
2284
|
export function textStyleFrom(props, inherited) {
|
|
2103
2285
|
const variantNumeric = props.fontVariantNumeric ?? inherited.variantNumeric;
|
|
2104
2286
|
const featureSettings =
|
|
2105
2287
|
props.fontFeatureSettings ?? inherited.featureSettings;
|
|
2106
2288
|
return {
|
|
2107
|
-
family: props.fontFamily
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2289
|
+
family: isFontFamily(props.fontFamily)
|
|
2290
|
+
? props.fontFamily
|
|
2291
|
+
: inherited.family,
|
|
2292
|
+
size: isFontSize(props.fontSize) ? props.fontSize : inherited.size,
|
|
2293
|
+
weight: isFontWeight(props.fontWeight)
|
|
2294
|
+
? props.fontWeight
|
|
2295
|
+
: inherited.weight,
|
|
2296
|
+
style: isFontStyle(props.fontStyle) ? props.fontStyle : inherited.style,
|
|
2111
2297
|
// ntk's name for it is `variations`; the prop is spelled after the CSS
|
|
2112
2298
|
// property, like every other name in this vocabulary
|
|
2113
2299
|
variations: props.fontVariationSettings ?? inherited.variations,
|
package/src/types/nodes.d.ts
CHANGED
|
@@ -148,7 +148,8 @@ export interface ScrollableNode extends DrawnNode {
|
|
|
148
148
|
canScroll(dx: number, dy: number): boolean;
|
|
149
149
|
/**
|
|
150
150
|
* Scroll the minimum amount on both axes that makes a descendant fully
|
|
151
|
-
* visible
|
|
151
|
+
* visible; one larger than the viewport is left where it covers it and
|
|
152
|
+
* otherwise brought in by its nearer edge (CSSOM View's `nearest`). Safe to call right after that node mounts — the request is
|
|
152
153
|
* resolved on the next layout pass, when it has geometry, and `onScroll`
|
|
153
154
|
* reports the move once that pass is over.
|
|
154
155
|
*/
|