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,465 @@
|
|
|
1
|
+
// Invalidation: what a node damages when it changes, and how far its paint
|
|
2
|
+
// reaches (paint bounds, the opaque rect). WindowNode's half is the other
|
|
3
|
+
// end: how a window takes that damage in and clamps it to a frame.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
FULL_DAMAGE,
|
|
7
|
+
NO_DAMAGE,
|
|
8
|
+
DAMAGE_SLOP,
|
|
9
|
+
NO_BOUNDS_CACHE,
|
|
10
|
+
INVALIDATE_REASONS,
|
|
11
|
+
EMPTY_REASONS,
|
|
12
|
+
MAX_DAMAGE_RECTS,
|
|
13
|
+
addDamageRect,
|
|
14
|
+
damageToPaint,
|
|
15
|
+
} from './damage.js';
|
|
16
|
+
import { insetRect, unionRect, rectsBounds, rectsOverlap } from './rects.js';
|
|
17
|
+
import { BLIT_POISONED } from './scrollblit.js';
|
|
18
|
+
import { DEV } from './util.js';
|
|
19
|
+
import { debugPaint } from './window/debugpaint.js';
|
|
20
|
+
|
|
21
|
+
/** Node's half of invalidation, installed onto `Node.prototype` by node.js. */
|
|
22
|
+
export class NodeInvalidate {
|
|
23
|
+
/**
|
|
24
|
+
* Ask the owning window to repaint. The damage lives on the window node,
|
|
25
|
+
* which is the only node with a frame clock — this forwards there, so an
|
|
26
|
+
* element says `this.invalidate(false, this, 'props')` and never has to
|
|
27
|
+
* know that. Overridden by WindowNode, which *is* the collector.
|
|
28
|
+
*
|
|
29
|
+
* `damage` is the node or rect that changed. Passing one is the difference
|
|
30
|
+
* between repainting a control and repainting the window, and `this` is
|
|
31
|
+
* almost always the right answer (docs/extending.md). Before the node is
|
|
32
|
+
* attached there is no window and nothing on screen, so this is a no-op —
|
|
33
|
+
* the mount invalidates in full anyway.
|
|
34
|
+
*/
|
|
35
|
+
invalidate(layoutChanged = false, damage = null, reason = null) {
|
|
36
|
+
// a layout change may grow what an enclosing scroll pane has to scroll,
|
|
37
|
+
// through a route yoga never sees (issue #405)
|
|
38
|
+
if (layoutChanged) this._markScrollMeasureDirty();
|
|
39
|
+
// a node that says its appearance changed may have changed how far it
|
|
40
|
+
// reaches — a shadow, an outline, a scene element's ink — so its cached
|
|
41
|
+
// paint reach goes with the claim
|
|
42
|
+
if (damage === this || damage === null) this._clearPaintBounds();
|
|
43
|
+
this.root?.invalidate(layoutChanged, damage, reason);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The rect this paint pass is repainting, or null when it is repainting
|
|
48
|
+
* the whole window — and null outside a paint, which reads the same way:
|
|
49
|
+
* nothing is bounding you, so draw everything (issue #301).
|
|
50
|
+
*
|
|
51
|
+
* The other end of `invalidate`. A frame repaints one damage rect per
|
|
52
|
+
* pass, clipped to it and with whole subtrees outside it culled, so an
|
|
53
|
+
* element whose node *is* one node — a `<box>`, a `<text>` — never needs
|
|
54
|
+
* this: being painted at all already means it is inside. An element that
|
|
55
|
+
* draws a **scene** into one node does: without it, a `<flow>` handed a
|
|
56
|
+
* pass over the 80×40 box a dragged node moved through redraws all three
|
|
57
|
+
* hundred nodes, all seven hundred edges, the grid and the minimap into a
|
|
58
|
+
* clip that throws almost all of it away. With it, the element culls the
|
|
59
|
+
* same way core culls the tree.
|
|
60
|
+
*
|
|
61
|
+
* Window coordinates, the same space as `abs`, `contentBox()` and an
|
|
62
|
+
* event's `x`/`y`. Read-only, like `abs`: it is this frame's own rect and
|
|
63
|
+
* the clip is already set from it.
|
|
64
|
+
*
|
|
65
|
+
* Never inside `paintCached`, which draws into a surface in its own
|
|
66
|
+
* coordinates: a cached copy culled against the window's damage is stored
|
|
67
|
+
* half-drawn under a key claiming it is whole, and every later frame that
|
|
68
|
+
* hits the key gets the hole.
|
|
69
|
+
*/
|
|
70
|
+
paintDamage() {
|
|
71
|
+
return this.root?._paintDamage ?? null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The rect this element writes opaque pixels over on every paint — in
|
|
76
|
+
* window coordinates like `abs`, in whole pixels — or null, the default,
|
|
77
|
+
* which promises nothing.
|
|
78
|
+
*
|
|
79
|
+
* What it buys: a pass that lies inside it is painted without the fills
|
|
80
|
+
* that would be under it — the window's background, this node's own and
|
|
81
|
+
* every ancestor's — because not one of those pixels survives. On the
|
|
82
|
+
* Cocoa backend those fills are full-area CoreGraphics passes, and for a
|
|
83
|
+
* streaming terminal they were a fifth of the frame; on X11 they are
|
|
84
|
+
* composites the server ran for nothing. An element with a retained
|
|
85
|
+
* surface it draws whole — a terminal, a media frame, a chart — answers
|
|
86
|
+
* with the rect it covers, and claims its damage as a **rect inside it**
|
|
87
|
+
* rather than as the node: a node claim is inflated by a pixel of slop,
|
|
88
|
+
* which is outside the rect and so never covered.
|
|
89
|
+
*
|
|
90
|
+
* The promise is the element's to keep: every pixel of the rect, alpha
|
|
91
|
+
* one, on every paint of this node, whatever the props. A translucent
|
|
92
|
+
* element, or one that draws a background only sometimes, answers null.
|
|
93
|
+
* The answer is read at paint time, so it may follow `contentBox()`, and
|
|
94
|
+
* a fractional edge is not opaque — core takes the whole pixels inside.
|
|
95
|
+
*/
|
|
96
|
+
opaqueRect() {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A layout-affecting change confined to this node: claim the subtree as
|
|
102
|
+
* it stands now, and queue it for a second claim once layout has run —
|
|
103
|
+
* the same before/after protocol `_childListChanged` uses. Anything
|
|
104
|
+
* *else* the reflow displaces claims itself through the layout diff in
|
|
105
|
+
* `flush()`, so the frame stays bounded instead of collapsing to
|
|
106
|
+
* FULL_DAMAGE the way a bare `invalidate(true, null)` would.
|
|
107
|
+
*/
|
|
108
|
+
_invalidateLayout(reason) {
|
|
109
|
+
this._markScrollMeasureDirty();
|
|
110
|
+
const root = this.root;
|
|
111
|
+
if (!root) return;
|
|
112
|
+
// Same walk, same frame, same answer — see `_childListBefore`, whose
|
|
113
|
+
// record this shares so that a reflow and a child-list change on one
|
|
114
|
+
// node in one frame walk the subtree once between them.
|
|
115
|
+
root.invalidate(true, this._childListBefore(), reason);
|
|
116
|
+
root._reflowed.add(this);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The region this node can put ink in: its own rect unioned with every
|
|
121
|
+
* descendant's. Not the same as `abs` — a child of a node that does not
|
|
122
|
+
* clip may stick out of it (absolute positioning, a negative margin), and
|
|
123
|
+
* culling a subtree by the parent's rect alone would drop that child's
|
|
124
|
+
* paint. Recomputed on demand rather than cached in `absolutize`, because
|
|
125
|
+
* it is only ever asked for on the handful of nodes that invalidate.
|
|
126
|
+
*/
|
|
127
|
+
paintBounds() {
|
|
128
|
+
const bounds = this._subtreeBounds();
|
|
129
|
+
// inflated once, here — doing it inside the recursion would compound the
|
|
130
|
+
// slop by one pixel per level of nesting
|
|
131
|
+
return {
|
|
132
|
+
x: bounds.x - DAMAGE_SLOP,
|
|
133
|
+
y: bounds.y - DAMAGE_SLOP,
|
|
134
|
+
width: bounds.width + DAMAGE_SLOP * 2,
|
|
135
|
+
height: bounds.height + DAMAGE_SLOP * 2,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* How far this node's drawing actually reaches, itself and its descendants.
|
|
141
|
+
*
|
|
142
|
+
* A node that clips its children ends the walk at its own rect: whatever
|
|
143
|
+
* they do beyond it never reaches the surface, so counting it would inflate
|
|
144
|
+
* every bound built from here. That matters most for a scrolling box, whose
|
|
145
|
+
* content is routinely thousands of pixels taller than the viewport — and
|
|
146
|
+
* can be ninety thousand pixels away mid-scroll (see `_offscreen`). Without
|
|
147
|
+
* this, damage claimed for a scrolled subtree covers the content extent
|
|
148
|
+
* instead of the viewport, and culling tests against a rect that misses
|
|
149
|
+
* almost nothing.
|
|
150
|
+
*/
|
|
151
|
+
_subtreeBounds() {
|
|
152
|
+
// Cached like the hit reach (`_hitBounds`), and for the same reason: a
|
|
153
|
+
// bounded frame asks every subtree on the way to its rect whether it
|
|
154
|
+
// reaches in, and answering by walking the subtree made a one-cell
|
|
155
|
+
// repaint cost the whole tree — a millisecond at four thousand nodes,
|
|
156
|
+
// five at fourteen thousand, every frame. The cache is dropped up the
|
|
157
|
+
// chain by whatever changes a reach: a rect assigned by layout, a
|
|
158
|
+
// child list mutation (`_clearHitBounds`), and every change a node
|
|
159
|
+
// announces about itself (`invalidate`, `setStyleState`) — so a stale
|
|
160
|
+
// answer would need a change nobody announced, which is already a
|
|
161
|
+
// repaint bug.
|
|
162
|
+
const cached = this._paintBoundsCache;
|
|
163
|
+
if (cached && !NO_BOUNDS_CACHE) return cached;
|
|
164
|
+
let bounds = this._ownPaintBounds();
|
|
165
|
+
if (!this.clipsChildren()) {
|
|
166
|
+
for (const child of this.children) {
|
|
167
|
+
if (child.isWindow || !child.yoga || child.hidden) continue;
|
|
168
|
+
if (child.style?.display === 'none') continue;
|
|
169
|
+
bounds = unionRect(bounds, child._subtreeBounds());
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
this._paintBoundsCache = bounds;
|
|
173
|
+
return bounds;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* This node's paint reach changed: drop the cached union here and up the
|
|
178
|
+
* chain, stopping where `_clearHitBounds` stops and for the same reasons
|
|
179
|
+
* — a clipping ancestor's reach is its own rect, and an ancestor already
|
|
180
|
+
* cleared has cleared the rest of the way up.
|
|
181
|
+
*/
|
|
182
|
+
_clearPaintBounds() {
|
|
183
|
+
this._paintBoundsCache = null;
|
|
184
|
+
for (let n = this.parent; n; n = n.parent) {
|
|
185
|
+
if (n.clipsChildren() || n._paintBoundsCache === null) return;
|
|
186
|
+
n._paintBoundsCache = null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* This node's own rect, grown by anything it draws outside it — the
|
|
192
|
+
* outline and the shadow, the only two. Per node rather than once at the
|
|
193
|
+
* top, because either can belong to any node and the bound has to cover it
|
|
194
|
+
* wherever it is; and the outline is counted even when the ring is
|
|
195
|
+
* currently *off*, because the frame that erases it is claimed after the
|
|
196
|
+
* state has already flipped back.
|
|
197
|
+
*
|
|
198
|
+
* A shadow cannot afford that trick — its extent is whatever the style
|
|
199
|
+
* says rather than a theme constant, so inflating for one that is not
|
|
200
|
+
* there would widen the claim of every node that has ever hovered. The
|
|
201
|
+
* frame that *removes* a shadow claims the old extent from `_retarget`
|
|
202
|
+
* instead, where both the old style and the new one are in hand.
|
|
203
|
+
*/
|
|
204
|
+
_ownPaintBounds() {
|
|
205
|
+
const extent = Math.max(this._outlineExtent(), this._shadowExtent());
|
|
206
|
+
if (extent <= 0) return this.abs;
|
|
207
|
+
return {
|
|
208
|
+
x: this.abs.x - extent,
|
|
209
|
+
y: this.abs.y - extent,
|
|
210
|
+
width: this.abs.width + extent * 2,
|
|
211
|
+
height: this.abs.height + extent * 2,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** WindowNode's half of invalidation, installed onto `WindowNode.prototype` by window/window.js. */
|
|
217
|
+
export class WindowInvalidate {
|
|
218
|
+
/**
|
|
219
|
+
* Mark the window as needing work before the next frame.
|
|
220
|
+
*
|
|
221
|
+
* `damage` is an optional node whose *appearance* changed, and it is what
|
|
222
|
+
* turns a full-window repaint into a partial one: the frame then repaints
|
|
223
|
+
* only the region that node covers, and skips emitting drawing for
|
|
224
|
+
* everything outside it. Two rules keep that safe:
|
|
225
|
+
*
|
|
226
|
+
* - a layout change gets no damage bound. Layout can move anything, and
|
|
227
|
+
* a node that moved leaves stale pixels behind at its old rect, which
|
|
228
|
+
* the new rect does not cover;
|
|
229
|
+
* - a caller that names no node means "something, somewhere", so it also
|
|
230
|
+
* repaints in full. Partial painting is therefore opt-in per call
|
|
231
|
+
* site, and forgetting to pass a node costs speed rather than
|
|
232
|
+
* correctness.
|
|
233
|
+
*
|
|
234
|
+
* `reason` is one word from INVALIDATE_REASONS saying *why* — purely
|
|
235
|
+
* diagnostic, collected per frame into `_lastReasons` so the frame log,
|
|
236
|
+
* REACT_X11_DEBUG_PAINT=full and the tracer can attribute a repaint.
|
|
237
|
+
* Omitting it costs nothing but attribution.
|
|
238
|
+
*/
|
|
239
|
+
invalidate(layoutChanged, damage = null, reason = null) {
|
|
240
|
+
if (this.destroyed || !this.window) return;
|
|
241
|
+
if (!layoutChanged && damage === NO_DAMAGE) {
|
|
242
|
+
// Nothing this node draws changed, so it contributes no region — and
|
|
243
|
+
// contributing *nothing* is not the same as contributing "unknown".
|
|
244
|
+
// Returning before `needsPaint` is what makes the difference: a commit
|
|
245
|
+
// in which every node says this schedules no frame at all, where
|
|
246
|
+
// falling through would have marked the window dirty with no region
|
|
247
|
+
// recorded and so repainted all of it. That is the common case for a
|
|
248
|
+
// React re-render whose output is identical — hovering a control whose
|
|
249
|
+
// hover state it does not actually use, for instance. Whoever did
|
|
250
|
+
// change records its own region and schedules its own frame.
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (reason) {
|
|
254
|
+
if (DEV && !INVALIDATE_REASONS.has(reason)) {
|
|
255
|
+
console.warn(
|
|
256
|
+
`react-x11: invalidate() got unknown reason ${JSON.stringify(reason)}`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
(this._frameReasons ??= new Set()).add(reason);
|
|
260
|
+
}
|
|
261
|
+
// A retained presenter keeps a per-node diff instead of damage rects,
|
|
262
|
+
// and this is the one channel every change already announces itself on
|
|
263
|
+
// (docs/macos.md §"One renderer, two presenters"). Feature-detected: an
|
|
264
|
+
// ntk window has no ear here and the X11 path is byte-identical. A
|
|
265
|
+
// presenter that answers `true` has taken the claim onto a layer of its
|
|
266
|
+
// own (src/cocoa/promotion.js): the bitmap owes nothing for it, and the
|
|
267
|
+
// frame that is still owed — for the presenter's half, `prepareFrame` —
|
|
268
|
+
// paints nothing unless something else claims.
|
|
269
|
+
const taken =
|
|
270
|
+
this.window?.noteInvalidate?.(damage, layoutChanged, reason) === true;
|
|
271
|
+
if (taken && !layoutChanged) {
|
|
272
|
+
this._damage ??= [];
|
|
273
|
+
this.needsPaint = true;
|
|
274
|
+
this._scheduleFrame();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (layoutChanged) {
|
|
278
|
+
this.needsLayout = true;
|
|
279
|
+
// The content floors are measured from the tree, so anything that
|
|
280
|
+
// changed it has to give them up — and **scrolling does not**, which is
|
|
281
|
+
// the whole reason this is not just `needsLayout`: a scroll moves an
|
|
282
|
+
// offset applied during `absolutize` and leaves every yoga node exactly
|
|
283
|
+
// as it was, at input rate, on the biggest trees in any app.
|
|
284
|
+
if (reason !== 'scroll') this._floorsDirty = true;
|
|
285
|
+
if (reason !== 'scroll' && reason !== 'resize') {
|
|
286
|
+
this._floorsContentDirty = true;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// A layout change with no bound named repaints everything, because a
|
|
290
|
+
// reflow can move any node and one that moved leaves stale pixels at a
|
|
291
|
+
// rect its new position does not cover. Naming a node alongside
|
|
292
|
+
// `layoutChanged` is an assertion by the caller that the change is
|
|
293
|
+
// confined to that node's subtree *and* that the node clips its children,
|
|
294
|
+
// so both the old and the new position of anything that moved are inside
|
|
295
|
+
// the bound. Scrolling is the case that matters: it reflows a viewport's
|
|
296
|
+
// contents and nothing else, and it happens at input rate.
|
|
297
|
+
if (!damage && this._damage !== FULL_DAMAGE && debugPaint === 'full') {
|
|
298
|
+
// This call is what makes the coming frame unbounded, so this stack —
|
|
299
|
+
// not flush's — is the one that answers "who repainted the window".
|
|
300
|
+
// Captured only under the debug switch: stacks are not free.
|
|
301
|
+
this._fullRepaintCause = {
|
|
302
|
+
reason: reason ?? '(no reason given)',
|
|
303
|
+
stack: new Error('invalidated here').stack,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
// A claim near a viewport that is waiting to blit makes that frame no
|
|
307
|
+
// longer a pure scroll — checked here, at claim time, because once the
|
|
308
|
+
// rects coalesce a change inside the viewport is indistinguishable from
|
|
309
|
+
// the scroll's own claim. (Unbounded claims need no check: FULL_DAMAGE
|
|
310
|
+
// fails the blit's damage gate by itself.)
|
|
311
|
+
// The region this claim actually covers — a node's paint reach, clipped
|
|
312
|
+
// to a blitting viewport above it (issue #398), or the bare rect a
|
|
313
|
+
// caller handed over. Null when the clip left nothing (the node draws
|
|
314
|
+
// where nothing can be seen, so it owes no pixels), and null on a frame
|
|
315
|
+
// that is already unbounded, which owes neither a rect nor the subtree
|
|
316
|
+
// walk that measures one — a blit cannot fire there either.
|
|
317
|
+
const bounds =
|
|
318
|
+
damage && damage !== NO_DAMAGE && this._damage !== FULL_DAMAGE
|
|
319
|
+
? damage._claimBounds
|
|
320
|
+
? damage._claimBounds()
|
|
321
|
+
: damage
|
|
322
|
+
: null;
|
|
323
|
+
const pendingScrolls = this._pendingScrolls;
|
|
324
|
+
if (pendingScrolls?.size && bounds && this._scrollClaim !== damage) {
|
|
325
|
+
const rect = bounds;
|
|
326
|
+
for (const sv of pendingScrolls) {
|
|
327
|
+
// An element blitting a region of its own drawing (issue #303) is
|
|
328
|
+
// waiting on that region, not on the whole node it lives in — and
|
|
329
|
+
// it is waiting on it *exactly* (issue #309). Its claim is the rect
|
|
330
|
+
// itself, and `_blitKeptDamage` recognises it as the rect itself, so
|
|
331
|
+
// a foreign claim that could be swallowed by it has to overlap it:
|
|
332
|
+
// the ring outside is beyond reach. A scroll container's claim is
|
|
333
|
+
// its viewport plus slop and is recognised to that tolerance, so a
|
|
334
|
+
// claim in that ring *can* merge into it without ever touching the
|
|
335
|
+
// viewport — and the wider zone is what keeps it out.
|
|
336
|
+
//
|
|
337
|
+
// The difference is what lets an element carve furniture out of the
|
|
338
|
+
// rect it blits — a minimap pinned to a corner, a strip it repaints
|
|
339
|
+
// itself — and keep the pan at blit cost while that furniture
|
|
340
|
+
// claims beside it.
|
|
341
|
+
const contents = sv._pendingBlitContents;
|
|
342
|
+
const waiting = contents
|
|
343
|
+
? contents.rect
|
|
344
|
+
: sv.abs && insetRect(sv.abs, -(DAMAGE_SLOP * 2 + 1));
|
|
345
|
+
if (!waiting || rectsOverlap(rect, waiting)) {
|
|
346
|
+
// …unless this viewport is keeping a ledger of what changed
|
|
347
|
+
// inside it (issue #398): the region goes in the ledger and
|
|
348
|
+
// `_applyScrollBlits` repaints it after the blit, which is the
|
|
349
|
+
// same pixels on screen for a fraction of the drawing. The
|
|
350
|
+
// ledger says no when the frame stops paying, and then this
|
|
351
|
+
// falls through to the poison exactly as before.
|
|
352
|
+
if (sv._blitLedgerOpen() && sv._recordBlitClaim(rect)) {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
// Poison rather than disarm (react-x11#295): a null here would
|
|
356
|
+
// let a second scrollTo in the same frame re-arm from a
|
|
357
|
+
// mid-frame origin, and the blit would then move pixels that
|
|
358
|
+
// were never repainted at that origin — a band displaced by the
|
|
359
|
+
// first scroll's delta. The node stays in pendingScrolls so the
|
|
360
|
+
// up-front clear in _applyScrollBlits resets the poison exactly
|
|
361
|
+
// like a real origin.
|
|
362
|
+
sv._pendingBlitFrom = BLIT_POISONED;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (layoutChanged && !damage) this._damage = FULL_DAMAGE;
|
|
367
|
+
else if (!layoutChanged && !damage) this._damage = FULL_DAMAGE;
|
|
368
|
+
else if (!bounds) {
|
|
369
|
+
// A layout change that names no region: either NO_DAMAGE, from a
|
|
370
|
+
// caller with a finer claim already in flight, or a node whose reach
|
|
371
|
+
// a clipping ancestor left nothing of (issue #398). Unlike `!damage`
|
|
372
|
+
// neither is "something, somewhere", so neither costs a full repaint.
|
|
373
|
+
} else if (this._damage !== FULL_DAMAGE) {
|
|
374
|
+
// a node, or a bare rect for a caller that has a region rather than a
|
|
375
|
+
// node — a subtree that is about to be removed, say. Claims accumulate
|
|
376
|
+
// as a list of rects rather than one box around them all, so two changes
|
|
377
|
+
// at opposite corners of the window no longer repaint everything
|
|
378
|
+
// between them.
|
|
379
|
+
this._damage = addDamageRect(this._damage, bounds, this._damageRectCap());
|
|
380
|
+
}
|
|
381
|
+
this.needsPaint = true;
|
|
382
|
+
this._scheduleFrame();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* The rects this frame will repaint, or null for the whole window.
|
|
387
|
+
*
|
|
388
|
+
* Clamped to the window: damage is recorded when a node invalidates, and
|
|
389
|
+
* the window may have been resized since. A region that no longer
|
|
390
|
+
* intersects the window means there is nothing to do, but the frame still
|
|
391
|
+
* has to clear the flag, so it degrades to a full repaint rather than
|
|
392
|
+
* painting nothing.
|
|
393
|
+
*/
|
|
394
|
+
/**
|
|
395
|
+
* How many rects a frame's damage may hold before `addDamageRect` merges
|
|
396
|
+
* the closest pair. Four on X11 (`MAX_DAMAGE_RECTS`), where every pass
|
|
397
|
+
* costs the server a clip mask; a backend whose pass is a client-side
|
|
398
|
+
* clip and a culled walk says so on its window (`damageRectCap`) and
|
|
399
|
+
* keeps more of them — a clock, a graph and a status row ticking in one
|
|
400
|
+
* frame stay three small rects instead of the box around all three.
|
|
401
|
+
*/
|
|
402
|
+
_damageRectCap() {
|
|
403
|
+
const cap = this.window?.damageRectCap;
|
|
404
|
+
return Number.isInteger(cap) && cap > 0 ? cap : MAX_DAMAGE_RECTS;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
_takeDamage(width, height) {
|
|
408
|
+
const damage = this._damage;
|
|
409
|
+
this._damage = null;
|
|
410
|
+
// what the frame about to run settled on, for the tests and for
|
|
411
|
+
// REACT_X11_DEBUG_LAYOUT to report; null means it repainted everything.
|
|
412
|
+
// `_lastDamage` is the box around the rects, which is what a caller
|
|
413
|
+
// wanting one number for "where did this frame paint" means by it.
|
|
414
|
+
// `_lastReasons` is why: every reason invalidate() was given since the
|
|
415
|
+
// previous frame, for the frame log and the full-repaint warning.
|
|
416
|
+
this._lastDamage = null;
|
|
417
|
+
this._lastDamageRects = null;
|
|
418
|
+
const reasons = this._frameReasons;
|
|
419
|
+
if (reasons?.size) {
|
|
420
|
+
this._lastReasons = [...reasons];
|
|
421
|
+
reasons.clear();
|
|
422
|
+
} else {
|
|
423
|
+
this._lastReasons = EMPTY_REASONS;
|
|
424
|
+
}
|
|
425
|
+
if (damage === FULL_DAMAGE || !damage) return null;
|
|
426
|
+
// an empty list: every claim this frame made was answered on a layer of
|
|
427
|
+
// its own (`invalidate`, the presenter's `true`), and the bitmap paints
|
|
428
|
+
// nothing — which is not the same as nothing having been claimed
|
|
429
|
+
if (damage.length === 0) {
|
|
430
|
+
this._lastDamageRects = [];
|
|
431
|
+
this._lastDamage = { x: 0, y: 0, width: 0, height: 0 };
|
|
432
|
+
return [];
|
|
433
|
+
}
|
|
434
|
+
const rects = [];
|
|
435
|
+
for (const claimed of damage) {
|
|
436
|
+
const clamped = this._clampDamage(claimed, width, height);
|
|
437
|
+
// one claim covering the window makes the whole frame unbounded, so
|
|
438
|
+
// there is nothing to learn from the rest of the list
|
|
439
|
+
if (clamped === FULL_DAMAGE) return null;
|
|
440
|
+
if (clamped) rects.push(clamped);
|
|
441
|
+
}
|
|
442
|
+
if (!rects.length) return null;
|
|
443
|
+
this._lastDamageRects = damageToPaint(rects);
|
|
444
|
+
this._lastDamage = rectsBounds(this._lastDamageRects);
|
|
445
|
+
return this._lastDamageRects;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* One claimed rect snapped to whole pixels inside the window: null when
|
|
450
|
+
* nothing of it is left, `FULL_DAMAGE` when it covers the window.
|
|
451
|
+
*/
|
|
452
|
+
_clampDamage(damage, width, height) {
|
|
453
|
+
const x = Math.max(0, Math.floor(damage.x));
|
|
454
|
+
const y = Math.max(0, Math.floor(damage.y));
|
|
455
|
+
const right = Math.min(width, Math.ceil(damage.x + damage.width));
|
|
456
|
+
const bottom = Math.min(height, Math.ceil(damage.y + damage.height));
|
|
457
|
+
if (right <= x || bottom <= y) return null;
|
|
458
|
+
// covering the window is the same as not being bounded at all, and the
|
|
459
|
+
// full path is one fill instead of a clip plus a fill
|
|
460
|
+
if (x === 0 && y === 0 && right >= width && bottom >= height) {
|
|
461
|
+
return FULL_DAMAGE;
|
|
462
|
+
}
|
|
463
|
+
return { x, y, width: right - x, height: bottom - y };
|
|
464
|
+
}
|
|
465
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The kind tables. registry.js fills them when an element registers, and
|
|
2
|
+
// the node classes read them — that direction, rather than the nodes
|
|
3
|
+
// importing the registry, is what keeps the two acyclic.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Kinds that lay out with yoga and paint into the owning window —
|
|
7
|
+
* `paintOrder` filters on this, so a kind missing from it lays out and
|
|
8
|
+
* never paints. Mutable because `registerElement` (registry.js) adds to it;
|
|
9
|
+
* that direction, rather than src/nodes/ importing the registry, is what
|
|
10
|
+
* keeps the two files acyclic.
|
|
11
|
+
*/
|
|
12
|
+
export const DRAWN_KINDS = new Set([
|
|
13
|
+
'box',
|
|
14
|
+
'text',
|
|
15
|
+
'image',
|
|
16
|
+
'canvas',
|
|
17
|
+
'textinput',
|
|
18
|
+
'textarea',
|
|
19
|
+
'svg',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
/** kind -> style names a registered element claims as its own semantics.
|
|
23
|
+
* Filled by registry.js; read by `Node.semanticNames`, so a registered
|
|
24
|
+
* element gets the exemption without subclassing the getter. */
|
|
25
|
+
export const CUSTOM_SEMANTIC_NAMES = new Map();
|
|
26
|
+
|
|
27
|
+
/** kind -> prop names a registered element claims the damage for itself.
|
|
28
|
+
* Filled by registry.js, read by `Node.selfDamagedProps` — the same
|
|
29
|
+
* arrangement as above, for the other declaration a scene-drawing element
|
|
30
|
+
* makes (issue #301). */
|
|
31
|
+
export const CUSTOM_SELF_DAMAGED = new Map();
|