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.
- package/README.md +278 -129
- package/package.json +12 -4
- package/src/Reconciler.js +19 -31
- package/src/a11y.js +2 -2
- package/src/anchor.js +7 -5
- package/src/appcontext.js +59 -30
- package/src/bootstrap.js +14 -0
- package/src/clientmessage.js +1 -1
- package/src/cocoa/app.js +303 -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 +24 -5
- package/src/cocoa/main.d.ts +8 -0
- package/src/cocoa/main.js +43 -0
- package/src/cocoa/overlay.js +159 -0
- package/src/cocoa/panehost.js +15 -5
- package/src/cocoa/presenter.js +13 -9
- package/src/cocoa/promotion.js +17 -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/embedding.js +31 -0
- package/src/errors.js +46 -0
- package/src/events.js +78 -18
- package/src/foreignnodes.js +59 -5
- package/src/frames.js +2 -2
- package/src/glnodes.js +172 -41
- package/src/gloverlay.js +383 -0
- package/src/grid.js +1653 -0
- package/src/host.d.ts +230 -1
- package/src/host.js +11 -3
- package/src/imagesource.js +1 -1
- package/src/index.d.ts +34 -4
- package/src/index.js +9 -1
- package/src/layouts.js +721 -0
- package/src/node.d.ts +16 -3
- 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 +369 -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 +954 -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 +26 -14
- 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
|
@@ -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
|
+
}
|