react-x11 2.11.0 → 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 +292 -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/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/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 +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
package/src/cocoa/window.js
CHANGED
|
@@ -12,6 +12,11 @@ import { CocoaLayerPresenter } from './presenter.js';
|
|
|
12
12
|
import { CocoaPromotion } from './promotion.js';
|
|
13
13
|
|
|
14
14
|
let nextWindowId = 1;
|
|
15
|
+
// How long a worker's flip may keep its window's back buffer before the
|
|
16
|
+
// window draws into it anyway (`_armFence`). The release is reported once
|
|
17
|
+
// the replacing frame has committed, a fraction of a millisecond later, and
|
|
18
|
+
// a window must not freeze on a report that never came.
|
|
19
|
+
const FENCE_TIMEOUT_MS = 100;
|
|
15
20
|
|
|
16
21
|
export class CocoaWindow {
|
|
17
22
|
constructor(app, attributes = {}) {
|
|
@@ -34,6 +39,9 @@ export class CocoaWindow {
|
|
|
34
39
|
// — the period of the display it is on (`_refreshFrameInterval`).
|
|
35
40
|
this._rafLast = 0;
|
|
36
41
|
this._frameInterval = 0;
|
|
42
|
+
// …and when a frame last reached glass, whoever painted it: the pump's
|
|
43
|
+
// paced frame or an input answered on the spot (`frameInFlight`)
|
|
44
|
+
this._presentedAt = -Infinity;
|
|
37
45
|
|
|
38
46
|
const s = this.scale;
|
|
39
47
|
// Snapped to whole POINTS: AppKit rounds window sizes to the point
|
|
@@ -75,7 +83,7 @@ export class CocoaWindow {
|
|
|
75
83
|
// Transparent to the pointer, the preview is passed over and the hit
|
|
76
84
|
// reaches what it covers (#488; @windowkit/appkit >= 0.6.0, an older
|
|
77
85
|
// bridge ignores the option). The drop side is excluded separately:
|
|
78
|
-
// nodes.js `_initDnd` gives a preview no DropSession and registers
|
|
86
|
+
// nodes/window/droptarget.js `_initDnd` gives a preview no DropSession and registers
|
|
79
87
|
// nothing.
|
|
80
88
|
if (attributes.dragPreview) options.ignoresMouseEvents = true;
|
|
81
89
|
// The root layer's background is the "what newly exposed area shows"
|
|
@@ -87,14 +95,48 @@ export class CocoaWindow {
|
|
|
87
95
|
const parsed = app._parseColor(attributes.backgroundColor);
|
|
88
96
|
if (parsed) options.backgroundColor = parsed;
|
|
89
97
|
}
|
|
98
|
+
// Where it was asked to go, until AppKit says where it went
|
|
99
|
+
// (`_refreshOrigin`) — which on a worker is not before the window exists.
|
|
100
|
+
this.x = Math.round(attributes.x ?? 0);
|
|
101
|
+
this.y = Math.round(attributes.y ?? 0);
|
|
102
|
+
this._screenOrigin = { x: this.x, y: this.y };
|
|
103
|
+
// AppKit's content size in points, as the bridge last reported it
|
|
104
|
+
// (`_frameSize`)
|
|
105
|
+
this._points = null;
|
|
106
|
+
// Threaded mode's swapchain fence (`frameInFlight`): the IOSurface id
|
|
107
|
+
// on the layer as far as this window has asked, the one the last flip
|
|
108
|
+
// is taking off glass until the bridge says it is released, and the
|
|
109
|
+
// catch-up copy the back buffer is owed once it is back.
|
|
110
|
+
this._onGlass = null;
|
|
111
|
+
this._awaiting = null;
|
|
112
|
+
this._catchUp = null;
|
|
113
|
+
this._fenceTimer = null;
|
|
114
|
+
this._shadowTimer = null;
|
|
115
|
+
// AppKit's live resize, between the bridge's `window-live-resize` begin
|
|
116
|
+
// and end (`CocoaApp._routeLiveResize`): a tick of it lays out with the
|
|
117
|
+
// floors it has, and fresh ones are measured after the drag
|
|
118
|
+
// (nodes/window/size.js, `_deferContentFloors`)
|
|
119
|
+
this.liveResizing = false;
|
|
90
120
|
this._h = this._native.createWindow2(options);
|
|
121
|
+
// On a worker the bridge answers a handle at the call and makes the
|
|
122
|
+
// window when its command runs: the number is null until then, and
|
|
123
|
+
// `window-created` brings it (`_created`).
|
|
91
124
|
this.windowNumber = this._native.windowNumber(this._h);
|
|
125
|
+
// What the app's window map and the events name this window by: the
|
|
126
|
+
// handle on a worker, where every window event carries it, and the
|
|
127
|
+
// number in pump mode, where the events carry only that.
|
|
128
|
+
this._key = app._threaded ? this._h : this.windowNumber;
|
|
92
129
|
this._layer = this._native.windowRootLayer(this._h);
|
|
130
|
+
// A worker's frame lands after AppKit has moved the window's edge, so
|
|
131
|
+
// AppKit is asked to wait for it (`RESIZE_WAIT_MS`, src/cocoa/app.js).
|
|
132
|
+
if (app._threaded && app._resizeWait > 0) {
|
|
133
|
+
this._native.setResizeHandshake(this._h, { waitMs: app._resizeWait });
|
|
134
|
+
}
|
|
93
135
|
this._refreshOrigin();
|
|
94
136
|
this._refreshFrameInterval();
|
|
95
137
|
if (attributes.sizeHints) this.setSizeHints(attributes.sizeHints);
|
|
96
138
|
|
|
97
|
-
// How many damage rects a frame may keep before merging them (nodes.js,
|
|
139
|
+
// How many damage rects a frame may keep before merging them (nodes/damage.js,
|
|
98
140
|
// MAX_DAMAGE_RECTS is the X11 answer). A pass here costs one CoreGraphics
|
|
99
141
|
// clip and a culled walk, where an X pass costs the server a clip mask,
|
|
100
142
|
// so a frame in which a clock, a graph and a status row all ticked keeps
|
|
@@ -106,7 +148,7 @@ export class CocoaWindow {
|
|
|
106
148
|
// The retained layer presenter (docs/macos.md Tier L), behind
|
|
107
149
|
// REACT_X11_COCOA_PRESENTER=layers while the surface path is the
|
|
108
150
|
// measured default. Its hooks exist only in this mode, so the feature
|
|
109
|
-
// detection in nodes
|
|
151
|
+
// detection in src/nodes/ keeps the surface path byte-identical; the
|
|
110
152
|
// scroll blit is shadowed off because a layer frame has no backing
|
|
111
153
|
// bitmap to blit. The last two are the animation seam: a transition or
|
|
112
154
|
// a loop the presenter takes runs in the render server and schedules no
|
|
@@ -159,6 +201,8 @@ export class CocoaWindow {
|
|
|
159
201
|
|
|
160
202
|
_refreshOrigin() {
|
|
161
203
|
const f = this._native.getWindowFrame(this._h);
|
|
204
|
+
// a worker's window AppKit has not made yet: the asked-for origin stands
|
|
205
|
+
if (!f) return;
|
|
162
206
|
const s = this.scale;
|
|
163
207
|
this.x = Math.round(f.x * s);
|
|
164
208
|
this.y = Math.round(f.y * s);
|
|
@@ -177,8 +221,17 @@ export class CocoaWindow {
|
|
|
177
221
|
this._frameInterval = this.app.frameIntervalFor(this);
|
|
178
222
|
}
|
|
179
223
|
|
|
224
|
+
/** `window-created`: AppKit has made the window a worker asked for, and
|
|
225
|
+
* published where it put it. */
|
|
226
|
+
_created(ev) {
|
|
227
|
+
this.windowNumber = ev.windowNumber;
|
|
228
|
+
this._refreshOrigin();
|
|
229
|
+
this._refreshFrameInterval();
|
|
230
|
+
}
|
|
231
|
+
|
|
180
232
|
/** Native geometry changed (delegate event, points). */
|
|
181
233
|
_nativeResized(points) {
|
|
234
|
+
this._points = { width: points.width, height: points.height };
|
|
182
235
|
const s = this.scale;
|
|
183
236
|
this.width = Math.max(1, Math.round(points.width * s));
|
|
184
237
|
this.height = Math.max(1, Math.round(points.height * s));
|
|
@@ -186,12 +239,6 @@ export class CocoaWindow {
|
|
|
186
239
|
this.y = Math.round(points.y * s);
|
|
187
240
|
this._screenOrigin = { x: this.x, y: this.y };
|
|
188
241
|
this._refreshFrameInterval();
|
|
189
|
-
// AppKit's inLiveResize, as the delegate reported it: the renderer
|
|
190
|
-
// answers a live tick with the layout floors it has and measures fresh
|
|
191
|
-
// ones after the drag (nodes.js, `_deferContentFloors`). Cleared by
|
|
192
|
-
// the pump (`_endLiveResizes`), because the pump cannot run while the
|
|
193
|
-
// resize loop owns the thread — a tick of it is the drag being over.
|
|
194
|
-
if (points.live === true) this.liveResizing = true;
|
|
195
242
|
}
|
|
196
243
|
|
|
197
244
|
resize(width, height) {
|
|
@@ -251,12 +298,14 @@ export class CocoaWindow {
|
|
|
251
298
|
this.app.cancelAttention(this._attentionRequest);
|
|
252
299
|
this._attentionRequest = null;
|
|
253
300
|
}
|
|
301
|
+
clearTimeout(this._shadowTimer);
|
|
302
|
+
this._shadowTimer = null;
|
|
254
303
|
this._promotion?.destroy();
|
|
255
304
|
this._native.destroyWindow2(this._h);
|
|
256
305
|
this._releaseBacking();
|
|
257
306
|
}
|
|
258
307
|
|
|
259
|
-
// --- window-manager-ish surface (feature-detected by nodes
|
|
308
|
+
// --- window-manager-ish surface (feature-detected by src/nodes/) ---------
|
|
260
309
|
|
|
261
310
|
setTitle(title) {
|
|
262
311
|
this.title = title;
|
|
@@ -287,7 +336,7 @@ export class CocoaWindow {
|
|
|
287
336
|
* until the state is removed or the window goes. Every other name resolves
|
|
288
337
|
* `false` — ntk's own contract for a state the server cannot honour —
|
|
289
338
|
* because the bridge has no zoom/miniaturize/fullscreen verbs yet
|
|
290
|
-
* (windowkit/appkit#15 scoped them out);
|
|
339
|
+
* (windowkit/appkit#15 scoped them out); WindowNode swallows the false.
|
|
291
340
|
*/
|
|
292
341
|
setWmState(names, action = 'add') {
|
|
293
342
|
const list = Array.isArray(names) ? names : [names];
|
|
@@ -312,9 +361,9 @@ export class CocoaWindow {
|
|
|
312
361
|
// --- drag and drop (src/cocoa/dnd.js) ------------------------------------
|
|
313
362
|
|
|
314
363
|
/**
|
|
315
|
-
* The drop side:
|
|
364
|
+
* The drop side: WindowNode hands over the window's DropSession at realize
|
|
316
365
|
* (`_initDnd`), and from then on the app routes this window's `drag-*`
|
|
317
|
-
* events into it. Its presence on the window is what tells
|
|
366
|
+
* events into it. Its presence on the window is what tells WindowNode the
|
|
318
367
|
* backend has drop machinery of its own.
|
|
319
368
|
*/
|
|
320
369
|
attachDropTransport(session, node) {
|
|
@@ -404,6 +453,9 @@ export class CocoaWindow {
|
|
|
404
453
|
* that frame is on glass.
|
|
405
454
|
*/
|
|
406
455
|
_ensureSurface() {
|
|
456
|
+
// a paint that did not wait for the fence: the back buffer gets its
|
|
457
|
+
// catch-up now, so what it paints lands over the frame before it
|
|
458
|
+
this._settleBack();
|
|
407
459
|
const w = this.width;
|
|
408
460
|
const h = this.height;
|
|
409
461
|
if (
|
|
@@ -461,10 +513,15 @@ export class CocoaWindow {
|
|
|
461
513
|
}
|
|
462
514
|
this._chain = null;
|
|
463
515
|
this._surface = null;
|
|
516
|
+
// a new pair owes nothing to what the old one was showing
|
|
517
|
+
clearTimeout(this._fenceTimer);
|
|
518
|
+
this._fenceTimer = null;
|
|
519
|
+
this._awaiting = null;
|
|
520
|
+
this._catchUp = null;
|
|
464
521
|
}
|
|
465
522
|
|
|
466
523
|
/**
|
|
467
|
-
* The per-flush painted rects (
|
|
524
|
+
* The per-flush painted rects (WindowNode's swapchain seam), accumulated
|
|
468
525
|
* until the next present: they are what the flip's catch-up copy covers.
|
|
469
526
|
* `'full'`/null collapse the set — one full copy beats bookkeeping.
|
|
470
527
|
*/
|
|
@@ -473,7 +530,7 @@ export class CocoaWindow {
|
|
|
473
530
|
if (this._freshSurface) {
|
|
474
531
|
this._freshSurface = false;
|
|
475
532
|
// A full flush painted every pixel of the new surface, and a resize
|
|
476
|
-
// event's flush is one (nodes.js, the 'resize' listener). A bounded
|
|
533
|
+
// event's flush is one (nodes/window/listeners.js, the 'resize' listener). A bounded
|
|
477
534
|
// one left garbage outside its rects: hold the present until the full
|
|
478
535
|
// frame asked for here lands, so the garbage is never on glass.
|
|
479
536
|
if (rects) {
|
|
@@ -537,6 +594,9 @@ export class CocoaWindow {
|
|
|
537
594
|
* caller falls back to the plain repaint. */
|
|
538
595
|
scrollRegion(rect, dx, dy) {
|
|
539
596
|
if (!this._surface) return false;
|
|
597
|
+
// the band moves over the frame before this one, not over a buffer
|
|
598
|
+
// still owed its catch-up
|
|
599
|
+
this._settleBack();
|
|
540
600
|
if (!Number.isInteger(dx) || !Number.isInteger(dy)) return false;
|
|
541
601
|
const moved = this._native.scrollSurface(
|
|
542
602
|
this._surface,
|
|
@@ -566,8 +626,51 @@ export class CocoaWindow {
|
|
|
566
626
|
return Boolean(moved);
|
|
567
627
|
}
|
|
568
628
|
|
|
629
|
+
/**
|
|
630
|
+
* Whether this window's last frame still holds the buffer the next one
|
|
631
|
+
* would be drawn into — the X11 contract's fence (src/frames.js). The
|
|
632
|
+
* pump never needs it: a pump-mode flip is applied in the call, so the
|
|
633
|
+
* other buffer is off glass before the next paint. A worker's flip is a
|
|
634
|
+
* command the UI thread applies later, and until the bridge reports the
|
|
635
|
+
* buffer it replaced as released (`_surfaceReleased`), drawing into that
|
|
636
|
+
* buffer draws into what is on glass. A new size is never in flight: it
|
|
637
|
+
* paints into a new pair that nothing is showing.
|
|
638
|
+
*
|
|
639
|
+
* And while a batch is being routed (`CocoaApp._routeBatch`), its frame
|
|
640
|
+
* is the one owed: it goes out when the batch is done, answering every
|
|
641
|
+
* input in it at once. The early flush a discrete event asks for on its
|
|
642
|
+
* way out (src/frames.js) waits those few microseconds, rather than
|
|
643
|
+
* painting a state the next event in the same batch replaces.
|
|
644
|
+
*/
|
|
569
645
|
frameInFlight() {
|
|
570
|
-
return
|
|
646
|
+
if (this.app._batching) return true;
|
|
647
|
+
return (
|
|
648
|
+
this._awaiting != null &&
|
|
649
|
+
this._surfaceSize?.width === this.width &&
|
|
650
|
+
this._surfaceSize?.height === this.height
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Whether this window flipped less than one of its frame intervals ago —
|
|
656
|
+
* the wheel's gate (`CocoaApp._routeWheel`), and only the wheel's.
|
|
657
|
+
*
|
|
658
|
+
* A trackpad routinely lands two scroll events in one pump tick, and each
|
|
659
|
+
* used to paint and flip on the spot: the second frame was drawn into the
|
|
660
|
+
* buffer the first flip had taken off glass microseconds before, while the
|
|
661
|
+
* WindowServer could still be compositing from it. A scroll's frame blits,
|
|
662
|
+
* then repaints what the shift got wrong, so a composite caught between the
|
|
663
|
+
* two shows the blit alone — a held sticky header dragged up a pixel for a
|
|
664
|
+
* frame, or the rows under it painted over it, then corrected. No other
|
|
665
|
+
* input has both halves of that: a resize paints into a pair it has just
|
|
666
|
+
* made, and a press or a key does not come in bursts inside a refresh.
|
|
667
|
+
*
|
|
668
|
+
* Off with the interval: `frameInterval: 0` asks for no pacing at all.
|
|
669
|
+
*/
|
|
670
|
+
_flippedRecently(now = performance.now()) {
|
|
671
|
+
return (
|
|
672
|
+
this._frameInterval > 0 && now - this._presentedAt < this._frameInterval
|
|
673
|
+
);
|
|
571
674
|
}
|
|
572
675
|
|
|
573
676
|
requestAnimationFrame(cb) {
|
|
@@ -597,47 +700,158 @@ export class CocoaWindow {
|
|
|
597
700
|
// again next tick and the frame goes out the moment the window is back.
|
|
598
701
|
if (this._holdPresent || !this._visible()) return false;
|
|
599
702
|
this._dirty = false;
|
|
703
|
+
this._presentedAt = performance.now();
|
|
600
704
|
if (this._chain) {
|
|
601
705
|
const shown = this._chain.back;
|
|
602
706
|
this._native.surfaceUnlock(shown.handle);
|
|
603
|
-
this.
|
|
707
|
+
this._flip(() =>
|
|
708
|
+
this._native.setLayerContentsIOSurface(this._layer, shown.iosurfaceId),
|
|
709
|
+
);
|
|
604
710
|
this._chain.back = this._chain.front;
|
|
605
711
|
this._chain.front = shown;
|
|
606
712
|
this._surface = this._chain.back.handle;
|
|
607
713
|
// a different native surface owns the graphics state now — the
|
|
608
714
|
// context re-syncs its sticky state off the generation
|
|
609
715
|
this._surfaceGen++;
|
|
610
|
-
|
|
611
|
-
const damage = this._flushDamage;
|
|
716
|
+
const catchUp = { from: shown.handle, damage: this._flushDamage };
|
|
612
717
|
this._flushDamage = null;
|
|
613
|
-
this
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
718
|
+
// The buffer drawn into next is the one this flip takes off glass —
|
|
719
|
+
// when it was on glass at all: a new pair's first flip replaces a
|
|
720
|
+
// buffer of the old pair, or nothing. On a worker the flip has not
|
|
721
|
+
// happened yet, so its catch-up and the next paint wait for the
|
|
722
|
+
// bridge to say it has (`frameInFlight`).
|
|
723
|
+
const previous = this._onGlass;
|
|
724
|
+
this._onGlass = shown.iosurfaceId;
|
|
725
|
+
if (this.app._threaded && previous === this._chain.back.iosurfaceId) {
|
|
726
|
+
this._awaiting = previous;
|
|
727
|
+
this._catchUp = catchUp;
|
|
728
|
+
this._armFence();
|
|
729
|
+
} else {
|
|
730
|
+
this._settle(catchUp);
|
|
731
|
+
}
|
|
732
|
+
this._shadowAfterFlip();
|
|
626
733
|
return true;
|
|
627
734
|
}
|
|
628
|
-
this._native.surfaceToLayer(this._surface, this._layer);
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
735
|
+
this._flip(() => this._native.surfaceToLayer(this._surface, this._layer));
|
|
736
|
+
this._shadowAfterFlip();
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Hand a frame to the layer. In pump mode that is the verb alone: the
|
|
742
|
+
* bridge flips in a transaction of its own, in the call. On a worker the
|
|
743
|
+
* flip is recorded and committed as one frame (windowkit/appkit#52), and
|
|
744
|
+
* the commit says what size the frame was painted at, which is what a
|
|
745
|
+
* resize waits for (`setResizeHandshake`, #53). Actions off, as the
|
|
746
|
+
* bridge's own transaction has them.
|
|
747
|
+
*/
|
|
748
|
+
_flip(apply) {
|
|
749
|
+
if (!this.app._threaded) return apply();
|
|
750
|
+
const native = this._native;
|
|
751
|
+
native.txBegin({ disableActions: true });
|
|
752
|
+
try {
|
|
753
|
+
return apply();
|
|
754
|
+
} finally {
|
|
755
|
+
native.txCommit(this._frameSize());
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* The size this window's frames are painted at, in the points AppKit
|
|
761
|
+
* measures a window by: the bridge's own figures when they describe the
|
|
762
|
+
* current size, so that the handshake's comparison is exact.
|
|
763
|
+
*/
|
|
764
|
+
_frameSize() {
|
|
765
|
+
const p = this._points;
|
|
766
|
+
const s = this.scale;
|
|
767
|
+
if (
|
|
768
|
+
p &&
|
|
769
|
+
Math.max(1, Math.round(p.width * s)) === this.width &&
|
|
770
|
+
Math.max(1, Math.round(p.height * s)) === this.height
|
|
771
|
+
) {
|
|
772
|
+
return { width: p.width, height: p.height };
|
|
773
|
+
}
|
|
774
|
+
return { width: this.width / s, height: this.height / s };
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/** The back buffer is off glass: lock it for drawing and copy across what
|
|
778
|
+
* the frame now shown painted, so that the next frame starts from it. */
|
|
779
|
+
_settle({ from, damage }) {
|
|
780
|
+
this._native.surfaceLock(this._surface);
|
|
781
|
+
this._native.copySurfaceRegion(
|
|
782
|
+
from,
|
|
783
|
+
this._surface,
|
|
784
|
+
damage === 'full' || !damage
|
|
785
|
+
? null
|
|
786
|
+
: damage.flatMap((r) => [
|
|
787
|
+
Math.floor(r.x),
|
|
788
|
+
Math.floor(r.y),
|
|
789
|
+
Math.ceil(r.width) + 1,
|
|
790
|
+
Math.ceil(r.height) + 1,
|
|
791
|
+
]),
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** Stop waiting on the fence: the back buffer's catch-up, now. */
|
|
796
|
+
_settleBack() {
|
|
797
|
+
if (this._awaiting == null) return;
|
|
798
|
+
clearTimeout(this._fenceTimer);
|
|
799
|
+
this._fenceTimer = null;
|
|
800
|
+
this._awaiting = null;
|
|
801
|
+
const catchUp = this._catchUp;
|
|
802
|
+
this._catchUp = null;
|
|
803
|
+
if (catchUp && this._chain) this._settle(catchUp);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/** `surface-released` for `id`: true when it was the buffer this window
|
|
807
|
+
* was waiting on. */
|
|
808
|
+
_surfaceReleased(id) {
|
|
809
|
+
if (this._awaiting == null || this._awaiting !== id) return false;
|
|
810
|
+
this._settleBack();
|
|
637
811
|
return true;
|
|
638
812
|
}
|
|
639
813
|
|
|
814
|
+
_armFence() {
|
|
815
|
+
clearTimeout(this._fenceTimer);
|
|
816
|
+
this._fenceTimer = setTimeout(() => {
|
|
817
|
+
this._fenceTimer = null;
|
|
818
|
+
if (this._awaiting == null || this.destroyed) return;
|
|
819
|
+
this._settleBack();
|
|
820
|
+
this.app._tickFrames();
|
|
821
|
+
this.app._presentAll();
|
|
822
|
+
}, FENCE_TIMEOUT_MS);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* AppKit derives a transparent window's shadow from the content's opaque
|
|
827
|
+
* shape and does not recompute it on repaints — a popup whose card lands
|
|
828
|
+
* a frame after the map keeps the full-frame square AppKit guessed first.
|
|
829
|
+
* Recompute — but only once this present's transaction has actually
|
|
830
|
+
* flushed to the render server, or the recompute reads the frame BEFORE
|
|
831
|
+
* this one and keeps the square rim for menus that paint once and are
|
|
832
|
+
* only hovered after. In pump mode that is the next pump tick; on a
|
|
833
|
+
* worker, where the flip is a command and the recompute would be another
|
|
834
|
+
* one drained beside it, a frame interval later.
|
|
835
|
+
*/
|
|
836
|
+
_shadowAfterFlip() {
|
|
837
|
+
if (!this._transparentWindow) return;
|
|
838
|
+
if (!this.app._threaded) {
|
|
839
|
+
this.app._shadowStale.add(this);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
if (this._shadowTimer) return;
|
|
843
|
+
this._shadowTimer = setTimeout(
|
|
844
|
+
() => {
|
|
845
|
+
this._shadowTimer = null;
|
|
846
|
+
if (!this.destroyed) this._native.invalidateWindowShadow(this._h);
|
|
847
|
+
},
|
|
848
|
+
Math.max(1, Math.round(this._frameInterval || 16)),
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/** The window's content as a PNG at `path`; resolves whether it was
|
|
853
|
+
* written. A promise on either thread: on a worker AppKit answers later. */
|
|
640
854
|
snapshot(path) {
|
|
641
|
-
return this.
|
|
855
|
+
return this.app._ask('snapshotWindow', this._h, path);
|
|
642
856
|
}
|
|
643
857
|
}
|
package/src/components/Select.js
CHANGED
|
@@ -73,7 +73,7 @@ const itemHeight = (fontSize) => capBand(fontSize) + ITEM_PAD * 2;
|
|
|
73
73
|
// what NSPopUpButtonCell does with a title too long for its cell. It is
|
|
74
74
|
// also what makes the label *give way*: an eliding `nowrap` floors at the
|
|
75
75
|
// mark it would end in, where a clipping one keeps its full width and
|
|
76
|
-
// pushes whatever contains it wider (nodes.js, `_wrapWidth`).
|
|
76
|
+
// pushes whatever contains it wider (nodes/text.js, `_wrapWidth`).
|
|
77
77
|
const ONE_LINE = Object.freeze({
|
|
78
78
|
textWrap: 'nowrap',
|
|
79
79
|
textOverflow: 'ellipsis',
|
|
@@ -139,7 +139,7 @@ const MENU_BORDER = 1;
|
|
|
139
139
|
// the trigger (`menuAnchorOptions`).
|
|
140
140
|
const TRIGGER_PAD_LEFT = 10;
|
|
141
141
|
// the scrollbar is drawn *over* the content rather than insetting it
|
|
142
|
-
// (`nodes.js`, SCROLLBAR_WIDTH), so a menu that scrolls reserves the room
|
|
142
|
+
// (`nodes/scrollbars.js`, SCROLLBAR_WIDTH), so a menu that scrolls reserves the room
|
|
143
143
|
const SCROLLBAR_WIDTH = 6;
|
|
144
144
|
|
|
145
145
|
function normalizeOption(option) {
|
package/src/components/anchor.js
CHANGED
|
@@ -50,7 +50,7 @@ function sameAnchorRect(a, b) {
|
|
|
50
50
|
* hanging over stale ground.
|
|
51
51
|
*
|
|
52
52
|
* Subscribes to the anchoring node's owner window (`WindowNode.onAnchorChange`,
|
|
53
|
-
* `nodes.js`) rather than polling: that fires exactly on the events which
|
|
53
|
+
* `nodes/window/anchoring.js`) rather than polling: that fires exactly on the events which
|
|
54
54
|
* can actually move the rect — a layout pass and a fresh `_screenOrigin` —
|
|
55
55
|
* so a still trigger costs nothing and a moved one is caught the same frame.
|
|
56
56
|
* `getOptions` is read fresh through a ref on every notification, so callers
|
|
@@ -64,7 +64,7 @@ function sameAnchorRect(a, b) {
|
|
|
64
64
|
* it floating over content it no longer belongs to, detached from anything
|
|
65
65
|
* the user can see it points at. So once the trigger is entirely past a
|
|
66
66
|
* clipping ancestor's own bounds or past the owner window itself
|
|
67
|
-
* (`Node._offscreen()`,
|
|
67
|
+
* (`Node._offscreen()`, paint culling's check less its slop), tracking calls
|
|
68
68
|
* `onOutOfView` instead of measuring — closing the popup, or hiding it, is
|
|
69
69
|
* the caller's call — and does not resume until re-opened. With no
|
|
70
70
|
* `onOutOfView` this just stops updating rather than snapping to a rect
|
|
@@ -133,7 +133,7 @@ export function useAnchorTracking(
|
|
|
133
133
|
* with it, so the first click anywhere goes to dismissing it.
|
|
134
134
|
*
|
|
135
135
|
* The window manager's own focus, then, rather than anything in the tree:
|
|
136
|
-
* `WindowNode.onWindowFocusChange` (nodes.js), which the event manager
|
|
136
|
+
* `WindowNode.onWindowFocusChange` (nodes/window/window.js), which the event manager
|
|
137
137
|
* notifies from the same place it suspends the caret.
|
|
138
138
|
*/
|
|
139
139
|
export function useDismissOnWindowBlur(ref, active, onDismiss) {
|
package/src/components/native.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// `native={false}` (the widgets keep today's drawn rendering everywhere the
|
|
18
18
|
// bezel is off, so custom-designed apps lose nothing).
|
|
19
19
|
|
|
20
|
-
import React, { useCallback } from 'react';
|
|
20
|
+
import React, { useCallback, useRef } from 'react';
|
|
21
21
|
import { useAppOrNull } from '../appcontext.js';
|
|
22
22
|
import { useTheme } from './theme.js';
|
|
23
23
|
|
|
@@ -252,18 +252,23 @@ export function Bezel({
|
|
|
252
252
|
if (value !== undefined) params.value = value;
|
|
253
253
|
const sig = JSON.stringify(params);
|
|
254
254
|
const store = app?.nativeBezels;
|
|
255
|
+
// What this canvas drew last, drawn again while a worker's store is still
|
|
256
|
+
// drawing the next one (src/cocoa/bezels.js, "On a worker"): the old
|
|
257
|
+
// state for a frame reads better than no control at all.
|
|
258
|
+
const last = useRef(null);
|
|
255
259
|
// Keyed on the parameter signature so an unchanged bezel keeps its
|
|
256
260
|
// `onDraw` identity — CanvasNode invalidates when the closure changes,
|
|
257
261
|
// and a Button re-rendered by its parent must not repaint its bezel.
|
|
258
262
|
const onDraw = useCallback(
|
|
259
263
|
(ctx, info) => {
|
|
260
264
|
if (!store || !info.width || !info.height) return;
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
info.width,
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
);
|
|
265
|
+
const node = info.node;
|
|
266
|
+
const bezel =
|
|
267
|
+
store.get(JSON.parse(sig), info.width, info.height, info.scale, () => {
|
|
268
|
+
if (!node.destroyed) node.invalidate();
|
|
269
|
+
}) ?? last.current;
|
|
270
|
+
if (!bezel) return;
|
|
271
|
+
last.current = bezel;
|
|
267
272
|
ctx.drawImage(
|
|
268
273
|
{ _surfaceHandle: bezel.surface },
|
|
269
274
|
bezel.sx,
|
package/src/components/theme.js
CHANGED
|
@@ -183,7 +183,7 @@ function planted(children, theme, style) {
|
|
|
183
183
|
*
|
|
184
184
|
* **With no provider it is the desktop's**, and this re-renders when the
|
|
185
185
|
* desktop changes, which is what makes a react-x11 app that says nothing
|
|
186
|
-
* about colour go dark on a dark desktop. `node.theme` in `nodes.js` answers
|
|
186
|
+
* about colour go dark on a dark desktop. `node.theme` in `nodes/cascade.js` answers
|
|
187
187
|
* the same question for the other route.
|
|
188
188
|
*
|
|
189
189
|
* Identity matters: widgets plant what this returns on their own root node,
|
|
@@ -368,7 +368,7 @@ export const capBand = (fontSize) => Math.round(fontSize * 0.72);
|
|
|
368
368
|
*
|
|
369
369
|
* `undefined` for the width when off, never `0`: a node with no
|
|
370
370
|
* `outlineWidth` of its own is one core can leave out of the widened damage
|
|
371
|
-
* rects entirely (`_outlineExtent`, src/nodes.js), and `0` is the documented
|
|
371
|
+
* rects entirely (`_outlineExtent`, src/nodes/boxpaint.js), and `0` is the documented
|
|
372
372
|
* way to opt *out* of a ring the node would otherwise get.
|
|
373
373
|
*/
|
|
374
374
|
export const focusRingStyle = (theme, on) =>
|
package/src/debug.js
CHANGED
|
@@ -243,7 +243,7 @@ function stackForSeq(X, seq16) {
|
|
|
243
243
|
|
|
244
244
|
/**
|
|
245
245
|
* Every 2d context this app paints windows through. react-x11 caches one
|
|
246
|
-
* per window node (
|
|
246
|
+
* per window node (`WindowNode`), which is where ntk keeps `shapeStats`.
|
|
247
247
|
*/
|
|
248
248
|
function contextsOf(app) {
|
|
249
249
|
return (app?._rootChildren ?? [])
|
package/src/decorations.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The two decorations that are not a colour: the gradient behind a box
|
|
2
2
|
// (`backgroundImage`) and the shadow under it (`boxShadow`). Both are pure
|
|
3
|
-
// here — strings in, numbers out — so the renderer half in `nodes.js` is
|
|
3
|
+
// here — strings in, numbers out — so the renderer half in `nodes/boxpaint.js` is
|
|
4
4
|
// only geometry and compositing, and the parsing can be tested without a
|
|
5
5
|
// server (issue #345).
|
|
6
6
|
//
|
package/src/editmenu.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* wiring at all. That rules out building it from the `Menu` components,
|
|
7
7
|
* which live a layer above the nodes and cannot be mounted from one — so
|
|
8
8
|
* the rows are measured and drawn here, and a node hangs them in a
|
|
9
|
-
* `<popup>` (`openEditMenu`, nodes.js, which is the public seam).
|
|
9
|
+
* `<popup>` (`openEditMenu`, nodes/editmenupopup.js, which is the public seam).
|
|
10
10
|
*
|
|
11
11
|
* Deliberately free of node imports: this takes plain data, a measuring
|
|
12
12
|
* function and a 2d context. It can be tested without a tree, and falls
|
|
@@ -64,7 +64,7 @@ export function editMenuColors(theme) {
|
|
|
64
64
|
/**
|
|
65
65
|
* The rows, in the standard order, each enabled exactly when it would do
|
|
66
66
|
* something — the whole of the menu's policy, as a pure function of the
|
|
67
|
-
* verbs a target offers (`openEditMenu`, nodes.js).
|
|
67
|
+
* verbs a target offers (`openEditMenu`, nodes/editmenupopup.js).
|
|
68
68
|
*
|
|
69
69
|
* **A verb that was not handed over is a row that is not there**, rather
|
|
70
70
|
* than a greyed one. It was motivated by `<textinput sensitive>`, where a
|
package/src/errors.js
CHANGED
|
@@ -59,6 +59,35 @@ export function reportHandlerError(node, handler, error) {
|
|
|
59
59
|
markFailed();
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* A throw from a layout or a placement (docs/extending.md) — code that runs
|
|
64
|
+
* inside a frame, where no error boundary is on the stack either. Reported
|
|
65
|
+
* the way a handler's throw is, `onUncaughtError` included, and the frame
|
|
66
|
+
* carries on with the fallback `consequence` names: one bad algorithm must
|
|
67
|
+
* not stop the window from painting.
|
|
68
|
+
*/
|
|
69
|
+
export function reportLayoutError(node, what, error, consequence) {
|
|
70
|
+
const element = node?.kind ? `<${node.kind}>` : '(unknown)';
|
|
71
|
+
const owner = ownerName(node);
|
|
72
|
+
const custom = node?.app && perContainer.get(node.app);
|
|
73
|
+
if (custom) {
|
|
74
|
+
custom(error, {
|
|
75
|
+
componentStack: owner ? `\n in ${owner}` : undefined,
|
|
76
|
+
element,
|
|
77
|
+
handler: what,
|
|
78
|
+
node,
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
console.error(
|
|
83
|
+
`react-x11: ${what} on ${element}${owner ? ` in ${owner}` : ''} threw. ` +
|
|
84
|
+
'It ran inside a frame rather than a render, so no error boundary ' +
|
|
85
|
+
`could catch it. ${consequence}.`,
|
|
86
|
+
error,
|
|
87
|
+
);
|
|
88
|
+
markFailed();
|
|
89
|
+
}
|
|
90
|
+
|
|
62
91
|
/**
|
|
63
92
|
* `REACT_X11_STRICT_TOKENS=1` makes a `$token` the theme does not define
|
|
64
93
|
* fatal again, for a build that would rather stop than paint something
|
|
@@ -104,6 +133,23 @@ export function reportStyleError(
|
|
|
104
133
|
markFailed();
|
|
105
134
|
}
|
|
106
135
|
|
|
136
|
+
/**
|
|
137
|
+
* A style that asks for something that is not there — a layout or a
|
|
138
|
+
* placement nobody registered, an option of the wrong type. Reported and
|
|
139
|
+
* carried on from, once per node and message, the way an unknown token is;
|
|
140
|
+
* with no strict switch, because the fallback it names is a real one and
|
|
141
|
+
* not a property silently dropped.
|
|
142
|
+
*/
|
|
143
|
+
export function reportStyleProblem(node, message, consequence) {
|
|
144
|
+
const seen = reportedStyleErrors.get(node);
|
|
145
|
+
if (seen?.has(message)) return;
|
|
146
|
+
if (seen) seen.add(message);
|
|
147
|
+
else reportedStyleErrors.set(node, new Set([message]));
|
|
148
|
+
const owner = ownerName(node);
|
|
149
|
+
console.error(`${message}${owner ? ` — in ${owner}` : ''}. ${consequence}.`);
|
|
150
|
+
markFailed();
|
|
151
|
+
}
|
|
152
|
+
|
|
107
153
|
/** Wrap a call to user code so a throw is reported instead of escaping. */
|
|
108
154
|
export function callHandler(node, handler, fn, ev) {
|
|
109
155
|
try {
|