react-x11 2.21.2 → 2.22.1
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 +2 -2
- package/src/gloverlay.js +8 -48
- package/src/node.d.ts +11 -1
- package/src/nodes/damage.js +52 -29
- package/src/nodes/invalidate.js +13 -1
- package/src/nodes/layout.js +24 -49
- package/src/nodes/moveblit.js +610 -0
- package/src/nodes/node.js +14 -15
- package/src/nodes/paint.js +103 -8
- package/src/nodes/position.js +1 -0
- package/src/nodes/rects.js +101 -6
- package/src/nodes/roundclip.js +214 -0
- package/src/nodes/scrollable.js +9 -4
- package/src/nodes/scrollbars.js +9 -3
- package/src/nodes/scrollblit.js +285 -92
- package/src/nodes/window/flush.js +12 -0
- package/src/nodes/window/window.js +8 -0
package/src/nodes/paint.js
CHANGED
|
@@ -7,7 +7,13 @@ import { Surface } from '../offscreen.js';
|
|
|
7
7
|
import { isPaintedColor } from './boxpaint.js';
|
|
8
8
|
import { DAMAGE_SLOP } from './damage.js';
|
|
9
9
|
import { DRAWN_KINDS } from './kinds.js';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
intersectRects,
|
|
12
|
+
isIntegerRect,
|
|
13
|
+
rectsOverlap,
|
|
14
|
+
roundedCorners,
|
|
15
|
+
} from './rects.js';
|
|
16
|
+
import { keepCorners } from './roundclip.js';
|
|
11
17
|
import { DEV } from './util.js';
|
|
12
18
|
import { devWarnWindowShadow } from './window/capabilities.js';
|
|
13
19
|
import { FLASH_COLORS, debugPaint } from './window/debugpaint.js';
|
|
@@ -289,12 +295,12 @@ export class NodePaint {
|
|
|
289
295
|
* truncates, and then almost every cell's text fits — 191 clips a frame, of
|
|
290
296
|
* which a handful do anything.
|
|
291
297
|
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
* inset is for antialiasing, which
|
|
298
|
+
* A rectangle test, so it answers for the box's rectangle only: rounded
|
|
299
|
+
* corners can cut a child this says fits, which `_paintRoundedChildren`
|
|
300
|
+
* settles before it asks. The one-pixel inset is for antialiasing, which
|
|
301
|
+
* can put ink just outside a glyph's box.
|
|
295
302
|
*/
|
|
296
303
|
_childrenCanOverflow() {
|
|
297
|
-
if (this.style?.borderRadius) return true;
|
|
298
304
|
const box = this.abs;
|
|
299
305
|
for (const child of this.children) {
|
|
300
306
|
if (child.isWindow || !child.yoga || child.hidden) continue;
|
|
@@ -320,19 +326,108 @@ export class NodePaint {
|
|
|
320
326
|
if (this._ownPaintOnly) return;
|
|
321
327
|
const order = this.paintOrder();
|
|
322
328
|
if (order.length === 0) return;
|
|
323
|
-
|
|
329
|
+
if (!this.clipsChildren()) {
|
|
330
|
+
this._paintChildList(ctx, order);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// a context with no `roundRect` has always clipped a rounded box to its
|
|
334
|
+
// rectangle, the way `_roundedPath` degrades
|
|
335
|
+
const radius =
|
|
336
|
+
typeof ctx.roundRect === 'function' ? (this.style.borderRadius ?? 0) : 0;
|
|
337
|
+
if (radius > 0 && this._paintRoundedChildren(ctx, order, radius)) return;
|
|
338
|
+
const clip = this._childrenCanOverflow();
|
|
324
339
|
if (clip) {
|
|
325
340
|
ctx.save();
|
|
326
|
-
this._roundedPath(ctx,
|
|
341
|
+
this._roundedPath(ctx, 0);
|
|
327
342
|
ctx.clip();
|
|
328
343
|
}
|
|
344
|
+
this._paintChildList(ctx, order);
|
|
345
|
+
if (clip) ctx.restore();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** `order` painted in turn, each child culled against the window, its
|
|
349
|
+
* clipping ancestors and the pass. */
|
|
350
|
+
_paintChildList(ctx, order) {
|
|
329
351
|
for (const child of order) {
|
|
330
352
|
if (child._promoted) continue; // on a layer of its own: a hole here
|
|
331
353
|
if (child._offscreen(child.abs, DAMAGE_SLOP)) continue;
|
|
332
354
|
if (child._outsideDamage()) continue;
|
|
333
355
|
child.paint(ctx);
|
|
334
356
|
}
|
|
335
|
-
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Would a pass over `rect` paint this node? The culling `_paintChildList`
|
|
361
|
+
* does, asked of a rect before it is the pass.
|
|
362
|
+
*/
|
|
363
|
+
_paintsInto(rect) {
|
|
364
|
+
if (this._promoted) return false;
|
|
365
|
+
if (!rectsOverlap(this._subtreeBounds(), rect, DAMAGE_SLOP)) return false;
|
|
366
|
+
return !this._offscreen(this.abs, DAMAGE_SLOP);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Children clipped to a rounded outline (issue #685) — false when this pass
|
|
371
|
+
* needs no more than a square box's clip, which the caller then applies.
|
|
372
|
+
*
|
|
373
|
+
* A clip that is not a rectangle is one ntk builds as an a8 mask the size
|
|
374
|
+
* of the window, and every fill and glyph run under it leaves the
|
|
375
|
+
* server-side fast paths for composites through that mask: a pane that
|
|
376
|
+
* clipped its content to a rounded border paid it for every drawing in
|
|
377
|
+
* every pass, to decide a few pixels in each corner. A `<Flow>` with
|
|
378
|
+
* `borderRadius: 6` panned at 10 fps where the square pane ran at 56.
|
|
379
|
+
*
|
|
380
|
+
* The outline is the box's rectangle everywhere but its four corner
|
|
381
|
+
* squares, so this asks which squares the pass reaches that a child draws
|
|
382
|
+
* into. None — a pass that misses them, or a box whose padding keeps its
|
|
383
|
+
* children off them — and the outline clips nothing the rectangle would
|
|
384
|
+
* not. Otherwise the children are drawn once, under the rectangle,
|
|
385
|
+
* and those squares are put back the way they were outside the arc
|
|
386
|
+
* (`keepCorners`): a few composites the size of a corner, whatever the
|
|
387
|
+
* children drew. A context that cannot read its pixels back clips to the
|
|
388
|
+
* outline, as does a box off the pixel grid or one that is corner all the
|
|
389
|
+
* way along — a pill.
|
|
390
|
+
*/
|
|
391
|
+
_paintRoundedChildren(ctx, order, radius) {
|
|
392
|
+
const damage = this.root?._paintDamage ?? null;
|
|
393
|
+
const corners =
|
|
394
|
+
!damage || isIntegerRect(damage)
|
|
395
|
+
? roundedCorners(this.abs, radius)
|
|
396
|
+
: null;
|
|
397
|
+
let kept = null;
|
|
398
|
+
if (corners) {
|
|
399
|
+
// nothing inside the outline is being repainted
|
|
400
|
+
const pass = damage ? intersectRects(damage, this.abs) : this.abs;
|
|
401
|
+
if (!pass) return true;
|
|
402
|
+
const cut = corners.squares.filter((square) => {
|
|
403
|
+
const piece = intersectRects(square, pass);
|
|
404
|
+
return piece && order.some((child) => child._paintsInto(piece));
|
|
405
|
+
});
|
|
406
|
+
if (cut.length === 0) return false;
|
|
407
|
+
kept = keepCorners(ctx, this.app, corners, cut);
|
|
408
|
+
}
|
|
409
|
+
if (!kept) {
|
|
410
|
+
ctx.save();
|
|
411
|
+
this._roundedPath(ctx, radius);
|
|
412
|
+
ctx.clip();
|
|
413
|
+
this._paintChildList(ctx, order);
|
|
414
|
+
ctx.restore();
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
const clip = this._childrenCanOverflow();
|
|
419
|
+
if (clip) {
|
|
420
|
+
ctx.save();
|
|
421
|
+
this._roundedPath(ctx, 0);
|
|
422
|
+
ctx.clip();
|
|
423
|
+
}
|
|
424
|
+
this._paintChildList(ctx, order);
|
|
425
|
+
if (clip) ctx.restore();
|
|
426
|
+
kept.restore();
|
|
427
|
+
} finally {
|
|
428
|
+
kept.release();
|
|
429
|
+
}
|
|
430
|
+
return true;
|
|
336
431
|
}
|
|
337
432
|
|
|
338
433
|
/**
|
package/src/nodes/position.js
CHANGED
package/src/nodes/rects.js
CHANGED
|
@@ -57,6 +57,16 @@ export function intersectRects(a, b) {
|
|
|
57
57
|
return { x, y, width: right - x, height: bottom - y };
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/** `rect` moved by (dx, dy). */
|
|
61
|
+
export function shiftRect(rect, dx, dy) {
|
|
62
|
+
return {
|
|
63
|
+
x: rect.x + dx,
|
|
64
|
+
y: rect.y + dy,
|
|
65
|
+
width: rect.width,
|
|
66
|
+
height: rect.height,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
export function unionRect(a, b) {
|
|
61
71
|
if (!b) return a ?? null;
|
|
62
72
|
if (!a) return { ...b };
|
|
@@ -81,12 +91,12 @@ export function rectsBounds(rects) {
|
|
|
81
91
|
return out;
|
|
82
92
|
}
|
|
83
93
|
|
|
84
|
-
/**
|
|
85
|
-
*
|
|
86
|
-
export function
|
|
94
|
+
/** The four `radius`-sized corner squares of `box` — the only part of a
|
|
95
|
+
* rounded border a translation cannot keep — none when there is no radius. */
|
|
96
|
+
export function cornerSquares(box, radius) {
|
|
87
97
|
const r = Math.min(radius, box.width / 2, box.height / 2);
|
|
88
|
-
if (!(r > 0)) return
|
|
89
|
-
|
|
98
|
+
if (!(r > 0)) return [];
|
|
99
|
+
return [
|
|
90
100
|
{ x: box.x, y: box.y, width: r, height: r },
|
|
91
101
|
{ x: box.x + box.width - r, y: box.y, width: r, height: r },
|
|
92
102
|
{ x: box.x, y: box.y + box.height - r, width: r, height: r },
|
|
@@ -97,7 +107,47 @@ export function cornerSquaresOverlap(box, radius, rect) {
|
|
|
97
107
|
height: r,
|
|
98
108
|
},
|
|
99
109
|
];
|
|
100
|
-
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Does `rect` reach into any of the four `radius`-sized corner squares of
|
|
113
|
+
* `box`? */
|
|
114
|
+
export function cornerSquaresOverlap(box, radius, rect) {
|
|
115
|
+
return cornerSquares(box, radius).some((square) =>
|
|
116
|
+
rectsOverlap(square, rect),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The four squares of `box` a rounded outline of `radius` is not a rectangle
|
|
122
|
+
* in: whole pixels, `side` on a side — the radius taken up to a whole pixel,
|
|
123
|
+
* since the arc spans exactly `radius` from each edge and a pixel it crosses
|
|
124
|
+
* is one it shades — and named for their corner. Outside them the outline
|
|
125
|
+
* *is* the box, so a clip to it can be the box's rectangle there.
|
|
126
|
+
*
|
|
127
|
+
* Null when that cut cannot be made: a box off the pixel grid, whose own
|
|
128
|
+
* edges are antialiased wherever they run, and a radius whose squares would
|
|
129
|
+
* overlap — a pill, which is corner all the way along.
|
|
130
|
+
*/
|
|
131
|
+
export function roundedCorners(box, radius) {
|
|
132
|
+
if (!isIntegerRect(box)) return null;
|
|
133
|
+
// the radius a canvas `roundRect` draws: scaled down so that two corners
|
|
134
|
+
// never meet — for a uniform radius, half the shorter side
|
|
135
|
+
const r = Math.min(radius, box.width / 2, box.height / 2);
|
|
136
|
+
if (!(r > 0)) return null;
|
|
137
|
+
const side = Math.ceil(r);
|
|
138
|
+
if (side * 2 > box.width || side * 2 > box.height) return null;
|
|
139
|
+
const right = box.x + box.width - side;
|
|
140
|
+
const bottom = box.y + box.height - side;
|
|
141
|
+
return {
|
|
142
|
+
radius: r,
|
|
143
|
+
side,
|
|
144
|
+
squares: [
|
|
145
|
+
{ corner: 'tl', x: box.x, y: box.y, width: side, height: side },
|
|
146
|
+
{ corner: 'tr', x: right, y: box.y, width: side, height: side },
|
|
147
|
+
{ corner: 'br', x: right, y: bottom, width: side, height: side },
|
|
148
|
+
{ corner: 'bl', x: box.x, y: bottom, width: side, height: side },
|
|
149
|
+
],
|
|
150
|
+
};
|
|
101
151
|
}
|
|
102
152
|
|
|
103
153
|
/**
|
|
@@ -113,3 +163,48 @@ export function rectsOverlap(a, b, margin = 0) {
|
|
|
113
163
|
b.y < a.y + a.height + margin
|
|
114
164
|
);
|
|
115
165
|
}
|
|
166
|
+
|
|
167
|
+
/** The parts of `rect` outside `hole`: up to four bands, full width above
|
|
168
|
+
* and below, the rest beside. */
|
|
169
|
+
export function outside(rect, hole) {
|
|
170
|
+
const cut = intersectRects(rect, hole);
|
|
171
|
+
if (!cut) return [rect];
|
|
172
|
+
const out = [];
|
|
173
|
+
const right = rect.x + rect.width;
|
|
174
|
+
const bottom = rect.y + rect.height;
|
|
175
|
+
if (cut.y > rect.y) {
|
|
176
|
+
out.push({
|
|
177
|
+
x: rect.x,
|
|
178
|
+
y: rect.y,
|
|
179
|
+
width: rect.width,
|
|
180
|
+
height: cut.y - rect.y,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const cutBottom = cut.y + cut.height;
|
|
184
|
+
if (cutBottom < bottom) {
|
|
185
|
+
out.push({
|
|
186
|
+
x: rect.x,
|
|
187
|
+
y: cutBottom,
|
|
188
|
+
width: rect.width,
|
|
189
|
+
height: bottom - cutBottom,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (cut.x > rect.x) {
|
|
193
|
+
out.push({
|
|
194
|
+
x: rect.x,
|
|
195
|
+
y: cut.y,
|
|
196
|
+
width: cut.x - rect.x,
|
|
197
|
+
height: cut.height,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const cutRight = cut.x + cut.width;
|
|
201
|
+
if (cutRight < right) {
|
|
202
|
+
out.push({
|
|
203
|
+
x: cutRight,
|
|
204
|
+
y: cut.y,
|
|
205
|
+
width: right - cutRight,
|
|
206
|
+
height: cut.height,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// A rounded outline clipping a box's children, made without a clip mask
|
|
2
|
+
// (issue #685). The outline is the box's rectangle everywhere but its four
|
|
3
|
+
// corner squares, so the children are drawn once under the rectangle — the
|
|
4
|
+
// clip every server-side fast path takes — and each corner square a child
|
|
5
|
+
// reaches is put back the way it was outside the arc: kept before the
|
|
6
|
+
// children draw, returned through the coverage of the arc's outside after.
|
|
7
|
+
//
|
|
8
|
+
// That needs a context that can read its own pixels back into a surface of
|
|
9
|
+
// its own and composite a coverage surface: ntk's, whose `picture` is the
|
|
10
|
+
// server-side picture it draws through. Anywhere else the caller clips to the
|
|
11
|
+
// outline as it always has.
|
|
12
|
+
|
|
13
|
+
import { Surface } from '../offscreen.js';
|
|
14
|
+
|
|
15
|
+
// per app: the coverage of each corner's outside, by corner, radius and
|
|
16
|
+
// side, and the surfaces a corner is kept in between the two halves
|
|
17
|
+
const states = new WeakMap();
|
|
18
|
+
|
|
19
|
+
function stateFor(app) {
|
|
20
|
+
let state = states.get(app);
|
|
21
|
+
if (!state) {
|
|
22
|
+
state = { coverage: new Map(), spare: [] };
|
|
23
|
+
states.set(app, state);
|
|
24
|
+
}
|
|
25
|
+
return state;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The outline inside one corner square, as a path: the square with the
|
|
30
|
+
* outside of the arc cut away. The arc is `roundRect`'s own — the same
|
|
31
|
+
* centre, radius and quarter, reached by the same line — so the pixels it
|
|
32
|
+
* shades are the ones a clip to the whole outline shades.
|
|
33
|
+
*/
|
|
34
|
+
function cornerPath(ctx, box, { radius: r, side: s }, corner) {
|
|
35
|
+
const left = box.x;
|
|
36
|
+
const top = box.y;
|
|
37
|
+
const right = box.x + box.width;
|
|
38
|
+
const bottom = box.y + box.height;
|
|
39
|
+
ctx.beginPath();
|
|
40
|
+
if (corner === 'tl') {
|
|
41
|
+
ctx.moveTo(left, top + s);
|
|
42
|
+
ctx.lineTo(left, top + r);
|
|
43
|
+
ctx.arc(left + r, top + r, r, Math.PI, Math.PI * 1.5);
|
|
44
|
+
ctx.lineTo(left + s, top);
|
|
45
|
+
ctx.lineTo(left + s, top + s);
|
|
46
|
+
} else if (corner === 'tr') {
|
|
47
|
+
ctx.moveTo(right - s, top);
|
|
48
|
+
ctx.lineTo(right - r, top);
|
|
49
|
+
ctx.arc(right - r, top + r, r, -Math.PI / 2, 0);
|
|
50
|
+
ctx.lineTo(right, top + s);
|
|
51
|
+
ctx.lineTo(right - s, top + s);
|
|
52
|
+
} else if (corner === 'br') {
|
|
53
|
+
ctx.moveTo(right, bottom - s);
|
|
54
|
+
ctx.lineTo(right, bottom - r);
|
|
55
|
+
ctx.arc(right - r, bottom - r, r, 0, Math.PI / 2);
|
|
56
|
+
ctx.lineTo(right - s, bottom);
|
|
57
|
+
ctx.lineTo(right - s, bottom - s);
|
|
58
|
+
} else {
|
|
59
|
+
ctx.moveTo(left + s, bottom);
|
|
60
|
+
ctx.lineTo(left + r, bottom);
|
|
61
|
+
ctx.arc(left + r, bottom - r, r, Math.PI / 2, Math.PI);
|
|
62
|
+
ctx.lineTo(left, bottom - s);
|
|
63
|
+
ctx.lineTo(left + s, bottom - s);
|
|
64
|
+
}
|
|
65
|
+
ctx.closePath();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* How much of each pixel of a corner square lies outside the outline, as an
|
|
70
|
+
* a8 surface the size of the square: the square filled, and the outline's
|
|
71
|
+
* corner taken out of it. One per corner, radius and side on an app, and
|
|
72
|
+
* kept — every box with that radius shares it.
|
|
73
|
+
*/
|
|
74
|
+
function outsideCoverage(app, corners, corner) {
|
|
75
|
+
const state = stateFor(app);
|
|
76
|
+
const s = corners.side;
|
|
77
|
+
const key = `${corner}|${corners.radius}|${s}`;
|
|
78
|
+
let surface = state.coverage.get(key);
|
|
79
|
+
if (surface) return surface;
|
|
80
|
+
surface = new Surface(app, { width: s, height: s, format: 'a8' });
|
|
81
|
+
// a box whose corner lands on the square's own, in the square's space
|
|
82
|
+
const far = corner === 'tr' || corner === 'br';
|
|
83
|
+
const low = corner === 'bl' || corner === 'br';
|
|
84
|
+
const box = { x: far ? -s : 0, y: low ? -s : 0, width: 2 * s, height: 2 * s };
|
|
85
|
+
surface.render((sctx) => {
|
|
86
|
+
sctx.fillStyle = '#ffffff';
|
|
87
|
+
sctx.fillRect(0, 0, s, s);
|
|
88
|
+
sctx.globalCompositeOperation = 'destination-out';
|
|
89
|
+
cornerPath(sctx, box, corners, corner);
|
|
90
|
+
sctx.fill();
|
|
91
|
+
});
|
|
92
|
+
state.coverage.set(key, surface);
|
|
93
|
+
return surface;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A surface at least `side` square to keep a corner in, from the app's
|
|
98
|
+
* spares, with the context it is drawn through: a box nested in a box keeps
|
|
99
|
+
* its corners while the outer one's are still kept, so there is one per
|
|
100
|
+
* corner being held, not one in all. The context is held with the surface —
|
|
101
|
+
* ntk makes a new one per `getContext`, and a surface drawn into every frame
|
|
102
|
+
* would otherwise make one a frame.
|
|
103
|
+
*/
|
|
104
|
+
function takeSpare(app, side) {
|
|
105
|
+
const spare = stateFor(app).spare;
|
|
106
|
+
for (let i = 0; i < spare.length; i++) {
|
|
107
|
+
const { surface } = spare[i];
|
|
108
|
+
if (surface.width >= side && surface.height >= side) {
|
|
109
|
+
return spare.splice(i, 1)[0];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const size = Math.max(16, side);
|
|
113
|
+
const surface = new Surface(app, { width: size, height: size });
|
|
114
|
+
return { surface, ctx: surface.getContext('2d') };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Where the context puts window coordinate (0, 0) on the picture it draws
|
|
119
|
+
* into — the only transform the paint walk runs under is a whole-pixel
|
|
120
|
+
* translation (a faded group's surface, a pane's) — and that picture, as
|
|
121
|
+
* something `drawImage` can read from; null when either is missing, and the
|
|
122
|
+
* corners cannot be kept this way.
|
|
123
|
+
*/
|
|
124
|
+
function readback(ctx) {
|
|
125
|
+
const m = typeof ctx.getTransform === 'function' ? ctx.getTransform() : null;
|
|
126
|
+
if (!m || m.a !== 1 || m.b !== 0 || m.c !== 0 || m.d !== 1) return null;
|
|
127
|
+
if (!Number.isInteger(m.e) || !Number.isInteger(m.f)) return null;
|
|
128
|
+
// reading it settles the picture's clip, which the next drawing re-stamps
|
|
129
|
+
if (!ctx.picture || !Number.isFinite(ctx.width)) return null;
|
|
130
|
+
return {
|
|
131
|
+
dx: m.e,
|
|
132
|
+
dy: m.f,
|
|
133
|
+
source: {
|
|
134
|
+
width: ctx.width,
|
|
135
|
+
height: ctx.height,
|
|
136
|
+
picture: () => ctx.picture,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Keep the corner squares in `cut` — the ones a child is about to draw into
|
|
143
|
+
* — before the children draw, and hand back what puts them back afterwards:
|
|
144
|
+
* `restore()` composites each square the way it was outside the arc, over
|
|
145
|
+
* whatever the children left there, and `release()` returns the surfaces.
|
|
146
|
+
* Null when the context cannot read its pixels back.
|
|
147
|
+
*
|
|
148
|
+
* Each kept square is premultiplied by the outside's coverage, so putting it
|
|
149
|
+
* back is two composites and no mask: the destination scaled by the inside's
|
|
150
|
+
* coverage (`destination-out` through the outside's), then the kept pixels
|
|
151
|
+
* added (`lighter`). Where the outline covers a pixel wholly, nothing of what
|
|
152
|
+
* the children drew changes; where it misses it wholly, what was there
|
|
153
|
+
* before comes back exactly; the antialiased pixels on the arc blend the two
|
|
154
|
+
* by the arc's coverage. That is the children clipped as one layer, which is
|
|
155
|
+
* what a clip means — a per-drawing clip lets a covered layer bleed into
|
|
156
|
+
* those pixels, and a stack of them darkens the edge.
|
|
157
|
+
*/
|
|
158
|
+
export function keepCorners(ctx, app, corners, cut) {
|
|
159
|
+
const at = app ? readback(ctx) : null;
|
|
160
|
+
if (!at) return null;
|
|
161
|
+
const s = corners.side;
|
|
162
|
+
const kept = [];
|
|
163
|
+
const release = () => {
|
|
164
|
+
const spare = stateFor(app).spare;
|
|
165
|
+
for (const { held } of kept) spare.push(held);
|
|
166
|
+
kept.length = 0;
|
|
167
|
+
};
|
|
168
|
+
try {
|
|
169
|
+
for (const square of cut) {
|
|
170
|
+
const outside = outsideCoverage(app, corners, square.corner);
|
|
171
|
+
const held = takeSpare(app, s);
|
|
172
|
+
kept.push({ square, outside, held });
|
|
173
|
+
const sctx = held.ctx;
|
|
174
|
+
sctx.save();
|
|
175
|
+
sctx.globalAlpha = 1;
|
|
176
|
+
sctx.globalCompositeOperation = 'copy';
|
|
177
|
+
sctx.drawImage(
|
|
178
|
+
at.source,
|
|
179
|
+
square.x + at.dx,
|
|
180
|
+
square.y + at.dy,
|
|
181
|
+
s,
|
|
182
|
+
s,
|
|
183
|
+
0,
|
|
184
|
+
0,
|
|
185
|
+
s,
|
|
186
|
+
s,
|
|
187
|
+
);
|
|
188
|
+
sctx.globalCompositeOperation = 'destination-in';
|
|
189
|
+
sctx.fillStyle = '#000000';
|
|
190
|
+
sctx.drawImage(outside, 0, 0);
|
|
191
|
+
sctx.restore();
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
// no surface to keep a corner in: the caller clips to the outline, and
|
|
195
|
+
// what was taken goes back
|
|
196
|
+
release();
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
restore() {
|
|
201
|
+
for (const { square, outside, held } of kept) {
|
|
202
|
+
ctx.save();
|
|
203
|
+
ctx.globalAlpha = 1;
|
|
204
|
+
ctx.globalCompositeOperation = 'destination-out';
|
|
205
|
+
ctx.fillStyle = '#000000';
|
|
206
|
+
ctx.drawImage(outside, square.x, square.y);
|
|
207
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
208
|
+
ctx.drawImage(held.surface, 0, 0, s, s, square.x, square.y, s, s);
|
|
209
|
+
ctx.restore();
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
release,
|
|
213
|
+
};
|
|
214
|
+
}
|
package/src/nodes/scrollable.js
CHANGED
|
@@ -163,7 +163,10 @@ export const Scrollable = (Base) =>
|
|
|
163
163
|
this.abs.y - wasY,
|
|
164
164
|
);
|
|
165
165
|
} finally {
|
|
166
|
-
if (move)
|
|
166
|
+
if (move) {
|
|
167
|
+
layoutDiff.shift = null;
|
|
168
|
+
layoutDiff.ride = false;
|
|
169
|
+
}
|
|
167
170
|
}
|
|
168
171
|
if (move) this._claimRigidMove(move);
|
|
169
172
|
}
|
|
@@ -316,9 +319,11 @@ export const Scrollable = (Base) =>
|
|
|
316
319
|
// content shrank, or the viewport grew, under the offset — a row
|
|
317
320
|
// collapsing at the end of a list scrolled to its end — and
|
|
318
321
|
// nothing claimed the viewport every child just moved in. Nor
|
|
319
|
-
// for a
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
+
// for a move this pane rides whose pixels are copied — a
|
|
323
|
+
// `<glarea>` child's on its pane (issue #644), a subtree's in
|
|
324
|
+
// the window (issue #681): its box claimed nothing, and a pane
|
|
325
|
+
// laid out again may have moved rows the move's copy carries
|
|
326
|
+
// where they were.
|
|
322
327
|
if (clamped || layoutDiff.ride) outer(vp, this);
|
|
323
328
|
} else {
|
|
324
329
|
layoutDiff.sink = (rect, node) => {
|
package/src/nodes/scrollbars.js
CHANGED
|
@@ -67,9 +67,15 @@ export function scrollbarGeometry({
|
|
|
67
67
|
// radius downstream draw from the same number.
|
|
68
68
|
const barWidth = SCROLLBAR_WIDTH * scale;
|
|
69
69
|
const rtl = direction === 'rtl';
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
// Never longer than the track: a pane shorter than the minimum gets a
|
|
71
|
+
// thumb that fills it and has no travel. The bar paints outside the
|
|
72
|
+
// pane's clip, and a node that clips its children reaches no further than
|
|
73
|
+
// its own box (`_subtreeBounds`), so a thumb run past the track put ink
|
|
74
|
+
// where no damage claim could reach — left behind by every move of the
|
|
75
|
+
// pane, and on a part the pointer could not hit.
|
|
76
|
+
const thumbLength = Math.min(
|
|
77
|
+
length,
|
|
78
|
+
Math.max(SCROLLBAR_MIN_THUMB * scale, (length * length) / content),
|
|
73
79
|
);
|
|
74
80
|
const range = content - viewport;
|
|
75
81
|
const travel = Math.max(0, length - thumbLength);
|