react-x11 2.22.13 → 2.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/cocoa/panewindow.js +11 -11
- package/src/decorations.js +28 -2
- 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/boxpaint.js +8 -4
- package/src/nodes/cascade.js +4 -1
- package/src/nodes/layouthost.js +3 -2
- package/src/nodes/paint.js +1 -1
- package/src/nodes/queries.js +20 -1
- package/src/nodes/scrollable.js +19 -8
- package/src/nodes/text.js +89 -10
- package/src/nodes/window/capabilities.js +11 -2
- package/src/nodes/window/floors.js +7 -9
- package/src/nodes/window/spine.js +82 -32
- package/src/palette.js +19 -0
- package/src/styles.js +264 -18
- package/src/types/nodes.d.ts +2 -1
- package/src/wayland/context2d.js +10 -53
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.23.1",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"dependencies": {
|
|
104
104
|
"ical.js": "^2.2.1",
|
|
105
105
|
"linebreak": "^1.1.0",
|
|
106
|
-
"ntk": "^8.
|
|
106
|
+
"ntk": "^8.14.3",
|
|
107
107
|
"react-reconciler": "^0.34.0",
|
|
108
108
|
"yoga-layout": "^3.2.1"
|
|
109
109
|
},
|
package/src/cocoa/panewindow.js
CHANGED
|
@@ -252,7 +252,6 @@ export class CocoaPaneWindow {
|
|
|
252
252
|
this._dirty = false;
|
|
253
253
|
const shown = this._ring[this._drawIndex];
|
|
254
254
|
this._native.surfaceUnlock(shown.handle);
|
|
255
|
-
const damage = this._flushDamage;
|
|
256
255
|
this._flushDamage = null;
|
|
257
256
|
this.app._paneSend?.({
|
|
258
257
|
type: 'pane-present',
|
|
@@ -270,16 +269,17 @@ export class CocoaPaneWindow {
|
|
|
270
269
|
this._surface = next.handle;
|
|
271
270
|
this._surfaceGen++;
|
|
272
271
|
this._native.surfaceLock(next.handle);
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
// what
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
272
|
+
// The next frame may paint only its damage, so `next` must first hold
|
|
273
|
+
// the last complete frame underneath — and the WHOLE of it, not just
|
|
274
|
+
// this frame's rects, because in a three-buffer ring `next` was last
|
|
275
|
+
// drawn two presents ago and is stale everywhere. A full copy of the
|
|
276
|
+
// just-shown buffer is the complete background; the safe target is what
|
|
277
|
+
// triple buffering buys. After a full frame as much as a partial one:
|
|
278
|
+
// what a frame was says nothing of what the next will be, and skipping
|
|
279
|
+
// the copy there had a partial frame after a full one — a status line,
|
|
280
|
+
// a hover's underline — drawn over the page as it was two frames back.
|
|
281
|
+
// The copy is wasted only where the next frame is full as well.
|
|
282
|
+
this._native.copySurfaceRegion(shown.handle, next.handle, null);
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
snapshot() {
|
package/src/decorations.js
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
// derives is the padding a coverage surface has to carry. Both are pure
|
|
35
35
|
// functions — no connection, nothing drawn — so this module stays testable
|
|
36
36
|
// without a server.
|
|
37
|
+
import { cssColorStraight } from 'ntk/color';
|
|
37
38
|
import { shadowReach, shadowSigma } from 'ntk/shadow-math';
|
|
38
39
|
|
|
39
40
|
/** A number, with or without the `px` CSS wants and this vocabulary does not. */
|
|
@@ -199,11 +200,33 @@ function readStop(part, value) {
|
|
|
199
200
|
if (pieces.length > 1 && (PERCENT.test(last) || LENGTH.test(last))) {
|
|
200
201
|
pieces.pop();
|
|
201
202
|
return {
|
|
202
|
-
color: pieces.join(' '),
|
|
203
|
+
color: stopColor(pieces.join(' '), value),
|
|
203
204
|
position: { value: number(last), unit: PERCENT.test(last) ? '%' : 'px' },
|
|
204
205
|
};
|
|
205
206
|
}
|
|
206
|
-
return { color: part, position: null };
|
|
207
|
+
return { color: stopColor(part, value), position: null };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** A stop's colour, which RENDER is handed as written — and ntk throws on
|
|
211
|
+
* one it cannot parse from inside the frame, where nothing can catch it. */
|
|
212
|
+
function stopColor(color, value) {
|
|
213
|
+
if (isColorOrToken(color)) return color;
|
|
214
|
+
throw new Error(
|
|
215
|
+
`react-x11: backgroundImage ${JSON.stringify(value)} has a colour stop ` +
|
|
216
|
+
`that is not a colour ("${color}"; expected one like '#2980b9', ` +
|
|
217
|
+
"'tomato' or 'rgba(0, 0, 0, .4)', or a $token, before its position)",
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Whether a piece of a gradient or a shadow can stand where its colour does.
|
|
223
|
+
* One that mentions a `$token` is let through where the style is written,
|
|
224
|
+
* since the token means nothing yet; the paint parses the value it resolves
|
|
225
|
+
* to, and a colour that is still not one drops the whole decoration there
|
|
226
|
+
* rather than throwing (see "memoized parsing" below).
|
|
227
|
+
*/
|
|
228
|
+
function isColorOrToken(color) {
|
|
229
|
+
return color.includes('$') || cssColorStraight(color) !== null;
|
|
207
230
|
}
|
|
208
231
|
|
|
209
232
|
/**
|
|
@@ -366,6 +389,9 @@ function readShadow(part, value) {
|
|
|
366
389
|
color = piece;
|
|
367
390
|
}
|
|
368
391
|
if (lengths.length < 2) throw bad('needs an x and a y offset');
|
|
392
|
+
if (color !== null && !isColorOrToken(color)) {
|
|
393
|
+
throw bad(`has "${color}", which is neither a length nor a colour`);
|
|
394
|
+
}
|
|
369
395
|
const [dx, dy, blur = 0, spread = 0] = lengths;
|
|
370
396
|
if (blur < 0) throw bad('has a negative blur radius');
|
|
371
397
|
return { dx, dy, blur, spread, color };
|
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/boxpaint.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_FOCUS_RING,
|
|
7
|
+
isColor,
|
|
7
8
|
resolveBorderWidths,
|
|
8
9
|
resolveBorderColors,
|
|
9
10
|
} from '../styles.js';
|
|
@@ -22,11 +23,14 @@ import { ntkRoot, x11Ntk } from '../ntkroot.js';
|
|
|
22
23
|
import { isFocusable as a11yFocusable } from '../a11y.js';
|
|
23
24
|
import { DEV } from './util.js';
|
|
24
25
|
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
/** Whether a style's colour paints anything. CSS's `transparent` keyword
|
|
27
|
+
* means "paint nothing" and is filtered out at the source alongside
|
|
28
|
+
* null/'', and so is a value that is not a colour at all (`isColor`): ntk
|
|
29
|
+
* throws on one deep inside the 2d context, taking the whole frame with
|
|
30
|
+
* it. Everything that plans a paint asks this too — the blits deciding
|
|
31
|
+
* what a box covers — so a colour that is dropped is dropped everywhere. */
|
|
28
32
|
export function isPaintedColor(color) {
|
|
29
|
-
return Boolean(color) && color !== 'transparent';
|
|
33
|
+
return Boolean(color) && color !== 'transparent' && isColor(color);
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
/**
|
package/src/nodes/cascade.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
applyLayoutStyle,
|
|
7
|
+
isColor,
|
|
7
8
|
textStyleFrom,
|
|
8
9
|
DEFAULT_TEXT_STYLE,
|
|
9
10
|
localTextStyleChanged,
|
|
@@ -332,7 +333,9 @@ export class NodeCascade {
|
|
|
332
333
|
return this._textScaled.style;
|
|
333
334
|
}
|
|
334
335
|
const theme = this.theme;
|
|
335
|
-
|
|
336
|
+
// a palette is app-written, and an ink no engine parses would throw from
|
|
337
|
+
// every paragraph's draw (`isColor`)
|
|
338
|
+
const color = isColor(theme.text) ? theme.text : DEFAULT_TEXT_STYLE.color;
|
|
336
339
|
// A palette can reach a node as a bare `theme` **prop** rather than a
|
|
337
340
|
// resolved one — `<box theme={{ text: 'red' }}>` merges and derives
|
|
338
341
|
// nothing (styling.md) — so neither of these is guaranteed to be there.
|
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/paint.js
CHANGED
|
@@ -681,7 +681,7 @@ export class WindowPaint {
|
|
|
681
681
|
// repainting the background only where it is about to be drawn over is
|
|
682
682
|
// the other half of the win: a full-window fill is a full-window
|
|
683
683
|
// composite however little changed
|
|
684
|
-
fill(
|
|
684
|
+
fill(this._windowBackground(), false);
|
|
685
685
|
if (gradient) fill(gradient, false);
|
|
686
686
|
}
|
|
687
687
|
}
|
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';
|
|
@@ -33,6 +37,9 @@ const STRIP_COVERAGE = 0.45;
|
|
|
33
37
|
|
|
34
38
|
const textStripBelow = new WeakMap();
|
|
35
39
|
|
|
40
|
+
/** `TextNode._trim`'s answer, per layout. */
|
|
41
|
+
const trims = new WeakMap();
|
|
42
|
+
|
|
36
43
|
/** The size under which this connection's text is painted as strips. */
|
|
37
44
|
export function setTextStripBelow(app, below) {
|
|
38
45
|
if (below === undefined) return;
|
|
@@ -406,20 +413,35 @@ export class TextNode extends Node {
|
|
|
406
413
|
if (!fonts) return null; // mock container in tests: no text metrics
|
|
407
414
|
const maxLines = this._maxLines();
|
|
408
415
|
const overflow = this.style.textOverflow;
|
|
409
|
-
// Both truncation options are inputs to the shaping, so
|
|
410
|
-
// the
|
|
411
|
-
// map on its way past — but a cache keyed on less
|
|
412
|
-
// one refactor away from answering with the wrong
|
|
413
|
-
// wrong paragraph here is glyphs on screen that no
|
|
414
|
-
|
|
415
|
-
|
|
416
|
+
// Both truncation options are inputs to the shaping, so a hit has to
|
|
417
|
+
// match them as well as the width. They can only change with the style,
|
|
418
|
+
// which clears the whole map on its way past — but a cache keyed on less
|
|
419
|
+
// than it depends on is one refactor away from answering with the wrong
|
|
420
|
+
// paragraph, and the wrong paragraph here is glyphs on screen that no
|
|
421
|
+
// error mentions.
|
|
422
|
+
//
|
|
423
|
+
// The width is the key as the number it is, and the other two are
|
|
424
|
+
// checked on the entry, rather than all three spelled into one string: a
|
|
425
|
+
// layout pass measures every label several times over, and turning a
|
|
426
|
+
// fractional width into text cost more than the rest of a hit together.
|
|
427
|
+
const entry = this._layouts.get(maxWidth);
|
|
428
|
+
let layout =
|
|
429
|
+
entry !== undefined &&
|
|
430
|
+
entry.maxLines === maxLines &&
|
|
431
|
+
entry.overflow === overflow
|
|
432
|
+
? entry.layout
|
|
433
|
+
: null;
|
|
434
|
+
if (!layout) {
|
|
435
|
+
layout = this._shapedWider(maxWidth, maxLines, overflow);
|
|
436
|
+
if (layout) this._layouts.set(maxWidth, { layout, maxLines, overflow });
|
|
437
|
+
}
|
|
416
438
|
if (!layout) {
|
|
417
439
|
const spans = this.collectSpans([]);
|
|
418
440
|
const base = this.resolvedTextStyle();
|
|
419
441
|
layout = fonts.layout(spans, base, {
|
|
420
442
|
maxWidth: Number.isFinite(maxWidth) ? maxWidth : undefined,
|
|
421
443
|
align: this.style.textAlign,
|
|
422
|
-
lineHeight: this.style.lineHeight,
|
|
444
|
+
lineHeight: lineHeightOf(this.style.lineHeight),
|
|
423
445
|
maxLines: Number.isFinite(maxLines) ? maxLines : undefined,
|
|
424
446
|
// 'clip' is ntk's default, so an unset property and the CSS default
|
|
425
447
|
// are the same request rather than two paths through the layout.
|
|
@@ -436,11 +458,52 @@ export class TextNode extends Node {
|
|
|
436
458
|
direction: this.direction,
|
|
437
459
|
});
|
|
438
460
|
if (this._layouts.size > 32) this._layouts.clear();
|
|
439
|
-
this._layouts.set(
|
|
461
|
+
this._layouts.set(maxWidth, { layout, maxLines, overflow });
|
|
440
462
|
}
|
|
441
463
|
return layout;
|
|
442
464
|
}
|
|
443
465
|
|
|
466
|
+
/**
|
|
467
|
+
* A layout this paragraph already has at a wider width that shaping it at
|
|
468
|
+
* `maxWidth` would come out the same as, or null.
|
|
469
|
+
*
|
|
470
|
+
* ntk fills lines greedily: a line takes tokens while they fit. So if
|
|
471
|
+
* every line a wider layout made fits `maxWidth` as well, the narrower
|
|
472
|
+
* layout breaks at the same tokens — each line was the longest run that
|
|
473
|
+
* fit the wider width, and so is the longest that fits a narrower one it
|
|
474
|
+
* still fits in — and a word too wide for either is cut at the same
|
|
475
|
+
* cluster. Past the breaks, only two things read the width, and both are
|
|
476
|
+
* ruled out here: placing a line anywhere but at the left edge (centred,
|
|
477
|
+
* right-aligned, or the start of a right-to-left paragraph), and eliding
|
|
478
|
+
* one. A line within a thousandth of a pixel of the width is left to be
|
|
479
|
+
* shaped, since the sum the fill tests and the width the line reports are
|
|
480
|
+
* added up in different orders.
|
|
481
|
+
*
|
|
482
|
+
* The paint's request is the common one: a label is laid out in a box the
|
|
483
|
+
* width it measured, a pixel or less narrower than the width it was
|
|
484
|
+
* measured at, and was shaped again for it — 37% of the layouts a fling
|
|
485
|
+
* down a <Tree> built.
|
|
486
|
+
*
|
|
487
|
+
* ntk's layouts only. CoreText and DirectWrite break lines their own way,
|
|
488
|
+
* and their layouts carry no `baseLevel`.
|
|
489
|
+
*/
|
|
490
|
+
_shapedWider(maxWidth, maxLines, overflow) {
|
|
491
|
+
if (overflow === 'ellipsis') return null;
|
|
492
|
+
const align = this.style.textAlign;
|
|
493
|
+
if (align !== undefined && align !== 'start' && align !== 'left') {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
const fits = maxWidth - 0.001;
|
|
497
|
+
for (const [width, entry] of this._layouts) {
|
|
498
|
+
if (!(width > maxWidth)) continue;
|
|
499
|
+
if (entry.maxLines !== maxLines || entry.overflow !== overflow) continue;
|
|
500
|
+
const layout = entry.layout;
|
|
501
|
+
if (layout?.baseLevel !== 0) continue;
|
|
502
|
+
if (layout.lines.every((line) => line.width <= fits)) return layout;
|
|
503
|
+
}
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
|
|
444
507
|
/**
|
|
445
508
|
* `textBoxTrim: 'cap-alphabetic'` — CSS's `text-box-trim: trim-both` with
|
|
446
509
|
* `text-box-edge: cap alphabetic`. How much of the line box to take off
|
|
@@ -478,6 +541,22 @@ export class TextNode extends Node {
|
|
|
478
541
|
if (this.style.textBoxTrim !== 'cap-alphabetic') return null;
|
|
479
542
|
const lines = layout?.lines;
|
|
480
543
|
if (!lines?.length) return null;
|
|
544
|
+
// Once per layout rather than once per measure, which a layout pass asks
|
|
545
|
+
// for several times over. A layout never changes once it is shaped, and
|
|
546
|
+
// the face and size read below are the ones it was shaped in: a style
|
|
547
|
+
// that moves either drops every layout on its way past
|
|
548
|
+
// (`_textContentChanged`), so the answer cannot go stale while its layout
|
|
549
|
+
// is still being handed out.
|
|
550
|
+
let trim = trims.get(layout);
|
|
551
|
+
if (trim === undefined) {
|
|
552
|
+
trim = this._trimOf(layout, lines);
|
|
553
|
+
trims.set(layout, trim);
|
|
554
|
+
}
|
|
555
|
+
return trim;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** `_trim`'s answer, worked out. */
|
|
559
|
+
_trimOf(layout, lines) {
|
|
481
560
|
const base = this.resolvedTextStyle();
|
|
482
561
|
const font = this.app?.fonts?.match?.(base.family, {
|
|
483
562
|
weight: base.weight,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
transparencyDisabled,
|
|
9
9
|
watchCompositing,
|
|
10
10
|
} from '../../compositing.js';
|
|
11
|
+
import { isColor } from '../../styles.js';
|
|
11
12
|
import { DEV } from '../util.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -129,9 +130,17 @@ export class WindowCapabilities {
|
|
|
129
130
|
* window that cannot be transparent is a square opaque one, not a broken
|
|
130
131
|
* one — and the alternative, black corners, is worse than square.
|
|
131
132
|
*/
|
|
132
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* What this window paints as its background — its own, or the palette's.
|
|
135
|
+
* A colour that does not parse is not one named (`isColor`), and a window
|
|
136
|
+
* has to answer with one: an opaque window cannot paint nothing, since
|
|
137
|
+
* whatever it skipped would show the last frame's pixels.
|
|
138
|
+
*/
|
|
133
139
|
_windowBackground() {
|
|
134
|
-
|
|
140
|
+
const own = this.style.backgroundColor;
|
|
141
|
+
if (isColor(own)) return own;
|
|
142
|
+
const palette = this.theme.background;
|
|
143
|
+
return isColor(palette) ? palette : 'white';
|
|
135
144
|
}
|
|
136
145
|
|
|
137
146
|
/**
|
|
@@ -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
|
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
// are left unmeasured, which is what the floors do with every extent
|
|
28
28
|
// nobody reads (`collectFloorStale`).
|
|
29
29
|
//
|
|
30
|
-
// Anything else — a row on the way, a margin
|
|
31
|
-
//
|
|
30
|
+
// Anything else — a row on the way, a margin or a width that is not a
|
|
31
|
+
// length — is not a spine, and the tree is measured whole.
|
|
32
32
|
|
|
33
33
|
import { declaresOwnMinimum, inFlow, mainAxisOf } from './floors.js';
|
|
34
34
|
import { Yoga } from '../../yoga.js';
|
|
@@ -118,6 +118,7 @@ export function spineScope(root, sources, listOnly) {
|
|
|
118
118
|
const roots = new Map();
|
|
119
119
|
const spine = new Set();
|
|
120
120
|
const stops = new Set();
|
|
121
|
+
const offered = new OfferedWidths(root);
|
|
121
122
|
for (const source of sources) {
|
|
122
123
|
const list = listOnly(source);
|
|
123
124
|
// the way up to where the change stops mattering
|
|
@@ -148,7 +149,7 @@ export function spineScope(root, sources, listOnly) {
|
|
|
148
149
|
if (!child.yoga || child.isWindow || child.style.display === 'none') {
|
|
149
150
|
continue;
|
|
150
151
|
}
|
|
151
|
-
const widths = rootWidths(child,
|
|
152
|
+
const widths = rootWidths(child, offered);
|
|
152
153
|
if (widths === null) return null;
|
|
153
154
|
roots.set(child, widths);
|
|
154
155
|
}
|
|
@@ -225,30 +226,90 @@ function staleChildren(node) {
|
|
|
225
226
|
|
|
226
227
|
/**
|
|
227
228
|
* The widths a box is measured alone at, or null when it cannot be: in flow
|
|
228
|
-
* and stretched across its column with no
|
|
229
|
-
*
|
|
230
|
-
* `width` is that box now — the width its heights are for —
|
|
231
|
-
* `widthPass` the one the width pass over the whole tree would give it
|
|
229
|
+
* and stretched across its column with no width or ceiling of its own
|
|
230
|
+
* across it, so that it is as wide as the column's content box less its
|
|
231
|
+
* margins across. `width` is that box now — the width its heights are for —
|
|
232
|
+
* and `widthPass` the one the width pass over the whole tree would give it
|
|
232
233
|
* (`widthPassWidth`), null where only a pass knows.
|
|
234
|
+
*
|
|
235
|
+
* Both are the width the box is laid out *in*, not its own: yoga lays a
|
|
236
|
+
* root with no width of its own out at the width on offer less its margins,
|
|
237
|
+
* which is what stretching it across the column does. So a margin across is
|
|
238
|
+
* a number the layout takes off either way — a list whose rows are inset
|
|
239
|
+
* from its edges, as a menu's are, is still a spine — and only a margin
|
|
240
|
+
* that is not a length, a percentage or `auto`, ends one: `auto` centres the
|
|
241
|
+
* box instead of stretching it.
|
|
233
242
|
*/
|
|
234
|
-
function rootWidths(node,
|
|
243
|
+
function rootWidths(node, offered) {
|
|
235
244
|
const style = node.style;
|
|
236
245
|
const parent = node.parent;
|
|
237
246
|
if (!parent || !inFlow(node) || node._host !== null) return null;
|
|
238
247
|
if (style.width !== undefined || style.maxWidth !== undefined) return null;
|
|
239
248
|
if (typeof style.minWidth === 'string') return null;
|
|
240
249
|
if (!stretched(node)) return null;
|
|
241
|
-
if (
|
|
242
|
-
LENGTHS_ACROSS.some((key) => style[key] !== undefined && style[key] !== 0)
|
|
243
|
-
) {
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
246
250
|
if (MARGINS.some((key) => typeof style[key] === 'string')) return null;
|
|
247
251
|
if (PADDINGS.some((key) => typeof style[key] === 'string')) return null;
|
|
248
|
-
return
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
+
return offered.in(parent);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The widths a column offers the roots in it (`rootWidths`), worked out once
|
|
257
|
+
* a column rather than once a root.
|
|
258
|
+
*
|
|
259
|
+
* Every root in a column is offered the same two, and working out the width
|
|
260
|
+
* pass's walks the way up to the window, with four yoga reads a box for its
|
|
261
|
+
* padding and border across (`insetAcross`). Asked afresh for each root, a
|
|
262
|
+
* fling down a list of rows — a dozen arriving a frame, every one of them a
|
|
263
|
+
* root in the same column — spent 1.8 ms of a 5.9 ms frame on it. Kept for
|
|
264
|
+
* one `spineScope` call, which lays nothing out, so nothing it has read can
|
|
265
|
+
* move under it.
|
|
266
|
+
*/
|
|
267
|
+
class OfferedWidths {
|
|
268
|
+
constructor(root) {
|
|
269
|
+
this.root = root;
|
|
270
|
+
this.offers = new Map();
|
|
271
|
+
this.insets = new Map();
|
|
272
|
+
this.passWidths = new Map();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** `{ width, widthPass }` for a root in `column`: its content box now,
|
|
276
|
+
* and in the width pass over the whole tree. */
|
|
277
|
+
in(column) {
|
|
278
|
+
let offer = this.offers.get(column);
|
|
279
|
+
if (offer === undefined) {
|
|
280
|
+
offer = {
|
|
281
|
+
width: Math.max(0, column.yoga.getComputedWidth() - this.inset(column)),
|
|
282
|
+
widthPass: this.passContent(column),
|
|
283
|
+
};
|
|
284
|
+
this.offers.set(column, offer);
|
|
285
|
+
}
|
|
286
|
+
return offer;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
inset(node) {
|
|
290
|
+
let inset = this.insets.get(node);
|
|
291
|
+
if (inset === undefined) {
|
|
292
|
+
inset = insetAcross(node);
|
|
293
|
+
this.insets.set(node, inset);
|
|
294
|
+
}
|
|
295
|
+
return inset;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** `widthPassWidth`, once a box. */
|
|
299
|
+
passWidth(node) {
|
|
300
|
+
let width = this.passWidths.get(node);
|
|
301
|
+
if (width === undefined) {
|
|
302
|
+
width = widthPassWidth(node, this);
|
|
303
|
+
this.passWidths.set(node, width);
|
|
304
|
+
}
|
|
305
|
+
return width;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** …and the content box inside it. */
|
|
309
|
+
passContent(node) {
|
|
310
|
+
const width = this.passWidth(node);
|
|
311
|
+
return width === null ? null : Math.max(0, width - this.inset(node));
|
|
312
|
+
}
|
|
252
313
|
}
|
|
253
314
|
|
|
254
315
|
/** Stretched across its parent's column — `alignSelf`, or the column's
|
|
@@ -274,11 +335,6 @@ function insetAcross(node) {
|
|
|
274
335
|
);
|
|
275
336
|
}
|
|
276
337
|
|
|
277
|
-
/** A box's content width in the last pass. */
|
|
278
|
-
function contentWidth(node) {
|
|
279
|
-
return Math.max(0, node.yoga.getComputedWidth() - insetAcross(node));
|
|
280
|
-
}
|
|
281
|
-
|
|
282
338
|
/**
|
|
283
339
|
* How wide `node` is in the width pass over the whole tree
|
|
284
340
|
* (`_measureContentSpans('width')`), worked out on the way down rather than
|
|
@@ -288,8 +344,8 @@ function contentWidth(node) {
|
|
|
288
344
|
* the way down is anything else: a row, whose children are as wide as the
|
|
289
345
|
* pass decides.
|
|
290
346
|
*/
|
|
291
|
-
function widthPassWidth(node,
|
|
292
|
-
if (node === root) return
|
|
347
|
+
function widthPassWidth(node, offered) {
|
|
348
|
+
if (node === offered.root) return offered.inset(node);
|
|
293
349
|
const style = node.style;
|
|
294
350
|
let width;
|
|
295
351
|
if (typeof style.width === 'number') width = style.width;
|
|
@@ -297,7 +353,7 @@ function widthPassWidth(node, root) {
|
|
|
297
353
|
else {
|
|
298
354
|
if (!inFlow(node) || !stretched(node)) return null;
|
|
299
355
|
if (MARGINS.some((key) => typeof style[key] === 'string')) return null;
|
|
300
|
-
const inner =
|
|
356
|
+
const inner = offered.passContent(node.parent);
|
|
301
357
|
if (inner === null) return null;
|
|
302
358
|
const yoga = node.yoga;
|
|
303
359
|
width =
|
|
@@ -311,11 +367,5 @@ function widthPassWidth(node, root) {
|
|
|
311
367
|
if (typeof style.minWidth === 'number')
|
|
312
368
|
width = Math.max(width, style.minWidth);
|
|
313
369
|
else if (style.minWidth !== undefined) return null;
|
|
314
|
-
return Math.max(width,
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/** …and the content box inside it. */
|
|
318
|
-
function widthPassContent(node, root) {
|
|
319
|
-
const width = widthPassWidth(node, root);
|
|
320
|
-
return width === null ? null : Math.max(0, width - insetAcross(node));
|
|
370
|
+
return Math.max(width, offered.inset(node));
|
|
321
371
|
}
|
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,
|
|
@@ -841,6 +841,20 @@ function queryMatches(q, size, supports, containers) {
|
|
|
841
841
|
|
|
842
842
|
const GRID_VALUES = new Set([...GRID_CONTAINER_PROPS, ...GRID_ITEM_PROPS]);
|
|
843
843
|
|
|
844
|
+
// The properties whose value is one colour.
|
|
845
|
+
const COLOR_PROPS = new Set([
|
|
846
|
+
'backgroundColor',
|
|
847
|
+
'borderColor',
|
|
848
|
+
'borderTopColor',
|
|
849
|
+
'borderRightColor',
|
|
850
|
+
'borderBottomColor',
|
|
851
|
+
'borderLeftColor',
|
|
852
|
+
'borderStartColor',
|
|
853
|
+
'borderEndColor',
|
|
854
|
+
'outlineColor',
|
|
855
|
+
'color',
|
|
856
|
+
]);
|
|
857
|
+
|
|
844
858
|
/**
|
|
845
859
|
* The style values that are a small language rather than a number — a
|
|
846
860
|
* gradient, a shadow, and grid's track lists, areas and lines — and
|
|
@@ -851,10 +865,39 @@ const GRID_VALUES = new Set([...GRID_CONTAINER_PROPS, ...GRID_ITEM_PROPS]);
|
|
|
851
865
|
* instead of as a blank panel three commits later. A grid value goes through
|
|
852
866
|
* the parser the layout itself runs (src/grid.js), so the two agree.
|
|
853
867
|
*
|
|
868
|
+
* A colour is the smallest of these languages and is checked the same way,
|
|
869
|
+
* on its own and inside a gradient or a shadow: `'nonsense'` or `7` used to
|
|
870
|
+
* pass here and throw from the frame, where no boundary could catch it.
|
|
854
871
|
* Tokens are still unresolved here (`$accent` is a colour as far as the
|
|
855
|
-
* grammar is concerned), so
|
|
872
|
+
* grammar is concerned), so a colour is checked only where it is written
|
|
873
|
+
* out; one a token resolves to is dropped at the paint if it does not parse
|
|
874
|
+
* (`isColor`), since a theme is not a call site.
|
|
856
875
|
*/
|
|
857
876
|
function validateValue(key, value, where) {
|
|
877
|
+
const text = TEXT_VALUE_CHECKS.get(key);
|
|
878
|
+
if (text !== undefined) {
|
|
879
|
+
// unset, the way a conditional writes it
|
|
880
|
+
if (value == null || value === '' || isToken(value)) return;
|
|
881
|
+
const [valid, expected] = text;
|
|
882
|
+
if (!valid(value)) {
|
|
883
|
+
throw new Error(
|
|
884
|
+
`react-x11: invalid ${key} ${JSON.stringify(value)} in ${where} ` +
|
|
885
|
+
`(expected ${expected})`,
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (COLOR_PROPS.has(key)) {
|
|
891
|
+
// unset, the way a conditional writes it — `on ? accent : undefined`,
|
|
892
|
+
// or `''`, which has always painted nothing
|
|
893
|
+
if (value == null || value === '') return;
|
|
894
|
+
if (isColor(value) || isToken(value) || mentionsToken(value)) return;
|
|
895
|
+
throw new Error(
|
|
896
|
+
`react-x11: invalid ${key} ${JSON.stringify(value)} in ${where} ` +
|
|
897
|
+
"(expected a CSS colour — '#2980b9', 'tomato', 'rgba(0, 0, 0, 0.5)' " +
|
|
898
|
+
"or 'transparent' — or a $token)",
|
|
899
|
+
);
|
|
900
|
+
}
|
|
858
901
|
if (key === 'letterSpacing') {
|
|
859
902
|
if (
|
|
860
903
|
value != null &&
|
|
@@ -913,6 +956,26 @@ function validateValue(key, value, where) {
|
|
|
913
956
|
}
|
|
914
957
|
return;
|
|
915
958
|
}
|
|
959
|
+
// A layout value the layout cannot take — `width: 'hidden'`, `padding:
|
|
960
|
+
// 'auto'`, `flexDirection: 'sideways'` — throws where it is applied, and
|
|
961
|
+
// inside a query block that is a frame, not a commit: a block that starts
|
|
962
|
+
// matching on a resize took the process down. So it is asked here, of the
|
|
963
|
+
// same appliers, wherever it is written. A `$token` is not a length until
|
|
964
|
+
// it resolves, and is answered by the frame then (`_sizeQueriesChanged`).
|
|
965
|
+
const refused =
|
|
966
|
+
isToken(value) || mentionsToken(value) ? null : layoutRefusal(key, value);
|
|
967
|
+
if (refused) {
|
|
968
|
+
// an enum's own message lists the choices; yoga's names only itself
|
|
969
|
+
if (refused.message.startsWith('react-x11:')) {
|
|
970
|
+
refused.message += `\n in ${where}`;
|
|
971
|
+
throw refused;
|
|
972
|
+
}
|
|
973
|
+
const auto = layoutRefusal(key, 'auto') === null ? ", or 'auto'" : '';
|
|
974
|
+
throw new Error(
|
|
975
|
+
`react-x11: invalid ${key} ${JSON.stringify(value)} in ${where} ` +
|
|
976
|
+
`(expected a number of pixels or a percentage like '50%'${auto})`,
|
|
977
|
+
);
|
|
978
|
+
}
|
|
916
979
|
const grid = GRID_VALUES.has(key);
|
|
917
980
|
if (!grid && key !== 'backgroundImage' && key !== 'boxShadow') return;
|
|
918
981
|
try {
|
|
@@ -1536,6 +1599,34 @@ export function tint(color, alpha) {
|
|
|
1536
1599
|
return rgba([c[0], c[1], c[2], c[3] * alpha]);
|
|
1537
1600
|
}
|
|
1538
1601
|
|
|
1602
|
+
// The answer per spelling, kept apart from the parse's own cache because
|
|
1603
|
+
// this is asked once a fill on every paint and wants no copy of the
|
|
1604
|
+
// components. Bounded and dropped whole, like that one: a palette is a few
|
|
1605
|
+
// dozen strings.
|
|
1606
|
+
const colorAnswers = new Map();
|
|
1607
|
+
const COLOR_ANSWERS_MAX = 512;
|
|
1608
|
+
|
|
1609
|
+
/**
|
|
1610
|
+
* Whether `value` is a colour the backends can paint: a string ntk's parser
|
|
1611
|
+
* reads, which is the grammar every context here parses with.
|
|
1612
|
+
*
|
|
1613
|
+
* What is not one is dropped where a style is painted, as CSS drops a
|
|
1614
|
+
* declaration it cannot parse — no fill, no stroke, the inherited ink for
|
|
1615
|
+
* text. The alternatives were worse and differed per backend: ntk throws
|
|
1616
|
+
* from the frame, taking the process with it, and the shared context paints
|
|
1617
|
+
* black. Development says so at the commit instead (`validateValue`).
|
|
1618
|
+
*/
|
|
1619
|
+
export function isColor(value) {
|
|
1620
|
+
if (typeof value !== 'string') return false;
|
|
1621
|
+
let known = colorAnswers.get(value);
|
|
1622
|
+
if (known === undefined) {
|
|
1623
|
+
known = cssColorStraight(value) !== null;
|
|
1624
|
+
if (colorAnswers.size >= COLOR_ANSWERS_MAX) colorAnswers.clear();
|
|
1625
|
+
colorAnswers.set(value, known);
|
|
1626
|
+
}
|
|
1627
|
+
return known;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1539
1630
|
// ease-out cubic: fast to start, settles gently — the shape almost every UI
|
|
1540
1631
|
// toolkit defaults to for state changes
|
|
1541
1632
|
export const ease = (t) => 1 - (1 - t) ** 3;
|
|
@@ -1566,11 +1657,15 @@ const TOKEN_IN_VALUE = /\$[A-Za-z_][A-Za-z0-9_-]*/g;
|
|
|
1566
1657
|
const mentionsToken = (v) =>
|
|
1567
1658
|
typeof v === 'string' && v.charCodeAt(0) !== 36 && v.includes('$');
|
|
1568
1659
|
|
|
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
|
|
1660
|
+
/** The nested blocks a token can hide in, one level down: a state block or
|
|
1661
|
+
* a query block, whose values are style values, and `animation`, whose
|
|
1662
|
+
* values are the per-property declarations *whose* values are style
|
|
1663
|
+
* values. All are walked by everything that looks for a `$name`, so a
|
|
1664
|
+
* themed loop resolves by the same rules as a themed hover — and a themed
|
|
1665
|
+
* wide layout too. The query blocks were missed: `'@width >= 600':
|
|
1666
|
+
* { padding: '$gutter' }` handed yoga the string `'$gutter'` the moment
|
|
1667
|
+
* it matched, which threw from the frame. */
|
|
1668
|
+
const holdsStyle = (key) => isState(key) || isQuery(key);
|
|
1574
1669
|
const animationBlocks = (style) =>
|
|
1575
1670
|
style.animation && typeof style.animation === 'object'
|
|
1576
1671
|
? Object.values(style.animation).filter((e) => e && typeof e === 'object')
|
|
@@ -1580,7 +1675,7 @@ export function styleUsesTokens(style) {
|
|
|
1580
1675
|
for (const key of Object.keys(style)) {
|
|
1581
1676
|
const v = style[key];
|
|
1582
1677
|
if (isToken(v) || mentionsToken(v)) return true;
|
|
1583
|
-
if (key
|
|
1678
|
+
if (holdsStyle(key) && v && styleUsesTokens(v)) return true;
|
|
1584
1679
|
}
|
|
1585
1680
|
return animationBlocks(style).some(styleUsesTokens);
|
|
1586
1681
|
}
|
|
@@ -1594,7 +1689,7 @@ export function tokenNames(style, out = new Set()) {
|
|
|
1594
1689
|
if (isToken(v)) out.add(v);
|
|
1595
1690
|
else if (mentionsToken(v))
|
|
1596
1691
|
for (const m of v.match(TOKEN_IN_VALUE) ?? []) out.add(m);
|
|
1597
|
-
else if (key
|
|
1692
|
+
else if (holdsStyle(key) && v) tokenNames(v, out);
|
|
1598
1693
|
}
|
|
1599
1694
|
for (const block of animationBlocks(style)) tokenNames(block, out);
|
|
1600
1695
|
return out;
|
|
@@ -1624,7 +1719,7 @@ export function stripTokens(style) {
|
|
|
1624
1719
|
if (key === 'animation' && animationBlocks(style).some(styleUsesTokens)) {
|
|
1625
1720
|
continue;
|
|
1626
1721
|
}
|
|
1627
|
-
out[key] = key
|
|
1722
|
+
out[key] = holdsStyle(key) && v ? stripTokens(v) : v;
|
|
1628
1723
|
}
|
|
1629
1724
|
return out;
|
|
1630
1725
|
}
|
|
@@ -1691,7 +1786,7 @@ export function resolveTokens(
|
|
|
1691
1786
|
else if (strict) {
|
|
1692
1787
|
found.push(unknownToken(unknown, theme, `${where} ${key}`));
|
|
1693
1788
|
}
|
|
1694
|
-
} else if (key
|
|
1789
|
+
} else if (holdsStyle(key) && v) {
|
|
1695
1790
|
out[key] = resolveTokens(v, theme, `${where} ${key}`, strict, found);
|
|
1696
1791
|
} else if (key === 'animation' && v && typeof v === 'object') {
|
|
1697
1792
|
const loops = {};
|
|
@@ -2011,9 +2106,22 @@ const NO_STYLE = Object.freeze({});
|
|
|
2011
2106
|
* name of both styles was most of the cost of a node that mounts — a
|
|
2012
2107
|
* virtualized list replaces its rows at input rate.
|
|
2013
2108
|
*
|
|
2109
|
+
* `problems` is for a caller React is not under — a query block that
|
|
2110
|
+
* starts matching mid-frame — where a throw would leave the frame and take
|
|
2111
|
+
* the process with it. A value the layout cannot take is then dropped, the
|
|
2112
|
+
* property going back to its default as CSS drops a declaration it cannot
|
|
2113
|
+
* parse, and `{ key, value, error }` is pushed for the caller to report.
|
|
2114
|
+
* Without it the value throws, which is what a commit wants: React routes
|
|
2115
|
+
* it to a boundary.
|
|
2116
|
+
*
|
|
2014
2117
|
* @returns true if any layout-affecting prop changed
|
|
2015
2118
|
*/
|
|
2016
|
-
export function applyLayoutStyle(
|
|
2119
|
+
export function applyLayoutStyle(
|
|
2120
|
+
yogaNode,
|
|
2121
|
+
props,
|
|
2122
|
+
oldProps = NO_STYLE,
|
|
2123
|
+
problems = null,
|
|
2124
|
+
) {
|
|
2017
2125
|
let changed = false;
|
|
2018
2126
|
// A placed node's insets — sticky's thresholds, or whatever a registered
|
|
2019
2127
|
// position reads them as — are the placement's, never offsets: yoga gets
|
|
@@ -2030,7 +2138,12 @@ export function applyLayoutStyle(yogaNode, props, oldProps = NO_STYLE) {
|
|
|
2030
2138
|
? !samePosition(props.position, oldProps.position)
|
|
2031
2139
|
: props[key] !== oldProps[key];
|
|
2032
2140
|
if (!moved && !(flipped && inset)) continue;
|
|
2033
|
-
|
|
2141
|
+
const value = placed && inset ? undefined : props[key];
|
|
2142
|
+
// `null` is unset, the way a conditional writes it — `wide ? 240 :
|
|
2143
|
+
// null` — and as it is for every other property in this vocabulary;
|
|
2144
|
+
// yoga's own setters throw on it
|
|
2145
|
+
if (problems === null) applier(yogaNode, value ?? undefined);
|
|
2146
|
+
else applyOrDrop(applier, yogaNode, key, value ?? undefined, problems);
|
|
2034
2147
|
changed = true;
|
|
2035
2148
|
}
|
|
2036
2149
|
// …and the ones the new style dropped, which go back to yoga's default
|
|
@@ -2051,6 +2164,72 @@ export function applyLayoutStyle(yogaNode, props, oldProps = NO_STYLE) {
|
|
|
2051
2164
|
return changed;
|
|
2052
2165
|
}
|
|
2053
2166
|
|
|
2167
|
+
/**
|
|
2168
|
+
* One layout property set on its own — a width put back after the content
|
|
2169
|
+
* floors pinned it, a minimum restored under a floor — by the rules
|
|
2170
|
+
* `applyLayoutStyle` sets it by: `null` is unset, and a value the layout
|
|
2171
|
+
* refuses is dropped. A refused one was reported where it was first
|
|
2172
|
+
* applied, and throwing here would throw from inside a layout pass.
|
|
2173
|
+
*/
|
|
2174
|
+
export function setLayoutValue(yogaNode, key, value) {
|
|
2175
|
+
const applier = LAYOUT_APPLIER_OF.get(key);
|
|
2176
|
+
try {
|
|
2177
|
+
applier(yogaNode, value ?? undefined);
|
|
2178
|
+
} catch {
|
|
2179
|
+
applier(yogaNode, undefined);
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
/** One declaration, dropped rather than thrown when the layout refuses it
|
|
2184
|
+
* (`applyLayoutStyle`'s `problems`). */
|
|
2185
|
+
function applyOrDrop(applier, yogaNode, key, value, problems) {
|
|
2186
|
+
try {
|
|
2187
|
+
applier(yogaNode, value);
|
|
2188
|
+
} catch (error) {
|
|
2189
|
+
applier(yogaNode, undefined);
|
|
2190
|
+
problems.push({ key, value, error });
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
// A yoga node no layout runs on, for development to ask whether a value is
|
|
2195
|
+
// one the layout can take: the question the frame asks, so the two cannot
|
|
2196
|
+
// disagree about what a length is.
|
|
2197
|
+
let scratchNode = null;
|
|
2198
|
+
|
|
2199
|
+
// What the layout has already taken, per property, so development asks
|
|
2200
|
+
// yoga once per spelling rather than once per style it checks: a list of a
|
|
2201
|
+
// thousand rows checks the same six values a thousand times, and a setter
|
|
2202
|
+
// is a call into WebAssembly. A property that takes one number takes them
|
|
2203
|
+
// all — a length, a factor — and one that refuses them is an enum, so
|
|
2204
|
+
// numbers are remembered as one entry.
|
|
2205
|
+
const takenValues = new Map(
|
|
2206
|
+
[...LAYOUT_APPLIER_OF.keys()].map((key) => [key, new Set()]),
|
|
2207
|
+
);
|
|
2208
|
+
const NUMBERS = Symbol('numbers');
|
|
2209
|
+
const TAKEN_MAX = 256;
|
|
2210
|
+
|
|
2211
|
+
/** What applying `value` to `key` throws, or null when the layout takes
|
|
2212
|
+
* it — and for a key that is not a layout property. */
|
|
2213
|
+
function layoutRefusal(key, value) {
|
|
2214
|
+
const taken = takenValues.get(key);
|
|
2215
|
+
if (taken === undefined || value == null) return null;
|
|
2216
|
+
if (taken.has(typeof value === 'number' ? NUMBERS : value)) return null;
|
|
2217
|
+
if (scratchNode === null) {
|
|
2218
|
+
// `createStyles` validates at module scope, before `createRoot` has
|
|
2219
|
+
// loaded the engine — and the commit validates the same style again
|
|
2220
|
+
if (!layoutLoaded()) return null;
|
|
2221
|
+
scratchNode = Yoga.Node.create();
|
|
2222
|
+
}
|
|
2223
|
+
try {
|
|
2224
|
+
LAYOUT_APPLIER_OF.get(key)(scratchNode, value);
|
|
2225
|
+
} catch (error) {
|
|
2226
|
+
return error;
|
|
2227
|
+
}
|
|
2228
|
+
if (typeof value === 'number') taken.add(NUMBERS);
|
|
2229
|
+
else if (taken.size < TAKEN_MAX) taken.add(value);
|
|
2230
|
+
return null;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2054
2233
|
/** Two `position` values asking for the same thing. A registered one written
|
|
2055
2234
|
* with its options is an object, and an inline object is a new one every
|
|
2056
2235
|
* render — which must not cost a layout pass per commit. */
|
|
@@ -2098,21 +2277,88 @@ export function paintPropsChanged(props, oldProps = {}) {
|
|
|
2098
2277
|
);
|
|
2099
2278
|
}
|
|
2100
2279
|
|
|
2280
|
+
// The text values a style can get wrong in a way the engines do not survive:
|
|
2281
|
+
// a `fontStyle` of `7` threw from inside ntk's face matching, a `lineHeight`
|
|
2282
|
+
// of `'24px'` made a paragraph NaN pixels tall — thrown from the layout
|
|
2283
|
+
// pass, where nothing catches it — and a `fontSize` of `'20px'` set no text
|
|
2284
|
+
// at all. A number is a number or a numeric string, the way the coercion
|
|
2285
|
+
// these always went through read one; anything else is not the property,
|
|
2286
|
+
// and text inherits past it, as CSS inherits past a declaration it cannot
|
|
2287
|
+
// parse. Development says so where the style is written (`validateValue`).
|
|
2288
|
+
const NUMERIC_TEXT = /^\s*[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?\s*$/i;
|
|
2289
|
+
const numericOf = (v) =>
|
|
2290
|
+
typeof v === 'number'
|
|
2291
|
+
? v
|
|
2292
|
+
: typeof v === 'string' && NUMERIC_TEXT.test(v)
|
|
2293
|
+
? Number(v)
|
|
2294
|
+
: NaN;
|
|
2295
|
+
const FONT_WEIGHT_WORDS = new Set(['normal', 'bold', 'bolder', 'lighter']);
|
|
2296
|
+
const FONT_STYLE = /^(?:normal|italic|oblique)\b/;
|
|
2297
|
+
|
|
2298
|
+
const isFontSize = (v) => {
|
|
2299
|
+
const n = numericOf(v);
|
|
2300
|
+
return n >= 0 && n < Infinity;
|
|
2301
|
+
};
|
|
2302
|
+
const isFontWeight = (v) => {
|
|
2303
|
+
if (FONT_WEIGHT_WORDS.has(v)) return true;
|
|
2304
|
+
const n = numericOf(v);
|
|
2305
|
+
return n >= 1 && n <= 1000;
|
|
2306
|
+
};
|
|
2307
|
+
const isFontStyle = (v) => typeof v === 'string' && FONT_STYLE.test(v);
|
|
2308
|
+
const isFontFamily = (v) => typeof v === 'string' && v.trim() !== '';
|
|
2309
|
+
|
|
2310
|
+
/** A `lineHeight` a layout can multiply by, or undefined for the font's own. */
|
|
2311
|
+
export function lineHeightOf(value) {
|
|
2312
|
+
const n = numericOf(value);
|
|
2313
|
+
return n >= 0 && n < Infinity ? n : undefined;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
/** What each of them is, for development's message. */
|
|
2317
|
+
const TEXT_VALUE_CHECKS = new Map([
|
|
2318
|
+
['fontSize', [isFontSize, 'a number of pixels, like fontSize: 14']],
|
|
2319
|
+
[
|
|
2320
|
+
'fontWeight',
|
|
2321
|
+
[
|
|
2322
|
+
isFontWeight,
|
|
2323
|
+
"a weight from 1 to 1000, like 600, or 'normal', 'bold', 'bolder' or " +
|
|
2324
|
+
"'lighter'",
|
|
2325
|
+
],
|
|
2326
|
+
],
|
|
2327
|
+
['fontStyle', [isFontStyle, "'normal', 'italic' or 'oblique'"]],
|
|
2328
|
+
[
|
|
2329
|
+
'fontFamily',
|
|
2330
|
+
[isFontFamily, "a family's name, like 'Inter' or 'monospace'"],
|
|
2331
|
+
],
|
|
2332
|
+
[
|
|
2333
|
+
'lineHeight',
|
|
2334
|
+
[
|
|
2335
|
+
(v) => lineHeightOf(v) !== undefined,
|
|
2336
|
+
"a multiple of the font's own line height, like lineHeight: 1.5",
|
|
2337
|
+
],
|
|
2338
|
+
],
|
|
2339
|
+
]);
|
|
2340
|
+
|
|
2101
2341
|
/** Resolved text style (TextLayout base style) from props + inherited. */
|
|
2102
2342
|
export function textStyleFrom(props, inherited) {
|
|
2103
2343
|
const variantNumeric = props.fontVariantNumeric ?? inherited.variantNumeric;
|
|
2104
2344
|
const featureSettings =
|
|
2105
2345
|
props.fontFeatureSettings ?? inherited.featureSettings;
|
|
2106
2346
|
return {
|
|
2107
|
-
family: props.fontFamily
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2347
|
+
family: isFontFamily(props.fontFamily)
|
|
2348
|
+
? props.fontFamily
|
|
2349
|
+
: inherited.family,
|
|
2350
|
+
size: isFontSize(props.fontSize) ? props.fontSize : inherited.size,
|
|
2351
|
+
weight: isFontWeight(props.fontWeight)
|
|
2352
|
+
? props.fontWeight
|
|
2353
|
+
: inherited.weight,
|
|
2354
|
+
style: isFontStyle(props.fontStyle) ? props.fontStyle : inherited.style,
|
|
2111
2355
|
// ntk's name for it is `variations`; the prop is spelled after the CSS
|
|
2112
2356
|
// property, like every other name in this vocabulary
|
|
2113
2357
|
variations: props.fontVariationSettings ?? inherited.variations,
|
|
2114
2358
|
textRendering: props.textRendering ?? inherited.textRendering,
|
|
2115
|
-
|
|
2359
|
+
// an ink no engine can parse is not an ink: the one above is inherited,
|
|
2360
|
+
// as CSS inherits past a `color` it could not read
|
|
2361
|
+
color: isColor(props.color) ? props.color : inherited.color,
|
|
2116
2362
|
letterSpacing: props.letterSpacing ?? inherited.letterSpacing,
|
|
2117
2363
|
// Inherited apart, as CSS inherits the two properties apart — a child's
|
|
2118
2364
|
// `fontVariantNumeric` replaces the keyword above it and leaves an
|
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
|
*/
|
package/src/wayland/context2d.js
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
import { createRequire } from 'node:module';
|
|
43
43
|
import path from 'node:path';
|
|
44
44
|
import { pathToFileURL } from 'node:url';
|
|
45
|
+
import { cssColor } from 'ntk/color';
|
|
45
46
|
import { Image as NtkImage } from 'ntk/image';
|
|
46
47
|
import { Path2D } from 'ntk/path';
|
|
47
48
|
import { glDevice } from './device.js';
|
|
@@ -238,63 +239,19 @@ const BLACK = Object.freeze([0, 0, 0, 1]);
|
|
|
238
239
|
const CLEAR = Object.freeze([0, 0, 0, 0]);
|
|
239
240
|
const OPAQUE = Object.freeze([1, 1, 1, 1]);
|
|
240
241
|
|
|
242
|
+
// ntk's parser, which is the one the other backends read a colour with, so
|
|
243
|
+
// one spelling paints one colour everywhere: this context had a parser of its
|
|
244
|
+
// own, which knew fifteen names and painted `tomato` black. What is not a
|
|
245
|
+
// colour is black here, as it is in the shared context, and core does not
|
|
246
|
+
// hand one over from a style (`isColor` in src/styles.js).
|
|
241
247
|
function parseColorUncached(value) {
|
|
242
248
|
const s = value.trim();
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
parseInt(len === 1 ? h[i] + h[i] : h.slice(i * 2, i * 2 + 2), 16) / 255;
|
|
248
|
-
if (n === 3 || n === 4)
|
|
249
|
-
return premul(q(0, 1), q(1, 1), q(2, 1), n === 4 ? q(3, 1) : 1);
|
|
250
|
-
if (n === 6 || n === 8)
|
|
251
|
-
return premul(q(0, 2), q(1, 2), q(2, 2), n === 8 ? q(3, 2) : 1);
|
|
252
|
-
}
|
|
253
|
-
const m = s.match(/^rgba?\(([^)]+)\)$/i);
|
|
254
|
-
if (m) {
|
|
255
|
-
const p = m[1].split(/[,\s/]+/).filter(Boolean);
|
|
256
|
-
const chan = (v) =>
|
|
257
|
-
(v.endsWith('%') ? parseFloat(v) * 2.55 : parseFloat(v)) / 255;
|
|
258
|
-
const alpha =
|
|
259
|
-
p.length > 3
|
|
260
|
-
? p[3].endsWith('%')
|
|
261
|
-
? parseFloat(p[3]) / 100
|
|
262
|
-
: parseFloat(p[3])
|
|
263
|
-
: 1;
|
|
264
|
-
return premul(
|
|
265
|
-
chan(p[0] || '0'),
|
|
266
|
-
chan(p[1] || '0'),
|
|
267
|
-
chan(p[2] || '0'),
|
|
268
|
-
Number.isFinite(alpha) ? alpha : 1,
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
if (s === 'transparent' || s === 'none') return CLEAR;
|
|
272
|
-
const named = NAMED[s.toLowerCase()];
|
|
273
|
-
if (named) return named;
|
|
274
|
-
return BLACK;
|
|
249
|
+
// SVG's word for no paint, which this context has always read as clear
|
|
250
|
+
if (s === 'none') return CLEAR;
|
|
251
|
+
const c = cssColor(s);
|
|
252
|
+
return c ? Object.freeze(c) : BLACK;
|
|
275
253
|
}
|
|
276
254
|
|
|
277
|
-
function premul(r, g, b, a) {
|
|
278
|
-
return Object.freeze([r * a, g * a, b * a, a]);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const NAMED = {
|
|
282
|
-
black: BLACK,
|
|
283
|
-
white: premul(1, 1, 1, 1),
|
|
284
|
-
red: premul(1, 0, 0, 1),
|
|
285
|
-
green: premul(0, 128 / 255, 0, 1),
|
|
286
|
-
blue: premul(0, 0, 1, 1),
|
|
287
|
-
gray: premul(128 / 255, 128 / 255, 128 / 255, 1),
|
|
288
|
-
grey: premul(128 / 255, 128 / 255, 128 / 255, 1),
|
|
289
|
-
yellow: premul(1, 1, 0, 1),
|
|
290
|
-
orange: premul(1, 165 / 255, 0, 1),
|
|
291
|
-
purple: premul(128 / 255, 0, 128 / 255, 1),
|
|
292
|
-
cyan: premul(0, 1, 1, 1),
|
|
293
|
-
magenta: premul(1, 0, 1, 1),
|
|
294
|
-
silver: premul(192 / 255, 192 / 255, 192 / 255, 1),
|
|
295
|
-
currentcolor: BLACK,
|
|
296
|
-
};
|
|
297
|
-
|
|
298
255
|
/**
|
|
299
256
|
* A gradient: stops, and a 256-texel ramp texture baked from them on first
|
|
300
257
|
* use. Interpolated premultiplied, which is what keeps a fade to transparent
|