react-x11 2.18.5 → 2.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/Reconciler.js +9 -2
- package/src/backend/context2d.js +39 -2
- package/src/cocoa/app.js +1 -1
- package/src/cocoa/fonts.js +2 -1
- package/src/cocoa/presenter.js +1 -1
- package/src/decorations.js +1 -1
- package/src/fonts.js +1 -1
- package/src/foreignnodes.js +6 -1
- package/src/glbackend.js +1 -1
- package/src/glnodes.js +1 -1
- package/src/gloverlay.js +1 -1
- package/src/imagesource.js +4 -1
- package/src/node.d.ts +12 -0
- package/src/nodes/animation.js +1 -0
- package/src/nodes/boxpaint.js +5 -3
- package/src/nodes/image.js +3 -3
- package/src/nodes/invalidate.js +26 -0
- package/src/nodes/layout.js +4 -4
- package/src/nodes/layouthost.js +2 -0
- package/src/nodes/paint.js +1 -1
- package/src/nodes/queries.js +6 -1
- package/src/nodes/scrollable.js +5 -5
- package/src/nodes/text.js +1 -1
- package/src/nodes/window/capabilities.js +1 -1
- package/src/nodes/window/floors.js +90 -6
- package/src/nodes/window/flush.js +7 -15
- package/src/nodes/window/size.js +214 -10
- package/src/nodes/window/window.js +6 -0
- package/src/ntk.js +8 -31
- package/src/ntkroot.js +50 -0
- package/src/offscreen.js +33 -0
- package/src/paintcache.js +26 -15
- package/src/refresh/loader.js +23 -4
- package/src/screencolor.js +1 -1
- package/src/styles.js +1 -1
- package/src/svgnodes.js +1 -1
- package/src/symbols.js +4 -2
- package/src/trayhooks.js +1 -1
- package/src/wayland/context2d.js +2 -1
- package/src/wayland/decorations.js +1 -1
- package/src/win32/fonts.js +1 -1
|
@@ -6,7 +6,7 @@ import { resolveFramePolicy } from '../../pacing.js';
|
|
|
6
6
|
import { paintCacheFor } from '../../paintcache.js';
|
|
7
7
|
import { hooks as traceHooks } from '../../trace-registry.js';
|
|
8
8
|
import { now } from '../animation.js';
|
|
9
|
-
import { FULL_DAMAGE, layoutDiff
|
|
9
|
+
import { FULL_DAMAGE, layoutDiff } from '../damage.js';
|
|
10
10
|
import { BLIT_POISONED } from '../scrollblit.js';
|
|
11
11
|
import { debugPaint } from './debugpaint.js';
|
|
12
12
|
|
|
@@ -204,12 +204,10 @@ export class WindowFlush {
|
|
|
204
204
|
(panes && this._paneDamage !== FULL_DAMAGE)
|
|
205
205
|
) {
|
|
206
206
|
const cap = this._damageRectCap();
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
if (panes) this._addPaneDamage(rect);
|
|
207
|
+
// each to the lists that paint the node that moved
|
|
208
|
+
// (`_claimLayoutMove`): a move inside a `<glarea>` is its panes'
|
|
209
|
+
layoutDiff.sink = (rect, node) => {
|
|
210
|
+
if (this._claimLayoutMove(rect, node, cap)) layoutMoved = true;
|
|
213
211
|
};
|
|
214
212
|
}
|
|
215
213
|
try {
|
|
@@ -259,14 +257,8 @@ export class WindowFlush {
|
|
|
259
257
|
if (sv && !sv._recordBlitClaim(after)) {
|
|
260
258
|
sv._pendingBlitFrom = BLIT_POISONED;
|
|
261
259
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
this._damage,
|
|
265
|
-
after,
|
|
266
|
-
this._damageRectCap(),
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
if (panes) this._addPaneDamage(after);
|
|
260
|
+
// to the lists that paint it, as the layout diff's claims go
|
|
261
|
+
this._claimLayoutMove(after, node, this._damageRectCap());
|
|
270
262
|
}
|
|
271
263
|
this._reflowed.clear();
|
|
272
264
|
} else if (this._reflowed.size) {
|
package/src/nodes/window/size.js
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import { measuringExactly } from '../../styles.js';
|
|
6
6
|
import { availableArea } from '../../screens.js';
|
|
7
|
+
import { Yoga } from '../../yoga.js';
|
|
7
8
|
import {
|
|
8
9
|
captureLeafHeights,
|
|
9
10
|
collectFloorStale,
|
|
11
|
+
dirtyOutside,
|
|
12
|
+
floorBoundaryOf,
|
|
10
13
|
probeHeightFloors,
|
|
11
14
|
setMeasuringShrink,
|
|
12
15
|
restoreShrink,
|
|
@@ -27,6 +30,26 @@ import {
|
|
|
27
30
|
scaleWindowGeometry,
|
|
28
31
|
} from './hints.js';
|
|
29
32
|
|
|
33
|
+
// Escape hatch, read once like the others: for measuring the scoped floors
|
|
34
|
+
// against the whole-tree ones on the same build, and as first aid if a
|
|
35
|
+
// change inside a card is ever measured wrong. `=== '1'` for the reason
|
|
36
|
+
// REACT_X11_NO_SCROLL_BLIT gives.
|
|
37
|
+
const NO_SCOPED_FLOORS = process.env.REACT_X11_NO_SCOPED_FLOORS === '1';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Mark a box laid out alone (`_measureBoundary`) for the window's pass to lay
|
|
41
|
+
* out again. It was laid out at the size that pass will give it, so yoga
|
|
42
|
+
* would answer from its cache and keep what the measurement left inside —
|
|
43
|
+
* placed off the pixel grid, with the shrink borrowed. A style set to
|
|
44
|
+
* something else and back is how a box with no measure function says it
|
|
45
|
+
* changed.
|
|
46
|
+
*/
|
|
47
|
+
function relayOut(node) {
|
|
48
|
+
const shrink = node.style.flexShrink ?? 1;
|
|
49
|
+
node.yoga.setFlexShrink(shrink + 1);
|
|
50
|
+
node.yoga.setFlexShrink(shrink);
|
|
51
|
+
}
|
|
52
|
+
|
|
30
53
|
/** Content-sized windows, installed onto `WindowNode.prototype` by window.js. */
|
|
31
54
|
export class WindowSize {
|
|
32
55
|
/**
|
|
@@ -102,6 +125,21 @@ export class WindowSize {
|
|
|
102
125
|
this._sweepLayoutHosts();
|
|
103
126
|
const found = { width: false, height: false };
|
|
104
127
|
this._floorsStale.clear();
|
|
128
|
+
// Decided afresh on every call rather than once a frame: something
|
|
129
|
+
// between the two calls a frame makes — a size query resolving — can
|
|
130
|
+
// turn a scoped change into one that is not, and the wider collection
|
|
131
|
+
// is the one that is always right.
|
|
132
|
+
const scope = this._floorsScope();
|
|
133
|
+
this._floorsScopeNow = scope;
|
|
134
|
+
if (scope !== null) {
|
|
135
|
+
for (const boundary of scope) {
|
|
136
|
+
// its children's floors are written from here; its own extent is
|
|
137
|
+
// its style's, and stands
|
|
138
|
+
this._floorsStale.add(boundary);
|
|
139
|
+
collectFloorStale(boundary, this._floorsStale, found, false);
|
|
140
|
+
}
|
|
141
|
+
return found;
|
|
142
|
+
}
|
|
105
143
|
// the root's own children are written from here too, and its direction
|
|
106
144
|
// can move like any node's
|
|
107
145
|
this._floorsStale.add(this);
|
|
@@ -109,6 +147,123 @@ export class WindowSize {
|
|
|
109
147
|
return found;
|
|
110
148
|
}
|
|
111
149
|
|
|
150
|
+
/**
|
|
151
|
+
* The boxes the changes since the last measurement are confined to, or
|
|
152
|
+
* null when a measurement has to look at the whole tree.
|
|
153
|
+
*
|
|
154
|
+
* Every change to a box tree announces itself through `invalidate` with
|
|
155
|
+
* the node it changed (`_floorsSources`), and every node above it is
|
|
156
|
+
* dirty in yoga for it — which is all `collectFloorStale` can see, so a
|
|
157
|
+
* text that changed inside a card took every extent from the card up to
|
|
158
|
+
* the window with it, and the measurement that followed laid the whole
|
|
159
|
+
* tree out three times to read them back. A box that sizes itself
|
|
160
|
+
* (`isFloorBoundary`) holds the change in: its extent is its style's, the
|
|
161
|
+
* extents above it are what they were, and what is inside it can be
|
|
162
|
+
* measured by laying it out alone.
|
|
163
|
+
*
|
|
164
|
+
* Scoped only when all of that is known to hold: every change has such a
|
|
165
|
+
* box around it, yoga has nothing dirty outside them and the way down to
|
|
166
|
+
* them (a change nobody named), the tree has been measured once already,
|
|
167
|
+
* the window is the width it was measured at and sizes itself from no
|
|
168
|
+
* content, and no layout host or container query is in play. Anything
|
|
169
|
+
* else is the whole tree, as it always was.
|
|
170
|
+
*/
|
|
171
|
+
_floorsScope() {
|
|
172
|
+
const sources = this._floorsSources;
|
|
173
|
+
if (
|
|
174
|
+
NO_SCOPED_FLOORS ||
|
|
175
|
+
this._floorsUnscoped ||
|
|
176
|
+
sources.size === 0 ||
|
|
177
|
+
!this._floorsSwept ||
|
|
178
|
+
this._layoutHosts.size !== 0 ||
|
|
179
|
+
this._containerQueryNodes.size !== 0 ||
|
|
180
|
+
this._floorsWidth !== (this.window?.width ?? this._requestedSize?.width)
|
|
181
|
+
) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const props = this.props;
|
|
185
|
+
if (
|
|
186
|
+
isAutoSize(props.width) ||
|
|
187
|
+
isAutoSize(props.height) ||
|
|
188
|
+
CONTENT_BOUND_PROPS.some((key) => isContentBound(props[key]))
|
|
189
|
+
) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const boundaries = new Set();
|
|
193
|
+
for (const source of sources) {
|
|
194
|
+
// gone, or moved to another window: its old parent's child list is
|
|
195
|
+
// the change that counts, and that parent said so itself
|
|
196
|
+
if (source.destroyed || source.root !== this) continue;
|
|
197
|
+
const boundary = floorBoundaryOf(source, this);
|
|
198
|
+
if (boundary === null) return null;
|
|
199
|
+
boundaries.add(boundary);
|
|
200
|
+
}
|
|
201
|
+
if (boundaries.size === 0) return null;
|
|
202
|
+
// the outermost of nested ones: measuring it measures what it holds
|
|
203
|
+
const paths = new Set();
|
|
204
|
+
for (const boundary of [...boundaries]) {
|
|
205
|
+
for (let n = boundary.parent; n && n !== this; n = n.parent) {
|
|
206
|
+
if (boundaries.has(n)) {
|
|
207
|
+
boundaries.delete(boundary);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const boundary of boundaries) {
|
|
213
|
+
for (let n = boundary.parent; n && n !== this; n = n.parent) {
|
|
214
|
+
paths.add(n);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (dirtyOutside(this, boundaries, paths)) return null;
|
|
218
|
+
return boundaries;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* One boundary's floors, measured with it laid out alone — the passes
|
|
223
|
+
* `_measureContentSpans` runs over the whole tree, run over the box: its
|
|
224
|
+
* size is its style's in either, so the space everything inside it is
|
|
225
|
+
* laid out in is the same. `probe` is the height measurement's check for
|
|
226
|
+
* leaves whose width moved, stopped at the box, whose own extent stands.
|
|
227
|
+
*/
|
|
228
|
+
_measureBoundary(boundary, axis, probe = false) {
|
|
229
|
+
const yoga = boundary.yoga;
|
|
230
|
+
const dir =
|
|
231
|
+
boundary.direction === 'rtl' ? Yoga.DIRECTION_RTL : Yoga.DIRECTION_LTR;
|
|
232
|
+
if (axis === 'width') {
|
|
233
|
+
const shrunk = [];
|
|
234
|
+
setMeasuringShrink(boundary, axis, shrunk);
|
|
235
|
+
this._scopedFloorPasses += 1;
|
|
236
|
+
yoga.calculateLayout(undefined, undefined, dir);
|
|
237
|
+
contentSpan(boundary, axis, null, this);
|
|
238
|
+
restoreShrink(shrunk);
|
|
239
|
+
relayOut(boundary);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
this._scopedFloorPasses += 1;
|
|
243
|
+
yoga.calculateLayout(undefined, undefined, dir);
|
|
244
|
+
if (probe) {
|
|
245
|
+
const hit = { marked: false, owed: false };
|
|
246
|
+
probeHeightFloors(boundary, this, hit, boundary);
|
|
247
|
+
if (hit.marked) {
|
|
248
|
+
this._writeFloors('height');
|
|
249
|
+
this._scopedFloorPasses += 1;
|
|
250
|
+
yoga.calculateLayout(undefined, undefined, dir);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const intrinsic = new Map();
|
|
254
|
+
captureLeafHeights(boundary, intrinsic);
|
|
255
|
+
const frozen = [];
|
|
256
|
+
freezeWidths(boundary, frozen);
|
|
257
|
+
const shrunk = [];
|
|
258
|
+
setMeasuringShrink(boundary, axis, shrunk);
|
|
259
|
+
this._scopedFloorPasses += 1;
|
|
260
|
+
yoga.calculateLayout(undefined, 0, dir);
|
|
261
|
+
contentSpan(boundary, axis, intrinsic, this);
|
|
262
|
+
restoreWidths(frozen);
|
|
263
|
+
restoreShrink(shrunk);
|
|
264
|
+
relayOut(boundary);
|
|
265
|
+
}
|
|
266
|
+
|
|
112
267
|
/** The floors on the children of every node found stale, from the extents
|
|
113
268
|
* they carry — `writeFloors` for each. */
|
|
114
269
|
_writeFloors(axis) {
|
|
@@ -203,28 +358,77 @@ export class WindowSize {
|
|
|
203
358
|
*/
|
|
204
359
|
_applyContentFloors(width, height) {
|
|
205
360
|
const found = this._collectFloorStale();
|
|
361
|
+
const scope = this._floorsScopeNow;
|
|
362
|
+
if (scope !== null) {
|
|
363
|
+
this._applyScopedFloors(scope, found, width, height);
|
|
364
|
+
} else {
|
|
365
|
+
measuringExactly(() => {
|
|
366
|
+
if (found.width) this._measureWidthFloors();
|
|
367
|
+
else this._writeFloors('width');
|
|
368
|
+
// from the extents on hand; a stale one writes the style's minimum,
|
|
369
|
+
// which is the floor coming off ahead of its measurement below
|
|
370
|
+
this._writeFloors('height');
|
|
371
|
+
});
|
|
372
|
+
let heights = found.height;
|
|
373
|
+
let probed = false;
|
|
374
|
+
if (!heights) {
|
|
375
|
+
this._layoutRoot(width, height);
|
|
376
|
+
heights = this._probeHeightFloors().owed;
|
|
377
|
+
probed = true;
|
|
378
|
+
}
|
|
379
|
+
if (heights) {
|
|
380
|
+
measuringExactly(() => this._measureHeightFloors(width, !probed));
|
|
381
|
+
this._layoutRoot(width, height);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
this._floorsDirty = false;
|
|
385
|
+
this._floorsContentDirty = false;
|
|
386
|
+
this._floorsSwept = true;
|
|
387
|
+
this._floorsWidth = width;
|
|
388
|
+
this._floorsSources.clear();
|
|
389
|
+
this._floorsUnscoped = false;
|
|
390
|
+
this._floorsScopeNow = null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* `_applyContentFloors` for changes confined to boxes that size
|
|
395
|
+
* themselves (`_floorsScope`): the same measurements in the same order,
|
|
396
|
+
* each over one box laid out alone rather than over the window, and the
|
|
397
|
+
* window's one real pass after them — which yoga answers from its cache
|
|
398
|
+
* everywhere but the boxes and the way down to them.
|
|
399
|
+
*/
|
|
400
|
+
_applyScopedFloors(boundaries, found, width, height) {
|
|
206
401
|
measuringExactly(() => {
|
|
207
|
-
if (found.width)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
402
|
+
if (found.width) {
|
|
403
|
+
this._writeFloors('width');
|
|
404
|
+
for (const boundary of boundaries) {
|
|
405
|
+
this._measureBoundary(boundary, 'width');
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
this._writeFloors('width');
|
|
211
409
|
this._writeFloors('height');
|
|
212
410
|
});
|
|
213
411
|
let heights = found.height;
|
|
214
412
|
let probed = false;
|
|
215
413
|
if (!heights) {
|
|
216
414
|
this._layoutRoot(width, height);
|
|
217
|
-
|
|
415
|
+
const hit = { marked: false, owed: false };
|
|
416
|
+
for (const boundary of boundaries) {
|
|
417
|
+
probeHeightFloors(boundary, this, hit, boundary);
|
|
418
|
+
}
|
|
419
|
+
heights = hit.owed;
|
|
218
420
|
probed = true;
|
|
219
421
|
}
|
|
220
422
|
if (heights) {
|
|
221
|
-
measuringExactly(() =>
|
|
423
|
+
measuringExactly(() => {
|
|
424
|
+
this._writeFloors('height');
|
|
425
|
+
for (const boundary of boundaries) {
|
|
426
|
+
this._measureBoundary(boundary, 'height', !probed);
|
|
427
|
+
}
|
|
428
|
+
this._writeFloors('height');
|
|
429
|
+
});
|
|
222
430
|
this._layoutRoot(width, height);
|
|
223
431
|
}
|
|
224
|
-
this._floorsDirty = false;
|
|
225
|
-
this._floorsContentDirty = false;
|
|
226
|
-
this._floorsSwept = true;
|
|
227
|
-
this._floorsWidth = width;
|
|
228
432
|
}
|
|
229
433
|
|
|
230
434
|
/**
|
|
@@ -267,6 +267,12 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
267
267
|
this._floorsDirty = true;
|
|
268
268
|
this._floorsWidth = null;
|
|
269
269
|
this._floorsStale = new Set();
|
|
270
|
+
// the nodes whose changes the next measurement owes, and whether one
|
|
271
|
+
// came with no node to name (`_floorsScope`)
|
|
272
|
+
this._floorsSources = new Set();
|
|
273
|
+
this._floorsUnscoped = false;
|
|
274
|
+
this._floorsScopeNow = null;
|
|
275
|
+
this._scopedFloorPasses = 0;
|
|
270
276
|
// whether the first floors pass has run (`collectFloorStale`'s sweep)
|
|
271
277
|
this._floorsSwept = false;
|
|
272
278
|
this._floorsMeasured = 0;
|
package/src/ntk.js
CHANGED
|
@@ -22,43 +22,20 @@
|
|
|
22
22
|
// `@react-x11/components` (`<Markdown>`, `<Formula>`). `SvgView` is still
|
|
23
23
|
// here; a drawing is not a document.
|
|
24
24
|
//
|
|
25
|
-
// One name is not a plain re-export. `Surface`
|
|
25
|
+
// One name is not a plain re-export. `Surface` (src/offscreen.js) asks the app it is
|
|
26
26
|
// handed for the implementation, because ntk's own is a pixmap and a
|
|
27
27
|
// Picture — an X connection's — and a component allocates its buffer
|
|
28
28
|
// without knowing which backend it was mounted on. This subpath is where a
|
|
29
29
|
// drawing-adjacent name gets its backend-neutral answer; the X-only names
|
|
30
30
|
// (`createClient`, `Pixmap`, `Picture`, `XEmbedSocket`) stay X-only.
|
|
31
|
-
import
|
|
31
|
+
import * as ntk from 'ntk';
|
|
32
|
+
|
|
33
|
+
import { adoptNtk } from './ntkroot.js';
|
|
32
34
|
|
|
33
35
|
export * from 'ntk';
|
|
34
36
|
export { default } from 'ntk';
|
|
37
|
+
export { Surface } from './offscreen.js';
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
* An app that makes its own surfaces answers `createSurface(options)` —
|
|
40
|
-
* the Cocoa app does, over a CG bitmap (src/cocoa/surface.js), the Wayland
|
|
41
|
-
* app over a GL render target (src/wayland/surface.js) — and an ntk
|
|
42
|
-
* connection has no such method and gets ntk's pixmap. The result is
|
|
43
|
-
* whichever implementation answered, not an instance of this class: the
|
|
44
|
-
* contract is the shape — `width`/`height`, `getContext('2d')`, `render`,
|
|
45
|
-
* `clear`, `copyWithin`, `destroy`, and `ctx.drawImage(surface, …)` —
|
|
46
|
-
* (docs/extending.md "Scrolling the pixels, not just the offset"), and
|
|
47
|
-
* nothing needs `instanceof`.
|
|
48
|
-
*
|
|
49
|
-
* Part of that shape is that a context may be **held**: ntk tells a caller
|
|
50
|
-
* doing many draws to take `getContext('2d')` once rather than one per
|
|
51
|
-
* frame, and that holds on every backend here, including the ones where a
|
|
52
|
-
* surface is a GPU target sharing a device with the window (#566). Draws
|
|
53
|
-
* through a held context land in the surface whenever they are made, and
|
|
54
|
-
* leave nothing for the caller to restore; `render(fn)` is the scoped form,
|
|
55
|
-
* for a caller that wants a frame's clean transform and clip.
|
|
56
|
-
*/
|
|
57
|
-
export class Surface {
|
|
58
|
-
constructor(app, options) {
|
|
59
|
-
if (typeof app?.createSurface === 'function') {
|
|
60
|
-
return app.createSurface(options);
|
|
61
|
-
}
|
|
62
|
-
return new NtkSurface(app, options);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
39
|
+
// what src/ntkroot.js would load, already loaded: handed over rather than
|
|
40
|
+
// imported twice
|
|
41
|
+
adoptNtk(ntk);
|
package/src/ntkroot.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// ntk's package root — the X11 client, ntk's windows and their contexts, the
|
|
2
|
+
// GL rungs, fontconfig and the rest of what the X11 backend draws with —
|
|
3
|
+
// loaded where an X connection is made rather than wherever react-x11 is
|
|
4
|
+
// imported.
|
|
5
|
+
//
|
|
6
|
+
// Only that backend needs it. The Cocoa, Windows and Wayland backends use a
|
|
7
|
+
// colour parser, the shadow arithmetic, the image and SVG decoders and the
|
|
8
|
+
// font parser of ntk's, each from a subpath of its own (`ntk/color`,
|
|
9
|
+
// `ntk/shadow-math`, `ntk/image`, `ntk/svg`, `ntk/font`, …), and importing
|
|
10
|
+
// the root for them was ~210 ms of module loading on Windows before the first
|
|
11
|
+
// window of an app that never opens an X connection.
|
|
12
|
+
//
|
|
13
|
+
// `createRoot` loads it on the X11 path, before the connection, and for an
|
|
14
|
+
// ntk app it is handed rather than makes — an app that could only have come
|
|
15
|
+
// from ntk's own `createClient`, so the import is already done and this is a
|
|
16
|
+
// lookup. `react-x11/ntk` hands over the root it imports. After that, the
|
|
17
|
+
// code that only ever runs against an X connection reads it synchronously.
|
|
18
|
+
let root = null;
|
|
19
|
+
let loading = null;
|
|
20
|
+
|
|
21
|
+
/** Load ntk's root, once; resolves to the module. */
|
|
22
|
+
export function loadNtk() {
|
|
23
|
+
return (loading ??= import('ntk').then((mod) => (root = mod)));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A root imported some other way (`react-x11/ntk`): the same module, so
|
|
27
|
+
* nothing is loaded twice by taking it. */
|
|
28
|
+
export function adoptNtk(mod) {
|
|
29
|
+
root ??= mod;
|
|
30
|
+
loading ??= Promise.resolve(mod);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** ntk's root, or null while no X connection has been made. */
|
|
34
|
+
export function ntkRoot() {
|
|
35
|
+
return root;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* ntk's root, for code that only runs against an X connection — which
|
|
40
|
+
* `createRoot` has loaded it for. Throws where it is not: reaching for an X
|
|
41
|
+
* resource on a backend that has none is the mistake, and this says so
|
|
42
|
+
* rather than failing inside ntk on an app that is not its own.
|
|
43
|
+
*/
|
|
44
|
+
export function x11Ntk(what) {
|
|
45
|
+
if (root) return root;
|
|
46
|
+
throw new Error(
|
|
47
|
+
`react-x11: ${what} needs an X connection, and this process has none ` +
|
|
48
|
+
'(ntk loads with the X11 backend, in createRoot).',
|
|
49
|
+
);
|
|
50
|
+
}
|
package/src/offscreen.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// An offscreen surface on whichever backend an app is — what the paint cache,
|
|
2
|
+
// a group's opacity and the symbol icons draw into, and what `react-x11/ntk`
|
|
3
|
+
// hands a component as `Surface`.
|
|
4
|
+
//
|
|
5
|
+
// An app that makes its own surfaces answers `createSurface(options)` — the
|
|
6
|
+
// Cocoa app over a CG bitmap (src/cocoa/surface.js), the Windows app over a
|
|
7
|
+
// Direct2D bitmap, the Wayland app over a GL render target
|
|
8
|
+
// (src/wayland/surface.js) — and an X connection has no such method and gets
|
|
9
|
+
// ntk's pixmap, from ntk's root, which only an X connection loads
|
|
10
|
+
// (src/ntkroot.js). The result is whichever implementation answered, not an
|
|
11
|
+
// instance of this class: the contract is the shape — `width`/`height`,
|
|
12
|
+
// `getContext('2d')`, `render`, `clear`, `copyWithin`, `destroy`, and
|
|
13
|
+
// `ctx.drawImage(surface, …)` — (docs/extending.md "Scrolling the pixels, not
|
|
14
|
+
// just the offset"), and nothing needs `instanceof`.
|
|
15
|
+
//
|
|
16
|
+
// Part of that shape is that a context may be **held**: ntk tells a caller
|
|
17
|
+
// doing many draws to take `getContext('2d')` once rather than one per frame,
|
|
18
|
+
// and that holds on every backend here, including the ones where a surface is
|
|
19
|
+
// a GPU target sharing a device with the window (#566). Draws through a held
|
|
20
|
+
// context land in the surface whenever they are made, and leave nothing for
|
|
21
|
+
// the caller to restore; `render(fn)` is the scoped form, for a caller that
|
|
22
|
+
// wants a frame's clean transform and clip.
|
|
23
|
+
import { x11Ntk } from './ntkroot.js';
|
|
24
|
+
|
|
25
|
+
export class Surface {
|
|
26
|
+
constructor(app, options) {
|
|
27
|
+
if (typeof app?.createSurface === 'function') {
|
|
28
|
+
return app.createSurface(options);
|
|
29
|
+
}
|
|
30
|
+
const { Surface: NtkSurface } = x11Ntk('an offscreen surface');
|
|
31
|
+
return new NtkSurface(app, options);
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/paintcache.js
CHANGED
|
@@ -42,13 +42,13 @@
|
|
|
42
42
|
// this package pins, and a named import of something absent is a *load-time*
|
|
43
43
|
// SyntaxError — the whole renderer, not just the cache. Same shape as the
|
|
44
44
|
// `typeof wnd?.scrollRegion !== 'function'` guard for ntk without #139.
|
|
45
|
-
import * as ntk from 'ntk';
|
|
46
45
|
|
|
47
46
|
// The surface this cache draws into is `react-x11/ntk`'s: ntk's pixmap on
|
|
48
47
|
// an X connection, the app's own (`app.createSurface`) on a backend that
|
|
49
48
|
// makes them — the Cocoa backend's CG bitmap. One import, so the cache
|
|
50
49
|
// names neither.
|
|
51
|
-
import {
|
|
50
|
+
import { ntkRoot } from './ntkroot.js';
|
|
51
|
+
import { Surface } from './offscreen.js';
|
|
52
52
|
|
|
53
53
|
/** Stale pixels are undebuggable, and every other optimization here has an
|
|
54
54
|
* escape hatch — see NO_SCROLL_BLIT. */
|
|
@@ -83,19 +83,26 @@ const MAX_ITEM_PIXELS = 256 * 256;
|
|
|
83
83
|
* grow this forever; the cost of forgetting is one extra live paint. */
|
|
84
84
|
const MAX_PENDING = 4096;
|
|
85
85
|
|
|
86
|
-
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
86
|
+
/**
|
|
87
|
+
* Whether `ctx` draws on the device's pixel grid, wherever its origin is: a
|
|
88
|
+
* surface rendered at device resolution composites through it pixel for
|
|
89
|
+
* pixel. An integer translation keeps that — and it is how an overlay pane
|
|
90
|
+
* over a `<glarea>` draws, from the window's coordinates into its layer's
|
|
91
|
+
* (src/win32/overlay.js) — so declining it painted every cached drawing on a
|
|
92
|
+
* surface's children live, every time a pane repainted: every card of a
|
|
93
|
+
* graph, on every step of a pan. A scale, a rotation or a fraction of a
|
|
94
|
+
* pixel does not, and paints live.
|
|
93
95
|
*/
|
|
94
96
|
function isDeviceSpace(ctx) {
|
|
95
97
|
const m = ctx.getTransform?.();
|
|
96
98
|
if (!m) return true;
|
|
97
99
|
return (
|
|
98
|
-
m.a === 1 &&
|
|
100
|
+
m.a === 1 &&
|
|
101
|
+
m.b === 0 &&
|
|
102
|
+
m.c === 0 &&
|
|
103
|
+
m.d === 1 &&
|
|
104
|
+
Number.isInteger(m.e) &&
|
|
105
|
+
Number.isInteger(m.f)
|
|
99
106
|
);
|
|
100
107
|
}
|
|
101
108
|
|
|
@@ -415,15 +422,19 @@ export class PaintCache {
|
|
|
415
422
|
}
|
|
416
423
|
}
|
|
417
424
|
|
|
418
|
-
/** Whether the installed ntk can make the surfaces this needs
|
|
419
|
-
*
|
|
420
|
-
|
|
425
|
+
/** Whether the installed ntk can make the surfaces this needs on an X
|
|
426
|
+
* connection — ntk's root, which only an X connection loads
|
|
427
|
+
* (src/ntkroot.js). Older ones cannot, and the answer is simply that nothing
|
|
428
|
+
* is cached there. */
|
|
429
|
+
export const paintCacheSupported = () =>
|
|
430
|
+
typeof ntkRoot()?.Surface === 'function';
|
|
421
431
|
|
|
422
432
|
/** Whether `app` can make an offscreen surface at all: an X connection
|
|
423
433
|
* with the Render extension (ntk's pixmap surface), or a backend with a
|
|
424
434
|
* surface of its own — the Cocoa app's `createSurface` (docs/macos.md). */
|
|
425
435
|
const canMakeSurfaces = (app) =>
|
|
426
|
-
Boolean(app?.display?.Render)
|
|
436
|
+
(Boolean(app?.display?.Render) && paintCacheSupported()) ||
|
|
437
|
+
typeof app?.createSurface === 'function';
|
|
427
438
|
|
|
428
439
|
/**
|
|
429
440
|
* The cache for an app, created on first use. Null when caching is off, when
|
|
@@ -432,6 +443,6 @@ const canMakeSurfaces = (app) =>
|
|
|
432
443
|
* there must paint live.
|
|
433
444
|
*/
|
|
434
445
|
export function paintCacheFor(app) {
|
|
435
|
-
if (DISABLED || !
|
|
446
|
+
if (DISABLED || !canMakeSurfaces(app)) return null;
|
|
436
447
|
return (app._paintCache ??= new PaintCache(app));
|
|
437
448
|
}
|
package/src/refresh/loader.js
CHANGED
|
@@ -326,6 +326,28 @@ function buildTransformer({ babel, jsxTransform, refreshTransform }, options) {
|
|
|
326
326
|
|
|
327
327
|
let registered = false;
|
|
328
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Which files stay out of the hot graph: every package in node_modules,
|
|
331
|
+
* react-x11's own src/, and whatever the app's `ignore` names.
|
|
332
|
+
*
|
|
333
|
+
* Compared with forward slashes, whichever the path arrives with: on
|
|
334
|
+
* Windows a file path's separators are backslashes, and a forward-slash
|
|
335
|
+
* test let every package in node_modules into the hot graph — where the
|
|
336
|
+
* HMR layer's rewrite of a package's modules could leave one reading an
|
|
337
|
+
* import before it was bound (entities, under htmlparser2).
|
|
338
|
+
*/
|
|
339
|
+
export function hotIgnoreFor(ignore, srcDir = SRC_DIR) {
|
|
340
|
+
const src = srcDir.replaceAll('\\', '/');
|
|
341
|
+
return (path) => {
|
|
342
|
+
const slashed = path.replaceAll('\\', '/');
|
|
343
|
+
return (
|
|
344
|
+
slashed.includes('/node_modules/') ||
|
|
345
|
+
slashed.startsWith(src) ||
|
|
346
|
+
(ignore ? ignore(path) === true : false)
|
|
347
|
+
);
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
329
351
|
/**
|
|
330
352
|
* Register the hot-reload loader. `react-x11/refresh/register` calls this
|
|
331
353
|
* with no options; a tool that needs the seams writes its own two-line
|
|
@@ -362,10 +384,7 @@ export async function registerRefresh(options = {}) {
|
|
|
362
384
|
const { ignore } = validated;
|
|
363
385
|
registered = true;
|
|
364
386
|
|
|
365
|
-
const hotIgnore = (
|
|
366
|
-
path.includes('/node_modules/') ||
|
|
367
|
-
path.startsWith(SRC_DIR) ||
|
|
368
|
-
(ignore ? ignore(path) === true : false);
|
|
387
|
+
const hotIgnore = hotIgnoreFor(ignore);
|
|
369
388
|
|
|
370
389
|
nodeModule.registerHooks({
|
|
371
390
|
load(url, context, nextLoad) {
|
package/src/screencolor.js
CHANGED
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
// and losing the grab to someone else is detected (the server says so, with
|
|
57
57
|
// a LeaveNotify of mode Ungrab) rather than waited out.
|
|
58
58
|
|
|
59
|
-
import { pixelLayout, toStraightRgba } from 'ntk';
|
|
59
|
+
import { pixelLayout, toStraightRgba } from 'ntk/imagedata';
|
|
60
60
|
|
|
61
61
|
import { sessionBus } from './bus.js';
|
|
62
62
|
import { XK_ESCAPE, XK_KP_ENTER, XK_RETURN, XK_SPACE } from './keysyms.js';
|
package/src/styles.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Numbers are pixels; strings like '50%' / 'auto' pass through to yoga.
|
|
4
4
|
// The engine is ours (`./yoga.js`) — the enum tables below are built at
|
|
5
5
|
// module scope, which is what that module's synchronous half is for.
|
|
6
|
-
import { cssColorStraight } from 'ntk';
|
|
6
|
+
import { cssColorStraight } from 'ntk/color';
|
|
7
7
|
|
|
8
8
|
import { parseBoxShadow, parseLinearGradient } from './decorations.js';
|
|
9
9
|
import {
|
package/src/svgnodes.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// `<Formula>`), composed from public host elements. `<svg>` stays here
|
|
11
11
|
// because it is not a document — it is a drawing, and one node is the right
|
|
12
12
|
// grain for it.
|
|
13
|
-
import
|
|
13
|
+
import SvgView from 'ntk/svg';
|
|
14
14
|
|
|
15
15
|
import { Node } from './nodes/node.js';
|
|
16
16
|
import { intrinsicSize } from './nodes/layout.js';
|
package/src/symbols.js
CHANGED
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
// the same bargain a tray icon's name makes — so an app that runs on both
|
|
18
18
|
// picks its names by platform, as it does for the tray.
|
|
19
19
|
|
|
20
|
-
import { cssColorStraight
|
|
20
|
+
import { cssColorStraight } from 'ntk/color';
|
|
21
|
+
import { decodeImage } from 'ntk/image';
|
|
22
|
+
import SvgView from 'ntk/svg';
|
|
21
23
|
import * as nodeFs from 'node:fs';
|
|
22
24
|
|
|
23
25
|
import { IconTheme } from './icontheme.js';
|
|
24
|
-
import { Surface } from './
|
|
26
|
+
import { Surface } from './offscreen.js';
|
|
25
27
|
import { xsettings } from './xsettings.js';
|
|
26
28
|
|
|
27
29
|
/** A `fontWeight` as the number both icon systems configure with. */
|
package/src/trayhooks.js
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
// frame is not available for anything that has to ask the desktop.
|
|
55
55
|
|
|
56
56
|
import { useEffect, useRef, useState } from 'react';
|
|
57
|
-
import { decodeImage } from 'ntk';
|
|
57
|
+
import { decodeImage } from 'ntk/image';
|
|
58
58
|
|
|
59
59
|
import { useAppOrNull } from './appcontext.js';
|
|
60
60
|
import { currentRegistration } from './application.js';
|
package/src/wayland/context2d.js
CHANGED
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
import { createRequire } from 'node:module';
|
|
43
43
|
import path from 'node:path';
|
|
44
44
|
import { pathToFileURL } from 'node:url';
|
|
45
|
-
import {
|
|
45
|
+
import { Image as NtkImage } from 'ntk/image';
|
|
46
|
+
import { Path2D } from 'ntk/path';
|
|
46
47
|
import { glDevice } from './device.js';
|
|
47
48
|
import { GlyphAtlas } from './glyphatlas.js';
|
|
48
49
|
import { GLTarget } from './target.js';
|