react-x11 2.23.0 → 2.23.2
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/app.js +1 -1
- package/src/cocoa/panewindow.js +24 -13
- package/src/decorations.js +28 -2
- package/src/events.js +13 -0
- package/src/frame/index.js +10 -0
- package/src/nodes/boxpaint.js +8 -4
- package/src/nodes/cascade.js +4 -1
- package/src/nodes/layout.js +26 -6
- package/src/nodes/node.js +74 -9
- package/src/nodes/paint.js +1 -1
- package/src/nodes/scrollbars.js +6 -0
- package/src/nodes/text.js +252 -12
- package/src/nodes/window/capabilities.js +11 -2
- package/src/nodes/window/spine.js +82 -32
- package/src/styles.js +62 -2
- package/src/wayland/context2d.js +10 -53
- package/src/win32/app.js +1 -1
- package/src/win32/panewindow.js +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.23.
|
|
3
|
+
"version": "2.23.2",
|
|
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/app.js
CHANGED
|
@@ -762,7 +762,7 @@ export class CocoaApp {
|
|
|
762
762
|
/**
|
|
763
763
|
* The pane's end of the frame channel (childmain hands it over,
|
|
764
764
|
* feature-detected so the X11 pane path never notices): geometry and
|
|
765
|
-
* input come in, pane-present
|
|
765
|
+
* input come in, pane-present and pane-cursor go out.
|
|
766
766
|
*/
|
|
767
767
|
attachPaneChannel(channel) {
|
|
768
768
|
if (!this._paneMode) return;
|
package/src/cocoa/panewindow.js
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
// What the node tree sees is the ordinary window contract: getContext,
|
|
13
13
|
// present, scrollRegion, noteFrameDamage, requestAnimationFrame, events
|
|
14
14
|
// via emit. Geometry and input arrive as channel messages (the host owns
|
|
15
|
-
// layout and hit-testing — this is CPU offloading, not isolation), and
|
|
16
|
-
//
|
|
15
|
+
// layout and hit-testing — this is CPU offloading, not isolation), and what
|
|
16
|
+
// goes out is pane-present, and pane-cursor for the cursor the host shows.
|
|
17
17
|
import { BackendContext2D } from '../backend/context2d.js';
|
|
18
18
|
|
|
19
19
|
let nextPaneId = 1;
|
|
@@ -95,6 +95,17 @@ export class CocoaPaneWindow {
|
|
|
95
95
|
|
|
96
96
|
setTitle() {}
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* The cursor this pane's tree names, sent to the host. A pane has no
|
|
100
|
+
* window the pointer is over — the host's window is — so the host shows
|
|
101
|
+
* it (`<Frame>`'s pane box, src/frame/index.js). Core asks only when the
|
|
102
|
+
* cursor changes, so this is a message per change, not per motion.
|
|
103
|
+
*/
|
|
104
|
+
setCursor(name) {
|
|
105
|
+
if (this.destroyed) return;
|
|
106
|
+
this.app._paneSend?.({ type: 'pane-cursor', cursor: name ?? null });
|
|
107
|
+
}
|
|
108
|
+
|
|
98
109
|
requestAnimationFrame(cb) {
|
|
99
110
|
return this.app._requestFrame(cb);
|
|
100
111
|
}
|
|
@@ -252,7 +263,6 @@ export class CocoaPaneWindow {
|
|
|
252
263
|
this._dirty = false;
|
|
253
264
|
const shown = this._ring[this._drawIndex];
|
|
254
265
|
this._native.surfaceUnlock(shown.handle);
|
|
255
|
-
const damage = this._flushDamage;
|
|
256
266
|
this._flushDamage = null;
|
|
257
267
|
this.app._paneSend?.({
|
|
258
268
|
type: 'pane-present',
|
|
@@ -270,16 +280,17 @@ export class CocoaPaneWindow {
|
|
|
270
280
|
this._surface = next.handle;
|
|
271
281
|
this._surfaceGen++;
|
|
272
282
|
this._native.surfaceLock(next.handle);
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
// what
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
+
// The next frame may paint only its damage, so `next` must first hold
|
|
284
|
+
// the last complete frame underneath — and the WHOLE of it, not just
|
|
285
|
+
// this frame's rects, because in a three-buffer ring `next` was last
|
|
286
|
+
// drawn two presents ago and is stale everywhere. A full copy of the
|
|
287
|
+
// just-shown buffer is the complete background; the safe target is what
|
|
288
|
+
// triple buffering buys. After a full frame as much as a partial one:
|
|
289
|
+
// what a frame was says nothing of what the next will be, and skipping
|
|
290
|
+
// the copy there had a partial frame after a full one — a status line,
|
|
291
|
+
// a hover's underline — drawn over the page as it was two frames back.
|
|
292
|
+
// The copy is wasted only where the next frame is full as well.
|
|
293
|
+
this._native.copySurfaceRegion(shown.handle, next.handle, null);
|
|
283
294
|
}
|
|
284
295
|
|
|
285
296
|
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
|
@@ -1322,6 +1322,19 @@ export class EventManager {
|
|
|
1322
1322
|
callHandler(node, 'unstable_onAttention', handler, ev);
|
|
1323
1323
|
}
|
|
1324
1324
|
|
|
1325
|
+
/**
|
|
1326
|
+
* Apply the cursor again with no motion: for a node whose cursor changed
|
|
1327
|
+
* under a pointer standing still — a `<Frame>` pane's, which arrives from
|
|
1328
|
+
* the pane's process after the motion that changed it was answered here.
|
|
1329
|
+
* A point cursor is not asked again, as there is no new point: each node
|
|
1330
|
+
* keeps what it last named. Nothing, once the window is gone — a pane's
|
|
1331
|
+
* box lets go of its cursor as it unmounts.
|
|
1332
|
+
*/
|
|
1333
|
+
refreshCursor() {
|
|
1334
|
+
if (this.node.destroyed || !this.node.window) return;
|
|
1335
|
+
this._updateCursor(this.hoverPath);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1325
1338
|
/** Apply the deepest hovered node's cursor to the window: its `cursor`
|
|
1326
1339
|
* style, or what it says of the point under the pointer (`cursorAt`), or
|
|
1327
1340
|
* its `defaultCursor` — and failing all three, its parent's. Asked on
|
package/src/frame/index.js
CHANGED
|
@@ -569,10 +569,20 @@ function PaneHostView({
|
|
|
569
569
|
if (msg?.type === 'pane-present') {
|
|
570
570
|
host.setRect(node.abs);
|
|
571
571
|
host.present(msg.id);
|
|
572
|
+
} else if (msg?.type === 'pane-cursor') {
|
|
573
|
+
// The pane names its cursor as an element that draws names one for
|
|
574
|
+
// a point: this box is the node the host's pointer is over, so the
|
|
575
|
+
// cursor is its default — a `cursor` style on the <Frame> still
|
|
576
|
+
// wins — and it is shown now, because the motion that changed it
|
|
577
|
+
// was answered in the host before the pane had said
|
|
578
|
+
node.defaultCursor = msg.cursor ?? undefined;
|
|
579
|
+
node.root?.events?.refreshCursor();
|
|
572
580
|
}
|
|
573
581
|
});
|
|
574
582
|
return () => {
|
|
575
583
|
off();
|
|
584
|
+
node.defaultCursor = undefined;
|
|
585
|
+
node.root?.events?.refreshCursor();
|
|
576
586
|
node.absolutize = origAbsolutize;
|
|
577
587
|
if (origShift) node._shiftAbs = origShift;
|
|
578
588
|
if (s.paneHost === host) s.paneHost = 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/layout.js
CHANGED
|
@@ -510,6 +510,8 @@ export class NodeLayout {
|
|
|
510
510
|
return;
|
|
511
511
|
}
|
|
512
512
|
this.abs = { x, y, width, height };
|
|
513
|
+
const placed = this._absAssigned;
|
|
514
|
+
this._absAssigned = true;
|
|
513
515
|
// moving or resizing changes where this subtree can be hit, and the
|
|
514
516
|
// cached unions all the way up with it
|
|
515
517
|
this._clearHitBounds();
|
|
@@ -517,10 +519,16 @@ export class NodeLayout {
|
|
|
517
519
|
// the reach `_ownPaintBounds` names, ring and shadow alike: a card
|
|
518
520
|
// pushed down by a row above it leaves its old shadow on the surface
|
|
519
521
|
// unless the claim for where it was covers that shadow too
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
+
const reach = Math.max(this._outlineExtent(), this._shadowExtent());
|
|
523
|
+
const grow = reach + DAMAGE_SLOP;
|
|
522
524
|
const shift = layoutDiff.shift;
|
|
523
|
-
|
|
525
|
+
// A rect of no area has no pixels of its own — but an outline or a
|
|
526
|
+
// shadow is drawn round it all the same, and a box of no height
|
|
527
|
+
// arriving with a ring round it claimed nothing, and was never drawn.
|
|
528
|
+
// The old rect counts only once layout has placed the node at all:
|
|
529
|
+
// before that it is the origin, where it never was.
|
|
530
|
+
const had =
|
|
531
|
+
(old.width > 0 && old.height > 0) || (reach > 0 && placed === true);
|
|
524
532
|
if (shift) {
|
|
525
533
|
// Riding a blit (issue #398): the rect this node *would* have had if
|
|
526
534
|
// nothing but the scroll had happened. Landing there is the blit's
|
|
@@ -555,7 +563,7 @@ export class NodeLayout {
|
|
|
555
563
|
return;
|
|
556
564
|
}
|
|
557
565
|
if (had) layoutDiff.sink(insetRect(was, -grow), this);
|
|
558
|
-
if (width > 0 && height > 0) {
|
|
566
|
+
if ((width > 0 && height > 0) || reach > 0) {
|
|
559
567
|
layoutDiff.sink(insetRect(this.abs, -grow), this);
|
|
560
568
|
}
|
|
561
569
|
return;
|
|
@@ -573,7 +581,7 @@ export class NodeLayout {
|
|
|
573
581
|
if (had) {
|
|
574
582
|
layoutDiff.sink(insetRect(old, -grow), this);
|
|
575
583
|
}
|
|
576
|
-
if (width > 0 && height > 0) {
|
|
584
|
+
if ((width > 0 && height > 0) || reach > 0) {
|
|
577
585
|
layoutDiff.sink(insetRect(this.abs, -grow), this);
|
|
578
586
|
}
|
|
579
587
|
}
|
|
@@ -600,6 +608,19 @@ export class NodeLayout {
|
|
|
600
608
|
const style = this.style;
|
|
601
609
|
if (style.backgroundImage) return null;
|
|
602
610
|
if (style.opacity !== undefined && style.opacity < 1) return null;
|
|
611
|
+
// A radius the box is too small for is drawn at half its shorter side
|
|
612
|
+
// (`roundedCorners`), so a resize that moves that moves all four
|
|
613
|
+
// corners — and their outline and shadow with them — and the ones at the
|
|
614
|
+
// edges that held are in no band along the edges that moved.
|
|
615
|
+
const radius = style.borderRadius ?? 0;
|
|
616
|
+
const now = this.abs;
|
|
617
|
+
if (
|
|
618
|
+
radius > 0 &&
|
|
619
|
+
Math.min(radius, old.width / 2, old.height / 2) !==
|
|
620
|
+
Math.min(radius, now.width / 2, now.height / 2)
|
|
621
|
+
) {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
603
624
|
const bw = resolveBorderWidths(style, this.direction);
|
|
604
625
|
const reach =
|
|
605
626
|
Math.max(
|
|
@@ -610,7 +631,6 @@ export class NodeLayout {
|
|
|
610
631
|
bw.left,
|
|
611
632
|
grow,
|
|
612
633
|
) + DAMAGE_SLOP;
|
|
613
|
-
const now = this.abs;
|
|
614
634
|
const bands = [];
|
|
615
635
|
const oldBottom = old.y + old.height;
|
|
616
636
|
const newBottom = now.y + now.height;
|
package/src/nodes/node.js
CHANGED
|
@@ -31,7 +31,7 @@ import { dropVisibleSelection, selectionSurfaceOf } from '../textselection.js';
|
|
|
31
31
|
import { NodeAnimation } from './animation.js';
|
|
32
32
|
import { NodeBoxPaint } from './boxpaint.js';
|
|
33
33
|
import { NodeCascade } from './cascade.js';
|
|
34
|
-
import { NO_DAMAGE } from './damage.js';
|
|
34
|
+
import { DAMAGE_SLOP, NO_DAMAGE } from './damage.js';
|
|
35
35
|
import { NodeHitTest } from './hittest.js';
|
|
36
36
|
import { installMethods } from './install.js';
|
|
37
37
|
import { NodeInvalidate } from './invalidate.js';
|
|
@@ -40,6 +40,7 @@ import { NodeLayout } from './layout.js';
|
|
|
40
40
|
import { NodeLayoutHost } from './layouthost.js';
|
|
41
41
|
import { NodeMoveBlit } from './moveblit.js';
|
|
42
42
|
import { NodePaint } from './paint.js';
|
|
43
|
+
import { unionRect } from './rects.js';
|
|
43
44
|
import { NodePosition } from './position.js';
|
|
44
45
|
import { NodeQueries } from './queries.js';
|
|
45
46
|
import { NodeScrollBlit } from './scrollblit.js';
|
|
@@ -66,6 +67,9 @@ const NO_SELF_DAMAGED = new Set();
|
|
|
66
67
|
* `_nonYogaKids` counts, so the count stays right for a parent that has no
|
|
67
68
|
* box of its own either. */
|
|
68
69
|
const outsideYoga = (child) => !child.yoga || child.isWindow;
|
|
70
|
+
// Drawn by the box it is in: a `<text>`'s characters, a span. A popup has no
|
|
71
|
+
// box here either, but draws in a window of its own.
|
|
72
|
+
const isBoxless = (child) => !child.yoga && !child.isPopup;
|
|
69
73
|
|
|
70
74
|
export class Node {
|
|
71
75
|
get ownerDocument() {
|
|
@@ -126,6 +130,8 @@ export class Node {
|
|
|
126
130
|
this.destroyed = false;
|
|
127
131
|
// absolute rect within the owning window, filled by absolutize()
|
|
128
132
|
this.abs = { x: 0, y: 0, width: 0, height: 0 };
|
|
133
|
+
// whether layout has ever given this node a rect (`_assignAbs`)
|
|
134
|
+
this._absAssigned = false;
|
|
129
135
|
// node states that style blocks can react to, owned by EventManager
|
|
130
136
|
this.states = {
|
|
131
137
|
':hover': false,
|
|
@@ -392,8 +398,9 @@ export class Node {
|
|
|
392
398
|
// nothing vacating — the child being added had no pixels — and the
|
|
393
399
|
// layout diff claims where it lands, so it names no region at all. A
|
|
394
400
|
// child already here is a move, and says so (`_childListFine`).
|
|
395
|
-
const
|
|
396
|
-
const
|
|
401
|
+
const boxless = isBoxless(child);
|
|
402
|
+
const fine = !boxless && this._childListFine(child.parent !== this);
|
|
403
|
+
const before = fine || boxless ? null : this._childListBefore();
|
|
397
404
|
// a move has to leave the yoga tree too — yoga aborts on insertChild of
|
|
398
405
|
// a node that still has a parent
|
|
399
406
|
if (child.parent === this && this._joinsYoga(child)) {
|
|
@@ -422,7 +429,8 @@ export class Node {
|
|
|
422
429
|
// against the app's, and only now can see the `scale` props above it.
|
|
423
430
|
child._rescaleSubtree(mounting);
|
|
424
431
|
this._textContentChanged();
|
|
425
|
-
this.
|
|
432
|
+
if (boxless) this._boxlessChildChanged();
|
|
433
|
+
else this._childListChanged(before, fine);
|
|
426
434
|
a11yHooks.attached?.(this, child);
|
|
427
435
|
}
|
|
428
436
|
|
|
@@ -535,10 +543,13 @@ export class Node {
|
|
|
535
543
|
// captured while the child is still attached, so it covers the rect the
|
|
536
544
|
// child is about to stop occupying — the child's own, for a viewport
|
|
537
545
|
// mid-blit, where this node's box is the whole scrolled band
|
|
538
|
-
const
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
546
|
+
const boxless = isBoxless(child);
|
|
547
|
+
const fine = !boxless && this._childListFine(true);
|
|
548
|
+
const before = boxless
|
|
549
|
+
? null
|
|
550
|
+
: fine
|
|
551
|
+
? (child._claimBounds() ?? NO_DAMAGE)
|
|
552
|
+
: this._childListBefore();
|
|
542
553
|
this.children.splice(index, 1);
|
|
543
554
|
if (outsideYoga(child)) this._nonYogaKids--;
|
|
544
555
|
if (this._joinsYoga(child)) {
|
|
@@ -557,7 +568,30 @@ export class Node {
|
|
|
557
568
|
child.yoga = null;
|
|
558
569
|
}
|
|
559
570
|
this._textContentChanged();
|
|
560
|
-
this.
|
|
571
|
+
if (boxless) this._boxlessChildChanged();
|
|
572
|
+
else this._childListChanged(before, fine);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* A child with no box of its own — a `<text>`'s run of characters, or a
|
|
577
|
+
* `<text>` nested in one — arrived or left. It is drawn by the nearest box
|
|
578
|
+
* above it, so what changed is that box's pixels, and no rect: the box
|
|
579
|
+
* claims where it is now and again once layout has run, the way a string
|
|
580
|
+
* that changes in place claims (`TextChunkNode.setText`).
|
|
581
|
+
*
|
|
582
|
+
* Not through `_childListChanged`. Under a scroll pane its claims are the
|
|
583
|
+
* finer ones (`_childListFine`): the leaving child's own rect, and the
|
|
584
|
+
* layout diff's claim of where an arriving one lands. A boxless child has
|
|
585
|
+
* neither, so a label emptied or filled inside a pane claimed nothing, and
|
|
586
|
+
* its old glyphs stayed on the screen. And a span that gains a child has
|
|
587
|
+
* no rect of its own to claim either.
|
|
588
|
+
*/
|
|
589
|
+
_boxlessChildChanged() {
|
|
590
|
+
this._clearHitBounds();
|
|
591
|
+
let owner = this;
|
|
592
|
+
while (owner && !owner.yoga) owner = owner.parent;
|
|
593
|
+
if (owner) owner._invalidateLayout('text');
|
|
594
|
+
else this.root?.invalidate(true, null, 'text');
|
|
561
595
|
}
|
|
562
596
|
|
|
563
597
|
/** Destroy real resources (X windows) in this subtree. Yoga nodes are
|
|
@@ -623,7 +657,10 @@ export class Node {
|
|
|
623
657
|
// factor. The walk restyles this node too, so the call after it hits
|
|
624
658
|
// the identity check and costs nothing twice.
|
|
625
659
|
if (newProps.scale !== prev.scale) this._rescaleSubtree();
|
|
660
|
+
// asked of the style still in force: see `_clipStarted`
|
|
661
|
+
const clipped = this.clipsChildren();
|
|
626
662
|
const style = this._syncStyle(newProps);
|
|
663
|
+
if (!clipped && this.clipsChildren()) this._clipStarted();
|
|
627
664
|
let layoutChanged = false;
|
|
628
665
|
// hoisted styles hit the identity check and skip the whole update
|
|
629
666
|
if (this.yoga && style !== prevStyle) {
|
|
@@ -696,6 +733,34 @@ export class Node {
|
|
|
696
733
|
a11yHooks.propsChanged?.(this);
|
|
697
734
|
}
|
|
698
735
|
|
|
736
|
+
/**
|
|
737
|
+
* This node started clipping what it holds (`overflow: 'hidden'` or
|
|
738
|
+
* `'scroll'`), so whatever its children drew past its box is gone, and
|
|
739
|
+
* nothing moved for the layout diff to notice. Its own claims are made
|
|
740
|
+
* after the new style is in, when its paint bounds already stop at its
|
|
741
|
+
* box, so the old reach is claimed here: what the children drew, which is
|
|
742
|
+
* theirs, and the same whether or not this node clips them.
|
|
743
|
+
*
|
|
744
|
+
* The other way — a box that stops clipping — needs nothing of its own:
|
|
745
|
+
* the claim once layout has run reaches as far as the children now do.
|
|
746
|
+
*/
|
|
747
|
+
_clipStarted() {
|
|
748
|
+
let reach = null;
|
|
749
|
+
for (const child of this.children) {
|
|
750
|
+
if (child.isWindow || !child.yoga || child.hidden) continue;
|
|
751
|
+
if (child.style?.display === 'none') continue;
|
|
752
|
+
const bounds = child._subtreeBounds();
|
|
753
|
+
reach = reach ? unionRect(reach, bounds) : bounds;
|
|
754
|
+
}
|
|
755
|
+
if (!reach) return;
|
|
756
|
+
this.invalidate(false, {
|
|
757
|
+
x: reach.x - DAMAGE_SLOP,
|
|
758
|
+
y: reach.y - DAMAGE_SLOP,
|
|
759
|
+
width: reach.width + 2 * DAMAGE_SLOP,
|
|
760
|
+
height: reach.height + 2 * DAMAGE_SLOP,
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
699
764
|
/**
|
|
700
765
|
* Did anything this node *draws* change? Answering true damages the whole
|
|
701
766
|
* node; answering false contributes no damage at all.
|
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/scrollbars.js
CHANGED
|
@@ -66,6 +66,12 @@ export function scrollbarGeometry({
|
|
|
66
66
|
// carries `scale` so the track, the hit slop and the thumb's corner
|
|
67
67
|
// radius downstream draw from the same number.
|
|
68
68
|
const barWidth = SCROLLBAR_WIDTH * scale;
|
|
69
|
+
// …and never wider than the pane across: a pane thinner than the bar and
|
|
70
|
+
// its inset shows none. Laid a bar's width in from its far edge, the bar
|
|
71
|
+
// of a pane four pixels tall stood four pixels above it, painted past the
|
|
72
|
+
// pane's box where none of the pane's claims reach — the same stray ink as
|
|
73
|
+
// a thumb run past its track, below, the other way round.
|
|
74
|
+
if (crossSize < barWidth + inset) return null;
|
|
69
75
|
const rtl = direction === 'rtl';
|
|
70
76
|
// Never longer than the track: a pane shorter than the minimum gets a
|
|
71
77
|
// thumb that fills it and has no travel. The bar paints outside the
|
package/src/nodes/text.js
CHANGED
|
@@ -9,8 +9,9 @@ import {
|
|
|
9
9
|
import { cssColorStraight } from 'ntk/color';
|
|
10
10
|
import { hooks as a11yHooks } from '../a11y.js';
|
|
11
11
|
import { codePointAtOffset, codeUnitOffsets } from '../textrange.js';
|
|
12
|
-
import { NO_DAMAGE } from './damage.js';
|
|
12
|
+
import { DAMAGE_SLOP, NO_DAMAGE, layoutDiff } from './damage.js';
|
|
13
13
|
import { Node } from './node.js';
|
|
14
|
+
import { insetRect, unionRect } from './rects.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Yoga's measure modes, in words. Indexed by the integer yoga hands a
|
|
@@ -37,6 +38,20 @@ const STRIP_COVERAGE = 0.45;
|
|
|
37
38
|
|
|
38
39
|
const textStripBelow = new WeakMap();
|
|
39
40
|
|
|
41
|
+
/** `ink`, when any of it lies outside `box`; else null. */
|
|
42
|
+
function outside(ink, box) {
|
|
43
|
+
if (!ink) return null;
|
|
44
|
+
return ink.x < box.x ||
|
|
45
|
+
ink.y < box.y ||
|
|
46
|
+
ink.x + ink.width > box.x + box.width ||
|
|
47
|
+
ink.y + ink.height > box.y + box.height
|
|
48
|
+
? ink
|
|
49
|
+
: null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `TextNode._trim`'s answer, per layout. */
|
|
53
|
+
const trims = new WeakMap();
|
|
54
|
+
|
|
40
55
|
/** The size under which this connection's text is painted as strips. */
|
|
41
56
|
export function setTextStripBelow(app, below) {
|
|
42
57
|
if (below === undefined) return;
|
|
@@ -183,6 +198,8 @@ export class TextNode extends Node {
|
|
|
183
198
|
super('text', props, app, { yoga: !span });
|
|
184
199
|
this.isSpan = span;
|
|
185
200
|
this._layouts = new Map();
|
|
201
|
+
// how far the ink of the last paint reached past the box (`_keepInkReach`)
|
|
202
|
+
this._inkReach = null;
|
|
186
203
|
}
|
|
187
204
|
|
|
188
205
|
/** Height for a width: the paragraph shaped into whatever is on offer.
|
|
@@ -214,6 +231,8 @@ export class TextNode extends Node {
|
|
|
214
231
|
return;
|
|
215
232
|
}
|
|
216
233
|
this._layouts.clear();
|
|
234
|
+
// the ink goes with the layout it was placed from (`_ownPaintBounds`)
|
|
235
|
+
this._clearPaintBounds();
|
|
217
236
|
if (this.yoga) this.yoga.markDirty();
|
|
218
237
|
}
|
|
219
238
|
|
|
@@ -233,6 +252,7 @@ export class TextNode extends Node {
|
|
|
233
252
|
return;
|
|
234
253
|
}
|
|
235
254
|
this._layouts.clear();
|
|
255
|
+
this._clearPaintBounds();
|
|
236
256
|
}
|
|
237
257
|
|
|
238
258
|
/**
|
|
@@ -405,18 +425,37 @@ export class TextNode extends Node {
|
|
|
405
425
|
return this._elides() ? maxWidth : Infinity;
|
|
406
426
|
}
|
|
407
427
|
|
|
408
|
-
|
|
428
|
+
/** The layout at `maxWidth`: kept, or shaped now. With `shape` false, only
|
|
429
|
+
* a kept one — for a question that must not cost a shaping, such as how
|
|
430
|
+
* far a paragraph not yet painted will ink (`_inkPlaced`). */
|
|
431
|
+
_layoutFor(maxWidth, shape = true) {
|
|
409
432
|
const fonts = this.app?.fonts;
|
|
410
433
|
if (!fonts) return null; // mock container in tests: no text metrics
|
|
411
434
|
const maxLines = this._maxLines();
|
|
412
435
|
const overflow = this.style.textOverflow;
|
|
413
|
-
// Both truncation options are inputs to the shaping, so
|
|
414
|
-
// the
|
|
415
|
-
// map on its way past — but a cache keyed on less
|
|
416
|
-
// one refactor away from answering with the wrong
|
|
417
|
-
// wrong paragraph here is glyphs on screen that no
|
|
418
|
-
|
|
419
|
-
|
|
436
|
+
// Both truncation options are inputs to the shaping, so a hit has to
|
|
437
|
+
// match them as well as the width. They can only change with the style,
|
|
438
|
+
// which clears the whole map on its way past — but a cache keyed on less
|
|
439
|
+
// than it depends on is one refactor away from answering with the wrong
|
|
440
|
+
// paragraph, and the wrong paragraph here is glyphs on screen that no
|
|
441
|
+
// error mentions.
|
|
442
|
+
//
|
|
443
|
+
// The width is the key as the number it is, and the other two are
|
|
444
|
+
// checked on the entry, rather than all three spelled into one string: a
|
|
445
|
+
// layout pass measures every label several times over, and turning a
|
|
446
|
+
// fractional width into text cost more than the rest of a hit together.
|
|
447
|
+
const entry = this._layouts.get(maxWidth);
|
|
448
|
+
let layout =
|
|
449
|
+
entry !== undefined &&
|
|
450
|
+
entry.maxLines === maxLines &&
|
|
451
|
+
entry.overflow === overflow
|
|
452
|
+
? entry.layout
|
|
453
|
+
: null;
|
|
454
|
+
if (!layout) {
|
|
455
|
+
layout = this._shapedWider(maxWidth, maxLines, overflow);
|
|
456
|
+
if (layout) this._layouts.set(maxWidth, { layout, maxLines, overflow });
|
|
457
|
+
}
|
|
458
|
+
if (!layout && !shape) return null;
|
|
420
459
|
if (!layout) {
|
|
421
460
|
const spans = this.collectSpans([]);
|
|
422
461
|
const base = this.resolvedTextStyle();
|
|
@@ -440,11 +479,52 @@ export class TextNode extends Node {
|
|
|
440
479
|
direction: this.direction,
|
|
441
480
|
});
|
|
442
481
|
if (this._layouts.size > 32) this._layouts.clear();
|
|
443
|
-
this._layouts.set(
|
|
482
|
+
this._layouts.set(maxWidth, { layout, maxLines, overflow });
|
|
444
483
|
}
|
|
445
484
|
return layout;
|
|
446
485
|
}
|
|
447
486
|
|
|
487
|
+
/**
|
|
488
|
+
* A layout this paragraph already has at a wider width that shaping it at
|
|
489
|
+
* `maxWidth` would come out the same as, or null.
|
|
490
|
+
*
|
|
491
|
+
* ntk fills lines greedily: a line takes tokens while they fit. So if
|
|
492
|
+
* every line a wider layout made fits `maxWidth` as well, the narrower
|
|
493
|
+
* layout breaks at the same tokens — each line was the longest run that
|
|
494
|
+
* fit the wider width, and so is the longest that fits a narrower one it
|
|
495
|
+
* still fits in — and a word too wide for either is cut at the same
|
|
496
|
+
* cluster. Past the breaks, only two things read the width, and both are
|
|
497
|
+
* ruled out here: placing a line anywhere but at the left edge (centred,
|
|
498
|
+
* right-aligned, or the start of a right-to-left paragraph), and eliding
|
|
499
|
+
* one. A line within a thousandth of a pixel of the width is left to be
|
|
500
|
+
* shaped, since the sum the fill tests and the width the line reports are
|
|
501
|
+
* added up in different orders.
|
|
502
|
+
*
|
|
503
|
+
* The paint's request is the common one: a label is laid out in a box the
|
|
504
|
+
* width it measured, a pixel or less narrower than the width it was
|
|
505
|
+
* measured at, and was shaped again for it — 37% of the layouts a fling
|
|
506
|
+
* down a <Tree> built.
|
|
507
|
+
*
|
|
508
|
+
* ntk's layouts only. CoreText and DirectWrite break lines their own way,
|
|
509
|
+
* and their layouts carry no `baseLevel`.
|
|
510
|
+
*/
|
|
511
|
+
_shapedWider(maxWidth, maxLines, overflow) {
|
|
512
|
+
if (overflow === 'ellipsis') return null;
|
|
513
|
+
const align = this.style.textAlign;
|
|
514
|
+
if (align !== undefined && align !== 'start' && align !== 'left') {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
const fits = maxWidth - 0.001;
|
|
518
|
+
for (const [width, entry] of this._layouts) {
|
|
519
|
+
if (!(width > maxWidth)) continue;
|
|
520
|
+
if (entry.maxLines !== maxLines || entry.overflow !== overflow) continue;
|
|
521
|
+
const layout = entry.layout;
|
|
522
|
+
if (layout?.baseLevel !== 0) continue;
|
|
523
|
+
if (layout.lines.every((line) => line.width <= fits)) return layout;
|
|
524
|
+
}
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
|
|
448
528
|
/**
|
|
449
529
|
* `textBoxTrim: 'cap-alphabetic'` — CSS's `text-box-trim: trim-both` with
|
|
450
530
|
* `text-box-edge: cap alphabetic`. How much of the line box to take off
|
|
@@ -482,6 +562,22 @@ export class TextNode extends Node {
|
|
|
482
562
|
if (this.style.textBoxTrim !== 'cap-alphabetic') return null;
|
|
483
563
|
const lines = layout?.lines;
|
|
484
564
|
if (!lines?.length) return null;
|
|
565
|
+
// Once per layout rather than once per measure, which a layout pass asks
|
|
566
|
+
// for several times over. A layout never changes once it is shaped, and
|
|
567
|
+
// the face and size read below are the ones it was shaped in: a style
|
|
568
|
+
// that moves either drops every layout on its way past
|
|
569
|
+
// (`_textContentChanged`), so the answer cannot go stale while its layout
|
|
570
|
+
// is still being handed out.
|
|
571
|
+
let trim = trims.get(layout);
|
|
572
|
+
if (trim === undefined) {
|
|
573
|
+
trim = this._trimOf(layout, lines);
|
|
574
|
+
trims.set(layout, trim);
|
|
575
|
+
}
|
|
576
|
+
return trim;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** `_trim`'s answer, worked out. */
|
|
580
|
+
_trimOf(layout, lines) {
|
|
485
581
|
const base = this.resolvedTextStyle();
|
|
486
582
|
const font = this.app?.fonts?.match?.(base.family, {
|
|
487
583
|
weight: base.weight,
|
|
@@ -510,9 +606,12 @@ export class TextNode extends Node {
|
|
|
510
606
|
* a differently-placed layout is a caret in the wrong place, and nothing
|
|
511
607
|
* about it would look like a bug in this function.
|
|
512
608
|
*/
|
|
513
|
-
_placedLayout() {
|
|
609
|
+
_placedLayout(shape = true) {
|
|
514
610
|
const content = this.contentBox();
|
|
515
|
-
const layout = this._layoutFor(
|
|
611
|
+
const layout = this._layoutFor(
|
|
612
|
+
this._wrapWidth(content.width || Infinity),
|
|
613
|
+
shape,
|
|
614
|
+
);
|
|
516
615
|
if (!layout) return null;
|
|
517
616
|
// the box was shortened from the top, so the glyphs come up with it
|
|
518
617
|
const trim = this._trim(layout);
|
|
@@ -566,6 +665,7 @@ export class TextNode extends Node {
|
|
|
566
665
|
paintContent(ctx) {
|
|
567
666
|
const placed = this._placedLayout();
|
|
568
667
|
if (!placed) return;
|
|
668
|
+
this._keepInkReach(placed);
|
|
569
669
|
this._paintSelection(ctx);
|
|
570
670
|
if (this._paintsStrip()) {
|
|
571
671
|
this._paintStrip(ctx, placed);
|
|
@@ -574,6 +674,146 @@ export class TextNode extends Node {
|
|
|
574
674
|
placed.layout.draw(ctx, placed.x, placed.y);
|
|
575
675
|
}
|
|
576
676
|
|
|
677
|
+
/**
|
|
678
|
+
* Where this paragraph puts ink, when that is outside its box: the box
|
|
679
|
+
* grown by the ink it has on the screen now, before a layout pass, and by
|
|
680
|
+
* the ink the frame is about to draw, after one.
|
|
681
|
+
*
|
|
682
|
+
* The glyphs are drawn where the layout puts them, and two things put them
|
|
683
|
+
* past the box. A word too wide for the box and with nowhere to break is
|
|
684
|
+
* drawn whole, across the box's edge. And a box trimmed to its capitals
|
|
685
|
+
* (`textBoxTrim`) leaves every descender and every ascender taller than a
|
|
686
|
+
* capital outside it, by design. A change that repaints the paragraph —
|
|
687
|
+
* its colour, its characters — claimed the box, so the ink outside it kept
|
|
688
|
+
* the old colour, or stayed after the word was gone.
|
|
689
|
+
*/
|
|
690
|
+
_ownPaintBounds() {
|
|
691
|
+
let bounds = super._ownPaintBounds();
|
|
692
|
+
// Both, whenever it is asked. A claim made before this frame's layout
|
|
693
|
+
// wants the ink on the screen, one made after it the ink about to be
|
|
694
|
+
// drawn — and so does everything the paint sizes from these bounds,
|
|
695
|
+
// which asks before this paragraph has ever drawn: an ancestor's
|
|
696
|
+
// `opacity` draws the subtree into a surface this big, and one sized
|
|
697
|
+
// before the ink was known cut a word's overflow off at the box, on the
|
|
698
|
+
// first frame only. The union is never smaller than either.
|
|
699
|
+
const shown = this._inkOnScreen();
|
|
700
|
+
if (shown) bounds = unionRect(bounds, shown);
|
|
701
|
+
const placed = this._inkPlaced();
|
|
702
|
+
if (placed) bounds = unionRect(bounds, placed);
|
|
703
|
+
return bounds;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* A paragraph that moved claims its ink past its box, where it was and
|
|
708
|
+
* where it lands, beside the claims the box makes (`Node._assignAbs`).
|
|
709
|
+
*
|
|
710
|
+
* Those are the box's rect, grown by its outline and shadow, and a rect
|
|
711
|
+
* of no area claims nothing — there were, or will be, no pixels there. A
|
|
712
|
+
* paragraph squeezed to no height by the row it sits in still draws its
|
|
713
|
+
* lines, though, below a box of none: moved along by a sibling, it left
|
|
714
|
+
* its old glyphs on the screen and drew its new ones nowhere.
|
|
715
|
+
*/
|
|
716
|
+
_assignAbs(x, y, width, height) {
|
|
717
|
+
const old = this.abs;
|
|
718
|
+
const shown = this._inkOnScreen();
|
|
719
|
+
super._assignAbs(x, y, width, height);
|
|
720
|
+
if (this.abs === old || !layoutDiff.sink) return;
|
|
721
|
+
const shift = layoutDiff.shift;
|
|
722
|
+
// riding a blit: the ink moved with the pixels, and claims nothing
|
|
723
|
+
if (
|
|
724
|
+
shift &&
|
|
725
|
+
old.x + shift.x === x &&
|
|
726
|
+
old.y + shift.y === y &&
|
|
727
|
+
old.width === width &&
|
|
728
|
+
old.height === height
|
|
729
|
+
) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (shown) {
|
|
733
|
+
const was = shift
|
|
734
|
+
? { ...shown, x: shown.x + shift.x, y: shown.y + shift.y }
|
|
735
|
+
: shown;
|
|
736
|
+
layoutDiff.sink(insetRect(was, -DAMAGE_SLOP), this);
|
|
737
|
+
}
|
|
738
|
+
const placed = this._inkPlaced();
|
|
739
|
+
if (placed) layoutDiff.sink(insetRect(placed, -DAMAGE_SLOP), this);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** The rect the lines of a placed layout cover, in window coordinates. */
|
|
743
|
+
_inkOf({ layout, x, y }) {
|
|
744
|
+
const lines = layout.lines;
|
|
745
|
+
if (!lines?.length) return null;
|
|
746
|
+
let left = Infinity;
|
|
747
|
+
let right = -Infinity;
|
|
748
|
+
for (const line of lines) {
|
|
749
|
+
left = Math.min(left, line.x);
|
|
750
|
+
right = Math.max(right, line.x + line.width);
|
|
751
|
+
}
|
|
752
|
+
return {
|
|
753
|
+
x: x + left,
|
|
754
|
+
y,
|
|
755
|
+
width: right - left,
|
|
756
|
+
height: layout.height,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/** The ink the frame will draw, where it reaches past the box. */
|
|
761
|
+
_inkPlaced() {
|
|
762
|
+
// a paragraph not laid out yet has no box to be placed in
|
|
763
|
+
const box = this.abs;
|
|
764
|
+
if (!(box.width > 0 || box.height > 0)) return null;
|
|
765
|
+
// Shaped only once this frame's layout has run, when the paint is about
|
|
766
|
+
// to need the very same layout: before it, a claim at the commit would
|
|
767
|
+
// shape every string a batch of input passes through, painted or not.
|
|
768
|
+
// The measure pass does not always make the one the paint draws — a
|
|
769
|
+
// box of no width is drawn unwrapped — and a paragraph moved into one
|
|
770
|
+
// claimed none of its new ink.
|
|
771
|
+
const placed = this._placedLayout(this.root?._laidOut === true);
|
|
772
|
+
return placed ? outside(this._inkOf(placed), box) : null;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** The ink on the screen, where it reaches past the box: kept by the last
|
|
776
|
+
* paint as distances from the box's edges, so it rides a scroll's shift
|
|
777
|
+
* with the box. */
|
|
778
|
+
_inkOnScreen() {
|
|
779
|
+
const reach = this._inkReach;
|
|
780
|
+
if (!reach) return null;
|
|
781
|
+
const box = this.abs;
|
|
782
|
+
return {
|
|
783
|
+
x: box.x - reach.left,
|
|
784
|
+
y: box.y - reach.top,
|
|
785
|
+
width: box.width + reach.left + reach.right,
|
|
786
|
+
height: box.height + reach.top + reach.bottom,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/** Remember how far past the box this paint's ink reaches. A reach that
|
|
791
|
+
* changed is a changed paint bound, so the cached ones above it go. */
|
|
792
|
+
_keepInkReach(placed) {
|
|
793
|
+
const ink = outside(this._inkOf(placed), this.abs);
|
|
794
|
+
const box = this.abs;
|
|
795
|
+
const reach = ink && {
|
|
796
|
+
left: Math.max(0, box.x - ink.x),
|
|
797
|
+
top: Math.max(0, box.y - ink.y),
|
|
798
|
+
right: Math.max(0, ink.x + ink.width - (box.x + box.width)),
|
|
799
|
+
bottom: Math.max(0, ink.y + ink.height - (box.y + box.height)),
|
|
800
|
+
};
|
|
801
|
+
const was = this._inkReach;
|
|
802
|
+
if (
|
|
803
|
+
reach === was ||
|
|
804
|
+
(reach &&
|
|
805
|
+
was &&
|
|
806
|
+
reach.left === was.left &&
|
|
807
|
+
reach.top === was.top &&
|
|
808
|
+
reach.right === was.right &&
|
|
809
|
+
reach.bottom === was.bottom)
|
|
810
|
+
) {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
this._inkReach = reach;
|
|
814
|
+
this._clearPaintBounds();
|
|
815
|
+
}
|
|
816
|
+
|
|
577
817
|
/**
|
|
578
818
|
* Whether this paragraph is too small to read, and is painted as a strip
|
|
579
819
|
* where its lines are instead of as glyphs (`_paintStrip`).
|
|
@@ -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
|
/**
|
|
@@ -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/styles.js
CHANGED
|
@@ -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,8 +865,13 @@ 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) {
|
|
858
877
|
const text = TEXT_VALUE_CHECKS.get(key);
|
|
@@ -868,6 +887,17 @@ function validateValue(key, value, where) {
|
|
|
868
887
|
}
|
|
869
888
|
return;
|
|
870
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
|
+
}
|
|
871
901
|
if (key === 'letterSpacing') {
|
|
872
902
|
if (
|
|
873
903
|
value != null &&
|
|
@@ -1569,6 +1599,34 @@ export function tint(color, alpha) {
|
|
|
1569
1599
|
return rgba([c[0], c[1], c[2], c[3] * alpha]);
|
|
1570
1600
|
}
|
|
1571
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
|
+
|
|
1572
1630
|
// ease-out cubic: fast to start, settles gently — the shape almost every UI
|
|
1573
1631
|
// toolkit defaults to for state changes
|
|
1574
1632
|
export const ease = (t) => 1 - (1 - t) ** 3;
|
|
@@ -2298,7 +2356,9 @@ export function textStyleFrom(props, inherited) {
|
|
|
2298
2356
|
// property, like every other name in this vocabulary
|
|
2299
2357
|
variations: props.fontVariationSettings ?? inherited.variations,
|
|
2300
2358
|
textRendering: props.textRendering ?? inherited.textRendering,
|
|
2301
|
-
|
|
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,
|
|
2302
2362
|
letterSpacing: props.letterSpacing ?? inherited.letterSpacing,
|
|
2303
2363
|
// Inherited apart, as CSS inherits the two properties apart — a child's
|
|
2304
2364
|
// `fontVariantNumeric` replaces the keyword above it and leaves an
|
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
|
package/src/win32/app.js
CHANGED
|
@@ -416,7 +416,7 @@ export class Win32App {
|
|
|
416
416
|
|
|
417
417
|
/**
|
|
418
418
|
* The pane process's end of the frame channel: geometry and input in,
|
|
419
|
-
* presents out. Feature-detected by the pane bootstrap
|
|
419
|
+
* presents and the cursor out. Feature-detected by the pane bootstrap
|
|
420
420
|
* (src/frame/childmain.js), and a no-op in a process that is not a pane.
|
|
421
421
|
*/
|
|
422
422
|
attachPaneChannel(channel) {
|
package/src/win32/panewindow.js
CHANGED
|
@@ -205,6 +205,15 @@ export class Win32PaneWindow {
|
|
|
205
205
|
this.title = title;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/** The cursor this pane's tree names, sent to the host, which shows it:
|
|
209
|
+
* the pointer is over the host's window, not this one (the Cocoa pane's
|
|
210
|
+
* `setCursor`). */
|
|
211
|
+
setCursor(name) {
|
|
212
|
+
this.cursor = name;
|
|
213
|
+
if (this.destroyed) return;
|
|
214
|
+
this.app._paneSend?.({ type: 'pane-cursor', cursor: name ?? null });
|
|
215
|
+
}
|
|
216
|
+
|
|
208
217
|
setSizeHints() {}
|
|
209
218
|
|
|
210
219
|
/** The host sizes a pane, not the pane itself: a `<Frame>`'s box is laid
|