react-x11 2.11.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +278 -129
- package/package.json +12 -4
- package/src/Reconciler.js +19 -31
- package/src/a11y.js +2 -2
- package/src/anchor.js +7 -5
- package/src/appcontext.js +59 -30
- package/src/bootstrap.js +14 -0
- package/src/clientmessage.js +1 -1
- package/src/cocoa/app.js +303 -49
- package/src/cocoa/bezels.js +175 -30
- package/src/cocoa/dnd.js +27 -13
- package/src/cocoa/fonts.js +3 -3
- package/src/cocoa/glarea.js +24 -5
- package/src/cocoa/main.d.ts +8 -0
- package/src/cocoa/main.js +43 -0
- package/src/cocoa/overlay.js +159 -0
- package/src/cocoa/panehost.js +15 -5
- package/src/cocoa/presenter.js +13 -9
- package/src/cocoa/promotion.js +17 -7
- package/src/cocoa/relaunch.js +207 -0
- package/src/cocoa/threaded.js +246 -0
- package/src/cocoa/window.js +256 -42
- package/src/components/Select.js +2 -2
- package/src/components/anchor.js +3 -3
- package/src/components/native.js +12 -7
- package/src/components/theme.js +2 -2
- package/src/debug.js +1 -1
- package/src/decorations.js +1 -1
- package/src/editmenu.js +2 -2
- package/src/embedding.js +31 -0
- package/src/errors.js +46 -0
- package/src/events.js +78 -18
- package/src/foreignnodes.js +59 -5
- package/src/frames.js +2 -2
- package/src/glnodes.js +172 -41
- package/src/gloverlay.js +383 -0
- package/src/grid.js +1653 -0
- package/src/host.d.ts +230 -1
- package/src/host.js +11 -3
- package/src/imagesource.js +1 -1
- package/src/index.d.ts +34 -4
- package/src/index.js +9 -1
- package/src/layouts.js +721 -0
- package/src/node.d.ts +16 -3
- package/src/node.js +19 -21
- package/src/nodes/animation.js +644 -0
- package/src/nodes/box.js +21 -0
- package/src/nodes/boxpaint.js +473 -0
- package/src/nodes/canvas.js +269 -0
- package/src/nodes/cascade.js +600 -0
- package/src/nodes/damage.js +183 -0
- package/src/nodes/edithistory.js +124 -0
- package/src/nodes/editmenupopup.js +260 -0
- package/src/nodes/hittest.js +185 -0
- package/src/nodes/image.js +266 -0
- package/src/nodes/install.js +75 -0
- package/src/nodes/invalidate.js +465 -0
- package/src/nodes/kinds.js +31 -0
- package/src/nodes/layout.js +439 -0
- package/src/nodes/layouthost.js +949 -0
- package/src/nodes/node.js +868 -0
- package/src/nodes/paint.js +466 -0
- package/src/nodes/position.js +366 -0
- package/src/nodes/preedit.js +127 -0
- package/src/nodes/queries.js +330 -0
- package/src/nodes/rects.js +102 -0
- package/src/nodes/scrollable.js +891 -0
- package/src/nodes/scrollbars.js +138 -0
- package/src/nodes/scrollblit.js +1034 -0
- package/src/nodes/selectable.js +142 -0
- package/src/nodes/styling.js +225 -0
- package/src/nodes/text.js +649 -0
- package/src/nodes/textarea.js +391 -0
- package/src/nodes/textinput.js +1146 -0
- package/src/nodes/util.js +17 -0
- package/src/nodes/window/anchoring.js +161 -0
- package/src/nodes/window/capabilities.js +190 -0
- package/src/nodes/window/debugpaint.js +83 -0
- package/src/nodes/window/droptarget.js +145 -0
- package/src/nodes/window/floors.js +577 -0
- package/src/nodes/window/flush.js +369 -0
- package/src/nodes/window/hints.js +482 -0
- package/src/nodes/window/listeners.js +222 -0
- package/src/nodes/window/popup.js +71 -0
- package/src/nodes/window/size.js +591 -0
- package/src/nodes/window/window.js +954 -0
- package/src/palette.js +1 -1
- package/src/registry.js +7 -3
- package/src/styles.js +137 -15
- package/src/svgnodes.js +2 -1
- package/src/testing/harness.js +2 -2
- package/src/textselection.js +5 -3
- package/src/trace-registry.js +1 -1
- package/src/types/components.d.ts +38 -6
- package/src/types/elements.d.ts +26 -14
- package/src/types/nodes.d.ts +17 -2
- package/src/types/style.d.ts +94 -3
- package/src/windowstate.js +1 -1
- package/src/yoga.js +1 -1
- package/src/nodes.js +0 -13120
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Hit testing, and the geometry a node reports about itself: containsPoint
|
|
2
|
+
// with and without slop, hitTest down the paint order, getClientRects, and
|
|
3
|
+
// the box-model measure DevTools reads.
|
|
4
|
+
|
|
5
|
+
import { resolveHitSlop } from '../styles.js';
|
|
6
|
+
|
|
7
|
+
/** Hit testing, installed onto `Node.prototype` by node.js. */
|
|
8
|
+
export class NodeHitTest {
|
|
9
|
+
containsPoint(x, y) {
|
|
10
|
+
return (
|
|
11
|
+
x >= this.abs.x &&
|
|
12
|
+
y >= this.abs.y &&
|
|
13
|
+
x < this.abs.x + this.abs.width &&
|
|
14
|
+
y < this.abs.y + this.abs.height
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The same test grown by `hitSlop`. Deliberately separate from
|
|
20
|
+
* `containsPoint`: slop belongs to *this* node's target and nothing else,
|
|
21
|
+
* so it must not widen the rect that clips this node's children, must not
|
|
22
|
+
* reach `paintBounds`, and must never touch yoga. A 16px slider with 4px
|
|
23
|
+
* of slop top and bottom is a 24px target that still draws 16px tall,
|
|
24
|
+
* which is the whole point — WCAG 2.2 SC 2.5.8 without a redesign.
|
|
25
|
+
*
|
|
26
|
+
* The slop can overlap a sibling's box. Hit testing is front-to-back over
|
|
27
|
+
* paint order, so the sibling on top keeps its own pixels either way.
|
|
28
|
+
*/
|
|
29
|
+
containsPointWithSlop(x, y) {
|
|
30
|
+
if (this.containsPoint(x, y)) return true;
|
|
31
|
+
const slop = resolveHitSlop(this.style.hitSlop);
|
|
32
|
+
if (!slop) return false;
|
|
33
|
+
return (
|
|
34
|
+
x >= this.abs.x - slop.left &&
|
|
35
|
+
y >= this.abs.y - slop.top &&
|
|
36
|
+
x < this.abs.x + this.abs.width + slop.right &&
|
|
37
|
+
y < this.abs.y + this.abs.height + slop.bottom
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** DOM-ish rect accessor. React DevTools' Highlighter requires host
|
|
42
|
+
* instances to expose getClientRects() with a non-empty rect before it
|
|
43
|
+
* emits showNativeHighlight — without this, hovering the tree silently
|
|
44
|
+
* no-ops. Anything with getClientRects is also measured at mount via
|
|
45
|
+
* `instance.ownerDocument.documentElement` (see ownerDocument below). */
|
|
46
|
+
getClientRects() {
|
|
47
|
+
const r = this.abs;
|
|
48
|
+
if (!(r.width > 0 || r.height > 0)) return [];
|
|
49
|
+
// `abs` is device pixels; this is public API, which is logical — the
|
|
50
|
+
// same conversion events make on the way out (src/scale.js)
|
|
51
|
+
const s = this.scale;
|
|
52
|
+
return [
|
|
53
|
+
{
|
|
54
|
+
x: r.x / s,
|
|
55
|
+
y: r.y / s,
|
|
56
|
+
left: r.x / s,
|
|
57
|
+
top: r.y / s,
|
|
58
|
+
width: r.width / s,
|
|
59
|
+
height: r.height / s,
|
|
60
|
+
right: (r.x + r.width) / s,
|
|
61
|
+
bottom: (r.y + r.height) / s,
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* React Native's measure contract, which is what DevTools' style editor
|
|
68
|
+
* calls to draw the box model beside the style it is editing:
|
|
69
|
+
* `(x, y, width, height, left, top)` — position within the parent, size,
|
|
70
|
+
* then position within the window. A node with no laid-out rect calls
|
|
71
|
+
* back with nothing, the "unmeasurable" answer the editor checks for.
|
|
72
|
+
*/
|
|
73
|
+
measure(callback) {
|
|
74
|
+
if (typeof callback !== 'function') return;
|
|
75
|
+
const r = this.abs;
|
|
76
|
+
if (!(r.width > 0 || r.height > 0)) {
|
|
77
|
+
callback();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const s = this.scale;
|
|
81
|
+
const parent = this.parent?.abs;
|
|
82
|
+
callback(
|
|
83
|
+
(parent ? r.x - parent.x : r.x) / s,
|
|
84
|
+
(parent ? r.y - parent.y : r.y) / s,
|
|
85
|
+
r.width / s,
|
|
86
|
+
r.height / s,
|
|
87
|
+
r.x / s,
|
|
88
|
+
r.y / s,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The rect a hit anywhere in this subtree must fall inside: this node's
|
|
94
|
+
* rect grown by its own hitSlop, unioned with every child's reach —
|
|
95
|
+
* except under a clipping node, whose children can only be hit while the
|
|
96
|
+
* point is inside its rect (hitTest never descends from outside one).
|
|
97
|
+
*
|
|
98
|
+
* A conservative superset, and only ever that: hidden and
|
|
99
|
+
* pointerEvents-none subtrees are counted anyway, and nothing shrinks a
|
|
100
|
+
* cached bound before the next invalidation. A bound too big costs a
|
|
101
|
+
* walk; one too small would drop real input. Every change that can
|
|
102
|
+
* *grow* the true reach funnels through `_clearHitBounds`: `_assignAbs`
|
|
103
|
+
* for whatever layout moves (mounts, reveals and scrolls all change
|
|
104
|
+
* `abs`), `_retarget` for `hitSlop` and `overflow`, `_childListChanged`
|
|
105
|
+
* for a subtree attached with its rect already laid out, and `flush`
|
|
106
|
+
* for the root's own rect, which is written without `_assignAbs`.
|
|
107
|
+
*/
|
|
108
|
+
_hitBounds() {
|
|
109
|
+
let b = this._hitBoundsCache;
|
|
110
|
+
if (b) return b;
|
|
111
|
+
const abs = this.abs;
|
|
112
|
+
const slop = resolveHitSlop(this.style.hitSlop);
|
|
113
|
+
let left = abs.x;
|
|
114
|
+
let top = abs.y;
|
|
115
|
+
let right = abs.x + abs.width;
|
|
116
|
+
let bottom = abs.y + abs.height;
|
|
117
|
+
if (slop) {
|
|
118
|
+
left -= slop.left;
|
|
119
|
+
top -= slop.top;
|
|
120
|
+
right += slop.right;
|
|
121
|
+
bottom += slop.bottom;
|
|
122
|
+
}
|
|
123
|
+
if (!this.clipsChildren()) {
|
|
124
|
+
for (const child of this.children) {
|
|
125
|
+
if (child.isWindow || !child.yoga) continue;
|
|
126
|
+
const cb = child._hitBounds();
|
|
127
|
+
if (cb.left < left) left = cb.left;
|
|
128
|
+
if (cb.top < top) top = cb.top;
|
|
129
|
+
if (cb.right > right) right = cb.right;
|
|
130
|
+
if (cb.bottom > bottom) bottom = cb.bottom;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
b = { left, top, right, bottom };
|
|
134
|
+
this._hitBoundsCache = b;
|
|
135
|
+
return b;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* This node's reach changed: drop the cached bounds here and up the
|
|
140
|
+
* chain of ancestors whose unions embed them. The walk stops at a
|
|
141
|
+
* clipping ancestor — its reach is its own rect, so nothing below it
|
|
142
|
+
* changes what it or anything above it reports — or at one already
|
|
143
|
+
* invalid, whose own clear walked the rest of the way up.
|
|
144
|
+
*/
|
|
145
|
+
_clearHitBounds() {
|
|
146
|
+
// what moves a hit reach moves the paint reach — first, because the
|
|
147
|
+
// walk below returns from wherever it finds an ancestor already clear
|
|
148
|
+
this._clearPaintBounds();
|
|
149
|
+
this._hitBoundsCache = null;
|
|
150
|
+
for (let n = this.parent; n; n = n.parent) {
|
|
151
|
+
if (n.clipsChildren() || n._hitBoundsCache === null) return;
|
|
152
|
+
n._hitBoundsCache = null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Front-to-back hit test. Returns the deepest hit node or null. */
|
|
157
|
+
hitTest(x, y) {
|
|
158
|
+
if (
|
|
159
|
+
this.hidden ||
|
|
160
|
+
this.style.display === 'none' ||
|
|
161
|
+
this.style.pointerEvents === 'none'
|
|
162
|
+
) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
// nothing in this subtree reaches the point: skip it whole, children
|
|
166
|
+
// and all — this is what keeps a motion event from walking every node
|
|
167
|
+
// in the window (issue #188)
|
|
168
|
+
const b = this._hitBounds();
|
|
169
|
+
if (x < b.left || y < b.top || x >= b.right || y >= b.bottom) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const inside = this.containsPoint(x, y);
|
|
173
|
+
// the children are culled on the strict rect — slop grows this node's
|
|
174
|
+
// target, not the region its clip lets through
|
|
175
|
+
if (!inside && this.clipsChildren()) {
|
|
176
|
+
return this.containsPointWithSlop(x, y) ? this : null;
|
|
177
|
+
}
|
|
178
|
+
const order = this.paintOrder();
|
|
179
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
180
|
+
const hit = order[i].hitTest(x, y);
|
|
181
|
+
if (hit) return hit;
|
|
182
|
+
}
|
|
183
|
+
return inside || this.containsPointWithSlop(x, y) ? this : null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// <image>: an image source, loaded or decoded, sized from its intrinsic size
|
|
2
|
+
// and drawn into the node's box.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DrawableSource,
|
|
6
|
+
PictureSource,
|
|
7
|
+
acquireImageSource,
|
|
8
|
+
decodeImageSource,
|
|
9
|
+
imageSourceChanged,
|
|
10
|
+
isDirectImageSource,
|
|
11
|
+
isPathImageSource,
|
|
12
|
+
isRawImageSource,
|
|
13
|
+
releaseImageSource,
|
|
14
|
+
toLoadablePath,
|
|
15
|
+
validateImageProps,
|
|
16
|
+
} from '../imagesource.js';
|
|
17
|
+
// Namespace import for `Surface`, the same shape and the same reason as
|
|
18
|
+
// `paintcache.js`: a named import of something an older ntk does not export
|
|
19
|
+
// is a *load-time* SyntaxError, which would take the renderer down rather
|
|
20
|
+
// than the one feature that needs it.
|
|
21
|
+
import * as ntk from 'ntk';
|
|
22
|
+
import { intrinsicSize } from './layout.js';
|
|
23
|
+
import { Node } from './node.js';
|
|
24
|
+
import { DEV } from './util.js';
|
|
25
|
+
|
|
26
|
+
/** The props `ImageNode.applyProps` diffs by value itself, kept out of the
|
|
27
|
+
* base class's identity walk — a buffer rebuilt under an unchanged
|
|
28
|
+
* `cacheKey` and a `{ id, … }` descriptor rebuilt with the same numbers are
|
|
29
|
+
* both "nothing changed". */
|
|
30
|
+
const IMAGE_SOURCE_PROPS = new Set(['src', 'picture', 'drawable', 'cacheKey']);
|
|
31
|
+
|
|
32
|
+
export class ImageNode extends Node {
|
|
33
|
+
constructor(props, app) {
|
|
34
|
+
super('image', props, app);
|
|
35
|
+
validateImageProps(props);
|
|
36
|
+
this.image = null;
|
|
37
|
+
this._loadToken = 0;
|
|
38
|
+
/** hold on the `cacheKey` cache entry, when one is held */
|
|
39
|
+
this._hold = null;
|
|
40
|
+
/** an Image decoded for this node alone (no cacheKey), freed on release */
|
|
41
|
+
this._ownedImage = null;
|
|
42
|
+
/** PictureSource/DrawableSource, when the source is server-side */
|
|
43
|
+
this._serverSource = null;
|
|
44
|
+
// Resolution waits for the first layout/paint: the constructor runs in
|
|
45
|
+
// the render phase, which React may discard, and resolving here would
|
|
46
|
+
// start file reads and take cache holds nothing would ever release.
|
|
47
|
+
this._sourceDirty = true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get selfDamagedProps() {
|
|
51
|
+
return IMAGE_SOURCE_PROPS;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
paintChanged(next, prev) {
|
|
55
|
+
return imageSourceChanged(next, prev) || super.paintChanged(next, prev);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The source's size, kept to its aspect ratio. Read per measure rather
|
|
59
|
+
* than once, because a decode can arrive late.
|
|
60
|
+
*
|
|
61
|
+
* Source pixels are *logical* pixels, the browser's rule: a 100px-wide
|
|
62
|
+
* PNG occupies 100 logical px at any display scale (upscaled onto the
|
|
63
|
+
* device grid at 2x, the way an `<img>` without `srcset` is), rather
|
|
64
|
+
* than shrinking to half its neighbours' size. The constraints are
|
|
65
|
+
* already device, so only the natural size converts (src/scale.js). */
|
|
66
|
+
measureContent(constraints) {
|
|
67
|
+
this._ensureSource();
|
|
68
|
+
const s = this.scale;
|
|
69
|
+
return intrinsicSize(
|
|
70
|
+
{
|
|
71
|
+
width: (this.image?.width ?? 0) * s,
|
|
72
|
+
height: (this.image?.height ?? 0) * s,
|
|
73
|
+
},
|
|
74
|
+
constraints,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** First layout or paint after a mount or a source change — both run
|
|
79
|
+
* after commit, so an instance a concurrent render threw away never
|
|
80
|
+
* resolves anything. */
|
|
81
|
+
_ensureSource() {
|
|
82
|
+
if (!this._sourceDirty || this.destroyed) return;
|
|
83
|
+
this._sourceDirty = false;
|
|
84
|
+
this._resolveSource();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_resolveSource() {
|
|
88
|
+
const { src, picture, drawable, cacheKey } = this.props;
|
|
89
|
+
if (picture != null || drawable != null) {
|
|
90
|
+
this._serverSource =
|
|
91
|
+
picture != null
|
|
92
|
+
? new PictureSource(this.app, picture)
|
|
93
|
+
: new DrawableSource(this.app, drawable);
|
|
94
|
+
this.image = this._serverSource;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (src == null) return;
|
|
98
|
+
if (isDirectImageSource(src)) {
|
|
99
|
+
// the caller's object — its upload cache is the dedupe, and it is
|
|
100
|
+
// never destroyed here
|
|
101
|
+
this.image = src;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (cacheKey != null) {
|
|
105
|
+
const entry = acquireImageSource(this.app, cacheKey, () =>
|
|
106
|
+
this._loadEntry(src),
|
|
107
|
+
);
|
|
108
|
+
this._hold = entry;
|
|
109
|
+
if (entry.image) {
|
|
110
|
+
this.image = entry.image;
|
|
111
|
+
if (DEV) this._devCheckCacheKey(src, entry.image, cacheKey);
|
|
112
|
+
} else {
|
|
113
|
+
entry.promise?.then((image) => {
|
|
114
|
+
if (image && this._hold === entry && !this.destroyed) {
|
|
115
|
+
this._setImage(image);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (isPathImageSource(src)) {
|
|
122
|
+
this._loadFile(src);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const image = this._decode(src);
|
|
126
|
+
if (image) {
|
|
127
|
+
this._ownedImage = image;
|
|
128
|
+
this.image = image;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** `{ image }` or `{ promise }` for the cache. Failures resolve to null
|
|
133
|
+
* and are reported once, here — not per node holding the key. */
|
|
134
|
+
_loadEntry(src) {
|
|
135
|
+
if (isPathImageSource(src)) {
|
|
136
|
+
return {
|
|
137
|
+
promise: ntk.loadImage(toLoadablePath(src)).then(
|
|
138
|
+
(image) => image,
|
|
139
|
+
(err) => {
|
|
140
|
+
console.error(
|
|
141
|
+
`react-x11: failed to load image ${src}:`,
|
|
142
|
+
err.message,
|
|
143
|
+
);
|
|
144
|
+
return null;
|
|
145
|
+
},
|
|
146
|
+
),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return { image: this._decode(src) };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_decode(src) {
|
|
153
|
+
try {
|
|
154
|
+
return decodeImageSource(src);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
// corrupt or unrecognized bytes are a content failure, not a
|
|
157
|
+
// programming error: log and show nothing, like a missing file
|
|
158
|
+
console.error(
|
|
159
|
+
'react-x11: <image src> bytes did not decode:',
|
|
160
|
+
err.message,
|
|
161
|
+
);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async _loadFile(src) {
|
|
167
|
+
const token = ++this._loadToken;
|
|
168
|
+
try {
|
|
169
|
+
const image = await ntk.loadImage(toLoadablePath(src));
|
|
170
|
+
if (token !== this._loadToken || this.destroyed) return;
|
|
171
|
+
this._ownedImage = image;
|
|
172
|
+
this._setImage(image);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
if (token === this._loadToken && !this.destroyed) {
|
|
175
|
+
console.error(`react-x11: failed to load image ${src}:`, err.message);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Adopt pixels that arrived after the frame that asked for them. A size
|
|
181
|
+
* change reflows; same-size new pixels claim only this node's box. */
|
|
182
|
+
_setImage(image) {
|
|
183
|
+
const prev = this.image;
|
|
184
|
+
this.image = image;
|
|
185
|
+
if (
|
|
186
|
+
(prev?.width ?? 0) !== (image?.width ?? 0) ||
|
|
187
|
+
(prev?.height ?? 0) !== (image?.height ?? 0)
|
|
188
|
+
) {
|
|
189
|
+
this.invalidateMeasure('content');
|
|
190
|
+
} else {
|
|
191
|
+
this.root?.invalidate(false, this, 'content');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Two different pictures sharing one cacheKey is the one mistake this
|
|
196
|
+
* design can make show stale pixels; the raw form carries enough to catch
|
|
197
|
+
* the common case cheaply. */
|
|
198
|
+
_devCheckCacheKey(src, image, key) {
|
|
199
|
+
if (!isRawImageSource(src)) return;
|
|
200
|
+
if (src.width === image.width && src.height === image.height) return;
|
|
201
|
+
console.error(
|
|
202
|
+
`react-x11: <image cacheKey=${JSON.stringify(key)}> is ` +
|
|
203
|
+
`${image.width}x${image.height} in the cache, but this src says ` +
|
|
204
|
+
`${src.width}x${src.height}. Two different pictures are sharing one ` +
|
|
205
|
+
'cacheKey — the key must name the content, so include whatever ' +
|
|
206
|
+
'distinguishes them.',
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
_releaseSource() {
|
|
211
|
+
this._loadToken++; // orphan any in-flight file read
|
|
212
|
+
if (this._hold) {
|
|
213
|
+
releaseImageSource(this.app, this._hold);
|
|
214
|
+
this._hold = null;
|
|
215
|
+
}
|
|
216
|
+
if (this._ownedImage) {
|
|
217
|
+
// frees the per-app upload; the caller's own Images are never here
|
|
218
|
+
this._ownedImage.destroy();
|
|
219
|
+
this._ownedImage = null;
|
|
220
|
+
}
|
|
221
|
+
if (this._serverSource) {
|
|
222
|
+
this._serverSource.destroy?.();
|
|
223
|
+
this._serverSource = null;
|
|
224
|
+
}
|
|
225
|
+
this.image = null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
applyProps(newProps, oldProps) {
|
|
229
|
+
const before = oldProps ?? this.props;
|
|
230
|
+
const sourceChanged = imageSourceChanged(newProps, before);
|
|
231
|
+
// before super touches anything, so a bad update leaves the node whole
|
|
232
|
+
if (sourceChanged) validateImageProps(newProps);
|
|
233
|
+
super.applyProps(newProps, oldProps);
|
|
234
|
+
if (!sourceChanged) return;
|
|
235
|
+
const prev = this.image;
|
|
236
|
+
this._releaseSource();
|
|
237
|
+
this._sourceDirty = false;
|
|
238
|
+
this._resolveSource();
|
|
239
|
+
// paintChanged already claimed this node's box through super; only a
|
|
240
|
+
// new intrinsic size needs more than that
|
|
241
|
+
if (
|
|
242
|
+
(prev?.width ?? 0) !== (this.image?.width ?? 0) ||
|
|
243
|
+
(prev?.height ?? 0) !== (this.image?.height ?? 0)
|
|
244
|
+
) {
|
|
245
|
+
this.invalidateMeasure('content');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
destroySubtree() {
|
|
250
|
+
this._releaseSource();
|
|
251
|
+
super.destroySubtree();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
paintContent(ctx) {
|
|
255
|
+
this._ensureSource();
|
|
256
|
+
if (!this.image) return;
|
|
257
|
+
const content = this.contentBox();
|
|
258
|
+
ctx.drawImage(
|
|
259
|
+
this.image,
|
|
260
|
+
content.x,
|
|
261
|
+
content.y,
|
|
262
|
+
content.width,
|
|
263
|
+
content.height,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Puts a concern's methods onto the class they belong to.
|
|
2
|
+
//
|
|
3
|
+
// The node classes are split by concern rather than by class: the scroll
|
|
4
|
+
// blit's methods live in scrollblit.js whether they are Node's or
|
|
5
|
+
// WindowNode's, the paint walk's in paint.js, and so on. Each concern writes
|
|
6
|
+
// its methods as the body of a class that is never instantiated — a part —
|
|
7
|
+
// and the class's own file installs its parts right after declaring it:
|
|
8
|
+
//
|
|
9
|
+
// installMethods(Node, NodeStyling, NodeCascade, …);
|
|
10
|
+
//
|
|
11
|
+
// What the class ends up with is what one class body would have given it: the
|
|
12
|
+
// same function objects, as non-enumerable properties of the same prototype.
|
|
13
|
+
// Anything that compares against a prototype (`node.paint !==
|
|
14
|
+
// Node.prototype.paint`) or overrides a method in a subclass sees no
|
|
15
|
+
// difference.
|
|
16
|
+
//
|
|
17
|
+
// Two guards keep the arrangement safe to edit. A name defined twice — in the
|
|
18
|
+
// class and a part, or in two parts — throws while the module loads, instead
|
|
19
|
+
// of whichever ran last silently winning. And each part's prototype is
|
|
20
|
+
// re-parented onto the class's parent, so `super` in a moved method means
|
|
21
|
+
// what it meant in the class body.
|
|
22
|
+
|
|
23
|
+
/** part -> the class it was installed onto */
|
|
24
|
+
const installed = new WeakMap();
|
|
25
|
+
|
|
26
|
+
/** prototype -> (key -> the part that defined it) */
|
|
27
|
+
const origins = new WeakMap();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Install each part's methods and accessors onto `target.prototype`.
|
|
31
|
+
*
|
|
32
|
+
* @param {Function} target the class
|
|
33
|
+
* @param {...Function} parts classes whose bodies hold the rest of its methods
|
|
34
|
+
*/
|
|
35
|
+
export function installMethods(target, ...parts) {
|
|
36
|
+
const proto = target.prototype;
|
|
37
|
+
const parent = Object.getPrototypeOf(proto);
|
|
38
|
+
if (!origins.has(proto)) origins.set(proto, new Map());
|
|
39
|
+
const defined = origins.get(proto);
|
|
40
|
+
for (const part of parts) {
|
|
41
|
+
if (installed.has(part)) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`react-x11: ${part.name} is already installed onto ` +
|
|
44
|
+
`${installed.get(part).name}; a part belongs to exactly one class.`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const keys = Reflect.ownKeys(part.prototype).filter(
|
|
48
|
+
(key) => key !== 'constructor',
|
|
49
|
+
);
|
|
50
|
+
for (const key of keys) {
|
|
51
|
+
if (Object.hasOwn(proto, key)) {
|
|
52
|
+
const first = defined.get(key) ?? `the ${target.name} class body`;
|
|
53
|
+
throw new Error(
|
|
54
|
+
`react-x11: ${target.name}.${String(key)} is defined twice, in ` +
|
|
55
|
+
`${first} and in ${part.name}. A method lives in exactly one ` +
|
|
56
|
+
`file under src/nodes/ — rename one of them, or delete it.`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
Object.setPrototypeOf(part.prototype, parent);
|
|
61
|
+
for (const key of keys) {
|
|
62
|
+
Object.defineProperty(
|
|
63
|
+
proto,
|
|
64
|
+
key,
|
|
65
|
+
Object.getOwnPropertyDescriptor(part.prototype, key),
|
|
66
|
+
);
|
|
67
|
+
defined.set(key, part.name);
|
|
68
|
+
}
|
|
69
|
+
installed.set(part, target);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The class `part` was installed onto, or undefined: what the test that
|
|
74
|
+
* every part under src/nodes/ is installed somewhere reads. */
|
|
75
|
+
export const installedOnto = (part) => installed.get(part);
|