react-x11 2.11.0 → 2.13.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.
Files changed (100) hide show
  1. package/README.md +278 -129
  2. package/package.json +12 -4
  3. package/src/Reconciler.js +19 -31
  4. package/src/a11y.js +2 -2
  5. package/src/anchor.js +7 -5
  6. package/src/appcontext.js +59 -30
  7. package/src/bootstrap.js +14 -0
  8. package/src/clientmessage.js +1 -1
  9. package/src/cocoa/app.js +303 -49
  10. package/src/cocoa/bezels.js +175 -30
  11. package/src/cocoa/dnd.js +27 -13
  12. package/src/cocoa/fonts.js +3 -3
  13. package/src/cocoa/glarea.js +24 -5
  14. package/src/cocoa/main.d.ts +8 -0
  15. package/src/cocoa/main.js +43 -0
  16. package/src/cocoa/overlay.js +159 -0
  17. package/src/cocoa/panehost.js +15 -5
  18. package/src/cocoa/presenter.js +13 -9
  19. package/src/cocoa/promotion.js +17 -7
  20. package/src/cocoa/relaunch.js +207 -0
  21. package/src/cocoa/threaded.js +246 -0
  22. package/src/cocoa/window.js +256 -42
  23. package/src/components/Select.js +2 -2
  24. package/src/components/anchor.js +3 -3
  25. package/src/components/native.js +12 -7
  26. package/src/components/theme.js +2 -2
  27. package/src/debug.js +1 -1
  28. package/src/decorations.js +1 -1
  29. package/src/editmenu.js +2 -2
  30. package/src/embedding.js +31 -0
  31. package/src/errors.js +46 -0
  32. package/src/events.js +78 -18
  33. package/src/foreignnodes.js +59 -5
  34. package/src/frames.js +2 -2
  35. package/src/glnodes.js +172 -41
  36. package/src/gloverlay.js +383 -0
  37. package/src/grid.js +1653 -0
  38. package/src/host.d.ts +230 -1
  39. package/src/host.js +11 -3
  40. package/src/imagesource.js +1 -1
  41. package/src/index.d.ts +34 -4
  42. package/src/index.js +9 -1
  43. package/src/layouts.js +721 -0
  44. package/src/node.d.ts +16 -3
  45. package/src/node.js +19 -21
  46. package/src/nodes/animation.js +644 -0
  47. package/src/nodes/box.js +21 -0
  48. package/src/nodes/boxpaint.js +473 -0
  49. package/src/nodes/canvas.js +269 -0
  50. package/src/nodes/cascade.js +600 -0
  51. package/src/nodes/damage.js +183 -0
  52. package/src/nodes/edithistory.js +124 -0
  53. package/src/nodes/editmenupopup.js +260 -0
  54. package/src/nodes/hittest.js +185 -0
  55. package/src/nodes/image.js +266 -0
  56. package/src/nodes/install.js +75 -0
  57. package/src/nodes/invalidate.js +465 -0
  58. package/src/nodes/kinds.js +31 -0
  59. package/src/nodes/layout.js +439 -0
  60. package/src/nodes/layouthost.js +949 -0
  61. package/src/nodes/node.js +868 -0
  62. package/src/nodes/paint.js +466 -0
  63. package/src/nodes/position.js +366 -0
  64. package/src/nodes/preedit.js +127 -0
  65. package/src/nodes/queries.js +330 -0
  66. package/src/nodes/rects.js +102 -0
  67. package/src/nodes/scrollable.js +891 -0
  68. package/src/nodes/scrollbars.js +138 -0
  69. package/src/nodes/scrollblit.js +1034 -0
  70. package/src/nodes/selectable.js +142 -0
  71. package/src/nodes/styling.js +225 -0
  72. package/src/nodes/text.js +649 -0
  73. package/src/nodes/textarea.js +391 -0
  74. package/src/nodes/textinput.js +1146 -0
  75. package/src/nodes/util.js +17 -0
  76. package/src/nodes/window/anchoring.js +161 -0
  77. package/src/nodes/window/capabilities.js +190 -0
  78. package/src/nodes/window/debugpaint.js +83 -0
  79. package/src/nodes/window/droptarget.js +145 -0
  80. package/src/nodes/window/floors.js +577 -0
  81. package/src/nodes/window/flush.js +369 -0
  82. package/src/nodes/window/hints.js +482 -0
  83. package/src/nodes/window/listeners.js +222 -0
  84. package/src/nodes/window/popup.js +71 -0
  85. package/src/nodes/window/size.js +591 -0
  86. package/src/nodes/window/window.js +954 -0
  87. package/src/palette.js +1 -1
  88. package/src/registry.js +7 -3
  89. package/src/styles.js +137 -15
  90. package/src/svgnodes.js +2 -1
  91. package/src/testing/harness.js +2 -2
  92. package/src/textselection.js +5 -3
  93. package/src/trace-registry.js +1 -1
  94. package/src/types/components.d.ts +38 -6
  95. package/src/types/elements.d.ts +26 -14
  96. package/src/types/nodes.d.ts +17 -2
  97. package/src/types/style.d.ts +94 -3
  98. package/src/windowstate.js +1 -1
  99. package/src/yoga.js +1 -1
  100. package/src/nodes.js +0 -13120
@@ -0,0 +1,466 @@
1
+ // The paint walk: the order children paint in, clipping, and culling against
2
+ // the frame's damage. WindowNode's half paints a damaged region of the
3
+ // window, background first.
4
+
5
+ import { isPlaced } from '../layouts.js';
6
+ import { isPaintedColor } from './boxpaint.js';
7
+ import { DAMAGE_SLOP } from './damage.js';
8
+ import { DRAWN_KINDS } from './kinds.js';
9
+ import { rectsOverlap } from './rects.js';
10
+ import { DEV } from './util.js';
11
+ import { devWarnWindowShadow } from './window/capabilities.js';
12
+ import { FLASH_COLORS, debugPaint } from './window/debugpaint.js';
13
+
14
+ /** Node's half of the paint walk, installed onto `Node.prototype` by node.js. */
15
+ export class NodePaint {
16
+ /** Drawn, visible children in paint order (stable sort by zIndex). */
17
+ paintOrder() {
18
+ // Hit testing asks at every node it visits and painting at every node
19
+ // it draws, so the filter-map-sort-map here was steady per-event
20
+ // allocation (issue #188). Cached — and verified against the live
21
+ // children on every read rather than invalidated: membership and
22
+ // z-keys are the same reads the filter always did, so a fresh cache
23
+ // costs no allocation and a stale one is impossible, whatever mutates
24
+ // children, styles, visibility or DRAWN_KINDS.
25
+ const cache = this._paintOrderCache;
26
+ if (cache && this._paintOrderFresh(cache)) return cache.order;
27
+ // `display: 'none'` takes a node out of the layout, and it has to leave
28
+ // the paint with it. They were separate before because the only way to
29
+ // hide something was the `hidden` flag, which does both — until a size
30
+ // query started setting `display` from a style block, and the hidden
31
+ // node carried on painting at the position it no longer had.
32
+ const drawn = [];
33
+ const z = [];
34
+ // A placed child — sticky, or a registered position — is lifted over its
35
+ // in-flow siblings of the same `zIndex`, the way CSS paints a positioned
36
+ // or transformed box over non-positioned ones: a header held at the top
37
+ // of a pane sits over the rows that scroll under it, which are its later
38
+ // siblings and would otherwise paint on top. Hit testing walks the same
39
+ // order backwards, so the header also takes the press.
40
+ const lift = [];
41
+ for (const c of this.children) {
42
+ if (
43
+ DRAWN_KINDS.has(c.kind) &&
44
+ c.yoga &&
45
+ !c.hidden &&
46
+ c.style.display !== 'none'
47
+ ) {
48
+ drawn.push(c);
49
+ z.push(c.style.zIndex ?? 0);
50
+ lift.push(isPlaced(c.style) ? 1 : 0);
51
+ }
52
+ }
53
+ // document order already answers the usual no-zIndex case; the sort is
54
+ // only paid when some key actually disagrees with it
55
+ let order = drawn;
56
+ for (let i = 1; i < z.length; i++) {
57
+ if (z[i] < z[i - 1] || (z[i] === z[i - 1] && lift[i] < lift[i - 1])) {
58
+ order = drawn
59
+ .map((node, j) => ({ node, z: z[j], lift: lift[j], j }))
60
+ .sort((a, b) => a.z - b.z || a.lift - b.lift || a.j - b.j)
61
+ .map((e) => e.node);
62
+ break;
63
+ }
64
+ }
65
+ this._paintOrderCache = { order, drawn, z, lift };
66
+ return order;
67
+ }
68
+
69
+ /** Do the cached drawn children and their sort keys still match the tree? */
70
+ _paintOrderFresh({ drawn, z, lift }) {
71
+ let j = 0;
72
+ for (const c of this.children) {
73
+ if (
74
+ !DRAWN_KINDS.has(c.kind) ||
75
+ !c.yoga ||
76
+ c.hidden ||
77
+ c.style.display === 'none'
78
+ ) {
79
+ continue;
80
+ }
81
+ if (
82
+ j >= drawn.length ||
83
+ drawn[j] !== c ||
84
+ z[j] !== (c.style.zIndex ?? 0) ||
85
+ lift[j] !== (isPlaced(c.style) ? 1 : 0)
86
+ ) {
87
+ return false;
88
+ }
89
+ j++;
90
+ }
91
+ return j === drawn.length;
92
+ }
93
+
94
+ clipsChildren() {
95
+ return this.style.overflow === 'hidden' || this.style.overflow === 'scroll';
96
+ }
97
+
98
+ paint(ctx) {
99
+ if (this.hidden) return;
100
+ // Outside the box and under everything, which is the whole of what makes
101
+ // a shadow different from a colour: it is drawn before this node's own
102
+ // background so a translucent background does not sit on top of it, and
103
+ // it inks pixels this node does not own — see `_ownPaintBounds`.
104
+ this._paintShadow(ctx);
105
+ this._paintBackground(ctx);
106
+ // The paint cache covers a node's *content* — the expensive part — and
107
+ // not its box: background and border are one composite each, and keeping
108
+ // them out keeps their styles out of the key. A node that does not
109
+ // implement the protocol has no `paintCachePlan` and pays one property
110
+ // lookup for the privilege.
111
+ const cache = this.paintCachePlan && this.root?._paintCache;
112
+ if (cache) cache.paint(this, ctx);
113
+ else this.paintContent(ctx);
114
+ this._paintChildren(ctx);
115
+ this._paintBorder(ctx);
116
+ // last, and outside the border box: a ring drawn under the border would
117
+ // be half-hidden by it on a control whose border is thicker than the gap
118
+ this._paintOutline(ctx);
119
+ }
120
+
121
+ /**
122
+ * What this element draws of its own, between its background and its
123
+ * children — where every built-in that draws anything draws it, and the
124
+ * seam an element that draws over its own scroll offset needs, since the
125
+ * scrollbars go on after the children and `paint` returning is too late
126
+ * to be underneath them.
127
+ */
128
+ paintContent(ctx) {}
129
+
130
+ /**
131
+ * The paint-cache protocol (issue #149). A node implements both methods or
132
+ * neither; the base class has neither, so nothing is cached until it opts
133
+ * in, and no existing or future node changes behaviour by default.
134
+ *
135
+ * paintCachePlan(ctx) -> null | {
136
+ * key, // identity: same key must mean same pixels
137
+ * x, y, // where the surface goes, in device pixels
138
+ * width, height, // its size, in device pixels
139
+ * format, // 'argb32', or 'a8' for coverage that gets tinted
140
+ * tint, // the colour an 'a8' surface is painted through
141
+ * }
142
+ * paintCached(ctx, box, ink) -> void // draw at the origin of `box`
143
+ *
144
+ * Returning null opts out for this frame, which is the right answer
145
+ * whenever the paint depends on something the key cannot see.
146
+ *
147
+ * `ink` is the colour a mono drawing — one that asked for `'a8'` — must
148
+ * paint in: white where the surface is coverage and the tint arrives at
149
+ * blit time, the tint itself on a backend without coverage surfaces,
150
+ * where the cache bakes the colour into an argb32 entry and puts it in
151
+ * the key (src/paintcache.js). A multi-colour drawing ignores it.
152
+ *
153
+ * **The key is the entire correctness surface.** It must name every input
154
+ * `paintCached` reads, derived from the same values `applyProps` compares
155
+ * so the two cannot drift. Never cache a paint that depends on state
156
+ * outside the key — a focus ring, a hover, a caret blink, anything
157
+ * animating. Run with `REACT_X11_PAINT_CACHE=verify` to have a key that
158
+ * misses something fail loudly instead of showing a stale pixel.
159
+ *
160
+ * `paintCached` draws in *surface-local* coordinates: `box` is at the
161
+ * origin, not at `this.abs`. Reaching for `this.abs.x` inside it is the
162
+ * mistake to look for first when a cached node draws in the wrong place.
163
+ */
164
+
165
+ /**
166
+ * Whether anything in this subtree actually reaches outside the clip box.
167
+ *
168
+ * A clip that clips nothing is far from free: each one rebuilds an a8 mask
169
+ * server-side, which is a FillRectangles plus trapezoid rasterization, and
170
+ * ntk brackets every glyph run under a clip with a SetPictureClipRectangles
171
+ * pair. A table sets `overflow: hidden` on every cell so that *long* text
172
+ * truncates, and then almost every cell's text fits — 191 clips a frame, of
173
+ * which a handful do anything.
174
+ *
175
+ * Rounded corners are never skipped: the clip is not a rectangle then, and
176
+ * the rounding can cut a child that a rect test says fits. The one-pixel
177
+ * inset is for antialiasing, which can put ink just outside a glyph's box.
178
+ */
179
+ _childrenCanOverflow() {
180
+ if (this.style?.borderRadius) return true;
181
+ const box = this.abs;
182
+ for (const child of this.children) {
183
+ if (child.isWindow || !child.yoga || child.hidden) continue;
184
+ if (child.style?.display === 'none') continue;
185
+ const b = child._subtreeBounds();
186
+ if (
187
+ b.x < box.x + 1 ||
188
+ b.y < box.y + 1 ||
189
+ b.x + b.width > box.x + box.width - 1 ||
190
+ b.y + b.height > box.y + box.height - 1
191
+ ) {
192
+ return true;
193
+ }
194
+ }
195
+ return false;
196
+ }
197
+
198
+ _paintChildren(ctx) {
199
+ // A retained presenter replays a node's `paint` into a visual of that
200
+ // node's own — its children have visuals of their own, so it sets this
201
+ // for the duration of the call (src/cocoa/presenter.js, `paintSelf`).
202
+ // Never set on the X11 or surface paths, where a frame is one walk.
203
+ if (this._ownPaintOnly) return;
204
+ const order = this.paintOrder();
205
+ if (order.length === 0) return;
206
+ const clip = this.clipsChildren() && this._childrenCanOverflow();
207
+ if (clip) {
208
+ ctx.save();
209
+ this._roundedPath(ctx, this.style.borderRadius ?? 0);
210
+ ctx.clip();
211
+ }
212
+ for (const child of order) {
213
+ if (child._promoted) continue; // on a layer of its own: a hole here
214
+ if (child._offscreen(child.abs, DAMAGE_SLOP)) continue;
215
+ if (child._outsideDamage()) continue;
216
+ child.paint(ctx);
217
+ }
218
+ if (clip) ctx.restore();
219
+ }
220
+
221
+ /**
222
+ * Nothing this subtree draws lands in the region being repainted, so its
223
+ * drawing does not need to be sent at all. This is where the protocol
224
+ * saving comes from — the clip alone would still put every request on the
225
+ * wire for the server to throw away.
226
+ *
227
+ * Tested against the subtree's bounds, not `abs`: see `paintBounds`. And
228
+ * with the pixel of `DAMAGE_SLOP` to spare. A claim is grown by it before
229
+ * it gets here, but a rect that never was a claim — the strip a scroll
230
+ * blit exposes, a scrollbar's repair — is exact, and antialiasing puts
231
+ * ink just outside a glyph's box (`_childrenCanOverflow` allows the same
232
+ * pixel). Text whose box ends where such a rect begins inks its first
233
+ * column; culled, that column keeps what was under the text where a full
234
+ * repaint has the ink. The pass is clipped to the rect, so painting the
235
+ * text lets that ink land and nothing else.
236
+ */
237
+ _outsideDamage() {
238
+ const damage = this.root?._paintDamage;
239
+ if (!damage) return false;
240
+ return !rectsOverlap(this._subtreeBounds(), damage, DAMAGE_SLOP);
241
+ }
242
+
243
+ /**
244
+ * Entirely outside the window, or entirely outside some nearer ancestor
245
+ * that clips its children — either way there is nothing to draw. Worth
246
+ * doing for its own sake, but it is also a correctness fix: X's render
247
+ * traps are 16.16 fixed point, so a coordinate past ±32767 overflows the
248
+ * request. A scrolled list is exactly how you get there — the frame
249
+ * between a scroll and the re-render that follows it can hold rows
250
+ * ninety thousand pixels above the viewport.
251
+ *
252
+ * The ancestor walk matters on its own (issue #211): a scrolling box's
253
+ * own box is often much smaller than the window around it, and its
254
+ * `ctx.clip()` in `_paintChildren` only keeps the *pixels* off the visible
255
+ * surface — every child below the fold still ran its full paint (canvas
256
+ * `onDraw`, text/tex layout, the XRender/PutImage requests that go with
257
+ * them) for the server to then discard. Checking the window alone missed
258
+ * that: a node can sit well inside the window and still be entirely past
259
+ * a scrolling ancestor whose own bounds are the real limit.
260
+ *
261
+ * `rect` asks the same question about part of the node instead — window
262
+ * coordinates, the node's own rect by default. What wants it is anchoring
263
+ * (`src/anchor.js`): a popup pointed at a caret has to know when *the
264
+ * caret* has scrolled out of the editor, which happens many screens before
265
+ * the editor itself goes anywhere.
266
+ *
267
+ * `slop` is how near counts as reaching in. The paint walk passes
268
+ * `DAMAGE_SLOP`, for the reason `_outsideDamage` does: antialiasing puts
269
+ * a glyph's ink a fraction of a pixel past its box, so right-aligned or
270
+ * RTL text whose box ends exactly at a pane's edge, or the window's, inks
271
+ * the first column inside it. Culled, that column goes without the ink in
272
+ * every pass, full repaints included, and the scroll blit carries it: a
273
+ * notch that moves the text in shows the bare column where a full repaint
274
+ * paints the text, and a notch the other way carries the ink onto a
275
+ * column whose full repaint culls it. The pixel's cost is a node whose box
276
+ * touches the edge, such as a list row abutting the viewport, painted and
277
+ * clipped away; its children further out are still culled. Anchoring
278
+ * asks with none, since a caret a pixel past the viewport is out of view.
279
+ */
280
+ _offscreen(rect = this.abs, slop = 0) {
281
+ const window = this.root?.abs;
282
+ if (!window) return false;
283
+ const { x, y, width, height } = rect;
284
+ if (
285
+ x + width <= -slop ||
286
+ y + height <= -slop ||
287
+ x >= window.width + slop ||
288
+ y >= window.height + slop
289
+ ) {
290
+ return true;
291
+ }
292
+ for (let n = this.parent; n && n !== this.root; n = n.parent) {
293
+ if (n.clipsChildren() && !rectsOverlap(rect, n.abs, slop)) return true;
294
+ }
295
+ return false;
296
+ }
297
+ }
298
+
299
+ /** WindowNode's half of the paint walk, installed onto `WindowNode.prototype` by window/window.js. */
300
+ export class WindowPaint {
301
+ /** Repaint one damage rect, or the whole window when `damage` is null. */
302
+ _paintRegion(ctx, damage, width, height) {
303
+ // An element that covers the pass with opaque pixels (`Node.opaqueRect`)
304
+ // makes every fill under it wasted work — the clear, the window's
305
+ // background, the node's own and its ancestors'. The first two are
306
+ // skipped here; `_paintBackground` skips the chain's while
307
+ // `_coverChain` names it.
308
+ const cover = this._coverFor(damage ?? { x: 0, y: 0, width, height });
309
+ // A transparent window erases where an opaque one paints over. Its
310
+ // backing store holds premultiplied ARGB, and compositing a translucent
311
+ // background onto the previous frame would compound towards opaque
312
+ // instead of replacing it — a popup that fades in would stick.
313
+ //
314
+ // Before the clip below, deliberately: clearing exactly the damage rect
315
+ // covers the same pixels, and an unclipped clearRect is one server-side
316
+ // FillRectangles where a clipped one has to rasterize a coverage mask.
317
+ //
318
+ // `transparencyEffective`, not `_transparent`: an ARGB window with
319
+ // nothing compositing it must not clear, because the server would show
320
+ // those zeroed pixels as black rather than as the desktop.
321
+ if (this.transparencyEffective && !cover) {
322
+ if (damage) {
323
+ ctx.clearRect(damage.x, damage.y, damage.width, damage.height);
324
+ } else {
325
+ ctx.clearRect(0, 0, width, height);
326
+ }
327
+ }
328
+ if (damage) {
329
+ // The clip is belt to the culling's braces: it bounds the server-side
330
+ // mask work for whatever *does* paint, and it contains any node that
331
+ // inks slightly outside its own rect. Rectangular clips take ntk's
332
+ // server-side fast path, so this is cheap. The unbounded pass stays
333
+ // unclipped on purpose: a clip would only re-report what ntk already
334
+ // does — its fallback present is clamped to min(window, backing)
335
+ // (ntk >= 5.3, window.js _presentNow) — at two SetPictureClipRectangles
336
+ // per composite, which the protocol bench prices at +207 requests for
337
+ // a hundred-icon full repaint.
338
+ ctx.save();
339
+ ctx.beginPath();
340
+ ctx.rect(damage.x, damage.y, damage.width, damage.height);
341
+ ctx.clip();
342
+ }
343
+ if (!cover) this._paintWindowBackground(ctx, damage, width, height);
344
+ if (cover) {
345
+ const chain = new Set();
346
+ for (let n = cover; n; n = n.parent) chain.add(n);
347
+ this._coverChain = chain;
348
+ }
349
+ this._paintDamage = damage;
350
+ try {
351
+ this._paintChildren(ctx);
352
+ // a scrolling window draws its own bars, which `Node.paint` would have
353
+ // done for a box — the window never goes through it
354
+ this._paintScrollbars(ctx);
355
+ if (process.env.REACT_X11_DEBUG_LAYOUT) {
356
+ this._paintDebugOverlay(ctx, this, 0);
357
+ }
358
+ const highlight = this._highlight;
359
+ if (highlight && !highlight.destroyed) {
360
+ const r = highlight.abs?.width
361
+ ? highlight.abs
362
+ : { x: 0, y: 0, width, height };
363
+ ctx.fillStyle = 'rgba(41, 128, 185, 0.35)';
364
+ ctx.fillRect(r.x, r.y, r.width, r.height);
365
+ }
366
+ if (this._traceUpdates) {
367
+ ctx.lineWidth = 2;
368
+ for (const r of this._traceUpdates) {
369
+ ctx.strokeStyle = r.color;
370
+ ctx.beginPath();
371
+ // inset by the stroke so an outline on a rect flush with the
372
+ // window edge is not half-clipped away
373
+ ctx.rect(r.x + 1, r.y + 1, r.width - 2, r.height - 2);
374
+ ctx.stroke();
375
+ }
376
+ }
377
+ if (debugPaint) {
378
+ // Stroke the pass's rect in this frame's colour ("repaint rainbow"):
379
+ // a region repainting every frame strobes, one that repaints once
380
+ // leaves a single outline behind. Inset a pixel so the stroke
381
+ // survives the clip on all four sides.
382
+ const r = damage ?? { x: 0, y: 0, width, height };
383
+ ctx.strokeStyle =
384
+ FLASH_COLORS[(this._flashTick ?? 0) % FLASH_COLORS.length];
385
+ ctx.lineWidth = 2;
386
+ ctx.beginPath();
387
+ ctx.rect(r.x + 1, r.y + 1, r.width - 2, r.height - 2);
388
+ ctx.stroke();
389
+ }
390
+ } finally {
391
+ this._paintDamage = null;
392
+ this._coverChain = null;
393
+ if (damage) ctx.restore();
394
+ }
395
+ }
396
+
397
+ /**
398
+ * The window's own background, painted under the whole tree.
399
+ *
400
+ * An opaque window falls back to white, because "no background" is not
401
+ * something X can show. A transparent one has no fallback and needs none:
402
+ * the clear in `_paintRegion` already left it empty, and empty is the
403
+ * point — a `<popup transparent>` with no `backgroundColor` is a floating
404
+ * tree with nothing behind it.
405
+ *
406
+ * `borderRadius` only means anything here, and only on a transparent
407
+ * window. This fill is the bottom-most thing in the window, so rounding it
408
+ * rounds the window itself, and the corners it gives up are the corners
409
+ * the compositor then shows the desktop through — antialiased, without the
410
+ * Shape extension's hard 1-bit edge.
411
+ */
412
+ _paintWindowBackground(ctx, damage, width, height) {
413
+ const { backgroundColor, borderRadius = 0 } = this.style;
414
+ if (DEV && this.style.boxShadow) devWarnWindowShadow(this.kind);
415
+ // A `backgroundImage` works wherever a `backgroundColor` does, which is
416
+ // the rule worth having — over the whole window, in window coordinates,
417
+ // and still filling only the damage rect: the gradient is a source
418
+ // picture in device space, so a slice of it is the slice that belongs
419
+ // there.
420
+ const gradient = this._backgroundGradient(ctx, {
421
+ x: 0,
422
+ y: 0,
423
+ width,
424
+ height,
425
+ });
426
+ const fill = (style, rounded) => {
427
+ ctx.fillStyle = style;
428
+ if (rounded) {
429
+ // The path is the whole window however small the damage rect is —
430
+ // the clip bounds it — so repainting one corner still draws that
431
+ // corner's curve rather than a square patch of background.
432
+ ctx.beginPath();
433
+ ctx.roundRect(0, 0, width, height, borderRadius);
434
+ ctx.fill();
435
+ } else if (damage) {
436
+ ctx.fillRect(damage.x, damage.y, damage.width, damage.height);
437
+ } else {
438
+ ctx.fillRect(0, 0, width, height);
439
+ }
440
+ };
441
+ if (this.transparencyEffective) {
442
+ if (!isPaintedColor(backgroundColor) && !gradient) return;
443
+ const rounded = borderRadius > 0 && typeof ctx.roundRect === 'function';
444
+ if (isPaintedColor(backgroundColor)) fill(backgroundColor, rounded);
445
+ if (gradient) fill(gradient, rounded);
446
+ return;
447
+ }
448
+ // An ARGB window that nothing is compositing has an alpha channel it
449
+ // must not use. It gets filled edge to edge and square — `borderRadius`
450
+ // is ignored, because giving up the corners here would expose the
451
+ // black those pixels really are. A translucent colour is flattened
452
+ // over white rather than composited onto the last frame, which on a
453
+ // window with alpha would otherwise creep towards opaque a frame at a
454
+ // time and never settle anywhere predictable.
455
+ if (this._transparent) fill('white', false);
456
+ // No `backgroundColor` means the desktop's, not white: a window whose
457
+ // widgets went dark on a dark desktop must not leave a white rectangle
458
+ // behind them. An app that named a colour gets the colour it named.
459
+ //
460
+ // repainting the background only where it is about to be drawn over is
461
+ // the other half of the win: a full-window fill is a full-window
462
+ // composite however little changed
463
+ fill(backgroundColor || this.theme.background, false);
464
+ if (gradient) fill(gradient, false);
465
+ }
466
+ }