react-x11 2.10.2 → 2.12.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 +10 -3
- package/src/Reconciler.js +15 -17
- package/src/a11y.js +2 -2
- package/src/anchor.js +7 -5
- package/src/bootstrap.js +14 -0
- package/src/clientmessage.js +1 -1
- package/src/cocoa/app.js +304 -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 +20 -3
- package/src/cocoa/main.d.ts +8 -0
- package/src/cocoa/main.js +43 -0
- package/src/cocoa/panehost.js +15 -5
- package/src/cocoa/presenter.js +13 -9
- package/src/cocoa/promotion.js +4 -7
- package/src/cocoa/relaunch.js +207 -0
- package/src/cocoa/screencolor.js +62 -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/errors.js +46 -0
- package/src/events.js +6 -6
- package/src/foreignnodes.js +3 -2
- package/src/frames.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1653 -0
- package/src/host.d.ts +230 -0
- package/src/host.js +11 -3
- package/src/imagesource.js +1 -1
- package/src/index.d.ts +21 -4
- package/src/index.js +9 -1
- package/src/layouts.js +721 -0
- package/src/node.d.ts +4 -2
- 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 +334 -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 +945 -0
- package/src/palette.js +1 -1
- package/src/registry.js +7 -3
- package/src/screencolor.js +212 -38
- package/src/screencolorhooks.js +6 -2
- 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 +11 -1
- package/src/types/nodes.d.ts +33 -5
- package/src/types/screencolor.d.ts +20 -14
- 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,269 @@
|
|
|
1
|
+
// <canvas>: an element that draws with a 2d context, and the putImageData
|
|
2
|
+
// path into it.
|
|
3
|
+
|
|
4
|
+
import { ownerName } from '../errors.js';
|
|
5
|
+
import { isPaintedColor } from './boxpaint.js';
|
|
6
|
+
import { Node } from './node.js';
|
|
7
|
+
import { DEV } from './util.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The one call `onDraw`'s translation cannot reach (#366).
|
|
11
|
+
*
|
|
12
|
+
* `putImageData` ignores the context's transform and clip — the HTML canvas
|
|
13
|
+
* rule, kept by ntk — so inside `onDraw`, whose whole contract is "you are at
|
|
14
|
+
* the node's origin", it is the one call whose coordinates are still the
|
|
15
|
+
* drawable's own. Nothing errors; the pixels simply land at the window
|
|
16
|
+
* origin instead of in the node. `DrawInfo.x`/`.y` is the way to say it
|
|
17
|
+
* right, and development watches for the write that says it wrong: a
|
|
18
|
+
* destination outside the node's box. Once per process — one thread to pull
|
|
19
|
+
* is enough, and `onDraw` runs on every repaint.
|
|
20
|
+
*/
|
|
21
|
+
let warnedPutImageData = false;
|
|
22
|
+
export function resetPutImageDataWarningForTests() {
|
|
23
|
+
warnedPutImageData = false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The rect `putImageData(data, x, y, …dirty)` actually writes — the spec's
|
|
27
|
+
* dirty-rect normalisation, reproduced so the watch judges the write and
|
|
28
|
+
* not the whole source image (a correct call may blit a window of a bigger
|
|
29
|
+
* atlas). Null when the write is empty. */
|
|
30
|
+
function putImageDataDest(data, x, y, dx = 0, dy = 0, dw, dh) {
|
|
31
|
+
const width = data?.width ?? 0;
|
|
32
|
+
const height = data?.height ?? 0;
|
|
33
|
+
dw ??= width;
|
|
34
|
+
dh ??= height;
|
|
35
|
+
if (dw < 0) {
|
|
36
|
+
dx += dw;
|
|
37
|
+
dw = -dw;
|
|
38
|
+
}
|
|
39
|
+
if (dh < 0) {
|
|
40
|
+
dy += dh;
|
|
41
|
+
dh = -dh;
|
|
42
|
+
}
|
|
43
|
+
const sx = Math.max(0, dx);
|
|
44
|
+
const sy = Math.max(0, dy);
|
|
45
|
+
const sw = Math.min(width, dx + dw) - sx;
|
|
46
|
+
const sh = Math.min(height, dy + dh) - sy;
|
|
47
|
+
if (!(sw > 0) || !(sh > 0)) return null;
|
|
48
|
+
return { x: x + sx, y: y + sy, width: sw, height: sh };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Escape hatch: a retained node whose content is painted by props.onDraw. */
|
|
52
|
+
export class CanvasNode extends Node {
|
|
53
|
+
constructor(props, app) {
|
|
54
|
+
super('canvas', props, app);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
applyProps(newProps, oldProps) {
|
|
58
|
+
const before = oldProps ?? this.props;
|
|
59
|
+
super.applyProps(newProps, oldProps);
|
|
60
|
+
// onDraw is read at paint time, so a new closure means new content — but
|
|
61
|
+
// it also matches /^on[A-Z]/, which is how the base class recognises an
|
|
62
|
+
// event handler, so `paintChanged` skips it and this is the only place
|
|
63
|
+
// that notices. Damage is bounded to this canvas: an unbounded call here
|
|
64
|
+
// made every re-render of a component that draws through <canvas> repaint
|
|
65
|
+
// the whole window, which is what a Checkbox's tick and a Select's chevron
|
|
66
|
+
// both do.
|
|
67
|
+
if (newProps.onDraw !== before.onDraw) {
|
|
68
|
+
this.root?.invalidate(false, this, 'props');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The ink a `mono` drawing is painted in: the node's own `color`, then what
|
|
74
|
+
* it inherits, then the palette's — exactly as `<text>` and `<svg>` resolve
|
|
75
|
+
* theirs, because it is literally the same resolution. An `<Icon>` in a
|
|
76
|
+
* row that dims itself dims with it, with nothing handed over at the call
|
|
77
|
+
* site.
|
|
78
|
+
*/
|
|
79
|
+
_monoColor() {
|
|
80
|
+
return this.resolvedTextStyle().color;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Preset the ink a `mono` drawing inherits, so `onDraw` never names a
|
|
84
|
+
* colour of its own. Called inside the `save()`/`restore()` pair. */
|
|
85
|
+
_presetMono(ctx, color) {
|
|
86
|
+
ctx.fillStyle = color;
|
|
87
|
+
ctx.strokeStyle = color;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
paintContent(ctx) {
|
|
91
|
+
const onDraw = this.props.onDraw;
|
|
92
|
+
if (typeof onDraw !== 'function') return;
|
|
93
|
+
ctx.save();
|
|
94
|
+
ctx.beginPath();
|
|
95
|
+
ctx.rect(this.abs.x, this.abs.y, this.abs.width, this.abs.height);
|
|
96
|
+
ctx.clip();
|
|
97
|
+
ctx.translate(this.abs.x, this.abs.y);
|
|
98
|
+
if (this.props.mono) this._presetMono(ctx, this._monoColor());
|
|
99
|
+
const unwatch = DEV ? this._watchPutImageData(ctx) : null;
|
|
100
|
+
try {
|
|
101
|
+
// Device pixels, the browser's own canvas contract: the backing
|
|
102
|
+
// store is the panel's grid and `scale` says how many of its pixels
|
|
103
|
+
// one logical pixel is worth. Deliberately NOT a ctx.scale() — ntk
|
|
104
|
+
// positions glyphs through the transform but sizes them from the
|
|
105
|
+
// font, so a scaled transform would move an app's fillText without
|
|
106
|
+
// growing it (src/scale.js).
|
|
107
|
+
onDraw(ctx, {
|
|
108
|
+
width: this.abs.width,
|
|
109
|
+
height: this.abs.height,
|
|
110
|
+
// the translation above, said out loud: raw-pixel calls address the
|
|
111
|
+
// drawable itself, so they are written at `info.x + x` (#366)
|
|
112
|
+
x: this.abs.x,
|
|
113
|
+
y: this.abs.y,
|
|
114
|
+
scale: this.scale,
|
|
115
|
+
node: this,
|
|
116
|
+
});
|
|
117
|
+
} finally {
|
|
118
|
+
unwatch?.();
|
|
119
|
+
ctx.restore();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Shadow `ctx.putImageData` for the length of one `onDraw`, and warn on a
|
|
125
|
+
* write whose destination escapes the node's box — either coordinates
|
|
126
|
+
* that were never offset by `info.x`/`info.y`, which is the trap the
|
|
127
|
+
* watch exists for, or a drawing genuinely reaching outside bounds every
|
|
128
|
+
* other call is clipped to. The box test has a pixel of slack per edge:
|
|
129
|
+
* `abs` can sit on a fractional grid, and a rounded `info.x` must not
|
|
130
|
+
* read as an escape. Development only, and the shadow is not installed
|
|
131
|
+
* again once the warning has fired, so steady state costs nothing.
|
|
132
|
+
*/
|
|
133
|
+
_watchPutImageData(ctx) {
|
|
134
|
+
const original = ctx.putImageData;
|
|
135
|
+
if (warnedPutImageData || typeof original !== 'function') return null;
|
|
136
|
+
const hadOwn = Object.hasOwn(ctx, 'putImageData');
|
|
137
|
+
const node = this;
|
|
138
|
+
ctx.putImageData = function (data, x, y, ...dirty) {
|
|
139
|
+
const dest = putImageDataDest(data, x, y, ...dirty);
|
|
140
|
+
const box = node.abs;
|
|
141
|
+
if (
|
|
142
|
+
!warnedPutImageData &&
|
|
143
|
+
dest &&
|
|
144
|
+
(dest.x < box.x - 1 ||
|
|
145
|
+
dest.y < box.y - 1 ||
|
|
146
|
+
dest.x + dest.width > box.x + box.width + 1 ||
|
|
147
|
+
dest.y + dest.height > box.y + box.height + 1)
|
|
148
|
+
) {
|
|
149
|
+
warnedPutImageData = true;
|
|
150
|
+
const owner = ownerName(node);
|
|
151
|
+
console.warn(
|
|
152
|
+
`react-x11: putImageData in <canvas onDraw>${owner ? ` (in ${owner})` : ''} ` +
|
|
153
|
+
`wrote ${dest.width}x${dest.height} at ${dest.x},${dest.y} — outside the ` +
|
|
154
|
+
`node, which is ${box.width}x${box.height} at ${box.x},${box.y}. ` +
|
|
155
|
+
"putImageData ignores the context's transform (the HTML canvas rule), " +
|
|
156
|
+
"so unlike every other call in onDraw its coordinates are the drawable's, " +
|
|
157
|
+
"not the node's. Add the node's origin, which onDraw is handed: " +
|
|
158
|
+
'ctx.putImageData(data, info.x + x, info.y + y). Better, draw through an ' +
|
|
159
|
+
'image source — it honours the transform and the clip, and caches its ' +
|
|
160
|
+
'upload server-side: ctx.drawImage(new Image({ width, height, data }), x, y), ' +
|
|
161
|
+
'with Image from \'react-x11/ntk\'. See docs/elements.md, "<canvas>".',
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return original.call(ctx, data, x, y, ...dirty);
|
|
165
|
+
};
|
|
166
|
+
return () => {
|
|
167
|
+
if (hadOwn) ctx.putImageData = original;
|
|
168
|
+
else delete ctx.putImageData;
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* `<canvas cacheKey>` opts a drawing into the paint cache.
|
|
174
|
+
*
|
|
175
|
+
* This one has to be opt-in, and the reason is worth stating: `onDraw` is
|
|
176
|
+
* an opaque closure. Nothing here can know what it reads — a prop, a ref, a
|
|
177
|
+
* clock, a module variable — and its identity changes on every render
|
|
178
|
+
* unless the app memoizes it, so it is not a key either. Only the author
|
|
179
|
+
* knows, so the author says:
|
|
180
|
+
*
|
|
181
|
+
* <canvas cacheKey={`spark:${series.id}:${w}x${h}`} onDraw={draw} />
|
|
182
|
+
*
|
|
183
|
+
* The rule is the protocol's rule: the key must name every input the
|
|
184
|
+
* drawing reads. A `cacheKey` that leaves one out shows stale pixels, so
|
|
185
|
+
* develop with `REACT_X11_PAINT_CACHE=verify`, which turns exactly that
|
|
186
|
+
* mistake into a loud complaint.
|
|
187
|
+
*
|
|
188
|
+
* `<canvas>` needs no `paintCached` of its own beyond the mono preset: it
|
|
189
|
+
* already draws origin-relative, so the cached render and the live one are
|
|
190
|
+
* the same code.
|
|
191
|
+
*
|
|
192
|
+
* ## `mono`: coverage, and the colour out of the key
|
|
193
|
+
*
|
|
194
|
+
* `<canvas mono>` is a promise about the drawing — *everything I paint is
|
|
195
|
+
* one colour, and it is not mine to choose*. `onDraw` then names no colour
|
|
196
|
+
* at all: `fillStyle` and `strokeStyle` arrive preset from `style.color`.
|
|
197
|
+
*
|
|
198
|
+
* That promise is what lets the entry be an **a8 coverage** surface with
|
|
199
|
+
* the colour applied at blit time, so the colour leaves the key: one
|
|
200
|
+
* rendered copy of a chevron serves the resting row, the highlighted row,
|
|
201
|
+
* the disabled one and both schemes. Without it each colour is a separate
|
|
202
|
+
* argb32 entry, which for an icon in four states is four rasterizations
|
|
203
|
+
* and four pixmaps of the same shape. `SvgView.paintKind` decides the same
|
|
204
|
+
* thing by scanning the document; a closure cannot be scanned, so here the
|
|
205
|
+
* author says it.
|
|
206
|
+
*
|
|
207
|
+
* A drawing that sets its own `fillStyle` under `mono` is a bug the digest
|
|
208
|
+
* catches: colour is out of the key, so two colours of one drawing collide
|
|
209
|
+
* on one entry and `REACT_X11_PAINT_CACHE=verify` complains.
|
|
210
|
+
*
|
|
211
|
+
* Needs **ntk ≥ 7.3.3**, and the floor is not cosmetic. Coverage
|
|
212
|
+
* composites through ntk's `_drawCoverage`, which routes a clip it cannot
|
|
213
|
+
* express as a rectangle through a scratch mask — and before 7.3.3 that
|
|
214
|
+
* path read the surface-sized mask from the origin rather than from the
|
|
215
|
+
* destination, so anything not drawn at (0, 0) was masked out entirely
|
|
216
|
+
* (sidorares/ntk#243). Nested rounded clips are the common case, not an
|
|
217
|
+
* exotic one: `examples/tasks.jsx` puts a checkbox tick under a rounded
|
|
218
|
+
* card, a scrolled list, a rounded row and a rounded well, and five of its
|
|
219
|
+
* six ticks came out blank. `package.json` carries the floor.
|
|
220
|
+
*/
|
|
221
|
+
paintCachePlan() {
|
|
222
|
+
const { cacheKey, onDraw, mono } = this.props;
|
|
223
|
+
if (cacheKey == null || typeof onDraw !== 'function') return null;
|
|
224
|
+
const width = Math.ceil(this.abs.width);
|
|
225
|
+
const height = Math.ceil(this.abs.height);
|
|
226
|
+
if (width <= 0 || height <= 0) return null;
|
|
227
|
+
const tint = mono ? this._monoColor() : null;
|
|
228
|
+
// Nothing to composite through: an unpainted colour would blit the
|
|
229
|
+
// coverage as-is, which is not what "invisible" looks like.
|
|
230
|
+
if (mono && !isPaintedColor(tint)) return null;
|
|
231
|
+
return {
|
|
232
|
+
key: `canvas|${width}x${height}@1|${mono ? 'mono|' : ''}${cacheKey}`,
|
|
233
|
+
x: Math.round(this.abs.x),
|
|
234
|
+
y: Math.round(this.abs.y),
|
|
235
|
+
width,
|
|
236
|
+
height,
|
|
237
|
+
format: mono ? 'a8' : 'argb32',
|
|
238
|
+
tint,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
paintCached(ctx, box, ink = '#ffffff') {
|
|
243
|
+
const onDraw = this.props.onDraw;
|
|
244
|
+
if (typeof onDraw !== 'function') return;
|
|
245
|
+
ctx.save();
|
|
246
|
+
ctx.beginPath();
|
|
247
|
+
ctx.rect(box.x, box.y, box.width, box.height);
|
|
248
|
+
ctx.clip();
|
|
249
|
+
ctx.translate(box.x, box.y);
|
|
250
|
+
// Into a coverage surface only the alpha of a paint survives and the
|
|
251
|
+
// tint arrives at blit time, so any opaque colour renders the same mask
|
|
252
|
+
// — and the cache says white then, or the tint where it bakes colour.
|
|
253
|
+
if (this.props.mono) this._presetMono(ctx, ink);
|
|
254
|
+
try {
|
|
255
|
+
onDraw(ctx, {
|
|
256
|
+
width: box.width,
|
|
257
|
+
height: box.height,
|
|
258
|
+
// the drawing goes into a surface of its own here, so the node's
|
|
259
|
+
// origin in it *is* the origin — and a raw-pixel write offset by
|
|
260
|
+
// `info.x`/`info.y` for the live path stays correct under a cacheKey
|
|
261
|
+
x: box.x,
|
|
262
|
+
y: box.y,
|
|
263
|
+
node: this,
|
|
264
|
+
});
|
|
265
|
+
} finally {
|
|
266
|
+
ctx.restore();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|