react-x11 2.40.0 → 2.41.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.
@@ -21,6 +21,15 @@
21
21
  // (src/cocoa/panehost.js), because nothing in a pane process runs AppKit.
22
22
  import { BackendContext2D } from '../backend/context2d.js';
23
23
 
24
+ // How long a pane counts on the host's `pane-shown` for a present before it
25
+ // stops waiting for one (`_answerOverdue`). A host that is only busy answers
26
+ // well inside it — in the browser example on a Mac, a present was looked up
27
+ // under a millisecond after it went at the median, and 180ms after at
28
+ // worst, the host laying out a page at each width of a drag on its first
29
+ // run — and one that never answers, a host stalled or gone, must not stop
30
+ // the pane drawing.
31
+ const ANSWER_TIMEOUT_MS = 250;
32
+
24
33
  let nextPaneId = 1;
25
34
 
26
35
  export class CocoaPaneWindow {
@@ -57,7 +66,23 @@ export class CocoaPaneWindow {
57
66
  this._dirty = false;
58
67
  this._flushDamage = 'full';
59
68
  this._seq = 0;
69
+ // a size from the host not yet presented (`pane-sized`)
70
+ this._owesSized = false;
60
71
  this._presentedAt = -Infinity;
72
+ // the window's own frame clock (`CocoaApp._frameDue`), at the app's
73
+ // interval: a pane has no display of its own to pace by
74
+ this._rafLast = 0;
75
+ // The newest present the host has looked up (`pane-shown`), and the
76
+ // buffers of retired rings still named by a present it has not: freed
77
+ // as it does (`_releaseRing`).
78
+ this._shownSeq = 0;
79
+ this._shownAt = -Infinity;
80
+ // what it had looked up before its latest answers, whose buffer the
81
+ // screen may still show (`_takeBack`)
82
+ this._shownBefore = 0;
83
+ this._pollTimer = null;
84
+ this._kickQueued = false;
85
+ this._retired = [];
61
86
  this._reactX11Node = null;
62
87
  // Where the pane's corner is on the screen, in device pixels, as the
63
88
  // host last said (`setPaneSize`): what a popup the pane anchors to one
@@ -89,6 +114,10 @@ export class CocoaPaneWindow {
89
114
  const moved =
90
115
  screen != null && (screen.x !== this.x || screen.y !== this.y);
91
116
  const resized = w !== this.width || h !== this.height;
117
+ // The host sends the next rect when this one is painted (`pane-sized`,
118
+ // src/frame/index.js): one that changes no size is painted already.
119
+ if (resized) this._owesSized = true;
120
+ else this._post({ type: 'pane-sized' });
92
121
  if (!moved && !resized) return;
93
122
  if (moved) {
94
123
  this.x = screen.x;
@@ -143,8 +172,10 @@ export class CocoaPaneWindow {
143
172
  this.app._paneSend?.(msg);
144
173
  }
145
174
 
175
+ /** On this window's clock, which the clock asks before it runs a frame
176
+ * (`frameHeld`). */
146
177
  requestAnimationFrame(cb) {
147
- return this.app._requestFrame(cb);
178
+ return this.app._requestFrame(cb, this);
148
179
  }
149
180
 
150
181
  /** A `<glarea>`'s frame, after this pane's own (`CocoaWindow`'s). */
@@ -152,34 +183,140 @@ export class CocoaPaneWindow {
152
183
  return this.app._requestFrame(cb, null, true);
153
184
  }
154
185
 
186
+ /** The clock's period (`CocoaApp._frameDue`): the app's, a pane having no
187
+ * display of its own. */
188
+ get _frameInterval() {
189
+ return this.app.frameIntervalFor(null);
190
+ }
191
+
192
+ /** What is on glass is the host's to know; a pane draws what it is
193
+ * asked to (`CocoaApp._tickFrames`). */
194
+ _visible() {
195
+ return true;
196
+ }
197
+
155
198
  /**
156
199
  * Whether the host may still be showing the frame before the last one —
157
200
  * the gate `flushPendingFrames` (src/frames.js) is written around: a
158
201
  * discrete input paints on the spot only when the last frame has landed,
159
202
  * which is what folds a burst into one paced frame instead of a frame per
160
- * event. On X11 the server says when a present was shown; a pane hears
161
- * nothing back from the host, which flips the layer on its next pump tick
162
- * and has Core Animation scan it out at the following refresh — so a
163
- * present counts as in flight for one frame interval. Without a gate here
164
- * every message the host queued while the pane was busy was answered
165
- * with a full frame of its own: a forty-tick resize of a pane whose frame
166
- * costs 300ms stepped through forty sizes for twelve seconds after the
167
- * drag had ended, each one the previous surface stretched to the layer.
203
+ * event. Without a gate here every message the host queued while the
204
+ * pane was busy was answered with a full frame of its own: a forty-tick
205
+ * resize of a pane whose frame costs 300ms stepped through forty sizes
206
+ * for twelve seconds after the drag had ended, each one the previous
207
+ * surface stretched to the layer.
208
+ *
209
+ * On X11 the server says when a present was shown. Here the host says
210
+ * when it has looked one up (`pane-shown`), which is half of it: the host
211
+ * commits on its own thread and the screen shows the frame at the
212
+ * refresh after. So a present is in flight until it is answered, and for
213
+ * a frame interval after it went: measured in the browser example on a
214
+ * 120Hz panel, the answer alone answered an input 4ms sooner, and gave
215
+ * input faster than the display a frame each — 245 a second at 250
216
+ * inputs a second, 355 at 1000 — where the interval keeps it at 118, and
217
+ * the frames that outran the WindowServer drew into buffers it still
218
+ * read. Nothing the pane can ask says when a frame is on glass: a buffer
219
+ * reads in use from the host's lookup, not from the refresh. And no frame
220
+ * is drawn while one is held (`frameHeld`), whatever asks.
168
221
  */
169
222
  frameInFlight() {
223
+ if (this.frameHeld()) return true;
224
+ if (this._seq > this._shownSeq && !this._answerOverdue()) return true;
170
225
  return (
171
226
  performance.now() - this._presentedAt < this.app.frameIntervalFor(null)
172
227
  );
173
228
  }
174
229
 
230
+ /**
231
+ * Whether this pane's next frame has no buffer to go into, which holds it
232
+ * on the clock (`CocoaApp._tickFrames`): every buffer but the one it last
233
+ * presented is named by a present the host is showing or has yet to look
234
+ * up, or the host is done with one and the WindowServer is still reading
235
+ * it. A frame drawn into one of those anyway was a frame half drawn on
236
+ * glass — the host showed the buffer the pane was drawing into, or looked
237
+ * it up after the pane had started, and showed that.
238
+ *
239
+ * The second is the host coming back from a long frame of its own. Its
240
+ * answers say it has looked the queued presents up, not that they are on
241
+ * glass: that is its UI thread's commit and the refresh after, and until
242
+ * then the buffer it showed all along is still what the screen shows —
243
+ * and which buffer that is, the answers cannot say, since a host that
244
+ * stalled between a lookup and its commit left the one before on glass.
245
+ * The WindowServer can: a buffer is taken once it has let go of one, which
246
+ * is asked every millisecond (`_pollHeld`), and for a frame interval after
247
+ * the host's answer at most, since it reads a buffer it no longer shows
248
+ * for as long as it likes and a pane must not wait on that.
249
+ *
250
+ * A new size is never held: it draws into a new ring, which nothing
251
+ * names. Nor is a host that stopped answering (`_answerOverdue`).
252
+ */
253
+ frameHeld() {
254
+ if (!this._ring || this._surface) return false;
255
+ if (
256
+ this._surfaceSize?.width !== this.width ||
257
+ this._surfaceSize?.height !== this.height
258
+ ) {
259
+ return false;
260
+ }
261
+ let done = false;
262
+ for (let i = 0; i < this._ring.length; i += 1) {
263
+ const s = this._ring[i];
264
+ if (i === this._shownIndex || !this._done(s)) continue;
265
+ if (!this._held(s)) return false;
266
+ done = true;
267
+ }
268
+ if (!done) return !this._answerOverdue();
269
+ const wait =
270
+ this._shownAt + this.app.frameIntervalFor(null) - performance.now();
271
+ if (!(wait > 0)) return false;
272
+ this._pollHeld();
273
+ return true;
274
+ }
275
+
276
+ /**
277
+ * Ask again in a millisecond whether the WindowServer has let go of a
278
+ * buffer (`frameHeld`): it says so by no event, and the pump's next tick
279
+ * is up to 8ms off — a frame held that long for a buffer let go of a
280
+ * millisecond later was a frame late.
281
+ */
282
+ _pollHeld() {
283
+ if (this._pollTimer) return;
284
+ this._pollTimer = setTimeout(() => {
285
+ this._pollTimer = null;
286
+ const app = this.app;
287
+ if (!this.destroyed && app._rafQueue.some((e) => e.wnd === this)) {
288
+ app._tickFrames();
289
+ app._presentAll();
290
+ }
291
+ }, 1);
292
+ this._pollTimer.unref?.();
293
+ }
294
+
295
+ /** Whether the host has looked up a present after the last one that named
296
+ * `buffer`, or none ever did: nothing will show it again. */
297
+ _done(buffer) {
298
+ return !buffer.presentedSeq || buffer.presentedSeq < this._shownSeq;
299
+ }
300
+
301
+ /** A present the host has not answered for longer than it would if it
302
+ * were answering at all (`ANSWER_TIMEOUT_MS`). */
303
+ _answerOverdue() {
304
+ return (
305
+ this._seq > this._shownSeq &&
306
+ performance.now() - this._presentedAt >= ANSWER_TIMEOUT_MS
307
+ );
308
+ }
309
+
175
310
  // Three buffers, not two. A pane is cross-process: the host keeps
176
311
  // scanning the last buffer it was handed until it processes the next
177
- // present message, and nothing here waits for that. With two buffers the
178
- // next paint (and the catch-up copy) lands in the very buffer the host is
179
- // still displaying — every present tears it, the flash. A third buffer is
180
- // always at least two presents behind what the host shows, so the pane
181
- // never writes a buffer the host might still be reading. Same-process
182
- // windows need only two because Core Animation latches the front buffer.
312
+ // present message. With two buffers the next paint (and the catch-up
313
+ // copy) lands in the very buffer the host is still displaying — every
314
+ // present tears it, the flash. With three, one present may be on its way
315
+ // while the next frame draws: the buffer the host shows, the one the
316
+ // present on its way names, and the one the host has let go of. A host
317
+ // two presents behind leaves none, and the frame waits for its answer
318
+ // (`frameHeld`, `_takeBack`). Same-process windows need only two because
319
+ // Core Animation latches the front buffer.
183
320
  static RING = 3;
184
321
 
185
322
  /**
@@ -188,11 +325,26 @@ export class CocoaPaneWindow {
188
325
  * drag resizes the pane a tick at a time, and each tick retires a ring
189
326
  * that the finalizer would have held until a collection happened to run.
190
327
  * The host keeps its own reference to whichever buffer its layer shows,
191
- * so the frame on glass survives the free; a present of a retired buffer
192
- * still in the channel finds no surface, and the host drops it
193
- * (`CocoaPaneHost.present`) — the next present is the full frame on the
194
- * new ring anyway. Bridges before 0.4 have no `releaseSurface`; there the
195
- * finalizer is still the only owner, and this is the drop it always was.
328
+ * so the frame on glass survives the free.
329
+ *
330
+ * All but a buffer a present names that the host has not looked up yet.
331
+ * The host finds a buffer by its IOSurface id, and the system hands a
332
+ * freed id to the next surface anyone makes — the three this ring's
333
+ * successor is about to make, the ring of another pane resized in the
334
+ * same layout, the host's own window. A present still in the channel
335
+ * named the id, not the surface, so the host scanned out whatever had it
336
+ * by the time it looked: a frame of this pane's next ring, cleared or
337
+ * half drawn, or another tab's page, for a frame at every few steps of a
338
+ * drag. So that buffer is kept (`_retired`) until the host says it has
339
+ * looked it up (`pane-shown`, `_shown`), and only then freed: its id
340
+ * stays its own until nothing can ask for it. That is the buffers of
341
+ * the presents in flight — one, as a rule, since a present goes a frame
342
+ * at a time and the host answers on its next turn — and never more than
343
+ * a ring's worth: a host that stops answering has the oldest freed
344
+ * anyway, the one it is likeliest to have looked up already.
345
+ *
346
+ * Bridges before 0.4 have no `releaseSurface`; there the finalizer is
347
+ * still the only owner, and this is the drop it always was.
196
348
  */
197
349
  _releaseRing() {
198
350
  const ring = this._ring;
@@ -201,7 +353,58 @@ export class CocoaPaneWindow {
201
353
  if (!ring) return;
202
354
  const release = this._native.releaseSurface;
203
355
  if (typeof release !== 'function') return;
204
- for (const s of ring) release.call(this._native, s.handle);
356
+ for (const s of ring) {
357
+ if (s.presentedSeq > this._shownSeq) this._retired.push(s);
358
+ else release.call(this._native, s.handle);
359
+ }
360
+ while (this._retired.length > CocoaPaneWindow.RING) {
361
+ release.call(this._native, this._retired.shift().handle);
362
+ }
363
+ }
364
+
365
+ /**
366
+ * The host has looked up every present up to `seq` (`pane-shown`): the
367
+ * retired buffers they named are its now, or nobody's, and go.
368
+ */
369
+ _shown(seq) {
370
+ if (!(seq > this._shownSeq)) return;
371
+ // the first of a run of answers: what was looked up before it
372
+ if (!this._kickQueued) this._shownBefore = this._shownSeq;
373
+ this._shownSeq = seq;
374
+ this._shownAt = performance.now();
375
+ if (this._retired.length > 0) {
376
+ const release = this._native.releaseSurface;
377
+ this._retired = this._retired.filter((s) => {
378
+ if (s.presentedSeq > seq) return true;
379
+ release.call(this._native, s.handle);
380
+ return false;
381
+ });
382
+ }
383
+ // A frame the clock held for want of a buffer (`frameHeld`) goes now,
384
+ // not on the next pump tick — once every answer that came with this one
385
+ // is in. A host back from a long frame answers a run of presents at
386
+ // once, the channel hands them over in one turn, and a frame run on the
387
+ // first found only the buffer the host had shown all along.
388
+ if (this._kickQueued) return;
389
+ this._kickQueued = true;
390
+ queueMicrotask(() => {
391
+ this._kickQueued = false;
392
+ const app = this.app;
393
+ if (!this.destroyed && app._rafQueue.some((e) => e.wnd === this)) {
394
+ app._tickFrames();
395
+ app._presentAll();
396
+ }
397
+ });
398
+ }
399
+
400
+ /** Every retired buffer, answered or not: the window is going, and so is
401
+ * the layer the host showed it on. */
402
+ _releaseRetired() {
403
+ const retired = this._retired;
404
+ this._retired = [];
405
+ const release = this._native.releaseSurface;
406
+ if (typeof release !== 'function') return;
407
+ for (const s of retired) release.call(this._native, s.handle);
205
408
  }
206
409
 
207
410
  _ensureSurface() {
@@ -230,10 +433,88 @@ export class CocoaPaneWindow {
230
433
  // decided when the flush reports its rects — see CocoaWindow's
231
434
  // `_ensureSurface` for why not a queued full frame from here
232
435
  if (hadSurface) this._freshSurface = true;
436
+ } else if (!this._surface) {
437
+ this._takeBack();
233
438
  }
234
439
  return this._surface;
235
440
  }
236
441
 
442
+ /**
443
+ * The buffer the frame about to be drawn goes into, taken at its first
444
+ * draw rather than at the present before it, so the host has had as long
445
+ * as it can to answer — CocoaWindow's `_takeBack`, over a ring the host
446
+ * reads from another process.
447
+ *
448
+ * One the host is done with (`_done`): a present after the last one that
449
+ * named it has been looked up, so it is neither on the layer nor on its
450
+ * way there. Of those, one the WindowServer has let go of as well —
451
+ * `surfaceIsInUse` false: it reads a buffer for a refresh or two after
452
+ * the layer has another, 8ms after the pane hears the answer at the
453
+ * median and 22 at the 90th percentile in the browser example — and of
454
+ * those the one presented longest ago. The clock holds a frame that would
455
+ * find none (`frameHeld`). One that comes anyway, once the WindowServer
456
+ * has held on for a frame interval, takes one it still reads — but the
457
+ * one the screen showed before the host's latest answers last, which a
458
+ * host back from a long frame has yet to commit the replacement for; and
459
+ * once the host is overdue, it takes a buffer whose present the host has
460
+ * yet to look up
461
+ * rather than the one on the layer: a host that comes back looks up
462
+ * everything queued in one turn, and only the last of it reaches glass,
463
+ * where the buffer on the layer is on glass now.
464
+ *
465
+ * Then the frame just presented, copied across whole: in a ring of three
466
+ * the buffer taken was last drawn two presents ago or more, and is stale
467
+ * everywhere. After a full frame as much as a partial one: what a frame
468
+ * was says nothing of what the next will be, and skipping the copy there
469
+ * had a partial frame after a full one — a status line, a hover's
470
+ * underline — drawn over the page as it was two frames back. The copy is
471
+ * wasted only where the next frame is full as well.
472
+ */
473
+ _takeBack() {
474
+ const ring = this._ring;
475
+ const last = ring[this._shownIndex];
476
+ let pick = -1;
477
+ let rank = Infinity;
478
+ for (let i = 0; i < ring.length; i += 1) {
479
+ const s = ring[i];
480
+ if (s === last) continue;
481
+ // done and let go of, then done, then on its way, then on the layer:
482
+ // the oldest of the best — but of the done, the one the screen showed
483
+ // before the host's latest answers last
484
+ const kind = this._done(s)
485
+ ? this._held(s)
486
+ ? 1 + Number(s.presentedSeq === this._shownBefore)
487
+ : 0
488
+ : s.presentedSeq > this._shownSeq
489
+ ? 3
490
+ : 4;
491
+ const r = kind * 2 ** 32 + (s.presentedSeq ?? 0);
492
+ if (r < rank) {
493
+ rank = r;
494
+ pick = i;
495
+ }
496
+ }
497
+ const back = ring[pick];
498
+ this._drawIndex = pick;
499
+ this._surface = back.handle;
500
+ // a different native surface owns the graphics state now — the context
501
+ // re-syncs its sticky state off the generation
502
+ this._surfaceGen++;
503
+ this._native.surfaceLock(back.handle);
504
+ if (last) this._native.copySurfaceRegion(last.handle, back.handle, null);
505
+ }
506
+
507
+ /** Whether the WindowServer still reads `buffer`: `IOSurfaceIsInUse`,
508
+ * which counts the host's render server from this process too. A bridge
509
+ * that cannot say (before 0.10) is answered no. */
510
+ _held(buffer) {
511
+ const inUse = this._native.surfaceIsInUse;
512
+ return (
513
+ typeof inUse === 'function' &&
514
+ inUse.call(this._native, buffer.handle) === true
515
+ );
516
+ }
517
+
237
518
  getContext() {
238
519
  if (!this._ctx) {
239
520
  this._ctx = new BackendContext2D(
@@ -278,10 +559,12 @@ export class CocoaPaneWindow {
278
559
  }
279
560
 
280
561
  scrollRegion(rect, dx, dy) {
281
- if (!this._surface) return false;
562
+ if (!this._ring) return false;
282
563
  if (!Number.isInteger(dx) || !Number.isInteger(dy)) return false;
564
+ // the frame's first draw, which takes its buffer (`_takeBack`)
565
+ const surface = this._ensureSurface();
283
566
  const moved = this._native.scrollSurface(
284
- this._surface,
567
+ surface,
285
568
  Math.round(rect.x),
286
569
  Math.round(rect.y),
287
570
  Math.round(rect.width),
@@ -301,33 +584,28 @@ export class CocoaPaneWindow {
301
584
  const shown = this._ring[this._drawIndex];
302
585
  this._native.surfaceUnlock(shown.handle);
303
586
  this._flushDamage = null;
587
+ // the buffer is named in the channel until the host says otherwise
588
+ // (`_releaseRing`, `_done`)
589
+ shown.presentedSeq = ++this._seq;
590
+ this._presentedAt = performance.now();
591
+ this._shownIndex = this._drawIndex;
592
+ // the next frame takes its buffer when it draws (`_takeBack`)
593
+ this._drawIndex = -1;
594
+ this._surface = null;
595
+ // last: the answer runs a held frame (`_shown`), which must find this
596
+ // present made
304
597
  this._post({
305
598
  type: 'pane-present',
306
- seq: ++this._seq,
599
+ seq: this._seq,
307
600
  id: shown.iosurfaceId,
308
601
  width: this.width,
309
602
  height: this.height,
310
603
  });
311
- this._presentedAt = performance.now();
312
- this._shownIndex = this._drawIndex;
313
- // the next buffer round the ring — two behind what the host will be
314
- // showing, so it is safe to write even before the host has switched
315
- this._drawIndex = (this._drawIndex + 1) % CocoaPaneWindow.RING;
316
- const next = this._ring[this._drawIndex];
317
- this._surface = next.handle;
318
- this._surfaceGen++;
319
- this._native.surfaceLock(next.handle);
320
- // The next frame may paint only its damage, so `next` must first hold
321
- // the last complete frame underneath — and the WHOLE of it, not just
322
- // this frame's rects, because in a three-buffer ring `next` was last
323
- // drawn two presents ago and is stale everywhere. A full copy of the
324
- // just-shown buffer is the complete background; the safe target is what
325
- // triple buffering buys. After a full frame as much as a partial one:
326
- // what a frame was says nothing of what the next will be, and skipping
327
- // the copy there had a partial frame after a full one — a status line,
328
- // a hover's underline — drawn over the page as it was two frames back.
329
- // The copy is wasted only where the next frame is full as well.
330
- this._native.copySurfaceRegion(shown.handle, next.handle, null);
604
+ // and the size the host last sent is on screen (`pane-sized`)
605
+ if (this._owesSized) {
606
+ this._owesSized = false;
607
+ this._post({ type: 'pane-sized' });
608
+ }
331
609
  }
332
610
 
333
611
  snapshot() {
@@ -338,7 +616,10 @@ export class CocoaPaneWindow {
338
616
  if (this.destroyed) return;
339
617
  this.destroyed = true;
340
618
  this.app._unregisterWindow(this);
619
+ clearTimeout(this._pollTimer);
620
+ this._pollTimer = null;
341
621
  this._releaseRing();
622
+ this._releaseRetired();
342
623
  }
343
624
  }
344
625
 
@@ -975,7 +975,15 @@ export class CocoaWindow {
975
975
  * painting a state the next event in the same batch replaces.
976
976
  */
977
977
  frameInFlight() {
978
- if (this.app._batching) return true;
978
+ return this.app._batching || this.frameHeld();
979
+ }
980
+
981
+ /**
982
+ * Whether the frame clock holds this window's next frame
983
+ * (`CocoaApp._tickFrames`): the fence half of `frameInFlight`, without the
984
+ * batch, which the clock never ticks inside.
985
+ */
986
+ frameHeld() {
979
987
  return (
980
988
  this._awaiting != null &&
981
989
  this._surfaceSize?.width === this.width &&
package/src/events.js CHANGED
@@ -1672,6 +1672,18 @@ export class EventManager {
1672
1672
  // its ordinary turn.
1673
1673
  if (step.consumed) return;
1674
1674
  }
1675
+ // A key that typed more than one character has no code point to type
1676
+ // them with: a backend whose keyboard layout composes on its own says
1677
+ // so with `text` — the accent and a letter it does not combine with,
1678
+ // `´s` (src/cocoa/keymap.js). Committed the way an input method's text
1679
+ // is, a composition that opens and closes on this key, which is where
1680
+ // an element that types inserts it — and like a composed key, it is
1681
+ // the key's whole answer.
1682
+ if (native.text && target.composes !== false) {
1683
+ this._composition('Start', target, '', native);
1684
+ this._composition('End', target, String(native.text), native);
1685
+ return;
1686
+ }
1675
1687
  // The element's own behaviour first, focus traversal after it — Tab is
1676
1688
  // an ordinary defaultable key rather than one the focus manager eats on
1677
1689
  // the way past. Cycling first meant an editor could only keep Tab as an
@@ -192,6 +192,11 @@ function shallowEqual(a, b) {
192
192
 
193
193
  const EMPTY_PROPS = {};
194
194
 
195
+ /** How long a rect sent to a pane that acknowledges sizes waits for its
196
+ * `pane-sized` before the newest is sent anyway: a frame of a slow page,
197
+ * several times over, so a lost answer costs a pause and not the size. */
198
+ export const PANE_SIZED_WAIT_MS = 500;
199
+
195
200
  /**
196
201
  * A module of this application, mounted in its own process, its window
197
202
  * embedded here.
@@ -552,7 +557,54 @@ function PaneHostView({
552
557
  // moves when the box does and when the window does, so it is sent on
553
558
  // layout, on a scroll that shifts the box, and on every anchor change
554
559
  // of the window, which a move of the window is.
560
+ //
561
+ // **The newest rect wins.** A drag sends the host a size a frame, and a
562
+ // pane whose page takes longer than that to lay out was sent them
563
+ // faster than it could take them: a pipe hands a pane process one
564
+ // message a turn of its loop, with a frame in between, so the pane laid
565
+ // out and painted every size the drag passed through, one a frame, and
566
+ // went on resizing seconds after the edge stopped. A pane that says it
567
+ // has painted a size (`pane-sized`) is sent one rect at a time: what
568
+ // the host comes to while one is out replaces what was waiting, and goes
569
+ // when the pane says it has painted the last. A pane that never says so
570
+ // is sent every rect, as before, and one that goes quiet is sent the
571
+ // newest anyway after `PANE_SIZED_WAIT_MS`.
555
572
  let sent = null;
573
+ let pending = null;
574
+ let acks = false;
575
+ let inFlight = false;
576
+ let waiting = null;
577
+ const transmit = (rect) => {
578
+ sent = rect;
579
+ s.trySend?.({
580
+ type: 'pane-rect',
581
+ width: rect.width,
582
+ height: rect.height,
583
+ scale: rect.scale,
584
+ ...(rect.screen && { screen: rect.screen }),
585
+ });
586
+ if (!acks) return;
587
+ inFlight = true;
588
+ if (waiting) clearTimeout(waiting);
589
+ waiting = setTimeout(sized, PANE_SIZED_WAIT_MS);
590
+ };
591
+ const sameRect = (a, b) =>
592
+ !!a &&
593
+ !!b &&
594
+ a.width === b.width &&
595
+ a.height === b.height &&
596
+ a.scale === b.scale &&
597
+ a.screen?.x === b.screen?.x &&
598
+ a.screen?.y === b.screen?.y;
599
+ const sized = () => {
600
+ if (waiting) clearTimeout(waiting);
601
+ waiting = null;
602
+ inFlight = false;
603
+ const next = pending;
604
+ pending = null;
605
+ // a drag that came back to where it was sent from waits for nothing
606
+ if (next && !sameRect(next, sent)) transmit(next);
607
+ };
556
608
  const syncSize = () => {
557
609
  host.setRect(node.abs);
558
610
  const sc = node.scale ?? 1;
@@ -565,23 +617,10 @@ function PaneHostView({
565
617
  y: Math.round(origin.y + node.abs.y),
566
618
  }
567
619
  : null;
568
- if (
569
- sent &&
570
- sent.width === width &&
571
- sent.height === height &&
572
- sent.screen?.x === screen?.x &&
573
- sent.screen?.y === screen?.y
574
- ) {
575
- return;
576
- }
577
- sent = { width, height, screen };
578
- s.trySend?.({
579
- type: 'pane-rect',
580
- width,
581
- height,
582
- scale: sc,
583
- ...(screen && { screen }),
584
- });
620
+ const rect = { width, height, scale: sc, screen };
621
+ if (sameRect(rect, pending ?? sent)) return;
622
+ if (inFlight) pending = rect;
623
+ else transmit(rect);
585
624
  };
586
625
  const origAbsolutize = node.absolutize.bind(node);
587
626
  node.absolutize = (ox, oy) => {
@@ -625,6 +664,11 @@ function PaneHostView({
625
664
  } else if (msg?.type === 'pane-present') {
626
665
  host.setRect(node.abs);
627
666
  host.present(msg.id, msg);
667
+ } else if (msg?.type === 'pane-sized') {
668
+ // the pane has painted the rect it was last sent: the newest one
669
+ // waiting goes now, and from here on one is out at a time
670
+ acks = true;
671
+ sized();
628
672
  } else if (msg?.type === 'pane-cursor') {
629
673
  // The pane names its cursor as an element that draws names one for
630
674
  // a point: this box is the node the host's pointer is over, so the
@@ -637,6 +681,8 @@ function PaneHostView({
637
681
  });
638
682
  return () => {
639
683
  off();
684
+ if (waiting) clearTimeout(waiting);
685
+ waiting = null;
640
686
  offAnchor?.();
641
687
  offFocus?.();
642
688
  node.defaultCursor = undefined;